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 }