| /* MenuSelectionManager.java -- |
| Copyright (C) 2002, 2004 Free Software Foundation, Inc. |
| |
| This file is part of GNU Classpath. |
| |
| GNU Classpath is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License as published by |
| the Free Software Foundation; either version 2, or (at your option) |
| any later version. |
| |
| GNU Classpath is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License |
| along with GNU Classpath; see the file COPYING. If not, write to the |
| Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| 02110-1301 USA. |
| |
| Linking this library statically or dynamically with other modules is |
| making a combined work based on this library. Thus, the terms and |
| conditions of the GNU General Public License cover the whole |
| combination. |
| |
| As a special exception, the copyright holders of this library give you |
| permission to link this library with independent modules to produce an |
| executable, regardless of the license terms of these independent |
| modules, and to copy and distribute the resulting executable under |
| terms of your choice, provided that you also meet, for each linked |
| independent module, the terms and conditions of the license of that |
| module. An independent module is a module which is not derived from |
| or based on this library. If you modify this library, you may extend |
| this exception to your version of the library, but you are not |
| obligated to do so. If you do not wish to do so, delete this |
| exception statement from your version. */ |
| |
| |
| package javax.swing; |
| |
| import java.awt.Component; |
| import java.awt.Dimension; |
| import java.awt.Point; |
| import java.awt.event.KeyEvent; |
| import java.awt.event.MouseEvent; |
| import java.util.ArrayList; |
| import java.util.Vector; |
| |
| import javax.swing.event.ChangeEvent; |
| import javax.swing.event.ChangeListener; |
| import javax.swing.event.EventListenerList; |
| |
| /** |
| * This class manages current menu selectection. It provides |
| * methods to clear and set current selected menu path. |
| * It also fires StateChange event to its registered |
| * listeners whenever selected path of the current menu hierarchy |
| * changes. |
| * |
| */ |
| public class MenuSelectionManager |
| { |
| /** ChangeEvent fired when selected path changes*/ |
| protected ChangeEvent changeEvent = new ChangeEvent(this); |
| |
| /** List of listeners for this MenuSelectionManager */ |
| protected EventListenerList listenerList = new EventListenerList(); |
| |
| /** Default manager for the current menu hierarchy*/ |
| private static final MenuSelectionManager manager = new MenuSelectionManager(); |
| |
| /** Path to the currently selected menu */ |
| private Vector selectedPath = new Vector(); |
| |
| /** |
| * Fires StateChange event to registered listeners |
| */ |
| protected void fireStateChanged() |
| { |
| ChangeListener[] listeners = getChangeListeners(); |
| |
| for (int i = 0; i < listeners.length; i++) |
| listeners[i].stateChanged(changeEvent); |
| } |
| |
| /** |
| * Adds ChangeListener to this MenuSelectionManager |
| * |
| * @param listener ChangeListener to add |
| */ |
| public void addChangeListener(ChangeListener listener) |
| { |
| listenerList.add(ChangeListener.class, listener); |
| } |
| |
| /** |
| * Removes ChangeListener from the list of registered listeners |
| * for this MenuSelectionManager. |
| * |
| * @param listener ChangeListner to remove |
| */ |
| public void removeChangeListener(ChangeListener listener) |
| { |
| listenerList.remove(ChangeListener.class, listener); |
| } |
| |
| /** |
| * Returns list of registered listeners with MenuSelectionManager |
| * |
| * @since 1.4 |
| */ |
| public ChangeListener[] getChangeListeners() |
| { |
| return (ChangeListener[]) listenerList.getListeners(ChangeListener.class); |
| } |
| |
| /** |
| * Unselects all the menu elements on the selection path |
| */ |
| public void clearSelectedPath() |
| { |
| // Send events from the bottom most item in the menu - hierarchy to the |
| // top most |
| for (int i = selectedPath.size() - 1; i >= 0; i--) |
| ((MenuElement) selectedPath.get(i)).menuSelectionChanged(false); |
| |
| // clear selected path |
| selectedPath.clear(); |
| |
| // notify all listeners that the selected path was changed |
| fireStateChanged(); |
| } |
| |
| /** |
| * This method returns menu element on the selected path that contains |
| * given source point. If no menu element on the selected path contains this |
| * point, then null is returned. |
| * |
| * @param source Component relative to which sourcePoint is given |
| * @param sourcePoint point for which we want to find menu element that contains it |
| * |
| * @return Returns menu element that contains given source point and belongs |
| * to the currently selected path. Null is return if no such menu element found. |
| */ |
| public Component componentForPoint(Component source, Point sourcePoint) |
| { |
| // Convert sourcePoint to screen coordinates. |
| Point sourcePointOnScreen = sourcePoint; |
| |
| if (source.isShowing()) |
| SwingUtilities.convertPointToScreen(sourcePointOnScreen, source); |
| |
| Point compPointOnScreen; |
| Component resultComp = null; |
| |
| // For each menu element on the selected path, express its location |
| // in terms of screen coordinates and check if there is any |
| // menu element on the selected path that contains given source point. |
| for (int i = 0; i < selectedPath.size(); i++) |
| { |
| Component comp = ((Component) selectedPath.get(i)); |
| Dimension size = comp.getSize(); |
| |
| // convert location of this menu item to screen coordinates |
| compPointOnScreen = comp.getLocationOnScreen(); |
| |
| if (compPointOnScreen.x <= sourcePointOnScreen.x |
| && sourcePointOnScreen.x < compPointOnScreen.x + size.width |
| && compPointOnScreen.y <= sourcePointOnScreen.y |
| && sourcePointOnScreen.y < compPointOnScreen.y + size.height) |
| { |
| Point p = sourcePointOnScreen; |
| |
| if (comp.isShowing()) |
| SwingUtilities.convertPointFromScreen(p, comp); |
| |
| resultComp = SwingUtilities.getDeepestComponentAt(comp, p.x, p.y); |
| break; |
| } |
| } |
| return resultComp; |
| } |
| |
| /** |
| * Returns shared instance of MenuSelection Manager |
| * |
| * @return default Manager |
| */ |
| public static MenuSelectionManager defaultManager() |
| { |
| return manager; |
| } |
| |
| /** |
| * Returns path representing current menu selection |
| * |
| * @return Current selection path |
| */ |
| public MenuElement[] getSelectedPath() |
| { |
| MenuElement[] path = new MenuElement[selectedPath.size()]; |
| |
| for (int i = 0; i < path.length; i++) |
| path[i] = (MenuElement) selectedPath.get(i); |
| |
| return path; |
| } |
| |
| /** |
| * Returns true if specified component is part of current menu |
| * heirarchy and false otherwise |
| * |
| * @param c Component for which to check |
| * @return True if specified component is part of current menu |
| */ |
| public boolean isComponentPartOfCurrentMenu(Component c) |
| { |
| MenuElement[] subElements; |
| boolean ret = false; |
| for (int i = 0; i < selectedPath.size(); i++) |
| { |
| // Check first element. |
| MenuElement first = (MenuElement) selectedPath.get(i); |
| if (SwingUtilities.isDescendingFrom(c, first.getComponent())) |
| { |
| ret = true; |
| break; |
| } |
| else |
| { |
| // Check sub elements. |
| subElements = first.getSubElements(); |
| for (int j = 0; j < subElements.length; j++) |
| { |
| MenuElement me = subElements[j]; |
| if (me != null |
| && (SwingUtilities.isDescendingFrom(c, me.getComponent()))) |
| { |
| ret = true; |
| break; |
| } |
| } |
| } |
| } |
| |
| return ret; |
| } |
| |
| /** |
| * Processes key events on behalf of the MenuElements. MenuElement |
| * instances should always forward their key events to this method and |
| * get their {@link MenuElement#processKeyEvent(KeyEvent, MenuElement[], |
| * MenuSelectionManager)} eventually called back. |
| * |
| * @param e the key event |
| */ |
| public void processKeyEvent(KeyEvent e) |
| { |
| MenuElement[] selection = (MenuElement[]) |
| selectedPath.toArray(new MenuElement[selectedPath.size()]); |
| if (selection.length == 0) |
| return; |
| |
| MenuElement[] path; |
| for (int index = selection.length - 1; index >= 0; index--) |
| { |
| MenuElement el = selection[index]; |
| // This method's main purpose is to forward key events to the |
| // relevant menu items, so that they can act in response to their |
| // mnemonics beeing typed. So we also need to forward the key event |
| // to all the subelements of the currently selected menu elements |
| // in the path. |
| MenuElement[] subEls = el.getSubElements(); |
| path = null; |
| for (int subIndex = 0; subIndex < subEls.length; subIndex++) |
| { |
| MenuElement sub = subEls[subIndex]; |
| // Skip elements that are not showing or not enabled. |
| if (sub == null || ! sub.getComponent().isShowing() |
| || ! sub.getComponent().isEnabled()) |
| { |
| continue; |
| } |
| |
| if (path == null) |
| { |
| path = new MenuElement[index + 2]; |
| System.arraycopy(selection, 0, path, 0, index + 1); |
| } |
| path[index + 1] = sub; |
| sub.processKeyEvent(e, path, this); |
| if (e.isConsumed()) |
| break; |
| } |
| if (e.isConsumed()) |
| break; |
| } |
| |
| // Dispatch to first element in selection if it hasn't been consumed. |
| if (! e.isConsumed()) |
| { |
| path = new MenuElement[1]; |
| path[0] = selection[0]; |
| path[0].processKeyEvent(e, path, this); |
| } |
| } |
| |
| /** |
| * Forwards given mouse event to all of the source subcomponents. |
| * |
| * @param event Mouse event |
| */ |
| public void processMouseEvent(MouseEvent event) |
| { |
| Component source = ((Component) event.getSource()); |
| |
| // In the case of drag event, event.getSource() returns component |
| // where drag event originated. However menu element processing this |
| // event should be the one over which mouse is currently located, |
| // which is not necessary the source of the drag event. |
| Component mouseOverMenuComp; |
| |
| // find over which menu element the mouse is currently located |
| if (event.getID() == MouseEvent.MOUSE_DRAGGED |
| || event.getID() == MouseEvent.MOUSE_RELEASED) |
| mouseOverMenuComp = componentForPoint(source, event.getPoint()); |
| else |
| mouseOverMenuComp = source; |
| |
| // Process this event only if mouse is located over some menu element |
| if (mouseOverMenuComp != null && (mouseOverMenuComp instanceof MenuElement)) |
| { |
| MenuElement[] path = getPath(mouseOverMenuComp); |
| ((MenuElement) mouseOverMenuComp).processMouseEvent(event, path, |
| manager); |
| |
| // FIXME: Java specification says that mouse events should be |
| // forwarded to subcomponents. The code below does it, but |
| // menu's work fine without it. This code is commented for now. |
| |
| /* |
| MenuElement[] subComponents = ((MenuElement) mouseOverMenuComp) |
| .getSubElements(); |
| |
| for (int i = 0; i < subComponents.length; i++) |
| { |
| subComponents[i].processMouseEvent(event, path, manager); |
| } |
| */ |
| } |
| else |
| { |
| if (event.getID() == MouseEvent.MOUSE_RELEASED) |
| clearSelectedPath(); |
| } |
| } |
| |
| /** |
| * Sets menu selection to the specified path |
| * |
| * @param path new selection path |
| */ |
| public void setSelectedPath(MenuElement[] path) |
| { |
| if (path == null) |
| { |
| clearSelectedPath(); |
| return; |
| } |
| |
| int minSize = path.length; // size of the smaller path. |
| int currentSize = selectedPath.size(); |
| int firstDiff = 0; |
| |
| // Search first item that is different in the current and new path. |
| for (int i = 0; i < minSize; i++) |
| { |
| if (i < currentSize && (MenuElement) selectedPath.get(i) == path[i]) |
| firstDiff++; |
| else |
| break; |
| } |
| |
| // Remove items from selection and send notification. |
| for (int i = currentSize - 1; i >= firstDiff; i--) |
| { |
| MenuElement el = (MenuElement) selectedPath.get(i); |
| selectedPath.remove(i); |
| el.menuSelectionChanged(false); |
| } |
| |
| // Add new items to selection and send notification. |
| for (int i = firstDiff; i < minSize; i++) |
| { |
| if (path[i] != null) |
| { |
| selectedPath.add(path[i]); |
| path[i].menuSelectionChanged(true); |
| } |
| } |
| |
| fireStateChanged(); |
| } |
| |
| /** |
| * Returns path to the specified component |
| * |
| * @param c component for which to find path for |
| * |
| * @return path to the specified component |
| */ |
| private MenuElement[] getPath(Component c) |
| { |
| // FIXME: There is the same method in BasicMenuItemUI. However I |
| // cannot use it here instead of this method, since I cannot assume that |
| // all the menu elements on the selected path are JMenuItem or JMenu. |
| // For now I've just duplicated it here. Please |
| // fix me or delete me if another better approach will be found, and |
| // this method will not be necessary. |
| ArrayList path = new ArrayList(); |
| |
| // if given component is JMenu, we also need to include |
| // it's popup menu in the path |
| if (c instanceof JMenu) |
| path.add(((JMenu) c).getPopupMenu()); |
| while (c instanceof MenuElement) |
| { |
| path.add(0, (MenuElement) c); |
| |
| if (c instanceof JPopupMenu) |
| c = ((JPopupMenu) c).getInvoker(); |
| else |
| c = c.getParent(); |
| } |
| |
| MenuElement[] pathArray = new MenuElement[path.size()]; |
| path.toArray(pathArray); |
| return pathArray; |
| } |
| } |