Converted SystemTray to use a singleton pattern for ease of use. Icon *must* be set to see it (obviously). Updated Readme.md example.

This commit is contained in:
nathan 2016-02-12 02:30:33 +01:00
parent 801baad635
commit 8b109f6d1c
6 changed files with 143 additions and 196 deletions

View File

@ -44,24 +44,22 @@ GnomeShellExtension.SHELL_RESTART_COMMAND (type String, default value 'gnome-s
SystemTray.TRAY_SIZE (type int, default value '24') SystemTray.TRAY_SIZE (type int, default value '24')
- Size of the tray, so that the icon can properly scale based on OS. (if it's not exact). This only applies for Swing tray icons. - Size of the tray, so that the icon can properly scale based on OS. (if it's not exact). This only applies for Swing tray icons.
- NOTE: Must be set after any other customization options, as a static call to SystemTray will cause initialization of the library. - NOTE: Must be set after any other customization options, as a static call to SystemTray will cause initialization of the library.
SystemTray.ICON_PATH (type String, default value '')
- Location of the icon (to make it easier when specifying icons)
- NOTE: Must be set after any other customization options, as a static call to SystemTray will cause initialization of the library.
of the library.
``` ```
The test application is [on GitHub](https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java), and a *simple* example is as follows: The test application is [on GitHub](https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java), and a *simple* example is as follows:
``` ```
// if using provided JNA jars. Not necessary if this.systemTray = SystemTray.getSystemTray();
//using JNA from https://github.com/twall/jna if (systemTray == null) {
System.load("Path to OS specific JNA jar"); throw new RuntimeException("Unable to load SystemTray!");
}
try {
this.systemTray = SystemTray.create("grey_icon.png"); this.systemTray.setIcon("grey_icon.png");
} catch (IOException e) {
e.printStackTrace();
}
this.systemTray.setStatus("Not Running"); this.systemTray.setStatus("Not Running");
@ -135,7 +133,7 @@ This project is **kept in sync** with the utilities library, so "jar hell" is no
<dependency> <dependency>
<groupId>com.dorkbox</groupId> <groupId>com.dorkbox</groupId>
<artifactId>SystemTray</artifactId> <artifactId>SystemTray</artifactId>
<version>1.15</version> <version>2.0</version>
</dependency> </dependency>
``` ```

View File

@ -17,6 +17,7 @@ package dorkbox.systemTray;
import dorkbox.systemTray.linux.AppIndicatorTray; import dorkbox.systemTray.linux.AppIndicatorTray;
import dorkbox.systemTray.linux.GnomeShellExtension; import dorkbox.systemTray.linux.GnomeShellExtension;
import dorkbox.systemTray.linux.GtkSystemTray;
import dorkbox.systemTray.swing.SwingSystemTray; import dorkbox.systemTray.swing.SwingSystemTray;
import dorkbox.util.OS; import dorkbox.util.OS;
import dorkbox.util.Property; import dorkbox.util.Property;
@ -24,7 +25,6 @@ import dorkbox.util.jna.linux.AppIndicator;
import dorkbox.util.jna.linux.AppIndicatorQuery; import dorkbox.util.jna.linux.AppIndicatorQuery;
import dorkbox.util.jna.linux.GtkSupport; import dorkbox.util.jna.linux.GtkSupport;
import dorkbox.util.process.ShellProcessBuilder; import dorkbox.util.process.ShellProcessBuilder;
import dorkbox.systemTray.linux.GtkSystemTray;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -42,7 +42,7 @@ import java.util.Iterator;
/** /**
* Interface for system tray implementations. * Factory and base-class for system tray implementations.
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public abstract public abstract
@ -53,11 +53,13 @@ class SystemTray {
/** Size of the tray, so that the icon can properly scale based on OS. (if it's not exact) */ /** Size of the tray, so that the icon can properly scale based on OS. (if it's not exact) */
public static int TRAY_SIZE = 22; public static int TRAY_SIZE = 22;
private static Class<? extends SystemTray> trayType; private static final SystemTray systemTray;
static boolean isKDE = false; static boolean isKDE = false;
static { static {
Class<? extends SystemTray> trayType = null;
// Note: AppIndicators DO NOT support tooltips. We could try to create one, by creating a GTK widget and attaching it on // Note: AppIndicators DO NOT support tooltips. We could try to create one, by creating a GTK widget and attaching it on
// mouseover or something, but I don't know how to do that. It seems that tooltips for app-indicators are a custom job, as // mouseover or something, but I don't know how to do that. It seems that tooltips for app-indicators are a custom job, as
// all examined ones sometimes have it (and it's more than just text), or they don't have it at all. // all examined ones sometimes have it (and it's more than just text), or they don't have it at all.
@ -215,9 +217,6 @@ class SystemTray {
// fallback... // fallback...
if (trayType == null) { if (trayType == null) {
trayType = GtkSystemTray.class; trayType = GtkSystemTray.class;
}
if (trayType == null) {
logger.error("Unable to load the system tray native library. Please write an issue and include your OS type and " + logger.error("Unable to load the system tray native library. Please write an issue and include your OS type and " +
"configuration"); "configuration");
} }
@ -226,20 +225,30 @@ class SystemTray {
// this is windows OR mac // this is windows OR mac
if (trayType == null && java.awt.SystemTray.isSupported()) { if (trayType == null && java.awt.SystemTray.isSupported()) {
trayType = SwingSystemTray.class; try {
java.awt.SystemTray.getSystemTray();
trayType = SwingSystemTray.class;
} catch (Throwable ignored) {
logger.error("Maybe you should grant the AWTPermission `accessSystemTray` in the SecurityManager.");
}
} }
if (trayType == null) { if (trayType == null) {
// unsupported tray // unsupported tray
logger.error("Unsupported tray type!"); logger.error("Unsupported tray type!");
systemTray = null;
} }
else { else {
SystemTray systemTray_ = null;
try { try {
ImageUtil.init(); ImageUtil.init();
systemTray_ = (SystemTray) trayType.getConstructors()[0].newInstance();
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
logger.error("Unsupported hashing algorithm!"); logger.error("Unsupported hashing algorithm!");
trayType = null; } catch (Exception e) {
logger.error("Unable to create tray type: '" + trayType.getSimpleName() + "'");
} }
systemTray = systemTray_;
} }
} }
@ -248,113 +257,21 @@ class SystemTray {
*/ */
public static public static
String getVersion() { String getVersion() {
return "1.15"; return "2.1";
} }
/** /**
* Because the cross-platform, underlying system uses a file path to load icons for the system tray, this will directly use the * This always returns the same instance per JVM (it's a singleton), and on some platforms the system tray may not be
* contents of the specified file. * supported, in which case this will return NULL.
* *
* @param iconPath the full path for an icon to use * <p>If this is using the Swing SystemTray and a SecurityManager is installed, the AWTPermission {@code accessSystemTray} must
* * be granted in order to get the {@code SystemTray} instance. Otherwise this will return null.
* @return a new SystemTray instance with the specified path for the icon
*/ */
public static public static
SystemTray create(String iconPath) { SystemTray getSystemTray() {
if (trayType != null) { return systemTray;
try {
iconPath = ImageUtil.iconPath(iconPath);
Object o = trayType.getConstructors()[0].newInstance(iconPath);
return (SystemTray) o;
} catch (Throwable e) {
e.printStackTrace();
}
}
// unsupported
return null;
} }
/**
* Because the cross-platform, underlying system uses a file path to load icons for the system tray, this will copy the contents of
* the URL to a temporary location on disk, based on the path specified by the URL.
*
* @param iconUrl the URL for the icon to use
*
* @return a new SystemTray instance with the specified URL for the icon
*/
public static
SystemTray create(final URL iconUrl) {
if (trayType != null) {
try {
String iconPath = ImageUtil.iconPath(iconUrl);
Object o = trayType.getConstructors()[0].newInstance(iconPath);
return (SystemTray) o;
} catch (Throwable e) {
e.printStackTrace();
}
}
// unsupported
return null;
}
/**
* Because the cross-platform, underlying system uses a file path to load icons for the system tray, this will copy the contents of
* the iconStream to a temporary location on disk, based on the `cacheName` specified.
*
* @param cacheName the name to use for the cache lookup for the iconStream. This can be anything you want, but should be
* consistently unique
* @param iconStream the InputStream to load the icon from
*
* @return a new SystemTray instance with the specified InputStream for the icon
*/
public static
SystemTray create(final String cacheName, final InputStream iconStream) {
if (trayType != null) {
try {
String iconPath = ImageUtil.iconPath(cacheName, iconStream);
Object o = trayType.getConstructors()[0].newInstance(iconPath);
return (SystemTray) o;
} catch (Throwable e) {
e.printStackTrace();
}
}
// unsupported
return null;
}
/**
* Because the cross-platform, underlying system uses a file path to load icons for the system tray, this will copy the contents of
* the iconStream to a temporary location on disk.
*
* This method **DOES NOT CACHE** the result, so multiple lookups for the same inputStream result in new files every time. This is
* also NOT RECOMMENDED, but is provided for simplicity.
*
* @param iconStream the InputStream to load the icon from
*
* @return a new SystemTray instance with the specified InputStream for the icon
*/
@Deprecated
public static
SystemTray create(final InputStream iconStream) {
if (trayType != null) {
try {
String iconPath = ImageUtil.iconPathNoCache(iconStream);
Object o = trayType.getConstructors()[0].newInstance(iconPath);
return (SystemTray) o;
} catch (Throwable e) {
e.printStackTrace();
}
}
// unsupported
return null;
}
protected final java.util.List<MenuEntry> menuEntries = new ArrayList<MenuEntry>(); protected final java.util.List<MenuEntry> menuEntries = new ArrayList<MenuEntry>();
protected protected
@ -391,6 +308,9 @@ class SystemTray {
/** /**
* Changes the tray icon used. * Changes the tray icon used.
* *
* Because the cross-platform, underlying system uses a file path to load icons for the system tray,
* this will directly use the contents of the specified file.
*
* @param imagePath the path of the icon to use * @param imagePath the path of the icon to use
*/ */
public public
@ -402,6 +322,9 @@ class SystemTray {
/** /**
* Changes the tray icon used. * Changes the tray icon used.
* *
* Because the cross-platform, underlying system uses a file path to load icons for the system tray, this will copy the contents of
* the URL to a temporary location on disk, based on the path specified by the URL.
*
* @param imageUrl the URL of the icon to use * @param imageUrl the URL of the icon to use
*/ */
public public
@ -413,6 +336,9 @@ class SystemTray {
/** /**
* Changes the tray icon used. * Changes the tray icon used.
* *
* Because the cross-platform, underlying system uses a file path to load icons for the system tray, this will copy the contents of
* the imageStream to a temporary location on disk, based on the `cacheName` specified.
*
* @param cacheName the name to use for lookup in the cache for the iconStream * @param cacheName the name to use for lookup in the cache for the iconStream
* @param imageStream the InputStream of the icon to use * @param imageStream the InputStream of the icon to use
*/ */
@ -425,6 +351,9 @@ class SystemTray {
/** /**
* Changes the tray icon used. * Changes the tray icon used.
* *
* Because the cross-platform, underlying system uses a file path to load icons for the system tray, this will copy the contents of
* the imageStream to a temporary location on disk.
*
* This method **DOES NOT CACHE** the result, so multiple lookups for the same inputStream result in new files every time. This is * This method **DOES NOT CACHE** the result, so multiple lookups for the same inputStream result in new files every time. This is
* also NOT RECOMMENDED, but is provided for simplicity. * also NOT RECOMMENDED, but is provided for simplicity.
* *

View File

@ -33,15 +33,14 @@ class AppIndicatorTray extends GtkTypeSystemTray {
private static final AppIndicator appindicator = AppIndicator.INSTANCE; private static final AppIndicator appindicator = AppIndicator.INSTANCE;
private AppIndicator.AppIndicatorInstanceStruct appIndicator; private AppIndicator.AppIndicatorInstanceStruct appIndicator;
private volatile boolean isActive = false;
public public
AppIndicatorTray(String iconPath) { AppIndicatorTray() {
gtk.gdk_threads_enter(); gtk.gdk_threads_enter();
this.appIndicator = appindicator.app_indicator_new(System.nanoTime() + "DBST", iconPath, this.appIndicator = appindicator.app_indicator_new(System.nanoTime() + "DBST", "",
AppIndicator.CATEGORY_APPLICATION_STATUS); AppIndicator.CATEGORY_APPLICATION_STATUS);
appindicator.app_indicator_set_status(this.appIndicator, AppIndicator.STATUS_ACTIVE);
gtk.gdk_threads_leave(); gtk.gdk_threads_leave();
GtkSupport.startGui(); GtkSupport.startGui();
@ -69,6 +68,13 @@ class AppIndicatorTray extends GtkTypeSystemTray {
void setIcon_(final String iconPath) { void setIcon_(final String iconPath) {
gtk.gdk_threads_enter(); gtk.gdk_threads_enter();
appindicator.app_indicator_set_icon(this.appIndicator, iconPath); appindicator.app_indicator_set_icon(this.appIndicator, iconPath);
if (!isActive) {
isActive = true;
appindicator.app_indicator_set_status(this.appIndicator, AppIndicator.STATUS_ACTIVE);
}
gtk.gdk_threads_leave(); gtk.gdk_threads_leave();
} }

View File

@ -36,11 +36,11 @@ class GtkSystemTray extends GtkTypeSystemTray {
@SuppressWarnings({"FieldCanBeLocal", "unused"}) @SuppressWarnings({"FieldCanBeLocal", "unused"})
private NativeLong button_press_event; private NativeLong button_press_event;
private volatile boolean isActive = false;
private volatile Pointer menu; private volatile Pointer menu;
public public
GtkSystemTray(String iconPath) { GtkSystemTray() {
super(); super();
gtk.gdk_threads_enter(); gtk.gdk_threads_enter();
@ -51,8 +51,6 @@ class GtkSystemTray extends GtkTypeSystemTray {
this.trayIcon = trayIcon; this.trayIcon = trayIcon;
gtk.gtk_status_icon_set_from_file(trayIcon, iconPath);
this.gtkCallback = new Gobject.GEventCallback() { this.gtkCallback = new Gobject.GEventCallback() {
@Override @Override
public public
@ -65,8 +63,6 @@ class GtkSystemTray extends GtkTypeSystemTray {
}; };
button_press_event = gobject.g_signal_connect_data(trayIcon, "button_press_event", gtkCallback, null, null, 0); button_press_event = gobject.g_signal_connect_data(trayIcon, "button_press_event", gtkCallback, null, null, 0);
gtk.gtk_status_icon_set_visible(trayIcon, true);
gtk.gdk_threads_leave(); gtk.gdk_threads_leave();
GtkSupport.startGui(); GtkSupport.startGui();
@ -101,7 +97,13 @@ class GtkSystemTray extends GtkTypeSystemTray {
protected synchronized protected synchronized
void setIcon_(final String iconPath) { void setIcon_(final String iconPath) {
gtk.gdk_threads_enter(); gtk.gdk_threads_enter();
gtk.gtk_status_icon_set_from_file(trayIcon, iconPath); gtk.gtk_status_icon_set_from_file(trayIcon, iconPath);
if (!isActive) {
isActive = true;
gtk.gtk_status_icon_set_visible(trayIcon, true);
}
gtk.gdk_threads_leave(); gtk.gdk_threads_leave();
} }
} }

View File

@ -16,11 +16,11 @@
package dorkbox.systemTray.swing; package dorkbox.systemTray.swing;
import dorkbox.systemTray.ImageUtil; import dorkbox.systemTray.ImageUtil;
import dorkbox.util.ScreenUtil;
import dorkbox.util.SwingUtil;
import dorkbox.systemTray.MenuEntry; import dorkbox.systemTray.MenuEntry;
import dorkbox.systemTray.SystemTrayMenuAction; import dorkbox.systemTray.SystemTrayMenuAction;
import dorkbox.systemTray.SystemTrayMenuPopup; import dorkbox.systemTray.SystemTrayMenuPopup;
import dorkbox.util.ScreenUtil;
import dorkbox.util.SwingUtil;
import javax.swing.ImageIcon; import javax.swing.ImageIcon;
import javax.swing.JMenuItem; import javax.swing.JMenuItem;
@ -49,11 +49,13 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray {
volatile SystemTray tray; volatile SystemTray tray;
volatile TrayIcon trayIcon; volatile TrayIcon trayIcon;
volatile boolean isActive = false;
/** /**
* Creates a new system tray handler class. * Creates a new system tray handler class.
*/ */
public public
SwingSystemTray(final String iconPath) { SwingSystemTray() {
super(); super();
SwingUtil.invokeAndWait(new Runnable() { SwingUtil.invokeAndWait(new Runnable() {
@Override @Override
@ -63,63 +65,6 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray {
if (SwingSystemTray.this.tray == null) { if (SwingSystemTray.this.tray == null) {
logger.error("The system tray is not available"); logger.error("The system tray is not available");
} }
else {
SwingSystemTray.this.menu = new SystemTrayMenuPopup();
Image trayImage = new ImageIcon(iconPath).getImage()
.getScaledInstance(TRAY_SIZE, TRAY_SIZE, Image.SCALE_SMOOTH);
trayImage.flush();
final TrayIcon trayIcon = new TrayIcon(trayImage);
SwingSystemTray.this.trayIcon = trayIcon;
// appindicators don't support this, so we cater to the lowest common denominator
// trayIcon.setToolTip(SwingSystemTray.this.appName);
trayIcon.addMouseListener(new MouseAdapter() {
@Override
public
void mousePressed(MouseEvent e) {
final SystemTrayMenuPopup menu = SwingSystemTray.this.menu;
Dimension size = menu.getPreferredSize();
Point point = e.getPoint();
Rectangle bounds = ScreenUtil.getScreenBoundsAt(point);
int x = point.x;
int y = point.y;
if (y < bounds.y) {
y = bounds.y;
}
else if (y + size.height > bounds.y + bounds.height) {
// our menu cannot have the top-edge snap to the mouse
// so we make the bottom-edge snap to the mouse
y -= size.height; // snap to edge of mouse
}
if (x < bounds.x) {
x = bounds.x;
}
else if (x + size.width > bounds.x + bounds.width) {
// our menu cannot have the left-edge snap to the mouse
// so we make the right-edge snap to the mouse
x -= size.width; // snap to edge of mouse
}
// weird voodoo to get this to popup with the correct parent
menu.setInvoker(menu);
menu.setLocation(x, y);
menu.setVisible(true);
menu.requestFocus();
}
});
try {
SwingSystemTray.this.tray.add(trayIcon);
} catch (AWTException e) {
logger.error("TrayIcon could not be added.", e);
}
}
} }
}); });
} }
@ -184,10 +129,70 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray {
void run() { void run() {
SwingSystemTray tray = SwingSystemTray.this; SwingSystemTray tray = SwingSystemTray.this;
synchronized (tray) { synchronized (tray) {
Image trayImage = new ImageIcon(iconPath).getImage() if (!isActive) {
.getScaledInstance(TRAY_SIZE, TRAY_SIZE, Image.SCALE_SMOOTH); isActive = true;
trayImage.flush();
tray.trayIcon.setImage(trayImage); SwingSystemTray.this.menu = new SystemTrayMenuPopup();
Image trayImage = new ImageIcon(iconPath).getImage()
.getScaledInstance(TRAY_SIZE, TRAY_SIZE, Image.SCALE_SMOOTH);
trayImage.flush();
final TrayIcon trayIcon = new TrayIcon(trayImage);
SwingSystemTray.this.trayIcon = trayIcon;
// appindicators don't support this, so we cater to the lowest common denominator
// trayIcon.setToolTip(SwingSystemTray.this.appName);
trayIcon.addMouseListener(new MouseAdapter() {
@Override
public
void mousePressed(MouseEvent e) {
final SystemTrayMenuPopup menu = SwingSystemTray.this.menu;
Dimension size = menu.getPreferredSize();
Point point = e.getPoint();
Rectangle bounds = ScreenUtil.getScreenBoundsAt(point);
int x = point.x;
int y = point.y;
if (y < bounds.y) {
y = bounds.y;
}
else if (y + size.height > bounds.y + bounds.height) {
// our menu cannot have the top-edge snap to the mouse
// so we make the bottom-edge snap to the mouse
y -= size.height; // snap to edge of mouse
}
if (x < bounds.x) {
x = bounds.x;
}
else if (x + size.width > bounds.x + bounds.width) {
// our menu cannot have the left-edge snap to the mouse
// so we make the right-edge snap to the mouse
x -= size.width; // snap to edge of mouse
}
// weird voodoo to get this to popup with the correct parent
menu.setInvoker(menu);
menu.setLocation(x, y);
menu.setVisible(true);
menu.requestFocus();
}
});
try {
SwingSystemTray.this.tray.add(trayIcon);
} catch (AWTException e) {
logger.error("TrayIcon could not be added.", e);
}
} else {
Image trayImage = new ImageIcon(iconPath).getImage()
.getScaledInstance(TRAY_SIZE, TRAY_SIZE, Image.SCALE_SMOOTH);
trayImage.flush();
tray.trayIcon.setImage(trayImage);
}
} }
} }
}); });

View File

@ -20,6 +20,7 @@ import dorkbox.systemTray.MenuEntry;
import dorkbox.systemTray.SystemTray; import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.SystemTrayMenuAction; import dorkbox.systemTray.SystemTrayMenuAction;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
@ -36,11 +37,11 @@ class TestTray {
public static public static
void main(String[] args) { void main(String[] args) {
// ONLY if manually loading JNA jars (which is how i do it). // ONLY if manually loading JNA jars.
// //
// Not necessary if using the official JNA downloaded from https://github.com/twall/jna AND THAT JAR is on the classpath // Not necessary if using the official JNA downloaded from https://github.com/twall/jna AND THAT JAR is on the classpath
// //
// System.load(new File("../../resources/Dependencies/jna/linux_64/libjna.so").getAbsolutePath()); //64bit linux library System.load(new File("../../resources/Dependencies/jna/linux_64/libjna.so").getAbsolutePath()); //64bit linux library
new TestTray(); new TestTray();
} }
@ -51,11 +52,17 @@ class TestTray {
public public
TestTray() { TestTray() {
this.systemTray = SystemTray.create(LT_GRAY_MAIL); this.systemTray = SystemTray.getSystemTray();
if (systemTray == null) { if (systemTray == null) {
throw new RuntimeException("Unable to load SystemTray!"); throw new RuntimeException("Unable to load SystemTray!");
} }
try {
this.systemTray.setIcon(LT_GRAY_MAIL);
} catch (IOException e) {
e.printStackTrace();
}
systemTray.setStatus("No Mail"); systemTray.setStatus("No Mail");
callbackGreen = new SystemTrayMenuAction() { callbackGreen = new SystemTrayMenuAction() {