1. /*
  2. * @(#)BasicMenuUI.java 1.151 03/05/06
  3. *
  4. * Copyright 2003 Sun Microsystems, Inc. All rights reserved.
  5. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
  6. */
  7. package javax.swing.plaf.basic;
  8. import java.awt.*;
  9. import java.awt.event.*;
  10. import java.beans.*;
  11. import javax.swing.*;
  12. import javax.swing.event.*;
  13. import javax.swing.plaf.*;
  14. import javax.swing.border.*;
  15. import java.util.Arrays;
  16. import java.util.ArrayList;
  17. /**
  18. * A default L&F implementation of MenuUI. This implementation
  19. * is a "combined" view/controller.
  20. *
  21. * @version 1.151 05/06/03
  22. * @author Georges Saab
  23. * @author David Karlton
  24. * @author Arnaud Weber
  25. */
  26. public class BasicMenuUI extends BasicMenuItemUI
  27. {
  28. protected ChangeListener changeListener;
  29. protected PropertyChangeListener propertyChangeListener;
  30. protected MenuListener menuListener;
  31. private int lastMnemonic = 0;
  32. /** Uses as the parent of the windowInputMap when selected. */
  33. private InputMap selectedWindowInputMap;
  34. /* diagnostic aids -- should be false for production builds. */
  35. private static final boolean TRACE = false; // trace creates and disposes
  36. private static final boolean VERBOSE = false; // show reuse hits/misses
  37. private static final boolean DEBUG = false; // show bad params, misc.
  38. private static boolean crossMenuMnemonic = true;
  39. public static ComponentUI createUI(JComponent x) {
  40. return new BasicMenuUI();
  41. }
  42. protected void installDefaults() {
  43. super.installDefaults();
  44. updateDefaultBackgroundColor();
  45. ((JMenu)menuItem).setDelay(200);
  46. crossMenuMnemonic = UIManager.getBoolean("Menu.crossMenuMnemonic");
  47. }
  48. protected String getPropertyPrefix() {
  49. return "Menu";
  50. }
  51. protected void installListeners() {
  52. super.installListeners();
  53. if (changeListener == null)
  54. changeListener = createChangeListener(menuItem);
  55. if (changeListener != null)
  56. menuItem.addChangeListener(changeListener);
  57. if (propertyChangeListener == null)
  58. propertyChangeListener = createPropertyChangeListener(menuItem);
  59. if (propertyChangeListener != null)
  60. menuItem.addPropertyChangeListener(propertyChangeListener);
  61. if (menuListener == null)
  62. menuListener = createMenuListener(menuItem);
  63. if (menuListener != null)
  64. ((JMenu)menuItem).addMenuListener(menuListener);
  65. }
  66. protected void installKeyboardActions() {
  67. super.installKeyboardActions();
  68. updateMnemonicBinding();
  69. }
  70. void updateMnemonicBinding() {
  71. int mnemonic = menuItem.getModel().getMnemonic();
  72. int[] shortcutKeys = (int[])UIManager.get("Menu.shortcutKeys");
  73. if (mnemonic == lastMnemonic || shortcutKeys == null) {
  74. return;
  75. }
  76. if (lastMnemonic != 0 && windowInputMap != null) {
  77. for (int i=0; i<shortcutKeys.length; i++) {
  78. windowInputMap.remove(KeyStroke.getKeyStroke
  79. (lastMnemonic, shortcutKeys[i], false));
  80. }
  81. }
  82. if (mnemonic != 0) {
  83. if (windowInputMap == null) {
  84. windowInputMap = createInputMap(JComponent.
  85. WHEN_IN_FOCUSED_WINDOW);
  86. SwingUtilities.replaceUIInputMap(menuItem, JComponent.
  87. WHEN_IN_FOCUSED_WINDOW, windowInputMap);
  88. }
  89. for (int i=0; i<shortcutKeys.length; i++) {
  90. windowInputMap.put(KeyStroke.getKeyStroke(mnemonic,
  91. shortcutKeys[i], false),
  92. "selectMenu");
  93. }
  94. }
  95. lastMnemonic = mnemonic;
  96. }
  97. protected void uninstallKeyboardActions() {
  98. super.uninstallKeyboardActions();
  99. }
  100. /**
  101. * The ActionMap for BasicMenUI can not be shared, this is subclassed
  102. * to create a new one for each invocation.
  103. */
  104. ActionMap getActionMap() {
  105. return createActionMap();
  106. }
  107. /**
  108. * Invoked to create the ActionMap.
  109. */
  110. ActionMap createActionMap() {
  111. ActionMap am = super.createActionMap();
  112. if (am != null) {
  113. am.put("selectMenu", new PostAction((JMenu)menuItem, true));
  114. }
  115. return am;
  116. }
  117. protected MouseInputListener createMouseInputListener(JComponent c) {
  118. return new MouseInputHandler();
  119. }
  120. protected MenuListener createMenuListener(JComponent c) {
  121. return null;
  122. }
  123. protected ChangeListener createChangeListener(JComponent c) {
  124. return null;
  125. }
  126. protected PropertyChangeListener createPropertyChangeListener(JComponent c) {
  127. return new PropertyChangeHandler();
  128. }
  129. protected void uninstallDefaults() {
  130. menuItem.setArmed(false);
  131. menuItem.setSelected(false);
  132. menuItem.resetKeyboardActions();
  133. super.uninstallDefaults();
  134. }
  135. protected void uninstallListeners() {
  136. super.uninstallListeners();
  137. if (changeListener != null)
  138. menuItem.removeChangeListener(changeListener);
  139. if (propertyChangeListener != null)
  140. menuItem.removePropertyChangeListener(propertyChangeListener);
  141. if (menuListener != null)
  142. ((JMenu)menuItem).removeMenuListener(menuListener);
  143. changeListener = null;
  144. propertyChangeListener = null;
  145. menuListener = null;
  146. }
  147. protected MenuDragMouseListener createMenuDragMouseListener(JComponent c) {
  148. return new MenuDragMouseHandler();
  149. }
  150. protected MenuKeyListener createMenuKeyListener(JComponent c) {
  151. return new MenuKeyHandler();
  152. }
  153. public Dimension getMaximumSize(JComponent c) {
  154. if (((JMenu)menuItem).isTopLevelMenu() == true) {
  155. Dimension d = c.getPreferredSize();
  156. return new Dimension(d.width, Short.MAX_VALUE);
  157. }
  158. return null;
  159. }
  160. protected void setupPostTimer(JMenu menu) {
  161. Timer timer = new Timer(menu.getDelay(),new PostAction(menu,false));
  162. timer.setRepeats(false);
  163. timer.start();
  164. }
  165. private static void appendPath(MenuElement[] path, MenuElement elem) {
  166. MenuElement newPath[] = new MenuElement[path.length+1];
  167. System.arraycopy(path, 0, newPath, 0, path.length);
  168. newPath[path.length] = elem;
  169. MenuSelectionManager.defaultManager().setSelectedPath(newPath);
  170. }
  171. private static class PostAction extends AbstractAction {
  172. JMenu menu;
  173. boolean force=false;
  174. PostAction(JMenu menu,boolean shouldForce) {
  175. this.menu = menu;
  176. this.force = shouldForce;
  177. }
  178. public void actionPerformed(ActionEvent e) {
  179. if (!crossMenuMnemonic) {
  180. JPopupMenu pm = getActivePopupMenu();
  181. if (pm != null && pm != menu.getParent()) {
  182. return;
  183. }
  184. }
  185. final MenuSelectionManager defaultManager = MenuSelectionManager.defaultManager();
  186. if(force) {
  187. Container cnt = menu.getParent();
  188. if(cnt != null && cnt instanceof JMenuBar) {
  189. MenuElement me[];
  190. MenuElement subElements[];
  191. subElements = menu.getPopupMenu().getSubElements();
  192. if(subElements.length > 0) {
  193. me = new MenuElement[4];
  194. me[0] = (MenuElement) cnt;
  195. me[1] = (MenuElement) menu;
  196. me[2] = (MenuElement) menu.getPopupMenu();
  197. me[3] = subElements[0];
  198. } else {
  199. me = new MenuElement[3];
  200. me[0] = (MenuElement)cnt;
  201. me[1] = menu;
  202. me[2] = (MenuElement) menu.getPopupMenu();
  203. }
  204. defaultManager.setSelectedPath(me);
  205. }
  206. } else {
  207. MenuElement path[] = defaultManager.getSelectedPath();
  208. if(path.length > 0 && path[path.length-1] == menu) {
  209. appendPath(path, menu.getPopupMenu());
  210. }
  211. }
  212. }
  213. public boolean isEnabled() {
  214. return menu.getModel().isEnabled();
  215. }
  216. }
  217. /*
  218. * Set the background color depending on whether this is a toplevel menu
  219. * in a menubar or a submenu of another menu.
  220. */
  221. private void updateDefaultBackgroundColor() {
  222. if (!UIManager.getBoolean("Menu.useMenuBarBackgroundForTopLevel")) {
  223. return;
  224. }
  225. JMenu menu = (JMenu)menuItem;
  226. if (menu.getBackground() instanceof UIResource) {
  227. if (menu.isTopLevelMenu()) {
  228. menu.setBackground(UIManager.getColor("MenuBar.background"));
  229. } else {
  230. menu.setBackground(UIManager.getColor(getPropertyPrefix() + ".background"));
  231. }
  232. }
  233. }
  234. private class PropertyChangeHandler implements PropertyChangeListener {
  235. public void propertyChange(PropertyChangeEvent e) {
  236. String prop = e.getPropertyName();
  237. if(prop.equals(AbstractButton.MNEMONIC_CHANGED_PROPERTY)) {
  238. updateMnemonicBinding();
  239. } else if (prop == "ancestor") {
  240. updateDefaultBackgroundColor();
  241. }
  242. }
  243. }
  244. /**
  245. * Instantiated and used by a menu item to handle the current menu selection
  246. * from mouse events. A MouseInputHandler processes and forwards all mouse events
  247. * to a shared instance of the MenuSelectionManager.
  248. * <p>
  249. * This class is protected so that it can be subclassed by other look and
  250. * feels to implement their own mouse handling behavior. All overridden
  251. * methods should call the parent methods so that the menu selection
  252. * is correct.
  253. *
  254. * @see javax.swing.MenuSelectionManager
  255. * @since 1.4
  256. */
  257. protected class MouseInputHandler implements MouseInputListener {
  258. public void mouseClicked(MouseEvent e) {}
  259. /**
  260. * Invoked when the mouse has been clicked on the menu. This
  261. * method clears or sets the selection path of the
  262. * MenuSelectionManager.
  263. *
  264. * @param e the mouse event
  265. */
  266. public void mousePressed(MouseEvent e) {
  267. JMenu menu = (JMenu)menuItem;
  268. if (!menu.isEnabled())
  269. return;
  270. MenuSelectionManager manager =
  271. MenuSelectionManager.defaultManager();
  272. if(menu.isTopLevelMenu()) {
  273. if(menu.isSelected()) {
  274. manager.clearSelectedPath();
  275. } else {
  276. Container cnt = menu.getParent();
  277. if(cnt != null && cnt instanceof JMenuBar) {
  278. MenuElement me[] = new MenuElement[2];
  279. me[0]=(MenuElement)cnt;
  280. me[1]=menu;
  281. manager.setSelectedPath(me);
  282. }
  283. }
  284. }
  285. MenuElement selectedPath[] = manager.getSelectedPath();
  286. if (selectedPath.length > 0 &&
  287. selectedPath[selectedPath.length-1] != menu.getPopupMenu()) {
  288. if(menu.isTopLevelMenu() ||
  289. menu.getDelay() == 0) {
  290. appendPath(selectedPath, menu.getPopupMenu());
  291. } else {
  292. setupPostTimer(menu);
  293. }
  294. }
  295. }
  296. /**
  297. * Invoked when the mouse has been released on the menu. Delegates the
  298. * mouse event to the MenuSelectionManager.
  299. *
  300. * @param e the mouse event
  301. */
  302. public void mouseReleased(MouseEvent e) {
  303. JMenu menu = (JMenu)menuItem;
  304. if (!menu.isEnabled())
  305. return;
  306. MenuSelectionManager manager =
  307. MenuSelectionManager.defaultManager();
  308. manager.processMouseEvent(e);
  309. if (!e.isConsumed())
  310. manager.clearSelectedPath();
  311. }
  312. /**
  313. * Invoked when the cursor enters the menu. This method sets the selected
  314. * path for the MenuSelectionManager and handles the case
  315. * in which a menu item is used to pop up an additional menu, as in a
  316. * hierarchical menu system.
  317. *
  318. * @param e the mouse event; not used
  319. */
  320. public void mouseEntered(MouseEvent e) {
  321. JMenu menu = (JMenu)menuItem;
  322. if (!menu.isEnabled())
  323. return;
  324. MenuSelectionManager manager =
  325. MenuSelectionManager.defaultManager();
  326. MenuElement selectedPath[] = manager.getSelectedPath();
  327. if (!menu.isTopLevelMenu()) {
  328. if(!(selectedPath.length > 0 &&
  329. selectedPath[selectedPath.length-1] ==
  330. menu.getPopupMenu())) {
  331. if(menu.getDelay() == 0) {
  332. appendPath(getPath(), menu.getPopupMenu());
  333. } else {
  334. manager.setSelectedPath(getPath());
  335. setupPostTimer(menu);
  336. }
  337. }
  338. } else {
  339. if(selectedPath.length > 0 &&
  340. selectedPath[0] == menu.getParent()) {
  341. MenuElement newPath[] = new MenuElement[3];
  342. // A top level menu's parent is by definition
  343. // a JMenuBar
  344. newPath[0] = (MenuElement)menu.getParent();
  345. newPath[1] = menu;
  346. newPath[2] = menu.getPopupMenu();
  347. manager.setSelectedPath(newPath);
  348. }
  349. }
  350. }
  351. public void mouseExited(MouseEvent e) {
  352. }
  353. /**
  354. * Invoked when a mouse button is pressed on the menu and then dragged.
  355. * Delegates the mouse event to the MenuSelectionManager.
  356. *
  357. * @param e the mouse event
  358. * @see java.awt.event.MouseMotionListener#mouseDragged
  359. */
  360. public void mouseDragged(MouseEvent e) {
  361. JMenu menu = (JMenu)menuItem;
  362. if (!menu.isEnabled())
  363. return;
  364. MenuSelectionManager.defaultManager().processMouseEvent(e);
  365. }
  366. public void mouseMoved(MouseEvent e) {
  367. }
  368. }
  369. /**
  370. * As of Java 2 platform 1.4, this previously undocumented class
  371. * is now obsolete. KeyBindings are now managed by the popup menu.
  372. */
  373. public class ChangeHandler implements ChangeListener {
  374. public JMenu menu;
  375. public BasicMenuUI ui;
  376. public boolean isSelected = false;
  377. public Component wasFocused;
  378. public ChangeHandler(JMenu m, BasicMenuUI ui) {
  379. menu = m;
  380. this.ui = ui;
  381. }
  382. public void stateChanged(ChangeEvent e) { }
  383. }
  384. private class MenuDragMouseHandler implements MenuDragMouseListener {
  385. public void menuDragMouseEntered(MenuDragMouseEvent e) {}
  386. public void menuDragMouseDragged(MenuDragMouseEvent e) {
  387. if (menuItem.isEnabled() == false)
  388. return;
  389. MenuSelectionManager manager = e.getMenuSelectionManager();
  390. MenuElement path[] = e.getPath();
  391. Point p = e.getPoint();
  392. if(p.x >= 0 && p.x < menuItem.getWidth() &&
  393. p.y >= 0 && p.y < menuItem.getHeight()) {
  394. JMenu menu = (JMenu)menuItem;
  395. MenuElement selectedPath[] = manager.getSelectedPath();
  396. if(!(selectedPath.length > 0 &&
  397. selectedPath[selectedPath.length-1] ==
  398. menu.getPopupMenu())) {
  399. if(menu.isTopLevelMenu() ||
  400. menu.getDelay() == 0 ||
  401. e.getID() == MouseEvent.MOUSE_DRAGGED) {
  402. appendPath(path, menu.getPopupMenu());
  403. } else {
  404. manager.setSelectedPath(path);
  405. setupPostTimer(menu);
  406. }
  407. }
  408. } else if(e.getID() == MouseEvent.MOUSE_RELEASED) {
  409. Component comp = manager.componentForPoint(e.getComponent(), e.getPoint());
  410. if (comp == null)
  411. manager.clearSelectedPath();
  412. }
  413. }
  414. public void menuDragMouseExited(MenuDragMouseEvent e) {}
  415. public void menuDragMouseReleased(MenuDragMouseEvent e) {}
  416. }
  417. static JPopupMenu getActivePopupMenu() {
  418. MenuElement[] path = MenuSelectionManager.defaultManager().
  419. getSelectedPath();
  420. for (int i=path.length-1; i>=0; i--) {
  421. MenuElement elem = path[i];
  422. if (elem instanceof JPopupMenu) {
  423. return (JPopupMenu)elem;
  424. }
  425. }
  426. return null;
  427. }
  428. /**
  429. * Handles the mnemonic handling for the JMenu and JMenuItems.
  430. */
  431. private class MenuKeyHandler implements MenuKeyListener {
  432. /**
  433. * Opens the SubMenu
  434. */
  435. public void menuKeyTyped(MenuKeyEvent e) {
  436. if (DEBUG) {
  437. System.out.println("in BasicMenuUI.menuKeyTyped for " + menuItem.getText());
  438. }
  439. if (!crossMenuMnemonic) {
  440. JPopupMenu pm = getActivePopupMenu();
  441. if (pm != null && pm != menuItem.getParent()) {
  442. return;
  443. }
  444. }
  445. int key = menuItem.getMnemonic();
  446. if(key == 0)
  447. return;
  448. MenuElement path[] = e.getPath();
  449. if(lower((char)key) == lower(e.getKeyChar())) {
  450. JPopupMenu popupMenu = ((JMenu)menuItem).getPopupMenu();
  451. ArrayList newList = new ArrayList(Arrays.asList(path));
  452. newList.add(popupMenu);
  453. MenuElement sub[] = popupMenu.getSubElements();
  454. if(sub.length > 0) {
  455. newList.add(sub[0]);
  456. }
  457. MenuSelectionManager manager = e.getMenuSelectionManager();
  458. MenuElement newPath[] = new MenuElement[0];;
  459. newPath = (MenuElement[]) newList.toArray(newPath);
  460. manager.setSelectedPath(newPath);
  461. e.consume();
  462. }
  463. }
  464. /**
  465. * Handles the mnemonics for the menu items. Will also handle duplicate mnemonics.
  466. * Perhaps this should be moved into BasicPopupMenuUI. See 4670831
  467. */
  468. public void menuKeyPressed(MenuKeyEvent e) {
  469. if (DEBUG) {
  470. System.out.println("in BasicMenuUI.menuKeyPressed for " + menuItem.getText());
  471. }
  472. // Handle the case for Escape or Enter...
  473. char keyChar = e.getKeyChar();
  474. if (!Character.isLetterOrDigit(keyChar))
  475. return;
  476. MenuSelectionManager manager = e.getMenuSelectionManager();
  477. MenuElement path[] = e.getPath();
  478. MenuElement selectedPath[] = manager.getSelectedPath();
  479. for (int i = selectedPath.length - 1; i >=0; i--) {
  480. if (selectedPath[i] == menuItem) {
  481. JPopupMenu popupMenu = ((JMenu)menuItem).getPopupMenu();
  482. if(!popupMenu.isVisible()) {
  483. return; // Do not invoke items from invisible popup
  484. }
  485. MenuElement items[] = popupMenu.getSubElements();
  486. MenuElement currentItem = selectedPath[selectedPath.length - 1];
  487. int currentIndex = -1;
  488. int matches = 0;
  489. int firstMatch = -1;
  490. int indexes[] = null;
  491. for (int j = 0; j < items.length; j++) {
  492. int key = ((JMenuItem)items[j]).getMnemonic();
  493. if(lower((char)key) == lower(keyChar)) {
  494. if (matches == 0) {
  495. firstMatch = j;
  496. matches++;
  497. } else {
  498. if (indexes == null) {
  499. indexes = new int[items.length];
  500. indexes[0] = firstMatch;
  501. }
  502. indexes[matches++] = j;
  503. }
  504. }
  505. if (currentItem == items[j]) {
  506. currentIndex = matches - 1;
  507. }
  508. }
  509. if (matches == 0) {
  510. ; // no op (consume)
  511. } else if (matches == 1) {
  512. // Invoke the menu action
  513. JMenuItem item = (JMenuItem)items[firstMatch];
  514. if (!(item instanceof JMenu)) {
  515. // Let Submenus be handled by menuKeyTyped
  516. manager.clearSelectedPath();
  517. item.doClick();
  518. }
  519. } else {
  520. // Select the menu item with the matching mnemonic. If
  521. // the same mnemonic has been invoked then select the next
  522. // menu item in the cycle.
  523. MenuElement newItem = null;
  524. newItem = items[indexes[(currentIndex + 1) % matches]];
  525. MenuElement newPath[] = new MenuElement[path.length+2];
  526. System.arraycopy(path, 0, newPath, 0, path.length);
  527. newPath[path.length] = popupMenu;
  528. newPath[path.length+1] = newItem;
  529. manager.setSelectedPath(newPath);
  530. }
  531. e.consume();
  532. return;
  533. }
  534. }
  535. }
  536. public void menuKeyReleased(MenuKeyEvent e) {}
  537. private char lower(char keyChar) {
  538. return Character.toLowerCase(keyChar);
  539. }
  540. }
  541. }