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