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 }