1 /*
   2  * Copyright (c) 2005, 2008, 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 java.awt;
  27 
  28 import java.util.Vector;
  29 import java.awt.peer.SystemTrayPeer;
  30 import java.beans.PropertyChangeListener;
  31 import java.beans.PropertyChangeSupport;
  32 import sun.awt.AppContext;
  33 import sun.awt.SunToolkit;
  34 import sun.awt.HeadlessToolkit;
  35 import sun.awt.AWTAccessor;
  36 import sun.awt.AWTPermissions;
  37 
  38 /**
  39  * The <code>SystemTray</code> class represents the system tray for a
  40  * desktop.  On Microsoft Windows it is referred to as the "Taskbar
  41  * Status Area", on Gnome it is referred to as the "Notification
  42  * Area", on KDE it is referred to as the "System Tray".  The system
  43  * tray is shared by all applications running on the desktop.
  44  *
  45  * <p> On some platforms the system tray may not be present or may not
  46  * be supported, in this case {@link SystemTray#getSystemTray()}
  47  * throws {@link UnsupportedOperationException}.  To detect whether the
  48  * system tray is supported, use {@link SystemTray#isSupported}.
  49  *
  50  * <p>The <code>SystemTray</code> may contain one or more {@link
  51  * TrayIcon TrayIcons}, which are added to the tray using the {@link
  52  * #add} method, and removed when no longer needed, using the
  53  * {@link #remove}.  <code>TrayIcon</code> consists of an
  54  * image, a popup menu and a set of associated listeners.  Please see
  55  * the {@link TrayIcon} class for details.
  56  *
  57  * <p>Every Java application has a single <code>SystemTray</code>
  58  * instance that allows the app to interface with the system tray of
  59  * the desktop while the app is running.  The <code>SystemTray</code>
  60  * instance can be obtained from the {@link #getSystemTray} method.
  61  * An application may not create its own instance of
  62  * <code>SystemTray</code>.
  63  *
  64  * <p>The following code snippet demonstrates how to access
  65  * and customize the system tray:
  66  * <pre>
  67  * <code>
  68  *     {@link TrayIcon} trayIcon = null;
  69  *     if (SystemTray.isSupported()) {
  70  *         // get the SystemTray instance
  71  *         SystemTray tray = SystemTray.{@link #getSystemTray};
  72  *         // load an image
  73  *         {@link java.awt.Image} image = {@link java.awt.Toolkit#getImage(String) Toolkit.getDefaultToolkit().getImage}(...);
  74  *         // create a action listener to listen for default action executed on the tray icon
  75  *         {@link java.awt.event.ActionListener} listener = new {@link java.awt.event.ActionListener ActionListener}() {
  76  *             public void {@link java.awt.event.ActionListener#actionPerformed actionPerformed}({@link java.awt.event.ActionEvent} e) {
  77  *                 // execute default action of the application
  78  *                 // ...
  79  *             }
  80  *         };
  81  *         // create a popup menu
  82  *         {@link java.awt.PopupMenu} popup = new {@link java.awt.PopupMenu#PopupMenu PopupMenu}();
  83  *         // create menu item for the default action
  84  *         MenuItem defaultItem = new MenuItem(...);
  85  *         defaultItem.addActionListener(listener);
  86  *         popup.add(defaultItem);
  87  *         /// ... add other items
  88  *         // construct a TrayIcon
  89  *         trayIcon = new {@link TrayIcon#TrayIcon(java.awt.Image, String, java.awt.PopupMenu) TrayIcon}(image, "Tray Demo", popup);
  90  *         // set the TrayIcon properties
  91  *         trayIcon.{@link TrayIcon#addActionListener(java.awt.event.ActionListener) addActionListener}(listener);
  92  *         // ...
  93  *         // add the tray image
  94  *         try {
  95  *             tray.{@link SystemTray#add(TrayIcon) add}(trayIcon);
  96  *         } catch (AWTException e) {
  97  *             System.err.println(e);
  98  *         }
  99  *         // ...
 100  *     } else {
 101  *         // disable tray option in your application or
 102  *         // perform other actions
 103  *         ...
 104  *     }
 105  *     // ...
 106  *     // some time later
 107  *     // the application state has changed - update the image
 108  *     if (trayIcon != null) {
 109  *         trayIcon.{@link TrayIcon#setImage(java.awt.Image) setImage}(updatedImage);
 110  *     }
 111  *     // ...
 112  * </code>
 113  * </pre>
 114  *
 115  * @since 1.6
 116  * @see TrayIcon
 117  *
 118  * @author Bino George
 119  * @author Denis Mikhalkin
 120  * @author Sharon Zakhour
 121  * @author Anton Tarasov
 122  */
 123 public class SystemTray {
 124     private static SystemTray systemTray;
 125     private int currentIconID = 0; // each TrayIcon added gets a unique ID
 126 
 127     transient private SystemTrayPeer peer;
 128 
 129     private static final TrayIcon[] EMPTY_TRAY_ARRAY = new TrayIcon[0];
 130 
 131     static {
 132         AWTAccessor.setSystemTrayAccessor(
 133             new AWTAccessor.SystemTrayAccessor() {
 134                 public void firePropertyChange(SystemTray tray,
 135                                                String propertyName,
 136                                                Object oldValue,
 137                                                Object newValue) {
 138                     tray.firePropertyChange(propertyName, oldValue, newValue);
 139                 }
 140             });
 141     }
 142 
 143     /**
 144      * Private <code>SystemTray</code> constructor.
 145      *
 146      */
 147     private SystemTray() {
 148         addNotify();
 149     }
 150 
 151     /**
 152      * Gets the <code>SystemTray</code> instance that represents the
 153      * desktop's tray area.  This always returns the same instance per
 154      * application.  On some platforms the system tray may not be
 155      * supported.  You may use the {@link #isSupported} method to
 156      * check if the system tray is supported.
 157      *
 158      * <p>If a SecurityManager is installed, the AWTPermission
 159      * {@code accessSystemTray} must be granted in order to get the
 160      * {@code SystemTray} instance. Otherwise this method will throw a
 161      * SecurityException.
 162      *
 163      * @return the <code>SystemTray</code> instance that represents
 164      * the desktop's tray area
 165      * @throws UnsupportedOperationException if the system tray isn't
 166      * supported by the current platform
 167      * @throws HeadlessException if
 168      * <code>GraphicsEnvironment.isHeadless()</code> returns <code>true</code>
 169      * @throws SecurityException if {@code accessSystemTray} permission
 170      * is not granted
 171      * @see #add(TrayIcon)
 172      * @see TrayIcon
 173      * @see #isSupported
 174      * @see SecurityManager#checkPermission
 175      * @see AWTPermission
 176      */
 177     public static SystemTray getSystemTray() {
 178         checkSystemTrayAllowed();
 179         if (GraphicsEnvironment.isHeadless()) {
 180             throw new HeadlessException();
 181         }
 182 
 183         initializeSystemTrayIfNeeded();
 184 
 185         if (!isSupported()) {
 186             throw new UnsupportedOperationException(
 187                 "The system tray is not supported on the current platform.");
 188         }
 189 
 190         return systemTray;
 191     }
 192 
 193     /**
 194      * Returns whether the system tray is supported on the current
 195      * platform.  In addition to displaying the tray icon, minimal
 196      * system tray support includes either a popup menu (see {@link
 197      * TrayIcon#setPopupMenu(PopupMenu)}) or an action event (see
 198      * {@link TrayIcon#addActionListener(ActionListener)}).
 199      *
 200      * <p>Developers should not assume that all of the system tray
 201      * functionality is supported.  To guarantee that the tray icon's
 202      * default action is always accessible, add the default action to
 203      * both the action listener and the popup menu.  See the {@link
 204      * SystemTray example} for an example of how to do this.
 205      *
 206      * <p><b>Note</b>: When implementing <code>SystemTray</code> and
 207      * <code>TrayIcon</code> it is <em>strongly recommended</em> that
 208      * you assign different gestures to the popup menu and an action
 209      * event.  Overloading a gesture for both purposes is confusing
 210      * and may prevent the user from accessing one or the other.
 211      *
 212      * @see #getSystemTray
 213      * @return <code>false</code> if no system tray access is supported; this
 214      * method returns <code>true</code> if the minimal system tray access is
 215      * supported but does not guarantee that all system tray
 216      * functionality is supported for the current platform
 217      */
 218     public static boolean isSupported() {
 219         Toolkit toolkit = Toolkit.getDefaultToolkit();
 220         if (toolkit instanceof SunToolkit) {
 221             // connecting tray to native resource
 222             initializeSystemTrayIfNeeded();
 223             return ((SunToolkit)toolkit).isTraySupported();
 224         } else if (toolkit instanceof HeadlessToolkit) {
 225             // skip initialization as the init routine
 226             // throws HeadlessException
 227             return ((HeadlessToolkit)toolkit).isTraySupported();
 228         } else {
 229             return false;
 230         }
 231     }
 232 
 233     /**
 234      * Adds a <code>TrayIcon</code> to the <code>SystemTray</code>.
 235      * The tray icon becomes visible in the system tray once it is
 236      * added.  The order in which icons are displayed in a tray is not
 237      * specified - it is platform and implementation-dependent.
 238      *
 239      * <p> All icons added by the application are automatically
 240      * removed from the <code>SystemTray</code> upon application exit
 241      * and also when the desktop system tray becomes unavailable.
 242      *
 243      * @param trayIcon the <code>TrayIcon</code> to be added
 244      * @throws NullPointerException if <code>trayIcon</code> is
 245      * <code>null</code>
 246      * @throws IllegalArgumentException if the same instance of
 247      * a <code>TrayIcon</code> is added more than once
 248      * @throws AWTException if the desktop system tray is missing
 249      * @see #remove(TrayIcon)
 250      * @see #getSystemTray
 251      * @see TrayIcon
 252      * @see java.awt.Image
 253      */
 254     public void add(TrayIcon trayIcon) throws AWTException {
 255         if (trayIcon == null) {
 256             throw new NullPointerException("adding null TrayIcon");
 257         }
 258         TrayIcon[] oldArray = null, newArray = null;
 259         Vector<TrayIcon> icons = null;
 260         synchronized (this) {
 261             oldArray = systemTray.getTrayIcons();
 262             icons = (Vector<TrayIcon>)AppContext.getAppContext().get(TrayIcon.class);
 263             if (icons == null) {
 264                 icons = new Vector<TrayIcon>(3);
 265                 AppContext.getAppContext().put(TrayIcon.class, icons);
 266 
 267             } else if (icons.contains(trayIcon)) {
 268                 throw new IllegalArgumentException("adding TrayIcon that is already added");
 269             }
 270             icons.add(trayIcon);
 271             newArray = systemTray.getTrayIcons();
 272 
 273             trayIcon.setID(++currentIconID);
 274         }
 275         try {
 276             trayIcon.addNotify();
 277         } catch (AWTException e) {
 278             icons.remove(trayIcon);
 279             throw e;
 280         }
 281         firePropertyChange("trayIcons", oldArray, newArray);
 282     }
 283 
 284     /**
 285      * Removes the specified <code>TrayIcon</code> from the
 286      * <code>SystemTray</code>.
 287      *
 288      * <p> All icons added by the application are automatically
 289      * removed from the <code>SystemTray</code> upon application exit
 290      * and also when the desktop system tray becomes unavailable.
 291      *
 292      * <p> If <code>trayIcon</code> is <code>null</code> or was not
 293      * added to the system tray, no exception is thrown and no action
 294      * is performed.
 295      *
 296      * @param trayIcon the <code>TrayIcon</code> to be removed
 297      * @see #add(TrayIcon)
 298      * @see TrayIcon
 299      */
 300     public void remove(TrayIcon trayIcon) {
 301         if (trayIcon == null) {
 302             return;
 303         }
 304         TrayIcon[] oldArray = null, newArray = null;
 305         synchronized (this) {
 306             oldArray = systemTray.getTrayIcons();
 307             Vector<TrayIcon> icons = (Vector<TrayIcon>)AppContext.getAppContext().get(TrayIcon.class);
 308             // TrayIcon with no peer is not contained in the array.
 309             if (icons == null || !icons.remove(trayIcon)) {
 310                 return;
 311             }
 312             trayIcon.removeNotify();
 313             newArray = systemTray.getTrayIcons();
 314         }
 315         firePropertyChange("trayIcons", oldArray, newArray);
 316     }
 317 
 318     /**
 319      * Returns an array of all icons added to the tray by this
 320      * application.  You can't access the icons added by another
 321      * application.  Some browsers partition applets in different
 322      * code bases into separate contexts, and establish walls between
 323      * these contexts.  In such a scenario, only the tray icons added
 324      * from this context will be returned.
 325      *
 326      * <p> The returned array is a copy of the actual array and may be
 327      * modified in any way without affecting the system tray.  To
 328      * remove a <code>TrayIcon</code> from the
 329      * <code>SystemTray</code>, use the {@link
 330      * #remove(TrayIcon)} method.
 331      *
 332      * @return an array of all tray icons added to this tray, or an
 333      * empty array if none has been added
 334      * @see #add(TrayIcon)
 335      * @see TrayIcon
 336      */
 337     public TrayIcon[] getTrayIcons() {
 338         Vector<TrayIcon> icons = (Vector<TrayIcon>)AppContext.getAppContext().get(TrayIcon.class);
 339         if (icons != null) {
 340             return (TrayIcon[])icons.toArray(new TrayIcon[icons.size()]);
 341         }
 342         return EMPTY_TRAY_ARRAY;
 343     }
 344 
 345     /**
 346      * Returns the size, in pixels, of the space that a tray icon will
 347      * occupy in the system tray.  Developers may use this methods to
 348      * acquire the preferred size for the image property of a tray icon
 349      * before it is created.  For convenience, there is a similar
 350      * method {@link TrayIcon#getSize} in the <code>TrayIcon</code> class.
 351      *
 352      * @return the default size of a tray icon, in pixels
 353      * @see TrayIcon#setImageAutoSize(boolean)
 354      * @see java.awt.Image
 355      * @see TrayIcon#getSize()
 356      */
 357     public Dimension getTrayIconSize() {
 358         return peer.getTrayIconSize();
 359     }
 360 
 361     /**
 362      * Adds a {@code PropertyChangeListener} to the list of listeners for the
 363      * specific property. The following properties are currently supported:
 364      *
 365      * <table border=1 summary="SystemTray properties">
 366      * <tr>
 367      *    <th>Property</th>
 368      *    <th>Description</th>
 369      * </tr>
 370      * <tr>
 371      *    <td>{@code trayIcons}</td>
 372      *    <td>The {@code SystemTray}'s array of {@code TrayIcon} objects.
 373      *        The array is accessed via the {@link #getTrayIcons} method.<br>
 374      *        This property is changed when a tray icon is added to (or removed
 375      *        from) the system tray.<br> For example, this property is changed
 376      *        when the system tray becomes unavailable on the desktop<br>
 377      *        and the tray icons are automatically removed.</td>
 378      * </tr>
 379      * <tr>
 380      *    <td>{@code systemTray}</td>
 381      *    <td>This property contains {@code SystemTray} instance when the system tray
 382      *        is available or <code>null</code> otherwise.<br> This property is changed
 383      *        when the system tray becomes available or unavailable on the desktop.<br>
 384      *        The property is accessed by the {@link #getSystemTray} method.</td>
 385      * </tr>
 386      * </table>
 387      * <p>
 388      * The {@code listener} listens to property changes only in this context.
 389      * <p>
 390      * If {@code listener} is {@code null}, no exception is thrown
 391      * and no action is performed.
 392      *
 393      * @param propertyName the specified property
 394      * @param listener the property change listener to be added
 395      *
 396      * @see #removePropertyChangeListener
 397      * @see #getPropertyChangeListeners
 398      */
 399     public synchronized void addPropertyChangeListener(String propertyName,
 400                                                        PropertyChangeListener listener)
 401     {
 402         if (listener == null) {
 403             return;
 404         }
 405         getCurrentChangeSupport().addPropertyChangeListener(propertyName, listener);
 406     }
 407 
 408     /**
 409      * Removes a {@code PropertyChangeListener} from the listener list
 410      * for a specific property.
 411      * <p>
 412      * The {@code PropertyChangeListener} must be from this context.
 413      * <p>
 414      * If {@code propertyName} or {@code listener} is {@code null} or invalid,
 415      * no exception is thrown and no action is taken.
 416      *
 417      * @param propertyName the specified property
 418      * @param listener the PropertyChangeListener to be removed
 419      *
 420      * @see #addPropertyChangeListener
 421      * @see #getPropertyChangeListeners
 422      */
 423     public synchronized void removePropertyChangeListener(String propertyName,
 424                                                           PropertyChangeListener listener)
 425     {
 426         if (listener == null) {
 427             return;
 428         }
 429         getCurrentChangeSupport().removePropertyChangeListener(propertyName, listener);
 430     }
 431 
 432     /**
 433      * Returns an array of all the listeners that have been associated
 434      * with the named property.
 435      * <p>
 436      * Only the listeners in this context are returned.
 437      *
 438      * @param propertyName the specified property
 439      * @return all of the {@code PropertyChangeListener}s associated with
 440      *         the named property; if no such listeners have been added or
 441      *         if {@code propertyName} is {@code null} or invalid, an empty
 442      *         array is returned
 443      *
 444      * @see #addPropertyChangeListener
 445      * @see #removePropertyChangeListener
 446      */
 447     public synchronized PropertyChangeListener[] getPropertyChangeListeners(String propertyName) {
 448         return getCurrentChangeSupport().getPropertyChangeListeners(propertyName);
 449     }
 450 
 451 
 452     // ***************************************************************
 453     // ***************************************************************
 454 
 455 
 456     /**
 457      * Support for reporting bound property changes for Object properties.
 458      * This method can be called when a bound property has changed and it will
 459      * send the appropriate PropertyChangeEvent to any registered
 460      * PropertyChangeListeners.
 461      *
 462      * @param propertyName the property whose value has changed
 463      * @param oldValue the property's previous value
 464      * @param newValue the property's new value
 465      */
 466     private void firePropertyChange(String propertyName,
 467                                     Object oldValue, Object newValue)
 468     {
 469         if (oldValue != null && newValue != null && oldValue.equals(newValue)) {
 470             return;
 471         }
 472         getCurrentChangeSupport().firePropertyChange(propertyName, oldValue, newValue);
 473     }
 474 
 475     /**
 476      * Returns the current PropertyChangeSupport instance for the
 477      * calling thread's context.
 478      *
 479      * @return this thread's context's PropertyChangeSupport
 480      */
 481     private synchronized PropertyChangeSupport getCurrentChangeSupport() {
 482         PropertyChangeSupport changeSupport =
 483             (PropertyChangeSupport)AppContext.getAppContext().get(SystemTray.class);
 484 
 485         if (changeSupport == null) {
 486             changeSupport = new PropertyChangeSupport(this);
 487             AppContext.getAppContext().put(SystemTray.class, changeSupport);
 488         }
 489         return changeSupport;
 490     }
 491 
 492     synchronized void addNotify() {
 493         if (peer == null) {
 494             Toolkit toolkit = Toolkit.getDefaultToolkit();
 495             if (toolkit instanceof SunToolkit) {
 496                 peer = ((SunToolkit)Toolkit.getDefaultToolkit()).createSystemTray(this);
 497             } else if (toolkit instanceof HeadlessToolkit) {
 498                 peer = ((HeadlessToolkit)Toolkit.getDefaultToolkit()).createSystemTray(this);
 499             }
 500         }
 501     }
 502 
 503     static void checkSystemTrayAllowed() {
 504         SecurityManager security = System.getSecurityManager();
 505         if (security != null) {
 506             security.checkPermission(AWTPermissions.ACCESS_SYSTEM_TRAY_PERMISSION);
 507         }
 508     }
 509 
 510     private static void initializeSystemTrayIfNeeded() {
 511         synchronized (SystemTray.class) {
 512             if (systemTray == null) {
 513                 systemTray = new SystemTray();
 514             }
 515         }
 516     }
 517 }