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