1 /* 2 * Copyright (c) 2011, 2015, 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 26 package com.apple.laf; 27 28 import java.awt.*; 29 import java.awt.event.*; 30 import java.util.Hashtable; 31 32 import javax.swing.*; 33 34 import sun.awt.AWTAccessor; 35 import sun.awt.SunToolkit; 36 import sun.lwawt.LWToolkit; 37 import sun.lwawt.macosx.*; 38 39 @SuppressWarnings("serial") // JDK implementation class 40 final class ScreenMenu extends Menu 41 implements ContainerListener, ComponentListener, 42 ScreenMenuPropertyHandler { 43 44 static { 45 java.security.AccessController.doPrivileged( 46 new java.security.PrivilegedAction<Void>() { 47 public Void run() { 48 System.loadLibrary("awt"); 49 return null; 50 } 51 }); 52 } 53 54 // screen menu stuff 55 private static native long addMenuListeners(ScreenMenu listener, long nativeMenu); 56 private static native void removeMenuListeners(long modelPtr); 57 58 private transient long fModelPtr; 59 60 private final Hashtable<Component, MenuItem> fItems; 61 private final JMenu fInvoker; 62 63 private Component fLastMouseEventTarget; 64 private Rectangle fLastTargetRect; 65 private volatile Rectangle[] fItemBounds; 66 67 private ScreenMenuPropertyListener fPropertyListener; 68 69 // Array of child hashes used to see if we need to recreate the Menu. 70 private int childHashArray[]; 71 72 ScreenMenu(final JMenu invoker) { 73 super(invoker.getText()); 74 fInvoker = invoker; 75 76 int count = fInvoker.getMenuComponentCount(); 77 if (count < 5) count = 5; 78 fItems = new Hashtable<Component, MenuItem>(count); 79 setEnabled(fInvoker.isEnabled()); 80 updateItems(); 81 } 82 83 /** 84 * Determine if we need to tear down the Menu and re-create it, since the contents may have changed in the Menu opened listener and 85 * we do not get notified of it, because EDT is busy in our code. We only need to update if the menu contents have changed in some 86 * way, such as the number of menu items, the text of the menuitems, icon, shortcut etc. 87 */ 88 private static boolean needsUpdate(final Component items[], final int childHashArray[]) { 89 if (items == null || childHashArray == null) { 90 return true; 91 } 92 if (childHashArray.length != items.length) { 93 return true; 94 } 95 for (int i = 0; i < items.length; i++) { 96 final int hashCode = getHashCode(items[i]); 97 if (hashCode != childHashArray[i]) { 98 return true; 99 } 100 } 101 return false; 102 } 103 104 /** 105 * Used to recreate the AWT based Menu structure that implements the Screen Menu. 106 * Also computes hashcode and stores them so that we can compare them later in needsUpdate. 107 */ 108 private void updateItems() { 109 final int count = fInvoker.getMenuComponentCount(); 110 final Component[] items = fInvoker.getMenuComponents(); 111 if (needsUpdate(items, childHashArray)) { 112 removeAll(); 113 fItems.clear(); 114 if (count <= 0) return; 115 116 childHashArray = new int[count]; 117 for (int i = 0; i < count; i++) { 118 addItem(items[i]); 119 childHashArray[i] = getHashCode(items[i]); 120 } 121 } 122 } 123 124 /** 125 * Callback from JavaMenuUpdater.m -- called when menu first opens 126 */ 127 public void invokeOpenLater() { 128 final JMenu invoker = fInvoker; 129 if (invoker == null) { 130 System.err.println("invoker is null!"); 131 return; 132 } 133 134 try { 135 LWCToolkit.invokeAndWait(new Runnable() { 136 public void run() { 137 invoker.setSelected(true); 138 invoker.validate(); 139 updateItems(); 140 fItemBounds = new Rectangle[invoker.getMenuComponentCount()]; 141 } 142 }, invoker); 143 } catch (final Exception e) { 144 System.err.println(e); 145 e.printStackTrace(); 146 } 147 } 148 149 /** 150 * Callback from JavaMenuUpdater.m -- called when menu closes. 151 */ 152 public void invokeMenuClosing() { 153 final JMenu invoker = fInvoker; 154 if (invoker == null) return; 155 156 try { 157 LWCToolkit.invokeAndWait(new Runnable() { 158 public void run() { 159 invoker.setSelected(false); 160 // Null out the tracking rectangles and the array. 161 if (fItemBounds != null) { 162 for (int i = 0; i < fItemBounds.length; i++) { 163 fItemBounds[i] = null; 164 } 165 } 166 fItemBounds = null; 167 } 168 }, invoker); 169 } catch (final Exception e) { 170 e.printStackTrace(); 171 } 172 } 173 174 /** 175 * Callback from JavaMenuUpdater.m -- called when menu item is hilighted. 176 * 177 * @param inWhichItem The menu item selected by the user. -1 if mouse moves off the menu. 178 * @param itemRectTop 179 * @param itemRectLeft 180 * @param itemRectBottom 181 * @param itemRectRight Tracking rectangle coordinates. 182 */ 183 public void handleItemTargeted(final int inWhichItem, final int itemRectTop, final int itemRectLeft, final int itemRectBottom, final int itemRectRight) { 184 if (fItemBounds == null || inWhichItem < 0 || inWhichItem > (fItemBounds.length - 1)) return; 185 final Rectangle itemRect = new Rectangle(itemRectLeft, itemRectTop, itemRectRight - itemRectLeft, itemRectBottom - itemRectTop); 186 fItemBounds[inWhichItem] = itemRect; 187 } 188 189 /** 190 * Callback from JavaMenuUpdater.m -- called when mouse event happens on the menu. 191 */ 192 public void handleMouseEvent(final int kind, final int x, final int y, final int modifiers, final long when) { 193 if (kind == 0) return; 194 if (fItemBounds == null) return; 195 196 SunToolkit.executeOnEventHandlerThread(fInvoker, new Runnable() { 197 @Override 198 public void run() { 199 Component target = null; 200 Rectangle targetRect = null; 201 for (int i = 0; i < fItemBounds.length; i++) { 202 final Rectangle testRect = fItemBounds[i]; 203 if (testRect != null) { 204 if (testRect.contains(x, y)) { 205 target = fInvoker.getMenuComponent(i); 206 targetRect = testRect; 207 break; 208 } 209 } 210 } 211 if (target == null && fLastMouseEventTarget == null) return; 212 213 // Send a mouseExited to the previously hilited item, if it wasn't 0. 214 if (target != fLastMouseEventTarget) { 215 if (fLastMouseEventTarget != null) { 216 LWToolkit.postEvent( 217 new MouseEvent(fLastMouseEventTarget, 218 MouseEvent.MOUSE_EXITED, when, 219 modifiers, x - fLastTargetRect.x, 220 y - fLastTargetRect.y, 0, 221 false)); 222 } 223 // Send a mouseEntered to the current hilited item, if it 224 // wasn't 0. 225 if (target != null) { 226 LWToolkit.postEvent( 227 new MouseEvent(target, MouseEvent.MOUSE_ENTERED, 228 when, modifiers, 229 x - targetRect.x, 230 y - targetRect.y, 0, false)); 231 } 232 fLastMouseEventTarget = target; 233 fLastTargetRect = targetRect; 234 } 235 // Post a mouse event to the current item. 236 if (target == null) return; 237 LWToolkit.postEvent( 238 new MouseEvent(target, kind, when, modifiers, 239 x - targetRect.x, y - targetRect.y, 0, 240 false)); 241 } 242 }); 243 } 244 245 @Override 246 public void addNotify() { 247 synchronized (getTreeLock()) { 248 super.addNotify(); 249 if (fModelPtr == 0) { 250 fInvoker.getPopupMenu().addContainerListener(this); 251 fInvoker.addComponentListener(this); 252 fPropertyListener = new ScreenMenuPropertyListener(this); 253 fInvoker.addPropertyChangeListener(fPropertyListener); 254 255 final Icon icon = fInvoker.getIcon(); 256 if (icon != null) { 257 setIcon(icon); 258 } 259 260 final String tooltipText = fInvoker.getToolTipText(); 261 if (tooltipText != null) { 262 setToolTipText(tooltipText); 263 } 264 final Object peer = AWTAccessor.getMenuComponentAccessor() 265 .getPeer(this); 266 if (peer instanceof CMenu) { 267 final CMenu menu = (CMenu) peer; 268 final long nativeMenu = menu.getNativeMenu(); 269 fModelPtr = addMenuListeners(this, nativeMenu); 270 } 271 } 272 } 273 } 274 275 @Override 276 public void removeNotify() { 277 synchronized (getTreeLock()) { 278 // Call super so that the NSMenu has been removed, before we release 279 // the delegate in removeMenuListeners 280 super.removeNotify(); 281 fItems.clear(); 282 if (fModelPtr != 0) { 283 removeMenuListeners(fModelPtr); 284 fModelPtr = 0; 285 fInvoker.getPopupMenu().removeContainerListener(this); 286 fInvoker.removeComponentListener(this); 287 fInvoker.removePropertyChangeListener(fPropertyListener); 288 } 289 } 290 } 291 292 /** 293 * Invoked when a component has been added to the container. 294 */ 295 @Override 296 public void componentAdded(final ContainerEvent e) { 297 addItem(e.getChild()); 298 } 299 300 /** 301 * Invoked when a component has been removed from the container. 302 */ 303 @Override 304 public void componentRemoved(final ContainerEvent e) { 305 final Component child = e.getChild(); 306 final MenuItem sm = fItems.remove(child); 307 if (sm == null) return; 308 309 remove(sm); 310 } 311 312 /** 313 * Invoked when the component's size changes. 314 */ 315 @Override 316 public void componentResized(final ComponentEvent e) {} 317 318 /** 319 * Invoked when the component's position changes. 320 */ 321 @Override 322 public void componentMoved(final ComponentEvent e) {} 323 324 /** 325 * Invoked when the component has been made visible. 326 * See componentHidden - we should still have a MenuItem 327 * it just isn't inserted 328 */ 329 @Override 330 public void componentShown(final ComponentEvent e) { 331 setVisible(true); 332 } 333 334 /** 335 * Invoked when the component has been made invisible. 336 * MenuComponent.setVisible does nothing, 337 * so we remove the ScreenMenuItem from the ScreenMenu 338 * but leave it in fItems 339 */ 340 @Override 341 public void componentHidden(final ComponentEvent e) { 342 setVisible(false); 343 } 344 345 private void setVisible(final boolean b) { 346 // Tell our parent to add/remove us 347 final MenuContainer parent = getParent(); 348 349 if (parent != null) { 350 if (parent instanceof ScreenMenu) { 351 final ScreenMenu sm = (ScreenMenu)parent; 352 sm.setChildVisible(fInvoker, b); 353 } 354 } 355 } 356 357 @Override 358 public void setChildVisible(final JMenuItem child, final boolean b) { 359 fItems.remove(child); 360 updateItems(); 361 } 362 363 @Override 364 public void setAccelerator(final KeyStroke ks) {} 365 366 // only check and radio items can be indeterminate 367 @Override 368 public void setIndeterminate(boolean indeterminate) { } 369 370 @Override 371 public void setToolTipText(final String text) { 372 Object peer = AWTAccessor.getMenuComponentAccessor().getPeer(this); 373 if (!(peer instanceof CMenuItem)) return; 374 375 final CMenuItem cmi = (CMenuItem)peer; 376 cmi.setToolTipText(text); 377 } 378 379 @Override 380 public void setIcon(final Icon i) { 381 Object peer = AWTAccessor.getMenuComponentAccessor().getPeer(this); 382 if (!(peer instanceof CMenuItem)) return; 383 384 final CMenuItem cmi = (CMenuItem)peer; 385 Image img = null; 386 387 if (i != null) { 388 if (i.getIconWidth() > 0 && i.getIconHeight() > 0) { 389 img = AquaIcon.getImageForIcon(i); 390 } 391 } 392 cmi.setImage(img); 393 } 394 395 396 /** 397 * Gets a hashCode for a JMenu or JMenuItem or subclass so that we can compare for 398 * changes in the Menu. 399 */ 400 private static int getHashCode(final Component m) { 401 int hashCode = m.hashCode(); 402 403 if (m instanceof JMenuItem) { 404 final JMenuItem mi = (JMenuItem) m; 405 406 final String text = mi.getText(); 407 if (text != null) hashCode ^= text.hashCode(); 408 409 final Icon icon = mi.getIcon(); 410 if (icon != null) hashCode ^= icon.hashCode(); 411 412 final Icon disabledIcon = mi.getDisabledIcon(); 413 if (disabledIcon != null) hashCode ^= disabledIcon.hashCode(); 414 415 final Action action = mi.getAction(); 416 if (action != null) hashCode ^= action.hashCode(); 417 418 final KeyStroke ks = mi.getAccelerator(); 419 if (ks != null) hashCode ^= ks.hashCode(); 420 421 hashCode ^= Boolean.valueOf(mi.isVisible()).hashCode(); 422 hashCode ^= Boolean.valueOf(mi.isEnabled()).hashCode(); 423 hashCode ^= Boolean.valueOf(mi.isSelected()).hashCode(); 424 425 } else if (m instanceof JSeparator) { 426 hashCode ^= "-".hashCode(); 427 } 428 429 return hashCode; 430 } 431 432 private void addItem(final Component m) { 433 if (!m.isVisible()) return; 434 MenuItem sm = fItems.get(m); 435 436 if (sm == null) { 437 if (m instanceof JMenu) { 438 sm = new ScreenMenu((JMenu)m); 439 } else if (m instanceof JCheckBoxMenuItem) { 440 sm = new ScreenMenuItemCheckbox((JCheckBoxMenuItem)m); 441 } else if (m instanceof JRadioButtonMenuItem) { 442 sm = new ScreenMenuItemCheckbox((JRadioButtonMenuItem)m); 443 } else if (m instanceof JMenuItem) { 444 sm = new ScreenMenuItem((JMenuItem)m); 445 } else if (m instanceof JPopupMenu.Separator || m instanceof JSeparator) { 446 sm = new MenuItem("-"); // This is what java.awt.Menu.addSeparator does 447 } 448 449 // Only place the menu item in the hashtable if we just created it. 450 if (sm != null) { 451 fItems.put(m, sm); 452 } 453 } 454 455 if (sm != null) { 456 add(sm); 457 } 458 } 459 }