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