This is a documentation for the Visual Library 2.0 API. The library is a successor of the Graph Library 1.0. Its provides general-purpose (not graph-oriented only) visualization.
WWW: https://netbeans.apache.org/front/main/projects/graph/
git: https://github.com/apache/netbeans/tree/master/platform/api.visual
Issues: https://github.com/apache/netbeans/issues
To install the library, just set a dependency on org.netbeans.api.visual module. The source code of Visual Library API is located at graph/lib directory in the NetBeans CVS repository.
The programming style is similar to Swing. You are building and modifying a tree of visual elements that are called Widget
s. The root of the tree is represented by a Scene
class which holds all visual data of the scene. Since neither Widget nor Scene is an AWT/Swing component you have to call Scene.createView
method for creating a Swing component which renders the scene. The created JComponent could be used anywhere.
Following code creates an empty scene, creates a JComponent view of the scene and embeds it into a dialog with scrollable view.
Scene scene = new Scene (); JComponent sceneView = scene.createView (); JScrollPane panel = new JScrollPane (sceneView); JDialog dia = new JDialog (new JDialog (), true); dia.add (panel, BorderLayout.CENTER); dia.setSize (800, 600); dia.setVisible (true); dia.dispose ();
Widget is a primitive visual element - similar to what JComponent is for Swing. It holds information about its location, boundary, preferred location/boundary, preferred/minimal/maximal sizes, layout, border, foreground, background, font, cursor, tooltip , accessible context, ...
Widget placement is defined by:
A widget placement is resolved by a layout associated to its parent widget. The placement is usually based on a preferred location and boundary. The preferred boundary can be set or calculated from the widget needs. Then it can be processed by preferred/minimum/maximum size. See Layout section for details.
Method | Description |
---|---|
getScene | Returns a scene where the widget belongs. The scene instance is assigned in the constructor and could not be changed anymore. |
getParentWidget getChildren addChild removeChild removeFromParent addChildren removeChildren bringToFront bringToBack | Methods for manipulation with the tree hierarchy of widgets. |
getChildConstraint setChildConstraint | Controls constraints assigned to child widgets. Used by Layouts similarly to Swing. See Layout section. |
isEnabled setEnabled | Controls whether events are processed by actions assigned to the widget or any of its children. If the widget is disabled, then events are not processed by children widgets too. |
getActions() | Returns a default actions chain. See WidgetAction section. |
getActions(String tool) createActions(String tool) | Returns (and optionally creates) a actions chain for an appropriate tool. See Action Tools section. |
getLookup | Returns lookup. Not used right now - it will be used later for extending the Widget functionality when the API is stable and incompatible API changes are forbidden. |
addDependency removeDependency getDependencies | Allows to assigned a Widget.Dependency listener to the widget for notifying about widget location or boundary changes. Widely used by Anchors. See: Widget Dependency section.
|
isVisible setVisible | Controls visibility of the widget. If not visible, then the widget is not painted and do not process events. True by default.
|
isOpaque setOpaque | Controls opacity of the widget. If opaque, then background is not painted. False by default.
|
getBackground setBackground getForeground setForeground | Controls the background paint and foreground color. Solid white background and black foreground by default. |
getFont setFont | Controls the font. By default or if set to null , then getFont returns value of Scene.getDefaultFont method.
|
getBorder setBorder | Controls the widget border. EmptyBorder by default. See Border section.
|
getCursor setCursor | Controls the widget cursor. null by default.
|
getLayout setLayout | Controls the widget layout. The layout defines placement of children. AbsoluteLayout by default. See Layout section.
|
getMinimumSize setMinimumSize getMaximumSize setMaximumSize getPreferredSize setPreferredSize | Controls minimum, maximum and preferred size. Null (means not set) by default. See Layout section.
|
getPreferredLocation setPreferredLocation | Controls preferred location. Widely used when parent widget has AbsoluteLayout , then the preferred location is used as the evaluated location. See Layout section. Null (means not set) by default.
|
getPreferredBounds setPreferredBounds isPreferredBoundsSet | Controls preferred boundaries. Not set by default not set. getPreferredBounds method always returns non-null boundaries - if the preferred bounds are not set, then it automatically calculate the boundaries from boundaries of its children, assigned layout and internal boundaries. The returned boundaries are checked for minimum, maximum and preferred sizes. See Layout section.
|
isCheckClipping setCheckClipping | Defines whether the clipping should be used for widget painting. False by default.
|
getToolTipText setToolTipText | Controls the tool tip text. |
getState setState | Defines the widget state. State.createNormal by default. See Object State section.
|
convertLocalToScene convertSceneToLocal | Converts local coordination system to scene coordination system and vice versa. See Coordination System section. |
getLocation | Returns evaluated widget location specified relatively to the parent widget coordination system. [0,0] by default. See Layout and Coordination System section.
|
getBounds | Returns evaluated widget boundaries specified relatively to the widget (local) coordination system. Null by default. See Layout and Coordination System section.
|
resolveBounds (Point location, Rectangle bounds) | Sets evaluated widget location and boundary. If location is null , then [0,0] is automatically used instead. If bounds are null , then return from getPreferredBounds is automatically used instead. This method should be called from implementation of Layout.layout method only. See Layout and Coordination System section.
|
getClientArea | Returns boundary from getBounds without (means: decreased by) insets of a border assigned to the widget.
|
isHitAt | Returns whether the widget is "hit" at specified location in local coordination system. The "hit" usually means that there is a pixel at the location controlled/painted by the widget. Widely used for mouse event processing. E.g. A circle-shape widget has rectangular boundary but it is hit on at points that are located inside the circle area. |
repaint | Schedules the widget for painting. Usually called from setters method of the Widget. Usually the developer should call setter methods and the setter methods should call repaint or revalidate (depends whether they affect widget location/boundary). See Validation Process section.
|
isValidated | Returns whether the widget is validated. See Validation Process section. |
revalidate() | Schedules the widget for validation. Usually called from setter methods of the Widget. Usually the developer should call setter methods and the setter methods should call repaint or revalidate (depends whether they affect widget location/boundary). See Validation Process section.
|
revalidate(boolean repaintOnly) | Calls repaint or revalidate method based on repaintOnly parameter.
|
paint | Paints the widget using the Graphics2D instance acquired by getGraphics method. Usually you should not call this method directly. It is called automatically by the library. For repainting the widget, use repaint method instead.
|
protected getGraphics | Returns Graphics2D instance that could be used for painting and calculating internal boundary. It is non-null only when the scene has a JComponent view created and it is added into AWT components hierarchy (means: after JComponent.addNotify method call). See Custom Widget section.
|
protected calculateClientArea | Returns calculated client area required by the widget itself without children. E.g. LabelWidget has the client area equal to the boundary of its label. Do not include children location and boundary - they are included automatically later. See Custom Widget section. |
protected paintBackground | Paints the widget background only. Do not invoke painting of children in this method. See Custom Widget section. |
protected paintBorder | Paints the widget border only. Do not invoke painting of children in this method. See Custom Widget section. |
protected paintWidget | Paints the widget only. Do not invoke painting of children in this method. See Custom Widget section. |
protected paintChildren | Paints the widget children. Default implementation of Widget.paintChildren method should to be called to for invoke painting of the children. You can also selectively call paint method on each child. Usually this is used for introducing painting effects on children. E.g. smooth alpha blending in LevelOfDetailsWidget or applying convolution in ConvolveWidget . See Custom Widget section.
|
protected notifyStateChanged | Called by setState method when the state is changed. This method should control the widget appearance based on the widget-specific state. E.g. change the background color based on the result of ObjectState.isSelected method. See ObjectState and Custom Widget sections.
|
protected isRepaintRequiredForRevalidating | Returns whether the repaint of the whole widget (specified by the widget boundaries) has to be repainted during validation. Used for speed optimalization. By default return true for everything except Scene and LayerWidget. See Custom Widget section.
|
protected notifyAdded protected notifyRemoved | Called to notify that a widget is being show or hidden in the view. Within these methods, do not modify the tree hierarchy of scene. |
protected getCursorAt(Point localLocation) | Called to get a cursor of a specified local location of the widget. |
The LabelWidget
represents a single line text. You can manipulate with the text using getLabel
and setLabel
methods or using a constructor parameter. The widget is not-opaque by default and uses foreground, background and font properties for rendering. The text could be aligned to the left (by default), right or center within the widget. The widget origin is a point on the left side of the baseline of the text.
Since version 2.1 the LabelWidget allows rendering vertical labels. It can be set by calling LabelWidget.setOrientation
method. Default value is LabelWidget.Orientation.NORMAL
which renders labels horizontally. The other value is LabelWidget.Orientation.ROTATE_90
which makes the label to render vertically.
Since version 2.7 the LabelWidget allows to use glyph-vector for rendering. This allows accurate text rendering independently on a scene zoom-factor (no more clipped text while zooming onto a scene).
The ImageWidget
represents an image. You can manipulate with the image using getImage
and setImage
methods or using a constructor parameter. The widget is not-opaque by default. The widget origin is top-left corner of the image.
The ImageWidget
supports animated images. For proper work, do not load images using Utilities.loadImage
because the method is producing static images. Instead use standard Toolkit.createImage
method for loading.
Sometimes you want to listen on a widget move or resize changes. The listener is defined by Widget.Dependency
interface. Listener could be (un)registered using Widget.addDependency
and Widget.removeDependency
methods.
Note 1: The Widget.Dependency.revalidateDependency
method is not called directly from Widget.resolveBounds
method. Instead it is called indirectly during widget or scene revalidation process (means from Widget.revalidate
and Scene.validate
method). See Validation Process and Layout sections for details.
Note 2: Widget.Dependency.revalidateDependency
method is called to notify all dependent objects that the widget is scheduled for revalidation and therefore all dependent objects should schedule their revalidation using dependentWidget.revalidate
method call too.
E.g.: An anchor depends on a widget where it is assigned. When Widget.Dependency.revalidateDependency
is called, then the anchor clears its caches and invokes Widget.revalidate
method on all ConnectionWidget
s (resp. Anchor.Entry
).
Therefore no bounds of any widget is valid at the time of the method call. The valid bounds can be obtained only in Layout
interface:
Layout.layout
method is called, initial bounds of all child widgets of the widget (where the layout is assigned) are resolved.
Layout.justify
method is called, bounds of all parent widgets of the widget (where the layout is assigned) are resolved.
Scene.addSceneListener
method and put your code into SceneListener.sceneValidated
method implementation.
Following code creates the iconNode instance that has an image with a label.
// create a scene Scene scene = new Scene (); // create the icon node widget Widget iconNode = new Widget (scene); // use a vertical layout iconNode.setLayout (LayoutFactory.createVerticalFlowLayout (LayoutFactory.SerialAlignment.CENTER, 4)); // add a image child widget iconNode.addChild (new ImageWidget (scene, Utilities.loadImage ("path/to/my/image.png"))); // add a label child widget iconNode.addChild (new LabelWidget (scene, "My Label")); // add it to the scene scene.addChild (iconNode);
Widget.setBorder
method. The border is always painted after the clearing the widget background and before the Widget.paintWidget
and Widget.paintChildren
methods. Therefore the border could be used for painting complex widget backgrounds. The border can be usually created using BorderFactory.create*Border
methods. Single instance of border class could be shared and assigned to more widgets.
There are following borders available:
ObjectScene
, GraphScene
, GraphPinScene
class which adds additional functionally for object-oriented and graph-oriented modeling.
Scene
class is derived from Widget
class and contains additional methods.
Method | Description |
---|---|
createView | Creates a single JComponent -based view of the scene. Could be called once only. The JComponent could be used anywhere in AWT/Swing. Scene and widgets starts to be validated/painted only when the view is created and added to the AWT/Swing components hierarchy (JComponent.addNotify is called).
|
getView | Returns the JComponent instance created by createView .
|
createSatelliteView | Creates a satellite view of the scene. It is showing an overview of the scene. In case the JComponent view is placed in a JScrollPane and the scene does not fit into the panel, then the satellite view paints an viewport rectangle which representing visible area. It also allows user to move with the actual viewport of the scene. |
createBirdView | Creates a bird view of the scene. It is enough to create one bird view and reuse it all the time. The bird view is tracking mouse cursor and when the cursor is over visible area of the scene view, then an additional always-on-top window is shown and shows pointed part of the scene with specified zoom factor. The method returns BirdViewController which allows control over the bird view. When the bird view is enabled, then it consumes all events of the scene view.
|
getGraphics | Returns an instance of Graphics2D used by whole scene. The instance is the one that was available during the last JComponent.addNotify or JComponent.paint method call on the JComponent view.
|
paint(Graphics2D) | Paints the scene using Graphics2D instance. This method could be invoked in AWT-thread only.
|
isValidated | Returns true if it is validated as using Widget.isValidated and there is no region or widget scheduled for repainting. See Layout section.
|
validate | This method validates whole scene with all widgets. Usually it is called automatically e.g. at the end of event processing. See Validation Process section. |
validate(Graphics2D) | This method validates whole scene with all widgets using specified Graphics2D. This is used for off-screen rendering when you do not have a main scene view created yet. Be aware that the scene is going to remain validated with specified font metrics after the method call. This may break scene layout when your main scene view is finally created and shown. See Validation Process section. |
getMaximumBounds setMaximumBounds | Controls the maximum bounds of the scene. By default it is [-Integer.MAX_VALUE/2,-Integer.MAX_VALUE/2,Integer.MAX_VALUE,Integer.MAX_VALUE] . This is used to limit the scene bounds e.g. to positive values only.
|
getZoomFactor setZoomFactor | Controls the zoom factor of the scene. |
getSceneAnimator | Returns a scene animator that controls animations on the scene. See Animator section. |
getLookFeel setLookFeel | Controls the scene look and feel. LookFeel.createDefaultLookFeel by default. See Look and Feel section.
|
getInputBindinds | Returns input bindings used by the scene. See Look and Feel section. |
getActiveTool setActiveTool | Controls the active action tool of the scene. See Action Tools section. |
getKeyEventProcessingType setKeyEventProcessingType | Controls the processing type used for key events. See Event Processing Types section. |
getPriorActions | Returns a chain of prior actions. These actions are executed because any other action of the scene. See AWT Event Processing section. |
getFocusedWidget setFocusedWidget | Controls a focused widget of the scene. Only one widget could be focused at the same time. The focused widget could be used for key event processing. See Event Processing Types section. |
addSceneListener removeSceneListener | (Un)registers scene listener for notification about scene validation and repaint. See SceneListener section. |
convertSceneToView convertViewToScene | Converts location and boundary between scene coordination system and JComponent view coordination system. See Coordination System section. |
createWidgetHoverAction | Creates a widget-specific hover action. See HoverAction section. |
This listener allows you to receive following events:
Method | Description |
---|---|
sceneRepaint | Called when scene should be repainted. |
sceneValidating | Called at the beginning of Scene.validate method when the scene is going to be validated. See Validation Process section.
|
sceneValidated | Called at the end of Scene.validate method when the scene is validated. See Validation Process section.
|
This listener could be (un)registered using Scene.addSceneListener
and Scene.removeSceneListener
methods. Usually it is used by animators, scene layouts, Swing bindings and satellite views.
Converting from local to scene coordination system:
Point Widget.convertLocalToScene (Widget widget, Point localSystemPoint) { Point sceneSystemPoint = new Point (localSystemPoint); while (widget != null && widget != widget.getScene ()) { sceneSystemPoint += widget.getLocation (); widget = widget.getParentWidget (); } return sceneSystemPoint; }
Converting from scene to view coordination system:
Point Scene.convertSceneToView (Scene scene, Point sceneSystemPoint) { return new Point ((scene.getLocation () + sceneSystemPoint) * scene.getZoomFactor ()); }
Each widget has a layout assigned (AbsoluteLayout
by default). Layout
interface cares about placement of children widgets of the widget where the layout is assigned. For details how layout is involved in the validation process, see Validation Process section. Almost all layouts implementations can be reused by more widgets at the same time.
Widget also keeps constraints assigned to children widgets. Layout can obtain a constraint using Widget.getChildConstraint (childWidget)
method call.
Widget could be invisible too. In that case, the layout assigned to its parent widget must act like the invisible widget is not there. It means it must resolve location and bounds to value which does not affect the parent widget boundary. If it is not possible, then the location and the boundary of invisible child must be resolved as [0,0] and [0,0,0,0].
Created by: LayoutFactory.createAbsoluteLayout
Placement algorithm:
childWidget.getPreferredLocation
and uses it as the evaluated childWidget location. If null
, then it uses [0,0]
point instead.
childWidget.getPreferredBounds
and uses it as the evaluated childWidget boundary. If null
, then it uses [0,0,0,0]
rectangle instead.
Created by: LayoutFactory.createVerticalFlowLayout
, LayoutFactory.createHorizontalFlowLayout
This layout places children serially in vertical or horizontal line - one widget after another. It allows top-left, center, bottom-right and justify alignments and specifying gaps between children. It used childWidget.getPreferredBounds
for resolving boundary. The layout will use the widest/highest boundaries for a cell dimension.
The layout allows to work with Number-class constraints. The Number constraint has to be a positive number assigned to a child widget. Then it represents a ratio in with the remaining space (height for VerticalFlowLayout and width for HorizontalFlowLayout) is split and assigned to a particular widget. See test.layout.WeightFlowLayoutTest
for example.
Created by: LayoutFactory.createCardLayout
This layout has a single childWidget as active one. The active widget will be evaluated using activeChildWidget.getPreferredLocation
(if null
, then [0,0]
point is used instead) and activeChildWidget.getPreferredBounds
. The other children are evaluated with [0,0,0,0]
rectangle as boundary and placed at the top-left corner of activeChildWidget
.
For switching active child widget, use LayoutFactory.setActiveCard (widget, newActiveChildWidget)
method.
Created by: LayoutFactory.createOverlayLayout
It finds the "minimal-encapsulation" (ME) boundary which contains all the children boundaries. Then it sets boundary of each child as the calculated ME one. The widget boundary is set to ME boundary too. During justification, the boundary of each child is sets to the boundary of the widget. Children are placed one over another - the last child is painted on top and receives events as the first child.
When any widget is added/removed in the tree hierarchy of a scene or a widget is changed the way that affects its placement or boundary, then the widget (and the scene too) has to be validated again.
The process is done in following steps:
Widget.revalidate
method. The method automatically schedules the widget for revalidation.
Scene.validate
method. Usually it is invoked automatically at the end of Swing event processing.
Widget.Dependency
interface.
The process is described in details in next sub-sections.
Each Widget has two flags: requiresFullValidation
(marks modified widget) and requiresPartValidation
(used for back-tracing the tree from the root/scene widget down to all modified widgets). When a widget is asked to be scheduled for revalidation using Widget.revalidate
method, then the widget is marked with requiresFullValidation and requiresPartValidation. All widgets upto the scene are marked as a requiresPartValidation. Now the scene can trace-down all modified widgets without storing their references. Each widget with requiresPartValidation flag also fires an event on its Widget.Dependency listeners because they could be affected by a change of some child widget.
When Scene.validate
method is invoked, then it repeatedly validates scene until there is no modification - means Scene.isValidated
returns true
.
The validation process deep dives into the tree hierarchy and evaluates location and boundary using assigned layout of widgets that have requiresPartValidation set to true. Then it fires an event on all Widget.Dependency listeners for all children (recursively) of the widget with requiredFullValidation set to true.
Now all widgets are evaluated for new data. Because of used layout algorithms, it resolves boundary as the minimal "packed" boundary of the widget and its children - because each widget calculates its boundaries from its children boundary too.
In some cases you would like to extend boundary of some widgets e.g. when a widget with FlowLayout (with "justify" alignment) is a child of another widget with FlowLayout (with "justify" alignment). This can be done in this moment since all widgets are evaluated for minimal boundary and they can just expand. Therefore the validation process is deep-diving into the tree hierarchy (the depth is controlled by result of Layout.requiresJustification
method of each widget. For each widget (from the root to the bottom) it reevaluates the boundary again and calculates children boundaries based on the widget boundary.
This 2-pass validation process allows to have similar layout support as Swing has.
When a widget is scheduled for repaint, then its current (before reevaluation) boundary in the view coordination system is scheduled for repaint and the widget itself is added into the list of widgets scheduled for repaint.
After the scene is validated completely, then all widgets in repaint-list are scheduled for repaint too. It takes their current (newly evaluated) view coordination system boundaries.
Layout (assigned to a widget) defines placement of its children widgets. But in some cases you would like to do a single-time layout based on different layout algorithm (it could be even high-level layout of graph-oriented data).
The possible solution could be to temporarily assign the desired layout to the widget and to start revalidation. The problem is that you cannot temporarily assign widget because you cannot assure when the layout and scene-validation will be invoked. Also you would not be able to invoke "initial" layout of a scene when the Scene is going to appear for the first time on the screen. It is because the JComponent is not initialized yet and therefore the scene is not validated yet and therefore widget boundaries (which are usually required by the layout algorithms) are not resolved yet too.
Therefore SceneLayout
class has been introduced. When the SceneLayout is invoked by the developer, it starts listening on the scene to be validated (using SceneListener
). When the SceneListener.sceneValidated
method is called then the listener is removed and SceneLayout.performLayout
method is invoked.
There is a LayoutFactory.createDevolveWidgetLayout
method that creates a SceneLayout with performLayout
method implementation which temporarily sets the desired layout and invokes the specified layout at the end of scene validation process. This allows you use a LayerWidget
with default AbsoluteLayout, so the widgets inside will be able to freely move using MoveAction
and from time to time (e.g. started by user action) the layer widget will be layout using the specified temporary layout to organize the layer.
The widgets are visual elements that just know where they are on the scene and how to draw themself. By default they do not know how to behave. For defining the behavior there are WidgetAction
.
You can create various actions usually using ActionFactory.create*Action
methods. For example: ActionFactory.createMoveAction
method creates an action which allows user to move a widget (where the action is assigned to).
Factory methods usually requires some parameters. There are two kinds of parameters: Decorators and Providers. Decorator is used for specifying the visual appearance of objects temporarily created by an action. E.g. RectangularSelectAction has RectangularSelectDecorator
for acquiring the widget that will represent actual selection rectangle. Provider is used for specifying the custom behavior of an action. E.g. EditAction has EditProvider
with edit
method which is called when an user double-clicks on a widget where the action assigned to. It is upto the EditProvider
implementation whether it invokes an editor or run an application or does something totally different. Usually there are default implementations of the decorators and providers available in ActionFactory
class.
Usually single instance of a particular action could be reused and assigned to more Widgets at the same time.
Actions could be grouped into chains. The chain is an action that distributes the user-events to appropriate actions that are stored inside. Each Widget has its own chain that can be acquired by calling Widget.getActions
method. Chain allows to manage and query the list of its actions using addAction
, removeAction
, getActions
methods.
Sometimes you would like to have a scene which will behave differently based on a scene state
. E.g. you have a toolbar with modes: "Select" (when active then user can only select object), "Connect" (when active then user can only create connection between objects), "Move" (when active then user can only move objects in a scene), ...
For this purpose there are action tools. A scene has a single active action tool specified. It is null
by default and could be changed by Scene.setActiveTool
method. The active tool specified as a String
used for the tool identification. Each widget has Widget.createActions (String tool)
method for creating an action chain for the specified tool. When a chain is created for particular tool, then you can use Widget.getActions (String tool)
for faster access to the chain.
Each mouse/keyboard/drag'n'drop/focus event that comes from the AWT has to be processed. The processing it done by walking through the tree hierarchy of widgets. For mouse events, the widget boundaries and Widget.isHitAt
methods are used for checking before dispatching the event. The AWT-event is converted into the library-specific event (which is very similar to AWT event. The library-specific event has event.getPoint
method which location in local coordination system of the widget where an appropriate action is attached to. For each valid widget, all default actions (from Widget.getActions ()
) are processed. The processing continues until an event is consumed by an action.
The walk through process for a widget is done by:
Widget.isHitAt (mouseCursor)
method returns true. If so, then process the event with default action chain of the widget and then optionally with an action chain appropriate for the tool returned by Scene.getActiveTool
method.
There is the Scene.getPriorActions
code which returns an actions chain. All actions in this prior chain are processing an AWT event before any other actions used in the scene. If any of these prior actions consumes the event, the event processing is stopped. A prior action cannot lock even processing for itself. For example: see test.tool.CtrlKeySwitchToolTest
example which is switching active tool of a scene based on a state of Ctrl key.
The Widget.isEnabled
controls whether events can be processed by actions assigned to the widget or its children. The same behavior implemented for all types of events.
It is important to have correct order of actions assigned to a widget because some actions requires locking. Usually the "locking" actions are the one that requires mouse-dragging e.g. MoveAction, ResizeAction, ConnectAction, ... Usually it is enough to put HoverAction and SelectAction as the first two actions and others.
Actions usually require various parameters including for their creation e.g. LayerWidget
. See LayerWidget section for details.
The action is processing library-specific events and returns the event status at the end. Based on the status the library decides whether the event is consumed, locked, ... There are following statuses:
WidgetAction.State.REJECTED
- The event is not processed by the action. Event-processing has to continue.
WidgetAction.State.CONSUMED
- The event is processed and consumed by the action. Event-processing is stopped.
WidgetAction.State.CHAIN_ONLY
- The event is processed and consumed by the action. The rest of actions in the same action chain should be invoked too and then the event-processing is stopped.
WidgetAction.State.createLocked (Widget, WidgetAction)
- The event is processed and consumed by the action. The next AWT event has to be processed by the specified WidgetAction
and it has to take the specified Widget
as a parameter. Only when the next AWT event is not consumed by the specified action, then the whole-tree event-processing (described in AWT Event Processing section) is invoked. This is used for locking event-processing by a specific action during processing multiple-events user-actions like moving (MoveAction), starting new connection (ConnectAction), ...
All Swing events are processed by all widgets by default. Key-events has to processed differently because they do not contain a "location" information. Therefore all widgets would have to process those events.
Key events are usually not desired to be processed by all widgets. Therefore you can specify a type of event processing using Scene.setKeyEventProcessingType
method. There are following types specified by EventProcessingType
enum:
ALL_WIDGETS
- by default - Has the same behavior as the other events processing described previously.
FOCUSED_WIDGET_AND_ITS_PARENTS
- Based on the focused widget where a key event is processed by a focused widget and then its parents up to the root of the widget hierarchy. The scene has always a focused widget (if not specified or set to null
, then the scene itself is taken as focused).
FOCUSED_WIDGET_AND_ITS_CHILDREN
- Based on the focused widget where a key event is processed by a focused widget and then by all its children.
FOCUSED_WIDGET_AND_ITS_CHILDREN_AND_ITS_PARENTS
- Combines FOCUSED_WIDGET_AND_ITS_CHILDREN and FOCUSED_WIDGET_AND_ITS_PARENTS types.
The focused widget is set by Scene.setFocusedWidget
method. This method is not called automatically and it has to be called by a developer. Anyway there are a few places which are calling it automatically when:
ObjectScene.setFocusedObject
method to set an focused object. It automatically sets its related widget as focused one.
ObjectScene.setSelectedObjects
method to set selection objects. The first one is automatically set as a focused object too using ObjectScene.setFocusedObject
method.
CycleFocusAction
in your ObjectScene
.
Created by: ActionFactory.createActionMapAction ()
, ActionFactory.createActionMapAction (InputMap, ActionMap)
The action handles key events and popup menu creation. The keys and actions are obtained from specified InputMap and ActionMap parameters. The method without parameters uses the InputMap and ActionMap assigned to a JComponent view of a scene.
Created by: ActionFactory.createAcceptAction (AcceptProvider)
The action allows to handle D'n'D drop operation. AcceptProvider.isAcceptable
method is called for resolving whether the drop can happen and AcceptProvider.accept
is called for handling the drop operation.
Created by: ActionFactory.createAddRemoveControlPointAction
The action allows user to add a control point by double-clicking on path or remove control point by double-clicking on it. The action has routingPolicy
which is automatically set to ConnectionWidget
to prevent discarding of user changes by router assigned to the connection widget. For fixing moving-anchor problem use FreeRectangularAnchor
.
Created by: ActionFactory.createAlignWithMoveAction (AlignWithMoveDecorator, AlignWithWidgetCollector)
The action is similar to MoveAction but it allows snapping feature similar to Form Editor module. The AlignWithMoveDecorator
supplies a graphic for snapping lines and the AlignWithWidgetCollector
gathers all widgets that the snapping has to be checked against.
The outerBounds boolean parameter allows to specify whether client area or bounds of widgets is going to be checked.
Created by: ActionFactory.createAlignWithResizeAction (AlignWithMoveDecorator, AlignWithWidgetCollector)
The action is similar to ResizeAction
but it allows snapping feature similar to Form Editor module. The AlignWithMoveDecorator
supplies a graphic for snapping lines and the AlignWithWidgetCollector
gathers all widgets that the snapping has to be checked against.
The outerBounds boolean parameter allows to specify whether client area or bounds of widgets is going to be checked.
Created by: ActionFactory.createCycleFocusAction(CycleFocusProvider)
The action is usually assigned to the scene widget and allows cycling focus using "Tab" key. The behavior of the cycling is defined by CycleFocusProvider
interface which has two methods for cycling forward and backward.
If the action is created using ActionFactory.createCycleObjectSceneFocusAction
method then using "Tab" key a focused object is cycling through all objects on an object scene. It is using the ObjectScene.getIdentityCode
method for resolving the order of objects on a scene. For details, see Object Identity Code section.
Created by: ActionFactory.createConnectAction (ConnectDecorator, ConnectProvider)
The action should be assigned to the widget from which a connection should be created. The ConnectDecorator
supplies the graphics for the guiding connection line. The ConnectProvider
handles a connection creation and the connection source and target checking.
Created by: ActionFactory.createEditAction (EditProvider)
The action allows to handle double-clicking on assigned widget. EditProvider
handles the edit.
Created by: ActionFactory.createHoverAction (HoverProvider)
or ActionFactory.createHoverAction (TwoStatedHoverProvider)
The action allows to handle "hovering" effect when a mouse is hovering a widget where the action is assigned to. The first method invokes HoverProvider
always when a mouse-cursor moves over assigned widget. The TwoStatedHoverProvider
allows handling of an old and a new hovered-widget and provides unsetHovering (Widget)
and setHovering (Widget)
methods.
Because there is no Widget-related focus event generated by the library, therefore the hover action is not unsetting the hover after the mouse cursor leaves the related widget space. For fixing this, the same instance of the hover action has to be assigned to the Scene instance too. This will tell the library that when you are over a widget with hover actions assigned, then it is set into hovered state. When there is no widget (with hover actions assigned) under mouse cursor, then the scene itself is in hovered state and therefore the unsetHovering method is called in the previous widget.
There is a Scene.createWidgetHoverAction
method which creates a widget-specific hover action. The actual hovered widget is held by the Scene
instance and it only calls Widget.setState
method with an appropriate parameter.
There is also ObjectScene.createObjectHoverAction
method which creates an object-specific hover action. The hovering is based actual hovered object (not a hovered widget) in the ObjectScene instance. It means the mouse cursor is changed against widget that are representing objects in ObjectScene
. The actual hovered object is held by ObjectScene
instance. See ObjectScene section.
HoverAction
(with your own implementation of HoverProvider
or TwoStatedHoverProvider
interfaces) and ObjectHoverAction
can be used exclusively only.
How to use HoverAction
:
HoverProvider
or TwoStateProvider
.
HoverAction
from it.
How to use ObjectSceneHoverAction
:
ObjectScene.createObjectHoverAction
method.
ObjectScene
) that can be hovered.
ObjectHoverAction
assigned, then the ObjectScene.hoveredObjectM
property is changed to an object which belongs to the widget and all widgets registered for the object are called using Widget.notifyStateChanged
method where the new ObjectState
is passed as an argument. Each widget itself is defining how it will change its apperance based on the provided ObjectState
.
Created by: ActionFactory.createInplaceEditorAction (InplaceEditorProvider)
The action is a specialized case of EditAction. InplaceEditorProvider
is called to supply the in-place editor represented as a Swing JComponent. The editor can be controlled using InplaceEditorProvider.EditorController
interface. There is also ActionFactory.createTextFieldInplaceEditorAction (TextFieldInplaceEditor)
method that allows text in-place editor using JTextField
.
Using ActionFactory.getInplaceEditorController
method you can obtain an EditorController
for a specific action. The EditorController
allows e.g. to programatically open in-place editor using EditorController.openEditor
method.
The action also allows to specify an expansion directions using InplaceEditorProvider.getExpansionDirections
method. Everytime an editor component is changed, it should call InplaceEditorProvider.EditorController.notifyEditorComponentBoundsChanged
method to invoke recalculation of editor placement based on the initial bounds and expansion directions. Expansion direction of for built-in text-field based in-place editor there is an additional ActionFactory.createTextFieldInplaceEditorAction (TextFieldInplaceEditor,EnumSet<InplaceEditorProvider.ExpansionDirection>)
factory method created. In this case the initial bounds of the editor component are resolved as the bounds of the related widget.
Created by: ActionFactory.createMoveAction (MoveStrategy, MoveProvider)
When the action is assigned to a widget, it allows to drag/move the widget. The movement is done by calling Widget.setPreferredLocation
. It usually requires to have AbsoluteLayout to be set on the parent widget. MoveStrategy
allows processing of the new suggested location and therefore supplying snap-to-grid or lock axis movement strategy. The MoveProvider
interface allows to define behavior of movement e.g. you can move another widget than the widget that has the MoveAction assigned. There are built-in strategies and providers in ActionFactory
too. For more details, see Strategies section.
Created by: ActionFactory.createMoveControlPointAction (MoveControlPointProvider)
The action allows to move control points of a ConnectionWidget. There are built-in providers in ActionFactory
too.
Created by: ActionFactory.createPanAction ()
, ActionFactory.createWheelPanAction ()
The action allows to scroll the view using mouse movement while mouse-middle-button is pressed. It requires to have JComponent view placed into a JScrollPane. This action is usually assigned to a Scene only.
The WheelPanAction
allows to scroll ascene view using mouse-wheel. Then no key modifier is pressed, then the view is scrolled vertically. If shift key is held, then the view is scrolled horizontally.
Created by: ActionFactory.createPopupMenuAction (PopupMenuProvider)
The action allows to invoke popup-menu that is supplied by the provider.
Created by: ActionFactory.createRectangularSelectAction (ObjectScene, LayerWidget)
The action allows to handle object-selection using a rectangle that is created by dragging the widget where the action is supplied. This action is usually assigned to an ObjectScene
or LayerWidget
. See ObjectScene section.
Created by: ActionFactory.createReconnectAction (ReconnectDecorator, ReconnectProvider)
The action should be assigned to the ConnectionWidget
and allows reconnecting its source and target. ReconnectDecorator
supplies the graphics of temporary reconnecting line. ReconnectProvider
handles the reconnection and the connection source and target checking.
Created by: ActionFactory.createResizeAction (ResizeStrategy, ResizeControlPointResolver, ResizeProvider)
It is similar to MoveAction but used Widget.setPreferredBounds
. Also there are ResizeStrategy
and ResizeProvider
interfaces. There are built-in providers in ActionFactory
class too. For more details, see Strategies section.
ResizeControlPointResolver
allows customization of control points for resize action - resolves what kind of resizing is available at particular location of related widget.
Created by: ActionFactory.createSelectAction (SelectProvider)
The action allows to handle the single-click. It is similar to EditAction. It also allowed to condition the selection and allows using "aimed" object state.
Created by: ActionFactory.createZoomAction (zoomFactorMultiplier, animated)
, ActionFactory.createCenteredZoomAction (zoomFactorMultiplier)
, ActionFactory.createMouseCenteredZoomAction (zoomFactorMultiplier)
The action allows to zoom the view using scrolling mouse-middle-button. The action is usually assigned to a Scene only. For correct functionality of the action, the JComponent has to be put into scrollable panel like JScrollPane.
While the CenteredZoomAction
is zooming, a view is still centered to the center of the view. While the MouseCenteredZoomAction
is zooming, a view is still centered to mouse cursor.
Following example allows zooming and panning and allows to have a IconNodeWidget
moving and widget-specific hovering.
Scene scene = new Scene (); // has AbsoluteLayout by default IconNodeWidget iconNode = new IconNodeWidget (scene); iconNode.setImage (Utilities.loadImage ("path/to/my/image.png")); iconNode.setLabel ("MyIconNode"); scene.addChild (iconNode); scene.getActions ().addAction (ActionFactory.createCenteredZoomAction (1.1)); scene.getActions ().addAction (ActionFactory.createPanAction ()); // assign HoverAction - the actual hovered widget is held by the scene iconNode.getActions ().addAction (scene.createWidgetHoverAction ()); // MoveAction has to be after the hover action // otherwise MoveAction will always consume the event and HoverAction will never be invoked iconNode.getActions ().addAction (ActionFactory.createMoveAction ());
The actions are created with a specific invocation for users. For example: The MoveAction is invoked by pressing mouse button on a widget. Then the action consumes all the events until the mouse button is released. Therefore you have to be aware of the order of the actions that you are assigning to the widget.
Here is usually order when a select, in-place editor, hover, move actions.
WidgetAction.Chain actions = nodeWidget.getActions (); // creates an in-place editor action // opens the in-place editor when user double-clicks on the node widget // this action consumes only double-click events actions.addAction (ActionFactory.createTextFieldInplaceEditor (...)); // creates a object-based select action // just selects the node when mouse-button is pressed on the node widget // this action does not consume any event actions.addAction (myObjectScene.createSelectAction ()); // creates a move action // this action moves the node widget // this action consumes all events when the mouse is dragged // (means: while the mouse button is pressed on the node widget) actions.addAction (ActionFactory.createMoveAction ()); // creates a object-based hover action // this action controls the hover state of the node (and node widget) // this action consumes all events while the mouse is over the node widget // (means: almost everytime) - therefore use it as the last action. actions.addAction (myObjectScene.createObjectHoverAction ());
If you would like to use the connect action together with move action, you have to use the extended connect action. It is because the connect action is invoked exactly the same way as the move action and therefore the invocation is clashing. This means that the action, which is added first, locks the event processing to itself and the second action will not be invoked at any time. This could be prevented by making two different invocation. This is defined by the extended connect action. The extended connect action is invoked only if the Ctrl key is pressed while you are pressing the mouse button and then dragging the mouse. Now these two action invocations are clashing and you can use them both. Also in this case the order of the actions does not matter.
There are various methods where instances of actions can be created. Usually it done by a method in the ActionFactory
class. These actions are generic.
The Scene
class has a createWidgetHoverAction
method for creating a widget specific hover action. Call this method after you have assigned all actions to the scene (since this action is internally added to the actions chain of the scene and the nature of the action is that it consumes all events while a mouse cursor is over the scene which usually happens all the time).
The ObjectScene
class has a few methods for creating object-specific actions that controls selection, hovering, ... They are defined on the object scene since they are tightly coupled with the object scene. The select action created by object scene also sets the focused object.
In the future, methods that are defined on a scene and object scene could be moved to the ActionFactory
class too.
Sometimes you want to restrict the widget movement or resizing. For that purpose there is a MoveStrategy
or ResizeStrategy
interfaces which implementations could be passed as an argument to ActionFactory.createMoveAction
and ActionFactory.createResizeAction
methods.
The interfaces contains a single method which is called when a new location or boundary is going to be set. The suggested value is passed to the method as argument and developer has to implement the logic and return the location or boundary which is going to be set.
Following code shows a few examples for strategies:
public final class SnapTo16x16GridMoveStrategy implements MoveStrategy { public Point locationSuggested (Widget widget, Point originalLocation, Point suggestedLocation) { return new Point (suggestedLocation.x - suggestedLocation.x % 16, suggestedLocation.y - suggestedLocation.y % 16); } } public final class PositiveCoordinatesOnlyMoveStrategy implements MoveStrategy { public Point locationSuggested (Widget widget, Point originalLocation, Point suggestedLocation) { return new Point (Math.max (suggestedLocation.x, 0), Math.max (suggestedLocation.y, 0)); } }
This widget represents a connection/path between source and target point. The source and target point is defined by Anchor
. The path is defined by control points which are resolved by assigned Router
. By default it is DirectRouter
which routes the path as a straight line between the source and target point. The source and target points are not specified directly but by using anchors.
The ConnectionWidget
has properties for setting source and target point shapes, control points shape, ability to calculate which segment of path or control point is hit by a specific point, ...
Line color is defined by the foregroundColor
property.
Note: If you are changing a state of a ConnectionWidget (e.g. using it as a representation of an object in ObjectScene, GraphScene or GraphPinScene classes, then the ConnectionWidget.notifyStateChanged method is automatically called. See ObjectScene section for details. The built-in implementation of this method overrides value of foregroundColor
property based on a new state of the widget (the particular color is resolved by the LookFeel of the scene).
Also there is the lineColor
property. By default it is null
. If it is non-null value, then it overrides the object-state-based behavior of the ConnectionWidget. Therefore the lineColor value to be written foregroundColor property and uses as strictly defined color of the line.
A connection can have assigned a source and a target AnchorShape
and a end-point and control-point PointShape
. Source AnchorShape is used for defining a shape of a source point. Target anchor shape is used for defining a shape of a target point. An end point shape is used defining a shape of first or last control point. A control point shape is used for defining a shape of other control points except first and last. There are 4 predefined anchor shapes: AnchorShape.NONE
, AnchorShape.TRIANGLE_HOLLOW
, AnchorShape.TRIANGLE_FILLED
, AnchorShape.TRIANGLE_OUT
. Other built-in anchor shapes can be created by AnchorShapeFactory
class. There are 2 predefined point shapes: PointShape.SQUARE_FILLED_BIG
, PointShape.SQUARE_FILLED_SMALL
. Other built-in point shapes can be created using PointShapeFactory
class.
There is a controlPointsCursor
property which defines a mouse cursor when the mouse is over a visible control point.
There is a controlPointCutDistance
property. If set to positive number (by default 0), then the connection path is cut at each control point to make smoother corners. The smooth corners are not used during calculation whether a mouse-cursor hits the path.
Router
cares about creating paths. There are following built-in implementations:
RouterFactory.createDirectRouter
creates a straight line between the source and the target anchor.
RouterFactory.createOrthogonalSeachRouter (CollisionsCollector)
creates an orthogonal path that tries to avoid overlapping areas specified by the CollisionsCollector
. There is a built-in implementation RouterFactory.createWidgetsCollisionCollector (LayerWidget...)
which collects all validated children widgets of the specified layer widgets and for each it gets the widget boundary and claims it as a vertical and horizontal collision. In case of ConnectionWidget, it takes the path (defined by its control points) and claims all horizontal and vertical segments as appropriate collisions.
RouterFactory.createOrthogonalSeachRouter (ConnectionWidgetCollisionsCollector)
creates an orthogonal path similarly as the previous case but the ConnectionWidgetCollisionsCollector
gets a context of currently routed connection widget.
RouterFactory.createFreeRouter
is similar to DirectRouter
but it modifies only the first and last point of the route. The "middle" control points stay the same. This effect is used by AddRemoveControlPointAction
to maintain the control points created by user. From now this router is no longer useful, since it can be replaced by more flexible way: use any router that you like and set "Update-end-points-only" routing policy to the ConnectionWidget.
ConnectionWidget
allows developer to attach children widgets too. Those children could be attached relatively to the path using constraints. The constraint could be set using ConnectionWidget.setConstraint
method for each child widget. If a child widget has non-null preferred location then this preferred location will be added to the relative location.
See test.connectionlabels.ConnectionLabelsTest
for details.
routingPolicy
property. It ease management of control points changed by users. There are 4 values:
ALWAYS_ROUTE
- This is default value. The router is always invoked when a ConnectionWidget is changed and needs to be re-routed.
UPDATE_END_POINTS_ONLY
- The router is not invoked. Instead location of the first and the last control points are changed/updated to location computed by source and target anchors.
DISABLE_ROUTING_UNTIL_END_POINT_IS_MOVED
- The router is not invoked until the first or the last control points is moved (means it has different location from one computed by source or target anchor).
DISABLE_ROUTING
- The router is not invoked. The control points are freezed at the same locations.
routingPolicy
value to factory method of AddRemoveControlPointAction
and MoveControlPointAction
. If not null, then the value is automatically set to a ConnectionWidget which control points are modified by the action. Usually UPDATE_END_POINT_ONLY
is used.
UPDATE_END_POINT_ONLY
or DISABLE_ROUTING_UNTIL_END_POINT_IS_MOVED
to prevent discard of paths routed by the graph-oriented layout.
For usages, see test.routing.ActionsWithRoutingPolicyTest
and test.routing.RoutingPolicyTest
examples.
Method | Description |
---|---|
getStroke setStroke | Controls a stroke that is used for painting the line. |
getLineColor setLineColor | Controls line color. If not set, then the line color is obtained from LookFeel of the scene. |
isPaintControlPoints setPaintControlPoints | Controls whether control-points has to be painted too. |
getSourceAnchor setSourceAnchor | Manipulates with the source anchor. This method calls: Anchor.removeEntry and Anchor.addEntry methods to register the usage on anchor (uses ConnectionWidget.getSourceAnchorEntry as an entry for registration).
|
getTargetAnchor setTargetAnchor | Manipulates with the target anchor. This method calls: Anchor.removeEntry and Anchor.addEntry methods to register the usage on anchor (uses ConnectionWidget.getTargetAnchorEntry as an entry for registration).
|
getSourceAnchorEntry getTargetAnchorEntry | Returns source and target entry of the connection widget. |
getSourceAnchorShape setSourceAnchorShape getTargetAnchorShape setTargetAnchorShape | Controls a shape of source and target anchor. Shape is defined by a AnchorShape interface.
|
getControlPointShape setControlPointShape | Controls a shape of control points. Shape is defined by PointShape interface. The control points are painted only if isPaintControlPoints returns true.
|
getEndPointShape setEndPointShape | Controls a shape of end points (source and target). Shape is defined by PointShape interface. The end points are painted only if isPaintControlPoints returns true.
|
getControlPointCutDistance setControlPointCutDistance | Controls a cut distance where the line cut with half-angle line at every control point. See test.widget.ConnectionWidgetCutDistanceTest example.
|
getRouter setRouter | Controls a router assigned to the connection widget. The routers is calculating a path and sets a new one using setControlPoints method.
|
getRoutingPolicy setRoutingPolicy | Controls a routing policy assigned to the connection widget. It controls whether and how control points are re-routed. For details, see Routing Policy section. |
getControlPoints() setControlPoints(List<Point>controlPoints,boolean sceneLocations) getControlPoint(int index) | Manipulates with control points. If the sceneLocations is true then the controlPoints are recalculated relatively to the connection widget location otherwise controlPoints are taken as they are (and therefore they are taken as local locations). This method is usually called by the assigned router.
|
setConstraint (childWidget, alignment, placementInPercentage) | This method sets a constraint to a child widget (of the connection widget). It uses the specified alignment. The placement is resolved by the placementInPercentage parameter: 0.0f is the source anchor location, 1.0f is the target anchor location. |
setConstraint (childWidget, alignment, placementAtDistance) | This method sets a constraint to the child widget (of the connection widget). It uses the specified alignment. The placement is resolved by the placementAtDistance (in pixels) parameter: <=0 is the source anchor location; if >=path-length means the target anchor location. |
removeConstraint (childWidget) | This method removes a constraint for the child Widget. |
calculateRouting | This method forces path re-routing. It is called automatically and developer does not have to call it. |
isValidated | Return true if the widget is validated and its path is routed.
|
isRouted | Return true if the widget has its path routed.
|
reroute | Schedules connection widget for routing its path. |
getFirstControlPoint getLastControlPoint | Returns local location of first or last control point. |
getSourceAnchorShapeRotation getTargetAnchorShapeRotation | Returns an angle of source or target anchor shape in radians. The rotation is calculated as a angle between first/last path segment to the x-axis counter-clockwise. It usually used by AnchorShape for painting.
|
isHitAt(localLocation) | Checks whether the specified location hits (is a part of) the widget. |
isFirstControlPointHitAt(localLocation) | Checks whether the specified location hits (is a part of) the first control point of the widget. The checked area is specified by the end-point radius. |
isLastControlPointHitAt(localLocation) | Checks whether the specified location hits (is a part of) the last control point of the widget. The checked area is specified by the end-point radius. |
getControlPointHitAt(localLocation) | Checks whether the specified location hits (is part of) of a control point of the widget. The checked area is specified by the control-point radius. If hit, then it returns the control point index. |
Anchors are resolving the proper location of the point that they represent. Usually Anchor
is attached to a Widget
and listens on its placement changes using Widget.Dependency
interface. If the widget is changed, then the anchor notifies (using Anchor.Entry.revalidateEntry
method) all connection widgets that are using the anchor. Then all affect connection widgets schedule routing their paths. Anchor has possibility to use a widget reference or reference point of the opposite anchor that is assigned to the same connection widget. Anchor (set as the source anchor) has the target anchor as its opposite anchor and vice versa.
AnchorFactory.createFixedAnchor (Point)
method creates an anchor with fixed location that is not changed.
AnchorFactory.createCenterAnchor (Widget)
method creates an anchor which follows the center of the widget.
AnchorFactory.createRectangularAnchor (Widget)
method creates an anchor which resolves the point as a point on the widget boundary that the closest a widget of opposite anchor (assigned to the connection widget).
AnchorFactory.createCircularAnchor (Widget, int radius)
method creates an anchor which resolves the point as a point on a circle with radius around the widget center that the closest a widget of opposite anchor (assigned to the connection widget).
AnchorFactory.createDirectionalAnchor (Widget, DirectionalAnchorKind directions)
method creates an anchor which resolves the point as a point at the north-south-west-east center of side of the widget boundary that the closest a widget of opposite anchor (assigned to the connection widget).
AnchorFactory.createProxyAnchor (StateModel, Anchor...)
method creates an anchor that contains an array of anchor and forwards the point resolving to the single active anchor. The active anchor is defined by the index stored in the StateModel
class. This is used in VMD plug-in for handling pin-anchors - there the pin could be hidden in a node and then the anchor placement resolving has to be forward from the pin to the anchor assigned to the node.
Anchor is dependent on the related widget which is specified using a constructor parameter. The anchor automatically listen on the widget whether it is not changed. If the widget changes then anchor notifies all registered Anchor.Entry
interface implementation (usually ConnectionWidget.getSourceAnchorEntry
or ConnectionWidget.getTargetAnchorEntry
). These entries are notifying connection widgets to schedule routing of their paths because the location of the anchor could be changed.
Each time the connection widget sets its new source or target anchor. It also removes from old and adds to new Anchor
an appropriate Anchor.Entry
. A list of entries can be obtained using Anchor.getEntries
method.
This anchor dependency approach allows to have a ProxyAnchor
that delegates the computation to one of its anchors. This mechanism is used for Minimize ability in VMD plug-in. See Minimize Ability section for details.
The following code shows how to create a connection widget with 2 anchors - source anchor is RectangularAnchor
bound to the w1
widget and target anchor is FixedAnchor
with a fixed location at scene origin [0,0]
. A shape of target anchor will be filled-triangle.
ConnectionWidget c = new ConnectionWidget (scene); c.setSourceAnchor (AnchorFactory.createRectangularAnchor (w1)); c.setTargetAnchor (AnchorFactory.createFixedAnchor (new Point (0, 0))); c.setTargetAnchorShape (AnchorShape.TRIANGLE_FILLED);
The loop connection is a connection where the source and the target has the same anchor/widget. The visualization of such connection should be a small loop attached to the widget. For making this you have to have:
The only built-in anchor implementation that is creating an anchor that generates different locations for source and target is VMDNodeAnchor
class. You have to create a new instance for each Widget where the anchor has to be assigned. The best is to use it with OrthogonalSearchRouter
that will create a small orthogonal "loop" path.
The structures used in the library supports parallel edges. Be aware of anchor used for source and target of connection widget. Almost all implementations are calculating the same location for the same widget where the anchor is attached too. The only built-in implementation that could be used for this visualization is VMDNodeAnchor
.
This widget represents a glass-pane - similarly to JGlassPane
from Swing. It is transparent by default. Usually a set of LayerWidget
s are used in a widget that does not have any layout assigned. Then all layers are placed one over another which could be used for various optimalizations.
For example scene layers: You have a scene with AbsoluteLayout
and you will add LayerWidget
with AbsoluteLayout
as the scene children. This allows to have independent panels.
Usually following scene layers are used:
Following code creates a scene with two layers: mainLayer for objects, connectionLayer for connections. This is usual implementation when there are ConnectionWidget
s used in a scene. Because during validation the mainLayer is calculated completely first and then the connectionLayer is calculated completely. This allow the mentioned 1-pass calculation. If connection widgets would be in single mainLayer together with the main objects, then it can happen that w1
and w2
are calculated after c
and therefore c
would have to be calculated again. This means that at the end the connection widgets could be calculated twice. With the 2 layers approach it is always once.
Scene scene = new Scene (); LayerWidget mainLayer = new LayerWidget (scene); scene.addChild (mainLayer); LayerWidget connectionLayer = new LayerWidget (scene); scene.addChild (connectionLayer); Widget w1 = new LabelWidget (scene, "Source"); mainLayer.addChild (w1); Widget w2 = new LabelWidget (scene, "Target"); mainLayer.addChild (w2); ConnectionWidget c = new ConnectionWidget (scene); c.setSourceAnchor (AnchorFactory.createRectangularAnchor (w1)); c.setTargetAnchor (AnchorFactory.createRectangularAnchor (w2)); connectionLayer.addChild (c);
ObjectScene
class. It manages the mapping between objects and widgets and allows object-oriented access to the scene including selection, highlighting (something like secondary selection), hovering, ... Each object can be mapped to multiple widgets.
The ObjectScene
does not create any widget or object. All objects and their widget peers has to be created by developer, placed in the tree hierarchy of widgets and then registered using ObjectScene.addObject (Object, Widget...)
method.
Internally objects are stored in Map
, therefore equals
method is used for comparing and identification. It is recommended to use objects which equals
method implemented as return object == this;
. Especially do not use Array
s, List
s, Set
s, Map
s, ... because:
ObjectScene scene = new ObjectScene (); Map m1 = new HashMap (); // registers "m1" instance scene.addObject (m1); Map m2 = new HashMap (); // the m2 object is already in the scene because "m1.equals (m2)" is true scene.isObject (m2); // return true scene.addObject (m2); // ERROR - already added
Each widget at scene has its state (defined with ObjectState
class). A state represents whether a widget has: selected, highlighted, widgetHovered, objectHovered, widgetFocused, objectFocused and aimed flags set. The default value is result of ObjectScene.createNormal
method which does not have any flags raised.
The state is stored at each widget. You can manipulate with it using Widget.getState
and Widget.setState
method. Widget.notifyStateChanged
method is called after any state change on the widget. This allows to define state-specific behavior directly inside the widget implementation class.
ObjectScene
class manages similar states for its objects. The state is changed automatically when the selection, highlighting or hovering is changed in ObjectScene using ObjectScene.setSelectedObjects
, ObjectScene.setHoveredObject
, ... methods. When the state of an object is changed then the state of its widget peer is changed according to the object-state-change too. You can obtain the state of an object using ObjectScene.getObjectState
method. When the state of a widget is changed, it does not affect the state of particular object.
Method | Description |
---|---|
addObject (Object, Widget...) | Registers an object with mapping to its widget peers. Widget array cannot contain null value. For backward compatibility the widgets array could be also a single null value array. Then it be takes as empty widgets array.
|
removeObject (Object) | Unregisters an object. |
getObjects () | Returns a set of objects in the scene. |
isObject (Object) | Returns whether the specified object is registered. |
getSelectedObjects () setSelectedObjects (Set) | Controls the set of selected objects. The object state is propagated to a widget related to the object. |
getHighlightedObjects () setHighlightedObjects (Set) | Controls the set of highlighted objects. The object state is propagated to a widget related to the object. |
getHoveredObject () setHoveredObject (Object) | Controls a hovered object of the scene. Usually there is no need to call it. The object state is propagated to a widget related to the object. There could be only one focused object set at the same time. |
getFocusedObject () setFocusedObject (Object) | Controls a focused object of the scene. The object state is propagated to a widget related to the object. There could be only one focused object set at the same time. |
addObjectSceneListener(listener,eventTypes) removeObjectSceneListener(listener,eventTypes) | Registers and unregisters an object scene listener for a specified event types. See ObjectSceneListener section for details. |
createSelectAction () | Creates a object-specific select action. Usually assigned to a widget peer of an object which should be selected by user. The selected object is also set as focused object. |
createObjectHoverAction () | Creates object-specific hover action. This action controls the hovered object on a scene. It opposite to the widget-specific hover action. |
findWidget (Object) | Finds a widget peer for an object. It return a single widget only which is the first widget parameter passed to the ObjectScene.addObject method.
|
findWidgets (Object) | Finds a list of all widget peers registered to an object. |
findObject (Widget) | Finds an object for a widget peer. |
findStoredObject (Object templateObject) | Returns the stored instance of an object where storedObject.equals (templateObject) .
|
getObjectState (Object) | Returns an object state for a specified object. See ObjectState section. |
userSelectionSuggested (Set) | This method is called from select action (created by ObjectScene.createSelectAction method) and RectangularSelectProvider (created by ActionFactory.createObjectSceneRectangularSelectProvider method). It allows you to filter the selection done by an user. Default implementation of this method just calls setSelectedObjects method with the specified Set as a parameter.
|
getIdentityCode (Object) | This method is called to get identity code for a specified object. The identity code is used for resolving an order of objects since objects are not sorted in an object scene. The method is meant to be overridden to provide. Default implementation just returns a system identity hash code of each object. See Object Identity Code section. |
The ObjectScene allows to listen on a changed done inside. There are following event types:
The listener could be (un)registered using ObjectScene.add/removeObjectSceneListener(ObjectSceneListener,ObjectSceneEventType...)
methods. The listener is registered for a specified event types only. This is due to performance and API reasons. There is a single listener used for all event types to prevent heavy addition of add/remove*Listener
methods in Scene
-derived classes. Also the implementation of an event processing has to as fast as possible - especially processing objectAdded, objectRemoved and objectStateChanged.
ObjectSceneListener could be reused for more object scenes. There is a ObjectSceneEvent instance passed as the first parameter of every listener method. You can acquire ObjectScene instance by calling ObjectSceneEvent.getObjectScene()
method.
Objects in an object scene are not sorted - they are kept in an unsorted set. Therefore there is a ObjectScene.getIdentityCode
method meant to be overridden, which defines an order of those objects. The order is defined the way that an identity code must exist for each object and it must be unique in an object scene. The identity code is represented by Comparable interface and therefore it can be used for sorting and comparing.
The method is meant to be overridden to provider more accurate ordering identity code. Default implementation is returning an system identity hash-code of an object. If you are using Comparable-implementing classes for objects in an object scene, then they can represents/define identity code automatically and you can use directly as a return value of the getIdentityCode
method.
The library has support for graph-oriented modeling. There are two models supported:
Both are using generics and therefore developers can specify their classes that represents nodes, pins and edges. Following code contains examples for specifying the classes:
// GraphScene class is abstract, it has to be extended to supply UI therefore MyGraphScene is used // MyNode class is used for node object. MyEdge class is used for edge object. GraphScene<MyNode, MyEdge> graph1 = new MyGraphScene<MyNode, MyEdge> (); graph1.addNode (new MyNode ()); graph1.addEdge (new MyEdge ()); // String class is used for node, pin and edge object. GraphPinScene<String, String, String> graph2 = new GraphPinScene<String, String, String> (); graph2.addNode ("node1"); graph2.addPin ("node1", "pin1"); graph2.addEdge ("edge1"); graph2.addEdge ("pin1"); // ERROR - see next paragraph for explanation
Both classes are derived from ObjectScene
and therefore inherits its functionality. Also the same rule for object identification (using equals
method) is applied. All nodes, pins and edges are objects and therefore each of them has to have unique identification across all nodes, pins and edges.
Both class are using the similar approach for binding objects with widgets. When a node or an edge is added using GraphScene.addNode
or GraphScene.addEdge
method, then appropriate protected GraphScene.attachNodeWidget (Node)
or protected GraphScene.attachEdgeWidget (Edge)
method is called. These methods are meant to be implemented by a developer to supply the graphics. These methods are responsible for:
null
too - that would mean that object does not have a widget peer and therefore it is a non-visual object.
Optionally the developer can override default implementation of protected detachNodeWidget (Node, Widget)
and protected detachEdgeWidget (Edge, Widget)
method. These methods are responsible for removing the graphics. Default implementation just removes the widget peer from its parent widget.
Edges can have source node assigned using GraphScene.setEdgeSource
method. When source is changed then protected GraphScene.setEdgeSourceAnchor
method is called. The method is meant to be implemented by a developer to supply the graphics for applying the source node change. Usually it sets an anchor (related to the new source node) as a source anchor of a connection widget that represents the edge. Similarly it is done for target node of an edge with GraphScene.setEdgeTarget
and protected GraphScene.setEdgeTargetAnchor
methods.
Now the UI is managed and implemented in a developer's class which derives from GraphScene
. Therefore a developer does not have to care about the UI at all once the class is implemented and can just use the class using graph-oriented model approach.
The GraphPinScene
is similar - it just contains methods related to pins.
The following code shows how to create a GraphScene
with a node (represented as a String
in a model and as a IconNodeWidget
in a scene) and a edge (represented as a String
in a model and as a ConnectionWidget
in a scene). Nodes and edges will react on selection and hovering - the UI behavior of object-state is defined in IconNodeWidget
and ConnectionWidget
directly in their notifyStateChanged
methods.
public class MyGraphScene extends GraphScene<String, String> { private Widget mainLayer; private Widget connectionLayer; private WidgetAction moveAction = ActionFactory.createMoveAction (); public MyGraphScene () { mainLayer = new Widget (this); addChild (mainLayer); connectionLayer = new Widget (this); addChild (connectionLayer); } protected Widget attachNodeWidget (String node) { IconNodeWidget widget = new IconNodeWidget (this); widget.setLabel ("Node: " + node); WidgetAction.Chain actions = widget.getActions (); actions.addAction (createObjectHoverAction ()); actions.addAction (createSelectAction ()); actions.addAction (moveAction); mainLayer.addChild (widget); return widget; } protected Widget attachEdgeWidget (String edge) { ConnectionWidget widget = new ConnectionWidget (this); widget.setTargetAnchorShape (AnchorShape.TRIANGLE_FILLED); WidgetAction.Chain actions = widget.getActions (); actions.addAction (createObjectHoverAction ()); actions.addAction (createSelectAction ()); connectionLayer.addChild (widget); return widget; } protected void attachEdgeSourceAnchor (String edge, String oldSourceNode, String sourceNode) { ConnectionWidget edgeWidget = (ConnectionWidget) findWidget (edge); Widget sourceNodeWidget = findWidget (sourceNode); Anchor sourceAnchor = AnchorFactory.createRectangularAnchor (sourceNodeWidget); edgeWidget.setSourceAnchor (sourceAnchor); } protected void attachEdgeTargetAnchor (String edge, String oldTargetNode, String targetNode) { ConnectionWidget edgeWidget = (ConnectionWidget) findWidget (edge); Widget targetNodeWidget = findWidget (targetNode); Anchor targetAnchor = AnchorFactory.createRectangularAnchor (targetNodeWidget); edgeWidget.setTargetAnchor (targetAnchor); } }
Method | Description |
---|---|
addNode (Node) addEdge (Edge) | Adds a node or an edge to the scene. The attachNodeWidget or attachEdgeWidget is called before the object and its widget peer is really registered. After the registration notifyNodeAdded or notifyEdgeAdded method is called.
|
removeNode (Node) removeEdge (Edge) removeNodeWithEdges (Node) | Removes a node or an edge from the scene. The detachNodeWidget or detachEdgeWidget is called after the unregistering. While removing an edge, setEdgeSource (edge, null) and setTargetSource (edge, null) are called first. removeNodeWithEdges method removes all connected edges before removing the node.
|
removeNodeWithEdges (Node) removePinWithEdges (Pin) | Similar to removeNode and removePin methods but also removes all edges that are connected to them.
|
getNodes getEdges | Returns a collection of registered nodes or edges. |
setEdgeSource (Edge, Node) setEdgeTarget (Edge, Node) | Sets a source or a target to the edge. attachEdgeSourceAnchor or attachEdgeTargetAnchor method is called after the change is registered.
|
getEdgeSource getEdgeTarget | Returns a source or target node of the edge. |
findNodeEdges (Node, allowOutputEdges, allowInputEdges) | Returns a collection of edges connected to the node. If allowOutputEdges is true then the collection contains edges where the node is as a source. If allowInputEdges is true then the collection contains edges where the node is as a target.
|
findEdgesBetween (sourceNode, targetNode) | Returns a collection of edges that are connected to the sourceNode and the targetNode .
|
isNode (object) isEdge(object) | Returns whether an object is registered as a node or an edge. |
protected attachNodeWidget (Node) protected attachEdgeWidget (Edge) | There methods are meant to be overridden to create a widget peer for a node or an edge. The method is responsible for creating a widget peer, adding it into the tree hierarchy of widgets and returning it from the method. This method is called at the beginning of addNode or addEdge method. It is not mandatory to create a widget - null can be returned too.
|
protected attachEdgeSourceAnchor (Edge, oldSourceNode, newSourceNode) protected attachEdgeTargetAnchor (Edge, oldTargetNode, newTargetNode) | These methods are responsible to set a proper anchor to a widget (usually ConnectionWidget ) peer of the edge. The anchor is usually related to a widget peer of the newSourceNode or the newTargetNode . This method is called at the end of setEdgeSource or setEdgeTarget method - the source and the target is registered already. It is not mandatory to assign an anchor - it can do nothing too.
|
protected detachNodeWidget (Node, Widget) protected detachEdgeWidget (Edge, Widget) | These methods are called to at the end of removeNode or removeEdge methods - objects are unregistered already. Default implementation just removes the widget peer from its parent widget.
|
protected notifyNodeAdded (Node, Widget) protected notifyEdgeAdded (Edge, Widget) | Called at the end of addNode or addEdge methods - objects are registered already.
|
Method | Description |
---|---|
addNode (Node) addPin (Node, Pin) addEdge (Edge) | Adds a node, a pin or an edge to the scene. The attachNodeWidget , attachPinWidget or attachEdgeWidget is called before the object and its widget peer is really registered. After the registration notifyNodeAdded , notifyPinAdded or notifyEdgeAdded is called. Adding a pin requires to specify the node where the pin is attached to. There is no possibility to change it later.
|
removeNode (Node) removePin (Pin) removeEdge (Edge) | Removes a node, a pin or an edge from the scene. The detachNodeWidget , detachPinWidget or detachEdgeWidget is called after the unregistering. While removing an edge, setEdgeSource (edge, null) and setTargetSource (edge, null) are called first. While removing a node, all attached pins are removed first using "removePin" method.
|
getNodes getPins getEdges | Returns a collection of registered nodes, pins or edges. |
getPinNode | Returns a node where the pin is attached to. |
getNodePins | Returns a collection of pins that are attached to the node. |
setEdgeSource (Edge, Pin) setEdgeTarget (Edge, Pin) | Sets a source or a target to the edge. attachEdgeSourceAnchor or attachEdgeTargetAnchor is called after the change is registered.
|
getEdgeSource getEdgeTarget | Returns a source or target pin of the edge. |
findPinEdges (Pin, allowOutputEdges, allowInputEdges) | Returns a collection of edges connected to the pin. If allowOutputEdges is true then the collection contains edges where the pin is as a source. If allowInputEdges is true then the collection contains edges where the pin is as a target.
|
findEdgesBetween (sourcePin, targetPin) | Returns a collection of edges that are connected to the sourcePin and the targetPin .
|
isNode (object) isPin (object) isEdge (object) | Returns whether an object is registered as a node, a pin or an edge. |
protected attachNodeWidget (Node) protected attachPinWidget (Pin) protected attachEdgeWidget (Edge) | These methods are meant to be overridden to create a widget peer for a node, a pin or an edge. The method is responsible for creating a widget peer, adding it into the tree hierarchy of widgets and returning it from the method. This method is called at the beginning of addNode , addPin or addEdge method. It is not mandatory to create a widget - null can be returned too.
|
protected attachEdgeSourceAnchor (Edge, oldSourcePin, newSourcePin) protected attachEdgeTargetAnchor (Edge, oldTargetPin, newTargetPin) | These methods are responsible to set a proper anchor to a widget (usually ConnectionWidget ) peer of the edge. The anchor is usually related the newSourcePin or newTargetPin . This method is called at the end of setEdgeSource or setEdgeTarget method - the source and the target is registered already. It is not mandatory to assign an anchor - it can do nothing too.
|
protected detachNodeWidget (Node, Widget) protected detachPinWidget (Pin, Widget) protected detachEdgeWidget (Edge, Widget) | These methods are called to at the end of removeNode , removePin or removeEdge methods - objects are unregistered already. Default implementation just removes the widget peer from its parent widget.
|
protected notifyNodeAdded (Node, Widget) protected notifyPinAdded (Pin, Widget) protected notifyEdgeAdded (Edge, Widget) | Called at the end of addNode , addPin or addEdge methods - objects are registered already.
|
Part of the graph support is graph-oriented layout of nodes in a scene. Since the library contains two independent graph-oriented models, there is also a universal graph which unifies them and provides a read-only nodes-edges view on them. The universal graph is represented with UniversalGraph
abstract class and contains methods: getNodes
, getEdges
, findNodeEdges
, getEdgeSource
, getEdgeTarget
. These methods provides read-only access to the model.
Even thought the UniversalGraph class is abstract, it does not have to be implemented by a developer. The library creates them automatically.
All graph-oriented layout algorithms are implemented by classes based on GraphLayout
class. The class contains layoutGraph(GraphScene)
and layoutGraph(GraphPinScene)
method for invoking the layout on a particular scene. These methods just converts the scene into an universal graph and calls performGraphLayout(UniversalGraph)>
method which should implement and perform a particular graph-oriented layout.
The layoutGraph
method is performing the graph-oriented layout immediately. If the method is called before the scene validation, the algorithm may calculate with obsolete or uninitialized data. Therefore the SceneLayout
should be used. Following example should how to invoke the layout correctly:
GraphScene scene = ...; // instantiate particular layout e.g.: "new GridGraphLayout ();" GraphLayout layout = ...; SceneLayout sceneLayout = LayoutFactory.createSceneGraphLayout (scene, layout); // schedules the graph-oriented layout after the scene validation // it just schedules it but does not perform immediatelly sceneLayout.invokeLayout (); // or you can call "sceneLayout.invokeLayoutImmediatelly ();" // to schedule the layout and force immediate scene validation // so the layout will be performed in the "invokeLayoutImmediatelly" method
Similarly there is a layoutNodes
method which could be used to resolve location of a sub-set of nodes on a scene. The UnsupportedOperationException
could be throws from performLayoutNodes
then the implementation does not support it.
The graph layout has animated
which specifies whether a new node location is set directly to the widget or if a animation is used.
When implementing your own GraphLayout, you have to use GraphLayout.setResolvedNodeLocation
method call for setting node location.
The GraphLayout
has possibility to attach a GraphLayoutListener
which allows notification about: graphLayoutStarted
(called when graph layout is started), nodeLocationChanged
(called when a node location is changed, graphLayoutFinished
(called when graph layout is finished).
The algorithm organizes nodes in to a tree. Nodes that are not in the tree (defined by a root node and connecting edges) are resolved at [0,0] location.
The layout can be created using GraphLayoutFactory.createTreeGraphLayout
factory method.
For proper work, you have to specify a root node using GraphLayoutSupport.setTreeGraphLayoutRootNode
method. See test.graphlayout.TreeGraphLayoutTest
example for usages.
Using GraphLayoutSupport.setTreeGraphLayoutProperties
method you can set all parameters of the tree graph layout that has been specified in the factory method.
The algorithm organizes nodes in to a grid.
You can call setChecker
method to enable checker style. If a parameter is true, that the layout uses only half of nodes on a grid like single color on a chess board.
You can define horizontal and vertical gaps between nodes using the setGaps
method.
Widgets are not implemented as AWT/Swing components and the library will never have a full replacement for them. Therefore there is a possibility to integrate Swing components together with the scene. You have to create an instance of ComponentWidget
class and pass your AWT/Swing Component
there as a parameter. When the widget is added to the tree, then the component will be shown in the JComponent view. When the widget is removed from the tree, then the component will be removed from the JComponent view.
The component widget resolves it bounds as preferred-size-of-Component / zoomFactor
. The size of Component
is still the same in JComponent view - independent on zoom factor. Bounds of a component widget depends on zoom factor.
The library contains two implementation of scrollable widget:
ScrollWidget
which is created using widgets only.
SwingScrollWidget
which is created using widgets and Swing JScrollBar
s.
In both implementations you can used getView
and setView
methods for supplying the inner view widget that should be scrolled.
By default the scrollable widget takes the bounds of the inner view widget. If you want to have it scrollable, you have to limit the bounds of the scroll widget using Widget.setPreferredBounds
, Widget.setMaximumSize
or Widget.setPreferredSize
method.
For using in-place editor ability, you have to have an InplaceEditorProvider
interface implemented first. Then you can create an action using ActionFactory.createInplaceEditorAction (InplaceEditorProvider)
method. Then you can add the action to a widget where the in-place editor should be available. When an user double-clicks on the widget, the action is invoked and the provider is asked for creating in-place-editor Component
. The editor is invoked by pressing Enter key too.
The library contains a pre-defined JTextField
-based editor. An action can be created using ActionFactory.createInplaceEditorAction (TextFieldInplaceEditor)
method. The TextFieldInplaceEditor
interface defines the initial text and how to set the text.
If you want to open in-place editor programatically, you have to have particular in-place editor action. Then using ActionFactory.getInplaceEditorController
method you can get EditorController
instance which allows you to open the editor using EditorController.openEditor(Widget)
method.
The editor component boundary is resolved from the bounds of a widget where the in-place editor action is assigned. Then the bounds are processed by InplaceEditorProvider.getInitialEditorComponentBounds
method. Then the boundary is expanded/collapsed based on expansion directions returned by InplaceEditorProvider.getExpansionDirection
method.
Everytime a InplaceEditorProvider.EditorController.notifyEditorComponentBoundsChanged
method is called, then the boundary is recalculated again.
Following code creates an editable label widget:
final LabelWidget label = new LabelWidget (scene, "Text"); label.getActions ().addAction (ActionFactory.createInplaceEditorAction ( new TextFieldInplaceEditor () { public boolean isEnabled (Widget widget) { return true; } public String getText (Widget widget) { return ((LabelWidget) widget).getLabel (); } public void setText (Widget widget, String text) { ((LabelWidget) widget).setLabel (text); } } ), EnumSet.<InplaceEditorProvider.ExpansionDirection>of (InplaceEditorProvider.ExpansionDirection.RIGHT)); scene.addChild (label);
The LevelOfDetailsWidget
allows level-of-details feature based on 4 following parameters: hardMinimalZoom, softMinimalZoom, softMaximalZoom, hardMaximalZoom.
Following table describes whether children widgets of the LevelOfDetailsWidget
are visible or not - based on the zoom factor of a scene:
zoom factor | hardMinimalZoom or less | between | softMinimalZoom to softMaximalZoom | between | hardMaximalZoom or more |
---|---|---|---|---|---|
children are | invisible | partly visible | visible | partly visible | invisible |
The partial visibility is done by alpha blending.
The library contains a preliminary look&feel support defined by LookFeel
abstract class. Almost every method is dependent on ObjectState
parameter. Usually the interface should be used in Widget.notifyStateChanged
method implementation. The Scene.getLookFeel
and Scene.setLookFeel
methods allows manipulation with a look&feel assigned to a scene. The default LookFeel
implementation can be obtained using LookFeel.createDefaultLookFeel
method.
There is also an InputBindings class which is assigned to a scene. It allows to manage action-related scene options e.g. InputEvent
modifier of zoom actions used in the scene.
The library supports various 500ms-long animations. The animations are available on SceneAnimator
class acquired by Scene.getSceneAnimator
method call.
Supported animations:
Method | Description |
---|---|
getScene | Returns a related scene instance. |
isAnimatingPreferredLocation (Widget) | Checks whether preferredLocation property of the widget is animated. |
animatePreferredLocation (Widget, Point targetLocation) | Starts preferredLocation animation for the widget. |
isAnimatingPreferredBounds (Widget) | Checks whether preferredBounds property of the widget is animated. |
animatePreferredBounds (Widget, Rectangle targetBounds) | Starts preferredBounds animation for the widget. |
isAnimatingZoomFactor () | Checks whether the zoomFactor property is animated. |
animateZoomFactor (double targetZoomFactor) | Starts zoomFactor animation. |
getTargetZoomFactor () | Returns a target zoom factor of animation. |
isAnimatingBackgroundColor (Widget) | Checks whether backgroundColor property of the widget is animated. |
animateBackgroundColor (Widget, Color targetBackgroundColor) | Starts backgroundColor animation for the widget. |
isAnimatingForegroundColor (Widget) | Checks whether foregroundColor property of the widget is animated. |
animateForegroundColor (Widget, Color targetForegroundColor) | Starts foregroundColor animation for the widget. |
getPreferredLocationAnimator | Returns the preferred location animator which controls animation of preferred location of all widgets in the scene. |
getPreferredBoundsAnimator | Returns the preferred bounds animator which controls animation of preferred bounds of all widgets in the scene. |
getZoomAnimator | Returns the zoom animator of the scene. |
getColorAnimator | Returns the color animator which controls backgorund and foreground animation of all widgets in the scene. |
Each animation is done by an Animator
interface implementation. The Animator
allows to listen
to important events of an animator started, finished, reset, pre-tick and post-tick events using AnimatorListener
interface.
Instances of built-in animators can be obtained using getters of the SceneAnimator
class.
Following code sets a widget to [100,100]
without animation and smoothly moves the widget to [300,300]
.
Scene scene = new Scene (); LayerWidget layer = new LayerWidget (scene); scene.addChild (layer); LabelWidget label = new LabelWidget (scene, "Label"); layer.addChild (label); // non-animated move label.setPreferredLocation (new Point (100, 100)); // animated move scene.getSceneAnimator ().animatePreferredLocation (label, new Point (300, 300));
The library can be extended on various places.
A custom widget can be created by extending Widget
class. For proper work you have to implement at least:
calculateClientArea
method for resolving client-area (widget boundary without border insets) required by the widget itself. Graphics2D
instance can be acquired by getGraphics
method.
paintWidget
method for painting the widget. Graphics2D
instance can be acquired by getGraphics
method with local coordination system - means: the [0,0]
of the instance corresponds to [0,0]
of the Widget.getBounds
.
notifyStateChanged
method for specifying widget-state related UI.
Following example create a new custom widget that renders a circle:
public class CircleWidget extends Widget { private int r; public CustomWidgetTest (Scene scene, int radius) { super (scene); r = radius; } protected Rectangle calculateClientArea () { return new Rectangle (- r, - r, 2 * r + 1, 2 * r + 1); } protected void paintWidget () { Graphics2D g = getGraphics (); g.setColor (getForeground ()); g.drawOval (- r, - r, 2 * r, 2 * r); } }
See that the CircleWidget.paint
is not using getLocation()
method since the graphics is automatically translated to a point where the [0,0] means the origin of the widget. Therefore the drawing area reserved for the widget is exactly the same as the one returned from the CircleWidget.calculateClientArea
.
Custom action can be created by implementing WidgetAction
interface. There is a WidgetAction.Adapter
class similar to Swing adapters - all methods return State.REJECTED
status.
There is a WidgetAction.LockedAction
abstract class. There all methods return State.createLocked (widget, this)
or State.REJECTED
depending on the result of isLocked
abstract method call. This is used by actions which requires to lock the event processing for themselves - e.g. MoveAction, ConnectAction, ...
Custom layout can be created by implementing Layout
interface.
The layout (widget)
method should implement layout algorithm of children of the widget. It has to calculate the children boundaries based on childWidget.getPreferredLocation
and childWidget.getPreferredBounds
methods and optional constraints. Do not use values from Widget.getLocation
and Widget.getBounds
methods because there are not resolved yet. At the end it has to set evaluated children locations and bounds using Widget.resolveBounds
methods.
The justify (widget)
method should implement justification of children of the widget. It has to justify location and boundary of the children based the specified Widget boundary. Now you can use theWidget.getBounds method because the widget is resolved already. Justification is invoked only when requiresJustification
method returns true
. This method is called after calling layout
method.
Custom border can be created by implementing Border
interface. Border insets are added to the client-area of a widget where the border is assigned. paint
method has to paint the borders within the specified boundary. If isOpaque
returns true
then the border is responsible for painting every single pixel of the area specified by insets. Also only widget client-area will be painted by background paint. If isOpaque
returns false then whole widget boundary will be painted by background paint (if the widget is opaque also) first and then the border will be called for painting.
Custom anchor can be created by implementing Anchor
abstract class. compute
method is responsible for computing the actual location of an anchor for the specified ConnectionWidget
. For computation, you can use all widget properties (including getLocation and getBounds) and Anchor.getOppositeSceneLocation
method. You have to return a Result
object that specifies the computed anchor location and the supported directions for orthogonal routing.
For connection widget there are also AnchorShape
and PointShape
interfaces that can be implemented and used for defining a shape of anchor and control points of a connection widget.
AnchorShape.getCutDistance method allows you to specify how far a line path of a connection should be cut. This is e.g. important for a hollow triangle shape. It requires that a line is not painted to the target point. It requires to paint it just to the edge to the triangle.
Custom animator could be implemented by extending Animator
class. For creating an instance you need a SceneAnimator
instance which can be acquired by calling Scene.getSceneAnimator
method. For starting an animation, you have to call start
method. For checking whether the animation is still running, call isRunning
method. The animation algorithm has to be implemented in tick (double progress)
method. The progress
parameter value is in <0.0,1.0> interval where 0.0 is start (time=0ms) and 1.0 is end (time=500ms).
Custom router could be implemented by implementing Router
interface. It has routeConnection
method that is called for performing the routing. The routing could be done based on various information - usually based on:
connectionWidget.getSourceAnchor ().compute(connectionWidget.getSourceAnchorEntry ()).getAnchorSceneLocation ()
connectionWidget.getTargetAnchor ().compute(connectionWidget.getTargetAnchorEntry ()).getAnchorSceneLocation ()
A custom graph-oriented could be implemented by extending GraphLayout
abstract class. The particular algorithms should be implemented in the performGraphLayout
method which is invoked from layoutGraph methods only. The performGraphLayout
takes a single parameter of an universal graph on which the layout should be performed.
The node location is set using GraphLayout.setResolvedNodeLocation(UniversalGraph graph, Node node, Point newLocation)
method. This method sets/animates node to a new location and invokes callback on all registered listeners.
Following example should how to implement setting of a resolve location to a particular node:
UniversalGraph<MyNode,?> graph = ...; // the graph from the "performGraphLayout" method MyNode node = ...; Point resolvedNodeLocation = ...; setResolvedNodeLocation (graph, node, resolvedNodeLocation); // performs the setting
All general-purpose widgets are located in api.widget.general package.
The IconNodeWidget
class represents an image with a label. It has a specific look defined in the notifyStateChanged method. You can specify the alignment (horizontal or vertical) of the image and the label. It is using a vertical/horizontal flow layout. You can set an image and a label using setImage
and setLabel
method. You can obtain the image and the label widgets using getImageWidget
and getLabelWidget
methods. This could be used for adding actions to the particular parts like InplaceEditor to the label.
This is an experimental widget which is not included into the public API packages. If you want to use them you have to set up an implementation dependency on org.netbeans.api.visual. Implementation dependency means that you have available all classes in the library. The ListWidget
is included in org.netbeans.modules.visual.experimental.widget.general
package. Please, do not use implementation dependency. If you need to use it, then use it only for classes in org.netbeans.modules.visual.experimental
package or below.
The ListWidget
class represents a list and can hold multiple ListItemWidget
s. This class is not stabilized yet and could be changed in the future.
The VMD plug-in used by the Visual Mobile Designer in NetBeans Mobility Pack. The package contains complex UI implementation for visualization of graphs using node, pin and edge objects. The plug-in is placed in vmd package. The API of this package is not stable and could be changed in the future.
The scene is held by VMDGraphScene
class. A node is represented by VMDNodeWidget
. An edge is represented by ConnectionWidget
. A pin is represented by VMDPinWidget
.
VMDGraphScene
contains 4 layers: background (for rectangular selection), main (for nodes and pins), connection (for edges), interaction (for interaction like ConnectAction). VMDNodeWidget has nodeName, nodeType, nodeImage and glyphs properties. VMDPinWidget
has pinName and glyphs properties. An edge is using OrthogonalSearchRouter and tries to not overlap valid objects in main and connection layers. Pin could be added to a node using attachPinWidget
only, otherwise they are not registered correctly - do not use VMDNodeWidget.addChild
method.
The VMDNodeWidget
allows to specify the order and categories of pins shown in the list. It is done using VMDNodeWidget.sort
method. By default there are no categories and pins are sorted in the order where the have been attached to the node using VMDNodeWidget.attachPinWidget
.
The VMDGraphScene.layoutGraph
method layouts nodes on a scene using GridGraphLayout
class. After calling the method, you have to call Scene.validate
method manually to trigger the scene validation and therefore graph-oriented layout of a scene.
The VMDColorScheme class defines a UI of VMD widgets. There are two implementations available using VMDFactory.createOriginalColorScheme
, VMDFactory.createNetBeans60ColorScheme
method calls. The color scheme can be used as a parameter in widget constructors.
Question: Common problem of visualization is: What to do, when a scene become a little bit complicated because of many objects and connections between them?
Answer: The answer is simple: Reduce the complexity, hide unimportant and expose important objects.
A pattern for implementing this behavior is described in test.expand
package in examples module. It just adds all the widgets that should be expanded/collapsed into a widget. When the details widget has to be collapsed, then set its preferredBounds
property to new Rectangle()
. When it has to be expanded, then set its preferredBounds
property to null
.
VMDNodeWidget
has ability to control minimize mode using isMinimized
, setMinimized
and toogleMinimized
methods. When a node is minimized then the header is shown only and all pins are hidden.
When an edge is attach to a pin and the node is going to be minimized, the pin is going to hidden and therefore the anchor of the pin has to be reattached to the anchor of the node. This is done automatically when you will:
VMDNodeWidget.createAnchorPin (Anchor)
on an appropriate instance of node widget.
The createAnchorPin
creates a proxy anchor that is driven by a minimized state of the node and automatically switches/uses the node or the pin anchor as the active anchor.
There is Scene.paint (Graphics2D)
method that renders the scene into the specified Graphics2D
instance. The instance could be taken from any device e.g. screen, printer, BufferedImage
, PDF file writer, ...
Be aware that it is completely different Graphics2D
instance than the instance which is used for rendering the scene into the main view and which is used for calculating the scene (especially FontMetrics
). Therefore fonts could be "broken", so they will not fit exactly as in the main view.
Following code shows how to export the scene into PNG file.
Scene scene = ...; BufferedImage bi = new BufferedImage (dim.width, dim.height, BufferedImage.TYPE_4BYTE_ABGR); Graphics2D graphics = bi.createGraphics (); scene.paint (graphics); graphics.dispose (); JFileChooser chooser = new JFileChooser (); chooser.setDialogTitle ("Export Scene As ...")); chooser.setDialogType (JFileChooser.SAVE_DIALOG); chooser.setMultiSelectionEnabled (false); chooser.setFileSelectionMode (JFileChooser.FILES_ONLY); chooser.setFileFilter (new FileFilter() { public boolean accept (File file) { if (file.isDirectory ()) return true; return file.getName ().toLowerCase ().endsWith (".png"); // NOI18N } public String getDescription () { return "Portable Network Graphics (.png)"; // NOI18N } }); if (chooser.showSaveDialog (view) != JFileChooser.APPROVE_OPTION) return; File file = chooser.getSelectedFile (); if (! file.getName ().toLowerCase ().endsWith (".png")) // NOI18N file = new File (file.getParentFile (), file.getName () + ".png"); // NOI18N if (file.exists ()) { DialogDescriptor descriptor = new DialogDescriptor ( "File (" + file.getAbsolutePath () + ") already exists. Do you want to overwrite it?", "File Exists", true, DialogDescriptor.YES_NO_OPTION, DialogDescriptor.NO_OPTION, null); DialogDisplayer.getDefault ().createDialog (descriptor).setVisible (true); if (descriptor.getValue () != DialogDescriptor.YES_OPTION) return; } try { ImageIO.write (bi, "png", file); // NOI18N } catch (IOException e) { ErrorManager.getDefault ().notify (e); }
The previous code uses a BufferedImage for storing the image in the memory. When the scene is big, it could happen that the image does not fit into the memory. Therefore you have to use your Graphics2D
instance and Scene.paint (Graphics2D)
method for exporting the scene on-the-fly e.g. PDF writer, SVG file writer or meta graphics file writer.
The library contains ConvolveWidget
which takes a ConvolveOp
parameter and uses it for processing its children. Therefore you can create effects like blur or drop-shadow.
The ConvolveWidget
is using BufferedImage
as a off-screen image where the children widgets are rendered first and then the image is rendered to the scene. Because of the off-screen image it can significantly increase memory consumption. It reallocates the off-screen image only when the image is not able to contain all children. You can free the off-screen image manually using ConvolveWidget.clearCache
method.
The widget automatically assigns an EmptyBorder
with appropriate size based on the size of kernel on ConvolveOp parameter.
There is no API for enabling antialiasing. Anyway it can be implemented by overriding Scene.paintChildren
method. See following code.
class MyScene extends Scene { public void paintChildren () { Object anti = getGraphics ().getRenderingHint (RenderingHints.KEY_ANTIALIASING); Object textAnti = getGraphics ().getRenderingHint (RenderingHints.KEY_TEXT_ANTIALIASING); getGraphics ().setRenderingHint ( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); getGraphics ().setRenderingHint ( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); super.paintChildren (); getGraphics ().setRenderingHint (RenderingHints.KEY_ANTIALIASING, anti); getGraphics ().setRenderingHint (RenderingHints.KEY_TEXT_ANTIALIASING, textAnti); } }
The same could be done for any class derived from a Widget class too.
By default there is not (de)serialization support because the library cannot know which data should be serialized. Especially when you are integrating the library with an external data-model.
For example: During serialization of external data-model, the external model is created from the data in the XML file. Then you are able recreate almost whole scene. Usually the only missing thing is locations of nodes and control-points of edges (in graph-oriented model). In this case these locations are the only serialized data related to the library.
On the other hand, when users are using GraphScene
as the only/main model in the application then they would like to store whole the scene. Also the serialization format is various - XML, JavaBinarySerialization, ...
It is hard to create, reused or integrate universal description language for it. Instead, there is no support for serialization in the library and has to be done by developer.
Following code is (de)serializing a scene. This is usually used when you have an external model and you are using GraphPinScene for holding visual data. The most of the visual data can be recreated from an external model. The only stored data are locations of nodes.
public class SceneDataSerializer { private static final String SCENE_NODE = "Scene"; // NOI18N private static final String VERSION_ATTR = "version"; // NOI18N private static final String NODE_NODE = "Node"; // NOI18N private static final String NODEID_ATTR = "nodeID"; // NOI18N private static final String X_NODE = "x"; // NOI18N private static final String Y_NODE = "y"; // NOI18N private static final String VERSION_VALUE_1 = "1"; // NOI18N // creates an sceneXMLNode that has to be placed to the XML file then public Node serializeData (GraphPinScene<String, String, String> scene, Document file) { Node sceneXMLNode = file.createElement (SCENE_NODE); setAttribute (file, sceneXMLNode, VERSION_ATTR, VERSION_VALUE_1); // NOI18N for (String node : scene.getNodes ()) { Widget widget = scene.findWidget (node); if (widget != null) { Point location = widget.getPreferredLocation (); if (location != null) { Node dataXMLNode = file.createElement (NODE_NODE); setAttribute (file, dataXMLNode, NODEID_ATTR, node); setAttribute (file, dataXMLNode, X_NODE, Integer.toString (location.x)); setAttribute (file, dataXMLNode, Y_NODE, Integer.toString (location.y)); node.appendChild (dataXMLNode); } } } } return node; } // Returns true if deserialization is successfull // sceneXMLNode has to be found in the XML file first public boolean deserializeData (final GraphPinScene<String, String, String> scene, final Node sceneXMLNode) { if (! VERSION_VALUE_1.equals (getAttributeValue (data, VERSION_ATTR))) return false; SwingUtilities.invokeLater (new Runnable() { public void run () { deserializeDataVersion1 (scene, sceneXMLNode); scene.validate (); } }); return true; } private void deserializeDataVersion1 (GraphPinScene<String, String, String> scene, Node data) { for (Node node : getChildNode (data)) { if (NODE_NODE.equals (node.getNodeName ())) { String nodeID = getAttributeValue (node, NODEID_ATTR)); int x = Integer.parseInt (getAttributeValue (node, X_NODE)); int y = Integer.parseInt (getAttributeValue (node, Y_NODE)); Widget widget = scene.findWidget (nodeID); if (widget != null) widget.setPreferredLocation (new Point (x, y)); } } } private static String getAttributeValue (Node node, String attr) { try { if (node != null) { NamedNodeMap map = node.getAttributes (); if (map != null) { node = map.getNamedItem (attr); if (node != null) return node.getNodeValue (); } } } catch (DOMException e) { Debug.warning (e); } return null; } private static void setAttribute (Document xml, Node node, String name, String value) { NamedNodeMap map = node.getAttributes (); Attr attribute = xml.createAttribute (name); attribute.setValue (value); map.setNamedItem (attribute); } private static Node[] getChildNode (Node node) { NodeList childNodes = node.getChildNodes (); Node[] nodes = new Node[childNodes != null ? childNodes.getLength () : 0]; for (int i = 0; i < nodes.length; i++) nodes[i] = childNodes.item (i); return nodes; } }
Object / Feature / Method | |
---|---|
Swing | Visual Library |
JComponent | Widget |
JDialog, JFrame | Scene |
EventListener | WidgetAction |
For preventing deadlocks, use AWT-thread when manipulating with the library structures.