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 }