001/* BasicPopupMenuUI.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
038package javax.swing.plaf.basic;
039
040import java.awt.Component;
041import java.awt.Dimension;
042import java.awt.KeyboardFocusManager;
043import java.awt.event.ActionEvent;
044import java.awt.event.ComponentEvent;
045import java.awt.event.ComponentListener;
046import java.awt.event.MouseEvent;
047import java.util.EventListener;
048
049import javax.swing.AbstractAction;
050import javax.swing.Action;
051import javax.swing.ActionMap;
052import javax.swing.BoxLayout;
053import javax.swing.InputMap;
054import javax.swing.JApplet;
055import javax.swing.JComponent;
056import javax.swing.JFrame;
057import javax.swing.JMenu;
058import javax.swing.JMenuBar;
059import javax.swing.JMenuItem;
060import javax.swing.JPopupMenu;
061import javax.swing.JRootPane;
062import javax.swing.LookAndFeel;
063import javax.swing.MenuElement;
064import javax.swing.MenuSelectionManager;
065import javax.swing.SwingUtilities;
066import javax.swing.UIManager;
067import javax.swing.event.ChangeEvent;
068import javax.swing.event.ChangeListener;
069import javax.swing.event.PopupMenuEvent;
070import javax.swing.event.PopupMenuListener;
071import javax.swing.plaf.ActionMapUIResource;
072import javax.swing.plaf.ComponentUI;
073import javax.swing.plaf.PopupMenuUI;
074
075/**
076 * UI Delegate for JPopupMenu
077 */
078public class BasicPopupMenuUI extends PopupMenuUI
079{
080  /**
081   * Handles keyboard navigation through menus.
082   */
083  private static class NavigateAction
084    extends AbstractAction
085  {
086
087    /**
088     * Creates a new NavigateAction instance.
089     *
090     * @param name the name of the action
091     */
092    NavigateAction(String name)
093    {
094      super(name);
095    }
096
097    /**
098     * Actually performs the action.
099     */
100    public void actionPerformed(ActionEvent event)
101    {
102      String name = (String) getValue(Action.NAME);
103      if (name.equals("selectNext"))
104        navigateNextPrevious(true);
105      else if (name.equals("selectPrevious"))
106        navigateNextPrevious(false);
107      else if (name.equals("selectChild"))
108        navigateParentChild(true);
109      else if (name.equals("selectParent"))
110        navigateParentChild(false);
111      else if (name.equals("cancel"))
112        cancel();
113      else if (name.equals("return"))
114        doReturn();
115      else
116        assert false : "Must not reach here";
117    }
118
119    /**
120     * Navigates to the next or previous menu item.
121     *
122     * @param dir <code>true</code>: navigate to next, <code>false</code>:
123     *        navigate to previous
124     */
125    private void navigateNextPrevious(boolean dir)
126    {
127      MenuSelectionManager msm = MenuSelectionManager.defaultManager();
128      MenuElement path[] = msm.getSelectedPath();
129      int len = path.length;
130      if (len >= 2)
131        {
132
133          if (path[0] instanceof JMenuBar &&
134              path[1] instanceof JMenu && len == 2)
135            {
136
137              // A toplevel menu is selected, but its popup not yet shown.
138              // Show the popup and select the first item
139              JPopupMenu popup = ((JMenu)path[1]).getPopupMenu();
140              MenuElement next =
141                findEnabledChild(popup.getSubElements(), -1, true);
142              MenuElement[] newPath;
143
144              if (next != null)
145                {
146                  newPath = new MenuElement[4];
147                  newPath[3] = next;
148                }
149              else
150                {
151                  // Menu has no enabled items, show the popup anyway.
152                  newPath = new MenuElement[3];
153                }
154              System.arraycopy(path, 0, newPath, 0, 2);
155              newPath[2] = popup;
156              msm.setSelectedPath(newPath);
157            }
158          else if (path[len - 1] instanceof JPopupMenu &&
159                   path[len - 2] instanceof JMenu)
160            {
161              // Select next item in already shown popup menu.
162              JMenu menu = (JMenu) path[len - 2];
163              JPopupMenu popup = menu.getPopupMenu();
164              MenuElement next =
165                findEnabledChild(popup.getSubElements(), -1, dir);
166
167              if (next != null)
168                {
169                  MenuElement[] newPath = new MenuElement[len + 1];
170                  System.arraycopy(path, 0, newPath, 0, len);
171                  newPath[len] = next;
172                  msm.setSelectedPath(newPath);
173                }
174              else
175                {
176                  // All items in the popup are disabled.
177                  // Find the parent popup menu and select
178                  // its next item. If there's no parent popup menu , do nothing.
179                  if (len > 2 && path[len - 3] instanceof JPopupMenu)
180                    {
181                      popup = ((JPopupMenu) path[len - 3]);
182                      next = findEnabledChild(popup.getSubElements(),
183                                              menu, dir);
184                      if (next != null && next != menu)
185                        {
186                          MenuElement[] newPath = new MenuElement[len - 1];
187                          System.arraycopy(path, 0, newPath, 0, len - 2);
188                          newPath[len - 2] = next;
189                          msm.setSelectedPath(newPath);
190                        }
191                    }
192                }
193            }
194          else
195            {
196              // Only select the next item.
197              MenuElement subs[] = path[len - 2].getSubElements();
198              MenuElement nextChild =
199                findEnabledChild(subs, path[len - 1], dir);
200              if (nextChild == null)
201                {
202                  nextChild = findEnabledChild(subs, -1, dir);
203                }
204              if (nextChild != null)
205                {
206                  path[len-1] = nextChild;
207                  msm.setSelectedPath(path);
208                }
209            }
210        }
211    }
212
213    private MenuElement findEnabledChild(MenuElement[] children,
214                                         MenuElement start, boolean dir)
215    {
216      MenuElement found = null;
217      for (int i = 0; i < children.length && found == null; i++)
218        {
219          if (children[i] == start)
220            {
221              found = findEnabledChild(children, i, dir);
222            }
223        }
224      return found;
225    }
226
227    /**
228     * Searches the next or previous enabled child menu element.
229     *
230     * @param children the children to search through
231     * @param start the index at which to start
232     * @param dir the direction (true == forward, false == backward)
233     *
234     * @return the found element or null
235     */
236    private MenuElement findEnabledChild(MenuElement[] children,
237                                         int start, boolean dir)
238    {
239      MenuElement result = null;
240      if (dir)
241        {
242          result = findNextEnabledChild(children, start + 1, children.length-1);
243          if (result == null)
244            result = findNextEnabledChild(children, 0, start - 1);
245        }
246      else
247        {
248          result = findPreviousEnabledChild(children, start - 1, 0);
249          if (result == null)
250            result = findPreviousEnabledChild(children, children.length-1,
251                                          start + 1);
252        }
253      return result;
254    }
255
256    /**
257     * Finds the next child element that is enabled and visible.
258     * 
259     * @param children the children to search through
260     * @param start the start index
261     * @param end the end index
262     *
263     * @return the found child, or null
264     */
265    private MenuElement findNextEnabledChild(MenuElement[] children, int start,
266                                             int end)
267    {
268      MenuElement found = null;
269      for (int i = start; i <= end && found == null; i++)
270        {
271          if (children[i] != null)
272            {
273              Component comp = children[i].getComponent();
274              if (comp != null && comp.isEnabled() && comp.isVisible())
275                {
276                  found = children[i];
277                }
278            }
279        }
280      return found;
281    }
282
283    /**
284     * Finds the previous child element that is enabled and visible.
285     * 
286     * @param children the children to search through
287     * @param start the start index
288     * @param end the end index
289     *
290     * @return the found child, or null
291     */
292    private MenuElement findPreviousEnabledChild(MenuElement[] children,
293                                                 int start, int end)
294    {
295      MenuElement found = null;
296      for (int i = start; i >= end && found == null; i--)
297        {
298          if (children[i] != null)
299            {
300              Component comp = children[i].getComponent();
301              if (comp != null && comp.isEnabled() && comp.isVisible())
302                {
303                  found = children[i];
304                }
305            }
306        }
307      return found;
308    }
309
310    /**
311     * Navigates to the parent or child menu item.
312     *
313     * @param selectChild <code>true</code>: navigate to child,
314     *        <code>false</code>: navigate to parent
315     */
316    private void navigateParentChild(boolean selectChild)
317    {
318      MenuSelectionManager msm = MenuSelectionManager.defaultManager();
319      MenuElement path[] = msm.getSelectedPath();
320      int len = path.length;
321
322      if (selectChild)
323        {
324          if (len > 0 && path[len - 1] instanceof JMenu
325              && ! ((JMenu) path[len-1]).isTopLevelMenu())
326            {
327              // We have a submenu, open it.
328              JMenu menu = (JMenu) path[len - 1];
329              JPopupMenu popup = menu.getPopupMenu();
330              MenuElement[] subs = popup.getSubElements();
331              MenuElement item = findEnabledChild(subs, -1, true);
332              MenuElement[] newPath;
333
334              if (item == null)
335                {
336                  newPath = new MenuElement[len + 1];
337                }
338              else
339                {
340                  newPath = new MenuElement[len + 2];
341                  newPath[len + 1] = item;
342                }
343              System.arraycopy(path, 0, newPath, 0, len);
344              newPath[len] = popup;
345              msm.setSelectedPath(newPath);
346              return;
347            }
348        }
349      else
350        {
351          int popupIndex = len-1;
352          if (len > 2 
353              && (path[popupIndex] instanceof JPopupMenu
354                  || path[--popupIndex] instanceof JPopupMenu)
355                  && ! ((JMenu) path[popupIndex - 1]).isTopLevelMenu())
356            {
357              // We have a submenu, close it.
358              MenuElement newPath[] = new MenuElement[popupIndex];
359              System.arraycopy(path, 0, newPath, 0, popupIndex);
360              msm.setSelectedPath(newPath);
361              return;
362            }
363        }
364
365      // If we got here, we have not selected a child or parent.
366      // Check if we have a toplevel menu selected. If so, then select
367      // another one.
368      if (len > 1 && path[0] instanceof JMenuBar)
369        {
370          MenuElement currentMenu = path[1];
371          MenuElement nextMenu = findEnabledChild(path[0].getSubElements(),
372                                                  currentMenu, selectChild);
373
374          if (nextMenu != null && nextMenu != currentMenu)
375            {
376              MenuElement newSelection[];
377              if (len == 2)
378                {
379                  // Menu is selected but its popup not shown.
380                  newSelection = new MenuElement[2];
381                  newSelection[0] = path[0];
382                  newSelection[1] = nextMenu;
383                }
384              else
385                {
386                  // Menu is selected and its popup is shown.
387                  newSelection = new MenuElement[3];
388                  newSelection[0] = path[0];
389                  newSelection[1] = nextMenu;
390                  newSelection[2] = ((JMenu) nextMenu).getPopupMenu();
391                }
392              msm.setSelectedPath(newSelection);
393            }
394        }
395    }
396
397    /**
398     * Handles cancel requests (ESC key).
399     */
400    private void cancel()
401    {
402      // Fire popup menu cancelled event. Unfortunately the
403      // firePopupMenuCancelled() is protected in JPopupMenu so we work
404      // around this limitation by fetching the listeners and notifying them
405      // directly.
406      JPopupMenu lastPopup = (JPopupMenu) getLastPopup();
407      EventListener[] ll = lastPopup.getListeners(PopupMenuListener.class);
408      for (int i = 0; i < ll.length; i++)
409        {
410          PopupMenuEvent ev = new PopupMenuEvent(lastPopup);
411          ((PopupMenuListener) ll[i]).popupMenuCanceled(ev);
412        }
413
414      // Close the last popup or the whole selection if there's only one
415      // popup left.
416      MenuSelectionManager msm = MenuSelectionManager.defaultManager();
417      MenuElement path[] = msm.getSelectedPath();
418      if(path.length > 4)
419        {
420          MenuElement newPath[] = new MenuElement[path.length - 2];
421          System.arraycopy(path,0,newPath,0,path.length-2);
422          MenuSelectionManager.defaultManager().setSelectedPath(newPath);
423        }
424      else
425          msm.clearSelectedPath();
426    }
427
428    /**
429     * Returns the last popup menu in the current selection or null.
430     *
431     * @return the last popup menu in the current selection or null
432     */
433    private JPopupMenu getLastPopup()
434    {
435      MenuSelectionManager msm = MenuSelectionManager.defaultManager();
436      MenuElement[] p = msm.getSelectedPath();
437      JPopupMenu popup = null;
438      for(int i = p.length - 1; popup == null && i >= 0; i--)
439        {
440          if (p[i] instanceof JPopupMenu)
441            popup = (JPopupMenu) p[i];
442        }
443      return popup;
444    }
445
446    /**
447     * Handles ENTER key requests. This normally opens submenus on JMenu
448     * items, or activates the menu item as if it's been clicked on it.
449     */
450    private void doReturn()
451    {
452      KeyboardFocusManager fmgr =
453        KeyboardFocusManager.getCurrentKeyboardFocusManager();
454      Component focusOwner = fmgr.getFocusOwner();
455      if((focusOwner == null || (focusOwner instanceof JRootPane)))
456        {
457          MenuSelectionManager msm = MenuSelectionManager.defaultManager();
458          MenuElement path[] = msm.getSelectedPath();
459          MenuElement lastElement;
460          if(path.length > 0)
461            {
462              lastElement = path[path.length - 1];
463              if(lastElement instanceof JMenu)
464                {
465                  MenuElement newPath[] = new MenuElement[path.length + 1];
466                  System.arraycopy(path,0,newPath,0,path.length);
467                  newPath[path.length] = ((JMenu) lastElement).getPopupMenu();
468                  msm.setSelectedPath(newPath);
469                }
470              else if(lastElement instanceof JMenuItem)
471                {
472                  JMenuItem mi = (JMenuItem)lastElement;
473                  if (mi.getUI() instanceof BasicMenuItemUI)
474                    {
475                      ((BasicMenuItemUI)mi.getUI()).doClick(msm);
476                    }
477                  else
478                    {
479                      msm.clearSelectedPath();
480                      mi.doClick(0);
481                    }
482                }
483            }
484        }
485    }
486  }
487
488  /**
489   * Installs keyboard actions when a popup is opened, and uninstalls the
490   * keyboard actions when closed. This listens on the default
491   * MenuSelectionManager.
492   */
493  private class KeyboardHelper
494    implements ChangeListener
495  {
496    private MenuElement[] lastSelectedPath = new MenuElement[0];
497    private Component lastFocused;
498    private JRootPane invokerRootPane;
499
500    public void stateChanged(ChangeEvent event)
501    {
502      MenuSelectionManager msm = (MenuSelectionManager) event.getSource();
503      MenuElement[] p = msm.getSelectedPath();
504      JPopupMenu popup = getActivePopup(p);
505      if (popup == null || popup.isFocusable())
506        {
507          if (lastSelectedPath.length != 0 && p.length != 0 )
508            {
509              if (! invokerEquals(p[0], lastSelectedPath[0]))
510                {
511                  uninstallKeyboardActionsImpl();
512                  lastSelectedPath = new MenuElement[0];
513                }
514            }
515
516          if (lastSelectedPath.length == 0 && p.length > 0)
517            {
518              JComponent invoker;
519              if (popup == null)
520                {
521                  if (p.length == 2 && p[0] instanceof JMenuBar
522                      && p[1] instanceof JMenu)
523                    {
524                      // A menu has been selected but not opened.
525                      invoker = (JComponent)p[1];
526                      popup = ((JMenu)invoker).getPopupMenu();
527                    }
528                  else
529                    {
530                      return;
531                    }
532                }
533              else
534                {
535                Component c = popup.getInvoker();
536                if(c instanceof JFrame)
537                  {
538                    invoker = ((JFrame) c).getRootPane();
539                  }
540                else if(c instanceof JApplet)
541                  {
542                    invoker = ((JApplet) c).getRootPane();
543                  }
544                else
545                  {
546                    while (!(c instanceof JComponent))
547                      {
548                        if (c == null)
549                          {
550                            return;
551                          }
552                        c = c.getParent();
553                      }
554                    invoker = (JComponent)c;
555                  }
556                }
557
558              // Remember current focus owner.
559              lastFocused = KeyboardFocusManager.
560                             getCurrentKeyboardFocusManager().getFocusOwner();
561
562              // Install keybindings used for menu navigation.
563              invokerRootPane = SwingUtilities.getRootPane(invoker);
564              if (invokerRootPane != null)
565                {
566                  invokerRootPane.requestFocus(true);
567                  installKeyboardActionsImpl();
568                }
569            }
570          else if (lastSelectedPath.length != 0 && p.length == 0)
571            {
572              // menu hidden -- return focus to where it had been before
573              // and uninstall menu keybindings
574              uninstallKeyboardActionsImpl();
575            }
576        }
577
578      // Remember the last path selected
579      lastSelectedPath = p;
580    }
581
582    private JPopupMenu getActivePopup(MenuElement[] path)
583    {
584      JPopupMenu active = null;
585      for (int i = path.length - 1; i >= 0 && active == null; i--)
586        {
587          MenuElement elem = path[i];
588          if (elem instanceof JPopupMenu)
589            {
590              active = (JPopupMenu) elem;
591            }
592        }
593      return active;
594    }
595
596    private boolean invokerEquals(MenuElement el1, MenuElement el2)
597    {
598      Component invoker1 = el1.getComponent();
599      Component invoker2 = el2.getComponent();
600      if (invoker1 instanceof JPopupMenu)
601        invoker1 = ((JPopupMenu) invoker1).getInvoker();
602      if (invoker2 instanceof JPopupMenu)
603        invoker2 = ((JPopupMenu) invoker2).getInvoker();
604      return invoker1 == invoker2;
605    }
606  }
607
608  /* popupMenu for which this UI delegate is for*/
609  protected JPopupMenu popupMenu;
610
611  /* PopupMenuListener listens to popup menu events fired by JPopupMenu*/
612  private transient PopupMenuListener popupMenuListener;
613
614  /* ComponentListener listening to popupMenu's invoker.
615   * This is package-private to avoid an accessor method.  */
616  TopWindowListener topWindowListener;
617
618  /**
619   * Counts how many popup menus are handled by this UI or a subclass.
620   * This is used to install a KeyboardHelper on the MenuSelectionManager
621   * for the first popup, and uninstall this same KeyboardHelper when the
622   * last popup is uninstalled.
623   */
624  private static int numPopups;
625
626  /**
627   * This is the KeyboardHelper that listens on the MenuSelectionManager.
628   */
629  private static KeyboardHelper keyboardHelper;
630
631  /**
632   * Creates a new BasicPopupMenuUI object.
633   */
634  public BasicPopupMenuUI()
635  {
636    popupMenuListener = new PopupMenuHandler();
637    topWindowListener = new TopWindowListener();
638  }
639
640  /**
641   * Factory method to create a BasicPopupMenuUI for the given {@link
642   * JComponent}, which should be a {@link JMenuItem}.
643   *
644   * @param x The {@link JComponent} a UI is being created for.
645   *
646   * @return A BasicPopupMenuUI for the {@link JComponent}.
647   */
648  public static ComponentUI createUI(JComponent x)
649  {
650    return new BasicPopupMenuUI();
651  }
652
653  /**
654   * Installs and initializes all fields for this UI delegate. Any properties
655   * of the UI that need to be initialized and/or set to defaults will be
656   * done now. It will also install any listeners necessary.
657   *
658   * @param c The {@link JComponent} that is having this UI installed.
659   */
660  public void installUI(JComponent c)
661  {
662    super.installUI(c);
663
664    // Install KeyboardHelper when the first popup is initialized.
665    if (numPopups == 0)
666      {
667        keyboardHelper = new KeyboardHelper();
668        MenuSelectionManager msm = MenuSelectionManager.defaultManager();
669        msm.addChangeListener(keyboardHelper);
670      }
671    numPopups++;
672
673    popupMenu = (JPopupMenu) c;
674    popupMenu.setLayout(new DefaultMenuLayout(popupMenu, BoxLayout.Y_AXIS));
675    popupMenu.setBorderPainted(true);
676    JPopupMenu.setDefaultLightWeightPopupEnabled(true);
677
678    installDefaults();
679    installListeners();
680    installKeyboardActions();
681  }
682
683  /**
684   * This method installs the defaults that are defined in  the Basic look
685   * and feel for this {@link JPopupMenu}.
686   */
687  public void installDefaults()
688  {
689    LookAndFeel.installColorsAndFont(popupMenu, "PopupMenu.background",
690                                     "PopupMenu.foreground", "PopupMenu.font");
691    LookAndFeel.installBorder(popupMenu, "PopupMenu.border");
692    popupMenu.setOpaque(true);
693  }
694
695  /**
696   * This method installs the listeners for the {@link JMenuItem}.
697   */
698  protected void installListeners()
699  {
700    popupMenu.addPopupMenuListener(popupMenuListener);
701  }
702
703  /**
704   * This method installs the keyboard actions for this {@link JPopupMenu}.
705   */
706  protected void installKeyboardActions()
707  {
708    // We can't install the keyboard actions here, because then all
709    // popup menus would have their actions registered in the KeyboardManager.
710    // So we install it when the popup menu is opened, and uninstall it
711    // when it's closed. This is done in the KeyboardHelper class.
712    // Install InputMap.
713  }
714
715  /**
716   * Called by the KeyboardHandler when a popup is made visible.
717   */
718  void installKeyboardActionsImpl()
719  {
720    Object[] bindings;
721    if (popupMenu.getComponentOrientation().isLeftToRight())
722      {
723        bindings = (Object[])
724             SharedUIDefaults.get("PopupMenu.selectedWindowInputMapBindings");
725      }
726    else
727      {
728        bindings = (Object[]) SharedUIDefaults.get
729                      ("PopupMenu.selectedWindowInputMapBindings.RightToLeft");
730      }
731    InputMap inputMap = LookAndFeel.makeComponentInputMap(popupMenu, bindings);
732    SwingUtilities.replaceUIInputMap(popupMenu,
733                                     JComponent.WHEN_IN_FOCUSED_WINDOW,
734                                     inputMap);
735
736    // Install ActionMap.
737    SwingUtilities.replaceUIActionMap(popupMenu, getActionMap());
738  }
739
740  /**
741   * Creates and returns the shared action map for JTrees.
742   *
743   * @return the shared action map for JTrees
744   */
745  private ActionMap getActionMap()
746  {
747    ActionMap am = (ActionMap) UIManager.get("PopupMenu.actionMap");
748    if (am == null)
749      {
750        am = createDefaultActions();
751        UIManager.getLookAndFeelDefaults().put("PopupMenu.actionMap", am);
752      }
753    return am;
754  }
755
756  /**
757   * Creates the default actions when there are none specified by the L&F.
758   *
759   * @return the default actions
760   */
761  private ActionMap createDefaultActions()
762  {
763    ActionMapUIResource am = new ActionMapUIResource();
764    Action action = new NavigateAction("selectNext");
765    am.put(action.getValue(Action.NAME), action);
766    action = new NavigateAction("selectPrevious");
767    am.put(action.getValue(Action.NAME), action);
768    action = new NavigateAction("selectParent");
769    am.put(action.getValue(Action.NAME), action);
770    action = new NavigateAction("selectChild");
771    am.put(action.getValue(Action.NAME), action);
772    action = new NavigateAction("return");
773    am.put(action.getValue(Action.NAME), action);
774    action = new NavigateAction("cancel");
775    am.put(action.getValue(Action.NAME), action);
776    
777    return am;
778  }
779
780  /**
781   * Performs the opposite of installUI. Any properties or resources that need
782   * to be cleaned up will be done now. It will also uninstall any listeners
783   * it has. In addition, any properties of this UI will be nulled.
784   *
785   * @param c The {@link JComponent} that is having this UI uninstalled.
786   */
787  public void uninstallUI(JComponent c)
788  {
789    uninstallListeners();
790    uninstallDefaults();
791    uninstallKeyboardActions();
792    popupMenu = null;
793
794    // Install KeyboardHelper when the first popup is initialized.
795    numPopups--;
796    if (numPopups == 0)
797      {
798        MenuSelectionManager msm = MenuSelectionManager.defaultManager();
799        msm.removeChangeListener(keyboardHelper);
800      }
801
802  }
803
804  /**
805   * This method uninstalls the defaults and sets any objects created during
806   * install to null
807   */
808  protected void uninstallDefaults()
809  {
810    popupMenu.setBackground(null);
811    popupMenu.setBorder(null);
812    popupMenu.setFont(null);
813    popupMenu.setForeground(null);
814  }
815
816  /**
817   * Unregisters all the listeners that this UI delegate was using.
818   */
819  protected void uninstallListeners()
820  {
821    popupMenu.removePopupMenuListener(popupMenuListener);
822  }
823
824  /**
825   * Uninstalls any keyboard actions.
826   */
827  protected void uninstallKeyboardActions()
828  {
829    // We can't install the keyboard actions here, because then all
830    // popup menus would have their actions registered in the KeyboardManager.
831    // So we install it when the popup menu is opened, and uninstall it
832    // when it's closed. This is done in the KeyboardHelper class.
833    // Install InputMap.
834  }
835
836  /**
837   * Called by the KeyboardHandler when a popup is made invisible.
838   */
839  void uninstallKeyboardActionsImpl()
840  {
841    SwingUtilities.replaceUIInputMap(popupMenu,
842                                     JComponent.WHEN_IN_FOCUSED_WINDOW, null);
843    SwingUtilities.replaceUIActionMap(popupMenu, null);
844  }
845
846  /**
847   * This method returns the minimum size of the JPopupMenu.
848   *
849   * @param c The JComponent to find a size for.
850   *
851   * @return The minimum size.
852   */
853  public Dimension getMinimumSize(JComponent c)
854  {
855    return null;
856  }
857
858  /**
859   * This method returns the preferred size of the JPopupMenu.
860   *
861   * @param c The JComponent to find a size for.
862   *
863   * @return The preferred size.
864   */
865  public Dimension getPreferredSize(JComponent c)
866  {
867    return null;
868  }
869
870  /**
871   * This method returns the minimum size of the JPopupMenu.
872   *
873   * @param c The JComponent to find a size for.
874   *
875   * @return The minimum size.
876   */
877  public Dimension getMaximumSize(JComponent c)
878  {
879    return null;
880  }
881
882  /**
883   * Return true if given mouse event is a platform popup trigger, and false
884   * otherwise
885   *
886   * @param e MouseEvent that is to be checked for popup trigger event
887   *
888   * @return true if given mouse event is a platform popup trigger, and false
889   *         otherwise
890   */
891  public boolean isPopupTrigger(MouseEvent e)
892  {
893    return false;
894  }
895
896  /**
897   * This listener handles PopupMenuEvents fired by JPopupMenu
898   */
899  private class PopupMenuHandler implements PopupMenuListener
900  {
901    /**
902     * This method is invoked when JPopupMenu is cancelled.
903     *
904     * @param event the PopupMenuEvent
905     */
906    public void popupMenuCanceled(PopupMenuEvent event)
907    {
908      MenuSelectionManager manager = MenuSelectionManager.defaultManager();
909      manager.clearSelectedPath();
910    }
911
912    /**
913     * This method is invoked when JPopupMenu becomes invisible
914     *
915     * @param event the PopupMenuEvent
916     */
917    public void popupMenuWillBecomeInvisible(PopupMenuEvent event)
918    {
919      // remove listener that listens to component events fired 
920      // by the top - level window that this popup belongs to.
921      Component invoker = popupMenu.getInvoker();
922      Component rootContainer = SwingUtilities.getRoot(invoker);
923      if (rootContainer != null)
924        rootContainer.removeComponentListener(topWindowListener);
925    }
926
927    /**
928     * This method is invoked when JPopupMenu becomes visible
929     *
930     * @param event the PopupMenuEvent
931     */
932    public void popupMenuWillBecomeVisible(PopupMenuEvent event)
933    {
934      // Adds topWindowListener to top-level window to listener to 
935      // ComponentEvents fired by it. We need to cancel this popup menu
936      // if topWindow to which this popup belongs was resized or moved.
937      Component invoker = popupMenu.getInvoker();            
938      Component rootContainer = SwingUtilities.getRoot(invoker);
939      if (rootContainer != null)
940        rootContainer.addComponentListener(topWindowListener);
941
942      // if this popup menu is a free floating popup menu,
943      // then by default its first element should be always selected when
944      // this popup menu becomes visible. 
945      MenuSelectionManager manager = MenuSelectionManager.defaultManager();
946
947      if (manager.getSelectedPath().length == 0)
948        {
949          // Set selected path to point to the first item in the popup menu
950          MenuElement[] path = new MenuElement[2];
951          path[0] = popupMenu;
952          Component[] comps = popupMenu.getComponents();
953          if (comps.length != 0 && comps[0] instanceof MenuElement)
954            {
955              path[1] = (MenuElement) comps[0];
956              manager.setSelectedPath(path);
957            }
958        }
959    }
960  }
961
962  /**
963   * ComponentListener that listens to Component Events fired by the top -
964   * level window to which popup menu belongs. If top-level window was
965   * resized, moved or hidded then popup menu will be hidded and selected
966   * path of current menu hierarchy will be set to null.
967   */
968  private class TopWindowListener implements ComponentListener
969  {
970    /**
971     * This method is invoked when top-level window is resized. This method
972     * closes current menu hierarchy.
973     *
974     * @param e The ComponentEvent
975     */
976    public void componentResized(ComponentEvent e)
977    {
978      MenuSelectionManager manager = MenuSelectionManager.defaultManager();
979      manager.clearSelectedPath();
980    }
981
982    /**
983     * This method is invoked when top-level window is moved. This method
984     * closes current menu hierarchy.
985     *
986     * @param e The ComponentEvent
987     */
988    public void componentMoved(ComponentEvent e)
989    {
990      MenuSelectionManager manager = MenuSelectionManager.defaultManager();
991      manager.clearSelectedPath();
992    }
993
994    /**
995     * This method is invoked when top-level window is shown This method
996     * does nothing by default.
997     *
998     * @param e The ComponentEvent
999     */
1000    public void componentShown(ComponentEvent e)
1001    {
1002      MenuSelectionManager manager = MenuSelectionManager.defaultManager();
1003      manager.clearSelectedPath();
1004    }
1005
1006    /**
1007     * This method is invoked when top-level window is hidden This method
1008     * closes current menu hierarchy.
1009     *
1010     * @param e The ComponentEvent
1011     */
1012    public void componentHidden(ComponentEvent e)
1013    {
1014      MenuSelectionManager manager = MenuSelectionManager.defaultManager();
1015      manager.clearSelectedPath();
1016    }
1017  }
1018
1019}