1 /*
2  * Copyright (c) 2011, 2016, 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 sun.awt.AWTAccessor;
29 import sun.awt.SunToolkit;
30 
31 import javax.swing.*;
32 import java.awt.*;
33 import java.awt.event.*;
34 import java.awt.geom.Point2D;
35 import java.awt.image.BufferedImage;
36 import java.awt.peer.TrayIconPeer;
37 import java.beans.PropertyChangeEvent;
38 import java.beans.PropertyChangeListener;
39 import java.util.concurrent.atomic.AtomicReference;
40 
41 public class CTrayIcon extends CFRetainedResource implements TrayIconPeer {
42     private TrayIcon target;
43     private PopupMenu popup;
44     private JDialog messageDialog;
45     private DialogEventHandler handler;
46 
47     // In order to construct MouseEvent object, we need to specify a
48     // Component target. Because TrayIcon isn't Component's subclass,
49     // we use this dummy frame instead
50     private final Frame dummyFrame;
51 
52     // A bitmask that indicates what mouse buttons produce MOUSE_CLICKED events
53     // on MOUSE_RELEASE. Click events are only generated if there were no drag
54     // events between MOUSE_PRESSED and MOUSE_RELEASED for particular button
55     private static int mouseClickButtons = 0;
56 
CTrayIcon(TrayIcon target)57     CTrayIcon(TrayIcon target) {
58         super(0, true);
59 
60         this.messageDialog = null;
61         this.handler = null;
62         this.target = target;
63         this.popup = target.getPopupMenu();
64         this.dummyFrame = new Frame();
65         setPtr(createModel());
66 
67         //if no one else is creating the peer.
68         checkAndCreatePopupPeer();
69         updateImage();
70     }
71 
checkAndCreatePopupPeer()72     private CPopupMenu checkAndCreatePopupPeer() {
73         CPopupMenu menuPeer = null;
74         if (popup != null) {
75             try {
76                 menuPeer = (CPopupMenu)popup.getPeer();
77                 if (menuPeer == null) {
78                     popup.addNotify();
79                     menuPeer = (CPopupMenu)popup.getPeer();
80                 }
81             } catch (Exception e) {
82                 e.printStackTrace();
83             }
84         }
85         return menuPeer;
86     }
87 
createModel()88     private long createModel() {
89         return nativeCreate();
90     }
91 
nativeCreate()92     private native long nativeCreate();
93 
94     //invocation from the AWTTrayIcon.m
getPopupMenuModel()95     public long getPopupMenuModel(){
96         if(popup == null) {
97             PopupMenu popupMenu = target.getPopupMenu();
98             if (popupMenu != null) {
99                 popup = popupMenu;
100             } else {
101                 return 0L;
102             }
103         }
104         // This method is executed on Appkit, so if ptr is not zero means that,
105         // it is still not deallocated(even if we call NSApp postRunnableEvent)
106         // and sent CFRelease to the native queue
107         return checkAndCreatePopupPeer().ptr;
108     }
109 
110     /**
111      * We display tray icon message as a small dialog with OK button.
112      * This is lame, but JDK 1.6 does basically the same. There is a new
113      * kind of window in Lion, NSPopover, so perhaps it could be used it
114      * to implement better looking notifications.
115      */
displayMessage(final String caption, final String text, final String messageType)116     public void displayMessage(final String caption, final String text,
117                                final String messageType) {
118 
119         if (SwingUtilities.isEventDispatchThread()) {
120             displayMessageOnEDT(caption, text, messageType);
121         } else {
122             try {
123                 SwingUtilities.invokeAndWait(new Runnable() {
124                     public void run() {
125                         displayMessageOnEDT(caption, text, messageType);
126                     }
127                 });
128             } catch (Exception e) {
129                 throw new AssertionError(e);
130             }
131         }
132     }
133 
134     @Override
dispose()135     public void dispose() {
136         if (messageDialog != null) {
137             disposeMessageDialog();
138         }
139 
140         dummyFrame.dispose();
141 
142         if (popup != null) {
143             popup.removeNotify();
144         }
145 
146         LWCToolkit.targetDisposedPeer(target, this);
147         target = null;
148 
149         super.dispose();
150     }
151 
152     @Override
setToolTip(String tooltip)153     public void setToolTip(String tooltip) {
154         execute(ptr -> nativeSetToolTip(ptr, tooltip));
155     }
156 
157     //adds tooltip to the NSStatusBar's NSButton.
nativeSetToolTip(long trayIconModel, String tooltip)158     private native void nativeSetToolTip(long trayIconModel, String tooltip);
159 
160     @Override
showPopupMenu(int x, int y)161     public void showPopupMenu(int x, int y) {
162         //Not used. The popupmenu is shown from the native code.
163     }
164 
165     @Override
updateImage()166     public void updateImage() {
167         Image image = target.getImage();
168         if (image == null) return;
169 
170         MediaTracker tracker = new MediaTracker(new Button(""));
171         tracker.addImage(image, 0);
172         try {
173             tracker.waitForAll();
174         } catch (InterruptedException ignore) { }
175 
176         if (image.getWidth(null) <= 0 ||
177             image.getHeight(null) <= 0)
178         {
179             return;
180         }
181 
182         CImage cimage = CImage.getCreator().createFromImage(image);
183         boolean imageAutoSize = target.isImageAutoSize();
184         cimage.execute(imagePtr -> {
185             execute(ptr -> {
186                 setNativeImage(ptr, imagePtr, imageAutoSize);
187             });
188         });
189     }
190 
setNativeImage(final long model, final long nsimage, final boolean autosize)191     private native void setNativeImage(final long model, final long nsimage, final boolean autosize);
192 
postEvent(final AWTEvent event)193     private void postEvent(final AWTEvent event) {
194         SunToolkit.executeOnEventHandlerThread(target, new Runnable() {
195             public void run() {
196                 SunToolkit.postEvent(SunToolkit.targetToAppContext(target), event);
197             }
198         });
199     }
200 
201     //invocation from the AWTTrayIcon.m
handleMouseEvent(NSEvent nsEvent)202     private void handleMouseEvent(NSEvent nsEvent) {
203         int buttonNumber = nsEvent.getButtonNumber();
204         final SunToolkit tk = (SunToolkit)Toolkit.getDefaultToolkit();
205         if ((buttonNumber > 2 && !tk.areExtraMouseButtonsEnabled())
206                 || buttonNumber > tk.getNumberOfButtons() - 1) {
207             return;
208         }
209 
210         int jeventType = NSEvent.nsToJavaEventType(nsEvent.getType());
211 
212         int jbuttonNumber = MouseEvent.NOBUTTON;
213         int jclickCount = 0;
214         if (jeventType != MouseEvent.MOUSE_MOVED) {
215             jbuttonNumber = NSEvent.nsToJavaButton(buttonNumber);
216             jclickCount = nsEvent.getClickCount();
217         }
218 
219         int jmodifiers = NSEvent.nsToJavaMouseModifiers(buttonNumber,
220                 nsEvent.getModifierFlags());
221         boolean isPopupTrigger = NSEvent.isPopupTrigger(jmodifiers);
222 
223         int eventButtonMask = (jbuttonNumber > 0)?
224                 MouseEvent.getMaskForButton(jbuttonNumber) : 0;
225         long when = System.currentTimeMillis();
226 
227         if (jeventType == MouseEvent.MOUSE_PRESSED) {
228             mouseClickButtons |= eventButtonMask;
229         } else if (jeventType == MouseEvent.MOUSE_DRAGGED) {
230             mouseClickButtons = 0;
231         }
232 
233         // The MouseEvent's coordinates are relative to screen
234         int absX = nsEvent.getAbsX();
235         int absY = nsEvent.getAbsY();
236 
237         MouseEvent mouseEvent = new MouseEvent(dummyFrame, jeventType, when,
238                 jmodifiers, absX, absY, absX, absY, jclickCount, isPopupTrigger,
239                 jbuttonNumber);
240         mouseEvent.setSource(target);
241         postEvent(mouseEvent);
242 
243         // fire ACTION event
244         if (jeventType == MouseEvent.MOUSE_PRESSED && isPopupTrigger) {
245             final String cmd = target.getActionCommand();
246             final ActionEvent event = new ActionEvent(target,
247                     ActionEvent.ACTION_PERFORMED, cmd);
248             postEvent(event);
249         }
250 
251         // synthesize CLICKED event
252         if (jeventType == MouseEvent.MOUSE_RELEASED) {
253             if ((mouseClickButtons & eventButtonMask) != 0) {
254                 MouseEvent clickEvent = new MouseEvent(dummyFrame,
255                         MouseEvent.MOUSE_CLICKED, when, jmodifiers, absX, absY,
256                         absX, absY, jclickCount, isPopupTrigger, jbuttonNumber);
257                 clickEvent.setSource(target);
258                 postEvent(clickEvent);
259             }
260 
261             mouseClickButtons &= ~eventButtonMask;
262         }
263     }
264 
nativeGetIconLocation(long trayIconModel)265     private native Point2D nativeGetIconLocation(long trayIconModel);
266 
displayMessageOnEDT(String caption, String text, String messageType)267     public void displayMessageOnEDT(String caption, String text,
268                                     String messageType) {
269         if (messageDialog != null) {
270             disposeMessageDialog();
271         }
272 
273         // obtain icon to show along the message
274         Icon icon = getIconForMessageType(messageType);
275         if (icon != null) {
276             icon = new ImageIcon(scaleIcon(icon, 0.75));
277         }
278 
279         // We want the message dialog text area to be about 1/8 of the screen
280         // size. There is nothing special about this value, it's just makes the
281         // message dialog to look nice
282         Dimension screenSize = java.awt.Toolkit.getDefaultToolkit().getScreenSize();
283         int textWidth = screenSize.width / 8;
284 
285         // create dialog to show
286         messageDialog = createMessageDialog(caption, text, textWidth, icon);
287 
288         // finally, show the dialog to user
289         showMessageDialog();
290     }
291 
292     /**
293      * Creates dialog window used to display the message
294      */
createMessageDialog(String caption, String text, int textWidth, Icon icon)295     private JDialog createMessageDialog(String caption, String text,
296                                      int textWidth, Icon icon) {
297         JDialog dialog;
298         handler = new DialogEventHandler();
299 
300         JTextArea captionArea = null;
301         if (caption != null) {
302             captionArea = createTextArea(caption, textWidth, false, true);
303         }
304 
305         JTextArea textArea = null;
306         if (text != null){
307             textArea = createTextArea(text, textWidth, true, false);
308         }
309 
310         Object[] panels = null;
311         if (captionArea != null) {
312             if (textArea != null) {
313                 panels = new Object[] {captionArea, new JLabel(), textArea};
314             } else {
315                 panels = new Object[] {captionArea};
316             }
317         } else {
318            if (textArea != null) {
319                 panels = new Object[] {textArea};
320             }
321         }
322 
323         // We want message dialog with small title bar. There is a client
324         // property property that does it, however, it must be set before
325         // dialog's native window is created. This is why we create option
326         // pane and dialog separately
327         final JOptionPane op = new JOptionPane(panels);
328         op.setIcon(icon);
329         op.addPropertyChangeListener(handler);
330 
331         // Make Ok button small. Most likely won't work for L&F other then Aqua
332         try {
333             JPanel buttonPanel = (JPanel)op.getComponent(1);
334             JButton ok = (JButton)buttonPanel.getComponent(0);
335             ok.putClientProperty("JComponent.sizeVariant", "small");
336         } catch (Throwable t) {
337             // do nothing, we tried and failed, no big deal
338         }
339 
340         dialog = new JDialog((Dialog) null);
341         JRootPane rp = dialog.getRootPane();
342 
343         // gives us dialog window with small title bar and not zoomable
344         rp.putClientProperty(CPlatformWindow.WINDOW_STYLE, "small");
345         rp.putClientProperty(CPlatformWindow.WINDOW_ZOOMABLE, "false");
346 
347         dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
348         dialog.setModal(false);
349         dialog.setModalExclusionType(Dialog.ModalExclusionType.TOOLKIT_EXCLUDE);
350         dialog.setAlwaysOnTop(true);
351         dialog.setAutoRequestFocus(false);
352         dialog.setResizable(false);
353         dialog.setContentPane(op);
354 
355         dialog.addWindowListener(handler);
356 
357         // suppress security warning for untrusted windows
358         AWTAccessor.getWindowAccessor().setTrayIconWindow(dialog, true);
359 
360         dialog.pack();
361 
362         return dialog;
363     }
364 
showMessageDialog()365     private void showMessageDialog() {
366 
367         Dimension screenSize = java.awt.Toolkit.getDefaultToolkit().getScreenSize();
368         AtomicReference<Point2D> ref = new AtomicReference<>();
369         execute(ptr -> {
370             ref.set(nativeGetIconLocation(ptr));
371         });
372         Point2D iconLoc = ref.get();
373         if (iconLoc == null) {
374             return;
375         }
376 
377         int dialogY = (int)iconLoc.getY();
378         int dialogX = (int)iconLoc.getX();
379         if (dialogX + messageDialog.getWidth() > screenSize.width) {
380             dialogX = screenSize.width - messageDialog.getWidth();
381         }
382 
383         messageDialog.setLocation(dialogX, dialogY);
384         messageDialog.setVisible(true);
385     }
386 
disposeMessageDialog()387    private void disposeMessageDialog() {
388         if (SwingUtilities.isEventDispatchThread()) {
389             disposeMessageDialogOnEDT();
390         } else {
391             try {
392                 SwingUtilities.invokeAndWait(new Runnable() {
393                     public void run() {
394                         disposeMessageDialogOnEDT();
395                     }
396                 });
397             } catch (Exception e) {
398                 throw new AssertionError(e);
399             }
400         }
401    }
402 
disposeMessageDialogOnEDT()403     private void disposeMessageDialogOnEDT() {
404         if (messageDialog != null) {
405             messageDialog.removeWindowListener(handler);
406             messageDialog.removePropertyChangeListener(handler);
407             messageDialog.dispose();
408 
409             messageDialog = null;
410             handler = null;
411         }
412     }
413 
414     /**
415      * Scales an icon using specified scale factor
416      *
417      * @param icon        icon to scale
418      * @param scaleFactor scale factor to use
419      * @return scaled icon as BuffedredImage
420      */
scaleIcon(Icon icon, double scaleFactor)421     private static BufferedImage scaleIcon(Icon icon, double scaleFactor) {
422         if (icon == null) {
423             return null;
424         }
425 
426         int w = icon.getIconWidth();
427         int h = icon.getIconHeight();
428 
429         GraphicsEnvironment ge =
430                 GraphicsEnvironment.getLocalGraphicsEnvironment();
431         GraphicsDevice gd = ge.getDefaultScreenDevice();
432         GraphicsConfiguration gc = gd.getDefaultConfiguration();
433 
434         // convert icon into image
435         BufferedImage iconImage = gc.createCompatibleImage(w, h,
436                 Transparency.TRANSLUCENT);
437         Graphics2D g = iconImage.createGraphics();
438         icon.paintIcon(null, g, 0, 0);
439         g.dispose();
440 
441         // and scale it nicely
442         int scaledW = (int) (w * scaleFactor);
443         int scaledH = (int) (h * scaleFactor);
444         BufferedImage scaledImage = gc.createCompatibleImage(scaledW, scaledH,
445                 Transparency.TRANSLUCENT);
446         g = scaledImage.createGraphics();
447         g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
448                 RenderingHints.VALUE_INTERPOLATION_BILINEAR);
449         g.drawImage(iconImage, 0, 0, scaledW, scaledH, null);
450         g.dispose();
451 
452         return scaledImage;
453     }
454 
455 
456     /**
457      * Gets Aqua icon used in message dialog.
458      */
getIconForMessageType(String messageType)459     private static Icon getIconForMessageType(String messageType) {
460         if (messageType.equals("ERROR")) {
461             return UIManager.getIcon("OptionPane.errorIcon");
462         } else if (messageType.equals("WARNING")) {
463             return UIManager.getIcon("OptionPane.warningIcon");
464         } else {
465             // this is just an application icon
466             return UIManager.getIcon("OptionPane.informationIcon");
467         }
468     }
469 
createTextArea(String text, int width, boolean isSmall, boolean isBold)470     private static JTextArea createTextArea(String text, int width,
471                                             boolean isSmall, boolean isBold) {
472         JTextArea textArea = new JTextArea(text);
473 
474         textArea.setLineWrap(true);
475         textArea.setWrapStyleWord(true);
476         textArea.setEditable(false);
477         textArea.setFocusable(false);
478         textArea.setBorder(null);
479         textArea.setBackground(new JLabel().getBackground());
480 
481         if (isSmall) {
482             textArea.putClientProperty("JComponent.sizeVariant", "small");
483         }
484 
485         if (isBold) {
486             Font font = textArea.getFont();
487             Font boldFont = new Font(font.getName(), Font.BOLD, font.getSize());
488             textArea.setFont(boldFont);
489         }
490 
491         textArea.setSize(width, 1);
492 
493         return textArea;
494     }
495 
496     /**
497      * Implements all the Listeners needed by message dialog
498      */
499     private final class DialogEventHandler extends WindowAdapter
500             implements PropertyChangeListener {
501 
windowClosing(WindowEvent we)502         public void windowClosing(WindowEvent we) {
503                 disposeMessageDialog();
504         }
505 
propertyChange(PropertyChangeEvent e)506         public void propertyChange(PropertyChangeEvent e) {
507             if (messageDialog == null) {
508                 return;
509             }
510 
511             String prop = e.getPropertyName();
512             Container cp = messageDialog.getContentPane();
513 
514             if (messageDialog.isVisible() && e.getSource() == cp &&
515                     (prop.equals(JOptionPane.VALUE_PROPERTY))) {
516                 disposeMessageDialog();
517             }
518         }
519     }
520 }
521 
522