| MoveNodeMode.java |
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