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