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