/**
 * GUI Commands
 * Copyright 2004 Andrew Pietsch
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * $Id: Command.java,v 1.32 2007/01/03 22:41:23 pietschy Exp $
 */

package org.pietschy.command;

import org.pietschy.command.log.Logger;

import javax.swing.*;
import javax.swing.event.EventListenerList;
import javax.swing.event.SwingPropertyChangeSupport;
import java.awt.*;
import java.beans.PropertyChangeListener;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;


/**
 * This is the base class for all Commands.  It provides all code for creating initialising
 * and managing the attachements (buttons/menus) of the command.  The two main subclasses
 * are {@link ActionCommand} and {@link CommandGroup}.
 *
 * @see ActionCommand
 * @see CommandGroup
 * @see Face
 */
public abstract class
Command
{
   private boolean enabled = true;
   private boolean visible = true;

   protected EventListenerList listenerList = new EventListenerList();
   protected SwingPropertyChangeSupport pcs;

   private ButtonFactory buttonFactory;
   private ToolbarFactory toolbarFactory;
   private MenuFactory menuFactory;

   private ButtonManager buttonManager = new ButtonManager(this);
   private FaceHelper faceHelper = new FaceHelper(this);

   protected Logger internalLog;
   private String id = null;

   private CommandManager commandManager;

   private boolean hovering = false;

   private HashMap properties = new HashMap();
   private static final String[] ALTERNATE_POPUP_FACES = new String[]{Face.MENU, Face.DEFAULT};
   private static final String[] ALTERNATE_TOOLBAR_FACES = new String[]{Face.BUTTON, Face.DEFAULT};
   private static final String[] ALTERNATE_HTML_FACES = new String[]{Face.BUTTON, Face.DEFAULT};
   private static final String[] DEFAULT_ALTERNATE_FACES = new String[]{Face.DEFAULT};


   /**
    * Creates a new anonymous command.  Anonymous commands can't must be fully programatically
    * generated and can't be exported to command containers.
    */
   protected Command(CommandManager commandManager)
   {
      this(commandManager, IdHelper.createAnonymousId());
   }

   /**
    * Constructs a new Command object with the specified Id.
    *
    * @param commandManager
    * @param id             the identifier of this command.
    */
   protected Command(CommandManager commandManager, String id)
   {
      if (commandManager == null)
         throw new NullPointerException("commandManager is null");

      if (id == null)
         throw new NullPointerException("Command identifier is null");

      pcs = new SwingPropertyChangeSupport(this);
      internalLog = CommandManager.getLogger(this.getClass());
      this.id = id;

      this.commandManager = commandManager;
      initCommandManager(commandManager);
   }

   protected void
   initCommandManager(CommandManager commandManager)
   {
      internalLog.enter("initCommandManager");
      internalLog.param("commandManager", String.valueOf(commandManager));

      commandManager.configure(this);

      internalLog.exit("initCommandManager");
   }

   /**
    * Gets this commands identifier.
    *
    * @return the identifier of this command.
    */
   public String
   getId()
   {
      return id;
   }

   /**
    * Checks if this is an anonymous command.  Anonymous commands have no identifier, and can't be
    * exported or configured by the {@link CommandManager}.  They are useful when you need
    * to programatically create a command.
    * <p/>
    * All commands created without an identifier are anonymous.
    *
    * @return <tt>true</tt> if this command is anonymous, <tt>false</tt> otherwise.
    */
   public boolean
   isAnonymous()
   {
      return IdHelper.isAnonymous(getId());
   }


   /**
    * Registers this command with its {@link CommandManager}.  Once exported the command
    * will be available for use in {@link CommandGroup groups}.
    *
    * @throws java.lang.IllegalStateException
    *          if this command is {@link #isAnonymous anonymous}.
    */
   public Command
   export()
   {
      if (!getCommandManager().isRegistered(this))
         getCommandManager().registerCommand(this);

      return this;
   }

   /**
    * Gets the {@link CommandManager} this command belongs to.
    *
    * @return the commands {@link CommandManager}.
    */
   public CommandManager
   getCommandManager()
   {
      return commandManager;
   }

   public void
   putProperty(String name, String value)
   {
      properties.put(name, value);
   }

   public String
   getProperty(String name)
   {
      return (String) properties.get(name);
   }

   public String
   getProperty(String name, String defaultValue)
   {
      String value = (String) properties.get(name);
      return value != null ? value : defaultValue;
   }

   public String[]
   getPropertyNames()
   {
      Set keys = properties.keySet();
      return (String[]) keys.toArray(new String[keys.size()]);
   }

   /**
    * Invoked whenever a command is added to a group.  Please note that Commands may
    * belong to more than one group at any given time.  This method does nothing by default,
    * subclasses may override to perform special configuration as required.
    *
    * @param parent the parent to which the command was added.
    */
   public void
   addNotify(CommandGroup parent)
   {
   }

   /**
    * Invoked whenever a command is remvoved from a group.  Please note that Commands may
    * belong to more than one group at any given time.  This method does nothing by default,
    * subclasses may override to perform special configuration as required.
    *
    * @param parent the parent from which the command was removed.
    */
   public void
   removeNotify(CommandGroup parent)
   {
   }

   /**
    * Attaches this command to the specified button. If the specified face doesn't exist, then
    * the default face is used.
    *
    * @param button the button to which this command should be attached.
    */
   public void
   attach(AbstractButton button, String faceName)
   {
      buttonManager.attachAndConfigure(button);
      configureButtonStates(button);

      RenderContext.bind(button, this, faceName);
      configureButtonAppearance(button);

   }

   /**
    * Detaches this command from the specified button.
    *
    * @param button the button from which this command is to be detached.
    */
   public void detach(AbstractButton button)
   {
      RenderContext.unbind(button, this);
      buttonManager.detach(button);
   }

   /**
    * Tests if this command is attached to the specified button.
    *
    * @param b the button to check
    * @return <tt>true</tt> if this command is attached to the button, <tt>false</tt> otherwise.
    */
   public boolean isAttachedTo(JComponent b)
   {
      if (b == null)
      {
         return false;
      }

      /**
       fixed bug 26 (cheremin.ruslan)
       if b is not attached to any button at all, RenderContext.get( b )==null and original code throws NPE
       */
      final RenderContext rc = RenderContext.get(b);
      if (rc != null)
      {
         return this.equals(rc.getCommand());
      }
      else
      {
         return false;
      }

   }


   /**
    * This method is called to configureMenu newly created buttons.   Subclasses may override this
    * method to perform special configuration if required.
    *
    * @param button the button to configureMenu.
    */
   protected void
   configureButtonStates(AbstractButton button)
   {
      button.setEnabled(enabled);
      button.setVisible(visible);
   }

   /**
    * Gets the button factory to use for this command.  If the button factory hasn't been
    * configured by calling {@link #setButtonFactory}, then the default factory specified
    * by {@link CommandManager#setButtonFactory}.
    *
    * @return this commands {@link ButtonFactory}.
    * @see #setButtonFactory
    * @see CommandManager#setButtonFactory
    */
   public ButtonFactory
   getButtonFactory()
   {
      if (buttonFactory == null)
      {
         buttonFactory = getCommandManager().getButtonFactory();
      }

      return buttonFactory;
   }

   /**
    * Sets the {@link ButtonFactory} for this command to use for default when creating
    * buttons.
    *
    * @param factory the {@link ButtonFactory} to use.
    */
   public void
   setButtonFactory(ButtonFactory factory)
   {
      buttonFactory = factory;
   }

   /**
    * Gets the toolbar factory to use for this command.  If the factory hasn't been
    * configured by calling {@link #setToolbarFactory}, then the default factory specified
    * by {@link CommandManager#setToolbarFactory} is used.
    *
    * @return this commands {@link ButtonFactory}.
    * @see #setToolbarFactory
    * @see CommandManager#setToolbarFactory
    */
   public ToolbarFactory
   getToolbarFactory()
   {
      if (toolbarFactory == null)
      {
         toolbarFactory = getCommandManager().getToolbarFactory();
      }

      return toolbarFactory;
   }

   /**
    * Sets the {@link ToolbarFactory} for this command to use for default when creating
    * buttons on toolbars.
    *
    * @param factory the {@link ButtonFactory} to use.
    */
   public void
   setToolbarFactory(ToolbarFactory factory)
   {
      this.toolbarFactory = factory;
   }

   /**
    * Gets the {@link MenuFactory} to use for this command when creating menu items.  If the
    * factory hasn't been configured by calling {@link #setMenuFactory} then the value provided
    * by {@link CommandManager#getMenuFactory} is used.
    *
    * @return the {@link MenuFactory} to use.
    */
   public MenuFactory getMenuFactory()
   {
      if (menuFactory == null)
      {
         menuFactory = getCommandManager().getMenuFactory();
      }

      return menuFactory;
   }

   /**
    * Sets the {@link MenuFactory} to use for menus created by this command.
    *
    * @param factory the {@link MenuFactory} to use.
    */
   public void setMenuFactory(MenuFactory factory)
   {
      menuFactory = factory;
   }

   /**
    * Updates the enabled state of this command and any attached buttons.
    *
    * @param enabled <code>true</code> to enable the command.
    */
   public void setEnabled(boolean enabled)
   {
      if (this.enabled != enabled)
      {
         this.enabled = enabled;
         Iterator iter = buttonIterator();
         while (iter.hasNext())
         {
            AbstractButton button = (AbstractButton) iter.next();
            button.setEnabled(enabled);
         }
         pcs.firePropertyChange("enabled", !enabled, enabled);
      }
   }

   /**
    * Checks the enabled state of this action and it's attached buttons.
    *
    * @return <code>true</code> if the command is enabled.
    */
   public boolean isEnabled()
   {
      return enabled;
   }

   /**
    * Sets the command and all it's buttons to be visible or not.
    *
    * @param value
    */
   public void setVisible(boolean value)
   {
      if (visible != value)
      {
         visible = value;
         for (Iterator iter = buttonIterator(); iter.hasNext();)
         {
            AbstractButton button = (AbstractButton) iter.next();
            button.setVisible(visible);
         }
         pcs.firePropertyChange("visible", !visible, visible);
      }
   }

   /**
    * Checks if the buttons of this command are visible.
    *
    * @return <tt>true</tt> if the command is visible, <tt>false</tt> otherwise.
    */
   public boolean isVisible()
   {
      return visible;
   }

   /**
    * @deprecated use {@link #getDefaultFace} instead.
    */
   public String getText()
   {
      return getDefaultFace().getText();
   }

   /**
    * @deprecated use {@link #getDefaultFace} instead.
    */
   public void setText(String text)
   {
      getDefaultFace().setText(text);
   }

   /**
    * @deprecated use {@link #getDefaultFace} instead.
    */
   public KeyStroke getAccelerator()
   {
      return getDefaultFace().getAccelerator();
   }

   /**
    * @deprecated use {@link #getDefaultFace} instead.
    */
   public void
   setAccelerator(KeyStroke accelerator)
   {
      getDefaultFace().setAccelerator(accelerator);
   }

   /**
    * @deprecated use {@link #getDefaultFace} instead.
    */
   public void
   setMnemonic(Integer mnemonic)
   {
      getDefaultFace().setMnemonic(mnemonic);
   }

   /**
    * @deprecated use {@link #getDefaultFace} instead.
    */
   public void
   setLongDescription(String longDescription)
   {
      getDefaultFace().setLongDescription(longDescription);
   }

   /**
    * @deprecated use {@link #getDefaultFace} instead.
    */
   public void setIcon(Icon icon)
   {
      getDefaultFace().setIcon(icon);
   }

   /**
    * @deprecated use {@link #getDefaultFace} instead.
    */
   public void setSelectedIcon(Icon newSelectedIcon)
   {
      getDefaultFace().setSelectedIcon(newSelectedIcon);
   }

   /**
    * @deprecated use {@link #getDefaultFace} instead.
    */
   public void setMnemonicIndex(Integer mnemonicIndex)
   {
      getDefaultFace().setMnemonicIndex(mnemonicIndex);
   }

   /**
    * @deprecated use {@link #getDefaultFace} instead.
    */
   public void setDescription(String shortDescription)
   {
      getDefaultFace().setDescription(shortDescription);
   }

   /**
    * @deprecated use {@link #getDefaultFace} instead.
    */
   public void setTextPosition(Integer textPosition)
   {
      getDefaultFace().setHorizontalTextPosition(textPosition);
   }

   /**
    * @deprecated use {@link #getDefaultFace} instead.
    */
   public Integer getTextPosition()
   {
      return getDefaultFace().getHorizontalTextPosition();
   }

   /**
    * @deprecated use {@link #getDefaultFace} instead.
    */
   public Integer getMnemonic()
   {
      return getDefaultFace().getMnemonic();
   }

   /**
    * @deprecated use {@link #getDefaultFace} instead.
    */
   public Icon getIcon()
   {
      return getDefaultFace().getIcon();
   }

   /**
    * Returns the selected Icon for the command.
    *
    * @return
    */
   /**
    * @deprecated use {@link #getDefaultFace} instead.
    */
   public Icon getSelectedIcon()
   {
      return getDefaultFace().getSelectedIcon();
   }

   /**
    * @deprecated use {@link #getDefaultFace} instead.
    */
   public Integer getMnemonicIndex()
   {
      return getDefaultFace().getMnemonicIndex();
   }

   /**
    * @deprecated use {@link #getDefaultFace} instead.
    */
   public String getDescription()
   {
      return getDefaultFace().getDescription();
   }

   /**
    * @deprecated use {@link #getDefaultFace} instead.
    */
   public String getLongDescription()
   {
      return getDefaultFace().getLongDescription();
   }


   /**
    * Adds a the specified face to the command.
    * <p/>
    * If a face with the same name already exists in the command, it is replaced with
    * the specified face and all affected buttons are updated.
    *
    * @param face the new Face to installFace.
    * @deprecated use {@link #getFace(String, boolean)} or {@link #getDefaultFace(boolean)} instead.
    */
   public void addFace(Face face)
   {
      installFace(face);
   }


   /**
    * Adds a the specified face to the command.
    * <p/>
    * If a face with the same name already exists in the command, it is replaced with
    * the specified face and all affected buttons are updated.
    *
    * @param face the new Face to installFace.
    */
   protected void installFace(Face face)
   {
      internalLog.enter("installFace");
      internalLog.param("face", String.valueOf(face));

      if (face == null)
         throw new NullPointerException("face is null");

      faceHelper.installFace(face);

      internalLog.exit("installFace");
   }


   /**
    * Creates and adds a new empty face with the specified name.
    *
    * @param faceName the name of the face, "button" for example.
    * @return the newly created face.
    * @deprecated @deprecated use {@link #getFace(String, boolean)} or {@link #getDefaultFace(boolean)} instead.
    */
   public Face addNewFace(String faceName)
   {
      return getFace(faceName, true);
   }


   /**
    * Creates and returns an iterator over the buttons.
    *
    * @return an Iterator over the attached buttons
    */
   protected Iterator
   buttonIterator()
   {
      return buttonManager.buttonIterator();
   }

   /**
    * Gets this commands default face.  This is either the face with the
    * name {@link Face#DEFAULT}.  If no face exists with this name, then the first
    * registered face is returned.
    *
    * @return this commands default face.
    */
   public Face
   getDefaultFace()
   {
      return faceHelper.getDefaultFace(false);
   }

   /**
    * Gets this commands default face.  This is either the face with the
    * name {@link Face#DEFAULT}.  If no face exists with this name, then the first
    * registered face is returned.
    *
    * @param createIfMissing <code>true</code> to create the face if it doesn't exist,
    *                        <code>false</code> to return null if it doesn't exists.
    * @return this commands default face.
    */
   public Face
   getDefaultFace(boolean createIfMissing)
   {
      return faceHelper.getDefaultFace(createIfMissing);
   }

   /**
    * Gets the best matching face for the specified name.  This method first attempts to get the
    * exact face, if that fails it iterates over the face names returned by {@link #getAlternativeFaceNames}.
    * If non of these faces exist, then the default face is returned.
    *
    * @param faceName the name of the face to retrieved.  {@link Face#BUTTON} for example.
    * @return the specified face, or the next best face according to {@link #getAlternativeFaceNames}.
    */
   public Face
   getFace(String faceName)
   {
      return getFace(faceName, false);
   }

   /**
    * Gets the face with the specified name.
    *
    * @param faceName        the name of the face to retrieved.  {@link Face#BUTTON} for example.
    * @param createIfMissing <code>true</code> to create the face if it doesn't exist, <code>false</code> to
    *                        return the best matching face as determined by {@link #getAlternativeFaceNames(String)}.
    * @return the specified face, or the next best face according to {@link #getAlternativeFaceNames}.
    */
   public Face
   getFace(String faceName, boolean createIfMissing)
   {
      return faceHelper.getFace(faceName, createIfMissing, getAlternativeFaceNames(faceName));
   }

   public boolean faceExists(String faceName)
   {
      return faceHelper.faceExists(faceName);
   }

   /**
    * Gets a list of face names that can be used in place of the specified name if it
    * hasn't been registered with the command.  This method doesn't need to return the
    * default face in the list of options.
    *
    * @param face the face name
    * @return a list of alternative faces with the best alternative at index 0.
    */
   public String[]
   getAlternativeFaceNames(String face)
   {
      if (Face.POPUP.equals(face))
         return ALTERNATE_POPUP_FACES;
      else if (Face.TOOLBAR.equals(face))
         return ALTERNATE_TOOLBAR_FACES;
      else if (Face.HTML.equals(face))
         return ALTERNATE_HTML_FACES;
      else
         return DEFAULT_ALTERNATE_FACES;
   }

   /**
    * Adds a property change listener to this command.
    *
    * @param l the listener
    */
   public void
   addPropertyChangeListener(PropertyChangeListener l)
   {
      pcs.addPropertyChangeListener(l);
   }

   /**
    * Adds a property change listener to this command.
    *
    * @param propertyName the property to listen to.
    * @param l            the listener
    */
   public void
   addPropertyChangeListener(String propertyName, PropertyChangeListener l)
   {
      pcs.addPropertyChangeListener(propertyName, l);
   }

   /**
    * Removes the listener from this command
    *
    * @param l the listener
    */
   public void
   removePropertyChangeListener(PropertyChangeListener l)
   {
      pcs.removePropertyChangeListener(l);
   }

   /**
    * Removes the listener from this command
    *
    * @param propertyName the property to stop listening to.
    * @param l            the listener
    */
   public void
   removePropertyChangeListener(String propertyName, PropertyChangeListener l)
   {
      pcs.removePropertyChangeListener(propertyName, l);
   }

   /**
    * Adds a {@link HoverListener} to the command manager.  The listener will be notified when
    * ever the mouse hovers over a command.
    *
    * @param l the hover listener
    * @see HoverListener
    * @see #removeHoverListener
    */
   public void addHoverListener(HoverListener l)
   {
      listenerList.add(HoverListener.class, l);
   }

   /**
    * Removes the {@link HoverListener} from the command manager.
    *
    * @param l the hover listener
    * @see HoverListener
    * @see #addHoverListener
    */
   public void removeHoverListener(HoverListener l)
   {
      listenerList.remove(HoverListener.class, l);
   }

   protected void fireHoverStarted(Face face, Component source)
   {
      hovering = true;

      HoverEvent e = null;
      // Guaranteed to return a non-null array
      Object[] listeners = listenerList.getListenerList();
      // Process the listeners last to first, notifying
      // those that are interested in this event
      for (int i = listeners.length - 2; i >= 0; i -= 2)
      {
         if (listeners[i] == HoverListener.class)
         {
            // Lazily create the event:
            if (e == null)
               e = new HoverEvent(this, face, source);
            ((HoverListener) listeners[i + 1]).hoverStarted(e);
         }
      }
   }

   protected void fireHoverEnded(Face face, Component source)
   {
      if (!hovering)
         return;

      hovering = false;

      HoverEvent e = null;
      // Guaranteed to return a non-null array
      Object[] listeners = listenerList.getListenerList();
      // Process the listeners last to first, notifying
      // those that are interested in this event
      for (int i = listeners.length - 2; i >= 0; i -= 2)
      {
         if (listeners[i] == HoverListener.class)
         {
            // Lazily create the event:
            if (e == null)
               e = new HoverEvent(this, face, source);
            ((HoverListener) listeners[i + 1]).hoverEnded(e);
         }
      }
   }


   /**
    * This method will find the first button from this command in the specified container
    * and call {@link javax.swing.JComponent#requestFocus} followed by {@link Container#repaint}.
    *
    * @param container the container that holds a button from this command.
    */
   public void requestFocusIn(Container container)
   {
      internalLog.enter("requestFocusIn");
      AbstractButton button = getButtonIn(container);
      if (button != null)
      {
         if (internalLog.isDebugEnabled())
            internalLog.debug("Setting focus on: " + button);
         button.requestFocusInWindow();
      }

      internalLog.exit("requestFocusIn");
   }


   /**
    * This method will find the first button from this command in the specified container.
    *
    * @param container the container that holds a button from this command.
    */
   public AbstractButton getButtonIn(Container container)
   {
      internalLog.enter("getButtonIn");
      Iterator iter = buttonIterator();
      while (iter.hasNext())
      {
         AbstractButton button = (AbstractButton) iter.next();
         if (SwingUtilities.isDescendingFrom(button, container))
         {
            internalLog.exit("getButtonIn");
            return button;
         }
      }

      internalLog.exit("getButtonIn");
      return null;
   }


   /**
    * Creates a new button that is attached to the command.
    *
    * @return the newly created button
    */
   public AbstractButton createButton()
   {
      return createButton(getButtonFactory(), Face.BUTTON);
   }

   /**
    * Creates a new button that is attached to the command using the specified face.
    * If the face doesn't exist, the default face is used.
    *
    * @return the newly created button
    */
   public AbstractButton createButton(String faceId)
   {
      return createButton(getButtonFactory(), faceId);
   }

   /**
    * Create a new button for this command using the specified {@link ButtonFactory}.  The default
    * {@link Face#BUTTON} is used, it it hasn't been defined, the {@link Face#DEFAULT} is used.
    *
    * @return a new JButton that will execute this command.
    * @see ButtonFactory
    */
   public AbstractButton createButton(ButtonFactory factory)
   {
      return createButton(factory, Face.BUTTON);
   }

   /**
    * Create a new button for this command using the specified {@link ButtonFactory} and {@link Face} .  If the {@link Face} is
    * null the default face {@link org.pietschy.command.Face#DEFAULT} for this command is used.
    *
    * @return a new JButton
    */
   public AbstractButton createButton(ButtonFactory factory, String faceId)
   {
      JButton button = factory.createButton();
      attach(button, faceId);
      return button;
   }


   /**
    * Creates a new menu item for this command.  This command uses {@link Face#MENU}
    * and the currently configured {@link MenuFactory}.  If {@link Face#MENU} hasn't been
    * configured then the default face is used.
    *
    * @return a new {@link JMenuItem}.
    * @see #createMenuItem(java.lang.String)
    * @see #createMenuItem(org.pietschy.command.MenuFactory, java.lang.String)
    * @see #createMenuItem(org.pietschy.command.MenuFactory)
    */
   public JMenuItem createMenuItem()
   {
      return createMenuItem(getMenuFactory(), Face.MENU);
   }

   /**
    * Creates a new menu item for this command using the specifed {@link Face} name and the
    * currently configured {@link MenuFactory}.
    *
    * @return a new {@link JMenuItem}.
    * @see #createMenuItem(org.pietschy.command.MenuFactory, java.lang.String)
    * @see #createMenuItem(org.pietschy.command.MenuFactory)
    */
   public JMenuItem createMenuItem(String faceId)
   {
      return createMenuItem(getMenuFactory(), faceId);
   }

   /**
    * Creates a new menu item for this command using {@link Face#MENU} and the specified
    * {@link MenuFactory}.  If {@link Face#MENU} hasn't been specified, then the default is
    * used.
    *
    * @return a new JMenuItem
    * @see #createMenuItem(org.pietschy.command.MenuFactory, java.lang.String)
    */
   public JMenuItem createMenuItem(MenuFactory factory)
   {
      return createMenuItem(factory, Face.MENU);
   }

   /**
    * Creates a new menu item for this command using the specified {@link Face} and the specified
    * {@link MenuFactory}.
    *
    * @return a new {@link JMenuItem}.
    */
   public JMenuItem createMenuItem(MenuFactory factory, String faceId)
   {
      JMenuItem item = factory.createMenuItem();
      attach(item, faceId);
      return item;
   }


   /**
    * Reconfigures the appearances of all buttons bound to this command.
    */
   protected void
   configureButtonAppearances()
   {
      Iterator iter = buttonIterator();
      while (iter.hasNext())
      {
         AbstractButton button = (AbstractButton) iter.next();
         configureButtonAppearance(button);
      }
   }


   /**
    * Reconfigures all the buttons attached to this command that have the specified face.  This method will
    * be automatically called whenever the property of a face is modified.
    *
    * @param face the face whose buttons are to be reconfigured.
    */
   protected void
   configureButtonAppearances(Face face)
   {
      Iterator iter = buttonIterator();
      while (iter.hasNext())
      {
         AbstractButton button = (AbstractButton) iter.next();

         if (face.equals(RenderContext.get(button).getFace()))
            configureButtonAppearance(button);
      }
   }

   /**
    * Configures the appearance of the specified button.  This method simply delegates
    * to {@link RenderManager#renderButton(javax.swing.AbstractButton)}.
    *
    * @param button the button to configure.
    */
   protected void
   configureButtonAppearance(AbstractButton button)
   {
      RenderManager.renderButton(button);
   }


   /**
    * Returns a string representation of the object. In general, the
    * <code>toString</code> method returns a string that
    * "textually represents" this object. The result should
    * be a concise but informative representation that is easy for a
    * person to read.
    * It is recommended that all subclasses override this method.
    * <p/>
    * The <code>toString</code> method for class <code>Object</code>
    * returns a string consisting of the name of the class of which the
    * object is an instance, the at-sign character '<code>@</code>', and
    * the unsigned hexadecimal representation of the hash code of the
    * object. In other words, this method returns a string equal to the
    * value of:
    * <blockquote>
    * <pre>
    * getClass().getName() + '@' + Integer.toHexString(hashCode())
    * </pre></blockquote>
    *
    * @return a string representation of the object.
    */
   public String toString()
   {
      return getClass().getName() + "[" + id + "]" + '@' + Integer.toHexString(hashCode());
   }

}
