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.orgchart;
29  
30  import y.base.DataMap;
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.base.NodeList;
38  import y.base.NodeMap;
39  import y.base.YList;
40  import y.geom.YPoint;
41  import y.geom.YVector;
42  import y.layout.NormalizingGraphElementOrderStage;
43  import y.util.GraphHider;
44  import y.util.Maps;
45  import y.view.BendList;
46  import y.view.EditMode;
47  import y.view.GenericNodeRealizer;
48  import y.view.Graph2D;
49  import y.view.Graph2DView;
50  import y.view.HitInfo;
51  import y.view.LineType;
52  import y.view.MoveSelectionMode;
53  import y.view.NavigationMode;
54  import y.view.NodeRealizer;
55  import y.view.Scroller;
56  import y.view.ViewCoordDrawableAdapter;
57  import y.view.ViewMode;
58  
59  import java.awt.Color;
60  import java.awt.Graphics2D;
61  import java.awt.Rectangle;
62  import java.awt.event.MouseEvent;
63  import java.awt.geom.AffineTransform;
64  import java.awt.geom.Ellipse2D;
65  import java.awt.geom.GeneralPath;
66  import java.awt.geom.Rectangle2D;
67  import javax.swing.tree.DefaultTreeModel;
68  import javax.swing.tree.MutableTreeNode;
69  import javax.swing.tree.TreeModel;
70  
71  /**
72   * Provides specialised view modes for interactively editing the tree
73   * structure or panning and collapsing and expanding group and folder nodes.
74   */
75  public class ViewModeFactory {
76    private ViewModeFactory() {
77    }
78  
79  
80    /**
81     * Creates a new view mode for panning and collapsing and expanding group and
82     * folder nodes. Panning is done by left- or right-dragging the mouse.
83     * Collapsing and expanding is done by double-clicking group or folder nodes.
84     * @param view the tree chart for which the view mode is created.
85     * @return a new {@link JTreeChartViewMode} instance.
86     */
87    static ViewMode newNavigationMode( final JTreeChart view ) {
88      final JTreeChartViewMode viewMode = new JTreeChartViewMode();
89      Scroller scroller = viewMode.getScroller();
90      scroller.setScrollSpeedFactor(2.0);
91      scroller.setDrawable(new ScrollerDrawable(view, scroller));
92      return viewMode;
93    }
94  
95    /**
96     * <code>NavigationMode</code> that provides custom single and double mouse
97     * click handling.
98     * <p>
99     * Single clicking a node representing business data (and not a business unit)
100    * will select said node; single clicking anything else will unselect any
101    * selected node.
102    * </p><p>
103    * Double clicking a node representing business data will invoke
104    * {@link JTreeChart#performNodeAction(y.base.Node)} for that node, double
105    * clicking a node representing a business unit will toggle the node's
106    * collapsed/expanded state, and finally double clicking anything but a node
107    * will trigger an animated fit content operation
108    * (see {@link demo.view.orgchart.JTreeChart#fitContent(boolean)}).
109    * </p>
110    */
111   public static class JTreeChartViewMode extends NavigationMode {
112     public JTreeChart getJTreeChart() {
113       return (JTreeChart) view;
114     }
115 
116     public void mouseClicked(double x, double y) {
117       if(lastClickEvent.getClickCount() > 1) {
118         mouseDoubleClicked(x, y);
119       }
120       else {
121         mouseSingleClicked(x,y);
122       }
123     }
124 
125     /**
126      * Handles single mouse clicks for the specified world coordinates.
127      * Single clicking a normal node (as opposed to a group or folder node)
128      * will select said node (exclusively). Single clicking anything else will
129      * unselect all previously selected items.
130      * @param x   the x-coordinate in the associated view's world coordinate
131      * system.
132      * @param y   the y-coordinate in the associated view's world coordinate
133      * system.
134      * @see y.view.hierarchy.HierarchyManager#isFolderNode(y.base.Node)
135      * @see y.view.hierarchy.HierarchyManager#isGroupNode(y.base.Node)
136      * @see y.view.hierarchy.HierarchyManager#isNormalNode(y.base.Node)
137      */
138     protected void mouseSingleClicked(double x, double y) {
139       view.getCanvasComponent().requestFocus();
140       HitInfo info = getHitInfo(x,y);
141       Node node = info.getHitNode();
142       Graph2D graph = getGraph2D();
143       if (node != null && getGraph2D().getHierarchyManager().isNormalNode(node)) {
144         if (!graph.isSelected(node)) {
145           graph.unselectAll();
146           graph.setSelected(node, true);
147         }
148       } else {
149         getGraph2D().unselectAll();
150       }
151       getGraph2D().updateViews();
152     }
153 
154     /**
155      * Handles double mouse clicks for the specified world coordinates.
156      * Double clicking a normal node will invoke
157      * {@link demo.view.orgchart.JTreeChart#performNodeAction(y.base.Node)} for
158      * that node; double clicking a group node will collapse or close the group
159      * (i.e. hide its content); double clicking a folder node will expand or
160      * open the folder (i.e. unhide its content).
161      * @param x   the x-coordinate in the associated view's world coordinate
162      * system.
163      * @param y   the y-coordinate in the associated view's world coordinate
164      * system.
165      * @see y.view.hierarchy.HierarchyManager#isFolderNode(y.base.Node)
166      * @see y.view.hierarchy.HierarchyManager#isGroupNode(y.base.Node)
167      * @see y.view.hierarchy.HierarchyManager#isNormalNode(y.base.Node)
168      */
169     protected void mouseDoubleClicked(double x, double y) {
170       if (lastClickEvent.getClickCount() == 2) {
171         HitInfo info = getHitInfo(x,y);
172         Node node = info.getHitNode();
173         if (node != null) {
174           if (getGraph2D().getHierarchyManager().isGroupNode(node)) {
175             getJTreeChart().collapseGroup(node);
176           } else if (getGraph2D().getHierarchyManager().isFolderNode(node)) {
177             getJTreeChart().expandGroup(node);
178           } else {
179             getJTreeChart().performNodeAction(node);
180           }
181         } else {
182           getJTreeChart().fitContent(true);
183         }
184       }
185     }
186   }
187 
188   /**
189    * Drawable implementation used by the NavigationMode Scroller. 
190    * The appearance of the Scroller Drawable is zoom invariant. To accomplish this
191    * it is drawn in view coordinate space.  
192    */
193   static class ScrollerDrawable extends ViewCoordDrawableAdapter {
194     Scroller scroller;
195 
196     public ScrollerDrawable(Graph2DView view, Scroller scroller) {
197       super(view, null);
198       this.scroller = scroller;
199     }
200 
201     protected void paintViewCoordinateDrawable(Graphics2D gfx) {
202       gfx = (Graphics2D)gfx.create();
203       YVector dir = scroller.getScrollDirection();
204       YPoint p = scroller.getScrollStart();
205       p = new YPoint(view.toViewCoordX(p.x), view.toViewCoordY(p.y));
206       Ellipse2D circle = new Ellipse2D.Double(p.x-15, p.y-15,30,30);
207       gfx.setColor(new Color(204,204,204,100));
208       gfx.fill(circle);
209       gfx.setColor(new Color(100,100,100,100));
210       gfx.setStroke(LineType.LINE_1);
211       AffineTransform trans = new AffineTransform(dir.getX(), dir.getY(),-dir.getY(), dir.getX(),p.x,p.y);
212       GeneralPath arrow = new GeneralPath(GeneralPath.WIND_NON_ZERO,6);
213       arrow.moveTo(15,0);
214       arrow.lineTo(0,5);
215       arrow.lineTo(0,-5);
216       gfx.fill(trans.createTransformedShape(arrow));
217       gfx.setStroke(LineType.LINE_2);
218       gfx.draw(circle);
219       gfx.dispose();
220     }
221 
222     protected Rectangle getViewCoordinateDrawableBounds() {
223       YPoint p = scroller.getScrollStart();
224       p = new YPoint(view.toViewCoordX(p.x), view.toViewCoordY(p.y));
225       return (new Rectangle2D.Double(p.x-20,p.y-20,40,40)).getBounds();
226     }
227   }
228 
229 
230   /**
231    * Creates a new view mode for interactively editing the tree structure.
232    * Left-dragging a node to another node relocates the dragged node.
233    * Additionally, left-clicking a node opens controls for 
234    * hiding and un-hiding the node's children, relocating the node together
235    * with some or all of its children, as well as deleting the node. 
236    * @param view the tree chart for which the view mode is created.
237    * @return a new {@link JTreeChartEditMode} instance.
238    */
239   static ViewMode newEditMode( final JTreeChart view ) {
240     final JTreeChartEditMode editMode = new JTreeChartEditMode(new HoverButton(view));
241     editMode.getMouseInputMode().setDrawableSearchingEnabled(true);
242     editMode.allowNodeCreation(false);
243     editMode.allowResizeNodes(false);
244     editMode.allowEdgeCreation(false);
245     return editMode;
246   }
247 
248   /**
249    * <code>EditMode</code> that provides custom mouse single  click handling as
250    * well as custom mouse drag handling.
251    * <p>
252    * Single clicking a node representing an employee (and not a business unit)
253    * will display a set of controls for re-assigning or deleting said employee,
254    * for adding a subordinate employee, and for collapsing or expanding all
255    * of the employee's subordinates or superiors.
256    * </p><p>
257    * Double clicking a node representing business data will invoke
258    * {@link JTreeChart#performNodeAction(y.base.Node)} for that node and double
259    * clicking anything but a node will trigger an animated fit content operation
260    * (see {@link demo.view.orgchart.JTreeChart#fitContent(boolean)}).
261    * </p><p>
262    * Dragging a node representing an employee will re-assign the employee to a
263    * new superior.
264    * </p><p>
265    * Note, this implementation assumes that no business units are displayed.
266    * (I.e. there are no group nodes in the displayed graph.)
267    * </p>
268    */
269   public static class JTreeChartEditMode extends EditMode {
270     private final HoverButton hoverButton;
271     private long lastClickWhen;
272 
273     public JTreeChartEditMode( final HoverButton hoverButton ) {
274       this.hoverButton = hoverButton;
275     }
276 
277     public JTreeChart getJTreeChart() {
278       return (JTreeChart) view;
279     }
280 
281     protected ViewMode createMoveSelectionMode() {
282       return new JTreeChartMoveSelectionMode();
283     }
284 
285     /**
286      * Start moving a node.
287      * Used by buttons to set moveSelectionMode active.
288      * @param node node to move
289      * @param moveMode the selected move mode
290      */
291     public void startMovement(final Node node, final int moveMode) {
292       final Graph2D graph = getGraph2D();
293       graph.setSelected(node, true);
294 
295       ((JTreeChartMoveSelectionMode) getMoveSelectionMode()).setMoveMode(moveMode);
296 
297       final NodeRealizer realizer = graph.getRealizer(node);
298       final int x = view.toViewCoordX(realizer.getCenterX());
299       final int y = view.toViewCoordY(realizer.getCenterY());
300       setChild(moveSelectionMode, newPressedEvent(x, y), null);
301     }
302 
303     private MouseEvent newPressedEvent( final int viewX, final int viewY ) {
304       return new MouseEvent(
305               view.getCanvasComponent(),
306               MouseEvent.MOUSE_PRESSED,
307               System.currentTimeMillis(),
308               0, //modifiers
309               viewX, viewY,
310               0, //click count
311               false,
312               MouseEvent.BUTTON1);
313     }
314 
315     /**
316      * Disable multi selection box
317      * @return null to disable multi selection box
318      */
319     public ViewMode getSelectionBoxMode() {
320       return null;
321     }
322 
323     /**
324      * Check double clicking to collapse/expand group nodes.
325      * @param x   the x-coordinate in the associated view's world coordinate
326      * system.
327      * @param y   the y-coordinate in the associated view's world coordinate
328      * system.
329      */
330     public void mouseClicked(final double x, final double y) {
331       final MouseEvent ce = lastClickEvent;
332       if (ce.getClickCount() == 2) {
333         final HitInfo info = getHitInfo(x, y);
334         final Node node = info.getHitNode();
335         if (node != null) {
336           getJTreeChart().performNodeAction(node);
337         } else {
338           getJTreeChart().fitContent(true);
339         }
340       } else {
341         // when one of HoverButton's move controls is clicked,
342         // JTreeChartMoveSelectionMode consumes mousePressed and mouseReleased
343         // and immediately returns control to JTreeChartEditMode
344         // which means JTreeChartEditMode receives the click event
345         // in this case, do not update HoverButton because the hitNode might
346         // not be the currently selected node
347         final MouseEvent pe = lastPressEvent;
348         if (pe != null) {
349           final long lastPressWhen = pe.getWhen();
350           if (lastPressWhen > lastClickWhen) {
351             hoverButton.setNode(getHitInfo(x, y).getHitNode());
352             getJTreeChart().updateView();
353           }
354         }
355       }
356       lastClickWhen = ce.getWhen();
357     }
358 
359     /**
360      * Checks if this <code>JTreeChartEditMode</code> can be used with the
361      * specified model.
362      * @param model the model to check.
363      * @return <code>true</code> if this <code>JTreeChartEditMode</code> can be
364      * used with the specified model; <code>false</code> otherwise.
365      */
366     public static boolean isCompatibleModel( final TreeModel model ) {
367       return model instanceof DefaultTreeModel;
368     }
369   }
370 
371 
372   /**
373    * <code>MoveSelectionMode</code> that provides mouse drag handling for
374    * interactive changes of the displayed organization structure.
375    * <p>
376    * Note, this implementation assumes that no business units are displayed.
377    * (I.e. there are no group nodes in the displayed graph.)
378    * </p>
379    */
380   public static class JTreeChartMoveSelectionMode extends MoveSelectionMode {
381     /**
382      * Movement policy to move only a single selected node.
383      */
384     public static final int MOVE_MODE_SINGLE = 0;
385     /**
386      * Movement policy to move a selected node and all predecessors that are assistants.
387      */
388     public static final int MOVE_MODE_ASSISTANT = 1;
389     /**
390      * Movement policy to move the whole subtree.
391      */
392     public static final int MOVE_MODE_ALL = 2;
393 
394 
395     private int moveMode;
396     private GraphHider hider;
397     private YPoint center;
398     private NodeList nodesToBeMoved;
399 
400     public int getMoveMode() {
401       return moveMode;
402     }
403 
404     public void setMoveMode(final int moveMode) {
405       this.moveMode = moveMode;
406     }
407 
408     /**
409      * Overwritten to automatically reassign employees to new business units
410      * if business units are displayed.
411      */
412     public void mousePressedLeft( final double x, final double y ) {
413       mouseShiftPressedLeft(x, y);
414     }
415 
416     /**
417      * Overwritten to calculate the nodes to be moved.
418      */
419     public void mouseShiftPressedLeft( final double x, final double y ) {
420       initNodesToBeMoved(x, y);
421       super.mouseShiftPressedLeft(x, y);
422     }
423 
424     /**
425      * Overwritten to automatically reassign employees to new business units
426      * if business units are displayed.
427      */
428     public void mouseReleasedLeft( final double x, final double y ) {
429       super.mouseShiftReleasedLeft(x, y);
430     }
431 
432     /**
433      * Caches the nodes to be moved.
434      */
435     private void initNodesToBeMoved( final double x, final double y ) {
436       nodesToBeMoved = getNodesToBeMovedImpl(x, y);
437     }
438 
439     /**
440      * Calculates the nodes to be moved depending on the specified location
441      * the current movement mode policy.
442      * @see #getMoveMode()
443      */
444     private NodeList getNodesToBeMovedImpl( final double x, final double y ) {
445       final HitInfo hitInfo = getHitInfo(x, y);
446       final NodeList objects = new NodeList();
447       if (hitInfo.hasHitNodes() && !getJTreeChart().isLocalViewEnabled()) {
448         final Node hitNode = hitInfo.getHitNode();
449         if (getHierarchyManager().isNormalNode(hitNode)) {
450           final OrgChartTreeModel.Employee hitEmployee = getEmployee(hitNode);
451           if (!hitEmployee.isRoot()) {
452             if (hitEmployee.vacant) {
453               return objects;
454             }
455             final int moveMode = getMoveMode();
456             if (MOVE_MODE_ALL == moveMode) {
457               final NodeList stack = new NodeList();
458               stack.add(hitNode);
459               while(!stack.isEmpty()) {
460                 final Node node = stack.popNode();
461                 objects.add(node);
462                 stack.addAll(node.successors());
463               }
464               return objects;
465             } else if (MOVE_MODE_SINGLE == moveMode) {
466               objects.add(hitNode);
467               return objects;
468             } else if (MOVE_MODE_ASSISTANT == moveMode) {
469               objects.add(hitNode);
470               for (NodeCursor nc = hitNode.successors(); nc.ok(); nc.next()) {
471                 final Node node = nc.node();
472                 final OrgChartTreeModel.Employee employee = getEmployee(node);
473                 if (employee.assistant) {
474                   objects.add(node);
475                 }
476               }
477               return objects;
478             }
479           }
480         }
481       }
482       return objects;
483     }
484 
485     /**
486      * Returns the cached nodes to be moved.
487      * @return nodes to be moved.
488      */
489     protected NodeList getNodesToBeMoved() {
490       return nodesToBeMoved;
491     }
492 
493     /**
494      * Returns bends of all edges connected to nodes returned by {@link #getNodesToBeMoved()}.
495      * @return bends to be moved
496      */
497     protected BendList getBendsToBeMoved() {
498       final BendList bends = new BendList();
499       final Graph2D graph = getGraph2D();
500       final int moveMode = getMoveMode();
501       if (MOVE_MODE_ALL == moveMode) {
502         for (NodeCursor nc = getNodesToBeMoved().nodes(); nc.ok(); nc.next()) {
503           for (EdgeCursor ec = nc.node().outEdges(); ec.ok(); ec.next()) {
504             bends.addAll(graph.getRealizer(ec.edge()).bends());
505           }
506         }
507       } else if (MOVE_MODE_ASSISTANT == moveMode) {
508         for (NodeCursor nc = getNodesToBeMoved().nodes(); nc.ok(); nc.next()) {
509           final Node node = nc.node();
510           final OrgChartTreeModel.Employee employee = getEmployee(node);
511           if (employee.assistant) {
512             for (EdgeCursor ec = node.inEdges(); ec.ok(); ec.next()) {
513               bends.addAll(graph.getRealizer(ec.edge()).bends());
514             }
515           }
516         }
517       }
518       return bends;
519     }
520 
521     /**
522      * Get nodes that will be marked, to show user where node will be added.
523      * Returns one node, if node will be added as child of this node.
524      * Returns many nodes, if node will be added as sibling to these nodes.
525      * @return nodes to be marked
526      */
527     private NodeList nodesToBeMarked( final Graph2D graph, final Node subject ) {
528       final NodeList result = new NodeList();
529 
530       final Node root = getJTreeChart().getRootNode();
531 
532       final Rectangle2D r = graph.getRealizer(subject).getBoundingBox();
533       final NodeList queue = new NodeList();
534       queue.add(root);
535       while (!queue.isEmpty()) {
536         final Node node = queue.popNode();
537         queue.addAll(node.successors());
538 
539         if (node != subject) {
540           final NodeRealizer nr = graph.getRealizer(node);
541           if (r.intersects(nr.getX(), nr.getY(), nr.getWidth(), nr.getHeight())) {
542             result.add(node);
543           }
544         }
545       }
546 
547       if (result.size() > 1) {
548         final Node first = result.firstNode();
549         if (root == first) {
550           result.clear();
551           result.add(first);
552         } else {
553           boolean commonParent = true;
554           final Node parent = first.firstInEdge().source();
555           for (NodeCursor nc = result.nodes(); nc.ok(); nc.next()) {
556             final Node node = nc.node();
557             if (node.inDegree() < 1 || node.firstInEdge().source() != parent) {
558               commonParent = false;
559               break;
560             }
561           }
562 
563           if (!commonParent) {
564             result.clear();
565             result.add(first);
566           }
567         }
568       }
569 
570       return result;
571     }
572 
573     /**
574      * Called by {@link MoveSelectionMode} when moving of node just started.
575      * Shows nodes returned by {@link #nodesToBeMoved} atop other nodes
576      * and hides edges connected between moved and unmoved nodes.
577      * @param x mouse x-coordinate
578      * @param y mouse y-coordinate
579      */
580     protected void selectionMoveStarted(final double x, final double y) {
581       view.setDrawingMode(JTreeChart.NORMAL_MODE);
582       final NodeMap atopMap = Maps.createHashedNodeMap();
583       final NodeList nodesToBeMoved = getNodesToBeMoved();
584       if (nodesToBeMoved.isEmpty()) {
585         cancelEditing();
586         return;
587       }
588       for (NodeCursor nc = nodesToBeMoved.nodes(); nc.ok(); nc.next()) {
589         atopMap.setBool(nc.node(), true);
590       }
591       final Graph2D graph = getGraph2D();
592       graph.addDataProvider(JTreeChart.ATOP_DPKEY, atopMap);
593 
594       final Node subject = nodesToBeMoved.firstNode();
595       center = new YPoint(graph.getCenterX(subject), graph.getCenterY(subject));
596 
597       final GraphHider hider = new GraphHider(graph);
598       hider.hide(subject.firstInEdge());
599       final int moveMode = getMoveMode();
600       if (MOVE_MODE_SINGLE == moveMode) {
601         hider.hide(subject.outEdges());
602       } else if(MOVE_MODE_ASSISTANT == moveMode) {
603         for (final EdgeCursor ec = subject.outEdges(); ec.ok(); ec.next()) {
604           final Edge edge = ec.edge();
605           final OrgChartTreeModel.Employee employee = getEmployee(edge.target());
606           if (!employee.assistant) {
607             hider.hide(edge);
608           }
609         }
610       }
611       this.hider = hider;
612     }
613 
614     /**
615      * Called when node was moved.
616      * Updates marked nodes.
617      * @param dx the difference between the given x-coordinate and the
618      * x-coordinate of the last mouse event handled by this mode.
619      * @param dy the difference between the given y-coordinate and the
620      * y-coordinate of the last mouse event handled by this mode.
621      * @param x the x-coordinate of the triggering mouse event in the world
622      * coordinate system.
623      * @param y the y-coordinate of the triggering mouse event in the world
624      */
625     protected void selectionOnMove(final double dx, final double dy, final double x, final double y) {
626       if (!isEditing()) {
627         return;
628       }
629 
630       final Graph2D graph = getGraph2D();
631       final Node subject = getSubject();
632       if (subject == null) {
633         removeDataProvider(graph, JTreeChart.MARKED_NODES_DPKEY);
634       } else {
635         final DataMap map = Maps.createHashedNodeMap();
636         final NodeList nodes = nodesToBeMarked(view.getGraph2D(), subject);
637         for (NodeCursor nc = nodes.nodes(); nc.ok(); nc.next()) {
638           map.setBool(nc.node(), true);
639         }
640         graph.addDataProvider(JTreeChart.MARKED_NODES_DPKEY, map);
641       }
642     }
643 
644     /**
645      * Called after moving was finished.
646      * Update the graph depending on where the node was placed
647      * @param dx the difference between the given x-coordinate and the
648      * x-coordinate of the last mouse event handled by this mode.
649      * @param dy the difference between the given y-coordinate and the
650      * y-coordinate of the last mouse event handled by this mode.
651      * @param x the x-coordinate of the triggering mouse event in the world
652      * coordinate system.
653      * @param y the y-coordinate of the triggering mouse event in the world
654      */
655     protected void selectionMovedAction(final double dx, final double dy, final double x, final double y) {
656       super.selectionMovedAction(dx, dy, x, y);
657 
658       if (!isEditing()) {
659         return;
660       }
661 
662       final Node subject = getSubject();
663       if (subject != null) {
664         hider.unhideEdges();
665         final Graph2D graph = getGraph2D();
666         removeDataProvider(graph, JTreeChart.ATOP_DPKEY);
667         removeDataProvider(graph, JTreeChart.MARKED_NODES_DPKEY);
668 
669         final NodeList nodesToBeMarked = nodesToBeMarked(graph, subject);
670 
671         Node newParent = null;
672         //determine new parent
673         if (nodesToBeMarked.size() == 1) {
674           newParent = nodesToBeMarked.popNode();
675         } else if (nodesToBeMarked.size() > 1) {
676           newParent = nodesToBeMarked.popNode().firstInEdge().source();
677         }
678 
679         final Node oldParent = subject.firstInEdge().source();
680         final int moveMode = getMoveMode();
681         if (newParent != null) {
682           final OrgChartTreeModel.Employee parentEmployee = getEmployee(newParent);
683           //only change when parent changed
684           if (newParent != oldParent || parentEmployee.vacant) {
685             if (MOVE_MODE_ALL == moveMode ||
686                 (MOVE_MODE_SINGLE == moveMode && subject.outDegree() == 0)) {
687               moveWholeSubtree(subject, graph, newParent);
688             } else if (MOVE_MODE_SINGLE == moveMode) {
689               moveLeavingVacant(subject, graph, newParent);
690             } else if (MOVE_MODE_ASSISTANT == moveMode) {
691               //Collect assistants
692               final EdgeList assistantEdges = new EdgeList();
693               final YList assistants = new YList();
694               for (EdgeCursor ec = subject.outEdges(); ec.ok(); ec.next()) {
695                 final Edge edge = ec.edge();
696                 final OrgChartTreeModel.Employee employee = getEmployee(edge.target());
697                 if (employee != null && employee.assistant) {
698                   assistantEdges.add(edge);
699                   assistants.add(employee);
700                 }
701               }
702               if (assistantEdges.size() < subject.outDegree()) {
703                 //employee has subordinates that are not assistants
704                 //-> move only assistants with him
705                 final Node node = moveLeavingVacant(subject, graph, newParent);
706                 final OrgChartTreeModel.Employee nodeEmployee = getEmployee(node);
707                 if (node != newParent) {
708                   nodeEmployee.removeAllChildren();
709                 }
710 
711                 changeBusinessUnit(assistants, nodeEmployee.businessUnit);
712 
713                 final DataMap comparableEdgeMap = (DataMap) graph.getDataProvider(
714                         NormalizingGraphElementOrderStage.COMPARABLE_EDGE_DPKEY);
715                 final DefaultTreeModel treeModel = getMutableTreeModel();
716                 for (EdgeCursor ec = assistantEdges.edges(); ec.ok(); ec.next()) {
717                   final Edge edge = ec.edge();
718                   final Node child = edge.target();
719                   final Edge newEdge = graph.createEdge(node, child);
720                   graph.removeEdge(edge);
721                   comparableEdgeMap.set(newEdge, new Integer(newEdge.index()));
722 
723                   //transfer assistants to employee
724                   treeModel.insertNodeInto(
725                           getEmployee(child),
726                           nodeEmployee,
727                           nodeEmployee.getChildCount());
728                 }
729               } else {
730                 moveWholeSubtree(subject, graph, newParent);
731               }
732             }
733           }
734         }
735 
736         setMoveMode(MOVE_MODE_SINGLE);
737 
738         getJTreeChart().layoutGraph(true);
739       }
740     }
741 
742     /**
743      * Returns the node that represents the employee that is actually moved.
744      * @return the node that represents the employee that is actually moved.
745      */
746     private Node getSubject() {
747       final NodeList list = nodesToBeMoved;
748       return list == null || list.isEmpty() ? null : list.firstNode();
749     }
750 
751     /**
752      * Whole subtree was moved, so only the parent of the subtrees root must be changed.
753      * @param hitNode node that was moved
754      * @param graph2D current <code>Graph2D</code>
755      * @param newParent new parent for <code>hitNode</code>
756      */
757     private void moveWholeSubtree(final Node hitNode, final Graph2D graph2D, final Node newParent) {
758       final OrgChartTreeModel.Employee parentEmployee = getEmployee(newParent);
759       final DataMap comparableEdgeMap = (DataMap) graph2D.getDataProvider(
760               NormalizingGraphElementOrderStage.COMPARABLE_EDGE_DPKEY);
761       final DefaultTreeModel treeModel = getMutableTreeModel();
762 
763       final YList movedEmployees = new YList();
764       for (NodeCursor nc = getNodesToBeMoved().nodes(); nc.ok(); nc.next()) {
765         movedEmployees.add(getEmployee(nc.node()));
766       }
767 
768       final OrgChartTreeModel.Employee employee = getEmployee(hitNode);
769       if (parentEmployee.vacant) {
770         employee.adoptStructuralData(parentEmployee);
771 
772         //modify graph
773         final GenericNodeRealizer gnr = (GenericNodeRealizer) graph2D.getRealizer(newParent);
774         gnr.setUserData(employee);
775         getJTreeChart().configureNodeRealizer(newParent);
776 
777         for (final EdgeCursor ec = hitNode.outEdges(); ec.ok(); ec.next()) {
778           final Edge edge = ec.edge();
779           final Edge newEdge = graph2D.createEdge(newParent, edge.target());
780           comparableEdgeMap.set(newEdge,new Integer(newEdge.index()));
781           graph2D.removeEdge(edge);
782         }
783 
784         //Modify tree structure
785         getJTreeChart().updateUserObject(newParent, employee);
786         final MutableTreeNode grandfather = (MutableTreeNode) parentEmployee.getParent();
787         if (grandfather != null) {
788           treeModel.insertNodeInto(employee, grandfather,grandfather.getIndex(parentEmployee));
789         }
790 
791         //transfer children to employee
792         while(parentEmployee.getChildCount() > 0) {
793           treeModel.insertNodeInto((MutableTreeNode) parentEmployee.getChildAt(0), employee, employee.getChildCount());
794         }
795         if (!parentEmployee.isRoot()) {
796           treeModel.removeNodeFromParent(parentEmployee);
797         }
798         gnr.setUserData(employee);
799         final boolean state = graph2D.isSelected(hitNode);
800         graph2D.removeNode(hitNode);
801         graph2D.setSelected(newParent, state);
802         getJTreeChart().configureNodeRealizer(newParent);
803       } else {
804         //change parent
805         graph2D.removeEdge(hitNode.firstInEdge());
806         final Edge edge = graph2D.createEdge(newParent, hitNode);
807         comparableEdgeMap.set(edge, new Integer(edge.index()));
808         treeModel.removeNodeFromParent(employee);
809         treeModel.insertNodeInto(employee, parentEmployee, parentEmployee.getChildCount());
810         employee.businessUnit = parentEmployee.businessUnit;
811       }
812       changeBusinessUnit(movedEmployees, employee.businessUnit);
813     }
814 
815     /**
816      * Changes the business unit of the given employees to the specified new
817      * business unit.
818      * @param employees a list of {@link OrgChartTreeModel.Employee} instances.
819      * @param businessUnit the new business unit.
820      */
821     private void changeBusinessUnit( final YList employees, final String businessUnit ) {
822       for (ListCell lc = employees.firstCell(); lc != null; lc = lc.succ()) {
823         final OrgChartTreeModel.Employee employee = (OrgChartTreeModel.Employee) lc.getInfo();
824         employee.businessUnit = businessUnit;
825       }
826     }
827 
828     /**
829      * Move node, leaving a vacant position.
830      * @param hitNode node that was moved
831      * @param graph2D current <code>Graph2D</code>
832      * @param newParent new parent for <code>hitNode</code>
833      * @return the node, where hitNode was moved to. Either a new node or <code>newParent</code>, if node
834      * was moved to a vacant position
835      */
836     private Node moveLeavingVacant(final Node hitNode, final Graph2D graph2D, final Node newParent) {
837       final DataMap comparableEdgeMap = (DataMap) graph2D.getDataProvider(
838               NormalizingGraphElementOrderStage.COMPARABLE_EDGE_DPKEY);
839       final DataMap comparableNodeMap = (DataMap) graph2D.getDataProvider(
840               NormalizingGraphElementOrderStage.COMPARABLE_NODE_DPKEY);
841       final OrgChartTreeModel.Employee parentEmployee = getEmployee(newParent);
842 
843       final Node node;
844       final OrgChartTreeModel.Employee employee = getEmployee(hitNode);
845       final OrgChartTreeModel.Employee newEmployee = (OrgChartTreeModel.Employee) employee.clone();
846       employee.vacate();
847       getJTreeChart().configureNodeRealizer(hitNode);
848       newEmployee.businessUnit = parentEmployee.businessUnit;
849 
850       final DefaultTreeModel treeModel = getMutableTreeModel();
851       if (parentEmployee.vacant) {
852         node = newParent;
853         final MutableTreeNode grandfather = (MutableTreeNode) parentEmployee.getParent();
854         if (grandfather != null) {
855           treeModel.insertNodeInto(newEmployee, grandfather, grandfather.getIndex(parentEmployee));
856         }
857         while(parentEmployee.getChildCount() > 0) {
858           treeModel.insertNodeInto((MutableTreeNode) parentEmployee.getChildAt(0), newEmployee,
859                                    newEmployee.getChildCount());
860         }
861         if (!parentEmployee.isRoot()) {
862           treeModel.removeNodeFromParent(parentEmployee);
863         }
864         newEmployee.adoptStructuralData(parentEmployee);
865       } else {
866         node = graph2D.createNode();
867         final Edge edge = graph2D.createEdge(newParent, node);
868         comparableEdgeMap.set(edge, new Integer(edge.index()));
869         comparableNodeMap.set(node, new Integer(node.index()));
870         treeModel.insertNodeInto(newEmployee,parentEmployee,parentEmployee.getChildCount());
871       }
872       final GenericNodeRealizer gnr = (GenericNodeRealizer) graph2D.getRealizer(node);
873       gnr.setCenter(graph2D.getCenterX(hitNode), graph2D.getCenterY(hitNode));
874       graph2D.setCenter(hitNode, center);
875       gnr.setUserData(newEmployee);
876       getJTreeChart().updateUserObject(node, newEmployee);
877       graph2D.firePreEvent();
878       graph2D.setSelected(hitNode, false);
879       graph2D.setSelected(node, true);
880       graph2D.firePostEvent();
881       getJTreeChart().configureNodeRealizer(node);
882       return node;
883     }
884 
885     /**
886      * Overwritten to free resources.
887      */
888     public void reactivateParent() {
889       hider = null;
890       center = null;
891       nodesToBeMoved = null;
892       super.reactivateParent();
893     }
894 
895     /**
896      * Removes the {@link y.base.DataProvider} instances registered for the
897      * specified key from the given graph.
898      * @param graph the graph from which to remove a {@link y.base.DataProvider}
899      * instance.
900      * @param key the key for which the {@link y.base.DataProvider} instance was
901      * registered.
902      */
903     private void removeDataProvider( final Graph2D graph, final Object key ) {
904       if (graph.getDataProvider(key) != null) {
905         graph.removeDataProvider(key);
906       }
907     }
908 
909     public JTreeChart getJTreeChart() {
910       return (JTreeChart) view;
911     }
912 
913     public DefaultTreeModel getMutableTreeModel() {
914       return (DefaultTreeModel) getJTreeChart().getModel();
915     }
916 
917     /**
918      * Retrieves the {@link OrgChartTreeModel.Employee} instance represented by
919      * the given node.
920      * @return the {@link OrgChartTreeModel.Employee} instance represented by
921      * the given node.
922      */
923     private OrgChartTreeModel.Employee getEmployee( final Node hitNode ) {
924       return (OrgChartTreeModel.Employee) getJTreeChart().getUserObject(hitNode);
925     }
926   }
927 }
928