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 demo.view.DemoBase;
31  import demo.view.orgchart.ViewModeFactory.JTreeChartMoveSelectionMode;
32  import demo.view.orgchart.ViewModeFactory.JTreeChartEditMode;
33  import y.algo.GraphConnectivity;
34  import y.algo.Paths;
35  import y.algo.Trees;
36  import y.base.DataMap;
37  import y.base.Edge;
38  import y.base.EdgeCursor;
39  import y.base.EdgeList;
40  import y.base.Node;
41  import y.base.NodeCursor;
42  import y.base.NodeList;
43  import y.base.NodeMap;
44  import y.geom.Geom;
45  import y.layout.NormalizingGraphElementOrderStage;
46  import y.util.Maps;
47  import y.view.AbstractMouseInputEditor;
48  import y.view.Drawable;
49  import y.view.GenericNodeRealizer;
50  import y.view.Graph2D;
51  import y.view.Graph2DView;
52  import y.view.HitInfo;
53  import y.view.Mouse2DEvent;
54  import y.view.MouseInputEditor;
55  import y.view.MouseInputEditorProvider;
56  import y.view.NodeRealizer;
57  import y.view.ViewMode;
58  import y.view.hierarchy.HierarchyManager;
59  
60  import javax.swing.Icon;
61  import javax.swing.ImageIcon;
62  import java.awt.Graphics2D;
63  import java.awt.Rectangle;
64  import java.util.ArrayList;
65  import java.util.Iterator;
66  import java.util.List;
67  
68  /**
69   * Shows and hides controls for re-assigning or deleting an employee,
70   * for adding a subordinate employee, and for collapsing or expanding all
71   * subordinates or superiors.
72   * <p>
73   * Note, all corresponding implementations assume that no business units
74   * are displayed. (I.e. there are no group nodes in the displayed graph.)
75   * </p>
76   */
77  public class HoverButton implements MouseInputEditorProvider, Drawable {
78    static final int VERTICAL_CONTROL_OFFSET = 25;
79  
80  
81    private Node node;
82    private final List buttons;
83    private final OrgChartTreeModel treeModel;
84    private final JTreeChart treeChart;
85    private final NodeMap hiddenNodesChild;
86    private final NodeMap hiddenEdgesChild;
87    private final NodeMap hiddenNodesParent;
88    private final NodeMap hiddenEdgesParent;
89  
90    public HoverButton( final JTreeChart treeChart ) {
91      this.treeChart = treeChart;
92      this.treeModel = (OrgChartTreeModel) treeChart.getModel();
93      buttons = new ArrayList();
94      hiddenNodesChild = Maps.createHashedNodeMap();
95      hiddenEdgesChild = Maps.createHashedNodeMap();
96      hiddenNodesParent = Maps.createHashedNodeMap();
97      hiddenEdgesParent = Maps.createHashedNodeMap();
98      buttons.add(new MoveButton("move.png", "move_disabled.png", 0, JTreeChartMoveSelectionMode.MOVE_MODE_SINGLE));
99      buttons.add(new MoveButton("move_plus.png", "move_plus_disabled.png", 1, JTreeChartMoveSelectionMode.MOVE_MODE_ASSISTANT));
100     buttons.add(new MoveButton("move_star.png", "move_star_disabled.png", 2, JTreeChartMoveSelectionMode.MOVE_MODE_ALL));
101     buttons.add(new ExpandButton("down.png", "down_disabled.png", -1, false));
102     buttons.add(new CollapseButton("up.png", "up_disabled.png", -1, false));
103     buttons.add(new AddButton("plus.png", null, 3));
104     buttons.add(new DeleteButton("trash.png", "trash_disabled.png", 4));
105     buttons.add(new ExpandButton("down.png", "down_disabled.png", 5, true));
106     buttons.add(new CollapseButton("up.png", "up_disabled.png", 5, true));
107     treeChart.addDrawable(this);
108   }
109 
110   public MouseInputEditor findMouseInputEditor(final Graph2DView view, final double x, final double y, final HitInfo hitInfo) {
111     if (isVisible()) {
112       for (final Iterator iterator = buttons.iterator(); iterator.hasNext(); ) {
113         final NodeButton nb = (NodeButton) iterator.next();
114         if (nb.getBounds().contains(x, y)) {
115           return nb;
116         }
117       }
118     }
119     return null;
120   }
121 
122   private boolean isVisible() {
123     return node != null;
124   }
125 
126   public void paint(final Graphics2D g) {
127     if (isVisible()) {
128       if (node.getGraph() == null) {
129         //Graph changed while HoverButton still shown
130         node = null;
131       } else {
132         for (final Iterator iterator = buttons.iterator(); iterator.hasNext(); ) {
133           final NodeButton nb = (NodeButton) iterator.next();
134           nb.paint(g);
135         }
136       }
137     }
138   }
139 
140   public Rectangle getBounds() {
141     final Rectangle r = new Rectangle(-1, -1, -1, -1);
142     if (isVisible()) {
143       for (final Iterator iterator = buttons.iterator(); iterator.hasNext(); ) {
144         final NodeButton nb = (NodeButton) iterator.next();
145         Geom.calcUnion(r, nb.getBounds(), r);
146       }
147     }
148     return r;
149   }
150 
151   /**
152    * Returns the tool tip text describing the control at the specified location.
153    * @param x the x-coordinates of the mouse position in world coordinates.
154    * @param y the x-coordinates of the mouse position in world coordinates.
155    * @return the tool tip text describing the control at the specified location.
156    */
157   public String getToolTipText( final double x, final double y ) {
158     if (isVisible()) {
159       for (Iterator it = buttons.iterator(); it.hasNext(); ) {
160         final NodeButton nb = (NodeButton) it.next();
161         if (nb.contains(x, y)) {
162           return nb.getToolTipText();
163         }
164       }
165     }
166     return null;
167   }
168 
169   /**
170    * Sets a new node active and changes all preferences.
171    * @param node the new node.
172    */
173   public void setNode(final Node node) {
174     if (node == null) {
175       this.node = node;
176     } else {
177       final HierarchyManager hm = treeChart.getGraph2D().getHierarchyManager();
178       if (hm.isNormalNode(node)) {
179         this.node = node;
180       } else {
181         this.node = null;
182       }
183     }
184   }
185 
186   static ImageIcon newIcon( final String path ) {
187     return path == null ? null : new ImageIcon(DemoBase.getResource(HoverButton.class, "resources/icons/" + path));
188   }
189 
190 
191   /**
192    * Button for adding a new employee.
193    */
194   private class AddButton extends NodeButton {
195     AddButton(final String enabled, final String disabled, final int position) {
196       super(enabled, disabled, position);
197     }
198 
199     /**
200      * Add a new employee as a child of current node.
201      */
202     void action() {
203       final Graph2D graph = treeChart.getGraph2D();
204       final DataMap comparableEdgeMap = (DataMap) graph.getDataProvider(
205           NormalizingGraphElementOrderStage.COMPARABLE_EDGE_DPKEY);
206       final DataMap comparableNodeMap = (DataMap) graph.getDataProvider(
207           NormalizingGraphElementOrderStage.COMPARABLE_NODE_DPKEY);
208       final DataMap graph2Tree = (DataMap) graph.getDataProvider(JTreeChart.GRAPH_2_TREE_MAP_DPKEY);
209       final DataMap tree2Graph = (DataMap) graph.getDataProvider(JTreeChart.TREE_2_GRAPH_MAP_DPKEY);
210 
211       final GenericNodeRealizer gnr = (GenericNodeRealizer) graph.getRealizer(node);
212       final OrgChartTreeModel.Employee employee = (OrgChartTreeModel.Employee) gnr.getUserData();
213       final OrgChartTreeModel.Employee newEmployee = new OrgChartTreeModel.Employee();
214 
215       newEmployee.icon = employee.icon;
216       newEmployee.businessUnit = employee.businessUnit;
217       newEmployee.status = employee.status;
218       treeModel.insertNodeInto(newEmployee, employee, employee.getChildCount());
219 
220       final Node newNode = graph.createNode();
221       graph.setCenter(newNode, gnr.getCenterX(), gnr.getCenterY());
222       final Edge edge = graph.createEdge(node, newNode);
223       graph2Tree.set(newNode, newEmployee);
224       tree2Graph.set(newEmployee,newNode);
225       comparableEdgeMap.set(edge, new Integer(edge.index()));
226       comparableNodeMap.set(newNode, new Integer(node.index()));
227 
228       final HierarchyManager hm = graph.getHierarchyManager();
229       final Node businessUnit = hm.getParentNode(node);
230       if (businessUnit != null) {
231         hm.setParentNode(newNode, businessUnit);
232       }
233 
234       setNode(null);
235 
236       graph.firePreEvent();
237       graph.unselectAll();
238       graph.setSelected(newNode, true);
239       graph.firePostEvent();
240 
241       treeChart.configureNodeRealizer(newNode);
242       treeChart.layoutGraph(true);
243     }
244 
245     /**
246      * Check if button should be active.
247      * @return always true, as a child could be added to every employee
248      */
249     boolean isActive() {
250       return true;
251     }
252 
253     String getToolTipText() {
254       return "Adds a new subordinate employee for the selected employee.";
255     }
256   }
257 
258   /**
259    * Button for deleting an employee.
260    */
261   private class DeleteButton extends NodeButton {
262     DeleteButton(final String enabled, final String disabled, final int position) {
263       super(enabled, disabled, position);
264     }
265 
266     /**
267      * Delete current node.
268      */
269     void action() {
270       final Graph2D graph = treeChart.getGraph2D();
271       final GenericNodeRealizer gnr = (GenericNodeRealizer) graph.getRealizer(node);
272       final OrgChartTreeModel.Employee employee = (OrgChartTreeModel.Employee) gnr.getUserData();
273       if (!employee.isRoot()) {
274         if (node.outDegree() > 0) {
275           employee.vacate();
276           treeChart.configureNodeRealizer(node);
277           // hack to update properties table because there is no property
278           // change support for employees
279           final boolean state = graph.isSelected(node);
280           graph.setSelected(node, !state);
281           graph.setSelected(node, state);
282         } else {
283           // IMPORTANT:
284           // remove the user object from the tree model first otherwise
285           // JOrgChart's tree selection listener trigger a global rebuild
286           treeModel.removeNodeFromParent(employee);
287           graph.removeNode(node);
288         }
289         setNode(null);
290         treeChart.layoutGraph(true);
291       }
292     }
293 
294     String getToolTipText() {
295       return "Removes the selected employee.";
296     }
297   }
298 
299   /**
300    * Button for moving a node.
301    */
302   private class MoveButton extends NodeButton {
303     private final int moveMode;
304 
305     MoveButton(final String enabled, final String disabled, final int position, final int moveMode) {
306       super(enabled, disabled, position);
307       this.moveMode = moveMode;
308     }
309 
310     /**
311      * Start the movement.
312      * Search for moveViewMode and trigger movement start.
313      */
314     void action() {
315       final Iterator it = treeChart.getViewModes();
316       if (it.hasNext()) {
317         // the first registered view mode is a dummy that does nothing by itself
318         // but serves as convenient way to switch between edit mode and
319         // navigation mode
320         final ViewMode masterMode = (ViewMode) it.next();
321         final ViewMode activeMode = masterMode.getChild();
322         if (activeMode instanceof JTreeChartEditMode) {
323           final JTreeChartEditMode editMode = (JTreeChartEditMode) activeMode;
324           editMode.startMovement(node, moveMode);
325           treeChart.updateView();
326         }
327       }
328       setNode(null);
329     }
330 
331     boolean isActive() {
332       return super.isActive() && !treeChart.isLocalViewEnabled() &&
333              !super.isExpandable(true) && !super.isExpandable(false);
334     }
335 
336     boolean acceptEvent( final Mouse2DEvent event ) {
337       return event.getId() == Mouse2DEvent.MOUSE_PRESSED &&
338              event.getButton() == 1;
339     }
340 
341     String getToolTipText() {
342       switch (moveMode) {
343         case JTreeChartMoveSelectionMode.MOVE_MODE_SINGLE:
344           return "Moves the selected employee.";
345         case JTreeChartMoveSelectionMode.MOVE_MODE_ASSISTANT:
346           return "Moves the selected employee and all of its assistants.";
347         case JTreeChartMoveSelectionMode.MOVE_MODE_ALL:
348           return "Moves the selected employee and all of its subordinate employees.";
349         default:
350           throw new IllegalStateException("Invalid movement mode: " + moveMode);
351       }
352     }
353   }
354 
355   /**
356    * Button for expanding children or parents.
357    * Children and parents are expanded layer by layer.
358    */
359   private class ExpandButton extends NodeButton {
360     private final boolean expandChildren;
361 
362     ExpandButton(final String enabled, final String disabled, final int position, final boolean expandChildren) {
363       super(enabled, disabled, position);
364       yOffset = expandChildren ? VERTICAL_CONTROL_OFFSET : 0;
365       this.expandChildren = expandChildren;
366     }
367 
368     public Rectangle getBounds() {
369       final Rectangle bounds = super.getBounds();
370       bounds.width *= 0.5;
371       bounds.height *= 0.5;
372       return bounds;
373     }
374 
375     /**
376      * Expand the next layer.
377      */
378     void action() {
379       final Graph2D graph2D = treeChart.getGraph2D();
380       if (expandChildren) {
381         int depth = graph2D.N();
382         if (hiddenNodesChild.get(node) != null) {
383           //node itself is collapsed => expand here
384           depth = 0;
385         } else {
386           //otherwise determine lowest depth where nodes are collapsed
387           final NodeList successors = GraphConnectivity.getSuccessors(graph2D, new NodeList(node), depth);
388           for (final NodeList leaves = Trees.getLeafNodes(graph2D, true);!leaves.isEmpty();) {
389             final Node n = leaves.popNode();
390             //is leaf of nodes subtree and has hidden nodes
391             if (successors.contains(n) && hiddenNodesChild.get(n) != null) {
392               depth = Math.min(depth, Paths.findPath(graph2D,node,n,false).size());
393             }
394           }
395         }
396         expandAtDepth(node, depth);
397       } else {
398         //search for farthest ancestor
399         Node currentNode = node;
400         while(currentNode.inDegree() >0) {
401           currentNode = currentNode.firstInEdge().source();
402         }
403         //show hidden parent and children, if there is any
404         final NodeList nodes = (NodeList) hiddenNodesParent.get(currentNode);
405         if (nodes != null) {
406           while(!nodes.isEmpty()) {
407             graph2D.reInsertNode(nodes.popNode());
408           }
409           final EdgeList edges = (EdgeList) hiddenEdgesParent.get(currentNode);
410           while(!edges.isEmpty()) {
411             graph2D.reInsertEdge(edges.popEdge());
412           }
413           hiddenNodesParent.set(currentNode,null);
414           hiddenEdgesParent.set(currentNode,null);
415         }
416       }
417       treeChart.layoutGraph(true);
418     }
419 
420     /**
421      * Expands children at a given depth.
422      * @param node node where to start
423      * @param depth the given depth
424      */
425     private void expandAtDepth(final Node node, final int depth) {
426       if (depth == 0) {
427         //reached destiny, show collapsed children
428         final NodeList nodes = (NodeList) hiddenNodesChild.get(node);
429         if (nodes != null) {
430           while(!nodes.isEmpty()) {
431             treeChart.getGraph2D().reInsertNode(nodes.popNode());
432           }
433           for (final EdgeList edges = (EdgeList) hiddenEdgesChild.get(node);!edges.isEmpty();) {
434             treeChart.getGraph2D().reInsertEdge(edges.popEdge());
435           }
436           hiddenNodesChild.set(node,null);
437           hiddenEdgesChild.set(node,null);
438           treeChart.updateView();
439         }
440       } else if (depth > 0) {
441         //not at the right depth => visit children
442         for (final EdgeCursor edgeCursor = node.outEdges(); edgeCursor.ok(); edgeCursor.next()) {
443           final Edge edge = edgeCursor.edge();
444           expandAtDepth(edge.target(),depth-1);
445         }
446       }
447     }
448 
449     /**
450      * Checks if expand button should be active.
451      * Button should be active, if it is possible to expand.
452      * @return Is there any parent/child that is collapsed
453      */
454     boolean isActive() {
455       return isExpandable(expandChildren);
456     }
457 
458     String getToolTipText() {
459       if (expandChildren) {
460         return "Displays previously hidden subordinates of the selected employee.";
461       } else {
462         return "Displays previously hidden superiors of the selected employee.";
463       }
464     }
465   }
466 
467   /**
468    * Button for collapsing children or parents.
469    * Children and parents are collapsed layer by layer
470    */
471   private class CollapseButton extends NodeButton {
472     private final boolean collapseChildren;
473 
474     CollapseButton(final String enabled, final String disabled, final int position, final boolean collapseChildren) {
475       super(enabled, disabled, position);
476       yOffset = collapseChildren ? 0 : VERTICAL_CONTROL_OFFSET;
477       this.collapseChildren = collapseChildren;
478     }
479 
480     public Rectangle getBounds() {
481       final Rectangle bounds = super.getBounds();
482       bounds.width *= 0.5;
483       bounds.height *= 0.5;
484       return bounds;
485     }
486 
487     /**
488      * Collapses the next layer.
489      */
490     void action() {
491       final Graph2D graph = treeChart.getGraph2D();
492 
493       if (collapseChildren) {
494         final NodeMap map = Maps.createHashedNodeMap();
495         Trees.getSubTreeDepths(graph, map);
496         hideAtDepth(graph, node, map.getInt(node));
497       } else {
498         //collapse the farthest ancestor
499         Node currentNode = node;
500         Node lastNode = null;
501         //search for the second farthest ancestor
502         while (currentNode.inDegree() > 0) {
503           lastNode = currentNode;
504           currentNode = currentNode.firstInEdge().source();
505         }
506         //if node has an ancestor to collapse
507         if (lastNode != null) {
508           final NodeList hideNodes = new NodeList(currentNode.successors());
509           final EdgeList hideEdges = new EdgeList();
510           //hide ancestor and its subtrees that not contains current node
511           hideNodes.remove(lastNode);
512           hideNodes.addAll(GraphConnectivity.getSuccessors(graph, hideNodes, graph.N()));
513           hideNodes.add(currentNode);
514           for (NodeCursor nc = hideNodes.nodes(); nc.ok(); nc.next()) {
515             final Node n = nc.node();
516             hideEdges.addAll(n.edges());
517             graph.removeNode(n);
518           }
519           hiddenEdgesParent.set(lastNode, hideEdges);
520           hiddenNodesParent.set(lastNode, hideNodes);
521         }
522       }
523       treeChart.layoutGraph(true);
524     }
525 
526     /**
527      * Collapses children at a given depth.
528      * @param root node where to start
529      * @param depth depth where to collapse
530      */
531     private void hideAtDepth( final Graph2D graph, final Node root, final int depth ) {
532       if (depth == 2) {
533         //if depth 2 is reach, children should be collapsed
534         final EdgeList edgesToHide = new EdgeList();
535         final NodeList nodesToHide = new NodeList(root.successors());
536 
537         for (NodeCursor nc = nodesToHide.nodes(); nc.ok(); nc.next()) {
538           final Node child = nc.node();
539           edgesToHide.add(child.firstInEdge());
540           graph.removeNode(child);
541         }
542 
543         final EdgeList oldHiddenEdges = (EdgeList) hiddenEdgesChild.get(root);
544         if (oldHiddenEdges != null) {
545           edgesToHide.splice(oldHiddenEdges);
546         }
547         hiddenEdgesChild.set(root, edgesToHide);
548         final NodeList oldHiddenNodes = (NodeList) hiddenNodesChild.get(root);
549         if (oldHiddenNodes != null) {
550           nodesToHide.splice(oldHiddenNodes);
551         }
552         hiddenNodesChild.set(root, nodesToHide);
553       } else if (depth > 2) {
554         //if depth is greater than 2, visit children
555         for (NodeCursor nc = root.successors(); nc.ok(); nc.next()) {
556           hideAtDepth(graph, nc.node(), depth - 1);
557         }
558       }
559     }
560 
561     /**
562      * Checks if collapse button should be active.
563      * Collapse button should be active, if there are nodes to hide.
564      * @return is in/out degree greater than zero
565      */
566     boolean isActive() {
567       return (collapseChildren ? node.outDegree() > 0 : node.inDegree() > 0);
568     }
569 
570     String getToolTipText() {
571       if (collapseChildren) {
572         return "Hides subordinates of the selected employee.";
573       } else {
574         return "Hides superiors of the selected employee.";
575       }
576     }
577   }
578 
579   /**
580    * Abstract class that provides button functionality.
581    */
582   private abstract class NodeButton extends AbstractMouseInputEditor {
583     private static final int BUTTON_RADIUS = 50;
584 
585     int xOffset;
586     int yOffset;
587     final Icon icon;
588     final Icon iconDisabled;
589 
590     NodeButton(final String enabled, final String disabled, final int position) {
591       this.icon = newIcon(enabled);
592       this.iconDisabled = newIcon(disabled);
593       if (position == -1) {
594         xOffset = (int) ((position-1.6) * BUTTON_RADIUS);
595       } else {
596         xOffset = (position-2) * BUTTON_RADIUS;
597       }
598     }
599 
600     /**
601      * Paints button zoom invariant.
602      * @param g current <code>Graphics2D</code>
603      */
604     public void paint(Graphics2D g) {
605       final Rectangle bounds = getBounds();
606       if (icon != null) {
607         //Make buttons zoom invariant
608         g = (Graphics2D) g.create();
609         g.translate(bounds.x, bounds.y);
610         final double z2 = 1 / treeChart.getZoom();
611         g.scale(z2, z2);
612         if (isActive()) {
613           icon.paintIcon(treeChart.getRootPane(), g, 0, 0);
614         } else {
615           iconDisabled.paintIcon(treeChart.getRootPane(), g, 0, 0);
616         }
617         g.dispose();
618       }
619     }
620 
621     /**
622      * Returns button bounds.
623      * @return button bounds
624      */
625     public Rectangle getBounds() {
626       final Rectangle r = new Rectangle(-1, -1, -1, -1);
627       if (node != null) {
628         final NodeRealizer realizer = treeChart.getGraph2D().getRealizer(node);
629         final double z2 = 1 / treeChart.getZoom();
630         r.x = (int) (realizer.getCenterX() + z2 * (xOffset - BUTTON_RADIUS * 0.5));
631         r.y = (int) (realizer.getY() + realizer.getHeight() + z2 * (10 + yOffset));
632         r.width = (int) (BUTTON_RADIUS * z2);
633         r.height = (int) (BUTTON_RADIUS * z2);
634       }
635       return r;
636     }
637 
638     /**
639      * Gets called when the button was pressed. Must be overwritten by subclasses.
640      */
641     void action() {
642     }
643 
644     /**
645      * Determines if the action of the button is possible for this node.
646      * Subclasses may want to change the behaviour.
647      * @return if node is not root
648      */
649     boolean isActive() {
650       final GenericNodeRealizer gnr = (GenericNodeRealizer) treeChart.getGraph2D().getRealizer(node);
651       final OrgChartTreeModel.Employee employee = (OrgChartTreeModel.Employee) gnr.getUserData();
652       return !employee.isRoot();
653     }
654 
655     /**
656      * Determines whether or not this button's associated <code>node</code>
657      * has collapsed successors or predecessors that may be expanded.
658      * @param successors if <code>true</code>, this method determines if there
659      * are collapsed successors; otherwise this method determines if there
660      * are collapsed predecessors.
661      */
662     boolean isExpandable( final boolean successors ) {
663       //check if node itself has children/parents to expand
664       final NodeMap hiddenNodes = successors
665               ? hiddenNodesChild : hiddenNodesParent;
666       if (hiddenNodes.get(node) != null) {
667         return true;
668       }
669 
670       //check if successors/predecessors have children to expand
671       final Graph2D graph = treeChart.getGraph2D();
672       final NodeList nl = new NodeList(node);
673       final NodeList neighbors = successors
674               ? GraphConnectivity.getSuccessors(graph, nl, graph.N())
675               : GraphConnectivity.getPredecessors(graph, nl, graph.N());
676       for (NodeCursor nc = neighbors.nodes(); nc.ok(); nc.next()) {
677         final Node n = nc.node();
678         if (hiddenNodes.get(n) != null) {
679           return true;
680         }
681       }
682 
683       return false;
684     }
685 
686     public boolean startsEditing(final Mouse2DEvent event) {
687       return contains(event.getX(), event.getY());
688     }
689 
690     public void mouse2DEventHappened(final Mouse2DEvent event) {
691       if (contains(event.getX(), event.getY())) {
692         final String text = "You're over it";
693         treeChart.setToolTipText(text);
694         if (acceptEvent(event) && isActive()) {
695           action();
696         }
697       } else {
698         stopEditing();
699       }
700     }
701 
702     boolean acceptEvent( final Mouse2DEvent event ) {
703       return event.getId() == Mouse2DEvent.MOUSE_CLICKED &&
704              event.getButton() == 1;
705     }
706 
707     boolean contains( final double x, final double y ) {
708       return getBounds().contains(x, y);
709     }
710 
711     String getToolTipText() {
712       return null;
713     }
714   }
715 }
716