1 /*
   2  * Copyright (c) 1997, 2009, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javax.swing.plaf.basic;
  27 
  28 import javax.swing.*;
  29 import javax.swing.event.*;
  30 import javax.swing.plaf.*;
  31 
  32 import java.applet.Applet;
  33 
  34 import java.awt.Component;
  35 import java.awt.KeyboardFocusManager;
  36 import java.awt.Window;
  37 import java.awt.event.*;
  38 import java.awt.AWTEvent;
  39 import java.awt.Toolkit;
  40 
  41 import java.util.*;
  42 
  43 import sun.swing.UIAction;
  44 
  45 import sun.awt.AppContext;
  46 
  47 /**
  48  * A Windows L&F implementation of PopupMenuUI.  This implementation
  49  * is a "combined" view/controller.
  50  *
  51  * @author Georges Saab
  52  * @author David Karlton
  53  * @author Arnaud Weber
  54  */
  55 public class BasicPopupMenuUI extends PopupMenuUI {
  56     static final Object MOUSE_GRABBER_KEY = new Object(); // javax.swing.plaf.basic.BasicPopupMenuUI.MouseGrabber
  57     static final Object MENU_KEYBOARD_HELPER_KEY = new Object(); // javax.swing.plaf.basic.BasicPopupMenuUI.MenuKeyboardHelper
  58 
  59     protected JPopupMenu popupMenu = null;
  60     private transient PopupMenuListener popupMenuListener = null;
  61     private MenuKeyListener menuKeyListener = null;
  62 
  63     private static boolean checkedUnpostPopup;
  64     private static boolean unpostPopup;
  65 
  66     public static ComponentUI createUI(JComponent x) {
  67         return new BasicPopupMenuUI();
  68     }
  69 
  70     public BasicPopupMenuUI() {
  71         BasicLookAndFeel.needsEventHelper = true;
  72         LookAndFeel laf = UIManager.getLookAndFeel();
  73         if (laf instanceof BasicLookAndFeel) {
  74             ((BasicLookAndFeel)laf).installAWTEventListener();
  75         }
  76     }
  77 
  78     public void installUI(JComponent c) {
  79         popupMenu = (JPopupMenu) c;
  80 
  81         installDefaults();
  82         installListeners();
  83         installKeyboardActions();
  84     }
  85 
  86     public void installDefaults() {
  87         if (popupMenu.getLayout() == null ||
  88             popupMenu.getLayout() instanceof UIResource)
  89             popupMenu.setLayout(new DefaultMenuLayout(popupMenu, BoxLayout.Y_AXIS));
  90 
  91         LookAndFeel.installProperty(popupMenu, "opaque", Boolean.TRUE);
  92         LookAndFeel.installBorder(popupMenu, "PopupMenu.border");
  93         LookAndFeel.installColorsAndFont(popupMenu,
  94                                          "PopupMenu.background",
  95                                          "PopupMenu.foreground",
  96                                          "PopupMenu.font");
  97     }
  98 
  99     protected void installListeners() {
 100         if (popupMenuListener == null) {
 101             popupMenuListener = new BasicPopupMenuListener();
 102         }
 103         popupMenu.addPopupMenuListener(popupMenuListener);
 104 
 105         if (menuKeyListener == null) {
 106             menuKeyListener = new BasicMenuKeyListener();
 107         }
 108         popupMenu.addMenuKeyListener(menuKeyListener);
 109 
 110         AppContext context = AppContext.getAppContext();
 111         synchronized (MOUSE_GRABBER_KEY) {
 112             MouseGrabber mouseGrabber = (MouseGrabber)context.get(
 113                                                      MOUSE_GRABBER_KEY);
 114             if (mouseGrabber == null) {
 115                 mouseGrabber = new MouseGrabber();
 116                 context.put(MOUSE_GRABBER_KEY, mouseGrabber);
 117             }
 118         }
 119         synchronized (MENU_KEYBOARD_HELPER_KEY) {
 120             MenuKeyboardHelper helper =
 121                     (MenuKeyboardHelper)context.get(MENU_KEYBOARD_HELPER_KEY);
 122             if (helper == null) {
 123                 helper = new MenuKeyboardHelper();
 124                 context.put(MENU_KEYBOARD_HELPER_KEY, helper);
 125                 MenuSelectionManager msm = MenuSelectionManager.defaultManager();
 126                 msm.addChangeListener(helper);
 127             }
 128         }
 129     }
 130 
 131     protected void installKeyboardActions() {
 132     }
 133 
 134     static InputMap getInputMap(JPopupMenu popup, JComponent c) {
 135         InputMap windowInputMap = null;
 136         Object[] bindings = (Object[])UIManager.get("PopupMenu.selectedWindowInputMapBindings");
 137         if (bindings != null) {
 138             windowInputMap = LookAndFeel.makeComponentInputMap(c, bindings);
 139             if (!popup.getComponentOrientation().isLeftToRight()) {
 140                 Object[] km = (Object[])UIManager.get("PopupMenu.selectedWindowInputMapBindings.RightToLeft");
 141                 if (km != null) {
 142                     InputMap rightToLeftInputMap = LookAndFeel.makeComponentInputMap(c, km);
 143                     rightToLeftInputMap.setParent(windowInputMap);
 144                     windowInputMap = rightToLeftInputMap;
 145                 }
 146             }
 147         }
 148         return windowInputMap;
 149     }
 150 
 151     static ActionMap getActionMap() {
 152         return LazyActionMap.getActionMap(BasicPopupMenuUI.class,
 153                                           "PopupMenu.actionMap");
 154     }
 155 
 156     static void loadActionMap(LazyActionMap map) {
 157         map.put(new Actions(Actions.CANCEL));
 158         map.put(new Actions(Actions.SELECT_NEXT));
 159         map.put(new Actions(Actions.SELECT_PREVIOUS));
 160         map.put(new Actions(Actions.SELECT_PARENT));
 161         map.put(new Actions(Actions.SELECT_CHILD));
 162         map.put(new Actions(Actions.RETURN));
 163         BasicLookAndFeel.installAudioActionMap(map);
 164     }
 165 
 166     public void uninstallUI(JComponent c) {
 167         uninstallDefaults();
 168         uninstallListeners();
 169         uninstallKeyboardActions();
 170 
 171         popupMenu = null;
 172     }
 173 
 174     protected void uninstallDefaults() {
 175         LookAndFeel.uninstallBorder(popupMenu);
 176     }
 177 
 178     protected void uninstallListeners() {
 179         if (popupMenuListener != null) {
 180             popupMenu.removePopupMenuListener(popupMenuListener);
 181         }
 182         if (menuKeyListener != null) {
 183             popupMenu.removeMenuKeyListener(menuKeyListener);
 184         }
 185     }
 186 
 187     protected void uninstallKeyboardActions() {
 188         SwingUtilities.replaceUIActionMap(popupMenu, null);
 189         SwingUtilities.replaceUIInputMap(popupMenu,
 190                                   JComponent.WHEN_IN_FOCUSED_WINDOW, null);
 191     }
 192 
 193     static MenuElement getFirstPopup() {
 194         MenuSelectionManager msm = MenuSelectionManager.defaultManager();
 195         MenuElement[] p = msm.getSelectedPath();
 196         MenuElement me = null;
 197 
 198         for(int i = 0 ; me == null && i < p.length ; i++) {
 199             if (p[i] instanceof JPopupMenu)
 200                 me = p[i];
 201         }
 202 
 203         return me;
 204     }
 205 
 206     static JPopupMenu getLastPopup() {
 207         MenuSelectionManager msm = MenuSelectionManager.defaultManager();
 208         MenuElement[] p = msm.getSelectedPath();
 209         JPopupMenu popup = null;
 210 
 211         for(int i = p.length - 1; popup == null && i >= 0; i--) {
 212             if (p[i] instanceof JPopupMenu)
 213                 popup = (JPopupMenu)p[i];
 214         }
 215         return popup;
 216     }
 217 
 218     static List getPopups() {
 219         MenuSelectionManager msm = MenuSelectionManager.defaultManager();
 220         MenuElement[] p = msm.getSelectedPath();
 221 
 222         List list = new ArrayList(p.length);
 223         for(int i = 0; i < p.length; i++) {
 224             if (p[i] instanceof JPopupMenu) {
 225                 list.add((JPopupMenu)p[i]);
 226             }
 227         }
 228         return list;
 229     }
 230 
 231     public boolean isPopupTrigger(MouseEvent e) {
 232         return ((e.getID()==MouseEvent.MOUSE_RELEASED)
 233                 && ((e.getModifiers() & MouseEvent.BUTTON3_MASK)!=0));
 234     }
 235 
 236     private static boolean checkInvokerEqual(MenuElement present, MenuElement last) {
 237         Component invokerPresent = present.getComponent();
 238         Component invokerLast = last.getComponent();
 239 
 240         if (invokerPresent instanceof JPopupMenu) {
 241             invokerPresent = ((JPopupMenu)invokerPresent).getInvoker();
 242     }
 243         if (invokerLast instanceof JPopupMenu) {
 244             invokerLast = ((JPopupMenu)invokerLast).getInvoker();
 245         }
 246         return (invokerPresent == invokerLast);
 247     }
 248 
 249 
 250     /**
 251      * This Listener fires the Action that provides the correct auditory
 252      * feedback.
 253      *
 254      * @since 1.4
 255      */
 256     private class BasicPopupMenuListener implements PopupMenuListener {
 257         public void popupMenuCanceled(PopupMenuEvent e) {
 258         }
 259 
 260         public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
 261         }
 262 
 263         public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
 264             BasicLookAndFeel.playSound((JPopupMenu)e.getSource(),
 265                                        "PopupMenu.popupSound");
 266         }
 267     }
 268 
 269     /**
 270      * Handles mnemonic for children JMenuItems.
 271      * @since 1.5
 272      */
 273     private class BasicMenuKeyListener implements MenuKeyListener {
 274         MenuElement menuToOpen = null;
 275 
 276         public void menuKeyTyped(MenuKeyEvent e) {
 277             if (menuToOpen != null) {
 278                 // we have a submenu to open
 279                 JPopupMenu subpopup = ((JMenu)menuToOpen).getPopupMenu();
 280                 MenuElement subitem = findEnabledChild(
 281                         subpopup.getSubElements(), -1, true);
 282 
 283                 ArrayList lst = new ArrayList(Arrays.asList(e.getPath()));
 284                 lst.add(menuToOpen);
 285                 lst.add(subpopup);
 286                 if (subitem != null) {
 287                     lst.add(subitem);
 288                 }
 289                 MenuElement newPath[] = new MenuElement[0];;
 290                 newPath = (MenuElement[])lst.toArray(newPath);
 291                 MenuSelectionManager.defaultManager().setSelectedPath(newPath);
 292                 e.consume();
 293             }
 294             menuToOpen = null;
 295         }
 296 
 297         public void menuKeyPressed(MenuKeyEvent e) {
 298             char keyChar = e.getKeyChar();
 299 
 300             // Handle the case for Escape or Enter...
 301             if (!Character.isLetterOrDigit(keyChar)) {
 302                 return;
 303             }
 304 
 305             MenuSelectionManager manager = e.getMenuSelectionManager();
 306             MenuElement path[] = e.getPath();
 307             MenuElement items[] = popupMenu.getSubElements();
 308             int currentIndex = -1;
 309             int matches = 0;
 310             int firstMatch = -1;
 311             int indexes[] = null;
 312 
 313             for (int j = 0; j < items.length; j++) {
 314                 if (! (items[j] instanceof JMenuItem)) {
 315                     continue;
 316                 }
 317                 JMenuItem item = (JMenuItem)items[j];
 318                 int mnemonic = item.getMnemonic();
 319                 if (item.isEnabled() &&
 320                     item.isVisible() && lower(keyChar) == lower(mnemonic)) {
 321                     if (matches == 0) {
 322                         firstMatch = j;
 323                         matches++;
 324                     } else {
 325                         if (indexes == null) {
 326                             indexes = new int[items.length];
 327                             indexes[0] = firstMatch;
 328                         }
 329                         indexes[matches++] = j;
 330                     }
 331                 }
 332                 if (item.isArmed()) {
 333                     currentIndex = matches - 1;
 334                 }
 335             }
 336 
 337             if (matches == 0) {
 338                 ; // no op
 339             } else if (matches == 1) {
 340                 // Invoke the menu action
 341                 JMenuItem item = (JMenuItem)items[firstMatch];
 342                 if (item instanceof JMenu) {
 343                     // submenus are handled in menuKeyTyped
 344                     menuToOpen = item;
 345                 } else if (item.isEnabled()) {
 346                     // we have a menu item
 347                     manager.clearSelectedPath();
 348                     item.doClick();
 349                 }
 350                 e.consume();
 351             } else {
 352                 // Select the menu item with the matching mnemonic. If
 353                 // the same mnemonic has been invoked then select the next
 354                 // menu item in the cycle.
 355                 MenuElement newItem = null;
 356 
 357                 newItem = items[indexes[(currentIndex + 1) % matches]];
 358 
 359                 MenuElement newPath[] = new MenuElement[path.length+1];
 360                 System.arraycopy(path, 0, newPath, 0, path.length);
 361                 newPath[path.length] = newItem;
 362                 manager.setSelectedPath(newPath);
 363                 e.consume();
 364             }
 365             return;
 366         }
 367 
 368         public void menuKeyReleased(MenuKeyEvent e) {
 369         }
 370 
 371         private char lower(char keyChar) {
 372             return Character.toLowerCase(keyChar);
 373         }
 374 
 375         private char lower(int mnemonic) {
 376             return Character.toLowerCase((char) mnemonic);
 377         }
 378     }
 379 
 380     private static class Actions extends UIAction {
 381         // Types of actions
 382         private static final String CANCEL = "cancel";
 383         private static final String SELECT_NEXT = "selectNext";
 384         private static final String SELECT_PREVIOUS = "selectPrevious";
 385         private static final String SELECT_PARENT = "selectParent";
 386         private static final String SELECT_CHILD = "selectChild";
 387         private static final String RETURN = "return";
 388 
 389         // Used for next/previous actions
 390         private static final boolean FORWARD = true;
 391         private static final boolean BACKWARD = false;
 392 
 393         // Used for parent/child actions
 394         private static final boolean PARENT = false;
 395         private static final boolean CHILD = true;
 396 
 397 
 398         Actions(String key) {
 399             super(key);
 400         }
 401 
 402         public void actionPerformed(ActionEvent e) {
 403             String key = getName();
 404             if (key == CANCEL) {
 405                 cancel();
 406             }
 407             else if (key == SELECT_NEXT) {
 408                 selectItem(FORWARD);
 409             }
 410             else if (key == SELECT_PREVIOUS) {
 411                 selectItem(BACKWARD);
 412             }
 413             else if (key == SELECT_PARENT) {
 414                 selectParentChild(PARENT);
 415             }
 416             else if (key == SELECT_CHILD) {
 417                 selectParentChild(CHILD);
 418             }
 419             else if (key == RETURN) {
 420                 doReturn();
 421             }
 422         }
 423 
 424         private void doReturn() {
 425             KeyboardFocusManager fmgr =
 426                 KeyboardFocusManager.getCurrentKeyboardFocusManager();
 427             Component focusOwner = fmgr.getFocusOwner();
 428             if(focusOwner != null && !(focusOwner instanceof JRootPane)) {
 429                 return;
 430             }
 431 
 432             MenuSelectionManager msm = MenuSelectionManager.defaultManager();
 433             MenuElement path[] = msm.getSelectedPath();
 434             MenuElement lastElement;
 435             if(path.length > 0) {
 436                 lastElement = path[path.length-1];
 437                 if(lastElement instanceof JMenu) {
 438                     MenuElement newPath[] = new MenuElement[path.length+1];
 439                     System.arraycopy(path,0,newPath,0,path.length);
 440                     newPath[path.length] = ((JMenu)lastElement).getPopupMenu();
 441                     msm.setSelectedPath(newPath);
 442                 } else if(lastElement instanceof JMenuItem) {
 443                     JMenuItem mi = (JMenuItem)lastElement;
 444 
 445                     if (mi.getUI() instanceof BasicMenuItemUI) {
 446                         ((BasicMenuItemUI)mi.getUI()).doClick(msm);
 447                     }
 448                     else {
 449                         msm.clearSelectedPath();
 450                         mi.doClick(0);
 451                     }
 452                 }
 453             }
 454         }
 455         private void selectParentChild(boolean direction) {
 456             MenuSelectionManager msm = MenuSelectionManager.defaultManager();
 457             MenuElement path[] = msm.getSelectedPath();
 458             int len = path.length;
 459 
 460             if (direction == PARENT) {
 461                 // selecting parent
 462                 int popupIndex = len-1;
 463 
 464                 if (len > 2 &&
 465                     // check if we have an open submenu. A submenu item may or
 466                     // may not be selected, so submenu popup can be either the
 467                     // last or next to the last item.
 468                     (path[popupIndex] instanceof JPopupMenu ||
 469                      path[--popupIndex] instanceof JPopupMenu) &&
 470                     !((JMenu)path[popupIndex-1]).isTopLevelMenu()) {
 471 
 472                     // we have a submenu, just close it
 473                     MenuElement newPath[] = new MenuElement[popupIndex];
 474                     System.arraycopy(path, 0, newPath, 0, popupIndex);
 475                     msm.setSelectedPath(newPath);
 476                     return;
 477                 }
 478             } else {
 479                 // selecting child
 480                 if (len > 0 && path[len-1] instanceof JMenu &&
 481                     !((JMenu)path[len-1]).isTopLevelMenu()) {
 482 
 483                     // we have a submenu, open it
 484                     JMenu menu = (JMenu)path[len-1];
 485                     JPopupMenu popup = menu.getPopupMenu();
 486                     MenuElement[] subs = popup.getSubElements();
 487                     MenuElement item = findEnabledChild(subs, -1, true);
 488                     MenuElement[] newPath;
 489 
 490                     if (item == null) {
 491                         newPath = new MenuElement[len+1];
 492                     } else {
 493                         newPath = new MenuElement[len+2];
 494                         newPath[len+1] = item;
 495                     }
 496                     System.arraycopy(path, 0, newPath, 0, len);
 497                     newPath[len] = popup;
 498                     msm.setSelectedPath(newPath);
 499                     return;
 500                 }
 501             }
 502 
 503             // check if we have a toplevel menu selected.
 504             // If this is the case, we select another toplevel menu
 505             if (len > 1 && path[0] instanceof JMenuBar) {
 506                 MenuElement currentMenu = path[1];
 507                 MenuElement nextMenu = findEnabledChild(
 508                     path[0].getSubElements(), currentMenu, direction);
 509 
 510                 if (nextMenu != null && nextMenu != currentMenu) {
 511                     MenuElement newSelection[];
 512                     if (len == 2) {
 513                         // menu is selected but its popup not shown
 514                         newSelection = new MenuElement[2];
 515                         newSelection[0] = path[0];
 516                         newSelection[1] = nextMenu;
 517                     } else {
 518                         // menu is selected and its popup is shown
 519                         newSelection = new MenuElement[3];
 520                         newSelection[0] = path[0];
 521                         newSelection[1] = nextMenu;
 522                         newSelection[2] = ((JMenu)nextMenu).getPopupMenu();
 523                     }
 524                     msm.setSelectedPath(newSelection);
 525                 }
 526             }
 527         }
 528 
 529         private void selectItem(boolean direction) {
 530             MenuSelectionManager msm = MenuSelectionManager.defaultManager();
 531             MenuElement path[] = msm.getSelectedPath();
 532             if (path.length == 0) {
 533                 return;
 534             }
 535             int len = path.length;
 536             if (len == 1 && path[0] instanceof JPopupMenu) {
 537 
 538                 JPopupMenu popup = (JPopupMenu) path[0];
 539                 MenuElement[] newPath = new MenuElement[2];
 540                 newPath[0] = popup;
 541                 newPath[1] = findEnabledChild(popup.getSubElements(), -1, direction);
 542                 msm.setSelectedPath(newPath);
 543             } else if (len == 2 &&
 544                     path[0] instanceof JMenuBar && path[1] instanceof JMenu) {
 545 
 546                 // a toplevel menu is selected, but its popup not shown.
 547                 // Show the popup and select the first item
 548                 JPopupMenu popup = ((JMenu)path[1]).getPopupMenu();
 549                 MenuElement next =
 550                     findEnabledChild(popup.getSubElements(), -1, FORWARD);
 551                 MenuElement[] newPath;
 552 
 553                 if (next != null) {
 554                     // an enabled item found -- include it in newPath
 555                     newPath = new MenuElement[4];
 556                     newPath[3] = next;
 557                 } else {
 558                     // menu has no enabled items -- still must show the popup
 559                     newPath = new MenuElement[3];
 560                 }
 561                 System.arraycopy(path, 0, newPath, 0, 2);
 562                 newPath[2] = popup;
 563                 msm.setSelectedPath(newPath);
 564 
 565             } else if (path[len-1] instanceof JPopupMenu &&
 566                        path[len-2] instanceof JMenu) {
 567 
 568                 // a menu (not necessarily toplevel) is open and its popup
 569                 // shown. Select the appropriate menu item
 570                 JMenu menu = (JMenu)path[len-2];
 571                 JPopupMenu popup = menu.getPopupMenu();
 572                 MenuElement next =
 573                     findEnabledChild(popup.getSubElements(), -1, direction);
 574 
 575                 if (next != null) {
 576                     MenuElement[] newPath = new MenuElement[len+1];
 577                     System.arraycopy(path, 0, newPath, 0, len);
 578                     newPath[len] = next;
 579                     msm.setSelectedPath(newPath);
 580                 } else {
 581                     // all items in the popup are disabled.
 582                     // We're going to find the parent popup menu and select
 583                     // its next item. If there's no parent popup menu (i.e.
 584                     // current menu is toplevel), do nothing
 585                     if (len > 2 && path[len-3] instanceof JPopupMenu) {
 586                         popup = ((JPopupMenu)path[len-3]);
 587                         next = findEnabledChild(popup.getSubElements(),
 588                                                 menu, direction);
 589 
 590                         if (next != null && next != menu) {
 591                             MenuElement[] newPath = new MenuElement[len-1];
 592                             System.arraycopy(path, 0, newPath, 0, len-2);
 593                             newPath[len-2] = next;
 594                             msm.setSelectedPath(newPath);
 595                         }
 596                     }
 597                 }
 598 
 599             } else {
 600                 // just select the next item, no path expansion needed
 601                 MenuElement subs[] = path[len-2].getSubElements();
 602                 MenuElement nextChild =
 603                     findEnabledChild(subs, path[len-1], direction);
 604                 if (nextChild == null) {
 605                     nextChild = findEnabledChild(subs, -1, direction);
 606                 }
 607                 if (nextChild != null) {
 608                     path[len-1] = nextChild;
 609                     msm.setSelectedPath(path);
 610                 }
 611             }
 612         }
 613 
 614         private void cancel() {
 615             // 4234793: This action should call JPopupMenu.firePopupMenuCanceled but it's
 616             // a protected method. The real solution could be to make
 617             // firePopupMenuCanceled public and call it directly.
 618             JPopupMenu lastPopup = (JPopupMenu)getLastPopup();
 619             if (lastPopup != null) {
 620                 lastPopup.putClientProperty("JPopupMenu.firePopupMenuCanceled", Boolean.TRUE);
 621             }
 622             String mode = UIManager.getString("Menu.cancelMode");
 623             if ("hideMenuTree".equals(mode)) {
 624                 MenuSelectionManager.defaultManager().clearSelectedPath();
 625             } else {
 626                 shortenSelectedPath();
 627             }
 628         }
 629 
 630         private void shortenSelectedPath() {
 631             MenuElement path[] = MenuSelectionManager.defaultManager().getSelectedPath();
 632             if (path.length <= 2) {
 633                 MenuSelectionManager.defaultManager().clearSelectedPath();
 634                 return;
 635             }
 636             // unselect MenuItem and its Popup by default
 637             int value = 2;
 638             MenuElement lastElement = path[path.length - 1];
 639             JPopupMenu lastPopup = getLastPopup();
 640             if (lastElement == lastPopup) {
 641                 MenuElement previousElement = path[path.length - 2];
 642                 if (previousElement instanceof JMenu) {
 643                     JMenu lastMenu = (JMenu) previousElement;
 644                     if (lastMenu.isEnabled() && lastPopup.getComponentCount() > 0) {
 645                         // unselect the last visible popup only
 646                         value = 1;
 647                     } else {
 648                         // unselect invisible popup and two visible elements
 649                         value = 3;
 650                     }
 651                 }
 652             }
 653             if (path.length - value <= 2
 654                     && !UIManager.getBoolean("Menu.preserveTopLevelSelection")) {
 655                 // clear selection for the topLevelMenu
 656                 value = path.length;
 657             }
 658             MenuElement newPath[] = new MenuElement[path.length - value];
 659             System.arraycopy(path, 0, newPath, 0, path.length - value);
 660             MenuSelectionManager.defaultManager().setSelectedPath(newPath);
 661         }
 662     }
 663 
 664     private static MenuElement nextEnabledChild(MenuElement e[],
 665                                                 int fromIndex, int toIndex) {
 666         for (int i=fromIndex; i<=toIndex; i++) {
 667             if (e[i] != null) {
 668                 Component comp = e[i].getComponent();
 669                 if ( comp != null
 670                         && (comp.isEnabled() || UIManager.getBoolean("MenuItem.disabledAreNavigable"))
 671                         && comp.isVisible()) {
 672                     return e[i];
 673                 }
 674             }
 675         }
 676         return null;
 677     }
 678 
 679     private static MenuElement previousEnabledChild(MenuElement e[],
 680                                                 int fromIndex, int toIndex) {
 681         for (int i=fromIndex; i>=toIndex; i--) {
 682             if (e[i] != null) {
 683                 Component comp = e[i].getComponent();
 684                 if ( comp != null
 685                         && (comp.isEnabled() || UIManager.getBoolean("MenuItem.disabledAreNavigable"))
 686                         && comp.isVisible()) {
 687                     return e[i];
 688                 }
 689             }
 690         }
 691         return null;
 692     }
 693 
 694     static MenuElement findEnabledChild(MenuElement e[], int fromIndex,
 695                                                 boolean forward) {
 696         MenuElement result = null;
 697         if (forward) {
 698             result = nextEnabledChild(e, fromIndex+1, e.length-1);
 699             if (result == null) result = nextEnabledChild(e, 0, fromIndex-1);
 700         } else {
 701             result = previousEnabledChild(e, fromIndex-1, 0);
 702             if (result == null) result = previousEnabledChild(e, e.length-1,
 703                                                               fromIndex+1);
 704         }
 705         return result;
 706     }
 707 
 708     static MenuElement findEnabledChild(MenuElement e[],
 709                                    MenuElement elem, boolean forward) {
 710         for (int i=0; i<e.length; i++) {
 711             if (e[i] == elem) {
 712                 return findEnabledChild(e, i, forward);
 713             }
 714         }
 715         return null;
 716     }
 717 
 718     static class MouseGrabber implements ChangeListener,
 719         AWTEventListener, ComponentListener, WindowListener {
 720 
 721         Window grabbedWindow;
 722         MenuElement[] lastPathSelected;
 723 
 724         public MouseGrabber() {
 725             MenuSelectionManager msm = MenuSelectionManager.defaultManager();
 726             msm.addChangeListener(this);
 727             this.lastPathSelected = msm.getSelectedPath();
 728             if(this.lastPathSelected.length != 0) {
 729                 grabWindow(this.lastPathSelected);
 730             }
 731         }
 732 
 733         void uninstall() {
 734             synchronized (MOUSE_GRABBER_KEY) {
 735                 MenuSelectionManager.defaultManager().removeChangeListener(this);
 736                 ungrabWindow();
 737                 AppContext.getAppContext().remove(MOUSE_GRABBER_KEY);
 738             }
 739         }
 740 
 741         void grabWindow(MenuElement[] newPath) {
 742             // A grab needs to be added
 743             final Toolkit tk = Toolkit.getDefaultToolkit();
 744             java.security.AccessController.doPrivileged(
 745                 new java.security.PrivilegedAction() {
 746                     public Object run() {
 747                         tk.addAWTEventListener(MouseGrabber.this,
 748                                 AWTEvent.MOUSE_EVENT_MASK |
 749                                 AWTEvent.MOUSE_MOTION_EVENT_MASK |
 750                                 AWTEvent.MOUSE_WHEEL_EVENT_MASK |
 751                                 AWTEvent.WINDOW_EVENT_MASK | sun.awt.SunToolkit.GRAB_EVENT_MASK);
 752                         return null;
 753                     }
 754                 }
 755             );
 756 
 757             Component invoker = newPath[0].getComponent();
 758             if (invoker instanceof JPopupMenu) {
 759                 invoker = ((JPopupMenu)invoker).getInvoker();
 760             }
 761             grabbedWindow = invoker instanceof Window?
 762                     (Window)invoker :
 763                     SwingUtilities.getWindowAncestor(invoker);
 764             if(grabbedWindow != null) {
 765                 if(tk instanceof sun.awt.SunToolkit) {
 766                     ((sun.awt.SunToolkit)tk).grab(grabbedWindow);
 767                 } else {
 768                     grabbedWindow.addComponentListener(this);
 769                     grabbedWindow.addWindowListener(this);
 770                 }
 771             }
 772         }
 773 
 774         void ungrabWindow() {
 775             final Toolkit tk = Toolkit.getDefaultToolkit();
 776             // The grab should be removed
 777              java.security.AccessController.doPrivileged(
 778                 new java.security.PrivilegedAction() {
 779                     public Object run() {
 780                         tk.removeAWTEventListener(MouseGrabber.this);
 781                         return null;
 782                     }
 783                 }
 784             );
 785             realUngrabWindow();
 786         }
 787 
 788         void realUngrabWindow() {
 789             Toolkit tk = Toolkit.getDefaultToolkit();
 790             if(grabbedWindow != null) {
 791                 if(tk instanceof sun.awt.SunToolkit) {
 792                     ((sun.awt.SunToolkit)tk).ungrab(grabbedWindow);
 793                 } else {
 794                     grabbedWindow.removeComponentListener(this);
 795                     grabbedWindow.removeWindowListener(this);
 796                 }
 797                 grabbedWindow = null;
 798             }
 799         }
 800 
 801         public void stateChanged(ChangeEvent e) {
 802             MenuSelectionManager msm = MenuSelectionManager.defaultManager();
 803             MenuElement[] p = msm.getSelectedPath();
 804 
 805             if (lastPathSelected.length == 0 && p.length != 0) {
 806                 grabWindow(p);
 807             }
 808 
 809             if (lastPathSelected.length != 0 && p.length == 0) {
 810                 ungrabWindow();
 811             }
 812 
 813             lastPathSelected = p;
 814         }
 815 
 816         public void eventDispatched(AWTEvent ev) {
 817             if(ev instanceof sun.awt.UngrabEvent) {
 818                 // Popup should be canceled in case of ungrab event
 819                 cancelPopupMenu( );
 820                 return;
 821             }
 822             if (!(ev instanceof MouseEvent)) {
 823                 // We are interested in MouseEvents only
 824                 return;
 825             }
 826             MouseEvent me = (MouseEvent) ev;
 827             Component src = me.getComponent();
 828             switch (me.getID()) {
 829             case MouseEvent.MOUSE_PRESSED:
 830                 if (isInPopup(src) ||
 831                     (src instanceof JMenu && ((JMenu)src).isSelected())) {
 832                     return;
 833                 }
 834                 if (!(src instanceof JComponent) ||
 835                    ! (((JComponent)src).getClientProperty("doNotCancelPopup")
 836                          == BasicComboBoxUI.HIDE_POPUP_KEY)) {
 837                     // Cancel popup only if this property was not set.
 838                     // If this property is set to TRUE component wants
 839                     // to deal with this event by himself.
 840                     cancelPopupMenu();
 841                     // Ask UIManager about should we consume event that closes
 842                     // popup. This made to match native apps behaviour.
 843                     boolean consumeEvent =
 844                         UIManager.getBoolean("PopupMenu.consumeEventOnClose");
 845                     // Consume the event so that normal processing stops.
 846                     if(consumeEvent && !(src instanceof MenuElement)) {
 847                         me.consume();
 848                     }
 849                 }
 850                 break;
 851 
 852             case MouseEvent.MOUSE_RELEASED:
 853                 if(!(src instanceof MenuElement)) {
 854                     // Do not forward event to MSM, let component handle it
 855                     if (isInPopup(src)) {
 856                         break;
 857                     }
 858                 }
 859                 if(src instanceof JMenu || !(src instanceof JMenuItem)) {
 860                     MenuSelectionManager.defaultManager().
 861                         processMouseEvent(me);
 862                 }
 863                 break;
 864             case MouseEvent.MOUSE_DRAGGED:
 865                 if(!(src instanceof MenuElement)) {
 866                     // For the MOUSE_DRAGGED event the src is
 867                     // the Component in which mouse button was pressed.
 868                     // If the src is in popupMenu,
 869                     // do not forward event to MSM, let component handle it.
 870                     if (isInPopup(src)) {
 871                         break;
 872                     }
 873                 }
 874                 MenuSelectionManager.defaultManager().
 875                     processMouseEvent(me);
 876                 break;
 877             case MouseEvent.MOUSE_WHEEL:
 878                 if (isInPopup(src)) {
 879                     return;
 880                 }
 881                 cancelPopupMenu();
 882                 break;
 883             }
 884         }
 885 
 886         boolean isInPopup(Component src) {
 887             for (Component c=src; c!=null; c=c.getParent()) {
 888                 if (c instanceof Applet || c instanceof Window) {
 889                     break;
 890                 } else if (c instanceof JPopupMenu) {
 891                     return true;
 892                 }
 893             }
 894             return false;
 895         }
 896 
 897         void cancelPopupMenu() {
 898             // We should ungrab window if a user code throws
 899             // an unexpected runtime exception. See 6495920.
 900             try {
 901                 // 4234793: This action should call firePopupMenuCanceled but it's
 902                 // a protected method. The real solution could be to make
 903                 // firePopupMenuCanceled public and call it directly.
 904                 List popups = getPopups();
 905                 Iterator iter = popups.iterator();
 906                 while (iter.hasNext()) {
 907                     JPopupMenu popup = (JPopupMenu) iter.next();
 908                     popup.putClientProperty("JPopupMenu.firePopupMenuCanceled", Boolean.TRUE);
 909                 }
 910                 MenuSelectionManager.defaultManager().clearSelectedPath();
 911             } catch (RuntimeException ex) {
 912                 realUngrabWindow();
 913                 throw ex;
 914             } catch (Error err) {
 915                 realUngrabWindow();
 916                 throw err;
 917             }
 918         }
 919 
 920         public void componentResized(ComponentEvent e) {
 921             cancelPopupMenu();
 922         }
 923         public void componentMoved(ComponentEvent e) {
 924             cancelPopupMenu();
 925         }
 926         public void componentShown(ComponentEvent e) {
 927             cancelPopupMenu();
 928         }
 929         public void componentHidden(ComponentEvent e) {
 930             cancelPopupMenu();
 931         }
 932         public void windowClosing(WindowEvent e) {
 933             cancelPopupMenu();
 934         }
 935         public void windowClosed(WindowEvent e) {
 936             cancelPopupMenu();
 937         }
 938         public void windowIconified(WindowEvent e) {
 939             cancelPopupMenu();
 940         }
 941         public void windowDeactivated(WindowEvent e) {
 942             cancelPopupMenu();
 943         }
 944         public void windowOpened(WindowEvent e) {}
 945         public void windowDeiconified(WindowEvent e) {}
 946         public void windowActivated(WindowEvent e) {}
 947     }
 948 
 949     /**
 950      * This helper is added to MenuSelectionManager as a ChangeListener to
 951      * listen to menu selection changes. When a menu is activated, it passes
 952      * focus to its parent JRootPane, and installs an ActionMap/InputMap pair
 953      * on that JRootPane. Those maps are necessary in order for menu
 954      * navigation to work. When menu is being deactivated, it restores focus
 955      * to the component that has had it before menu activation, and uninstalls
 956      * the maps.
 957      * This helper is also installed as a KeyListener on root pane when menu
 958      * is active. It forwards key events to MenuSelectionManager for mnemonic
 959      * keys handling.
 960      */
 961     static class MenuKeyboardHelper
 962         implements ChangeListener, KeyListener {
 963 
 964         private Component lastFocused = null;
 965         private MenuElement[] lastPathSelected = new MenuElement[0];
 966         private JPopupMenu lastPopup;
 967 
 968         private JRootPane invokerRootPane;
 969         private ActionMap menuActionMap = getActionMap();
 970         private InputMap menuInputMap;
 971         private boolean focusTraversalKeysEnabled;
 972 
 973         /*
 974          * Fix for 4213634
 975          * If this is false, KEY_TYPED and KEY_RELEASED events are NOT
 976          * processed. This is needed to avoid activating a menuitem when
 977          * the menu and menuitem share the same mnemonic.
 978          */
 979         private boolean receivedKeyPressed = false;
 980 
 981         void removeItems() {
 982             if (lastFocused != null) {
 983                 if(!lastFocused.requestFocusInWindow()) {
 984                     // Workarounr for 4810575.
 985                     // If lastFocused is not in currently focused window
 986                     // requestFocusInWindow will fail. In this case we must
 987                     // request focus by requestFocus() if it was not
 988                     // transferred from our popup.
 989                     Window cfw = KeyboardFocusManager
 990                                  .getCurrentKeyboardFocusManager()
 991                                   .getFocusedWindow();
 992                     if(cfw != null &&
 993                        "###focusableSwingPopup###".equals(cfw.getName())) {
 994                         lastFocused.requestFocus();
 995                     }
 996 
 997                 }
 998                 lastFocused = null;
 999             }
1000             if (invokerRootPane != null) {
1001                 invokerRootPane.removeKeyListener(this);
1002                 invokerRootPane.setFocusTraversalKeysEnabled(focusTraversalKeysEnabled);
1003                 removeUIInputMap(invokerRootPane, menuInputMap);
1004                 removeUIActionMap(invokerRootPane, menuActionMap);
1005                 invokerRootPane = null;
1006             }
1007             receivedKeyPressed = false;
1008         }
1009 
1010         private FocusListener rootPaneFocusListener = new FocusAdapter() {
1011                 public void focusGained(FocusEvent ev) {
1012                     Component opposite = ev.getOppositeComponent();
1013                     if (opposite != null) {
1014                         lastFocused = opposite;
1015                     }
1016                     ev.getComponent().removeFocusListener(this);
1017                 }
1018             };
1019 
1020         /**
1021          * Return the last JPopupMenu in <code>path</code>,
1022          * or <code>null</code> if none found
1023          */
1024         JPopupMenu getActivePopup(MenuElement[] path) {
1025             for (int i=path.length-1; i>=0; i--) {
1026                 MenuElement elem = path[i];
1027                 if (elem instanceof JPopupMenu) {
1028                     return (JPopupMenu)elem;
1029                 }
1030             }
1031             return null;
1032         }
1033 
1034         void addUIInputMap(JComponent c, InputMap map) {
1035             InputMap lastNonUI = null;
1036             InputMap parent = c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
1037 
1038             while (parent != null && !(parent instanceof UIResource)) {
1039                 lastNonUI = parent;
1040                 parent = parent.getParent();
1041             }
1042 
1043             if (lastNonUI == null) {
1044                 c.setInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW, map);
1045             } else {
1046                 lastNonUI.setParent(map);
1047             }
1048             map.setParent(parent);
1049         }
1050 
1051         void addUIActionMap(JComponent c, ActionMap map) {
1052             ActionMap lastNonUI = null;
1053             ActionMap parent = c.getActionMap();
1054 
1055             while (parent != null && !(parent instanceof UIResource)) {
1056                 lastNonUI = parent;
1057                 parent = parent.getParent();
1058             }
1059 
1060             if (lastNonUI == null) {
1061                 c.setActionMap(map);
1062             } else {
1063                 lastNonUI.setParent(map);
1064             }
1065             map.setParent(parent);
1066         }
1067 
1068         void removeUIInputMap(JComponent c, InputMap map) {
1069             InputMap im = null;
1070             InputMap parent = c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
1071 
1072             while (parent != null) {
1073                 if (parent == map) {
1074                     if (im == null) {
1075                         c.setInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW,
1076                                       map.getParent());
1077                     } else {
1078                         im.setParent(map.getParent());
1079                     }
1080                     break;
1081                 }
1082                 im = parent;
1083                 parent = parent.getParent();
1084             }
1085         }
1086 
1087         void removeUIActionMap(JComponent c, ActionMap map) {
1088             ActionMap im = null;
1089             ActionMap parent = c.getActionMap();
1090 
1091             while (parent != null) {
1092                 if (parent == map) {
1093                     if (im == null) {
1094                         c.setActionMap(map.getParent());
1095                     } else {
1096                         im.setParent(map.getParent());
1097                     }
1098                     break;
1099                 }
1100                 im = parent;
1101                 parent = parent.getParent();
1102             }
1103         }
1104 
1105         public void stateChanged(ChangeEvent ev) {
1106             if (!(UIManager.getLookAndFeel() instanceof BasicLookAndFeel)) {
1107                 uninstall();
1108                 return;
1109             }
1110             MenuSelectionManager msm = (MenuSelectionManager)ev.getSource();
1111             MenuElement[] p = msm.getSelectedPath();
1112             JPopupMenu popup = getActivePopup(p);
1113             if (popup != null && !popup.isFocusable()) {
1114                 // Do nothing for non-focusable popups
1115                 return;
1116             }
1117 
1118             if (lastPathSelected.length != 0 && p.length != 0 ) {
1119                 if (!checkInvokerEqual(p[0],lastPathSelected[0])) {
1120                     removeItems();
1121                     lastPathSelected = new MenuElement[0];
1122                 }
1123             }
1124 
1125             if (lastPathSelected.length == 0 && p.length > 0) {
1126                 // menu posted
1127                 JComponent invoker;
1128 
1129                 if (popup == null) {
1130                     if (p.length == 2 && p[0] instanceof JMenuBar &&
1131                         p[1] instanceof JMenu) {
1132                         // a menu has been selected but not open
1133                         invoker = (JComponent)p[1];
1134                         popup = ((JMenu)invoker).getPopupMenu();
1135                     } else {
1136                         return;
1137                     }
1138                 } else {
1139                     Component c = popup.getInvoker();
1140                     if(c instanceof JFrame) {
1141                         invoker = ((JFrame)c).getRootPane();
1142                     } else if(c instanceof JDialog) {
1143                         invoker = ((JDialog)c).getRootPane();
1144                     } else if(c instanceof JApplet) {
1145                         invoker = ((JApplet)c).getRootPane();
1146                     } else {
1147                         while (!(c instanceof JComponent)) {
1148                             if (c == null) {
1149                                 return;
1150                             }
1151                             c = c.getParent();
1152                         }
1153                         invoker = (JComponent)c;
1154                     }
1155                 }
1156 
1157                 // remember current focus owner
1158                 lastFocused = KeyboardFocusManager.
1159                     getCurrentKeyboardFocusManager().getFocusOwner();
1160 
1161                 // request focus on root pane and install keybindings
1162                 // used for menu navigation
1163                 invokerRootPane = SwingUtilities.getRootPane(invoker);
1164                 if (invokerRootPane != null) {
1165                     invokerRootPane.addFocusListener(rootPaneFocusListener);
1166                     invokerRootPane.requestFocus(true);
1167                     invokerRootPane.addKeyListener(this);
1168                     focusTraversalKeysEnabled = invokerRootPane.
1169                                       getFocusTraversalKeysEnabled();
1170                     invokerRootPane.setFocusTraversalKeysEnabled(false);
1171 
1172                     menuInputMap = getInputMap(popup, invokerRootPane);
1173                     addUIInputMap(invokerRootPane, menuInputMap);
1174                     addUIActionMap(invokerRootPane, menuActionMap);
1175                 }
1176             } else if (lastPathSelected.length != 0 && p.length == 0) {
1177                 // menu hidden -- return focus to where it had been before
1178                 // and uninstall menu keybindings
1179                    removeItems();
1180             } else {
1181                 if (popup != lastPopup) {
1182                     receivedKeyPressed = false;
1183                 }
1184             }
1185 
1186             // Remember the last path selected
1187             lastPathSelected = p;
1188             lastPopup = popup;
1189         }
1190 
1191         public void keyPressed(KeyEvent ev) {
1192             receivedKeyPressed = true;
1193             MenuSelectionManager.defaultManager().processKeyEvent(ev);
1194         }
1195 
1196         public void keyReleased(KeyEvent ev) {
1197             if (receivedKeyPressed) {
1198                 receivedKeyPressed = false;
1199                 MenuSelectionManager.defaultManager().processKeyEvent(ev);
1200             }
1201         }
1202 
1203         public void keyTyped(KeyEvent ev) {
1204             if (receivedKeyPressed) {
1205                 MenuSelectionManager.defaultManager().processKeyEvent(ev);
1206             }
1207         }
1208 
1209         void uninstall() {
1210             synchronized (MENU_KEYBOARD_HELPER_KEY) {
1211                 MenuSelectionManager.defaultManager().removeChangeListener(this);
1212                 AppContext.getAppContext().remove(MENU_KEYBOARD_HELPER_KEY);
1213             }
1214         }
1215     }
1216 }