diff --git a/src/dorkbox/systemTray/SystemTray.java b/src/dorkbox/systemTray/SystemTray.java index 2840fdd..8426dff 100644 --- a/src/dorkbox/systemTray/SystemTray.java +++ b/src/dorkbox/systemTray/SystemTray.java @@ -18,11 +18,11 @@ package dorkbox.systemTray; import dorkbox.systemTray.linux.AppIndicatorTray; import dorkbox.systemTray.linux.GnomeShellExtension; import dorkbox.systemTray.linux.GtkSystemTray; +import dorkbox.systemTray.linux.jna.AppIndicator; +import dorkbox.systemTray.linux.jna.GtkSupport; import dorkbox.systemTray.swing.SwingSystemTray; import dorkbox.util.OS; import dorkbox.util.Property; -import dorkbox.util.jna.linux.AppIndicator; -import dorkbox.util.jna.linux.GtkSupport; import dorkbox.util.process.ShellProcessBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -60,168 +60,193 @@ class SystemTray { static { Class trayType = null; - // Note: AppIndicators DO NOT support tooltips. We could try to create one, by creating a GTK widget and attaching it on - // mouseover or something, but I don't know how to do that. It seems that tooltips for app-indicators are a custom job, as - // all examined ones sometimes have it (and it's more than just text), or they don't have it at all. - - if (OS.isWindows()) { - // the tray icon size in windows is DIFFERENT than on Mac (TODO: test on mac with retina stuff). - TRAY_SIZE -= 4; + boolean isJavaFxLoaded = false; + boolean isSwtLoaded = false; + try { + // First check if JavaFX is loaded - if it's NOT LOADED, then we only proceed if JAVAFX_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"); + isSwtLoaded = null != m.invoke(cl, "org.eclipse.swt.widgets.Display"); + } catch (Throwable ignored) { } - if (OS.isLinux()) { - // see: https://askubuntu.com/questions/72549/how-to-determine-which-window-manager-is-running + // maybe we should load the SWT version? (SWT's use of GTK is incompatible with how we use GTK) - // quick check, because we know that unity uses app-indicator. Maybe REALLY old versions do not. We support 14.04 LTE at least - String XDG = System.getenv("XDG_CURRENT_DESKTOP"); - if ("Unity".equalsIgnoreCase(XDG)) { - try { - trayType = AppIndicatorTray.class; - } catch (Throwable ignored) { - } - } - else if ("XFCE".equalsIgnoreCase(XDG)) { - try { - trayType = AppIndicatorTray.class; - } catch (Throwable ignored) { - // we can fail on AppIndicator, so this is the fallback - //noinspection EmptyCatchBlock - try { - trayType = GtkSystemTray.class; - } catch (Throwable i) { - } - } - } - else if ("LXDE".equalsIgnoreCase(XDG)) { - try { - trayType = GtkSystemTray.class; - } catch (Throwable ignored) { - } - } - else if ("KDE".equalsIgnoreCase(XDG)) { - isKDE = true; - try { - trayType = AppIndicatorTray.class; - } catch (Throwable ignored) { - } - } - else if ("GNOME".equalsIgnoreCase(XDG)) { - // check other DE - String GDM = System.getenv("GDMSESSION"); + if (isSwtLoaded) { - if ("cinnamon".equalsIgnoreCase(GDM)) { + } + else { + // Note: AppIndicators DO NOT support tooltips. We could try to create one, by creating a GTK widget and attaching it on + // mouseover or something, but I don't know how to do that. It seems that tooltips for app-indicators are a custom job, as + // all examined ones sometimes have it (and it's more than just text), or they don't have it at all. + + if (OS.isWindows()) { + // the tray icon size in windows is DIFFERENT than on Mac (TODO: test on mac with retina stuff). + TRAY_SIZE -= 4; + } + + if (OS.isLinux()) { + // see: https://askubuntu.com/questions/72549/how-to-determine-which-window-manager-is-running + + if (isJavaFxLoaded) { + // we MUST use GTK2 with javaFX! + GtkSupport.FORCE_GTK2 = isJavaFxLoaded; + } + + // quick check, because we know that unity uses app-indicator. Maybe REALLY old versions do not. We support 14.04 LTE at least + String XDG = System.getenv("XDG_CURRENT_DESKTOP"); + if ("Unity".equalsIgnoreCase(XDG)) { try { - trayType = GtkSystemTray.class; + trayType = AppIndicatorTray.class; } catch (Throwable ignored) { } } - else if ("gnome-classic".equalsIgnoreCase(GDM)) { + else if ("XFCE".equalsIgnoreCase(XDG)) { try { - trayType = GtkSystemTray.class; + trayType = AppIndicatorTray.class; } catch (Throwable ignored) { - } - } - else if ("gnome-fallback".equalsIgnoreCase(GDM)) { - try { - trayType = GtkSystemTray.class; - } catch (Throwable ignored) { - } - } - - - // unknown exactly, install extension and go from there - if (trayType == null) { - // if the "topicons" extension is installed, don't install us (because it will override what we do, where ours - // is more specialized - so it only modified our tray icon (instead of ALL tray icons) - - try { - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(8196); - PrintStream outputStream = new PrintStream(byteArrayOutputStream); - - // gnome-shell --version - final ShellProcessBuilder shellVersion = new ShellProcessBuilder(outputStream); - shellVersion.setExecutable("gnome-shell"); - shellVersion.addArgument("--version"); - shellVersion.start(); - - String output = ShellProcessBuilder.getOutput(byteArrayOutputStream); - - if (!output.isEmpty()) { - GnomeShellExtension.install(logger, output); - trayType = GtkSystemTray.class; - } - } catch (Throwable ignored) { - trayType = null; - } - } - } - - // Try to autodetect if we can use app indicators (or if we need to fallback to GTK indicators) - if (trayType == null) { - BufferedReader bin = null; - try { - // the ONLY guaranteed way to determine if indicator-application-service is running (and thus, using app-indicator), - // is to look through all /proc//status, and first line should be Name:\tindicator-appli - File proc = new File("/proc"); - File[] listFiles = proc.listFiles(); - if (listFiles != null) { - for (File procs : listFiles) { - String name = procs.getName(); - - if (!Character.isDigit(name.charAt(0))) { - continue; - } - - File status = new File(procs, "status"); - if (!status.canRead()) { - continue; - } - - try { - bin = new BufferedReader(new FileReader(status)); - String readLine = bin.readLine(); - - if (readLine != null && readLine.contains("indicator-app")) { - // make sure we can also load the library (it might be the wrong version) - try { - //noinspection unused - final AppIndicator instance = AppIndicator.INSTANCE; - trayType = AppIndicatorTray.class; - - if (AppIndicator.IS_VERSION_3) { - - } - } catch (Throwable e) { - logger.error("AppIndicator support detected, but unable to load the library. Falling back to GTK"); - e.printStackTrace(); - } - break; - } - } finally { - if (bin != null) { - bin.close(); - bin = null; - } - } - } - } - } catch (Throwable ignored) { - } finally { - if (bin != null) { + // we can fail on AppIndicator, so this is the fallback + //noinspection EmptyCatchBlock try { - bin.close(); - } catch (IOException ignored) { + trayType = GtkSystemTray.class; + } catch (Throwable i) { } } } - } + else if ("LXDE".equalsIgnoreCase(XDG)) { + try { + trayType = GtkSystemTray.class; + } catch (Throwable ignored) { + } + } + else if ("KDE".equalsIgnoreCase(XDG)) { + isKDE = true; + try { + trayType = AppIndicatorTray.class; + } catch (Throwable ignored) { + } + } + else if ("GNOME".equalsIgnoreCase(XDG)) { + // check other DE + String GDM = System.getenv("GDMSESSION"); + + if ("cinnamon".equalsIgnoreCase(GDM)) { + try { + trayType = GtkSystemTray.class; + } catch (Throwable ignored) { + } + } + else if ("gnome-classic".equalsIgnoreCase(GDM)) { + try { + trayType = GtkSystemTray.class; + } catch (Throwable ignored) { + } + } + else if ("gnome-fallback".equalsIgnoreCase(GDM)) { + try { + trayType = GtkSystemTray.class; + } catch (Throwable ignored) { + } + } - // fallback... - if (trayType == null) { - trayType = GtkSystemTray.class; - logger.error("Unable to load the system tray native library. Please write an issue and include your OS type and " + - "configuration"); + // unknown exactly, install extension and go from there + if (trayType == null) { + // if the "topicons" extension is installed, don't install us (because it will override what we do, where ours + // is more specialized - so it only modified our tray icon (instead of ALL tray icons) + + try { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(8196); + PrintStream outputStream = new PrintStream(byteArrayOutputStream); + + // gnome-shell --version + final ShellProcessBuilder shellVersion = new ShellProcessBuilder(outputStream); + shellVersion.setExecutable("gnome-shell"); + shellVersion.addArgument("--version"); + shellVersion.start(); + + String output = ShellProcessBuilder.getOutput(byteArrayOutputStream); + + if (!output.isEmpty()) { + GnomeShellExtension.install(logger, output); + trayType = GtkSystemTray.class; + } + } catch (Throwable ignored) { + trayType = null; + } + } + } + + // Try to autodetect if we can use app indicators (or if we need to fallback to GTK indicators) + if (trayType == null) { + BufferedReader bin = null; + try { + // the ONLY guaranteed way to determine if indicator-application-service is running (and thus, using app-indicator), + // is to look through all /proc//status, and first line should be Name:\tindicator-appli + File proc = new File("/proc"); + File[] listFiles = proc.listFiles(); + if (listFiles != null) { + for (File procs : listFiles) { + String name = procs.getName(); + + if (!Character.isDigit(name.charAt(0))) { + continue; + } + + File status = new File(procs, "status"); + if (!status.canRead()) { + continue; + } + + try { + bin = new BufferedReader(new FileReader(status)); + String readLine = bin.readLine(); + + if (readLine != null && readLine.contains("indicator-app")) { + // make sure we can also load the library (it might be the wrong version) + try { + //noinspection unused + final AppIndicator instance = AppIndicator.INSTANCE; + trayType = AppIndicatorTray.class; + + if (AppIndicator.IS_VERSION_3) { + + } + } catch (Throwable e) { + logger.error("AppIndicator support detected, but unable to load the library. Falling back to GTK"); + e.printStackTrace(); + } + break; + } + } finally { + if (bin != null) { + bin.close(); + bin = null; + } + } + } + } + } catch (Throwable ignored) { + } finally { + if (bin != null) { + try { + bin.close(); + } catch (IOException ignored) { + } + } + } + } + + + // fallback... + if (trayType == null) { + trayType = GtkSystemTray.class; + logger.error("Unable to load the system tray native library. Please write an issue and include your OS type and " + + "configuration"); + } } } @@ -237,11 +262,12 @@ class SystemTray { if (trayType == null) { // unsupported tray - logger.error("Unsupported tray type!"); + logger.error("Unable to discover what tray implementation to use!"); systemTray = null; } else { SystemTray systemTray_ = null; + try { ImageUtil.init(); @@ -251,8 +277,6 @@ class SystemTray { AppIndicator.IS_VERSION_3 && // this initializes the appindicator (since we specified that via the trayType) GtkSupport.isGtk2) { - final boolean isVersion3 = AppIndicator.IS_VERSION_3; - // NOTE: // ALSO WHAT VERSION OF GTK to use? appindiactor1 -> GTk2, appindicator3 -> GTK3. // appindicator3 doesn't support menu icons via GTK2. AT THIS POINT, we DO NOT have GTK3 @@ -280,22 +304,9 @@ class SystemTray { systemTray = systemTray_; - // Necessary because javaFX **ALSO** runs a gtk main loop, and when it stops (if we don't stop first), we become unresponsive. // we ONLY need this on linux for compatibility with JavaFX... (windows/mac don't use gtk) if (OS.isLinux()) { - boolean isJavaFxLoaded = false; - - try { - // First check if JavaFX is loaded - if it's NOT LOADED, then we only proceed if JAVAFX_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"); - } catch (Throwable ignored) { - } - if (isJavaFxLoaded || GtkSupport.JAVAFX_COMPATIBILITY_MODE) { // com.sun.javafx.tk.Toolkit.getToolkit() // .addShutdownHook(new Runnable() { diff --git a/src/dorkbox/systemTray/linux/AppIndicatorTray.java b/src/dorkbox/systemTray/linux/AppIndicatorTray.java index 1b2423d..ffb740c 100644 --- a/src/dorkbox/systemTray/linux/AppIndicatorTray.java +++ b/src/dorkbox/systemTray/linux/AppIndicatorTray.java @@ -16,8 +16,8 @@ package dorkbox.systemTray.linux; import com.sun.jna.Pointer; -import dorkbox.util.jna.linux.AppIndicator; -import dorkbox.util.jna.linux.GtkSupport; +import dorkbox.systemTray.linux.jna.AppIndicator; +import dorkbox.systemTray.linux.jna.GtkSupport; /** * Class for handling all system tray interactions. diff --git a/src/dorkbox/systemTray/linux/GtkMenuEntry.java b/src/dorkbox/systemTray/linux/GtkMenuEntry.java index e282f8e..4207248 100644 --- a/src/dorkbox/systemTray/linux/GtkMenuEntry.java +++ b/src/dorkbox/systemTray/linux/GtkMenuEntry.java @@ -20,10 +20,10 @@ import com.sun.jna.Pointer; import dorkbox.systemTray.ImageUtil; import dorkbox.systemTray.MenuEntry; import dorkbox.systemTray.SystemTrayMenuAction; -import dorkbox.util.jna.linux.Gobject; -import dorkbox.util.jna.linux.Gobject.GCallback; -import dorkbox.util.jna.linux.Gtk; -import dorkbox.util.jna.linux.GtkSupport; +import dorkbox.systemTray.linux.jna.Gobject; +import dorkbox.systemTray.linux.jna.Gobject.GCallback; +import dorkbox.systemTray.linux.jna.Gtk; +import dorkbox.systemTray.linux.jna.GtkSupport; import java.io.InputStream; import java.net.URL; diff --git a/src/dorkbox/systemTray/linux/GtkSystemTray.java b/src/dorkbox/systemTray/linux/GtkSystemTray.java index 2968bc0..84db898 100644 --- a/src/dorkbox/systemTray/linux/GtkSystemTray.java +++ b/src/dorkbox/systemTray/linux/GtkSystemTray.java @@ -17,9 +17,9 @@ package dorkbox.systemTray.linux; import com.sun.jna.NativeLong; import com.sun.jna.Pointer; -import dorkbox.util.jna.linux.Gobject; -import dorkbox.util.jna.linux.Gtk; -import dorkbox.util.jna.linux.GtkSupport; +import dorkbox.systemTray.linux.jna.Gobject; +import dorkbox.systemTray.linux.jna.Gtk; +import dorkbox.systemTray.linux.jna.GtkSupport; /** * Class for handling all system tray interactions via GTK. diff --git a/src/dorkbox/systemTray/linux/GtkTypeSystemTray.java b/src/dorkbox/systemTray/linux/GtkTypeSystemTray.java index 48ad03c..77b84f2 100644 --- a/src/dorkbox/systemTray/linux/GtkTypeSystemTray.java +++ b/src/dorkbox/systemTray/linux/GtkTypeSystemTray.java @@ -21,9 +21,9 @@ import dorkbox.systemTray.ImageUtil; import dorkbox.systemTray.SystemTray; import dorkbox.systemTray.SystemTrayMenuAction; import dorkbox.util.NamedThreadFactory; -import dorkbox.util.jna.linux.Gobject; -import dorkbox.util.jna.linux.Gtk; -import dorkbox.util.jna.linux.GtkSupport; +import dorkbox.systemTray.linux.jna.Gobject; +import dorkbox.systemTray.linux.jna.Gtk; +import dorkbox.systemTray.linux.jna.GtkSupport; import java.io.InputStream; import java.net.URL; diff --git a/src/dorkbox/systemTray/linux/jna/AppIndicator.java b/src/dorkbox/systemTray/linux/jna/AppIndicator.java new file mode 100644 index 0000000..9106945 --- /dev/null +++ b/src/dorkbox/systemTray/linux/jna/AppIndicator.java @@ -0,0 +1,69 @@ +/* + * 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.Library; +import com.sun.jna.Pointer; +import com.sun.jna.Structure; +import dorkbox.util.Keep; + +import java.util.Arrays; +import java.util.List; + +/* bindings for libappindicator */ +public +interface AppIndicator extends Library { + // effing retarded. There are DIFFERENT versions, of which they all share the same basic compatibility (of the methods that + // we use), however -- we cannot just LOAD via the 'base-name', we actually have to try each one. There are bash commands that + // will tell us the linked library name, however - I'd rather not run bash commands to determine this. + // This is so hacky it makes me sick. + AppIndicator INSTANCE = AppIndicatorQuery.get(); + + /** Necessary to provide warnings, because libappindicator3 won't properly work with GTK2 */ + boolean IS_VERSION_3 = AppIndicatorQuery.isVersion3; + + int CATEGORY_APPLICATION_STATUS = 0; + int CATEGORY_COMMUNICATIONS = 1; + int CATEGORY_SYSTEM_SERVICES = 2; + int CATEGORY_HARDWARE = 3; + int CATEGORY_OTHER = 4; + + int STATUS_PASSIVE = 0; + int STATUS_ACTIVE = 1; + int STATUS_ATTENTION = 2; + + + @Keep + class AppIndicatorInstanceStruct extends Structure { + public Gobject.GObjectStruct parent; + public Pointer priv; + + @Override + protected + List getFieldOrder() { + return Arrays.asList("parent", "priv"); + } + } + + // Note: AppIndicators DO NOT support tooltips, as per mark shuttleworth. Rather stupid IMHO. + // See: https://bugs.launchpad.net/indicator-application/+bug/527458/comments/12 + + AppIndicatorInstanceStruct app_indicator_new(String id, String icon_name, int category); + + void app_indicator_set_status(AppIndicatorInstanceStruct self, int status); + void app_indicator_set_menu(AppIndicatorInstanceStruct self, Pointer menu); + void app_indicator_set_icon(AppIndicatorInstanceStruct self, String icon_name); +} diff --git a/src/dorkbox/systemTray/linux/jna/AppIndicatorQuery.java b/src/dorkbox/systemTray/linux/jna/AppIndicatorQuery.java new file mode 100644 index 0000000..c7eadfa --- /dev/null +++ b/src/dorkbox/systemTray/linux/jna/AppIndicatorQuery.java @@ -0,0 +1,120 @@ +/* + * 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.Native; + +/** + * Helper for AppIndicator, because it is absolutely mindboggling how those whom maintain the standard, can't agree to what that standard + * library naming convention or features set is. We just try until we find one that work, and are able to map the symbols we need. + */ +class AppIndicatorQuery { + + /** + * must call get() before accessing this! Only "AppIndicator" interface should access this! + */ + static volatile boolean isVersion3 = false; + + /** + * Is AppIndicator loaded yet? + */ + static volatile boolean isLoaded = false; + + + public static + AppIndicator get() { + Object library; + + // NOTE: GtkSupport uses this info to figure out WHAT VERSION OF GTK to use: appindiactor1 -> GTk2, appindicator3 -> GTK3. + + if (GtkSupport.FORCE_GTK2) { + // try loading appindicator1 first, maybe it's there? + + try { + library = Native.loadLibrary("appindicator1", AppIndicator.class); + if (library != null) { + return (AppIndicator) library; + } + } catch (Throwable ignored) { + } + } + + // start with base version + try { + library = Native.loadLibrary("appindicator", AppIndicator.class); + if (library != null) { + String s = library.toString(); + if (s.indexOf("appindicator3") > 0) { + isVersion3 = true; + } + + isLoaded = true; + return (AppIndicator) library; + } + } catch (Throwable ignored) { + } + + // whoops. Symbolic links are bugged out. Look manually for it... + + try { + library = Native.loadLibrary("appindicator1", AppIndicator.class); + if (library != null) { + return (AppIndicator) library; + } + } catch (Throwable ignored) { + } + + // now check all others. super hacky way to do this. + for (int i = 10; i >= 0; i--) { + try { + library = Native.loadLibrary("appindicator" + i, AppIndicator.class); + } catch (Throwable ignored) { + library = null; + } + + if (library != null) { + String s = library.toString(); + // version 3 WILL NOT work with icons in the menu. This allows us to show a warning (in the System tray initialization) + if (i == 3 || s.indexOf("appindicator3") > 0) { + isVersion3 = true; + } + return (AppIndicator) library; + } + } + + // another type. who knows... + try { + library = Native.loadLibrary("appindicator-gtk", AppIndicator.class); + if (library != null) { + return (AppIndicator) library; + } + } catch (Throwable ignored) { + } + + // this is HORRID. such a PITA + try { + library = Native.loadLibrary("appindicator-gtk3", AppIndicator.class); + if (library != null) { + return (AppIndicator) library; + } + } catch (Throwable ignored) { + } + + throw new RuntimeException("We apologize for this, but we are unable to determine the appIndicator library is in use, if " + + "or even if it is in use... Please create an issue for this and include your OS type and configuration."); + } +} diff --git a/src/dorkbox/systemTray/linux/jna/GThread.java b/src/dorkbox/systemTray/linux/jna/GThread.java new file mode 100644 index 0000000..3ce9e09 --- /dev/null +++ b/src/dorkbox/systemTray/linux/jna/GThread.java @@ -0,0 +1,27 @@ +/* + * 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.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; + +public +interface GThread extends Library { + GThread INSTANCE = (GThread) Native.loadLibrary("gthread-2.0", GThread.class); + + void g_thread_init(Pointer GThreadFunctions); +} diff --git a/src/dorkbox/systemTray/linux/jna/Gobject.java b/src/dorkbox/systemTray/linux/jna/Gobject.java new file mode 100644 index 0000000..052aeba --- /dev/null +++ b/src/dorkbox/systemTray/linux/jna/Gobject.java @@ -0,0 +1,174 @@ +/* + * 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.*; +import dorkbox.util.Keep; +import dorkbox.systemTray.linux.jna.Gtk.GdkEventButton; + +import java.util.Arrays; +import java.util.List; + +public +interface Gobject extends Library { + Gobject INSTANCE = (Gobject) Native.loadLibrary("gobject-2.0", Gobject.class); + + @Keep + class GTypeClassStruct extends Structure { + public + class ByValue extends GTypeClassStruct implements Structure.ByValue {} + + + public + class ByReference extends GTypeClassStruct implements Structure.ByReference {} + + + public NativeLong g_type; + + @Override + protected + List getFieldOrder() { + return Arrays.asList("g_type"); + } + } + + + @Keep + class GTypeInstanceStruct extends Structure { + public + class ByValue extends GTypeInstanceStruct implements Structure.ByValue {} + + + public + class ByReference extends GTypeInstanceStruct implements Structure.ByReference {} + + + public Pointer g_class; + + @Override + protected + List getFieldOrder() { + return Arrays.asList("g_class"); + } + } + + + @Keep + class GObjectStruct extends Structure { + public + class ByValue extends GObjectStruct implements Structure.ByValue {} + + + public + class ByReference extends GObjectStruct implements Structure.ByReference {} + + + public GTypeInstanceStruct g_type_instance; + public int ref_count; + public Pointer qdata; + + @Override + protected + List getFieldOrder() { + return Arrays.asList("g_type_instance", "ref_count", "qdata"); + } + } + + + @Keep + class GObjectClassStruct extends Structure { + public + class ByValue extends GObjectClassStruct implements Structure.ByValue {} + + + public + class ByReference extends GObjectClassStruct implements Structure.ByReference {} + + + public GTypeClassStruct g_type_class; + public Pointer construct_properties; + public Pointer constructor; + public Pointer set_property; + public Pointer get_property; + public Pointer dispose; + public Pointer finalize; + public Pointer dispatch_properties_changed; + public Pointer notify; + public Pointer constructed; + public NativeLong flags; + public Pointer dummy1; + public Pointer dummy2; + public Pointer dummy3; + public Pointer dummy4; + public Pointer dummy5; + public Pointer dummy6; + + @Override + protected + List getFieldOrder() { + return Arrays.asList("g_type_class", "construct_properties", "constructor", "set_property", "get_property", "dispose", + "finalize", "dispatch_properties_changed", "notify", "constructed", "flags", "dummy1", "dummy2", "dummy3", + "dummy4", "dummy5", "dummy6"); + } + } + + + @Keep + interface GCallback extends Callback { + /** + * @return Gtk.TRUE if we handled this event + */ + int callback(Pointer instance, Pointer data); + } + + + @Keep + interface GEventCallback extends Callback { + void callback(Pointer instance, GdkEventButton event); + } + + + @Keep + class xyPointer extends Structure { + public int value; + + @Override + protected + List getFieldOrder() { + return Arrays.asList("value"); + } + } + + + @Keep + interface GPositionCallback extends Callback { + void callback(Pointer menu, xyPointer x, xyPointer y, Pointer push_in_bool, Pointer user_data); + } + + + + void g_free(Pointer object); + void g_object_ref(Pointer object); + void g_object_unref(Pointer object); + void g_object_ref_sink(Pointer object); + + NativeLong g_signal_connect_data(Pointer instance, String detailed_signal, Callback c_handler, Pointer data, Pointer destroy_data, + int connect_flags); + + void g_signal_handler_disconnect(Pointer instance, NativeLong longAddress); + + Pointer g_markup_printf_escaped(String pattern, String inputString); +} diff --git a/src/dorkbox/systemTray/linux/jna/Gtk.java b/src/dorkbox/systemTray/linux/jna/Gtk.java new file mode 100644 index 0000000..1d25820 --- /dev/null +++ b/src/dorkbox/systemTray/linux/jna/Gtk.java @@ -0,0 +1,140 @@ +/* + * 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.Function; +import com.sun.jna.Library; +import com.sun.jna.Pointer; +import com.sun.jna.Structure; +import dorkbox.util.Keep; + +import java.util.Arrays; +import java.util.List; + +public +interface Gtk extends Library { + + // 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 + Gtk INSTANCE = GtkSupport.get(); + Function gtk_status_icon_position_menu = GtkSupport.gtk_status_icon_position_menu; + + int FALSE = 0; + int TRUE = 1; + + + @Keep + class GdkEventButton extends Structure { + public int type; + public Pointer window; + public int send_event; + public int time; + public double x; + public double y; + public Pointer axes; + public int state; + public int button; + public Pointer device; + public double x_root; + public double y_root; + + @Override + protected + List getFieldOrder() { + return Arrays.asList("type", "window", "send_event", "time", "x", "y", "axes", "state", "button", "device", "x_root", "y_root"); + } + } + + boolean gtk_init_check(int argc, String[] argv); + + /** + * Runs the main loop until gtk_main_quit() is called. You can nest calls to gtk_main(). In that case gtk_main_quit() will make the + * innermost invocation of the main loop return. + */ + void gtk_main(); + + + /** sks for the current nesting level of the main loop. Useful to determine (at startup) if GTK is already runnign */ + int gtk_main_level(); + + /** + * Makes the innermost invocation of the main loop return when it regains control. ONLY CALL FROM THE GtkSupport class, UNLESS you know + * what you're doing! + */ + void gtk_main_quit(); + + void gdk_threads_init(); + + // tricky business. This should only be in the dispatch thread + void gdk_threads_enter(); + void gdk_threads_leave(); + + Pointer gtk_menu_new(); + + Pointer gtk_menu_item_new(); + + Pointer gtk_menu_item_new_with_label(String label); + + // to create a menu entry WITH an icon. + Pointer gtk_image_new_from_file(String iconPath); + + + Pointer gtk_image_menu_item_new_with_label(String label); + + void gtk_image_menu_item_set_image(Pointer image_menu_item, Pointer image); + + void gtk_image_menu_item_set_always_show_image(Pointer menu_item, int forceShow); + + Pointer gtk_bin_get_child(Pointer parent); + + void gtk_label_set_text(Pointer label, String text); + + void gtk_label_set_markup(Pointer label, Pointer markup); + + void gtk_label_set_use_markup(Pointer label, int gboolean); + + Pointer gtk_status_icon_new(); + + void gtk_status_icon_set_from_file(Pointer widget, String lablel); + + void gtk_status_icon_set_visible(Pointer widget, boolean visible); + + // app indicators don't support this, and we cater to the lowest common denominator +// void gtk_status_icon_set_tooltip(Pointer widget, String tooltipText); + + void gtk_status_icon_set_title(Pointer widget, String titleText); + + void gtk_status_icon_set_name(Pointer widget, String name); + + void gtk_menu_popup(Pointer menu, Pointer widget, Pointer bla, Function func, Pointer data, int button, int time); + + void gtk_menu_item_set_label(Pointer menu_item, String label); + + void gtk_menu_shell_append(Pointer menu_shell, Pointer child); + + void gtk_menu_shell_deactivate(Pointer menu_shell, Pointer child); + + void gtk_widget_set_sensitive(Pointer widget, int sensitive); + + void gtk_container_remove(Pointer menu, Pointer subItem); + + void gtk_widget_show(Pointer widget); + + void gtk_widget_show_all(Pointer widget); + + void gtk_widget_destroy(Pointer widget); +} + diff --git a/src/dorkbox/systemTray/linux/jna/GtkSupport.java b/src/dorkbox/systemTray/linux/jna/GtkSupport.java new file mode 100644 index 0000000..758346c --- /dev/null +++ b/src/dorkbox/systemTray/linux/jna/GtkSupport.java @@ -0,0 +1,264 @@ +/* + * 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.Function; +import com.sun.jna.Native; +import dorkbox.util.Property; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.CountDownLatch; + +public +class GtkSupport { + // RE: SWT + // https://developer.gnome.org/glib/stable/glib-Deprecated-Thread-APIs.html#g-thread-init + // Since version >= 2.24, threads can only init once. Multiple calls do nothing, and we can nest gtk_main() + // in a nested loop. + + private static volatile boolean started = false; + private static final ArrayBlockingQueue dispatchEvents = new ArrayBlockingQueue(256); + private static volatile Thread gtkDispatchThread; + + @Property + /** Forces the system to always choose GTK2 (even when GTK3 might be available). JavaFX uses GTK2! */ + public static boolean FORCE_GTK2 = false; + + @Property + /** + * Forces the system to enter into JavaFX 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 JAVAFX_COMPATIBILITY_MODE = false; + + /** + * must call get() before accessing this! Only "Gtk" interface should access this! + */ + static volatile Function gtk_status_icon_position_menu = null; + + public static volatile boolean isGtk2 = false; + + private static volatile boolean alreadyRunningGTK = false; + + /** + * Helper for GTK, because we could have v3 or v2. + * + * Observations: JavaFX uses GTK2, and we can't load GTK3 if GTK2 symbols are loaded + */ + @SuppressWarnings("Duplicates") + public static + Gtk get() { + Gtk library; + + boolean shouldUseGtk2 = GtkSupport.FORCE_GTK2 || JAVAFX_COMPATIBILITY_MODE; + alreadyRunningGTK = JAVAFX_COMPATIBILITY_MODE; + + + // 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+. + + // in some cases, we ALWAYS want to try GTK2 first + if (shouldUseGtk2) { + try { + gtk_status_icon_position_menu = Function.getFunction("gtk-x11-2.0", "gtk_status_icon_position_menu"); + library = (Gtk) Native.loadLibrary("gtk-x11-2.0", Gtk.class); + if (library != null) { + isGtk2 = true; + + // 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 |= library.gtk_main_level() != 0; + return library; + } + } catch (Throwable ignored) { + } + } + + if (AppIndicatorQuery.isLoaded) { + if (AppIndicatorQuery.isVersion3) { + // appindicator3 requires GTK3 + try { + gtk_status_icon_position_menu = Function.getFunction("libgtk-3.so.0", "gtk_status_icon_position_menu"); + library = (Gtk) Native.loadLibrary("libgtk-3.so.0", Gtk.class); + if (library != null) { + // 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 |= library.gtk_main_level() != 0; + return library; + } + } catch (Throwable ignored) { + } + } else { + // appindicator1 requires GTK2 + try { + gtk_status_icon_position_menu = Function.getFunction("gtk-x11-2.0", "gtk_status_icon_position_menu"); + library = (Gtk) Native.loadLibrary("gtk-x11-2.0", Gtk.class); + if (library != null) { + isGtk2 = true; + + // 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 |= library.gtk_main_level() != 0; + return library; + } + } catch (Throwable ignored) { + } + } + } + + // now for the defaults... + + // start with version 3 + try { + gtk_status_icon_position_menu = Function.getFunction("libgtk-3.so.0", "gtk_status_icon_position_menu"); + library = (Gtk) Native.loadLibrary("libgtk-3.so.0", Gtk.class); + if (library != null) { + // 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 |= library.gtk_main_level() != 0; + return library; + } + } catch (Throwable ignored) { + } + + // now version 2 + try { + gtk_status_icon_position_menu = Function.getFunction("gtk-x11-2.0", "gtk_status_icon_position_menu"); + library = (Gtk) Native.loadLibrary("gtk-x11-2.0", Gtk.class); + if (library != null) { + isGtk2 = true; + + // 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 |= library.gtk_main_level() != 0; + return library; + } + } catch (Throwable ignored) { + } + + throw new RuntimeException("We apologize for this, but we are unable to determine the GTK library is in use, if " + + "or even if it is in use... Please create an issue for this and include your OS type and configuration."); + } + + public static + void startGui() { + // only permit one startup per JVM instance + if (!started) { + started = true; + + // GTK specifies that we ONLY run from a single thread. This guarantees that. + gtkDispatchThread = new Thread() { + @Override + public + void run() { + final Gtk gtk = Gtk.INSTANCE; + while (started) { + try { + final Runnable take = dispatchEvents.take(); + + gtk.gdk_threads_enter(); + take.run(); + gtk.gdk_threads_leave(); + + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + }; + gtkDispatchThread.setName("GTK Event Loop"); + gtkDispatchThread.start(); + + + // 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); + Thread gtkUpdateThread = new Thread() { + @Override + public + void run() { + Gtk gtk = Gtk.INSTANCE; + + // prep for the event loop. + gtk.gdk_threads_init(); + gtk.gdk_threads_enter(); + + GThread.INSTANCE.g_thread_init(null); + gtk.gtk_init_check(0, null); + + // notify our main thread to continue + blockUntilStarted.countDown(); + + // blocks unit quit + gtk.gtk_main(); + + gtk.gdk_threads_leave(); + } + }; + gtkUpdateThread.setName("GTK Event Loop (Native)"); + gtkUpdateThread.start(); + + try { + // we CANNOT continue until the GTK thread has started! + blockUntilStarted.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + } + + /** + * Best practices for GTK, is to call EVERYTHING for it on a SINGLE THREAD. This accomplishes that. + */ + public static + void dispatch(Runnable runnable) { + try { + dispatchEvents.put(runnable); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public static + void shutdownGui() { + // If JavaFX/SWT is used, this is UNNECESSARY (an will break SWT/JavaFX shutdown) + if (!alreadyRunningGTK) { + Gtk.INSTANCE.gtk_main_quit(); + } + + started = false; + + // put it in a NEW dispatch event (so that we cleanup AFTER this one is finished) + dispatch(new Runnable() { + @Override + public + void run() { + new Thread(new Runnable() { + @Override + public + void run() { + // this should happen in a new thread + gtkDispatchThread.interrupt(); + } + }).run(); + } + }); + } +}