All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jidefx.scene.control.popup.TooltipEx Maven / Gradle / Ivy

/*
 * Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package jidefx.scene.control.popup;

import com.jidefx.scene.control.skin.TooltipExSkin;
import com.sun.javafx.css.StyleManager;
import com.sun.javafx.css.converters.BooleanConverter;
import com.sun.javafx.css.converters.EnumConverter;
import com.sun.javafx.css.converters.SizeConverter;
import com.sun.javafx.css.converters.StringConverter;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.property.*;
import javafx.css.*;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.NodeOrientation;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.OverrunStyle;
import javafx.scene.control.PopupControl;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.text.Font;
import javafx.scene.text.TextAlignment;
import javafx.stage.Window;
import javafx.util.Duration;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;


/**
 * Copied from {@code Tooltip} (jdk8 ea build 96) to support tooltip position. The code changes are tagged by // Added in JideFX. This
 * class is not intended to be a public API. We might remove this class in the future if JavaFX tooltip supports the
 * tooltip position or we find a better solution.
 */
public class TooltipEx extends PopupControl {
    //    private static TooltipBehavior BEHAVIOR = new TooltipBehavior(
//        new Duration(1000), new Duration(5000), new Duration(600), true);
    private static String TOOLTIP_PROP_KEY = "javafx.scene.control.Tooltip";
    private static JideTooltipBehavior BEHAVIOR = new JideTooltipBehavior(
//            new Duration(1000), new Duration(5000), new Duration(200), false);
// Added by JIDE
            new Duration(200), new Duration(2000), new Duration(200), false);
// End

    /**
     * Associates the given {@link TooltipEx} with the given {@link javafx.scene.Node}. The tooltip can then behave
     * similar to when it is set on any {@link javafx.scene.control.Control}. A single tooltip can be associated with
     * multiple nodes.
     *
     * @see TooltipEx
     */
    public static void install(Node node, TooltipEx t) {
        BEHAVIOR.install(node, t);
    }

    /**
     * Removes the association of the given {@link TooltipEx} on the specified {@link Node}. Hence hovering on the node
     * will no longer result in showing of the tooltip.
     *
     * @see TooltipEx
     */
    public static void uninstall(Node node, TooltipEx t) {
        BEHAVIOR.uninstall(node);
    }

    /***************************************************************************
     *                                                                         *
     * Constructors                                                            *
     *                                                                         *
     **************************************************************************/

    /**
     * Creates a tooltip with an empty string for its text.
     */
    public TooltipEx() {
        super();
        this.bridge = new CSSBridge();
        initialize();
    }

    /**
     * Creates a tooltip with the specified text.
     *
     * @param text A text string for the tooltip.
     */
    public TooltipEx(String text) {
        bridge = new CSSBridge();
        setText(text);
        initialize();
    }

    private void initialize() {

        // undo PopupControl's bridge and replace it with Tooltip's
        if (bridge != null) {
            getContent().clear();
            bridge.idProperty().unbind();
            bridge.styleProperty().unbind();

            // Bind up these two properties. Note that the third, styleClass, is
            // handled in the onChange listener for that list.
            bridge.idProperty().bind(idProperty());
            bridge.styleProperty().bind(styleProperty());
        }

        getContent().add(bridge);

        getStyleClass().setAll("tooltip");
    }

    /***************************************************************************
     *                                                                         *
     * Properties                                                              *
     *                                                                         *
     **************************************************************************/
    /**
     * The text to display in the tooltip. If the text is set to null, an empty string will be displayed, despite the
     * value being null.
     */
    private final StringProperty text = new SimpleStringProperty(this, "text", "");

    public final StringProperty textProperty() {
        return text;
    }

    public final void setText(String value) {
        if (isShowing() && value != null && !value.equals(getText())) {
            //Dynamic tooltip content is location-dependant.
            //Chromium trick.
            setX(BEHAVIOR.lastMouseX);
            setY(BEHAVIOR.lastMouseY);
        }
        textProperty().setValue(value);
    }

    public final String getText() {
        return text == null ? "" : text.getValue();
    }

    public final void setTextAlignment(TextAlignment value) {
        textAlignmentProperty().setValue(value);
    }

    public final TextAlignment getTextAlignment() {
        return ((TooltipEx.CSSBridge) bridge).textAlignment == null
                ? TextAlignment.LEFT
                : ((TooltipEx.CSSBridge) bridge).textAlignment.getValue();
    }

    /**
     * Specifies the behavior for lines of text when text is multiline. Unlike {@link #contentDisplayProperty()
     * contentDisplay} which affects the graphic and text, this setting only affects multiple lines of text relative to
     * the text bounds.
     */
    public final ObjectProperty textAlignmentProperty() {
        return ((TooltipEx.CSSBridge) bridge).textAlignmentProperty();
    }

    public final void setTextOverrun(OverrunStyle value) {
        textOverrunProperty().setValue(value);
    }

    public final OverrunStyle getTextOverrun() {
        return ((TooltipEx.CSSBridge) bridge).textOverrun == null
                ? OverrunStyle.ELLIPSIS
                : ((TooltipEx.CSSBridge) bridge).textOverrun.getValue();
    }

    /**
     * Specifies the behavior to use if the text of the {@code Tooltip} exceeds the available space for rendering the
     * text.
     */
    public final ObjectProperty textOverrunProperty() {
        return ((TooltipEx.CSSBridge) bridge).textOverrunProperty();
    }

    public final void setWrapText(boolean value) {
        wrapTextProperty().setValue(value);
    }

    public final boolean isWrapText() {
        return ((TooltipEx.CSSBridge) bridge).wrapText == null
                ? false
                : ((TooltipEx.CSSBridge) bridge).wrapText.getValue();
    }

    /**
     * If a run of text exceeds the width of the Tooltip, then this variable indicates whether the text should wrap onto
     * another line.
     */
    public final BooleanProperty wrapTextProperty() {
        return ((TooltipEx.CSSBridge) bridge).wrapTextProperty();
    }

    public final void setFont(Font value) {
        fontProperty().setValue(value);
    }

    public final Font getFont() {
        return ((TooltipEx.CSSBridge) bridge).font == null
                ? Font.getDefault()
                : ((TooltipEx.CSSBridge) bridge).font.getValue();
    }

    /**
     * The default font to use for text in the Tooltip. If the Tooltip's text is rich text then this font may or may not
     * be used depending on the font information embedded in the rich text, but in any case where a default font is
     * required, this font will be used.
     */
    public final ObjectProperty fontProperty() {
        return ((TooltipEx.CSSBridge) bridge).fontProperty();
    }

    /**
     * An optional icon for the Tooltip. This can be positioned relative to the text by using the {@link
     * #contentDisplayProperty() content display} property. The node specified for this variable cannot appear elsewhere
     * in the scene graph, otherwise the {@code IllegalArgumentException} is thrown. See the class description of {@link
     * javafx.scene.Node Node} for more detail.
     */
    private ObjectProperty graphic;

    public final void setGraphic(Node value) {
        graphicProperty().setValue(value);
    }

    public final Node getGraphic() {
        return graphic == null ? null : graphic.getValue();
    }

    public final ObjectProperty graphicProperty() {
        if (graphic == null) {
            graphic = new ObjectPropertyBase() {

                @Override
                public Object getBean() {
                    return TooltipEx.this;
                }

                @Override
                public String getName() {
                    return "graphic"; //NON-NLS
                }
            };
        }
        return graphic;
    }

    public final void setContentDisplay(ContentDisplay value) {
        contentDisplayProperty().setValue(value);
    }

    public final ContentDisplay getContentDisplay() {
        return ((TooltipEx.CSSBridge) bridge).contentDisplay == null
                ? ContentDisplay.LEFT
                : ((TooltipEx.CSSBridge) bridge).contentDisplay.getValue();
    }

    /**
     * Specifies the positioning of the graphic relative to the text.
     */
    public final ObjectProperty contentDisplayProperty() {
        return ((TooltipEx.CSSBridge) bridge).contentDisplayProperty();
    }

    public final void setGraphicTextGap(double value) {
        graphicTextGapProperty().setValue(value);
    }

    public final double getGraphicTextGap() {
        return ((TooltipEx.CSSBridge) bridge).graphicTextGap == null
                ? 4
                : ((TooltipEx.CSSBridge) bridge).graphicTextGap.getValue();
    }

    /**
     * The amount of space between the graphic and text
     */
    public final DoubleProperty graphicTextGapProperty() {
        return ((TooltipEx.CSSBridge) bridge).graphicTextGapProperty();
    }

    /**
     * Typically, the tooltip is "activated" when the mouse moves over a Control. There is usually some delay between
     * when the Tooltip becomes "activated" and when it is actually shown. The details (such as the amount of delay,
     * etc) is left to the Skin implementation.
     */
    private final ReadOnlyBooleanWrapper activated = new ReadOnlyBooleanWrapper(this, "activated"); //NON-NLS

    final void setActivated(boolean value) {
        activated.set(value);
    }

    public final boolean isActivated() {
        return activated.get();
    }

    public final ReadOnlyBooleanProperty activatedProperty() {
        return activated.getReadOnlyProperty();
    }

    /***************************************************************************
     *                                                                         *
     * Methods                                                                 *
     *                                                                         *
     **************************************************************************/

    /**
     * {@inheritDoc}
     */
    @Override
    protected Skin createDefaultSkin() {
        return new TooltipExSkin(this);
    }

    /**
     * ************************************************************************ * Stylesheet Handling * *
     * ************************************************************************
     */

    private static class StyleableProperties {
        private static final CssMetaData FONT =
                new FontCssMetaData("-fx-font", Font.getDefault()) { //NON-NLS

                    @Override
                    public boolean isSettable(CSSBridge n) {
                        return n.font == null || !n.font.isBound();
                    }

                    @Override
                    public StyleableProperty getStyleableProperty(CSSBridge n) {
                        return (StyleableProperty) n.fontProperty();
                    }
                };

        private static final CssMetaData TEXT_ALIGNMENT =
                new CssMetaData("-fx-text-alignment", //NON-NLS
                        new EnumConverter<>(TextAlignment.class),
                        TextAlignment.LEFT) {

                    @Override
                    public boolean isSettable(CSSBridge n) {
                        return n.textAlignment == null || !n.textAlignment.isBound();
                    }

                    @Override
                    public StyleableProperty getStyleableProperty(CSSBridge n) {
                        return (StyleableProperty) n.textAlignmentProperty();
                    }
                };

        private static final CssMetaData TEXT_OVERRUN =
                new CssMetaData("-fx-text-overrun", //NON-NLS
                        new EnumConverter<>(OverrunStyle.class),
                        OverrunStyle.ELLIPSIS) {

                    @Override
                    public boolean isSettable(CSSBridge n) {
                        return n.textOverrun == null || !n.textOverrun.isBound();
                    }

                    @Override
                    public StyleableProperty getStyleableProperty(CSSBridge n) {
                        return (StyleableProperty) n.textOverrunProperty();
                    }
                };

        private static final CssMetaData WRAP_TEXT =
                new CssMetaData("-fx-wrap-text", //NON-NLS
                        BooleanConverter.getInstance(), Boolean.FALSE) {

                    @Override
                    public boolean isSettable(CSSBridge n) {
                        return n.wrapText == null || !n.wrapText.isBound();
                    }

                    @Override
                    public StyleableProperty getStyleableProperty(CSSBridge n) {
                        return (StyleableProperty) n.wrapTextProperty();
                    }
                };

        private static final CssMetaData GRAPHIC =
                new CssMetaData("-fx-graphic", //NON-NLS
                        StringConverter.getInstance()) {

                    @Override
                    public boolean isSettable(CSSBridge n) {
                        return n.imageUrl == null || !n.imageUrl.isBound();
                    }

                    @Override
                    public StyleableProperty getStyleableProperty(CSSBridge n) {
                        return (StyleableProperty) n.imageUrlProperty();
                    }
                };

        private static final CssMetaData CONTENT_DISPLAY =
                new CssMetaData("-fx-content-display", //NON-NLS
                        new EnumConverter<>(ContentDisplay.class),
                        ContentDisplay.LEFT) {

                    @Override
                    public boolean isSettable(CSSBridge n) {
                        return n.contentDisplay == null || !n.contentDisplay.isBound();
                    }

                    @Override
                    public StyleableProperty getStyleableProperty(CSSBridge n) {
                        return (StyleableProperty) n.contentDisplayProperty();
                    }
                };

        private static final CssMetaData GRAPHIC_TEXT_GAP =
                new CssMetaData("-fx-graphic-text-gap", //NON-NLS
                        SizeConverter.getInstance(), 4.0) {

                    @Override
                    public boolean isSettable(CSSBridge n) {
                        return n.graphicTextGap == null || !n.graphicTextGap.isBound();
                    }

                    @Override
                    public StyleableProperty getStyleableProperty(CSSBridge n) {
                        return (StyleableProperty) n.graphicTextGapProperty();
                    }
                };

        private static final List> STYLEABLES;

        static {
            final List> styleables =
                    new ArrayList<>(PopupControl.getClassCssMetaData());
            styleables.add(FONT);
            styleables.add(TEXT_ALIGNMENT);
            styleables.add(TEXT_OVERRUN);
            styleables.add(WRAP_TEXT);
            styleables.add(GRAPHIC);
            styleables.add(CONTENT_DISPLAY);
            styleables.add(GRAPHIC_TEXT_GAP);
            STYLEABLES = Collections.unmodifiableList(styleables);
        }
    }

    /**
     * @return The CssMetaData associated with this class, which may include the CssMetaData of its super classes.
     */
    public static List> getClassCssMetaData() {
        return StyleableProperties.STYLEABLES;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List> getCssMetaData() {
        return getClassCssMetaData();
    }

    @Override public Styleable getStyleableParent() {
        return BEHAVIOR.hoveredNode;
    }

    final class CSSBridge extends PopupControl.CSSBridge {

        @Override
        public List> getCssMetaData() {
            return TooltipEx.this.getCssMetaData();
        }

        private ObjectProperty textAlignment;

        private final ObjectProperty textAlignmentProperty() {
            if (textAlignment == null) {
                textAlignment = new StyleableObjectProperty(TextAlignment.LEFT) {
                    @Override
                    public CssMetaData getCssMetaData() {
                        return StyleableProperties.TEXT_ALIGNMENT;
                    }

                    @Override
                    public Object getBean() {
                        return CSSBridge.this;
                    }

                    @Override
                    public String getName() {
                        return "textAlignment"; //NON-NLS
                    }
                };
            }
            return textAlignment;
        }

        private ObjectProperty textOverrun;

        private final ObjectProperty textOverrunProperty() {
            if (textOverrun == null) {
                textOverrun = new StyleableObjectProperty(OverrunStyle.ELLIPSIS) {
                    @Override
                    public CssMetaData getCssMetaData() {
                        return StyleableProperties.TEXT_OVERRUN;
                    }

                    @Override
                    public Object getBean() {
                        return CSSBridge.this;
                    }

                    @Override
                    public String getName() {
                        return "textOverrun"; //NON-NLS
                    }
                };
            }
            return textOverrun;
        }

        private BooleanProperty wrapText;

        private final BooleanProperty wrapTextProperty() {
            if (wrapText == null) {
                wrapText = new StyleableBooleanProperty(false) {
                    @Override
                    public CssMetaData getCssMetaData() {
                        return StyleableProperties.WRAP_TEXT;
                    }

                    @Override
                    public Object getBean() {
                        return CSSBridge.this;
                    }

                    @Override
                    public String getName() {
                        return "wrapText"; //NON-NLS
                    }
                };
            }
            return wrapText;
        }

        private ObjectProperty font;

        private final ObjectProperty fontProperty() {
            if (font == null) {
                font = new StyleableObjectProperty(Font.getDefault()) {
                    @Override
                    public CssMetaData getCssMetaData() {
                        return StyleableProperties.FONT;
                    }

                    @Override
                    public Object getBean() {
                        return CSSBridge.this;
                    }

                    @Override
                    public String getName() {
                        return "font";
                    }
                };
            }
            return font;
        }

        private StringProperty imageUrl = null;

        /**
         * The imageUrl property is set from CSS and then the graphic property is set from the invalidated method. This
         * ensures that the same image isn't reloaded.
         */
        private StringProperty imageUrlProperty() {
            if (imageUrl == null) {
                imageUrl = new StyleableStringProperty() {

                    @Override
                    protected void invalidated() {

                        if (get() != null) {
                            URL url = null;
                            try {
                                url = new URL(get());
                            }
                            catch (MalformedURLException malf) {
                                // This may be a relative URL, so try resolving
                                // it using the application classloader
                                final ClassLoader cl = Thread.currentThread().getContextClassLoader();
                                url = cl.getResource(get());
                            }
                            if (url != null) {
                                final Image img = StyleManager.getInstance().getCachedImage(url.toExternalForm());
                                setGraphic(new ImageView(img));
                            }
                        }
                        else {
                            setGraphic(null);
                        }
                    }

                    @Override
                    public Object getBean() {
                        return CSSBridge.this;
                    }

                    @Override
                    public String getName() {
                        return "imageUrl"; //NON-NLS
                    }

                    @Override
                    public CssMetaData getCssMetaData() {
                        return TooltipEx.StyleableProperties.GRAPHIC;
                    }

                };
            }
            return imageUrl;
        }

        private ObjectProperty contentDisplay;

        private final ObjectProperty contentDisplayProperty() {
            if (contentDisplay == null) {
                contentDisplay = new StyleableObjectProperty(ContentDisplay.LEFT) {
                    @Override
                    public CssMetaData getCssMetaData() {
                        return StyleableProperties.CONTENT_DISPLAY;
                    }

                    @Override
                    public Object getBean() {
                        return CSSBridge.this;
                    }

                    @Override
                    public String getName() {
                        return "contentDisplay"; //NON-NLS
                    }
                };
            }
            return contentDisplay;
        }

        private DoubleProperty graphicTextGap;

        private final DoubleProperty graphicTextGapProperty() {
            if (graphicTextGap == null) {
                graphicTextGap = new StyleableDoubleProperty(4) {
                    @Override
                    public CssMetaData getCssMetaData() {
                        return StyleableProperties.GRAPHIC_TEXT_GAP;
                    }

                    @Override
                    public Object getBean() {
                        return CSSBridge.this;
                    }

                    @Override
                    public String getName() {
                        return "graphicTextGap"; //NON-NLS
                    }
                };
            }
            return graphicTextGap;
        }

    }

    private static class JideTooltipBehavior {

        /*
         * There are two key concepts with Tooltip: activated and visible. A Tooltip
         * is activated as soon as a mouse move occurs over the target node. When it
         * becomes activated, we start off the ACTIVATION_TIMER. If the
         * ACTIVATION_TIMER expires before another mouse event occurs, then we will
         * show the popup. This timer typically lasts about 1 second.
         *
         * Once visible, we reset the ACTIVATION_TIMER and start the HIDE_TIMER.
         * This second timer will allow the tooltip to remain visible for some time
         * period (such as 5 seconds). If the mouse hasn't moved, and the HIDE_TIMER
         * expires, then the tooltip is hidden and the tooltip is no longer
         * activated.
         *
         * If another mouse move occurs, the ACTIVATION_TIMER starts again, and the
         * same rules apply as above.
         *
         * If a mouse exit event occurs while the HIDE_TIMER is ticking, we reset
         * the HIDE_TIMER. Thus, the tooltip disappears after 5 seconds from the
         * last mouse move.
         *
         * If some other mouse event occurs while the HIDE_TIMER is running, other
         * than mouse move or mouse enter/exit (such as a click), then the tooltip
         * is hidden, the HIDE_TIMER stopped, and activated set to false.
         *
         * If a mouse exit occurs while the HIDE_TIMER is running, we stop the
         * HIDE_TIMER and start the LEFT_TIMER, and immediately hide the tooltip.
         * This timer is very short, maybe about a 1/2 second. If the mouse enters a
         * new node which also has a tooltip before LEFT_TIMER expires, then the
         * second tooltip is activated and shown immediately (the ACTIVATION_TIMER
         * having been bypassed), and the HIDE_TIMER is started. If the LEFT_TIMER
         * expires and there is no mouse movement over a control with a tooltip,
         * then we are back to the initial steady state where the next mouse move
         * over a node with a tooltip installed will start the ACTIVATION_TIMER.
         */

        private Timeline activationTimer = new Timeline();
        private Timeline hideTimer = new Timeline();
        private Timeline leftTimer = new Timeline();

        /**
         * The Node with a tooltip over which the mouse is hovering. There can only be one of these at a time.
         */
        private Node hoveredNode;

        /**
         * The tooltip that is currently activated. There can only be one of these at a time.
         */
        private TooltipEx activatedTooltip;

        /**
         * The tooltip that is currently visible. There can only be one of these at a time.
         */
        private TooltipEx visibleTooltip;

        /**
         * The last position of the mouse, in screen coordinates.
         */
        private double lastMouseX;
        private double lastMouseY;

        private boolean hideOnExit;

        JideTooltipBehavior(Duration openDelay, Duration visibleDuration, Duration closeDelay, final boolean hideOnExit) {
            this.hideOnExit = hideOnExit;

            activationTimer.getKeyFrames().add(new KeyFrame(openDelay));
            activationTimer.setOnFinished(new EventHandler() {
                @Override
                public void handle(ActionEvent event) {
                    // Show the currently activated tooltip and start the
                    // HIDE_TIMER.
                    assert activatedTooltip != null;
                    final Window owner = getWindow(hoveredNode);
                    final boolean treeVisible = isWindowHierarchyVisible(hoveredNode);

                    // If the ACTIVATED tooltip is part of a visible window
                    // hierarchy, we can go ahead and show the tooltip and
                    // start the HIDE_TIMER.
                    //
                    // If the owner is null or invisible, then it either means a
                    // bug in our code, the node was removed from a scene or
                    // window or made invisible, or the node is not part of a
                    // visible window hierarchy. In that case, we don't show the
                    // tooltip, and we don't start the HIDE_TIMER. We simply let
                    // ACTIVATED_TIMER expire, and wait until the next mouse
                    // the movement to start it again.
                    if (owner != null && owner.isShowing() && treeVisible) {
                        double x = lastMouseX;
                        double y = lastMouseY;

                        // The tooltip always inherits the nodeOrientation of
                        // the Node that it is attached to (see RT-26147). It
                        // is possible to override this for the Tooltip plicontent
                        // (but not the popup placement) by setting the
                        // nodeOrientation on tooltip.getScene().getRoot().
                        NodeOrientation nodeOrientation = hoveredNode.getEffectiveNodeOrientation();
                        activatedTooltip.getScene().setNodeOrientation(nodeOrientation);
                        if (nodeOrientation == NodeOrientation.RIGHT_TO_LEFT) {
                            x -= activatedTooltip.getWidth();
                        }

                        // Added in JideFX
                        activatedTooltip.show(owner, x, y);
                        Point2D p = adjustTooltipLocation(hoveredNode, activatedTooltip);
                        if (p != null) {
                            activatedTooltip.setX(p.getX());
                            activatedTooltip.setY(p.getY());
                        }
                        // End Added


                        visibleTooltip = activatedTooltip;
                        hoveredNode = null;
                        hideTimer.playFromStart();
                    }

                    // Once the activation timer has expired, the tooltip is no
                    // longer in the activated state, it is only in the visible
                    // state, so we go ahead and set activated to false
                    activatedTooltip.setActivated(false);
                    activatedTooltip = null;
                }
            });

            hideTimer.getKeyFrames().add(new KeyFrame(visibleDuration));
            hideTimer.setOnFinished(new EventHandler() {
                @Override
                public void handle(ActionEvent event) {
                    // Hide the currently visible tooltip.
                    assert visibleTooltip != null;
                    visibleTooltip.hide();
                    visibleTooltip = null;
                    hoveredNode = null;
                }
            });

            leftTimer.getKeyFrames().add(new KeyFrame(closeDelay));
            leftTimer.setOnFinished(new EventHandler() {
                @Override
                public void handle(ActionEvent event) {
                    if (!hideOnExit) {
                        // Hide the currently visible tooltip.
                        assert visibleTooltip != null;
                        visibleTooltip.hide();
                        visibleTooltip = null;
                        hoveredNode = null;
                    }
                }
            });
        }

        /**
         * Registers for mouse move events only. When the mouse is moved, this handler will detect it and decide whether
         * to start the ACTIVATION_TIMER (if the ACTIVATION_TIMER is not started), restart the ACTIVATION_TIMER (if
         * ACTIVATION_TIMER is running), or skip the ACTIVATION_TIMER and just show the tooltip (if the LEFT_TIMER is
         * running).
         */
        private EventHandler MOVE_HANDLER = new EventHandler() {
            @Override
            public void handle(MouseEvent event) {
                //Screen coordinates need to be actual for dynamic tooltip.
                //See Tooltip.setText

                // detect bogus mouse moved events, if it didn't really move then ignore it
                double newMouseX = event.getScreenX();
                double newMouseY = event.getScreenY();
                if (newMouseX == lastMouseX && newMouseY == lastMouseY) {
                    return;
                }
                lastMouseX = newMouseX;
                lastMouseY = newMouseY;

                // If the HIDE_TIMER is running, then we don't want this event
                // handler to do anything, or change any state at all.
                if (hideTimer.getStatus() == Timeline.Status.RUNNING) {
                    return;
                }

                // Note that the "install" step will both register this handler
                // with the target node and also associate the tooltip with the
                // target node, by stashing it in the client properties of the node.
                hoveredNode = (Node) event.getSource();
                TooltipEx t = (TooltipEx) hoveredNode.getProperties().get(TOOLTIP_PROP_KEY);
                if (t != null) {
                    // In theory we should never get here with an invisible or
                    // non-existant window hierarchy, but might in some cases where
                    // people are feeding fake mouse events into the hierarchy. So
                    // we'll guard against that case.
                    final Window owner = getWindow(hoveredNode);
                    final boolean treeVisible = isWindowHierarchyVisible(hoveredNode);
                    if (owner != null && treeVisible) {
                        // Now we know that the currently HOVERED node has a tooltip
                        // and that it is part of a visible window Hierarchy.
                        // If LEFT_TIMER is running, then we make this tooltip
                        // visible immediately, stop the LEFT_TIMER, and start the
                        // HIDE_TIMER.
                        if (leftTimer.getStatus() == Timeline.Status.RUNNING) {
                            if (visibleTooltip != null) visibleTooltip.hide();
                            visibleTooltip = t;

                            // Added in JideFX
                            double x = event.getScreenX();
                            double y = event.getScreenY();
                            Point2D p = adjustTooltipLocation(hoveredNode, t);
                            if (p != null) {
                                t.show(owner, p.getX(), p.getY());
                            }
                            else {
                                t.show(owner, x, y);
                            }
                            // End Added
                            // t.show(owner, event.getScreenX(), event.getScreenY());


                            leftTimer.stop();
                            hideTimer.playFromStart();
                        }
                        else {
                            // Start / restart the timer and make sure the tooltip
                            // is marked as activated.
                            t.setActivated(true);
                            activatedTooltip = t;
                            activationTimer.stop();
                            activationTimer.playFromStart();
                        }
                    }
                }
                else {
                    // TODO should deregister, no point being here anymore!
                }
            }
        };

        /**
         * Registers for mouse exit events. If the ACTIVATION_TIMER is running then this will simply stop it. If the
         * HIDE_TIMER is running then this will stop the HIDE_TIMER, hide the tooltip, and start the LEFT_TIMER.
         */
        private EventHandler LEAVING_HANDLER = new EventHandler() {
            @Override
            public void handle(MouseEvent event) {
                // detect bogus mouse exit events, if it didn't really move then ignore it
                double newMouseX = event.getScreenX();
                double newMouseY = event.getScreenY();
                if (newMouseX == lastMouseX && newMouseY == lastMouseY) {
                    return;
                }

                if (activationTimer.getStatus() == Timeline.Status.RUNNING) {
                    activationTimer.stop();
                }
                else if (hideTimer.getStatus() == Timeline.Status.RUNNING) {
                    assert visibleTooltip != null;
                    hideTimer.stop();
                    if (hideOnExit) visibleTooltip.hide();
                    leftTimer.playFromStart();
                }

                hoveredNode = null;
                activatedTooltip = null;
                if (hideOnExit) visibleTooltip = null;
            }
        };

        /**
         * Registers for mouse click, press, release, drag events. If any of these occur, then the tooltip is hidden (if
         * it is visible), it is deactivated, and any and all timers are stopped.
         */
        private EventHandler KILL_HANDLER = new EventHandler() {
            @Override
            public void handle(MouseEvent event) {
                activationTimer.stop();
                hideTimer.stop();
                leftTimer.stop();
                if (visibleTooltip != null) visibleTooltip.hide();
                hoveredNode = null;
                activatedTooltip = null;
                visibleTooltip = null;
            }
        };

        private void install(Node node, TooltipEx t) {
            // Install the MOVE_HANDLER, LEAVING_HANDLER, and KILL_HANDLER on
            // the given node. Stash the tooltip in the node's client properties
            // map so that it is not gc'd. The handlers must all be installed
            // with a TODO weak reference so as not to cause a memory leak
            if (node == null) return;
            node.addEventHandler(MouseEvent.MOUSE_MOVED, MOVE_HANDLER);
            node.addEventHandler(MouseEvent.MOUSE_EXITED, LEAVING_HANDLER);
            node.addEventHandler(MouseEvent.MOUSE_PRESSED, KILL_HANDLER);
            node.getProperties().put(TOOLTIP_PROP_KEY, t);
        }

        private void uninstall(Node node) {
            if (node == null) return;
            node.removeEventHandler(MouseEvent.MOUSE_MOVED, MOVE_HANDLER);
            node.removeEventHandler(MouseEvent.MOUSE_EXITED, LEAVING_HANDLER);
            node.removeEventHandler(MouseEvent.MOUSE_PRESSED, KILL_HANDLER);
            TooltipEx t = (TooltipEx) node.getProperties().get(TOOLTIP_PROP_KEY);
            if (t != null) {
                node.getProperties().remove(TOOLTIP_PROP_KEY);
                if (t.equals(visibleTooltip) || t.equals(activatedTooltip)) {
                    KILL_HANDLER.handle(null);
                }
            }
        }

        /**
         * Gets the top level window associated with this node.
         *
         * @param node the node
         * @return the top level window
         */
        private Window getWindow(final Node node) {
            final Scene scene = node == null ? null : node.getScene();
            return scene == null ? null : scene.getWindow();
        }

        /**
         * Gets whether the entire window hierarchy is visible for this node.
         *
         * @param node the node to check
         * @return true if entire hierarchy is visible
         */
        private boolean isWindowHierarchyVisible(Node node) {
            boolean treeVisible = node != null;
            Parent parent = node == null ? null : node.getParent();
            while (parent != null && treeVisible) {
                treeVisible = parent.isVisible();
                parent = parent.getParent();
            }
            return treeVisible;
        }

    }

    // Added in JideFX

    private ObjectProperty _posProperty;

    public ObjectProperty posProperty() {
        if (_posProperty == null) {
            _posProperty = new SimpleObjectProperty<>(Pos.BOTTOM_RIGHT);
        }
        return _posProperty;
    }

    public Pos getPos() {
        return posProperty().get();
    }

    public void setPos(Pos pos) {
        posProperty().set(pos);
    }

    // Added in JideFX
    private static Point2D adjustTooltipLocation(Node hoveredNode, TooltipEx tooltip) {
        Pos pos = tooltip.getPos();
        Point2D nodeLocation = hoveredNode.localToScreen(0, 0);
        Point2D p = null;
        if (pos != null) {
            switch (pos) {
                case BOTTOM_RIGHT:
                    p = new Point2D(nodeLocation.getX() + hoveredNode.getLayoutBounds().getWidth(),
                            nodeLocation.getY() + hoveredNode.getLayoutBounds().getHeight());
                    break;
                case BOTTOM_LEFT:
                    p = new Point2D(nodeLocation.getX() - tooltip.prefWidth(-1),
                            nodeLocation.getY() + hoveredNode.getLayoutBounds().getHeight());
                    break;
            }
        }
        return p;
    }
    // End Added
}