1. /*
  2. * @(#)MenuSelectionManager.java 1.38 03/12/19
  3. *
  4. * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
  5. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
  6. */
  7. package javax.swing;
  8. import java.awt.*;
  9. import java.util.*;
  10. import java.awt.event.*;
  11. import javax.swing.event.*;
  12. /**
  13. * A MenuSelectionManager owns the selection in menu hierarchy.
  14. *
  15. * @version 1.38 12/19/03
  16. * @author Arnaud Weber
  17. */
  18. public class MenuSelectionManager {
  19. private static final MenuSelectionManager instance =
  20. new MenuSelectionManager();
  21. private Vector selection = new Vector();
  22. /* diagnostic aids -- should be false for production builds. */
  23. private static final boolean TRACE = false; // trace creates and disposes
  24. private static final boolean VERBOSE = false; // show reuse hits/misses
  25. private static final boolean DEBUG = false; // show bad params, misc.
  26. /**
  27. * Returns the default menu selection manager.
  28. *
  29. * @return a MenuSelectionManager object
  30. */
  31. public static MenuSelectionManager defaultManager() {
  32. return instance;
  33. }
  34. /**
  35. * Only one ChangeEvent is needed per button model instance since the
  36. * event's only state is the source property. The source of events
  37. * generated is always "this".
  38. */
  39. protected transient ChangeEvent changeEvent = null;
  40. protected EventListenerList listenerList = new EventListenerList();
  41. /**
  42. * Changes the selection in the menu hierarchy. The elements
  43. * in the array are sorted in order from the root menu
  44. * element to the currently selected menu element.
  45. * <p>
  46. * Note that this method is public but is used by the look and
  47. * feel engine and should not be called by client applications.
  48. *
  49. * @param path an array of <code>MenuElement</code> objects specifying
  50. * the selected path
  51. */
  52. public void setSelectedPath(MenuElement[] path) {
  53. int i,c;
  54. int currentSelectionCount = selection.size();
  55. int firstDifference = 0;
  56. if(path == null) {
  57. path = new MenuElement[0];
  58. }
  59. if (DEBUG) {
  60. System.out.print("Previous: "); printMenuElementArray(getSelectedPath());
  61. System.out.print("New: "); printMenuElementArray(path);
  62. }
  63. for(i=0,c=path.length;i<c;i++) {
  64. if(i < currentSelectionCount && (MenuElement)selection.elementAt(i) == path[i])
  65. firstDifference++;
  66. else
  67. break;
  68. }
  69. for(i=currentSelectionCount - 1 ; i >= firstDifference ; i--) {
  70. MenuElement me = (MenuElement)selection.elementAt(i);
  71. selection.removeElementAt(i);
  72. me.menuSelectionChanged(false);
  73. }
  74. for(i = firstDifference, c = path.length ; i < c ; i++) {
  75. if (path[i] != null) {
  76. selection.addElement(path[i]);
  77. path[i].menuSelectionChanged(true);
  78. }
  79. }
  80. fireStateChanged();
  81. }
  82. /**
  83. * Returns the path to the currently selected menu item
  84. *
  85. * @return an array of MenuElement objects representing the selected path
  86. */
  87. public MenuElement[] getSelectedPath() {
  88. MenuElement res[] = new MenuElement[selection.size()];
  89. int i,c;
  90. for(i=0,c=selection.size();i<c;i++)
  91. res[i] = (MenuElement) selection.elementAt(i);
  92. return res;
  93. }
  94. /**
  95. * Tell the menu selection to close and unselect all the menu components. Call this method
  96. * when a choice has been made
  97. */
  98. public void clearSelectedPath() {
  99. if (selection.size() > 0) {
  100. setSelectedPath(null);
  101. }
  102. }
  103. /**
  104. * Adds a ChangeListener to the button.
  105. *
  106. * @param l the listener to add
  107. */
  108. public void addChangeListener(ChangeListener l) {
  109. listenerList.add(ChangeListener.class, l);
  110. }
  111. /**
  112. * Removes a ChangeListener from the button.
  113. *
  114. * @param l the listener to remove
  115. */
  116. public void removeChangeListener(ChangeListener l) {
  117. listenerList.remove(ChangeListener.class, l);
  118. }
  119. /**
  120. * Returns an array of all the <code>ChangeListener</code>s added
  121. * to this MenuSelectionManager with addChangeListener().
  122. *
  123. * @return all of the <code>ChangeListener</code>s added or an empty
  124. * array if no listeners have been added
  125. * @since 1.4
  126. */
  127. public ChangeListener[] getChangeListeners() {
  128. return (ChangeListener[])listenerList.getListeners(
  129. ChangeListener.class);
  130. }
  131. /**
  132. * Notifies all listeners that have registered interest for
  133. * notification on this event type. The event instance
  134. * is created lazily.
  135. *
  136. * @see EventListenerList
  137. */
  138. protected void fireStateChanged() {
  139. // Guaranteed to return a non-null array
  140. Object[] listeners = listenerList.getListenerList();
  141. // Process the listeners last to first, notifying
  142. // those that are interested in this event
  143. for (int i = listeners.length-2; i>=0; i-=2) {
  144. if (listeners[i]==ChangeListener.class) {
  145. // Lazily create the event:
  146. if (changeEvent == null)
  147. changeEvent = new ChangeEvent(this);
  148. ((ChangeListener)listeners[i+1]).stateChanged(changeEvent);
  149. }
  150. }
  151. }
  152. /**
  153. * When a MenuElement receives an event from a MouseListener, it should never process the event
  154. * directly. Instead all MenuElements should call this method with the event.
  155. *
  156. * @param event a MouseEvent object
  157. */
  158. public void processMouseEvent(MouseEvent event) {
  159. int screenX,screenY;
  160. Point p;
  161. int i,c,j,d;
  162. Component mc;
  163. Rectangle r2;
  164. int cWidth,cHeight;
  165. MenuElement menuElement;
  166. MenuElement subElements[];
  167. MenuElement path[];
  168. Vector tmp;
  169. int selectionSize;
  170. p = event.getPoint();
  171. Component source = (Component)event.getSource();
  172. if (!source.isShowing()) {
  173. // This can happen if a mouseReleased removes the
  174. // containing component -- bug 4146684
  175. return;
  176. }
  177. int type = event.getID();
  178. int modifiers = event.getModifiers();
  179. // 4188027: drag enter/exit added in JDK 1.1.7A, JDK1.2
  180. if ((type==MouseEvent.MOUSE_ENTERED||
  181. type==MouseEvent.MOUSE_EXITED)
  182. && ((modifiers & (InputEvent.BUTTON1_MASK |
  183. InputEvent.BUTTON2_MASK | InputEvent.BUTTON3_MASK)) !=0 )) {
  184. return;
  185. }
  186. SwingUtilities.convertPointToScreen(p,source);
  187. screenX = p.x;
  188. screenY = p.y;
  189. tmp = (Vector)selection.clone();
  190. selectionSize = tmp.size();
  191. boolean success = false;
  192. for (i=selectionSize - 1;i >= 0 && success == false; i--) {
  193. menuElement = (MenuElement) tmp.elementAt(i);
  194. subElements = menuElement.getSubElements();
  195. path = null;
  196. for (j = 0, d = subElements.length;j < d && success == false; j++) {
  197. if (subElements[j] == null)
  198. continue;
  199. mc = subElements[j].getComponent();
  200. if(!mc.isShowing())
  201. continue;
  202. if(mc instanceof JComponent) {
  203. cWidth = ((JComponent)mc).getWidth();
  204. cHeight = ((JComponent)mc).getHeight();
  205. } else {
  206. r2 = mc.getBounds();
  207. cWidth = r2.width;
  208. cHeight = r2.height;
  209. }
  210. p.x = screenX;
  211. p.y = screenY;
  212. SwingUtilities.convertPointFromScreen(p,mc);
  213. /** Send the event to visible menu element if menu element currently in
  214. * the selected path or contains the event location
  215. */
  216. if(
  217. (p.x >= 0 && p.x < cWidth && p.y >= 0 && p.y < cHeight)) {
  218. int k;
  219. if(path == null) {
  220. path = new MenuElement[i+2];
  221. for(k=0;k<=i;k++)
  222. path[k] = (MenuElement)tmp.elementAt(k);
  223. }
  224. path[i+1] = subElements[j];
  225. MenuElement currentSelection[] = getSelectedPath();
  226. // Enter/exit detection -- needs tuning...
  227. if (currentSelection[currentSelection.length-1] !=
  228. path[i+1] &&
  229. (currentSelection.length < 2 ||
  230. currentSelection[currentSelection.length-2] !=
  231. path[i+1])) {
  232. Component oldMC = currentSelection[currentSelection.length-1].getComponent();
  233. MouseEvent exitEvent = new MouseEvent(oldMC, MouseEvent.MOUSE_EXITED,
  234. event.getWhen(),
  235. event.getModifiers(), p.x, p.y,
  236. event.getClickCount(),
  237. event.isPopupTrigger());
  238. currentSelection[currentSelection.length-1].
  239. processMouseEvent(exitEvent, path, this);
  240. MouseEvent enterEvent = new MouseEvent(mc,
  241. MouseEvent.MOUSE_ENTERED,
  242. event.getWhen(),
  243. event.getModifiers(), p.x, p.y,
  244. event.getClickCount(),
  245. event.isPopupTrigger());
  246. subElements[j].processMouseEvent(enterEvent, path, this);
  247. }
  248. MouseEvent mouseEvent = new MouseEvent(mc, event.getID(),event. getWhen(),
  249. event.getModifiers(), p.x, p.y,
  250. event.getClickCount(),
  251. event.isPopupTrigger());
  252. subElements[j].processMouseEvent(mouseEvent, path, this);
  253. success = true;
  254. event.consume();
  255. }
  256. }
  257. }
  258. }
  259. private void printMenuElementArray(MenuElement path[]) {
  260. printMenuElementArray(path, false);
  261. }
  262. private void printMenuElementArray(MenuElement path[], boolean dumpStack) {
  263. System.out.println("Path is(");
  264. int i, j;
  265. for(i=0,j=path.length; i<j ;i++){
  266. for (int k=0; k<=i; k++)
  267. System.out.print(" ");
  268. MenuElement me = (MenuElement) path[i];
  269. if(me instanceof JMenuItem) {
  270. System.out.println(((JMenuItem)me).getText() + ", ");
  271. } else if (me instanceof JMenuBar) {
  272. System.out.println("JMenuBar, ");
  273. } else if(me instanceof JPopupMenu) {
  274. System.out.println("JPopupMenu, ");
  275. } else if (me == null) {
  276. System.out.println("NULL , ");
  277. } else {
  278. System.out.println("" + me + ", ");
  279. }
  280. }
  281. System.out.println(")");
  282. if (dumpStack == true)
  283. Thread.dumpStack();
  284. }
  285. /**
  286. * Returns the component in the currently selected path
  287. * which contains sourcePoint.
  288. *
  289. * @param source The component in whose coordinate space sourcePoint
  290. * is given
  291. * @param sourcePoint The point which is being tested
  292. * @return The component in the currently selected path which
  293. * contains sourcePoint (relative to the source component's
  294. * coordinate space. If sourcePoint is not inside a component
  295. * on the currently selected path, null is returned.
  296. */
  297. public Component componentForPoint(Component source, Point sourcePoint) {
  298. int screenX,screenY;
  299. Point p = sourcePoint;
  300. int i,c,j,d;
  301. Component mc;
  302. Rectangle r2;
  303. int cWidth,cHeight;
  304. MenuElement menuElement;
  305. MenuElement subElements[];
  306. Vector tmp;
  307. int selectionSize;
  308. SwingUtilities.convertPointToScreen(p,source);
  309. screenX = p.x;
  310. screenY = p.y;
  311. tmp = (Vector)selection.clone();
  312. selectionSize = tmp.size();
  313. for(i=selectionSize - 1 ; i >= 0 ; i--) {
  314. menuElement = (MenuElement) tmp.elementAt(i);
  315. subElements = menuElement.getSubElements();
  316. for(j = 0, d = subElements.length ; j < d ; j++) {
  317. if (subElements[j] == null)
  318. continue;
  319. mc = subElements[j].getComponent();
  320. if(!mc.isShowing())
  321. continue;
  322. if(mc instanceof JComponent) {
  323. cWidth = ((JComponent)mc).getWidth();
  324. cHeight = ((JComponent)mc).getHeight();
  325. } else {
  326. r2 = mc.getBounds();
  327. cWidth = r2.width;
  328. cHeight = r2.height;
  329. }
  330. p.x = screenX;
  331. p.y = screenY;
  332. SwingUtilities.convertPointFromScreen(p,mc);
  333. /** Return the deepest component on the selection
  334. * path in whose bounds the event's point occurs
  335. */
  336. if (p.x >= 0 && p.x < cWidth && p.y >= 0 && p.y < cHeight) {
  337. return mc;
  338. }
  339. }
  340. }
  341. return null;
  342. }
  343. /**
  344. * When a MenuElement receives an event from a KeyListener, it should never process the event
  345. * directly. Instead all MenuElements should call this method with the event.
  346. *
  347. * @param e a KeyEvent object
  348. */
  349. public void processKeyEvent(KeyEvent e) {
  350. MenuElement[] sel2 = new MenuElement[0];
  351. sel2 = (MenuElement[])selection.toArray(sel2);
  352. int selSize = sel2.length;
  353. MenuElement[] path;
  354. if (selSize < 1) {
  355. return;
  356. }
  357. for (int i=selSize-1; i>=0; i--) {
  358. MenuElement elem = sel2[i];
  359. MenuElement[] subs = elem.getSubElements();
  360. path = null;
  361. for (int j=0; j<subs.length; j++) {
  362. if (subs[j] == null || !subs[j].getComponent().isShowing()
  363. || !subs[j].getComponent().isEnabled()) {
  364. continue;
  365. }
  366. if(path == null) {
  367. path = new MenuElement[i+2];
  368. System.arraycopy(sel2, 0, path, 0, i+1);
  369. }
  370. path[i+1] = subs[j];
  371. subs[j].processKeyEvent(e, path, this);
  372. if (e.isConsumed()) {
  373. return;
  374. }
  375. }
  376. }
  377. // finally dispatch event to the first component in path
  378. path = new MenuElement[1];
  379. path[0] = sel2[0];
  380. path[0].processKeyEvent(e, path, this);
  381. if (e.isConsumed()) {
  382. return;
  383. }
  384. }
  385. /**
  386. * Return true if c is part of the currently used menu
  387. */
  388. public boolean isComponentPartOfCurrentMenu(Component c) {
  389. if(selection.size() > 0) {
  390. MenuElement me = (MenuElement)selection.elementAt(0);
  391. return isComponentPartOfCurrentMenu(me,c);
  392. } else
  393. return false;
  394. }
  395. private boolean isComponentPartOfCurrentMenu(MenuElement root,Component c) {
  396. MenuElement children[];
  397. int i,d;
  398. if (root == null)
  399. return false;
  400. if(root.getComponent() == c)
  401. return true;
  402. else {
  403. children = root.getSubElements();
  404. for(i=0,d=children.length;i<d;i++) {
  405. if(isComponentPartOfCurrentMenu(children[i],c))
  406. return true;
  407. }
  408. }
  409. return false;
  410. }
  411. }