org.netbeans.api.visual.widget.Scene Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.api.visual.widget;
import org.netbeans.api.visual.action.ActionFactory;
import org.netbeans.api.visual.action.TwoStateHoverProvider;
import org.netbeans.api.visual.action.WidgetAction;
import org.netbeans.api.visual.animator.SceneAnimator;
import org.netbeans.api.visual.laf.InputBindings;
import org.netbeans.api.visual.laf.LookFeel;
import org.netbeans.modules.visual.util.GeomUtil;
import org.netbeans.modules.visual.widget.SatelliteComponent;
import javax.swing.*;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.HashSet;
/**
* The scene is a widget which also controls and represents whole rendered area.
*
* After all changes in a scene is done, the validate method have to be called for validating changed
* and calculating new locations and boundaries of all modified widgets.
*
* The scene allows to create a view JComponent which can be used anywhere in Swing based application. Only one view
* can be created using the createView method.
* The scene allows to create multiple satellite views using the createSatelliteView method. The satellite view is just
* showing the scene and allows quick navigator and panning using a mouse.
*
* The scene contains additional scene-specific properties like lookFeel, activeTool, defaultFont, animator.
*
* It is able to create a widget-specific hover action.
*
* @author David Kaspar
*/
// TODO - take SceneComponent dimension and correct Scene.resolveBounds
// TODO - remove SuppressWarnings
public class Scene extends Widget {
private double zoomFactor = 1.0;
private SceneAnimator sceneAnimator;
private JComponent component = null;
private Graphics2D graphics = null;
private boolean viewShowing = false;
private boolean paintEverything = true;
private Rectangle repaintRegion = null;
private HashSet repaintWidgets = new HashSet ();
private Font defaultFont;
private LookFeel lookFeel = LookFeel.createDefaultLookFeel ();
private InputBindings inputBindings = InputBindings.create ();
private String activeTool = null;
private Rectangle maximumBounds = new Rectangle (Integer.MIN_VALUE / 2, Integer.MIN_VALUE / 2, Integer.MAX_VALUE, Integer.MAX_VALUE);
private final ArrayList sceneListeners = new ArrayList (); // TODO - use CopyOnWriteArrayList
private EventProcessingType keyEventProcessingType = EventProcessingType.ALL_WIDGETS;
private WidgetAction.Chain priorActions = new WidgetAction.Chain ();
private Widget focusedWidget = this;
private WidgetAction widgetHoverAction;
boolean extendSceneOnly = false;
private ResourceTable resourceTable = null;
/**
* Creates a scene.
*/
public Scene () {
super (null);
defaultFont = Font.decode (null);
resolveBounds (new Point (), new Rectangle ());
setOpaque(true);
setFont (defaultFont);
setBackground (lookFeel.getBackground ());
setForeground (lookFeel.getForeground ());
sceneAnimator = new SceneAnimator (this);
}
/**
* Creates a view. This method could be called once only. Call the getView method for getting created instance of a view.
* @return the created view
*/
public JComponent createView () {
assert component == null;
component = new SceneComponent (this);
component.addAncestorListener (new AncestorListener() {
public void ancestorAdded (AncestorEvent event) {
repaintSatellite ();
}
public void ancestorRemoved (AncestorEvent event) {
repaintSatellite ();
}
public void ancestorMoved (AncestorEvent event) {
}
});
return component;
}
/**
* Returns an instance of created view
* @return the instance of created view; null if no view is created yet
*/
public JComponent getView () {
return component;
}
/**
* Creates a satellite view.
* @return the satellite view
*/
public JComponent createSatelliteView () {
return new SatelliteComponent (this);
}
/**
* Creates a bird view with specific zoom factor.
* @return the bird view controller
* @since 2.7
*/
public BirdViewController createBirdView () {
return new BirdViewController (this);
}
void setViewShowing (boolean viewShowing) {
assert this.viewShowing != viewShowing : "Duplicate setViewShowing: " + viewShowing;
this.viewShowing = viewShowing;
if (viewShowing)
dispatchNotifyAddedCore ();
else
dispatchNotifyRemovedCore ();
}
void dispatchNotifyAdded (Widget widget) {
assert widget != null;
Widget w = widget;
for (; ;) {
if (w == this)
break;
w = w.getParentWidget ();
if (w == null)
return;
}
if (! viewShowing)
return;
widget.dispatchNotifyAddedCore ();
}
void dispatchNotifyRemoved (Widget widget) {
assert widget != null;
Widget w = widget;
for (;;) {
if (w == this) {
if (viewShowing)
return;
break;
}
w = w.getParentWidget ();
if (w == null)
break;
}
if (! viewShowing)
return;
widget.dispatchNotifyRemovedCore ();
}
/**
* Returns an instance of Graphics2D which is used for calculating boundaries and rendering all widgets in the scene.
* @return the instance of Graphics2D
*/
@Override
public final Graphics2D getGraphics () {
return graphics;
}
// HACK - used by SceneComponent and ConvolutionWidget
final void setGraphics (Graphics2D graphics) {
this.graphics = graphics;
}
/**
* This method invokes Scene.validate method with a specific Graphics2D instance. This is useful for rendering a scene off-screen
* without creating and showning the main scene view. See test.view.OffscreenRenderingTest example for usages.
*
* Note: Do not call this method unless you know the consequences. The scene is going to be validated using the specified Graphics2D
* instance even after the method call therefore it may break scene layout when your main scene view is finally created and shown.
* @param graphics the graphics instance used for validation
* @since 2.7
*/
public final void validate (Graphics2D graphics) {
Graphics2D prevoiusGraphics = getGraphics ();
setGraphics (graphics);
validate ();
setGraphics (prevoiusGraphics);
}
/**
* Paints the whole scene into the graphics instance. The method calls validate before rendering.
* @param graphics the Graphics2D instance where the scene is going to be painted
*/
public final void paint (Graphics2D graphics) {
validate ();
Graphics2D prevoiusGraphics = getGraphics ();
setGraphics (graphics);
paint ();
setGraphics (prevoiusGraphics);
}
/**
* Returns maximum bounds of the scene.
* @return the maximum bounds
*/
public final Rectangle getMaximumBounds () {
return new Rectangle (maximumBounds);
}
/**
* Sets maximum bounds of the scene.
* @param maximumBounds the non-null maximum bounds
*/
public final void setMaximumBounds (Rectangle maximumBounds) {
assert maximumBounds != null;
this.maximumBounds = new Rectangle (maximumBounds);
revalidate ();
}
/**
* Returns a default font of the scene.
* @return the default font
*/
public Font getDefaultFont () {
return defaultFont;
}
/**
* Returns whether the whole scene is validated and there is no widget or region that has to be revalidated.
* @return true, if the whole scene is validated
*/
@Override
public boolean isValidated () {
return super.isValidated () && repaintRegion == null && repaintWidgets.isEmpty ();
}
/**
* Returns whether the layer widget requires to repainted after revalidation.
* @return always false
*/
@Override
protected boolean isRepaintRequiredForRevalidating () {
return false;
}
// TODO - maybe it could improve the perfomance, if bounds != null then do nothing
// WARNING - you have to asure that there will be no component/widget that will change its location/bounds between this and validate method calls
final void revalidateWidget (Widget widget) {
Rectangle widgetBounds = widget.getBounds ();
if (widgetBounds != null) {
Rectangle sceneBounds = widget.convertLocalToScene (widgetBounds);
if (repaintRegion == null)
repaintRegion = sceneBounds;
else
repaintRegion.add (sceneBounds);
}
repaintWidgets.add (widget);
}
// TODO - requires optimalization while changing preferred size and calling revalidate/repaint
private void layoutScene () {
Point preLocation = getLocation ();
Rectangle preBounds = getBounds ();
layout (false);
resolveBounds (null, null);
justify ();
Rectangle rect = null;
for (Widget widget : getChildren ()) {
Point location = widget.getLocation ();
Rectangle bounds = widget.getBounds ();
bounds.translate (location.x, location.y);
if (rect == null)
rect = bounds;
else
rect.add (bounds);
}
if (rect != null) {
Insets insets = getBorder ().getInsets ();
rect.x -= insets.left;
rect.y -= insets.top;
rect.width += insets.left + insets.right;
rect.height += insets.top + insets.bottom;
rect = rect.intersection (maximumBounds);
}
if (extendSceneOnly && rect != null && preBounds != null)
rect.add (new Rectangle (preBounds.x + preLocation.x, preBounds.y + preLocation.y, preBounds.width, preBounds.height));
resolveBounds (rect != null ? new Point (- rect.x, - rect.y) : new Point (), rect);
Dimension preferredSize = rect != null ? rect.getSize () : new Dimension ();
preferredSize = new Dimension ((int) (preferredSize.width * zoomFactor), (int) (preferredSize.height * zoomFactor));
Rectangle bounds = getBounds ();
if (component != null) {
if (! preferredSize.equals (component.getPreferredSize ())) {
component.setPreferredSize (preferredSize);
component.revalidate ();
bounds = getBounds ();
// repaintSatellite ();
}
Dimension componentSize = component.getSize ();
componentSize.width = (int) (componentSize.width / zoomFactor);
componentSize.height = (int) (componentSize.height / zoomFactor);
boolean sceneResized = false;
if (bounds.width < componentSize.width) {
bounds.width = componentSize.width;
sceneResized = true;
}
if (bounds.height < componentSize.height) {
bounds.height = componentSize.height;
sceneResized = true;
}
if (sceneResized)
resolveBounds (getLocation (), bounds);
}
if (! getLocation ().equals (preLocation) || ! getBounds ().equals (preBounds)) {
Rectangle rectangle = convertLocalToScene (getBounds ());
if (repaintRegion == null)
repaintRegion = rectangle;
else
repaintRegion.add (rectangle);
}
}
/**
* Validates all widget in the whole scene. The validation is done repeatively until there is no invalid widget
* in the scene after validating process. It also schedules invalid regions in the view for repainting.
*/
@SuppressWarnings("unchecked")
public final void validate () {
if (graphics == null)
return;
while (! isValidated ()) {
SceneListener[] ls;
synchronized (sceneListeners) {
ls = sceneListeners.toArray (new SceneListener[0]);
}
for (SceneListener listener : ls)
listener.sceneValidating ();
layoutScene ();
resolveRepaints ();
for (SceneListener listener : ls)
listener.sceneValidated ();
}
}
private void resolveRepaints () {
assert SwingUtilities.isEventDispatchThread();
for (Widget widget : repaintWidgets) {
Rectangle repaintBounds = widget.getBounds ();
if (repaintBounds == null)
continue;
repaintBounds = widget.convertLocalToScene (repaintBounds);
if (repaintRegion != null)
repaintRegion.add (repaintBounds);
else
repaintRegion = repaintBounds;
}
repaintWidgets.clear ();
// System.out.println ("r = " + r);
// NOTE - maybe improves performance when component.repaint will be called for all widgets/rectangles separately
if (repaintRegion != null) {
Rectangle r = convertSceneToView (repaintRegion);
r.grow (1, 1);
if (component != null)
component.repaint (r);
repaintSatellite ();
repaintRegion = null;
}
// System.out.println ("time: " + System.currentTimeMillis ());
}
/**
* This methods makes the scene to be extended only - no shrunk.
* @param extendSceneOnly if true, the scene is going to be extended only
*/
void setExtendSceneOnly (boolean extendSceneOnly) {
this.extendSceneOnly = extendSceneOnly;
}
private void repaintSatellite () {
SceneListener[] ls;
synchronized (sceneListeners) {
ls = sceneListeners.toArray (new SceneListener[0]);
}
for (SceneListener listener : ls)
listener.sceneRepaint ();
}
final boolean isPaintEverything () {
return paintEverything;
}
final void setPaintEverything (boolean paintEverything) {
this.paintEverything = paintEverything;
}
/**
* Returns a key events processing type of the scene.
* @return the processing type for key events
*/
public final EventProcessingType getKeyEventProcessingType () {
return keyEventProcessingType;
}
/**
* Sets a key events processing type of the scene.
* @param keyEventProcessingType the processing type for key events
*/
public final void setKeyEventProcessingType (EventProcessingType keyEventProcessingType) {
assert keyEventProcessingType != null;
this.keyEventProcessingType = keyEventProcessingType;
}
/**
* Returns a prior actions. These actions are executed before any other action in the scene.
* If any of these actions consumes an event that the event processsing is stopped. Action locking is ignored.
* @return the prior actions
*/
public final WidgetAction.Chain getPriorActions () {
return priorActions;
}
/**
* Returns a focused widget of the scene.
* @return the focused widget; null if no widget is focused
*/
public final Widget getFocusedWidget () {
return focusedWidget;
}
/**
* Sets a focused widget of the scene.
* @param focusedWidget the focused widget; if null, then the scene itself is taken as the focused widget
*/
public final void setFocusedWidget (Widget focusedWidget) {
if (focusedWidget == null)
focusedWidget = this;
else
assert focusedWidget.getScene () == this;
this.focusedWidget.setState (this.focusedWidget.getState ().deriveWidgetFocused (false));
this.focusedWidget = focusedWidget;
this.focusedWidget.setState (this.focusedWidget.getState ().deriveWidgetFocused (true));
}
/**
* Returns a zoom factor.
* @return the zoom factor
*/
public final double getZoomFactor () {
return zoomFactor;
}
/**
* Sets a zoom factor for the scene.
* @param zoomFactor the zoom factor
*/
public final void setZoomFactor (double zoomFactor) {
this.zoomFactor = zoomFactor;
revalidate ();
}
/**
* Returns a scene animator of the scene.
* @return the scene animator
*/
public final SceneAnimator getSceneAnimator () {
return sceneAnimator;
}
/**
* Returns a look'n'feel of the scene.
* @return the look'n'feel
*/
public final LookFeel getLookFeel () {
return lookFeel;
}
/**
* Sets a look'n'feel of the scene. This method does affect current state of the scene - already created components
* will not be refreshed.
* @param lookFeel the look'n'feel
*/
public final void setLookFeel (LookFeel lookFeel) {
assert lookFeel != null;
this.lookFeel = lookFeel;
}
/**
* Returns input bindings of the scene.
* @return the input bindings
* @since 2.4
*/
public final InputBindings getInputBindings () {
return inputBindings;
}
/**
* Returns an active tool of the scene.
* @return the active tool; if null, then only default action chain of widgets will be used
*/
public final String getActiveTool () {
return activeTool;
}
/**
* Sets an active tool.
* @param activeTool the active tool; if null, then the active tool is unset and only default action chain of widgets will be used
*/
public void setActiveTool (String activeTool) {
this.activeTool = activeTool;
}
/**
* Registers a scene listener.
* @param listener the scene listener
*/
public final void addSceneListener (SceneListener listener) {
assert listener != null;
synchronized (sceneListeners) {
sceneListeners.add (listener);
}
}
/**
* Unregisters a scene listener.
* @param listener the scene listener
*/
public final void removeSceneListener (SceneListener listener) {
synchronized (sceneListeners) {
sceneListeners.remove (listener);
}
}
/**
* Converts a location in the scene coordination system to the view coordination system.
* @param sceneLocation the scene location
* @return the view location
*/
public final Point convertSceneToView (Point sceneLocation) {
Point location = getLocation ();
return new Point ((int) (zoomFactor * (location.x + sceneLocation.x)), (int) (zoomFactor * (location.y + sceneLocation.y)));
}
/**
* Converts a rectangle in the scene coordination system to the view coordination system.
* @param sceneRectangle the scene rectangle
* @return the view rectangle
*/
public final Rectangle convertSceneToView (Rectangle sceneRectangle) {
Point location = getLocation ();
return GeomUtil.roundRectangle (new Rectangle2D.Double (
(double) (sceneRectangle.x + location.x) * zoomFactor,
(double) (sceneRectangle.y + location.y) * zoomFactor,
(double) sceneRectangle.width * zoomFactor,
(double) sceneRectangle.height * zoomFactor));
}
/**
* Converts a rectangle in the view coordination system into the scene one. It is just the inverse
* fiunction to {@link #convertSceneToView(java.awt.Rectangle)}.
*
* @param viewRect the rectangle, in view coordinates
* @return the same rectangle, in scene coordinates
* @since 2.49
*/
public Rectangle convertViewToScene(Rectangle viewRect) {
Point pt1 = new Point(viewRect.x, viewRect.y);
pt1 = convertViewToScene(pt1);
if (viewRect.isEmpty()) {
return new Rectangle(pt1.x, pt1.y, 0, 0);
}
Point pt2 = new Point(viewRect.x + viewRect.width - 1, viewRect.y + viewRect.height - 1);
pt2 = convertViewToScene(pt2);
Rectangle r = new Rectangle(pt1);
r.add(pt2);
return r;
}
/**
* Converts a location in the view coordination system to the scene coordination system.
* @param viewLocation the view location
* @return the scene location
*/
public Point convertViewToScene (Point viewLocation) {
return new Point ((int) ((double) viewLocation.x / zoomFactor) - getLocation ().x, (int) ((double) viewLocation.y / zoomFactor) - getLocation ().y);
}
/**
* Creates a widget-specific hover action.
* @return the widget-specific hover action
*/
public WidgetAction createWidgetHoverAction () {
if (widgetHoverAction == null) {
widgetHoverAction = ActionFactory.createHoverAction (new WidgetHoverAction ());
getActions ().addAction (widgetHoverAction);
}
return widgetHoverAction;
}
@Override
public void setResourceTable(ResourceTable table) {
// TODO: What to do if a resource table already exist.
resourceTable = table;
}
@Override
public ResourceTable getResourceTable() {
return resourceTable;
}
private class WidgetHoverAction implements TwoStateHoverProvider {
public void unsetHovering (Widget widget) {
widget.setState (widget.getState ().deriveWidgetHovered (false));
}
public void setHovering (Widget widget) {
widget.setState (widget.getState ().deriveWidgetHovered (true));
}
}
/**
* The scene listener which is notified about repainting, validating progress.
*/
public interface SceneListener {
/**
* Called to notify that the whole scene was repainted.
*/
void sceneRepaint ();
/**
* Called to notify that the scene is going to be validated.
*/
void sceneValidating ();
/**
* Called to notify that the scene has been validated.
*/
void sceneValidated ();
}
}