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