| NodePortsDemo.java |
1 /****************************************************************************
2 * This demo file is part of yFiles for Java 2.14.
3 * Copyright (c) 2000-2017 by yWorks GmbH, Vor dem Kreuzberg 28,
4 * 72070 Tuebingen, Germany. All rights reserved.
5 *
6 * yFiles demo files exhibit yFiles for Java functionalities. Any redistribution
7 * of demo files in source code or binary form, with or without
8 * modification, is not permitted.
9 *
10 * Owners of a valid software license for a yFiles for Java version that this
11 * demo is shipped with are allowed to use the demo source code as basis
12 * for their own yFiles for Java powered applications. Use of such programs is
13 * governed by the rights and conditions as set out in the yFiles for Java
14 * license agreement.
15 *
16 * THIS SOFTWARE IS PROVIDED ''AS IS'' AND ANY EXPRESS OR IMPLIED
17 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
18 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
19 * NO EVENT SHALL yWorks BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
21 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 *
27 ***************************************************************************/
28 package demo.view.advanced.ports;
29
30 import demo.view.hierarchy.GroupingDemo;
31 import y.base.Edge;
32 import y.base.Node;
33 import y.base.NodeCursor;
34 import y.base.NodeMap;
35 import y.geom.YRectangle;
36 import y.io.GraphMLIOHandler;
37 import y.io.graphml.graph2d.Graph2DGraphMLHandler;
38 import y.layout.LayoutOrientation;
39 import y.layout.Layouter;
40 import y.layout.NodeHalo;
41 import y.layout.hierarchic.IncrementalHierarchicLayouter;
42 import y.layout.hierarchic.incremental.RoutingStyle;
43 import y.layout.hierarchic.incremental.SimplexNodePlacer;
44 import y.layout.router.polyline.EdgeRouter;
45 import y.util.Maps;
46 import y.view.EdgeRealizer;
47 import y.view.EditMode;
48 import y.view.GenericNodeRealizer;
49 import y.view.Graph2D;
50 import y.view.Graph2DLayoutExecutor;
51 import y.view.Graph2DViewActions;
52 import y.view.NodeLabel;
53 import y.view.NodePort;
54 import y.view.NodePortLayoutConfigurator;
55 import y.view.NodeRealizer;
56 import y.view.NodeStateChangeEdgeRouter;
57 import y.view.ProxyShapeNodeRealizer;
58 import y.view.ShapeNodePainter;
59 import y.view.TooltipMode;
60 import y.view.hierarchy.DefaultGenericAutoBoundsFeature;
61 import y.view.hierarchy.DefaultHierarchyGraphFactory;
62 import y.view.hierarchy.GenericGroupNodeRealizer;
63 import y.view.hierarchy.GroupNodePainter;
64 import y.view.hierarchy.HierarchyManager;
65
66 import java.awt.BorderLayout;
67 import java.awt.Color;
68 import java.awt.EventQueue;
69 import java.awt.Graphics2D;
70 import java.awt.Shape;
71 import java.awt.event.ActionEvent;
72 import java.awt.event.ItemEvent;
73 import java.awt.event.ItemListener;
74 import java.awt.geom.Rectangle2D;
75 import java.util.Locale;
76 import java.util.Map;
77 import java.util.WeakHashMap;
78 import javax.swing.AbstractAction;
79 import javax.swing.Action;
80 import javax.swing.ActionMap;
81 import javax.swing.BorderFactory;
82 import javax.swing.ButtonGroup;
83 import javax.swing.JSplitPane;
84 import javax.swing.JToggleButton;
85 import javax.swing.JToolBar;
86
87 /**
88 * Demonstrates how to use {@link y.view.NodePort}s.
89 * <p>
90 * Things to try:
91 * </p>
92 * <ul>
93 * <li>Left-click on a port to select it.</li>
94 * <li>Drag a selected port to move it around.</li>
95 * <li>Press DELETE to remove all selected ports as well as all edges
96 * connecting to selected ports and all labels associated top selected
97 * ports.</li>
98 * <li>Drag a port that is not selected to start creating an edge from that
99 * port.</li>
100 * <li>Right-click on a node to display a context menu that allows for
101 * adding additional ports to a node.</li>
102 * <li>Right-click on a port to display a context menu that allows for
103 * <ul>
104 * <li>... adding a label that is associated to the port.</li>
105 * <li>... changing the valid positions of the port.</li>
106 * <li>... removing the port.</li>
107 * </ul>
108 * </li>
109 * <li>Select one or more ports then press CONTROL+A to select all ports.</li>
110 * <li>Select one or more ports then use the selection box (by dragging from
111 * an empty point) to select additional ports.</li>
112 * <li>Select one or more nodes then press CONTROL+ALT+G to create a common
113 * parent group node for the selected nodes.
114 * </ul>
115 * <p>
116 * Class {@link PortConfigurations} demonstrate how to customize the visual
117 * appearance of ports by re-using existing visualizations for nodes.
118 * <p>
119 * Nested classes {@link NormalEdgeProcessor} and {@link InterEdgeProcessor}
120 * demonstrate how to update edge-to-port associations when a group node is
121 * closed or a folder node is opened.
122 * Method {@link #createNodeHaloMap()} shows how to use
123 * {@link y.layout.NodeHalo}s to prevent layout and/or edge routing algorithms
124 * from generating node port overlaps.
125 * </p>
126 *
127 * @see <a href="http://docs.yworks.com/yfiles/doc/api/index.html#/dguide/realizer_related" target="_blank">Section Realizer-Related Features</a> in the yFiles for Java Developer's Guide
128 */
129 public class NodePortsDemo extends GroupingDemo {
130 private boolean fixPortsForLayout;
131 private boolean sharedPorts;
132 private boolean groupEdges;
133
134 public NodePortsDemo() {
135 this(null);
136 }
137
138 public NodePortsDemo( final String helpFilePath ) {
139 final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, new Palette(view), view);
140 splitPane.setBorder(BorderFactory.createEmptyBorder());
141 contentPane.add(splitPane, BorderLayout.CENTER);
142 addHelpPane(helpFilePath);
143 }
144
145 /**
146 * Overwritten to initialize the demo's hierarchy manager.
147 */
148 protected void initialize() {
149 super.initialize();
150
151 new HierarchyManager(view.getGraph2D());
152 }
153
154 /**
155 * Overwritten to add {@link y.view.NodePort} aware actions to handle closing group nodes and
156 * opening folder nodes.
157 */
158 protected void registerViewActions() {
159 super.registerViewActions();
160
161 final ActionMap amap = view.getCanvasComponent().getActionMap();
162
163 final Graph2DViewActions.CloseGroupsAction closeGroups =
164 new Graph2DViewActions.CloseGroupsAction(view);
165 // add callback implementations that ensure edges connected to ports are
166 // correctly reassigned when proxy realizers are used
167 // additionally, InterEdgeProcessor will assign all inter edges to the
168 // corresponding folder node's first port (if there is one)
169 // see class documentation
170 closeGroups.setNodeStateChangeHandler(new MyNodeStateChangeEdgeRouter(
171 new InterEdgeProcessor(new NormalEdgeProcessor())));
172 amap.put(Graph2DViewActions.CLOSE_GROUPS, closeGroups);
173 final Graph2DViewActions.OpenFoldersAction openFolders =
174 new Graph2DViewActions.OpenFoldersAction(view);
175 // add callback implementations that ensure edges connected to ports are
176 // correctly reassigned when proxy realizers are used
177 // see class documentation
178 openFolders.setNodeStateChangeHandler(new MyNodeStateChangeEdgeRouter(
179 new NormalEdgeProcessor()));
180 amap.put(Graph2DViewActions.OPEN_FOLDERS, openFolders);
181 }
182
183 /**
184 * Overwritten to create a {@link demo.view.advanced.ports.PortEditMode}
185 * instance that provides {@link y.view.NodePort} support.
186 * @return a {@link demo.view.advanced.ports.PortEditMode} instance.
187 */
188 protected EditMode createEditMode() {
189 return new PortEditMode();
190 }
191
192 /**
193 * Overwritten to enable tooltips only for node ports.
194 * <p>
195 * A tool tip will be displayed for a node port, if the node port has
196 * at least one label. The tool tip text will be the label text.
197 * Tool tip texts for node ports may be customized by overwriting
198 * {@link TooltipMode#getNodePortTip(y.view.NodePort)}.
199 * </p>
200 */
201 protected TooltipMode createTooltipMode() {
202 final TooltipMode tooltipMode = new TooltipMode();
203 tooltipMode.setNodeTipEnabled(false);
204 tooltipMode.setEdgeTipEnabled(false);
205 tooltipMode.setPortTipEnabled(true);
206 return tooltipMode;
207 }
208
209 /**
210 * Overwritten to add controls for layout and router options.
211 * @return the application tool bar.
212 */
213 protected JToolBar createToolBar() {
214 final Action edgeRoutingAction = new AbstractAction("Route Edges") {
215 public void actionPerformed(final ActionEvent e) {
216 routeOrthogonally();
217 }
218 };
219 edgeRoutingAction.putValue(Action.SMALL_ICON, SHARED_LAYOUT_ICON);
220
221 final Action layoutAction = new AbstractAction("Layout") {
222 public void actionPerformed(final ActionEvent e) {
223 layoutHierarchically();
224 }
225 };
226 layoutAction.putValue(Action.SMALL_ICON, SHARED_LAYOUT_ICON);
227
228
229 final JToolBar jtb = super.createToolBar();
230 jtb.addSeparator();
231 jtb.add(createActionControl(layoutAction));
232 jtb.add(createActionControl(edgeRoutingAction));
233 jtb.addSeparator(TOOLBAR_SMALL_SEPARATOR);
234 final JToggleButton fpJb = new JToggleButton("Fixed Ports");
235 fpJb.setToolTipText("Toggle the 'Fixed Ports' feature of the layout algorithms");
236 fpJb.addItemListener(new ItemListener() {
237 public void itemStateChanged( final ItemEvent e ) {
238 fixPortsForLayout = ItemEvent.SELECTED == e.getStateChange();
239 }
240 });
241 fpJb.doClick();
242 jtb.add(fpJb);
243 final JToggleButton geJb = new JToggleButton("Edge Grouping");
244 geJb.setToolTipText("Toggle the 'Automatic Edge Grouping' feature of the layout algorithms");
245 geJb.addItemListener(new ItemListener() {
246 public void itemStateChanged( final ItemEvent e ) {
247 groupEdges = ItemEvent.SELECTED == e.getStateChange();
248 }
249 });
250 jtb.add(geJb);
251 final JToggleButton spJb = new JToggleButton("Shared Ports");
252 spJb.setToolTipText("Toggle the 'Automatic Port Grouping' feature of the layout algorithms");
253 spJb.addItemListener(new ItemListener() {
254 public void itemStateChanged( final ItemEvent e ) {
255 sharedPorts = ItemEvent.SELECTED == e.getStateChange();
256 }
257 });
258 jtb.add(spJb);
259
260 final ButtonGroup buttonGroup = new ButtonGroup();
261 buttonGroup.add(fpJb);
262 buttonGroup.add(geJb);
263 buttonGroup.add(spJb);
264
265 return jtb;
266 }
267
268 /**
269 * Overwritten to support (de-)serialization of {@link SidePortLocationModel}.
270 * @return a configured {@link GraphMLIOHandler} instance.
271 */
272 protected GraphMLIOHandler createGraphMLIOHandler() {
273 final SidePortLocationModel.Handler modelHandler = new SidePortLocationModel.Handler();
274 final GraphMLIOHandler graphMLIOHandler = super.createGraphMLIOHandler();
275 final Graph2DGraphMLHandler graphMLHandler = graphMLIOHandler.getGraphMLHandler();
276 graphMLHandler.addDeserializationHandler(modelHandler);
277 graphMLHandler.addSerializationHandler(modelHandler);
278 return graphMLIOHandler;
279 }
280
281 /**
282 * Overwritten to prevent the {@link y.view.hierarchy.HierarchyManager}
283 * instance that is created in {@link #initialize()} from being replaced.
284 * @param rootGraph the graph for which a
285 * {@link y.view.hierarchy.HierarchyManager} has to be created.
286 * @return the {@link y.view.hierarchy.HierarchyManager} for the specified
287 * graph.
288 */
289 protected HierarchyManager createHierarchyManager( final Graph2D rootGraph ) {
290 return rootGraph.getHierarchyManager();
291 }
292
293 protected void loadInitialGraph() {
294 // ensure that port configurations are already registered
295 PortConfigurations.INSTANCE.getClass();
296
297 loadGraph("resource/NodePortsDemo.graphml");
298 }
299
300 /**
301 * Overwritten to change the default group and folder node representations.
302 */
303 protected void configureDefaultGroupNodeRealizers() {
304 //Create additional configuration for default group node realizers
305 Map map = GenericGroupNodeRealizer.createDefaultConfigurationMap();
306
307 GroupNodePainter gnp = new GroupNodePainter(new GroupShapeNodePainter());
308 map.put(GenericNodeRealizer.Painter.class, gnp);
309 map.put(GenericNodeRealizer.ContainsTest.class, gnp);
310 map.put(GenericNodeRealizer.GenericMouseInputEditorProvider.class, gnp);
311 map.put(GenericNodeRealizer.Initializer.class, gnp);
312
313 DefaultGenericAutoBoundsFeature abf = new DefaultGenericAutoBoundsFeature();
314 abf.setConsiderNodeLabelSize(true);
315 map.put(GenericGroupNodeRealizer.GenericAutoBoundsFeature.class, abf);
316 map.put(GenericNodeRealizer.GenericSizeConstraintProvider.class, abf);
317 map.put(GenericNodeRealizer.LabelBoundsChangedHandler.class, abf);
318
319 GenericNodeRealizer.Factory factory = GenericNodeRealizer.getFactory();
320 factory.addConfiguration(CONFIGURATION_GROUP, map);
321
322
323 GenericGroupNodeRealizer gnr = new GenericGroupNodeRealizer();
324
325 //Register first, since this will also configure the node label
326 gnr.setConfiguration(CONFIGURATION_GROUP);
327
328 //Nicer colors
329 gnr.setFillColor(new Color(202,236,255,132));
330 gnr.setLineColor(new Color(102, 102,153,255));
331 NodeLabel label = gnr.getLabel();
332 label.setBackgroundColor(null);
333 label.setTextColor(Color.BLACK);
334 label.setFontSize(15);
335
336
337 //Set default group and folder node realizers
338 DefaultHierarchyGraphFactory hgf = (DefaultHierarchyGraphFactory)
339 getHierarchyManager().getGraphFactory();
340
341 hgf.setProxyNodeRealizerEnabled(true);
342
343 hgf.setDefaultGroupNodeRealizer(gnr.createCopy());
344 hgf.setDefaultFolderNodeRealizer(gnr.createCopy());
345 }
346
347
348
349 private void layoutHierarchically() {
350 final SimplexNodePlacer placer = new SimplexNodePlacer();
351 placer.setBaryCenterModeEnabled(true);
352
353 final IncrementalHierarchicLayouter layouter =
354 new IncrementalHierarchicLayouter();
355 layouter.setLayoutOrientation(LayoutOrientation.LEFT_TO_RIGHT);
356 layouter.setNodePlacer(placer);
357
358 // ensures that normal edges, self loops, back loops, same layer edges,
359 // and edge groups are all routed using the same routing style
360 final RoutingStyle ers = new RoutingStyle(RoutingStyle.EDGE_STYLE_POLYLINE);
361 layouter.getEdgeLayoutDescriptor().setRoutingStyle(ers);
362
363 // a minimum distance of 15 will prevent edges from being routed through
364 // node ports of adjacent nodes because non-free node ports protrude their
365 // nodes by 10 size units
366 layouter.getEdgeLayoutDescriptor().setMinimumDistance(15);
367
368 layouter.getEdgeLayoutDescriptor().setMinimumFirstSegmentLength(20);
369 layouter.getEdgeLayoutDescriptor().setMinimumLastSegmentLength(40);
370 layouter.setMinimumLayerDistance(60);
371
372 doLayout(layouter, false);
373 }
374
375 private void routeOrthogonally() {
376 doLayout(new EdgeRouter(), true);
377 }
378
379 private void doLayout(
380 final Layouter layouter, final boolean edgeRouting
381 ) {
382 final Graph2DLayoutExecutor executor = new Graph2DLayoutExecutor();
383
384 final NodePortLayoutConfigurator configurator =
385 executor.getNodePortConfigurator();
386 configurator.setAutomaticPortConstraintsEnabled(fixPortsForLayout);
387 if (edgeRouting) {
388 // y.layout.router.polyline.EdgeRouter does not yet support port groups.
389 // Thus automatic edge grouping is used for shared ports as well.
390 final boolean grouped = groupEdges || sharedPorts;
391 configurator.setAutomaticEdgeGroupsEnabled(grouped);
392 } else {
393 configurator.setAutomaticEdgeGroupsEnabled(groupEdges);
394 configurator.setAutomaticPortGroupsEnabled(sharedPorts);
395 }
396 configurator.setStrongGroupConstraintsEnabled(edgeRouting);
397
398 final NodeMap haloMap = createNodeHaloMap();
399 view.getGraph2D().addDataProvider(NodeHalo.NODE_HALO_DPKEY, haloMap);
400 executor.doLayout(view, layouter);
401 view.getGraph2D().removeDataProvider(NodeHalo.NODE_HALO_DPKEY);
402 }
403
404 /**
405 * Creates a node map with a {@link NodeHalo} for each node with ports.
406 * A <code>NodeHalo</code> defines a rectangular area around a node.
407 * Layout algorithms avoid placing other nodes, edges, or labels in this area.
408 * this method creates node halos that reserve enough space for the visual
409 * representation of the node ports thereby preventing layout and/or edge
410 * routing algorithms from generating node port overlaps.
411 */
412 private NodeMap createNodeHaloMap() {
413 final NodeMap haloMap = Maps.createHashedNodeMap();
414 final Graph2D graph = view.getGraph2D();
415
416 // The node ports in this demo extend at most 10 space units beyond the
417 // node border. Therefore it is possible to use a shared halo instance
418 // for all nodes.
419 // In a real world application, it usually will be necessary to create
420 // distinct halo instances for each node depending on the geometry of the
421 // node ports of said node.
422 final NodeHalo sharedHalo = NodeHalo.create(10, 10, 10, 10);
423
424 for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
425 if (graph.getRealizer(nc.node()).portCount() > 0) {
426 haloMap.set(nc.node(), sharedHalo);
427 }
428 }
429 return haloMap;
430 }
431
432
433 public static void main( String[] args ) {
434 EventQueue.invokeLater(new Runnable() {
435 public void run() {
436 Locale.setDefault(Locale.ENGLISH);
437 initLnF();
438 (new NodePortsDemo("resource/nodeportshelp.html")).start();
439 }
440 });
441 }
442
443
444 /**
445 * Specifies the contract of a post-processor for
446 * {@link demo.view.advanced.ports.NodePortsDemo.MyNodeStateChangeEdgeRouter}.
447 */
448 private static interface Processor {
449 /**
450 * Post-processing for
451 * {@link demo.view.advanced.ports.NodePortsDemo.MyNodeStateChangeEdgeRouter#preEdgeStateChange(y.base.Edge, y.base.Node)}.
452 * @param edge the edge whose state is about to change.
453 * @param groupNode the node whose state change will trigger the edge state
454 * change.
455 */
456 public void preEdgeStateChange( Edge edge, Node groupNode );
457
458 /**
459 * Post-processing for
460 * {@link demo.view.advanced.ports.NodePortsDemo.MyNodeStateChangeEdgeRouter#postEdgeStateChange(y.base.Edge, y.base.Node)}.
461 * @param edge the edge whose state has changed.
462 * @param groupNode the node whose state change has triggered the edge state
463 * change.
464 */
465 public void postEdgeStateChange( Edge edge, Node groupNode );
466
467 /**
468 * Post-processing for
469 * {@link demo.view.advanced.ports.NodePortsDemo.MyNodeStateChangeEdgeRouter#postNodeStateChange(y.base.Node)}.
470 * @param groupNode the node whose state has changed.
471 */
472 public void postNodeStateChange( Node groupNode );
473 }
474
475 /**
476 * Simple {@link y.view.NodeStateChangeEdgeRouter} implementation that allows
477 * for arbitrary post-processing in
478 * {@link #preEdgeStateChange(y.base.Edge, y.base.Node)},
479 * {@link #postEdgeStateChange(y.base.Edge, y.base.Node)}, and
480 * {@link #postNodeStateChange(y.base.Node)}.
481 */
482 private static final class MyNodeStateChangeEdgeRouter
483 extends NodeStateChangeEdgeRouter {
484 private final Processor impl;
485
486 MyNodeStateChangeEdgeRouter( final Processor impl ) {
487 this.impl = impl;
488 }
489
490 /**
491 * Overwritten to allow for post-processing.
492 * @param edge the edge whose state is about to change.
493 * @param groupNode the node whose state change will trigger the edge state
494 * change.
495 */
496 protected void preEdgeStateChange( final Edge edge, final Node groupNode ) {
497 if (impl != null) {
498 impl.preEdgeStateChange(edge, groupNode);
499 }
500
501 super.preEdgeStateChange(edge, groupNode);
502 }
503
504 /**
505 * Overwritten to allow for post-processing.
506 * @param edge the edge whose state has changed.
507 * @param groupNode the node whose state change has triggered the edge state
508 * change.
509 */
510 protected void postEdgeStateChange( final Edge edge, final Node groupNode ) {
511 super.postEdgeStateChange(edge, groupNode);
512
513 if (impl != null) {
514 impl.postEdgeStateChange(edge, groupNode);
515 }
516 }
517
518 /**
519 * Overwritten to allow for post-processing.
520 * @param groupNode the node whose state has changed.
521 */
522 public void postNodeStateChange( final Node groupNode ) {
523 super.postNodeStateChange(groupNode);
524
525 if (impl != null) {
526 impl.postNodeStateChange(groupNode);
527 }
528 }
529 }
530
531 /**
532 * Reassigns node ports of normal edges from one realizer delegate to the
533 * other at nodes that use {@link y.view.ProxyShapeNodeRealizer} upon node
534 * state changes.
535 * By default, edges connecting to the <code>i</code>-th port of one realizer
536 * delegate are assigned to the <code>i</code>-th port of the other realizer
537 * delegate (if it exists).
538 * More sophisticated strategies can be realized by customizing method
539 * {@link #remapPort(y.view.NodeRealizer, y.view.EdgeRealizer, y.view.NodePort, boolean)}.
540 */
541 private static final class NormalEdgeProcessor implements Processor {
542 private final Map node2edgeState;
543
544 NormalEdgeProcessor() {
545 node2edgeState = new WeakHashMap();
546 }
547
548
549 /**
550 * Stores the current port information of the specified edge.
551 * @param edge the edge whose state is about to change.
552 * @param groupNode the node whose state change will trigger the edge state
553 */
554 public void preEdgeStateChange( final Edge edge, final Node groupNode ) {
555 if (groupNode.getGraph() instanceof Graph2D) {
556 final Graph2D graph = (Graph2D) groupNode.getGraph();
557 final HierarchyManager hm = graph.getHierarchyManager();
558 if (acceptEdge(hm, edge, groupNode)) {
559 Map edge2state = (Map) node2edgeState.get(groupNode);
560 if (edge2state == null) {
561 edge2state = new WeakHashMap();
562 node2edgeState.put(groupNode, edge2state);
563 }
564
565 if (edge.source() == groupNode) {
566 final NodePort port = NodePort.getSourcePort(graph.getRealizer(edge));
567 if (port != null &&
568 matches(port.getRealizer(), graph.getRealizer(groupNode))) {
569 EdgeState state = (EdgeState) edge2state.get(edge);
570 if (state == null) {
571 state = new EdgeState();
572 edge2state.put(edge, state);
573 }
574 state.sourcePort = port;
575 }
576 }
577 if (edge.target() == groupNode) {
578 final NodePort port = NodePort.getTargetPort(graph.getRealizer(edge));
579 if (port != null &&
580 matches(port.getRealizer(), graph.getRealizer(groupNode))) {
581 EdgeState state = (EdgeState) edge2state.get(edge);
582 if (state == null) {
583 state = new EdgeState();
584 edge2state.put(edge, state);
585 }
586 state.targetPort = port;
587 }
588 }
589 }
590 }
591 }
592
593 /**
594 * Assigns an appropriate port to the specified edge.
595 * Calls
596 * {@link #remapPort(y.view.NodeRealizer, y.view.EdgeRealizer, y.view.NodePort, boolean)}
597 * to determine which port is appropriate.
598 * @param edge the edge whose state has changed.
599 * @param groupNode the node whose state change has triggered the edge state
600 */
601 public void postEdgeStateChange( final Edge edge, final Node groupNode ) {
602 if (groupNode.getGraph() instanceof Graph2D) {
603 final Graph2D graph = (Graph2D) groupNode.getGraph();
604 final HierarchyManager hm = graph.getHierarchyManager();
605 if (acceptEdge(hm, edge, groupNode)) {
606 final Map edge2state = (Map) node2edgeState.get(groupNode);
607 final EdgeState state = (EdgeState) edge2state.get(edge);
608 if (state != null) {
609 if (edge.source() == groupNode) {
610 remapPort(
611 graph.getRealizer(groupNode), graph.getRealizer(edge),
612 state.sourcePort,
613 true);
614 }
615 if (edge.target() == groupNode) {
616 remapPort(
617 graph.getRealizer(groupNode), graph.getRealizer(edge),
618 state.targetPort,
619 false);
620 }
621 }
622 }
623 }
624 }
625
626 /**
627 * Removes the no longer needed stored port information for all edges
628 * related to the specified node.
629 * @param groupNode the node whose state has changed.
630 */
631 public void postNodeStateChange( final Node groupNode ) {
632 node2edgeState.remove(groupNode);
633 }
634
635
636 /**
637 * Remaps the specified edge realizer's port to one that belongs to the
638 * specified node realizer.
639 * @param nr the realizer that provides possible new ports.
640 * @param er the realizer whose port has to be remapped.
641 * @param port the old port.
642 * @param source if <code>true</code> the source port has to be remapped;
643 * otherwise the target port has to be remapped.
644 */
645 void remapPort(
646 final NodeRealizer nr,
647 final EdgeRealizer er,
648 final NodePort port,
649 final boolean source
650 ) {
651 if (port != null) {
652 final NodeRealizer oldNr = port.getRealizer();
653 if (!matches(oldNr, nr) &&
654 oldNr != null &&
655 oldNr.portCount() == nr.portCount()) {
656 bindPort(nr.getPort(indexOf(port)), er, source);
657 }
658 }
659 }
660
661
662 /**
663 * Binds the specified port to the specified edge.
664 * @param port the port to bind to the specified edge.
665 * @param er the realizer representing the edge.
666 * @param source if <code>true</code> the source port has to be bound;
667 * otherwise the target port has to be bound.
668 */
669 private static void bindPort(
670 final NodePort port,
671 final EdgeRealizer er,
672 final boolean source
673 ) {
674 if (source) {
675 NodePort.bindSourcePort(port, er);
676 } else {
677 NodePort.bindTargetPort(port, er);
678 }
679 }
680
681 /**
682 * Returns <code>true</code> if the specified edge's port assignment has to
683 * be remapped and <code>false</code> otherwise.
684 * @param hm the nesting structure of the specified edge's graph.
685 * @param edge the edge to check.
686 * @param groupNode the node whose state changes.
687 * @return <code>true</code> if the specified edge's port assignment has to
688 * be remapped and <code>false</code> otherwise.
689 */
690 private static boolean acceptEdge(
691 final HierarchyManager hm,
692 final Edge edge,
693 final Node groupNode
694 ) {
695 if (hm != null && hm.isFolderNode(groupNode) && hm.isInterEdge(edge)) {
696 return hm.getRealSource(edge) == groupNode ||
697 hm.getRealTarget(edge) == groupNode;
698 } else {
699 return edge.source() == groupNode || edge.target() == groupNode;
700 }
701 }
702
703 /**
704 * Returns the zero-based index of the specified port within its associated
705 * node realizer's collection of ports.
706 * @param port the port whose index is to be determined.
707 * @return the zero-based index of the specified port within its associated
708 * node realizer's collection of ports or <code>-1>/code> if the port
709 * currently is not associated to any node realizer.
710 */
711 private static int indexOf( final NodePort port ) {
712 final NodeRealizer owner = port.getRealizer();
713 if (owner != null) {
714 for (int i = 0, n = owner.portCount(); i < n; ++i) {
715 if (owner.getPort(i) == port) {
716 return i;
717 }
718 }
719 }
720 return -1;
721 }
722
723 /**
724 * Returns <code>true</code> if the first realizer equals the second or the
725 * second realizer's delegate and <code>false</code> otherwise.
726 * @param portNr a realizer associated to a {@link y.view.NodePort}.
727 * @param otherNr another realizer, possibly a
728 * {@link y.view.ProxyShapeNodeRealizer}.
729 * @return <code>true</code> if the first realizer equals the second or the
730 * second realizer's delegate and <code>false</code> otherwise.
731 */
732 private static boolean matches(
733 final NodeRealizer portNr,
734 final NodeRealizer otherNr
735 ) {
736 return portNr == otherNr ||
737 (otherNr instanceof ProxyShapeNodeRealizer &&
738 portNr == ((ProxyShapeNodeRealizer) otherNr).getRealizerDelegate());
739 }
740
741
742 /**
743 * Stores port information for an edge.
744 */
745 private static final class EdgeState {
746 NodePort sourcePort;
747 NodePort targetPort;
748 }
749 }
750
751 /**
752 * Reassigns node ports of edges that are converted to inter edges.
753 * By default, edges are automatically assigned to the first port of the
754 * corresponding folder node (if the node has any ports at all).
755 * More sophisticated strategies can be realized by customizing method
756 * {@link #remapPort(y.view.NodeRealizer, y.view.EdgeRealizer, boolean)}.
757 */
758 private static final class InterEdgeProcessor implements Processor {
759 private final Processor impl;
760
761 InterEdgeProcessor( final Processor impl ) {
762 this.impl = impl;
763 }
764
765 public void preEdgeStateChange( final Edge edge, final Node groupNode ) {
766 if (impl != null) {
767 impl.preEdgeStateChange(edge, groupNode);
768 }
769 }
770
771 /**
772 * Assigns an appropriate port to the specified edge.
773 * Calls
774 * {@link #remapPort(y.view.NodeRealizer, y.view.EdgeRealizer, boolean)}
775 * to determine which port is appropriate.
776 * @param edge the edge whose state has changed.
777 * @param groupNode the node whose state change has triggered the edge state
778 */
779 public void postEdgeStateChange( final Edge edge, final Node groupNode ) {
780 if (impl != null) {
781 impl.postEdgeStateChange(edge, groupNode);
782 }
783
784 if (groupNode.getGraph() instanceof Graph2D) {
785 final Graph2D graph = (Graph2D) groupNode.getGraph();
786 final HierarchyManager hm = graph.getHierarchyManager();
787 if (hm != null && hm.isFolderNode(groupNode) && hm.isInterEdge(edge)) {
788 if (edge.source() == groupNode && hm.getRealSource(edge) != groupNode) {
789 remapPort(
790 graph.getRealizer(groupNode),
791 graph.getRealizer(edge),
792 true);
793 }
794 if (edge.target() == groupNode && hm.getRealTarget(edge) != groupNode) {
795 remapPort(
796 graph.getRealizer(groupNode),
797 graph.getRealizer(edge),
798 false);
799 }
800 }
801 }
802 }
803
804 public void postNodeStateChange( final Node groupNode ) {
805 if (impl != null) {
806 impl.postNodeStateChange(groupNode);
807 }
808 }
809
810 /**
811 * Remaps the specified edge realizer's port to the first port of the
812 * specified node realizer.
813 * @param nr the realizer that provides possible new ports.
814 * @param er the realizer whose port has to be remapped.
815 * @param source if <code>true</code> the source port has to be remapped;
816 * otherwise the target port has to be remapped.
817 */
818 protected void remapPort(
819 final NodeRealizer nr,
820 final EdgeRealizer er,
821 final boolean source
822 ) {
823 if (nr.portCount() > 0) {
824 bindPort(nr.getPort(0), er, source);
825 }
826 }
827
828
829 /**
830 * Binds the specified port to the specified edge.
831 * @param port the port to bind to the specified edge.
832 * @param er the realizer representing the edge.
833 * @param source if <code>true</code> the source port has to be bound;
834 * otherwise the target port has to be bound.
835 */
836 private static void bindPort(
837 final NodePort port,
838 final EdgeRealizer er,
839 final boolean source
840 ) {
841 if (source) {
842 NodePort.bindSourcePort(port, er);
843 } else {
844 NodePort.bindTargetPort(port, er);
845 }
846 }
847 }
848
849 /**
850 * Painter implementation for group nodes that draws a special header
851 * compartment below the groups default label.
852 * In the default group node configuration, this compartment is drawn by
853 * the default label. This means said compartment will be painted over node
854 * ports because node labels are rendered after node ports.
855 */
856 private static final class GroupShapeNodePainter extends ShapeNodePainter {
857 private static final Color BACKGROUND = new Color(153, 204, 255, 255);
858
859 GroupShapeNodePainter() {
860 super(ROUND_RECT);
861 }
862
863 protected void paintFilledShape(
864 final NodeRealizer context,
865 final Graphics2D graphics,
866 final Shape shape
867 ) {
868 super.paintFilledShape(context, graphics, shape);
869
870 if (context.labelCount() > 0) {
871 final Shape oldClip = graphics.getClip();
872 final Color oldColor = graphics.getColor();
873
874 final Rectangle2D cb = oldClip.getBounds2D();
875 final YRectangle r = context.getLabel().getBox();
876 graphics.clip(new Rectangle2D.Double(cb.getX(), r.getY(), cb.getWidth(), r.getHeight()));
877 graphics.setColor(BACKGROUND);
878 graphics.fill(shape);
879
880 graphics.setColor(oldColor);
881 graphics.setClip(oldClip);
882 }
883 }
884 }
885 }
886