1 /*
   2  * Copyright (c) 2011, 2015, 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         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
  55     private static native long addMenuListeners(ScreenMenu listener, long nativeMenu);
  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 
  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      */
  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      */
 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      */
 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      */
 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      */
 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      */
 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
 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
 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
 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
 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
 316     public void componentResized(final ComponentEvent e) {}
 317 
 318     /**
 319      * Invoked when the component's position changes.
 320      */
 321     @Override
 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
 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
 341     public void componentHidden(final ComponentEvent e) {
 342         setVisible(false);
 343     }
 344 
 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
 358     public void setChildVisible(final JMenuItem child, final boolean b) {
 359         fItems.remove(child);
 360         updateItems();
 361     }
 362 
 363     @Override
 364     public void setAccelerator(final KeyStroke ks) {}
 365 
 366     // only check and radio items can be indeterminate
 367     @Override
 368     public void setIndeterminate(boolean indeterminate) { }
 369 
 370     @Override
 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
 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      */
 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 
 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 }