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