       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&amp;F implementation of MenuUI.  This implementation
      43  * is a "combined" view/controller.
      44  *
      45  * @author Georges Saab
      46  * @author David Karlton
      47  * @author Arnaud Weber
      48  */
      49 public class BasicMenuUI extends BasicMenuItemUI
      50 {
      51     /**
      52      * The instance of {@code ChangeListener}.
      53      */
      54     protected ChangeListener         changeListener;
      55
      56     /**
      57      * The instance of {@code MenuListener}.
      58      */
      59     protected MenuListener           menuListener;
      60
      61     private int lastMnemonic = 0;
      62
      63     /** Uses as the parent of the windowInputMap when selected. */
      64     private InputMap selectedWindowInputMap;
      65
      66     /* diagnostic aids -- should be false for production builds. */
      67     private static final boolean TRACE =   false; // trace creates and disposes
      68     private static final boolean VERBOSE = false; // show reuse hits/misses
      69     private static final boolean DEBUG =   false;  // show bad params, misc.
      70
      71     private static boolean crossMenuMnemonic = true;
      72
      73     /**
      74      * Constructs a new instance of {@code BasicMenuUI}.
      75      *
      76      * @param x a component
      77      * @return a new instance of {@code BasicMenuUI}
      78      */
      79     public static ComponentUI createUI(JComponent x) {
      80         return new BasicMenuUI();
      81     }
      82
      83     static void loadActionMap(LazyActionMap map) {
      84         BasicMenuItemUI.loadActionMap(map);
      85         map.put(new Actions(Actions.SELECT, null, true));
      86     }
      87
      88
      89     protected void installDefaults() {
      90         super.installDefaults();
      91         updateDefaultBackgroundColor();
      92         ((JMenu)menuItem).setDelay(200);
      93         crossMenuMnemonic = UIManager.getBoolean("Menu.crossMenuMnemonic");
      94     }
      95
      96     protected String getPropertyPrefix() {
      97         return "Menu";
      98     }
      99
     100     protected void installListeners() {
     101         super.installListeners();
     102
     103         if (changeListener == null)
     104             changeListener = createChangeListener(menuItem);
     105
     106         if (changeListener != null)
     107             menuItem.addChangeListener(changeListener);
     108
     109         if (menuListener == null)
     110             menuListener = createMenuListener(menuItem);
     111
     112         if (menuListener != null)
     113             ((JMenu)menuItem).addMenuListener(menuListener);
     114     }
     115
     116     protected void installKeyboardActions() {
     117         super.installKeyboardActions();
     118         updateMnemonicBinding();
     119     }
     120
     121     void installLazyActionMap() {
     122         LazyActionMap.installLazyActionMap(menuItem, BasicMenuUI.class,
     123                                            getPropertyPrefix() + ".actionMap");
     124     }
     125
     126     @SuppressWarnings("deprecation")
     127     void updateMnemonicBinding() {
     128         int mnemonic = menuItem.getModel().getMnemonic();
     129         int[] shortcutKeys = (int[])DefaultLookup.get(menuItem, this,
     130                                                    "Menu.shortcutKeys");
     131         if (shortcutKeys == null) {
     132             shortcutKeys = new int[] {KeyEvent.ALT_MASK};

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