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