Spec-Zone .ru
спецификации, руководства, описания, API
|
001/* 002 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. 003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 004 * 005 * This code is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU General Public License version 2 only, as 007 * published by the Free Software Foundation. Oracle designates this 008 * particular file as subject to the "Classpath" exception as provided 009 * by Oracle in the LICENSE file that accompanied this code. 010 * 011 * This code is distributed in the hope that it will be useful, but WITHOUT 012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 013 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 014 * version 2 for more details (a copy is included in the LICENSE file that 015 * accompanied this code). 016 * 017 * You should have received a copy of the GNU General Public License version 018 * 2 along with this work; if not, write to the Free Software Foundation, 019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 020 * 021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 022 * or visit www.oracle.com if you need additional information or have any 023 * questions. 024 */ 025 026package javafx.scene.control; 027 028 029import com.sun.javafx.css.StyleManager; 030import javafx.css.StyleableBooleanProperty; 031import javafx.css.StyleableDoubleProperty; 032import javafx.css.StyleableObjectProperty; 033import javafx.css.StyleableStringProperty; 034import com.sun.javafx.css.converters.BooleanConverter; 035import com.sun.javafx.css.converters.EnumConverter; 036import com.sun.javafx.css.converters.SizeConverter; 037import com.sun.javafx.css.converters.StringConverter; 038import com.sun.javafx.scene.control.skin.TooltipSkin; 039import java.net.MalformedURLException; 040import java.net.URL; 041import java.util.ArrayList; 042import java.util.Collections; 043import java.util.List; 044 045import javafx.animation.KeyFrame; 046import javafx.animation.Timeline; 047import javafx.beans.property.*; 048import javafx.css.CssMetaData; 049import javafx.css.FontCssMetaData; 050import javafx.css.Styleable; 051import javafx.css.StyleableProperty; 052import javafx.event.ActionEvent; 053import javafx.event.EventHandler; 054import javafx.geometry.NodeOrientation; 055import javafx.scene.Node; 056import javafx.scene.Parent; 057import javafx.scene.Scene; 058import javafx.scene.image.Image; 059import javafx.scene.image.ImageView; 060import javafx.scene.input.MouseEvent; 061import javafx.scene.text.Font; 062import javafx.scene.text.TextAlignment; 063import javafx.stage.Window; 064import javafx.util.Duration; 065 066 067/** 068 * Tooltips are common UI elements which are typically used for showing 069 * additional information about a Node in the scenegraph when the Node is 070 * hovered over by the mouse. Any Node can show a tooltip. In most cases a 071 * Tooltip is created and its {@link #textProperty() text} property is modified 072 * to show plain text to the user. However, a Tooltip is able to show within it 073 * an arbitrary scenegraph of nodes - this is done by creating the scenegraph 074 * and setting it inside the Tooltip {@link #graphicProperty() graphic} 075 * property. 076 * 077 * <p>You use the following approach to set a Tooltip on any node: 078 * 079 * <pre> 080 * Rectangle rect = new Rectangle(0, 0, 100, 100); 081 * Tooltip t = new Tooltip("A Square"); 082 * Tooltip.install(rect, t); 083 * </pre> 084 * 085 * This tooltip will then participate with the typical tooltip semantics (i.e. 086 * appearing on hover, etc). Note that the Tooltip does not have to be 087 * uninstalled: it will be garbage collected when it is not referenced by any 088 * Node. It is possible to manually uninstall the tooltip, however. 089 * 090 * <p>A single tooltip can be installed on multiple target nodes or multiple 091 * controls. 092 * 093 * <p>Because most Tooltips are shown on UI controls, there is special API 094 * for all controls to make installing a Tooltip less verbose. The example below 095 * shows how to create a tooltip for a Button control: 096 * 097 * <pre> 098 * import javafx.scene.control.Tooltip; 099 * import javafx.scene.control.Button; 100 * 101 * Button button = new Button("Hover Over Me"); 102 * button.setTooltip(new Tooltip("Tooltip for Button")); 103 * </pre> 104 */ 105public class Tooltip extends PopupControl { 106// private static TooltipBehavior BEHAVIOR = new TooltipBehavior( 107// new Duration(1000), new Duration(5000), new Duration(600), true); 108 private static String TOOLTIP_PROP_KEY = "javafx.scene.control.Tooltip"; 109 private static TooltipBehavior BEHAVIOR = new TooltipBehavior( 110 new Duration(1000), new Duration(5000), new Duration(200), false); 111 112 /** 113 * Associates the given {@link Tooltip} with the given {@link Node}. The tooltip 114 * can then behave similar to when it is set on any {@link Control}. A single 115 * tooltip can be associated with multiple nodes. 116 * @see Tooltip 117 */ 118 public static void install(Node node, Tooltip t) { 119 BEHAVIOR.install(node, t); 120 } 121 122 /** 123 * Removes the association of the given {@link Tooltip} on the specified 124 * {@link Node}. Hence hovering on the node will no longer result in showing of the 125 * tooltip. 126 * @see Tooltip 127 */ 128 public static void uninstall(Node node, Tooltip t) { 129 BEHAVIOR.uninstall(node); 130 } 131 132 /*************************************************************************** 133 * * 134 * Constructors * 135 * * 136 **************************************************************************/ 137 138 /** 139 * Creates a tooltip with an empty string for its text. 140 */ 141 public Tooltip() { 142 super(); 143 this.bridge = new CSSBridge(); 144 initialize(); 145 } 146 147 /** 148 * Creates a tooltip with the specified text. 149 * 150 * @param text A text string for the tooltip. 151 */ 152 public Tooltip(String text) { 153 bridge = new CSSBridge(); 154 setText(text); 155 initialize(); 156 } 157 158 private void initialize() { 159 160 // undo PopupControl's bridge and replace it with Tooltip's 161 if (bridge != null) { 162 getContent().clear(); 163 bridge.idProperty().unbind(); 164 bridge.styleProperty().unbind(); 165 166 // Bind up these two properties. Note that the third, styleClass, is 167 // handled in the onChange listener for that list. 168 bridge.idProperty().bind(idProperty()); 169 bridge.styleProperty().bind(styleProperty()); 170 } 171 172 getContent().add(bridge); 173 174 getStyleClass().setAll("tooltip"); 175 } 176 177 /*************************************************************************** 178 * * 179 * Properties * 180 * * 181 **************************************************************************/ 182 /** 183 * The text to display in the tooltip. If the text is set to null, an empty 184 * string will be displayed, despite the value being null. 185 */ 186 private final StringProperty text = new SimpleStringProperty(this, "text", ""); 187 public final StringProperty textProperty() { return text; } 188 public final void setText(String value) { 189 if (isShowing() && value != null && !value.equals(getText())) { 190 //Dynamic tooltip content is location-dependant. 191 //Chromium trick. 192 setX(BEHAVIOR.lastMouseX); 193 setY(BEHAVIOR.lastMouseY); 194 } 195 textProperty().setValue(value); 196 } 197 public final String getText() { return text == null ? "" : text.getValue(); } 198 199 public final void setTextAlignment(TextAlignment value) { textAlignmentProperty().setValue(value); } 200 public final TextAlignment getTextAlignment() { 201 return ((Tooltip.CSSBridge)bridge).textAlignment == null 202 ? TextAlignment.LEFT 203 : ((Tooltip.CSSBridge)bridge).textAlignment.getValue(); 204 } 205 /** 206 * Specifies the behavior for lines of text <em>when text is multiline</em>. 207 * Unlike {@link #contentDisplayProperty() contentDisplay} which affects the 208 * graphic and text, this setting only affects multiple lines of text 209 * relative to the text bounds. 210 */ 211 public final ObjectProperty<TextAlignment> textAlignmentProperty() { 212 return ((Tooltip.CSSBridge)bridge).textAlignmentProperty(); 213 } 214 215 public final void setTextOverrun(OverrunStyle value) { textOverrunProperty().setValue(value); } 216 public final OverrunStyle getTextOverrun() { 217 return ((Tooltip.CSSBridge)bridge).textOverrun == null 218 ? OverrunStyle.ELLIPSIS 219 : ((Tooltip.CSSBridge)bridge).textOverrun.getValue(); 220 } 221 /** 222 * Specifies the behavior to use if the text of the {@code Tooltip} 223 * exceeds the available space for rendering the text. 224 */ 225 public final ObjectProperty<OverrunStyle> textOverrunProperty() { 226 return ((Tooltip.CSSBridge)bridge).textOverrunProperty(); 227 } 228 229 public final void setWrapText(boolean value) { wrapTextProperty().setValue(value); } 230 public final boolean isWrapText() { 231 return ((Tooltip.CSSBridge)bridge).wrapText == null 232 ? false 233 : ((Tooltip.CSSBridge)bridge).wrapText.getValue(); } 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 ((Tooltip.CSSBridge)bridge).wrapTextProperty(); 240 } 241 242 public final void setFont(Font value) { fontProperty().setValue(value); } 243 public final Font getFont() { 244 return ((Tooltip.CSSBridge)bridge).font == null 245 ? Font.getDefault() 246 : ((Tooltip.CSSBridge)bridge).font.getValue(); } 247 /** 248 * The default font to use for text in the Tooltip. If the Tooltip's text is 249 * rich text then this font may or may not be used depending on the font 250 * information embedded in the rich text, but in any case where a default 251 * font is required, this font will be used. 252 */ 253 public final ObjectProperty<Font> fontProperty() { 254 return ((Tooltip.CSSBridge)bridge).fontProperty(); 255 } 256 257 /** 258 * An optional icon for the Tooltip. This can be positioned relative to the 259 * text by using the {@link #contentDisplayProperty() content display} 260 * property. 261 * The node specified for this variable cannot appear elsewhere in the 262 * scene graph, otherwise the {@code IllegalArgumentException} is thrown. 263 * See the class description of {@link javafx.scene.Node Node} for more detail. 264 */ 265 private ObjectProperty<Node> graphic; 266 public final void setGraphic(Node value) { 267 graphicProperty().setValue(value); 268 } 269 public final Node getGraphic() { return graphic == null ? null : graphic.getValue(); } 270 public final ObjectProperty<Node> graphicProperty() { 271 if (graphic == null) { 272 graphic = new ObjectPropertyBase<Node>() { 273 274 @Override 275 public Object getBean() { 276 return Tooltip.this; 277 } 278 279 @Override 280 public String getName() { 281 return "graphic"; 282 } 283 }; 284 } 285 return graphic; 286 } 287 288 public final void setContentDisplay(ContentDisplay value) { contentDisplayProperty().setValue(value); } 289 public final ContentDisplay getContentDisplay() { 290 return ((Tooltip.CSSBridge)bridge).contentDisplay == null 291 ? ContentDisplay.LEFT 292 : ((Tooltip.CSSBridge)bridge).contentDisplay.getValue(); } 293 /** 294 * Specifies the positioning of the graphic relative to the text. 295 */ 296 public final ObjectProperty<ContentDisplay> contentDisplayProperty() { 297 return ((Tooltip.CSSBridge)bridge).contentDisplayProperty(); 298 } 299 300 public final void setGraphicTextGap(double value) { graphicTextGapProperty().setValue(value); } 301 public final double getGraphicTextGap() { 302 return ((Tooltip.CSSBridge)bridge).graphicTextGap == null 303 ? 4 304 : ((Tooltip.CSSBridge)bridge).graphicTextGap.getValue(); } 305 /** 306 * The amount of space between the graphic and text 307 */ 308 public final DoubleProperty graphicTextGapProperty() { 309 return ((Tooltip.CSSBridge)bridge).graphicTextGapProperty(); 310 } 311 312 /** 313 * Typically, the tooltip is "activated" when the mouse moves over a Control. 314 * There is usually some delay between when the Tooltip becomes "activated" 315 * and when it is actually shown. The details (such as the amount of delay, etc) 316 * is left to the Skin implementation. 317 */ 318 private final ReadOnlyBooleanWrapper activated = new ReadOnlyBooleanWrapper(this, "activated"); 319 final void setActivated(boolean value) { activated.set(value); } 320 public final boolean isActivated() { return activated.get(); } 321 public final ReadOnlyBooleanProperty activatedProperty() { return activated.getReadOnlyProperty(); } 322 323 /*************************************************************************** 324 * * 325 * Methods * 326 * * 327 **************************************************************************/ 328 329 /** {@inheritDoc} */ 330 @Override protected Skin<?> createDefaultSkin() { 331 return new TooltipSkin(this); 332 } 333 334 /*************************************************************************** 335 * * 336 * Stylesheet Handling * 337 * * 338 **************************************************************************/ 339 340 private static class StyleableProperties { 341 private static final CssMetaData<CSSBridge,Font> FONT = 342 new FontCssMetaData<CSSBridge>("-fx-font", Font.getDefault()) { 343 344 @Override 345 public boolean isSettable(CSSBridge n) { 346 return n.font == null || !n.font.isBound(); 347 } 348 349 @Override 350 public StyleableProperty<Font> getStyleableProperty(CSSBridge n) { 351 return (StyleableProperty<Font>)n.fontProperty(); 352 } 353 }; 354 355 private static final CssMetaData<CSSBridge,TextAlignment> TEXT_ALIGNMENT = 356 new CssMetaData<CSSBridge,TextAlignment>("-fx-text-alignment", 357 new EnumConverter<TextAlignment>(TextAlignment.class), 358 TextAlignment.LEFT) { 359 360 @Override 361 public boolean isSettable(CSSBridge n) { 362 return n.textAlignment == null || !n.textAlignment.isBound(); 363 } 364 365 @Override 366 public StyleableProperty<TextAlignment> getStyleableProperty(CSSBridge n) { 367 return (StyleableProperty<TextAlignment>)n.textAlignmentProperty(); 368 } 369 }; 370 371 private static final CssMetaData<CSSBridge,OverrunStyle> TEXT_OVERRUN = 372 new CssMetaData<CSSBridge,OverrunStyle>("-fx-text-overrun", 373 new EnumConverter<OverrunStyle>(OverrunStyle.class), 374 OverrunStyle.ELLIPSIS) { 375 376 @Override 377 public boolean isSettable(CSSBridge n) { 378 return n.textOverrun == null || !n.textOverrun.isBound(); 379 } 380 381 @Override 382 public StyleableProperty<OverrunStyle> getStyleableProperty(CSSBridge n) { 383 return (StyleableProperty<OverrunStyle>)n.textOverrunProperty(); 384 } 385 }; 386 387 private static final CssMetaData<CSSBridge,Boolean> WRAP_TEXT = 388 new CssMetaData<CSSBridge,Boolean>("-fx-wrap-text", 389 BooleanConverter.getInstance(), Boolean.FALSE) { 390 391 @Override 392 public boolean isSettable(CSSBridge n) { 393 return n.wrapText == null || !n.wrapText.isBound(); 394 } 395 396 @Override 397 public StyleableProperty<Boolean> getStyleableProperty(CSSBridge n) { 398 return (StyleableProperty<Boolean>)n.wrapTextProperty(); 399 } 400 }; 401 402 private static final CssMetaData<CSSBridge,String> GRAPHIC = 403 new CssMetaData<CSSBridge,String>("-fx-graphic", 404 StringConverter.getInstance()) { 405 406 @Override 407 public boolean isSettable(CSSBridge n) { 408 return n.imageUrl == null || !n.imageUrl.isBound(); 409 } 410 411 @Override 412 public StyleableProperty<String> getStyleableProperty(CSSBridge n) { 413 return (StyleableProperty<String>)n.imageUrlProperty(); 414 } 415 }; 416 417 private static final CssMetaData<CSSBridge,ContentDisplay> CONTENT_DISPLAY = 418 new CssMetaData<CSSBridge,ContentDisplay>("-fx-content-display", 419 new EnumConverter<ContentDisplay>(ContentDisplay.class), 420 ContentDisplay.LEFT) { 421 422 @Override 423 public boolean isSettable(CSSBridge n) { 424 return n.contentDisplay == null || !n.contentDisplay.isBound(); 425 } 426 427 @Override 428 public StyleableProperty<ContentDisplay> getStyleableProperty(CSSBridge n) { 429 return (StyleableProperty<ContentDisplay>)n.contentDisplayProperty(); 430 } 431 }; 432 433 private static final CssMetaData<CSSBridge,Number> GRAPHIC_TEXT_GAP = 434 new CssMetaData<CSSBridge,Number>("-fx-graphic-text-gap", 435 SizeConverter.getInstance(), 4.0) { 436 437 @Override 438 public boolean isSettable(CSSBridge n) { 439 return n.graphicTextGap == null || !n.graphicTextGap.isBound(); 440 } 441 442 @Override 443 public StyleableProperty<Number> getStyleableProperty(CSSBridge n) { 444 return (StyleableProperty<Number>)n.graphicTextGapProperty(); 445 } 446 }; 447 448 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 449 static { 450 final List<CssMetaData<? extends Styleable, ?>> styleables = 451 new ArrayList<CssMetaData<? extends Styleable, ?>>(PopupControl.getClassCssMetaData()); 452 styleables.add(FONT); 453 styleables.add(TEXT_ALIGNMENT); 454 styleables.add(TEXT_OVERRUN); 455 styleables.add(WRAP_TEXT); 456 styleables.add(GRAPHIC); 457 styleables.add(CONTENT_DISPLAY); 458 styleables.add(GRAPHIC_TEXT_GAP); 459 STYLEABLES = Collections.unmodifiableList(styleables); 460 } 461 } 462 463 /** 464 * @return The CssMetaData associated with this class, which may include the 465 * CssMetaData of its super classes. 466 */ 467 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 468 return StyleableProperties.STYLEABLES; 469 } 470 471 /** 472 * {@inheritDoc} 473 */ 474 @Override 475 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 476 return getClassCssMetaData(); 477 } 478 479 @Override public Styleable getStyleableParent() { 480 return BEHAVIOR.hoveredNode; 481 } 482 483 private final class CSSBridge extends PopupControl.CSSBridge { 484 485 @Override public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 486 return Tooltip.this.getCssMetaData(); 487 } 488 489 private ObjectProperty<TextAlignment> textAlignment; 490 private final ObjectProperty<TextAlignment> textAlignmentProperty() { 491 if (textAlignment == null) { 492 textAlignment = new StyleableObjectProperty<TextAlignment>(TextAlignment.LEFT) { 493 @Override 494 public CssMetaData<CSSBridge,TextAlignment> getCssMetaData() { 495 return StyleableProperties.TEXT_ALIGNMENT; 496 } 497 498 @Override 499 public Object getBean() { 500 return CSSBridge.this; 501 } 502 503 @Override 504 public String getName() { 505 return "textAlignment"; 506 } 507 }; 508 } 509 return textAlignment; 510 } 511 512 private ObjectProperty<OverrunStyle> textOverrun; 513 private final ObjectProperty<OverrunStyle> textOverrunProperty() { 514 if (textOverrun == null) { 515 textOverrun = new StyleableObjectProperty<OverrunStyle>(OverrunStyle.ELLIPSIS) { 516 @Override 517 public CssMetaData<CSSBridge,OverrunStyle> getCssMetaData() { 518 return StyleableProperties.TEXT_OVERRUN; 519 } 520 521 @Override 522 public Object getBean() { 523 return CSSBridge.this; 524 } 525 526 @Override 527 public String getName() { 528 return "textOverrun"; 529 } 530 }; 531 } 532 return textOverrun; 533 } 534 535 private BooleanProperty wrapText; 536 private final BooleanProperty wrapTextProperty() { 537 if (wrapText == null) { 538 wrapText = new StyleableBooleanProperty(false) { 539 @Override 540 public CssMetaData<CSSBridge,Boolean> getCssMetaData() { 541 return StyleableProperties.WRAP_TEXT; 542 } 543 544 @Override 545 public Object getBean() { 546 return CSSBridge.this; 547 } 548 549 @Override 550 public String getName() { 551 return "wrapText"; 552 } 553 }; 554 } 555 return wrapText; 556 } 557 558 private ObjectProperty<Font> font; 559 private final ObjectProperty<Font> fontProperty() { 560 if (font == null) { 561 font = new StyleableObjectProperty<Font>(Font.getDefault()) { 562 @Override 563 public CssMetaData<CSSBridge,Font> getCssMetaData() { 564 return StyleableProperties.FONT; 565 } 566 567 @Override 568 public Object getBean() { 569 return CSSBridge.this; 570 } 571 572 @Override 573 public String getName() { 574 return "font"; 575 } 576 }; 577 } 578 return font; 579 } 580 581 private StringProperty imageUrl = null; 582 /** 583 * The imageUrl property is set from CSS and then the graphic property is 584 * set from the invalidated method. This ensures that the same image isn't 585 * reloaded. 586 */ 587 private StringProperty imageUrlProperty() { 588 if (imageUrl == null) { 589 imageUrl = new StyleableStringProperty() { 590 591 @Override 592 protected void invalidated() { 593 594 if (get() != null) { 595 URL url = null; 596 try { 597 url = new URL(get()); 598 } catch (MalformedURLException malf) { 599 // This may be a relative URL, so try resolving 600 // it using the application classloader 601 final ClassLoader cl = Thread.currentThread().getContextClassLoader(); 602 url = cl.getResource(get()); 603 } 604 if (url != null) { 605 final Image img = StyleManager.getInstance().getCachedImage(url.toExternalForm()); 606 setGraphic(new ImageView(img)); 607 } 608 } else { 609 setGraphic(null); 610 } 611 } 612 613 @Override 614 public Object getBean() { 615 return CSSBridge.this; 616 } 617 618 @Override 619 public String getName() { 620 return "imageUrl"; 621 } 622 623 @Override 624 public CssMetaData<CSSBridge,String> getCssMetaData() { 625 return Tooltip.StyleableProperties.GRAPHIC; 626 } 627 628 }; 629 } 630 return imageUrl; 631 } 632 633 private ObjectProperty<ContentDisplay> contentDisplay; 634 private final ObjectProperty<ContentDisplay> contentDisplayProperty() { 635 if (contentDisplay == null) { 636 contentDisplay = new StyleableObjectProperty<ContentDisplay>(ContentDisplay.LEFT) { 637 @Override 638 public CssMetaData<CSSBridge,ContentDisplay> getCssMetaData() { 639 return StyleableProperties.CONTENT_DISPLAY; 640 } 641 642 @Override 643 public Object getBean() { 644 return CSSBridge.this; 645 } 646 647 @Override 648 public String getName() { 649 return "contentDisplay"; 650 } 651 }; 652 } 653 return contentDisplay; 654 } 655 656 private DoubleProperty graphicTextGap; 657 private final DoubleProperty graphicTextGapProperty() { 658 if (graphicTextGap == null) { 659 graphicTextGap = new StyleableDoubleProperty(4) { 660 @Override 661 public CssMetaData<CSSBridge,Number> getCssMetaData() { 662 return StyleableProperties.GRAPHIC_TEXT_GAP; 663 } 664 665 @Override 666 public Object getBean() { 667 return CSSBridge.this; 668 } 669 670 @Override 671 public String getName() { 672 return "graphicTextGap"; 673 } 674 }; 675 } 676 return graphicTextGap; 677 } 678 679 } 680 681 private static class TooltipBehavior { 682 683 /* 684 * There are two key concepts with Tooltip: activated and visible. A Tooltip 685 * is activated as soon as a mouse move occurs over the target node. When it 686 * becomes activated, we start off the ACTIVATION_TIMER. If the 687 * ACTIVATION_TIMER expires before another mouse event occurs, then we will 688 * show the popup. This timer typically lasts about 1 second. 689 * 690 * Once visible, we reset the ACTIVATION_TIMER and start the HIDE_TIMER. 691 * This second timer will allow the tooltip to remain visible for some time 692 * period (such as 5 seconds). If the mouse hasn't moved, and the HIDE_TIMER 693 * expires, then the tooltip is hidden and the tooltip is no longer 694 * activated. 695 * 696 * If another mouse move occurs, the ACTIVATION_TIMER starts again, and the 697 * same rules apply as above. 698 * 699 * If a mouse exit event occurs while the HIDE_TIMER is ticking, we reset 700 * the HIDE_TIMER. Thus, the tooltip disappears after 5 seconds from the 701 * last mouse move. 702 * 703 * If some other mouse event occurs while the HIDE_TIMER is running, other 704 * than mouse move or mouse enter/exit (such as a click), then the tooltip 705 * is hidden, the HIDE_TIMER stopped, and activated set to false. 706 * 707 * If a mouse exit occurs while the HIDE_TIMER is running, we stop the 708 * HIDE_TIMER and start the LEFT_TIMER, and immediately hide the tooltip. 709 * This timer is very short, maybe about a 1/2 second. If the mouse enters a 710 * new node which also has a tooltip before LEFT_TIMER expires, then the 711 * second tooltip is activated and shown immediately (the ACTIVATION_TIMER 712 * having been bypassed), and the HIDE_TIMER is started. If the LEFT_TIMER 713 * expires and there is no mouse movement over a control with a tooltip, 714 * then we are back to the initial steady state where the next mouse move 715 * over a node with a tooltip installed will start the ACTIVATION_TIMER. 716 */ 717 718 private Timeline activationTimer = new Timeline(); 719 private Timeline hideTimer = new Timeline(); 720 private Timeline leftTimer = new Timeline(); 721 722 /** 723 * The Node with a tooltip over which the mouse is hovering. There can 724 * only be one of these at a time. 725 */ 726 private Node hoveredNode; 727 728 /** 729 * The tooltip that is currently activated. There can only be one 730 * of these at a time. 731 */ 732 private Tooltip activatedTooltip; 733 734 /** 735 * The tooltip that is currently visible. There can only be one 736 * of these at a time. 737 */ 738 private Tooltip visibleTooltip; 739 740 /** 741 * The last position of the mouse, in screen coordinates. 742 */ 743 private double lastMouseX; 744 private double lastMouseY; 745 746 private boolean hideOnExit; 747 748 TooltipBehavior(Duration openDelay, Duration visibleDuration, Duration closeDelay, final boolean hideOnExit) { 749 this.hideOnExit = hideOnExit; 750 751 activationTimer.getKeyFrames().add(new KeyFrame(openDelay)); 752 activationTimer.setOnFinished(new EventHandler<ActionEvent>() { 753 @Override public void handle(ActionEvent event) { 754 // Show the currently activated tooltip and start the 755 // HIDE_TIMER. 756 assert activatedTooltip != null; 757 final Window owner = getWindow(hoveredNode); 758 final boolean treeVisible = isWindowHierarchyVisible(hoveredNode); 759 760 // If the ACTIVATED tooltip is part of a visible window 761 // hierarchy, we can go ahead and show the tooltip and 762 // start the HIDE_TIMER. 763 // 764 // If the owner is null or invisible, then it either means a 765 // bug in our code, the node was removed from a scene or 766 // window or made invisible, or the node is not part of a 767 // visible window hierarchy. In that case, we don't show the 768 // tooltip, and we don't start the HIDE_TIMER. We simply let 769 // ACTIVATED_TIMER expire, and wait until the next mouse 770 // the movement to start it again. 771 if (owner != null && owner.isShowing() && treeVisible) { 772 double x = lastMouseX; 773 double y = lastMouseY; 774 775 // The tooltip always inherits the nodeOrientation of 776 // the Node that it is attached to (see RT-26147). It 777 // is possible to override this for the Tooltip content 778 // (but not the popup placement) by setting the 779 // nodeOrientation on tooltip.getScene().getRoot(). 780 NodeOrientation nodeOrientation = hoveredNode.getEffectiveNodeOrientation(); 781 activatedTooltip.getScene().setNodeOrientation(nodeOrientation); 782 if (nodeOrientation == NodeOrientation.RIGHT_TO_LEFT) { 783 x -= activatedTooltip.getWidth(); 784 } 785 786 activatedTooltip.show(owner, x, y); 787 visibleTooltip = activatedTooltip; 788 hoveredNode = null; 789 hideTimer.playFromStart(); 790 } 791 792 // Once the activation timer has expired, the tooltip is no 793 // longer in the activated state, it is only in the visible 794 // state, so we go ahead and set activated to false 795 activatedTooltip.setActivated(false); 796 activatedTooltip = null; 797 } 798 }); 799 800 hideTimer.getKeyFrames().add(new KeyFrame(visibleDuration)); 801 hideTimer.setOnFinished(new EventHandler<ActionEvent>() { 802 @Override public void handle(ActionEvent event) { 803 // Hide the currently visible tooltip. 804 assert visibleTooltip != null; 805 visibleTooltip.hide(); 806 visibleTooltip = null; 807 hoveredNode = null; 808 } 809 }); 810 811 leftTimer.getKeyFrames().add(new KeyFrame(closeDelay)); 812 leftTimer.setOnFinished(new EventHandler<ActionEvent>() { 813 @Override public void handle(ActionEvent event) { 814 if (!hideOnExit) { 815 // Hide the currently visible tooltip. 816 assert visibleTooltip != null; 817 visibleTooltip.hide(); 818 visibleTooltip = null; 819 hoveredNode = null; 820 } 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 = new EventHandler<MouseEvent>() { 833 @Override public void handle(MouseEvent event) { 834 //Screen coordinates need to be actual for dynamic tooltip. 835 //See Tooltip.setText 836 837 // detect bogus mouse moved events, if it didn't really move then ignore it 838 double newMouseX = event.getScreenX(); 839 double newMouseY = event.getScreenY(); 840 if (newMouseX == lastMouseX && newMouseY == lastMouseY) { 841 return; 842 } 843 lastMouseX = newMouseX; 844 lastMouseY = newMouseY; 845 846 // If the HIDE_TIMER is running, then we don't want this event 847 // handler to do anything, or change any state at all. 848 if (hideTimer.getStatus() == Timeline.Status.RUNNING) { 849 return; 850 } 851 852 // Note that the "install" step will both register this handler 853 // with the target node and also associate the tooltip with the 854 // target node, by stashing it in the client properties of the node. 855 hoveredNode = (Node) event.getSource(); 856 Tooltip t = (Tooltip) hoveredNode.getProperties().get(TOOLTIP_PROP_KEY); 857 if (t != null) { 858 // In theory we should never get here with an invisible or 859 // non-existant window hierarchy, but might in some cases where 860 // people are feeding fake mouse events into the hierarchy. So 861 // we'll guard against that case. 862 final Window owner = getWindow(hoveredNode); 863 final boolean treeVisible = isWindowHierarchyVisible(hoveredNode); 864 if (owner != null && treeVisible) { 865 // Now we know that the currently HOVERED node has a tooltip 866 // and that it is part of a visible window Hierarchy. 867 // If LEFT_TIMER is running, then we make this tooltip 868 // visible immediately, stop the LEFT_TIMER, and start the 869 // HIDE_TIMER. 870 if (leftTimer.getStatus() == Timeline.Status.RUNNING) { 871 if (visibleTooltip != null) visibleTooltip.hide(); 872 visibleTooltip = t; 873 t.show(owner, event.getScreenX(), event.getScreenY()); 874 leftTimer.stop(); 875 hideTimer.playFromStart(); 876 } else { 877 // Start / restart the timer and make sure the tooltip 878 // is marked as activated. 879 t.setActivated(true); 880 activatedTooltip = t; 881 activationTimer.stop(); 882 activationTimer.playFromStart(); 883 } 884 } 885 } else { 886 // TODO should deregister, no point being here anymore! 887 } 888 } 889 }; 890 891 /** 892 * Registers for mouse exit events. If the ACTIVATION_TIMER is running then 893 * this will simply stop it. If the HIDE_TIMER is running then this will 894 * stop the HIDE_TIMER, hide the tooltip, and start the LEFT_TIMER. 895 */ 896 private EventHandler<MouseEvent> LEAVING_HANDLER = new EventHandler<MouseEvent>() { 897 @Override public void handle(MouseEvent event) { 898 // detect bogus mouse exit events, if it didn't really move then ignore it 899 double newMouseX = event.getScreenX(); 900 double newMouseY = event.getScreenY(); 901 if (newMouseX == lastMouseX && newMouseY == lastMouseY) { 902 return; 903 } 904 905 if (activationTimer.getStatus() == Timeline.Status.RUNNING) { 906 activationTimer.stop(); 907 } else if (hideTimer.getStatus() == Timeline.Status.RUNNING) { 908 assert visibleTooltip != null; 909 hideTimer.stop(); 910 if (hideOnExit) visibleTooltip.hide(); 911 leftTimer.playFromStart(); 912 } 913 914 hoveredNode = null; 915 activatedTooltip = null; 916 if (hideOnExit) visibleTooltip = null; 917 } 918 }; 919 920 /** 921 * Registers for mouse click, press, release, drag events. If any of these 922 * occur, then the tooltip is hidden (if it is visible), it is deactivated, 923 * and any and all timers are stopped. 924 */ 925 private EventHandler<MouseEvent> KILL_HANDLER = new EventHandler<MouseEvent>() { 926 @Override public void handle(MouseEvent event) { 927 activationTimer.stop(); 928 hideTimer.stop(); 929 leftTimer.stop(); 930 if (visibleTooltip != null) visibleTooltip.hide(); 931 hoveredNode = null; 932 activatedTooltip = null; 933 visibleTooltip = null; 934 } 935 }; 936 937 private void install(Node node, Tooltip t) { 938 // Install the MOVE_HANDLER, LEAVING_HANDLER, and KILL_HANDLER on 939 // the given node. Stash the tooltip in the node's client properties 940 // map so that it is not gc'd. The handlers must all be installed 941 // with a TODO weak reference so as not to cause a memory leak 942 if (node == null) return; 943 node.addEventHandler(MouseEvent.MOUSE_MOVED, MOVE_HANDLER); 944 node.addEventHandler(MouseEvent.MOUSE_EXITED, LEAVING_HANDLER); 945 node.addEventHandler(MouseEvent.MOUSE_PRESSED, KILL_HANDLER); 946 node.getProperties().put(TOOLTIP_PROP_KEY, t); 947 } 948 949 private void uninstall(Node node) { 950 if (node == null) return; 951 node.removeEventHandler(MouseEvent.MOUSE_MOVED, MOVE_HANDLER); 952 node.removeEventHandler(MouseEvent.MOUSE_EXITED, LEAVING_HANDLER); 953 node.removeEventHandler(MouseEvent.MOUSE_PRESSED, KILL_HANDLER); 954 Tooltip t = (Tooltip)node.getProperties().get(TOOLTIP_PROP_KEY); 955 if (t != null) { 956 node.getProperties().remove(TOOLTIP_PROP_KEY); 957 if (t.equals(visibleTooltip) || t.equals(activatedTooltip)) { 958 KILL_HANDLER.handle(null); 959 } 960 } 961 } 962 963 /** 964 * Gets the top level window associated with this node. 965 * @param node the node 966 * @return the top level window 967 */ 968 private Window getWindow(final Node node) { 969 final Scene scene = node == null ? null : node.getScene(); 970 return scene == null ? null : scene.getWindow(); 971 } 972 973 /** 974 * Gets whether the entire window hierarchy is visible for this node. 975 * @param node the node to check 976 * @return true if entire hierarchy is visible 977 */ 978 private boolean isWindowHierarchyVisible(Node node) { 979 boolean treeVisible = node != null; 980 Parent parent = node == null ? null : node.getParent(); 981 while (parent != null && treeVisible) { 982 treeVisible = parent.isVisible(); 983 parent = parent.getParent(); 984 } 985 return treeVisible; 986 } 987 988 } 989}