blob: 1070f35983c20a7f7596f47f4d81e084040dc5dd [file] [log] [blame]
J. Duke319a3b92007-12-01 00:00:00 +00001/*
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 */
25package javax.swing;
26
27import java.awt.*;
28import java.util.*;
29import java.awt.event.*;
30import javax.swing.event.*;
31
32import sun.awt.AppContext;
33
34/**
35 * A MenuSelectionManager owns the selection in menu hierarchy.
36 *
37 * @author Arnaud Weber
38 */
39public 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 = (Component)event.getSource();
222
223 if (!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 SwingUtilities.convertPointToScreen(p,source);
240
241 screenX = p.x;
242 screenY = p.y;
243
244 tmp = (Vector)selection.clone();
245 selectionSize = tmp.size();
246 boolean success = false;
247 for (i=selectionSize - 1;i >= 0 && success == false; i--) {
248 menuElement = (MenuElement) tmp.elementAt(i);
249 subElements = menuElement.getSubElements();
250
251 path = null;
252 for (j = 0, d = subElements.length;j < d && success == false; j++) {
253 if (subElements[j] == null)
254 continue;
255 mc = subElements[j].getComponent();
256 if(!mc.isShowing())
257 continue;
258 if(mc instanceof JComponent) {
259 cWidth = ((JComponent)mc).getWidth();
260 cHeight = ((JComponent)mc).getHeight();
261 } else {
262 r2 = mc.getBounds();
263 cWidth = r2.width;
264 cHeight = r2.height;
265 }
266 p.x = screenX;
267 p.y = screenY;
268 SwingUtilities.convertPointFromScreen(p,mc);
269
270 /** Send the event to visible menu element if menu element currently in
271 * the selected path or contains the event location
272 */
273 if(
274 (p.x >= 0 && p.x < cWidth && p.y >= 0 && p.y < cHeight)) {
275 int k;
276 if(path == null) {
277 path = new MenuElement[i+2];
278 for(k=0;k<=i;k++)
279 path[k] = (MenuElement)tmp.elementAt(k);
280 }
281 path[i+1] = subElements[j];
282 MenuElement currentSelection[] = getSelectedPath();
283
284 // Enter/exit detection -- needs tuning...
285 if (currentSelection[currentSelection.length-1] !=
286 path[i+1] &&
287 (currentSelection.length < 2 ||
288 currentSelection[currentSelection.length-2] !=
289 path[i+1])) {
290 Component oldMC = currentSelection[currentSelection.length-1].getComponent();
291
292 MouseEvent exitEvent = new MouseEvent(oldMC, MouseEvent.MOUSE_EXITED,
293 event.getWhen(),
294 event.getModifiers(), p.x, p.y,
295 event.getXOnScreen(),
296 event.getYOnScreen(),
297 event.getClickCount(),
298 event.isPopupTrigger(),
299 MouseEvent.NOBUTTON);
300 currentSelection[currentSelection.length-1].
301 processMouseEvent(exitEvent, path, this);
302
303 MouseEvent enterEvent = new MouseEvent(mc,
304 MouseEvent.MOUSE_ENTERED,
305 event.getWhen(),
306 event.getModifiers(), p.x, p.y,
307 event.getXOnScreen(),
308 event.getYOnScreen(),
309 event.getClickCount(),
310 event.isPopupTrigger(),
311 MouseEvent.NOBUTTON);
312 subElements[j].processMouseEvent(enterEvent, path, this);
313 }
314 MouseEvent mouseEvent = new MouseEvent(mc, event.getID(),event. getWhen(),
315 event.getModifiers(), p.x, p.y,
316 event.getXOnScreen(),
317 event.getYOnScreen(),
318 event.getClickCount(),
319 event.isPopupTrigger(),
320 MouseEvent.NOBUTTON);
321 subElements[j].processMouseEvent(mouseEvent, path, this);
322 success = true;
323 event.consume();
324 }
325 }
326 }
327 }
328
329 private void printMenuElementArray(MenuElement path[]) {
330 printMenuElementArray(path, false);
331 }
332
333 private void printMenuElementArray(MenuElement path[], boolean dumpStack) {
334 System.out.println("Path is(");
335 int i, j;
336 for(i=0,j=path.length; i<j ;i++){
337 for (int k=0; k<=i; k++)
338 System.out.print(" ");
339 MenuElement me = (MenuElement) path[i];
340 if(me instanceof JMenuItem) {
341 System.out.println(((JMenuItem)me).getText() + ", ");
342 } else if (me instanceof JMenuBar) {
343 System.out.println("JMenuBar, ");
344 } else if(me instanceof JPopupMenu) {
345 System.out.println("JPopupMenu, ");
346 } else if (me == null) {
347 System.out.println("NULL , ");
348 } else {
349 System.out.println("" + me + ", ");
350 }
351 }
352 System.out.println(")");
353
354 if (dumpStack == true)
355 Thread.dumpStack();
356 }
357
358 /**
359 * Returns the component in the currently selected path
360 * which contains sourcePoint.
361 *
362 * @param source The component in whose coordinate space sourcePoint
363 * is given
364 * @param sourcePoint The point which is being tested
365 * @return The component in the currently selected path which
366 * contains sourcePoint (relative to the source component's
367 * coordinate space. If sourcePoint is not inside a component
368 * on the currently selected path, null is returned.
369 */
370 public Component componentForPoint(Component source, Point sourcePoint) {
371 int screenX,screenY;
372 Point p = sourcePoint;
373 int i,c,j,d;
374 Component mc;
375 Rectangle r2;
376 int cWidth,cHeight;
377 MenuElement menuElement;
378 MenuElement subElements[];
379 Vector tmp;
380 int selectionSize;
381
382 SwingUtilities.convertPointToScreen(p,source);
383
384 screenX = p.x;
385 screenY = p.y;
386
387 tmp = (Vector)selection.clone();
388 selectionSize = tmp.size();
389 for(i=selectionSize - 1 ; i >= 0 ; i--) {
390 menuElement = (MenuElement) tmp.elementAt(i);
391 subElements = menuElement.getSubElements();
392
393 for(j = 0, d = subElements.length ; j < d ; j++) {
394 if (subElements[j] == null)
395 continue;
396 mc = subElements[j].getComponent();
397 if(!mc.isShowing())
398 continue;
399 if(mc instanceof JComponent) {
400 cWidth = ((JComponent)mc).getWidth();
401 cHeight = ((JComponent)mc).getHeight();
402 } else {
403 r2 = mc.getBounds();
404 cWidth = r2.width;
405 cHeight = r2.height;
406 }
407 p.x = screenX;
408 p.y = screenY;
409 SwingUtilities.convertPointFromScreen(p,mc);
410
411 /** Return the deepest component on the selection
412 * path in whose bounds the event's point occurs
413 */
414 if (p.x >= 0 && p.x < cWidth && p.y >= 0 && p.y < cHeight) {
415 return mc;
416 }
417 }
418 }
419 return null;
420 }
421
422 /**
423 * When a MenuElement receives an event from a KeyListener, it should never process the event
424 * directly. Instead all MenuElements should call this method with the event.
425 *
426 * @param e a KeyEvent object
427 */
428 public void processKeyEvent(KeyEvent e) {
429 MenuElement[] sel2 = new MenuElement[0];
430 sel2 = (MenuElement[])selection.toArray(sel2);
431 int selSize = sel2.length;
432 MenuElement[] path;
433
434 if (selSize < 1) {
435 return;
436 }
437
438 for (int i=selSize-1; i>=0; i--) {
439 MenuElement elem = sel2[i];
440 MenuElement[] subs = elem.getSubElements();
441 path = null;
442
443 for (int j=0; j<subs.length; j++) {
444 if (subs[j] == null || !subs[j].getComponent().isShowing()
445 || !subs[j].getComponent().isEnabled()) {
446 continue;
447 }
448
449 if(path == null) {
450 path = new MenuElement[i+2];
451 System.arraycopy(sel2, 0, path, 0, i+1);
452 }
453 path[i+1] = subs[j];
454 subs[j].processKeyEvent(e, path, this);
455 if (e.isConsumed()) {
456 return;
457 }
458 }
459 }
460
461 // finally dispatch event to the first component in path
462 path = new MenuElement[1];
463 path[0] = sel2[0];
464 path[0].processKeyEvent(e, path, this);
465 if (e.isConsumed()) {
466 return;
467 }
468 }
469
470 /**
471 * Return true if c is part of the currently used menu
472 */
473 public boolean isComponentPartOfCurrentMenu(Component c) {
474 if(selection.size() > 0) {
475 MenuElement me = (MenuElement)selection.elementAt(0);
476 return isComponentPartOfCurrentMenu(me,c);
477 } else
478 return false;
479 }
480
481 private boolean isComponentPartOfCurrentMenu(MenuElement root,Component c) {
482 MenuElement children[];
483 int i,d;
484
485 if (root == null)
486 return false;
487
488 if(root.getComponent() == c)
489 return true;
490 else {
491 children = root.getSubElements();
492 for(i=0,d=children.length;i<d;i++) {
493 if(isComponentPartOfCurrentMenu(children[i],c))
494 return true;
495 }
496 }
497 return false;
498 }
499}