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.hierarchy;
29  
30  import demo.view.DemoBase;
31  import y.base.Node;
32  import y.view.CreateEdgeMode;
33  import y.view.EditMode;
34  import y.view.GenericNodeRealizer;
35  import y.view.Graph2D;
36  import y.view.Graph2DView;
37  import y.view.Graph2DViewActions;
38  import y.view.NodeRealizer;
39  import y.view.PopupMode;
40  import y.view.ProxyShapeNodeRealizer;
41  import y.view.ViewMode;
42  import y.view.NodeLabel;
43  import y.view.LineType;
44  import y.view.hierarchy.DefaultGenericAutoBoundsFeature;
45  import y.view.hierarchy.DefaultHierarchyGraphFactory;
46  import y.view.hierarchy.GenericGroupNodeRealizer;
47  import y.view.hierarchy.HierarchyManager;
48  
49  import javax.swing.Action;
50  import javax.swing.ActionMap;
51  import javax.swing.InputMap;
52  import javax.swing.JMenu;
53  import javax.swing.JMenuBar;
54  import javax.swing.JMenuItem;
55  import javax.swing.JPopupMenu;
56  import javax.swing.KeyStroke;
57  import java.awt.event.ActionEvent;
58  import java.awt.event.KeyEvent;
59  import java.awt.event.InputEvent;
60  import java.awt.Color;
61  import java.awt.EventQueue;
62  import java.util.HashMap;
63  import java.util.Locale;
64  import java.util.Map;
65  
66  /**
67   * This application demonstrates the basic use of <b>Nested Graph Hierarchy</b> technology
68   * and also <b>Node Grouping</b>.
69   * <p>
70   * So-called folder nodes are used to nest graphs within them, so-called group nodes
71   * are used to group a set of nodes.
72   * <br>
73   * Both these types of node look similar but represent different concepts: while
74   * grouped nodes still belong to the same graph as their enclosing group node, the
75   * graph that is contained within a folder node is a separate entity.
76   * </p>
77   * <p>
78   * There are several ways provided to create, modify, and navigate a graph hierarchy:
79   * <ul>
80   * <li>
81   * By means of popup menu actions and actions from the "Grouping" submenu selected nodes can be grouped and also nested.
82   * Reverting these operations is also supported. For these actions, predefined key bindings are provided in class
83   * {@link y.view.Graph2DViewActions}.
84   * </li>
85   * <li>
86   * By Shift-dragging nodes they can be moved into and out of group nodes.
87   * </li>
88   * <li>
89   * Folder node and group node both allow switching to the other type by either using
90   * popup menu actions or clicking the icon in their upper-left corner.
91   * </li>
92   * </ul>
93   * </p>
94   * <p>
95   * Note that the size of group nodes is determined by the space requirements of
96   * their content, i.e., their resizing behavior is restricted.
97   * </p>
98   * @see <a href="http://docs.yworks.com/yfiles/doc/api/index.html#/dguide/hier_mvc_model" target="_blank">Section Managing Graph Hierarchies</a> in the yFiles for Java Developer's Guide
99   * @see <a href="http://docs.yworks.com/yfiles/doc/api/index.html#/dguide/hier_mvc_controller" target="_blank">Section User Interaction</a> in the yFiles for Java Developer's Guide
100  */
101 public class GroupingDemo extends DemoBase {
102 
103   /** The name of the configuration for {@link GenericGroupNodeRealizer} */
104   public static final String CONFIGURATION_GROUP = "GroupingDemo_GROUP_NODE";
105 
106   protected static final Map actionNames;
107   static {
108     actionNames = new HashMap();
109     actionNames.put(Graph2DViewActions.CLOSE_GROUPS, "Close Selected Groups");
110     actionNames.put(Graph2DViewActions.OPEN_FOLDERS, "Open Selected Folders");
111     actionNames.put(Graph2DViewActions.GROUP_SELECTION, "Group Selection");
112     actionNames.put(Graph2DViewActions.UNGROUP_SELECTION, "Ungroup Selection");
113     actionNames.put(Graph2DViewActions.FOLD_SELECTION, "Fold Selection");
114 
115     actionNames.put("CREATE_NEW_GROUP_NODE_ACTION", "Create Empty Group");
116     actionNames.put("CREATE_NEW_FOLDER_NODE_ACTION", "Create Empty Folder");
117   }
118 
119   /**
120    * Instantiates this demo. Builds the GUI.
121    */
122   public GroupingDemo() {
123     configureDefaultGroupNodeRealizers();
124     loadInitialGraph();
125   }
126 
127   protected void initialize() {
128     // create hierarchy manager with root graph before undo manager
129     // is created (for view actions) to ensure undo/redo works for grouped graphs
130     createHierarchyManager(view.getGraph2D());
131   }
132 
133   protected void loadInitialGraph() {
134     loadGraph("resource/grouping.graphml");    
135   }
136   
137   protected HierarchyManager createHierarchyManager(Graph2D rootGraph) {
138     return new HierarchyManager(rootGraph);
139   }
140 
141   protected void configureDefaultGroupNodeRealizers() {
142     //Create additional configuration for default group node realizers
143     DefaultHierarchyGraphFactory hgf = (DefaultHierarchyGraphFactory) getHierarchyManager().getGraphFactory();
144 
145     Map map = GenericGroupNodeRealizer.createDefaultConfigurationMap();
146     GenericNodeRealizer.Factory factory = GenericNodeRealizer.getFactory();
147 
148     factory.addConfiguration(CONFIGURATION_GROUP, map);
149     Object abf = factory.getImplementation(CONFIGURATION_GROUP,
150         GenericGroupNodeRealizer.GenericAutoBoundsFeature.class);
151     if (abf instanceof DefaultGenericAutoBoundsFeature) {
152       ((DefaultGenericAutoBoundsFeature) abf).setConsiderNodeLabelSize(true);
153     }
154 
155     GenericGroupNodeRealizer gnr = new GenericGroupNodeRealizer();
156 
157     //Register first, since this will also configure the node label
158     gnr.setConfiguration(CONFIGURATION_GROUP);
159 
160     //Nicer colors
161     gnr.setFillColor(new Color(202,236,255,132));
162     gnr.setLineColor(new Color(102, 102,153,255));
163     gnr.setLineType(LineType.DOTTED_1);
164     NodeLabel label = gnr.getLabel();
165     label.setBackgroundColor(new Color(153,204,255,255));
166     label.setTextColor(Color.BLACK);
167     label.setFontSize(15);
168 
169     hgf.setProxyNodeRealizerEnabled(true);
170 
171     hgf.setDefaultGroupNodeRealizer(gnr.createCopy());
172 
173     //Set the correct initial group state for folder nodes
174     gnr.setGroupClosed(true);
175     hgf.setDefaultFolderNodeRealizer(gnr.createCopy());
176   }
177 
178   protected JMenuBar createMenuBar() {
179     JMenuBar mb = super.createMenuBar();
180     JMenu menu = new JMenu("Grouping");
181     populateGroupingMenu(menu);
182     mb.add(menu);
183     return mb;
184   }
185 
186   /**
187    * Populates the "Grouping" menu with grouping specific actions.
188    *
189    * These actions are provided by class {@link y.view.Graph2DViewActions} and
190    * are already present in {@link Graph2DView}'s {@link ActionMap}.
191    *
192    */
193   protected void populateGroupingMenu(JMenu hierarchyMenu) {
194     // Predefined actions for open/close groups
195     registerAction(hierarchyMenu, Graph2DViewActions.CLOSE_GROUPS, true);
196     registerAction(hierarchyMenu, Graph2DViewActions.OPEN_FOLDERS, true);
197 
198     hierarchyMenu.addSeparator();
199 
200     // Predefined actions for group/fold/ungroup
201     registerAction(hierarchyMenu, Graph2DViewActions.GROUP_SELECTION, true);
202     registerAction(hierarchyMenu, Graph2DViewActions.UNGROUP_SELECTION, true);
203     registerAction(hierarchyMenu, Graph2DViewActions.FOLD_SELECTION, true);
204   }
205 
206 
207   protected EditMode createEditMode() {
208     EditMode mode = super.createEditMode();
209     //add hierarchy actions to the views popup menu
210     mode.setPopupMode(createPopupMode());
211     mode.getMouseInputMode().setNodeSearchingEnabled(true);
212 
213     //Add a visual indicator for the target node of an edge creation - makes it easier to
214     //see the target for nested graphs
215     ViewMode createEdgeMode = mode.getCreateEdgeMode();
216     if (createEdgeMode instanceof CreateEdgeMode) {
217       ((CreateEdgeMode) createEdgeMode).setIndicatingTargetNode(true);
218     }
219     return mode;
220   }
221 
222   protected PopupMode createPopupMode() {
223     return new HierarchicPopupMode();
224   }
225 
226 
227   /**
228    * Register key bindings for both predefined actions and our custom actions.
229    */
230   protected void registerViewActions() {
231     super.registerViewActions();
232 
233     ActionMap actionMap = view.getCanvasComponent().getActionMap();
234     actionMap.put(Graph2DViewActions.DELETE_SELECTION, createDeleteSelectionActionImpl());
235     actionMap.put("CREATE_NEW_GROUP_NODE_ACTION", new CreateNewGroupNodeAction());
236     actionMap.put("CREATE_NEW_FOLDER_NODE_ACTION", new CreateNewFolderNodeAction());
237     InputMap inputMap = view.getCanvasComponent().getInputMap();
238     inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_G, InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK), "CREATE_NEW_GROUP_NODE_ACTION");
239     inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK), "CREATE_NEW_FOLDER_NODE_ACTION");
240   }
241 
242   protected Action createDeleteSelectionAction() {
243     final Action action = createDeleteSelectionActionImpl();
244     action.putValue(Action.SMALL_ICON, getIconResource("resource/delete.png"));
245     action.putValue(Action.SHORT_DESCRIPTION, "Delete Selection");
246     return action;
247   }
248 
249   /**
250    * Creates a {@link y.view.Graph2DViewActions.DeleteSelectionAction} instance
251    * that is configured to prevent group nodes from shrinking when child nodes
252    * are deleted.
253    * @return {@link y.view.Graph2DViewActions.DeleteSelectionAction} instance.
254    */
255   private Action createDeleteSelectionActionImpl() {
256     final Graph2DViewActions.DeleteSelectionAction action =
257             new Graph2DViewActions.DeleteSelectionAction(view);
258     action.setKeepingParentGroupNodeSizes(true);
259     return action;
260   }
261 
262 
263   /**
264    * Launches this demo.
265    */
266   public static void main(String[] args) {
267     EventQueue.invokeLater(new Runnable() {
268       public void run() {
269         Locale.setDefault(Locale.ENGLISH);
270         initLnF();
271         (new GroupingDemo()).start();
272       }
273     });
274   }
275 
276   /**
277    * Populates the popup menu with grouping specific actions.
278    *
279    * These actions are provided by class {@link y.view.Graph2DViewActions} and
280    * are already present in {@link Graph2DView}'s {@link ActionMap}.
281    *
282    */
283   protected void populateGroupingPopup(JPopupMenu pm, final double x, final double y, Node node, boolean selected) {
284     // Predefined actions for open/close groups
285     registerAction(
286         pm, Graph2DViewActions.CLOSE_GROUPS,
287         node != null && getHierarchyManager().isGroupNode(node));
288     registerAction(
289         pm, Graph2DViewActions.OPEN_FOLDERS,
290         node != null && getHierarchyManager().isFolderNode(node));
291 
292     pm.addSeparator();
293 
294     // Predefined actions for group/fold/ungroup
295     registerAction(pm, Graph2DViewActions.GROUP_SELECTION, selected);
296     registerAction(pm, Graph2DViewActions.UNGROUP_SELECTION, selected);
297     registerAction(pm, Graph2DViewActions.FOLD_SELECTION, selected);
298 
299     pm.addSeparator();
300 
301     //We customize both "Create..." actions so that the newly created node lies at the coordinates of the mouse click
302     //(for "Group Selection"/"Fold Selection", the location is determined by the content's location instead.
303     JMenuItem item = new JMenuItem(new CreateNewGroupNodeAction(view){
304       protected void setGroupNodeBounds(Graph2DView view, Graph2D graph, Node groupNode) {
305         graph.setLocation(groupNode, x, y);
306       }
307     });
308     item.setText("Create Empty Group");
309     item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_G, InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK));
310     pm.add(item);
311 
312     item = new JMenuItem(new CreateNewFolderNodeAction(view){
313       protected void setFolderNodeBounds(Graph2DView view, Graph2D graph, Node groupNode) {
314         graph.setLocation(groupNode, x, y);
315       }
316     });
317     item.setText("Create Empty Folder");
318     item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK));
319     pm.add(item);
320   }
321 
322   /**
323    * Register a single action that is already present in the view's ActionMap.
324    *
325    * @param key The key under which the Action is registered
326    * @param enabled Whether to enable the action.
327    */
328   protected void registerAction(final Object menu, final Object key, final boolean enabled) {
329     final ActionMap viewActions = view.getCanvasComponent().getActionMap();
330 
331     final Action action = viewActions.get(key);
332     if (action != null) {
333       final JMenuItem item = new JMenuItem(action);
334       final String name = (String) actionNames.get(key);
335       if (name != null) {
336         item.setText(name);
337       }
338       item.setEnabled(enabled);
339 
340       // explicitly setting an accelerator for these menu items is actually
341       // not necessary here because the actions are already registered in
342       // DemoBase.registerViewActions
343       // we do it nonetheless as a simple way to display the default
344       // key bindings of each action
345       final InputMap imap = view.getCanvasComponent().getInputMap();
346       final KeyStroke[] keyStrokes = imap.allKeys();
347       if (keyStrokes != null) {
348         for (int i = 0; i < keyStrokes.length; ++i) {
349           if (imap.get(keyStrokes[i]) == key) {
350             item.setAccelerator(keyStrokes[i]);
351             break;
352           }
353         }
354       }
355 
356       if (menu instanceof JMenu) {
357         ((JMenu) menu).add(item);
358       } else if (menu instanceof JPopupMenu) {
359         ((JPopupMenu) menu).add(item);
360       }
361     }
362   }
363 
364   protected HierarchyManager getHierarchyManager() {
365     return view.getGraph2D().getHierarchyManager();
366   }
367 
368   //////////////////////////////////////////////////////////////////////////////
369   // VIEW MODES ////////////////////////////////////////////////////////////////
370   //////////////////////////////////////////////////////////////////////////////
371 
372   /**
373    * provides the context sensitive popup menus
374    */
375   class HierarchicPopupMode extends PopupMode {
376     public JPopupMenu getPaperPopup(double x, double y) {
377       JPopupMenu pm = new JPopupMenu();
378       populateGroupingPopup(pm, x, y, null, false);
379       return pm;
380     }
381 
382     public JPopupMenu getNodePopup(Node v) {
383       Graph2D graph = getGraph2D();
384       JPopupMenu pm = new JPopupMenu();
385       populateGroupingPopup(pm, graph.getCenterX(v), graph.getCenterY(v), v, true);
386       return pm;
387     }
388 
389     public JPopupMenu getSelectionPopup(double x, double y) {
390       JPopupMenu pm = new JPopupMenu();
391       populateGroupingPopup(pm, x, y, null, getGraph2D().selectedNodes().ok());
392       return pm;
393     }
394   }
395 
396   /**
397    * Action that creates a new empty group node.
398    */
399   public static class CreateNewGroupNodeAction extends Graph2DViewActions.AbstractGroupingAction {
400 
401     public CreateNewGroupNodeAction() {
402       this(null);
403     }
404 
405     public CreateNewGroupNodeAction(final Graph2DView view) {
406       super("CREATE_NEW_GROUP_NODE", view);
407     }
408 
409     public void actionPerformed(ActionEvent e) {
410       final Graph2DView graph2DView = getView(e);
411       if (graph2DView != null) {
412         createGroupNode(graph2DView);
413         graph2DView.getGraph2D().updateViews();
414       }
415     }
416 
417     /**
418      * Create an empty group node, assigns a name and sets the node bounds.
419      */
420     protected Node createGroupNode(Graph2DView view) {
421       final Graph2D graph = view.getGraph2D();
422       graph.firePreEvent();
423       Node groupNode;
424       try {
425         groupNode = createGroupNodeImpl(graph);
426         assignGroupName(groupNode, view);
427         setGroupNodeBounds(view, graph, groupNode);
428       } finally {
429         graph.firePostEvent();
430       }
431       return groupNode;
432     }
433 
434     protected Node createGroupNodeImpl(Graph2D graph) {
435       return getHierarchyManager(graph).createGroupNode(graph);
436     }
437 
438     protected void setGroupNodeBounds(Graph2DView view, Graph2D graph, Node groupNode) {
439       double x = view.getCenter().getX();
440       double y = view.getCenter().getY();
441       graph.setCenter(groupNode, x, y);
442     }
443 
444     protected void assignGroupName(Node groupNode, Graph2DView view) {
445       NodeRealizer nr = view.getGraph2D().getRealizer(groupNode);
446       if (nr instanceof ProxyShapeNodeRealizer) {
447         ProxyShapeNodeRealizer pnr = (ProxyShapeNodeRealizer) nr;
448         pnr.getRealizer(0).setLabelText(createGroupName(groupNode, view));
449         pnr.getRealizer(1).setLabelText(createFolderName(groupNode, view));
450       } else {
451         nr.setLabelText(createGroupName(groupNode, view));
452       }
453     }
454 
455     protected String createFolderName(Node folderNode, Graph2DView view) {
456       return "Folder";
457     }
458 
459     protected String createGroupName(Node groupNode, Graph2DView view) {
460       return "Group";
461     }
462   }
463 
464   /**
465    * Action that creates a new empty folder node.
466    */
467   public static class CreateNewFolderNodeAction extends Graph2DViewActions.AbstractGroupingAction {
468 
469     public CreateNewFolderNodeAction() {
470       this(null);
471     }
472 
473     public CreateNewFolderNodeAction(final Graph2DView view) {
474       super("CREATE_NEW_FOLDER_NODE", view);
475     }
476 
477     public void actionPerformed(ActionEvent e) {
478       final Graph2DView graph2DView = getView(e);
479       if (graph2DView != null) {
480         createFolderNode(graph2DView);
481         graph2DView.getGraph2D().updateViews();
482       }
483     }
484 
485     /**
486      * Create an empty folder node, assigns a name and sets the node bounds.
487      */
488     protected Node createFolderNode(Graph2DView view) {
489       final Graph2D graph = view.getGraph2D();
490       graph.firePreEvent();
491       Node groupNode;
492       try {
493         groupNode = createFolderNodeImpl(graph);
494         assignFolderName(groupNode, view);
495         setFolderNodeBounds(view, graph, groupNode);
496       } finally {
497         graph.firePostEvent();
498       }
499       return groupNode;
500     }
501 
502     protected Node createFolderNodeImpl(Graph2D graph) {
503       return getHierarchyManager(graph).createFolderNode(graph);
504     }
505 
506     protected void setFolderNodeBounds(Graph2DView view, Graph2D graph, Node folderNode) {
507       double x = view.getCenter().getX();
508       double y = view.getCenter().getY();
509       graph.setCenter(folderNode, x, y);
510     }
511 
512     protected void assignFolderName(Node groupNode, Graph2DView view) {
513       NodeRealizer nr = view.getGraph2D().getRealizer(groupNode);
514       if (nr instanceof ProxyShapeNodeRealizer) {
515         ProxyShapeNodeRealizer pnr = (ProxyShapeNodeRealizer) nr;
516         pnr.getRealizer(0).setLabelText(createGroupName(groupNode, view));
517         pnr.getRealizer(1).setLabelText(createFolderName(groupNode, view));
518       } else {
519         nr.setLabelText(createGroupName(groupNode, view));
520       }
521     }
522 
523     protected String createFolderName(Node folderNode, Graph2DView view) {
524       return "Folder";
525     }
526 
527     protected String createGroupName(Node groupNode, Graph2DView view) {
528       return "Group";
529     }
530   }
531 }
532