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 }