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