001/* BasicMenuItemUI.java --
002   Copyright (C) 2002, 2004, 2005  Free Software Foundation, Inc.
003
004This file is part of GNU Classpath.
005
006GNU Classpath is free software; you can redistribute it and/or modify
007it under the terms of the GNU General Public License as published by
008the Free Software Foundation; either version 2, or (at your option)
009any later version.
010
011GNU Classpath is distributed in the hope that it will be useful, but
012WITHOUT ANY WARRANTY; without even the implied warranty of
013MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014General Public License for more details.
015
016You should have received a copy of the GNU General Public License
017along with GNU Classpath; see the file COPYING.  If not, write to the
018Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
01902110-1301 USA.
020
021Linking this library statically or dynamically with other modules is
022making a combined work based on this library.  Thus, the terms and
023conditions of the GNU General Public License cover the whole
024combination.
025
026As a special exception, the copyright holders of this library give you
027permission to link this library with independent modules to produce an
028executable, regardless of the license terms of these independent
029modules, and to copy and distribute the resulting executable under
030terms of your choice, provided that you also meet, for each linked
031independent module, the terms and conditions of the license of that
032module.  An independent module is a module which is not derived from
033or based on this library.  If you modify this library, you may extend
034this exception to your version of the library, but you are not
035obligated to do so.  If you do not wish to do so, delete this
036exception statement from your version. */
037
038
039package javax.swing.plaf.basic;
040
041import gnu.classpath.SystemProperties;
042
043import java.awt.Color;
044import java.awt.Component;
045import java.awt.Container;
046import java.awt.Dimension;
047import java.awt.Font;
048import java.awt.FontMetrics;
049import java.awt.Graphics;
050import java.awt.Insets;
051import java.awt.Rectangle;
052import java.awt.event.ActionEvent;
053import java.awt.event.ItemEvent;
054import java.awt.event.ItemListener;
055import java.awt.event.KeyEvent;
056import java.awt.event.MouseEvent;
057import java.awt.font.FontRenderContext;
058import java.awt.font.TextLayout;
059import java.awt.geom.AffineTransform;
060import java.beans.PropertyChangeEvent;
061import java.beans.PropertyChangeListener;
062import java.util.ArrayList;
063
064import javax.swing.AbstractAction;
065import javax.swing.AbstractButton;
066import javax.swing.ActionMap;
067import javax.swing.ButtonModel;
068import javax.swing.Icon;
069import javax.swing.InputMap;
070import javax.swing.JCheckBoxMenuItem;
071import javax.swing.JComponent;
072import javax.swing.JMenu;
073import javax.swing.JMenuItem;
074import javax.swing.JPopupMenu;
075import javax.swing.KeyStroke;
076import javax.swing.LookAndFeel;
077import javax.swing.MenuElement;
078import javax.swing.MenuSelectionManager;
079import javax.swing.SwingConstants;
080import javax.swing.SwingUtilities;
081import javax.swing.UIDefaults;
082import javax.swing.UIManager;
083import javax.swing.event.MenuDragMouseEvent;
084import javax.swing.event.MenuDragMouseListener;
085import javax.swing.event.MenuKeyEvent;
086import javax.swing.event.MenuKeyListener;
087import javax.swing.event.MouseInputListener;
088import javax.swing.plaf.ActionMapUIResource;
089import javax.swing.plaf.ComponentInputMapUIResource;
090import javax.swing.plaf.ComponentUI;
091import javax.swing.plaf.MenuItemUI;
092import javax.swing.text.View;
093
094/**
095 * UI Delegate for JMenuItem.
096 */
097public class BasicMenuItemUI extends MenuItemUI
098{
099  /**
100   * Font to be used when displaying menu item's accelerator.
101   */
102  protected Font acceleratorFont;
103
104  /**
105   * Color to be used when displaying menu item's accelerator.
106   */
107  protected Color acceleratorForeground;
108
109  /**
110   * Color to be used when displaying menu item's accelerator when menu item is
111   * selected.
112   */
113  protected Color acceleratorSelectionForeground;
114
115  /**
116   * Icon that is displayed after the text to indicated that this menu contains
117   * submenu.
118   */
119  protected Icon arrowIcon;
120
121  /**
122   * Icon that is displayed before the text. This icon is only used in
123   * JCheckBoxMenuItem or JRadioBoxMenuItem.
124   */
125  protected Icon checkIcon;
126
127  /**
128   * Number of spaces between icon and text.
129   */
130  protected int defaultTextIconGap = 4;
131
132  /**
133   * Color of the text when menu item is disabled
134   */
135  protected Color disabledForeground;
136
137  /**
138   * The menu Drag mouse listener listening to the menu item.
139   */
140  protected MenuDragMouseListener menuDragMouseListener;
141
142  /**
143   * The menu item itself
144   */
145  protected JMenuItem menuItem;
146
147  /**
148   * Menu Key listener listening to the menu item.
149   */
150  protected MenuKeyListener menuKeyListener;
151
152  /**
153   * mouse input listener listening to menu item.
154   */
155  protected MouseInputListener mouseInputListener;
156
157  /**
158   * Indicates if border should be painted
159   */
160  protected boolean oldBorderPainted;
161
162  /**
163   * Color of text that is used when menu item is selected
164   */
165  protected Color selectionBackground;
166
167  /**
168   * Color of the text that is used when menu item is selected.
169   */
170  protected Color selectionForeground;
171
172  /**
173   * String that separates description of the modifiers and the key
174   */
175  private String acceleratorDelimiter;
176
177  /**
178   * ItemListener to listen for item changes in the menu item
179   */
180  private ItemListener itemListener;
181
182  /**
183   * A PropertyChangeListener to make UI updates after property changes.
184   */
185  private PropertyChangeHandler propertyChangeListener;
186
187  /**
188   * The view rectangle used for layout of the menu item.
189   */
190  private Rectangle viewRect;
191
192  /**
193   * The rectangle that holds the area of the label.
194   */
195  private Rectangle textRect;
196
197  /**
198   * The rectangle that holds the area of the accelerator.
199   */
200  private Rectangle accelRect;
201
202  /**
203   * The rectangle that holds the area of the icon.
204   */
205  private Rectangle iconRect;
206
207  /**
208   * The rectangle that holds the area of the icon.
209   */
210  private Rectangle arrowIconRect;
211
212  /**
213   * The rectangle that holds the area of the check icon.
214   */
215  private Rectangle checkIconRect;
216
217  /**
218   * A rectangle used for temporary storage to avoid creation of new
219   * rectangles.
220   */
221  private Rectangle cachedRect;
222
223  /**
224   * A class to handle PropertChangeEvents for the JMenuItem
225   * @author Anthony Balkissoon abalkiss at redhat dot com.
226   */
227  class PropertyChangeHandler implements PropertyChangeListener
228  {
229    /**
230     * This method is called when a property of the menuItem is changed.
231     * Currently it is only used to update the accelerator key bindings.
232     *
233     * @param e
234     *          the PropertyChangeEvent
235     */
236    public void propertyChange(PropertyChangeEvent e)
237    {
238      String property = e.getPropertyName();
239      if (property.equals("accelerator"))
240        {
241          InputMap map = SwingUtilities.getUIInputMap(menuItem,
242              JComponent.WHEN_IN_FOCUSED_WINDOW);
243          if (map != null)
244            map.remove((KeyStroke) e.getOldValue());
245          else
246            map = new ComponentInputMapUIResource(menuItem);
247
248          KeyStroke accelerator = (KeyStroke) e.getNewValue();
249          if (accelerator != null)
250            map.put(accelerator, "doClick");
251        }
252      // TextLayout caching for speed-up drawing of text.
253      else if ((property.equals(AbstractButton.TEXT_CHANGED_PROPERTY)
254                || property.equals("font"))
255               && SystemProperties.getProperty("gnu.javax.swing.noGraphics2D")
256               == null)
257        {
258          AbstractButton b = (AbstractButton) e.getSource();
259          String text = b.getText();
260          if (text == null)
261            text = "";
262          FontRenderContext frc = new FontRenderContext(new AffineTransform(),
263                                                        false, false);
264          TextLayout layout = new TextLayout(text, b.getFont(), frc);
265          b.putClientProperty(BasicGraphicsUtils.CACHED_TEXT_LAYOUT, layout);
266        }
267    }
268  }
269
270  /**
271   * A class to handle accelerator keys.  This is the Action we will
272   * perform when the accelerator key for this JMenuItem is pressed.
273   * @author Anthony Balkissoon abalkiss at redhat dot com
274   *
275   */
276  class ClickAction extends AbstractAction
277  {
278    /**
279     * This is what is done when the accelerator key for the JMenuItem is
280     * pressed.
281     */
282    public void actionPerformed(ActionEvent event)
283    {
284      doClick(MenuSelectionManager.defaultManager());
285    }
286  }
287
288  /**
289   * Creates a new BasicMenuItemUI object.
290   */
291  public BasicMenuItemUI()
292  {
293    mouseInputListener = createMouseInputListener(menuItem);
294    menuDragMouseListener = createMenuDragMouseListener(menuItem);
295    menuKeyListener = createMenuKeyListener(menuItem);
296    itemListener = new ItemHandler();
297    propertyChangeListener = new PropertyChangeHandler();
298
299    // Initialize rectangles for layout.
300    viewRect = new Rectangle();
301    textRect = new Rectangle();
302    iconRect = new Rectangle();
303    arrowIconRect = new Rectangle();
304    checkIconRect = new Rectangle();
305    accelRect = new Rectangle();
306    cachedRect = new Rectangle();
307  }
308
309  /**
310   * Create MenuDragMouseListener to listen for mouse dragged events.
311   *
312   * @param c
313   *          menu item to listen to
314   * @return The MenuDragMouseListener
315   */
316  protected MenuDragMouseListener createMenuDragMouseListener(JComponent c)
317  {
318    return new MenuDragMouseHandler();
319  }
320
321  /**
322   * Creates MenuKeyListener to listen to key events occuring when menu item is
323   * visible on the screen.
324   *
325   * @param c
326   *          menu item to listen to
327   * @return The MenuKeyListener
328   */
329  protected MenuKeyListener createMenuKeyListener(JComponent c)
330  {
331    return new MenuKeyHandler();
332  }
333
334  /**
335   * Handles mouse input events occuring for this menu item
336   *
337   * @param c
338   *          menu item to listen to
339   * @return The MouseInputListener
340   */
341  protected MouseInputListener createMouseInputListener(JComponent c)
342  {
343    return new MouseInputHandler();
344  }
345
346  /**
347   * Factory method to create a BasicMenuItemUI for the given {@link
348   * JComponent}, which should be a {@link JMenuItem}.
349   *
350   * @param c
351   *          The {@link JComponent} a UI is being created for.
352   * @return A BasicMenuItemUI for the {@link JComponent}.
353   */
354  public static ComponentUI createUI(JComponent c)
355  {
356    return new BasicMenuItemUI();
357  }
358
359  /**
360   * Programatically clicks menu item.
361   *
362   * @param msm
363   *          MenuSelectionManager for the menu hierarchy
364   */
365  protected void doClick(MenuSelectionManager msm)
366  {
367    menuItem.doClick(0);
368    msm.clearSelectedPath();
369  }
370
371  /**
372   * Returns maximum size for the specified menu item
373   *
374   * @param c
375   *          component for which to get maximum size
376   * @return Maximum size for the specified menu item.
377   */
378  public Dimension getMaximumSize(JComponent c)
379  {
380    return null;
381  }
382
383  /**
384   * Returns minimum size for the specified menu item
385   *
386   * @param c
387   *          component for which to get minimum size
388   * @return Minimum size for the specified menu item.
389   */
390  public Dimension getMinimumSize(JComponent c)
391  {
392    return null;
393  }
394
395  /**
396   * Returns path to this menu item.
397   *
398   * @return $MenuElement[]$ Returns array of menu elements that constitute a
399   *         path to this menu item.
400   */
401  public MenuElement[] getPath()
402  {
403    ArrayList path = new ArrayList();
404
405    Component c = menuItem;
406    while (c instanceof MenuElement)
407      {
408        path.add(0, c);
409
410        if (c instanceof JPopupMenu)
411          c = ((JPopupMenu) c).getInvoker();
412        else
413          c = c.getParent();
414      }
415
416    MenuElement[] pathArray = new MenuElement[path.size()];
417    path.toArray(pathArray);
418    return pathArray;
419  }
420
421  /**
422   * Returns preferred size for the given menu item.
423   *
424   * @param c
425   *          menu item for which to get preferred size
426   * @param checkIcon
427   *          check icon displayed in the given menu item
428   * @param arrowIcon
429   *          arrow icon displayed in the given menu item
430   * @param defaultTextIconGap
431   *          space between icon and text in the given menuItem
432   * @return $Dimension$ preferred size for the given menu item
433   */
434  protected Dimension getPreferredMenuItemSize(JComponent c, Icon checkIcon,
435                                               Icon arrowIcon,
436                                               int defaultTextIconGap)
437  {
438    JMenuItem m = (JMenuItem) c;
439    String accelText = getAcceleratorString(m);
440
441    // Layout the menu item. The result gets stored in the rectangle
442    // fields of this class.
443    resetRectangles(null);
444    layoutMenuItem(m, accelText);
445
446    // The union of the text and icon areas is the label area.
447    cachedRect.setBounds(textRect);
448    Rectangle pref = SwingUtilities.computeUnion(iconRect.x, iconRect.y,
449                                                 iconRect.width,
450                                                 iconRect.height,
451                                                 cachedRect);
452
453    // Find the widest menu item text and accelerator and store it in
454    // client properties of the parent, so that we can align the accelerators
455    // properly. Of course, we only need can do this, if the parent is
456    // a JComponent and this menu item is not a toplevel menu.
457    Container parent = m.getParent();
458    if (parent != null && parent instanceof JComponent
459        && !(m instanceof JMenu && ((JMenu) m).isTopLevelMenu()))
460      {
461        JComponent p = (JComponent) parent;
462
463        // The widest text so far.
464        Integer maxTextWidth = (Integer) p.getClientProperty("maxTextWidth");
465        int maxTextValue = maxTextWidth == null ? 0 : maxTextWidth.intValue();
466        if (pref.width < maxTextValue)
467          pref.width = maxTextValue;
468        else
469          p.putClientProperty("maxTextWidth", new Integer(pref.width));
470
471        // The widest accelerator so far.
472        Integer maxAccelWidth = (Integer) p.getClientProperty("maxAccelWidth");
473        int maxAccelValue = maxAccelWidth == null ? 0
474                                                  : maxAccelWidth.intValue();
475        if (accelRect.width > maxAccelValue)
476          {
477            maxAccelValue = accelRect.width;
478            p.putClientProperty("maxAccelWidth", new Integer(accelRect.width));
479          }
480        pref.width += maxAccelValue;
481        pref.width += defaultTextIconGap;
482      }
483
484    // Add arrow and check size if appropriate.
485    if (! (m instanceof JMenu && ((JMenu) m).isTopLevelMenu()))
486      {
487        pref.width += checkIconRect.width;
488        pref.width += defaultTextIconGap;
489        pref.width += arrowIconRect.width;
490        pref.width += defaultTextIconGap;
491      }
492
493    // Add a gap ~2 times as wide as the defaultTextIconGap.
494    pref.width += 2 * defaultTextIconGap;
495
496    // Respect the insets of the menu item.
497    Insets i = m.getInsets();
498    pref.width += i.left + i.right;
499    pref.height += i.top + i.bottom;
500
501    // Return a copy, so that nobody messes with our textRect.
502    return pref.getSize();
503  }
504
505  /**
506   * Returns preferred size of the given component
507   *
508   * @param c
509   *          component for which to return preferred size
510   * @return $Dimension$ preferred size for the given component
511   */
512  public Dimension getPreferredSize(JComponent c)
513  {
514    return getPreferredMenuItemSize(c, checkIcon, arrowIcon,
515                                    defaultTextIconGap);
516  }
517
518  /**
519   * Returns the prefix for entries in the {@link UIDefaults} table.
520   *
521   * @return "MenuItem"
522   */
523  protected String getPropertyPrefix()
524  {
525    return "MenuItem";
526  }
527
528  /**
529   * This method installs the components for this {@link JMenuItem}.
530   *
531   * @param menuItem
532   *          The {@link JMenuItem} to install components for.
533   */
534  protected void installComponents(JMenuItem menuItem)
535  {
536    // FIXME: Need to implement
537  }
538
539  /**
540   * This method installs the defaults that are defined in the Basic look and
541   * feel for this {@link JMenuItem}.
542   */
543  protected void installDefaults()
544  {
545    String prefix = getPropertyPrefix();
546    LookAndFeel.installBorder(menuItem, prefix + ".border");
547    LookAndFeel.installColorsAndFont(menuItem, prefix + ".background",
548                                     prefix + ".foreground", prefix + ".font");
549    menuItem.setMargin(UIManager.getInsets(prefix + ".margin"));
550    acceleratorFont = UIManager.getFont(prefix + ".acceleratorFont");
551    acceleratorForeground = UIManager.getColor(prefix
552        + ".acceleratorForeground");
553    acceleratorSelectionForeground = UIManager.getColor(prefix
554        + ".acceleratorSelectionForeground");
555    selectionBackground = UIManager.getColor(prefix + ".selectionBackground");
556    selectionForeground = UIManager.getColor(prefix + ".selectionForeground");
557    acceleratorDelimiter = UIManager.getString(prefix + ".acceleratorDelimiter");
558    checkIcon = UIManager.getIcon(prefix + ".checkIcon");
559
560    menuItem.setHorizontalTextPosition(SwingConstants.TRAILING);
561    menuItem.setHorizontalAlignment(SwingConstants.LEADING);
562  }
563
564  /**
565   * This method installs the keyboard actions for this {@link JMenuItem}.
566   */
567  protected void installKeyboardActions()
568  {
569    InputMap focusedWindowMap = SwingUtilities.getUIInputMap(menuItem,
570        JComponent.WHEN_IN_FOCUSED_WINDOW);
571    if (focusedWindowMap == null)
572      focusedWindowMap = new ComponentInputMapUIResource(menuItem);
573    KeyStroke accelerator = menuItem.getAccelerator();
574    if (accelerator != null)
575      focusedWindowMap.put(accelerator, "doClick");
576    SwingUtilities.replaceUIInputMap(menuItem,
577        JComponent.WHEN_IN_FOCUSED_WINDOW, focusedWindowMap);
578
579    ActionMap UIActionMap = SwingUtilities.getUIActionMap(menuItem);
580    if (UIActionMap == null)
581      UIActionMap = new ActionMapUIResource();
582    UIActionMap.put("doClick", new ClickAction());
583    SwingUtilities.replaceUIActionMap(menuItem, UIActionMap);
584  }
585
586  /**
587   * This method installs the listeners for the {@link JMenuItem}.
588   */
589  protected void installListeners()
590  {
591    menuItem.addMouseListener(mouseInputListener);
592    menuItem.addMouseMotionListener(mouseInputListener);
593    menuItem.addMenuDragMouseListener(menuDragMouseListener);
594    menuItem.addMenuKeyListener(menuKeyListener);
595    menuItem.addItemListener(itemListener);
596    menuItem.addPropertyChangeListener(propertyChangeListener);
597    // Fire synthetic property change event to let the listener update
598    // the TextLayout cache.
599    propertyChangeListener.propertyChange(new PropertyChangeEvent(menuItem,
600                                                                  "font", null,
601                                                          menuItem.getFont()));
602  }
603
604  /**
605   * Installs and initializes all fields for this UI delegate. Any properties of
606   * the UI that need to be initialized and/or set to defaults will be done now.
607   * It will also install any listeners necessary.
608   *
609   * @param c
610   *          The {@link JComponent} that is having this UI installed.
611   */
612  public void installUI(JComponent c)
613  {
614    super.installUI(c);
615    menuItem = (JMenuItem) c;
616    installDefaults();
617    installComponents(menuItem);
618    installListeners();
619    installKeyboardActions();
620  }
621
622  /**
623   * Paints given menu item using specified graphics context
624   *
625   * @param g
626   *          The graphics context used to paint this menu item
627   * @param c
628   *          Menu Item to paint
629   */
630  public void paint(Graphics g, JComponent c)
631  {
632    paintMenuItem(g, c, checkIcon, arrowIcon, selectionBackground,
633                  c.getForeground(), defaultTextIconGap);
634  }
635
636  /**
637   * Paints background of the menu item
638   *
639   * @param g
640   *          The graphics context used to paint this menu item
641   * @param menuItem
642   *          menu item to paint
643   * @param bgColor
644   *          Background color to use when painting menu item
645   */
646  protected void paintBackground(Graphics g, JMenuItem menuItem, Color bgColor)
647  {
648    // Menu item is considered to be highlighted when it is selected.
649    // But we don't want to paint the background of JCheckBoxMenuItems
650    ButtonModel mod = menuItem.getModel();
651    Color saved = g.getColor();
652    if (mod.isArmed() || ((menuItem instanceof JMenu) && mod.isSelected()))
653      {
654        g.setColor(bgColor);
655        g.fillRect(0, 0, menuItem.getWidth(), menuItem.getHeight());
656      }
657    else if (menuItem.isOpaque())
658      {
659        g.setColor(menuItem.getBackground());
660        g.fillRect(0, 0, menuItem.getWidth(), menuItem.getHeight());
661      }
662    g.setColor(saved);
663  }
664
665  /**
666   * Paints specified menu item
667   *
668   * @param g
669   *          The graphics context used to paint this menu item
670   * @param c
671   *          menu item to paint
672   * @param checkIcon
673   *          check icon to use when painting menu item
674   * @param arrowIcon
675   *          arrow icon to use when painting menu item
676   * @param background
677   *          Background color of the menu item
678   * @param foreground
679   *          Foreground color of the menu item
680   * @param defaultTextIconGap
681   *          space to use between icon and text when painting menu item
682   */
683  protected void paintMenuItem(Graphics g, JComponent c, Icon checkIcon,
684                               Icon arrowIcon, Color background,
685                               Color foreground, int defaultTextIconGap)
686  {
687    JMenuItem m = (JMenuItem) c;
688
689    // Fetch fonts.
690    Font oldFont = g.getFont();
691    Font font = c.getFont();
692    g.setFont(font);
693    FontMetrics accelFm = m.getFontMetrics(acceleratorFont);
694
695    // Create accelerator string.
696    String accelText = getAcceleratorString(m);
697
698    // Layout menu item. The result gets stored in the rectangle fields
699    // of this class.
700    resetRectangles(m);
701
702    layoutMenuItem(m, accelText);
703
704    // Paint the background.
705    paintBackground(g, m, background);
706
707    Color oldColor = g.getColor();
708
709    // Paint the check icon.
710    if (checkIcon != null)
711      {
712        checkIcon.paintIcon(m, g, checkIconRect.x, checkIconRect.y);
713      }
714
715    // Paint the icon.
716    ButtonModel model = m.getModel();
717    if (m.getIcon() != null)
718      {
719        // Determine icon depending on the menu item
720        // state (normal/disabled/pressed).
721        Icon icon;
722        if (! m.isEnabled())
723          {
724            icon = m.getDisabledIcon();
725          }
726        else if (model.isPressed() && model.isArmed())
727          {
728            icon = m.getPressedIcon();
729            if (icon == null)
730              {
731                icon = m.getIcon();
732              }
733          }
734        else
735          {
736            icon = m.getIcon();
737          }
738
739        if (icon != null)
740          {
741            icon.paintIcon(m, g, iconRect.x, iconRect.y);
742          }
743      }
744
745    // Paint the text.
746    String text = m.getText();
747    if (text != null)
748      {
749        // Handle HTML.
750        View html = (View) m.getClientProperty(BasicHTML.propertyKey);
751        if (html != null)
752          {
753            html.paint(g, textRect);
754          }
755        else
756          {
757            paintText(g, m, textRect, text);
758          }
759      }
760
761    // Paint accelerator text.
762    if (! accelText.equals(""))
763      {
764        // Align the accelerator text. In getPreferredMenuItemSize() we
765        // store a client property 'maxAccelWidth' in the parent which holds
766        // the maximum accelerator width for the children of this parent.
767        // We use this here to align the accelerators properly.
768        int accelOffset = 0;
769        Container parent = m.getParent();
770        if (parent != null && parent instanceof JComponent)
771          {
772            JComponent p = (JComponent) parent;
773            Integer maxAccelWidth =
774              (Integer) p.getClientProperty("maxAccelWidth");
775            int maxAccelValue = maxAccelWidth == null ? 0
776                                                    : maxAccelWidth.intValue();
777            accelOffset = maxAccelValue - accelRect.width;
778          }
779
780        g.setFont(acceleratorFont);
781        if (! m.isEnabled())
782          {
783            // Paint accelerator disabled.
784            g.setColor(disabledForeground);
785          }
786        else
787          {
788            if (m.isArmed() || (m instanceof JMenu && m.isSelected()))
789              g.setColor(acceleratorSelectionForeground);
790            else
791              g.setColor(acceleratorForeground);
792          }
793        g.drawString(accelText, accelRect.x - accelOffset,
794                     accelRect.y + accelFm.getAscent());
795      }
796
797    // Paint arrow.
798    if (arrowIcon != null
799        && ! (m instanceof JMenu && ((JMenu) m).isTopLevelMenu()))
800      {
801        arrowIcon.paintIcon(m, g, arrowIconRect.x, arrowIconRect.y);
802      }
803
804    g.setFont(oldFont);
805    g.setColor(oldColor);
806
807  }
808
809  /**
810   * Paints label for the given menu item
811   *
812   * @param g
813   *          The graphics context used to paint this menu item
814   * @param menuItem
815   *          menu item for which to draw its label
816   * @param textRect
817   *          rectangle specifiying position of the text relative to the given
818   *          menu item
819   * @param text
820   *          label of the menu item
821   */
822  protected void paintText(Graphics g, JMenuItem menuItem, Rectangle textRect,
823                           String text)
824  {
825    Font f = menuItem.getFont();
826    g.setFont(f);
827    FontMetrics fm = g.getFontMetrics(f);
828
829    if (text != null && !text.equals(""))
830      {
831        if (menuItem.isEnabled())
832          {
833            // Menu item is considered to be highlighted when it is selected.
834            // But not if it's a JCheckBoxMenuItem
835            ButtonModel mod = menuItem.getModel();
836            if ((menuItem.isSelected() && checkIcon == null)
837                || (mod != null && mod.isArmed())
838                && (menuItem.getParent() instanceof MenuElement))
839              g.setColor(selectionForeground);
840            else
841              g.setColor(menuItem.getForeground());
842          }
843        else
844          // FIXME: should fix this to use 'disabledForeground', but its
845          // default value in BasicLookAndFeel is null.
846
847          // FIXME: should there be different foreground colours for selected
848          // or deselected, when disabled?
849          g.setColor(Color.gray);
850
851        int mnemonicIndex = menuItem.getDisplayedMnemonicIndex();
852
853        if (mnemonicIndex != -1)
854          BasicGraphicsUtils.drawStringUnderlineCharAt(menuItem, g, text,
855                                                       mnemonicIndex,
856                                                       textRect.x,
857                                                       textRect.y
858                                                           + fm.getAscent());
859        else
860          BasicGraphicsUtils.drawString(menuItem, g, text, 0, textRect.x,
861                                        textRect.y + fm.getAscent());
862      }
863  }
864
865  /**
866   * This method uninstalls the components for this {@link JMenuItem}.
867   *
868   * @param menuItem
869   *          The {@link JMenuItem} to uninstall components for.
870   */
871  protected void uninstallComponents(JMenuItem menuItem)
872  {
873    // FIXME: need to implement
874  }
875
876  /**
877   * This method uninstalls the defaults and sets any objects created during
878   * install to null
879   */
880  protected void uninstallDefaults()
881  {
882    menuItem.setForeground(null);
883    menuItem.setBackground(null);
884    menuItem.setBorder(null);
885    menuItem.setMargin(null);
886    menuItem.setBackground(null);
887    menuItem.setBorder(null);
888    menuItem.setFont(null);
889    menuItem.setForeground(null);
890    menuItem.setMargin(null);
891    acceleratorFont = null;
892    acceleratorForeground = null;
893    acceleratorSelectionForeground = null;
894    arrowIcon = null;
895    selectionBackground = null;
896    selectionForeground = null;
897    acceleratorDelimiter = null;
898  }
899
900  /**
901   * Uninstalls any keyboard actions.
902   */
903  protected void uninstallKeyboardActions()
904  {
905    SwingUtilities.replaceUIInputMap(menuItem,
906                                     JComponent.WHEN_IN_FOCUSED_WINDOW, null);
907  }
908
909  /**
910   * Unregisters all the listeners that this UI delegate was using.
911   */
912  protected void uninstallListeners()
913  {
914    menuItem.removeMouseListener(mouseInputListener);
915    menuItem.removeMenuDragMouseListener(menuDragMouseListener);
916    menuItem.removeMenuKeyListener(menuKeyListener);
917    menuItem.removeItemListener(itemListener);
918    menuItem.removePropertyChangeListener(propertyChangeListener);
919  }
920
921  /**
922   * Performs the opposite of installUI. Any properties or resources that need
923   * to be cleaned up will be done now. It will also uninstall any listeners it
924   * has. In addition, any properties of this UI will be nulled.
925   *
926   * @param c
927   *          The {@link JComponent} that is having this UI uninstalled.
928   */
929  public void uninstallUI(JComponent c)
930  {
931    uninstallListeners();
932    uninstallDefaults();
933    uninstallComponents(menuItem);
934    c.putClientProperty(BasicGraphicsUtils.CACHED_TEXT_LAYOUT, null);
935    menuItem = null;
936  }
937
938  /**
939   * This method calls paint.
940   *
941   * @param g
942   *          The graphics context used to paint this menu item
943   * @param c
944   *          The menu item to paint
945   */
946  public void update(Graphics g, JComponent c)
947  {
948    paint(g, c);
949  }
950
951  /**
952   * This class handles mouse events occuring inside the menu item. Most of the
953   * events are forwarded for processing to MenuSelectionManager of the current
954   * menu hierarchy.
955   */
956  protected class MouseInputHandler implements MouseInputListener
957  {
958    /**
959     * Creates a new MouseInputHandler object.
960     */
961    protected MouseInputHandler()
962    {
963      // Nothing to do here.
964    }
965
966    /**
967     * This method is called when mouse is clicked on the menu item. It forwards
968     * this event to MenuSelectionManager.
969     *
970     * @param e
971     *          A {@link MouseEvent}.
972     */
973    public void mouseClicked(MouseEvent e)
974    {
975      MenuSelectionManager manager = MenuSelectionManager.defaultManager();
976      manager.processMouseEvent(e);
977    }
978
979    /**
980     * This method is called when mouse is dragged inside the menu item. It
981     * forwards this event to MenuSelectionManager.
982     *
983     * @param e
984     *          A {@link MouseEvent}.
985     */
986    public void mouseDragged(MouseEvent e)
987    {
988      MenuSelectionManager manager = MenuSelectionManager.defaultManager();
989      manager.processMouseEvent(e);
990    }
991
992    /**
993     * This method is called when mouse enters menu item. When this happens menu
994     * item is considered to be selected and selection path in
995     * MenuSelectionManager is set. This event is also forwarded to
996     * MenuSelection Manager for further processing.
997     *
998     * @param e
999     *          A {@link MouseEvent}.
1000     */
1001    public void mouseEntered(MouseEvent e)
1002    {
1003      Component source = (Component) e.getSource();
1004      if (source.getParent() instanceof MenuElement)
1005        {
1006          MenuSelectionManager manager = MenuSelectionManager.defaultManager();
1007          manager.setSelectedPath(getPath());
1008          manager.processMouseEvent(e);
1009        }
1010    }
1011
1012    /**
1013     * This method is called when mouse exits menu item. The event is forwarded
1014     * to MenuSelectionManager for processing.
1015     *
1016     * @param e
1017     *          A {@link MouseEvent}.
1018     */
1019    public void mouseExited(MouseEvent e)
1020    {
1021      MenuSelectionManager manager = MenuSelectionManager.defaultManager();
1022      manager.processMouseEvent(e);
1023    }
1024
1025    /**
1026     * This method is called when mouse is inside the menu item. This event is
1027     * forwarder to MenuSelectionManager for further processing.
1028     *
1029     * @param e
1030     *          A {@link MouseEvent}.
1031     */
1032    public void mouseMoved(MouseEvent e)
1033    {
1034      MenuSelectionManager manager = MenuSelectionManager.defaultManager();
1035      manager.processMouseEvent(e);
1036    }
1037
1038    /**
1039     * This method is called when mouse is pressed. This event is forwarded to
1040     * MenuSelectionManager for further processing.
1041     *
1042     * @param e
1043     *          A {@link MouseEvent}.
1044     */
1045    public void mousePressed(MouseEvent e)
1046    {
1047      MenuSelectionManager manager = MenuSelectionManager.defaultManager();
1048      manager.processMouseEvent(e);
1049    }
1050
1051    /**
1052     * This method is called when mouse is released. If the mouse is released
1053     * inside this menuItem, then this menu item is considered to be chosen and
1054     * the menu hierarchy should be closed.
1055     *
1056     * @param e
1057     *          A {@link MouseEvent}.
1058     */
1059    public void mouseReleased(MouseEvent e)
1060    {
1061      MenuSelectionManager manager = MenuSelectionManager.defaultManager();
1062      int x = e.getX();
1063      int y = e.getY();
1064      if (x > 0 && x < menuItem.getWidth() && y > 0
1065          && y < menuItem.getHeight())
1066        {
1067          doClick(manager);
1068        }
1069      else
1070        manager.processMouseEvent(e);
1071    }
1072  }
1073
1074  /**
1075   * This class handles mouse dragged events.
1076   */
1077  private class MenuDragMouseHandler implements MenuDragMouseListener
1078  {
1079    /**
1080     * Tbis method is invoked when mouse is dragged over the menu item.
1081     *
1082     * @param e
1083     *          The MenuDragMouseEvent
1084     */
1085    public void menuDragMouseDragged(MenuDragMouseEvent e)
1086    {
1087      MenuSelectionManager manager = e.getMenuSelectionManager();
1088      manager.setSelectedPath(e.getPath());
1089    }
1090
1091    /**
1092     * Tbis method is invoked when mouse enters the menu item while it is being
1093     * dragged.
1094     *
1095     * @param e
1096     *          The MenuDragMouseEvent
1097     */
1098    public void menuDragMouseEntered(MenuDragMouseEvent e)
1099    {
1100      MenuSelectionManager manager = e.getMenuSelectionManager();
1101      manager.setSelectedPath(e.getPath());
1102    }
1103
1104    /**
1105     * Tbis method is invoked when mouse exits the menu item while it is being
1106     * dragged
1107     *
1108     * @param e the MenuDragMouseEvent
1109     */
1110    public void menuDragMouseExited(MenuDragMouseEvent e)
1111    {
1112      // Nothing to do here yet.
1113    }
1114
1115    /**
1116     * Tbis method is invoked when mouse was dragged and released inside the
1117     * menu item.
1118     *
1119     * @param e
1120     *          The MenuDragMouseEvent
1121     */
1122    public void menuDragMouseReleased(MenuDragMouseEvent e)
1123    {
1124      MenuSelectionManager manager = e.getMenuSelectionManager();
1125      int x = e.getX();
1126      int y = e.getY();
1127      if (x >= 0 && x < menuItem.getWidth() && y >= 0
1128          && y < menuItem.getHeight())
1129        doClick(manager);
1130      else
1131        manager.clearSelectedPath();
1132    }
1133  }
1134
1135  /**
1136   * This class handles key events occuring when menu item is visible on the
1137   * screen.
1138   */
1139  private class MenuKeyHandler implements MenuKeyListener
1140  {
1141    /**
1142     * This method is invoked when key has been pressed
1143     *
1144     * @param e
1145     *          A {@link MenuKeyEvent}.
1146     */
1147    public void menuKeyPressed(MenuKeyEvent e)
1148    {
1149      // TODO: What should be done here, if anything?
1150    }
1151
1152    /**
1153     * This method is invoked when key has been pressed
1154     *
1155     * @param e
1156     *          A {@link MenuKeyEvent}.
1157     */
1158    public void menuKeyReleased(MenuKeyEvent e)
1159    {
1160      // TODO: What should be done here, if anything?
1161    }
1162
1163    /**
1164     * This method is invoked when key has been typed It handles the mnemonic
1165     * key for the menu item.
1166     *
1167     * @param e
1168     *          A {@link MenuKeyEvent}.
1169     */
1170    public void menuKeyTyped(MenuKeyEvent e)
1171    {
1172      // TODO: What should be done here, if anything?
1173    }
1174  }
1175
1176  /**
1177   * Helper class that listens for item changes to the properties of the {@link
1178   * JMenuItem}.
1179   */
1180  private class ItemHandler implements ItemListener
1181  {
1182    /**
1183     * This method is called when one of the menu item changes.
1184     *
1185     * @param evt A {@link ItemEvent}.
1186     */
1187    public void itemStateChanged(ItemEvent evt)
1188    {
1189      boolean state = false;
1190      if (menuItem instanceof JCheckBoxMenuItem)
1191        {
1192          if (evt.getStateChange() == ItemEvent.SELECTED)
1193            state = true;
1194          ((JCheckBoxMenuItem) menuItem).setState(state);
1195        }
1196      menuItem.revalidate();
1197      menuItem.repaint();
1198    }
1199  }
1200
1201  /**
1202   * A helper method to create the accelerator string from the menu item's
1203   * accelerator property. The returned string is empty if there is
1204   * no accelerator defined.
1205   *
1206   * @param m the menu item
1207   *
1208   * @return the accelerator string, not null
1209   */
1210  private String getAcceleratorString(JMenuItem m)
1211  {
1212    // Create accelerator string.
1213    KeyStroke accel = m.getAccelerator();
1214    String accelText = "";
1215    if (accel != null)
1216      {
1217        int mods = accel.getModifiers();
1218        if (mods > 0)
1219          {
1220            accelText = KeyEvent.getKeyModifiersText(mods);
1221            accelText += acceleratorDelimiter;
1222          }
1223        int keycode = accel.getKeyCode();
1224        if (keycode != 0)
1225          accelText += KeyEvent.getKeyText(keycode);
1226        else
1227          accelText += accel.getKeyChar();
1228      }
1229    return accelText;
1230  }
1231
1232  /**
1233   * Resets the cached layout rectangles. If <code>i</code> is not null, then
1234   * the view rectangle is set to the inner area of the component, otherwise
1235   * it is set to (0, 0, Short.MAX_VALUE, Short.MAX_VALUE), this is needed
1236   * for layouting.
1237   *
1238   * @param i the component for which to initialize the rectangles
1239   */
1240  private void resetRectangles(JMenuItem i)
1241  {
1242    // Reset rectangles.
1243    iconRect.setBounds(0, 0, 0, 0);
1244    textRect.setBounds(0, 0, 0, 0);
1245    accelRect.setBounds(0, 0, 0, 0);
1246    checkIconRect.setBounds(0, 0, 0, 0);
1247    arrowIconRect.setBounds(0, 0, 0, 0);
1248    if (i == null)
1249      viewRect.setBounds(0, 0, Short.MAX_VALUE, Short.MAX_VALUE);
1250    else
1251      {
1252        Insets insets = i.getInsets();
1253        viewRect.setBounds(insets.left, insets.top,
1254                           i.getWidth() - insets.left - insets.right,
1255                           i.getHeight() - insets.top - insets.bottom);
1256      }
1257  }
1258
1259  /**
1260   * A helper method that lays out the menu item. The layout is stored
1261   * in the fields of this class.
1262   *
1263   * @param m the menu item to layout
1264   * @param accelText the accelerator text
1265   */
1266  private void layoutMenuItem(JMenuItem m, String accelText)
1267  {
1268    // Fetch the fonts.
1269    Font font = m.getFont();
1270    FontMetrics fm = m.getFontMetrics(font);
1271    FontMetrics accelFm = m.getFontMetrics(acceleratorFont);
1272
1273    String text = m.getText();
1274    SwingUtilities.layoutCompoundLabel(m, fm, text, m.getIcon(),
1275                                       m.getVerticalAlignment(),
1276                                       m.getHorizontalAlignment(),
1277                                       m.getVerticalTextPosition(),
1278                                       m.getHorizontalTextPosition(),
1279                                       viewRect, iconRect, textRect,
1280                                       defaultTextIconGap);
1281
1282    // Initialize accelerator width and height.
1283    if (! accelText.equals(""))
1284      {
1285        accelRect.width = accelFm.stringWidth(accelText);
1286        accelRect.height = accelFm.getHeight();
1287      }
1288
1289    // Initialize check and arrow icon width and height.
1290    if (! (m instanceof JMenu && ((JMenu) m).isTopLevelMenu()))
1291      {
1292        if (checkIcon != null)
1293          {
1294            checkIconRect.width = checkIcon.getIconWidth();
1295            checkIconRect.height = checkIcon.getIconHeight();
1296          }
1297        if (arrowIcon != null)
1298          {
1299            arrowIconRect.width = arrowIcon.getIconWidth();
1300            arrowIconRect.height = arrowIcon.getIconHeight();
1301          }
1302      }
1303
1304    // The union of the icon and text of the menu item is the 'label area'.
1305    cachedRect.setBounds(textRect);
1306    Rectangle labelRect = SwingUtilities.computeUnion(iconRect.x,
1307                                                      iconRect.y,
1308                                                      iconRect.width,
1309                                                      iconRect.height,
1310                                                      cachedRect);
1311    textRect.x += defaultTextIconGap;
1312    iconRect.x += defaultTextIconGap;
1313
1314    // Layout accelerator rect.
1315    accelRect.x = viewRect.x + viewRect.width - arrowIconRect.width
1316      - defaultTextIconGap - accelRect.width;
1317    // Layout check and arrow icons only when not in toplevel menu.
1318    if (! (m instanceof JMenu && ((JMenu) m).isTopLevelMenu()))
1319      {
1320        checkIconRect.x = viewRect.x + defaultTextIconGap;
1321        textRect.x += defaultTextIconGap + checkIconRect.width;
1322        iconRect.x += defaultTextIconGap + checkIconRect.width;
1323        arrowIconRect.x = viewRect.x + viewRect.width - defaultTextIconGap
1324          - arrowIconRect.width;
1325      }
1326
1327    // Align the accelerator text and all the icons vertically centered to
1328    // the menu text.
1329    accelRect.y = labelRect.y + (labelRect.height / 2)
1330      - (accelRect.height / 2);
1331    if (! (m instanceof JMenu && ((JMenu) m).isTopLevelMenu()))
1332      {
1333        arrowIconRect.y = labelRect.y + (labelRect.height / 2)
1334          - (arrowIconRect.height / 2);
1335        checkIconRect.y = labelRect.y + (labelRect.height / 2)
1336          - (checkIconRect.height / 2);
1337      }
1338  }
1339}