001 /* MenuSelectionManager.java --
002 Copyright (C) 2002, 2004 Free Software Foundation, Inc.
003
004 This file is part of GNU Classpath.
005
006 GNU Classpath is free software; you can redistribute it and/or modify
007 it under the terms of the GNU General Public License as published by
008 the Free Software Foundation; either version 2, or (at your option)
009 any later version.
010
011 GNU Classpath is distributed in the hope that it will be useful, but
012 WITHOUT ANY WARRANTY; without even the implied warranty of
013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014 General Public License for more details.
015
016 You should have received a copy of the GNU General Public License
017 along with GNU Classpath; see the file COPYING. If not, write to the
018 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
019 02110-1301 USA.
020
021 Linking this library statically or dynamically with other modules is
022 making a combined work based on this library. Thus, the terms and
023 conditions of the GNU General Public License cover the whole
024 combination.
025
026 As a special exception, the copyright holders of this library give you
027 permission to link this library with independent modules to produce an
028 executable, regardless of the license terms of these independent
029 modules, and to copy and distribute the resulting executable under
030 terms of your choice, provided that you also meet, for each linked
031 independent module, the terms and conditions of the license of that
032 module. An independent module is a module which is not derived from
033 or based on this library. If you modify this library, you may extend
034 this exception to your version of the library, but you are not
035 obligated to do so. If you do not wish to do so, delete this
036 exception statement from your version. */
037
038
039 package javax.swing;
040
041 import java.awt.Component;
042 import java.awt.Dimension;
043 import java.awt.Point;
044 import java.awt.event.KeyEvent;
045 import java.awt.event.MouseEvent;
046 import java.util.ArrayList;
047 import java.util.Vector;
048
049 import javax.swing.event.ChangeEvent;
050 import javax.swing.event.ChangeListener;
051 import javax.swing.event.EventListenerList;
052
053 /**
054 * This class manages current menu selectection. It provides
055 * methods to clear and set current selected menu path.
056 * It also fires StateChange event to its registered
057 * listeners whenever selected path of the current menu hierarchy
058 * changes.
059 *
060 */
061 public class MenuSelectionManager
062 {
063 /** ChangeEvent fired when selected path changes*/
064 protected ChangeEvent changeEvent = new ChangeEvent(this);
065
066 /** List of listeners for this MenuSelectionManager */
067 protected EventListenerList listenerList = new EventListenerList();
068
069 /** Default manager for the current menu hierarchy*/
070 private static final MenuSelectionManager manager = new MenuSelectionManager();
071
072 /** Path to the currently selected menu */
073 private Vector selectedPath = new Vector();
074
075 /**
076 * Fires StateChange event to registered listeners
077 */
078 protected void fireStateChanged()
079 {
080 ChangeListener[] listeners = getChangeListeners();
081
082 for (int i = 0; i < listeners.length; i++)
083 listeners[i].stateChanged(changeEvent);
084 }
085
086 /**
087 * Adds ChangeListener to this MenuSelectionManager
088 *
089 * @param listener ChangeListener to add
090 */
091 public void addChangeListener(ChangeListener listener)
092 {
093 listenerList.add(ChangeListener.class, listener);
094 }
095
096 /**
097 * Removes ChangeListener from the list of registered listeners
098 * for this MenuSelectionManager.
099 *
100 * @param listener ChangeListner to remove
101 */
102 public void removeChangeListener(ChangeListener listener)
103 {
104 listenerList.remove(ChangeListener.class, listener);
105 }
106
107 /**
108 * Returns list of registered listeners with MenuSelectionManager
109 *
110 * @since 1.4
111 */
112 public ChangeListener[] getChangeListeners()
113 {
114 return (ChangeListener[]) listenerList.getListeners(ChangeListener.class);
115 }
116
117 /**
118 * Unselects all the menu elements on the selection path
119 */
120 public void clearSelectedPath()
121 {
122 // Send events from the bottom most item in the menu - hierarchy to the
123 // top most
124 for (int i = selectedPath.size() - 1; i >= 0; i--)
125 ((MenuElement) selectedPath.get(i)).menuSelectionChanged(false);
126
127 // clear selected path
128 selectedPath.clear();
129
130 // notify all listeners that the selected path was changed
131 fireStateChanged();
132 }
133
134 /**
135 * This method returns menu element on the selected path that contains
136 * given source point. If no menu element on the selected path contains this
137 * point, then null is returned.
138 *
139 * @param source Component relative to which sourcePoint is given
140 * @param sourcePoint point for which we want to find menu element that contains it
141 *
142 * @return Returns menu element that contains given source point and belongs
143 * to the currently selected path. Null is return if no such menu element found.
144 */
145 public Component componentForPoint(Component source, Point sourcePoint)
146 {
147 // Convert sourcePoint to screen coordinates.
148 Point sourcePointOnScreen = sourcePoint;
149
150 if (source.isShowing())
151 SwingUtilities.convertPointToScreen(sourcePointOnScreen, source);
152
153 Point compPointOnScreen;
154 Component resultComp = null;
155
156 // For each menu element on the selected path, express its location
157 // in terms of screen coordinates and check if there is any
158 // menu element on the selected path that contains given source point.
159 for (int i = 0; i < selectedPath.size(); i++)
160 {
161 Component comp = ((Component) selectedPath.get(i));
162 Dimension size = comp.getSize();
163
164 // convert location of this menu item to screen coordinates
165 compPointOnScreen = comp.getLocationOnScreen();
166
167 if (compPointOnScreen.x <= sourcePointOnScreen.x
168 && sourcePointOnScreen.x < compPointOnScreen.x + size.width
169 && compPointOnScreen.y <= sourcePointOnScreen.y
170 && sourcePointOnScreen.y < compPointOnScreen.y + size.height)
171 {
172 Point p = sourcePointOnScreen;
173
174 if (comp.isShowing())
175 SwingUtilities.convertPointFromScreen(p, comp);
176
177 resultComp = SwingUtilities.getDeepestComponentAt(comp, p.x, p.y);
178 break;
179 }
180 }
181 return resultComp;
182 }
183
184 /**
185 * Returns shared instance of MenuSelection Manager
186 *
187 * @return default Manager
188 */
189 public static MenuSelectionManager defaultManager()
190 {
191 return manager;
192 }
193
194 /**
195 * Returns path representing current menu selection
196 *
197 * @return Current selection path
198 */
199 public MenuElement[] getSelectedPath()
200 {
201 MenuElement[] path = new MenuElement[selectedPath.size()];
202
203 for (int i = 0; i < path.length; i++)
204 path[i] = (MenuElement) selectedPath.get(i);
205
206 return path;
207 }
208
209 /**
210 * Returns true if specified component is part of current menu
211 * heirarchy and false otherwise
212 *
213 * @param c Component for which to check
214 * @return True if specified component is part of current menu
215 */
216 public boolean isComponentPartOfCurrentMenu(Component c)
217 {
218 MenuElement[] subElements;
219 boolean ret = false;
220 for (int i = 0; i < selectedPath.size(); i++)
221 {
222 // Check first element.
223 MenuElement first = (MenuElement) selectedPath.get(i);
224 if (SwingUtilities.isDescendingFrom(c, first.getComponent()))
225 {
226 ret = true;
227 break;
228 }
229 else
230 {
231 // Check sub elements.
232 subElements = first.getSubElements();
233 for (int j = 0; j < subElements.length; j++)
234 {
235 MenuElement me = subElements[j];
236 if (me != null
237 && (SwingUtilities.isDescendingFrom(c, me.getComponent())))
238 {
239 ret = true;
240 break;
241 }
242 }
243 }
244 }
245
246 return ret;
247 }
248
249 /**
250 * Processes key events on behalf of the MenuElements. MenuElement
251 * instances should always forward their key events to this method and
252 * get their {@link MenuElement#processKeyEvent(KeyEvent, MenuElement[],
253 * MenuSelectionManager)} eventually called back.
254 *
255 * @param e the key event
256 */
257 public void processKeyEvent(KeyEvent e)
258 {
259 MenuElement[] selection = (MenuElement[])
260 selectedPath.toArray(new MenuElement[selectedPath.size()]);
261 if (selection.length == 0)
262 return;
263
264 MenuElement[] path;
265 for (int index = selection.length - 1; index >= 0; index--)
266 {
267 MenuElement el = selection[index];
268 // This method's main purpose is to forward key events to the
269 // relevant menu items, so that they can act in response to their
270 // mnemonics beeing typed. So we also need to forward the key event
271 // to all the subelements of the currently selected menu elements
272 // in the path.
273 MenuElement[] subEls = el.getSubElements();
274 path = null;
275 for (int subIndex = 0; subIndex < subEls.length; subIndex++)
276 {
277 MenuElement sub = subEls[subIndex];
278 // Skip elements that are not showing or not enabled.
279 if (sub == null || ! sub.getComponent().isShowing()
280 || ! sub.getComponent().isEnabled())
281 {
282 continue;
283 }
284
285 if (path == null)
286 {
287 path = new MenuElement[index + 2];
288 System.arraycopy(selection, 0, path, 0, index + 1);
289 }
290 path[index + 1] = sub;
291 sub.processKeyEvent(e, path, this);
292 if (e.isConsumed())
293 break;
294 }
295 if (e.isConsumed())
296 break;
297 }
298
299 // Dispatch to first element in selection if it hasn't been consumed.
300 if (! e.isConsumed())
301 {
302 path = new MenuElement[1];
303 path[0] = selection[0];
304 path[0].processKeyEvent(e, path, this);
305 }
306 }
307
308 /**
309 * Forwards given mouse event to all of the source subcomponents.
310 *
311 * @param event Mouse event
312 */
313 public void processMouseEvent(MouseEvent event)
314 {
315 Component source = ((Component) event.getSource());
316
317 // In the case of drag event, event.getSource() returns component
318 // where drag event originated. However menu element processing this
319 // event should be the one over which mouse is currently located,
320 // which is not necessary the source of the drag event.
321 Component mouseOverMenuComp;
322
323 // find over which menu element the mouse is currently located
324 if (event.getID() == MouseEvent.MOUSE_DRAGGED
325 || event.getID() == MouseEvent.MOUSE_RELEASED)
326 mouseOverMenuComp = componentForPoint(source, event.getPoint());
327 else
328 mouseOverMenuComp = source;
329
330 // Process this event only if mouse is located over some menu element
331 if (mouseOverMenuComp != null && (mouseOverMenuComp instanceof MenuElement))
332 {
333 MenuElement[] path = getPath(mouseOverMenuComp);
334 ((MenuElement) mouseOverMenuComp).processMouseEvent(event, path,
335 manager);
336
337 // FIXME: Java specification says that mouse events should be
338 // forwarded to subcomponents. The code below does it, but
339 // menu's work fine without it. This code is commented for now.
340
341 /*
342 MenuElement[] subComponents = ((MenuElement) mouseOverMenuComp)
343 .getSubElements();
344
345 for (int i = 0; i < subComponents.length; i++)
346 {
347 subComponents[i].processMouseEvent(event, path, manager);
348 }
349 */
350 }
351 else
352 {
353 if (event.getID() == MouseEvent.MOUSE_RELEASED)
354 clearSelectedPath();
355 }
356 }
357
358 /**
359 * Sets menu selection to the specified path
360 *
361 * @param path new selection path
362 */
363 public void setSelectedPath(MenuElement[] path)
364 {
365 if (path == null)
366 {
367 clearSelectedPath();
368 return;
369 }
370
371 int minSize = path.length; // size of the smaller path.
372 int currentSize = selectedPath.size();
373 int firstDiff = 0;
374
375 // Search first item that is different in the current and new path.
376 for (int i = 0; i < minSize; i++)
377 {
378 if (i < currentSize && (MenuElement) selectedPath.get(i) == path[i])
379 firstDiff++;
380 else
381 break;
382 }
383
384 // Remove items from selection and send notification.
385 for (int i = currentSize - 1; i >= firstDiff; i--)
386 {
387 MenuElement el = (MenuElement) selectedPath.get(i);
388 selectedPath.remove(i);
389 el.menuSelectionChanged(false);
390 }
391
392 // Add new items to selection and send notification.
393 for (int i = firstDiff; i < minSize; i++)
394 {
395 if (path[i] != null)
396 {
397 selectedPath.add(path[i]);
398 path[i].menuSelectionChanged(true);
399 }
400 }
401
402 fireStateChanged();
403 }
404
405 /**
406 * Returns path to the specified component
407 *
408 * @param c component for which to find path for
409 *
410 * @return path to the specified component
411 */
412 private MenuElement[] getPath(Component c)
413 {
414 // FIXME: There is the same method in BasicMenuItemUI. However I
415 // cannot use it here instead of this method, since I cannot assume that
416 // all the menu elements on the selected path are JMenuItem or JMenu.
417 // For now I've just duplicated it here. Please
418 // fix me or delete me if another better approach will be found, and
419 // this method will not be necessary.
420 ArrayList path = new ArrayList();
421
422 // if given component is JMenu, we also need to include
423 // it's popup menu in the path
424 if (c instanceof JMenu)
425 path.add(((JMenu) c).getPopupMenu());
426 while (c instanceof MenuElement)
427 {
428 path.add(0, (MenuElement) c);
429
430 if (c instanceof JPopupMenu)
431 c = ((JPopupMenu) c).getInvoker();
432 else
433 c = c.getParent();
434 }
435
436 MenuElement[] pathArray = new MenuElement[path.size()];
437 path.toArray(pathArray);
438 return pathArray;
439 }
440 }