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