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