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 instanceof Window?
 801                     (Window)invoker :
 802                     SwingUtilities.getWindowAncestor(invoker);
 803             if(grabbedWindow != null) {
 804                 if(tk instanceof sun.awt.SunToolkit) {
 805                     ((sun.awt.SunToolkit)tk).grab(grabbedWindow);
 806                 } else {
 807                     grabbedWindow.addComponentListener(this);
 808                     grabbedWindow.addWindowListener(this);
 809                 }
 810             }
 811         }
 812 
 813         void ungrabWindow() {
 814             final Toolkit tk = Toolkit.getDefaultToolkit();
 815             // The grab should be removed
 816              java.security.AccessController.doPrivileged(
 817                 new java.security.PrivilegedAction<Object>() {
 818                     public Object run() {
 819                         tk.removeAWTEventListener(MouseGrabber.this);
 820                         return null;
 821                     }
 822                 }
 823             );
 824             realUngrabWindow();
 825         }
 826 
 827         void realUngrabWindow() {
 828             Toolkit tk = Toolkit.getDefaultToolkit();
 829             if(grabbedWindow != null) {
 830                 if(tk instanceof sun.awt.SunToolkit) {
 831                     ((sun.awt.SunToolkit)tk).ungrab(grabbedWindow);
 832                 } else {
 833                     grabbedWindow.removeComponentListener(this);
 834                     grabbedWindow.removeWindowListener(this);
 835                 }
 836                 grabbedWindow = null;
 837             }
 838         }
 839 
 840         public void stateChanged(ChangeEvent e) {
 841             MenuSelectionManager msm = MenuSelectionManager.defaultManager();
 842             MenuElement[] p = msm.getSelectedPath();
 843 
 844             if (lastPathSelected.length == 0 && p.length != 0) {
 845                 grabWindow(p);
 846             }
 847 
 848             if (lastPathSelected.length != 0 && p.length == 0) {
 849                 ungrabWindow();
 850             }
 851 
 852             lastPathSelected = p;
 853         }
 854 
 855         public void eventDispatched(AWTEvent ev) {
 856             if(ev instanceof sun.awt.UngrabEvent) {
 857                 // Popup should be canceled in case of ungrab event
 858                 cancelPopupMenu( );
 859                 return;
 860             }
 861             if (!(ev instanceof MouseEvent)) {
 862                 // We are interested in MouseEvents only
 863                 return;
 864             }
 865             MouseEvent me = (MouseEvent) ev;
 866             Component src = me.getComponent();
 867             switch (me.getID()) {
 868             case MouseEvent.MOUSE_PRESSED:
 869                 if (isInPopup(src) ||
 870                     (src instanceof JMenu && ((JMenu)src).isSelected())) {
 871                     return;
 872                 }
 873                 if (!(src instanceof JComponent) ||
 874                    ! (((JComponent)src).getClientProperty("doNotCancelPopup")
 875                          == BasicComboBoxUI.HIDE_POPUP_KEY)) {
 876                     // Cancel popup only if this property was not set.
 877                     // If this property is set to TRUE component wants
 878                     // to deal with this event by himself.
 879                     cancelPopupMenu();
 880                     // Ask UIManager about should we consume event that closes
 881                     // popup. This made to match native apps behaviour.
 882                     boolean consumeEvent =
 883                         UIManager.getBoolean("PopupMenu.consumeEventOnClose");
 884                     // Consume the event so that normal processing stops.
 885                     if(consumeEvent && !(src instanceof MenuElement)) {
 886                         me.consume();
 887                     }
 888                 }
 889                 break;
 890 
 891             case MouseEvent.MOUSE_RELEASED:
 892                 if(!(src instanceof MenuElement)) {
 893                     // Do not forward event to MSM, let component handle it
 894                     if (isInPopup(src)) {
 895                         break;
 896                     }
 897                 }
 898                 if(src instanceof JMenu || !(src instanceof JMenuItem)) {
 899                     MenuSelectionManager.defaultManager().
 900                         processMouseEvent(me);
 901                 }
 902                 break;
 903             case MouseEvent.MOUSE_DRAGGED:
 904                 if(!(src instanceof MenuElement)) {
 905                     // For the MOUSE_DRAGGED event the src is
 906                     // the Component in which mouse button was pressed.
 907                     // If the src is in popupMenu,
 908                     // do not forward event to MSM, let component handle it.
 909                     if (isInPopup(src)) {
 910                         break;
 911                     }
 912                 }
 913                 MenuSelectionManager.defaultManager().
 914                     processMouseEvent(me);
 915                 break;
 916             case MouseEvent.MOUSE_WHEEL:
 917                 if (isInPopup(src)
 918                     || ((src instanceof JComboBox) && ((JComboBox) src).isPopupVisible())) {
 919 
 920                     return;
 921                 }
 922                 cancelPopupMenu();
 923                 break;
 924             }
 925         }
 926 
 927         boolean isInPopup(Component src) {
 928             for (Component c=src; c!=null; c=c.getParent()) {
 929                 if (c instanceof Applet || c instanceof Window) {
 930                     break;
 931                 } else if (c instanceof JPopupMenu) {
 932                     return true;
 933                 }
 934             }
 935             return false;
 936         }
 937 
 938         void cancelPopupMenu() {
 939             // We should ungrab window if a user code throws
 940             // an unexpected runtime exception. See 6495920.
 941             try {
 942                 // 4234793: This action should call firePopupMenuCanceled but it's
 943                 // a protected method. The real solution could be to make
 944                 // firePopupMenuCanceled public and call it directly.
 945                 List<JPopupMenu> popups = getPopups();
 946                 for (JPopupMenu popup : popups) {
 947                     popup.putClientProperty("JPopupMenu.firePopupMenuCanceled", Boolean.TRUE);
 948                 }
 949                 MenuSelectionManager.defaultManager().clearSelectedPath();
 950             } catch (RuntimeException ex) {
 951                 realUngrabWindow();
 952                 throw ex;
 953             } catch (Error err) {
 954                 realUngrabWindow();
 955                 throw err;
 956             }
 957         }
 958 
 959         public void componentResized(ComponentEvent e) {
 960             cancelPopupMenu();
 961         }
 962         public void componentMoved(ComponentEvent e) {
 963             cancelPopupMenu();
 964         }
 965         public void componentShown(ComponentEvent e) {
 966             cancelPopupMenu();
 967         }
 968         public void componentHidden(ComponentEvent e) {
 969             cancelPopupMenu();
 970         }
 971         public void windowClosing(WindowEvent e) {
 972             cancelPopupMenu();
 973         }
 974         public void windowClosed(WindowEvent e) {
 975             cancelPopupMenu();
 976         }
 977         public void windowIconified(WindowEvent e) {
 978             cancelPopupMenu();
 979         }
 980         public void windowDeactivated(WindowEvent e) {
 981             cancelPopupMenu();
 982         }
 983         public void windowOpened(WindowEvent e) {}
 984         public void windowDeiconified(WindowEvent e) {}
 985         public void windowActivated(WindowEvent e) {}
 986     }
 987 
 988     /**
 989      * This helper is added to MenuSelectionManager as a ChangeListener to
 990      * listen to menu selection changes. When a menu is activated, it passes
 991      * focus to its parent JRootPane, and installs an ActionMap/InputMap pair
 992      * on that JRootPane. Those maps are necessary in order for menu
 993      * navigation to work. When menu is being deactivated, it restores focus
 994      * to the component that has had it before menu activation, and uninstalls
 995      * the maps.
 996      * This helper is also installed as a KeyListener on root pane when menu
 997      * is active. It forwards key events to MenuSelectionManager for mnemonic
 998      * keys handling.
 999      */
1000     static class MenuKeyboardHelper
1001         implements ChangeListener, KeyListener {
1002 
1003         private Component lastFocused = null;
1004         private MenuElement[] lastPathSelected = new MenuElement[0];
1005         private JPopupMenu lastPopup;
1006 
1007         private JRootPane invokerRootPane;
1008         private ActionMap menuActionMap = getActionMap();
1009         private InputMap menuInputMap;
1010         private boolean focusTraversalKeysEnabled;
1011 
1012         /*
1013          * Fix for 4213634
1014          * If this is false, KEY_TYPED and KEY_RELEASED events are NOT
1015          * processed. This is needed to avoid activating a menuitem when
1016          * the menu and menuitem share the same mnemonic.
1017          */
1018         private boolean receivedKeyPressed = false;
1019 
1020         void removeItems() {
1021             if (lastFocused != null) {
1022                 if(!lastFocused.requestFocusInWindow()) {
1023                     // Workarounr for 4810575.
1024                     // If lastFocused is not in currently focused window
1025                     // requestFocusInWindow will fail. In this case we must
1026                     // request focus by requestFocus() if it was not
1027                     // transferred from our popup.
1028                     Window cfw = KeyboardFocusManager
1029                                  .getCurrentKeyboardFocusManager()
1030                                   .getFocusedWindow();
1031                     if(cfw != null &&
1032                        "###focusableSwingPopup###".equals(cfw.getName())) {
1033                         lastFocused.requestFocus();
1034                     }
1035 
1036                 }
1037                 lastFocused = null;
1038             }
1039             if (invokerRootPane != null) {
1040                 invokerRootPane.removeKeyListener(this);
1041                 invokerRootPane.setFocusTraversalKeysEnabled(focusTraversalKeysEnabled);
1042                 removeUIInputMap(invokerRootPane, menuInputMap);
1043                 removeUIActionMap(invokerRootPane, menuActionMap);
1044                 invokerRootPane = null;
1045             }
1046             receivedKeyPressed = false;
1047         }
1048 
1049         private FocusListener rootPaneFocusListener = new FocusAdapter() {
1050                 public void focusGained(FocusEvent ev) {
1051                     Component opposite = ev.getOppositeComponent();
1052                     if (opposite != null) {
1053                         lastFocused = opposite;
1054                     }
1055                     ev.getComponent().removeFocusListener(this);
1056                 }
1057             };
1058 
1059         /**
1060          * Return the last JPopupMenu in <code>path</code>,
1061          * or <code>null</code> if none found
1062          */
1063         JPopupMenu getActivePopup(MenuElement[] path) {
1064             for (int i=path.length-1; i>=0; i--) {
1065                 MenuElement elem = path[i];
1066                 if (elem instanceof JPopupMenu) {
1067                     return (JPopupMenu)elem;
1068                 }
1069             }
1070             return null;
1071         }
1072 
1073         void addUIInputMap(JComponent c, InputMap map) {
1074             InputMap lastNonUI = null;
1075             InputMap parent = c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
1076 
1077             while (parent != null && !(parent instanceof UIResource)) {
1078                 lastNonUI = parent;
1079                 parent = parent.getParent();
1080             }
1081 
1082             if (lastNonUI == null) {
1083                 c.setInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW, map);
1084             } else {
1085                 lastNonUI.setParent(map);
1086             }
1087             map.setParent(parent);
1088         }
1089 
1090         void addUIActionMap(JComponent c, ActionMap map) {
1091             ActionMap lastNonUI = null;
1092             ActionMap parent = c.getActionMap();
1093 
1094             while (parent != null && !(parent instanceof UIResource)) {
1095                 lastNonUI = parent;
1096                 parent = parent.getParent();
1097             }
1098 
1099             if (lastNonUI == null) {
1100                 c.setActionMap(map);
1101             } else {
1102                 lastNonUI.setParent(map);
1103             }
1104             map.setParent(parent);
1105         }
1106 
1107         void removeUIInputMap(JComponent c, InputMap map) {
1108             InputMap im = null;
1109             InputMap parent = c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
1110 
1111             while (parent != null) {
1112                 if (parent == map) {
1113                     if (im == null) {
1114                         c.setInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW,
1115                                       map.getParent());
1116                     } else {
1117                         im.setParent(map.getParent());
1118                     }
1119                     break;
1120                 }
1121                 im = parent;
1122                 parent = parent.getParent();
1123             }
1124         }
1125 
1126         void removeUIActionMap(JComponent c, ActionMap map) {
1127             ActionMap im = null;
1128             ActionMap parent = c.getActionMap();
1129 
1130             while (parent != null) {
1131                 if (parent == map) {
1132                     if (im == null) {
1133                         c.setActionMap(map.getParent());
1134                     } else {
1135                         im.setParent(map.getParent());
1136                     }
1137                     break;
1138                 }
1139                 im = parent;
1140                 parent = parent.getParent();
1141             }
1142         }
1143 
1144         public void stateChanged(ChangeEvent ev) {
1145             if (!(UIManager.getLookAndFeel() instanceof BasicLookAndFeel)) {
1146                 uninstall();
1147                 return;
1148             }
1149             MenuSelectionManager msm = (MenuSelectionManager)ev.getSource();
1150             MenuElement[] p = msm.getSelectedPath();
1151             JPopupMenu popup = getActivePopup(p);
1152             if (popup != null && !popup.isFocusable()) {
1153                 // Do nothing for non-focusable popups
1154                 return;
1155             }
1156 
1157             if (lastPathSelected.length != 0 && p.length != 0 ) {
1158                 if (!checkInvokerEqual(p[0],lastPathSelected[0])) {
1159                     removeItems();
1160                     lastPathSelected = new MenuElement[0];
1161                 }
1162             }
1163 
1164             if (lastPathSelected.length == 0 && p.length > 0) {
1165                 // menu posted
1166                 JComponent invoker;
1167 
1168                 if (popup == null) {
1169                     if (p.length == 2 && p[0] instanceof JMenuBar &&
1170                         p[1] instanceof JMenu) {
1171                         // a menu has been selected but not open
1172                         invoker = (JComponent)p[1];
1173                         popup = ((JMenu)invoker).getPopupMenu();
1174                     } else {
1175                         return;
1176                     }
1177                 } else {
1178                     Component c = popup.getInvoker();
1179                     if(c instanceof JFrame) {
1180                         invoker = ((JFrame)c).getRootPane();
1181                     } else if(c instanceof JDialog) {
1182                         invoker = ((JDialog)c).getRootPane();
1183                     } else if(c instanceof JApplet) {
1184                         invoker = ((JApplet)c).getRootPane();
1185                     } else {
1186                         while (!(c instanceof JComponent)) {
1187                             if (c == null) {
1188                                 return;
1189                             }
1190                             c = c.getParent();
1191                         }
1192                         invoker = (JComponent)c;
1193                     }
1194                 }
1195 
1196                 // remember current focus owner
1197                 lastFocused = KeyboardFocusManager.
1198                     getCurrentKeyboardFocusManager().getFocusOwner();
1199 
1200                 // request focus on root pane and install keybindings
1201                 // used for menu navigation
1202                 invokerRootPane = SwingUtilities.getRootPane(invoker);
1203                 if (invokerRootPane != null) {
1204                     invokerRootPane.addFocusListener(rootPaneFocusListener);
1205                     invokerRootPane.requestFocus(true);
1206                     invokerRootPane.addKeyListener(this);
1207                     focusTraversalKeysEnabled = invokerRootPane.
1208                                       getFocusTraversalKeysEnabled();
1209                     invokerRootPane.setFocusTraversalKeysEnabled(false);
1210 
1211                     menuInputMap = getInputMap(popup, invokerRootPane);
1212                     addUIInputMap(invokerRootPane, menuInputMap);
1213                     addUIActionMap(invokerRootPane, menuActionMap);
1214                 }
1215             } else if (lastPathSelected.length != 0 && p.length == 0) {
1216                 // menu hidden -- return focus to where it had been before
1217                 // and uninstall menu keybindings
1218                    removeItems();
1219             } else {
1220                 if (popup != lastPopup) {
1221                     receivedKeyPressed = false;
1222                 }
1223             }
1224 
1225             // Remember the last path selected
1226             lastPathSelected = p;
1227             lastPopup = popup;
1228         }
1229 
1230         public void keyPressed(KeyEvent ev) {
1231             receivedKeyPressed = true;
1232             MenuSelectionManager.defaultManager().processKeyEvent(ev);
1233         }
1234 
1235         public void keyReleased(KeyEvent ev) {
1236             if (receivedKeyPressed) {
1237                 receivedKeyPressed = false;
1238                 MenuSelectionManager.defaultManager().processKeyEvent(ev);
1239             }
1240         }
1241 
1242         public void keyTyped(KeyEvent ev) {
1243             if (receivedKeyPressed) {
1244                 MenuSelectionManager.defaultManager().processKeyEvent(ev);
1245             }
1246         }
1247 
1248         void uninstall() {
1249             synchronized (MENU_KEYBOARD_HELPER_KEY) {
1250                 MenuSelectionManager.defaultManager().removeChangeListener(this);
1251                 AppContext.getAppContext().remove(MENU_KEYBOARD_HELPER_KEY);
1252             }
1253         }
1254     }
1255 }