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