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