1 /* 2 * Copyright (c) 1997, 2014, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 package javax.swing; 26 27 import java.awt.*; 28 import java.util.*; 29 import java.awt.event.*; 30 import javax.swing.event.*; 31 32 import sun.awt.AppContext; 33 import sun.swing.SwingUtilities2; 34 35 /** 36 * A MenuSelectionManager owns the selection in menu hierarchy. 37 * 38 * @author Arnaud Weber 39 * @since 1.2 40 */ 41 public class MenuSelectionManager { 42 private Vector<MenuElement> selection = new Vector<MenuElement>(); 43 44 /* diagnostic aids -- should be false for production builds. */ 45 private static final boolean TRACE = false; // trace creates and disposes 46 private static final boolean VERBOSE = false; // show reuse hits/misses 47 private static final boolean DEBUG = false; // show bad params, misc. 48 49 private static final StringBuilder MENU_SELECTION_MANAGER_KEY = 50 new StringBuilder("javax.swing.MenuSelectionManager"); 51 52 /** 53 * Returns the default menu selection manager. 54 * 55 * @return a MenuSelectionManager object 56 */ 57 public static MenuSelectionManager defaultManager() { 58 synchronized (MENU_SELECTION_MANAGER_KEY) { 59 AppContext context = AppContext.getAppContext(); 60 MenuSelectionManager msm = (MenuSelectionManager)context.get( 61 MENU_SELECTION_MANAGER_KEY); 62 if (msm == null) { 63 msm = new MenuSelectionManager(); 64 context.put(MENU_SELECTION_MANAGER_KEY, msm); 65 66 // installing additional listener if found in the AppContext 67 Object o = context.get(SwingUtilities2.MENU_SELECTION_MANAGER_LISTENER_KEY); 68 if (o != null && o instanceof ChangeListener) { 69 msm.addChangeListener((ChangeListener) o); 70 } 71 } 72 73 return msm; 74 } 75 } 76 77 /** 78 * Only one ChangeEvent is needed per button model instance since the 79 * event's only state is the source property. The source of events 80 * generated is always "this". 81 */ 82 protected transient ChangeEvent changeEvent = null; 83 /** The collection of registered listeners */ 84 protected EventListenerList listenerList = new EventListenerList(); 85 86 /** 87 * Changes the selection in the menu hierarchy. The elements 88 * in the array are sorted in order from the root menu 89 * element to the currently selected menu element. 90 * <p> 91 * Note that this method is public but is used by the look and 92 * feel engine and should not be called by client applications. 93 * 94 * @param path an array of <code>MenuElement</code> objects specifying 95 * the selected path 96 */ 97 public void setSelectedPath(MenuElement[] path) { 98 int i,c; 99 int currentSelectionCount = selection.size(); 100 int firstDifference = 0; 101 102 if(path == null) { 103 path = new MenuElement[0]; 104 } 105 106 if (DEBUG) { 107 System.out.print("Previous: "); printMenuElementArray(getSelectedPath()); 108 System.out.print("New: "); printMenuElementArray(path); 109 } 110 111 for(i=0,c=path.length;i<c;i++) { 112 if (i < currentSelectionCount && selection.elementAt(i) == path[i]) 113 firstDifference++; 114 else 115 break; 116 } 117 118 for(i=currentSelectionCount - 1 ; i >= firstDifference ; i--) { 119 MenuElement me = selection.elementAt(i); 120 selection.removeElementAt(i); 121 me.menuSelectionChanged(false); 122 } 123 124 for(i = firstDifference, c = path.length ; i < c ; i++) { 125 if (path[i] != null) { 126 selection.addElement(path[i]); 127 path[i].menuSelectionChanged(true); 128 } 129 } 130 131 fireStateChanged(); 132 } 133 134 /** 135 * Returns the path to the currently selected menu item 136 * 137 * @return an array of MenuElement objects representing the selected path 138 */ 139 public MenuElement[] getSelectedPath() { 140 MenuElement res[] = new MenuElement[selection.size()]; 141 int i,c; 142 for(i=0,c=selection.size();i<c;i++) 143 res[i] = selection.elementAt(i); 144 return res; 145 } 146 147 /** 148 * Tell the menu selection to close and unselect all the menu components. Call this method 149 * when a choice has been made 150 */ 151 public void clearSelectedPath() { 152 if (selection.size() > 0) { 153 setSelectedPath(null); 154 } 155 } 156 157 /** 158 * Adds a ChangeListener to the button. 159 * 160 * @param l the listener to add 161 */ 162 public void addChangeListener(ChangeListener l) { 163 listenerList.add(ChangeListener.class, l); 164 } 165 166 /** 167 * Removes a ChangeListener from the button. 168 * 169 * @param l the listener to remove 170 */ 171 public void removeChangeListener(ChangeListener l) { 172 listenerList.remove(ChangeListener.class, l); 173 } 174 175 /** 176 * Returns an array of all the <code>ChangeListener</code>s added 177 * to this MenuSelectionManager with addChangeListener(). 178 * 179 * @return all of the <code>ChangeListener</code>s added or an empty 180 * array if no listeners have been added 181 * @since 1.4 182 */ 183 public ChangeListener[] getChangeListeners() { 184 return listenerList.getListeners(ChangeListener.class); 185 } 186 187 /** 188 * Notifies all listeners that have registered interest for 189 * notification on this event type. The event instance 190 * is created lazily. 191 * 192 * @see EventListenerList 193 */ 194 protected void fireStateChanged() { 195 // Guaranteed to return a non-null array 196 Object[] listeners = listenerList.getListenerList(); 197 // Process the listeners last to first, notifying 198 // those that are interested in this event 199 for (int i = listeners.length-2; i>=0; i-=2) { 200 if (listeners[i]==ChangeListener.class) { 201 // Lazily create the event: 202 if (changeEvent == null) 203 changeEvent = new ChangeEvent(this); 204 ((ChangeListener)listeners[i+1]).stateChanged(changeEvent); 205 } 206 } 207 } 208 209 /** 210 * When a MenuElement receives an event from a MouseListener, it should never process the event 211 * directly. Instead all MenuElements should call this method with the event. 212 * 213 * @param event a MouseEvent object 214 */ 215 public void processMouseEvent(MouseEvent event) { 216 int screenX,screenY; 217 Point p; 218 int i,c,j,d; 219 Component mc; 220 Rectangle r2; 221 int cWidth,cHeight; 222 MenuElement menuElement; 223 MenuElement subElements[]; 224 MenuElement path[]; 225 int selectionSize; 226 p = event.getPoint(); 227 228 Component source = event.getComponent(); 229 230 if ((source != null) && !source.isShowing()) { 231 // This can happen if a mouseReleased removes the 232 // containing component -- bug 4146684 233 return; 234 } 235 236 int type = event.getID(); 237 int modifiers = event.getModifiers(); 238 // 4188027: drag enter/exit added in JDK 1.1.7A, JDK1.2 239 if ((type==MouseEvent.MOUSE_ENTERED|| 240 type==MouseEvent.MOUSE_EXITED) 241 && ((modifiers & (InputEvent.BUTTON1_MASK | 242 InputEvent.BUTTON2_MASK | InputEvent.BUTTON3_MASK)) !=0 )) { 243 return; 244 } 245 246 if (source != null) { 247 SwingUtilities.convertPointToScreen(p, source); 248 } 249 250 screenX = p.x; 251 screenY = p.y; 252 253 @SuppressWarnings("unchecked") 254 Vector<MenuElement> tmp = (Vector<MenuElement>)selection.clone(); 255 selectionSize = tmp.size(); 256 boolean success = false; 257 for (i=selectionSize - 1;i >= 0 && success == false; i--) { 258 menuElement = tmp.elementAt(i); 259 subElements = menuElement.getSubElements(); 260 261 path = null; 262 for (j = 0, d = subElements.length;j < d && success == false; j++) { 263 if (subElements[j] == null) 264 continue; 265 mc = subElements[j].getComponent(); 266 if(!mc.isShowing()) 267 continue; 268 if(mc instanceof JComponent) { 269 cWidth = mc.getWidth(); 270 cHeight = mc.getHeight(); 271 } else { 272 r2 = mc.getBounds(); 273 cWidth = r2.width; 274 cHeight = r2.height; 275 } 276 p.x = screenX; 277 p.y = screenY; 278 SwingUtilities.convertPointFromScreen(p,mc); 279 280 /** Send the event to visible menu element if menu element currently in 281 * the selected path or contains the event location 282 */ 283 if( 284 (p.x >= 0 && p.x < cWidth && p.y >= 0 && p.y < cHeight)) { 285 int k; 286 if(path == null) { 287 path = new MenuElement[i+2]; 288 for(k=0;k<=i;k++) 289 path[k] = tmp.elementAt(k); 290 } 291 path[i+1] = subElements[j]; 292 MenuElement currentSelection[] = getSelectedPath(); 293 294 // Enter/exit detection -- needs tuning... 295 if (currentSelection[currentSelection.length-1] != 296 path[i+1] && 297 (currentSelection.length < 2 || 298 currentSelection[currentSelection.length-2] != 299 path[i+1])) { 300 Component oldMC = currentSelection[currentSelection.length-1].getComponent(); 301 302 MouseEvent exitEvent = new MouseEvent(oldMC, MouseEvent.MOUSE_EXITED, 303 event.getWhen(), 304 event.getModifiers(), p.x, p.y, 305 event.getXOnScreen(), 306 event.getYOnScreen(), 307 event.getClickCount(), 308 event.isPopupTrigger(), 309 MouseEvent.NOBUTTON); 310 currentSelection[currentSelection.length-1]. 311 processMouseEvent(exitEvent, path, this); 312 313 MouseEvent enterEvent = new MouseEvent(mc, 314 MouseEvent.MOUSE_ENTERED, 315 event.getWhen(), 316 event.getModifiers(), p.x, p.y, 317 event.getXOnScreen(), 318 event.getYOnScreen(), 319 event.getClickCount(), 320 event.isPopupTrigger(), 321 MouseEvent.NOBUTTON); 322 subElements[j].processMouseEvent(enterEvent, path, this); 323 } 324 MouseEvent mouseEvent = new MouseEvent(mc, event.getID(),event. getWhen(), 325 event.getModifiers(), p.x, p.y, 326 event.getXOnScreen(), 327 event.getYOnScreen(), 328 event.getClickCount(), 329 event.isPopupTrigger(), 330 MouseEvent.NOBUTTON); 331 subElements[j].processMouseEvent(mouseEvent, path, this); 332 success = true; 333 event.consume(); 334 } 335 } 336 } 337 } 338 339 private void printMenuElementArray(MenuElement path[]) { 340 printMenuElementArray(path, false); 341 } 342 343 private void printMenuElementArray(MenuElement path[], boolean dumpStack) { 344 System.out.println("Path is("); 345 int i, j; 346 for(i=0,j=path.length; i<j ;i++){ 347 for (int k=0; k<=i; k++) 348 System.out.print(" "); 349 MenuElement me = path[i]; 350 if(me instanceof JMenuItem) { 351 System.out.println(((JMenuItem)me).getText() + ", "); 352 } else if (me instanceof JMenuBar) { 353 System.out.println("JMenuBar, "); 354 } else if(me instanceof JPopupMenu) { 355 System.out.println("JPopupMenu, "); 356 } else if (me == null) { 357 System.out.println("NULL , "); 358 } else { 359 System.out.println("" + me + ", "); 360 } 361 } 362 System.out.println(")"); 363 364 if (dumpStack == true) 365 Thread.dumpStack(); 366 } 367 368 /** 369 * Returns the component in the currently selected path 370 * which contains sourcePoint. 371 * 372 * @param source The component in whose coordinate space sourcePoint 373 * is given 374 * @param sourcePoint The point which is being tested 375 * @return The component in the currently selected path which 376 * contains sourcePoint (relative to the source component's 377 * coordinate space. If sourcePoint is not inside a component 378 * on the currently selected path, null is returned. 379 */ 380 public Component componentForPoint(Component source, Point sourcePoint) { 381 int screenX,screenY; 382 Point p = sourcePoint; 383 int i,c,j,d; 384 Component mc; 385 Rectangle r2; 386 int cWidth,cHeight; 387 MenuElement menuElement; 388 MenuElement subElements[]; 389 int selectionSize; 390 391 SwingUtilities.convertPointToScreen(p,source); 392 393 screenX = p.x; 394 screenY = p.y; 395 396 @SuppressWarnings("unchecked") 397 Vector<MenuElement> tmp = (Vector<MenuElement>)selection.clone(); 398 selectionSize = tmp.size(); 399 for(i=selectionSize - 1 ; i >= 0 ; i--) { 400 menuElement = tmp.elementAt(i); 401 subElements = menuElement.getSubElements(); 402 403 for(j = 0, d = subElements.length ; j < d ; j++) { 404 if (subElements[j] == null) 405 continue; 406 mc = subElements[j].getComponent(); 407 if(!mc.isShowing()) 408 continue; 409 if(mc instanceof JComponent) { 410 cWidth = mc.getWidth(); 411 cHeight = mc.getHeight(); 412 } else { 413 r2 = mc.getBounds(); 414 cWidth = r2.width; 415 cHeight = r2.height; 416 } 417 p.x = screenX; 418 p.y = screenY; 419 SwingUtilities.convertPointFromScreen(p,mc); 420 421 /** Return the deepest component on the selection 422 * path in whose bounds the event's point occurs 423 */ 424 if (p.x >= 0 && p.x < cWidth && p.y >= 0 && p.y < cHeight) { 425 return mc; 426 } 427 } 428 } 429 return null; 430 } 431 432 /** 433 * When a MenuElement receives an event from a KeyListener, it should never process the event 434 * directly. Instead all MenuElements should call this method with the event. 435 * 436 * @param e a KeyEvent object 437 */ 438 public void processKeyEvent(KeyEvent e) { 439 MenuElement[] sel2 = new MenuElement[0]; 440 sel2 = selection.toArray(sel2); 441 int selSize = sel2.length; 442 MenuElement[] path; 443 444 if (selSize < 1) { 445 return; 446 } 447 448 for (int i=selSize-1; i>=0; i--) { 449 MenuElement elem = sel2[i]; 450 MenuElement[] subs = elem.getSubElements(); 451 path = null; 452 453 for (int j=0; j<subs.length; j++) { 454 if (subs[j] == null || !subs[j].getComponent().isShowing() 455 || !subs[j].getComponent().isEnabled()) { 456 continue; 457 } 458 459 if(path == null) { 460 path = new MenuElement[i+2]; 461 System.arraycopy(sel2, 0, path, 0, i+1); 462 } 463 path[i+1] = subs[j]; 464 subs[j].processKeyEvent(e, path, this); 465 if (e.isConsumed()) { 466 return; 467 } 468 } 469 } 470 471 // finally dispatch event to the first component in path 472 path = new MenuElement[1]; 473 path[0] = sel2[0]; 474 path[0].processKeyEvent(e, path, this); 475 if (e.isConsumed()) { 476 return; 477 } 478 } 479 480 /** 481 * Return true if {@code c} is part of the currently used menu 482 * 483 * @param c a {@code Component} 484 * @return true if {@code c} is part of the currently used menu, 485 * false otherwise 486 */ 487 public boolean isComponentPartOfCurrentMenu(Component c) { 488 if(selection.size() > 0) { 489 MenuElement me = selection.elementAt(0); 490 return isComponentPartOfCurrentMenu(me,c); 491 } else 492 return false; 493 } 494 495 private boolean isComponentPartOfCurrentMenu(MenuElement root,Component c) { 496 MenuElement children[]; 497 int i,d; 498 499 if (root == null) 500 return false; 501 502 if(root.getComponent() == c) 503 return true; 504 else { 505 children = root.getSubElements(); 506 for(i=0,d=children.length;i<d;i++) { 507 if(isComponentPartOfCurrentMenu(children[i],c)) 508 return true; 509 } 510 } 511 return false; 512 } 513 }