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