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