1 /*
2  * Copyright (c) 2011, 2017, 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 sun.lwawt.macosx;
27 
28 import java.awt.AWTEvent;
29 import java.awt.Button;
30 import java.awt.Frame;
31 import java.awt.Graphics2D;
32 import java.awt.GraphicsConfiguration;
33 import java.awt.GraphicsDevice;
34 import java.awt.GraphicsEnvironment;
35 import java.awt.Image;
36 import java.awt.MediaTracker;
37 import java.awt.PopupMenu;
38 import java.awt.RenderingHints;
39 import java.awt.Toolkit;
40 import java.awt.Transparency;
41 import java.awt.TrayIcon;
42 import java.awt.event.ActionEvent;
43 import java.awt.event.MouseEvent;
44 import java.awt.geom.Point2D;
45 import java.awt.image.BufferedImage;
46 import java.awt.image.ImageObserver;
47 import java.awt.peer.TrayIconPeer;
48 
49 import javax.swing.Icon;
50 import javax.swing.UIManager;
51 
52 import sun.awt.SunToolkit;
53 
54 import static sun.awt.AWTAccessor.MenuComponentAccessor;
55 import static sun.awt.AWTAccessor.getMenuComponentAccessor;
56 
57 public class CTrayIcon extends CFRetainedResource implements TrayIconPeer {
58     private TrayIcon target;
59     private PopupMenu popup;
60 
61     // In order to construct MouseEvent object, we need to specify a
62     // Component target. Because TrayIcon isn't Component's subclass,
63     // we use this dummy frame instead
64     private final Frame dummyFrame;
65     IconObserver observer = new IconObserver();
66 
67     // A bitmask that indicates what mouse buttons produce MOUSE_CLICKED events
68     // on MOUSE_RELEASE. Click events are only generated if there were no drag
69     // events between MOUSE_PRESSED and MOUSE_RELEASED for particular button
70     private static int mouseClickButtons = 0;
71 
CTrayIcon(TrayIcon target)72     CTrayIcon(TrayIcon target) {
73         super(0, true);
74 
75         this.target = target;
76         this.popup = target.getPopupMenu();
77         this.dummyFrame = new Frame();
78         setPtr(createModel());
79 
80         //if no one else is creating the peer.
81         checkAndCreatePopupPeer();
82         updateImage();
83     }
84 
checkAndCreatePopupPeer()85     private CPopupMenu checkAndCreatePopupPeer() {
86         CPopupMenu menuPeer = null;
87         if (popup != null) {
88             try {
89                 final MenuComponentAccessor acc = getMenuComponentAccessor();
90                 menuPeer = acc.getPeer(popup);
91                 if (menuPeer == null) {
92                     popup.addNotify();
93                     menuPeer = acc.getPeer(popup);
94                 }
95             } catch (Exception e) {
96                 e.printStackTrace();
97             }
98         }
99         return menuPeer;
100     }
101 
createModel()102     private long createModel() {
103         return nativeCreate();
104     }
105 
nativeCreate()106     private native long nativeCreate();
107 
108     //invocation from the AWTTrayIcon.m
getPopupMenuModel()109     public long getPopupMenuModel() {
110         PopupMenu newPopup = target.getPopupMenu();
111 
112         if (popup == newPopup) {
113             if (popup == null) {
114                 return 0L;
115             }
116         } else {
117             if (newPopup != null) {
118                 if (popup != null) {
119                     popup.removeNotify();
120                     popup = newPopup;
121                 } else {
122                     popup = newPopup;
123                 }
124             } else {
125                 return 0L;
126             }
127         }
128 
129         // This method is executed on Appkit, so if ptr is not zero means that,
130         // it is still not deallocated(even if we call NSApp postRunnableEvent)
131         // and sent CFRelease to the native queue
132         return checkAndCreatePopupPeer().ptr;
133     }
134 
135     /**
136      * We display tray icon message as a small dialog with OK button.
137      * This is lame, but JDK 1.6 does basically the same. There is a new
138      * kind of window in Lion, NSPopover, so perhaps it could be used it
139      * to implement better looking notifications.
140      */
displayMessage(final String caption, final String text, final String messageType)141     public void displayMessage(final String caption, final String text,
142                                final String messageType) {
143         // obtain icon to show along the message
144         Icon icon = getIconForMessageType(messageType);
145         CImage cimage = null;
146         if (icon != null) {
147             BufferedImage image = scaleIcon(icon, 0.75);
148             cimage = CImage.getCreator().createFromImage(image, null);
149         }
150         if (cimage != null) {
151             cimage.execute(imagePtr -> {
152                 execute(ptr -> nativeShowNotification(ptr, caption, text,
153                                                       imagePtr));
154             });
155         } else {
156             execute(ptr -> nativeShowNotification(ptr, caption, text, 0));
157         }
158     }
159 
160     @Override
dispose()161     public void dispose() {
162         dummyFrame.dispose();
163 
164         if (popup != null) {
165             popup.removeNotify();
166         }
167 
168         LWCToolkit.targetDisposedPeer(target, this);
169         target = null;
170 
171         super.dispose();
172     }
173 
174     @Override
setToolTip(String tooltip)175     public void setToolTip(String tooltip) {
176         execute(ptr -> nativeSetToolTip(ptr, tooltip));
177     }
178 
179     //adds tooltip to the NSStatusBar's NSButton.
nativeSetToolTip(long trayIconModel, String tooltip)180     private native void nativeSetToolTip(long trayIconModel, String tooltip);
181 
182     @Override
showPopupMenu(int x, int y)183     public void showPopupMenu(int x, int y) {
184         //Not used. The popupmenu is shown from the native code.
185     }
186 
187     @Override
updateImage()188     public void updateImage() {
189 
190         Image image = target.getImage();
191         if (image != null) {
192             updateNativeImage(image);
193         }
194     }
195 
updateNativeImage(Image image)196     void updateNativeImage(Image image) {
197         MediaTracker tracker = new MediaTracker(new Button(""));
198         tracker.addImage(image, 0);
199         try {
200             tracker.waitForAll();
201         } catch (InterruptedException ignore) { }
202 
203         if (image.getWidth(null) <= 0 ||
204             image.getHeight(null) <= 0)
205         {
206             return;
207         }
208 
209         CImage cimage = CImage.getCreator().createFromImage(image, observer);
210         boolean imageAutoSize = target.isImageAutoSize();
211         if (cimage != null) {
212             cimage.execute(imagePtr -> {
213                 execute(ptr -> {
214                     setNativeImage(ptr, imagePtr, imageAutoSize);
215                 });
216             });
217         }
218     }
219 
setNativeImage(final long model, final long nsimage, final boolean autosize)220     private native void setNativeImage(final long model, final long nsimage, final boolean autosize);
221 
postEvent(final AWTEvent event)222     private void postEvent(final AWTEvent event) {
223         SunToolkit.executeOnEventHandlerThread(target, new Runnable() {
224             public void run() {
225                 SunToolkit.postEvent(SunToolkit.targetToAppContext(target), event);
226             }
227         });
228     }
229 
230     //invocation from the AWTTrayIcon.m
handleMouseEvent(NSEvent nsEvent)231     private void handleMouseEvent(NSEvent nsEvent) {
232         int buttonNumber = nsEvent.getButtonNumber();
233         final SunToolkit tk = (SunToolkit)Toolkit.getDefaultToolkit();
234         if ((buttonNumber > 2 && !tk.areExtraMouseButtonsEnabled())
235                 || buttonNumber > tk.getNumberOfButtons() - 1) {
236             return;
237         }
238 
239         int jeventType = NSEvent.nsToJavaEventType(nsEvent.getType());
240 
241         int jbuttonNumber = MouseEvent.NOBUTTON;
242         int jclickCount = 0;
243         if (jeventType != MouseEvent.MOUSE_MOVED) {
244             jbuttonNumber = NSEvent.nsToJavaButton(buttonNumber);
245             jclickCount = nsEvent.getClickCount();
246         }
247 
248         int jmodifiers = NSEvent.nsToJavaModifiers(
249                 nsEvent.getModifierFlags());
250         boolean isPopupTrigger = NSEvent.isPopupTrigger(jmodifiers);
251 
252         int eventButtonMask = (jbuttonNumber > 0)?
253                 MouseEvent.getMaskForButton(jbuttonNumber) : 0;
254         long when = System.currentTimeMillis();
255 
256         if (jeventType == MouseEvent.MOUSE_PRESSED) {
257             mouseClickButtons |= eventButtonMask;
258         } else if (jeventType == MouseEvent.MOUSE_DRAGGED) {
259             mouseClickButtons = 0;
260         }
261 
262         // The MouseEvent's coordinates are relative to screen
263         int absX = nsEvent.getAbsX();
264         int absY = nsEvent.getAbsY();
265 
266         MouseEvent mouseEvent = new MouseEvent(dummyFrame, jeventType, when,
267                 jmodifiers, absX, absY, absX, absY, jclickCount, isPopupTrigger,
268                 jbuttonNumber);
269         mouseEvent.setSource(target);
270         postEvent(mouseEvent);
271 
272         // fire ACTION event
273         if (jeventType == MouseEvent.MOUSE_PRESSED && isPopupTrigger) {
274             final String cmd = target.getActionCommand();
275             final ActionEvent event = new ActionEvent(target,
276                     ActionEvent.ACTION_PERFORMED, cmd);
277             postEvent(event);
278         }
279 
280         // synthesize CLICKED event
281         if (jeventType == MouseEvent.MOUSE_RELEASED) {
282             if ((mouseClickButtons & eventButtonMask) != 0) {
283                 MouseEvent clickEvent = new MouseEvent(dummyFrame,
284                         MouseEvent.MOUSE_CLICKED, when, jmodifiers, absX, absY,
285                         absX, absY, jclickCount, isPopupTrigger, jbuttonNumber);
286                 clickEvent.setSource(target);
287                 postEvent(clickEvent);
288             }
289 
290             mouseClickButtons &= ~eventButtonMask;
291         }
292     }
293 
nativeShowNotification(long trayIconModel, String caption, String text, long nsimage)294     private native void nativeShowNotification(long trayIconModel,
295                                                String caption, String text,
296                                                long nsimage);
297 
298     /**
299      * Used by the automated tests.
300      */
nativeGetIconLocation(long trayIconModel)301     private native Point2D nativeGetIconLocation(long trayIconModel);
302 
303     /**
304      * Scales an icon using specified scale factor
305      *
306      * @param icon        icon to scale
307      * @param scaleFactor scale factor to use
308      * @return scaled icon as BuffedredImage
309      */
scaleIcon(Icon icon, double scaleFactor)310     private static BufferedImage scaleIcon(Icon icon, double scaleFactor) {
311         if (icon == null) {
312             return null;
313         }
314 
315         int w = icon.getIconWidth();
316         int h = icon.getIconHeight();
317 
318         GraphicsEnvironment ge =
319                 GraphicsEnvironment.getLocalGraphicsEnvironment();
320         GraphicsDevice gd = ge.getDefaultScreenDevice();
321         GraphicsConfiguration gc = gd.getDefaultConfiguration();
322 
323         // convert icon into image
324         BufferedImage iconImage = gc.createCompatibleImage(w, h,
325                 Transparency.TRANSLUCENT);
326         Graphics2D g = iconImage.createGraphics();
327         icon.paintIcon(null, g, 0, 0);
328         g.dispose();
329 
330         // and scale it nicely
331         int scaledW = (int) (w * scaleFactor);
332         int scaledH = (int) (h * scaleFactor);
333         BufferedImage scaledImage = gc.createCompatibleImage(scaledW, scaledH,
334                 Transparency.TRANSLUCENT);
335         g = scaledImage.createGraphics();
336         g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
337                 RenderingHints.VALUE_INTERPOLATION_BILINEAR);
338         g.drawImage(iconImage, 0, 0, scaledW, scaledH, null);
339         g.dispose();
340 
341         return scaledImage;
342     }
343 
344 
345     /**
346      * Gets Aqua icon used in message dialog.
347      */
getIconForMessageType(String messageType)348     private static Icon getIconForMessageType(String messageType) {
349         if (messageType.equals("ERROR")) {
350             return UIManager.getIcon("OptionPane.errorIcon");
351         } else if (messageType.equals("WARNING")) {
352             return UIManager.getIcon("OptionPane.warningIcon");
353         } else {
354             // this is just an application icon
355             return UIManager.getIcon("OptionPane.informationIcon");
356         }
357     }
358 
359     class IconObserver implements ImageObserver {
360         @Override
imageUpdate(Image image, int flags, int x, int y, int width, int height)361         public boolean imageUpdate(Image image, int flags, int x, int y, int width, int height) {
362             if (target == null || image != target.getImage()) //if the image has been changed
363             {
364                 return false;
365             }
366             if ((flags & (ImageObserver.FRAMEBITS | ImageObserver.ALLBITS |
367                           ImageObserver.WIDTH | ImageObserver.HEIGHT)) != 0)
368             {
369                 SunToolkit.executeOnEventHandlerThread(target, new Runnable() {
370                             public void run() {
371                                 updateNativeImage(image);
372                             }
373                         });
374             }
375             return (flags & ImageObserver.ALLBITS) == 0;
376         }
377     }
378 }
379 
380