diff --git a/README.md b/README.md index d9349d2..e11b255 100644 --- a/README.md +++ b/README.md @@ -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 com.dorkbox SystemTray - 1.15 + 2.0 ``` diff --git a/src/dorkbox/systemTray/SystemTray.java b/src/dorkbox/systemTray/SystemTray.java index dd758d0..b784791 100644 --- a/src/dorkbox/systemTray/SystemTray.java +++ b/src/dorkbox/systemTray/SystemTray.java @@ -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 trayType; + private static final SystemTray systemTray; static boolean isKDE = false; static { + Class 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 + *

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 menuEntries = new ArrayList(); 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. * diff --git a/src/dorkbox/systemTray/linux/AppIndicatorTray.java b/src/dorkbox/systemTray/linux/AppIndicatorTray.java index a835fa8..4989e6a 100644 --- a/src/dorkbox/systemTray/linux/AppIndicatorTray.java +++ b/src/dorkbox/systemTray/linux/AppIndicatorTray.java @@ -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(); } diff --git a/src/dorkbox/systemTray/linux/GtkSystemTray.java b/src/dorkbox/systemTray/linux/GtkSystemTray.java index 3953b4e..b4f57bb 100644 --- a/src/dorkbox/systemTray/linux/GtkSystemTray.java +++ b/src/dorkbox/systemTray/linux/GtkSystemTray.java @@ -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(); } } diff --git a/src/dorkbox/systemTray/swing/SwingSystemTray.java b/src/dorkbox/systemTray/swing/SwingSystemTray.java index abc3516..3fee7c2 100644 --- a/src/dorkbox/systemTray/swing/SwingSystemTray.java +++ b/src/dorkbox/systemTray/swing/SwingSystemTray.java @@ -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); + } } } }); diff --git a/test/dorkbox/TestTray.java b/test/dorkbox/TestTray.java index 22515bd..7538cde 100644 --- a/test/dorkbox/TestTray.java +++ b/test/dorkbox/TestTray.java @@ -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() {