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 sun.swing.DefaultLookup; 29 import sun.swing.UIAction; 30 import java.awt.*; 31 import java.awt.event.*; 32 import java.beans.*; 33 import javax.swing.*; 34 import javax.swing.event.*; 35 import javax.swing.plaf.*; 36 import javax.swing.border.*; 37 import java.util.Arrays; 38 import java.util.ArrayList; 39 40 41 /** 42 * A default L&F implementation of MenuUI. This implementation 43 * is a "combined" view/controller. 44 * 45 * @author Georges Saab 46 * @author David Karlton 47 * @author Arnaud Weber 48 */ 49 public class BasicMenuUI extends BasicMenuItemUI 50 { 51 /** 52 * The instance of {@code ChangeListener}. 53 */ 54 protected ChangeListener changeListener; 55 56 /** 57 * The instance of {@code MenuListener}. 58 */ 59 protected MenuListener menuListener; 60 61 private int lastMnemonic = 0; 62 63 /** Uses as the parent of the windowInputMap when selected. */ 64 private InputMap selectedWindowInputMap; 65 66 /* diagnostic aids -- should be false for production builds. */ 67 private static final boolean TRACE = false; // trace creates and disposes 68 private static final boolean VERBOSE = false; // show reuse hits/misses 69 private static final boolean DEBUG = false; // show bad params, misc. 70 71 private static boolean crossMenuMnemonic = true; 72 73 /** 74 * Constructs a new instance of {@code BasicMenuUI}. 75 * 76 * @param x a component 77 * @return a new instance of {@code BasicMenuUI} 78 */ 79 public static ComponentUI createUI(JComponent x) { 80 return new BasicMenuUI(); 81 } 82 83 static void loadActionMap(LazyActionMap map) { 84 BasicMenuItemUI.loadActionMap(map); 85 map.put(new Actions(Actions.SELECT, null, true)); 86 } 87 88 89 protected void installDefaults() { 90 super.installDefaults(); 91 updateDefaultBackgroundColor(); 92 ((JMenu)menuItem).setDelay(200); 93 crossMenuMnemonic = UIManager.getBoolean("Menu.crossMenuMnemonic"); 94 } 95 96 protected String getPropertyPrefix() { 97 return "Menu"; 98 } 99 100 protected void installListeners() { 101 super.installListeners(); 102 103 if (changeListener == null) 104 changeListener = createChangeListener(menuItem); 105 106 if (changeListener != null) 107 menuItem.addChangeListener(changeListener); 108 109 if (menuListener == null) 110 menuListener = createMenuListener(menuItem); 111 112 if (menuListener != null) 113 ((JMenu)menuItem).addMenuListener(menuListener); 114 } 115 116 protected void installKeyboardActions() { 117 super.installKeyboardActions(); 118 updateMnemonicBinding(); 119 } 120 121 void installLazyActionMap() { 122 LazyActionMap.installLazyActionMap(menuItem, BasicMenuUI.class, 123 getPropertyPrefix() + ".actionMap"); 124 } 125 126 @SuppressWarnings("deprecation") 127 void updateMnemonicBinding() { 128 int mnemonic = menuItem.getModel().getMnemonic(); 129 int[] shortcutKeys = (int[])DefaultLookup.get(menuItem, this, 130 "Menu.shortcutKeys"); 131 if (shortcutKeys == null) { 132 shortcutKeys = new int[] {KeyEvent.ALT_MASK}; 133 } 134 if (mnemonic == lastMnemonic) { 135 return; 136 } 137 InputMap windowInputMap = SwingUtilities.getUIInputMap( 138 menuItem, JComponent.WHEN_IN_FOCUSED_WINDOW); 139 if (lastMnemonic != 0 && windowInputMap != null) { 140 for (int shortcutKey : shortcutKeys) { 141 windowInputMap.remove(KeyStroke.getKeyStroke 142 (lastMnemonic, shortcutKey, false)); 143 } 144 } 145 if (mnemonic != 0) { 146 if (windowInputMap == null) { 147 windowInputMap = createInputMap(JComponent. 148 WHEN_IN_FOCUSED_WINDOW); 149 SwingUtilities.replaceUIInputMap(menuItem, JComponent. 150 WHEN_IN_FOCUSED_WINDOW, windowInputMap); 151 } 152 for (int shortcutKey : shortcutKeys) { 153 windowInputMap.put(KeyStroke.getKeyStroke(mnemonic, 154 shortcutKey, false), "selectMenu"); 155 } 156 } 157 lastMnemonic = mnemonic; 158 } 159 160 protected void uninstallKeyboardActions() { 161 super.uninstallKeyboardActions(); 162 lastMnemonic = 0; 163 } 164 165 protected MouseInputListener createMouseInputListener(JComponent c) { 166 return getHandler(); 167 } 168 169 /** 170 * Returns an instance of {@code MenuListener}. 171 * 172 * @param c a component 173 * @return an instance of {@code MenuListener} 174 */ 175 protected MenuListener createMenuListener(JComponent c) { 176 return null; 177 } 178 179 /** 180 * Returns an instance of {@code ChangeListener}. 181 * 182 * @param c a component 183 * @return an instance of {@code ChangeListener} 184 */ 185 protected ChangeListener createChangeListener(JComponent c) { 186 return null; 187 } 188 189 protected PropertyChangeListener createPropertyChangeListener(JComponent c) { 190 return getHandler(); 191 } 192 193 BasicMenuItemUI.Handler getHandler() { 194 if (handler == null) { 195 handler = new Handler(); 196 } 197 return handler; 198 } 199 200 protected void uninstallDefaults() { 201 menuItem.setArmed(false); 202 menuItem.setSelected(false); 203 menuItem.resetKeyboardActions(); 204 super.uninstallDefaults(); 205 } 206 207 protected void uninstallListeners() { 208 super.uninstallListeners(); 209 210 if (changeListener != null) 211 menuItem.removeChangeListener(changeListener); 212 213 if (menuListener != null) 214 ((JMenu)menuItem).removeMenuListener(menuListener); 215 216 changeListener = null; 217 menuListener = null; 218 handler = null; 219 } 220 221 protected MenuDragMouseListener createMenuDragMouseListener(JComponent c) { 222 return getHandler(); 223 } 224 225 protected MenuKeyListener createMenuKeyListener(JComponent c) { 226 return (MenuKeyListener)getHandler(); 227 } 228 229 public Dimension getMinimumSize(JComponent c) { 230 return (((JMenu)menuItem).isTopLevelMenu() == true) ? 231 c.getPreferredSize() : null; 232 } 233 234 public Dimension getMaximumSize(JComponent c) { 235 if (((JMenu)menuItem).isTopLevelMenu() == true) { 236 Dimension d = c.getPreferredSize(); 237 return new Dimension(d.width, Short.MAX_VALUE); 238 } 239 return null; 240 } 241 242 /** 243 * Sets timer to the {@code menu}. 244 * 245 * @param menu an instance of {@code JMenu}. 246 */ 247 protected void setupPostTimer(JMenu menu) { 248 Timer timer = new Timer(menu.getDelay(), new Actions( 249 Actions.SELECT, menu,false)); 250 timer.setRepeats(false); 251 timer.start(); 252 } 253 254 private static void appendPath(MenuElement[] path, MenuElement elem) { 255 MenuElement newPath[] = new MenuElement[path.length+1]; 256 System.arraycopy(path, 0, newPath, 0, path.length); 257 newPath[path.length] = elem; 258 MenuSelectionManager.defaultManager().setSelectedPath(newPath); 259 } 260 261 private static class Actions extends UIAction { 262 private static final String SELECT = "selectMenu"; 263 264 // NOTE: This will be null if the action is registered in the 265 // ActionMap. For the timer use it will be non-null. 266 private JMenu menu; 267 private boolean force=false; 268 269 Actions(String key, JMenu menu, boolean shouldForce) { 270 super(key); 271 this.menu = menu; 272 this.force = shouldForce; 273 } 274 275 private JMenu getMenu(ActionEvent e) { 276 if (e.getSource() instanceof JMenu) { 277 return (JMenu)e.getSource(); 278 } 279 return menu; 280 } 281 282 public void actionPerformed(ActionEvent e) { 283 JMenu menu = getMenu(e); 284 if (!crossMenuMnemonic) { 285 JPopupMenu pm = BasicPopupMenuUI.getLastPopup(); 286 if (pm != null && pm != menu.getParent()) { 287 return; 288 } 289 } 290 291 final MenuSelectionManager defaultManager = MenuSelectionManager.defaultManager(); 292 if(force) { 293 Container cnt = menu.getParent(); 294 if(cnt != null && cnt instanceof JMenuBar) { 295 MenuElement me[]; 296 MenuElement subElements[]; 297 298 subElements = menu.getPopupMenu().getSubElements(); 299 if(subElements.length > 0) { 300 me = new MenuElement[4]; 301 me[0] = (MenuElement) cnt; 302 me[1] = menu; 303 me[2] = menu.getPopupMenu(); 304 me[3] = subElements[0]; 305 } else { 306 me = new MenuElement[3]; 307 me[0] = (MenuElement)cnt; 308 me[1] = menu; 309 me[2] = menu.getPopupMenu(); 310 } 311 defaultManager.setSelectedPath(me); 312 } 313 } else { 314 MenuElement path[] = defaultManager.getSelectedPath(); 315 if(path.length > 0 && path[path.length-1] == menu) { 316 appendPath(path, menu.getPopupMenu()); 317 } 318 } 319 } 320 321 @Override 322 public boolean accept(Object c) { 323 if (c instanceof JMenu) { 324 return ((JMenu)c).isEnabled(); 325 } 326 return true; 327 } 328 } 329 330 /* 331 * Set the background color depending on whether this is a toplevel menu 332 * in a menubar or a submenu of another menu. 333 */ 334 private void updateDefaultBackgroundColor() { 335 if (!UIManager.getBoolean("Menu.useMenuBarBackgroundForTopLevel")) { 336 return; 337 } 338 JMenu menu = (JMenu)menuItem; 339 if (menu.getBackground() instanceof UIResource) { 340 if (menu.isTopLevelMenu()) { 341 menu.setBackground(UIManager.getColor("MenuBar.background")); 342 } else { 343 menu.setBackground(UIManager.getColor(getPropertyPrefix() + ".background")); 344 } 345 } 346 } 347 348 /** 349 * Instantiated and used by a menu item to handle the current menu selection 350 * from mouse events. A MouseInputHandler processes and forwards all mouse events 351 * to a shared instance of the MenuSelectionManager. 352 * <p> 353 * This class is protected so that it can be subclassed by other look and 354 * feels to implement their own mouse handling behavior. All overridden 355 * methods should call the parent methods so that the menu selection 356 * is correct. 357 * 358 * @see javax.swing.MenuSelectionManager 359 * @since 1.4 360 */ 361 protected class MouseInputHandler implements MouseInputListener { 362 // NOTE: This class exists only for backward compatibility. All 363 // its functionality has been moved into Handler. If you need to add 364 // new functionality add it to the Handler, but make sure this 365 // class calls into the Handler. 366 367 public void mouseClicked(MouseEvent e) { 368 getHandler().mouseClicked(e); 369 } 370 371 /** 372 * Invoked when the mouse has been clicked on the menu. This 373 * method clears or sets the selection path of the 374 * MenuSelectionManager. 375 * 376 * @param e the mouse event 377 */ 378 public void mousePressed(MouseEvent e) { 379 getHandler().mousePressed(e); 380 } 381 382 /** 383 * Invoked when the mouse has been released on the menu. Delegates the 384 * mouse event to the MenuSelectionManager. 385 * 386 * @param e the mouse event 387 */ 388 public void mouseReleased(MouseEvent e) { 389 getHandler().mouseReleased(e); 390 } 391 392 /** 393 * Invoked when the cursor enters the menu. This method sets the selected 394 * path for the MenuSelectionManager and handles the case 395 * in which a menu item is used to pop up an additional menu, as in a 396 * hierarchical menu system. 397 * 398 * @param e the mouse event; not used 399 */ 400 public void mouseEntered(MouseEvent e) { 401 getHandler().mouseEntered(e); 402 } 403 public void mouseExited(MouseEvent e) { 404 getHandler().mouseExited(e); 405 } 406 407 /** 408 * Invoked when a mouse button is pressed on the menu and then dragged. 409 * Delegates the mouse event to the MenuSelectionManager. 410 * 411 * @param e the mouse event 412 * @see java.awt.event.MouseMotionListener#mouseDragged 413 */ 414 public void mouseDragged(MouseEvent e) { 415 getHandler().mouseDragged(e); 416 } 417 418 public void mouseMoved(MouseEvent e) { 419 getHandler().mouseMoved(e); 420 } 421 } 422 423 /** 424 * As of Java 2 platform 1.4, this previously undocumented class 425 * is now obsolete. KeyBindings are now managed by the popup menu. 426 */ 427 public class ChangeHandler implements ChangeListener { 428 /** 429 * The instance of {@code JMenu}. 430 */ 431 public JMenu menu; 432 433 /** 434 * The instance of {@code BasicMenuUI}. 435 */ 436 public BasicMenuUI ui; 437 438 /** 439 * {@code true} if an item of popup menu is selected. 440 */ 441 public boolean isSelected = false; 442 443 /** 444 * The component that was focused. 445 */ 446 public Component wasFocused; 447 448 /** 449 * Constructs a new instance of {@code ChangeHandler}. 450 * 451 * @param m an instance of {@code JMenu} 452 * @param ui an instance of {@code BasicMenuUI} 453 */ 454 public ChangeHandler(JMenu m, BasicMenuUI ui) { 455 menu = m; 456 this.ui = ui; 457 } 458 459 public void stateChanged(ChangeEvent e) { } 460 } 461 462 private class Handler extends BasicMenuItemUI.Handler implements MenuKeyListener { 463 // 464 // PropertyChangeListener 465 // 466 public void propertyChange(PropertyChangeEvent e) { 467 if (e.getPropertyName() == AbstractButton. 468 MNEMONIC_CHANGED_PROPERTY) { 469 updateMnemonicBinding(); 470 } 471 else { 472 if (e.getPropertyName().equals("ancestor")) { 473 updateDefaultBackgroundColor(); 474 } 475 super.propertyChange(e); 476 } 477 } 478 479 // 480 // MouseInputListener 481 // 482 public void mouseClicked(MouseEvent e) { 483 } 484 485 /** 486 * Invoked when the mouse has been clicked on the menu. This 487 * method clears or sets the selection path of the 488 * MenuSelectionManager. 489 * 490 * @param e the mouse event 491 */ 492 public void mousePressed(MouseEvent e) { 493 JMenu menu = (JMenu)menuItem; 494 if (!menu.isEnabled()) 495 return; 496 497 MenuSelectionManager manager = 498 MenuSelectionManager.defaultManager(); 499 if(menu.isTopLevelMenu()) { 500 if(menu.isSelected() && menu.getPopupMenu().isShowing()) { 501 manager.clearSelectedPath(); 502 } else { 503 Container cnt = menu.getParent(); 504 if(cnt != null && cnt instanceof JMenuBar) { 505 MenuElement me[] = new MenuElement[2]; 506 me[0]=(MenuElement)cnt; 507 me[1]=menu; 508 manager.setSelectedPath(me); 509 } 510 } 511 } 512 513 MenuElement selectedPath[] = manager.getSelectedPath(); 514 if (selectedPath.length > 0 && 515 selectedPath[selectedPath.length-1] != menu.getPopupMenu()) { 516 517 if(menu.isTopLevelMenu() || 518 menu.getDelay() == 0) { 519 appendPath(selectedPath, menu.getPopupMenu()); 520 } else { 521 setupPostTimer(menu); 522 } 523 } 524 } 525 526 /** 527 * Invoked when the mouse has been released on the menu. Delegates the 528 * mouse event to the MenuSelectionManager. 529 * 530 * @param e the mouse event 531 */ 532 public void mouseReleased(MouseEvent e) { 533 JMenu menu = (JMenu)menuItem; 534 if (!menu.isEnabled()) 535 return; 536 MenuSelectionManager manager = 537 MenuSelectionManager.defaultManager(); 538 manager.processMouseEvent(e); 539 if (!e.isConsumed()) 540 manager.clearSelectedPath(); 541 } 542 543 /** 544 * Invoked when the cursor enters the menu. This method sets the selected 545 * path for the MenuSelectionManager and handles the case 546 * in which a menu item is used to pop up an additional menu, as in a 547 * hierarchical menu system. 548 * 549 * @param e the mouse event; not used 550 */ 551 public void mouseEntered(MouseEvent e) { 552 JMenu menu = (JMenu)menuItem; 553 // only disable the menu highlighting if it's disabled and the property isn't 554 // true. This allows disabled rollovers to work in WinL&F 555 if (!menu.isEnabled() && !UIManager.getBoolean("MenuItem.disabledAreNavigable")) { 556 return; 557 } 558 559 MenuSelectionManager manager = 560 MenuSelectionManager.defaultManager(); 561 MenuElement selectedPath[] = manager.getSelectedPath(); 562 if (!menu.isTopLevelMenu()) { 563 if(!(selectedPath.length > 0 && 564 selectedPath[selectedPath.length-1] == 565 menu.getPopupMenu())) { 566 if(menu.getDelay() == 0) { 567 appendPath(getPath(), menu.getPopupMenu()); 568 } else { 569 manager.setSelectedPath(getPath()); 570 setupPostTimer(menu); 571 } 572 } 573 } else { 574 if(selectedPath.length > 0 && 575 selectedPath[0] == menu.getParent()) { 576 MenuElement newPath[] = new MenuElement[3]; 577 // A top level menu's parent is by definition 578 // a JMenuBar 579 newPath[0] = (MenuElement)menu.getParent(); 580 newPath[1] = menu; 581 if (BasicPopupMenuUI.getLastPopup() != null) { 582 newPath[2] = menu.getPopupMenu(); 583 } 584 manager.setSelectedPath(newPath); 585 } 586 } 587 } 588 public void mouseExited(MouseEvent e) { 589 } 590 591 /** 592 * Invoked when a mouse button is pressed on the menu and then dragged. 593 * Delegates the mouse event to the MenuSelectionManager. 594 * 595 * @param e the mouse event 596 * @see java.awt.event.MouseMotionListener#mouseDragged 597 */ 598 public void mouseDragged(MouseEvent e) { 599 JMenu menu = (JMenu)menuItem; 600 if (!menu.isEnabled()) 601 return; 602 MenuSelectionManager.defaultManager().processMouseEvent(e); 603 } 604 public void mouseMoved(MouseEvent e) { 605 } 606 607 608 // 609 // MenuDragHandler 610 // 611 public void menuDragMouseEntered(MenuDragMouseEvent e) {} 612 public void menuDragMouseDragged(MenuDragMouseEvent e) { 613 if (menuItem.isEnabled() == false) 614 return; 615 616 MenuSelectionManager manager = e.getMenuSelectionManager(); 617 MenuElement path[] = e.getPath(); 618 619 Point p = e.getPoint(); 620 if(p.x >= 0 && p.x < menuItem.getWidth() && 621 p.y >= 0 && p.y < menuItem.getHeight()) { 622 JMenu menu = (JMenu)menuItem; 623 MenuElement selectedPath[] = manager.getSelectedPath(); 624 if(!(selectedPath.length > 0 && 625 selectedPath[selectedPath.length-1] == 626 menu.getPopupMenu())) { 627 if(menu.isTopLevelMenu() || 628 menu.getDelay() == 0 || 629 e.getID() == MouseEvent.MOUSE_DRAGGED) { 630 appendPath(path, menu.getPopupMenu()); 631 } else { 632 manager.setSelectedPath(path); 633 setupPostTimer(menu); 634 } 635 } 636 } else if(e.getID() == MouseEvent.MOUSE_RELEASED) { 637 Component comp = manager.componentForPoint(e.getComponent(), e.getPoint()); 638 if (comp == null) 639 manager.clearSelectedPath(); 640 } 641 642 } 643 public void menuDragMouseExited(MenuDragMouseEvent e) {} 644 public void menuDragMouseReleased(MenuDragMouseEvent e) {} 645 646 // 647 // MenuKeyListener 648 // 649 /** 650 * Open the Menu 651 */ 652 public void menuKeyTyped(MenuKeyEvent e) { 653 if (!crossMenuMnemonic && BasicPopupMenuUI.getLastPopup() != null) { 654 // when crossMenuMnemonic is not set, we don't open a toplevel 655 // menu if another toplevel menu is already open 656 return; 657 } 658 659 if (BasicPopupMenuUI.getPopups().size() != 0) { 660 //Fix 6939261: to return in case not on the main menu 661 //and has a pop-up. 662 //after return code will be handled in BasicPopupMenuUI.java 663 return; 664 } 665 666 char key = Character.toLowerCase((char)menuItem.getMnemonic()); 667 MenuElement path[] = e.getPath(); 668 if (key == Character.toLowerCase(e.getKeyChar())) { 669 JPopupMenu popupMenu = ((JMenu)menuItem).getPopupMenu(); 670 ArrayList<MenuElement> newList = new ArrayList<>(Arrays.asList(path)); 671 newList.add(popupMenu); 672 MenuElement subs[] = popupMenu.getSubElements(); 673 MenuElement sub = 674 BasicPopupMenuUI.findEnabledChild(subs, -1, true); 675 if(sub != null) { 676 newList.add(sub); 677 } 678 MenuSelectionManager manager = e.getMenuSelectionManager(); 679 MenuElement newPath[] = new MenuElement[0];; 680 newPath = newList.toArray(newPath); 681 manager.setSelectedPath(newPath); 682 e.consume(); 683 } 684 } 685 686 public void menuKeyPressed(MenuKeyEvent e) {} 687 public void menuKeyReleased(MenuKeyEvent e) {} 688 } 689 }