001/* MenuSelectionManager.java --
002   Copyright (C) 2002, 2004 Free Software Foundation, Inc.
003
004This file is part of GNU Classpath.
005
006GNU Classpath is free software; you can redistribute it and/or modify
007it under the terms of the GNU General Public License as published by
008the Free Software Foundation; either version 2, or (at your option)
009any later version.
010
011GNU Classpath is distributed in the hope that it will be useful, but
012WITHOUT ANY WARRANTY; without even the implied warranty of
013MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014General Public License for more details.
015
016You should have received a copy of the GNU General Public License
017along with GNU Classpath; see the file COPYING.  If not, write to the
018Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
01902110-1301 USA.
020
021Linking this library statically or dynamically with other modules is
022making a combined work based on this library.  Thus, the terms and
023conditions of the GNU General Public License cover the whole
024combination.
025
026As a special exception, the copyright holders of this library give you
027permission to link this library with independent modules to produce an
028executable, regardless of the license terms of these independent
029modules, and to copy and distribute the resulting executable under
030terms of your choice, provided that you also meet, for each linked
031independent module, the terms and conditions of the license of that
032module.  An independent module is a module which is not derived from
033or based on this library.  If you modify this library, you may extend
034this exception to your version of the library, but you are not
035obligated to do so.  If you do not wish to do so, delete this
036exception statement from your version. */
037
038
039package javax.swing;
040
041import java.awt.Component;
042import java.awt.Dimension;
043import java.awt.Point;
044import java.awt.event.KeyEvent;
045import java.awt.event.MouseEvent;
046import java.util.ArrayList;
047import java.util.Vector;
048
049import javax.swing.event.ChangeEvent;
050import javax.swing.event.ChangeListener;
051import 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 */
061public 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}