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