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