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