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