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