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 
  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 
  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 
 102     private long createModel() {
 103         return nativeCreate();
 104     }
 105 
 106     private native long nativeCreate();
 107 
 108     //invocation from the AWTTrayIcon.m
 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      */
 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
 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
 175     public void setToolTip(String tooltip) {
 176         execute(ptr -> nativeSetToolTip(ptr, tooltip));
 177     }
 178 
 179     //adds tooltip to the NSStatusBar's NSButton.
 180     private native void nativeSetToolTip(long trayIconModel, String tooltip);
 181 
 182     @Override
 183     public void showPopupMenu(int x, int y) {
 184         //Not used. The popupmenu is shown from the native code.
 185     }
 186 
 187     @Override
 188     public void updateImage() {
 189 
 190         Image image = target.getImage();
 191         if (image != null) {
 192             updateNativeImage(image);
 193         }
 194     }
 195 
 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 
 220     private native void setNativeImage(final long model, final long nsimage, final boolean autosize);
 221 
 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
 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 
 294     private native void nativeShowNotification(long trayIconModel,
 295                                                String caption, String text,
 296                                                long nsimage);
 297 
 298     /**
 299      * Used by the automated tests.
 300      */
 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      */
 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      */
 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
 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