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 @SuppressWarnings("deprecation") 216 public void processMouseEvent(MouseEvent event) { 217 int screenX,screenY; 218 Point p; 219 int i,c,j,d; 220 Component mc; 221 Rectangle r2; 222 int cWidth,cHeight; 223 MenuElement menuElement; 224 MenuElement subElements[]; 225 MenuElement path[]; 226 int selectionSize; 227 p = event.getPoint(); 228 229 Component source = event.getComponent(); 230 231 if ((source != null) && !source.isShowing()) { 232 // This can happen if a mouseReleased removes the 233 // containing component -- bug 4146684 234 return; 235 } 236 237 int type = event.getID(); 238 int modifiers = event.getModifiers(); 239 // 4188027: drag enter/exit added in JDK 1.1.7A, JDK1.2 240 if ((type==MouseEvent.MOUSE_ENTERED|| 241 type==MouseEvent.MOUSE_EXITED) 242 && ((modifiers & (InputEvent.BUTTON1_MASK | 243 InputEvent.BUTTON2_MASK | InputEvent.BUTTON3_MASK)) !=0 )) { 244 return; 245 } 246 247 if (source != null) { 248 SwingUtilities.convertPointToScreen(p, source); 249 } 250 251 screenX = p.x; 252 screenY = p.y; 253 254 @SuppressWarnings("unchecked") 255 Vector<MenuElement> tmp = (Vector<MenuElement>)selection.clone(); 256 selectionSize = tmp.size(); 257 boolean success = false; 258 for (i=selectionSize - 1;i >= 0 && success == false; i--) { 259 menuElement = tmp.elementAt(i); 260 subElements = menuElement.getSubElements(); 261 262 path = null; 263 for (j = 0, d = subElements.length;j < d && success == false; j++) { 264 if (subElements[j] == null) 265 continue; 266 mc = subElements[j].getComponent(); 267 if(!mc.isShowing()) 268 continue; 269 if(mc instanceof JComponent) { 270 cWidth = mc.getWidth(); 271 cHeight = mc.getHeight(); 272 } else { 273 r2 = mc.getBounds(); 274 cWidth = r2.width; 275 cHeight = r2.height; 276 } 277 p.x = screenX; 278 p.y = screenY; 279 SwingUtilities.convertPointFromScreen(p,mc); 280 281 /** Send the event to visible menu element if menu element currently in 282 * the selected path or contains the event location 283 */ 284 if( 285 (p.x >= 0 && p.x < cWidth && p.y >= 0 && p.y < cHeight)) { 286 int k; 287 if(path == null) { 288 path = new MenuElement[i+2]; 289 for(k=0;k<=i;k++) 290 path[k] = tmp.elementAt(k); 291 } 292 path[i+1] = subElements[j]; 293 MenuElement currentSelection[] = getSelectedPath(); 294 295 // Enter/exit detection -- needs tuning... 296 if (currentSelection[currentSelection.length-1] != 297 path[i+1] && 298 (currentSelection.length < 2 || 299 currentSelection[currentSelection.length-2] != 300 path[i+1])) { 301 Component oldMC = currentSelection[currentSelection.length-1].getComponent(); 302 303 MouseEvent exitEvent = new MouseEvent(oldMC, MouseEvent.MOUSE_EXITED, 304 event.getWhen(), 305 event.getModifiers(), p.x, p.y, 306 event.getXOnScreen(), 307 event.getYOnScreen(), 308 event.getClickCount(), 309 event.isPopupTrigger(), 310 MouseEvent.NOBUTTON); 311 currentSelection[currentSelection.length-1]. 312 processMouseEvent(exitEvent, path, this); 313 314 MouseEvent enterEvent = new MouseEvent(mc, 315 MouseEvent.MOUSE_ENTERED, 316 event.getWhen(), 317 event.getModifiers(), p.x, p.y, 318 event.getXOnScreen(), 319 event.getYOnScreen(), 320 event.getClickCount(), 321 event.isPopupTrigger(), 322 MouseEvent.NOBUTTON); 323 subElements[j].processMouseEvent(enterEvent, path, this); 324 } 325 MouseEvent mouseEvent = new MouseEvent(mc, event.getID(),event. getWhen(), 326 event.getModifiers(), p.x, p.y, 327 event.getXOnScreen(), 328 event.getYOnScreen(), 329 event.getClickCount(), 330 event.isPopupTrigger(), 331 MouseEvent.NOBUTTON); 332 subElements[j].processMouseEvent(mouseEvent, path, this); 333 success = true; 334 event.consume(); 335 } 336 } 337 } 338 } 339 340 private void printMenuElementArray(MenuElement path[]) { 341 printMenuElementArray(path, false); 342 } 343 344 private void printMenuElementArray(MenuElement path[], boolean dumpStack) { 345 System.out.println("Path is("); 346 int i, j; 347 for(i=0,j=path.length; i<j ;i++){ 348 for (int k=0; k<=i; k++) 349 System.out.print(" "); 350 MenuElement me = path[i]; 351 if(me instanceof JMenuItem) { 352 System.out.println(((JMenuItem)me).getText() + ", "); 353 } else if (me instanceof JMenuBar) { 354 System.out.println("JMenuBar, "); 355 } else if(me instanceof JPopupMenu) { 356 System.out.println("JPopupMenu, "); 357 } else if (me == null) { 358 System.out.println("NULL , "); 359 } else { 360 System.out.println("" + me + ", "); 361 } 362 } 363 System.out.println(")"); 364 365 if (dumpStack == true) 366 Thread.dumpStack(); 367 } 368 369 /** 370 * Returns the component in the currently selected path 371 * which contains sourcePoint. 372 * 373 * @param source The component in whose coordinate space sourcePoint 374 * is given 375 * @param sourcePoint The point which is being tested 376 * @return The component in the currently selected path which 377 * contains sourcePoint (relative to the source component's 378 * coordinate space. If sourcePoint is not inside a component 379 * on the currently selected path, null is returned. 380 */ 381 public Component componentForPoint(Component source, Point sourcePoint) { 382 int screenX,screenY; 383 Point p = sourcePoint; 384 int i,c,j,d; 385 Component mc; 386 Rectangle r2; 387 int cWidth,cHeight; 388 MenuElement menuElement; 389 MenuElement subElements[]; 390 int selectionSize; 391 392 SwingUtilities.convertPointToScreen(p,source); 393 394 screenX = p.x; 395 screenY = p.y; 396 397 @SuppressWarnings("unchecked") 398 Vector<MenuElement> tmp = (Vector<MenuElement>)selection.clone(); 399 selectionSize = tmp.size(); 400 for(i=selectionSize - 1 ; i >= 0 ; i--) { 401 menuElement = tmp.elementAt(i); 402 subElements = menuElement.getSubElements(); 403 404 for(j = 0, d = subElements.length ; j < d ; j++) { 405 if (subElements[j] == null) 406 continue; 407 mc = subElements[j].getComponent(); 408 if(!mc.isShowing()) 409 continue; 410 if(mc instanceof JComponent) { 411 cWidth = mc.getWidth(); 412 cHeight = mc.getHeight(); 413 } else { 414 r2 = mc.getBounds(); 415 cWidth = r2.width; 416 cHeight = r2.height; 417 } 418 p.x = screenX; 419 p.y = screenY; 420 SwingUtilities.convertPointFromScreen(p,mc); 421 422 /** Return the deepest component on the selection 423 * path in whose bounds the event's point occurs 424 */ 425 if (p.x >= 0 && p.x < cWidth && p.y >= 0 && p.y < cHeight) { 426 return mc; 427 } 428 } 429 } 430 return null; 431 } 432 433 /** 434 * When a MenuElement receives an event from a KeyListener, it should never process the event 435 * directly. Instead all MenuElements should call this method with the event. 436 * 437 * @param e a KeyEvent object 438 */ 439 public void processKeyEvent(KeyEvent e) { 440 MenuElement[] sel2 = new MenuElement[0]; 441 sel2 = selection.toArray(sel2); 442 int selSize = sel2.length; 443 MenuElement[] path; 444 445 if (selSize < 1) { 446 return; 447 } 448 449 for (int i=selSize-1; i>=0; i--) { 450 MenuElement elem = sel2[i]; 451 MenuElement[] subs = elem.getSubElements(); 452 path = null; 453 454 for (int j=0; j<subs.length; j++) { 455 if (subs[j] == null || !subs[j].getComponent().isShowing() 456 || !subs[j].getComponent().isEnabled()) { 457 continue; 458 } 459 460 if(path == null) { 461 path = new MenuElement[i+2]; 462 System.arraycopy(sel2, 0, path, 0, i+1); 463 } 464 path[i+1] = subs[j]; 465 subs[j].processKeyEvent(e, path, this); 466 if (e.isConsumed()) { 467 return; 468 } 469 } 470 } 471 472 // finally dispatch event to the first component in path 473 path = new MenuElement[1]; 474 path[0] = sel2[0]; 475 path[0].processKeyEvent(e, path, this); 476 if (e.isConsumed()) { 477 return; 478 } 479 } 480 481 /** 482 * Return true if {@code c} is part of the currently used menu 483 * 484 * @param c a {@code Component} 485 * @return true if {@code c} is part of the currently used menu, 486 * false otherwise 487 */ 488 public boolean isComponentPartOfCurrentMenu(Component c) { 489 if(selection.size() > 0) { 490 MenuElement me = selection.elementAt(0); 491 return isComponentPartOfCurrentMenu(me,c); 492 } else 493 return false; 494 } 495 496 private boolean isComponentPartOfCurrentMenu(MenuElement root,Component c) { 497 MenuElement children[]; 498 int i,d; 499 500 if (root == null) 501 return false; 502 503 if(root.getComponent() == c) 504 return true; 505 else { 506 children = root.getSubElements(); 507 for(i=0,d=children.length;i<d;i++) { 508 if(isComponentPartOfCurrentMenu(children[i],c)) 509 return true; 510 } 511 } 512 return false; 513 } 514 }