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