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