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