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 demo.view.mindmap.StateIconProvider.StateIcon;
31  
32  import y.anim.AnimationObject;
33  import y.anim.AnimationPlayer;
34  import y.base.Node;
35  import y.base.NodeList;
36  import y.geom.Geom;
37  import y.view.AbstractMouseInputEditor;
38  import y.view.Drawable;
39  import y.view.Graph2D;
40  import y.view.Graph2DView;
41  import y.view.Graph2DViewRepaintManager;
42  import y.view.HitInfo;
43  import y.view.Mouse2DEvent;
44  import y.view.MouseInputEditor;
45  import y.view.MouseInputEditorProvider;
46  import y.view.NodeRealizer;
47  import y.view.ViewMode;
48  
49  import java.awt.Color;
50  import java.awt.Graphics2D;
51  import java.awt.Rectangle;
52  import java.awt.geom.GeneralPath;
53  import java.util.Iterator;
54  import java.util.NoSuchElementException;
55  import javax.swing.Icon;
56  
57  /**
58   * Provides inline controls for adding icons, changing colors, adding edges,
59   * adding, child nodes, and deleting nodes.
60   */
61  class HoverButton implements Drawable, MouseInputEditorProvider, AnimationObject {
62    private static final Color PANEL_BACKGROUND = new Color(28, 181, 255, 85);
63    private static final Color GREEN = new Color(21, 186, 7);
64    private static final Color YELLOW = new Color(255, 237, 0);
65  
66  
67    /**
68     * The target node for edit actions.
69     */
70    private Node currentNode;
71  
72    /**
73     * The visible state of the inline editing controls.
74     */
75    private boolean visible;
76  
77    /**
78     * true, if menu to choose color or icon is open.
79     */
80    private boolean showsChooser;
81  
82    /**
83     * offset to open the hover button at the mouse position.
84     */
85    private int mouseX;
86  
87    /**
88     * vertical offset during the animation
89     */
90    private double animationYOffset;
91    private final AnimationPlayer player;
92  
93    private final Graph2DView view;
94  
95    private final DeleteButton delete;
96    private final ColorButton color;
97    private final IconButton icon;
98    private final AddButton add;
99    private final CrossReferenceButton reference;
100 
101   /**
102    * Initializes a new <code>HoverButton</code> instance for the specified view.
103    */
104   HoverButton( final Graph2DView view ) {
105     this.view = view;
106     delete = new DeleteButton();
107     color = new ColorButton(this);
108     icon = new IconButton(this);
109     add = new AddButton();
110     reference = new CrossReferenceButton();
111     visible = false;
112     player = new AnimationPlayer();
113     Graph2DViewRepaintManager manager = new Graph2DViewRepaintManager(view);
114     manager.add(this);
115     player.addAnimationListener(manager);
116     view.addViewMode(new HoverViewMode());
117   }
118 
119   /**
120    * Returns the control implementations for the current node.
121    * @return the control implementations for the current node.
122    */
123   private Iterator activeButtons() {
124     return new ControlsIterator(ViewModel.instance.isRoot(currentNode));
125   }
126 
127   /**
128    * Specifies the target node for edit actions.
129    * @param node the target node for edit actions.
130    */
131   public void setNode( final Node node, final double mouseOffset ) {
132     //prevent hover button from appearing at other items when choosing new icon/color
133     if (!color.isVisible() && !icon.isVisible()) {
134       //mouse moved away from item
135       if (node == null) {
136         hideButtons();
137         currentNode = null;
138         view.updateView();
139       } else if (currentNode != node) {
140         if (!visible) {
141           view.addDrawable(this);
142           visible = true;
143           final double zoom = 1 / view.getZoom();
144           mouseX = (int) (mouseOffset - view.getGraph2D().getRealizer(node).getX() - (80*zoom));
145           if (ViewModel.instance.isRoot(node)) {
146             mouseX = 0;
147           }
148         }
149         for (Iterator it = new ControlsIterator(false); it.hasNext();) {
150           ((InlineControl) it.next()).setNode(node);
151         }
152         currentNode = node;
153         player.animate(this);
154       }
155     }
156   }
157 
158   /**
159    * Removes the editing controls from the associated view.
160    */
161   private void hideButtons() {
162     player.stop();
163     view.removeDrawable(this);
164     visible = false;
165   }
166 
167   /**
168    * Paints the editing controls.
169    * This method delegates painting to each specific editing control.
170    * @param g Graphics2D to paint on
171    */
172   public void paint(final Graphics2D g) {
173     if (isValid()) {
174       Color oldColor = g.getColor();
175       g.setColor(PANEL_BACKGROUND);
176       g.fill(getBounds());
177       g.setColor(oldColor);
178 
179       for (Iterator it = activeButtons(); it.hasNext();) {
180         ((InlineControl) it.next()).paint(g);
181       }
182     }
183   }
184 
185   /**
186    * Returns the bounds of the editing controls.
187    * @return the bounds of the editing controls.
188    */
189   public Rectangle getBounds() {
190     Rectangle r = new Rectangle(0,0,-1,-1);
191     if (isValid()) {
192       for (final Iterator it = activeButtons(); it.hasNext(); ) {
193         Geom.calcUnion(r, ((InlineControl) it.next()).getBounds(), r);
194       }
195     }
196     return r;
197   }
198 
199   /**
200    * Determines if the current target node for edit actions is a valid node. 
201    * @return <code>true</code> if edit action may be executed;
202    * <code>false</code> otherwise.
203    */
204   private boolean isValid() {
205     //graph may changed while button visible, e.g. when loading new graph
206     if (currentNode != null && currentNode.getGraph() != null) {
207       return true;
208     }
209     currentNode = null;
210     return false;
211   }
212 
213   /**
214    * Returns an editor for the edit action that is appropriate for the specified
215    * mouse position.
216    * @param view the view that will host the editor
217    * @param x the x coordinate of the mouse event
218    * @param y the y coordinate of the mouse event
219    * @param hitInfo the HitInfo that may be used to determine what instance to return or <code>null</code>
220    * @return an editor that is appropriate for the specified mouse position
221    * or <code>null</code> if the specified mouse position cannot trigger
222    * any edit action.
223    */
224   public MouseInputEditor findMouseInputEditor(Graph2DView view, double x, double y, HitInfo hitInfo) {
225     if (isValid()) {
226       for (Iterator it = activeButtons(); it.hasNext();) {
227         final InlineControl button = (InlineControl) it.next();
228         if (button.getBounds().contains(x, y)) {
229           return button;
230         }
231       }
232     }
233     return null;
234   }
235 
236   /**
237    * Closes secondary editing controls for choosing colors or icons.
238    */
239   public void closeAll() {
240     if (color.isVisible()) {
241       color.toggleVisibility();
242     }
243     if (icon.isVisible()) {
244       icon.toggleVisibility();
245     }
246   }
247 
248   /**
249    * Initializes the start position of the editing controls for the controls'
250    * animated fade-in effect.
251    */
252   public void initAnimation() {
253     animationYOffset = 10;
254   }
255 
256   /**
257    * Calculates the appropriate position of the editing controls for the
258    * controls' animated fade-in effect.
259    */
260   public void calcFrame(final double time) {
261     animationYOffset = (1 - time) * 10;
262   }
263 
264   /**
265    * Ensures the correct final position of the editing controls after the
266    * controls' animated fade-in effect.
267    */
268   public void disposeAnimation() {
269     animationYOffset = 0;
270   }
271 
272   /**
273    * Returns the preferred duration  of the editing controls after the
274    * controls' animated fade-in effect in milliseconds.
275    * @return the preferred duration  of the editing controls after the
276    * controls' animated fade-in effect in milliseconds.
277    */
278   public long preferredDuration() {
279     return 150;
280   }
281 
282   String getToolTipText( final double x, final double y ) {
283     if (isValid()) {
284       for (Iterator it = activeButtons(); it.hasNext();) {
285         final InlineControl button = (InlineControl) it.next();
286         if (button.getBounds().contains(x, y)) {
287           return button.getToolTipText();
288         }
289       }
290     }
291     return null;
292   }
293 
294 
295   /**
296    * Initiates interactive creation of a cross-reference edge for existing
297    * nodes.
298    */
299   private class CrossReferenceButton extends InlineControl {
300     CrossReferenceButton() {
301       super(90);
302     }
303 
304     /**
305      * Returns <em>Add a cross reference.</em>
306      * @return <em>Add a cross reference.</em>
307      */
308     String getToolTipText() {
309       return "Add a cross reference.";
310     }
311 
312     /**
313      * Initiates interactive creation of a cross-reference edge for existing
314      * nodes.
315      */
316     void action() {
317       for(final Iterator viewModes = view.getViewModes();viewModes.hasNext();) {
318         final ViewMode viewMode = (ViewMode) viewModes.next();
319         if (viewMode instanceof MoveNodeMode) {
320           MoveNodeMode moveMode = (MoveNodeMode) viewMode;
321           moveMode.startCrossEdgeCreation(node);
322           view.updateView();
323         }
324       }
325     }
326 
327     /**
328      * Paints a blue cross-reference symbol.
329      */
330     void paint(Graphics2D g) {
331       g = newZoomInvariant(g);
332 
333       g.scale(1.5, 1.5);
334 
335       g.setColor(MindMapUtil.CROSS_EDGE_COLOR);
336       g.fillOval(0, 0, 16, 16);
337 
338       g.setColor(Color.WHITE);
339       GeneralPath gp = new GeneralPath();
340       gp.moveTo(8, 3);
341       gp.lineTo(12, 8);
342       gp.lineTo(10, 8);
343       gp.lineTo(10, 12);
344       gp.lineTo(6, 12);
345       gp.lineTo(6, 8);
346       gp.lineTo(4, 8);
347       g.fill(gp);
348 
349       g.dispose();
350     }
351   }
352 
353   /**
354    * Adds a new child node to the current target node.
355    */
356   private class AddButton extends InlineControl {
357     AddButton() {
358       super(120);
359     }
360 
361     /**
362      * Returns <em>Add a new child item.</em>
363      * @return <em>Add a new child item.</em>
364      */
365     String getToolTipText() {
366       return "Add a new child item.";
367     }
368 
369     /**
370      * Adds a new child node.
371      */
372     void action() {
373       MindMapUtil.addNode(view, node);
374     }
375 
376     /**
377      * Paints a add symbol in a green circle.
378      */
379     void paint(Graphics2D g) {
380       g = newZoomInvariant(g);
381 
382       g.scale(1.5,1.5);
383       g.setColor(GREEN);
384       g.fillOval(0, 0, 16, 16);
385       g.setColor(Color.WHITE);
386       g.fillRect(7, 4, 2, 8);
387       g.fillRect(4, 7, 8, 2);
388 
389       g.dispose();
390     }
391   }
392 
393   /**
394    * Removes the current target node and all its tree successors.
395    */
396   private class DeleteButton extends InlineControl {
397     DeleteButton() {
398       super(150);
399     }
400 
401     /**
402      * Returns <em>Remove this item and all of its children.</em>
403      * @return <em>Remove this item and all of its children.</em>
404      */
405     String getToolTipText() {
406       return "Remove this item and all of its children.";
407     }
408 
409     /**
410      * Removes the current target node and all its tree successors.
411      */
412     void action() {
413       hideButtons();
414 
415       final Graph2D graph2D = view.getGraph2D();
416       graph2D.firePreEvent();
417       MindMapUtil.removeSubtree(graph2D, node);
418       LayoutUtil.layout(graph2D);
419       graph2D.firePostEvent();
420     }
421 
422     /**
423      * Paints a remove symbol in a red circle.
424      */
425     void paint( Graphics2D g ) {
426       g = newZoomInvariant(g);
427 
428       g.scale(1.5, 1.5);
429       g.setColor(Color.RED);
430       g.fillOval(0, 0, 16, 16);
431       g.setColor(Color.WHITE);
432       g.fillRect(4, 7, 8, 2);
433 
434       g.dispose();
435     }
436   }
437 
438   /**
439    * Displays an inline icon chooser.
440    */
441   private class IconButton extends InlineControl {
442     private final Icon icon;
443     private final IconChooser iconChooser;
444     private final HoverButton parent;
445     private boolean visible;
446 
447     IconButton( final HoverButton parent ) {
448       super(30);
449       this.icon = MindMapUtil.getIcon("smiley-happy-24.png");
450       this.parent = parent;
451       iconChooser = new IconChooser(this);
452       visible = false;
453     }
454 
455     /**
456      * Returns <em>Choose a state icon.</em>
457      * @return <em>Choose a state icon.</em>
458      */
459     String getToolTipText() {
460       return "Choose a state icon.";
461     }
462 
463     /**
464      * Paints a smiley symbol.
465      */
466     void paint( Graphics2D g ) {
467       if (icon != null) {
468         g = newZoomInvariant(g);
469 
470         icon.paintIcon(view.getRootPane(), g, 0, 0);
471 
472         g.dispose();
473       }
474     }
475 
476     /**
477      * Sets the target node for this control's edit action.
478      * @param node the node being edited.
479      */
480     void setNode( final Node node ) {
481       super.setNode(node);
482       iconChooser.setNode(node);
483     }
484 
485     /**
486      * Toggles visibility of the inline icon chooser.
487      */
488     void action() {
489       toggleVisibility();
490     }
491 
492     /**
493      * Toggles visibility of the inline icon chooser.
494      */
495     void toggleVisibility() {
496       if (visible) {
497         view.removeDrawable(iconChooser);
498       } else if (!parent.showsChooser) {
499         view.addDrawable(iconChooser);
500       } else {
501         return;
502       }
503       parent.showsChooser = !parent.showsChooser;
504       visible = !visible;
505       view.updateView();
506     }
507 
508     /**
509      * Returns <code>true</code> if the inline icon chooser is currently visible
510      * and <code>false</code> otherwise.
511      * @return <code>true</code> if the inline icon chooser is currently visible
512      * and <code>false</code> otherwise.
513      */
514     boolean isVisible() {
515       return visible;
516     }
517   }
518 
519   /**
520    * Lets a user choose one of several different icons.
521    */
522   private class IconChooser extends InlineControl implements Drawable, MouseInputEditorProvider {
523     private static final int V_OFFSET = 2;
524     private static final int H_OFFSET = 2;
525 
526     private final IconButton parent;
527     private final StateIcon[] icons;
528 
529     IconChooser( final IconButton parent ) {
530       super(36, 0);
531       this.parent = parent;
532       final StateIconProvider provider = StateIconProvider.instance;
533       icons = new StateIcon[] {
534               StateIconProvider.NULL_ICON,
535               provider.getIcon("smiley-happy"),
536               provider.getIcon("smiley-not-amused"),
537               provider.getIcon("smiley-grumpy"),
538               provider.getIcon("abstract-green"),
539               provider.getIcon("abstract-red"),
540               provider.getIcon("abstract-blue"),
541               provider.getIcon("questionmark"),
542               provider.getIcon("exclamationmark"),
543               provider.getIcon("delete"),
544               provider.getIcon("checkmark"),
545               provider.getIcon("star"),
546       };
547 
548       int maxW = 0;
549       int maxH = 0;
550       int w = 0;
551       int h = 0;
552       final int l = icons.length;
553       for (int i = 0; i < l; ++i) {
554         final Icon icon = icons[i];
555         w += icon.getIconWidth();
556         final int ih = icon.getIconHeight();
557         if (maxH < ih) {
558           maxH = ih;
559         }
560 
561         if (i == (l - 1)/2) {
562           maxW = w;
563           w = 0;
564           h = maxH;
565           maxH = 0;
566         }
567       }
568       w = Math.max(w, maxW) + (l > 0 ? ((l - 1) / 2) * H_OFFSET : 0);
569       h += maxH + V_OFFSET;
570       this.yOffset = -h - parent.height;
571       this.width = w;
572       this.height = h;
573     }
574 
575     /**
576      * Returns <code>null</code>.
577      * @return <code>null</code>.
578      */
579     String getToolTipText() {
580       return null;
581     }
582 
583     /**
584      * Unsupported.
585      */
586     void action() {
587       throw new UnsupportedOperationException();
588     }
589 
590     public Rectangle getBounds() {
591       return super.getBounds();
592     }
593 
594     /**
595      * Paints the available icons in this icon chooser.
596      */
597     public void paint( Graphics2D g ) {
598       if (parent.isVisible()) {
599         g = newZoomInvariant(g);
600 
601         int x = 0;
602         int y = 0;
603         int maxH = 0;
604         final Graph2DView view = HoverButton.this.view;
605         for (int i = 0, l = icons.length; i < l; ++i) {
606           final Icon icon = icons[i];
607           icon.paintIcon(view, g, x, y);
608 
609           x += icon.getIconWidth() + H_OFFSET;
610           final int h = icon.getIconHeight();
611           if (maxH < h) {
612             maxH = h;
613           }
614 
615           if (i == (l - 1)/2) {
616             x = 0;
617             y = maxH + V_OFFSET;
618           }
619         }
620         g.dispose();
621       }
622     }
623 
624     /**
625      * Returns an editor for interactively choosing one of this icon chooser's
626      * icons.
627      * @param view the view that will host the editor
628      * @param x the x coordinate of the mouse event
629      * @param y the y coordinate of the mouse event
630      * @param hitInfo the HitInfo that may be used to determine what instance to return or <code>null</code>
631      * @return an editor for interactively choosing one of this icon chooser's
632      * icons.
633      */
634     public MouseInputEditor findMouseInputEditor(
635             final Graph2DView view, final double x, final double y, final HitInfo hitInfo
636     ) {
637       return getBounds().contains(x, y) ? this : null;
638     }
639 
640     /**
641      * Chooses the icon at the specified relative location in this icon chooser.
642      * @param relativeX the x-coordinate of the triggering mouse event relative
643      * to this icon chooser's current location.
644      * @param relativeY the y-coordinate of the triggering mouse event relative
645      * to this icon chooser's current location.
646      */
647     void action( final double relativeX, final double relativeY ) {
648       final StateIcon icon = findIcon(relativeX, relativeY);
649 
650       final Node node = this.node;
651       final Graph2D graph = view.getGraph2D();
652       graph.firePreEvent();
653       graph.backupRealizers();
654       MindMapNodePainter.setStateIcon(graph.getRealizer(node), icon);
655       MindMapUtil.updateWidth(graph, node);
656       LayoutUtil.layout(graph);
657       graph.firePostEvent();
658 
659       closeAll();
660       view.updateView();
661     }
662 
663     /**
664      * Determines the icon at the specified relative location in this icon
665      * chooser.
666      * @param relativeX the x-coordinate of the triggering mouse event relative
667      * to this icon chooser's current location.
668      * @param relativeY the y-coordinate of the triggering mouse event relative
669      * to this icon chooser's current location.
670      * @return the icon at the specified relative location in this icon
671      * chooser.
672      */
673     private StateIcon findIcon( final double relativeX, final double relativeY ) {
674       final double z = 1 / view.getZoom();
675       double x = 0;
676       double y = 0;
677       int maxH = 0;
678       for (int i = 0, l = icons.length; i < l; ++i) {
679         final StateIcon icon = icons[i];
680         final int w = icon.getIconWidth();
681         final int h = icon.getIconHeight();
682         if (x <= relativeX && relativeX <= x + w * z &&
683             y <= relativeY && relativeY <= y + h * z) {
684           return icon;
685         }
686 
687         if (maxH < h) {
688           maxH = h;
689         }
690 
691         if (i == (l - 1)/2) {
692           x = 0;
693           y = (maxH + V_OFFSET) * z;
694         } else {
695           x += (w + H_OFFSET) * z;
696         }
697       }
698       return StateIconProvider.NULL_ICON;
699     }
700   }
701 
702   /**
703    * Displays an inline color chooser.
704    */
705   private class ColorButton extends InlineControl {
706     private final ColorChooser colorChooser;
707     private boolean visible;
708     private HoverButton parent;
709 
710     ColorButton( final HoverButton parent ) {
711       super(60);
712       this.parent = parent;
713       this.colorChooser = new ColorChooser(this);
714       this.visible = false;
715     }
716 
717     /**
718      * Returns <em>Choose a color.</em>
719      * @return <em>Choose a color.</em>
720      */
721     String getToolTipText() {
722       return "Choose a color.";
723     }
724 
725     /**
726      * Paints a multi-colored circle.
727      */
728     void paint(Graphics2D g) {
729       g = newZoomInvariant(g);
730 
731       g.scale(1.5, 1.5);
732       g.setColor(Color.RED);
733       g.fillArc(0, 0, 16, 16, 180, -90);
734       g.setColor(GREEN);
735       g.fillArc(0, 0, 16, 16, 270, -90);
736       g.setColor(Color.BLUE);
737       g.fillArc(0, 0, 16, 16, 0, -90);
738       g.setColor(YELLOW);
739       g.fillArc(0,0,16,16,90,-90);
740 
741       g.dispose();
742     }
743 
744     /**
745      * Sets the target node for this control's edit action.
746      * @param node the node being edited.
747      */
748     void setNode(final Node node) {
749       super.setNode(node);
750       colorChooser.setNode(node);
751     }
752 
753     /**
754      * Toggles visibility of the inline color chooser.
755      */
756     void action() {
757       toggleVisibility();
758     }
759 
760     /**
761      * Toggles visibility of the inline color chooser.
762      */
763     void toggleVisibility() {
764       if (visible) {
765         view.getGraph2D().removeDrawable(colorChooser);
766       } else if (!parent.showsChooser) {
767         view.getGraph2D().addDrawable(colorChooser);
768       } else {
769         return;
770       }
771       parent.showsChooser = !parent.showsChooser;
772       visible = !visible;
773       view.updateView();
774     }
775 
776     /**
777      * Returns <code>true</code> if the inline color chooser is currently
778      * visible and <code>false</code> otherwise.
779      * @return <code>true</code> if the inline color chooser is currently
780      * visible and <code>false</code> otherwise.
781      */
782     boolean isVisible() {
783       return visible;
784     }
785   }
786 
787   /**
788    * Lets a user choose one of several different colors.
789    */
790   private class ColorChooser extends InlineControl implements Drawable, MouseInputEditorProvider {
791     private static final int WIDTH = 60;
792     private static final int HEIGHT = 15;
793     private static final int V_OFFSET = 2;
794 
795     private final ColorButton parent;
796     private final Color[] colors;
797 
798     ColorChooser( final ColorButton parent ) {
799       super(36, 0);
800       this.parent = parent;
801       colors = new Color[] {
802               MindMapUtil.ORANGE,
803               MindMapUtil.RED,
804               MindMapUtil.MAGENTA,
805               MindMapUtil.GREEN,
806               MindMapUtil.DARK_GREEN,
807               MindMapUtil.LIGHT_BLUE,
808               MindMapUtil.BLUE,
809               MindMapUtil.BROWN,
810               MindMapUtil.BLACK,
811       };
812       final int l = colors.length;
813       final int w = WIDTH;
814       final int h = l > 0 ? l * HEIGHT + (l - 1) * V_OFFSET : 0;
815       this.yOffset = -h - parent.height;
816       this.width = w;
817       this.height = h;
818     }
819 
820     /**
821      * Returns <code>null</code>.
822      * @return <code>null</code>.
823      */
824     String getToolTipText() {
825       return null;
826     }
827 
828     /**
829      * Unsupported.
830      */
831     void action() {
832       throw new UnsupportedOperationException();
833     }
834 
835     public Rectangle getBounds() {
836       return super.getBounds();
837     }
838 
839     /**
840      * Paints the available colors in this color chooser.
841      */
842     public void paint( Graphics2D g ) {
843       if (parent.isVisible()) {
844         g = newZoomInvariant(g);
845 
846         int y = 0;
847         for (int i = 0, l = colors.length; i < l; ++i) {
848           g.setColor(colors[i]);
849           g.fillRect(0, y, WIDTH, HEIGHT);
850           y += HEIGHT + V_OFFSET;
851         }
852 
853         g.dispose();
854       }
855     }
856 
857     /**
858      * Returns an editor for interactively choosing one of this color chooser's
859      * colors.
860      * @param view the view that will host the editor
861      * @param x the x coordinate of the mouse event
862      * @param y the y coordinate of the mouse event
863      * @param hitInfo the HitInfo that may be used to determine what instance to return or <code>null</code>
864      * @return an editor for interactively choosing one of this color chooser's
865      * colors.
866      */
867     public MouseInputEditor findMouseInputEditor(
868             final Graph2DView view, final double x, final double y, final HitInfo hitInfo
869     ) {
870       return getBounds().contains(x, y) ? this : null;
871     }
872 
873     /**
874      * Chooses the color at the specified relative location in this color
875      * chooser.
876      * @param relativeX the x-coordinate of the triggering mouse event relative
877      * to this color chooser's current location.
878      * @param relativeY the y-coordinate of the triggering mouse event relative
879      * to this color chooser's current location.
880      */
881     void action( final double relativeX, final double relativeY ) {
882       final Color color = findColor(relativeY);
883       if (color != null) {
884         final Graph2D graph = view.getGraph2D();
885         final Node node = this.node;
886         graph.backupRealizers(new NodeList(node).nodes());
887         final NodeRealizer nr = graph.getRealizer(node);
888         nr.setFillColor(color);
889       }
890 
891       closeAll();
892       view.updateView();
893     }
894 
895     /**
896      * Determines the color at the specified relative location in this color
897      * chooser.
898      * @param relativeY the y-coordinate of the triggering mouse event relative
899      * to this color chooser's current location.
900      * @return the color at the specified relative location in this color
901      * chooser or <code>null</code> if there is no corresponding color. 
902      */
903     private Color findColor( final double relativeY ) {
904       final double z = 1 / view.getZoom();
905       double y = 0;
906       for (int i = 0, l = colors.length; i < l; ++i) {
907         if (y <= relativeY && relativeY <= y + HEIGHT * z) {
908           return colors[i];
909         }
910         y += (HEIGHT + V_OFFSET) * z;
911       }
912       return null;
913     }
914   }
915 
916   /**
917    * Abstract base class for inline editing controls.
918    */
919   private abstract class InlineControl extends AbstractMouseInputEditor {
920     Node node;
921 
922     final int xOffset;
923     int yOffset;
924     int width;
925     int height;
926 
927     InlineControl( final int xOffset ) {
928       this(xOffset, -22);
929     }
930 
931     InlineControl( final int xOffset, final int yOffset ) {
932       this.xOffset = xOffset;
933       this.yOffset = yOffset;
934       this.width = 24;
935       this.height = 24;
936     }
937 
938     /**
939      * Returns a short description for this control.
940      * @return a short description for this control.
941      */
942     abstract String getToolTipText();
943 
944     /**
945      * Paints the control.
946      */
947     abstract void paint( Graphics2D g );
948 
949     /**
950      * Edits the control's current target node in response to mouse clicks.
951      */
952     abstract void action();
953 
954     /**
955      * Edits the control's current target node in response to mouse clicks.
956      * The default implementation calls {@link #action()}.
957      * @param relativeX the x-coordinate of the mouse position relative to the
958      * control's location.
959      * @param relativeY the x-coordinate of the mouse position relative to the
960      * control's location.
961      */
962     void action( final double relativeX, final double relativeY ) {
963       action();
964     }
965 
966     /**
967      * Creates a {@link Graphics2D} instance configured for zoom-invariant
968      * painting. {@link Graphics2D} instances created by this method have to be
969      * {@link java.awt.Graphics2D#dispose() disposed} after use.
970      * @param g the original graphics context. 
971      * @return a {@link Graphics2D} instance configured for zoom-invariant
972      * painting.
973      */
974     Graphics2D newZoomInvariant( final Graphics2D g ) {
975       final Graphics2D gfx = (Graphics2D) g.create();
976 
977       final Rectangle bnds = getBounds();
978       gfx.translate(bnds.x, bnds.y);
979 
980       final double invZoom = 1 / view.getZoom();
981       gfx.scale(invZoom, invZoom);
982 
983       return gfx;
984     }
985 
986     /**
987      * Specifies the current target node for the control's edit action.
988      * @param node the node to be edited.
989      */
990     void setNode(final Node node) {
991       this.node = node;
992     }
993 
994     /**
995      * Returns the zoom-invariant bounds of this control in the graph (world)
996      * coordinate space.
997      * @return the zoom-invariant bounds of this control in the graph (world)
998      * coordinate space.
999      */
1000    Rectangle getBounds() {
1001      final Rectangle r = new Rectangle(width, height);
1002      final NodeRealizer realizer = view.getGraph2D().getRealizer(node);
1003      final double z2 = 1 / view.getZoom();
1004      r.x = (int) (realizer.getX() + (xOffset * z2 + mouseX) );
1005      r.width *= z2;
1006      r.height *= z2;
1007      r.y = (int) (realizer.getY() + (yOffset * z2) + animationYOffset);
1008      return r;
1009    }
1010
1011    /**
1012     * Determines if this control should be activated for the given event.
1013     * @param event the event that happened
1014     * @return <code>true</code> if the event position lies inside the bounds
1015     * of this control and <code>false</code> otherwise.
1016     */
1017    public boolean startsEditing( final Mouse2DEvent event ) {
1018      return getBounds().contains(event.getX(), event.getY());
1019    }
1020
1021    /**
1022     * Handles mouse events while this control is active.
1023     * The default implementation calls {@link #action(double, double)} for
1024     * mouse clicks.
1025     * @param event the event that happened
1026     */
1027    public void mouse2DEventHappened( final Mouse2DEvent event ) {
1028      final double evtX = event.getX();
1029      final double evtY = event.getY();
1030      final Rectangle bnds = getBounds();
1031      if (bnds.contains(evtX, evtY)) {
1032        if (event.getId() == Mouse2DEvent.MOUSE_CLICKED) {
1033          action(evtX - bnds.x, evtY- bnds.y);
1034        }
1035      } else {
1036        stopEditing();
1037      }
1038    }
1039  }
1040
1041  /**
1042   * ViewMode to handle HoverButton related Mouse Actions.
1043   * Register a mouse click on free plane and updates the position of the
1044   * hover button when mouse is moved
1045   */
1046  class HoverViewMode extends ViewMode {
1047    public void mouseClicked( final double x, final double y ) {
1048      final HitInfo hitInfo = getHitInfo(x, y);
1049      if (!hitInfo.hasHits()) {
1050        setNode(null, 0);
1051        closeAll();
1052      }
1053      super.mouseClicked(x, y);
1054    }
1055
1056    public void mouseMoved( final double x, final double y ) {
1057      view.setToolTipText(null);
1058
1059      final HoverButton controls = HoverButton.this;
1060      final Node lastNode = controls.currentNode;
1061      if (lastNode != null) {
1062        if (controls.getBounds().contains(x, y)) {
1063          view.setToolTipText(controls.getToolTipText(x, y));
1064          return;
1065        }
1066        if (contains(getGraph2D().getRealizer(lastNode), x, y)) {
1067          return;
1068        }
1069      }
1070
1071      final HitInfo hitInfo = getHitInfo(x, y);
1072      if (hitInfo.hasHitNodes()) {
1073        setNode(hitInfo.getHitNode(), x);
1074      } else if (lastNode != null) {
1075        setNode(null, 0);
1076      }
1077    }
1078
1079    private boolean contains( final NodeRealizer nr, final double x, final double y ) {
1080      return nr.getX() <= x && x <= nr.getX() + nr.getWidth() &&
1081             nr.getY() <= y && y <= nr.getY() + nr.getHeight();
1082    }
1083  }
1084
1085  /**
1086   * Iterates over the inline controls provided by the enclosing
1087   * {@link HoverButton} instance.
1088   */
1089  private final class ControlsIterator implements Iterator {
1090    private int index;
1091
1092    ControlsIterator( final boolean root ) {
1093      index = root ? 3 : 0;
1094    }
1095
1096    public void remove() {
1097      throw new UnsupportedOperationException();
1098    }
1099
1100    public boolean hasNext() {
1101      return index < 5;
1102    }
1103
1104    public Object next() {
1105      if (hasNext()) {
1106        switch (index++) {
1107          case 0:
1108            return delete;
1109          case 1:
1110            return color;
1111          case 2:
1112            return icon;
1113          case 3:
1114            return add;
1115          case 4:
1116            return reference;
1117          default:
1118            throw new IllegalStateException();
1119        }
1120      } else {
1121        throw new NoSuchElementException();
1122      }
1123    }
1124  }
1125}
1126