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 
  30 import javax.swing.*;
  31 import java.awt.*;
  32 import java.awt.event.*;
  33 import java.awt.geom.Point2D;
  34 import java.awt.image.BufferedImage;
  35 import java.awt.peer.TrayIconPeer;
  36 import java.beans.PropertyChangeEvent;
  37 import java.beans.PropertyChangeListener;
  38 
  39 public class CTrayIcon extends CFRetainedResource implements TrayIconPeer {
  40     private TrayIcon target;
  41     private PopupMenu popup;
  42     private JDialog messageDialog;
  43     private DialogEventHandler handler;
  44 
  45     CTrayIcon(TrayIcon target) {
  46         super(0, true);
  47 
  48         this.messageDialog = null;
  49         this.handler = null;
  50         this.target = target;
  51         this.popup = target.getPopupMenu();
  52         setPtr(createModel());
  53 
  54         //if no one else is creating the peer.
  55         checkAndCreatePopupPeer();
  56         updateImage();
  57     }
  58 
  59     private CPopupMenu checkAndCreatePopupPeer() {
  60         CPopupMenu menuPeer = null;
  61         if (popup != null) {
  62             try {
  63                 menuPeer = (CPopupMenu)popup.getPeer();
  64                 if (menuPeer == null) {
  65                     popup.addNotify();
  66                 }
  67             }catch (Exception e){
  68                 e.printStackTrace();
  69             }
  70         }
  71         return menuPeer;
  72     }
  73 
  74     private long createModel() {
  75         return nativeCreate();
  76     }
  77 
  78     private long getModel() {
  79         return ptr;
  80     }
  81 
  82     private native long nativeCreate();
  83 
  84     //invocation from the AWTTrayIcon.m
  85     public long getPopupMenuModel(){
  86         if(popup == null) {
  87             return 0L;
  88         }
  89         return checkAndCreatePopupPeer().getModel();
  90     }
  91 
  92     /**
  93      * We display tray icon message as a small dialog with OK button.
  94      * This is lame, but JDK 1.6 does basically the same. There is a new
  95      * kind of window in Lion, NSPopover, so perhaps it could be used it
  96      * to implement better looking notifications.
  97      */
  98     public void displayMessage(final String caption, final String text,
  99                                final String messageType) {
 100 
 101         if (SwingUtilities.isEventDispatchThread()) {
 102             displayMessageOnEDT(caption, text, messageType);
 103         } else {
 104             try {
 105                 SwingUtilities.invokeAndWait(new Runnable() {
 106                     public void run() {
 107                         displayMessageOnEDT(caption, text, messageType);
 108                     }
 109                 });
 110             } catch (Exception e) {
 111                 throw new AssertionError(e);
 112             }
 113         }
 114     }
 115 
 116     @Override
 117     public void dispose() {
 118         if (messageDialog != null) {
 119             disposeMessageDialog();
 120         }
 121 
 122         LWCToolkit.targetDisposedPeer(target, this);
 123         target = null;
 124 
 125         super.dispose();
 126     }
 127 
 128     @Override
 129     public void setToolTip(String tooltip) {
 130         nativeSetToolTip(getModel(), tooltip);
 131     }
 132 
 133     //adds tooltip to the NSStatusBar's NSButton.
 134     private native void nativeSetToolTip(long trayIconModel, String tooltip);
 135 
 136     @Override
 137     public void showPopupMenu(int x, int y) {
 138         //Not used. The popupmenu is shown from the native code.
 139     }
 140 
 141     @Override
 142     public void updateImage() {
 143         Image image = target.getImage();
 144         if (image == null) return;
 145 
 146         MediaTracker tracker = new MediaTracker(new Button(""));
 147         tracker.addImage(image, 0);
 148         try {
 149             tracker.waitForAll();
 150         } catch (InterruptedException ignore) { }
 151 
 152         if (image.getWidth(null) <= 0 ||
 153             image.getHeight(null) <= 0)
 154         {
 155             return;
 156         }
 157 
 158         CImage cimage = CImage.getCreator().createFromImage(image);
 159         setNativeImage(getModel(), cimage.ptr, target.isImageAutoSize());
 160     }
 161 
 162     private native void setNativeImage(final long model, final long nsimage, final boolean autosize);
 163 
 164     //invocation from the AWTTrayIcon.m
 165     public void performAction() {
 166         SunToolkit.executeOnEventHandlerThread(target, new Runnable() {
 167             public void run() {
 168                 final String cmd = target.getActionCommand();
 169                 final ActionEvent event = new ActionEvent(target, ActionEvent.ACTION_PERFORMED, cmd);
 170                 SunToolkit.postEvent(SunToolkit.targetToAppContext(target), event);
 171             }
 172         });
 173     }
 174 
 175     private native Point2D nativeGetIconLocation(long trayIconModel);
 176 
 177     public void displayMessageOnEDT(String caption, String text,
 178                                     String messageType) {
 179         if (messageDialog != null) {
 180             disposeMessageDialog();
 181         }
 182 
 183         // obtain icon to show along the message
 184         Icon icon = getIconForMessageType(messageType);
 185         if (icon != null) {
 186             icon = new ImageIcon(scaleIcon(icon, 0.75));
 187         }
 188 
 189         // We want the message dialog text area to be about 1/8 of the screen
 190         // size. There is nothing special about this value, it's just makes the
 191         // message dialog to look nice
 192         Dimension screenSize = java.awt.Toolkit.getDefaultToolkit().getScreenSize();
 193         int textWidth = screenSize.width / 8;
 194 
 195         // create dialog to show
 196         messageDialog = createMessageDialog(caption, text, textWidth, icon);
 197 
 198         // finally, show the dialog to user
 199         showMessageDialog();
 200     }
 201 
 202     /**
 203      * Creates dialog window used to display the message
 204      */
 205     private JDialog createMessageDialog(String caption, String text,
 206                                      int textWidth, Icon icon) {
 207         JDialog dialog;
 208         handler = new DialogEventHandler();
 209 
 210         JTextArea captionArea = null;
 211         if (caption != null) {
 212             captionArea = createTextArea(caption, textWidth, false, true);
 213         }
 214 
 215         JTextArea textArea = null;
 216         if (text != null){
 217             textArea = createTextArea(text, textWidth, true, false);
 218         }
 219 
 220         Object[] panels = null;
 221         if (captionArea != null) {
 222             if (textArea != null) {
 223                 panels = new Object[] {captionArea, new JLabel(), textArea};
 224             } else {
 225                 panels = new Object[] {captionArea};
 226             }
 227         } else {
 228            if (textArea != null) {
 229                 panels = new Object[] {textArea};
 230             }
 231         }
 232 
 233         // We want message dialog with small title bar. There is a client
 234         // property property that does it, however, it must be set before
 235         // dialog's native window is created. This is why we create option
 236         // pane and dialog separately
 237         final JOptionPane op = new JOptionPane(panels);
 238         op.setIcon(icon);
 239         op.addPropertyChangeListener(handler);
 240 
 241         // Make Ok button small. Most likely won't work for L&F other then Aqua
 242         try {
 243             JPanel buttonPanel = (JPanel)op.getComponent(1);
 244             JButton ok = (JButton)buttonPanel.getComponent(0);
 245             ok.putClientProperty("JComponent.sizeVariant", "small");
 246         } catch (Throwable t) {
 247             // do nothing, we tried and failed, no big deal
 248         }
 249 
 250         dialog = new JDialog((Dialog) null);
 251         JRootPane rp = dialog.getRootPane();
 252 
 253         // gives us dialog window with small title bar and not zoomable
 254         rp.putClientProperty(CPlatformWindow.WINDOW_STYLE, "small");
 255         rp.putClientProperty(CPlatformWindow.WINDOW_ZOOMABLE, "false");
 256 
 257         dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
 258         dialog.setModal(false);
 259         dialog.setResizable(false);
 260         dialog.setContentPane(op);
 261 
 262         dialog.addWindowListener(handler);
 263 
 264         dialog.pack();
 265 
 266         return dialog;
 267     }
 268 
 269     private void showMessageDialog() {
 270 
 271         Dimension screenSize = java.awt.Toolkit.getDefaultToolkit().getScreenSize();
 272         Point2D iconLoc = nativeGetIconLocation(getModel());
 273 
 274         int dialogY = (int)iconLoc.getY();
 275         int dialogX = (int)iconLoc.getX();
 276         if (dialogX + messageDialog.getWidth() > screenSize.width) {
 277             dialogX = screenSize.width - messageDialog.getWidth();
 278         }
 279 
 280         messageDialog.setLocation(dialogX, dialogY);
 281         messageDialog.setVisible(true);
 282     }
 283 
 284    private void disposeMessageDialog() {
 285         if (SwingUtilities.isEventDispatchThread()) {
 286             disposeMessageDialogOnEDT();
 287         } else {
 288             try {
 289                 SwingUtilities.invokeAndWait(new Runnable() {
 290                     public void run() {
 291                         disposeMessageDialogOnEDT();
 292                     }
 293                 });
 294             } catch (Exception e) {
 295                 throw new AssertionError(e);
 296             }
 297         }
 298    }
 299 
 300     private void disposeMessageDialogOnEDT() {
 301         if (messageDialog != null) {
 302             messageDialog.removeWindowListener(handler);
 303             messageDialog.removePropertyChangeListener(handler);
 304             messageDialog.dispose();
 305 
 306             messageDialog = null;
 307             handler = null;
 308         }
 309     }
 310 
 311     /**
 312      * Scales an icon using specified scale factor
 313      *
 314      * @param icon        icon to scale
 315      * @param scaleFactor scale factor to use
 316      * @return scaled icon as BuffedredImage
 317      */
 318     private static BufferedImage scaleIcon(Icon icon, double scaleFactor) {
 319         if (icon == null) {
 320             return null;
 321         }
 322 
 323         int w = icon.getIconWidth();
 324         int h = icon.getIconHeight();
 325 
 326         GraphicsEnvironment ge =
 327                 GraphicsEnvironment.getLocalGraphicsEnvironment();
 328         GraphicsDevice gd = ge.getDefaultScreenDevice();
 329         GraphicsConfiguration gc = gd.getDefaultConfiguration();
 330 
 331         // convert icon into image
 332         BufferedImage iconImage = gc.createCompatibleImage(w, h,
 333                 Transparency.TRANSLUCENT);
 334         Graphics2D g = iconImage.createGraphics();
 335         icon.paintIcon(null, g, 0, 0);
 336         g.dispose();
 337 
 338         // and scale it nicely
 339         int scaledW = (int) (w * scaleFactor);
 340         int scaledH = (int) (h * scaleFactor);
 341         BufferedImage scaledImage = gc.createCompatibleImage(scaledW, scaledH,
 342                 Transparency.TRANSLUCENT);
 343         g = scaledImage.createGraphics();
 344         g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
 345                 RenderingHints.VALUE_INTERPOLATION_BILINEAR);
 346         g.drawImage(iconImage, 0, 0, scaledW, scaledH, null);
 347         g.dispose();
 348 
 349         return scaledImage;
 350     }
 351 
 352 
 353     /**
 354      * Gets Aqua icon used in message dialog.
 355      */
 356     private static Icon getIconForMessageType(String messageType) {
 357         if (messageType.equals("ERROR")) {
 358             return UIManager.getIcon("OptionPane.errorIcon");
 359         } else if (messageType.equals("WARNING")) {
 360             return UIManager.getIcon("OptionPane.warningIcon");
 361         } else {
 362             // this is just an application icon
 363             return UIManager.getIcon("OptionPane.informationIcon");
 364         }
 365     }
 366 
 367     private static JTextArea createTextArea(String text, int width,
 368                                             boolean isSmall, boolean isBold) {
 369         JTextArea textArea = new JTextArea(text);
 370 
 371         textArea.setLineWrap(true);
 372         textArea.setWrapStyleWord(true);
 373         textArea.setEditable(false);
 374         textArea.setFocusable(false);
 375         textArea.setBorder(null);
 376         textArea.setBackground(new JLabel().getBackground());
 377 
 378         if (isSmall) {
 379             textArea.putClientProperty("JComponent.sizeVariant", "small");
 380         }
 381 
 382         if (isBold) {
 383             Font font = textArea.getFont();
 384             Font boldFont = new Font(font.getName(), Font.BOLD, font.getSize());
 385             textArea.setFont(boldFont);
 386         }
 387 
 388         textArea.setSize(width, 1);
 389 
 390         return textArea;
 391     }
 392 
 393     /**
 394      * Implements all the Listeners needed by message dialog
 395      */
 396     private final class DialogEventHandler extends WindowAdapter
 397             implements PropertyChangeListener {
 398 
 399         public void windowClosing(WindowEvent we) {
 400                 disposeMessageDialog();
 401         }
 402 
 403         public void propertyChange(PropertyChangeEvent e) {
 404             if (messageDialog == null) {
 405                 return;
 406             }
 407 
 408             String prop = e.getPropertyName();
 409             Container cp = messageDialog.getContentPane();
 410 
 411             if (messageDialog.isVisible() && e.getSource() == cp &&
 412                     (prop.equals(JOptionPane.VALUE_PROPERTY))) {
 413                 disposeMessageDialog();
 414             }
 415         }
 416     }
 417 }
 418