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