Source for javax.swing.MenuSelectionManager

   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:    */
  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:    */
  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:    */
 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: }