From 0a8c5524db025381f74381492764ad0272316db5 Mon Sep 17 00:00:00 2001 From: mdb Date: Tue, 15 Apr 2003 20:28:36 +0000 Subject: [PATCH] git-svn-id: https://samskivert.googlecode.com/svn/trunk@1098 6335cc39-0255-0410-8fd6-9bcaacd3b74c --- .../com/samskivert/swing/LabelSausage.java | 172 +++++ .../samskivert/swing/RadialLabelSausage.java | 195 ++++++ .../java/com/samskivert/swing/RadialMenu.java | 634 ++++++++++++++++++ .../com/samskivert/swing/RadialMenuItem.java | 109 +++ 4 files changed, 1110 insertions(+) create mode 100644 projects/samskivert/src/java/com/samskivert/swing/LabelSausage.java create mode 100644 projects/samskivert/src/java/com/samskivert/swing/RadialLabelSausage.java create mode 100644 projects/samskivert/src/java/com/samskivert/swing/RadialMenu.java create mode 100644 projects/samskivert/src/java/com/samskivert/swing/RadialMenuItem.java diff --git a/projects/samskivert/src/java/com/samskivert/swing/LabelSausage.java b/projects/samskivert/src/java/com/samskivert/swing/LabelSausage.java new file mode 100644 index 00000000..3af588d8 --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/swing/LabelSausage.java @@ -0,0 +1,172 @@ +// +// $Id: LabelSausage.java,v 1.1 2003/04/15 20:28:36 mdb Exp $ + +package com.samskivert.swing; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; + +import javax.swing.Icon; + +import com.samskivert.swing.util.SwingUtil; + +/** + * An abstract lightweight renderer that sizes and renders a label (with + * optional icon) in a roundy-ended sausage. + */ +public abstract class LabelSausage +{ + /** + * Constructs a label sausage. + */ + protected LabelSausage (Label label, Icon icon) + { + _label = label; + _icon = icon; + } + + /** + * Lays out the label sausage. It is assumed that the desired label + * font is already set in the label. + */ + protected void layout (Graphics2D gfx, int extraPadding) + { + // if we have an icon, let that dictate our size; otherwise just + // lay out our label all on one line + int sqwid, sqhei; + if (_icon == null) { + sqwid = sqhei = 0; + + } else { + sqwid = _icon.getIconWidth(); + sqhei = _icon.getIconHeight(); + _label.setTargetHeight(sqhei); + } + + // lay out our label + _label.layout(gfx); + Dimension lsize = _label.getSize(); + + // if we have no icon, make sure that the label has enough room + if (_icon == null) { + sqhei = lsize.height + extraPadding * 2; + sqwid = extraPadding * 2; + } + + // compute the diameter of the circle that perfectly encompasses our + // icon + int hhei = sqhei / 2; + int hwid = sqwid / 2; + _dia = (int) (Math.sqrt(hwid * hwid + hhei * hhei) * 2); + + // compute the x and y offsets at which we'll start rendering + _xoff = (_dia - sqwid) / 2; + _yoff = (_dia - sqhei) / 2; + + // and for the label + _lxoff = _dia - _xoff; + _lyoff = (_dia - lsize.height) / 2; + + // now compute our closed and open sizes + _size.height = _dia; + + // width is the diameter of the circle that contains + // the icon plus space for the label when we're open + _size.width = _dia + lsize.width + _xoff; + + // and if we are actually rendering the icon, we need to + // account for the space between it and the label. + if (_icon != null) { + _size.width += _xoff; + } + } + + /** + * Paints the label sausage. + */ + protected void paint ( + Graphics2D gfx, int x, int y, Color background, Object cliData) + { + // turn on anti-aliasing + Object oalias = SwingUtil.activateAntiAliasing(gfx); + + // draw the base sausage + gfx.setColor(background); + drawBase(gfx, x, y); + + // render our icon if we've got one + if (_icon != null) { + _icon.paintIcon(null, gfx, x + _xoff, y + _yoff); + } + + drawLabel(gfx, x, y); + drawBorder(gfx, x, y); + + drawExtras(gfx, x, y, cliData); + + // restore original hints + SwingUtil.restoreAntiAliasing(gfx, oalias); + } + + /** + * Draws the base sausage within which all the other decorations are + * added. + */ + protected void drawBase (Graphics2D gfx, int x, int y) + { + gfx.fillRoundRect( + x, y, _size.width - 1, _size.height - 1, _dia, _dia); + } + + /** + * Draws the label. + */ + protected void drawLabel (Graphics2D gfx, int x, int y) + { + _label.render(gfx, x + _lxoff, y + _lyoff); + } + + /** + * Draws the black outer border. + */ + protected void drawBorder (Graphics2D gfx, int x, int y) + { + // draw the black outer border + gfx.setColor(Color.black); + gfx.drawRoundRect(x, y, _size.width - 1, _size.height - 1, + _dia, _dia); + } + + /** + * Draws any extras that may be required. + */ + protected void drawExtras (Graphics2D gfx, int x, int y, Object cliData) + { + // nothing by default + } + + /** The label. */ + protected Label _label; + + /** The optional icon. */ + protected Icon _icon; + + /** The size of this label sausage. */ + protected Dimension _size = new Dimension(); + + /** The diameter of the circle that perfectly surrounds our icon. */ + protected int _dia = 0; + + /** The x offset of the icon. */ + protected int _xoff = 0; + + /** The y offset of the icon. */ + protected int _yoff = 0; + + /** The y offset for the label. */ + protected int _lyoff = 0; + + /** The x offset for the label. */ + protected int _lxoff = 0; +} diff --git a/projects/samskivert/src/java/com/samskivert/swing/RadialLabelSausage.java b/projects/samskivert/src/java/com/samskivert/swing/RadialLabelSausage.java new file mode 100644 index 00000000..43b00fc4 --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/swing/RadialLabelSausage.java @@ -0,0 +1,195 @@ +// +// $Id: RadialLabelSausage.java,v 1.1 2003/04/15 20:28:36 mdb Exp $ + +package com.samskivert.swing; + +import java.awt.AlphaComposite; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.Stroke; + +import javax.swing.Icon; +import javax.swing.UIManager; + +/** + * Specializes the label sausage to suit the needs of items placed within + * a radial menu. The colors used to render the radial label sausage must + * be configured via the {@link UIManager}. These colors are: + * + *
+ * RadialLabelSausage.activeBorder
+ * RadialLabelSausage.inactiveBorder
+ * RadialLabelSausage.background
+ * 
+ * + * This would be configured in a custom look and feel like so: + * + *
+ * protected void initComponentDefaults (UIDefaults table)
+ * {
+ *     super.initComponentDefaults(table);
+ *     Object[] defaults = {
+ *         "RadialLabelSausage.inactiveBorder", new ColorUIResource(0x478ABA),
+ *         "RadialLabelSausage.activeBorder", new ColorUIResource(0xE0A000),
+ *         "RadialLabelSausage.background", new ColorUIResource(0xD5E7E7),
+ *     };
+ *     table.putDefaults(defaults);
+ * }
+ * 
+ * + * An application that is not using a custom look and feel can simply set + * these values with {@link UIManager#put}. + */ +public class RadialLabelSausage extends LabelSausage +{ + /** The dimensions of this item when we are in our closed state as + * well as our most recently laid out coordinates. This is only valid + * after a call to {@link #layout}. */ + public Rectangle closedBounds = new Rectangle(); + + /** The dimensions of this item when we are in open state. This is + * only valid after a call to {@link #layout}. */ + public Rectangle openBounds = new Rectangle(); + + /** + * Constructs a radial label sausage. + */ + public RadialLabelSausage (String label, Icon icon) + { + super(new Label(label), icon); + } + + /** + * Returns true if this menu item should be enabled when it is + * displayed. The default implementation always returns true. + */ + public boolean isEnabled (RadialMenu menu) + { + return true; + } + + /** + * Sets whether the label is to be rendered as an active label. + */ + public void setActive (boolean active) + { + _active = active; + } + + /** + * Computes the dimensions of this label based on the specified font + * and in the specified graphics context. + */ + public void layout (Graphics2D gfx, Font font) + { + _label.setFont(font); + layout(gfx, BORDER_THICKNESS); + + openBounds.width = _size.width; + openBounds.height = _size.height; + + // and closed up, we're just a circle + closedBounds.height = closedBounds.width = _size.height; + } + + // documentation inherited + protected void drawLabel (Graphics2D gfx, int x, int y) + { + if (_active) { + super.drawLabel(gfx, x, y); + } + } + + // documentation inherited + protected void drawBorder (Graphics2D gfx, int x, int y) + { + // then around all that draw the borders + Stroke ostroke = gfx.getStroke(); + gfx.setStroke(BORDER_STROKE); + if (_active) { + // draw the active border + gfx.setColor(UIManager.getColor("RadialLabelSausage.activeBorder")); + gfx.drawRoundRect( + x + (BORDER_THICKNESS / 2), y + (BORDER_THICKNESS / 2), + openBounds.width - 1 - BORDER_THICKNESS, + openBounds.height - 1 - BORDER_THICKNESS, + _dia - BORDER_THICKNESS, _dia - BORDER_THICKNESS); + + // draw the black outer border + gfx.setStroke(ostroke); + super.drawBorder(gfx, x, y); + + } else { + // draw the inactive border + gfx.setColor(UIManager.getColor( + "RadialLabelSausage.inactiveBorder")); + gfx.drawOval( + x + (BORDER_THICKNESS / 2), y + (BORDER_THICKNESS / 2), + closedBounds.width - 1 - BORDER_THICKNESS, + closedBounds.height - 1 - BORDER_THICKNESS); + + // draw the black outer border + gfx.setStroke(ostroke); + gfx.setColor(Color.black); + gfx.drawOval(x, y, closedBounds.width - 1, closedBounds.height - 1); + } + } + + // documentation inherited + protected void drawExtras (Graphics2D gfx, int x, int y, Object cliData) + { + RadialMenu menu = (RadialMenu) cliData; + // finally, if we're dimmed out: dim us out + if (!isEnabled(menu)) { + Composite ocomp = gfx.getComposite(); + gfx.setComposite(DISABLED_ALPHA); + gfx.setColor(Color.black); + drawBase(gfx, x, y); + gfx.setComposite(ocomp); + } + } + + /** + * Draw the base circle or sausage within which all the other + * decorations are added. + */ + protected void drawBase (Graphics2D gfx, int x, int y) + { + if (_active) { + // render the sausage + super.drawBase(gfx, x, y); + + } else { + // render the circle + gfx.fillOval( + x, y, closedBounds.width - 1, closedBounds.height - 1); + } + } + + /** + * Paints the radial label sausage. + */ + protected void paint (Graphics2D gfx, int x, int y, RadialMenu menu) + { + paint(gfx, x, y, UIManager.getColor( + "RadialLabelSausage.background"), menu); + } + + /** Indicates whether or not this label is active. */ + protected boolean _active = false; + + /** The thickness of the colored active/inactive border. */ + protected static final int BORDER_THICKNESS = 4; + + /** The stroke to use when drawing the selected/unselected color. */ + protected static final Stroke BORDER_STROKE = + new BasicStroke(BORDER_THICKNESS); + + /** The alpha level for how much we dim an inactive menu item by. */ + protected static final Composite DISABLED_ALPHA = + AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f); +} diff --git a/projects/samskivert/src/java/com/samskivert/swing/RadialMenu.java b/projects/samskivert/src/java/com/samskivert/swing/RadialMenu.java new file mode 100644 index 00000000..0331e340 --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/swing/RadialMenu.java @@ -0,0 +1,634 @@ +// +// $Id: RadialMenu.java,v 1.1 2003/04/15 20:28:36 mdb Exp $ + +package com.samskivert.swing; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Point; +import java.awt.Rectangle; + +import java.awt.event.ActionEvent; +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 javax.swing.UIManager; + +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.StringUtil; + +/** + * Provides a radial menu with iconic menu items that expand to include + * textual descriptions when moused over. Menu selections are posted as + * controller commands. + */ +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 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); + } + + /** + * 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 = (RadialMenuItem)_items.get(i); + if (item.command.equals(command)) { + _items.remove(i); + return true; + } + } + + return false; + } + + /** + * 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 () + { + Component comp = _host.getComponent(); + + // unwire ourselves from the host component + comp.removeMouseListener(this); + comp.removeMouseMotionListener(this); + + // reinstate the previous mouse listeners + _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 = (RadialMenuItem)_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) && + (event.getWhen() > _poptime + DEBOUNCE_DELAY)) { + + int icount = _items.size(); + for (int i = 0; i < icount; i++) { + RadialMenuItem item = (RadialMenuItem)_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 = event.getWhen(); + 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)) { + // if the item has an overriding argument, use that + Object itemArg = _activeItem.argument; + Object arg = (itemArg == null) ? _argument : itemArg; + + // post the action + final CommandEvent evt = new CommandEvent( + _host.getComponent(), _activeItem.command, arg); + Controller.postAction(evt); + + // and notify our listeners + _actlist.apply(new ObserverList.ObserverOp() { + public boolean apply (Object obs) { + ((ActionListener)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 items = new ArrayList(); + for (int i = 0; i < icount; i++) { + RadialMenuItem item = (RadialMenuItem)_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 = (RadialMenuItem)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 = (RadialMenuItem)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 = (RadialMenuItem)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 = (RadialMenuItem)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 = new Rectangle(_host.getComponent().getSize()); + 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 _items = new ArrayList(); + + /** 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 _actlist = new ObserverList( + ObserverList.SAFE_IN_ORDER_NOTIFY); +} diff --git a/projects/samskivert/src/java/com/samskivert/swing/RadialMenuItem.java b/projects/samskivert/src/java/com/samskivert/swing/RadialMenuItem.java new file mode 100644 index 00000000..5388984a --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/swing/RadialMenuItem.java @@ -0,0 +1,109 @@ +// +// $Id: RadialMenuItem.java,v 1.1 2003/04/15 20:28:36 mdb Exp $ + +package com.samskivert.swing; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Font; +import java.awt.Graphics2D; + +import javax.swing.Icon; + +/** + * Used to track info for each menu item in a {@link RadialMenu}. + */ +public class RadialMenuItem extends RadialLabelSausage +{ + /** The command to issue if this item is selected. */ + public String command; + + /** A special argument to be used when this menu item is selected, + * rather than the argument provided to the radial menu when it was + * activated. */ + public Object argument; + + /** Used to determine whether or not this menu item should be included + * in a menu and whether or not it should be enabled. If no predicate + * is available, a menu item is assumed always to be included and + * enabled. */ + public RadialMenu.Predicate predicate; + + /** + * Constructs a radial menu item with the specified command and label. + * No icon or menu predicate will be used. + */ + public RadialMenuItem (String command, String label) + { + this(command, label, null, null); + } + + /** + * Constructs a radial menu item with the specified command, label and + * icon. No menu predicate will be used. + */ + public RadialMenuItem (String command, String label, Icon icon) + { + this(command, label, icon, null); + } + + /** + * Constructs a radial menu item with the specified command, label and + * icon. + */ + public RadialMenuItem (String command, String label, Icon icon, + RadialMenu.Predicate predicate) + { + super(label, icon); + + _label.setTextColor(Color.black); + + this.command = command; + this.predicate = predicate; + } + + /** + * Returns true if this menu item should be included in a menu when it + * is displayed. Calls through to the {@link RadialMenu.Predicate} if + * we have one. + */ + public boolean isIncluded (RadialMenu menu) + { + return (predicate == null || predicate.isIncluded(menu, this)); + } + + /** + * Returns true if this menu item should be enabled when it is + * displayed. Calls through to the {@link RadialMenu.Predicate} if we + * have one. + */ + public boolean isEnabled (RadialMenu menu) + { + return (predicate == null || predicate.isEnabled(menu, this)); + } + + /** + * Menu items are equal if their commands are equal. We also + * declare ourselves to be equal to a string with the same value + * as our command. + */ + public boolean equals (Object other) + { + if (other instanceof RadialMenuItem) { + return command.equals(((RadialMenuItem)other).command); + } else if (other instanceof String) { + return command.equals(other); + } else { + return false; + } + } + + /** + * Renders this menu item at the specified location. + */ + public void render ( + Component host, RadialMenu menu, Graphics2D gfx, int x, int y) + { + paint(gfx, x, y, menu); + } +}