1 /*
   2  * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.control;
  27 
  28 
  29 import com.sun.javafx.beans.IDProperty;
  30 import com.sun.javafx.css.StyleManager;
  31 
  32 import javafx.css.SimpleStyleableBooleanProperty;
  33 import javafx.css.SimpleStyleableDoubleProperty;
  34 import javafx.css.SimpleStyleableObjectProperty;
  35 import javafx.css.StyleOrigin;
  36 import javafx.css.StyleableObjectProperty;
  37 import javafx.css.StyleableStringProperty;
  38 
  39 import com.sun.javafx.css.converters.BooleanConverter;
  40 import com.sun.javafx.css.converters.EnumConverter;
  41 import com.sun.javafx.css.converters.SizeConverter;
  42 import com.sun.javafx.css.converters.StringConverter;
  43 import com.sun.javafx.scene.control.skin.TooltipSkin;
  44 
  45 import java.util.ArrayList;
  46 import java.util.Collections;
  47 import java.util.List;
  48 
  49 import javafx.animation.KeyFrame;
  50 import javafx.animation.Timeline;
  51 import javafx.beans.property.*;
  52 import javafx.beans.value.WritableValue;
  53 import javafx.css.CssMetaData;
  54 import javafx.css.FontCssMetaData;
  55 import javafx.css.Styleable;
  56 import javafx.css.StyleableProperty;
  57 import javafx.event.EventHandler;
  58 import javafx.geometry.NodeOrientation;
  59 import javafx.scene.AccessibleRole;
  60 import javafx.scene.Node;
  61 import javafx.scene.Parent;
  62 import javafx.scene.Scene;
  63 import javafx.scene.image.Image;
  64 import javafx.scene.image.ImageView;
  65 import javafx.scene.input.MouseEvent;
  66 import javafx.scene.text.Font;
  67 import javafx.scene.text.TextAlignment;
  68 import javafx.stage.Window;
  69 import javafx.util.Duration;
  70 
  71 
  72 /**
  73  * Tooltips are common UI elements which are typically used for showing
  74  * additional information about a Node in the scenegraph when the Node is 
  75  * hovered over by the mouse. Any Node can show a tooltip. In most cases a 
  76  * Tooltip is created and its {@link #textProperty() text} property is modified
  77  * to show plain text to the user. However, a Tooltip is able to show within it
  78  * an arbitrary scenegraph of nodes - this is done by creating the scenegraph
  79  * and setting it inside the Tooltip {@link #graphicProperty() graphic} 
  80  * property. 
  81  * 
  82  * <p>You use the following approach to set a Tooltip on any node:
  83  *
  84  * <pre>
  85  * Rectangle rect = new Rectangle(0, 0, 100, 100);
  86  * Tooltip t = new Tooltip("A Square");
  87  * Tooltip.install(rect, t);
  88  * </pre>
  89  *
  90  * This tooltip will then participate with the typical tooltip semantics (i.e.
  91  * appearing on hover, etc). Note that the Tooltip does not have to be 
  92  * uninstalled: it will be garbage collected when it is not referenced by any
  93  * Node. It is possible to manually uninstall the tooltip, however.
  94  *
  95  * <p>A single tooltip can be installed on multiple target nodes or multiple
  96  * controls.
  97  * 
  98  * <p>Because most Tooltips are shown on UI controls, there is special API
  99  * for all controls to make installing a Tooltip less verbose. The example below 
 100  * shows how to create a tooltip for a Button control:
 101  *
 102  * <pre>
 103  * import javafx.scene.control.Tooltip;
 104  * import javafx.scene.control.Button;
 105  *
 106  * Button button = new Button("Hover Over Me");
 107  * button.setTooltip(new Tooltip("Tooltip for Button"));
 108  * </pre>
 109  * @since JavaFX 2.0
 110  */
 111 @IDProperty("id")
 112 public class Tooltip extends PopupControl {
 113     private static String TOOLTIP_PROP_KEY = "javafx.scene.control.Tooltip";
 114 
 115     // RT-31134 : the tooltip style includes a shadow around the tooltip with a
 116     // width of 9 and height of 5. This causes mouse events to not reach the control
 117     // underneath resulting in losing hover state on the control while the tooltip is showing.
 118     // Displaying the tooltip at an offset indicated below resolves this issue.
 119     // RT-37107: The y-offset was upped to 7 to ensure no overlaps when the tooltip
 120     // is shown near the right edge of the screen.
 121     private static int TOOLTIP_XOFFSET = 10;
 122     private static int TOOLTIP_YOFFSET = 7;
 123 
 124     private static TooltipBehavior BEHAVIOR = new TooltipBehavior(
 125         new Duration(1000), new Duration(5000), new Duration(200), false);
 126 
 127     /**
 128      * Associates the given {@link Tooltip} with the given {@link Node}. The tooltip
 129      * can then behave similar to when it is set on any {@link Control}. A single
 130      * tooltip can be associated with multiple nodes.
 131      * @see Tooltip
 132      */
 133     public static void install(Node node, Tooltip t) {
 134         BEHAVIOR.install(node, t);
 135     }
 136 
 137     /**
 138      * Removes the association of the given {@link Tooltip} on the specified
 139      * {@link Node}. Hence hovering on the node will no longer result in showing of the
 140      * tooltip.
 141      * @see Tooltip
 142      */
 143     public static void uninstall(Node node, Tooltip t) {
 144         BEHAVIOR.uninstall(node);
 145     }
 146     
 147     /***************************************************************************
 148      *                                                                         *
 149      * Constructors                                                            *
 150      *                                                                         *
 151      **************************************************************************/
 152     
 153     /**
 154      * Creates a tooltip with an empty string for its text.
 155      */
 156     public Tooltip() {
 157         this(null);
 158     }
 159 
 160     /**
 161      * Creates a tooltip with the specified text.
 162      *
 163      * @param text A text string for the tooltip.
 164      */
 165     public Tooltip(String text) {
 166         super();
 167         if (text != null) setText(text);
 168         bridge = new CSSBridge();
 169         getContent().setAll(bridge);
 170         getStyleClass().setAll("tooltip");
 171     }
 172 
 173     /***************************************************************************
 174      *                                                                         *
 175      * Properties                                                              *
 176      *                                                                         *
 177      **************************************************************************/
 178     /**
 179      * The text to display in the tooltip. If the text is set to null, an empty
 180      * string will be displayed, despite the value being null.
 181      */
 182     public final StringProperty textProperty() { return text; }
 183     public final void setText(String value) {
 184         textProperty().setValue(value);
 185     }
 186     public final String getText() { return text.getValue() == null ? "" : text.getValue(); }
 187     private final StringProperty text = new SimpleStringProperty(this, "text", "") {
 188         @Override protected void invalidated() {
 189             super.invalidated();
 190             final String value = get();
 191             if (isShowing() && value != null && !value.equals(getText())) {
 192                 //Dynamic tooltip content is location-dependant.
 193                 //Chromium trick.
 194                 setAnchorX(BEHAVIOR.lastMouseX);
 195                 setAnchorY(BEHAVIOR.lastMouseY);
 196             }
 197         }
 198     };
 199 
 200     /**
 201      * Specifies the behavior for lines of text <em>when text is multiline</em>.
 202      * Unlike {@link #contentDisplayProperty() contentDisplay} which affects the 
 203      * graphic and text, this setting only affects multiple lines of text 
 204      * relative to the text bounds.
 205      */
 206     public final ObjectProperty<TextAlignment> textAlignmentProperty() {  
 207         return textAlignment;
 208     }
 209     public final void setTextAlignment(TextAlignment value) {
 210         textAlignmentProperty().setValue(value);
 211     }
 212     public final TextAlignment getTextAlignment() {
 213         return textAlignmentProperty().getValue();
 214     }
 215     private final ObjectProperty<TextAlignment> textAlignment =
 216             new SimpleStyleableObjectProperty<>(TEXT_ALIGNMENT, this, "textAlignment", TextAlignment.LEFT);;
 217 
 218     /**
 219      * Specifies the behavior to use if the text of the {@code Tooltip}
 220      * exceeds the available space for rendering the text.
 221      */
 222     public final ObjectProperty<OverrunStyle> textOverrunProperty() {
 223         return textOverrun;
 224     }
 225     public final void setTextOverrun(OverrunStyle value) {
 226         textOverrunProperty().setValue(value);
 227     }
 228     public final OverrunStyle getTextOverrun() {
 229         return textOverrunProperty().getValue();
 230     }
 231     private final ObjectProperty<OverrunStyle> textOverrun =
 232             new SimpleStyleableObjectProperty<OverrunStyle>(TEXT_OVERRUN, this, "textOverrun", OverrunStyle.ELLIPSIS);
 233 
 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 wrapText;
 240     }
 241     public final void setWrapText(boolean value) {
 242         wrapTextProperty().setValue(value);
 243     }
 244     public final boolean isWrapText() {
 245         return wrapTextProperty().getValue();
 246     }
 247     private final BooleanProperty wrapText =
 248             new SimpleStyleableBooleanProperty(WRAP_TEXT, this, "wrapText", false);
 249 
 250 
 251     /**
 252      * The default font to use for text in the Tooltip. If the Tooltip's text is
 253      * rich text then this font may or may not be used depending on the font
 254      * information embedded in the rich text, but in any case where a default
 255      * font is required, this font will be used.
 256      */
 257     public final ObjectProperty<Font> fontProperty() {
 258         return font;
 259     }
 260     public final void setFont(Font value) {
 261         fontProperty().setValue(value);
 262     }
 263     public final Font getFont() {
 264         return fontProperty().getValue();
 265     }
 266     private final ObjectProperty<Font> font = new StyleableObjectProperty<Font>(Font.getDefault()) {
 267         private boolean fontSetByCss = false;
 268 
 269         @Override public void applyStyle(StyleOrigin newOrigin, Font value) {
 270             // RT-20727 - if CSS is setting the font, then make sure invalidate doesn't call impl_reapplyCSS
 271             try {
 272                 // super.applyStyle calls set which might throw if value is bound.
 273                 // Have to make sure fontSetByCss is reset.
 274                 fontSetByCss = true;
 275                 super.applyStyle(newOrigin, value);
 276             } catch(Exception e) {
 277                 throw e;
 278             } finally {
 279                 fontSetByCss = false;
 280             }
 281         }
 282 
 283         @Override public void set(Font value) {
 284             final Font oldValue = get();
 285             if (value != null ? !value.equals(oldValue) : oldValue != null) {
 286                 super.set(value);
 287             }
 288         }
 289 
 290         @Override protected void invalidated() {
 291             // RT-20727 - if font is changed by calling setFont, then
 292             // css might need to be reapplied since font size affects
 293             // calculated values for styles with relative values
 294             if(fontSetByCss == false) {
 295                 Tooltip.this.bridge.impl_reapplyCSS();
 296             }
 297         }
 298 
 299         @Override public CssMetaData<Tooltip.CSSBridge,Font> getCssMetaData() {
 300             return FONT;
 301         }
 302 
 303         @Override public Object getBean() {
 304             return Tooltip.this;
 305         }
 306 
 307         @Override public String getName() {
 308             return "font";
 309         }
 310     };
 311 
 312     /**
 313      * An optional icon for the Tooltip. This can be positioned relative to the
 314      * text by using the {@link #contentDisplayProperty() content display} 
 315      * property.
 316      * The node specified for this variable cannot appear elsewhere in the
 317      * scene graph, otherwise the {@code IllegalArgumentException} is thrown.
 318      * See the class description of {@link javafx.scene.Node Node} for more detail.
 319      */
 320     public final ObjectProperty<Node> graphicProperty() {
 321         return graphic;
 322     }
 323     public final void setGraphic(Node value) {
 324         graphicProperty().setValue(value);
 325     }
 326     public final Node getGraphic() {
 327         return graphicProperty().getValue();
 328     }
 329     private final ObjectProperty<Node> graphic = new StyleableObjectProperty<Node>() {
 330         // The graphic is styleable by css, but it is the
 331         // imageUrlProperty that handles the style value.
 332         @Override public CssMetaData getCssMetaData() {
 333             return GRAPHIC;
 334         }
 335 
 336         @Override public Object getBean() {
 337             return Tooltip.this;
 338         }
 339 
 340         @Override public String getName() {
 341             return "graphic";
 342         }
 343 
 344     };
 345 
 346     private StyleableStringProperty imageUrlProperty() {
 347         if (imageUrl == null) {
 348             imageUrl = new StyleableStringProperty() {
 349                 // If imageUrlProperty is invalidated, this is the origin of the style that
 350                 // triggered the invalidation. This is used in the invaildated() method where the
 351                 // value of super.getStyleOrigin() is not valid until after the call to set(v) returns,
 352                 // by which time invalidated will have been called.
 353                 // This value is initialized to USER in case someone calls set on the imageUrlProperty, which
 354                 // is possible:
 355                 //     CssMetaData metaData = ((StyleableProperty)labeled.graphicProperty()).getCssMetaData();
 356                 //     StyleableProperty prop = metaData.getStyleableProperty(labeled);
 357                 //     prop.set(someUrl);
 358                 //
 359                 // TODO: Note that prop != labeled, which violates the contract between StyleableProperty and CssMetaData.
 360                 StyleOrigin origin = StyleOrigin.USER;
 361 
 362                 @Override public void applyStyle(StyleOrigin origin, String v) {
 363 
 364                     this.origin = origin;
 365 
 366                     // Don't want applyStyle to throw an exception which would leave this.origin set to the wrong value
 367                     if (graphic == null || graphic.isBound() == false) super.applyStyle(origin, v);
 368 
 369                     // Origin is only valid for this invocation of applyStyle, so reset it to USER in case someone calls set.
 370                     this.origin = StyleOrigin.USER;
 371                 }
 372 
 373                 @Override protected void invalidated() {
 374 
 375                     // need to call super.get() here since get() is overridden to return the graphicProperty's value
 376                     final String url = super.get();
 377 
 378                     if (url == null) {
 379                         ((StyleableProperty<Node>)(WritableValue<Node>)graphicProperty()).applyStyle(origin, null);
 380                     } else {
 381                         // RT-34466 - if graphic's url is the same as this property's value, then don't overwrite.
 382                         final Node graphicNode = Tooltip.this.getGraphic();
 383                         if (graphicNode instanceof ImageView) {
 384                             final ImageView imageView = (ImageView)graphicNode;
 385                             final Image image = imageView.getImage();
 386                             if (image != null) {
 387                                 final String imageViewUrl = image.impl_getUrl();
 388                                 if (url.equals(imageViewUrl)) return;
 389                             }
 390 
 391                         }
 392 
 393                         final Image img = StyleManager.getInstance().getCachedImage(url);
 394 
 395                         if (img != null) {
 396                             // Note that it is tempting to try to re-use existing ImageView simply by setting
 397                             // the image on the current ImageView, if there is one. This would effectively change
 398                             // the image, but not the ImageView which means that no graphicProperty listeners would
 399                             // be notified. This is probably not what we want.
 400 
 401                             // Have to call applyStyle on graphicProperty so that the graphicProperty's
 402                             // origin matches the imageUrlProperty's origin.
 403                             ((StyleableProperty<Node>)(WritableValue<Node>)graphicProperty()).applyStyle(origin, new ImageView(img));
 404                         }
 405                     }
 406                 }
 407 
 408                 @Override public String get() {
 409                     // The value of the imageUrlProperty is that of the graphicProperty.
 410                     // Return the value in a way that doesn't expand the graphicProperty.
 411                     final Node graphic = getGraphic();
 412                     if (graphic instanceof ImageView) {
 413                         final Image image = ((ImageView)graphic).getImage();
 414                         if (image != null) {
 415                             return image.impl_getUrl();
 416                         }
 417                     }
 418                     return null;
 419                 }
 420 
 421                 @Override public StyleOrigin getStyleOrigin() {
 422                     // The origin of the imageUrlProperty is that of the graphicProperty.
 423                     // Return the origin in a way that doesn't expand the graphicProperty.
 424                     return graphic != null ? ((StyleableProperty<Node>)(WritableValue<Node>)graphic).getStyleOrigin() : null;
 425                 }
 426 
 427                 @Override public Object getBean() {
 428                     return Tooltip.this;
 429                 }
 430 
 431                 @Override public String getName() {
 432                     return "imageUrl";
 433                 }
 434 
 435                 @Override public CssMetaData<Tooltip.CSSBridge,String> getCssMetaData() {
 436                     return GRAPHIC;
 437                 }
 438             };
 439         }
 440         return imageUrl;
 441     }
 442 
 443     private StyleableStringProperty imageUrl = null;
 444 
 445     /**
 446      * Specifies the positioning of the graphic relative to the text.
 447      */
 448     public final ObjectProperty<ContentDisplay> contentDisplayProperty() {
 449         return contentDisplay;
 450     }
 451     public final void setContentDisplay(ContentDisplay value) {
 452         contentDisplayProperty().setValue(value);
 453     }
 454     public final ContentDisplay getContentDisplay() {
 455         return contentDisplayProperty().getValue();
 456     }
 457     private final ObjectProperty<ContentDisplay> contentDisplay =
 458             new SimpleStyleableObjectProperty<>(CONTENT_DISPLAY, this, "contentDisplay", ContentDisplay.LEFT);
 459 
 460     /**
 461      * The amount of space between the graphic and text
 462      */
 463     public final DoubleProperty graphicTextGapProperty() {
 464         return graphicTextGap;
 465     }
 466     public final void setGraphicTextGap(double value) {
 467         graphicTextGapProperty().setValue(value);
 468     }
 469     public final double getGraphicTextGap() {
 470         return graphicTextGapProperty().getValue();
 471     }
 472     private final DoubleProperty graphicTextGap =
 473             new SimpleStyleableDoubleProperty(GRAPHIC_TEXT_GAP, this, "graphicTextGap", 4d);
 474 
 475     /**
 476      * Typically, the tooltip is "activated" when the mouse moves over a Control.
 477      * There is usually some delay between when the Tooltip becomes "activated"
 478      * and when it is actually shown. The details (such as the amount of delay, etc)
 479      * is left to the Skin implementation.
 480      */
 481     private final ReadOnlyBooleanWrapper activated = new ReadOnlyBooleanWrapper(this, "activated");
 482     final void setActivated(boolean value) { activated.set(value); }
 483     public final boolean isActivated() { return activated.get(); }
 484     public final ReadOnlyBooleanProperty activatedProperty() { return activated.getReadOnlyProperty(); }
 485 
 486 
 487 
 488     /***************************************************************************
 489      *                                                                         *
 490      * Methods                                                                 *
 491      *                                                                         *
 492      **************************************************************************/
 493 
 494     /** {@inheritDoc} */
 495     @Override protected Skin<?> createDefaultSkin() {
 496         return new TooltipSkin(this);
 497     }
 498 
 499 
 500 
 501     /***************************************************************************
 502      *                                                                         *
 503      *                         Stylesheet Handling                             *
 504      *                                                                         *
 505      **************************************************************************/
 506 
 507 
 508     private static final CssMetaData<Tooltip.CSSBridge,Font> FONT =
 509             new FontCssMetaData<Tooltip.CSSBridge>("-fx-font", Font.getDefault()) {
 510 
 511                 @Override
 512                 public boolean isSettable(Tooltip.CSSBridge cssBridge) {
 513                     return !cssBridge.tooltip.fontProperty().isBound();
 514                 }
 515 
 516                 @Override
 517                 public StyleableProperty<Font> getStyleableProperty(Tooltip.CSSBridge cssBridge) {
 518                     return (StyleableProperty<Font>)(WritableValue<Font>)cssBridge.tooltip.fontProperty();
 519                 }
 520             };
 521 
 522     private static final CssMetaData<Tooltip.CSSBridge,TextAlignment> TEXT_ALIGNMENT =
 523             new CssMetaData<Tooltip.CSSBridge,TextAlignment>("-fx-text-alignment",
 524                     new EnumConverter<TextAlignment>(TextAlignment.class),
 525                     TextAlignment.LEFT) {
 526 
 527                 @Override
 528                 public boolean isSettable(Tooltip.CSSBridge cssBridge) {
 529                     return !cssBridge.tooltip.textAlignmentProperty().isBound();
 530                 }
 531 
 532                 @Override
 533                 public StyleableProperty<TextAlignment> getStyleableProperty(Tooltip.CSSBridge cssBridge) {
 534                     return (StyleableProperty<TextAlignment>)(WritableValue<TextAlignment>)cssBridge.tooltip.textAlignmentProperty();
 535                 }
 536             };
 537 
 538     private static final CssMetaData<Tooltip.CSSBridge,OverrunStyle> TEXT_OVERRUN =
 539             new CssMetaData<Tooltip.CSSBridge,OverrunStyle>("-fx-text-overrun",
 540                     new EnumConverter<OverrunStyle>(OverrunStyle.class),
 541                     OverrunStyle.ELLIPSIS) {
 542 
 543                 @Override
 544                 public boolean isSettable(Tooltip.CSSBridge cssBridge) {
 545                     return !cssBridge.tooltip.textOverrunProperty().isBound();
 546                 }
 547 
 548                 @Override
 549                 public StyleableProperty<OverrunStyle> getStyleableProperty(Tooltip.CSSBridge cssBridge) {
 550                     return (StyleableProperty<OverrunStyle>)(WritableValue<OverrunStyle>)cssBridge.tooltip.textOverrunProperty();
 551                 }
 552             };
 553 
 554     private static final CssMetaData<Tooltip.CSSBridge,Boolean> WRAP_TEXT =
 555             new CssMetaData<Tooltip.CSSBridge,Boolean>("-fx-wrap-text",
 556                     BooleanConverter.getInstance(), Boolean.FALSE) {
 557 
 558                 @Override
 559                 public boolean isSettable(Tooltip.CSSBridge cssBridge) {
 560                     return !cssBridge.tooltip.wrapTextProperty().isBound();
 561                 }
 562 
 563                 @Override
 564                 public StyleableProperty<Boolean> getStyleableProperty(Tooltip.CSSBridge cssBridge) {
 565                     return (StyleableProperty<Boolean>)(WritableValue<Boolean>)cssBridge.tooltip.wrapTextProperty();
 566                 }
 567             };
 568 
 569     private static final CssMetaData<Tooltip.CSSBridge,String> GRAPHIC =
 570             new CssMetaData<Tooltip.CSSBridge,String>("-fx-graphic",
 571                     StringConverter.getInstance()) {
 572 
 573                 @Override
 574                 public boolean isSettable(Tooltip.CSSBridge cssBridge) {
 575                     return !cssBridge.tooltip.graphicProperty().isBound();
 576                 }
 577 
 578                 @Override
 579                 public StyleableProperty<String> getStyleableProperty(Tooltip.CSSBridge cssBridge) {
 580                     return (StyleableProperty<String>)cssBridge.tooltip.imageUrlProperty();
 581                 }
 582             };
 583 
 584     private static final CssMetaData<Tooltip.CSSBridge,ContentDisplay> CONTENT_DISPLAY =
 585             new CssMetaData<Tooltip.CSSBridge,ContentDisplay>("-fx-content-display",
 586                     new EnumConverter<ContentDisplay>(ContentDisplay.class),
 587                     ContentDisplay.LEFT) {
 588 
 589                 @Override
 590                 public boolean isSettable(Tooltip.CSSBridge cssBridge) {
 591                     return !cssBridge.tooltip.contentDisplayProperty().isBound();
 592                 }
 593 
 594                 @Override
 595                 public StyleableProperty<ContentDisplay> getStyleableProperty(Tooltip.CSSBridge cssBridge) {
 596                     return (StyleableProperty<ContentDisplay>)(WritableValue<ContentDisplay>)cssBridge.tooltip.contentDisplayProperty();
 597                 }
 598             };
 599 
 600     private static final CssMetaData<Tooltip.CSSBridge,Number> GRAPHIC_TEXT_GAP =
 601             new CssMetaData<Tooltip.CSSBridge,Number>("-fx-graphic-text-gap",
 602                     SizeConverter.getInstance(), 4.0) {
 603 
 604                 @Override
 605                 public boolean isSettable(Tooltip.CSSBridge cssBridge) {
 606                     return !cssBridge.tooltip.graphicTextGapProperty().isBound();
 607                 }
 608 
 609                 @Override
 610                 public StyleableProperty<Number> getStyleableProperty(Tooltip.CSSBridge cssBridge) {
 611                     return (StyleableProperty<Number>)(WritableValue<Number>)cssBridge.tooltip.graphicTextGapProperty();
 612                 }
 613             };
 614 
 615     private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 616     static {
 617         final List<CssMetaData<? extends Styleable, ?>> styleables =
 618                 new ArrayList<CssMetaData<? extends Styleable, ?>>(PopupControl.getClassCssMetaData());
 619         styleables.add(FONT);
 620         styleables.add(TEXT_ALIGNMENT);
 621         styleables.add(TEXT_OVERRUN);
 622         styleables.add(WRAP_TEXT);
 623         styleables.add(GRAPHIC);
 624         styleables.add(CONTENT_DISPLAY);
 625         styleables.add(GRAPHIC_TEXT_GAP);
 626         STYLEABLES = Collections.unmodifiableList(styleables);
 627     }
 628 
 629     /**
 630      * @return The CssMetaData associated with this class, which may include the
 631      * CssMetaData of its super classes.
 632      * @since JavaFX 8.0
 633      */
 634     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 635         return STYLEABLES;
 636     }
 637 
 638     /**
 639      * {@inheritDoc}
 640      * @since JavaFX 8.0
 641      */
 642     @Override
 643     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
 644         return getClassCssMetaData();
 645     }
 646 
 647     @Override public Styleable getStyleableParent() {
 648         if (BEHAVIOR.hoveredNode == null) {
 649             return super.getStyleableParent();
 650         }
 651         return BEHAVIOR.hoveredNode;
 652     }
 653 
 654 
 655 
 656     /***************************************************************************
 657      *                                                                         *
 658      * Support classes                                                         *
 659      *                                                                         *
 660      **************************************************************************/
 661 
 662     private final class CSSBridge extends PopupControl.CSSBridge {
 663         private Tooltip tooltip = Tooltip.this;
 664 
 665         CSSBridge() {
 666             super();
 667             setAccessibleRole(AccessibleRole.TOOLTIP);
 668         }
 669     }
 670 
 671 
 672     private static class TooltipBehavior {
 673         /*
 674          * There are two key concepts with Tooltip: activated and visible. A Tooltip
 675          * is activated as soon as a mouse move occurs over the target node. When it
 676          * becomes activated, we start off the ACTIVATION_TIMER. If the
 677          * ACTIVATION_TIMER expires before another mouse event occurs, then we will
 678          * show the popup. This timer typically lasts about 1 second.
 679          *
 680          * Once visible, we reset the ACTIVATION_TIMER and start the HIDE_TIMER.
 681          * This second timer will allow the tooltip to remain visible for some time
 682          * period (such as 5 seconds). If the mouse hasn't moved, and the HIDE_TIMER
 683          * expires, then the tooltip is hidden and the tooltip is no longer
 684          * activated.
 685          *
 686          * If another mouse move occurs, the ACTIVATION_TIMER starts again, and the
 687          * same rules apply as above.
 688          *
 689          * If a mouse exit event occurs while the HIDE_TIMER is ticking, we reset
 690          * the HIDE_TIMER. Thus, the tooltip disappears after 5 seconds from the
 691          * last mouse move.
 692          *
 693          * If some other mouse event occurs while the HIDE_TIMER is running, other
 694          * than mouse move or mouse enter/exit (such as a click), then the tooltip
 695          * is hidden, the HIDE_TIMER stopped, and activated set to false.
 696          *
 697          * If a mouse exit occurs while the HIDE_TIMER is running, we stop the
 698          * HIDE_TIMER and start the LEFT_TIMER, and immediately hide the tooltip.
 699          * This timer is very short, maybe about a 1/2 second. If the mouse enters a
 700          * new node which also has a tooltip before LEFT_TIMER expires, then the
 701          * second tooltip is activated and shown immediately (the ACTIVATION_TIMER
 702          * having been bypassed), and the HIDE_TIMER is started. If the LEFT_TIMER
 703          * expires and there is no mouse movement over a control with a tooltip,
 704          * then we are back to the initial steady state where the next mouse move
 705          * over a node with a tooltip installed will start the ACTIVATION_TIMER.
 706          */
 707 
 708         private Timeline activationTimer = new Timeline();
 709         private Timeline hideTimer = new Timeline();
 710         private Timeline leftTimer = new Timeline();
 711 
 712         /**
 713          * The Node with a tooltip over which the mouse is hovering. There can
 714          * only be one of these at a time.
 715          */
 716         private Node hoveredNode;
 717 
 718         /**
 719          * The tooltip that is currently activated. There can only be one
 720          * of these at a time.
 721          */
 722         private Tooltip activatedTooltip;
 723 
 724         /**
 725          * The tooltip that is currently visible. There can only be one
 726          * of these at a time.
 727          */
 728         private Tooltip visibleTooltip;
 729 
 730         /**
 731          * The last position of the mouse, in screen coordinates.
 732          */
 733         private double lastMouseX;
 734         private double lastMouseY;
 735 
 736         private boolean hideOnExit;
 737 
 738         TooltipBehavior(Duration openDelay, Duration visibleDuration, Duration closeDelay, final boolean hideOnExit) {
 739             this.hideOnExit = hideOnExit;
 740 
 741             activationTimer.getKeyFrames().add(new KeyFrame(openDelay));
 742             activationTimer.setOnFinished(event -> {
 743                 // Show the currently activated tooltip and start the
 744                 // HIDE_TIMER.
 745                 assert activatedTooltip != null;
 746                 final Window owner = getWindow(hoveredNode);
 747                 final boolean treeVisible = isWindowHierarchyVisible(hoveredNode);
 748 
 749                 // If the ACTIVATED tooltip is part of a visible window
 750                 // hierarchy, we can go ahead and show the tooltip and
 751                 // start the HIDE_TIMER.
 752                 //
 753                 // If the owner is null or invisible, then it either means a
 754                 // bug in our code, the node was removed from a scene or
 755                 // window or made invisible, or the node is not part of a
 756                 // visible window hierarchy. In that case, we don't show the
 757                 // tooltip, and we don't start the HIDE_TIMER. We simply let
 758                 // ACTIVATED_TIMER expire, and wait until the next mouse
 759                 // the movement to start it again.
 760                 if (owner != null && owner.isShowing() && treeVisible) {
 761                     double x = lastMouseX;
 762                     double y = lastMouseY;
 763 
 764                     // The tooltip always inherits the nodeOrientation of
 765                     // the Node that it is attached to (see RT-26147). It
 766                     // is possible to override this for the Tooltip content
 767                     // (but not the popup placement) by setting the
 768                     // nodeOrientation on tooltip.getScene().getRoot().
 769                     NodeOrientation nodeOrientation = hoveredNode.getEffectiveNodeOrientation();
 770                     activatedTooltip.getScene().setNodeOrientation(nodeOrientation);
 771                     if (nodeOrientation == NodeOrientation.RIGHT_TO_LEFT) {
 772                         x -= activatedTooltip.getWidth();
 773                     }
 774 
 775                     activatedTooltip.show(owner, x+TOOLTIP_XOFFSET, y+TOOLTIP_YOFFSET);
 776 
 777                     // RT-37107: Ensure the tooltip is displayed in a position
 778                     // where it will not be under the mouse, even when the tooltip
 779                     // is near the edge of the screen
 780                     if ((y+TOOLTIP_YOFFSET) > activatedTooltip.getAnchorY()) {
 781                         // the tooltip has been shifted vertically upwards,
 782                         // most likely to be underneath the mouse cursor, so we
 783                         // need to shift it further by hiding and reshowing
 784                         // in another location
 785                         activatedTooltip.hide();
 786 
 787                         y -= activatedTooltip.getHeight();
 788                         activatedTooltip.show(owner, x+TOOLTIP_XOFFSET, y);
 789                     }
 790 
 791                     visibleTooltip = activatedTooltip;
 792                     hoveredNode = null;
 793                     hideTimer.playFromStart();
 794                 }
 795 
 796                 // Once the activation timer has expired, the tooltip is no
 797                 // longer in the activated state, it is only in the visible
 798                 // state, so we go ahead and set activated to false
 799                 activatedTooltip.setActivated(false);
 800                 activatedTooltip = null;
 801             });
 802 
 803             hideTimer.getKeyFrames().add(new KeyFrame(visibleDuration));
 804             hideTimer.setOnFinished(event -> {
 805                 // Hide the currently visible tooltip.
 806                 assert visibleTooltip != null;
 807                 visibleTooltip.hide();
 808                 visibleTooltip = null;
 809                 hoveredNode = null;
 810             });
 811 
 812             leftTimer.getKeyFrames().add(new KeyFrame(closeDelay));
 813             leftTimer.setOnFinished(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          * Registers for mouse move events only. When the mouse is moved, this
 826          * handler will detect it and decide whether to start the ACTIVATION_TIMER
 827          * (if the ACTIVATION_TIMER is not started), restart the ACTIVATION_TIMER
 828          * (if ACTIVATION_TIMER is running), or skip the ACTIVATION_TIMER and just
 829          * show the tooltip (if the LEFT_TIMER is running).
 830          */
 831         private EventHandler<MouseEvent> MOVE_HANDLER = (MouseEvent event) -> {
 832             //Screen coordinates need to be actual for dynamic tooltip.
 833             //See Tooltip.setText
 834 
 835             lastMouseX = event.getScreenX();
 836             lastMouseY = event.getScreenY();
 837 
 838             // If the HIDE_TIMER is running, then we don't want this event
 839             // handler to do anything, or change any state at all.
 840             if (hideTimer.getStatus() == Timeline.Status.RUNNING) {
 841                 return;
 842             }
 843 
 844             // Note that the "install" step will both register this handler
 845             // with the target node and also associate the tooltip with the
 846             // target node, by stashing it in the client properties of the node.
 847             hoveredNode = (Node) event.getSource();
 848             Tooltip t = (Tooltip) hoveredNode.getProperties().get(TOOLTIP_PROP_KEY);
 849             if (t != null) {
 850                 // In theory we should never get here with an invisible or
 851                 // non-existant window hierarchy, but might in some cases where
 852                 // people are feeding fake mouse events into the hierarchy. So
 853                 // we'll guard against that case.
 854                 final Window owner = getWindow(hoveredNode);
 855                 final boolean treeVisible = isWindowHierarchyVisible(hoveredNode);
 856                 if (owner != null && treeVisible) {
 857                     // Now we know that the currently HOVERED node has a tooltip
 858                     // and that it is part of a visible window Hierarchy.
 859                     // If LEFT_TIMER is running, then we make this tooltip
 860                     // visible immediately, stop the LEFT_TIMER, and start the
 861                     // HIDE_TIMER.
 862                     if (leftTimer.getStatus() == Timeline.Status.RUNNING) {
 863                         if (visibleTooltip != null) visibleTooltip.hide();
 864                         visibleTooltip = t;
 865                         t.show(owner, event.getScreenX()+TOOLTIP_XOFFSET,
 866                                 event.getScreenY()+TOOLTIP_YOFFSET);
 867                         leftTimer.stop();
 868                         hideTimer.playFromStart();
 869                     } else {
 870                         // Start / restart the timer and make sure the tooltip
 871                         // is marked as activated.
 872                         t.setActivated(true);
 873                         activatedTooltip = t;
 874                         activationTimer.stop();
 875                         activationTimer.playFromStart();
 876                     }
 877                 }
 878             } else {
 879                 // TODO should deregister, no point being here anymore!
 880             }
 881         };
 882 
 883         /**
 884          * Registers for mouse exit events. If the ACTIVATION_TIMER is running then
 885          * this will simply stop it. If the HIDE_TIMER is running then this will
 886          * stop the HIDE_TIMER, hide the tooltip, and start the LEFT_TIMER.
 887          */
 888         private EventHandler<MouseEvent> LEAVING_HANDLER = (MouseEvent event) -> {
 889             // detect bogus mouse exit events, if it didn't really move then ignore it
 890             if (activationTimer.getStatus() == Timeline.Status.RUNNING) {
 891                 activationTimer.stop();
 892             } else if (hideTimer.getStatus() == Timeline.Status.RUNNING) {
 893                 assert visibleTooltip != null;
 894                 hideTimer.stop();
 895                 if (hideOnExit) visibleTooltip.hide();
 896                 leftTimer.playFromStart();
 897             }
 898 
 899             hoveredNode = null;
 900             activatedTooltip = null;
 901             if (hideOnExit) visibleTooltip = null;
 902         };
 903 
 904         /**
 905          * Registers for mouse click, press, release, drag events. If any of these
 906          * occur, then the tooltip is hidden (if it is visible), it is deactivated,
 907          * and any and all timers are stopped.
 908          */
 909         private EventHandler<MouseEvent> KILL_HANDLER = (MouseEvent event) -> {
 910             activationTimer.stop();
 911             hideTimer.stop();
 912             leftTimer.stop();
 913             if (visibleTooltip != null) visibleTooltip.hide();
 914             hoveredNode = null;
 915             activatedTooltip = null;
 916             visibleTooltip = null;
 917         };
 918 
 919         private void install(Node node, Tooltip t) {
 920             // Install the MOVE_HANDLER, LEAVING_HANDLER, and KILL_HANDLER on
 921             // the given node. Stash the tooltip in the node's client properties
 922             // map so that it is not gc'd. The handlers must all be installed
 923             // with a TODO weak reference so as not to cause a memory leak
 924             if (node == null) return;
 925             node.addEventHandler(MouseEvent.MOUSE_MOVED, MOVE_HANDLER);
 926             node.addEventHandler(MouseEvent.MOUSE_EXITED, LEAVING_HANDLER);
 927             node.addEventHandler(MouseEvent.MOUSE_PRESSED, KILL_HANDLER);
 928             node.getProperties().put(TOOLTIP_PROP_KEY, t);
 929         }
 930 
 931         private void uninstall(Node node) {
 932             if (node == null) return;
 933             node.removeEventHandler(MouseEvent.MOUSE_MOVED, MOVE_HANDLER);
 934             node.removeEventHandler(MouseEvent.MOUSE_EXITED, LEAVING_HANDLER);
 935             node.removeEventHandler(MouseEvent.MOUSE_PRESSED, KILL_HANDLER);
 936             Tooltip t = (Tooltip)node.getProperties().get(TOOLTIP_PROP_KEY);
 937             if (t != null) {
 938                 node.getProperties().remove(TOOLTIP_PROP_KEY);
 939                 if (t.equals(visibleTooltip) || t.equals(activatedTooltip)) {
 940                     KILL_HANDLER.handle(null);
 941                 }
 942             }
 943         }
 944 
 945         /**
 946          * Gets the top level window associated with this node.
 947          * @param node the node
 948          * @return the top level window
 949          */
 950         private Window getWindow(final Node node) {
 951             final Scene scene = node == null ? null : node.getScene();
 952             return scene == null ? null : scene.getWindow();
 953         }
 954 
 955         /**
 956          * Gets whether the entire window hierarchy is visible for this node.
 957          * @param node the node to check
 958          * @return true if entire hierarchy is visible
 959          */
 960         private boolean isWindowHierarchyVisible(Node node) {
 961             boolean treeVisible = node != null;
 962             Parent parent = node == null ? null : node.getParent();
 963             while (parent != null && treeVisible) {
 964                 treeVisible = parent.isVisible();
 965                 parent = parent.getParent();
 966             }
 967             return treeVisible;
 968         }
 969     }
 970 }