Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package javafx.scene.control;
027
028
029import com.sun.javafx.css.StyleManager;
030import javafx.css.StyleableBooleanProperty;
031import javafx.css.StyleableDoubleProperty;
032import javafx.css.StyleableObjectProperty;
033import javafx.css.StyleableStringProperty;
034import com.sun.javafx.css.converters.BooleanConverter;
035import com.sun.javafx.css.converters.EnumConverter;
036import com.sun.javafx.css.converters.SizeConverter;
037import com.sun.javafx.css.converters.StringConverter;
038import com.sun.javafx.scene.control.skin.TooltipSkin;
039import java.net.MalformedURLException;
040import java.net.URL;
041import java.util.ArrayList;
042import java.util.Collections;
043import java.util.List;
044
045import javafx.animation.KeyFrame;
046import javafx.animation.Timeline;
047import javafx.beans.property.*;
048import javafx.css.CssMetaData;
049import javafx.css.FontCssMetaData;
050import javafx.css.Styleable;
051import javafx.css.StyleableProperty;
052import javafx.event.ActionEvent;
053import javafx.event.EventHandler;
054import javafx.geometry.NodeOrientation;
055import javafx.scene.Node;
056import javafx.scene.Parent;
057import javafx.scene.Scene;
058import javafx.scene.image.Image;
059import javafx.scene.image.ImageView;
060import javafx.scene.input.MouseEvent;
061import javafx.scene.text.Font;
062import javafx.scene.text.TextAlignment;
063import javafx.stage.Window;
064import javafx.util.Duration;
065
066
067/**
068 * Tooltips are common UI elements which are typically used for showing
069 * additional information about a Node in the scenegraph when the Node is 
070 * hovered over by the mouse. Any Node can show a tooltip. In most cases a 
071 * Tooltip is created and its {@link #textProperty() text} property is modified
072 * to show plain text to the user. However, a Tooltip is able to show within it
073 * an arbitrary scenegraph of nodes - this is done by creating the scenegraph
074 * and setting it inside the Tooltip {@link #graphicProperty() graphic} 
075 * property. 
076 * 
077 * <p>You use the following approach to set a Tooltip on any node:
078 *
079 * <pre>
080 * Rectangle rect = new Rectangle(0, 0, 100, 100);
081 * Tooltip t = new Tooltip("A Square");
082 * Tooltip.install(rect, t);
083 * </pre>
084 *
085 * This tooltip will then participate with the typical tooltip semantics (i.e.
086 * appearing on hover, etc). Note that the Tooltip does not have to be 
087 * uninstalled: it will be garbage collected when it is not referenced by any
088 * Node. It is possible to manually uninstall the tooltip, however.
089 *
090 * <p>A single tooltip can be installed on multiple target nodes or multiple
091 * controls.
092 * 
093 * <p>Because most Tooltips are shown on UI controls, there is special API
094 * for all controls to make installing a Tooltip less verbose. The example below 
095 * shows how to create a tooltip for a Button control:
096 *
097 * <pre>
098 * import javafx.scene.control.Tooltip;
099 * import javafx.scene.control.Button;
100 *
101 * Button button = new Button("Hover Over Me");
102 * button.setTooltip(new Tooltip("Tooltip for Button"));
103 * </pre>
104 */
105public class Tooltip extends PopupControl {
106//    private static TooltipBehavior BEHAVIOR = new TooltipBehavior(
107//        new Duration(1000), new Duration(5000), new Duration(600), true);
108    private static String TOOLTIP_PROP_KEY = "javafx.scene.control.Tooltip";
109    private static TooltipBehavior BEHAVIOR = new TooltipBehavior(
110        new Duration(1000), new Duration(5000), new Duration(200), false);
111
112    /**
113     * Associates the given {@link Tooltip} with the given {@link Node}. The tooltip
114     * can then behave similar to when it is set on any {@link Control}. A single
115     * tooltip can be associated with multiple nodes.
116     * @see Tooltip
117     */
118    public static void install(Node node, Tooltip t) {
119        BEHAVIOR.install(node, t);
120    }
121
122    /**
123     * Removes the association of the given {@link Tooltip} on the specified
124     * {@link Node}. Hence hovering on the node will no longer result in showing of the
125     * tooltip.
126     * @see Tooltip
127     */
128    public static void uninstall(Node node, Tooltip t) {
129        BEHAVIOR.uninstall(node);
130    }
131    
132    /***************************************************************************
133     *                                                                         *
134     * Constructors                                                            *
135     *                                                                         *
136     **************************************************************************/
137    
138    /**
139     * Creates a tooltip with an empty string for its text.
140     */
141    public Tooltip() {
142        super();
143        this.bridge = new CSSBridge();
144        initialize();
145    }
146
147    /**
148     * Creates a tooltip with the specified text.
149     *
150     * @param text A text string for the tooltip.
151     */
152    public Tooltip(String text) {
153        bridge = new CSSBridge();
154        setText(text);
155        initialize();
156    }
157
158    private void initialize() {
159        
160        // undo PopupControl's bridge and replace it with Tooltip's
161        if (bridge != null) {
162            getContent().clear();
163            bridge.idProperty().unbind();
164            bridge.styleProperty().unbind();
165            
166            // Bind up these two properties. Note that the third, styleClass, is
167            // handled in the onChange listener for that list.
168            bridge.idProperty().bind(idProperty());
169            bridge.styleProperty().bind(styleProperty());
170        }
171
172        getContent().add(bridge);
173        
174        getStyleClass().setAll("tooltip");
175    }
176
177    /***************************************************************************
178     *                                                                         *
179     * Properties                                                              *
180     *                                                                         *
181     **************************************************************************/
182    /**
183     * The text to display in the tooltip. If the text is set to null, an empty
184     * string will be displayed, despite the value being null.
185     */
186    private final StringProperty text = new SimpleStringProperty(this, "text", "");
187    public final StringProperty textProperty() { return text; }
188    public final void setText(String value) {
189        if (isShowing() && value != null && !value.equals(getText())) {
190            //Dynamic tooltip content is location-dependant.
191            //Chromium trick.
192            setX(BEHAVIOR.lastMouseX);
193            setY(BEHAVIOR.lastMouseY);
194        }
195        textProperty().setValue(value);
196    }
197    public final String getText() { return text == null ? "" : text.getValue(); }
198
199    public final void setTextAlignment(TextAlignment value) { textAlignmentProperty().setValue(value); }
200    public final TextAlignment getTextAlignment() { 
201        return ((Tooltip.CSSBridge)bridge).textAlignment == null 
202                ? TextAlignment.LEFT 
203                : ((Tooltip.CSSBridge)bridge).textAlignment.getValue(); 
204    }
205    /**
206     * Specifies the behavior for lines of text <em>when text is multiline</em>.
207     * Unlike {@link #contentDisplayProperty() contentDisplay} which affects the 
208     * graphic and text, this setting only affects multiple lines of text 
209     * relative to the text bounds.
210     */
211    public final ObjectProperty<TextAlignment> textAlignmentProperty() {
212        return ((Tooltip.CSSBridge)bridge).textAlignmentProperty();
213    }
214    
215    public final void setTextOverrun(OverrunStyle value) { textOverrunProperty().setValue(value); }
216    public final OverrunStyle getTextOverrun() { 
217        return ((Tooltip.CSSBridge)bridge).textOverrun == null 
218                ? OverrunStyle.ELLIPSIS 
219                : ((Tooltip.CSSBridge)bridge).textOverrun.getValue(); 
220    }
221    /**
222     * Specifies the behavior to use if the text of the {@code Tooltip}
223     * exceeds the available space for rendering the text.
224     */
225    public final ObjectProperty<OverrunStyle> textOverrunProperty() {
226        return ((Tooltip.CSSBridge)bridge).textOverrunProperty();
227    }
228
229    public final void setWrapText(boolean value) { wrapTextProperty().setValue(value); }
230    public final boolean isWrapText() { 
231        return ((Tooltip.CSSBridge)bridge).wrapText == null 
232                ? false 
233                : ((Tooltip.CSSBridge)bridge).wrapText.getValue(); }
234    /**
235     * If a run of text exceeds the width of the Tooltip, then this variable
236     * indicates whether the text should wrap onto another line.
237     */
238    public final BooleanProperty wrapTextProperty() {
239        return ((Tooltip.CSSBridge)bridge).wrapTextProperty();
240    }
241
242    public final void setFont(Font value) { fontProperty().setValue(value); }
243    public final Font getFont() { 
244        return ((Tooltip.CSSBridge)bridge).font == null 
245                ? Font.getDefault() 
246                : ((Tooltip.CSSBridge)bridge).font.getValue(); }
247    /**
248     * The default font to use for text in the Tooltip. If the Tooltip's text is
249     * rich text then this font may or may not be used depending on the font
250     * information embedded in the rich text, but in any case where a default
251     * font is required, this font will be used.
252     */
253    public final ObjectProperty<Font> fontProperty() {
254        return ((Tooltip.CSSBridge)bridge).fontProperty();
255    }
256
257    /**
258     * An optional icon for the Tooltip. This can be positioned relative to the
259     * text by using the {@link #contentDisplayProperty() content display} 
260     * property.
261     * The node specified for this variable cannot appear elsewhere in the
262     * scene graph, otherwise the {@code IllegalArgumentException} is thrown.
263     * See the class description of {@link javafx.scene.Node Node} for more detail.
264     */
265    private ObjectProperty<Node> graphic;
266    public final void setGraphic(Node value) {
267        graphicProperty().setValue(value);
268    }
269    public final Node getGraphic() { return graphic == null ? null : graphic.getValue(); }
270    public final ObjectProperty<Node> graphicProperty() {
271        if (graphic == null) {
272            graphic = new ObjectPropertyBase<Node>() {
273
274                @Override
275                public Object getBean() {
276                    return Tooltip.this;
277                }
278
279                @Override
280                public String getName() {
281                    return "graphic";
282                }
283            };
284        }
285        return graphic;
286    }
287
288    public final void setContentDisplay(ContentDisplay value) { contentDisplayProperty().setValue(value); }
289    public final ContentDisplay getContentDisplay() { 
290        return ((Tooltip.CSSBridge)bridge).contentDisplay == null 
291                ? ContentDisplay.LEFT 
292                : ((Tooltip.CSSBridge)bridge).contentDisplay.getValue(); }
293    /**
294     * Specifies the positioning of the graphic relative to the text.
295     */
296    public final ObjectProperty<ContentDisplay> contentDisplayProperty() {
297        return ((Tooltip.CSSBridge)bridge).contentDisplayProperty();
298    }
299
300    public final void setGraphicTextGap(double value) { graphicTextGapProperty().setValue(value); }
301    public final double getGraphicTextGap() { 
302        return ((Tooltip.CSSBridge)bridge).graphicTextGap == null 
303                ? 4 
304                : ((Tooltip.CSSBridge)bridge).graphicTextGap.getValue(); }
305    /**
306     * The amount of space between the graphic and text
307     */
308    public final DoubleProperty graphicTextGapProperty() {
309        return ((Tooltip.CSSBridge)bridge).graphicTextGapProperty();
310    }
311
312    /**
313     * Typically, the tooltip is "activated" when the mouse moves over a Control.
314     * There is usually some delay between when the Tooltip becomes "activated"
315     * and when it is actually shown. The details (such as the amount of delay, etc)
316     * is left to the Skin implementation.
317     */
318    private final ReadOnlyBooleanWrapper activated = new ReadOnlyBooleanWrapper(this, "activated");
319    final void setActivated(boolean value) { activated.set(value); }
320    public final boolean isActivated() { return activated.get(); }
321    public final ReadOnlyBooleanProperty activatedProperty() { return activated.getReadOnlyProperty(); }
322
323    /***************************************************************************
324     *                                                                         *
325     * Methods                                                                 *
326     *                                                                         *
327     **************************************************************************/
328
329    /** {@inheritDoc} */
330    @Override protected Skin<?> createDefaultSkin() {
331        return new TooltipSkin(this);
332    }
333
334    /***************************************************************************
335     *                                                                         *
336     *                         Stylesheet Handling                             *
337     *                                                                         *
338     **************************************************************************/
339
340    private static class StyleableProperties {
341        private static final CssMetaData<CSSBridge,Font> FONT = 
342            new FontCssMetaData<CSSBridge>("-fx-font", Font.getDefault()) {
343
344            @Override
345            public boolean isSettable(CSSBridge n) {
346                return n.font == null || !n.font.isBound();
347            }
348
349            @Override
350            public StyleableProperty<Font> getStyleableProperty(CSSBridge n) {
351                return (StyleableProperty<Font>)n.fontProperty();
352            }
353        };
354        
355        private static final CssMetaData<CSSBridge,TextAlignment> TEXT_ALIGNMENT = 
356            new CssMetaData<CSSBridge,TextAlignment>("-fx-text-alignment",
357                new EnumConverter<TextAlignment>(TextAlignment.class), 
358                TextAlignment.LEFT) {
359
360            @Override
361            public boolean isSettable(CSSBridge n) {
362                return n.textAlignment == null || !n.textAlignment.isBound();
363            }
364
365            @Override
366            public StyleableProperty<TextAlignment> getStyleableProperty(CSSBridge n) {
367                return (StyleableProperty<TextAlignment>)n.textAlignmentProperty();
368            }
369        };
370        
371        private static final CssMetaData<CSSBridge,OverrunStyle> TEXT_OVERRUN = 
372            new CssMetaData<CSSBridge,OverrunStyle>("-fx-text-overrun",
373                new EnumConverter<OverrunStyle>(OverrunStyle.class),
374                OverrunStyle.ELLIPSIS) {
375
376            @Override
377            public boolean isSettable(CSSBridge n) {
378                return n.textOverrun == null || !n.textOverrun.isBound();
379            }
380
381            @Override
382            public StyleableProperty<OverrunStyle> getStyleableProperty(CSSBridge n) {
383                return (StyleableProperty<OverrunStyle>)n.textOverrunProperty();
384            }
385        };
386        
387        private static final CssMetaData<CSSBridge,Boolean> WRAP_TEXT = 
388            new CssMetaData<CSSBridge,Boolean>("-fx-wrap-text",
389                BooleanConverter.getInstance(), Boolean.FALSE) {
390
391            @Override
392            public boolean isSettable(CSSBridge n) {
393                return n.wrapText == null || !n.wrapText.isBound();
394            }
395
396            @Override
397            public StyleableProperty<Boolean> getStyleableProperty(CSSBridge n) {
398                return (StyleableProperty<Boolean>)n.wrapTextProperty();
399            }
400        };
401        
402        private static final CssMetaData<CSSBridge,String> GRAPHIC = 
403            new CssMetaData<CSSBridge,String>("-fx-graphic",
404                StringConverter.getInstance()) {
405
406            @Override
407            public boolean isSettable(CSSBridge n) {
408                return n.imageUrl == null || !n.imageUrl.isBound();
409            }
410
411            @Override
412            public StyleableProperty<String> getStyleableProperty(CSSBridge n) {
413                return (StyleableProperty<String>)n.imageUrlProperty();
414            }
415        };
416        
417        private static final CssMetaData<CSSBridge,ContentDisplay> CONTENT_DISPLAY = 
418            new CssMetaData<CSSBridge,ContentDisplay>("-fx-content-display",
419                new EnumConverter<ContentDisplay>(ContentDisplay.class),
420                ContentDisplay.LEFT) {
421
422            @Override
423            public boolean isSettable(CSSBridge n) {
424                return n.contentDisplay == null || !n.contentDisplay.isBound();
425            }
426
427            @Override
428            public StyleableProperty<ContentDisplay> getStyleableProperty(CSSBridge n) {
429                return (StyleableProperty<ContentDisplay>)n.contentDisplayProperty();
430            }
431        };
432    
433        private static final CssMetaData<CSSBridge,Number> GRAPHIC_TEXT_GAP = 
434            new CssMetaData<CSSBridge,Number>("-fx-graphic-text-gap",
435                SizeConverter.getInstance(), 4.0) {
436
437            @Override
438            public boolean isSettable(CSSBridge n) {
439                return n.graphicTextGap == null || !n.graphicTextGap.isBound();
440            }
441
442            @Override
443            public StyleableProperty<Number> getStyleableProperty(CSSBridge n) {
444                return (StyleableProperty<Number>)n.graphicTextGapProperty();
445            }
446        };
447    
448        private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
449        static {
450            final List<CssMetaData<? extends Styleable, ?>> styleables =
451                new ArrayList<CssMetaData<? extends Styleable, ?>>(PopupControl.getClassCssMetaData());
452            styleables.add(FONT);
453            styleables.add(TEXT_ALIGNMENT);
454            styleables.add(TEXT_OVERRUN);
455            styleables.add(WRAP_TEXT);
456            styleables.add(GRAPHIC);
457            styleables.add(CONTENT_DISPLAY);
458            styleables.add(GRAPHIC_TEXT_GAP);
459            STYLEABLES = Collections.unmodifiableList(styleables);
460        }
461    }
462
463    /**
464     * @return The CssMetaData associated with this class, which may include the
465     * CssMetaData of its super classes.
466     */
467    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
468        return StyleableProperties.STYLEABLES;
469    }
470
471    /**
472     * {@inheritDoc}
473     */
474    @Override
475    public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
476        return getClassCssMetaData();
477    }
478
479    @Override public Styleable getStyleableParent() {
480        return BEHAVIOR.hoveredNode;
481    }
482
483    private final class CSSBridge extends PopupControl.CSSBridge {
484        
485        @Override public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
486            return Tooltip.this.getCssMetaData();
487        }
488        
489        private ObjectProperty<TextAlignment> textAlignment;
490        private final ObjectProperty<TextAlignment> textAlignmentProperty() {        
491            if (textAlignment == null) {
492                textAlignment = new StyleableObjectProperty<TextAlignment>(TextAlignment.LEFT) {
493                    @Override 
494                    public CssMetaData<CSSBridge,TextAlignment> getCssMetaData() {
495                        return StyleableProperties.TEXT_ALIGNMENT;
496                    }
497
498                    @Override
499                    public Object getBean() {
500                        return CSSBridge.this;
501                    }
502
503                    @Override
504                    public String getName() {
505                        return "textAlignment";
506                    }
507                };
508            }
509            return textAlignment;
510        }
511
512        private ObjectProperty<OverrunStyle> textOverrun;
513        private final ObjectProperty<OverrunStyle> textOverrunProperty() {
514            if (textOverrun == null) {
515                textOverrun = new StyleableObjectProperty<OverrunStyle>(OverrunStyle.ELLIPSIS) {
516                    @Override 
517                    public CssMetaData<CSSBridge,OverrunStyle> getCssMetaData() {
518                        return StyleableProperties.TEXT_OVERRUN;
519                    }
520
521                    @Override
522                    public Object getBean() {
523                        return CSSBridge.this;
524                    }
525
526                    @Override
527                    public String getName() {
528                        return "textOverrun";
529                    }
530                };
531            }
532            return textOverrun;
533        }
534
535        private BooleanProperty wrapText;
536        private final BooleanProperty wrapTextProperty() {
537            if (wrapText == null) {
538                wrapText = new StyleableBooleanProperty(false) {
539                    @Override 
540                    public CssMetaData<CSSBridge,Boolean> getCssMetaData() {
541                        return StyleableProperties.WRAP_TEXT;
542                    }
543
544                    @Override
545                    public Object getBean() {
546                        return CSSBridge.this;
547                    }
548
549                    @Override
550                    public String getName() {
551                        return "wrapText";
552                    }
553                };
554            }
555            return wrapText;
556        }        
557
558        private ObjectProperty<Font> font;
559        private final ObjectProperty<Font> fontProperty() {
560            if (font == null) {
561                font = new StyleableObjectProperty<Font>(Font.getDefault()) {
562                    @Override 
563                    public CssMetaData<CSSBridge,Font> getCssMetaData() {
564                        return StyleableProperties.FONT;
565                    }
566
567                    @Override
568                    public Object getBean() {
569                        return CSSBridge.this;
570                    }
571
572                    @Override
573                    public String getName() {
574                        return "font";
575                    }
576                };
577            }
578            return font;
579        }        
580        
581        private StringProperty imageUrl = null;
582        /**
583        * The imageUrl property is set from CSS and then the graphic property is
584        * set from the invalidated method. This ensures that the same image isn't
585        * reloaded. 
586        */
587        private StringProperty imageUrlProperty() {
588            if (imageUrl == null) {
589                imageUrl = new StyleableStringProperty() {
590
591                    @Override
592                    protected void invalidated() {
593
594                        if (get() != null) {
595                            URL url = null;
596                            try {
597                                url = new URL(get());
598                            } catch (MalformedURLException malf) {
599                                // This may be a relative URL, so try resolving
600                                // it using the application classloader
601                                final ClassLoader cl = Thread.currentThread().getContextClassLoader();
602                                url = cl.getResource(get());
603                            }
604                            if (url != null) {
605                                final Image img = StyleManager.getInstance().getCachedImage(url.toExternalForm());
606                                setGraphic(new ImageView(img));                            
607                            }
608                        } else {
609                            setGraphic(null);
610                        }                    
611                    }
612
613                    @Override
614                    public Object getBean() {
615                        return CSSBridge.this;
616                    }
617
618                    @Override
619                    public String getName() {
620                        return "imageUrl";
621                    }
622
623                    @Override
624                    public CssMetaData<CSSBridge,String> getCssMetaData() {
625                        return Tooltip.StyleableProperties.GRAPHIC;
626                    }
627
628                };
629            }
630            return imageUrl;
631        }
632
633        private ObjectProperty<ContentDisplay> contentDisplay;
634        private final ObjectProperty<ContentDisplay> contentDisplayProperty() {
635            if (contentDisplay == null) {
636                contentDisplay = new StyleableObjectProperty<ContentDisplay>(ContentDisplay.LEFT) {
637                    @Override 
638                    public CssMetaData<CSSBridge,ContentDisplay> getCssMetaData() {
639                        return StyleableProperties.CONTENT_DISPLAY;
640                    }
641
642                    @Override
643                    public Object getBean() {
644                        return CSSBridge.this;
645                    }
646
647                    @Override
648                    public String getName() {
649                        return "contentDisplay";
650                    }
651                };
652            }
653            return contentDisplay;
654        }
655    
656        private DoubleProperty graphicTextGap;
657        private final DoubleProperty graphicTextGapProperty() {
658            if (graphicTextGap == null) {
659                graphicTextGap = new StyleableDoubleProperty(4) {
660                    @Override 
661                    public CssMetaData<CSSBridge,Number> getCssMetaData() {
662                        return StyleableProperties.GRAPHIC_TEXT_GAP;
663                    }
664
665                    @Override
666                    public Object getBean() {
667                        return CSSBridge.this;
668                    }
669
670                    @Override
671                    public String getName() {
672                        return "graphicTextGap";
673                    }
674                };
675            }
676            return graphicTextGap;
677        }
678        
679    }
680
681    private static class TooltipBehavior {
682
683        /*
684         * There are two key concepts with Tooltip: activated and visible. A Tooltip
685         * is activated as soon as a mouse move occurs over the target node. When it
686         * becomes activated, we start off the ACTIVATION_TIMER. If the
687         * ACTIVATION_TIMER expires before another mouse event occurs, then we will
688         * show the popup. This timer typically lasts about 1 second.
689         *
690         * Once visible, we reset the ACTIVATION_TIMER and start the HIDE_TIMER.
691         * This second timer will allow the tooltip to remain visible for some time
692         * period (such as 5 seconds). If the mouse hasn't moved, and the HIDE_TIMER
693         * expires, then the tooltip is hidden and the tooltip is no longer
694         * activated.
695         *
696         * If another mouse move occurs, the ACTIVATION_TIMER starts again, and the
697         * same rules apply as above.
698         *
699         * If a mouse exit event occurs while the HIDE_TIMER is ticking, we reset
700         * the HIDE_TIMER. Thus, the tooltip disappears after 5 seconds from the
701         * last mouse move.
702         *
703         * If some other mouse event occurs while the HIDE_TIMER is running, other
704         * than mouse move or mouse enter/exit (such as a click), then the tooltip
705         * is hidden, the HIDE_TIMER stopped, and activated set to false.
706         *
707         * If a mouse exit occurs while the HIDE_TIMER is running, we stop the
708         * HIDE_TIMER and start the LEFT_TIMER, and immediately hide the tooltip.
709         * This timer is very short, maybe about a 1/2 second. If the mouse enters a
710         * new node which also has a tooltip before LEFT_TIMER expires, then the
711         * second tooltip is activated and shown immediately (the ACTIVATION_TIMER
712         * having been bypassed), and the HIDE_TIMER is started. If the LEFT_TIMER
713         * expires and there is no mouse movement over a control with a tooltip,
714         * then we are back to the initial steady state where the next mouse move
715         * over a node with a tooltip installed will start the ACTIVATION_TIMER.
716         */
717
718        private Timeline activationTimer = new Timeline();
719        private Timeline hideTimer = new Timeline();
720        private Timeline leftTimer = new Timeline();
721
722        /**
723         * The Node with a tooltip over which the mouse is hovering. There can
724         * only be one of these at a time.
725         */
726        private Node hoveredNode;
727
728        /**
729         * The tooltip that is currently activated. There can only be one
730         * of these at a time.
731         */
732        private Tooltip activatedTooltip;
733
734        /**
735         * The tooltip that is currently visible. There can only be one
736         * of these at a time.
737         */
738        private Tooltip visibleTooltip;
739
740        /**
741         * The last position of the mouse, in screen coordinates.
742         */
743        private double lastMouseX;
744        private double lastMouseY;
745
746        private boolean hideOnExit;
747
748        TooltipBehavior(Duration openDelay, Duration visibleDuration, Duration closeDelay, final boolean hideOnExit) {
749            this.hideOnExit = hideOnExit;
750
751            activationTimer.getKeyFrames().add(new KeyFrame(openDelay));
752            activationTimer.setOnFinished(new EventHandler<ActionEvent>() {
753                @Override public void handle(ActionEvent event) {
754                    // Show the currently activated tooltip and start the
755                    // HIDE_TIMER.
756                    assert activatedTooltip != null;
757                    final Window owner = getWindow(hoveredNode);
758                    final boolean treeVisible = isWindowHierarchyVisible(hoveredNode);
759
760                    // If the ACTIVATED tooltip is part of a visible window
761                    // hierarchy, we can go ahead and show the tooltip and
762                    // start the HIDE_TIMER.
763                    //
764                    // If the owner is null or invisible, then it either means a
765                    // bug in our code, the node was removed from a scene or
766                    // window or made invisible, or the node is not part of a
767                    // visible window hierarchy. In that case, we don't show the
768                    // tooltip, and we don't start the HIDE_TIMER. We simply let
769                    // ACTIVATED_TIMER expire, and wait until the next mouse
770                    // the movement to start it again.
771                    if (owner != null && owner.isShowing() && treeVisible) {
772                        double x = lastMouseX;
773                        double y = lastMouseY;
774
775                        // The tooltip always inherits the nodeOrientation of
776                        // the Node that it is attached to (see RT-26147). It
777                        // is possible to override this for the Tooltip content
778                        // (but not the popup placement) by setting the
779                        // nodeOrientation on tooltip.getScene().getRoot().
780                        NodeOrientation nodeOrientation = hoveredNode.getEffectiveNodeOrientation();
781                        activatedTooltip.getScene().setNodeOrientation(nodeOrientation);
782                        if (nodeOrientation == NodeOrientation.RIGHT_TO_LEFT) {
783                            x -= activatedTooltip.getWidth();
784                        }
785
786                        activatedTooltip.show(owner, x, y);
787                        visibleTooltip = activatedTooltip;
788                        hoveredNode = null;
789                        hideTimer.playFromStart();
790                    }
791
792                    // Once the activation timer has expired, the tooltip is no
793                    // longer in the activated state, it is only in the visible
794                    // state, so we go ahead and set activated to false
795                    activatedTooltip.setActivated(false);
796                    activatedTooltip = null;
797                }
798            });
799
800            hideTimer.getKeyFrames().add(new KeyFrame(visibleDuration));
801            hideTimer.setOnFinished(new EventHandler<ActionEvent>() {
802                @Override public void handle(ActionEvent event) {
803                    // Hide the currently visible tooltip.
804                    assert visibleTooltip != null;
805                    visibleTooltip.hide();
806                    visibleTooltip = null;
807                    hoveredNode = null;
808                }
809            });
810
811            leftTimer.getKeyFrames().add(new KeyFrame(closeDelay));
812            leftTimer.setOnFinished(new EventHandler<ActionEvent>() {
813                @Override public void handle(ActionEvent event) {
814                    if (!hideOnExit) {
815                        // Hide the currently visible tooltip.
816                        assert visibleTooltip != null;
817                        visibleTooltip.hide();
818                        visibleTooltip = null;
819                        hoveredNode = null;
820                    }
821                }
822            });
823        }
824
825        /**
826         * Registers for mouse move events only. When the mouse is moved, this
827         * handler will detect it and decide whether to start the ACTIVATION_TIMER
828         * (if the ACTIVATION_TIMER is not started), restart the ACTIVATION_TIMER
829         * (if ACTIVATION_TIMER is running), or skip the ACTIVATION_TIMER and just
830         * show the tooltip (if the LEFT_TIMER is running).
831         */
832        private EventHandler<MouseEvent> MOVE_HANDLER = new EventHandler<MouseEvent>() {
833            @Override public void handle(MouseEvent event) {
834                //Screen coordinates need to be actual for dynamic tooltip.
835                //See Tooltip.setText
836
837                // detect bogus mouse moved events, if it didn't really move then ignore it
838                double newMouseX = event.getScreenX();
839                double newMouseY = event.getScreenY();
840                if (newMouseX == lastMouseX && newMouseY == lastMouseY) {
841                    return;
842                }
843                lastMouseX = newMouseX;
844                lastMouseY = newMouseY;
845
846                // If the HIDE_TIMER is running, then we don't want this event
847                // handler to do anything, or change any state at all.
848                if (hideTimer.getStatus() == Timeline.Status.RUNNING) {
849                    return;
850                }
851
852                // Note that the "install" step will both register this handler
853                // with the target node and also associate the tooltip with the
854                // target node, by stashing it in the client properties of the node.
855                hoveredNode = (Node) event.getSource();
856                Tooltip t = (Tooltip) hoveredNode.getProperties().get(TOOLTIP_PROP_KEY);
857                if (t != null) {
858                    // In theory we should never get here with an invisible or
859                    // non-existant window hierarchy, but might in some cases where
860                    // people are feeding fake mouse events into the hierarchy. So
861                    // we'll guard against that case.
862                    final Window owner = getWindow(hoveredNode);
863                    final boolean treeVisible = isWindowHierarchyVisible(hoveredNode);
864                    if (owner != null && treeVisible) {
865                        // Now we know that the currently HOVERED node has a tooltip
866                        // and that it is part of a visible window Hierarchy.
867                        // If LEFT_TIMER is running, then we make this tooltip
868                        // visible immediately, stop the LEFT_TIMER, and start the
869                        // HIDE_TIMER.
870                        if (leftTimer.getStatus() == Timeline.Status.RUNNING) {
871                            if (visibleTooltip != null) visibleTooltip.hide();
872                            visibleTooltip = t;
873                            t.show(owner, event.getScreenX(), event.getScreenY());
874                            leftTimer.stop();
875                            hideTimer.playFromStart();
876                        } else {
877                            // Start / restart the timer and make sure the tooltip
878                            // is marked as activated.
879                            t.setActivated(true);
880                            activatedTooltip = t;
881                            activationTimer.stop();
882                            activationTimer.playFromStart();
883                        }
884                    }
885                } else {
886                    // TODO should deregister, no point being here anymore!
887                }
888            }
889        };
890
891        /**
892         * Registers for mouse exit events. If the ACTIVATION_TIMER is running then
893         * this will simply stop it. If the HIDE_TIMER is running then this will
894         * stop the HIDE_TIMER, hide the tooltip, and start the LEFT_TIMER.
895         */
896        private EventHandler<MouseEvent> LEAVING_HANDLER = new EventHandler<MouseEvent>() {
897            @Override public void handle(MouseEvent event) {
898                // detect bogus mouse exit events, if it didn't really move then ignore it
899                double newMouseX = event.getScreenX();
900                double newMouseY = event.getScreenY();
901                if (newMouseX == lastMouseX && newMouseY == lastMouseY) {
902                    return;
903                } 
904                
905                if (activationTimer.getStatus() == Timeline.Status.RUNNING) {
906                    activationTimer.stop();
907                } else if (hideTimer.getStatus() == Timeline.Status.RUNNING) {
908                    assert visibleTooltip != null;
909                    hideTimer.stop();
910                    if (hideOnExit) visibleTooltip.hide();
911                    leftTimer.playFromStart();
912                }
913
914                hoveredNode = null;
915                activatedTooltip = null;
916                if (hideOnExit) visibleTooltip = null;
917            }
918        };
919
920        /**
921         * Registers for mouse click, press, release, drag events. If any of these
922         * occur, then the tooltip is hidden (if it is visible), it is deactivated,
923         * and any and all timers are stopped.
924         */
925        private EventHandler<MouseEvent> KILL_HANDLER = new EventHandler<MouseEvent>() {
926            @Override public void handle(MouseEvent event) {
927                activationTimer.stop();
928                hideTimer.stop();
929                leftTimer.stop();
930                if (visibleTooltip != null) visibleTooltip.hide();
931                hoveredNode = null;
932                activatedTooltip = null;
933                visibleTooltip = null;
934            }
935        };
936
937        private void install(Node node, Tooltip t) {
938            // Install the MOVE_HANDLER, LEAVING_HANDLER, and KILL_HANDLER on
939            // the given node. Stash the tooltip in the node's client properties
940            // map so that it is not gc'd. The handlers must all be installed
941            // with a TODO weak reference so as not to cause a memory leak
942            if (node == null) return;
943            node.addEventHandler(MouseEvent.MOUSE_MOVED, MOVE_HANDLER);
944            node.addEventHandler(MouseEvent.MOUSE_EXITED, LEAVING_HANDLER);
945            node.addEventHandler(MouseEvent.MOUSE_PRESSED, KILL_HANDLER);
946            node.getProperties().put(TOOLTIP_PROP_KEY, t);
947        }
948
949        private void uninstall(Node node) {
950            if (node == null) return;
951            node.removeEventHandler(MouseEvent.MOUSE_MOVED, MOVE_HANDLER);
952            node.removeEventHandler(MouseEvent.MOUSE_EXITED, LEAVING_HANDLER);
953            node.removeEventHandler(MouseEvent.MOUSE_PRESSED, KILL_HANDLER);
954            Tooltip t = (Tooltip)node.getProperties().get(TOOLTIP_PROP_KEY);
955            if (t != null) {
956                node.getProperties().remove(TOOLTIP_PROP_KEY);
957                if (t.equals(visibleTooltip) || t.equals(activatedTooltip)) {
958                    KILL_HANDLER.handle(null);
959                }
960            }
961        }
962
963        /**
964         * Gets the top level window associated with this node.
965         * @param node the node
966         * @return the top level window
967         */
968        private Window getWindow(final Node node) {
969            final Scene scene = node == null ? null : node.getScene();
970            return scene == null ? null : scene.getWindow();
971        }
972
973        /**
974         * Gets whether the entire window hierarchy is visible for this node.
975         * @param node the node to check
976         * @return true if entire hierarchy is visible
977         */
978        private boolean isWindowHierarchyVisible(Node node) {
979            boolean treeVisible = node != null;
980            Parent parent = node == null ? null : node.getParent();
981            while (parent != null && treeVisible) {
982                treeVisible = parent.isVisible();
983                parent = parent.getParent();
984            }
985            return treeVisible;
986        }
987
988    }
989}