1. /*
  2. * @(#)BasicPopupMenuUI.java 1.101 03/01/31
  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 javax.swing.*;
  9. import javax.swing.event.*;
  10. import javax.swing.plaf.*;
  11. import javax.swing.border.*;
  12. import java.awt.Component;
  13. import java.awt.Container;
  14. import java.awt.Dimension;
  15. import java.awt.KeyboardFocusManager;
  16. import java.awt.Window;
  17. import java.awt.event.*;
  18. import java.beans.PropertyChangeListener;
  19. import java.beans.PropertyChangeEvent;
  20. import java.util.*;
  21. /**
  22. * A Windows L&F implementation of PopupMenuUI. This implementation
  23. * is a "combined" view/controller.
  24. *
  25. * @version 1.101 01/31/03
  26. * @author Georges Saab
  27. * @author David Karlton
  28. * @author Arnaud Weber
  29. */
  30. public class BasicPopupMenuUI extends PopupMenuUI {
  31. protected JPopupMenu popupMenu = null;
  32. static boolean menuKeyboardHelperInstalled = false;
  33. static MenuKeyboardHelper menuKeyboardHelper = null;
  34. private static boolean checkInvokerEqual(MenuElement present, MenuElement last) {
  35. Component invokerPresent = present.getComponent();
  36. Component invokerLast = last.getComponent();
  37. if (invokerPresent instanceof JPopupMenu) {
  38. invokerPresent = ((JPopupMenu)invokerPresent).getInvoker();
  39. }
  40. if (invokerLast instanceof JPopupMenu) {
  41. invokerLast = ((JPopupMenu)invokerLast).getInvoker();
  42. }
  43. return (invokerPresent == invokerLast);
  44. }
  45. public static ComponentUI createUI(JComponent x) {
  46. return new BasicPopupMenuUI();
  47. }
  48. public void installUI(JComponent c) {
  49. popupMenu = (JPopupMenu) c;
  50. installDefaults();
  51. installListeners();
  52. installKeyboardActions();
  53. }
  54. public void installDefaults() {
  55. if (popupMenu.getLayout() == null ||
  56. popupMenu.getLayout() instanceof UIResource)
  57. popupMenu.setLayout(new DefaultMenuLayout(popupMenu, BoxLayout.Y_AXIS));
  58. popupMenu.setOpaque(true);
  59. LookAndFeel.installBorder(popupMenu, "PopupMenu.border");
  60. LookAndFeel.installColorsAndFont(popupMenu,
  61. "PopupMenu.background",
  62. "PopupMenu.foreground",
  63. "PopupMenu.font");
  64. }
  65. protected void installListeners() {
  66. if (mouseGrabber == null) {
  67. mouseGrabber = new MouseGrabber();
  68. }
  69. if (basicPopupMenuListener == null) {
  70. basicPopupMenuListener = createPopupMenuListener();
  71. }
  72. popupMenu.addPopupMenuListener(basicPopupMenuListener);
  73. if (!menuKeyboardHelperInstalled) {
  74. if (menuKeyboardHelper == null) {
  75. menuKeyboardHelper = new MenuKeyboardHelper();
  76. }
  77. MenuSelectionManager msm = MenuSelectionManager.defaultManager();
  78. msm.addChangeListener(menuKeyboardHelper);
  79. }
  80. }
  81. protected void installKeyboardActions() {
  82. }
  83. static InputMap getInputMap(JPopupMenu popup, JComponent c) {
  84. InputMap windowInputMap = null;
  85. Object[] bindings = (Object[])UIManager.get("PopupMenu.selectedWindowInputMapBindings");
  86. if (bindings != null) {
  87. windowInputMap = LookAndFeel.makeComponentInputMap(c, bindings);
  88. if (!popup.getComponentOrientation().isLeftToRight()) {
  89. Object[] km = (Object[])UIManager.get("PopupMenu.selectedWindowInputMapBindings.RightToLeft");
  90. if (km != null) {
  91. InputMap rightToLeftInputMap = LookAndFeel.makeComponentInputMap(c, km);
  92. rightToLeftInputMap.setParent(windowInputMap);
  93. windowInputMap = rightToLeftInputMap;
  94. }
  95. }
  96. }
  97. return windowInputMap;
  98. }
  99. static ActionMap getActionMap() {
  100. ActionMap map = (ActionMap)UIManager.get("PopupMenu.actionMap");
  101. if (map == null) {
  102. map = createActionMap();
  103. if (map != null) {
  104. UIManager.getLookAndFeelDefaults().put("PopupMenu.actionMap",
  105. map);
  106. }
  107. }
  108. return map;
  109. }
  110. static ActionMap createActionMap() {
  111. ActionMap map = new ActionMapUIResource();
  112. // Set the ActionMap's parent to the Auditory Feedback Action Map
  113. BasicLookAndFeel lf = (BasicLookAndFeel)UIManager.getLookAndFeel();
  114. ActionMap audioMap = lf.getAudioActionMap();
  115. map.setParent(audioMap);
  116. map.put("cancel", new CancelAction());
  117. map.put("selectNext",
  118. new SelectNextItemAction(SelectNextItemAction.FORWARD));
  119. map.put("selectPrevious",
  120. new SelectNextItemAction(SelectNextItemAction.BACKWARD));
  121. map.put("selectParent",
  122. new SelectParentChildAction(SelectParentChildAction.PARENT));
  123. map.put("selectChild",
  124. new SelectParentChildAction(SelectParentChildAction.CHILD));
  125. map.put("return", new ReturnAction());
  126. // return the created map
  127. return map;
  128. }
  129. public void uninstallUI(JComponent c) {
  130. uninstallDefaults();
  131. uninstallListeners();
  132. uninstallKeyboardActions();
  133. popupMenu = null;
  134. }
  135. protected void uninstallDefaults() {
  136. LookAndFeel.uninstallBorder(popupMenu);
  137. }
  138. protected void uninstallListeners() {
  139. if (basicPopupMenuListener != null) {
  140. popupMenu.removePopupMenuListener(basicPopupMenuListener);
  141. }
  142. }
  143. protected void uninstallKeyboardActions() {
  144. SwingUtilities.replaceUIActionMap(popupMenu, null);
  145. SwingUtilities.replaceUIInputMap(popupMenu,
  146. JComponent.WHEN_IN_FOCUSED_WINDOW, null);
  147. }
  148. public Dimension getMinimumSize(JComponent c) {
  149. return getPreferredSize(c);
  150. }
  151. public Dimension getPreferredSize(JComponent c) {
  152. return null;
  153. }
  154. public Dimension getMaximumSize(JComponent c) {
  155. return getPreferredSize(c);
  156. }
  157. ///////////////////////////////////////////////////////////////////////////////
  158. //// Grab Code
  159. ///////////////////////////////////////////////////////////////////////////////
  160. private static Window getWindow(Component c) {
  161. Component w = c;
  162. while(!(w instanceof Window) && (w!=null)) {
  163. w = w.getParent();
  164. }
  165. return (Window)w;
  166. }
  167. private static MenuElement getFirstPopup() {
  168. MenuSelectionManager msm = MenuSelectionManager.defaultManager();
  169. MenuElement[] p = msm.getSelectedPath();
  170. MenuElement me = null;
  171. for(int i = 0 ; me == null && i < p.length ; i++) {
  172. if (p[i] instanceof JPopupMenu)
  173. me = p[i];
  174. }
  175. return me;
  176. }
  177. private static JPopupMenu getLastPopup() {
  178. MenuSelectionManager msm = MenuSelectionManager.defaultManager();
  179. MenuElement[] p = msm.getSelectedPath();
  180. JPopupMenu popup = null;
  181. for(int i = p.length - 1; popup == null && i >= 0; i--) {
  182. if (p[i] instanceof JPopupMenu)
  183. popup = (JPopupMenu)p[i];
  184. }
  185. return popup;
  186. }
  187. private static List getPopups() {
  188. MenuSelectionManager msm = MenuSelectionManager.defaultManager();
  189. MenuElement[] p = msm.getSelectedPath();
  190. List list = new ArrayList(p.length);
  191. for(int i = 0; i < p.length; i++) {
  192. if (p[i] instanceof JPopupMenu) {
  193. list.add((JPopupMenu)p[i]);
  194. }
  195. }
  196. return list;
  197. }
  198. private transient static MouseGrabber mouseGrabber = null;
  199. private static class MouseGrabber
  200. implements MouseListener, MouseMotionListener, MouseWheelListener,
  201. WindowListener, WindowFocusListener, ComponentListener, ChangeListener {
  202. Vector grabbed = new Vector();
  203. MenuElement lastGrabbed = null;
  204. boolean lastGrabbedMenuBarChild = false;
  205. public MouseGrabber() {
  206. MenuSelectionManager msm = MenuSelectionManager.defaultManager();
  207. msm.addChangeListener(this);
  208. }
  209. private void requestAddGrab(Component invoker) {
  210. Window ancestor;
  211. ancestor = getWindow(invoker);
  212. MenuSelectionManager msm = MenuSelectionManager.defaultManager();
  213. MenuElement[] p = msm.getSelectedPath();
  214. Component excluded = null;
  215. for(int i = 0 ; excluded == null && i < p.length ; i++) {
  216. if (p[i] instanceof JPopupMenu)
  217. excluded = p[i].getComponent();
  218. }
  219. grabContainer(ancestor, excluded);
  220. }
  221. private void requestRemoveGrab() {
  222. ungrabContainers();
  223. }
  224. void cancelPopupMenu() {
  225. MenuSelectionManager msm = MenuSelectionManager.defaultManager();
  226. MenuElement[] p = msm.getSelectedPath();
  227. JPopupMenu firstPopup = (JPopupMenu)getFirstPopup();
  228. if (lastGrabbed == firstPopup) {
  229. // 4234793: This action should call firePopupMenuCanceled but it's
  230. // a protected method. The real solution could be to make
  231. // firePopupMenuCanceled public and call it directly.
  232. List popups = getPopups();
  233. Iterator iter = popups.iterator();
  234. while (iter.hasNext()) {
  235. JPopupMenu popup = (JPopupMenu)iter.next();
  236. popup.putClientProperty("JPopupMenu.firePopupMenuCanceled", Boolean.TRUE);
  237. }
  238. MenuSelectionManager.defaultManager().clearSelectedPath();
  239. ungrabContainers();
  240. } else {
  241. // The cancel has cause another menu selection
  242. lastGrabbed = firstPopup;
  243. if (p[0] instanceof JMenuBar) {
  244. lastGrabbedMenuBarChild = true;
  245. } else {
  246. lastGrabbedMenuBarChild = false;
  247. }
  248. }
  249. }
  250. MenuElement[] lastPathSelected = new MenuElement[0];
  251. public void stateChanged(ChangeEvent e) {
  252. MenuSelectionManager msm = MenuSelectionManager.defaultManager();
  253. MenuElement[] p = msm.getSelectedPath();
  254. if (lastPathSelected.length != 0 && p.length != 0 ) {
  255. if (!checkInvokerEqual(p[0],lastPathSelected[0])) {
  256. requestRemoveGrab();
  257. lastPathSelected = new MenuElement[0];
  258. }
  259. }
  260. if (lastPathSelected.length == 0 &&
  261. p.length != 0) {
  262. // A grab needs to be added
  263. Component invoker = p[0].getComponent();
  264. if (invoker instanceof JPopupMenu)
  265. invoker = ((JPopupMenu)invoker).getInvoker();
  266. requestAddGrab(invoker);
  267. }
  268. if (lastPathSelected.length != 0 &&
  269. p.length == 0) {
  270. // The grab should be removed
  271. requestRemoveGrab();
  272. }
  273. // Switch between menubar children
  274. if (p!=null && p.length>2 && (p[0] instanceof JMenuBar &&
  275. lastGrabbedMenuBarChild == true)) {
  276. if (!(lastGrabbed==getFirstPopup())) {
  277. lastGrabbed=getFirstPopup();
  278. if (p[0] instanceof JMenuBar) {
  279. lastGrabbedMenuBarChild = true;
  280. } else {
  281. lastGrabbedMenuBarChild = false;
  282. }
  283. }
  284. }
  285. // Remember the last path selected
  286. lastPathSelected = p;
  287. }
  288. void grabContainer(Container c, Component excluded) {
  289. if(c == excluded)
  290. return;
  291. MenuSelectionManager msm = MenuSelectionManager.defaultManager();
  292. MenuElement[] p = msm.getSelectedPath();
  293. lastGrabbed = getFirstPopup();
  294. if (p[0] instanceof JMenuBar) {
  295. lastGrabbedMenuBarChild = true;
  296. } else {
  297. lastGrabbedMenuBarChild = false;
  298. }
  299. Component ignore = null;
  300. if(c instanceof java.awt.Window) {
  301. ((java.awt.Window)c).addWindowListener(this);
  302. ((java.awt.Window)c).addWindowFocusListener(this);
  303. ((java.awt.Window)c).addComponentListener(this);
  304. grabbed.addElement(c);
  305. }
  306. else if (c instanceof JRootPane) {
  307. // Only add listeners to the glass pane if it listeners,
  308. // otherwise if the glass pane is visible no mouse move
  309. // events will come to us.
  310. ignore = ((JRootPane)c).getGlassPane();
  311. if (ignore.getMouseListeners().length > 0 ||
  312. ignore.getMouseMotionListeners().length > 0 ||
  313. ignore.getMouseWheelListeners().length > 0) {
  314. ignore = null;
  315. }
  316. }
  317. synchronized(c.getTreeLock()) {
  318. int ncomponents = c.getComponentCount();
  319. Component[] component = c.getComponents();
  320. for (int i = 0 ; i < ncomponents ; i++) {
  321. Component comp = component[i];
  322. if(!comp.isVisible())
  323. continue;
  324. if (comp != ignore) {
  325. comp.addMouseListener(this);
  326. comp.addMouseMotionListener(this);
  327. comp.addMouseWheelListener(this);
  328. grabbed.addElement(comp);
  329. }
  330. if (comp instanceof Container) {
  331. Container cont = (Container) comp;
  332. grabContainer(cont, excluded);
  333. }
  334. }
  335. }
  336. }
  337. void ungrabContainers() {
  338. int i,c;
  339. Component cpn;
  340. for(i=0,c=grabbed.size();i<c;i++) {
  341. cpn = (Component)grabbed.elementAt(i);
  342. if(cpn instanceof java.awt.Window) {
  343. ((java.awt.Window)cpn).removeWindowListener(this);
  344. ((java.awt.Window)cpn).removeWindowFocusListener(this);
  345. ((java.awt.Window)cpn).removeComponentListener(this);
  346. } else {
  347. cpn.removeMouseListener(this);
  348. cpn.removeMouseMotionListener(this);
  349. cpn.removeMouseWheelListener(this);
  350. }
  351. }
  352. grabbed = new Vector();
  353. lastGrabbed = null;
  354. lastGrabbedMenuBarChild = false;
  355. }
  356. public void mousePressed(MouseEvent e) {
  357. Component c = (Component)e.getSource();
  358. MenuSelectionManager msm = MenuSelectionManager.defaultManager();
  359. cancelPopupMenu();
  360. /*
  361. if(msm.isComponentPartOfCurrentMenu(popupMenu) && msm.isComponentPartOfCurrentMenu(c)) {
  362. return;
  363. } else {
  364. cancelPopupMenu();
  365. }
  366. */
  367. }
  368. public void mouseReleased(MouseEvent e) {
  369. MenuSelectionManager.defaultManager().processMouseEvent(e);
  370. }
  371. public void mouseEntered(MouseEvent e) {
  372. // MenuSelectionManager.defaultManager().processMouseEvent(e);
  373. }
  374. public void mouseExited(MouseEvent e) {
  375. // MenuSelectionManager.defaultManager().processMouseEvent(e);
  376. }
  377. public void mouseMoved(MouseEvent e) {
  378. // MenuSelectionManager.defaultManager().processMouseEvent(e);
  379. }
  380. public void mouseDragged(MouseEvent e) {
  381. MenuSelectionManager.defaultManager().processMouseEvent(e);
  382. }
  383. public void mouseClicked(MouseEvent e) {
  384. }
  385. public void mouseWheelMoved(MouseWheelEvent e) {
  386. cancelPopupMenu();
  387. }
  388. public void componentResized(ComponentEvent e) {
  389. cancelPopupMenu();
  390. }
  391. public void componentMoved(ComponentEvent e) {
  392. cancelPopupMenu();
  393. }
  394. public void componentShown(ComponentEvent e) {
  395. cancelPopupMenu();
  396. }
  397. public void componentHidden(ComponentEvent e) {
  398. cancelPopupMenu();
  399. }
  400. public void windowOpened(WindowEvent e) {}
  401. public void windowClosing(WindowEvent e) {
  402. cancelPopupMenu();
  403. }
  404. public void windowClosed(WindowEvent e) {
  405. cancelPopupMenu();
  406. }
  407. public void windowIconified(WindowEvent e) {
  408. cancelPopupMenu();
  409. }
  410. public void windowDeiconified(WindowEvent e) {}
  411. public void windowActivated(WindowEvent e) {
  412. }
  413. public void windowDeactivated(WindowEvent e) {
  414. }
  415. public void windowLostFocus(WindowEvent e) {
  416. }
  417. public void windowGainedFocus(WindowEvent e) {
  418. }
  419. }
  420. public boolean isPopupTrigger(MouseEvent e) {
  421. return ((e.getID()==MouseEvent.MOUSE_RELEASED)
  422. && ((e.getModifiers() & MouseEvent.BUTTON3_MASK)!=0));
  423. }
  424. // for auditory feedback
  425. private transient PopupMenuListener basicPopupMenuListener = null;
  426. // for auditory feedback
  427. private PopupMenuListener createPopupMenuListener() {
  428. return new BasicPopupMenuListener();
  429. }
  430. /**
  431. * This Listener fires the Action that provides the correct auditory
  432. * feedback.
  433. *
  434. * @since 1.4
  435. */
  436. private class BasicPopupMenuListener implements PopupMenuListener {
  437. public void popupMenuCanceled(PopupMenuEvent e) {
  438. }
  439. public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
  440. }
  441. public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
  442. // Fire the Action that renders the auditory cue.
  443. JPopupMenu pm = (JPopupMenu)e.getSource();
  444. ActionMap map = pm.getActionMap();
  445. if (map != null) {
  446. Action audioAction = map.get("PopupMenu.popupSound");
  447. if (audioAction != null) {
  448. // pass off firing the Action to a utility method
  449. BasicLookAndFeel lf = (BasicLookAndFeel)
  450. UIManager.getLookAndFeel();
  451. lf.playSound(audioAction);
  452. }
  453. }
  454. }
  455. }
  456. private static class CancelAction extends AbstractAction {
  457. public void actionPerformed(ActionEvent e) {
  458. // 4234793: This action should call JPopupMenu.firePopupMenuCanceled but it's
  459. // a protected method. The real solution could be to make
  460. // firePopupMenuCanceled public and call it directly.
  461. JPopupMenu lastPopup = (JPopupMenu)getLastPopup();
  462. if (lastPopup != null) {
  463. lastPopup.putClientProperty("JPopupMenu.firePopupMenuCanceled", Boolean.TRUE);
  464. }
  465. MenuElement path[] = MenuSelectionManager.defaultManager().getSelectedPath();
  466. if(path.length > 4) { /* PENDING(arnaud) Change this to 2 when a mouse grabber is available for MenuBar */
  467. MenuElement newPath[] = new MenuElement[path.length - 2];
  468. System.arraycopy(path,0,newPath,0,path.length-2);
  469. MenuSelectionManager.defaultManager().setSelectedPath(newPath);
  470. } else
  471. MenuSelectionManager.defaultManager().clearSelectedPath();
  472. }
  473. }
  474. private static class ReturnAction extends AbstractAction {
  475. public void actionPerformed(ActionEvent e) {
  476. KeyboardFocusManager fmgr =
  477. KeyboardFocusManager.getCurrentKeyboardFocusManager();
  478. Component focusOwner = fmgr.getFocusOwner();
  479. if(focusOwner != null && !(focusOwner instanceof JRootPane)) {
  480. return;
  481. }
  482. MenuSelectionManager msm = MenuSelectionManager.defaultManager();
  483. MenuElement path[] = msm.getSelectedPath();
  484. MenuElement lastElement;
  485. if(path.length > 0) {
  486. lastElement = path[path.length-1];
  487. if(lastElement instanceof JMenu) {
  488. MenuElement newPath[] = new MenuElement[path.length+1];
  489. System.arraycopy(path,0,newPath,0,path.length);
  490. newPath[path.length] = ((JMenu)lastElement).getPopupMenu();
  491. msm.setSelectedPath(newPath);
  492. } else if(lastElement instanceof JMenuItem) {
  493. JMenuItem mi = (JMenuItem)lastElement;
  494. if (mi.getUI() instanceof BasicMenuItemUI) {
  495. ((BasicMenuItemUI)mi.getUI()).doClick(msm);
  496. }
  497. else {
  498. msm.clearSelectedPath();
  499. mi.doClick(0);
  500. }
  501. }
  502. }
  503. }
  504. }
  505. private static MenuElement nextEnabledChild(MenuElement e[],
  506. int fromIndex, int toIndex) {
  507. for (int i=fromIndex; i<=toIndex; i++) {
  508. if (e[i] != null) {
  509. Component comp = e[i].getComponent();
  510. if (comp != null && comp.isEnabled()) return e[i];
  511. }
  512. }
  513. return null;
  514. }
  515. private static MenuElement previousEnabledChild(MenuElement e[],
  516. int fromIndex, int toIndex) {
  517. for (int i=fromIndex; i>=toIndex; i--) {
  518. if (e[i] != null) {
  519. Component comp = e[i].getComponent();
  520. if (comp != null && comp.isEnabled()) return e[i];
  521. }
  522. }
  523. return null;
  524. }
  525. private static MenuElement findEnabledChild(MenuElement e[], int fromIndex,
  526. boolean forward) {
  527. MenuElement result = null;
  528. if (forward) {
  529. result = nextEnabledChild(e, fromIndex+1, e.length-1);
  530. if (result == null) result = nextEnabledChild(e, 0, fromIndex-1);
  531. } else {
  532. result = previousEnabledChild(e, fromIndex-1, 0);
  533. if (result == null) result = previousEnabledChild(e, e.length-1,
  534. fromIndex+1);
  535. }
  536. return result;
  537. }
  538. private static MenuElement findEnabledChild(MenuElement e[],
  539. MenuElement elem, boolean forward) {
  540. for (int i=0; i<e.length; i++) {
  541. if (e[i] == elem) {
  542. return findEnabledChild(e, i, forward);
  543. }
  544. }
  545. return null;
  546. }
  547. private static class SelectNextItemAction extends AbstractAction {
  548. static boolean FORWARD = true;
  549. static boolean BACKWARD = false;
  550. boolean direction;
  551. SelectNextItemAction(boolean dir) {
  552. direction = dir;
  553. }
  554. public void actionPerformed(ActionEvent e) {
  555. MenuSelectionManager msm = MenuSelectionManager.defaultManager();
  556. MenuElement path[] = msm.getSelectedPath();
  557. if (path.length < 2) {
  558. return;
  559. }
  560. int len = path.length;
  561. if (path[0] instanceof JMenuBar &&
  562. path[1] instanceof JMenu && len == 2) {
  563. // a toplevel menu is selected, but its popup not shown.
  564. // Show the popup and select the first item
  565. JPopupMenu popup = ((JMenu)path[1]).getPopupMenu();
  566. MenuElement next =
  567. findEnabledChild(popup.getSubElements(), -1, FORWARD);
  568. MenuElement[] newPath;
  569. if (next != null) {
  570. // an enabled item found -- include it in newPath
  571. newPath = new MenuElement[4];
  572. newPath[3] = next;
  573. } else {
  574. // menu has no enabled items -- still must show the popup
  575. newPath = new MenuElement[3];
  576. }
  577. System.arraycopy(path, 0, newPath, 0, 2);
  578. newPath[2] = popup;
  579. msm.setSelectedPath(newPath);
  580. } else if (path[len-1] instanceof JPopupMenu &&
  581. path[len-2] instanceof JMenu) {
  582. // a menu (not necessarily toplevel) is open and its popup
  583. // shown. Select the appropriate menu item
  584. JMenu menu = (JMenu)path[len-2];
  585. JPopupMenu popup = menu.getPopupMenu();
  586. MenuElement next =
  587. findEnabledChild(popup.getSubElements(), -1, direction);
  588. if (next != null) {
  589. MenuElement[] newPath = new MenuElement[len+1];
  590. System.arraycopy(path, 0, newPath, 0, len);
  591. newPath[len] = next;
  592. msm.setSelectedPath(newPath);
  593. } else {
  594. // all items in the popup are disabled.
  595. // We're going to find the parent popup menu and select
  596. // its next item. If there's no parent popup menu (i.e.
  597. // current menu is toplevel), do nothing
  598. if (len > 2 && path[len-3] instanceof JPopupMenu) {
  599. popup = ((JPopupMenu)path[len-3]);
  600. next = findEnabledChild(popup.getSubElements(),
  601. menu, direction);
  602. if (next != null && next != menu) {
  603. MenuElement[] newPath = new MenuElement[len-1];
  604. System.arraycopy(path, 0, newPath, 0, len-2);
  605. newPath[len-2] = next;
  606. msm.setSelectedPath(newPath);
  607. }
  608. }
  609. }
  610. } else {
  611. // just select the next item, no path expansion needed
  612. MenuElement subs[] = path[len-2].getSubElements();
  613. MenuElement nextChild =
  614. findEnabledChild(subs, path[len-1], direction);
  615. if (nextChild == null) {
  616. nextChild = findEnabledChild(subs, -1, direction);
  617. }
  618. if (nextChild != null) {
  619. path[len-1] = nextChild;
  620. msm.setSelectedPath(path);
  621. }
  622. }
  623. }
  624. }
  625. private static class SelectParentChildAction extends AbstractAction {
  626. static boolean PARENT = false;
  627. static boolean CHILD = true;
  628. boolean direction;
  629. SelectParentChildAction(boolean dir) {
  630. direction = dir;
  631. }
  632. public void actionPerformed(ActionEvent e) {
  633. MenuSelectionManager msm = MenuSelectionManager.defaultManager();
  634. MenuElement path[] = msm.getSelectedPath();
  635. int len = path.length;
  636. if (direction == PARENT) {
  637. // selecting parent
  638. int popupIndex = len-1;
  639. if (len > 2 &&
  640. // check if we have an open submenu. A submenu item may or
  641. // may not be selected, so submenu popup can be either the
  642. // last or next to the last item.
  643. (path[popupIndex] instanceof JPopupMenu ||
  644. path[--popupIndex] instanceof JPopupMenu) &&
  645. !((JMenu)path[popupIndex-1]).isTopLevelMenu()) {
  646. // we have a submenu, just close it
  647. MenuElement newPath[] = new MenuElement[popupIndex];
  648. System.arraycopy(path, 0, newPath, 0, popupIndex);
  649. msm.setSelectedPath(newPath);
  650. return;
  651. }
  652. } else {
  653. // selecting child
  654. if (len > 0 && path[len-1] instanceof JMenu &&
  655. !((JMenu)path[len-1]).isTopLevelMenu()) {
  656. // we have a submenu, open it
  657. JMenu menu = (JMenu)path[len-1];
  658. JPopupMenu popup = menu.getPopupMenu();
  659. MenuElement[] subs = popup.getSubElements();
  660. MenuElement item = findEnabledChild(subs, -1, true);
  661. MenuElement[] newPath;
  662. if (item == null) {
  663. newPath = new MenuElement[len+1];
  664. } else {
  665. newPath = new MenuElement[len+2];
  666. newPath[len+1] = item;
  667. }
  668. System.arraycopy(path, 0, newPath, 0, len);
  669. newPath[len] = popup;
  670. msm.setSelectedPath(newPath);
  671. return;
  672. }
  673. }
  674. // check if we have a toplevel menu selected.
  675. // If this is the case, we select another toplevel menu
  676. if (len > 1 && path[0] instanceof JMenuBar) {
  677. MenuElement currentMenu = path[1];
  678. MenuElement nextMenu = findEnabledChild(
  679. path[0].getSubElements(), currentMenu, direction);
  680. if (nextMenu != null && nextMenu != currentMenu) {
  681. MenuElement newSelection[];
  682. if (len == 2) {
  683. // menu is selected but its popup not shown
  684. newSelection = new MenuElement[2];
  685. newSelection[0] = path[0];
  686. newSelection[1] = nextMenu;
  687. } else {
  688. // menu is selected and its popup is shown
  689. newSelection = new MenuElement[3];
  690. newSelection[0] = path[0];
  691. newSelection[1] = nextMenu;
  692. newSelection[2] = ((JMenu)nextMenu).getPopupMenu();
  693. }
  694. msm.setSelectedPath(newSelection);
  695. }
  696. }
  697. }
  698. }
  699. /**
  700. * This helper is added to MenuSelectionManager as a ChangeListener to
  701. * listen to menu selection changes. When a menu is activated, it passes
  702. * focus to its parent JRootPane, and installs an ActionMap/InputMap pair
  703. * on that JRootPane. Those maps are necessary in order for menu
  704. * navigation to work. When menu is being deactivated, it restores focus
  705. * to the component that has had it before menu activation, and uninstalls
  706. * the maps.
  707. * This helper is also installed as a KeyListener on root pane when menu
  708. * is active. It forwards key events to MenuSelectionManager for mnemonic
  709. * keys handling.
  710. */
  711. private static class MenuKeyboardHelper
  712. implements ChangeListener, KeyListener {
  713. private Component lastFocused = null;
  714. private MenuElement[] lastPathSelected = new MenuElement[0];
  715. private JPopupMenu lastPopup;
  716. private JRootPane invokerRootPane;
  717. private ActionMap menuActionMap = getActionMap();
  718. private InputMap menuInputMap;
  719. private boolean focusTraversalKeysEnabled;
  720. /*
  721. * Fix for 4213634
  722. * If this is false, KEY_TYPED and KEY_RELEASED events are NOT
  723. * processed. This is needed to avoid activating a menuitem when
  724. * the menu and menuitem share the same mnemonic.
  725. */
  726. private boolean receivedKeyPressed = false;
  727. void removeItems() {
  728. if (lastFocused != null) {
  729. if(!lastFocused.requestFocusInWindow()) {
  730. // Workarounr for 4810575.
  731. // If lastFocused is not in currently focused window
  732. // requestFocusInWindow will fail. In this case we must
  733. // request focus by requestFocus() if it was not
  734. // transferred from our popup.
  735. Window cfw = KeyboardFocusManager
  736. .getCurrentKeyboardFocusManager()
  737. .getFocusedWindow();
  738. if(cfw != null &&
  739. "###focusableSwingPopup###".equals(cfw.getName())) {
  740. lastFocused.requestFocus();
  741. }
  742. }
  743. lastFocused = null;
  744. }
  745. if (invokerRootPane != null) {
  746. invokerRootPane.removeKeyListener(menuKeyboardHelper);
  747. invokerRootPane.setFocusTraversalKeysEnabled(focusTraversalKeysEnabled);
  748. removeUIInputMap(invokerRootPane, menuInputMap);
  749. removeUIActionMap(invokerRootPane, menuActionMap);
  750. invokerRootPane = null;
  751. }
  752. receivedKeyPressed = false;
  753. }
  754. private FocusListener rootPaneFocusListener = new FocusAdapter() {
  755. public void focusGained(FocusEvent ev) {
  756. Component opposite = ev.getOppositeComponent();
  757. if (opposite != null) {
  758. lastFocused = opposite;
  759. }
  760. ev.getComponent().removeFocusListener(this);
  761. }
  762. };
  763. /**
  764. * Return the last JPopupMenu in <code>path</code>,
  765. * or <code>null</code> if none found
  766. */
  767. JPopupMenu getActivePopup(MenuElement[] path) {
  768. for (int i=path.length-1; i>=0; i--) {
  769. MenuElement elem = path[i];
  770. if (elem instanceof JPopupMenu) {
  771. return (JPopupMenu)elem;
  772. }
  773. }
  774. return null;
  775. }
  776. void addUIInputMap(JComponent c, InputMap map) {
  777. InputMap lastNonUI = null;
  778. InputMap parent = c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
  779. while (parent != null && !(parent instanceof UIResource)) {
  780. lastNonUI = parent;
  781. parent = parent.getParent();
  782. }
  783. if (lastNonUI == null) {
  784. c.setInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW, map);
  785. } else {
  786. lastNonUI.setParent(map);
  787. }
  788. map.setParent(parent);
  789. }
  790. void addUIActionMap(JComponent c, ActionMap map) {
  791. ActionMap lastNonUI = null;
  792. ActionMap parent = c.getActionMap();
  793. while (parent != null && !(parent instanceof UIResource)) {
  794. lastNonUI = parent;
  795. parent = parent.getParent();
  796. }
  797. if (lastNonUI == null) {
  798. c.setActionMap(map);
  799. } else {
  800. lastNonUI.setParent(map);
  801. }
  802. map.setParent(parent);
  803. }
  804. void removeUIInputMap(JComponent c, InputMap map) {
  805. InputMap im = null;
  806. InputMap parent = c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
  807. while (parent != null) {
  808. if (parent == map) {
  809. if (im == null) {
  810. c.setInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW,
  811. map.getParent());
  812. } else {
  813. im.setParent(map.getParent());
  814. }
  815. break;
  816. }
  817. im = parent;
  818. parent = parent.getParent();
  819. }
  820. }
  821. void removeUIActionMap(JComponent c, ActionMap map) {
  822. ActionMap im = null;
  823. ActionMap parent = c.getActionMap();
  824. while (parent != null) {
  825. if (parent == map) {
  826. if (im == null) {
  827. c.setActionMap(map.getParent());
  828. } else {
  829. im.setParent(map.getParent());
  830. }
  831. break;
  832. }
  833. im = parent;
  834. parent = parent.getParent();
  835. }
  836. }
  837. public void stateChanged(ChangeEvent ev) {
  838. if (!(UIManager.getLookAndFeel() instanceof BasicLookAndFeel)) {
  839. MenuSelectionManager msm = MenuSelectionManager.
  840. defaultManager();
  841. msm.removeChangeListener(this);
  842. menuKeyboardHelperInstalled = false;
  843. return;
  844. }
  845. MenuSelectionManager msm = (MenuSelectionManager)ev.getSource();
  846. MenuElement[] p = msm.getSelectedPath();
  847. JPopupMenu popup = getActivePopup(p);
  848. if (popup != null && !popup.isFocusable()) {
  849. // Do nothing for non-focusable popups
  850. return;
  851. }
  852. if (lastPathSelected.length != 0 && p.length != 0 ) {
  853. if (!checkInvokerEqual(p[0],lastPathSelected[0])) {
  854. removeItems();
  855. lastPathSelected = new MenuElement[0];
  856. }
  857. }
  858. if (lastPathSelected.length == 0 && p.length > 0) {
  859. // menu posted
  860. JComponent invoker;
  861. if (popup == null) {
  862. if (p.length == 2 && p[0] instanceof JMenuBar &&
  863. p[1] instanceof JMenu) {
  864. // a menu has been selected but not open
  865. invoker = (JComponent)p[1];
  866. popup = ((JMenu)invoker).getPopupMenu();
  867. } else {
  868. return;
  869. }
  870. } else {
  871. Component c = popup.getInvoker();
  872. if(c instanceof JFrame) {
  873. invoker = ((JFrame)c).getRootPane();
  874. } else if(c instanceof JApplet) {
  875. invoker = ((JApplet)c).getRootPane();
  876. } else {
  877. while (!(c instanceof JComponent)) {
  878. if (c == null) {
  879. return;
  880. }
  881. c = c.getParent();
  882. }
  883. invoker = (JComponent)c;
  884. }
  885. }
  886. // remember current focus owner
  887. lastFocused = KeyboardFocusManager.
  888. getCurrentKeyboardFocusManager().getFocusOwner();
  889. // request focus on root pane and install keybindings
  890. // used for menu navigation
  891. invokerRootPane = SwingUtilities.getRootPane(invoker);
  892. if (invokerRootPane != null) {
  893. invokerRootPane.addFocusListener(rootPaneFocusListener);
  894. invokerRootPane.requestFocus(true);
  895. invokerRootPane.addKeyListener(menuKeyboardHelper);
  896. focusTraversalKeysEnabled = invokerRootPane.
  897. getFocusTraversalKeysEnabled();
  898. invokerRootPane.setFocusTraversalKeysEnabled(false);
  899. menuInputMap = getInputMap(popup, invokerRootPane);
  900. addUIInputMap(invokerRootPane, menuInputMap);
  901. addUIActionMap(invokerRootPane, menuActionMap);
  902. }
  903. } else if (lastPathSelected.length != 0 && p.length == 0) {
  904. // menu hidden -- return focus to where it had been before
  905. // and uninstall menu keybindings
  906. removeItems();
  907. } else {
  908. if (popup != lastPopup) {
  909. receivedKeyPressed = false;
  910. }
  911. }
  912. // Remember the last path selected
  913. lastPathSelected = p;
  914. lastPopup = popup;
  915. }
  916. public void keyPressed(KeyEvent ev) {
  917. receivedKeyPressed = true;
  918. MenuSelectionManager.defaultManager().processKeyEvent(ev);
  919. }
  920. public void keyReleased(KeyEvent ev) {
  921. if (receivedKeyPressed) {
  922. receivedKeyPressed = false;
  923. MenuSelectionManager.defaultManager().processKeyEvent(ev);
  924. }
  925. }
  926. public void keyTyped(KeyEvent ev) {
  927. if (receivedKeyPressed) {
  928. MenuSelectionManager.defaultManager().processKeyEvent(ev);
  929. }
  930. }
  931. }
  932. }