1 /* 2 * Copyright (c) 2011, 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 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 { java.security.AccessController.doPrivileged( new java.security.PrivilegedAction<Void>() { public Void run() { System.loadLibrary(R); return null; } })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 addMenuListeners(ScreenMenu listener, long nativeMenu)55 private static native long addMenuListeners(ScreenMenu listener, long nativeMenu); removeMenuListeners(long modelPtr)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 ScreenMenu(final JMenu invoker)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 */ needsUpdate(final Component[] items, final int[] childHashArray)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 */ updateItems()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 */ invokeOpenLater()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 */ invokeMenuClosing()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 */ handleItemTargeted(final int inWhichItem, final int itemRectTop, final int itemRectLeft, final int itemRectBottom, final int itemRectRight)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 */ handleMouseEvent(final int kind, final int x, final int y, final int modifiers, final long when)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 addNotify()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 removeNotify()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 componentAdded(final ContainerEvent e)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 componentRemoved(final ContainerEvent e)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 componentResized(final ComponentEvent e)316 public void componentResized(final ComponentEvent e) {} 317 318 /** 319 * Invoked when the component's position changes. 320 */ 321 @Override componentMoved(final ComponentEvent e)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 componentShown(final ComponentEvent e)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 componentHidden(final ComponentEvent e)341 public void componentHidden(final ComponentEvent e) { 342 setVisible(false); 343 } 344 setVisible(final boolean b)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 setChildVisible(final JMenuItem child, final boolean b)358 public void setChildVisible(final JMenuItem child, final boolean b) { 359 fItems.remove(child); 360 updateItems(); 361 } 362 363 @Override setAccelerator(final KeyStroke ks)364 public void setAccelerator(final KeyStroke ks) {} 365 366 // only check and radio items can be indeterminate 367 @Override setIndeterminate(boolean indeterminate)368 public void setIndeterminate(boolean indeterminate) { } 369 370 @Override setToolTipText(final String text)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 setIcon(final Icon i)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 */ getHashCode(final Component m)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 addItem(final Component m)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 } 460