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.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 */ 42 public class MenuSelectionManager { 43 private Vector<MenuElement> selection = new Vector<MenuElement>(); 44 45 /* diagnostic aids -- should be false for production builds. */ 46 private static final boolean TRACE = false; // trace creates and disposes 47 private static final boolean VERBOSE = false; // show reuse hits/misses 48 private static final boolean DEBUG = false; // show bad params, misc. 49 50 private static final StringBuilder MENU_SELECTION_MANAGER_KEY = 51 new StringBuilder("javax.swing.MenuSelectionManager"); 52 53 /** 54 * Returns the default menu selection manager. 55 * 56 * @return a MenuSelectionManager object 57 */ 58 public static MenuSelectionManager defaultManager() { 59 synchronized (MENU_SELECTION_MANAGER_KEY) { 60 AppContext context = AppContext.getAppContext(); 61 MenuSelectionManager msm = (MenuSelectionManager)context.get( 62 MENU_SELECTION_MANAGER_KEY); 63 if (msm == null) { 64 msm = new MenuSelectionManager(); 65 context.put(MENU_SELECTION_MANAGER_KEY, msm); 66 67 // installing additional listener if found in the AppContext 68 Object o = context.get(SwingUtilities2.MENU_SELECTION_MANAGER_LISTENER_KEY); 69 if (o != null && o instanceof ChangeListener) { 70 msm.addChangeListener((ChangeListener) o); 71 } 72 } 73 74 return msm; 75 } 76 } 77 78 /** 79 * Only one ChangeEvent is needed per button model instance since the 80 * event's only state is the source property. The source of events 81 * generated is always "this". 82 */ 83 protected transient ChangeEvent changeEvent = null; 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 Vector<MenuElement> tmp; 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 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 = (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] = (MenuElement)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 MouseEventAccessor meAccessor = AWTAccessor.getMouseEventAccessor(); 311 meAccessor.setCausedByTouchEvent(exitEvent, 312 meAccessor.isCausedByTouchEvent(event)); 313 currentSelection[currentSelection.length-1]. 314 processMouseEvent(exitEvent, path, this); 315 316 MouseEvent enterEvent = new MouseEvent(mc, 317 MouseEvent.MOUSE_ENTERED, 318 event.getWhen(), 319 event.getModifiers(), p.x, p.y, 320 event.getXOnScreen(), 321 event.getYOnScreen(), 322 event.getClickCount(), 323 event.isPopupTrigger(), 324 MouseEvent.NOBUTTON); 325 meAccessor.setCausedByTouchEvent(enterEvent, 326 meAccessor.isCausedByTouchEvent(event)); 327 subElements[j].processMouseEvent(enterEvent, path, this); 328 } 329 MouseEvent mouseEvent = new MouseEvent(mc, event.getID(),event. getWhen(), 330 event.getModifiers(), p.x, p.y, 331 event.getXOnScreen(), 332 event.getYOnScreen(), 333 event.getClickCount(), 334 event.isPopupTrigger(), 335 MouseEvent.NOBUTTON); 336 MouseEventAccessor meAccessor = AWTAccessor.getMouseEventAccessor(); 337 meAccessor.setCausedByTouchEvent(mouseEvent, 338 meAccessor.isCausedByTouchEvent(event)); 339 subElements[j].processMouseEvent(mouseEvent, path, this); 340 success = true; 341 event.consume(); 342 } 343 } 344 } 345 } 346 347 private void printMenuElementArray(MenuElement path[]) { 348 printMenuElementArray(path, false); 349 } 350 351 private void printMenuElementArray(MenuElement path[], boolean dumpStack) { 352 System.out.println("Path is("); 353 int i, j; 354 for(i=0,j=path.length; i<j ;i++){ 355 for (int k=0; k<=i; k++) 356 System.out.print(" "); 357 MenuElement me = path[i]; 358 if(me instanceof JMenuItem) { 359 System.out.println(((JMenuItem)me).getText() + ", "); 360 } else if (me instanceof JMenuBar) { 361 System.out.println("JMenuBar, "); 362 } else if(me instanceof JPopupMenu) { 363 System.out.println("JPopupMenu, "); 364 } else if (me == null) { 365 System.out.println("NULL , "); 366 } else { 367 System.out.println("" + me + ", "); 368 } 369 } 370 System.out.println(")"); 371 372 if (dumpStack == true) 373 Thread.dumpStack(); 374 } 375 376 /** 377 * Returns the component in the currently selected path 378 * which contains sourcePoint. 379 * 380 * @param source The component in whose coordinate space sourcePoint 381 * is given 382 * @param sourcePoint The point which is being tested 383 * @return The component in the currently selected path which 384 * contains sourcePoint (relative to the source component's 385 * coordinate space. If sourcePoint is not inside a component 386 * on the currently selected path, null is returned. 387 */ 388 public Component componentForPoint(Component source, Point sourcePoint) { 389 int screenX,screenY; 390 Point p = sourcePoint; 391 int i,c,j,d; 392 Component mc; 393 Rectangle r2; 394 int cWidth,cHeight; 395 MenuElement menuElement; 396 MenuElement subElements[]; 397 Vector<MenuElement> tmp; 398 int selectionSize; 399 400 SwingUtilities.convertPointToScreen(p,source); 401 402 screenX = p.x; 403 screenY = p.y; 404 405 tmp = (Vector<MenuElement>)selection.clone(); 406 selectionSize = tmp.size(); 407 for(i=selectionSize - 1 ; i >= 0 ; i--) { 408 menuElement = (MenuElement) tmp.elementAt(i); 409 subElements = menuElement.getSubElements(); 410 411 for(j = 0, d = subElements.length ; j < d ; j++) { 412 if (subElements[j] == null) 413 continue; 414 mc = subElements[j].getComponent(); 415 if(!mc.isShowing()) 416 continue; 417 if(mc instanceof JComponent) { 418 cWidth = mc.getWidth(); 419 cHeight = mc.getHeight(); 420 } else { 421 r2 = mc.getBounds(); 422 cWidth = r2.width; 423 cHeight = r2.height; 424 } 425 p.x = screenX; 426 p.y = screenY; 427 SwingUtilities.convertPointFromScreen(p,mc); 428 429 /** Return the deepest component on the selection 430 * path in whose bounds the event's point occurs 431 */ 432 if (p.x >= 0 && p.x < cWidth && p.y >= 0 && p.y < cHeight) { 433 return mc; 434 } 435 } 436 } 437 return null; 438 } 439 440 /** 441 * When a MenuElement receives an event from a KeyListener, it should never process the event 442 * directly. Instead all MenuElements should call this method with the event. 443 * 444 * @param e a KeyEvent object 445 */ 446 public void processKeyEvent(KeyEvent e) { 447 MenuElement[] sel2 = new MenuElement[0]; 448 sel2 = selection.toArray(sel2); 449 int selSize = sel2.length; 450 MenuElement[] path; 451 452 if (selSize < 1) { 453 return; 454 } 455 456 for (int i=selSize-1; i>=0; i--) { 457 MenuElement elem = sel2[i]; 458 MenuElement[] subs = elem.getSubElements(); 459 path = null; 460 461 for (int j=0; j<subs.length; j++) { 462 if (subs[j] == null || !subs[j].getComponent().isShowing() 463 || !subs[j].getComponent().isEnabled()) { 464 continue; 465 } 466 467 if(path == null) { 468 path = new MenuElement[i+2]; 469 System.arraycopy(sel2, 0, path, 0, i+1); 470 } 471 path[i+1] = subs[j]; 472 subs[j].processKeyEvent(e, path, this); 473 if (e.isConsumed()) { 474 return; 475 } 476 } 477 } 478 479 // finally dispatch event to the first component in path 480 path = new MenuElement[1]; 481 path[0] = sel2[0]; 482 path[0].processKeyEvent(e, path, this); 483 if (e.isConsumed()) { 484 return; 485 } 486 } 487 488 /** 489 * Return true if c is part of the currently used menu 490 */ 491 public boolean isComponentPartOfCurrentMenu(Component c) { 492 if(selection.size() > 0) { 493 MenuElement me = selection.elementAt(0); 494 return isComponentPartOfCurrentMenu(me,c); 495 } else 496 return false; 497 } 498 499 private boolean isComponentPartOfCurrentMenu(MenuElement root,Component c) { 500 MenuElement children[]; 501 int i,d; 502 503 if (root == null) 504 return false; 505 506 if(root.getComponent() == c) 507 return true; 508 else { 509 children = root.getSubElements(); 510 for(i=0,d=children.length;i<d;i++) { 511 if(isComponentPartOfCurrentMenu(children[i],c)) 512 return true; 513 } 514 } 515 return false; 516 } 517 }