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