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.Command;
31  import y.base.Edge;
32  import y.base.EdgeList;
33  import y.base.Node;
34  import y.base.NodeCursor;
35  import y.base.NodeList;
36  import y.view.BendList;
37  import y.view.CreateEdgeMode;
38  import y.view.EdgeRealizer;
39  import y.view.EditMode;
40  import y.view.GenericEdgeRealizer;
41  import y.view.Graph2D;
42  import y.view.Graph2DUndoManager;
43  import y.view.HitInfo;
44  import y.view.MoveSelectionMode;
45  import y.view.NodeRealizer;
46  import y.view.ViewMode;
47  
48  import java.awt.event.MouseEvent;
49  
50  /**
51   * Handles mouse dragged events to rearrange the mind map.
52   */
53  class MoveNodeMode extends EditMode {
54    private final Graph2DUndoManager undoManager;
55  
56    MoveNodeMode( final Graph2DUndoManager undoManager ) {
57      this.undoManager = undoManager;
58    }
59  
60    public void mouseClicked( final MouseEvent e ) {
61      super.mouseClicked(e);
62    }
63  
64    /**
65     * Called when the mouse is dragged outside the graph.
66     * Empty method prevents multi-selection
67     *
68     * @param graph     the graph which resides in the canvas
69     * @param x         the x coordinate where the mouse was clicked
70     * @param y         the y coordinate where the mouse was clicked
71     * @param firstDrag <code>true</code> if the previous mouse event captured by
72     *                  this <code>ViewMode</code> was <em>not</em> a drag event;
73     */
74    protected void paperDragged(
75        final Graph2D graph,
76        final double x,
77        final double y,
78        final boolean firstDrag
79    ) {
80  
81    }
82  
83    /**
84     * Creates a view mode for interactive creation of cross-reference edges.
85     */
86    protected ViewMode createCreateEdgeMode() {
87      return new MyCreateEdgeMode();
88    }
89  
90    /**
91     * use {@link MyCreateEdgeMode} to create a cross edge when the cross edge button was pressed.
92     * @param startNode source for cross edge
93     */
94    public void startCrossEdgeCreation(Node startNode) {
95      final NodeRealizer realizer = getGraph2D().getRealizer(startNode);
96      //initialize edge creation
97      setChild(createEdgeMode,null,null);
98      //trigger start of edge creation
99      createEdgeMode.mousePressedLeft(realizer.getCenterX(), realizer.getCenterY());
100   }
101 
102   /**
103    * Override to introduce own MoveSelectionMode
104    *
105    * @return own MoveSelectionMode
106    */
107   protected ViewMode createMoveSelectionMode() {
108     return new MindMapSelectionMode();
109   }
110 
111   /**
112    * Cross edge is created between selected and clicked node
113    *
114    * @param x horizontal mouse position
115    * @param y vertical mouse position
116    */
117   public void mouseShiftReleasedLeft(final double x, final double y) {
118     final Graph2D graph2D = view.getGraph2D();
119     final NodeCursor selectedNodes = graph2D.selectedNodes();
120     if (selectedNodes.ok()) {
121       final Node startNode = selectedNodes.node();
122       final HitInfo hitInfo = getHitInfo(x, y);
123       if (hitInfo.hasHitNodes()) {
124         final Node endNode = hitInfo.getHitNode();
125         if (endNode != startNode) {
126           final Edge edge = graph2D.createEdge(startNode, endNode);
127           ViewModel.instance.setCrossReference(edge);
128           graph2D.setSelected(startNode, false);
129         }
130       }
131     } else {
132       super.mouseShiftPressedLeft(x, y);
133     }
134   }
135 
136 
137   private class MindMapSelectionMode extends MoveSelectionMode {
138     /**
139      * The maximum distance to the next parent item.
140      * If an item is dragged around and has a greater distance than this to his next parent,
141      * it will be removed from the mind map
142      */
143     public static final int MAX_KEEP_DISTANCE = 200;
144     /**
145      * the item that is been dragged around
146      */
147     private Node node;
148     /**
149      * the last side of the node, true for left
150      */
151     private boolean oldSide;
152 
153     private MindMapSelectionMode() {
154     }
155 
156     public void mouseShiftPressedLeft(final double x, final double y) {
157       setChild(createEdgeMode, lastPressEvent, lastDragEvent);
158     }
159 
160     /**
161      * initialize node moving
162      */
163     protected void selectionMoveStarted(double x, double y) {
164       if (node == null) {
165         return;
166       }
167       final Graph2D graph2D = view.getGraph2D();
168       //backup incoming edge to make port change undoable
169       graph2D.backupRealizers();
170       oldSide = ViewModel.instance.isLeft(node);
171     }
172 
173     /**
174      * Called when mouse is moved.
175      * updates the side and the parent
176      * @param dx the difference between the given x-coordinate and the
177      * x-coordinate of the last mouse event handled by this mode.
178      * @param dy the difference between the given y-coordinate and the
179      * y-coordinate of the last mouse event handled by this mode.
180      * @param x the x-coordinate of the triggering mouse event in the world
181      * coordinate system.
182      * @param y the y-coordinate of the triggering mouse event in the world
183      */
184     protected void selectionOnMove(double dx, double dy, double x, double y)  {
185       if (node != null) {
186         final Graph2D graph = view.getGraph2D();
187         final ViewModel model = ViewModel.instance;
188         final NodeRealizer rootRealizer = graph.getRealizer(model.getRoot());
189         final boolean isRight = x > rootRealizer.getCenterX();
190         final boolean wasLeft = model.isLeft(node);
191         Edge inEdge = MindMapUtil.inEdge(node);
192         //if mouse changed side of center item
193         if (wasLeft == isRight) {
194           // update visuals of subtree after a possible side change
195           MindMapUtil.updateVisualsRecursive(graph, node, !wasLeft);
196 
197           LayoutUtil.layoutSubtree(graph, node);
198 
199           //inform the undo manager of a side change
200           if (oldSide != model.isLeft(node)) {
201             undoManager.push(new SideChangeAction(!oldSide, node));
202           }
203 
204           //MoveSelectionMode keeps the relative position of all nodes
205           //until the mouse button is released, layout changes didn't affect
206           //MoveSelectionMode. Triggering mousePressedLeft forces MoveSelectionMode
207           //to reinitialize the node positions.
208           mousePressedLeft(x, y);
209         }
210 
211         //determine (new) parent
212         final Node parent = calcClosestParent(node);
213 
214         //if no parent is in range, delete edge to last parent,
215         //showing the user there is currently no connection.
216         //Item should not be deleted until mouse button is released
217         if (parent == null) {
218           if (inEdge != null) {
219             graph.removeEdge(inEdge);
220           }
221         //change the parent item
222         } else {
223           final Node source = (inEdge != null) ? inEdge.source() : null;
224           if (parent != source) {
225             if (graph.containsEdge(parent, node)) {
226               if (inEdge != null && graph.contains(inEdge)) {
227                 graph.removeEdge(inEdge);
228               }
229             } else {
230               if (inEdge == null) {
231                 inEdge = graph.createEdge(
232                         parent, node, new GenericEdgeRealizer("BezierGradientEdge"));
233               } else {
234                 if (!graph.contains(inEdge)) {
235                   graph.reInsertEdge(inEdge);
236                 }
237                 graph.changeEdge(inEdge, parent, node);
238               }
239 
240               // make sure the edge connects at the bottom line of the node
241               final EdgeRealizer er = graph.getRealizer(inEdge);
242               er.clearBends();
243               if (model.isRoot(parent)) {
244                 er.getSourcePort().setOffsets(0, 0);
245               } else {
246                 final NodeRealizer src = graph.getRealizer(parent);
247                 final double srcX = src.getWidth() * 0.5 * (isRight ? 1 : -1);
248                 er.getSourcePort().setOffsets(srcX, src.getHeight() * 0.5);
249               }
250               final NodeRealizer tgt = graph.getRealizer(node);
251               final double tgtX = tgt.getWidth() * 0.5 * (isRight ? -1 : 1);
252               er.getTargetPort().setOffsets(tgtX, tgt.getHeight() * 0.5);
253             }
254           }
255         }
256       }
257     }
258 
259     /**
260      * Called after the left button was released.
261      * Updates the node settings, depending where it was placed.
262      * @param dx the difference between the given x-coordinate and the
263      * x-coordinate of the last mouse event handled by this mode.
264      * @param dy the difference between the given y-coordinate and the
265      * y-coordinate of the last mouse event handled by this mode.
266      * @param x the x-coordinate of the triggering mouse event in the world
267      * coordinate system.
268      * @param y the y-coordinate of the triggering mouse event in the world
269      */
270     protected void selectionMovedAction(double dx, double dy, double x, double y) {
271       final Node node = this.node;
272       if (node != null) {
273         final Graph2D graph = view.getGraph2D();
274         final ViewModel model = ViewModel.instance;
275 
276         Node parent = null;
277         if (!model.isRoot(node)) {
278           //determine parent item, if there is any
279           final Edge inEdge = MindMapUtil.inEdge(node);
280           parent = inEdge == null ? null : inEdge.source();
281           if (parent != null) {
282             // update visuals of subtree after a possible side change
283             final boolean isLeft = model.isLeft(node);
284             MindMapUtil.updateVisualsRecursive(graph, node, isLeft);
285 
286             //make siblings visible
287             //exclude this node from the undo collapse action
288             if (model.isCollapsed(parent)) {
289               MindMapUtil.expandNode(graph, parent);
290             }
291           }
292         }
293 
294         //remove item when not connected to the mind map anymore
295         if (parent == null && !model.isRoot(node)) {
296           MindMapUtil.removeSubtree(graph, node);
297         }
298 
299         LayoutUtil.layout(graph);
300 
301         this.node = null;
302       }
303     }
304 
305     /**
306      * Returns the whole subtree of a node.
307      * Ignores cross edges.
308      * @return nodes subtree
309      */
310     protected NodeList getNodesToBeMoved() {
311       NodeList nodes = new NodeList();
312       final HitInfo lastHitInfo = getLastHitInfo();
313       node = lastHitInfo.getHitNode();
314       //root node should not be able to move
315       if (ViewModel.instance.isRoot(node)) {
316         node = null;
317       }
318       if (node != null) {
319         NodeList stack = new NodeList(node);
320         while (!stack.isEmpty()) {
321           Node n = stack.popNode();
322           for (EdgeList edges = MindMapUtil.outEdges(n); !edges.isEmpty();) {
323             stack.push(edges.popEdge().target());
324           }
325           nodes.push(n);
326         }
327       } else {
328         nodes = new NodeList();
329       }
330       return nodes;
331     }
332 
333     /**
334      * bends of nodes returned by {@link this.getNodesToBeMoved}
335      * @return all bends
336      */
337     protected BendList getBendsToBeMoved() {
338       if (node != null) {
339         BendList bends = new BendList();
340         final NodeList nodesToBeMoved = getNodesToBeMoved();
341         while (!nodesToBeMoved.isEmpty()) {
342           for (EdgeList edges = MindMapUtil.outEdges(nodesToBeMoved.popNode());!edges.isEmpty();) {
343             bends.addAll(getGraph2D().getRealizer(edges.popEdge()).bends());
344           }
345         }
346         return bends;
347       } else {
348         return super.getBendsToBeMoved();
349       }
350     }
351 
352     /**
353      * Calc the nearest possible parent.
354      * @param node node whose parents are calculated
355      * @return nearest item, when distance is less then {@link this.MAX_KEEP_DISTANCE}
356      */
357     private Node calcClosestParent(Node node) {
358       final NodeList possibleParents = calcPossibleParents(node);
359       Node bestParent = possibleParents.popNode();
360       final Graph2D graph2D = view.getGraph2D();
361       final NodeRealizer nodeRealizer = graph2D.getRealizer(node);
362       double dist = calcDist(nodeRealizer, graph2D.getRealizer(bestParent));
363       while (!possibleParents.isEmpty()) {
364         final Node otherParent = (Node) possibleParents.pop();
365         final double otherDist = calcDist(nodeRealizer, graph2D.getRealizer(otherParent));
366         if (otherDist < dist) {
367           bestParent = otherParent;
368           dist = otherDist;
369         }
370       }
371       final NodeRealizer bestParentRealizer = graph2D.getRealizer(bestParent);
372       //make distance independent from the width of the nodes
373       dist -= ((bestParentRealizer.getWidth() + nodeRealizer.getWidth()) * 0.5);
374       if (dist < MAX_KEEP_DISTANCE) {
375         return bestParent;
376       } else {
377         return null;
378       }
379     }
380 
381     /**
382      * Calculate the distance between two items
383      * @param firstRealizer  one node realizer
384      * @param secondRealizer second node realizer
385      * @return euclidean distance between two items
386      */
387     private double calcDist(final NodeRealizer firstRealizer, final NodeRealizer secondRealizer) {
388       final double dx = firstRealizer.getCenterX() - secondRealizer.getCenterX();
389       final double dy = firstRealizer.getCenterY() - secondRealizer.getCenterY();
390       return Math.sqrt(dx * dx + dy * dy);
391     }
392 
393     /**
394      * Calculate nodes that may be a valid parent.
395      * Valid parent are nearer to the center item than this item
396      *
397      * @param node node whose parents are calculated
398      * @return all valid parents
399      */
400     private NodeList calcPossibleParents(Node node) {
401       final Graph2D graph2D = getGraph2D();
402       final ViewModel model = ViewModel.instance;
403       final NodeRealizer nodeRealizer = graph2D.getRealizer(node);
404       final boolean nodeIsLeft = model.isLeft(node);
405 
406       final NodeList possibleParents = new NodeList();
407       final NodeList stack = new NodeList();
408       final Node root = model.getRoot();
409       stack.add(root);
410       //the center item is always valid
411       possibleParents.add(root);
412       while (!stack.isEmpty()) {
413         Node n = stack.popNode();
414         for (EdgeList edges = MindMapUtil.outEdges(n); !edges.isEmpty(); ) {
415           n = edges.popEdge().target();
416           final NodeRealizer nr = graph2D.getRealizer(n);
417           if (nodeIsLeft) {
418             //if both items on the left side and <code>n</code> "lefter" than <code>node</code>
419             if (model.isLeft(n) &&
420                 nodeRealizer.getX() + nodeRealizer.getWidth() < nr.getX()) {
421               possibleParents.add(n);
422               stack.add(n);
423             }
424           //if both items on the right side and <code>n</code> "righter" than <code>node</code>
425           } else if (!model.isLeft(n) && ((nr.getX() + nr.getWidth()) < nodeRealizer.getX())) {
426             possibleParents.add(n);
427             stack.add(n);
428           }
429         }
430       }
431       return possibleParents;
432     }
433   }
434 
435   /**
436    * Action to make a side change of a node undo/redo able.
437    */
438   private class SideChangeAction implements Command {
439     private final boolean newSide;
440     private final Node node;
441 
442     private SideChangeAction(final boolean newSide, final Node node) {
443       this.newSide = newSide;
444       this.node = node;
445     }
446 
447     public void execute() {}
448 
449     public void undo() {
450       MindMapUtil.updateVisualsRecursive(view.getGraph2D(), node, !newSide);
451     }
452 
453     public void redo() {
454       MindMapUtil.updateVisualsRecursive(view.getGraph2D(), node, newSide);
455     }
456   }
457 
458   /**
459    * Marks interactively created edges as cross-reference edges.
460    */
461   private static final class MyCreateEdgeMode extends CreateEdgeMode {
462     /**
463      * Marks all interactively created edges as cross-reference edges.
464      * @param edge the newly created <code>Edge</code> instance.
465      */
466     protected void edgeCreated( final Edge edge ) {
467       ViewModel.instance.setCrossReference(edge);
468     }
469   }
470 }
471