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