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