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