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
26package javax.swing.plaf.basic;
27
28import sun.swing.DefaultLookup;
29import sun.swing.UIAction;
30import java.awt.*;
31import java.awt.event.*;
32import java.beans.*;
33import javax.swing.*;
34import javax.swing.event.*;
35import javax.swing.plaf.*;
36import javax.swing.border.*;
37import java.util.Arrays;
38import 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 */
49public 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}
685