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 cimage.execute(imagePtr -> { 212 execute(ptr -> { 213 setNativeImage(ptr, imagePtr, imageAutoSize); 214 }); 215 }); 216 } 217 218 private native void setNativeImage(final long model, final long nsimage, final boolean autosize); 219 220 private void postEvent(final AWTEvent event) { 221 SunToolkit.executeOnEventHandlerThread(target, new Runnable() { 222 public void run() { 223 SunToolkit.postEvent(SunToolkit.targetToAppContext(target), event); 224 } 225 }); 226 } 227 228 //invocation from the AWTTrayIcon.m 229 private void handleMouseEvent(NSEvent nsEvent) { 230 int buttonNumber = nsEvent.getButtonNumber(); 231 final SunToolkit tk = (SunToolkit)Toolkit.getDefaultToolkit(); 232 if ((buttonNumber > 2 && !tk.areExtraMouseButtonsEnabled()) 233 || buttonNumber > tk.getNumberOfButtons() - 1) { 234 return; 235 } 236 237 int jeventType = NSEvent.nsToJavaEventType(nsEvent.getType()); 238 239 int jbuttonNumber = MouseEvent.NOBUTTON; 240 int jclickCount = 0; 241 if (jeventType != MouseEvent.MOUSE_MOVED) { 242 jbuttonNumber = NSEvent.nsToJavaButton(buttonNumber); 243 jclickCount = nsEvent.getClickCount(); 244 } 245 246 int jmodifiers = NSEvent.nsToJavaModifiers( 247 nsEvent.getModifierFlags()); 248 boolean isPopupTrigger = NSEvent.isPopupTrigger(jmodifiers); 249 250 int eventButtonMask = (jbuttonNumber > 0)? 251 MouseEvent.getMaskForButton(jbuttonNumber) : 0; 252 long when = System.currentTimeMillis(); 253 254 if (jeventType == MouseEvent.MOUSE_PRESSED) { 255 mouseClickButtons |= eventButtonMask; 256 } else if (jeventType == MouseEvent.MOUSE_DRAGGED) { 257 mouseClickButtons = 0; 258 } 259 260 // The MouseEvent's coordinates are relative to screen 261 int absX = nsEvent.getAbsX(); 262 int absY = nsEvent.getAbsY(); 263 264 MouseEvent mouseEvent = new MouseEvent(dummyFrame, jeventType, when, 265 jmodifiers, absX, absY, absX, absY, jclickCount, isPopupTrigger, 266 jbuttonNumber); 267 mouseEvent.setSource(target); 268 postEvent(mouseEvent); 269 270 // fire ACTION event 271 if (jeventType == MouseEvent.MOUSE_PRESSED && isPopupTrigger) { 272 final String cmd = target.getActionCommand(); 273 final ActionEvent event = new ActionEvent(target, 274 ActionEvent.ACTION_PERFORMED, cmd); 275 postEvent(event); 276 } 277 278 // synthesize CLICKED event 279 if (jeventType == MouseEvent.MOUSE_RELEASED) { 280 if ((mouseClickButtons & eventButtonMask) != 0) { 281 MouseEvent clickEvent = new MouseEvent(dummyFrame, 282 MouseEvent.MOUSE_CLICKED, when, jmodifiers, absX, absY, 283 absX, absY, jclickCount, isPopupTrigger, jbuttonNumber); 284 clickEvent.setSource(target); 285 postEvent(clickEvent); 286 } 287 288 mouseClickButtons &= ~eventButtonMask; 289 } 290 } 291 292 private native void nativeShowNotification(long trayIconModel, 293 String caption, String text, 294 long nsimage); 295 296 /** 297 * Used by the automated tests. 298 */ 299 private native Point2D nativeGetIconLocation(long trayIconModel); 300 301 /** 302 * Scales an icon using specified scale factor 303 * 304 * @param icon icon to scale 305 * @param scaleFactor scale factor to use 306 * @return scaled icon as BuffedredImage 307 */ 308 private static BufferedImage scaleIcon(Icon icon, double scaleFactor) { 309 if (icon == null) { 310 return null; 311 } 312 313 int w = icon.getIconWidth(); 314 int h = icon.getIconHeight(); 315 316 GraphicsEnvironment ge = 317 GraphicsEnvironment.getLocalGraphicsEnvironment(); 318 GraphicsDevice gd = ge.getDefaultScreenDevice(); 319 GraphicsConfiguration gc = gd.getDefaultConfiguration(); 320 321 // convert icon into image 322 BufferedImage iconImage = gc.createCompatibleImage(w, h, 323 Transparency.TRANSLUCENT); 324 Graphics2D g = iconImage.createGraphics(); 325 icon.paintIcon(null, g, 0, 0); 326 g.dispose(); 327 328 // and scale it nicely 329 int scaledW = (int) (w * scaleFactor); 330 int scaledH = (int) (h * scaleFactor); 331 BufferedImage scaledImage = gc.createCompatibleImage(scaledW, scaledH, 332 Transparency.TRANSLUCENT); 333 g = scaledImage.createGraphics(); 334 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, 335 RenderingHints.VALUE_INTERPOLATION_BILINEAR); 336 g.drawImage(iconImage, 0, 0, scaledW, scaledH, null); 337 g.dispose(); 338 339 return scaledImage; 340 } 341 342 343 /** 344 * Gets Aqua icon used in message dialog. 345 */ 346 private static Icon getIconForMessageType(String messageType) { 347 if (messageType.equals("ERROR")) { 348 return UIManager.getIcon("OptionPane.errorIcon"); 349 } else if (messageType.equals("WARNING")) { 350 return UIManager.getIcon("OptionPane.warningIcon"); 351 } else { 352 // this is just an application icon 353 return UIManager.getIcon("OptionPane.informationIcon"); 354 } 355 } 356 357 class IconObserver implements ImageObserver { 358 @Override 359 public boolean imageUpdate(Image image, int flags, int x, int y, int width, int height) { 360 if (image != target.getImage()) // if the image has been changed 361 { 362 return false; 363 } 364 if ((flags & (ImageObserver.FRAMEBITS | ImageObserver.ALLBITS | 365 ImageObserver.WIDTH | ImageObserver.HEIGHT)) != 0) 366 { 367 SunToolkit.executeOnEventHandlerThread(target, new Runnable() { 368 public void run() { 369 updateNativeImage(image); 370 } 371 }); 372 } 373 return (flags & ImageObserver.ALLBITS) == 0; 374 } 375 } 376 } 377