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')
- 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.
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:
```
// if using provided JNA jars. Not necessary if
//using JNA from https://github.com/twall/jna
System.load("Path to OS specific JNA jar");
this.systemTray = SystemTray.getSystemTray();
if (systemTray == null) {
throw new RuntimeException("Unable to load SystemTray!");
}
this.systemTray = SystemTray.create("grey_icon.png");
try {
this.systemTray.setIcon("grey_icon.png");
} catch (IOException e) {
e.printStackTrace();
}
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>
<groupId>com.dorkbox</groupId>
<artifactId>SystemTray</artifactId>
<version>1.15</version>
<version>2.0</version>
</dependency>
```

View File

@ -17,6 +17,7 @@ package dorkbox.systemTray;
import dorkbox.systemTray.linux.AppIndicatorTray;
import dorkbox.systemTray.linux.GnomeShellExtension;
import dorkbox.systemTray.linux.GtkSystemTray;
import dorkbox.systemTray.swing.SwingSystemTray;
import dorkbox.util.OS;
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.GtkSupport;
import dorkbox.util.process.ShellProcessBuilder;
import dorkbox.systemTray.linux.GtkSystemTray;
import org.slf4j.Logger;
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")
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) */
public static int TRAY_SIZE = 22;
private static Class<? extends SystemTray> trayType;
private static final SystemTray systemTray;
static boolean isKDE = false;
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
// 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.
@ -215,9 +217,6 @@ class SystemTray {
// fallback...
if (trayType == null) {
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 " +
"configuration");
}
@ -226,20 +225,30 @@ class SystemTray {
// this is windows OR mac
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) {
// unsupported tray
logger.error("Unsupported tray type!");
systemTray = null;
}
else {
SystemTray systemTray_ = null;
try {
ImageUtil.init();
systemTray_ = (SystemTray) trayType.getConstructors()[0].newInstance();
} catch (NoSuchAlgorithmException e) {
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
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
* contents of the specified file.
* This always returns the same instance per JVM (it's a singleton), and on some platforms the system tray may not be
* supported, in which case this will return NULL.
*
* @param iconPath the full path for an icon to use
*
* @return a new SystemTray instance with the specified path for the icon
* <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.
*/
public static
SystemTray create(String iconPath) {
if (trayType != null) {
try {
iconPath = ImageUtil.iconPath(iconPath);
Object o = trayType.getConstructors()[0].newInstance(iconPath);
return (SystemTray) o;
} catch (Throwable e) {
e.printStackTrace();
}
}
// unsupported
return null;
SystemTray getSystemTray() {
return systemTray;
}
/**
* 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
@ -391,6 +308,9 @@ class SystemTray {
/**
* 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
*/
public
@ -402,6 +322,9 @@ class SystemTray {
/**
* 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
*/
public
@ -413,6 +336,9 @@ class SystemTray {
/**
* 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 imageStream the InputStream of the icon to use
*/
@ -425,6 +351,9 @@ class SystemTray {
/**
* 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
* 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 AppIndicator.AppIndicatorInstanceStruct appIndicator;
private volatile boolean isActive = false;
public
AppIndicatorTray(String iconPath) {
AppIndicatorTray() {
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.app_indicator_set_status(this.appIndicator, AppIndicator.STATUS_ACTIVE);
gtk.gdk_threads_leave();
GtkSupport.startGui();
@ -69,6 +68,13 @@ class AppIndicatorTray extends GtkTypeSystemTray {
void setIcon_(final String iconPath) {
gtk.gdk_threads_enter();
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();
}

View File

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

View File

@ -16,11 +16,11 @@
package dorkbox.systemTray.swing;
import dorkbox.systemTray.ImageUtil;
import dorkbox.util.ScreenUtil;
import dorkbox.util.SwingUtil;
import dorkbox.systemTray.MenuEntry;
import dorkbox.systemTray.SystemTrayMenuAction;
import dorkbox.systemTray.SystemTrayMenuPopup;
import dorkbox.util.ScreenUtil;
import dorkbox.util.SwingUtil;
import javax.swing.ImageIcon;
import javax.swing.JMenuItem;
@ -49,11 +49,13 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray {
volatile SystemTray tray;
volatile TrayIcon trayIcon;
volatile boolean isActive = false;
/**
* Creates a new system tray handler class.
*/
public
SwingSystemTray(final String iconPath) {
SwingSystemTray() {
super();
SwingUtil.invokeAndWait(new Runnable() {
@Override
@ -63,63 +65,6 @@ class SwingSystemTray extends dorkbox.systemTray.SystemTray {
if (SwingSystemTray.this.tray == null) {
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() {
SwingSystemTray tray = SwingSystemTray.this;
synchronized (tray) {
Image trayImage = new ImageIcon(iconPath).getImage()
.getScaledInstance(TRAY_SIZE, TRAY_SIZE, Image.SCALE_SMOOTH);
trayImage.flush();
tray.trayIcon.setImage(trayImage);
if (!isActive) {
isActive = true;
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.SystemTrayMenuAction;
import java.io.File;
import java.io.IOException;
import java.net.URL;
@ -36,11 +37,11 @@ class TestTray {
public static
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
//
// 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();
}
@ -51,11 +52,17 @@ class TestTray {
public
TestTray() {
this.systemTray = SystemTray.create(LT_GRAY_MAIL);
this.systemTray = SystemTray.getSystemTray();
if (systemTray == null) {
throw new RuntimeException("Unable to load SystemTray!");
}
try {
this.systemTray.setIcon(LT_GRAY_MAIL);
} catch (IOException e) {
e.printStackTrace();
}
systemTray.setStatus("No Mail");
callbackGreen = new SystemTrayMenuAction() {