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);
+ }
+}