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.layout.partial;
15  
16  import demo.view.hierarchy.GroupingDemo;
17  
18  import java.awt.Dimension;
19  import java.awt.BorderLayout;
20  import java.awt.Color;
21  import java.awt.event.ActionEvent;
22  import java.awt.event.KeyEvent;
23  import java.awt.event.InputEvent;
24  import java.awt.event.MouseEvent;
25  import java.util.Map;
26  import java.net.URL;
27  
28  import y.layout.Layouter;
29  import y.layout.partial.PartialLayouter;
30  import y.util.DataProviderAdapter;
31  import y.view.Graph2D;
32  import y.view.Graph2DLayoutExecutor;
33  import y.view.Graph2DTraversal;
34  import y.view.Graph2DView;
35  import y.view.HitInfo;
36  import y.view.NodeRealizer;
37  import y.view.Graph2DViewActions;
38  import y.view.GenericNodeRealizer;
39  import y.view.LineType;
40  import y.view.NodeLabel;
41  import y.view.EdgeRealizer;
42  import y.view.ProxyShapeNodeRealizer;
43  import y.view.Selections;
44  import y.view.ViewMode;
45  import y.view.hierarchy.DefaultHierarchyGraphFactory;
46  import y.view.hierarchy.GenericGroupNodeRealizer;
47  import y.view.hierarchy.DefaultGenericAutoBoundsFeature;
48  import y.view.hierarchy.HierarchyManager;
49  import y.base.Node;
50  import y.base.NodeCursor;
51  import y.base.EdgeCursor;
52  import y.base.Edge;
53  import y.base.DataProvider;
54  import y.option.TableEditorFactory;
55  import y.option.OptionHandler;
56  import y.option.Editor;
57  
58  import javax.swing.Action;
59  import javax.swing.BorderFactory;
60  import javax.swing.JToolBar;
61  import javax.swing.AbstractAction;
62  import javax.swing.ActionMap;
63  import javax.swing.InputMap;
64  import javax.swing.KeyStroke;
65  import javax.swing.JComponent;
66  import javax.swing.JSplitPane;
67  import javax.swing.JPanel;
68  
69  
70  /**
71   * Base class for {@link y.layout.partial.PartialLayouter} demos.
72   */
73  public abstract class PartialLayoutBase extends GroupingDemo {
74    //Define colors for fixed/partial nodes/edges.
75    protected static final Color COLOR_FIXED_NODE = Color.GRAY;
76    protected static final Color COLOR_PARTIAL_NODE = new Color(255, 153, 0);
77    protected static final Color COLOR_FIXED_EDGE = Color.BLACK;
78    protected static final Color COLOR_PARTIAL_EDGE = COLOR_PARTIAL_NODE;
79    protected final OptionHandler optionHandler;
80  
81    /**
82     * Initializes a new <code>PartialLayoutBase</code> instance.
83     * @param helpFilePath the path to the help document to be displayed or
84     * <code>null</code> if no help document should be displayed.
85     */
86    protected PartialLayoutBase( final String helpFilePath ) {
87      super();
88  
89      JComponent helpPane = null;
90      if (helpFilePath != null) {
91        final URL url = getClass().getResource(helpFilePath);
92        if (url == null) {
93          System.err.println("Could not locate help file: " + helpFilePath);
94        } else {
95          helpPane = createHelpPane(url);
96        }
97      }
98  
99      //Init GUI components:
100     final JPanel propertiesPanel = new JPanel(new BorderLayout());
101     optionHandler = createOptionHandler();
102     propertiesPanel.add(createOptionTable(optionHandler), BorderLayout.NORTH);
103     if (helpPane != null) {
104       helpPane.setPreferredSize(new Dimension(400, 400));
105       propertiesPanel.add(helpPane);
106     }
107 
108     final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, view, propertiesPanel);
109     splitPane.setBorder(BorderFactory.createEmptyBorder());
110     splitPane.setResizeWeight(0.95);
111     splitPane.setContinuousLayout(false);
112     contentPane.add(splitPane, BorderLayout.CENTER);
113   }
114 
115   /**
116    * Adds the following actions to the default tool bar:
117    * <ul>
118    * <li>LayoutAction - layouts the graph</li>
119    * <li>FixColorSelectionAction - determines fix nodes</li>
120    * <li>PartialColorSelectionAction - determines partial nodes</li>
121    * </ul>
122    */
123   protected JToolBar createToolBar() {
124     final Action reloadAction = new AbstractAction("Reload") {
125       public void actionPerformed(ActionEvent e) {
126         loadInitialGraph();
127       }
128     };
129     reloadAction.putValue(Action.SHORT_DESCRIPTION, "Reload the initial graph");
130 
131     JToolBar bar = super.createToolBar();
132 
133     bar.addSeparator();
134     bar.add(new ColorSelectionAction("Lock Selection", true, view));
135     bar.addSeparator(TOOLBAR_SMALL_SEPARATOR);
136     bar.add(new ColorSelectionAction("Unlock Selection", false, view));
137 
138     bar.addSeparator();
139     bar.add(reloadAction);
140 
141     bar.addSeparator();
142     bar.add(createActionControl(new LayoutAction("Layout")));
143 
144     return bar;
145   }
146 
147   /**
148    * Adds a view mode which allows to toggle the fixed/partial state of nodes
149    * or edges (on mouse double click).
150    */
151   protected void registerViewModes() {
152     super.registerViewModes();
153 
154     view.addViewMode(new ToggleColorMode());
155   }
156   
157   protected void configureDefaultGroupNodeRealizers() {
158     //Create additional configuration for default group node realizers
159     DefaultHierarchyGraphFactory hgf = (DefaultHierarchyGraphFactory) getHierarchyManager().getGraphFactory();
160 
161     Map map = GenericGroupNodeRealizer.createDefaultConfigurationMap();
162     GenericNodeRealizer.Factory factory = GenericNodeRealizer.getFactory();
163 
164     factory.addConfiguration(CONFIGURATION_GROUP, map);
165     Object abf = factory.getImplementation(CONFIGURATION_GROUP,
166         GenericGroupNodeRealizer.GenericAutoBoundsFeature.class);
167     if (abf instanceof DefaultGenericAutoBoundsFeature) {
168       ((DefaultGenericAutoBoundsFeature) abf).setConsiderNodeLabelSize(true);
169     }
170 
171     GenericGroupNodeRealizer gnr = new GenericGroupNodeRealizer();
172 
173     //Register first, since this will also configure the node label
174     gnr.setConfiguration(CONFIGURATION_GROUP);
175 
176     //Nicer colors
177     gnr.setFillColor(new Color(202, 236, 255, 132));
178     gnr.setLineColor(new Color(102, 102, 153, 255));
179     gnr.setLineType(LineType.DOTTED_1);
180     NodeLabel label = gnr.getLabel();
181     label.setBackgroundColor(COLOR_PARTIAL_NODE);
182     label.setTextColor(Color.BLACK);
183     label.setFontSize(15);
184 
185     hgf.setProxyNodeRealizerEnabled(true);
186 
187     hgf.setDefaultGroupNodeRealizer(gnr.createCopy());
188     hgf.setDefaultFolderNodeRealizer(gnr.createCopy());
189   }
190 
191 
192   /**
193    * Sets the background color of the specified realizer's default label.
194    * If the specified realizer is a proxy realizer, the default labels of
195    * the proxy's possible delegates are changed recursively.
196    * @param nr the realizer whose default label is changed.
197    * @param color the new background color for the default label.
198    */
199   private static void setLabelBackgroundColor( final NodeRealizer nr, final Color color ) {
200     if (nr instanceof ProxyShapeNodeRealizer) {
201       final ProxyShapeNodeRealizer pnr = (ProxyShapeNodeRealizer) nr;
202       for (int i = 0, n = pnr.realizerCount(); i < n; ++i) {
203         setLabelBackgroundColor(pnr.getRealizer(i), color);
204       }
205     } else {
206       nr.getLabel().setBackgroundColor(color);
207     }
208   }
209 
210   /** Creates a table editor component for the specified option handler. */
211   private JComponent createOptionTable(OptionHandler oh) {
212     oh.setAttribute(TableEditorFactory.ATTRIBUTE_INFO_POSITION, TableEditorFactory.InfoPosition.NONE);
213 
214     TableEditorFactory tef = new TableEditorFactory();
215     Editor editor = tef.createEditor(oh);
216   
217     JComponent optionComponent = editor.getComponent();
218     optionComponent.setPreferredSize(new Dimension(400, 200));
219     optionComponent.setMaximumSize(new Dimension(400, 200));
220     return optionComponent;
221   }
222 
223   /**
224    * Creates an option handler to manage layout properties.
225    * @return an option handler for the layout.
226    */
227   protected abstract OptionHandler createOptionHandler();
228 
229   /**
230    * Callback method to partially layout a graph.
231    * This method is called from
232    * {@link demo.layout.partial.PartialLayoutBase.LayoutAction} and
233    * {@link demo.layout.partial.PartialLayoutBase.DuplicateSubgraphAction}.
234    *
235    */
236   protected void layoutSubgraph() {
237     Graph2DLayoutExecutor layoutExecutor = new Graph2DLayoutExecutor();
238     layoutExecutor.getLayoutMorpher().setKeepZoomFactor(true);
239     layoutExecutor.doLayout(view, createConfiguredPartialLayouter());
240   }
241 
242     /**
243    * Creates a configured partial Layouter instance
244    * @return an partial layouter instance.
245    */
246   protected abstract Layouter createConfiguredPartialLayouter();
247 
248   /**
249    * Registers key bindings for both predefined actions and custom actions.
250    */
251   protected void registerViewActions() {
252     super.registerViewActions();
253 
254     ActionMap actionMap = view.getCanvasComponent().getActionMap();
255     actionMap.put("DUPLICATE_ACTION", new DuplicateSubgraphAction("Duplicate and Layout Subgraph"));
256 
257     InputMap inputMap = view.getCanvasComponent().getInputMap();
258     inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, InputEvent.CTRL_MASK), "DUPLICATE_ACTION");
259 
260   }
261 
262   /**
263    * Triggers a layout calculation for the subgraph that is induced by
264    * elements marked as partial.
265    */
266   class LayoutAction extends AbstractAction {
267     LayoutAction(String name) {
268       super(name, SHARED_LAYOUT_ICON);
269     }
270 
271     public void actionPerformed(ActionEvent e) {
272       //Select subgraph by color:
273       PartialElementsMarkers ps = new PartialElementsMarkers();
274       boolean selectionExists = ps.markByColor(view.getGraph2D());
275       if (selectionExists) {
276         layoutSubgraph();
277       }
278       ps.resetMarkers(view.getGraph2D());
279     }
280   }
281 
282   /**
283    * Action that duplicates the subgraph that is induced by the currently
284    * selected nodes or edges.
285    */
286   class DuplicateSubgraphAction extends AbstractAction {
287     DuplicateSubgraphAction(String name) {
288       super(name);
289     }
290 
291     /**
292      * Duplicates the subgraph that is induced by the currently selected nodes or
293      * edges.
294      */
295     public void actionPerformed(ActionEvent e) {
296       //Duplicate selection
297       final Graph2DViewActions.DuplicateAction duplicateAction = new Graph2DViewActions.DuplicateAction(view);
298       duplicateAction.duplicate(view);
299 
300       //Layout the duplicated nodes:
301       PartialElementsMarkers ps = new PartialElementsMarkers();
302       final boolean selectionExists = ps.markBySelection(view.getGraph2D());
303       if (selectionExists) {
304         layoutSubgraph();
305       }
306       ps.resetMarkers(view.getGraph2D());
307     }
308   }
309 
310   /**
311    * Provides methods to mark nodes and/or edges as <em>partial</em>
312    * (with regards to {@link PartialLayouter#PARTIAL_NODES_DPKEY} and
313    * {@link PartialLayouter#PARTIAL_EDGES_DPKEY} according
314    * to either their color or their selection state (with regards to
315    * {@link y.view.Graph2D#isSelected(y.base.Edge)} and
316    * {@link y.view.Graph2D#isSelected(y.base.Node)}).
317    */
318   static class PartialElementsMarkers {
319     DataProvider odpSelectedNodes;
320     DataProvider odpSelectedEdges;
321 
322     /**
323      * Adds data providers to the given graph for keys
324      * {@link PartialLayouter#PARTIAL_NODES_DPKEY} and {@link PartialLayouter#PARTIAL_EDGES_DPKEY}
325      * that reflect the selection state (with regards to
326      * {@link y.view.Graph2D#isSelected(y.base.Edge)} and
327      * {@link y.view.Graph2D#isSelected(y.base.Node)}) of nodes and edges.
328      * @param graph the graph for which selection markers are created.
329      * @return <code>true</code> if the specified graph has selected nodes
330      * and/or selected edges and <code>false</code> otherwise.
331      */
332     boolean markBySelection(final Graph2D graph) {
333       //store the old data provider
334       odpSelectedNodes = graph.getDataProvider(PartialLayouter.PARTIAL_NODES_DPKEY);
335       odpSelectedEdges = graph.getDataProvider(PartialLayouter.PARTIAL_EDGES_DPKEY);
336 
337       //register dp
338       graph.addDataProvider(PartialLayouter.PARTIAL_NODES_DPKEY, Selections.createSelectionNodeMap(graph));
339       graph.addDataProvider(PartialLayouter.PARTIAL_EDGES_DPKEY, Selections.createSelectionEdgeMap(graph));
340 
341       return (graph.selectedNodes().ok() || graph.selectedEdges().ok());
342     }
343 
344     /**
345      * Adds data providers to the given graph for keys
346      * {@link PartialLayouter#PARTIAL_NODES_DPKEY} and {@link PartialLayouter#PARTIAL_EDGES_DPKEY}
347      * that report an edge as <em>partial</em> if its state color equals
348      * the specified partial edge color and a node if its state color equals
349      * the specified partial node color.
350      * @param graph the graph for which selection markers are created.
351      * @return <code>true</code> if the specified graph has nodes and/or edges
352      * with the appropriate state colors and <code>false</code> otherwise.
353      */
354     boolean markByColor(final Graph2D graph) {
355       //store the old data provider
356       odpSelectedNodes = graph.getDataProvider(PartialLayouter.PARTIAL_NODES_DPKEY);
357       odpSelectedEdges = graph.getDataProvider(PartialLayouter.PARTIAL_EDGES_DPKEY);
358         
359       //Determine partial nodes/edges by color:
360       DataProviderAdapter isPartialNode = new DataProviderAdapter() {
361         public boolean getBool(Object dataHolder) {
362           Node n = (Node) dataHolder;
363           if (graph.getHierarchyManager().isGroupNode(n) ||
364               graph.getHierarchyManager().isFolderNode(n)) {
365             return COLOR_PARTIAL_NODE.equals(graph.getRealizer(n).getLabel().getBackgroundColor());
366           } else {
367             return COLOR_PARTIAL_NODE.equals(graph.getRealizer(n).getFillColor());
368           }
369         }
370       };
371 
372       DataProviderAdapter isPartialEdge = new DataProviderAdapter() {
373         public boolean getBool(Object dataHolder) {
374           return COLOR_PARTIAL_EDGE.equals(graph.getRealizer((Edge) dataHolder).getLineColor());
375         }
376       };
377 
378       graph.addDataProvider(PartialLayouter.PARTIAL_NODES_DPKEY, isPartialNode);
379       graph.addDataProvider(PartialLayouter.PARTIAL_EDGES_DPKEY, isPartialEdge);
380 
381       for (NodeCursor nodeCursor = graph.nodes(); nodeCursor.ok(); nodeCursor.next()) {
382         Node node = nodeCursor.node();
383         if (isPartialNode.getBool(node)){
384           return true;
385         }
386       }
387       for (EdgeCursor edgeCursor = graph.edges(); edgeCursor.ok(); edgeCursor.next()) {
388         Edge edge = edgeCursor.edge();
389         if (isPartialEdge.getBool(edge)){
390           return true;
391         }
392       }
393 
394       return false;
395     }
396     
397     void resetMarkers(Graph2D graph) {
398       //reset data provider
399       graph.removeDataProvider(PartialLayouter.PARTIAL_NODES_DPKEY);
400       if (odpSelectedNodes != null) {
401         //set the old data provider
402         graph.addDataProvider(PartialLayouter.PARTIAL_NODES_DPKEY, odpSelectedNodes);
403       }
404 
405       graph.removeDataProvider(PartialLayouter.PARTIAL_EDGES_DPKEY);
406       if (odpSelectedEdges != null) {
407         //set the old data provider
408         graph.addDataProvider(PartialLayouter.PARTIAL_EDGES_DPKEY, odpSelectedEdges);
409       }
410     }
411   }
412 
413   /**
414    * {@link y.view.ViewMode} that toggles that fixed/partial state of a node
415    * or an edge by coloring the element in the appropriate state color.
416    * @see demo.layout.partial.PartialLayoutBase#COLOR_FIXED_EDGE
417    * @see demo.layout.partial.PartialLayoutBase#COLOR_FIXED_NODE
418    * @see demo.layout.partial.PartialLayoutBase#COLOR_PARTIAL_EDGE
419    * @see demo.layout.partial.PartialLayoutBase#COLOR_PARTIAL_NODE
420    */
421   static class ToggleColorMode extends ViewMode {
422     /**
423      * Toggles the fixed/partial state color of double-clicked nodes or edges.
424      * @param x the x-coordinate of the mouse event in world coordinates.
425      * @param y the y-coordinate of the mouse event in world coordinates.
426      */
427     public void mouseClicked( final double x, final double y ) {
428       if (lastClickEvent.getButton() == MouseEvent.BUTTON1 &&
429           lastClickEvent.getClickCount() == 2) {
430         mouseClickedImpl(x, y);
431       }
432     }
433 
434     private void mouseClickedImpl( final double x, final double y ) {
435       final HitInfo info = view.getHitInfoFactory().createHitInfo(
436               x, y, Graph2DTraversal.NODES | Graph2DTraversal.EDGES, true);
437       if (info.hasHitNodes()) {
438         toggleColor(info.getHitNode());
439       } else if (info.hasHitEdges()) {
440         toggleColor(info.getHitEdge());
441       }
442     }
443 
444     /**
445      * Toggles the fixed/partial state color of the specified edge.
446      * @param edge the edge whose state color is changed.
447      */
448     private void toggleColor( final Edge edge ) {
449       final EdgeRealizer er = view.getGraph2D().getRealizer(edge);
450       if (er.getLineColor() == COLOR_PARTIAL_EDGE) {
451         er.setLineColor(COLOR_FIXED_EDGE);
452       } else {
453         er.setLineColor(COLOR_PARTIAL_EDGE);
454       }
455       view.getGraph2D().updateViews();
456     }
457 
458     /**
459      * Toggles the fixed/partial state color of the specified node.
460      * @param node the node whose state color is changed.
461      */
462     private void toggleColor( final Node node ) {
463       final Graph2D g = view.getGraph2D();
464       final NodeRealizer nr = g.getRealizer(node);
465       final HierarchyManager hierarchyManager = g.getHierarchyManager();
466       if (hierarchyManager.isGroupNode(node) ||
467           hierarchyManager.isFolderNode(node)) {            
468         if (nr.getLabel().getBackgroundColor() == COLOR_PARTIAL_NODE) {
469           setLabelBackgroundColor(nr, COLOR_FIXED_NODE);
470         } else {
471           setLabelBackgroundColor(nr, COLOR_PARTIAL_NODE);
472         }
473       } else {
474         if (nr.getFillColor() == COLOR_PARTIAL_NODE) {
475           nr.setFillColor(COLOR_FIXED_NODE);
476         } else {
477           nr.setFillColor(COLOR_PARTIAL_NODE);
478         }
479       }
480       g.updateViews();
481     }
482   }
483 
484   /**
485    * Marks selected nodes and/or edges as either fixed or partial.
486    * Marking is done by coloring said elements using one of the appropriate
487    * state colors {@link demo.layout.partial.PartialLayoutBase#COLOR_FIXED_EDGE},
488    * {@link demo.layout.partial.PartialLayoutBase#COLOR_FIXED_NODE},
489    * {@link demo.layout.partial.PartialLayoutBase#COLOR_PARTIAL_EDGE}, and
490    * {@link demo.layout.partial.PartialLayoutBase#COLOR_PARTIAL_NODE}
491    */
492   static class ColorSelectionAction extends AbstractAction {
493     private final boolean fixed;
494     private final Graph2DView view;
495 
496     /**
497      * Initializes a new <code>ColorSelectionAction</code> instance.
498      * @param name the display name for a control triggering the action.
499      * @param fixed if <code>true</code> edges are colored using
500      * {@link demo.layout.partial.PartialLayoutBase#COLOR_FIXED_EDGE} and
501      * nodes are colored using
502      * {@link demo.layout.partial.PartialLayoutBase#COLOR_FIXED_NODE}; if
503      * <code>false</code> edges are colored using
504      * {@link demo.layout.partial.PartialLayoutBase#COLOR_PARTIAL_EDGE} and
505      * nodes are colored using
506      * {@link demo.layout.partial.PartialLayoutBase#COLOR_PARTIAL_NODE}.
507      * @param view the view displaying the graph whose selected elements are
508      * to be colored.
509      */
510     ColorSelectionAction(
511             final String name,
512             final boolean fixed,
513             final Graph2DView view
514     ) {
515       super(name);
516       this.fixed = fixed;
517       this.view = view;
518     }
519 
520     public void actionPerformed( final ActionEvent e ) {
521       final Graph2D g = view.getGraph2D();
522 
523       final Color nodeColor = fixed ? COLOR_FIXED_NODE : COLOR_PARTIAL_NODE;
524       for (NodeCursor nc = g.nodes(); nc.ok(); nc.next()) {
525         final Node n = nc.node();
526         if (g.isSelected(n)) {
527           if (g.getHierarchyManager().isGroupNode(n) ||
528               g.getHierarchyManager().isFolderNode(n)) {
529             setLabelBackgroundColor(g.getRealizer(n), nodeColor);
530           } else {
531             g.getRealizer(n).setFillColor(nodeColor);
532           }
533         }
534       }
535 
536       final Color edgeColor = fixed ? COLOR_FIXED_EDGE : COLOR_PARTIAL_EDGE;
537       for (EdgeCursor ec = g.edges(); ec.ok(); ec.next()) {
538         final Edge edge = ec.edge();
539         if (g.isSelected(edge)) {
540           g.getRealizer(edge).setLineColor(edgeColor);
541         }
542       }
543 
544       g.updateViews();
545     }
546   }
547 }