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

org.pushingpixels.radiance.component.api.common.RichTooltipManager Maven / Gradle / Ivy

/*
 * Copyright (c) 2005-2022 Radiance Kirill Grouchnikov. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  o Neither the name of the copyright holder nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.pushingpixels.radiance.component.api.common;

import org.pushingpixels.radiance.component.api.common.popup.JPopupPanel;
import org.pushingpixels.radiance.component.api.common.popup.PopupPanelManager;
import org.pushingpixels.radiance.component.api.ribbon.AbstractRibbonBand;
import org.pushingpixels.radiance.component.internal.ui.common.JRichTooltipPanel;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.util.List;

public class RichTooltipManager {
    private Timer initialDelayTimer;

    private Timer dismissTimer;

    private RichTooltip richTooltip;

    private JTrackableComponent currentActiveTrackable;

    private MouseEvent lastMouseEvent;

    private MouseEvent lastMouseEventInCurrentActiveTrackableCoordinates;

    private final static RichTooltipManager sharedInstance = new RichTooltipManager();

    private Popup tipWindow;

    private JRichTooltipPanel tip;

    private boolean tipShowing = false;

    public static abstract class JTrackableComponent extends JComponent {
        public abstract RichTooltip getRichTooltip(MouseEvent mouseEvent);
    }

    private RichTooltipManager() {
        initialDelayTimer = new Timer(750, new InitialDelayTimerAction());
        initialDelayTimer.setRepeats(false);
        dismissTimer = new Timer(20000, new DismissTimerAction());
        dismissTimer.setRepeats(false);

        Toolkit.getDefaultToolkit().addAWTEventListener((AWTEvent event) -> {
            MouseEvent mouseEvent = (MouseEvent) event;
            switch (mouseEvent.getID()) {
                case MouseEvent.MOUSE_PRESSED:
                    hideTipWindow();
                    initialDelayTimer.stop();
                    currentActiveTrackable = null;
                    lastMouseEvent = null;
                    lastMouseEventInCurrentActiveTrackableCoordinates = null;
                    break;
                case MouseEvent.MOUSE_MOVED:
                    int x = mouseEvent.getX();
                    int y = mouseEvent.getY();
                    Component source = (Component) event.getSource();
                    Component deepest = SwingUtilities.getDeepestComponentAt(source, x, y);
                    JTrackableComponent trackableComponent =
                            (deepest instanceof JTrackableComponent) ?
                                    (JTrackableComponent) deepest :
                                    (JTrackableComponent) SwingUtilities.getAncestorOfClass(
                                            JTrackableComponent.class, deepest);
                    if (trackableComponent != null) {
                        Point inTrackableComponent = SwingUtilities.convertPoint(
                                source, x, y, trackableComponent);
                        if (trackableComponent.contains(inTrackableComponent)) {
                            // The mouse event is currently inside a trackable component
                            if (currentActiveTrackable == trackableComponent) {
                                // Still in the same trackable component
                                if (tipShowing) {
                                    checkForTipChange(mouseEvent, trackableComponent);
                                } else {
                                    // Lazily lookup the values from within insideTimerAction
                                    currentActiveTrackable = trackableComponent;
                                    lastMouseEvent = mouseEvent;
                                    lastMouseEventInCurrentActiveTrackableCoordinates =
                                            retarget(mouseEvent, source, trackableComponent);
                                    richTooltip = null;
                                    initialDelayTimer.restart();
                                }
                            } else {
                                // In a new trackable component
                                initiateToolTip(mouseEvent, trackableComponent);
                            }
                        } else {
                            // Not inside a trackable component
                            if (currentActiveTrackable != null) {
                                // Moved outside of a trackable component
                                windDownTooltip();
                            }
                        }
                    } else {
                        if (currentActiveTrackable != null) {
                            // Moved outside of a trackable component
                            windDownTooltip();
                        }
                    }
            }
        }, AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK);
    }

    /**
     * Specifies the initial delay value.
     *
     * @param milliseconds the number of milliseconds to delay (after the cursor has
     *                     paused) before displaying the tooltip
     * @see #getInitialDelay()
     */
    public void setInitialDelay(int milliseconds) {
        initialDelayTimer.setInitialDelay(milliseconds);
    }

    /**
     * Returns the initial delay value.
     *
     * @return an integer representing the initial delay value, in milliseconds
     * @see #setInitialDelay(int)
     */
    public int getInitialDelay() {
        return initialDelayTimer.getInitialDelay();
    }

    /**
     * Specifies the dismissal delay value.
     *
     * @param milliseconds the number of milliseconds to delay before taking away the
     *                     tooltip
     * @see #getDismissDelay()
     */
    public void setDismissDelay(int milliseconds) {
        dismissTimer.setInitialDelay(milliseconds);
    }

    /**
     * Returns the dismissal delay value.
     *
     * @return an integer representing the dismissal delay value, in
     * milliseconds
     * @see #setDismissDelay(int)
     */
    public int getDismissDelay() {
        return dismissTimer.getInitialDelay();
    }

    private MouseEvent retarget(MouseEvent original, Component source, Component target) {
        Point inTarget = SwingUtilities.convertPoint(
                source, original.getX(), original.getY(), target);
        return new MouseEvent(target, original.getID(), original.getWhen(),
                original.getModifiersEx(), inTarget.x, inTarget.y, original.getClickCount(),
                original.isPopupTrigger(), original.getButton());
    }

    private void showTipWindow(MouseEvent mouseEvent) {
        if (currentActiveTrackable == null || !currentActiveTrackable.isShowing()) {
            return;
        }
        Dimension size;
        Point screenLocation = currentActiveTrackable.getLocationOnScreen();
        Point location = new Point();
        GraphicsConfiguration gc;
        gc = currentActiveTrackable.getGraphicsConfiguration();
        Rectangle sBounds = gc.getBounds();
        Insets screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(gc);
        // Take into account screen insets, decrease viewport
        sBounds.x += screenInsets.left;
        sBounds.y += screenInsets.top;
        sBounds.width -= (screenInsets.left + screenInsets.right);
        sBounds.height -= (screenInsets.top + screenInsets.bottom);

        // Just to be paranoid
        hideTipWindow();

        tip = new JRichTooltipPanel(currentActiveTrackable.getRichTooltip(mouseEvent));
        tip.applyComponentOrientation(currentActiveTrackable.getComponentOrientation());
        size = tip.getPreferredSize();

        AbstractRibbonBand ribbonBand = (AbstractRibbonBand) SwingUtilities
                .getAncestorOfClass(AbstractRibbonBand.class, currentActiveTrackable);
        boolean ltr = tip.getComponentOrientation().isLeftToRight();
        boolean isInRibbonBand = (ribbonBand != null);
        if (isInRibbonBand) {
            // display directly below or above ribbon band
            location.x = ltr ? screenLocation.x : screenLocation.x
                    + currentActiveTrackable.getWidth() - size.width;
            Point bandLocationOnScreen = ribbonBand.getLocationOnScreen();
            location.y = bandLocationOnScreen.y + ribbonBand.getHeight() + 4;
            if ((location.y + size.height) > (sBounds.y + sBounds.height)) {
                location.y = bandLocationOnScreen.y - size.height;
            }
        } else {
            // display directly below or above it
            location.x = ltr ? screenLocation.x : screenLocation.x
                    + currentActiveTrackable.getWidth() - size.width;
            location.y = screenLocation.y + currentActiveTrackable.getHeight();
            if ((location.y + size.height) > (sBounds.y + sBounds.height)) {
                location.y = screenLocation.y - size.height;
            }
        }

        // Tweak the X location to not overflow the screen
        if (location.x < sBounds.x) {
            location.x = sBounds.x;
        } else if (location.x - sBounds.x + size.width > sBounds.width) {
            location.x = sBounds.x + Math.max(0, sBounds.width - size.width);
        }

        PopupFactory popupFactory = PopupFactory.getSharedInstance();
        tipWindow = popupFactory.getPopup(currentActiveTrackable, tip, location.x, location.y);
        tipWindow.show();

        dismissTimer.start();
        tipShowing = true;
    }

    private void windDownTooltip() {
        initialDelayTimer.stop();
        currentActiveTrackable = null;
        richTooltip = null;
        lastMouseEvent = null;
        lastMouseEventInCurrentActiveTrackableCoordinates = null;
        hideTipWindow();
    }

    private void hideTipWindow() {
        if (tipWindow != null) {
            tipWindow.hide();
            tipWindow = null;
            tipShowing = false;
            tip = null;
            dismissTimer.stop();
        }
    }

    /**
     * Returns a shared RichTooltipManager instance.
     *
     * @return a shared RichTooltipManager object
     */
    public static RichTooltipManager sharedInstance() {
        return sharedInstance;
    }

    public void hideCurrentlyShowingTipIfNecessary() {
        this.hideTipWindow();
    }

    private void initiateToolTip(MouseEvent original, JTrackableComponent component) {
        // do not show tooltips on components in popup panels that are not in the last shown one
        List popups =
                PopupPanelManager.defaultManager().getShownPath();
        if (popups.size() > 0) {
            JPopupPanel popupPanel = popups.get(popups.size() - 1).getPopupPanel();
            boolean ignore = true;
            Component c = component;
            while (c != null) {
                if (c == popupPanel) {
                    ignore = false;
                    break;
                }
                c = c.getParent();
            }
            if (ignore) {
                return;
            }
        }

        if (currentActiveTrackable != null) {
            initialDelayTimer.stop();
        }

        currentActiveTrackable = component;
        lastMouseEvent = original;
        lastMouseEventInCurrentActiveTrackableCoordinates =
                retarget(original, lastMouseEvent.getComponent(), component);
        initialDelayTimer.start();
    }

    private void checkForTipChange(MouseEvent event, JTrackableComponent component) {
        RichTooltip newTooltip = component.getRichTooltip(event);

        // is it different?
        boolean isDifferent = (richTooltip != newTooltip);
        if (isDifferent) {
            hideTipWindow();
            if (newTooltip != null) {
                richTooltip = newTooltip;
                initialDelayTimer.restart();
            }
        }
    }

    private class InitialDelayTimerAction implements ActionListener {
        public void actionPerformed(ActionEvent e) {
            if ((currentActiveTrackable != null) && currentActiveTrackable.isShowing()) {
                // Lazy lookup
                if (richTooltip == null && lastMouseEvent != null) {
                    richTooltip = currentActiveTrackable.getRichTooltip(
                            lastMouseEventInCurrentActiveTrackableCoordinates);
                }
                if (richTooltip != null) {
                    boolean showRichTooltip = true;
                    // check that no visible popup is originating in this
                    // component
                    for (PopupPanelManager.PopupInfo pi : PopupPanelManager
                            .defaultManager().getShownPath()) {
                        if (pi.getPopupOriginator() == currentActiveTrackable) {
                            showRichTooltip = false;
                            break;
                        }
                    }

                    if (showRichTooltip) {
                        showTipWindow(lastMouseEventInCurrentActiveTrackableCoordinates);
                    }
                } else {
                    currentActiveTrackable = null;
                    richTooltip = null;
                    lastMouseEvent = null;
                    lastMouseEventInCurrentActiveTrackableCoordinates = null;
                    hideTipWindow();
                }
            }
        }
    }

    private class DismissTimerAction implements ActionListener {
        public void actionPerformed(ActionEvent e) {
            hideTipWindow();
            initialDelayTimer.stop();
            currentActiveTrackable = null;
            lastMouseEvent = null;
            lastMouseEventInCurrentActiveTrackableCoordinates = null;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy