001 /*
002 * Copyright 1997-2006 Sun Microsystems, Inc. All Rights Reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation. Sun designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Sun in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
022 * CA 95054 USA or visit www.sun.com if you need additional information or
023 * have any questions.
024 */
025 package javax.swing;
026
027 import java.awt.*;
028 import java.util.*;
029 import java.awt.event.*;
030 import javax.swing.event.*;
031
032 import sun.awt.AppContext;
033
034 /**
035 * A MenuSelectionManager owns the selection in menu hierarchy.
036 *
037 * @version 1.47 05/05/07
038 * @author Arnaud Weber
039 */
040 public class MenuSelectionManager {
041 private Vector selection = new Vector();
042
043 /* diagnostic aids -- should be false for production builds. */
044 private static final boolean TRACE = false; // trace creates and disposes
045 private static final boolean VERBOSE = false; // show reuse hits/misses
046 private static final boolean DEBUG = false; // show bad params, misc.
047
048 private static final StringBuilder MENU_SELECTION_MANAGER_KEY = new StringBuilder(
049 "javax.swing.MenuSelectionManager");
050
051 /**
052 * Returns the default menu selection manager.
053 *
054 * @return a MenuSelectionManager object
055 */
056 public static MenuSelectionManager defaultManager() {
057 synchronized (MENU_SELECTION_MANAGER_KEY) {
058 AppContext context = AppContext.getAppContext();
059 MenuSelectionManager msm = (MenuSelectionManager) context
060 .get(MENU_SELECTION_MANAGER_KEY);
061 if (msm == null) {
062 msm = new MenuSelectionManager();
063 context.put(MENU_SELECTION_MANAGER_KEY, msm);
064 }
065
066 return msm;
067 }
068 }
069
070 /**
071 * Only one ChangeEvent is needed per button model instance since the
072 * event's only state is the source property. The source of events
073 * generated is always "this".
074 */
075 protected transient ChangeEvent changeEvent = null;
076 protected EventListenerList listenerList = new EventListenerList();
077
078 /**
079 * Changes the selection in the menu hierarchy. The elements
080 * in the array are sorted in order from the root menu
081 * element to the currently selected menu element.
082 * <p>
083 * Note that this method is public but is used by the look and
084 * feel engine and should not be called by client applications.
085 *
086 * @param path an array of <code>MenuElement</code> objects specifying
087 * the selected path
088 */
089 public void setSelectedPath(MenuElement[] path) {
090 int i, c;
091 int currentSelectionCount = selection.size();
092 int firstDifference = 0;
093
094 if (path == null) {
095 path = new MenuElement[0];
096 }
097
098 if (DEBUG) {
099 System.out.print("Previous: ");
100 printMenuElementArray(getSelectedPath());
101 System.out.print("New: ");
102 printMenuElementArray(path);
103 }
104
105 for (i = 0, c = path.length; i < c; i++) {
106 if (i < currentSelectionCount
107 && (MenuElement) selection.elementAt(i) == path[i])
108 firstDifference++;
109 else
110 break;
111 }
112
113 for (i = currentSelectionCount - 1; i >= firstDifference; i--) {
114 MenuElement me = (MenuElement) selection.elementAt(i);
115 selection.removeElementAt(i);
116 me.menuSelectionChanged(false);
117 }
118
119 for (i = firstDifference, c = path.length; i < c; i++) {
120 if (path[i] != null) {
121 selection.addElement(path[i]);
122 path[i].menuSelectionChanged(true);
123 }
124 }
125
126 fireStateChanged();
127 }
128
129 /**
130 * Returns the path to the currently selected menu item
131 *
132 * @return an array of MenuElement objects representing the selected path
133 */
134 public MenuElement[] getSelectedPath() {
135 MenuElement res[] = new MenuElement[selection.size()];
136 int i, c;
137 for (i = 0, c = selection.size(); i < c; i++)
138 res[i] = (MenuElement) selection.elementAt(i);
139 return res;
140 }
141
142 /**
143 * Tell the menu selection to close and unselect all the menu components. Call this method
144 * when a choice has been made
145 */
146 public void clearSelectedPath() {
147 if (selection.size() > 0) {
148 setSelectedPath(null);
149 }
150 }
151
152 /**
153 * Adds a ChangeListener to the button.
154 *
155 * @param l the listener to add
156 */
157 public void addChangeListener(ChangeListener l) {
158 listenerList.add(ChangeListener.class, l);
159 }
160
161 /**
162 * Removes a ChangeListener from the button.
163 *
164 * @param l the listener to remove
165 */
166 public void removeChangeListener(ChangeListener l) {
167 listenerList.remove(ChangeListener.class, l);
168 }
169
170 /**
171 * Returns an array of all the <code>ChangeListener</code>s added
172 * to this MenuSelectionManager with addChangeListener().
173 *
174 * @return all of the <code>ChangeListener</code>s added or an empty
175 * array if no listeners have been added
176 * @since 1.4
177 */
178 public ChangeListener[] getChangeListeners() {
179 return (ChangeListener[]) listenerList
180 .getListeners(ChangeListener.class);
181 }
182
183 /**
184 * Notifies all listeners that have registered interest for
185 * notification on this event type. The event instance
186 * is created lazily.
187 *
188 * @see EventListenerList
189 */
190 protected void fireStateChanged() {
191 // Guaranteed to return a non-null array
192 Object[] listeners = listenerList.getListenerList();
193 // Process the listeners last to first, notifying
194 // those that are interested in this event
195 for (int i = listeners.length - 2; i >= 0; i -= 2) {
196 if (listeners[i] == ChangeListener.class) {
197 // Lazily create the event:
198 if (changeEvent == null)
199 changeEvent = new ChangeEvent(this );
200 ((ChangeListener) listeners[i + 1])
201 .stateChanged(changeEvent);
202 }
203 }
204 }
205
206 /**
207 * When a MenuElement receives an event from a MouseListener, it should never process the event
208 * directly. Instead all MenuElements should call this method with the event.
209 *
210 * @param event a MouseEvent object
211 */
212 public void processMouseEvent(MouseEvent event) {
213 int screenX, screenY;
214 Point p;
215 int i, c, j, d;
216 Component mc;
217 Rectangle r2;
218 int cWidth, cHeight;
219 MenuElement menuElement;
220 MenuElement subElements[];
221 MenuElement path[];
222 Vector tmp;
223 int selectionSize;
224 p = event.getPoint();
225
226 Component source = (Component) event.getSource();
227
228 if (!source.isShowing()) {
229 // This can happen if a mouseReleased removes the
230 // containing component -- bug 4146684
231 return;
232 }
233
234 int type = event.getID();
235 int modifiers = event.getModifiers();
236 // 4188027: drag enter/exit added in JDK 1.1.7A, JDK1.2
237 if ((type == MouseEvent.MOUSE_ENTERED || type == MouseEvent.MOUSE_EXITED)
238 && ((modifiers & (InputEvent.BUTTON1_MASK
239 | InputEvent.BUTTON2_MASK | InputEvent.BUTTON3_MASK)) != 0)) {
240 return;
241 }
242
243 SwingUtilities.convertPointToScreen(p, source);
244
245 screenX = p.x;
246 screenY = p.y;
247
248 tmp = (Vector) selection.clone();
249 selectionSize = tmp.size();
250 boolean success = false;
251 for (i = selectionSize - 1; i >= 0 && success == false; i--) {
252 menuElement = (MenuElement) tmp.elementAt(i);
253 subElements = menuElement.getSubElements();
254
255 path = null;
256 for (j = 0, d = subElements.length; j < d
257 && success == false; j++) {
258 if (subElements[j] == null)
259 continue;
260 mc = subElements[j].getComponent();
261 if (!mc.isShowing())
262 continue;
263 if (mc instanceof JComponent) {
264 cWidth = ((JComponent) mc).getWidth();
265 cHeight = ((JComponent) mc).getHeight();
266 } else {
267 r2 = mc.getBounds();
268 cWidth = r2.width;
269 cHeight = r2.height;
270 }
271 p.x = screenX;
272 p.y = screenY;
273 SwingUtilities.convertPointFromScreen(p, mc);
274
275 /** Send the event to visible menu element if menu element currently in
276 * the selected path or contains the event location
277 */
278 if ((p.x >= 0 && p.x < cWidth && p.y >= 0 && p.y < cHeight)) {
279 int k;
280 if (path == null) {
281 path = new MenuElement[i + 2];
282 for (k = 0; k <= i; k++)
283 path[k] = (MenuElement) tmp.elementAt(k);
284 }
285 path[i + 1] = subElements[j];
286 MenuElement currentSelection[] = getSelectedPath();
287
288 // Enter/exit detection -- needs tuning...
289 if (currentSelection[currentSelection.length - 1] != path[i + 1]
290 && (currentSelection.length < 2 || currentSelection[currentSelection.length - 2] != path[i + 1])) {
291 Component oldMC = currentSelection[currentSelection.length - 1]
292 .getComponent();
293
294 MouseEvent exitEvent = new MouseEvent(oldMC,
295 MouseEvent.MOUSE_EXITED, event
296 .getWhen(), event
297 .getModifiers(), p.x, p.y,
298 event.getXOnScreen(), event
299 .getYOnScreen(), event
300 .getClickCount(), event
301 .isPopupTrigger(),
302 MouseEvent.NOBUTTON);
303 currentSelection[currentSelection.length - 1]
304 .processMouseEvent(exitEvent, path,
305 this );
306
307 MouseEvent enterEvent = new MouseEvent(mc,
308 MouseEvent.MOUSE_ENTERED, event
309 .getWhen(), event
310 .getModifiers(), p.x, p.y,
311 event.getXOnScreen(), event
312 .getYOnScreen(), event
313 .getClickCount(), event
314 .isPopupTrigger(),
315 MouseEvent.NOBUTTON);
316 subElements[j].processMouseEvent(enterEvent,
317 path, this );
318 }
319 MouseEvent mouseEvent = new MouseEvent(mc, event
320 .getID(), event.getWhen(), event
321 .getModifiers(), p.x, p.y, event
322 .getXOnScreen(), event.getYOnScreen(),
323 event.getClickCount(), event
324 .isPopupTrigger(),
325 MouseEvent.NOBUTTON);
326 subElements[j].processMouseEvent(mouseEvent, path,
327 this );
328 success = true;
329 event.consume();
330 }
331 }
332 }
333 }
334
335 private void printMenuElementArray(MenuElement path[]) {
336 printMenuElementArray(path, false);
337 }
338
339 private void printMenuElementArray(MenuElement path[],
340 boolean dumpStack) {
341 System.out.println("Path is(");
342 int i, j;
343 for (i = 0, j = path.length; i < j; i++) {
344 for (int k = 0; k <= i; k++)
345 System.out.print(" ");
346 MenuElement me = (MenuElement) path[i];
347 if (me instanceof JMenuItem) {
348 System.out.println(((JMenuItem) me).getText() + ", ");
349 } else if (me instanceof JMenuBar) {
350 System.out.println("JMenuBar, ");
351 } else if (me instanceof JPopupMenu) {
352 System.out.println("JPopupMenu, ");
353 } else if (me == null) {
354 System.out.println("NULL , ");
355 } else {
356 System.out.println("" + me + ", ");
357 }
358 }
359 System.out.println(")");
360
361 if (dumpStack == true)
362 Thread.dumpStack();
363 }
364
365 /**
366 * Returns the component in the currently selected path
367 * which contains sourcePoint.
368 *
369 * @param source The component in whose coordinate space sourcePoint
370 * is given
371 * @param sourcePoint The point which is being tested
372 * @return The component in the currently selected path which
373 * contains sourcePoint (relative to the source component's
374 * coordinate space. If sourcePoint is not inside a component
375 * on the currently selected path, null is returned.
376 */
377 public Component componentForPoint(Component source,
378 Point sourcePoint) {
379 int screenX, screenY;
380 Point p = sourcePoint;
381 int i, c, j, d;
382 Component mc;
383 Rectangle r2;
384 int cWidth, cHeight;
385 MenuElement menuElement;
386 MenuElement subElements[];
387 Vector tmp;
388 int selectionSize;
389
390 SwingUtilities.convertPointToScreen(p, source);
391
392 screenX = p.x;
393 screenY = p.y;
394
395 tmp = (Vector) selection.clone();
396 selectionSize = tmp.size();
397 for (i = selectionSize - 1; i >= 0; i--) {
398 menuElement = (MenuElement) tmp.elementAt(i);
399 subElements = menuElement.getSubElements();
400
401 for (j = 0, d = subElements.length; j < d; j++) {
402 if (subElements[j] == null)
403 continue;
404 mc = subElements[j].getComponent();
405 if (!mc.isShowing())
406 continue;
407 if (mc instanceof JComponent) {
408 cWidth = ((JComponent) mc).getWidth();
409 cHeight = ((JComponent) mc).getHeight();
410 } else {
411 r2 = mc.getBounds();
412 cWidth = r2.width;
413 cHeight = r2.height;
414 }
415 p.x = screenX;
416 p.y = screenY;
417 SwingUtilities.convertPointFromScreen(p, mc);
418
419 /** Return the deepest component on the selection
420 * path in whose bounds the event's point occurs
421 */
422 if (p.x >= 0 && p.x < cWidth && p.y >= 0
423 && p.y < cHeight) {
424 return mc;
425 }
426 }
427 }
428 return null;
429 }
430
431 /**
432 * When a MenuElement receives an event from a KeyListener, it should never process the event
433 * directly. Instead all MenuElements should call this method with the event.
434 *
435 * @param e a KeyEvent object
436 */
437 public void processKeyEvent(KeyEvent e) {
438 MenuElement[] sel2 = new MenuElement[0];
439 sel2 = (MenuElement[]) selection.toArray(sel2);
440 int selSize = sel2.length;
441 MenuElement[] path;
442
443 if (selSize < 1) {
444 return;
445 }
446
447 for (int i = selSize - 1; i >= 0; i--) {
448 MenuElement elem = sel2[i];
449 MenuElement[] subs = elem.getSubElements();
450 path = null;
451
452 for (int j = 0; j < subs.length; j++) {
453 if (subs[j] == null
454 || !subs[j].getComponent().isShowing()
455 || !subs[j].getComponent().isEnabled()) {
456 continue;
457 }
458
459 if (path == null) {
460 path = new MenuElement[i + 2];
461 System.arraycopy(sel2, 0, path, 0, i + 1);
462 }
463 path[i + 1] = subs[j];
464 subs[j].processKeyEvent(e, path, this );
465 if (e.isConsumed()) {
466 return;
467 }
468 }
469 }
470
471 // finally dispatch event to the first component in path
472 path = new MenuElement[1];
473 path[0] = sel2[0];
474 path[0].processKeyEvent(e, path, this );
475 if (e.isConsumed()) {
476 return;
477 }
478 }
479
480 /**
481 * Return true if c is part of the currently used menu
482 */
483 public boolean isComponentPartOfCurrentMenu(Component c) {
484 if (selection.size() > 0) {
485 MenuElement me = (MenuElement) selection.elementAt(0);
486 return isComponentPartOfCurrentMenu(me, c);
487 } else
488 return false;
489 }
490
491 private boolean isComponentPartOfCurrentMenu(MenuElement root,
492 Component c) {
493 MenuElement children[];
494 int i, d;
495
496 if (root == null)
497 return false;
498
499 if (root.getComponent() == c)
500 return true;
501 else {
502 children = root.getSubElements();
503 for (i = 0, d = children.length; i < d; i++) {
504 if (isComponentPartOfCurrentMenu(children[i], c))
505 return true;
506 }
507 }
508 return false;
509 }
510 }
|