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