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.mindmap;
29  
30  import y.base.DataProvider;
31  import y.base.Edge;
32  import y.base.EdgeCursor;
33  import y.base.EdgeList;
34  import y.base.ListCell;
35  import y.base.Node;
36  import y.base.NodeCursor;
37  import y.util.DataProviderAdapter;
38  import y.view.Graph2D;
39  import y.view.Graph2DView;
40  import y.view.Graph2DViewActions;
41  import y.view.HitInfo;
42  import y.view.NodeLabel;
43  import y.view.ViewMode;
44  import y.view.YLabel;
45  
46  import javax.swing.AbstractAction;
47  import javax.swing.Action;
48  import javax.swing.ActionMap;
49  import javax.swing.InputMap;
50  import javax.swing.JComponent;
51  import javax.swing.KeyStroke;
52  import java.awt.event.ActionEvent;
53  import java.awt.event.KeyEvent;
54  
55  /**
56   * This class provides several actions for keyboard interaction and
57   * a method to register the actions
58   */
59  class KeyboardHandling {
60    /**
61     * Prevent instantiation of utility class.
62     */
63    private KeyboardHandling() {
64    }
65  
66  
67    static void setKeyActions( final Graph2DView view ) {
68      final ActionMap actionMap = view.getActionMap();
69      final InputMap inputMap = view.getInputMap();
70      //register actions
71      actionMap.put("INSERT_CHILD", new AddChildAction(view));
72      actionMap.put("INSERT_SIBLING", new AddSiblingAction(view));
73      actionMap.put("RIGHT", new CursorAction(CursorAction.RIGHT, view));
74      actionMap.put("LEFT", new CursorAction(CursorAction.LEFT, view));
75      actionMap.put("UP", new CursorAction(CursorAction.UP, view));
76      actionMap.put("DOWN", new CursorAction(CursorAction.DOWN, view));
77      actionMap.put("DELETE", new DeleteSelection(view.getGraph2D()));
78      actionMap.put("PLUS", new CollapseExpandAction(true, view.getGraph2D()));
79      actionMap.put("MINUS", new CollapseExpandAction(false, view.getGraph2D()));
80      actionMap.put("EDIT", new EditAction(view));
81      //connect keys and registered actions
82      inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0), "INSERT_CHILD");
83      inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "INSERT_SIBLING");
84      inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), "RIGHT");
85      inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), "LEFT");
86      inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "UP");
87      inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), "DOWN");
88      inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "DELETE");
89      inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE,0),"DELETE");
90      inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, 0), "PLUS");
91      inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD,0), "PLUS");
92      inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, 0), "MINUS");
93      inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0), "MINUS");
94      inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), "EDIT");
95      inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F2,0), "EDIT");
96      view.getCanvasComponent().setInputMap(JComponent.WHEN_FOCUSED, inputMap);
97      view.getCanvasComponent().setActionMap(actionMap);
98    }
99  
100   /**
101    * Used by {@see MindMapDemo.createDeleteSelectionAction} to register the delete action for the toolbar.
102    * @param view the current Graph2DView
103    * @return {@link DeleteSelection}
104    */
105   static Action createDeleteSelectionAction( final Graph2DView view ) {
106     final DeleteSelection ds = new DeleteSelection(view.getGraph2D());
107     final ActionMap actionMap = view.getCanvasComponent().getActionMap();
108     actionMap.put(Graph2DViewActions.DELETE_SELECTION, ds);
109     return ds;
110   }
111 
112   private static class DeleteSelection extends AbstractAction {
113     private final Graph2D graph2D;
114 
115     public DeleteSelection( final Graph2D graph2D ) {
116       super("Delete Selection");
117       this.graph2D = graph2D;
118       this.putValue(Action.SMALL_ICON, MindMapUtil.getIcon("delete.png"));
119       this.putValue(Action.SHORT_DESCRIPTION, "Delete Selection");
120     }
121 
122     public void actionPerformed(final ActionEvent e) {
123       //Deletion of cross edges
124       if (graph2D.selectedEdges().size() > 0) {
125         final Edge edge = graph2D.selectedEdges().edge();
126         if (ViewModel.instance.isCrossReference(edge)) {
127           graph2D.removeEdge(edge);
128           graph2D.getCurrentView().updateView();
129         }
130       //Deletion of items, except the center item
131       } else if (!graph2D.isSelectionEmpty()) {
132         final Node node = graph2D.selectedNodes().node();
133         if (!ViewModel.instance.isRoot(node)) {
134           graph2D.firePreEvent();
135           final Edge inEdge = MindMapUtil.inEdge(node);
136           MindMapUtil.removeSubtree(graph2D, node);
137           if (inEdge != null) {
138             graph2D.setSelected(inEdge.source(), true);
139           }
140           LayoutUtil.layout(graph2D);
141           graph2D.firePostEvent();
142         }
143       }
144     }
145   }
146 
147   /**
148    * Edits labels of selected nodes and selected cross-reference edges.
149    */
150   private static class EditAction extends Graph2DViewActions.EditLabelAction {
151     EditAction( final Graph2DView view ) {
152       super(view);
153     }
154 
155     /**
156      * Determines the label instance to edit.
157      * @param view the view whose graph is searched for a label to edit.
158      * @return the label instance to edit.
159      */
160     protected YLabel findLabel( final Graph2DView view ) {
161       final Graph2D graph = view.getGraph2D();
162       final NodeCursor nc = graph.selectedNodes();
163       if (nc.ok()) {
164         return graph.getRealizer(nc.node()).getLabel();
165       } else {
166         for (EdgeCursor ec = graph.selectedEdges(); ec.ok(); ec.next()) {
167           final Edge edge = ec.edge();
168           if (ViewModel.instance.isCrossReference(edge)) {
169             return graph.getRealizer(edge).getLabel();
170           }
171         }
172       }
173       return null;
174     }
175 
176     /**
177      * Sets the text of the specified label.
178      * In case of a node label, the size of the corresponding node is adjusted
179      * to match the new label text and a new layout is calculated for the mind
180      * map.
181      * @param label the label whose text content is set.
182      * @param text the new text content.
183      */
184     protected void setText( final YLabel label, final String text ) {
185       if (label instanceof NodeLabel) {
186         final NodeLabel nl = (NodeLabel) label;
187         final Graph2D graph = nl.getGraph2D();
188         // backupRealizers is necessary here to ensure undo is working properly
189         // (the latter call to MindMapUtil.layout may change properties of
190         // *all* node and edge realizers)
191         graph.backupRealizers();
192         nl.setText(text);
193         MindMapUtil.updateWidth(graph, nl.getNode());
194         LayoutUtil.layout(graph);
195       } else {
196         label.setText(text);
197       }
198     }
199   }
200 
201   /**
202    * Edits labels when double-clicking on nodes, cross-reference edges, and/or
203    * labels.
204    */
205   static class LabelChangeViewMode extends ViewMode {
206     /**
207      * Starts label editing for double-clicks on nodes, cross-reference edges,
208      * and/or labels.
209      * @param x the x-coordinate of the mouse event in world coordinates.
210      * @param y the y-coordinate of the mouse event in world coordinates.
211      */
212     public void mouseClicked( final double x, final double y ) {
213       if (lastClickEvent != null && lastClickEvent.getClickCount() == 2) {
214         final HitInfo hitInfo = getHitInfo(x, y);
215         if (hitInfo.hasHitNodeLabels()) {
216           editLabel(hitInfo.getHitNodeLabel());
217         } else if (hitInfo.hasHitNodes()) {
218           final Graph2D graph2D = getGraph2D();
219           editLabel(graph2D.getRealizer(hitInfo.getHitNode()).getLabel());
220         } else if (hitInfo.hasHitEdges()) {
221           final Edge edge = hitInfo.getHitEdge();
222           // only cross-reference edges should have labels
223           // (to explain why there is a connection)
224           if (ViewModel.instance.isCrossReference(edge)) {
225             final Graph2D graph2D = getGraph2D();
226             editLabel(graph2D.getRealizer(edge).getLabel());
227           }
228         } else if (hitInfo.hasHitEdgeLabels()) {
229           editLabel(hitInfo.getHitEdgeLabel());
230         }
231       }
232     }
233 
234     private void editLabel( final YLabel label ) {
235       KeyboardHandling.editLabel(view, label);
236     }
237   }
238 
239   /**
240    * Displays an inline text editor for the specified label's text.
241    * @param view the view that displays the inline editor.
242    * @param label the label whose text is edited.
243    */
244   static void editLabel( final Graph2DView view, final YLabel label ) {
245     final EditAction helper = new EditAction(view) {
246       protected YLabel findLabel( final Graph2DView view ) {
247         return label;
248       }
249     };
250     helper.editLabel(view);
251   }
252 
253   /**
254    * Collapse or expand an items children.
255    */
256   private static class CollapseExpandAction extends AbstractAction {
257     /**
258      * Distinguish between expand and collapse. if true, expand an item
259      */
260     private final boolean expand;
261     private final Graph2D graph2D;
262 
263     public CollapseExpandAction(final boolean expand, final Graph2D graph2D) {
264       this.expand = expand;
265       this.graph2D = graph2D;
266     }
267 
268     public void actionPerformed(ActionEvent e) {
269       final NodeCursor nodeCursor = graph2D.selectedNodes();
270       if (nodeCursor.size() > 0) {
271         graph2D.firePreEvent();
272         final Node n = nodeCursor.node();
273         if (expand) {
274           MindMapUtil.expandNode(graph2D, n);
275         } else {
276           MindMapUtil.collapseNode(graph2D, n);
277         }
278         LayoutUtil.layout(graph2D);
279         graph2D.firePostEvent();
280       }
281     }
282   }
283 
284   /**
285    * Adds a sibling for a given item.
286    * If the given item is the root item, a new child item is added to the root
287    * item instead.
288    */
289   private static class AddSiblingAction extends AbstractAction {
290     private final Graph2DView view;
291 
292     AddSiblingAction( final Graph2DView view ) {
293       this.view = view;
294     }
295 
296     public void actionPerformed(final ActionEvent e) {
297       final NodeCursor nc = view.getGraph2D().selectedNodes();
298       if (nc.ok()) {
299         final Node node = nc.node();
300         if (ViewModel.instance.isRoot(node)) {
301           MindMapUtil.addNode(view, node);
302         } else {
303           final Node parent = MindMapUtil.inEdge(node).source();
304           MindMapUtil.addNode(view, parent, ViewModel.instance.isLeft(node));
305         }
306       }
307     }
308   }
309 
310   /**
311    * Adds a child item for a given item.
312    */
313   private static class AddChildAction extends AbstractAction {
314     private final Graph2DView view;
315 
316     AddChildAction( final Graph2DView view ) {
317       this.view = view;
318     }
319 
320     public void actionPerformed( final ActionEvent e ) {
321       final NodeCursor nc = view.getGraph2D().selectedNodes();
322       if (nc.ok()) {
323         MindMapUtil.addNode(view, nc.node());
324       }
325     }
326   }
327 
328   /**
329    * Navigate through the mind map
330    */
331   private static class CursorAction extends AbstractAction {
332     private final int cursorMode;
333     private final Graph2DView view;
334 
335     static final int UP = 1;
336     static final int DOWN = 2;
337     static final int LEFT = 3;
338     static final int RIGHT = 4;
339 
340     public CursorAction(final int cursorMode, final Graph2DView view) {
341       this.cursorMode = cursorMode;
342       this.view = view;
343     }
344 
345     public void actionPerformed(ActionEvent e) {
346       final Graph2D graph2D = view.getGraph2D();
347       final NodeCursor nodeCursor = graph2D.selectedNodes();
348       if (nodeCursor.size() > 0) {
349         final Node node = nodeCursor.node();
350         Node target = null;
351         final ViewModel model = ViewModel.instance;
352         switch (cursorMode) {
353           case DOWN:
354             if (!model.isRoot(node)) {
355               final Node parent = MindMapUtil.inEdge(node).source();
356               //only care for items on the same side (important if parent is center item)
357               final boolean side = model.isLeft(node);
358               final EdgeList outEdges = new EdgeList(parent.outEdges(), getSameSidePredicate(side, model));
359               //sort edges according to their y-coordinate which is the order of navigating down
360               outEdges.sort(new LayoutUtil.YCoordComparator());
361               // find and choose successor edge of the current edge
362               final ListCell currentCell = outEdges.findCell(MindMapUtil.inEdge(node));
363               final ListCell succCell = outEdges.cyclicSucc(currentCell);
364               target = ((Edge) succCell.getInfo()).target();
365             }
366             break;
367           case UP:
368             if (!model.isRoot(node)) {
369               final Node parent = MindMapUtil.inEdge(node).source();
370               //only care for items on the same side (important if parent is center item)
371               final boolean side = model.isLeft(node);
372               final EdgeList outEdges = new EdgeList(parent.outEdges(), getSameSidePredicate(side, model));
373               //sort edges according to their y-coordinate which is the order of navigating up
374               outEdges.sort(new LayoutUtil.YCoordComparator());
375               //find and choose successor edge of the current edge
376               final ListCell currentCell = outEdges.findCell(MindMapUtil.inEdge(node));
377               final ListCell succCell = outEdges.cyclicPred(currentCell);
378               target = ((Edge) succCell.getInfo()).target();
379             }
380             break;
381           case LEFT:
382             if (!model.isRoot(node)) {
383               //depending on the side of an item, move to its parent or the first children
384               if (model.isLeft(node)) {
385                 final EdgeList edgeList = MindMapUtil.outEdges(node);
386                 if (!edgeList.isEmpty()) {
387                   target = edgeList.popEdge().target();
388                 }
389               } else {
390                 target = MindMapUtil.inEdge(node).source();
391               }
392             } else {
393               //if item is root, move to the first left item, if there's any
394               if (node.outDegree() > 0) {
395                 for (EdgeList edges = MindMapUtil.outEdges(node);!edges.isEmpty();) {
396                   final Edge edge = edges.popEdge();
397                   if (model.isLeft(edge.target())) {
398                     target = edge.target();
399                     break;
400                   }
401                 }
402               }
403             }
404             break;
405           case RIGHT:
406             if (!model.isRoot(node)) {
407               //depending on the side of an item, move to its parent or the first children
408               if (model.isLeft(node)) {
409                 target = MindMapUtil.inEdge(node).source();
410               } else {
411                 final EdgeList edgeList = MindMapUtil.outEdges(node);
412                 if (!edgeList.isEmpty()) {
413                   target = edgeList.popEdge().target();
414                 }
415               }
416             } else {
417               //if item is root, move to the first right item, if there's any
418               if (node.outDegree() > 0) {
419                 for (EdgeList edges = MindMapUtil.outEdges(node); !edges.isEmpty(); ) {
420                   final Edge edge = edges.popEdge();
421                   if (!model.isLeft(edge.target())) {
422                     target = edge.target();
423                     break;
424                   }
425                 }
426               }
427             }
428             break;
429         }
430         if (target != null) {
431           graph2D.setSelected(node, false);
432           view.updateView();
433           graph2D.setSelected(target, true);
434         }
435       }
436     }
437 
438     /**
439      * Creates a {@link y.base.DataProvider} that returns <code>true</code> for every edge that goes to the specified
440      * side and is not a cross reference.
441      * 
442      * @param side  The side of the graph (<code>true</code> -> left, <code>false</code> -> right). 
443      * @param model The view model which is used to determine the side of the graph.
444      * @return a data provider that returns if the checked edge goes to the specified side.
445      */
446     private DataProvider getSameSidePredicate(final boolean side, final ViewModel model) {
447       return new DataProviderAdapter() {
448         public boolean getBool(Object dataHolder) {
449           final Edge edge = (Edge) dataHolder;
450           return side == model.isLeft(edge.target()) && !model.isCrossReference(edge);
451         }
452       };
453     }
454   }
455 }
456