1 /*
2  * Copyright (c) 2011, 2018, 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 {
java.security.AccessController.doPrivileged( new java.security.PrivilegedAction<Void>() { public Void run() { System.loadLibrary(R); return null; } })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
addMenuListeners(ScreenMenu listener, long nativeMenu)55     private static native long addMenuListeners(ScreenMenu listener, long nativeMenu);
removeMenuListeners(long modelPtr)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 
ScreenMenu(final JMenu invoker)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      */
needsUpdate(final Component[] items, final int[] childHashArray)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      */
updateItems()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      */
invokeOpenLater()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      */
invokeMenuClosing()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      */
handleItemTargeted(final int inWhichItem, final int itemRectTop, final int itemRectLeft, final int itemRectBottom, final int itemRectRight)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      */
handleMouseEvent(final int kind, final int x, final int y, final int modifiers, final long when)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
addNotify()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
removeNotify()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
componentAdded(final ContainerEvent e)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
componentRemoved(final ContainerEvent e)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
componentResized(final ComponentEvent e)316     public void componentResized(final ComponentEvent e) {}
317 
318     /**
319      * Invoked when the component's position changes.
320      */
321     @Override
componentMoved(final ComponentEvent e)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
componentShown(final ComponentEvent e)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
componentHidden(final ComponentEvent e)341     public void componentHidden(final ComponentEvent e) {
342         setVisible(false);
343     }
344 
setVisible(final boolean b)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
setChildVisible(final JMenuItem child, final boolean b)358     public void setChildVisible(final JMenuItem child, final boolean b) {
359         fItems.remove(child);
360         updateItems();
361     }
362 
363     @Override
setAccelerator(final KeyStroke ks)364     public void setAccelerator(final KeyStroke ks) {}
365 
366     // only check and radio items can be indeterminate
367     @Override
setIndeterminate(boolean indeterminate)368     public void setIndeterminate(boolean indeterminate) { }
369 
370     @Override
setToolTipText(final String text)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
setIcon(final Icon i)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      */
getHashCode(final Component m)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 
addItem(final Component m)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 }
460