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    }