diff --git a/src/dorkbox/systemTray/Menu.java b/src/dorkbox/systemTray/Menu.java index 143d365..9bfba2c 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 8160768..d9cc66e 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 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 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 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 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 2652d3f..5a6554f 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 0000000..a4d2df6 --- /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 1179eca..84b7073 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 ae65e1b..1f624a8 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 0000000..b46b240 --- /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 0000000..15fad7f --- /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 0000000..b6e7f88 --- /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 0000000..5df5fa7 --- /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 0000000..27f7874 --- /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

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 AwtMenu(getSystemTray(), AwtMenu.this, new java.awt.Menu()); + _native.add(((AwtMenu) entry)._native); // have to add it separately + + entry.setText(menuText); + entry.setImage(imagePath); + value.set((Menu) entry); + + } else if (entry instanceof AwtMenu) { + entry.setText(menuText); + entry.setImage(imagePath); + } + + menuEntries.add(entry); + } + } + }); + + return value.get(); + } + + + + // public here so that Swing/Gtk/AppIndicator can override this + public + void setImage_(final File imageFile) { + // not supported! + } + + // not supported! + @Override + public + boolean hasImage() { + return false; + } + + // public here so that Swing/Gtk/AppIndicator can override this + @Override + public + void setEnabled(final boolean enabled) { + dispatch(new Runnable() { + @Override + public + void run() { + _native.setEnabled(enabled); + } + }); + } + + + /** + * NOT ALWAYS CALLED ON EDT + */ + @Override + public final + void addSeparator() { + dispatch(new Runnable() { + @Override + public + void run() { + synchronized (menuEntries) { + synchronized (menuEntries) { + Entry entry = new AwtEntrySeparator(AwtMenu.this); + menuEntries.add(entry); + } + } + } + }); + } + +// TODO: buggy. The menu will **sometimes** stop responding to the "enter" key after this. Mnemonics still work however. +// public +// Entry addWidget(final JComponent widget) { +// if (widget == null) { +// throw new NullPointerException("Widget cannot be null"); +// } +// +// final AtomicReference value = new AtomicReference(); +// +// dispatchAndWait(new Runnable() { +// @Override +// public +// void run() { +// synchronized (menuEntries) { +// // must always be called on the EDT +// Entry entry = new SwingEntryWidget(SwingMenu.this, widget); +// value.set(entry); +// menuEntries.add(entry); +// } +// } +// }); +// +// return value.get(); +// } + + + // public here so that Swing/Gtk/AppIndicator can access this + public final + void setStatus(final String statusText) { + final AwtMenu _this = this; + dispatchAndWait(new Runnable() { + @Override + public + void run() { + synchronized (menuEntries) { + // status is ALWAYS at 0 index... + AwtEntry menuEntry = null; + if (!menuEntries.isEmpty()) { + menuEntry = (AwtEntry) menuEntries.get(0); + } + + if (menuEntry instanceof Status) { + // set the text or delete... + + if (statusText == null) { + // delete + remove(menuEntry); + } + else { + // set text + menuEntry.setText(statusText); + } + + } else { + // create a new one + menuEntry = new AwtEntryStatus(_this, statusText); + // status is ALWAYS at 0 index... + menuEntries.add(0, menuEntry); + } + } + } + }); + } + + @Override + public final + void setShortcut(final char key) { + if (!(_native instanceof PopupMenu)) { + // yikes... + final int vKey = SystemTrayFixes.getVirtualKey(key); + + dispatch(new Runnable() { + @Override + public + void run() { + _native.setShortcut(new MenuShortcut(vKey)); + } + }); + } + } + + @Override + public final + void remove() { + dispatchAndWait(new Runnable() { + @Override + public + void run() { + AwtMenu parent = (AwtMenu) getParent(); + if (parent != null) { + parent._native.remove(_native); + } + } + }); + } +} diff --git a/src/dorkbox/systemTray/nativeUI/GtkEntry.java b/src/dorkbox/systemTray/nativeUI/GtkEntry.java new file mode 100644 index 0000000..a73c2ec --- /dev/null +++ b/src/dorkbox/systemTray/nativeUI/GtkEntry.java @@ -0,0 +1,218 @@ +/* + * 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.io.File; +import java.io.InputStream; +import java.net.URL; + +import com.sun.jna.Pointer; + +import dorkbox.systemTray.Entry; +import dorkbox.systemTray.Menu; +import dorkbox.systemTray.linux.jna.Gtk; +import dorkbox.systemTray.util.ImageUtils; + +abstract +class GtkEntry implements Entry { + private final int id = GtkMenu.MENU_ID_COUNTER.getAndIncrement(); + + private final GtkMenu parent; + final Pointer _native; + + // this have to be volatile, because they can be changed from any thread + private volatile String text; + + /** + * called from inside dispatch thread. ONLY creates the menu item, but DOES NOT attach it! + * this is a FLOATING reference. See: https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#floating-ref + */ + GtkEntry(final GtkMenu parent, final Pointer menuItem) { + this.parent = parent; + this._native = menuItem; + } + + public + Menu getParent() { + return parent; + } + + /** + * the menu entry looks FUNKY when there are a mis-match of entries WITH and WITHOUT images + * + * always called on the DISPATCH thread + */ + abstract + void setSpacerImage(final boolean everyoneElseHasImages); + + /** + * must always be called in the GTK thread + */ + abstract + void renderText(final String text); + + /** + * must always be called in the GTK thread + */ + abstract + void setImage_(final File imageFile); + + /** + * must always be called in the GTK thread + * called when this item is removed. Necessary to cleanup/remove itself + */ + abstract + void removePrivate(); + + /** + * Enables, or disables the sub-menu entry. + */ + @Override + public + void setEnabled(final boolean enabled) { + if (enabled) { + Gtk.gtk_widget_set_sensitive(_native, Gtk.TRUE); + } else { + Gtk.gtk_widget_set_sensitive(_native, Gtk.FALSE); + } + } + + @Override + public + void setShortcut(final char key) { + } + + @Override + public + String getText() { + return text; + } + + @Override + public final + void setText(final String newText) { + text = newText; + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + renderText(text); + } + }); + } + + @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)); + } + } + + // a child will always remove itself from the parent. + @Override + public final + void remove() { + parent.dispatchAndWait(new Runnable() { + @Override + public + void run() { + Gtk.gtk_container_remove(parent._native, _native); + Gtk.gtk_menu_shell_deactivate(parent._native, _native); + + removePrivate(); + + Gtk.gtk_widget_destroy(_native); + + // have to rebuild the menu now... + parent.deleteMenu(); + parent.createMenu(); + } + }); + } + + @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; + } + + GtkEntry other = (GtkEntry) obj; + return this.id == other.id; + } +} diff --git a/src/dorkbox/systemTray/nativeUI/GtkEntryItem.java b/src/dorkbox/systemTray/nativeUI/GtkEntryItem.java new file mode 100644 index 0000000..93a18e1 --- /dev/null +++ b/src/dorkbox/systemTray/nativeUI/GtkEntryItem.java @@ -0,0 +1,192 @@ +/* + * 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.io.File; + +import com.sun.jna.NativeLong; +import com.sun.jna.Pointer; + +import dorkbox.systemTray.Action; +import dorkbox.systemTray.linux.jna.GCallback; +import dorkbox.systemTray.linux.jna.Gobject; +import dorkbox.systemTray.linux.jna.Gtk; +import dorkbox.systemTray.util.ImageUtils; + +class GtkEntryItem extends GtkEntry implements GCallback { + private static File transparentIcon = null; + + @SuppressWarnings({"FieldCanBeLocal", "unused"}) + private final NativeLong nativeLong; + + // these have to be volatile, because they can be changed from any thread + private volatile Action callback; + private volatile Pointer image; + + // these are necessary BECAUSE GTK menus look funky as hell when there are some menu entries WITH icons and some WITHOUT + protected volatile boolean hasLegitIcon = true; + + // The mnemonic will ONLY show-up once a menu entry is selected. IT WILL NOT show up before then! + // AppIndicators will only show if you use the keyboard to navigate + // GtkStatusIconTray will show on mouse+keyboard movement + private volatile char mnemonicKey = 0; + + /** + * called from inside dispatch thread. ONLY creates the menu item, but DOES NOT attach it! + * this is a FLOATING reference. See: https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#floating-ref + */ + GtkEntryItem(final GtkMenu parent, final Action callback) { + super(parent, Gtk.gtk_image_menu_item_new_with_mnemonic("")); + this.callback = callback; + + // cannot be done in a static initializer, because the tray icon size might not yet have been determined + if (transparentIcon == null) { + transparentIcon = ImageUtils.getTransparentImage(ImageUtils.ENTRY_SIZE); + } + + if (callback != null) { + Gtk.gtk_widget_set_sensitive(_native, Gtk.TRUE); + nativeLong = Gobject.g_signal_connect_object(_native, "activate", this, null, 0); + } + else { + Gtk.gtk_widget_set_sensitive(_native, Gtk.FALSE); + nativeLong = null; + } + } + + @Override + public + void setShortcut(final char key) { + this.mnemonicKey = Character.toLowerCase(key); + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + renderText(getText()); + } + }); + } + + @Override + public + void setCallback(final Action callback) { + this.callback = callback; + } + + // called by native code + @Override + public + int callback(final Pointer instance, final Pointer data) { + final Action cb = this.callback; + if (cb != null) { + Gtk.proxyClick(getParent(), GtkEntryItem.this, cb); + } + + return Gtk.TRUE; + } + + @Override + public + boolean hasImage() { + return hasLegitIcon; + } + + /** + * the menu entry looks FUNKY when there are a mis-match of entries WITH and WITHOUT images. + * This is primarily only with AppIndicators, although not always. + *

+ * called on the DISPATCH thread + */ + void setSpacerImage(final boolean everyoneElseHasImages) { + if (hasLegitIcon) { + // we have a legit icon, so there is nothing else we can do. + return; + } + + if (image != null) { + Gtk.gtk_widget_destroy(image); + image = null; + Gtk.gtk_widget_show_all(_native); + } + + if (everyoneElseHasImages) { + image = Gtk.gtk_image_new_from_file(transparentIcon.getAbsolutePath()); + Gtk.gtk_image_menu_item_set_image(_native, image); + + // must always re-set always-show after setting the image + Gtk.gtk_image_menu_item_set_always_show_image(_native, Gtk.TRUE); + } + + Gtk.gtk_widget_show_all(_native); + } + + /** + * must always be called in the GTK thread + */ + void renderText(String text) { + if (this.mnemonicKey != 0) { + // they are CASE INSENSITIVE! + int i = text.toLowerCase() + .indexOf(this.mnemonicKey); + + if (i >= 0) { + text = text.substring(0, i) + "_" + text.substring(i); + } + } + + Gtk.gtk_menu_item_set_label(_native, text); + Gtk.gtk_widget_show_all(_native); + } + + // NOTE: XFCE used to use appindicator3, which DOES NOT support images in the menu. This change was reverted. + // see: https://ask.fedoraproject.org/en/question/23116/how-to-fix-missing-icons-in-program-menus-and-context-menus/ + // see: https://git.gnome.org/browse/gtk+/commit/?id=627a03683f5f41efbfc86cc0f10e1b7c11e9bb25 + void setImage_(final File imageFile) { + hasLegitIcon = imageFile != null; + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + if (image != null) { + Gtk.gtk_widget_destroy(image); + image = null; + Gtk.gtk_widget_show_all(_native); + } + + if (imageFile != null) { + image = Gtk.gtk_image_new_from_file(imageFile.getAbsolutePath()); + Gtk.gtk_image_menu_item_set_image(_native, image); + + // must always re-set always-show after setting the image + Gtk.gtk_image_menu_item_set_always_show_image(_native, Gtk.TRUE); + } + + Gtk.gtk_widget_show_all(_native); + } + }); + } + + void removePrivate() { + callback = null; + + if (image != null) { + Gtk.gtk_widget_destroy(image); + image = null; + } + } +} diff --git a/src/dorkbox/systemTray/nativeUI/GtkEntrySeparator.java b/src/dorkbox/systemTray/nativeUI/GtkEntrySeparator.java new file mode 100644 index 0000000..5f33e7b --- /dev/null +++ b/src/dorkbox/systemTray/nativeUI/GtkEntrySeparator.java @@ -0,0 +1,66 @@ +/* + * 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.io.File; + +import dorkbox.systemTray.Action; +import dorkbox.systemTray.Separator; +import dorkbox.systemTray.linux.jna.Gtk; + +class GtkEntrySeparator extends GtkEntry implements Separator { + + /** + * called from inside dispatch thread. ONLY creates the menu item, but DOES NOT attach it! + * this is a FLOATING reference. See: https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#floating-ref + */ + GtkEntrySeparator(final GtkMenu parent) { + super(parent, Gtk.gtk_separator_menu_item_new()); + } + + @Override + void setSpacerImage(final boolean everyoneElseHasImages) { + } + + // called in the GTK thread + @Override + void renderText(final String text) { + } + + @Override + void setImage_(final File imageFile) { + } + + @Override + void removePrivate() { + } + + @Override + public + boolean hasImage() { + return false; + } + + @Override + public + void setCallback(final Action callback) { + } + + @Override + public + void setEnabled(final boolean enabled) { + } +} diff --git a/src/dorkbox/systemTray/nativeUI/GtkEntryStatus.java b/src/dorkbox/systemTray/nativeUI/GtkEntryStatus.java new file mode 100644 index 0000000..ee135c5 --- /dev/null +++ b/src/dorkbox/systemTray/nativeUI/GtkEntryStatus.java @@ -0,0 +1,57 @@ +/* + * 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 dorkbox.systemTray.Action; +import dorkbox.systemTray.linux.jna.Gtk; + +// you might wonder WHY this extends MenuEntryItem -- the reason is that an AppIndicator "status" will be offset from everyone else, +// where a GtkStatusIconTray + SwingUI will have everything lined up. (with or without icons). This is to normalize how it looks +class GtkEntryStatus extends GtkEntryItem { + + /** + * called from inside dispatch thread. ONLY creates the menu item, but DOES NOT attach it! + * this is a FLOATING reference. See: https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#floating-ref + */ + GtkEntryStatus(final GtkMenu parent, final String text) { + super(parent, null); + // need that extra space so it matches windows/mac + hasLegitIcon = false; + setText(text); + } + + // called in the GTK thread + @Override + void renderText(final String text) { + // AppIndicator strips out markup text. + // https://mail.gnome.org/archives/commits-list/2016-March/msg05444.html + + Gtk.gtk_menu_item_set_label(_native, text); + Gtk.gtk_widget_show_all(_native); + + Gtk.gtk_widget_set_sensitive(_native, Gtk.FALSE); + } + + @Override + public + void setCallback(final Action callback) { + } + + @Override + public + void setEnabled(final boolean enabled) { + } +} diff --git a/src/dorkbox/systemTray/nativeUI/GtkMenu.java b/src/dorkbox/systemTray/nativeUI/GtkMenu.java new file mode 100644 index 0000000..4d6e898 --- /dev/null +++ b/src/dorkbox/systemTray/nativeUI/GtkMenu.java @@ -0,0 +1,500 @@ +/* + * 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.io.File; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import com.sun.jna.Pointer; + +import dorkbox.systemTray.Action; +import dorkbox.systemTray.Entry; +import dorkbox.systemTray.Menu; +import dorkbox.systemTray.SystemTray; +import dorkbox.systemTray.linux.jna.Gobject; +import dorkbox.systemTray.linux.jna.Gtk; +import dorkbox.systemTray.util.MenuBase; + +class GtkMenu extends MenuBase implements NativeUI { + static int TIMEOUT = 2; + + // menu entry that this menu is attached to. Will be NULL when it's the system tray + private final GtkEntryItem menuEntry; + + // must ONLY be created at the end of delete! + volatile Pointer _native; + + // have to make sure no other methods can call obliterate, delete, or create menu once it's already started + private boolean obliterateInProgress = false; + + // called on dispatch + GtkMenu(final SystemTray systemTray, final GtkMenu parent) { + super(systemTray, parent); + + if (parent != null) { + this.menuEntry = new GtkEntryItem(parent, null); + // by default, no callback on a menu entry means it's DISABLED. we have to undo that, because we don't have a callback for menus + menuEntry.setEnabled(true); + } else { + this.menuEntry = null; + } + } + + /** + * Called inside the gdk_threads block + */ + protected + void onMenuAdded(final Pointer menu) { + // only needed for AppIndicator + } + + /** + * Necessary to guarantee all updates occur on the dispatch thread + */ + protected + void dispatch(final Runnable runnable) { + Gtk.dispatch(runnable); + } + + /** + * Necessary to guarantee all updates occur on the dispatch thread + */ + protected + void dispatchAndWait(final Runnable runnable) { + final CountDownLatch countDownLatch = new CountDownLatch(1); + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + try { + runnable.run(); + } finally { + countDownLatch.countDown(); + } + } + }); + + // this is slightly different than how swing does it. We have a timeout here so that we can make sure that updates on the GUI + // thread occur in REASONABLE time-frames, and alert the user if not. + try { + if (!countDownLatch.await(TIMEOUT, TimeUnit.SECONDS)) { + if (SystemTray.DEBUG) { + SystemTray.logger.error("Event dispatch queue took longer than " + TIMEOUT + " seconds to complete. Please adjust " + + "`SystemTray.TIMEOUT` to a value which better suites your environment."); + } else { + throw new RuntimeException("Event dispatch queue took longer than " + TIMEOUT + " seconds to complete. Please adjust " + + "`SystemTray.TIMEOUT` to a value which better suites your environment."); + } + } + } catch (InterruptedException e) { + SystemTray.logger.error("Error waiting for dispatch to complete.", new Exception()); + } + } + + public + void shutdown() { + dispatchAndWait(new Runnable() { + @Override + public + void run() { + obliterateMenu(); + + Gtk.shutdownGui(); + } + }); + } + + // public here so that Swing/Gtk/AppIndicator can access this + public final + void setStatus(final String statusText) { + dispatch(new Runnable() { + @Override + public + void run() { + // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. + // To work around this issue, we destroy then recreate the menu every time something is changed. + synchronized (menuEntries) { + // status is ALWAYS at 0 index... + GtkEntry menuEntry = null; + if (!menuEntries.isEmpty()) { + menuEntry = (GtkEntry) menuEntries.get(0); + } + + if (menuEntry instanceof GtkEntryStatus) { + // always delete... + remove(menuEntry); + } + + // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. + // To work around this issue, we destroy then recreate the menu every time something is changed. + deleteMenu(); + + if (menuEntry == null) { + menuEntry = new GtkEntryStatus(GtkMenu.this, statusText); + // status is ALWAYS at 0 index... + menuEntries.add(0, menuEntry); + } + else if (menuEntry instanceof GtkEntryStatus) { + // change the text? + if (statusText != null) { + menuEntry = new GtkEntryStatus(GtkMenu.this, statusText); + menuEntries.add(0, menuEntry); + } + } + + createMenu(); + } + } + }); + } + + + + + + + + + // public here so that Swing/Gtk/AppIndicator can override this + @Override + public + boolean hasImage() { + return menuEntry.hasImage(); + } + + + // public here so that Swing/Gtk/AppIndicator can override this + @Override + protected + void setImage_(final File imageFile) { + menuEntry.setImage_(imageFile); + } + + // public here so that Swing/Gtk/AppIndicator can override this + @Override + public + void setEnabled(final boolean enabled) { + if (enabled) { + Gtk.gtk_widget_set_sensitive(menuEntry._native, Gtk.TRUE); + } else { + Gtk.gtk_widget_set_sensitive(menuEntry._native, Gtk.FALSE); + } + } + + @Override + public + String getText() { + return menuEntry.getText(); + } + + @Override + public + void setText(final String newText) { + menuEntry.setText(newText); + } + + @Override + public final + void addSeparator() { + dispatch(new Runnable() { + @Override + public + void run() { + // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. + // To work around this issue, we destroy then recreate the menu every time something is changed. + synchronized (menuEntries) { + // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. + // To work around this issue, we destroy then recreate the menu every time something is changed. + deleteMenu(); + + GtkEntry menuEntry = new GtkEntrySeparator(GtkMenu.this); + menuEntries.add(menuEntry); + + createMenu(); + } + } + }); + } + + @Override + public final + void setShortcut(final char key) { + menuEntry.setShortcut(key); + } + + // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. + // To work around this issue, we destroy then recreate the menu every time something is changed. + /** + * Deletes the menu, and unreferences everything in it. ALSO recreates ONLY the menu object. + */ + void deleteMenu() { + if (obliterateInProgress) { + return; + } + + if (_native != null) { + // have to remove all other menu entries + synchronized (menuEntries) { + for (int i = 0, menuEntriesSize = menuEntries.size(); i < menuEntriesSize; i++) { + final Entry menuEntry__ = menuEntries.get(i); + if (menuEntry__ instanceof GtkEntry) { + GtkEntry entry = (GtkEntry) menuEntry__; + + Gobject.g_object_force_floating(entry._native); + Gtk.gtk_container_remove(_native, entry._native); + } + else if (menuEntry__ instanceof GtkMenu) { + GtkMenu subMenu = (GtkMenu) menuEntry__; + + Gobject.g_object_force_floating(subMenu.menuEntry._native); + Gtk.gtk_container_remove(_native, subMenu.menuEntry._native); + } + } + + Gtk.gtk_widget_destroy(_native); + } + } + + if (getParent() != null) { + ((GtkMenu) getParent()).deleteMenu(); + } + + // makes a new one + _native = Gtk.gtk_menu_new(); + + // binds sub-menu to entry (if it exists! it does not for the root menu) + if (menuEntry != null) { + Gtk.gtk_menu_item_set_submenu(menuEntry._native, _native); + } + } + + // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. + // To work around this issue, we destroy then recreate the menu every time something is changed. + void createMenu() { + if (obliterateInProgress) { + return; + } + + if (getParent() != null) { + ((GtkMenu) getParent()).createMenu(); + } + + boolean hasImages = false; + + // now add back other menu entries + synchronized (menuEntries) { + for (int i = 0, menuEntriesSize = menuEntries.size(); i < menuEntriesSize; i++) { + final Entry menuEntry__ = menuEntries.get(i); + hasImages |= menuEntry__.hasImage(); + } + + for (int i = 0, menuEntriesSize = menuEntries.size(); i < menuEntriesSize; i++) { + final Entry menuEntry__ = menuEntries.get(i); + // the menu entry looks FUNKY when there are a mis-match of entries WITH and WITHOUT images + if (menuEntry__ instanceof GtkEntry) { + GtkEntry entry = (GtkEntry) menuEntry__; + entry.setSpacerImage(hasImages); + + // will also get: gsignal.c:2516: signal 'child-added' is invalid for instance '0x7f1df8244080' of type 'GtkMenu' + Gtk.gtk_menu_shell_append(this._native, entry._native); + Gobject.g_object_ref_sink(entry._native); // undoes "floating" + } + else if (menuEntry__ instanceof GtkMenu) { + GtkMenu subMenu = (GtkMenu) menuEntry__; + + // will also get: gsignal.c:2516: signal 'child-added' is invalid for instance '0x7f1df8244080' of type 'GtkMenu' + Gtk.gtk_menu_shell_append(this._native, subMenu.menuEntry._native); + Gobject.g_object_ref_sink(subMenu.menuEntry._native); // undoes "floating" + + if (subMenu.getParent() != GtkMenu.this) { + // we don't want to "createMenu" on our sub-menu that is assigned to us directly, as they are already doing it + subMenu.createMenu(); + } + } + } + + onMenuAdded(_native); + Gtk.gtk_widget_show_all(_native); + } + } + + /** + * must be called on the dispatch thread + * + * Completely obliterates the menu, no possible way to reconstruct it. + */ + private + void obliterateMenu() { + if (_native != null && !obliterateInProgress) { + obliterateInProgress = true; + + // have to remove all other menu entries + synchronized (menuEntries) { + // a copy is made because sub-menus remove themselves from parents when .remove() is called. If we don't + // do this, errors will be had because indices don't line up anymore. + ArrayList menuEntriesCopy = new ArrayList(this.menuEntries); + + for (int i = 0, menuEntriesSize = menuEntriesCopy.size(); i < menuEntriesSize; i++) { + final Entry menuEntry__ = menuEntriesCopy.get(i); + menuEntry__.remove(); + } + this.menuEntries.clear(); + menuEntriesCopy.clear(); + + Gtk.gtk_widget_destroy(_native); + } + + obliterateInProgress = false; + } + } + + + /** + * Will add a new menu entry, or update one if it already exists + */ + protected + Entry addEntry_(final String menuText, final File imagePath, final Action callback) { + // some implementations of appindicator, do NOT like having a menu added, which has no menu items yet. + // see: https://bugs.launchpad.net/glipper/+bug/1203888 + + if (menuText == null) { + throw new NullPointerException("Menu text cannot be null"); + } + + // have to wait for the value + final AtomicReference value = new AtomicReference(); + + dispatchAndWait(new Runnable() { + @Override + public + void run() { + synchronized (menuEntries) { + Entry menuEntry = get(menuText); + if (menuEntry == null) { + // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. + // To work around this issue, we destroy then recreate the menu every time something is changed. + deleteMenu(); + + menuEntry = new GtkEntryItem(GtkMenu.this, callback); + menuEntry.setText(menuText); + menuEntry.setImage(imagePath); + menuEntries.add(menuEntry); + + createMenu(); + } else if (menuEntry instanceof GtkEntryItem) { + menuEntry.setText(menuText); + menuEntry.setImage(imagePath); + } + + value.set(menuEntry); + } + } + }); + + return value.get(); + } + + /** + * Will add a new menu entry, or update one if it already exists + */ + protected + Menu addMenu_(final String menuText, final File imagePath) { + // some implementations of appindicator, do NOT like having a menu added, which has no menu items yet. + // see: https://bugs.launchpad.net/glipper/+bug/1203888 + + 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 menuEntry = get(menuText); + if (menuEntry == null) { + // some GTK libraries DO NOT let us add items AFTER the menu has been attached to the indicator. + // To work around this issue, we destroy then recreate the menu every time something is changed. + deleteMenu(); + + GtkMenu subMenu = new GtkMenu(getSystemTray(), GtkMenu.this); + subMenu.setText(menuText); + subMenu.setImage(imagePath); + + menuEntries.add(subMenu); + + value.set(subMenu); + + createMenu(); + } else if (menuEntry instanceof GtkMenu) { + menuEntry.setText(menuText); + menuEntry.setImage(imagePath); + + value.set(((GtkMenu) menuEntry)); + } + } + } + }); + + return value.get(); + } + + // a child will always remove itself from the parent. + @Override + public + void remove() { + dispatchAndWait(new Runnable() { + @Override + public + void run() { + GtkMenu parent = (GtkMenu) getParent(); + + // have to remove from the parent.menuEntries first + for (Iterator iterator = parent.menuEntries.iterator(); iterator.hasNext(); ) { + final Entry entry = iterator.next(); + if (entry == GtkMenu.this) { + iterator.remove(); + break; + } + } + + // cleans up the menu +// parent.remove__(null); + + // delete all of the children of this submenu (must happen before the menuEntry is removed) + obliterateMenu(); + + // remove the gtk entry item from our parent menu NATIVE components + // NOTE: this will rebuild the parent menu + if (menuEntry != null) { + menuEntry.remove(); + } else { + // have to rebuild the menu now... + parent.deleteMenu(); + parent.createMenu(); + } + } + }); + } +} diff --git a/src/dorkbox/systemTray/nativeUI/NativeUI.java b/src/dorkbox/systemTray/nativeUI/NativeUI.java new file mode 100644 index 0000000..7b5b0af --- /dev/null +++ b/src/dorkbox/systemTray/nativeUI/NativeUI.java @@ -0,0 +1,30 @@ +/* + * Copyright 2016 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; + +/** + * Represents a System Tray or menu, that will have it's menu rendered via the native subsystem. + *

+ * This is does not have as many features as the swing-based UI, however the trade off is that this will always have the native L&F of + * the system (with the exception of Windows, whose native menu looks absolutely terrible). + *

+ * Noticeable differences that are limitations for the NativeUI only: + * - AppIndicator Status entries must be plain text (they are not bold as they are everywhere else). + * - MacOS cannot have images in their menu or sub-menu's -- only plain text is possible + */ +public +interface NativeUI +{} diff --git a/src/dorkbox/systemTray/nativeUI/_AppIndicatorNativeTray.java b/src/dorkbox/systemTray/nativeUI/_AppIndicatorNativeTray.java new file mode 100644 index 0000000..8a18b49 --- /dev/null +++ b/src/dorkbox/systemTray/nativeUI/_AppIndicatorNativeTray.java @@ -0,0 +1,185 @@ +/* + * 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.io.File; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.sun.jna.Pointer; + +import dorkbox.systemTray.SystemTray; +import dorkbox.systemTray.linux.jna.AppIndicator; +import dorkbox.systemTray.linux.jna.AppIndicatorInstanceStruct; +import dorkbox.systemTray.linux.jna.Gobject; +import dorkbox.systemTray.linux.jna.Gtk; +import dorkbox.systemTray.util.ImageUtils; + +/** + * Class for handling all system tray interactions. + * specialization for using app indicators in ubuntu unity + * + * Derived from + * Lantern: https://github.com/getlantern/lantern/ Apache 2.0 License Copyright 2010 Brave New Software Project, Inc. + * + * AppIndicators DO NOT support anything other than plain gtk-menus, because of how they use dbus so no tooltips AND no custom widgets + * + * + * + * As a result of this decision by Canonical, we have to resort to hacks to get it to do what we want. BY NO MEANS IS THIS PERFECT. + * + * + * We still cannot have tooltips, but we *CAN* have custom widgets in the menu (because it's our swing menu now...) + * + * + * It would be too much work to re-implement AppIndicators, or even to use LD_PRELOAD + restart service to do what we want. + * + * As a result, we have some wicked little hacks which are rather effective (but have a small side-effect of very briefly + * showing a blank menu) + * + * // What are AppIndicators? + * http://unity.ubuntu.com/projects/appindicators/ + * + * + * // Entry-point into appindicators + * http://bazaar.launchpad.net/~unity-team/unity/trunk/view/head:/services/panel-main.c + * + * + * // The idiocy of appindicators + * https://bugs.launchpad.net/screenlets/+bug/522152 + * + * // Code of how the dbus menus work + * http://bazaar.launchpad.net/~dbusmenu-team/libdbusmenu/trunk.16.10/view/head:/libdbusmenu-gtk/client.c + * https://developer.ubuntu.com/api/devel/ubuntu-12.04/c/dbusmenugtk/index.html + * + * // more info about trying to put widgets into GTK menus + * http://askubuntu.com/questions/16431/putting-an-arbitrary-gtk-widget-into-an-appindicator-indicator + * + * // possible idea on how to get GTK widgets into GTK menus + * https://launchpad.net/ido + * http://bazaar.launchpad.net/~canonical-dx-team/ido/trunk/view/head:/src/idoentrymenuitem.c + * http://bazaar.launchpad.net/~ubuntu-desktop/ido/gtk3/files + */ +@SuppressWarnings("Duplicates") +public +class _AppIndicatorNativeTray extends GtkMenu { + private volatile AppIndicatorInstanceStruct appIndicator; + private boolean isActive = false; + + // This is required if we have JavaFX or SWT shutdown hooks (to prevent us from shutting down twice...) + private AtomicBoolean shuttingDown = new AtomicBoolean(); + + // is the system tray visible or not. + private volatile boolean visible = true; + + // appindicators DO NOT support anything other than PLAIN gtk-menus (which we hack to support swing menus) + // they ALSO do not support tooltips, so we cater to the lowest common denominator + // trayIcon.setToolTip("app name"); + + public + _AppIndicatorNativeTray(final SystemTray systemTray) { + super(systemTray, null); + + Gtk.startGui(); + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + // we initialize with a blank image + File image = ImageUtils.getTransparentImage(ImageUtils.ENTRY_SIZE); + String id = System.nanoTime() + "DBST"; + appIndicator = AppIndicator.app_indicator_new(id, image.getAbsolutePath(), AppIndicator.CATEGORY_APPLICATION_STATUS); + } + }); + + Gtk.waitForStartup(); + } + + public final + void shutdown() { + if (!shuttingDown.getAndSet(true)) { + // must happen asap, so our hook properly notices we are in shutdown mode + final AppIndicatorInstanceStruct savedAppIndicator = appIndicator; + appIndicator = null; + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + // STATUS_PASSIVE hides the indicator + AppIndicator.app_indicator_set_status(savedAppIndicator, AppIndicator.STATUS_PASSIVE); + Pointer p = savedAppIndicator.getPointer(); + Gobject.g_object_unref(p); + } + }); + + super.shutdown(); + } + } + + @Override + public final + boolean hasImage() { + return true; + } + + @Override + public final + void setImage_(final File imageFile) { + dispatch(new Runnable() { + @Override + public + void run() { + AppIndicator.app_indicator_set_icon(appIndicator, imageFile.getAbsolutePath()); + + if (!isActive) { + isActive = true; + + AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_ACTIVE); + } + } + }); + } + + @Override + public final + void setEnabled(final boolean setEnabled) { + visible = !setEnabled; + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + if (visible && !setEnabled) { + // STATUS_PASSIVE hides the indicator + AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_PASSIVE); + } + else if (!visible && setEnabled) { + AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_ACTIVE); + } + } + }); + } + + /** + * MUST BE AFTER THE ITEM IS ADDED/CHANGED from the menu + */ + protected final + void onMenuAdded(final Pointer menu) { + // see: https://code.launchpad.net/~mterry/libappindicator/fix-menu-leak/+merge/53247 + AppIndicator.app_indicator_set_menu(appIndicator, menu); + } +} diff --git a/src/dorkbox/systemTray/nativeUI/_AwtTray.java b/src/dorkbox/systemTray/nativeUI/_AwtTray.java new file mode 100644 index 0000000..6ec2f36 --- /dev/null +++ b/src/dorkbox/systemTray/nativeUI/_AwtTray.java @@ -0,0 +1,125 @@ +/* + * 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.AWTException; +import java.awt.Image; +import java.awt.PopupMenu; +import java.awt.SystemTray; +import java.awt.TrayIcon; +import java.io.File; + +import javax.swing.ImageIcon; + +/** + * Class for handling all system tray interaction, via AWT. + * + * It doesn't work well on linux. See bugs: + * http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6267936 + * http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6453521 + * https://stackoverflow.com/questions/331407/java-trayicon-using-image-with-transparent-background/3882028#3882028 + */ +@SuppressWarnings({"SynchronizationOnLocalVariableOrMethodParameter", "WeakerAccess"}) +public +class _AwtTray extends AwtMenu { + private volatile SystemTray tray; + private volatile TrayIcon trayIcon; + + // is the system tray visible or not. + private volatile boolean visible = true; + + // Called in the EDT + public + _AwtTray(final dorkbox.systemTray.SystemTray systemTray) { + super(systemTray, null, new PopupMenu()); + + if (!SystemTray.isSupported()) { + throw new RuntimeException("System Tray is not supported in this configuration! Please write an issue and include your OS " + + "type and configuration"); + } + + _AwtTray.this.tray = SystemTray.getSystemTray(); + } + + public + void shutdown() { + dispatch(new Runnable() { + @Override + public + void run() { + removeAll(); + remove(); + + tray.remove(trayIcon); + } + }); + } + + public + void setImage_(final File iconFile) { + dispatch(new Runnable() { + @Override + public + void run() { + // stupid java won't scale it right away, so we have to do this twice to get the correct size + final Image trayImage = new ImageIcon(iconFile.getAbsolutePath()).getImage(); + trayImage.flush(); + + if (trayIcon == null) { + // here we init. everything + trayIcon = new TrayIcon(trayImage); + + // appindicators DO NOT support anything other than PLAIN gtk-menus (which we hack to support swing menus) + // they ALSO do not support tooltips, so we cater to the lowest common denominator + // trayIcon.setToolTip("app name"); + + trayIcon.setPopupMenu((PopupMenu) _native); + + try { + tray.add(trayIcon); + } catch (AWTException e) { + dorkbox.systemTray.SystemTray.logger.error("TrayIcon could not be added.", e); + } + } else { + trayIcon.setImage(trayImage); + } + } + }); + } + + @SuppressWarnings("Duplicates") + public + void setEnabled(final boolean setEnabled) { + visible = !setEnabled; + + dispatch(new Runnable() { + @Override + public + void run() { + if (visible && !setEnabled) { + tray.remove(trayIcon); + } + else if (!visible && setEnabled) { + try { + tray.add(trayIcon); + } catch (AWTException e) { + dorkbox.systemTray.SystemTray.logger.error("Error adding the icon back to the tray"); + } + } + } + }); + } +} diff --git a/src/dorkbox/systemTray/nativeUI/_GtkStatusIconNativeTray.java b/src/dorkbox/systemTray/nativeUI/_GtkStatusIconNativeTray.java new file mode 100644 index 0000000..1fc00c9 --- /dev/null +++ b/src/dorkbox/systemTray/nativeUI/_GtkStatusIconNativeTray.java @@ -0,0 +1,186 @@ +/* + * 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.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.sun.jna.NativeLong; +import com.sun.jna.Pointer; + +import dorkbox.systemTray.SystemTray; +import dorkbox.systemTray.linux.jna.GEventCallback; +import dorkbox.systemTray.linux.jna.GdkEventButton; +import dorkbox.systemTray.linux.jna.Gobject; +import dorkbox.systemTray.linux.jna.Gtk; + +/** + * Class for handling all system tray interactions via GTK. + *

+ * This is the "old" way to do it, and does not work with some newer desktop environments. + */ +@SuppressWarnings("Duplicates") +public +class _GtkStatusIconNativeTray extends GtkMenu { + private volatile Pointer trayIcon; + + // http://code.metager.de/source/xref/gnome/Platform/gtk%2B/gtk/deprecated/gtkstatusicon.c + // https://github.com/djdeath/glib/blob/master/gobject/gobject.c + + // have to save these in a field to prevent GC on the objects (since they go out-of-scope from java) + private final List gtkCallbacks = new ArrayList(); + + // This is required if we have JavaFX or SWT shutdown hooks (to prevent us from shutting down twice...) + private AtomicBoolean shuttingDown = new AtomicBoolean(); + + private volatile boolean isActive = false; + + // is the system tray visible or not. + private volatile boolean visible = true; + + // called on the EDT + public + _GtkStatusIconNativeTray(final SystemTray systemTray) { + super(systemTray, null); + + // appindicators DO NOT support anything other than PLAIN gtk-menus (which we hack to support swing menus) + // they ALSO do not support tooltips, so we cater to the lowest common denominator + // trayIcon.setToolTip("app name"); + + Gtk.startGui(); + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + trayIcon = Gtk.gtk_status_icon_new(); + + final GEventCallback gtkCallback = new GEventCallback() { + @Override + public + void callback(Pointer notUsed, final GdkEventButton event) { + // show the swing menu on the EDT + // BUTTON_PRESS only (any mouse click) + if (event.type == 4) { + Gtk.gtk_menu_popup(_native, null, null, Gtk.gtk_status_icon_position_menu, trayIcon, 0, event.time); + } + } + }; + final NativeLong button_press_event = Gobject.g_signal_connect_object(trayIcon, "button_press_event", + gtkCallback, null, 0); + + // have to do this to prevent GC on these objects + gtkCallbacks.add(gtkCallback); + gtkCallbacks.add(button_press_event); + } + }); + + Gtk.waitForStartup(); + + // we have to be able to set our title, otherwise the gnome-shell extension WILL NOT work + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + // by default, the title/name of the tray icon is "java". We are the only java-based tray icon, so we just use that. + // If you change "SystemTray" to something else, make sure to change it in extension.js as well + + // necessary for gnome icon detection/placement because we move tray icons around by title. This is hardcoded + // in extension.js, so don't change it + Gtk.gtk_status_icon_set_title(trayIcon, "SystemTray"); + + // can cause + // Gdk-CRITICAL **: gdk_window_thaw_toplevel_updates: assertion 'window->update_and_descendants_freeze_count > 0' failed + // Gdk-CRITICAL **: IA__gdk_window_thaw_toplevel_updates_libgtk_only: assertion 'private->update_and_descendants_freeze_count > 0' failed + + // ... so, bizzaro things going on here. These errors DO NOT happen if JavaFX is dispatching the events. + // BUT this is REQUIRED when running JavaFX. For unknown reasons, the title isn't pushed to GTK, so our + // gnome-shell extension cannot see our tray icon -- so naturally, it won't move it to the "top" area and + // we appear broken. + if (SystemTray.isJavaFxLoaded) { + Gtk.gtk_status_icon_set_name(trayIcon, "SystemTray"); + } + } + }); + } + + + @SuppressWarnings("FieldRepeatedlyAccessedInMethod") + public final + void shutdown() { + if (!shuttingDown.getAndSet(true)) { + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + // this hides the indicator + Gtk.gtk_status_icon_set_visible(trayIcon, false); + Gobject.g_object_unref(trayIcon); + + // mark for GC + trayIcon = null; + gtkCallbacks.clear(); + } + }); + + super.shutdown(); + } + } + + @Override + public final + boolean hasImage() { + return true; + } + + @Override + public final + void setImage_(final File iconFile) { + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + Gtk.gtk_status_icon_set_from_file(trayIcon, iconFile.getAbsolutePath()); + + if (!isActive) { + isActive = true; + Gtk.gtk_status_icon_set_visible(trayIcon, true); + } + } + }); + } + + @Override + public final + void setEnabled(final boolean setEnabled) { + visible = !setEnabled; + + Gtk.dispatch(new Runnable() { + @Override + public + void run() { + if (visible && !setEnabled) { + Gtk.gtk_status_icon_set_visible(trayIcon, setEnabled); + } else if (!visible && setEnabled) { + Gtk.gtk_status_icon_set_visible(trayIcon, setEnabled); + } + } + }); + } +} diff --git a/src/dorkbox/systemTray/swing/EntryImpl.java b/src/dorkbox/systemTray/swing/EntryImpl.java deleted file mode 100644 index 027dc9a..0000000 --- a/src/dorkbox/systemTray/swing/EntryImpl.java +++ /dev/null @@ -1,317 +0,0 @@ -/* - * 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.swing; - -import java.awt.event.KeyEvent; -import java.io.File; -import java.io.InputStream; -import java.net.URL; - -import javax.swing.JComponent; -import javax.swing.JMenuItem; - -import dorkbox.systemTray.Entry; -import dorkbox.systemTray.Menu; -import dorkbox.systemTray.util.ImageUtils; - -abstract -class EntryImpl implements Entry { - private final int id = MenuImpl.MENU_ID_COUNTER.getAndIncrement(); - - private final MenuImpl parent; - final JComponent _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. - EntryImpl(final MenuImpl parent, final JComponent 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 JMenuItem) { - // yikes... - final int vKey = getVkKey(key); - - parent.dispatch(new Runnable() { - @Override - public - void run() { - ((JMenuItem) _native).setMnemonic(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; - } - - EntryImpl other = (EntryImpl) obj; - return this.id == other.id; - } - - - /** - * Converts a key character into it's corresponding VK entry - */ - static - int getVkKey(final char key) { - switch (key) { - case 0x08: return KeyEvent.VK_BACK_SPACE; - case 0x09: return KeyEvent.VK_TAB; - case 0x0a: return KeyEvent.VK_ENTER; - case 0x1B: return KeyEvent.VK_ESCAPE; - case 0x20AC: return KeyEvent.VK_EURO_SIGN; - case 0x20: return KeyEvent.VK_SPACE; - case 0x21: return KeyEvent.VK_EXCLAMATION_MARK; - case 0x22: return KeyEvent.VK_QUOTEDBL; - case 0x23: return KeyEvent.VK_NUMBER_SIGN; - case 0x24: return KeyEvent.VK_DOLLAR; - case 0x26: return KeyEvent.VK_AMPERSAND; - case 0x27: return KeyEvent.VK_QUOTE; - case 0x28: return KeyEvent.VK_LEFT_PARENTHESIS; - case 0x29: return KeyEvent.VK_RIGHT_PARENTHESIS; - case 0x2A: return KeyEvent.VK_ASTERISK; - case 0x2B: return KeyEvent.VK_PLUS; - case 0x2C: return KeyEvent.VK_COMMA; - case 0x2D: return KeyEvent.VK_MINUS; - case 0x2E: return KeyEvent.VK_PERIOD; - case 0x2F: return KeyEvent.VK_SLASH; - case 0x30: return KeyEvent.VK_0; - case 0x31: return KeyEvent.VK_1; - case 0x32: return KeyEvent.VK_2; - case 0x33: return KeyEvent.VK_3; - case 0x34: return KeyEvent.VK_4; - case 0x35: return KeyEvent.VK_5; - case 0x36: return KeyEvent.VK_6; - case 0x37: return KeyEvent.VK_7; - case 0x38: return KeyEvent.VK_8; - case 0x39: return KeyEvent.VK_9; - case 0x3A: return KeyEvent.VK_COLON; - case 0x3B: return KeyEvent.VK_SEMICOLON; - case 0x3C: return KeyEvent.VK_LESS; - case 0x3D: return KeyEvent.VK_EQUALS; - case 0x3E: return KeyEvent.VK_GREATER; - case 0x40: return KeyEvent.VK_AT; - case 0x41: return KeyEvent.VK_A; - case 0x42: return KeyEvent.VK_B; - case 0x43: return KeyEvent.VK_C; - case 0x44: return KeyEvent.VK_D; - case 0x45: return KeyEvent.VK_E; - case 0x46: return KeyEvent.VK_F; - case 0x47: return KeyEvent.VK_G; - case 0x48: return KeyEvent.VK_H; - case 0x49: return KeyEvent.VK_I; - case 0x4A: return KeyEvent.VK_J; - case 0x4B: return KeyEvent.VK_K; - case 0x4C: return KeyEvent.VK_L; - case 0x4D: return KeyEvent.VK_M; - case 0x4E: return KeyEvent.VK_N; - case 0x4F: return KeyEvent.VK_O; - case 0x50: return KeyEvent.VK_P; - case 0x51: return KeyEvent.VK_Q; - case 0x52: return KeyEvent.VK_R; - case 0x53: return KeyEvent.VK_S; - case 0x54: return KeyEvent.VK_T; - case 0x55: return KeyEvent.VK_U; - case 0x56: return KeyEvent.VK_V; - case 0x57: return KeyEvent.VK_W; - case 0x58: return KeyEvent.VK_X; - case 0x59: return KeyEvent.VK_Y; - case 0x5A: return KeyEvent.VK_Z; - case 0x5B: return KeyEvent.VK_OPEN_BRACKET; - case 0x5C: return KeyEvent.VK_BACK_SLASH; - case 0x5D: return KeyEvent.VK_CLOSE_BRACKET; - case 0x5E: return KeyEvent.VK_CIRCUMFLEX; - case 0x5F: return KeyEvent.VK_UNDERSCORE; - case 0x60: return KeyEvent.VK_BACK_QUOTE; - case 0x61: return KeyEvent.VK_A; - case 0x62: return KeyEvent.VK_B; - case 0x63: return KeyEvent.VK_C; - case 0x64: return KeyEvent.VK_D; - case 0x65: return KeyEvent.VK_E; - case 0x66: return KeyEvent.VK_F; - case 0x67: return KeyEvent.VK_G; - case 0x68: return KeyEvent.VK_H; - case 0x69: return KeyEvent.VK_I; - case 0x6A: return KeyEvent.VK_J; - case 0x6B: return KeyEvent.VK_K; - case 0x6C: return KeyEvent.VK_L; - case 0x6D: return KeyEvent.VK_M; - case 0x6E: return KeyEvent.VK_N; - case 0x6F: return KeyEvent.VK_O; - case 0x70: return KeyEvent.VK_P; - case 0x71: return KeyEvent.VK_Q; - case 0x72: return KeyEvent.VK_R; - case 0x73: return KeyEvent.VK_S; - case 0x74: return KeyEvent.VK_T; - case 0x75: return KeyEvent.VK_U; - case 0x76: return KeyEvent.VK_V; - case 0x77: return KeyEvent.VK_W; - case 0x78: return KeyEvent.VK_X; - case 0x79: return KeyEvent.VK_Y; - case 0x7A: return KeyEvent.VK_Z; - case 0x7B: return KeyEvent.VK_BRACELEFT; - case 0x7D: return KeyEvent.VK_BRACERIGHT; - case 0x7F: return KeyEvent.VK_DELETE; - case 0xA1: return KeyEvent.VK_INVERTED_EXCLAMATION_MARK; - } - - return 0; - } -} diff --git a/src/dorkbox/systemTray/swing/AdjustedJMenu.java b/src/dorkbox/systemTray/swingUI/AdjustedJMenu.java similarity index 97% rename from src/dorkbox/systemTray/swing/AdjustedJMenu.java rename to src/dorkbox/systemTray/swingUI/AdjustedJMenu.java index d4b2e1e..355f988 100644 --- a/src/dorkbox/systemTray/swing/AdjustedJMenu.java +++ b/src/dorkbox/systemTray/swingUI/AdjustedJMenu.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dorkbox.systemTray.swing; +package dorkbox.systemTray.swingUI; import java.awt.Insets; diff --git a/src/dorkbox/systemTray/swing/AdjustedJMenuItem.java b/src/dorkbox/systemTray/swingUI/AdjustedJMenuItem.java similarity index 96% rename from src/dorkbox/systemTray/swing/AdjustedJMenuItem.java rename to src/dorkbox/systemTray/swingUI/AdjustedJMenuItem.java index 2d41694..4adb7d4 100644 --- a/src/dorkbox/systemTray/swing/AdjustedJMenuItem.java +++ b/src/dorkbox/systemTray/swingUI/AdjustedJMenuItem.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dorkbox.systemTray.swing; +package dorkbox.systemTray.swingUI; import java.awt.Insets; diff --git a/src/dorkbox/systemTray/swingUI/SwingEntry.java b/src/dorkbox/systemTray/swingUI/SwingEntry.java new file mode 100644 index 0000000..495d582 --- /dev/null +++ b/src/dorkbox/systemTray/swingUI/SwingEntry.java @@ -0,0 +1,208 @@ +/* + * 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.swingUI; + +import java.io.File; +import java.io.InputStream; +import java.net.URL; + +import javax.swing.JComponent; +import javax.swing.JMenuItem; + +import dorkbox.systemTray.Entry; +import dorkbox.systemTray.Menu; +import dorkbox.systemTray.util.ImageUtils; +import dorkbox.systemTray.util.MenuBase; +import dorkbox.systemTray.util.SystemTrayFixes; + +abstract +class SwingEntry implements Entry, SwingUI { + private final int id = MenuBase.MENU_ID_COUNTER.getAndIncrement(); + + private final SwingMenu parent; + final JComponent _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. + SwingEntry(final SwingMenu parent, final JComponent 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 JMenuItem) { + // yikes... + final int vKey = SystemTrayFixes.getVirtualKey(key); + + parent.dispatch(new Runnable() { + @Override + public + void run() { + ((JMenuItem) _native).setMnemonic(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; + } + + SwingEntry other = (SwingEntry) obj; + return this.id == other.id; + } +} diff --git a/src/dorkbox/systemTray/swing/EntryItem.java b/src/dorkbox/systemTray/swingUI/SwingEntryItem.java similarity index 95% rename from src/dorkbox/systemTray/swing/EntryItem.java rename to src/dorkbox/systemTray/swingUI/SwingEntryItem.java index 9ffb51f..85ba747 100644 --- a/src/dorkbox/systemTray/swing/EntryItem.java +++ b/src/dorkbox/systemTray/swingUI/SwingEntryItem.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dorkbox.systemTray.swing; +package dorkbox.systemTray.swingUI; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -25,7 +25,7 @@ import javax.swing.JMenuItem; import dorkbox.systemTray.Action; import dorkbox.util.SwingUtil; -class EntryItem extends EntryImpl { +class SwingEntryItem extends SwingEntry { private final ActionListener swingCallback; @@ -33,7 +33,7 @@ class EntryItem extends EntryImpl { private volatile Action callback; // this is ALWAYS called on the EDT. - EntryItem(final MenuImpl parent, final Action callback) { + SwingEntryItem(final SwingMenu parent, final Action callback) { super(parent, new AdjustedJMenuItem()); this.callback = callback; diff --git a/src/dorkbox/systemTray/swing/EntrySeparator.java b/src/dorkbox/systemTray/swingUI/SwingEntrySeparator.java similarity index 87% rename from src/dorkbox/systemTray/swing/EntrySeparator.java rename to src/dorkbox/systemTray/swingUI/SwingEntrySeparator.java index 508b39b..b1d74b2 100644 --- a/src/dorkbox/systemTray/swing/EntrySeparator.java +++ b/src/dorkbox/systemTray/swingUI/SwingEntrySeparator.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dorkbox.systemTray.swing; +package dorkbox.systemTray.swingUI; import java.io.File; @@ -21,10 +21,10 @@ import javax.swing.JSeparator; import dorkbox.systemTray.Action; -class EntrySeparator extends EntryImpl implements dorkbox.systemTray.Separator { +class SwingEntrySeparator extends SwingEntry implements dorkbox.systemTray.Separator { // this is ALWAYS called on the EDT. - EntrySeparator(final MenuImpl parent) { + SwingEntrySeparator(final SwingMenu parent) { super(parent, new JSeparator(JSeparator.HORIZONTAL)); } diff --git a/src/dorkbox/systemTray/swing/EntryStatus.java b/src/dorkbox/systemTray/swingUI/SwingEntryStatus.java similarity index 90% rename from src/dorkbox/systemTray/swing/EntryStatus.java rename to src/dorkbox/systemTray/swingUI/SwingEntryStatus.java index 94ae2a5..7eab9f2 100644 --- a/src/dorkbox/systemTray/swing/EntryStatus.java +++ b/src/dorkbox/systemTray/swingUI/SwingEntryStatus.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dorkbox.systemTray.swing; +package dorkbox.systemTray.swingUI; import java.awt.Font; import java.io.File; @@ -23,10 +23,10 @@ import javax.swing.JMenuItem; import dorkbox.systemTray.Action; import dorkbox.systemTray.Status; -class EntryStatus extends EntryImpl implements Status { +class SwingEntryStatus extends SwingEntry implements Status { // this is ALWAYS called on the EDT. - EntryStatus(final MenuImpl parent, final String label) { + SwingEntryStatus(final SwingMenu parent, final String label) { super(parent, new JMenuItem()); setText(label); } @@ -34,11 +34,12 @@ class EntryStatus extends EntryImpl implements Status { // called in the EDT thread @Override void renderText(final String text) { - ((JMenuItem) _native).setText(text); Font font = _native.getFont(); Font font1 = font.deriveFont(Font.BOLD); _native.setFont(font1); + ((JMenuItem) _native).setText(text); + // this makes sure it can't be selected _native.setEnabled(false); } diff --git a/src/dorkbox/systemTray/swing/EntryWidget.java b/src/dorkbox/systemTray/swingUI/SwingEntryWidget.java similarity index 87% rename from src/dorkbox/systemTray/swing/EntryWidget.java rename to src/dorkbox/systemTray/swingUI/SwingEntryWidget.java index d9399ff..6093863 100644 --- a/src/dorkbox/systemTray/swing/EntryWidget.java +++ b/src/dorkbox/systemTray/swingUI/SwingEntryWidget.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dorkbox.systemTray.swing; +package dorkbox.systemTray.swingUI; import java.io.File; @@ -22,10 +22,10 @@ import javax.swing.JComponent; import dorkbox.systemTray.Action; // TODO: buggy. The menu will **sometimes** stop responding to the "enter" key after this. Mnemonics still work however. -class EntryWidget extends EntryImpl implements dorkbox.systemTray.Separator { +class SwingEntryWidget extends SwingEntry implements dorkbox.systemTray.Separator { // this is ALWAYS called on the EDT. - EntryWidget(final MenuImpl parent, JComponent widget) { + SwingEntryWidget(final SwingMenu parent, JComponent widget) { super(parent, widget); _native.setEnabled(true); diff --git a/src/dorkbox/systemTray/swingUI/SwingMenu.java b/src/dorkbox/systemTray/swingUI/SwingMenu.java new file mode 100644 index 0000000..7cee2ec --- /dev/null +++ b/src/dorkbox/systemTray/swingUI/SwingMenu.java @@ -0,0 +1,349 @@ +/* + * 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.swingUI; + + +import java.io.File; +import java.util.concurrent.atomic.AtomicReference; + +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.JMenuItem; + +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 SwingMenu extends MenuBase implements SwingUI { + + // sub-menu = AdjustedJMenu + // systemtray = TrayPopup + volatile JComponent _native; + + // this have to be volatile, because they can be changed from any thread + private volatile String text; + private volatile boolean hasLegitIcon = false; + + /** + * 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 + */ + SwingMenu(final SystemTray systemTray, final Menu parent, final JComponent _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) { + ((JMenuItem) _native).setText(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 SwingEntryItem(SwingMenu.this, callback); + entry.setText(menuText); + entry.setImage(imagePath); + + menuEntries.add(entry); + } else if (entry instanceof SwingEntryItem) { + 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 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 SwingMenu(getSystemTray(), SwingMenu.this, new AdjustedJMenu()); + _native.add(((SwingMenu) entry)._native); // have to add it separately + + entry.setText(menuText); + entry.setImage(imagePath); + value.set((Menu) entry); + + } else if (entry instanceof SwingMenu) { + entry.setText(menuText); + entry.setImage(imagePath); + } + + menuEntries.add(entry); + } + } + }); + + return value.get(); + } + + + + // public here so that Swing/Gtk/AppIndicator can override this + public + void setImage_(final File imageFile) { + hasLegitIcon = imageFile != null; + + dispatch(new Runnable() { + @Override + public + void run() { + if (imageFile != null) { + ImageIcon origIcon = new ImageIcon(imageFile.getAbsolutePath()); + ((JMenuItem) _native).setIcon(origIcon); + } + else { + ((JMenuItem) _native).setIcon(null); + } + } + }); + } + + + + + + + + @Override + public + boolean hasImage() { + return hasLegitIcon; + } + + // public here so that Swing/Gtk/AppIndicator can override this + @Override + public + void setEnabled(final boolean enabled) { + dispatch(new Runnable() { + @Override + public + void run() { + _native.setEnabled(enabled); + } + }); + } + + + /** + * NOT ALWAYS CALLED ON EDT + */ + @Override + public final + void addSeparator() { + dispatch(new Runnable() { + @Override + public + void run() { + synchronized (menuEntries) { + synchronized (menuEntries) { + Entry entry = new SwingEntrySeparator(SwingMenu.this); + menuEntries.add(entry); + } + } + } + }); + } + +// TODO: buggy. The menu will **sometimes** stop responding to the "enter" key after this. Mnemonics still work however. +// public +// Entry addWidget(final JComponent widget) { +// if (widget == null) { +// throw new NullPointerException("Widget cannot be null"); +// } +// +// final AtomicReference value = new AtomicReference(); +// +// dispatchAndWait(new Runnable() { +// @Override +// public +// void run() { +// synchronized (menuEntries) { +// // must always be called on the EDT +// Entry entry = new SwingEntryWidget(SwingMenu.this, widget); +// value.set(entry); +// menuEntries.add(entry); +// } +// } +// }); +// +// return value.get(); +// } + + + // public here so that Swing/Gtk/AppIndicator can access this + public final + void setStatus(final String statusText) { + final SwingMenu _this = this; + dispatchAndWait(new Runnable() { + @Override + public + void run() { + synchronized (menuEntries) { + // status is ALWAYS at 0 index... + SwingEntry menuEntry = null; + if (!menuEntries.isEmpty()) { + menuEntry = (SwingEntry) menuEntries.get(0); + } + + if (menuEntry instanceof Status) { + // set the text or delete... + + if (statusText == null) { + // delete + remove(menuEntry); + } + else { + // set text + menuEntry.setText(statusText); + } + + } else { + // create a new one + menuEntry = new SwingEntryStatus(_this, statusText); + // status is ALWAYS at 0 index... + menuEntries.add(0, menuEntry); + } + } + } + }); + } + + @Override + public final + void setShortcut(final char key) { + if (_native instanceof JMenuItem) { + // yikes... + final int vKey = SystemTrayFixes.getVirtualKey(key); + dispatch(new Runnable() { + @Override + public + void run() { + ((JMenuItem) _native).setMnemonic(vKey); + } + }); + } + } + + @Override + public final + void remove() { + dispatchAndWait(new Runnable() { + @Override + public + void run() { + _native.setVisible(false); + if (_native instanceof TrayPopup) { + ((TrayPopup) _native).close(); + } + + SwingMenu parent = (SwingMenu) getParent(); + if (parent != null) { + parent._native.remove(_native); + } + } + }); + } +} diff --git a/src/dorkbox/systemTray/swingUI/SwingUI.java b/src/dorkbox/systemTray/swingUI/SwingUI.java new file mode 100644 index 0000000..8a5876c --- /dev/null +++ b/src/dorkbox/systemTray/swingUI/SwingUI.java @@ -0,0 +1,30 @@ +/* + * Copyright 2016 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.swingUI; + +/** + * Represents a System Tray or menu, that will have it's menu rendered via Swing. + *

+ * This has the most standard L&F across all systems (as all systems will render this menu the exact same way), however the tradeoff is that + * one loses the native L&F of the system (with the exception of Windows, whose native menu looks absolutely terrible). + *

+ * Noticeable differences that are limitations for the NativeUI only: + * - AppIndicator Status entries must be plain text (they are not bold as they are everywhere else). + * - MacOS cannot have images in their menu or sub-menu's -- only plain text is possible + */ +public +interface SwingUI +{} diff --git a/src/dorkbox/systemTray/swing/TrayPopup.java b/src/dorkbox/systemTray/swingUI/TrayPopup.java similarity index 99% rename from src/dorkbox/systemTray/swing/TrayPopup.java rename to src/dorkbox/systemTray/swingUI/TrayPopup.java index b47f699..3e9e2f5 100644 --- a/src/dorkbox/systemTray/swing/TrayPopup.java +++ b/src/dorkbox/systemTray/swingUI/TrayPopup.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dorkbox.systemTray.swing; +package dorkbox.systemTray.swingUI; import java.awt.Dimension; import java.awt.Frame; diff --git a/src/dorkbox/systemTray/swing/_AppIndicatorTray.java b/src/dorkbox/systemTray/swingUI/_AppIndicatorTray.java similarity index 94% rename from src/dorkbox/systemTray/swing/_AppIndicatorTray.java rename to src/dorkbox/systemTray/swingUI/_AppIndicatorTray.java index 8757c22..11074c2 100644 --- a/src/dorkbox/systemTray/swing/_AppIndicatorTray.java +++ b/src/dorkbox/systemTray/swingUI/_AppIndicatorTray.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dorkbox.systemTray.swing; +package dorkbox.systemTray.swingUI; import java.awt.MouseInfo; import java.awt.Point; @@ -79,8 +79,9 @@ import dorkbox.util.SwingUtil; * http://bazaar.launchpad.net/~canonical-dx-team/ido/trunk/view/head:/src/idoentrymenuitem.c * http://bazaar.launchpad.net/~ubuntu-desktop/ido/gtk3/files */ +@SuppressWarnings("Duplicates") public -class _AppIndicatorTray extends MenuImpl { +class _AppIndicatorTray extends SwingMenu { private volatile AppIndicatorInstanceStruct appIndicator; private boolean isActive = false; private final Runnable popupRunnable; @@ -89,7 +90,9 @@ class _AppIndicatorTray extends MenuImpl { private AtomicBoolean shuttingDown = new AtomicBoolean(); // necessary to prevent GC on these objects + @SuppressWarnings({"FieldCanBeLocal", "unused"}) private NativeLong nativeLong; + @SuppressWarnings("FieldCanBeLocal") private GEventCallback gtkCallback; @@ -107,12 +110,6 @@ class _AppIndicatorTray extends MenuImpl { _AppIndicatorTray(final SystemTray systemTray) { super(systemTray,null, new TrayPopup()); - if (SystemTray.FORCE_TRAY_TYPE != 0 && SystemTray.FORCE_TRAY_TYPE != SystemTray.TYPE_APP_INDICATOR) { - throw new IllegalArgumentException("Unable to start AppIndicator Tray if 'SystemTray.FORCE_TRAY_TYPE' does not match"); - } - - ImageUtils.determineIconSize(); - TrayPopup popupMenu = (TrayPopup) _native; popupMenu.pack(); popupMenu.setFocusable(true); @@ -198,11 +195,11 @@ class _AppIndicatorTray extends MenuImpl { AppIndicator.app_indicator_set_menu(appIndicator, dummyMenu); } - public + public final void shutdown() { if (!shuttingDown.getAndSet(true)) { // must happen asap, so our hook properly notices we are in shutdown mode - final AppIndicatorInstanceStruct savedAppI = appIndicator; + final AppIndicatorInstanceStruct savedAppIndicator = appIndicator; appIndicator = null; Gtk.dispatch(new Runnable() { @@ -210,8 +207,8 @@ class _AppIndicatorTray extends MenuImpl { public void run() { // STATUS_PASSIVE hides the indicator - AppIndicator.app_indicator_set_status(savedAppI, AppIndicator.STATUS_PASSIVE); - Pointer p = savedAppI.getPointer(); + AppIndicator.app_indicator_set_status(savedAppIndicator, AppIndicator.STATUS_PASSIVE); + Pointer p = savedAppIndicator.getPointer(); Gobject.g_object_unref(p); } }); @@ -224,7 +221,14 @@ class _AppIndicatorTray extends MenuImpl { } } - public + @Override + public final + boolean hasImage() { + return true; + } + + @Override + public final void setImage_(final File imageFile) { dispatch(new Runnable() { @Override @@ -252,7 +256,8 @@ class _AppIndicatorTray extends MenuImpl { }); } - public + @Override + public final void setEnabled(final boolean setEnabled) { visible = !setEnabled; diff --git a/src/dorkbox/systemTray/swing/_GtkStatusIconTray.java b/src/dorkbox/systemTray/swingUI/_GtkStatusIconTray.java similarity index 94% rename from src/dorkbox/systemTray/swing/_GtkStatusIconTray.java rename to src/dorkbox/systemTray/swingUI/_GtkStatusIconTray.java index 298e84f..3c3329d 100644 --- a/src/dorkbox/systemTray/swing/_GtkStatusIconTray.java +++ b/src/dorkbox/systemTray/swingUI/_GtkStatusIconTray.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dorkbox.systemTray.swing; +package dorkbox.systemTray.swingUI; import java.awt.MouseInfo; import java.awt.Point; @@ -32,7 +32,6 @@ import dorkbox.systemTray.linux.jna.GEventCallback; import dorkbox.systemTray.linux.jna.GdkEventButton; import dorkbox.systemTray.linux.jna.Gobject; import dorkbox.systemTray.linux.jna.Gtk; -import dorkbox.systemTray.util.ImageUtils; /** * Class for handling all system tray interactions via GTK. @@ -40,8 +39,9 @@ import dorkbox.systemTray.util.ImageUtils; * This is the "old" way to do it, and does not work with some desktop environments. This is a hybrid class, because we want to show the * swing menu popup INSTEAD of GTK menu popups. The "golden standard" is our swing menu popup, since we have 100% control over it. */ +@SuppressWarnings("Duplicates") public -class _GtkStatusIconTray extends MenuImpl { +class _GtkStatusIconTray extends SwingMenu { private volatile Pointer trayIcon; // http://code.metager.de/source/xref/gnome/Platform/gtk%2B/gtk/deprecated/gtkstatusicon.c @@ -63,12 +63,6 @@ class _GtkStatusIconTray extends MenuImpl { _GtkStatusIconTray(final SystemTray systemTray) { super(systemTray, null, new TrayPopup()); - if (SystemTray.FORCE_TRAY_TYPE != 0 && SystemTray.FORCE_TRAY_TYPE != SystemTray.TYPE_GTK_STATUSICON) { - throw new IllegalArgumentException("Unable to start GtkStatusIcon if 'SystemTray.FORCE_TRAY_TYPE' does not match"); - } - - ImageUtils.determineIconSize(); - JPopupMenu popupMenu = (JPopupMenu) _native; popupMenu.pack(); popupMenu.setFocusable(true); diff --git a/src/dorkbox/systemTray/swing/_SwingTray.java b/src/dorkbox/systemTray/swingUI/_SwingTray.java similarity index 88% rename from src/dorkbox/systemTray/swing/_SwingTray.java rename to src/dorkbox/systemTray/swingUI/_SwingTray.java index 9f631e8..ac32f22 100644 --- a/src/dorkbox/systemTray/swing/_SwingTray.java +++ b/src/dorkbox/systemTray/swingUI/_SwingTray.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dorkbox.systemTray.swing; +package dorkbox.systemTray.swingUI; import java.awt.AWTException; import java.awt.Image; @@ -26,19 +26,17 @@ import java.io.File; import javax.swing.ImageIcon; import javax.swing.JPopupMenu; -import dorkbox.systemTray.util.ImageUtils; - /** - * Class for handling all system tray interaction, via SWING. + * Class for handling all system tray interaction, via Swing. * - * It doesn't work well on linux. See bugs: + * It doesn't work well AT ALL on linux. See bugs: * http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6267936 * http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6453521 * https://stackoverflow.com/questions/331407/java-trayicon-using-image-with-transparent-background/3882028#3882028 */ @SuppressWarnings({"SynchronizationOnLocalVariableOrMethodParameter", "WeakerAccess"}) public -class _SwingTray extends MenuImpl { +class _SwingTray extends SwingMenu { private volatile SystemTray tray; private volatile TrayIcon trayIcon; @@ -50,12 +48,11 @@ class _SwingTray extends MenuImpl { _SwingTray(final dorkbox.systemTray.SystemTray systemTray) { super(systemTray, null, new TrayPopup()); - if (dorkbox.systemTray.SystemTray.FORCE_TRAY_TYPE != 0 && dorkbox.systemTray.SystemTray.FORCE_TRAY_TYPE != dorkbox.systemTray.SystemTray.TYPE_SWING) { - throw new IllegalArgumentException("Unable to start Swing SystemTray if 'SystemTray.FORCE_TRAY_TYPE' does not match"); + if (!SystemTray.isSupported()) { + throw new RuntimeException("System Tray is not supported in this configuration! Please write an issue and include your OS " + + "type and configuration"); } - ImageUtils.determineIconSize(); - _SwingTray.this.tray = SystemTray.getSystemTray(); } @@ -65,10 +62,10 @@ class _SwingTray extends MenuImpl { @Override public void run() { - tray.remove(trayIcon); - removeAll(); remove(); + + tray.remove(trayIcon); } }); } @@ -118,6 +115,7 @@ class _SwingTray extends MenuImpl { }); } + @SuppressWarnings("Duplicates") public void setEnabled(final boolean setEnabled) { visible = !setEnabled; @@ -126,7 +124,6 @@ class _SwingTray extends MenuImpl { @Override public void run() { - if (visible && !setEnabled) { tray.remove(trayIcon); } diff --git a/src/dorkbox/systemTray/swing/MenuImpl.java b/src/dorkbox/systemTray/util/MenuBase.java similarity index 61% rename from src/dorkbox/systemTray/swing/MenuImpl.java rename to src/dorkbox/systemTray/util/MenuBase.java index d17e4e5..f27f608 100644 --- a/src/dorkbox/systemTray/swing/MenuImpl.java +++ b/src/dorkbox/systemTray/util/MenuBase.java @@ -13,186 +13,72 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dorkbox.systemTray.swing; +package dorkbox.systemTray.util; -import static dorkbox.systemTray.swing.EntryImpl.getVkKey; - import java.io.File; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.Iterator; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -import javax.swing.ImageIcon; -import javax.swing.JComponent; -import javax.swing.JMenuItem; import dorkbox.systemTray.Action; import dorkbox.systemTray.Entry; import dorkbox.systemTray.Menu; +import dorkbox.systemTray.Separator; import dorkbox.systemTray.Status; import dorkbox.systemTray.SystemTray; -import dorkbox.systemTray.util.ImageUtils; -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 MenuImpl implements Menu { - static final AtomicInteger MENU_ID_COUNTER = new AtomicInteger(); - private final int id = MenuImpl.MENU_ID_COUNTER.getAndIncrement(); +public abstract +class MenuBase implements Menu { + public static final AtomicInteger MENU_ID_COUNTER = new AtomicInteger(); + private final int id = MenuBase.MENU_ID_COUNTER.getAndIncrement(); - private final java.util.List menuEntries = new ArrayList(); + protected final java.util.List menuEntries = new ArrayList(); private final SystemTray systemTray; private final Menu parent; - // sub-menu = AdjustedJMenu - // systemtray = TrayPopup - volatile JComponent _native; - - // this have to be volatile, because they can be changed from any thread - private volatile String text; - private volatile boolean hasLegitIcon = false; - /** - * Called in the EDT + * Called in the EDT/GTK dispatch threads * * @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 */ - MenuImpl(final SystemTray systemTray, final Menu parent, final JComponent _native) { + public + MenuBase(final SystemTray systemTray, final Menu parent) { this.systemTray = systemTray; this.parent = parent; - this._native = _native; } - void dispatch(final Runnable runnable) { - // this will properly check if we are running on the EDT - SwingUtil.invokeLater(runnable); - } - - 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 - private - void renderText(final String text) { - ((JMenuItem) _native).setText(text); - } + protected abstract + void dispatch(final Runnable runnable); + protected abstract + void dispatchAndWait(final Runnable runnable); /** * Will add a new menu entry, or update one if it already exists - * NOT ALWAYS CALLED ON EDT + * NOT ALWAYS CALLED ON DISPATCH */ - private - 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 EntryItem(MenuImpl.this, callback); - entry.setText(menuText); - entry.setImage(imagePath); - - menuEntries.add(entry); - } else if (entry instanceof EntryItem) { - entry.setText(menuText); - entry.setImage(imagePath); - } - - value.set(entry); - } - } - }); - - return value.get(); - } + protected abstract + Entry addEntry_(final String menuText, final File imagePath, final Action callback); /** * Will add a new sub-menu entry, or update one if it already exists - * NOT ALWAYS CALLED ON EDT + * NOT ALWAYS CALLED ON DISPATCH */ - private - Menu addMenu_(final String menuText, final File imagePath) { - 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 MenuImpl(getSystemTray(), MenuImpl.this, new AdjustedJMenu()); - _native.add(((MenuImpl) entry)._native); // have to add it separately - - entry.setText(menuText); - entry.setImage(imagePath); - value.set((Menu) entry); - - } else if (entry instanceof MenuImpl) { - entry.setText(menuText); - entry.setImage(imagePath); - } - - menuEntries.add(entry); - } - } - }); - - return value.get(); - } - + protected abstract + Menu addMenu_(final String menuText, final File imagePath); // public here so that Swing/Gtk/AppIndicator can override this - public - void setImage_(final File imageFile) { - hasLegitIcon = imageFile != null; - - dispatch(new Runnable() { - @Override - public - void run() { - if (imageFile != null) { - ImageIcon origIcon = new ImageIcon(imageFile.getAbsolutePath()); - ((JMenuItem) _native).setIcon(origIcon); - } - else { - ((JMenuItem) _native).setIcon(null); - } - } - }); - } + protected abstract + void setImage_(final File imageFile); @@ -201,58 +87,29 @@ class MenuImpl implements Menu { - - public + @Override + public final Menu getParent() { return parent; } - public + @Override + public final SystemTray getSystemTray() { return systemTray; } - - @Override - public - boolean hasImage() { - return hasLegitIcon; - } - - /** - * Enables, or disables the sub-menu entry. - */ - @Override - public - void setEnabled(final boolean enabled) { - dispatch(new Runnable() { - @Override - public - void run() { - _native.setEnabled(enabled); + // public here so that Swing/Gtk/AppIndicator can access this + public final + String getStatus() { + synchronized (menuEntries) { + Entry entry = menuEntries.get(0); + if (entry instanceof Status) { + return entry.getText(); } - }); - } + } - - /** - * NOT ALWAYS CALLED ON EDT - */ - @Override - public - void addSeparator() { - dispatch(new Runnable() { - @Override - public - void run() { - synchronized (menuEntries) { - synchronized (menuEntries) { - Entry entry = new EntrySeparator(MenuImpl.this); - menuEntries.add(entry); - } - } - } - }); + return null; } // TODO: buggy. The menu will **sometimes** stop responding to the "enter" key after this. Mnemonics still work however. @@ -281,7 +138,8 @@ class MenuImpl implements Menu { // } - public + @Override + public final Entry get(final String menuText) { if (menuText == null || menuText.isEmpty()) { return null; @@ -291,6 +149,11 @@ class MenuImpl implements Menu { synchronized (menuEntries) { for (int i = 0, menuEntriesSize = menuEntries.size(); i < menuEntriesSize; i++) { final Entry entry = menuEntries.get(i); + + if (entry instanceof Separator || entry instanceof Status) { + continue; + } + String text = entry.getText(); // text can be null @@ -304,13 +167,15 @@ class MenuImpl implements Menu { } // ignores status + separators - public + @Override + public final Entry getFirst() { return get(0); } // ignores status + separators - public + @Override + public final Entry getLast() { // Must be wrapped in a synchronized block for object visibility synchronized (menuEntries) { @@ -319,7 +184,7 @@ class MenuImpl implements Menu { for (int i = menuEntries.size()-1; i >= 0; i--) { entry = menuEntries.get(i); - if (!(entry instanceof dorkbox.systemTray.Separator || entry instanceof Status)) { + if (!(entry instanceof Separator || entry instanceof Status)) { return entry; } } @@ -330,7 +195,8 @@ class MenuImpl implements Menu { } // ignores status + separators - public + @Override + public final Entry get(final int menuIndex) { if (menuIndex < 0) { return null; @@ -341,7 +207,7 @@ class MenuImpl implements Menu { if (!menuEntries.isEmpty()) { int count = 0; for (Entry entry : menuEntries) { - if (entry instanceof dorkbox.systemTray.Separator || entry instanceof Status) { + if (entry instanceof Separator || entry instanceof Status) { continue; } @@ -357,13 +223,14 @@ class MenuImpl implements Menu { return null; } - - public + @Override + public final Entry addEntry(String menuText, Action callback) { return addEntry(menuText, (String) null, callback); } - public + @Override + public final Entry addEntry(String menuText, String imagePath, Action callback) { if (imagePath == null) { return addEntry_(menuText, null, callback); @@ -373,7 +240,8 @@ class MenuImpl implements Menu { } } - public + @Override + public final Entry addEntry(String menuText, URL imageUrl, Action callback) { if (imageUrl == null) { return addEntry_(menuText, null, callback); @@ -383,7 +251,8 @@ class MenuImpl implements Menu { } } - public + @Override + public final Entry addEntry(String menuText, String cacheName, InputStream imageStream, Action callback) { if (imageStream == null) { return addEntry_(menuText, null, callback); @@ -393,7 +262,8 @@ class MenuImpl implements Menu { } } - public + @Override + public final Entry addEntry(String menuText, InputStream imageStream, Action callback) { if (imageStream == null) { return addEntry_(menuText, null, callback); @@ -407,13 +277,14 @@ class MenuImpl implements Menu { - - public + @Override + public final Menu addMenu(String menuText) { return addMenu(menuText, (String) null); } - public + @Override + public final Menu addMenu(String menuText, String imagePath) { if (imagePath == null) { return addMenu_(menuText, null); @@ -423,7 +294,8 @@ class MenuImpl implements Menu { } } - public + @Override + public final Menu addMenu(String menuText, URL imageUrl) { if (imageUrl == null) { return addMenu_(menuText, null); @@ -433,7 +305,8 @@ class MenuImpl implements Menu { } } - public + @Override + public final Menu addMenu(String menuText, String cacheName, InputStream imageStream) { if (imageStream == null) { return addMenu_(menuText, null); @@ -443,7 +316,8 @@ class MenuImpl implements Menu { } } - public + @Override + public final Menu addMenu(String menuText, InputStream imageStream) { if (imageStream == null) { return addMenu_(menuText, null); @@ -511,97 +385,16 @@ class MenuImpl implements Menu { } } - public - String getStatus() { - synchronized (menuEntries) { - Entry entry = menuEntries.get(0); - if (entry instanceof EntryStatus) { - return entry.getText(); - } - } - - return null; - } - - public - void setStatus(final String statusText) { - final MenuImpl _this = this; - dispatchAndWait(new Runnable() { - @Override - public - void run() { - synchronized (menuEntries) { - // status is ALWAYS at 0 index... - EntryImpl menuEntry = null; - if (!menuEntries.isEmpty()) { - menuEntry = (EntryImpl) menuEntries.get(0); - } - - if (menuEntry instanceof EntryStatus) { - // set the text or delete... - - if (statusText == null) { - // delete - remove(menuEntry); - } - else { - // set text - menuEntry.setText(statusText); - } - - } else { - // create a new one - menuEntry = new EntryStatus(_this, statusText); - // status is ALWAYS at 0 index... - menuEntries.add(0, menuEntry); - } - } - } - }); - } @Override - public - String getText() { - return text; - } - - @Override - public - void setText(final String newText) { - text = newText; - dispatch(new Runnable() { - @Override - public - void run() { - renderText(newText); - } - }); - } - - @Override - public + public final void setCallback(final Action callback) { } - @Override - public - void setShortcut(final char key) { - if (_native instanceof JMenuItem) { - // yikes... - final int vKey = getVkKey(key); - dispatch(new Runnable() { - @Override - public - void run() { - ((JMenuItem) _native).setMnemonic(vKey); - } - }); - } - } + @@ -628,7 +421,8 @@ class MenuImpl implements Menu { * * @param entry This is the menu entry to remove */ - public + @Override + public final void remove(final Entry entry) { if (entry == null) { throw new NullPointerException("No menu entry exists for entry"); @@ -649,7 +443,7 @@ class MenuImpl implements Menu { * @param menu This is the menu entry to remove */ @Override - public + public final void remove(final Menu menu) { final Menu parent = getParent(); if (parent == null) { @@ -668,14 +462,14 @@ class MenuImpl implements Menu { @Override public void run() { - ((MenuImpl) parent).remove__(_this); + ((MenuBase) parent).remove__(_this); } }); } } // NOT ALWAYS CALLED ON EDT - private + protected void remove__(final Object menuEntry) { try { synchronized (menuEntries) { @@ -715,7 +509,7 @@ class MenuImpl implements Menu { * * @param menuText This is the label for the menu entry or sub-menu to remove */ - public + public final void remove(final String menuText) { dispatchAndWait(new Runnable() { @Override @@ -733,28 +527,28 @@ class MenuImpl implements Menu { } +// @Override +// public final +// void remove() { +// dispatchAndWait(new Runnable() { +// @Override +// public +// void run() { +// _native.setVisible(false); +// if (_native instanceof TrayPopup) { +// ((TrayPopup) _native).close(); +// } +// +// MenuBase parent = (MenuBase) getParent(); +// if (parent != null) { +// parent._native.remove(_native); +// } +// } +// }); +// } + @Override public final - void remove() { - dispatchAndWait(new Runnable() { - @Override - public - void run() { - _native.setVisible(false); - if (_native instanceof TrayPopup) { - ((TrayPopup) _native).close(); - } - - MenuImpl parent = (MenuImpl) getParent(); - if (parent != null) { - parent._native.remove(_native); - } - } - }); - } - - @Override - public void removeAll() { dispatch(new Runnable() { @Override @@ -762,7 +556,7 @@ class MenuImpl implements Menu { void run() { synchronized (menuEntries) { // have to make copy because we are deleting all of them, and sub-menus remove themselves from parents - ArrayList menuEntriesCopy = new ArrayList(MenuImpl.this.menuEntries); + ArrayList menuEntriesCopy = new ArrayList(MenuBase.this.menuEntries); for (Entry entry : menuEntriesCopy) { entry.remove(); } @@ -792,7 +586,7 @@ class MenuImpl implements Menu { return false; } - MenuImpl other = (MenuImpl) obj; + MenuBase other = (MenuBase) obj; return this.id == other.id; } } diff --git a/src/dorkbox/systemTray/util/WindowsSystemTraySwing.java b/src/dorkbox/systemTray/util/SystemTrayFixes.java similarity index 57% rename from src/dorkbox/systemTray/util/WindowsSystemTraySwing.java rename to src/dorkbox/systemTray/util/SystemTrayFixes.java index 8f57ae8..497859f 100644 --- a/src/dorkbox/systemTray/util/WindowsSystemTraySwing.java +++ b/src/dorkbox/systemTray/util/SystemTrayFixes.java @@ -18,6 +18,7 @@ package dorkbox.systemTray.util; import static dorkbox.systemTray.SystemTray.logger; import java.awt.Robot; +import java.awt.event.KeyEvent; import java.util.Locale; import dorkbox.systemTray.SystemTray; @@ -31,11 +32,11 @@ import javassist.CtMethod; * Fixes issues with some java runtimes */ public -class WindowsSystemTraySwing { +class SystemTrayFixes { // oh my. Java likes to think that ALL windows tray icons are 16x16.... Lets fix that! - public static void fix() { - // if we are using swing (in windows only) the icon size is usually incorrect. Here we have to fix that. + // https://stackoverflow.com/questions/16378886/java-trayicon-right-click-disabled-on-mac-osx/35919788#35919788 + public static void fixWindows() { if (!OS.isWindows()) { return; } @@ -61,7 +62,7 @@ class WindowsSystemTraySwing { (null != m.invoke(cl, "java.awt.SystemTray")); } catch (Throwable e) { if (SystemTray.DEBUG) { - logger.debug("Error detecting javaFX/SWT mode", e); + logger.debug("Error detecting if the Swing SystemTray is loaded", e); } } @@ -200,4 +201,152 @@ class WindowsSystemTraySwing { logger.error("Error setting tray icon size to: {}", ImageUtils.TRAY_SIZE, e); } } + + // MacOS AWT is hardcoded to respond only to "popup trigger" for menus, where it should be any mouse button + // https://stackoverflow.com/questions/16378886/java-trayicon-right-click-disabled-on-mac-osx/35919788#35919788 + // https://bugs.openjdk.java.net/browse/JDK-7158615 + public static void fixMacOS() { + if (!OS.isWindows()) { + return; + } + + String vendor = System.getProperty("java.vendor").toLowerCase(Locale.US); + // spaces at the end to make sure we check for words + if (!(vendor.contains("sun ") || vendor.contains("oracle "))) { + // not fixing things that are not broken. + return; + } + + + + boolean isMacSwingTrayLoaded = false; + + try { + // this is important to use reflection, because if JavaFX is not being used, calling getToolkit() will initialize it... + java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod("findLoadedClass", String.class); + m.setAccessible(true); + ClassLoader cl = ClassLoader.getSystemClassLoader(); + + // if we are using swing (in windows only) the icon size is usually incorrect. We cannot fix that if it's already loaded. + isMacSwingTrayLoaded = (null != m.invoke(cl, "sun.lwawt.macosx.CTrayIcon")) || + (null != m.invoke(cl, "java.awt.SystemTray")); + } catch (Throwable e) { + if (SystemTray.DEBUG) { + logger.debug("Error detecting if the MacOS SystemTray is loaded", e); + } + } + + if (isMacSwingTrayLoaded) { + throw new RuntimeException("Unable to initialize the swing tray in windows, it has already been created!"); + } + } + + /** + * Converts a key character into it's corresponding VK entry + */ + public static + int getVirtualKey(final char key) { + switch (key) { + case 0x08: return KeyEvent.VK_BACK_SPACE; + case 0x09: return KeyEvent.VK_TAB; + case 0x0a: return KeyEvent.VK_ENTER; + case 0x1B: return KeyEvent.VK_ESCAPE; + case 0x20AC: return KeyEvent.VK_EURO_SIGN; + case 0x20: return KeyEvent.VK_SPACE; + case 0x21: return KeyEvent.VK_EXCLAMATION_MARK; + case 0x22: return KeyEvent.VK_QUOTEDBL; + case 0x23: return KeyEvent.VK_NUMBER_SIGN; + case 0x24: return KeyEvent.VK_DOLLAR; + case 0x26: return KeyEvent.VK_AMPERSAND; + case 0x27: return KeyEvent.VK_QUOTE; + case 0x28: return KeyEvent.VK_LEFT_PARENTHESIS; + case 0x29: return KeyEvent.VK_RIGHT_PARENTHESIS; + case 0x2A: return KeyEvent.VK_ASTERISK; + case 0x2B: return KeyEvent.VK_PLUS; + case 0x2C: return KeyEvent.VK_COMMA; + case 0x2D: return KeyEvent.VK_MINUS; + case 0x2E: return KeyEvent.VK_PERIOD; + case 0x2F: return KeyEvent.VK_SLASH; + case 0x30: return KeyEvent.VK_0; + case 0x31: return KeyEvent.VK_1; + case 0x32: return KeyEvent.VK_2; + case 0x33: return KeyEvent.VK_3; + case 0x34: return KeyEvent.VK_4; + case 0x35: return KeyEvent.VK_5; + case 0x36: return KeyEvent.VK_6; + case 0x37: return KeyEvent.VK_7; + case 0x38: return KeyEvent.VK_8; + case 0x39: return KeyEvent.VK_9; + case 0x3A: return KeyEvent.VK_COLON; + case 0x3B: return KeyEvent.VK_SEMICOLON; + case 0x3C: return KeyEvent.VK_LESS; + case 0x3D: return KeyEvent.VK_EQUALS; + case 0x3E: return KeyEvent.VK_GREATER; + case 0x40: return KeyEvent.VK_AT; + case 0x41: return KeyEvent.VK_A; + case 0x42: return KeyEvent.VK_B; + case 0x43: return KeyEvent.VK_C; + case 0x44: return KeyEvent.VK_D; + case 0x45: return KeyEvent.VK_E; + case 0x46: return KeyEvent.VK_F; + case 0x47: return KeyEvent.VK_G; + case 0x48: return KeyEvent.VK_H; + case 0x49: return KeyEvent.VK_I; + case 0x4A: return KeyEvent.VK_J; + case 0x4B: return KeyEvent.VK_K; + case 0x4C: return KeyEvent.VK_L; + case 0x4D: return KeyEvent.VK_M; + case 0x4E: return KeyEvent.VK_N; + case 0x4F: return KeyEvent.VK_O; + case 0x50: return KeyEvent.VK_P; + case 0x51: return KeyEvent.VK_Q; + case 0x52: return KeyEvent.VK_R; + case 0x53: return KeyEvent.VK_S; + case 0x54: return KeyEvent.VK_T; + case 0x55: return KeyEvent.VK_U; + case 0x56: return KeyEvent.VK_V; + case 0x57: return KeyEvent.VK_W; + case 0x58: return KeyEvent.VK_X; + case 0x59: return KeyEvent.VK_Y; + case 0x5A: return KeyEvent.VK_Z; + case 0x5B: return KeyEvent.VK_OPEN_BRACKET; + case 0x5C: return KeyEvent.VK_BACK_SLASH; + case 0x5D: return KeyEvent.VK_CLOSE_BRACKET; + case 0x5E: return KeyEvent.VK_CIRCUMFLEX; + case 0x5F: return KeyEvent.VK_UNDERSCORE; + case 0x60: return KeyEvent.VK_BACK_QUOTE; + case 0x61: return KeyEvent.VK_A; + case 0x62: return KeyEvent.VK_B; + case 0x63: return KeyEvent.VK_C; + case 0x64: return KeyEvent.VK_D; + case 0x65: return KeyEvent.VK_E; + case 0x66: return KeyEvent.VK_F; + case 0x67: return KeyEvent.VK_G; + case 0x68: return KeyEvent.VK_H; + case 0x69: return KeyEvent.VK_I; + case 0x6A: return KeyEvent.VK_J; + case 0x6B: return KeyEvent.VK_K; + case 0x6C: return KeyEvent.VK_L; + case 0x6D: return KeyEvent.VK_M; + case 0x6E: return KeyEvent.VK_N; + case 0x6F: return KeyEvent.VK_O; + case 0x70: return KeyEvent.VK_P; + case 0x71: return KeyEvent.VK_Q; + case 0x72: return KeyEvent.VK_R; + case 0x73: return KeyEvent.VK_S; + case 0x74: return KeyEvent.VK_T; + case 0x75: return KeyEvent.VK_U; + case 0x76: return KeyEvent.VK_V; + case 0x77: return KeyEvent.VK_W; + case 0x78: return KeyEvent.VK_X; + case 0x79: return KeyEvent.VK_Y; + case 0x7A: return KeyEvent.VK_Z; + case 0x7B: return KeyEvent.VK_BRACELEFT; + case 0x7D: return KeyEvent.VK_BRACERIGHT; + case 0x7F: return KeyEvent.VK_DELETE; + case 0xA1: return KeyEvent.VK_INVERTED_EXCLAMATION_MARK; + } + + return 0; + } }