Package com.samskivert.swing

Source Code of com.samskivert.swing.RadialMenu$Predicate

//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2012 Michael Bayne, et al.
// http://github.com/samskivert/samskivert/blob/master/COPYING

package com.samskivert.swing;

import java.awt.Component;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;

import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;

import java.util.ArrayList;

import javax.swing.Icon;
import javax.swing.ImageIcon;

import com.samskivert.swing.event.CommandEvent;
import com.samskivert.swing.util.MouseHijacker;
import com.samskivert.swing.util.SwingUtil;
import com.samskivert.util.ObserverList;
import com.samskivert.util.RunAnywhere;

/**
* Provides a radial menu with iconic menu items that expand to include textual descriptions when
* moused over.
*/
public class RadialMenu
    implements MouseMotionListener, MouseListener
{
    /**
     * The interface used to communicate back to the component that hosts this menu.
     */
    public static interface Host
    {
        /**
         * Returns the component on which we will be rendered.
         */
        public Component getComponent ();

        /**
         * Requests the bounds of the visible region in which we'll be rendering the menu. The menu
         * will be constrained to fit within these bounds starting with a natural position around
         * the supplied target bounds and adjusting minimally to fit.
         */
        public Rectangle getViewBounds ();

        /**
         * Requests that the appropriate region of the host be repainted.
         */
        public void repaintRect (int x, int y, int width, int height);

        /**
         * Instructs the host component to stop rendering this menu because it has been popped
         * down.
         */
        public void menuDeactivated (RadialMenu menu);
    }

    /**
     * Used to determine at the time that a menu is shown, whether its items should be included
     * and/or enabled. Predicate methods are called immediately before showing a radial menu and
     * thus should not perform complicated computations. They generally will check some simple
     * model to determine a menu item's status.
     */
    public static interface Predicate
    {
        /**
         * If true, the menu will be included in the character menu when it is displayed.
         *
         * @param menu the menu that will include or not include the item in question.
         * @param item the menu item that will or will not be included.
         */
        public boolean isIncluded (RadialMenu menu, RadialMenuItem item);

        /**
         * If true, the menu will be enabled when displayed in a character's menu.
         *
         * @param menu the menu that contains the item in question.
         * @param item the menu item that will or will not be enabled.
         */
        public boolean isEnabled (RadialMenu menu, RadialMenuItem item);
    }

    /**
     * An additional interface that can be implemented by the predicate to display extra status.
     */
    public static interface IconPredicate extends Predicate
    {
        /**
         * Return an additional predicate that should be drawn.
         */
        public Icon getIcon (RadialMenu menu, RadialMenuItem item);
    }

    /**
     * Constructs a radial menu.
     */
    public RadialMenu ()
    {
    }

    /**
     * Adds a menu item to the menu. The menu should not currently be active.
     *
     * @param command the command to be issued when the item is selected.
     * @param label the textual label to be displayed with the menu item.
     * @param icon the icon to display next to the menu text or null if no
     * icon is desired.
     * @param predicate a predicate that will be used to determine whether or not this menu item
     * should be included in the menu and enabled when it is shown.
     *
     * @return the item that was added to the menu. It can be modified while the menu is not being
     * displayed.
     */
    public RadialMenuItem addMenuItem (String command, String label, Image icon, Predicate predicate)
    {
        RadialMenuItem item = new RadialMenuItem(command, label, new ImageIcon(icon), predicate);
        addMenuItem(item);
        return item;
    }

    /**
     * Adds an already constructed menu item to the menu. The menu should not currently be active.
     *
     * @param item the menu item instance to be added.
     */
    public void addMenuItem (RadialMenuItem item, boolean makeDefault)
    {
        addMenuItem(item);
        if (makeDefault) {
            _defaultItem = item;
        }
    }

    /**
     * Adds an already constructed menu item to the menu. The menu should not currently be active.
     *
     * @param item the menu item instance to be added.
     */
    public void addMenuItem (RadialMenuItem item)
    {
        _items.add(item);
    }

    /**
     * Removes the specified menu item from this menu.
     */
    public void removeMenuItem (RadialMenuItem item)
    {
        _items.remove(item);
    }

    /**
     * Removes the first menu item that matches the specified command from this menu.
     *
     * @return true if a matching menu item was removed, false if not.
     */
    public boolean removeMenuItem (String command)
    {
        int icount = _items.size();
        for (int i = 0; i < icount; i++) {
            RadialMenuItem item = _items.get(i);
            if (item.command.equals(command)) {
                _items.remove(i);
                return true;
            }
        }

        return false;
    }

    /**
     * May be called by an item when it changes in a material way.
     */
    public void itemUpdated ()
    {
        if (_host != null) {
            layout();
            repaint();
        }
    }

    /**
     * Adds a listener that will be notified when a menu item is selected. When the the menu is
     * popped down, all listeners will be cleared.
     */
    public void addActionListener (ActionListener listener)
    {
        _actlist.add(listener);
    }

    /**
     * Sets the optional centerpiece icon to be displayed in the center of the menu.
     */
    public void setCenterIcon (Icon icon)
    {
        _centerIcon = icon;
    }

    /**
     * Activates the radial menu, rendering a menu around the prescribed bounds. It is expected
     * that the host component will subsequently call {@link #render} if the menu invalidates the
     * region of the component occupied by the menu and requests it to repaint.
     *
     * @param host the host component within which the radial menu is displayed.
     * @param bounds the bounds of the object that was clicked to activate the menu.
     * @param argument a reference to an object that will be provided along with the command that
     * is issued if the user selects a menu item (unless that item has an overriding argument, in
     * which case the overriding argument will be used).
     */
    public void activate (Host host, Rectangle bounds, Object argument)
    {
        setActivationArgument(argument);
        activate(host, bounds);
    }

    /**
     * Activates the radial menu, rendering a menu around the prescribed bounds. It is expected
     * that the host component will subsequently call {@link #render} if the menu invalidates the
     * region of the component occupied by the menu and requests it to repaint.
     *
     * @param host the host component within which the radial menu is displayed.
     * @param bounds the bounds of the object that was clicked to activate the menu.
     */
    public void activate (Host host, Rectangle bounds)
    {
        _host = host;
        _tbounds = bounds;
        _poptime = System.currentTimeMillis();
        _msMode = false;

        Component comp = _host.getComponent();
        _hijacker = new MouseHijacker(comp);

        // start listening to the host component
        comp.addMouseListener(this);
        comp.addMouseMotionListener(this);

        // lay ourselves out and repaint
        layout();
        repaint();
    }

    /**
     * Sets the argument that will be defaultly posted along with commands.
     */
    public void setActivationArgument (Object arg)
    {
        _argument = arg;
    }

    /**
     * Returns the argument provided to this menu when {@link #activate} was called. This will only
     * be non-null while the menu is active and only then if an argument was provided in the call
     * to {@link #activate}.
     */
    public Object getActivationArgument ()
    {
        return _argument;
    }

    /**
     * Deactivates the menu.
     */
    public void deactivate ()
    {
        if (_host != null) {
            // unwire ourselves from the host component
            Component comp = _host.getComponent();
            comp.removeMouseListener(this);
            comp.removeMouseMotionListener(this);

            // reinstate the previous mouse listeners
            if (_hijacker != null) {
                _hijacker.release();
                _hijacker = null;
            }

            // tell our host that we are no longer
            _host.menuDeactivated(this);

            // fire off a last repaint to clean up after ourselves
            repaint();
        }

        // clear out our references
        _host = null;
        _tbounds = null;
        _argument = null;

        // clear out our action listeners
        _actlist.clear();
    }

    /**
     * Renders the current configuration of this menu.
     */
    public void render (Graphics2D gfx)
    {
        Component host = _host.getComponent();

        int x = _bounds.x, y = _bounds.y;

        if (_centerLabel != null) {
            // render the centerpiece label
            _centerLabel.paint(gfx, x + _centerLabel.closedBounds.x,
                               y + _centerLabel.closedBounds.y, this);
        }

        // render each of our items in turn
        int icount = _items.size();
        for (int i = 0; i < icount; i++) {
            RadialMenuItem item = _items.get(i);
            if (!item.isIncluded(this)) {
                continue;
            }
            // we have to wait and render the active item last
            if (item != _activeItem) {
                item.render(host, this, gfx, x + item.closedBounds.x, y + item.closedBounds.y);
            }
        }

        if (_activeItem != null) {
            _activeItem.render(host, this, gfx, x + _activeItem.closedBounds.x,
                               y + _activeItem.closedBounds.y);
        }

        // render our bounds
//         gfx.setColor(Color.green);
//         gfx.draw(_bounds);
//         gfx.setColor(Color.blue);
//         gfx.draw(_tbounds);
    }

    // documentation inherited
    public void mouseDragged (MouseEvent event)
    {
        // same as moved
        mouseMoved(event);
    }

    // documentation inherited
    public void mouseMoved (MouseEvent event)
    {
        int x = event.getX(), y = event.getY();
        // translate the coords into our space
        x -= _bounds.x;
        y -= _bounds.y;
        boolean repaint = false;

        // see if we dragged out of the active item
        if (_activeItem != null) {
            // oldway: if (!_activeItem.openBounds.contains(x, y)) {
            if (!_activeItem.closedBounds.contains(x, y)) {
                _activeItem.setActive(false);
                _activeItem = null;
                repaint = true;
            }
        }

        // see if we dragged into a new menu item
        if ((_activeItem == null) &&
            (RunAnywhere.getWhen(event) > _poptime + DEBOUNCE_DELAY)) {

            int icount = _items.size();
            for (int i = 0; i < icount; i++) {
                RadialMenuItem item = _items.get(i);
                if (item.isIncluded(this) &&
                    item.closedBounds.contains(x, y)) {
                    _activeItem = item;
                    _activeItem.setActive(true);
                    repaint = true;

                    // if we got here with a mouse moved, we've definately made the decision to be
                    // in non drag mode
                    if (event.getID() == MouseEvent.MOUSE_MOVED) {
                        _msMode = true;
                    }
                    break;
                }
            }
        }

        // repaint ourselves if something changed
        if (repaint) {
            repaint();
        }
    }

    // documentation inherited
    public void mouseClicked (MouseEvent event)
    {
    }

    // documentation inherited
    public void mouseEntered (MouseEvent event)
    {
    }

    // documentation inherited
    public void mouseExited (MouseEvent event)
    {
    }

    // documentation inherited
    public void mousePressed (MouseEvent event)
    {
    }

    // documentation inherited
    public void mouseReleased (MouseEvent event)
    {
        if (_activeItem == null) {
            long when = RunAnywhere.getWhen(event);
            if (!_msMode && (when < _poptime + DRAGMODE_DELAY)) {
                // if the dragmode delay time hasn't passed, then we're going to leave the menu up
                // because the user wants it to behave like a MS Windows menu.
                _msMode = true;
                return;

            // if they've double clicked, then activate the default item
            } else if (_msMode && (when < _poptime + DOUBLECLICK_DELAY)) {
                _activeItem = _defaultItem;
            }
        }

        // deactivate the active item and post a command for it
        if (_activeItem != null) {
            // only process the selection if the item is enabled
            if (!_activeItem.isEnabled(this)) {
                return;
            }

            // if the item has an overriding argument, use that
            if (_activeItem.command != null) {
                Object itemArg = _activeItem.argument;
                Object arg = (itemArg == null) ? _argument : itemArg;

                // notify our listeners that the action is posted
                final CommandEvent evt = new CommandEvent(
                    _host.getComponent(), _activeItem.command, arg);
                _actlist.apply(new ObserverList.ObserverOp<ActionListener>() {
                    public boolean apply (ActionListener obs) {
                        obs.actionPerformed(evt);
                        return true;
                    }
                });
            }

            _activeItem.setActive(false);
            _activeItem = null;
        }

        // deactive ourselves
        deactivate();
    }

    /**
     * Lays the radial menu items out based on the currently configured bounds information.
     */
    protected void layout ()
    {
        Graphics2D gfx = (Graphics2D)_host.getComponent().getGraphics();
        Font font = gfx.getFont();
        _bounds = new Rectangle();

        // figure out which items are included
        int icount = _items.size();
        ArrayList<RadialMenuItem> items = new ArrayList<RadialMenuItem>();
        for (int i = 0; i < icount; i++) {
            RadialMenuItem item = _items.get(i);
            if (item.isIncluded(this)) {
                items.add(item);
            }
        }

        // lay out all of our menu items
        int maxwid = 0, maxhei = 0;
        icount = items.size();
        for (int i = 0; i < icount; i++) {
            RadialMenuItem item = items.get(i);
            item.layout(gfx, font);

            // track maximum menu item size
            if (item.closedBounds.width > maxwid) {
                maxwid = item.closedBounds.width;
            }
            if (item.closedBounds.height > maxhei) {
                maxhei = item.closedBounds.height;
            }
        }
        gfx.dispose();

        // use the maximum of either width or height and make a circle around that
        double radius = Math.max(_tbounds.height, _tbounds.width) / 2;

        // be sure to add a gap and space for the menu item itself
        radius += (5 + maxwid/2);

        // compute the angle between menu items (we use the diameter of the menu items as an
        // approximate measure of the distance along the circumference)
        double theta = (maxwid + 10) / radius ;

        // now position each item accordingly
        double angle = -Math.PI/2;
        for (int i = 0; i < icount; i++) {
            RadialMenuItem item = items.get(i);
            int ix = (int)(radius * Math.cos(angle));
            int iy = (int)(radius * Math.sin(angle));
            item.openBounds.x = item.closedBounds.x = ix - maxwid/2;
            item.openBounds.y = item.closedBounds.y = iy - maxhei/2;

            // move along the circle
            angle += theta;
        }

        // create and position the centerpiece label
        if (_centerIcon != null) {
            _centerLabel = new RadialLabelSausage("", _centerIcon);
            _centerLabel.layout(gfx, font);
            _centerLabel.openBounds.x = _centerLabel.closedBounds.x =
                -(_centerLabel.closedBounds.width / 2);
            _centerLabel.openBounds.y = _centerLabel.closedBounds.y =
                -(_centerLabel.closedBounds.height / 2);
        }

        // now compute the rectangle that encloses the entire menu

        // include the bounds for the centerpiece label
        if (_centerLabel != null) {
            _bounds.add(_centerLabel.openBounds);
        }

        // include the bounds for all menu items
        for (int i = 0; i < icount; i++) {
            RadialMenuItem item = items.get(i);
            // we need the open bounds rather than the closed ones
            _bounds.add(item.openBounds);
        }

        // now translate everything from the center of the target bounds to the upper left of the
        // menu bounds
        if (_centerLabel != null) {
            _centerLabel.openBounds.translate(-_bounds.x, -_bounds.y);
            _centerLabel.closedBounds.translate(-_bounds.x, -_bounds.y);
        }
        for (int i = 0; i < icount; i++) {
            RadialMenuItem item = items.get(i);
            item.openBounds.translate(-_bounds.x, -_bounds.y);
            item.closedBounds.translate(-_bounds.x, -_bounds.y);
        }

        // the origin was at the center of the encircled rectangle; we need to translate it such
        // that the origin of our bounds are in screen coordinates and at the upper left rather
        // than the center
        _bounds.x += (_tbounds.x + _tbounds.width/2);
        _bounds.y += (_tbounds.y + _tbounds.height/2);

        // now make sure the whole shebang is fully visible within the host component
        Rectangle hbounds = _host.getViewBounds();
        Point pos = SwingUtil.fitRectInRect(_bounds, hbounds);
        _bounds.setLocation(pos);
    }

    /**
     * Requests that our host component repaint the part of itself that we occupy.
     */
    protected void repaint ()
    {
        // only repaint the area that we overlap
        _host.repaintRect(_bounds.x, _bounds.y, _bounds.width+1, _bounds.height+1);
    }

    /** Our host component. */
    protected Host _host;

    /** The bounds around which we're rendering a menu. */
    protected Rectangle _tbounds;

    /** The bounds that all of our menu items occupy. */
    protected Rectangle _bounds;

    /** The argument object, which will be delivered along with a posted command when a menu item
     * is selected. */
    protected Object _argument;

    /** The centerpiece icon or null if there is none. */
    protected Icon _centerIcon;

    /** The label used to render the centerpiece icon if there is one. */
    protected RadialLabelSausage _centerLabel;

    /** Our menu items. */
    protected ArrayList<RadialMenuItem> _items = new ArrayList<RadialMenuItem>();

    /** The item that has the mouse over it presently, and the default item if we're double
     * clicked. */
    protected RadialMenuItem _activeItem, _defaultItem;

    /** The amount of time the user must hold down the mouse in order to put it in drag mode, after
     * which the first mouse-up will pop down the menu. */
    protected static final long DRAGMODE_DELAY = 1000L;

    /** The amount of time after the menu has been popped up before we accept selections. */
    protected static final long DEBOUNCE_DELAY = 200L;

    /** How quickly a second click must arrive for us to count it as a double click and not two
     * single clicks. */
    protected static final long DOUBLECLICK_DELAY = 400L;

    /** The time at which the menu was popped up. */
    protected long _poptime;

    /** Whether we're popping the menu up in microsoft mode, where the user does not need to drag
     * to the item they want to select. */
    protected boolean _msMode = false;

    /** This hijacks and holds onto the previous mouse listeners. */
    protected MouseHijacker _hijacker;

    /** Maintains a list of action listeners. */
    protected ObserverList<ActionListener> _actlist = ObserverList.newSafeInOrder();
}
TOP

Related Classes of com.samskivert.swing.RadialMenu$Predicate

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.