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 protected EventListenerList listenerList = new EventListenerList(); 84 85 /** 86 * Changes the selection in the menu hierarchy. The elements 87 * in the array are sorted in order from the root menu 88 * element to the currently selected menu element. 89 * <p> 90 * Note that this method is public but is used by the look and 91 * feel engine and should not be called by client applications. 92 * 93 * @param path an array of <code>MenuElement</code> objects specifying 94 * the selected path 95 */ 96 public void setSelectedPath(MenuElement[] path) { 97 int i,c; 98 int currentSelectionCount = selection.size(); 99 int firstDifference = 0; 100 101 if(path == null) { 102 path = new MenuElement[0]; 103 } 104 105 if (DEBUG) { 106 System.out.print("Previous: "); printMenuElementArray(getSelectedPath()); 107 System.out.print("New: "); printMenuElementArray(path); 108 } 109 110 for(i=0,c=path.length;i<c;i++) { 111 if (i < currentSelectionCount && selection.elementAt(i) == path[i]) 112 firstDifference++; 113 else 114 break; 115 } 116 117 for(i=currentSelectionCount - 1 ; i >= firstDifference ; i--) { 118 MenuElement me = selection.elementAt(i); 119 selection.removeElementAt(i); 120 me.menuSelectionChanged(false); 121 } 122 123 for(i = firstDifference, c = path.length ; i < c ; i++) { 124 if (path[i] != null) { 125 selection.addElement(path[i]); 126 path[i].menuSelectionChanged(true); 127 } 128 } 129 130 fireStateChanged(); 131 } 132 133 /** 134 * Returns the path to the currently selected menu item 135 * 136 * @return an array of MenuElement objects representing the selected path 137 */ 138 public MenuElement[] getSelectedPath() { 139 MenuElement res[] = new MenuElement[selection.size()]; 140 int i,c; 141 for(i=0,c=selection.size();i<c;i++) 142 res[i] = selection.elementAt(i); 143 return res; 144 } 145 146 /** 147 * Tell the menu selection to close and unselect all the menu components. Call this method 148 * when a choice has been made 149 */ 150 public void clearSelectedPath() { 151 if (selection.size() > 0) { 152 setSelectedPath(null); 153 } 154 } 155 156 /** 157 * Adds a ChangeListener to the button. 158 * 159 * @param l the listener to add 160 */ 161 public void addChangeListener(ChangeListener l) { 162 listenerList.add(ChangeListener.class, l); 163 } 164 165 /** 166 * Removes a ChangeListener from the button. 167 * 168 * @param l the listener to remove 169 */ 170 public void removeChangeListener(ChangeListener l) { 171 listenerList.remove(ChangeListener.class, l); 172 } 173 174 /** 175 * Returns an array of all the <code>ChangeListener</code>s added 176 * to this MenuSelectionManager with addChangeListener(). 177 * 178 * @return all of the <code>ChangeListener</code>s added or an empty 179 * array if no listeners have been added 180 * @since 1.4 181 */ 182 public ChangeListener[] getChangeListeners() { 183 return listenerList.getListeners(ChangeListener.class); 184 } 185 186 /** 187 * Notifies all listeners that have registered interest for 188 * notification on this event type. The event instance 189 * is created lazily. 190 * 191 * @see EventListenerList 192 */ 193 protected void fireStateChanged() { 194 // Guaranteed to return a non-null array 195 Object[] listeners = listenerList.getListenerList(); 196 // Process the listeners last to first, notifying 197 // those that are interested in this event 198 for (int i = listeners.length-2; i>=0; i-=2) { 199 if (listeners[i]==ChangeListener.class) { 200 // Lazily create the event: 201 if (changeEvent == null) 202 changeEvent = new ChangeEvent(this); 203 ((ChangeListener)listeners[i+1]).stateChanged(changeEvent); 204 } 205 } 206 } 207 208 /** 209 * When a MenuElement receives an event from a MouseListener, it should never process the event 210 * directly. Instead all MenuElements should call this method with the event. 211 * 212 * @param event a MouseEvent object 213 */ 214 public void processMouseEvent(MouseEvent event) { 215 int screenX,screenY; 216 Point p; 217 int i,c,j,d; 218 Component mc; 219 Rectangle r2; 220 int cWidth,cHeight; 221 MenuElement menuElement; 222 MenuElement subElements[]; 223 MenuElement path[]; 224 Vector<MenuElement> tmp; 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 tmp = (Vector<MenuElement>)selection.clone(); 254 selectionSize = tmp.size(); 255 boolean success = false; 256 for (i=selectionSize - 1;i >= 0 && success == false; i--) { 257 menuElement = tmp.elementAt(i); 258 subElements = menuElement.getSubElements(); 259 260 path = null; 261 for (j = 0, d = subElements.length;j < d && success == false; j++) { 262 if (subElements[j] == null) 263 continue; 264 mc = subElements[j].getComponent(); 265 if(!mc.isShowing()) 266 continue; 267 if(mc instanceof JComponent) { 268 cWidth = mc.getWidth(); 269 cHeight = mc.getHeight(); 270 } else { 271 r2 = mc.getBounds(); 272 cWidth = r2.width; 273 cHeight = r2.height; 274 } 275 p.x = screenX; 276 p.y = screenY; 277 SwingUtilities.convertPointFromScreen(p,mc); 278 279 /** Send the event to visible menu element if menu element currently in 280 * the selected path or contains the event location 281 */ 282 if( 283 (p.x >= 0 && p.x < cWidth && p.y >= 0 && p.y < cHeight)) { 284 int k; 285 if(path == null) { 286 path = new MenuElement[i+2]; 287 for(k=0;k<=i;k++) 288 path[k] = tmp.elementAt(k); 289 } 290 path[i+1] = subElements[j]; 291 MenuElement currentSelection[] = getSelectedPath(); 292 293 // Enter/exit detection -- needs tuning... 294 if (currentSelection[currentSelection.length-1] != 295 path[i+1] && 296 (currentSelection.length < 2 || 297 currentSelection[currentSelection.length-2] != 298 path[i+1])) { 299 Component oldMC = currentSelection[currentSelection.length-1].getComponent(); 300 301 MouseEvent exitEvent = new MouseEvent(oldMC, MouseEvent.MOUSE_EXITED, 302 event.getWhen(), 303 event.getModifiers(), p.x, p.y, 304 event.getXOnScreen(), 305 event.getYOnScreen(), 306 event.getClickCount(), 307 event.isPopupTrigger(), 308 MouseEvent.NOBUTTON); 309 currentSelection[currentSelection.length-1]. 310 processMouseEvent(exitEvent, path, this); 311 312 MouseEvent enterEvent = new MouseEvent(mc, 313 MouseEvent.MOUSE_ENTERED, 314 event.getWhen(), 315 event.getModifiers(), p.x, p.y, 316 event.getXOnScreen(), 317 event.getYOnScreen(), 318 event.getClickCount(), 319 event.isPopupTrigger(), 320 MouseEvent.NOBUTTON); 321 subElements[j].processMouseEvent(enterEvent, path, this); 322 } 323 MouseEvent mouseEvent = new MouseEvent(mc, event.getID(),event. getWhen(), 324 event.getModifiers(), p.x, p.y, 325 event.getXOnScreen(), 326 event.getYOnScreen(), 327 event.getClickCount(), 328 event.isPopupTrigger(), 329 MouseEvent.NOBUTTON); 330 subElements[j].processMouseEvent(mouseEvent, path, this); 331 success = true; 332 event.consume(); 333 } 334 } 335 } 336 } 337 338 private void printMenuElementArray(MenuElement path[]) { 339 printMenuElementArray(path, false); 340 } 341 342 private void printMenuElementArray(MenuElement path[], boolean dumpStack) { 343 System.out.println("Path is("); 344 int i, j; 345 for(i=0,j=path.length; i<j ;i++){ 346 for (int k=0; k<=i; k++) 347 System.out.print(" "); 348 MenuElement me = path[i]; 349 if(me instanceof JMenuItem) { 350 System.out.println(((JMenuItem)me).getText() + ", "); 351 } else if (me instanceof JMenuBar) { 352 System.out.println("JMenuBar, "); 353 } else if(me instanceof JPopupMenu) { 354 System.out.println("JPopupMenu, "); 355 } else if (me == null) { 356 System.out.println("NULL , "); 357 } else { 358 System.out.println("" + me + ", "); 359 } 360 } 361 System.out.println(")"); 362 363 if (dumpStack == true) 364 Thread.dumpStack(); 365 } 366 367 /** 368 * Returns the component in the currently selected path 369 * which contains sourcePoint. 370 * 371 * @param source The component in whose coordinate space sourcePoint 372 * is given 373 * @param sourcePoint The point which is being tested 374 * @return The component in the currently selected path which 375 * contains sourcePoint (relative to the source component's 376 * coordinate space. If sourcePoint is not inside a component 377 * on the currently selected path, null is returned. 378 */ 379 public Component componentForPoint(Component source, Point sourcePoint) { 380 int screenX,screenY; 381 Point p = sourcePoint; 382 int i,c,j,d; 383 Component mc; 384 Rectangle r2; 385 int cWidth,cHeight; 386 MenuElement menuElement; 387 MenuElement subElements[]; 388 Vector<MenuElement> tmp; 389 int selectionSize; 390 391 SwingUtilities.convertPointToScreen(p,source); 392 393 screenX = p.x; 394 screenY = p.y; 395 396 tmp = (Vector<MenuElement>)selection.clone(); 397 selectionSize = tmp.size(); 398 for(i=selectionSize - 1 ; i >= 0 ; i--) { 399 menuElement = tmp.elementAt(i); 400 subElements = menuElement.getSubElements(); 401 402 for(j = 0, d = subElements.length ; j < d ; j++) { 403 if (subElements[j] == null) 404 continue; 405 mc = subElements[j].getComponent(); 406 if(!mc.isShowing()) 407 continue; 408 if(mc instanceof JComponent) { 409 cWidth = mc.getWidth(); 410 cHeight = mc.getHeight(); 411 } else { 412 r2 = mc.getBounds(); 413 cWidth = r2.width; 414 cHeight = r2.height; 415 } 416 p.x = screenX; 417 p.y = screenY; 418 SwingUtilities.convertPointFromScreen(p,mc); 419 420 /** Return the deepest component on the selection 421 * path in whose bounds the event's point occurs 422 */ 423 if (p.x >= 0 && p.x < cWidth && p.y >= 0 && p.y < cHeight) { 424 return mc; 425 } 426 } 427 } 428 return null; 429 } 430 431 /** 432 * When a MenuElement receives an event from a KeyListener, it should never process the event 433 * directly. Instead all MenuElements should call this method with the event. 434 * 435 * @param e a KeyEvent object 436 */ 437 public void processKeyEvent(KeyEvent e) { 438 MenuElement[] sel2 = new MenuElement[0]; 439 sel2 = selection.toArray(sel2); 440 int selSize = sel2.length; 441 MenuElement[] path; 442 443 if (selSize < 1) { 444 return; 445 } 446 447 for (int i=selSize-1; i>=0; i--) { 448 MenuElement elem = sel2[i]; 449 MenuElement[] subs = elem.getSubElements(); 450 path = null; 451 452 for (int j=0; j<subs.length; j++) { 453 if (subs[j] == null || !subs[j].getComponent().isShowing() 454 || !subs[j].getComponent().isEnabled()) { 455 continue; 456 } 457 458 if(path == null) { 459 path = new MenuElement[i+2]; 460 System.arraycopy(sel2, 0, path, 0, i+1); 461 } 462 path[i+1] = subs[j]; 463 subs[j].processKeyEvent(e, path, this); 464 if (e.isConsumed()) { 465 return; 466 } 467 } 468 } 469 470 // finally dispatch event to the first component in path 471 path = new MenuElement[1]; 472 path[0] = sel2[0]; 473 path[0].processKeyEvent(e, path, this); 474 if (e.isConsumed()) { 475 return; 476 } 477 } 478 479 /** 480 * Return true if c is part of the currently used menu 481 */ 482 public boolean isComponentPartOfCurrentMenu(Component c) { 483 if(selection.size() > 0) { 484 MenuElement me = selection.elementAt(0); 485 return isComponentPartOfCurrentMenu(me,c); 486 } else 487 return false; 488 } 489 490 private boolean isComponentPartOfCurrentMenu(MenuElement root,Component c) { 491 MenuElement children[]; 492 int i,d; 493 494 if (root == null) 495 return false; 496 497 if(root.getComponent() == c) 498 return true; 499 else { 500 children = root.getSubElements(); 501 for(i=0,d=children.length;i<d;i++) { 502 if(isComponentPartOfCurrentMenu(children[i],c)) 503 return true; 504 } 505 } 506 return false; 507 } 508 }