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