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