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