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