diff --git a/src/dorkbox/systemTray/Menu.java b/src/dorkbox/systemTray/Menu.java
index 143d3655..9bfba2c3 100644
--- a/src/dorkbox/systemTray/Menu.java
+++ b/src/dorkbox/systemTray/Menu.java
@@ -168,6 +168,7 @@ interface Menu extends Entry {
*/
Menu addMenu(String menuText, InputStream imageStream);
+
/**
* Adds a swing widget as a menu entry.
*
diff --git a/src/dorkbox/systemTray/SystemTray.java b/src/dorkbox/systemTray/SystemTray.java
index 81607686..d9cc66e8 100644
--- a/src/dorkbox/systemTray/SystemTray.java
+++ b/src/dorkbox/systemTray/SystemTray.java
@@ -32,12 +32,18 @@ import org.slf4j.LoggerFactory;
import dorkbox.systemTray.linux.GnomeShellExtension;
import dorkbox.systemTray.linux.jna.AppIndicator;
import dorkbox.systemTray.linux.jna.Gtk;
-import dorkbox.systemTray.swing._AppIndicatorTray;
-import dorkbox.systemTray.swing._GtkStatusIconTray;
-import dorkbox.systemTray.swing._SwingTray;
+import dorkbox.systemTray.nativeUI.NativeUI;
+import dorkbox.systemTray.nativeUI._AppIndicatorNativeTray;
+import dorkbox.systemTray.nativeUI._AwtTray;
+import dorkbox.systemTray.nativeUI._GtkStatusIconNativeTray;
+import dorkbox.systemTray.swingUI.SwingUI;
+import dorkbox.systemTray.swingUI._AppIndicatorTray;
+import dorkbox.systemTray.swingUI._GtkStatusIconTray;
+import dorkbox.systemTray.swingUI._SwingTray;
+import dorkbox.systemTray.util.ImageUtils;
import dorkbox.systemTray.util.JavaFX;
import dorkbox.systemTray.util.Swt;
-import dorkbox.systemTray.util.WindowsSystemTraySwing;
+import dorkbox.systemTray.util.SystemTrayFixes;
import dorkbox.util.CacheUtil;
import dorkbox.util.IO;
import dorkbox.util.OS;
@@ -54,10 +60,12 @@ public
class SystemTray implements Menu {
public static final Logger logger = LoggerFactory.getLogger(SystemTray.class);
- public static final int TYPE_AUTO_DETECT = 0;
- public static final int TYPE_GTK_STATUSICON = 1;
- public static final int TYPE_APP_INDICATOR = 2;
- public static final int TYPE_SWING = 3;
+ public enum TrayType {
+ AutoDetect,
+ GtkStatusIcon,
+ AppIndicator,
+ Swing
+ }
@Property
/** Enables auto-detection for the system tray. This should be mostly successful.
@@ -97,11 +105,11 @@ class SystemTray implements Menu {
@Property
/**
- * Forces the system tray detection to be Automatic (0), GtkStatusIcon (1), AppIndicator (2), or Swing (3).
+ * Forces the system tray detection to be AutoDetect, GtkStatusIcon, AppIndicator, or Swing.
*
- * This is an advanced feature, and it is recommended to leave at 0.
+ * This is an advanced feature, and it is recommended to leave at AutoDetect.
*/
- public static int FORCE_TRAY_TYPE = 0;
+ public static TrayType FORCE_TRAY_TYPE = TrayType.Swing;
@Property
/**
@@ -124,6 +132,8 @@ class SystemTray implements Menu {
public final static boolean isJavaFxLoaded;
public final static boolean isSwtLoaded;
+ private static boolean forceNativeMenus = false;
+
static {
boolean isJavaFxLoaded_ = false;
@@ -151,7 +161,49 @@ class SystemTray implements Menu {
isSwtLoaded = isSwtLoaded_;
}
+ private static
+ Class extends Menu> selectType(final TrayType trayType) throws Exception {
+ if (trayType == TrayType.GtkStatusIcon) {
+ if (forceNativeMenus) {
+ return _GtkStatusIconNativeTray.class;
+ } else {
+ return _GtkStatusIconTray.class;
+ }
+ } else if (trayType == TrayType.AppIndicator) {
+ if (forceNativeMenus) {
+ return _AppIndicatorNativeTray.class;
+ }
+ else {
+ return _AppIndicatorTray.class;
+ }
+ }
+ else if (trayType == TrayType.Swing) {
+ if (forceNativeMenus && !OS.isWindows()) {
+ // AWT on windows looks like crap
+ return _AwtTray.class;
+ }
+ else {
+ return _SwingTray.class;
+ }
+ }
+ return null;
+ }
+
+ private static
+ Class extends Menu> selectTypeQuietly(final TrayType trayType) {
+ try {
+ return selectType(trayType);
+ } catch (Throwable t) {
+ if (DEBUG) {
+ logger.error("Cannot initialize {}", trayType.name(), t);
+ }
+ }
+
+ return null;
+ }
+
+ @SuppressWarnings("ConstantConditions")
private static void init() {
if (systemTray != null) {
return;
@@ -173,15 +225,15 @@ class SystemTray implements Menu {
} else {
// windows and mac ONLY support the Swing SystemTray.
// Linux CAN support Swing SystemTray, but it looks like crap (so we wrote our own GtkStatusIcon/AppIndicator)
- if (OS.isWindows() && FORCE_TRAY_TYPE != TYPE_SWING) {
+ if (OS.isWindows() && FORCE_TRAY_TYPE != TrayType.Swing) {
throw new RuntimeException("Windows is incompatible with the specified option for FORCE_TRAY_TYPE: " + FORCE_TRAY_TYPE);
- } else if (OS.isMacOsX() && FORCE_TRAY_TYPE != TYPE_SWING) {
+ } else if (OS.isMacOsX() && FORCE_TRAY_TYPE != TrayType.Swing) {
throw new RuntimeException("MacOSx is incompatible with the specified option for FORCE_TRAY_TYPE: " + FORCE_TRAY_TYPE);
}
}
// kablooie if SWT is not configured in a way that works with us.
- if (FORCE_TRAY_TYPE != TYPE_SWING && OS.isLinux()) {
+ if (FORCE_TRAY_TYPE != TrayType.Swing && OS.isLinux()) {
if (isSwtLoaded) {
// Necessary for us to work with SWT based on version info. We can try to set us to be compatible with whatever it is set to
// System.setProperty("SWT_GTK3", "0");
@@ -240,17 +292,11 @@ class SystemTray implements Menu {
}
}
- if (FORCE_TRAY_TYPE < 0 || FORCE_TRAY_TYPE > 3) {
- throw new RuntimeException("Invalid option for FORCE_TRAY_TYPE: " + FORCE_TRAY_TYPE);
- }
-
if (DEBUG) {
- switch (FORCE_TRAY_TYPE) {
- case 1: logger.debug("Forced tray type: GtkStatusIcon"); break;
- case 2: logger.debug("Forced tray type: AppIndicator"); break;
- case 3: logger.debug("Forced tray type: Swing"); break;
-
- default: logger.debug("Auto-detecting tray type"); break;
+ if (FORCE_TRAY_TYPE == TrayType.AutoDetect) {
+ logger.debug("Auto-detecting tray type");
+ } else {
+ logger.debug("Forced tray type: {}", FORCE_TRAY_TYPE.name());
}
logger.debug("FORCE_GTK2: {}", FORCE_GTK2);
}
@@ -259,7 +305,7 @@ class SystemTray implements Menu {
// 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.
- if (FORCE_TRAY_TYPE != TYPE_SWING && OS.isLinux()) {
+ if (FORCE_TRAY_TYPE != TrayType.Swing && OS.isLinux()) {
// see: https://askubuntu.com/questions/72549/how-to-determine-which-window-manager-is-running
// For funsies, SyncThing did a LOT of work on compatibility (unfortunate for us) in python.
@@ -275,26 +321,12 @@ class SystemTray implements Menu {
}
}
- if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_GTK_STATUSICON) {
- try {
- trayType = _GtkStatusIconTray.class;
- } catch (Throwable e1) {
- if (DEBUG) {
- logger.error("Cannot initialize _GtkStatusIconTray", e1);
- }
- }
- }
- else if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_APP_INDICATOR) {
- try {
- trayType = _AppIndicatorTray.class;
- } catch (Throwable e1) {
- if (DEBUG) {
- logger.error("Cannot initialize _AppIndicatorTray", e1);
- }
- }
- }
- // don't check for SWING type at this spot, it is done elsewhere.
+ // this can never be swing
+ // don't check for SWING type at this spot, it is done elsewhere.
+ if (SystemTray.FORCE_TRAY_TYPE != TrayType.AutoDetect) {
+ trayType = selectTypeQuietly(SystemTray.FORCE_TRAY_TYPE);
+ }
// quick check, because we know that unity uses app-indicator. Maybe REALLY old versions do not. We support 14.04 LTE at least
@@ -341,13 +373,7 @@ class SystemTray implements Menu {
if (trayType == null) {
if ("unity".equalsIgnoreCase(XDG)) {
- try {
- trayType = _AppIndicatorTray.class;
- } catch (Throwable e) {
- if (DEBUG) {
- logger.error("Cannot initialize _AppIndicatorTray", e);
- }
- }
+ trayType = selectTypeQuietly(TrayType.AppIndicator);
}
else if ("xfce".equalsIgnoreCase(XDG)) {
// NOTE: XFCE used to use appindicator3, which DOES NOT support images in the menu. This change was reverted.
@@ -355,32 +381,14 @@ class SystemTray implements Menu {
// see: https://git.gnome.org/browse/gtk+/commit/?id=627a03683f5f41efbfc86cc0f10e1b7c11e9bb25
// so far, it is OK to use GtkStatusIcon on XFCE <-> XFCE4 inclusive
- try {
- trayType = _GtkStatusIconTray.class;
- } catch (Throwable e1) {
- if (DEBUG) {
- logger.error("Cannot initialize _GtkStatusIconTray", e1);
- }
- }
+ trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
}
else if ("lxde".equalsIgnoreCase(XDG)) {
- try {
- trayType = _GtkStatusIconTray.class;
- } catch (Throwable e) {
- if (DEBUG) {
- logger.error("Cannot initialize _GtkStatusIconTray", e);
- }
- }
+ trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
}
else if ("kde".equalsIgnoreCase(XDG)) {
// kde (at least, plasma 5.5.6) requires appindicator
- try {
- trayType = _AppIndicatorTray.class;
- } catch (Throwable e) {
- if (DEBUG) {
- logger.error("Cannot initialize _AppIndicatorTray", e);
- }
- }
+ trayType = selectTypeQuietly(TrayType.AppIndicator);
}
else if ("gnome".equalsIgnoreCase(XDG)) {
// check other DE
@@ -391,31 +399,13 @@ class SystemTray implements Menu {
}
if ("cinnamon".equalsIgnoreCase(GDM)) {
- try {
- trayType = _GtkStatusIconTray.class;
- } catch (Throwable e) {
- if (DEBUG) {
- logger.error("Cannot initialize _GtkStatusIconTray", e);
- }
- }
+ trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
}
else if ("gnome-classic".equalsIgnoreCase(GDM)) {
- try {
- trayType = _GtkStatusIconTray.class;
- } catch (Throwable e) {
- if (DEBUG) {
- logger.error("Cannot initialize _GtkStatusIconTray", e);
- }
- }
+ trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
}
else if ("gnome-fallback".equalsIgnoreCase(GDM)) {
- try {
- trayType = _GtkStatusIconTray.class;
- } catch (Throwable e) {
- if (DEBUG) {
- logger.error("Cannot initialize _GtkStatusIconTray", e);
- }
- }
+ trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
}
else if ("ubuntu".equalsIgnoreCase(GDM)) {
// have to install the gnome extension AND customize the restart command
@@ -453,7 +443,7 @@ class SystemTray implements Menu {
GnomeShellExtension.install(output);
// we might be running gnome-shell, we MIGHT NOT. If we are forced to be app-indicator or swing, don't do this.
if (trayType == null) {
- trayType = _GtkStatusIconTray.class;
+ trayType = selectType(TrayType.GtkStatusIcon);
}
}
} catch (Throwable e) {
@@ -491,8 +481,8 @@ class SystemTray implements Menu {
if (readLine != null && readLine.contains("indicator-app")) {
// make sure we can also load the library (it might be the wrong version)
try {
- trayType = _AppIndicatorTray.class;
- } catch (Throwable e) {
+ trayType = selectType(TrayType.AppIndicator);
+ } catch (Exception e) {
if (DEBUG) {
logger.error("AppIndicator support detected, but unable to load the library. Falling back to GTK", e);
} else {
@@ -516,7 +506,7 @@ class SystemTray implements Menu {
// fallback...
if (trayType == null) {
- trayType = _GtkStatusIconTray.class;
+ trayType = selectTypeQuietly(TrayType.GtkStatusIcon);
logger.error("Unable to load the system tray native library. Please write an issue and include your OS type and " +
"configuration");
}
@@ -526,14 +516,19 @@ class SystemTray implements Menu {
// this has to happen BEFORE any sort of swing system tray stuff is accessed
if (OS.isWindows()) {
// windows is funky, and is hardcoded to 16x16. We fix that.
- WindowsSystemTraySwing.fix();
+ SystemTrayFixes.fixWindows();
+ }
+ else if (OS.isMacOsX()) {
+ // macos doesn't respond to all buttons (but should)
+ SystemTrayFixes.fixMacOS();
}
- // this is windows OR mac
- if (trayType == null && java.awt.SystemTray.isSupported()) {
+ ImageUtils.determineIconSize();
+
+ // this is likely windows OR mac
+ if (trayType == null) {
try {
- java.awt.SystemTray.getSystemTray();
- trayType = _SwingTray.class;
+ trayType = selectType(TrayType.Swing);
} catch (Throwable e) {
if (DEBUG) {
logger.error("Maybe you should grant the AWTPermission `accessSystemTray` in the SecurityManager.", e);
@@ -564,7 +559,7 @@ class SystemTray implements Menu {
AppIndicator.isVersion3) {
try {
- trayType = _GtkStatusIconTray.class;
+ trayType = selectType(TrayType.GtkStatusIcon);
logger.warn("AppIndicator3 detected with GTK2, falling back to GTK2 system tray type. " +
"Please install libappindicator1 OR GTK3, for example: 'sudo apt-get install libappindicator1'");
} catch (Throwable e) {
@@ -578,27 +573,52 @@ class SystemTray implements Menu {
}
}
- // have to construct swing stuff inside the swing EDT
- // this is the safest way to do this.
- final Class extends Menu> finalTrayType = trayType;
- SwingUtil.invokeAndWait(new Runnable() {
- @Override
- public
- void run() {
- try {
- reference.set((Menu) finalTrayType.getConstructors()[0].newInstance(systemTray));
- logger.info("Successfully Loaded: {}", finalTrayType.getSimpleName());
- } catch (Exception e) {
- logger.error("Unable to create tray type: '" + finalTrayType.getSimpleName() + "'", e);
- }
+
+ // if it's native + linux, have to do GTK instead. Don't need to be on the dispatch thread though.
+ // _AwtTray must be constructed on the EDT...
+ if (OS.isLinux() && NativeUI.class.isAssignableFrom(trayType) && trayType == _AwtTray.class) {
+ try {
+ reference.set((Menu) trayType.getConstructors()[0].newInstance(systemTray));
+ logger.info("Successfully Loaded: {}", trayType.getSimpleName());
+ } catch (Exception e) {
+ logger.error("Unable to create tray type: '" + trayType.getSimpleName() + "'", e);
}
- });
+ } else {
+ // have to construct swing stuff inside the swing EDT
+ // this is the safest way to do this.
+ final Class extends Menu> finalTrayType = trayType;
+ SwingUtil.invokeAndWait(new Runnable() {
+ @Override
+ public
+ void run() {
+ try {
+ reference.set((Menu) finalTrayType.getConstructors()[0].newInstance(systemTray));
+ logger.info("Successfully Loaded: {}", finalTrayType.getSimpleName());
+ } catch (Exception e) {
+ logger.error("Unable to create tray type: '" + finalTrayType.getSimpleName() + "'", e);
+ }
+ }
+ });
+ }
} catch (Exception e) {
logger.error("Unable to create tray type: '" + trayType.getSimpleName() + "'", e);
}
systemTrayMenu = reference.get();
+ // verify that we have what we are expecting.
+ if (OS.isWindows() && systemTrayMenu instanceof SwingUI) {
+ // this configuration is OK.
+ }
+ else if (forceNativeMenus && systemTrayMenu instanceof NativeUI) {
+ // this configuration is OK.
+ } else if (!forceNativeMenus && systemTrayMenu instanceof SwingUI) {
+ // this configuration is OK.
+ } else {
+ logger.error("Unable to correctly initialize the System Tray. Please write an issue and include your OS type and " +
+ "configuration");
+ }
+
// These install a shutdown hook in JavaFX/SWT, so that when the main window is closed -- the system tray is ALSO closed.
if (ENABLE_SHUTDOWN_HOOK) {
@@ -642,18 +662,41 @@ class SystemTray implements Menu {
}
/**
+ * Returns a SystemTray instance that uses a custom Swing menus, which is more advanced than the native menus. The drawback is that
+ * this menu is not native, and so loses the specific Look and Feel of that platform.
+ *
* 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.
- *
- *
If this is using the Swing SystemTray and a SecurityManager is installed, the AWTPermission {@code accessSystemTray} must
+ *
+ * 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 get() {
+ forceNativeMenus = true; // TODO set to false for final build
init();
return systemTray;
}
+ /**
+ * Enables native menus on Linux/OSX instead of the custom swing menu. Windows will always use a custom Swing menu.
+ *
+ * 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.
+ *
+ * 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 getNative() {
+ forceNativeMenus = true;
+ init();
+ return systemTray;
+ }
+
+ /**
+ * Shuts-down the SystemTray, by removing the menus + tray icon.
+ */
public
void shutdown() {
final Menu menu = systemTrayMenu;
@@ -661,10 +704,19 @@ class SystemTray implements Menu {
if (menu instanceof _AppIndicatorTray) {
((_AppIndicatorTray) menu).shutdown();
}
+ else if (menu instanceof _AppIndicatorNativeTray) {
+ ((_AppIndicatorNativeTray) menu).shutdown();
+ }
else if (menu instanceof _GtkStatusIconTray) {
((_GtkStatusIconTray) menu).shutdown();
- } else {
- // swing
+ }
+ else if (menu instanceof _GtkStatusIconNativeTray) {
+ ((_GtkStatusIconNativeTray) menu).shutdown();
+ }
+ else if (menu instanceof _AwtTray) {
+ ((_AwtTray) menu).shutdown();
+ }
+ else {
((_SwingTray) menu).shutdown();
}
}
@@ -675,13 +727,23 @@ class SystemTray implements Menu {
public
String getStatus() {
final Menu menu = systemTrayMenu;
+
if (menu instanceof _AppIndicatorTray) {
return ((_AppIndicatorTray) menu).getStatus();
}
+ else if (menu instanceof _AppIndicatorNativeTray) {
+ return ((_AppIndicatorNativeTray) menu).getStatus();
+ }
else if (menu instanceof _GtkStatusIconTray) {
return ((_GtkStatusIconTray) menu).getStatus();
- } else {
- // swing
+ }
+ else if (menu instanceof _GtkStatusIconNativeTray) {
+ return ((_GtkStatusIconNativeTray) menu).getStatus();
+ }
+ else if (menu instanceof _AwtTray) {
+ return ((_AwtTray) menu).getStatus();
+ }
+ else {
return ((_SwingTray) menu).getStatus();
}
}
@@ -698,10 +760,19 @@ class SystemTray implements Menu {
if (menu instanceof _AppIndicatorTray) {
((_AppIndicatorTray) menu).setStatus(statusText);
}
+ else if (menu instanceof _AppIndicatorNativeTray) {
+ ((_AppIndicatorNativeTray) menu).setStatus(statusText);
+ }
else if (menu instanceof _GtkStatusIconTray) {
((_GtkStatusIconTray) menu).setStatus(statusText);
- } else {
- // swing
+ }
+ else if (menu instanceof _GtkStatusIconNativeTray) {
+ ((_GtkStatusIconNativeTray) menu).setStatus(statusText);
+ }
+ else if (menu instanceof _AwtTray) {
+ ((_AwtTray) menu).setStatus(statusText);
+ }
+ else {
((_SwingTray) menu).setStatus(statusText);
}
}
@@ -1063,6 +1134,7 @@ class SystemTray implements Menu {
+
/**
* This removes a menu entry from the dropdown menu.
*
diff --git a/src/dorkbox/systemTray/linux/jna/AppIndicator.java b/src/dorkbox/systemTray/linux/jna/AppIndicator.java
index 2652d3f6..5a6554f1 100644
--- a/src/dorkbox/systemTray/linux/jna/AppIndicator.java
+++ b/src/dorkbox/systemTray/linux/jna/AppIndicator.java
@@ -50,7 +50,7 @@ class AppIndicator {
// ALSO WHAT VERSION OF GTK to use? appindiactor1 -> GTk2, appindicator3 -> GTK3.
// appindicator3 doesn't support menu icons via GTK2!!
- if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_GTK_STATUSICON) {
+ if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TrayType.GtkStatusIcon) {
// if we force GTK type system tray, don't attempt to load AppIndicator libs
if (SystemTray.DEBUG) {
logger.debug("Forcing GTK tray, not using appindicator");
diff --git a/src/dorkbox/systemTray/linux/jna/GCallback.java b/src/dorkbox/systemTray/linux/jna/GCallback.java
new file mode 100644
index 00000000..a4d2df67
--- /dev/null
+++ b/src/dorkbox/systemTray/linux/jna/GCallback.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2015 dorkbox, llc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dorkbox.systemTray.linux.jna;
+
+import com.sun.jna.Callback;
+import com.sun.jna.Pointer;
+import dorkbox.util.Keep;
+
+@Keep
+public
+interface GCallback extends Callback {
+ /**
+ * @return Gtk.TRUE if we handled this event
+ */
+ int callback(Pointer instance, Pointer data);
+}
diff --git a/src/dorkbox/systemTray/linux/jna/Gobject.java b/src/dorkbox/systemTray/linux/jna/Gobject.java
index 1179eca0..84b70738 100644
--- a/src/dorkbox/systemTray/linux/jna/Gobject.java
+++ b/src/dorkbox/systemTray/linux/jna/Gobject.java
@@ -37,5 +37,8 @@ class Gobject {
public static native void g_object_unref(Pointer object);
+ public static native void g_object_force_floating(Pointer object);
+ public static native void g_object_ref_sink(Pointer object);
+
public static native NativeLong g_signal_connect_object(Pointer instance, String detailed_signal, Callback c_handler, Pointer object, int connect_flags);
}
diff --git a/src/dorkbox/systemTray/linux/jna/Gtk.java b/src/dorkbox/systemTray/linux/jna/Gtk.java
index ae65e1bc..1f624a80 100644
--- a/src/dorkbox/systemTray/linux/jna/Gtk.java
+++ b/src/dorkbox/systemTray/linux/jna/Gtk.java
@@ -24,6 +24,9 @@ import java.util.concurrent.TimeUnit;
import com.sun.jna.Function;
import com.sun.jna.Pointer;
+import dorkbox.systemTray.Action;
+import dorkbox.systemTray.Entry;
+import dorkbox.systemTray.Menu;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.util.JavaFX;
import dorkbox.systemTray.util.Swt;
@@ -71,7 +74,7 @@ class Gtk {
String gtk3LibName = "libgtk-3.so.0";
// we can force the system to use the swing indicator, which WORKS, but doesn't support transparency in the icon.
- if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_SWING) {
+ if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TrayType.Swing) {
isLoaded = true;
}
@@ -247,7 +250,7 @@ class Gtk {
}
}
} else if (SystemTray.isSwtLoaded) {
- if (SystemTray.FORCE_TRAY_TYPE != SystemTray.TYPE_GTK_STATUSICON) {
+ if (SystemTray.FORCE_TRAY_TYPE != SystemTray.TrayType.GtkStatusIcon) {
// GTK system tray has threading issues if we block here (because it is likely in the event thread)
// AppIndicator version doesn't have this problem
@@ -375,6 +378,23 @@ class Gtk {
});
}
+ /**
+ * required to properly setup the dispatch flag
+ * @param callback will never be null.
+ */
+ public static
+ void proxyClick(final Menu parent, final Entry menuEntry, final Action callback) {
+ Gtk.isDispatch = true;
+
+ try {
+ callback.onClick(parent.getSystemTray(), parent, menuEntry);
+ } catch (Throwable throwable) {
+ SystemTray.logger.error("Error calling menu entry {} click event.", menuEntry.getText(), throwable);
+ }
+
+ Gtk.isDispatch = false;
+ }
+
/**
* This would NORMALLY have a 2nd argument that is a String[] -- however JNA direct-mapping DOES NOT support this. We are lucky
* enough that we just pass 'null' as the second argument, therefore, we don't have to define that parameter here.
@@ -400,10 +420,22 @@ class Gtk {
public static native Pointer gtk_menu_new();
+ public static native Pointer gtk_menu_item_set_submenu(Pointer menuEntry, Pointer menu);
+
+
+
+ public static native Pointer gtk_separator_menu_item_new();
+
+ // to create a menu entry WITH an icon.
+ public static native Pointer gtk_image_new_from_file(String iconPath);
// uses '_' to define which key is the mnemonic
public static native Pointer gtk_image_menu_item_new_with_mnemonic(String label);
+ public static native void gtk_image_menu_item_set_image(Pointer image_menu_item, Pointer image);
+
+ public static native void gtk_image_menu_item_set_always_show_image(Pointer menu_item, int forceShow);
+
public static native Pointer gtk_status_icon_new();
public static native void gtk_status_icon_set_from_file(Pointer widget, String label);
@@ -417,8 +449,18 @@ class Gtk {
public static native void gtk_status_icon_set_name(Pointer widget, String name);
+ public static native void gtk_menu_popup(Pointer menu, Pointer widget, Pointer bla, Function func, Pointer data, int button, int time);
+
+ public static native void gtk_menu_item_set_label(Pointer menu_item, String label);
+
public static native void gtk_menu_shell_append(Pointer menu_shell, Pointer child);
+ public static native void gtk_menu_shell_deactivate(Pointer menu_shell, Pointer child);
+
+ public static native void gtk_widget_set_sensitive(Pointer widget, int sensitive);
+
+ public static native void gtk_container_remove(Pointer menu, Pointer subItem);
+
public static native void gtk_widget_show_all(Pointer widget);
public static native void gtk_widget_destroy(Pointer widget);
diff --git a/src/dorkbox/systemTray/nativeUI/AwtEntry.java b/src/dorkbox/systemTray/nativeUI/AwtEntry.java
new file mode 100644
index 00000000..b46b2406
--- /dev/null
+++ b/src/dorkbox/systemTray/nativeUI/AwtEntry.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2014 dorkbox, llc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dorkbox.systemTray.nativeUI;
+
+import java.awt.MenuItem;
+import java.awt.MenuShortcut;
+import java.awt.PopupMenu;
+import java.io.File;
+import java.io.InputStream;
+import java.net.URL;
+
+import dorkbox.systemTray.Entry;
+import dorkbox.systemTray.Menu;
+import dorkbox.systemTray.swingUI.SwingUI;
+import dorkbox.systemTray.util.ImageUtils;
+import dorkbox.systemTray.util.MenuBase;
+import dorkbox.systemTray.util.SystemTrayFixes;
+
+abstract
+class AwtEntry implements Entry, SwingUI {
+ private final int id = MenuBase.MENU_ID_COUNTER.getAndIncrement();
+
+ private final AwtMenu parent;
+ final MenuItem _native;
+
+ // this have to be volatile, because they can be changed from any thread
+ private volatile String text;
+
+ // this is ALWAYS called on the EDT.
+ AwtEntry(final AwtMenu parent, final MenuItem menuItem) {
+ this.parent = parent;
+ this._native = menuItem;
+
+ parent._native.add(menuItem);
+ }
+
+ @Override
+ public
+ Menu getParent() {
+ return parent;
+ }
+
+ /**
+ * must always be called in the EDT thread
+ */
+ abstract
+ void renderText(final String text);
+
+ /**
+ * Not always called on the EDT thread
+ */
+ abstract
+ void setImage_(final File imageFile);
+
+ /**
+ * Enables, or disables the sub-menu entry.
+ */
+ @Override
+ public
+ void setEnabled(final boolean enabled) {
+ _native.setEnabled(enabled);
+ }
+
+ @Override
+ public
+ void setShortcut(final char key) {
+ if (!(_native instanceof PopupMenu)) {
+ // yikes...
+ final int vKey = SystemTrayFixes.getVirtualKey(key);
+
+ parent.dispatch(new Runnable() {
+ @Override
+ public
+ void run() {
+ _native.setShortcut(new MenuShortcut(vKey));
+ }
+ });
+ }
+ }
+
+ @Override
+ public
+ String getText() {
+ return text;
+ }
+
+ @Override
+ public
+ void setText(final String newText) {
+ this.text = newText;
+
+ parent.dispatch(new Runnable() {
+ @Override
+ public
+ void run() {
+ renderText(newText);
+ }
+ });
+ }
+
+ @Override
+ public
+ void setImage(final File imageFile) {
+ if (imageFile == null) {
+ setImage_(null);
+ }
+ else {
+ setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageFile));
+ }
+ }
+
+ @Override
+ public final
+ void setImage(final String imagePath) {
+ if (imagePath == null) {
+ setImage_(null);
+ }
+ else {
+ setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imagePath));
+ }
+ }
+
+ @Override
+ public final
+ void setImage(final URL imageUrl) {
+ if (imageUrl == null) {
+ setImage_(null);
+ }
+ else {
+ setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageUrl));
+ }
+ }
+
+ @Override
+ public final
+ void setImage(final String cacheName, final InputStream imageStream) {
+ if (imageStream == null) {
+ setImage_(null);
+ }
+ else {
+ setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, cacheName, imageStream));
+ }
+ }
+
+ @Override
+ public final
+ void setImage(final InputStream imageStream) {
+ if (imageStream == null) {
+ setImage_(null);
+ }
+ else {
+ setImage_(ImageUtils.resizeAndCache(ImageUtils.ENTRY_SIZE, imageStream));
+ }
+ }
+
+ @Override
+ public final
+ void remove() {
+ parent.dispatchAndWait(new Runnable() {
+ @Override
+ public
+ void run() {
+ removePrivate();
+ parent._native.remove(_native);
+ }
+ });
+ }
+
+ // called when this item is removed. Necessary to cleanup/remove itself
+ abstract
+ void removePrivate();
+
+ @Override
+ public final
+ int hashCode() {
+ return id;
+ }
+
+
+ @Override
+ public final
+ boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ AwtEntry other = (AwtEntry) obj;
+ return this.id == other.id;
+ }
+}
diff --git a/src/dorkbox/systemTray/nativeUI/AwtEntryItem.java b/src/dorkbox/systemTray/nativeUI/AwtEntryItem.java
new file mode 100644
index 00000000..15fad7f5
--- /dev/null
+++ b/src/dorkbox/systemTray/nativeUI/AwtEntryItem.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2014 dorkbox, llc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dorkbox.systemTray.nativeUI;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.File;
+
+import dorkbox.systemTray.Action;
+
+class AwtEntryItem extends AwtEntry {
+
+ private final ActionListener swingCallback;
+
+ private volatile Action callback;
+
+ // this is ALWAYS called on the EDT.
+ AwtEntryItem(final AwtMenu parent, final Action callback) {
+ super(parent, new java.awt.MenuItem());
+ this.callback = callback;
+
+
+ if (callback != null) {
+ _native.setEnabled(true);
+ swingCallback = new ActionListener() {
+ @Override
+ public
+ void actionPerformed(ActionEvent e) {
+ // we want it to run on the EDT
+ handle();
+ }
+ };
+
+ _native.addActionListener(swingCallback);
+ } else {
+ _native.setEnabled(false);
+ swingCallback = null;
+ }
+ }
+
+ @Override
+ public
+ void setCallback(final Action callback) {
+ this.callback = callback;
+ }
+
+ private
+ void handle() {
+ if (callback != null) {
+ callback.onClick(getParent().getSystemTray(), getParent(), this);
+ }
+ }
+
+ // always called in the EDT
+ @Override
+ void renderText(final String text) {
+ _native.setLabel(text);
+ }
+
+
+ // not supported!
+ @Override
+ public
+ boolean hasImage() {
+ return false;
+ }
+
+ // not supported!
+ @Override
+ void setImage_(final File imageFile) {
+ }
+
+ @Override
+ void removePrivate() {
+ _native.removeActionListener(swingCallback);
+ }
+}
diff --git a/src/dorkbox/systemTray/nativeUI/AwtEntrySeparator.java b/src/dorkbox/systemTray/nativeUI/AwtEntrySeparator.java
new file mode 100644
index 00000000..b6e7f880
--- /dev/null
+++ b/src/dorkbox/systemTray/nativeUI/AwtEntrySeparator.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2014 dorkbox, llc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dorkbox.systemTray.nativeUI;
+
+import java.awt.MenuItem;
+import java.io.File;
+
+import dorkbox.systemTray.Action;
+
+class AwtEntrySeparator extends AwtEntry implements dorkbox.systemTray.Separator {
+
+ // this is ALWAYS called on the EDT.
+ AwtEntrySeparator(final AwtMenu parent) {
+ super(parent, new MenuItem("-"));
+ }
+
+ // called in the EDT thread
+ @Override
+ void renderText(final String text) {
+ }
+
+ @Override
+ void setImage_(final File imageFile) {
+ }
+
+ @Override
+ void removePrivate() {
+ }
+
+ @Override
+ public
+ void setShortcut(final char key) {
+ }
+
+ @Override
+ public
+ boolean hasImage() {
+ return false;
+ }
+
+ @Override
+ public
+ void setCallback(final Action callback) {
+ }
+}
diff --git a/src/dorkbox/systemTray/nativeUI/AwtEntryStatus.java b/src/dorkbox/systemTray/nativeUI/AwtEntryStatus.java
new file mode 100644
index 00000000..5df5fa76
--- /dev/null
+++ b/src/dorkbox/systemTray/nativeUI/AwtEntryStatus.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2014 dorkbox, llc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dorkbox.systemTray.nativeUI;
+
+import static java.awt.Font.DIALOG;
+
+import java.awt.Font;
+import java.awt.MenuItem;
+import java.io.File;
+
+import dorkbox.systemTray.Action;
+import dorkbox.systemTray.Status;
+
+class AwtEntryStatus extends AwtEntry implements Status {
+
+ // this is ALWAYS called on the EDT.
+ AwtEntryStatus(final AwtMenu parent, final String label) {
+ super(parent, new MenuItem());
+ setText(label);
+ }
+
+ // called in the EDT thread
+ @Override
+ void renderText(final String text) {
+ Font font = _native.getFont();
+ if (font == null) {
+ font = new Font(DIALOG, Font.BOLD, 12); // the default font used for dialogs.
+ } else {
+ font = font.deriveFont(Font.BOLD);
+ }
+
+ _native.setFont(font);
+ _native.setLabel(text);
+
+ // this makes sure it can't be selected
+ _native.setEnabled(false);
+ }
+
+ @Override
+ void setImage_(final File imageFile) {
+ }
+
+ @Override
+ void removePrivate() {
+ }
+
+ @Override
+ public
+ void setShortcut(final char key) {
+ }
+
+ @Override
+ public
+ boolean hasImage() {
+ return false;
+ }
+
+ @Override
+ public
+ void setCallback(final Action callback) {
+ }
+}
diff --git a/src/dorkbox/systemTray/nativeUI/AwtMenu.java b/src/dorkbox/systemTray/nativeUI/AwtMenu.java
new file mode 100644
index 00000000..27f7874c
--- /dev/null
+++ b/src/dorkbox/systemTray/nativeUI/AwtMenu.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright 2014 dorkbox, llc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package dorkbox.systemTray.nativeUI;
+
+
+import java.awt.MenuShortcut;
+import java.awt.PopupMenu;
+import java.io.File;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dorkbox.systemTray.Action;
+import dorkbox.systemTray.Entry;
+import dorkbox.systemTray.Menu;
+import dorkbox.systemTray.Status;
+import dorkbox.systemTray.SystemTray;
+import dorkbox.systemTray.util.MenuBase;
+import dorkbox.systemTray.util.SystemTrayFixes;
+import dorkbox.util.SwingUtil;
+
+// this is a weird composite class, because it must be a Menu, but ALSO a Entry -- so it has both
+@SuppressWarnings("ForLoopReplaceableByForEach")
+class AwtMenu extends MenuBase implements NativeUI {
+
+ // sub-menu = java.awt.Menu
+ // systemtray = java.awt.PopupMenu
+ volatile java.awt.Menu _native;
+
+ // this have to be volatile, because they can be changed from any thread
+ private volatile String text;
+
+ /**
+ * Called in the EDT
+ *
+ * @param systemTray the system tray (which is the object that sits in the system tray)
+ * @param parent the parent of this menu, null if the parent is the system tray
+ * @param _native the native element that represents this menu
+ */
+ AwtMenu(final SystemTray systemTray, final Menu parent, final java.awt.Menu _native) {
+ super(systemTray, parent);
+ this._native = _native;
+ }
+
+ @Override
+ protected final
+ void dispatch(final Runnable runnable) {
+ // this will properly check if we are running on the EDT
+ SwingUtil.invokeLater(runnable);
+ }
+
+ @Override
+ protected final
+ void dispatchAndWait(final Runnable runnable) {
+ // this will properly check if we are running on the EDT
+ try {
+ SwingUtil.invokeAndWait(runnable);
+ } catch (Exception e) {
+ SystemTray.logger.error("Error processing event on the dispatch thread.", e);
+ }
+ }
+
+ // always called in the EDT
+ protected final
+ void renderText(final String text) {
+ _native.setLabel(text);
+ }
+
+ @Override
+ public final
+ String getText() {
+ return text;
+ }
+
+ @Override
+ public final
+ void setText(final String newText) {
+ text = newText;
+ dispatch(new Runnable() {
+ @Override
+ public
+ void run() {
+ renderText(newText);
+ }
+ });
+ }
+
+ /**
+ * Will add a new menu entry, or update one if it already exists
+ * NOT ALWAYS CALLED ON EDT
+ */
+ protected final
+ Entry addEntry_(final String menuText, final File imagePath, final Action callback) {
+ if (menuText == null) {
+ throw new NullPointerException("Menu text cannot be null");
+ }
+
+ final AtomicReference value = new AtomicReference();
+
+ dispatchAndWait(new Runnable() {
+ @Override
+ public
+ void run() {
+ synchronized (menuEntries) {
+ Entry entry = get(menuText);
+
+ if (entry == null) {
+ // must always be called on the EDT
+ entry = new AwtEntryItem(AwtMenu.this, callback);
+ entry.setText(menuText);
+ entry.setImage(imagePath);
+
+ menuEntries.add(entry);
+ } else if (entry instanceof AwtEntryItem) {
+ entry.setText(menuText);
+ entry.setImage(imagePath);
+ }
+
+ value.set(entry);
+ }
+ }
+ });
+
+ return value.get();
+ }
+
+ /**
+ * Will add a new sub-menu entry, or update one if it already exists
+ * NOT ALWAYS CALLED ON EDT
+ */
+ protected final
+ Menu addMenu_(final String menuText, final File imagePath) {
+ if (menuText == null) {
+ throw new NullPointerException("Menu text cannot be null");
+ }
+
+ final AtomicReference