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 {
AWTAccessor.setSystemTrayAccessor( new AWTAccessor.SystemTrayAccessor() { public void firePropertyChange(SystemTray tray, String propertyName, Object oldValue, Object newValue) { tray.firePropertyChange(propertyName, oldValue, newValue); } })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      */
SystemTray()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      */
getSystemTray()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      */
isSupported()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      */
add(TrayIcon trayIcon)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      */
remove(TrayIcon trayIcon)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      */
getTrayIcons()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      */
getTrayIconSize()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 class="striped">
372      * <caption>SystemTray properties</caption>
373      * <thead>
374      *   <tr>
375      *     <th scope="col">Property
376      *     <th scope="col">Description
377      * </thead>
378      * <tbody>
379      *   <tr>
380      *     <th scope="row">{@code trayIcons}
381      *     <td>The {@code SystemTray}'s array of {@code TrayIcon} objects. The
382      *     array is accessed via the {@link #getTrayIcons} method. This property
383      *     is changed when a tray icon is added to (or removed from) the system
384      *     tray. For example, this property is changed when the system tray
385      *     becomes unavailable on the desktop and the tray icons are
386      *     automatically removed.
387      *   <tr>
388      *     <th scope="row">{@code systemTray}
389      *     <td>This property contains {@code SystemTray} instance when the
390      *     system tray is available or {@code null} otherwise. This property is
391      *     changed when the system tray becomes available or unavailable on the
392      *     desktop. The property is accessed by the {@link #getSystemTray}
393      *     method.
394      * </tbody>
395      * </table>
396      * <p>
397      * The {@code listener} listens to property changes only in this context.
398      * <p>
399      * If {@code listener} is {@code null}, no exception is thrown
400      * and no action is performed.
401      *
402      * @param propertyName the specified property
403      * @param listener the property change listener to be added
404      *
405      * @see #removePropertyChangeListener
406      * @see #getPropertyChangeListeners
407      */
addPropertyChangeListener(String propertyName, PropertyChangeListener listener)408     public synchronized void addPropertyChangeListener(String propertyName,
409                                                        PropertyChangeListener listener)
410     {
411         if (listener == null) {
412             return;
413         }
414         getCurrentChangeSupport().addPropertyChangeListener(propertyName, listener);
415     }
416 
417     /**
418      * Removes a {@code PropertyChangeListener} from the listener list
419      * for a specific property.
420      * <p>
421      * The {@code PropertyChangeListener} must be from this context.
422      * <p>
423      * If {@code propertyName} or {@code listener} is {@code null} or invalid,
424      * no exception is thrown and no action is taken.
425      *
426      * @param propertyName the specified property
427      * @param listener the PropertyChangeListener to be removed
428      *
429      * @see #addPropertyChangeListener
430      * @see #getPropertyChangeListeners
431      */
removePropertyChangeListener(String propertyName, PropertyChangeListener listener)432     public synchronized void removePropertyChangeListener(String propertyName,
433                                                           PropertyChangeListener listener)
434     {
435         if (listener == null) {
436             return;
437         }
438         getCurrentChangeSupport().removePropertyChangeListener(propertyName, listener);
439     }
440 
441     /**
442      * Returns an array of all the listeners that have been associated
443      * with the named property.
444      * <p>
445      * Only the listeners in this context are returned.
446      *
447      * @param propertyName the specified property
448      * @return all of the {@code PropertyChangeListener}s associated with
449      *         the named property; if no such listeners have been added or
450      *         if {@code propertyName} is {@code null} or invalid, an empty
451      *         array is returned
452      *
453      * @see #addPropertyChangeListener
454      * @see #removePropertyChangeListener
455      */
getPropertyChangeListeners(String propertyName)456     public synchronized PropertyChangeListener[] getPropertyChangeListeners(String propertyName) {
457         return getCurrentChangeSupport().getPropertyChangeListeners(propertyName);
458     }
459 
460 
461     // ***************************************************************
462     // ***************************************************************
463 
464 
465     /**
466      * Support for reporting bound property changes for Object properties.
467      * This method can be called when a bound property has changed and it will
468      * send the appropriate PropertyChangeEvent to any registered
469      * PropertyChangeListeners.
470      *
471      * @param propertyName the property whose value has changed
472      * @param oldValue the property's previous value
473      * @param newValue the property's new value
474      */
firePropertyChange(String propertyName, Object oldValue, Object newValue)475     private void firePropertyChange(String propertyName,
476                                     Object oldValue, Object newValue)
477     {
478         if (oldValue != null && newValue != null && oldValue.equals(newValue)) {
479             return;
480         }
481         getCurrentChangeSupport().firePropertyChange(propertyName, oldValue, newValue);
482     }
483 
484     /**
485      * Returns the current PropertyChangeSupport instance for the
486      * calling thread's context.
487      *
488      * @return this thread's context's PropertyChangeSupport
489      */
getCurrentChangeSupport()490     private synchronized PropertyChangeSupport getCurrentChangeSupport() {
491         PropertyChangeSupport changeSupport =
492             (PropertyChangeSupport)AppContext.getAppContext().get(SystemTray.class);
493 
494         if (changeSupport == null) {
495             changeSupport = new PropertyChangeSupport(this);
496             AppContext.getAppContext().put(SystemTray.class, changeSupport);
497         }
498         return changeSupport;
499     }
500 
addNotify()501     synchronized void addNotify() {
502         if (peer == null) {
503             Toolkit toolkit = Toolkit.getDefaultToolkit();
504             if (toolkit instanceof SunToolkit) {
505                 peer = ((SunToolkit)Toolkit.getDefaultToolkit()).createSystemTray(this);
506             } else if (toolkit instanceof HeadlessToolkit) {
507                 peer = ((HeadlessToolkit)Toolkit.getDefaultToolkit()).createSystemTray(this);
508             }
509         }
510     }
511 
checkSystemTrayAllowed()512     static void checkSystemTrayAllowed() {
513         SecurityManager security = System.getSecurityManager();
514         if (security != null) {
515             security.checkPermission(AWTPermissions.ACCESS_SYSTEM_TRAY_PERMISSION);
516         }
517     }
518 
initializeSystemTrayIfNeeded()519     private static void initializeSystemTrayIfNeeded() {
520         synchronized (SystemTray.class) {
521             if (systemTray == null) {
522                 systemTray = new SystemTray();
523             }
524         }
525     }
526 }
527