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