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