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