1 /* MenuSelectionManager.java --
2    Copyright (C) 2002, 2004 Free Software Foundation, Inc.
3 
4 This file is part of GNU Classpath.
5 
6 GNU Classpath is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2, or (at your option)
9 any later version.
10 
11 GNU Classpath is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 General Public License for more details.
15 
16 You should have received a copy of the GNU General Public License
17 along with GNU Classpath; see the file COPYING.  If not, write to the
18 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 02110-1301 USA.
20 
21 Linking this library statically or dynamically with other modules is
22 making a combined work based on this library.  Thus, the terms and
23 conditions of the GNU General Public License cover the whole
24 combination.
25 
26 As a special exception, the copyright holders of this library give you
27 permission to link this library with independent modules to produce an
28 executable, regardless of the license terms of these independent
29 modules, and to copy and distribute the resulting executable under
30 terms of your choice, provided that you also meet, for each linked
31 independent module, the terms and conditions of the license of that
32 module.  An independent module is a module which is not derived from
33 or based on this library.  If you modify this library, you may extend
34 this exception to your version of the library, but you are not
35 obligated to do so.  If you do not wish to do so, delete this
36 exception statement from your version. */
37 
38 
39 package javax.swing;
40 
41 import java.awt.Component;
42 import java.awt.Dimension;
43 import java.awt.Point;
44 import java.awt.event.KeyEvent;
45 import java.awt.event.MouseEvent;
46 import java.util.ArrayList;
47 import java.util.Vector;
48 
49 import javax.swing.event.ChangeEvent;
50 import javax.swing.event.ChangeListener;
51 import javax.swing.event.EventListenerList;
52 
53 /**
54  * This class manages current menu selectection. It provides
55  * methods to clear and set current selected menu path.
56  * It also fires StateChange event to its registered
57  * listeners whenever selected path of the current menu hierarchy
58  * changes.
59  *
60  */
61 public class MenuSelectionManager
62 {
63   /** ChangeEvent fired when selected path changes*/
64   protected ChangeEvent changeEvent = new ChangeEvent(this);
65 
66   /** List of listeners for this MenuSelectionManager */
67   protected EventListenerList listenerList = new EventListenerList();
68 
69   /** Default manager for the current menu hierarchy*/
70   private static final MenuSelectionManager manager = new MenuSelectionManager();
71 
72   /** Path to the currently selected menu */
73   private Vector selectedPath = new Vector();
74 
75   /**
76    * Fires StateChange event to registered listeners
77    */
fireStateChanged()78   protected void fireStateChanged()
79   {
80     ChangeListener[] listeners = getChangeListeners();
81 
82     for (int i = 0; i < listeners.length; i++)
83       listeners[i].stateChanged(changeEvent);
84   }
85 
86   /**
87    * Adds ChangeListener to this MenuSelectionManager
88    *
89    * @param listener ChangeListener to add
90    */
addChangeListener(ChangeListener listener)91   public void addChangeListener(ChangeListener listener)
92   {
93     listenerList.add(ChangeListener.class, listener);
94   }
95 
96   /**
97    * Removes ChangeListener from the list of registered listeners
98    * for this MenuSelectionManager.
99    *
100    * @param listener ChangeListner to remove
101    */
removeChangeListener(ChangeListener listener)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    */
getChangeListeners()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    */
clearSelectedPath()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    */
componentForPoint(Component source, Point sourcePoint)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    */
defaultManager()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    */
getSelectedPath()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    */
isComponentPartOfCurrentMenu(Component c)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    */
processKeyEvent(KeyEvent e)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    */
processMouseEvent(MouseEvent event)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    */
setSelectedPath(MenuElement[] path)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    */
getPath(Component c)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 }
441