diff --git a/src/dorkbox/systemTray/SystemTray.java b/src/dorkbox/systemTray/SystemTray.java index 3ea5311..3cdf652 100644 --- a/src/dorkbox/systemTray/SystemTray.java +++ b/src/dorkbox/systemTray/SystemTray.java @@ -56,6 +56,7 @@ class SystemTray { public static final int LINUX_GTK = 1; public static final int LINUX_APP_INDICATOR = 2; + public static final int SWING_INDICATOR = 3; @Property /** How long to wait when updating menu entries before the request times-out */ @@ -70,16 +71,9 @@ class SystemTray { public static boolean FORCE_GTK2 = false; @Property - /** If != 0, forces the system tray in linux to be GTK (1) or AppIndicator (2). This is an advanced feature. */ + /** If != 0, forces the system tray in linux to be GTK (1), AppIndicator (2), or Swing (3). This is an advanced feature. */ public static int FORCE_LINUX_TYPE = 0; - @Property - /** - * Forces the system to enter into JavaFX/SWT compatibility mode, where it will use GTK2 AND will not start/stop the GTK main loop. - * This is only necessary if autodetection fails. - */ - public static boolean COMPATIBILITY_MODE = false; - @Property /** * When in compatibility mode, and the JavaFX/SWT primary windows are closed, we want to make sure that the SystemTray is also closed. @@ -93,9 +87,40 @@ class SystemTray { */ public static boolean DEBUG = false; + private static volatile SystemTray systemTray = null; static boolean isKDE = false; + public final static boolean isJavaFxLoaded; + public final static boolean isSwtLoaded; + + static { + // maybe we should load the SWT version? (In order for us to work with SWT, BOTH must be GTK2!! + // SWT is GTK2, but if -DSWT_GTK3=1 is specified, it can be GTK3 + // JavaFX Java7,8 is GTK2 only. Java9 can have it be GTK3 if -Djdk.gtk.version=3 is specified + // see http://mail.openjdk.java.net/pipermail/openjfx-dev/2016-May/019100.html + + boolean isJavaFxLoaded_ = false; + boolean isSwtLoaded_ = false; + try { + // First check if JavaFX is loaded - if it's NOT LOADED, then we only proceed if COMPATIBILITY_MODE is enabled. + // this is important, 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(); + isJavaFxLoaded_ = (null != m.invoke(cl, "com.sun.javafx.tk.Toolkit")) || (null != m.invoke(cl, "javafx.application.Application")); + isSwtLoaded_ = null != m.invoke(cl, "org.eclipse.swt.widgets.Display"); + } catch (Throwable e) { + if (DEBUG) { + logger.debug("Error detecting javaFX/SWT mode", e); + } + } + + isJavaFxLoaded = isJavaFxLoaded_; + isSwtLoaded = isSwtLoaded_; + } + + private static void init() { if (systemTray != null) { return; @@ -109,52 +134,73 @@ class SystemTray { Class trayType = null; - boolean isJavaFxLoaded = false; - boolean isSwtLoaded = false; - try { - // First check if JavaFX is loaded - if it's NOT LOADED, then we only proceed if COMPATIBILITY_MODE is enabled. - // this is important, 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(); - isJavaFxLoaded = (null != m.invoke(cl, "com.sun.javafx.tk.Toolkit")) || (null != m.invoke(cl, "javafx.application.Application")); - isSwtLoaded = null != m.invoke(cl, "org.eclipse.swt.widgets.Display"); - } catch (Throwable e) { - if (DEBUG) { - logger.debug("Error detecting compatibility mode", e); + // kablooie if SWT is not configured in a way that works with us. + if (FORCE_LINUX_TYPE != SWING_INDICATOR && 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"); + + // was SWT forced? + boolean isSwt_GTK3 = !System.getProperty("SWT_GTK3").equals("0"); + if (!isSwt_GTK3) { + // check a different property + isSwt_GTK3 = !System.getProperty("org.eclipse.swt.internal.gtk.version").startsWith("2."); + } + + if (isSwt_GTK3 && FORCE_GTK2) { + logger.error("Unable to use the SystemTray when SWT is configured to use GTK3 and the SystemTray is configured to use " + + "GTK2. Please configure SWT to use GTK2, via `System.setProperty(\"SWT_GTK3\", \"0\");` before SWT is " + + "initialized, or set `SystemTray.FORCE_GTK2=false;`"); + + throw new RuntimeException("SWT configured to use GTK3 and is incompatible with the SystemTray GTK2."); + } else if (!isSwt_GTK3 && !FORCE_GTK2) { + // we must use GTK2, because SWT is GTK2 + if (DEBUG) { + logger.debug("Forcing GTK2 because SWT is GTK2"); + } + FORCE_GTK2 = true; + } + } + else if (isJavaFxLoaded) { + // JavaFX Java7,8 is GTK2 only. Java9 can MAYBE have it be GTK3 if `-Djdk.gtk.version=3` is specified + // see + // http://mail.openjdk.java.net/pipermail/openjfx-dev/2016-May/019100.html + // https://docs.oracle.com/javafx/2/system_requirements_2-2-3/jfxpub-system_requirements_2-2-3.htm + // from the page: JavaFX 2.2.3 for Linux requires gtk2 2.18+. + boolean isJFX_GTK3 = System.getProperty("jdk.gtk.version", "2").equals("3"); + if (isJFX_GTK3 && FORCE_GTK2) { + // if we are java9, then we can change it -- otherwise we cannot. + if (OS.javaVersion == 9) { + logger.error("Unable to use the SystemTray when JavaFX is configured to use GTK3 and the SystemTray is configured to use " + + "GTK2. Please configure JavaFX to use GTK2 (via `System.setProperty(\"jdk.gtk.version\", \"3\");`) " + + "before JavaFX is initialized, or set `SystemTray.FORCE_GTK2=false;`"); + + throw new RuntimeException("JavaFX configured to use GTK3 and is incompatible with the SystemTray GTK2."); + } else { + logger.error("Unable to use the SystemTray when JavaFX is configured to use GTK3 and the SystemTray is configured to use " + + "GTK2. Please set `SystemTray.FORCE_GTK2=false;` if that is not possible then it will not work."); + + throw new RuntimeException("JavaFX configured to use GTK3 and is incompatible with the SystemTray GTK2."); + } + } else if (!isJFX_GTK3 && !FORCE_GTK2) { + // we must use GTK2, because JavaFX is GTK2 + if (DEBUG) { + logger.debug("Forcing GTK2 because JavaFX is GTK2"); + } + FORCE_GTK2 = true; + } } } - // maybe we should load the SWT version? (In order for us to work with SWT, BOTH must be GTK2!! - // SWT is GTK2, but if -DSWT_GTK3=1 is specified, it can be GTK3 - // JavaFX Java7,8 is GTK2 only. Java9 can have it be GTK3 if -Djdk.gtk.version=3 is specified - // see http://mail.openjdk.java.net/pipermail/openjfx-dev/2016-May/019100.html - COMPATIBILITY_MODE = OS.isLinux() && (isJavaFxLoaded || isSwtLoaded); - - if (DEBUG) { + switch (FORCE_LINUX_TYPE) { + case 1: logger.debug("Forcing GTK type"); break; + case 2: logger.debug("Forcing AppIndicator type"); break; + case 3: logger.debug("Forcing Swing type"); break; + + default: logger.debug("Auto-detecting indicator type"); break; + } logger.debug("FORCE_GTK2: {}", FORCE_GTK2); - logger.debug("COMPATIBILITY_MODE: {}", COMPATIBILITY_MODE); - } - - // kablooie if SWT is not configured in a way that works with us. - if (OS.isLinux() && isSwtLoaded) { - // Necessary for us to work with SWT - // System.setProperty("SWT_GTK3", "0"); // Necessary for us to work with SWT - - // was SWT forced? - boolean isSwt_GTK3 = !System.getProperty("SWT_GTK3").equals("0"); - if (!isSwt_GTK3) { - // check a different property - isSwt_GTK3 = !System.getProperty("org.eclipse.swt.internal.gtk.version").startsWith("2."); - } - - if (isSwt_GTK3) { - logger.error("Unable to use the SystemTray when SWT is configured to use GTK3. Please configure SWT to use GTK2, one such " + - "example is to set the system property `System.setProperty(\"SWT_GTK3\", \"0\");` before SWT is initialized"); - - throw new RuntimeException("SWT configured to use GTK3 and is incompatible with the SystemTray."); - } } @@ -163,11 +209,11 @@ class SystemTray { // all examined ones sometimes have it (and it's more than just text), or they don't have it at all. if (OS.isWindows()) { - // the tray icon size in windows is DIFFERENT than on Mac (TODO: test on mac with retina stuff). + // the tray icon size in windows is DIFFERENT than on Mac (TODO: test on mac with retina stuff. Also check HiDpi setups). TRAY_SIZE -= 4; } - if (OS.isLinux()) { + if (FORCE_LINUX_TYPE != SWING_INDICATOR && 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. @@ -201,6 +247,8 @@ class SystemTray { } } } + // don't check for SWING type at this spot, it is done elsewhere. + // quick check, because we know that unity uses app-indicator. Maybe REALLY old versions do not. We support 14.04 LTE at least @@ -322,8 +370,9 @@ class SystemTray { } } else if ("ubuntu".equalsIgnoreCase(GDM)) { - // have to install the gnome extension + // have to install the gnome extension AND customize the restart command trayType = null; + GnomeShellExtension.SHELL_RESTART_COMMAND = "unity --replace &"; } } @@ -494,7 +543,7 @@ class SystemTray { // These install a shutdown hook in JavaFX/SWT, so that when the main window is closed -- the system tray is ALSO closed. - if (COMPATIBILITY_MODE && ENABLE_SHUTDOWN_HOOK) { + if (ENABLE_SHUTDOWN_HOOK) { if (isJavaFxLoaded) { // Necessary because javaFX **ALSO** runs a gtk main loop, and when it stops (if we don't stop first), we become unresponsive. // Also, it's nice to have us shutdown at the same time as the main application @@ -518,7 +567,9 @@ class SystemTray { @Override public void run() { - systemTray.shutdown(); + if (systemTray != null) { + systemTray.shutdown(); + } } }); } catch (Throwable e) { @@ -542,7 +593,9 @@ class SystemTray { @Override public void run() { - systemTray.shutdown(); + if (systemTray != null) { + systemTray.shutdown(); + } } }); } catch (Throwable e) { diff --git a/src/dorkbox/systemTray/linux/GtkMenuEntry.java b/src/dorkbox/systemTray/linux/GtkMenuEntry.java index 082e8a5..424f120 100644 --- a/src/dorkbox/systemTray/linux/GtkMenuEntry.java +++ b/src/dorkbox/systemTray/linux/GtkMenuEntry.java @@ -15,8 +15,13 @@ */ package dorkbox.systemTray.linux; +import java.io.InputStream; +import java.net.URL; +import java.util.concurrent.atomic.AtomicInteger; + import com.sun.jna.NativeLong; import com.sun.jna.Pointer; + import dorkbox.systemTray.ImageUtil; import dorkbox.systemTray.MenuEntry; import dorkbox.systemTray.SystemTrayMenuAction; @@ -24,10 +29,6 @@ import dorkbox.systemTray.linux.jna.GCallback; import dorkbox.systemTray.linux.jna.Gobject; import dorkbox.systemTray.linux.jna.Gtk; -import java.io.InputStream; -import java.net.URL; -import java.util.concurrent.atomic.AtomicInteger; - class GtkMenuEntry implements MenuEntry, GCallback { private static final AtomicInteger ID_COUNTER = new AtomicInteger(); private final int id = ID_COUNTER.getAndIncrement(); diff --git a/src/dorkbox/systemTray/linux/GtkSystemTray.java b/src/dorkbox/systemTray/linux/GtkSystemTray.java index a7d8818..1e9b6c8 100644 --- a/src/dorkbox/systemTray/linux/GtkSystemTray.java +++ b/src/dorkbox/systemTray/linux/GtkSystemTray.java @@ -17,15 +17,19 @@ package dorkbox.systemTray.linux; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; 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; +import javafx.application.Platform; /** * Class for handling all system tray interactions via GTK. @@ -34,7 +38,7 @@ import dorkbox.systemTray.linux.jna.Gtk; */ public class GtkSystemTray extends GtkTypeSystemTray { - private Pointer trayIcon; + private volatile Pointer trayIcon; // 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(); @@ -49,23 +53,21 @@ class GtkSystemTray extends GtkTypeSystemTray { super(); Gtk.startGui(); + final CountDownLatch blockUntilStarted = new CountDownLatch(1); dispatch(new Runnable() { @Override public void run() { final Pointer trayIcon_ = Gtk.gtk_status_icon_new(); + trayIcon = trayIcon_; + // necessary for gnome icon detection/placement because we move tray icons around by name. The name is hardcoded // in extension.js, so don't change it Gtk.gtk_status_icon_set_title(trayIcon_, "SystemTray"); - // can cause on fedora 23 - // // Gdk-CRITICAL **: gdk_window_thaw_toplevel_updates: assertion 'window->update_and_descendants_freeze_count > 0' failed - Gtk.gtk_status_icon_set_name(trayIcon_, "SystemTray"); - trayIcon = trayIcon_; - - final GEventCallback gtkCallback = new GEventCallback() { + final GEventCallback gtkCallback2 = new GEventCallback() { @Override public void callback(Pointer notUsed, final GdkEventButton event) { @@ -75,13 +77,42 @@ class GtkSystemTray extends GtkTypeSystemTray { } } }; - final NativeLong button_press_event = Gobject.g_signal_connect_object(trayIcon, "button_press_event", gtkCallback, null, 0); + final NativeLong button_press_event = Gobject.g_signal_connect_object(trayIcon_, "button_press_event", gtkCallback2, + null, 0); // have to do this to prevent GC on these objects - gtkCallbacks.add(gtkCallback); + gtkCallbacks.add(gtkCallback2); gtkCallbacks.add(button_press_event); + + blockUntilStarted.countDown(); } }); + + if (SystemTray.isJavaFxLoaded) { + if (!Platform.isFxApplicationThread()) { + try { + blockUntilStarted.await(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } else if (SystemTray.isSwtLoaded) { + if (SystemTray.FORCE_LINUX_TYPE != SystemTray.LINUX_GTK) { + // 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 + try { + blockUntilStarted.await(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } else { + try { + blockUntilStarted.await(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } } @SuppressWarnings("FieldRepeatedlyAccessedInMethod") @@ -118,6 +149,13 @@ class GtkSystemTray extends GtkTypeSystemTray { if (!isActive) { isActive = true; + + // 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 + // it THIS PLACE IN CODE, these errors don't appear to happen. Nobody knows wtf why this is... This was trial and error + Gtk.gtk_status_icon_set_name(trayIcon, "SystemTray"); + Gtk.gtk_status_icon_set_visible(trayIcon, true); } } diff --git a/src/dorkbox/systemTray/linux/GtkTypeSystemTray.java b/src/dorkbox/systemTray/linux/GtkTypeSystemTray.java index c73eaa6..3c0f98f 100644 --- a/src/dorkbox/systemTray/linux/GtkTypeSystemTray.java +++ b/src/dorkbox/systemTray/linux/GtkTypeSystemTray.java @@ -16,16 +16,17 @@ package dorkbox.systemTray.linux; +import java.io.InputStream; +import java.net.URL; + import com.sun.jna.Pointer; + import dorkbox.systemTray.ImageUtil; import dorkbox.systemTray.SystemTray; import dorkbox.systemTray.SystemTrayMenuAction; import dorkbox.systemTray.linux.jna.Gobject; import dorkbox.systemTray.linux.jna.Gtk; -import java.io.InputStream; -import java.net.URL; - /** * Derived from * Lantern: https://github.com/getlantern/lantern/ Apache 2.0 License Copyright 2010 Brave New Software Project, Inc. diff --git a/src/dorkbox/systemTray/linux/jna/AppIndicator.java b/src/dorkbox/systemTray/linux/jna/AppIndicator.java index 768c798..e8d2753 100644 --- a/src/dorkbox/systemTray/linux/jna/AppIndicator.java +++ b/src/dorkbox/systemTray/linux/jna/AppIndicator.java @@ -58,7 +58,7 @@ class AppIndicator { isLoaded = true; } - if (!isLoaded && (SystemTray.FORCE_GTK2 || SystemTray.COMPATIBILITY_MODE)) { + if (!isLoaded && SystemTray.FORCE_GTK2) { // if specified, try loading appindicator1 first, maybe it's there? try { final NativeLibrary library = JnaHelper.register("appindicator1", AppIndicator.class); diff --git a/src/dorkbox/systemTray/linux/jna/Gobject.java b/src/dorkbox/systemTray/linux/jna/Gobject.java index b5f95ae..da4e89b 100644 --- a/src/dorkbox/systemTray/linux/jna/Gobject.java +++ b/src/dorkbox/systemTray/linux/jna/Gobject.java @@ -31,6 +31,9 @@ class Gobject { JnaHelper.register("gobject-2.0", Gobject.class); } + + public static native void g_idle_add(FuncCallback callback, Pointer data); + public static native void g_free(Pointer object); public static native void g_object_unref(Pointer object); diff --git a/src/dorkbox/systemTray/linux/jna/Gtk.java b/src/dorkbox/systemTray/linux/jna/Gtk.java index 44fbc7e..998e8e7 100644 --- a/src/dorkbox/systemTray/linux/jna/Gtk.java +++ b/src/dorkbox/systemTray/linux/jna/Gtk.java @@ -16,9 +16,12 @@ package dorkbox.systemTray.linux.jna; import static dorkbox.systemTray.SystemTray.logger; +import static dorkbox.systemTray.linux.jna.Gobject.g_idle_add; +import java.lang.reflect.Method; import java.util.LinkedList; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import com.sun.jna.Function; import com.sun.jna.Pointer; @@ -47,6 +50,8 @@ class Gtk { private static boolean alreadyRunningGTK = false; private static boolean isLoaded = false; + private static Method swtDispatchMethod = null; + /** * We can have GTK v3 or v2. @@ -58,26 +63,24 @@ class Gtk { // objdump -T /usr/lib/x86_64-linux-gnu/libgtk-x11-2.0.so.0 | grep gtk // objdump -T /usr/lib/x86_64-linux-gnu/libgtk-3.so.0 | grep gtk + static { - boolean shouldUseGtk2 = SystemTray.FORCE_GTK2 || SystemTray.COMPATIBILITY_MODE; - - // JavaFX Java7,8 is GTK2 only. Java9 can have it be GTK3 if -Djdk.gtk.version=3 is specified - // see http://mail.openjdk.java.net/pipermail/openjfx-dev/2016-May/019100.html - - if (SystemTray.COMPATIBILITY_MODE && System.getProperty("jdk.gtk.version", "2").equals("3") && !SystemTray.FORCE_GTK2) { - // the user specified to use GTK3, so we should honor that - shouldUseGtk2 = false; - } - - // for more info on JavaFX: https://docs.oracle.com/javafx/2/system_requirements_2-2-3/jfxpub-system_requirements_2-2-3.htm - // from the page: JavaFX 2.2.3 for Linux requires gtk2 2.18+. - + boolean shouldUseGtk2 = SystemTray.FORCE_GTK2; // in some cases, we ALWAYS want to try GTK2 first - if (shouldUseGtk2) { + String gtk2LibName = "gtk-x11-2.0"; + 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_LINUX_TYPE == SystemTray.SWING_INDICATOR) { + isLoaded = true; + } + + + if (!isLoaded && shouldUseGtk2) { try { - JnaHelper.register("gtk-x11-2.0", Gtk.class); - gtk_status_icon_position_menu = Function.getFunction("gtk-x11-2.0", "gtk_status_icon_position_menu"); + JnaHelper.register(gtk2LibName, Gtk.class); + gtk_status_icon_position_menu = Function.getFunction(gtk2LibName, "gtk_status_icon_position_menu"); isGtk2 = true; // when running inside of JavaFX, this will be '1'. All other times this should be '0' @@ -86,7 +89,7 @@ class Gtk { isLoaded = true; if (SystemTray.DEBUG) { - logger.info("GTK: gtk-x11-2.0"); + logger.info("GTK: {}", gtk2LibName); } } catch (Throwable e) { if (SystemTray.DEBUG) { @@ -100,15 +103,15 @@ class Gtk { // start with version 3 if (!isLoaded) { try { - JnaHelper.register("libgtk-3.so.0", Gtk.class); - gtk_status_icon_position_menu = Function.getFunction("libgtk-3.so.0", "gtk_status_icon_position_menu"); + JnaHelper.register(gtk3LibName, Gtk.class); + gtk_status_icon_position_menu = Function.getFunction(gtk3LibName, "gtk_status_icon_position_menu"); // when running inside of JavaFX, this will be '1'. All other times this should be '0' // when it's '1', it means that someone else has stared GTK -- so we DO NOT NEED TO. alreadyRunningGTK = gtk_main_level() != 0; isLoaded = true; if (SystemTray.DEBUG) { - logger.info("GTK: libgtk-3.so.0"); + logger.info("GTK: {}", gtk3LibName); } } catch (Throwable e) { if (SystemTray.DEBUG) { @@ -120,8 +123,8 @@ class Gtk { // now version 2 if (!isLoaded) { try { - JnaHelper.register("gtk-x11-2.0", Gtk.class); - gtk_status_icon_position_menu = Function.getFunction("gtk-x11-2.0", "gtk_status_icon_position_menu"); + JnaHelper.register(gtk2LibName, Gtk.class); + gtk_status_icon_position_menu = Function.getFunction(gtk2LibName, "gtk_status_icon_position_menu"); isGtk2 = true; // when running inside of JavaFX, this will be '1'. All other times this should be '0' @@ -130,7 +133,7 @@ class Gtk { isLoaded = true; if (SystemTray.DEBUG) { - logger.info("GTK: gtk-x11-2.0"); + logger.info("GTK: {}", gtk2LibName); } } catch (Throwable e) { if (SystemTray.DEBUG) { @@ -139,6 +142,10 @@ class Gtk { } } + // depending on how the system is initialized, SWT may, or may not, have the gtk_main loop running. It will EVENTUALLY run, so we + // do not want to run our own GTK event loop. + alreadyRunningGTK |= SystemTray.isSwtLoaded; + if (SystemTray.DEBUG) { logger.info("Is the system already running GTK? {}", alreadyRunningGTK); } @@ -168,51 +175,73 @@ class Gtk { // startup the GTK GUI event loop. There can be multiple/nested loops. - // If JavaFX/SWT is used, this is UNNECESSARY - if (!alreadyRunningGTK) { + // only necessary if we are the only GTK instance running... + final CountDownLatch blockUntilStarted = new CountDownLatch(1); + + if (!alreadyRunningGTK ) { + // If JavaFX/SWT is used, this is UNNECESSARY (we can detect if the GTK main_loop is running) if (SystemTray.DEBUG) { logger.error("Running GTK Native Event Loop"); } - // only necessary if we are the only GTK instance running... - final CountDownLatch blockUntilStarted = new CountDownLatch(1); - gtkUpdateThread = new Thread() { @Override public void run() { // prep for the event loop. - gdk_threads_init(); - gdk_threads_enter(); - GThread.g_thread_init(null); - if (!SystemTray.COMPATIBILITY_MODE) { - gtk_init_check(0); + if (!gtk_init_check(0)) { + if (SystemTray.DEBUG) { + logger.error("Error starting GTK"); + } + return; } - // notify our main thread to continue - blockUntilStarted.countDown(); - - if (!SystemTray.COMPATIBILITY_MODE) { - // blocks unit quit - gtk_main(); - } - - gdk_threads_leave(); + // blocks unit quit + gtk_main(); } }; gtkUpdateThread.setName("GTK Native Event Loop"); gtkUpdateThread.start(); - try { + + // notify our main thread to continue + // Please note: we don't need to do this on SWT/JavaFX, because they startup the main_loop BEFORE the app starts. + dispatch(new Runnable() { + @Override + public + void run() { + blockUntilStarted.countDown(); + } + }); + + + try { // we CANNOT continue until the GTK thread has started! - blockUntilStarted.await(); + boolean await = blockUntilStarted.await(10, TimeUnit.SECONDS); + if (!await) { + throw new RuntimeException("Unable to initialize GTK. Something is HORRIBLY wrong, aborting."); + } } catch (InterruptedException e) { e.printStackTrace(); } } } + + if (SystemTray.isSwtLoaded) { + try { + Class clazz = Class.forName("dorkbox.systemTray.swt.Swt"); + swtDispatchMethod = clazz.getMethod("dispatch", Runnable.class); + } catch (Throwable e) { + if (SystemTray.DEBUG) { + logger.error("Cannot initialize SWT dispatch", e); + e.printStackTrace(); + } + logger.error("Unable to call into dispatch method for SWT. Please create an issue with your OS and Java " + + "version so we may further investigate this issue."); + } + } } /** @@ -220,17 +249,37 @@ class Gtk { */ public static void dispatch(final Runnable runnable) { - if (gtkUpdateThread == Thread.currentThread()) { - // if we are ALREADY inside the native event - runnable.run(); - } else { + if (alreadyRunningGTK) { + // SWT/JavaFX + + if (SystemTray.isJavaFxLoaded) { + if (javafx.application.Platform.isFxApplicationThread()) { + // Run directly on the JavaFX event thread + runnable.run(); + } + else { + javafx.application.Platform.runLater(runnable); + } + } + else if (SystemTray.isSwtLoaded) { + try { + swtDispatchMethod.invoke(null, runnable); + } catch (Exception e) { + logger.error("Unable to call into dispatch method for SWT. Please create an issue with your OS and Java " + + "version so we may further investigate this issue."); + throw new RuntimeException("Unable to invoke required SWT method"); + } + } + } + else { final FuncCallback callback = new FuncCallback() { @Override public int callback(final Pointer data) { synchronized (gtkCallbacks) { - gtkCallbacks.removeFirst(); // now that we've 'handled' it, we can remove it from our callback list + gtkCallbacks.removeFirst();// now that we've 'handled' it, we can remove it from our callback list } + runnable.run(); return Gtk.FALSE; // don't want to call this again @@ -240,22 +289,29 @@ class Gtk { synchronized (gtkCallbacks) { gtkCallbacks.offer(callback); // prevent GC from collecting this object before it can be called } - gdk_threads_add_idle(callback, null); + + // the correct way to do it + g_idle_add(callback, null); } } public static void shutdownGui() { - // If JavaFX/SWT is used, this is UNNECESSARY (and will break SWT/JavaFX shutdown) - if (!(alreadyRunningGTK || SystemTray.COMPATIBILITY_MODE)) { - gtk_main_quit(); - } + dispatch(new Runnable() { + @Override + public + void run() { + // If JavaFX/SWT is used, this is UNNECESSARY (and will break SWT/JavaFX shutdown) + if (!alreadyRunningGTK) { + gtk_main_quit(); + } - started = false; + started = 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. @@ -268,13 +324,6 @@ class Gtk { */ private static native void gtk_main(); - /** - * using g_idle_add() instead would require thread protection in the callback - * - * @return TRUE to run this callback again, FALSE to remove from the list of event sources (and not call it again) - */ - private static native int gdk_threads_add_idle(FuncCallback callback, Pointer data); - /** * aks for the current nesting level of the main loop. Useful to determine (at startup) if GTK is already running */ @@ -288,7 +337,11 @@ class Gtk { private static native void gdk_threads_init(); - // tricky business. This should only be in the dispatch thread + // tricky business. Only when using SWT/JavaFX, because they do it wrong. + /** + * @return TRUE to run this callback again, FALSE to remove from the list of event sources (and not call it again) + */ + private static native int gdk_threads_add_idle(FuncCallback callback, Pointer data); private static native void gdk_threads_enter(); private static native void gdk_threads_leave(); @@ -298,8 +351,6 @@ class Gtk { public static native Pointer gtk_menu_new(); - public static native Pointer gtk_menu_item_new(); - public static native Pointer gtk_menu_item_new_with_label(String label); // to create a menu entry WITH an icon. @@ -320,9 +371,10 @@ class Gtk { public static native void gtk_label_set_use_markup(Pointer label, int gboolean); + public static native Pointer gtk_status_icon_new_from_icon_name(String iconName);; public static native Pointer gtk_status_icon_new(); - public static native void gtk_status_icon_set_from_file(Pointer widget, String lablel); + public static native void gtk_status_icon_set_from_file(Pointer widget, String label); public static native void gtk_status_icon_set_visible(Pointer widget, boolean visible);