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

com.alee.managers.hover.HoverManager Maven / Gradle / Ivy

/*
 * This file is part of WebLookAndFeel library.
 *
 * WebLookAndFeel library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * WebLookAndFeel library 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 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with WebLookAndFeel library.  If not, see .
 */

package com.alee.managers.hover;

import com.alee.api.annotations.NotNull;
import com.alee.api.annotations.Nullable;
import com.alee.api.jdk.BiConsumer;
import com.alee.utils.CoreSwingUtils;
import com.alee.utils.collection.ImmutableList;
import com.alee.utils.swing.WeakComponentDataList;

import javax.swing.*;
import java.awt.*;
import java.awt.event.AWTEventListener;
import java.awt.event.MouseEvent;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * This manager allows you to track certain component their children hover state by adding your custom {@link HoverTracker} or
 * {@link GlobalHoverListener} to track component hover state.
 *
 * @author Mikle Garin
 * @see How to use HoverManager
 */
public final class HoverManager
{
    /**
     * {@link List} of all registered {@link GlobalHoverListener}s.
     * Use these listeners with care as they are not tied to anything and will remain in memory as long as you keep them registered.
     */
    @NotNull
    private static final List globalHoverListeners = new ArrayList ( 5 );

    /**
     * {@link GlobalHoverListener}s registered for specific {@link JComponent}s.
     */
    @NotNull
    private static final WeakComponentDataList globalComponentHoverListeners =
            new WeakComponentDataList ( "HoverManager.GlobalHoverListener", 5 );

    /**
     * {@link HoverTracker}s registered for specific {@link JComponent}s.
     */
    @NotNull
    private static final WeakComponentDataList trackers =
            new WeakComponentDataList ( "HoverManager.HoverTracker", 200 );

    /**
     * Reference to previously hovered {@link Component}.
     */
    private static WeakReference previousHoverOwner;

    /**
     * Reference to currently hovered {@link Component}.
     */
    private static WeakReference hoverOwner;

    /**
     * Reference to currently hovered {@link Component}'s {@link Window}.
     * It is saved as a more reliable source of the new hover {@link Component} rather than previous hover {@link Component}.
     */
    private static WeakReference hoverOwnerWindow;

    /**
     * Queue of hover events for {@link Component}s.
     * Event time acts as a key, order is irrelevant hence the {@link Map} usage.
     * All events are queued on EDT and will simply use this {@link Map} to retrieve hover {@link Component}.
     */
    private static final Map hoverEventQueue = new HashMap ();

    /**
     * Whether manager is initialized or not.
     */
    private static boolean initialized = false;

    /**
     * Initializes manager if it wasn't already initialized.
     */
    public static synchronized void initialize ()
    {
        // To avoid more than one initialization
        if ( !initialized )
        {
            // Remember that initialization happened
            initialized = true;

            // Initial values
            previousHoverOwner = new WeakReference ( null );
            hoverOwner = new WeakReference ( null );
            hoverOwnerWindow = new WeakReference ( null );

            /**
             * Listening to mouse ENTER & EXIT events, equivalent of {@link java.awt.event.MouseListener}.
             * This one is important as it is the only listener that will properly fire EXIT event when mouse leaves window bounds.
             * EXIT event will also be fired whenever user alt-tabs out of the window.
             * Listening to ENTER event is added to avoid unnecessary updates when EXIT & ENTER are fired in a sequence.
             */
            Toolkit.getDefaultToolkit ().addAWTEventListener ( new AWTEventListener ()
            {
                @Override
                public void eventDispatched ( final AWTEvent event )
                {
                    final MouseEvent mouseEvent = ( MouseEvent ) event;
                    if ( mouseEvent.getID () == MouseEvent.MOUSE_ENTERED )
                    {
                        queueEvent (
                                mouseEvent.getWhen (),
                                findNearbyHoverOwner ( mouseEvent.getComponent (), mouseEvent.getPoint () )
                        );
                    }
                    else if ( mouseEvent.getID () == MouseEvent.MOUSE_EXITED )
                    {
                        queueEvent (
                                mouseEvent.getWhen (),
                                null
                        );
                    }
                }
            }, AWTEvent.MOUSE_EVENT_MASK );

            /**
             * Listening to mouse MOVE & DRAG events, equivalent of {@link java.awt.event.MouseMotionListener}.
             * Listening to these events is necessary because {@link AWTEvent#MOUSE_EVENT_MASK} doesn't work for all components.
             * Basically mouse ENTER & EXIT are only fired for components that have {@link java.awt.event.MouseListener} installed.
             * That makes usage of {@link AWTEvent#MOUSE_EVENT_MASK} alone impossible even though it would be perfect for hover updates.
             * So instead we're listening to mouse movement over the window to track hovered component and only push updates when needed.
             */
            Toolkit.getDefaultToolkit ().addAWTEventListener ( new AWTEventListener ()
            {
                @Override
                public void eventDispatched ( final AWTEvent event )
                {
                    final MouseEvent mouseEvent = ( MouseEvent ) event;
                    if ( mouseEvent.getID () == MouseEvent.MOUSE_MOVED || mouseEvent.getID () == MouseEvent.MOUSE_DRAGGED )
                    {
                        queueEvent (
                                mouseEvent.getWhen (),
                                findNearbyHoverOwner ( mouseEvent.getComponent (), mouseEvent.getPoint () )
                        );
                    }
                }
            }, AWTEvent.MOUSE_MOTION_EVENT_MASK );

            /**
             * Listening for component SHOW, HIDE, RESIZE and MOVE events, equivalent of {@link java.awt.event.ComponentListener}.
             * This is necesssary to update hover state upon various component state changes, here are some examples:
             * - Component becoming hidden or visible under the cursor location
             * - Component moving under or out of the cursor location due to layout/hierarchy change
             * - Component moving under or out of the cursor location due to window location change
             * - Component moving under or out of the cursor location due to component decoration changes
             */
            Toolkit.getDefaultToolkit ().addAWTEventListener ( new AWTEventListener ()
            {
                @Override
                public void eventDispatched ( final AWTEvent event )
                {
                    queuePostLayoutUpdateEvent ();
                }
            }, AWTEvent.COMPONENT_EVENT_MASK );
        }
    }

    /**
     * Queues hover state change update to be performed later.
     * This is necessary to avoid unwanted updates to be skipped.
     * Most prominent case being {@link MouseEvent#MOUSE_ENTERED} being fired right after {@link MouseEvent#MOUSE_EXITED}.
     * Due to how these events are separated there is no convenient way to perform correct update right away.
     * We have to wait and see whether {@link MouseEvent#MOUSE_EXITED} was fired alone or not.
     *
     * @param time      event time in milliseconds, used to determine paired events
     * @param component {@link Component} that event was fired on
     */
    private static void queueEvent ( final long time, @Nullable final Component component )
    {
        if ( !hoverEventQueue.containsKey ( time ) )
        {
            SwingUtilities.invokeLater ( new Runnable ()
            {
                @Override
                public void run ()
                {
                    if ( hoverEventQueue.containsKey ( time ) )
                    {
                        fireHoverChanged ( hoverEventQueue.remove ( time ) );
                    }
                }
            } );
        }
        hoverEventQueue.put ( time, component );
    }

    /**
     * Queues hover state change update to be performed after all layout updates are done.
     * This call queues magical double {@link SwingUtilities#invokeLater(Runnable)} to make sure it runs past layot updates.
     * This is tricky, but can be explained by the order operations are performed in Swing when some event occurs.
     *
     * Let's say something happens on EDT and UI has to handle it, this is how it goes:
     * (EDT1, EDT2, etc - these are simply different {@link Runnable}s, obviously all executed on EDT)
     *
     * [EDT1]
     * 1. Something has changed, for instance component was removed from layout
     * 2. Past it's removal events are being fired for various listeners (ancestor/container/hierarchy/etc)
     * 3. Our {@link HoverManager} receives {@link AWTEvent} somewhere here and {@link #queuePostLayoutUpdateEvent()} is called
     * 4. Listeners of those events queue later layout {@link JComponent#revalidate()} and {@link JComponent#repaint()}
     * [EDT2]
     * x. Some other stuff might execute here, whatever was queued for "later" in previous EDT run
     * x. It could be some other components performing actions or firing events
     * x. Can also simply be UI repainting something due to user interactions or system event
     * [EDT3]
     * 5. We finally got to validation of our layout, it is being updated here
     * [EDT4]
     * 6. On a separate run we also get a visual update in case layout changed
     * [EDT5]
     * 7. We're finally where we want our {@link #queuePostLayoutUpdateEvent()} method body to actually be performed
     *
     * So the important part in this order is when (3) and (4) are executed.
     * If (4) is done before (3) - which can happen, but depends on implementation - we would only need to send our update event to
     * {@link SwingUtilities#invokeLater(Runnable)} once and we would receive wanted result because (7) will occur after (5) and (6).
     * In reality there is no guarantee that our listener won't receive event earlier than listeners that queue layour and visual updates.
     * That is why we have to use double {@link SwingUtilities#invokeLater(Runnable)} to ensure that it gets past (5) and (6).
     */
    private static void queuePostLayoutUpdateEvent ()
    {
        SwingUtilities.invokeLater ( new Runnable ()
        {
            @Override
            public void run ()
            {
                SwingUtilities.invokeLater ( new Runnable ()
                {
                    @Override
                    public void run ()
                    {
                        fireHoverChanged ( findPossibleHoverOwner () );
                    }
                } );
            }
        } );
    }

    /**
     * Returns possible new hover owner on the {@link Window} of the previous hover owner.
     * We don't need to use previous hover owner {@link Component} because it is less reliable.
     * We also will be extracting it's {@link JRootPane} owner anyway, so it is faster to use {@link Window}.
     * We don't want to use any other {@link Window}s because even though we can access them - there is no guarantee they are on top.
     * Otherwise we're rising to provide hover component in a {@link Window} that is not even physically visible to user.
     *
     * @return possible new hover owner on the {@link Window} of the previous hover owner
     */
    @Nullable
    private static Component findPossibleHoverOwner ()
    {
        final Component newHover;
        final Window currentHoverWindow = hoverOwnerWindow.get ();
        if ( currentHoverWindow != null && currentHoverWindow.isShowing () )
        {
            newHover = findNearbyHoverOwner (
                    currentHoverWindow,
                    CoreSwingUtils.getMouseLocation ( currentHoverWindow )
            );
        }
        else
        {
            newHover = null;
        }
        return newHover;
    }

    /**
     * Returns hover owner in the same window where {@link Component} is located or {@code null} if none are hovered.
     * Note that this method limits hover detection to window implementations that have {@link JRootPane}.
     * Problem with {@link Window} is that we might step outside of the root pane but still hit window decoration with the check.
     *
     * We're not using {@link SwingUtilities#convertPoint(Component, Point, Component)} here for optimization reasons.
     * Retrieving component location on screen is way more time-consuming operation and we don't really need it here.
     *
     * @param component {@link Component} to find hover owner nearby
     * @param point     {@link MouseEvent} point relative to {@link Component}
     * @return hover owner in the same window where {@link Component} is located or {@code null} if none are hovered
     */
    @Nullable
    private static Component findNearbyHoverOwner ( @Nullable final Component component, @NotNull final Point point )
    {
        final Component newHoverOwner;
        if ( component != null )
        {
            final JRootPane rootPane = CoreSwingUtils.getRootPane ( component );
            if ( rootPane != null && rootPane.isShowing () )
            {
                if ( rootPane == component )
                {
                    // Point is relative to the root pane
                    newHoverOwner = CoreSwingUtils.getTopComponentAt ( rootPane, point );
                }
                else if ( rootPane.isAncestorOf ( component ) )
                {
                    // Recalculating point relative to one of the root pane children
                    final Point relative = new Point ( point );
                    Component parent = component;
                    while ( parent != rootPane && parent != null )
                    {
                        final Point location = parent.getLocation ();
                        relative.x += location.x;
                        relative.y += location.y;
                        parent = parent.getParent ();
                    }
                    if ( parent != null )
                    {
                        // Point is relative to one of the root pane children
                        newHoverOwner = CoreSwingUtils.getTopComponentAt ( rootPane, relative );
                    }
                    else
                    {
                        // Something changed in structure and we can't find top component anymore
                        // This happened on toolbar drag, hence this workaround was added to avoid NPE
                        newHoverOwner = null;
                    }
                }
                else
                {
                    // Recalculating point relative to one of the root pane parents
                    final Point relative = new Point ( point );
                    Component parent = rootPane;
                    while ( parent != component && parent != null )
                    {
                        final Point location = parent.getLocation ();
                        relative.x -= location.x;
                        relative.y -= location.y;
                        parent = parent.getParent ();
                    }
                    if ( parent != null )
                    {
                        // Point is relative to one of the root pane parents
                        newHoverOwner = CoreSwingUtils.getTopComponentAt ( rootPane, relative );
                    }
                    else
                    {
                        // Something changed in structure and we can't find top component anymore
                        // This happened on toolbar drag, hence this workaround was added to avoid NPE
                        newHoverOwner = null;
                    }
                }
            }
            else
            {
                // Component window doesn't have root pane
                newHoverOwner = null;
            }
        }
        else
        {
            // Component is unavailable
            newHoverOwner = null;
        }
        return newHoverOwner;
    }

    /**
     * Returns previously hovered {@link Component}.
     *
     * @return previously hovered {@link Component}
     */
    @Nullable
    public static Component getPreviousHoverOwner ()
    {
        return previousHoverOwner.get ();
    }

    /**
     * Returns currently hovered {@link Component}.
     *
     * @return currently hovered {@link Component}
     */
    @Nullable
    public static Component getHoverOwner ()
    {
        return hoverOwner != null ? hoverOwner.get () : null;
    }

    /**
     * Returns currently hovered {@link Window}.
     *
     * @return currently hovered {@link Window}
     */
    @Nullable
    public static Window getHoverOwnerWindow ()
    {
        return hoverOwnerWindow != null ? hoverOwnerWindow.get () : null;
    }

    /**
     * Registers new {@link GlobalHoverListener}.
     * Use these listeners with care as they are not tied to anything and will remain in memory as long as you keep them registered.
     *
     * @param listener {@link GlobalHoverListener} to register
     */
    public static void registerGlobalHoverListener ( @NotNull final GlobalHoverListener listener )
    {
        synchronized ( globalHoverListeners )
        {
            globalHoverListeners.add ( listener );
        }
    }

    /**
     * Unregisters {@link GlobalHoverListener}.
     * Use these listeners with care as they are not tied to anything and will remain in memory as long as you keep them registered.
     *
     * @param listener {@link GlobalHoverListener} to unregister
     */
    public static void unregisterGlobalHoverListener ( @NotNull final GlobalHoverListener listener )
    {
        synchronized ( globalHoverListeners )
        {
            globalHoverListeners.remove ( listener );
        }
    }

    /**
     * Registers new {@link GlobalHoverListener}.
     *
     * @param component {@link JComponent} to register {@link GlobalHoverListener} for
     * @param listener  {@link GlobalHoverListener} to register
     */
    public static void registerGlobalHoverListener ( @NotNull final JComponent component, @NotNull final GlobalHoverListener listener )
    {
        globalComponentHoverListeners.add ( component, listener );
    }

    /**
     * Unregisters {@link GlobalHoverListener}.
     *
     * @param component {@link JComponent} to unregister {@link GlobalHoverListener} from
     * @param listener  {@link GlobalHoverListener} to unregister
     */
    public static void unregisterGlobalHoverListener ( @NotNull final JComponent component, @NotNull final GlobalHoverListener listener )
    {
        globalComponentHoverListeners.remove ( component, listener );
    }

    /**
     * Registers hover tracker.
     *
     * @param component    component to add tracker for
     * @param hoverTracker new hover tracker
     */
    public static void addHoverTracker ( @NotNull final JComponent component, @NotNull final HoverTracker hoverTracker )
    {
        trackers.add ( component, hoverTracker );
    }

    /**
     * Unregisters specified hover tracker.
     *
     * @param component    component to remove tracker from
     * @param hoverTracker hover tracker to unregister
     */
    public static void removeHoverTracker ( @NotNull final JComponent component, @NotNull final HoverTracker hoverTracker )
    {
        trackers.remove ( component, hoverTracker );
    }

    /**
     * Unregisters all hover trackers from the specified component.
     *
     * @param component component to unregister all hover trackers from
     */
    public static void removeHoverTrackers ( @NotNull final JComponent component )
    {
        trackers.clear ( component );
    }

    /**
     * Fires all hover listeners.
     *
     * @param hover newly hovered {@link Component}
     */
    private static void fireHoverChanged ( @Nullable final Component hover )
    {
        /**
         * Last second check that hovered {@link Component} is still showing.
         * We don't ever want to pass hover to {@link Component} that is not currently showing on the screen.
         * Also we don't want to fire events when actual hover component haven't changed.
         */
        final Component newHover = hover == null || hover.isShowing () ? hover : null;
        final Component oldHover = hoverOwner.get ();
        if ( newHover != oldHover )
        {
            // Updating weak references
            previousHoverOwner = new WeakReference ( oldHover );
            hoverOwner = new WeakReference ( newHover );
            hoverOwnerWindow = new WeakReference ( CoreSwingUtils.getWindowAncestor ( newHover ) );

            // Iterating through all registered hover trackers
            trackers.forEachData ( new BiConsumer ()
            {
                @Override
                public void accept ( final JComponent tracked, final HoverTracker hoverTracker )
                {
                    // Checking whether or not tracker is currently enabled
                    if ( hoverTracker.isEnabled () )
                    {
                        // Checking whether or not component is related to this hover change
                        final boolean isOldHovered = hoverTracker.isInvolved ( tracked, oldHover );
                        final boolean isNewHovered = hoverTracker.isInvolved ( tracked, newHover );
                        if ( isOldHovered || isNewHovered )
                        {
                            // Checking whether or not hover state actually changed for the tracked component
                            if ( hoverTracker.isHovered () != isNewHovered )
                            {
                                // Updating hover state
                                hoverTracker.setHovered ( isNewHovered );

                                // Informing tracker about hover change
                                hoverTracker.hoverChanged ( isNewHovered );
                            }
                        }
                    }
                }
            } );

            // Firing global component listeners
            globalComponentHoverListeners.forEachData ( new BiConsumer ()
            {
                @Override
                public void accept ( final JComponent component, final GlobalHoverListener listener )
                {
                    listener.hoverChanged ( oldHover, newHover );
                }
            } );

            // Firing global listeners
            final ImmutableList globalListenersCopy;
            synchronized ( globalHoverListeners )
            {
                globalListenersCopy = new ImmutableList ( HoverManager.globalHoverListeners );
            }
            for ( final GlobalHoverListener listener : globalListenersCopy )
            {
                listener.hoverChanged ( oldHover, newHover );
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy