1   /****************************************************************************
2    **
3    ** This file is part of yFiles-2.9. 
4    ** 
5    ** yWorks proprietary/confidential. Use is subject to license terms.
6    **
7    ** Redistribution of this file or of an unauthorized byte-code version
8    ** of this file is strictly forbidden.
9    **
10   ** Copyright (c) 2000-2011 by yWorks GmbH, Vor dem Kreuzberg 28, 
11   ** 72070 Tuebingen, Germany. All rights reserved.
12   **
13   ***************************************************************************/
14  package demo.view.advanced.ports;
15  
16  import demo.view.hierarchy.GroupingDemo;
17  import y.base.Edge;
18  import y.base.Node;
19  import y.geom.YRectangle;
20  import y.layout.LayoutOrientation;                                              
21  import y.layout.Layouter;                                                       
22  import y.layout.hierarchic.IncrementalHierarchicLayouter;                       
23  import y.layout.hierarchic.incremental.SimplexNodePlacer;                       
24  import y.layout.router.EdgeGroupRouterStage;                                    
25  import y.layout.router.GroupNodeRouterStage;                                    
26  import y.layout.router.OrthogonalEdgeRouter;                                    
27  import y.layout.router.PatchRouterStage;                                        
28  import y.view.EdgeRealizer;
29  import y.view.EditMode;
30  import y.view.GenericNodeRealizer;
31  import y.view.Graph2D;
32  import y.view.Graph2DClipboard;
33  import y.view.Graph2DLayoutExecutor;                                            
34  import y.view.Graph2DUndoManager;
35  import y.view.Graph2DViewActions;
36  import y.view.NodeLabel;
37  import y.view.NodePort;
38  import y.view.NodePortLayoutConfigurator;                                       
39  import y.view.NodeRealizer;
40  import y.view.NodeStateChangeEdgeRouter;
41  import y.view.ProxyShapeNodeRealizer;
42  import y.view.ShapeNodePainter;
43  import y.view.TooltipMode;
44  import y.view.hierarchy.DefaultGenericAutoBoundsFeature;
45  import y.view.hierarchy.DefaultHierarchyGraphFactory;
46  import y.view.hierarchy.GenericGroupNodeRealizer;
47  import y.view.hierarchy.GroupNodePainter;
48  import y.view.hierarchy.HierarchyManager;
49  
50  import java.awt.BorderLayout;
51  import java.awt.Color;
52  import java.awt.EventQueue;
53  import java.awt.Graphics2D;
54  import java.awt.Shape;
55  import java.awt.event.ActionEvent;
56  import java.awt.event.InputEvent;
57  import java.awt.event.KeyEvent;
58  import java.awt.geom.Rectangle2D;
59  import java.util.Locale;
60  import java.util.Map;
61  import java.util.WeakHashMap;
62  import javax.swing.AbstractAction;
63  import javax.swing.Action;
64  import javax.swing.ActionMap;
65  import javax.swing.BorderFactory;
66  import javax.swing.InputMap;
67  import javax.swing.JSplitPane;
68  import javax.swing.JToggleButton;
69  import javax.swing.JToolBar;
70  import javax.swing.KeyStroke;
71  
72  /**
73   * Demonstrates how to use {@link y.view.NodePort}s.
74   * <p>
75   * Things to try:
76   * </p>
77   * <ul>
78   *   <li>Left-click on a port to select it.</li>
79   *   <li>Drag a selected port to move it around.</li>
80   *   <li>Press DELETE to remove all selected ports as well as all edges
81   *       connecting to selected ports and all labels associated top selected
82   *       ports.</li>
83   *   <li>Drag a port that is not selected to start creating an edge from that
84   *       port.</li>
85   *   <li>Right-click on a node to display a context menu that allows for
86   *       adding additional ports to a node.</li>
87   *   <li>Right-click on a port to display a context menu that allows for
88   *     <ul>
89   *       <li>... adding a label that is associated to the port.</li>
90   *       <li>... changing the valid positions of the port.</li>
91   *       <li>... removing the port.</li>
92   *     </ul>
93   *   </li>
94   *   <li>Select one or more ports then press CONTROL+A to select all ports.</li>
95   *   <li>Select one or more ports then use the selection box (by dragging from
96   *       an empty point) to select additional ports.</li>
97   *   <li>Select one or more nodes then press CONTROL+ALT+G to create a common
98   *       parent group node for the selected nodes.
99   * </ul>
100  * <p>
101  * Class {@link PortConfigurations} demonstrate how to customize the visual
102  * appearance of ports by re-using existing visualizations for nodes.
103  * <p>
104  * Nested classes {@link NormalEdgeProcessor} and {@link InterEdgeProcessor}
105  * demonstrate how to update edge-to-port associations when a group node is
106  * closed or a folder node is opened.
107  * </p>
108  *
109  * @see <a href="http://docs.yworks.com/yfiles/doc/developers-guide/realizer_related.html">Section Realizer-Related Features</a> in the yFiles for Java Developer's Guide
110  */
111 public class NodePortsDemo extends GroupingDemo {
112   private Graph2DUndoManager undoManager;
113 
114   private boolean fixPortsForLayout;                                            
115   private boolean groupEdges;                                                   
116 
117   public NodePortsDemo() {
118     this(null);
119   }
120 
121   public NodePortsDemo( final String helpFilePath ) {
122     final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, new Palette(view), view);
123     splitPane.setBorder(BorderFactory.createEmptyBorder());
124     contentPane.add(splitPane, BorderLayout.CENTER);
125     addHelpPane(helpFilePath);
126   }
127 
128   /**
129    * Overwritten to initialize the demo's undo engine.
130    */
131   protected void initialize() {
132     super.initialize();
133 
134     new HierarchyManager(view.getGraph2D());
135 
136     undoManager = new Graph2DUndoManager(view.getGraph2D());
137     undoManager.setViewContainer(view);
138   }
139 
140   /**
141    * Overwritten to add copy/cut/paste key bindings as well as
142    * {@link y.view.NodePort} aware actions to handle closing group nodes and
143    * opening folder nodes.
144    */
145   protected void registerViewActions() {
146     super.registerViewActions();
147 
148     final ActionMap amap = view.getCanvasComponent().getActionMap();
149     final InputMap imap = view.getCanvasComponent().getInputMap();
150 
151     final Graph2DClipboard clipboard = new Graph2DClipboard(view);
152     amap.put("CUT", clipboard.getCutAction());
153     imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, InputEvent.CTRL_MASK),"CUT");
154 
155     amap.put("COPY", clipboard.getCopyAction());
156     imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_MASK), "COPY");
157 
158     amap.put("PASTE", clipboard.getPasteAction());
159     imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_MASK), "PASTE");
160 
161 
162     final Graph2DViewActions.CloseGroupsAction closeGroups =
163             new Graph2DViewActions.CloseGroupsAction(view);
164     // add callback implementations that ensure edges connected to ports are
165     // correctly reassigned when proxy realizers are used
166     // additionally, InterEdgeProcessor will assign all inter edges to the
167     // corresponding folder node's first port (if there is one)
168     // see class documentation
169     closeGroups.setNodeStateChangeHandler(new MyNodeStateChangeEdgeRouter(
170             new InterEdgeProcessor(new NormalEdgeProcessor())));
171     amap.put(Graph2DViewActions.CLOSE_GROUPS, closeGroups);
172     final Graph2DViewActions.OpenFoldersAction openFolders =
173             new Graph2DViewActions.OpenFoldersAction(view);
174     // add callback implementations that ensure edges connected to ports are
175     // correctly reassigned when proxy realizers are used
176     // see class documentation
177     openFolders.setNodeStateChangeHandler(new MyNodeStateChangeEdgeRouter(
178             new NormalEdgeProcessor()));
179     amap.put(Graph2DViewActions.OPEN_FOLDERS, openFolders);
180   }
181 
182   /**
183    * Overwritten to create a {@link demo.view.advanced.ports.PortEditMode}
184    * instance that provides {@link y.view.NodePort} support.
185    * @return a {@link demo.view.advanced.ports.PortEditMode} instance.
186    */
187   protected EditMode createEditMode() {
188     return new PortEditMode();
189   }
190 
191   /**
192    * Overwritten to enable tooltips only for node ports.
193    */
194   protected TooltipMode createTooltipMode() {
195     TooltipMode tooltipMode = new TooltipMode();
196     tooltipMode.setNodeTipEnabled(false);
197     tooltipMode.setEdgeTipEnabled(false);
198     tooltipMode.setNodeTipEnabled(true);
199     return tooltipMode;
200   }
201 
202   /**
203    * Overwritten to add controls for undo/redo.
204    * @return the application tool bar.
205    */
206   protected JToolBar createToolBar() {
207     final Action undoAction = undoManager.getUndoAction();
208     undoAction.putValue(Action.SMALL_ICON, getIconResource("resource/undo.png"));
209     undoAction.putValue(Action.SHORT_DESCRIPTION, "Undo");
210 
211     final Action redoAction = undoManager.getRedoAction();
212     redoAction.putValue(Action.SMALL_ICON, getIconResource("resource/redo.png"));
213     redoAction.putValue(Action.SHORT_DESCRIPTION, "Redo");
214 
215     final Action edgeRoutingAction = new AbstractAction("Route Edges") {                       
216       public void actionPerformed(final ActionEvent e) {                                       
217         routeOrthogonally();                                                                   
218       }                                                                                        
219     };                                                                                         
220     edgeRoutingAction.putValue(Action.SMALL_ICON, SHARED_LAYOUT_ICON);                         
221 
222     final Action layoutAction = new AbstractAction("Layout") {                                 
223       public void actionPerformed(final ActionEvent e) {                                       
224         layoutHierarchically();                                                                
225       }                                                                                        
226     };                                                                                         
227     layoutAction.putValue(Action.SMALL_ICON, SHARED_LAYOUT_ICON);                              
228 
229     final Action toggleFixedPortsAction = new AbstractAction("Fixed Ports") {                  
230       public void actionPerformed(final ActionEvent e) {                                       
231         fixPortsForLayout = !fixPortsForLayout;                                                
232       }                                                                                        
233     };                                                                                         
234     toggleFixedPortsAction.putValue(Action.SHORT_DESCRIPTION,                                  
235         "Toggle the 'Fixed Ports' feature of the layout algorithms");                          
236 
237     final Action toggleGroupEdgesAction = new AbstractAction("Edge Grouping") {                
238       public void actionPerformed(final ActionEvent e) {                                       
239         groupEdges = !groupEdges;                                                              
240       }                                                                                        
241     };                                                                                         
242     toggleGroupEdgesAction.putValue(Action.SHORT_DESCRIPTION,                                  
243         "Toggle the 'Automatic Edge Grouping' feature of the layout algorithms");              
244 
245     final JToolBar jtb = super.createToolBar();
246     jtb.addSeparator();
247     jtb.add(undoAction);
248     jtb.add(redoAction);
249 
250     jtb.addSeparator();                                                                        
251     jtb.add(createActionControl(layoutAction));                                                
252     jtb.add(createActionControl(edgeRoutingAction));                                           
253     jtb.addSeparator(TOOLBAR_SMALL_SEPARATOR);                                                 
254     jtb.add(new JToggleButton(toggleFixedPortsAction));                                        
255     jtb.addSeparator(TOOLBAR_SMALL_SEPARATOR);                                                 
256     jtb.add(new JToggleButton(toggleGroupEdgesAction));                                        
257 
258     return jtb;
259   }
260 
261   /**
262    * Overwritten to prevent the {@link y.view.hierarchy.HierarchyManager}
263    * instance that is created in {@link #initialize()} from being replaced.
264    * @param rootGraph the graph for which a
265    * {@link y.view.hierarchy.HierarchyManager} has to be created.
266    * @return the {@link y.view.hierarchy.HierarchyManager} for the specified
267    * graph.
268    */
269   protected HierarchyManager createHierarchyManager( final Graph2D rootGraph ) {
270     return rootGraph.getHierarchyManager();
271   }
272 
273   protected void loadInitialGraph() {
274     // ensure that port configurations are already registered
275     PortConfigurations.INSTANCE.getClass();
276 
277     loadGraph("resource/NodePortsDemo.graphml");
278 
279     undoManager.resetQueue();
280   }
281 
282   /**
283    * Overwritten to change the default group and folder node representations.
284    */
285   protected void configureDefaultGroupNodeRealizers() {
286     //Create additional configuration for default group node realizers
287     Map map = GenericGroupNodeRealizer.createDefaultConfigurationMap();
288 
289     GroupNodePainter gnp = new GroupNodePainter(new GroupShapeNodePainter());
290     map.put(GenericNodeRealizer.Painter.class, gnp);
291     map.put(GenericNodeRealizer.ContainsTest.class, gnp);
292     map.put(GenericNodeRealizer.GenericMouseInputEditorProvider.class, gnp);
293     map.put(GenericNodeRealizer.Initializer.class, gnp);
294 
295     DefaultGenericAutoBoundsFeature abf = new DefaultGenericAutoBoundsFeature();
296     abf.setConsiderNodeLabelSize(true);
297     map.put(GenericGroupNodeRealizer.GenericAutoBoundsFeature.class, abf);
298     map.put(GenericNodeRealizer.GenericSizeConstraintProvider.class, abf);
299     map.put(GenericNodeRealizer.LabelBoundsChangedHandler.class, abf);
300 
301     GenericNodeRealizer.Factory factory = GenericNodeRealizer.getFactory();
302     factory.addConfiguration(CONFIGURATION_GROUP, map);
303 
304 
305     GenericGroupNodeRealizer gnr = new GenericGroupNodeRealizer();
306 
307     //Register first, since this will also configure the node label
308     gnr.setConfiguration(CONFIGURATION_GROUP);
309 
310     //Nicer colors
311     gnr.setFillColor(new Color(202,236,255,132));
312     gnr.setLineColor(new Color(102, 102,153,255));
313     NodeLabel label = gnr.getLabel();
314     label.setBackgroundColor(null);
315     label.setTextColor(Color.BLACK);
316     label.setFontSize(15);
317 
318 
319     //Set default group and folder node realizers
320     DefaultHierarchyGraphFactory hgf = (DefaultHierarchyGraphFactory)
321             getHierarchyManager().getGraphFactory();
322 
323     hgf.setProxyNodeRealizerEnabled(true);
324 
325     hgf.setDefaultGroupNodeRealizer(gnr.createCopy());
326     hgf.setDefaultFolderNodeRealizer(gnr.createCopy());
327   }
328 
329 
330 
331   private void layoutHierarchically() {                                         
332     final SimplexNodePlacer placer = new SimplexNodePlacer();                   
333     placer.setBaryCenterModeEnabled(true);                                      
334                                                                                 
335     final IncrementalHierarchicLayouter layouter =                              
336             new IncrementalHierarchicLayouter();                                
337     layouter.setLayoutOrientation(LayoutOrientation.LEFT_TO_RIGHT);             
338     layouter.setNodePlacer(placer);                                             
339     layouter.getEdgeLayoutDescriptor().setMinimumFirstSegmentLength(20);        
340     layouter.getEdgeLayoutDescriptor().setMinimumLastSegmentLength(40);         
341     layouter.setMinimumLayerDistance(60);                                       
342                                                                                 
343     doLayout(layouter);                                                         
344   }                                                                             
345                                                                                 
346   private void routeOrthogonally() {                                            
347     doLayout(                                                                   
348       new EdgeGroupRouterStage(                                                 
349         new GroupNodeRouterStage(                                               
350           new PatchRouterStage(                                                 
351             new OrthogonalEdgeRouter()))));                                     
352   }                                                                             
353                                                                                 
354   private void doLayout( final Layouter layouter ) {                            
355     final Graph2DLayoutExecutor executor = new Graph2DLayoutExecutor();         
356                                                                                 
357     final NodePortLayoutConfigurator configurator =                             
358     executor.getNodePortConfigurator();                                         
359     configurator.setAutomaticPortConstraintsEnabled(fixPortsForLayout);         
360     configurator.setAutomaticEdgeGroupsEnabled(groupEdges);                     
361                                                                                 
362     executor.doLayout(view, layouter);                                          
363   }                                                                             
364 
365 
366   public static void main( String[] args ) {
367     EventQueue.invokeLater(new Runnable() {
368       public void run() {
369         Locale.setDefault(Locale.ENGLISH);
370         initLnF();
371         (new NodePortsDemo("resource/nodeportshelp.html")).start();
372       }
373     });
374   }
375 
376 
377   /**
378    * Specifies the contract of a post-processor for
379    * {@link demo.view.advanced.ports.NodePortsDemo.MyNodeStateChangeEdgeRouter}.
380    */
381   private static interface Processor {
382     /**
383      * Post-processing for
384      * {@link demo.view.advanced.ports.NodePortsDemo.MyNodeStateChangeEdgeRouter#preEdgeStateChange(y.base.Edge, y.base.Node)}.
385      * @param edge the edge whose state is about to change.
386      * @param groupNode the node whose state change will trigger the edge state
387      * change.
388      */
389     public void preEdgeStateChange( Edge edge, Node groupNode );
390 
391     /**
392      * Post-processing for
393      * {@link demo.view.advanced.ports.NodePortsDemo.MyNodeStateChangeEdgeRouter#postEdgeStateChange(y.base.Edge, y.base.Node)}.
394      * @param edge the edge whose state has changed.
395      * @param groupNode the node whose state change has triggered the edge state
396      * change.
397      */
398     public void postEdgeStateChange( Edge edge, Node groupNode );
399 
400     /**
401      * Post-processing for
402      * {@link demo.view.advanced.ports.NodePortsDemo.MyNodeStateChangeEdgeRouter#postNodeStateChange(y.base.Node)}.
403      * @param groupNode the node whose state has changed.
404      */
405     public void postNodeStateChange( Node groupNode );
406   }
407 
408   /**
409    * Simple {@link y.view.NodeStateChangeEdgeRouter} implementation that allows
410    * for arbitrary post-processing in
411    * {@link #preEdgeStateChange(y.base.Edge, y.base.Node)},
412    * {@link #postEdgeStateChange(y.base.Edge, y.base.Node)}, and
413    * {@link #postNodeStateChange(y.base.Node)}.
414    */
415   private static final class MyNodeStateChangeEdgeRouter
416           extends NodeStateChangeEdgeRouter {
417     private final Processor impl;
418 
419     MyNodeStateChangeEdgeRouter( final Processor impl ) {
420       this.impl = impl;
421     }
422 
423     /**
424      * Overwritten to allow for post-processing.
425      * @param edge the edge whose state is about to change.
426      * @param groupNode the node whose state change will trigger the edge state
427      * change.
428      */
429     protected void preEdgeStateChange( final Edge edge, final Node groupNode ) {
430       if (impl != null) {
431         impl.preEdgeStateChange(edge, groupNode);
432       }
433 
434       super.preEdgeStateChange(edge, groupNode);
435     }
436 
437     /**
438      * Overwritten to allow for post-processing.
439      * @param edge the edge whose state has changed.
440      * @param groupNode the node whose state change has triggered the edge state
441      * change.
442      */
443     protected void postEdgeStateChange( final Edge edge, final Node groupNode ) {
444       super.postEdgeStateChange(edge, groupNode);
445 
446       if (impl != null) {
447         impl.postEdgeStateChange(edge, groupNode);
448       }
449     }
450 
451     /**
452      * Overwritten to allow for post-processing.
453      * @param groupNode the node whose state has changed.
454      */
455     public void postNodeStateChange( final Node groupNode ) {
456       super.postNodeStateChange(groupNode);
457 
458       if (impl != null) {
459         impl.postNodeStateChange(groupNode);
460       }
461     }
462   }
463 
464   /**
465    * Reassigns node ports of normal edges from one realizer delegate to the
466    * other at nodes that use {@link y.view.ProxyShapeNodeRealizer} upon node
467    * state changes.
468    * By default, edges connecting to the <code>i</code>-th port of one realizer
469    * delegate are assigned to the <code>i</code>-th port of the other realizer
470    * delegate (if it exists).
471    * More sophisticated strategies can be realized by customizing method
472    * {@link #remapPort(y.view.NodeRealizer, y.view.EdgeRealizer, y.view.NodePort, boolean)}.
473    */
474   private static final class NormalEdgeProcessor implements Processor {
475     private final Map node2edgeState;
476 
477     NormalEdgeProcessor() {
478       node2edgeState = new WeakHashMap();
479     }
480 
481 
482     /**
483      * Stores the current port information of the specified edge.
484      * @param edge the edge whose state is about to change.
485      * @param groupNode the node whose state change will trigger the edge state
486      */
487     public void preEdgeStateChange( final Edge edge, final Node groupNode ) {
488       if (groupNode.getGraph() instanceof Graph2D) {
489         final Graph2D graph = (Graph2D) groupNode.getGraph();
490         final HierarchyManager hm = graph.getHierarchyManager();
491         if (acceptEdge(hm, edge, groupNode)) {
492           Map edge2state = (Map) node2edgeState.get(groupNode);
493           if (edge2state == null) {
494             edge2state = new WeakHashMap();
495             node2edgeState.put(groupNode, edge2state);
496           }
497 
498           if (edge.source() == groupNode) {
499             final NodePort port = NodePort.getSourcePort(graph.getRealizer(edge));
500             if (port != null &&
501                 matches(port.getRealizer(), graph.getRealizer(groupNode))) {
502               EdgeState state = (EdgeState) edge2state.get(edge);
503               if (state == null) {
504                 state = new EdgeState();
505                 edge2state.put(edge, state);
506               }
507               state.sourcePort = port;
508             }
509           }
510           if (edge.target() == groupNode) {
511             final NodePort port = NodePort.getTargetPort(graph.getRealizer(edge));
512             if (port != null &&
513                 matches(port.getRealizer(), graph.getRealizer(groupNode))) {
514               EdgeState state = (EdgeState) edge2state.get(edge);
515               if (state == null) {
516                 state = new EdgeState();
517                 edge2state.put(edge, state);
518               }
519               state.targetPort = port;
520             }
521           }
522         }
523       }
524     }
525 
526     /**
527      * Assigns an appropriate port to the specified edge.
528      * Calls
529      * {@link #remapPort(y.view.NodeRealizer, y.view.EdgeRealizer, y.view.NodePort, boolean)}
530      * to determine which port is appropriate.
531      * @param edge the edge whose state has changed.
532      * @param groupNode the node whose state change has triggered the edge state
533      */
534     public void postEdgeStateChange( final Edge edge, final Node groupNode ) {
535       if (groupNode.getGraph() instanceof Graph2D) {
536         final Graph2D graph = (Graph2D) groupNode.getGraph();
537         final HierarchyManager hm = graph.getHierarchyManager();
538         if (acceptEdge(hm, edge, groupNode)) {
539           final Map edge2state = (Map) node2edgeState.get(groupNode);
540           final EdgeState state = (EdgeState) edge2state.get(edge);
541           if (state != null) {
542             if (edge.source() == groupNode) {
543               remapPort(
544                       graph.getRealizer(groupNode), graph.getRealizer(edge),
545                       state.sourcePort,
546                       true);
547             }
548             if (edge.target() == groupNode) {
549               remapPort(
550                       graph.getRealizer(groupNode), graph.getRealizer(edge),
551                       state.targetPort,
552                       false);
553             }
554           }
555         }
556       }
557     }
558 
559     /**
560      * Removes the no longer needed stored port information for all edges
561      * related to the specified node.
562      * @param groupNode the node whose state has changed.
563      */
564     public void postNodeStateChange( final Node groupNode ) {
565       node2edgeState.remove(groupNode);
566     }
567 
568 
569     /**
570      * Remaps the specified edge realizer's port to one that belongs to the
571      * specified node realizer.
572      * @param nr the realizer that provides possible new ports.
573      * @param er the realizer whose port has to be remapped.
574      * @param port the old port.
575      * @param source if <code>true</code> the source port has to be remapped;
576      * otherwise the target port has to be remapped.
577      */
578     void remapPort(
579             final NodeRealizer nr,
580             final EdgeRealizer er,
581             final NodePort port,
582             final boolean source
583     ) {
584       if (port != null) {
585         final NodeRealizer oldNr = port.getRealizer();
586         if (!matches(oldNr, nr) &&
587             oldNr != null &&
588             oldNr.portCount() == nr.portCount()) {
589           bindPort(nr.getPort(indexOf(port)), er, source);
590         }
591       }
592     }
593 
594 
595     /**
596      * Binds the specified port to the specified edge.
597      * @param port the port to bind to the specified edge.
598      * @param er the realizer representing the edge.
599      * @param source if <code>true</code> the source port has to be bound;
600      * otherwise the target port has to be bound.
601      */
602     private static void bindPort(
603             final NodePort port,
604             final EdgeRealizer er,
605             final boolean source
606     ) {
607       if (source) {
608         NodePort.bindSourcePort(port, er);
609       } else {
610         NodePort.bindTargetPort(port, er);
611       }
612     }
613 
614     /**
615      * Returns <code>true</code> if the specified edge's port assignment has to
616      * be remapped and <code>false</code> otherwise.
617      * @param hm the nesting structure of the specified edge's graph.
618      * @param edge the edge to check.
619      * @param groupNode the node whose state changes.
620      * @return <code>true</code> if the specified edge's port assignment has to
621      * be remapped and <code>false</code> otherwise.
622      */
623     private static boolean acceptEdge(
624             final HierarchyManager hm,
625             final Edge edge,
626             final Node groupNode
627     ) {
628       if (hm != null && hm.isFolderNode(groupNode) && hm.isInterEdge(edge)) {
629         return hm.getRealSource(edge) == groupNode ||
630                hm.getRealTarget(edge) == groupNode;
631       } else {
632         return edge.source() == groupNode || edge.target() == groupNode;
633       }
634     }
635 
636     /**
637      * Returns the zero-based index of the specified port within its associated
638      * node realizer's collection of ports.
639      * @param port the port whose index is to be determined.
640      * @return the zero-based  index of the specified port within its associated
641      * node realizer's collection of ports or <code>-1>/code> if the port
642      * currently is not associated to any node realizer.
643      */
644     private static int indexOf( final NodePort port ) {
645       final NodeRealizer owner = port.getRealizer();
646       if (owner != null) {
647         for (int i = 0, n = owner.portCount(); i < n; ++i) {
648           if (owner.getPort(i) == port) {
649             return i;
650           }
651         }
652       }
653       return -1;
654     }
655 
656     /**
657      * Returns <code>true</code> if the first realizer equals the second or the
658      * second realizer's delegate and <code>false</code> otherwise.
659      * @param portNr a realizer associated to a {@link y.view.NodePort}.
660      * @param otherNr another realizer, possibly a
661      * {@link y.view.ProxyShapeNodeRealizer}.
662      * @return <code>true</code> if the first realizer equals the second or the
663      * second realizer's delegate and <code>false</code> otherwise.
664      */
665     private static boolean matches(
666             final NodeRealizer portNr,
667             final NodeRealizer otherNr
668     ) {
669       return portNr == otherNr ||
670              (otherNr instanceof ProxyShapeNodeRealizer &&
671               portNr == ((ProxyShapeNodeRealizer) otherNr).getRealizerDelegate());
672     }
673 
674 
675     /**
676      * Stores port information for an edge.
677      */
678     private static final class EdgeState {
679       NodePort sourcePort;
680       NodePort targetPort;
681     }
682   }
683 
684   /**
685    * Reassigns node ports of edges that are converted to inter edges.
686    * By default, edges are automatically assigned to the first port of the
687    * corresponding folder node (if the node has any ports at all).
688    * More sophisticated strategies can be realized by customizing method
689    * {@link #remapPort(y.view.NodeRealizer, y.view.EdgeRealizer, boolean)}.
690    */
691   private static final class InterEdgeProcessor implements Processor {
692     private final Processor impl;
693 
694     InterEdgeProcessor( final Processor impl ) {
695       this.impl = impl;
696     }
697 
698     public void preEdgeStateChange( final Edge edge, final Node groupNode ) {
699       if (impl != null) {
700         impl.preEdgeStateChange(edge, groupNode);
701       }
702     }
703 
704     /**
705      * Assigns an appropriate port to the specified edge.
706      * Calls
707      * {@link #remapPort(y.view.NodeRealizer, y.view.EdgeRealizer, boolean)}
708      * to determine which port is appropriate.
709      * @param edge the edge whose state has changed.
710      * @param groupNode the node whose state change has triggered the edge state
711      */
712     public void postEdgeStateChange( final Edge edge, final Node groupNode ) {
713       if (impl != null) {
714         impl.postEdgeStateChange(edge, groupNode);
715       }
716 
717       if (groupNode.getGraph() instanceof Graph2D) {
718         final Graph2D graph = (Graph2D) groupNode.getGraph();
719         final HierarchyManager hm = graph.getHierarchyManager();
720         if (hm != null && hm.isFolderNode(groupNode) && hm.isInterEdge(edge)) {
721           if (edge.source() == groupNode && hm.getRealSource(edge) != groupNode) {
722             remapPort(
723                     graph.getRealizer(groupNode),
724                     graph.getRealizer(edge),
725                     true);
726           }
727           if (edge.target() == groupNode && hm.getRealTarget(edge) != groupNode) {
728             remapPort(
729                     graph.getRealizer(groupNode),
730                     graph.getRealizer(edge),
731                     false);
732           }
733         }
734       }
735     }
736 
737     public void postNodeStateChange( final Node groupNode ) {
738       if (impl != null) {
739         impl.postNodeStateChange(groupNode);
740       }
741     }
742 
743     /**
744      * Remaps the specified edge realizer's port to the first port of the
745      * specified node realizer.
746      * @param nr the realizer that provides possible new ports.
747      * @param er the realizer whose port has to be remapped.
748      * @param source if <code>true</code> the source port has to be remapped;
749      * otherwise the target port has to be remapped.
750      */
751     protected void remapPort(
752             final NodeRealizer nr,
753             final EdgeRealizer er,
754             final boolean source
755     ) {
756       if (nr.portCount() > 0) {
757         bindPort(nr.getPort(0), er, source);
758       }
759     }
760 
761 
762     /**
763      * Binds the specified port to the specified edge.
764      * @param port the port to bind to the specified edge.
765      * @param er the realizer representing the edge.
766      * @param source if <code>true</code> the source port has to be bound;
767      * otherwise the target port has to be bound.
768      */
769     private static void bindPort(
770             final NodePort port,
771             final EdgeRealizer er,
772             final boolean source
773     ) {
774       if (source) {
775         NodePort.bindSourcePort(port, er);
776       } else {
777         NodePort.bindTargetPort(port, er);
778       }
779     }
780   }
781 
782   /**
783    * Painter implementation for group nodes that draws a special header
784    * compartment below the groups default label.
785    * In the default group node configuration, this compartment is drawn by
786    * the default label. This means said compartment will be painted over node
787    * ports because node labels are rendered after node ports.
788    */
789   private static final class GroupShapeNodePainter extends ShapeNodePainter {
790     private final static Color BACKGROUND = new Color(153, 204, 255, 255);
791 
792     GroupShapeNodePainter() {
793       super(ROUND_RECT);
794     }
795 
796     protected void paintFilledShape(
797             final NodeRealizer context,
798             final Graphics2D graphics,
799             final Shape shape
800     ) {
801       super.paintFilledShape(context, graphics, shape);
802 
803       if (context.labelCount() > 0) {
804         final Shape oldClip = graphics.getClip();
805         final Color oldColor = graphics.getColor();
806 
807         final Rectangle2D cb = oldClip.getBounds2D();
808         final YRectangle r = context.getLabel().getBox();
809         graphics.clip(new Rectangle2D.Double(cb.getX(), r.getY(), cb.getWidth(), r.getHeight()));
810         graphics.setColor(BACKGROUND);
811         graphics.fill(shape);
812 
813         graphics.setColor(oldColor);
814         graphics.setClip(oldClip);
815       }
816     }
817   }
818 }
819