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