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