1 /*
   2  * Copyright (c) 2010, 2015, 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             StyleOrigin origin = ((StyleableObjectProperty<Font>)font).getStyleOrigin();
 286             if (origin == null || (value != null ? !value.equals(oldValue) : oldValue != null)) {
 287                 super.set(value);
 288             }
 289         }
 290 
 291         @Override protected void invalidated() {
 292             // RT-20727 - if font is changed by calling setFont, then
 293             // css might need to be reapplied since font size affects
 294             // calculated values for styles with relative values
 295             if(fontSetByCss == false) {
 296                 Tooltip.this.bridge.impl_reapplyCSS();
 297             }
 298         }
 299 
 300         @Override public CssMetaData<Tooltip.CSSBridge,Font> getCssMetaData() {
 301             return FONT;
 302         }
 303 
 304         @Override public Object getBean() {
 305             return Tooltip.this;
 306         }
 307 
 308         @Override public String getName() {
 309             return "font";
 310         }
 311     };
 312 
 313     /**
 314      * An optional icon for the Tooltip. This can be positioned relative to the
 315      * text by using the {@link #contentDisplayProperty() content display} 
 316      * property.
 317      * The node specified for this variable cannot appear elsewhere in the
 318      * scene graph, otherwise the {@code IllegalArgumentException} is thrown.
 319      * See the class description of {@link javafx.scene.Node Node} for more detail.
 320      */
 321     public final ObjectProperty<Node> graphicProperty() {
 322         return graphic;
 323     }
 324     public final void setGraphic(Node value) {
 325         graphicProperty().setValue(value);
 326     }
 327     public final Node getGraphic() {
 328         return graphicProperty().getValue();
 329     }
 330     private final ObjectProperty<Node> graphic = new StyleableObjectProperty<Node>() {
 331         // The graphic is styleable by css, but it is the
 332         // imageUrlProperty that handles the style value.
 333         @Override public CssMetaData getCssMetaData() {
 334             return GRAPHIC;
 335         }
 336 
 337         @Override public Object getBean() {
 338             return Tooltip.this;
 339         }
 340 
 341         @Override public String getName() {
 342             return "graphic";
 343         }
 344 
 345     };
 346 
 347     private StyleableStringProperty imageUrlProperty() {
 348         if (imageUrl == null) {
 349             imageUrl = new StyleableStringProperty() {
 350                 // If imageUrlProperty is invalidated, this is the origin of the style that
 351                 // triggered the invalidation. This is used in the invaildated() method where the
 352                 // value of super.getStyleOrigin() is not valid until after the call to set(v) returns,
 353                 // by which time invalidated will have been called.
 354                 // This value is initialized to USER in case someone calls set on the imageUrlProperty, which
 355                 // is possible:
 356                 //     CssMetaData metaData = ((StyleableProperty)labeled.graphicProperty()).getCssMetaData();
 357                 //     StyleableProperty prop = metaData.getStyleableProperty(labeled);
 358                 //     prop.set(someUrl);
 359                 //
 360                 // TODO: Note that prop != labeled, which violates the contract between StyleableProperty and CssMetaData.
 361                 StyleOrigin origin = StyleOrigin.USER;
 362 
 363                 @Override public void applyStyle(StyleOrigin origin, String v) {
 364 
 365                     this.origin = origin;
 366 
 367                     // Don't want applyStyle to throw an exception which would leave this.origin set to the wrong value
 368                     if (graphic == null || graphic.isBound() == false) super.applyStyle(origin, v);
 369 
 370                     // Origin is only valid for this invocation of applyStyle, so reset it to USER in case someone calls set.
 371                     this.origin = StyleOrigin.USER;
 372                 }
 373 
 374                 @Override protected void invalidated() {
 375 
 376                     // need to call super.get() here since get() is overridden to return the graphicProperty's value
 377                     final String url = super.get();
 378 
 379                     if (url == null) {
 380                         ((StyleableProperty<Node>)(WritableValue<Node>)graphicProperty()).applyStyle(origin, null);
 381                     } else {
 382                         // RT-34466 - if graphic's url is the same as this property's value, then don't overwrite.
 383                         final Node graphicNode = Tooltip.this.getGraphic();
 384                         if (graphicNode instanceof ImageView) {
 385                             final ImageView imageView = (ImageView)graphicNode;
 386                             final Image image = imageView.getImage();
 387                             if (image != null) {
 388                                 final String imageViewUrl = image.impl_getUrl();
 389                                 if (url.equals(imageViewUrl)) return;
 390                             }
 391 
 392                         }
 393 
 394                         final Image img = StyleManager.getInstance().getCachedImage(url);
 395 
 396                         if (img != null) {
 397                             // Note that it is tempting to try to re-use existing ImageView simply by setting
 398                             // the image on the current ImageView, if there is one. This would effectively change
 399                             // the image, but not the ImageView which means that no graphicProperty listeners would
 400                             // be notified. This is probably not what we want.
 401 
 402                             // Have to call applyStyle on graphicProperty so that the graphicProperty's
 403                             // origin matches the imageUrlProperty's origin.
 404                             ((StyleableProperty<Node>)(WritableValue<Node>)graphicProperty()).applyStyle(origin, new ImageView(img));
 405                         }
 406                     }
 407                 }
 408 
 409                 @Override public String get() {
 410                     // The value of the imageUrlProperty is that of the graphicProperty.
 411                     // Return the value in a way that doesn't expand the graphicProperty.
 412                     final Node graphic = getGraphic();
 413                     if (graphic instanceof ImageView) {
 414                         final Image image = ((ImageView)graphic).getImage();
 415                         if (image != null) {
 416                             return image.impl_getUrl();
 417                         }
 418                     }
 419                     return null;
 420                 }
 421 
 422                 @Override public StyleOrigin getStyleOrigin() {
 423                     // The origin of the imageUrlProperty is that of the graphicProperty.
 424                     // Return the origin in a way that doesn't expand the graphicProperty.
 425                     return graphic != null ? ((StyleableProperty<Node>)(WritableValue<Node>)graphic).getStyleOrigin() : null;
 426                 }
 427 
 428                 @Override public Object getBean() {
 429                     return Tooltip.this;
 430                 }
 431 
 432                 @Override public String getName() {
 433                     return "imageUrl";
 434                 }
 435 
 436                 @Override public CssMetaData<Tooltip.CSSBridge,String> getCssMetaData() {
 437                     return GRAPHIC;
 438                 }
 439             };
 440         }
 441         return imageUrl;
 442     }
 443 
 444     private StyleableStringProperty imageUrl = null;
 445 
 446     /**
 447      * Specifies the positioning of the graphic relative to the text.
 448      */
 449     public final ObjectProperty<ContentDisplay> contentDisplayProperty() {
 450         return contentDisplay;
 451     }
 452     public final void setContentDisplay(ContentDisplay value) {
 453         contentDisplayProperty().setValue(value);
 454     }
 455     public final ContentDisplay getContentDisplay() {
 456         return contentDisplayProperty().getValue();
 457     }
 458     private final ObjectProperty<ContentDisplay> contentDisplay =
 459             new SimpleStyleableObjectProperty<>(CONTENT_DISPLAY, this, "contentDisplay", ContentDisplay.LEFT);
 460 
 461     /**
 462      * The amount of space between the graphic and text
 463      */
 464     public final DoubleProperty graphicTextGapProperty() {
 465         return graphicTextGap;
 466     }
 467     public final void setGraphicTextGap(double value) {
 468         graphicTextGapProperty().setValue(value);
 469     }
 470     public final double getGraphicTextGap() {
 471         return graphicTextGapProperty().getValue();
 472     }
 473     private final DoubleProperty graphicTextGap =
 474             new SimpleStyleableDoubleProperty(GRAPHIC_TEXT_GAP, this, "graphicTextGap", 4d);
 475 
 476     /**
 477      * Typically, the tooltip is "activated" when the mouse moves over a Control.
 478      * There is usually some delay between when the Tooltip becomes "activated"
 479      * and when it is actually shown. The details (such as the amount of delay, etc)
 480      * is left to the Skin implementation.
 481      */
 482     private final ReadOnlyBooleanWrapper activated = new ReadOnlyBooleanWrapper(this, "activated");
 483     final void setActivated(boolean value) { activated.set(value); }
 484     public final boolean isActivated() { return activated.get(); }
 485     public final ReadOnlyBooleanProperty activatedProperty() { return activated.getReadOnlyProperty(); }
 486 
 487 
 488 
 489     /***************************************************************************
 490      *                                                                         *
 491      * Methods                                                                 *
 492      *                                                                         *
 493      **************************************************************************/
 494 
 495     /** {@inheritDoc} */
 496     @Override protected Skin<?> createDefaultSkin() {
 497         return new TooltipSkin(this);
 498     }
 499 
 500 
 501 
 502     /***************************************************************************
 503      *                                                                         *
 504      *                         Stylesheet Handling                             *
 505      *                                                                         *
 506      **************************************************************************/
 507 
 508 
 509     private static final CssMetaData<Tooltip.CSSBridge,Font> FONT =
 510             new FontCssMetaData<Tooltip.CSSBridge>("-fx-font", Font.getDefault()) {
 511 
 512                 @Override
 513                 public boolean isSettable(Tooltip.CSSBridge cssBridge) {
 514                     return !cssBridge.tooltip.fontProperty().isBound();
 515                 }
 516 
 517                 @Override
 518                 public StyleableProperty<Font> getStyleableProperty(Tooltip.CSSBridge cssBridge) {
 519                     return (StyleableProperty<Font>)(WritableValue<Font>)cssBridge.tooltip.fontProperty();
 520                 }
 521             };
 522 
 523     private static final CssMetaData<Tooltip.CSSBridge,TextAlignment> TEXT_ALIGNMENT =
 524             new CssMetaData<Tooltip.CSSBridge,TextAlignment>("-fx-text-alignment",
 525                     new EnumConverter<TextAlignment>(TextAlignment.class),
 526                     TextAlignment.LEFT) {
 527 
 528                 @Override
 529                 public boolean isSettable(Tooltip.CSSBridge cssBridge) {
 530                     return !cssBridge.tooltip.textAlignmentProperty().isBound();
 531                 }
 532 
 533                 @Override
 534                 public StyleableProperty<TextAlignment> getStyleableProperty(Tooltip.CSSBridge cssBridge) {
 535                     return (StyleableProperty<TextAlignment>)(WritableValue<TextAlignment>)cssBridge.tooltip.textAlignmentProperty();
 536                 }
 537             };
 538 
 539     private static final CssMetaData<Tooltip.CSSBridge,OverrunStyle> TEXT_OVERRUN =
 540             new CssMetaData<Tooltip.CSSBridge,OverrunStyle>("-fx-text-overrun",
 541                     new EnumConverter<OverrunStyle>(OverrunStyle.class),
 542                     OverrunStyle.ELLIPSIS) {
 543 
 544                 @Override
 545                 public boolean isSettable(Tooltip.CSSBridge cssBridge) {
 546                     return !cssBridge.tooltip.textOverrunProperty().isBound();
 547                 }
 548 
 549                 @Override
 550                 public StyleableProperty<OverrunStyle> getStyleableProperty(Tooltip.CSSBridge cssBridge) {
 551                     return (StyleableProperty<OverrunStyle>)(WritableValue<OverrunStyle>)cssBridge.tooltip.textOverrunProperty();
 552                 }
 553             };
 554 
 555     private static final CssMetaData<Tooltip.CSSBridge,Boolean> WRAP_TEXT =
 556             new CssMetaData<Tooltip.CSSBridge,Boolean>("-fx-wrap-text",
 557                     BooleanConverter.getInstance(), Boolean.FALSE) {
 558 
 559                 @Override
 560                 public boolean isSettable(Tooltip.CSSBridge cssBridge) {
 561                     return !cssBridge.tooltip.wrapTextProperty().isBound();
 562                 }
 563 
 564                 @Override
 565                 public StyleableProperty<Boolean> getStyleableProperty(Tooltip.CSSBridge cssBridge) {
 566                     return (StyleableProperty<Boolean>)(WritableValue<Boolean>)cssBridge.tooltip.wrapTextProperty();
 567                 }
 568             };
 569 
 570     private static final CssMetaData<Tooltip.CSSBridge,String> GRAPHIC =
 571             new CssMetaData<Tooltip.CSSBridge,String>("-fx-graphic",
 572                     StringConverter.getInstance()) {
 573 
 574                 @Override
 575                 public boolean isSettable(Tooltip.CSSBridge cssBridge) {
 576                     return !cssBridge.tooltip.graphicProperty().isBound();
 577                 }
 578 
 579                 @Override
 580                 public StyleableProperty<String> getStyleableProperty(Tooltip.CSSBridge cssBridge) {
 581                     return (StyleableProperty<String>)cssBridge.tooltip.imageUrlProperty();
 582                 }
 583             };
 584 
 585     private static final CssMetaData<Tooltip.CSSBridge,ContentDisplay> CONTENT_DISPLAY =
 586             new CssMetaData<Tooltip.CSSBridge,ContentDisplay>("-fx-content-display",
 587                     new EnumConverter<ContentDisplay>(ContentDisplay.class),
 588                     ContentDisplay.LEFT) {
 589 
 590                 @Override
 591                 public boolean isSettable(Tooltip.CSSBridge cssBridge) {
 592                     return !cssBridge.tooltip.contentDisplayProperty().isBound();
 593                 }
 594 
 595                 @Override
 596                 public StyleableProperty<ContentDisplay> getStyleableProperty(Tooltip.CSSBridge cssBridge) {
 597                     return (StyleableProperty<ContentDisplay>)(WritableValue<ContentDisplay>)cssBridge.tooltip.contentDisplayProperty();
 598                 }
 599             };
 600 
 601     private static final CssMetaData<Tooltip.CSSBridge,Number> GRAPHIC_TEXT_GAP =
 602             new CssMetaData<Tooltip.CSSBridge,Number>("-fx-graphic-text-gap",
 603                     SizeConverter.getInstance(), 4.0) {
 604 
 605                 @Override
 606                 public boolean isSettable(Tooltip.CSSBridge cssBridge) {
 607                     return !cssBridge.tooltip.graphicTextGapProperty().isBound();
 608                 }
 609 
 610                 @Override
 611                 public StyleableProperty<Number> getStyleableProperty(Tooltip.CSSBridge cssBridge) {
 612                     return (StyleableProperty<Number>)(WritableValue<Number>)cssBridge.tooltip.graphicTextGapProperty();
 613                 }
 614             };
 615 
 616     private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 617     static {
 618         final List<CssMetaData<? extends Styleable, ?>> styleables =
 619                 new ArrayList<CssMetaData<? extends Styleable, ?>>(PopupControl.getClassCssMetaData());
 620         styleables.add(FONT);
 621         styleables.add(TEXT_ALIGNMENT);
 622         styleables.add(TEXT_OVERRUN);
 623         styleables.add(WRAP_TEXT);
 624         styleables.add(GRAPHIC);
 625         styleables.add(CONTENT_DISPLAY);
 626         styleables.add(GRAPHIC_TEXT_GAP);
 627         STYLEABLES = Collections.unmodifiableList(styleables);
 628     }
 629 
 630     /**
 631      * @return The CssMetaData associated with this class, which may include the
 632      * CssMetaData of its super classes.
 633      * @since JavaFX 8.0
 634      */
 635     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 636         return STYLEABLES;
 637     }
 638 
 639     /**
 640      * {@inheritDoc}
 641      * @since JavaFX 8.0
 642      */
 643     @Override
 644     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
 645         return getClassCssMetaData();
 646     }
 647 
 648     @Override public Styleable getStyleableParent() {
 649         if (BEHAVIOR.hoveredNode == null) {
 650             return super.getStyleableParent();
 651         }
 652         return BEHAVIOR.hoveredNode;
 653     }
 654 
 655 
 656 
 657     /***************************************************************************
 658      *                                                                         *
 659      * Support classes                                                         *
 660      *                                                                         *
 661      **************************************************************************/
 662 
 663     private final class CSSBridge extends PopupControl.CSSBridge {
 664         private Tooltip tooltip = Tooltip.this;
 665 
 666         CSSBridge() {
 667             super();
 668             setAccessibleRole(AccessibleRole.TOOLTIP);
 669         }
 670     }
 671 
 672 
 673     private static class TooltipBehavior {
 674         /*
 675          * There are two key concepts with Tooltip: activated and visible. A Tooltip
 676          * is activated as soon as a mouse move occurs over the target node. When it
 677          * becomes activated, we start off the ACTIVATION_TIMER. If the
 678          * ACTIVATION_TIMER expires before another mouse event occurs, then we will
 679          * show the popup. This timer typically lasts about 1 second.
 680          *
 681          * Once visible, we reset the ACTIVATION_TIMER and start the HIDE_TIMER.
 682          * This second timer will allow the tooltip to remain visible for some time
 683          * period (such as 5 seconds). If the mouse hasn't moved, and the HIDE_TIMER
 684          * expires, then the tooltip is hidden and the tooltip is no longer
 685          * activated.
 686          *
 687          * If another mouse move occurs, the ACTIVATION_TIMER starts again, and the
 688          * same rules apply as above.
 689          *
 690          * If a mouse exit event occurs while the HIDE_TIMER is ticking, we reset
 691          * the HIDE_TIMER. Thus, the tooltip disappears after 5 seconds from the
 692          * last mouse move.
 693          *
 694          * If some other mouse event occurs while the HIDE_TIMER is running, other
 695          * than mouse move or mouse enter/exit (such as a click), then the tooltip
 696          * is hidden, the HIDE_TIMER stopped, and activated set to false.
 697          *
 698          * If a mouse exit occurs while the HIDE_TIMER is running, we stop the
 699          * HIDE_TIMER and start the LEFT_TIMER, and immediately hide the tooltip.
 700          * This timer is very short, maybe about a 1/2 second. If the mouse enters a
 701          * new node which also has a tooltip before LEFT_TIMER expires, then the
 702          * second tooltip is activated and shown immediately (the ACTIVATION_TIMER
 703          * having been bypassed), and the HIDE_TIMER is started. If the LEFT_TIMER
 704          * expires and there is no mouse movement over a control with a tooltip,
 705          * then we are back to the initial steady state where the next mouse move
 706          * over a node with a tooltip installed will start the ACTIVATION_TIMER.
 707          */
 708 
 709         private Timeline activationTimer = new Timeline();
 710         private Timeline hideTimer = new Timeline();
 711         private Timeline leftTimer = new Timeline();
 712 
 713         /**
 714          * The Node with a tooltip over which the mouse is hovering. There can
 715          * only be one of these at a time.
 716          */
 717         private Node hoveredNode;
 718 
 719         /**
 720          * The tooltip that is currently activated. There can only be one
 721          * of these at a time.
 722          */
 723         private Tooltip activatedTooltip;
 724 
 725         /**
 726          * The tooltip that is currently visible. There can only be one
 727          * of these at a time.
 728          */
 729         private Tooltip visibleTooltip;
 730 
 731         /**
 732          * The last position of the mouse, in screen coordinates.
 733          */
 734         private double lastMouseX;
 735         private double lastMouseY;
 736 
 737         private boolean hideOnExit;
 738 
 739         TooltipBehavior(Duration openDelay, Duration visibleDuration, Duration closeDelay, final boolean hideOnExit) {
 740             this.hideOnExit = hideOnExit;
 741 
 742             activationTimer.getKeyFrames().add(new KeyFrame(openDelay));
 743             activationTimer.setOnFinished(event -> {
 744                 // Show the currently activated tooltip and start the
 745                 // HIDE_TIMER.
 746                 assert activatedTooltip != null;
 747                 final Window owner = getWindow(hoveredNode);
 748                 final boolean treeVisible = isWindowHierarchyVisible(hoveredNode);
 749 
 750                 // If the ACTIVATED tooltip is part of a visible window
 751                 // hierarchy, we can go ahead and show the tooltip and
 752                 // start the HIDE_TIMER.
 753                 //
 754                 // If the owner is null or invisible, then it either means a
 755                 // bug in our code, the node was removed from a scene or
 756                 // window or made invisible, or the node is not part of a
 757                 // visible window hierarchy. In that case, we don't show the
 758                 // tooltip, and we don't start the HIDE_TIMER. We simply let
 759                 // ACTIVATED_TIMER expire, and wait until the next mouse
 760                 // the movement to start it again.
 761                 if (owner != null && owner.isShowing() && treeVisible) {
 762                     double x = lastMouseX;
 763                     double y = lastMouseY;
 764 
 765                     // The tooltip always inherits the nodeOrientation of
 766                     // the Node that it is attached to (see RT-26147). It
 767                     // is possible to override this for the Tooltip content
 768                     // (but not the popup placement) by setting the
 769                     // nodeOrientation on tooltip.getScene().getRoot().
 770                     NodeOrientation nodeOrientation = hoveredNode.getEffectiveNodeOrientation();
 771                     activatedTooltip.getScene().setNodeOrientation(nodeOrientation);
 772                     if (nodeOrientation == NodeOrientation.RIGHT_TO_LEFT) {
 773                         x -= activatedTooltip.getWidth();
 774                     }
 775 
 776                     activatedTooltip.show(owner, x+TOOLTIP_XOFFSET, y+TOOLTIP_YOFFSET);
 777 
 778                     // RT-37107: Ensure the tooltip is displayed in a position
 779                     // where it will not be under the mouse, even when the tooltip
 780                     // is near the edge of the screen
 781                     if ((y+TOOLTIP_YOFFSET) > activatedTooltip.getAnchorY()) {
 782                         // the tooltip has been shifted vertically upwards,
 783                         // most likely to be underneath the mouse cursor, so we
 784                         // need to shift it further by hiding and reshowing
 785                         // in another location
 786                         activatedTooltip.hide();
 787 
 788                         y -= activatedTooltip.getHeight();
 789                         activatedTooltip.show(owner, x+TOOLTIP_XOFFSET, y);
 790                     }
 791 
 792                     visibleTooltip = activatedTooltip;
 793                     hoveredNode = null;
 794                     hideTimer.playFromStart();
 795                 }
 796 
 797                 // Once the activation timer has expired, the tooltip is no
 798                 // longer in the activated state, it is only in the visible
 799                 // state, so we go ahead and set activated to false
 800                 activatedTooltip.setActivated(false);
 801                 activatedTooltip = null;
 802             });
 803 
 804             hideTimer.getKeyFrames().add(new KeyFrame(visibleDuration));
 805             hideTimer.setOnFinished(event -> {
 806                 // Hide the currently visible tooltip.
 807                 assert visibleTooltip != null;
 808                 visibleTooltip.hide();
 809                 visibleTooltip = null;
 810                 hoveredNode = null;
 811             });
 812 
 813             leftTimer.getKeyFrames().add(new KeyFrame(closeDelay));
 814             leftTimer.setOnFinished(event -> {
 815                 if (!hideOnExit) {
 816                     // Hide the currently visible tooltip.
 817                     assert visibleTooltip != null;
 818                     visibleTooltip.hide();
 819                     visibleTooltip = null;
 820                     hoveredNode = null;
 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 = (MouseEvent event) -> {
 833             //Screen coordinates need to be actual for dynamic tooltip.
 834             //See Tooltip.setText
 835 
 836             lastMouseX = event.getScreenX();
 837             lastMouseY = event.getScreenY();
 838 
 839             // If the HIDE_TIMER is running, then we don't want this event
 840             // handler to do anything, or change any state at all.
 841             if (hideTimer.getStatus() == Timeline.Status.RUNNING) {
 842                 return;
 843             }
 844 
 845             // Note that the "install" step will both register this handler
 846             // with the target node and also associate the tooltip with the
 847             // target node, by stashing it in the client properties of the node.
 848             hoveredNode = (Node) event.getSource();
 849             Tooltip t = (Tooltip) hoveredNode.getProperties().get(TOOLTIP_PROP_KEY);
 850             if (t != null) {
 851                 // In theory we should never get here with an invisible or
 852                 // non-existant window hierarchy, but might in some cases where
 853                 // people are feeding fake mouse events into the hierarchy. So
 854                 // we'll guard against that case.
 855                 final Window owner = getWindow(hoveredNode);
 856                 final boolean treeVisible = isWindowHierarchyVisible(hoveredNode);
 857                 if (owner != null && treeVisible) {
 858                     // Now we know that the currently HOVERED node has a tooltip
 859                     // and that it is part of a visible window Hierarchy.
 860                     // If LEFT_TIMER is running, then we make this tooltip
 861                     // visible immediately, stop the LEFT_TIMER, and start the
 862                     // HIDE_TIMER.
 863                     if (leftTimer.getStatus() == Timeline.Status.RUNNING) {
 864                         if (visibleTooltip != null) visibleTooltip.hide();
 865                         visibleTooltip = t;
 866                         t.show(owner, event.getScreenX()+TOOLTIP_XOFFSET,
 867                                 event.getScreenY()+TOOLTIP_YOFFSET);
 868                         leftTimer.stop();
 869                         hideTimer.playFromStart();
 870                     } else {
 871                         // Start / restart the timer and make sure the tooltip
 872                         // is marked as activated.
 873                         t.setActivated(true);
 874                         activatedTooltip = t;
 875                         activationTimer.stop();
 876                         activationTimer.playFromStart();
 877                     }
 878                 }
 879             } else {
 880                 // TODO should deregister, no point being here anymore!
 881             }
 882         };
 883 
 884         /**
 885          * Registers for mouse exit events. If the ACTIVATION_TIMER is running then
 886          * this will simply stop it. If the HIDE_TIMER is running then this will
 887          * stop the HIDE_TIMER, hide the tooltip, and start the LEFT_TIMER.
 888          */
 889         private EventHandler<MouseEvent> LEAVING_HANDLER = (MouseEvent event) -> {
 890             // detect bogus mouse exit events, if it didn't really move then ignore it
 891             if (activationTimer.getStatus() == Timeline.Status.RUNNING) {
 892                 activationTimer.stop();
 893             } else if (hideTimer.getStatus() == Timeline.Status.RUNNING) {
 894                 assert visibleTooltip != null;
 895                 hideTimer.stop();
 896                 if (hideOnExit) visibleTooltip.hide();
 897                 leftTimer.playFromStart();
 898             }
 899 
 900             hoveredNode = null;
 901             activatedTooltip = null;
 902             if (hideOnExit) visibleTooltip = null;
 903         };
 904 
 905         /**
 906          * Registers for mouse click, press, release, drag events. If any of these
 907          * occur, then the tooltip is hidden (if it is visible), it is deactivated,
 908          * and any and all timers are stopped.
 909          */
 910         private EventHandler<MouseEvent> KILL_HANDLER = (MouseEvent event) -> {
 911             activationTimer.stop();
 912             hideTimer.stop();
 913             leftTimer.stop();
 914             if (visibleTooltip != null) visibleTooltip.hide();
 915             hoveredNode = null;
 916             activatedTooltip = null;
 917             visibleTooltip = null;
 918         };
 919 
 920         private void install(Node node, Tooltip t) {
 921             // Install the MOVE_HANDLER, LEAVING_HANDLER, and KILL_HANDLER on
 922             // the given node. Stash the tooltip in the node's client properties
 923             // map so that it is not gc'd. The handlers must all be installed
 924             // with a TODO weak reference so as not to cause a memory leak
 925             if (node == null) return;
 926             node.addEventHandler(MouseEvent.MOUSE_MOVED, MOVE_HANDLER);
 927             node.addEventHandler(MouseEvent.MOUSE_EXITED, LEAVING_HANDLER);
 928             node.addEventHandler(MouseEvent.MOUSE_PRESSED, KILL_HANDLER);
 929             node.getProperties().put(TOOLTIP_PROP_KEY, t);
 930         }
 931 
 932         private void uninstall(Node node) {
 933             if (node == null) return;
 934             node.removeEventHandler(MouseEvent.MOUSE_MOVED, MOVE_HANDLER);
 935             node.removeEventHandler(MouseEvent.MOUSE_EXITED, LEAVING_HANDLER);
 936             node.removeEventHandler(MouseEvent.MOUSE_PRESSED, KILL_HANDLER);
 937             Tooltip t = (Tooltip)node.getProperties().get(TOOLTIP_PROP_KEY);
 938             if (t != null) {
 939                 node.getProperties().remove(TOOLTIP_PROP_KEY);
 940                 if (t.equals(visibleTooltip) || t.equals(activatedTooltip)) {
 941                     KILL_HANDLER.handle(null);
 942                 }
 943             }
 944         }
 945 
 946         /**
 947          * Gets the top level window associated with this node.
 948          * @param node the node
 949          * @return the top level window
 950          */
 951         private Window getWindow(final Node node) {
 952             final Scene scene = node == null ? null : node.getScene();
 953             return scene == null ? null : scene.getWindow();
 954         }
 955 
 956         /**
 957          * Gets whether the entire window hierarchy is visible for this node.
 958          * @param node the node to check
 959          * @return true if entire hierarchy is visible
 960          */
 961         private boolean isWindowHierarchyVisible(Node node) {
 962             boolean treeVisible = node != null;
 963             Parent parent = node == null ? null : node.getParent();
 964             while (parent != null && treeVisible) {
 965                 treeVisible = parent.isVisible();
 966                 parent = parent.getParent();
 967             }
 968             return treeVisible;
 969         }
 970     }
 971 }