diff --git a/README.md b/README.md index 769177c..722dd0c 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,10 @@ There are a number of problems on Linux with the Swing (and SWT) system-tray ico This is for cross-platform use, specifically - linux 32/64, mac 32/64, and windows 32/64. Java 6+ +We also cater to the *lowest-common-denominator* when it comes to system-tray/indicator functionality, and there are some features that we don't support. +Specifically, **tooltips**. Rather a stupid decision, IMHO, but for more information why ask Mark Shuttleworth. +See: https://bugs.launchpad.net/indicator-application/+bug/527458/comments/12 + ``` Customization parameters: @@ -88,16 +92,34 @@ Note: This project was heavily influenced by the excellent Lantern project, ``` Note: Gnome-shell users will experience an extension install to support this functionality. Additionally, a shell restart is necessary for the extension - to be registered by the shell. You can disable the restart behavior if you like, - and the 'system tray' functionality will be picked up on log out/in, or a - system restart. + to be registered by the shell. You can disable the restart behavior if you + like, and the 'system tray' functionality will be picked up on log out/in, + or a system restart. Also, screw you gnome-project leads, for making it such a pain-in-the-ass to do something so incredibly simple and basic. Note: Some desktop environments might use a dated version of libappindicator, when icon support in menus was removed, then put back. This happened in version 3. - This library will try to load a GTK indicator instead when it can, or will try - to load libappindicator1 first. Thank you RedHat for putting it back. + This library will try to load a GTK indicator instead when it can, or will + try to load libappindicator1 first. Thank you RedHat for putting it back. + + +ISSUES: + 'Trying to remove a child that doesn't believe we're it's parent.' + + This is a known appindicator bug, and is rather old. Some distributions use + an OLD version of libappindicator, and will see this error. + See: https://github.com/ValveSoftware/steam-for-linux/issues/1077 + + + 'gsignal.c: signal 'child-added' is invalid for instance 'xyz' of type 'GtkMenu'' + This is a known appindicator bug, and is rather old. Some distributions use an + OLD version of libappindicator, and will see this error. + + The fallout from this issue (ie: menu entries not displaying) has been + *worked around*, so the menus should still show correctly. + See: https://askubuntu.com/questions/364594/has-the-appindicator-or-gtkmenu-api-changed-in-saucy + ``` diff --git a/src/dorkbox/util/tray/SystemTray.java b/src/dorkbox/util/tray/SystemTray.java index 68f5306..5588f7a 100644 --- a/src/dorkbox/util/tray/SystemTray.java +++ b/src/dorkbox/util/tray/SystemTray.java @@ -95,7 +95,7 @@ class SystemTray { } } else if ("XFCE".equalsIgnoreCase(XDG)) { - // XFCE uses a OLD version of appindicators, which DO NOT support images in the menu. + // XFCE uses a BAD version of libappindicator by default, which DOES NOT support images in the menu. try { trayType = GtkSystemTray.class; } catch (Throwable ignored) { @@ -107,6 +107,12 @@ class SystemTray { } catch (Throwable ignored) { } } + else if ("KDE".equalsIgnoreCase(XDG)) { + try { + trayType = AppIndicatorTray.class; + } catch (Throwable ignored) { + } + } else if ("GNOME".equalsIgnoreCase(XDG)) { // check other DE String GDM = System.getenv("GDMSESSION"); @@ -331,17 +337,15 @@ class SystemTray { * @param origMenuText the original menu text * @param newMenuText the new menu text (this will replace the original menu text) */ - public final + public final synchronized void updateMenuEntry_Text(String origMenuText, String newMenuText) { - synchronized (this.menuEntries) { - MenuEntry menuEntry = getMenuEntry(origMenuText); + MenuEntry menuEntry = getMenuEntry(origMenuText); - if (menuEntry == null) { - throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'"); - } - else { - menuEntry.setText(newMenuText); - } + if (menuEntry == null) { + throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'"); + } + else { + menuEntry.setText(newMenuText); } } @@ -352,17 +356,15 @@ class SystemTray { * @param origMenuText the original menu text * @param imagePath the new path for the image to use. Null to remove an image. */ - public final + public final synchronized void updateMenuEntry_Image(String origMenuText, String imagePath) { - synchronized (this.menuEntries) { - MenuEntry menuEntry = getMenuEntry(origMenuText); + MenuEntry menuEntry = getMenuEntry(origMenuText); - if (menuEntry == null) { - throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'"); - } - else { - menuEntry.setImage(imagePath); - } + if (menuEntry == null) { + throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'"); + } + else { + menuEntry.setImage(imagePath); } } @@ -373,17 +375,15 @@ class SystemTray { * @param origMenuText the original menu text * @param newCallback the new callback (this will replace the original callback) */ - public final + public final synchronized void updateMenuEntry_Callback(String origMenuText, SystemTrayMenuAction newCallback) { - synchronized (this.menuEntries) { - MenuEntry menuEntry = getMenuEntry(origMenuText); + MenuEntry menuEntry = getMenuEntry(origMenuText); - if (menuEntry != null) { - menuEntry.setCallback(newCallback); - } - else { - throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'"); - } + if (menuEntry != null) { + menuEntry.setCallback(newCallback); + } + else { + throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'"); } } @@ -395,18 +395,16 @@ class SystemTray { * @param newMenuText the new menu text (this will replace the original menu text) * @param newCallback the new callback (this will replace the original callback) */ - public final + public final synchronized void updateMenuEntry(String origMenuText, String newMenuText, SystemTrayMenuAction newCallback) { - synchronized (this.menuEntries) { - MenuEntry menuEntry = getMenuEntry(origMenuText); + MenuEntry menuEntry = getMenuEntry(origMenuText); - if (menuEntry == null) { - throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'"); - } - else { - menuEntry.setText(newMenuText); - menuEntry.setCallback(newCallback); - } + if (menuEntry == null) { + throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'"); + } + else { + menuEntry.setText(newMenuText); + menuEntry.setCallback(newCallback); } } @@ -416,27 +414,26 @@ class SystemTray { * * @param menuEntry This is the menu entry to remove */ - public final + public final synchronized void removeMenuEntry(final MenuEntry menuEntry) { if (menuEntry == null) { throw new NullPointerException("No menu entry exists for menuEntry"); } - synchronized (this.menuEntries) { - final String label = menuEntry.getText(); + final String label = menuEntry.getText(); - for (Iterator iterator = menuEntries.iterator(); iterator.hasNext(); ) { - final MenuEntry entry = iterator.next(); - if (entry.getText() - .equals(label)) { - iterator.remove(); + for (Iterator iterator = menuEntries.iterator(); iterator.hasNext(); ) { + final MenuEntry entry = iterator.next(); + if (entry.getText() + .equals(label)) { + iterator.remove(); - menuEntry.remove(); - return; - } + // this will also reset the menu + menuEntry.remove(); + return; } - throw new NullPointerException("Menu entry '" + label + "'not found in list while trying to remove it."); } + throw new NullPointerException("Menu entry '" + label + "'not found in list while trying to remove it."); } @@ -445,22 +442,22 @@ class SystemTray { * * @param menuText This is the label for the menu entry to remove */ - public final + public final synchronized void removeMenuEntry(final String menuText) { - synchronized (this.menuEntries) { - MenuEntry menuEntry = getMenuEntry(menuText); + MenuEntry menuEntry = getMenuEntry(menuText); - if (menuEntry == null) { - throw new NullPointerException("No menu entry exists for string '" + menuText + "'"); - } - else { - removeMenuEntry(menuEntry); - } + if (menuEntry == null) { + throw new NullPointerException("No menu entry exists for string '" + menuText + "'"); + } + else { + removeMenuEntry(menuEntry); } } /** + * UNSAFE. must be called inside sync + * * appIndicator/gtk require strings (which is the path) * swing version loads as an image (which can be stream or path, we use path) */ diff --git a/src/dorkbox/util/tray/linux/AppIndicatorTray.java b/src/dorkbox/util/tray/linux/AppIndicatorTray.java index 19f9f80..646543e 100644 --- a/src/dorkbox/util/tray/linux/AppIndicatorTray.java +++ b/src/dorkbox/util/tray/linux/AppIndicatorTray.java @@ -32,18 +32,14 @@ public class AppIndicatorTray extends GtkTypeSystemTray { private static final AppIndicator libappindicator = AppIndicator.INSTANCE; - private volatile AppIndicator.AppIndicatorInstanceStruct appIndicator; + private AppIndicator.AppIndicatorInstanceStruct appIndicator; public AppIndicatorTray(String iconName) { libgtk.gdk_threads_enter(); - this.appIndicator = libappindicator.app_indicator_new(System.nanoTime() + "DBST", "indicator-messages-new", + String icon_name = iconPath(iconName); + this.appIndicator = libappindicator.app_indicator_new(System.nanoTime() + "DBST", icon_name, AppIndicator.CATEGORY_APPLICATION_STATUS); - - this.menu = libgtk.gtk_menu_new(); - libappindicator.app_indicator_set_menu(this.appIndicator, this.menu); - - libappindicator.app_indicator_set_icon(this.appIndicator, iconPath(iconName)); libappindicator.app_indicator_set_status(this.appIndicator, AppIndicator.STATUS_ACTIVE); libgtk.gdk_threads_leave(); @@ -52,13 +48,12 @@ class AppIndicatorTray extends GtkTypeSystemTray { } @Override - public + public synchronized void shutdown() { libgtk.gdk_threads_enter(); // this hides the indicator libappindicator.app_indicator_set_status(this.appIndicator, AppIndicator.STATUS_PASSIVE); - this.appIndicator.write(); Pointer p = this.appIndicator.getPointer(); libgobject.g_object_unref(p); @@ -70,10 +65,19 @@ class AppIndicatorTray extends GtkTypeSystemTray { } @Override - public + public synchronized void setIcon(final String iconName) { libgtk.gdk_threads_enter(); libappindicator.app_indicator_set_icon(this.appIndicator, iconPath(iconName)); libgtk.gdk_threads_leave(); } + + + /** + * Called inside the gdk_threads block. MUST BE AFTER THE ITEM IS ADDED/CHANGED from the menu + */ + protected + void onMenuAdded(final Pointer menu) { + libappindicator.app_indicator_set_menu(this.appIndicator, menu); + } } diff --git a/src/dorkbox/util/tray/linux/GtkMenuEntry.java b/src/dorkbox/util/tray/linux/GtkMenuEntry.java index 5b3a312..c13c252 100644 --- a/src/dorkbox/util/tray/linux/GtkMenuEntry.java +++ b/src/dorkbox/util/tray/linux/GtkMenuEntry.java @@ -21,7 +21,6 @@ import dorkbox.util.jna.linux.Gobject; import dorkbox.util.jna.linux.Gobject.GCallback; import dorkbox.util.jna.linux.Gtk; import dorkbox.util.tray.MenuEntry; -import dorkbox.util.tray.SystemTray; import dorkbox.util.tray.SystemTrayMenuAction; class GtkMenuEntry implements MenuEntry { @@ -29,17 +28,18 @@ class GtkMenuEntry implements MenuEntry { private static final Gobject libgobject = Gobject.INSTANCE; private final GCallback gtkCallback; - private final Pointer menuItem; + final Pointer menuItem; private final Pointer parentMenu; - private final SystemTray systemTray; + final GtkTypeSystemTray systemTray; private final NativeLong nativeLong; + // these have to be volatile, because they can be changed from any thread private volatile String text; private volatile SystemTrayMenuAction callback; private volatile Pointer image; GtkMenuEntry(final Pointer parentMenu, final String label, final String imagePath, final SystemTrayMenuAction callback, - final SystemTray systemTray) { + final GtkTypeSystemTray systemTray) { this.parentMenu = parentMenu; this.text = label; this.callback = callback; @@ -55,7 +55,6 @@ class GtkMenuEntry implements MenuEntry { } }; - menuItem = libgtk.gtk_image_menu_item_new_with_label(label); if (imagePath != null && !imagePath.isEmpty()) { @@ -70,9 +69,6 @@ class GtkMenuEntry implements MenuEntry { } nativeLong = libgobject.g_signal_connect_data(menuItem, "activate", gtkCallback, null, null, 0); - libgtk.gtk_menu_shell_append(parentMenu, menuItem); - - libgtk.gtk_widget_show_all(menuItem); } private @@ -117,7 +113,10 @@ class GtkMenuEntry implements MenuEntry { if (image != null) { libgtk.gtk_widget_destroy(image); } + libgtk.gtk_widget_show_all(parentMenu); + libgtk.gdk_threads_leave(); + libgtk.gdk_threads_enter(); image = libgtk.gtk_image_new_from_file(imagePath); libgtk.gtk_image_menu_item_set_image(menuItem, image); @@ -125,7 +124,6 @@ class GtkMenuEntry implements MenuEntry { // must always re-set always-show after setting the image libgtk.gtk_image_menu_item_set_always_show_image(menuItem, Gtk.TRUE); } - libgtk.gtk_widget_show_all(parentMenu); libgtk.gdk_threads_leave(); @@ -137,10 +135,23 @@ class GtkMenuEntry implements MenuEntry { this.callback = callback; } + /** + * This is ONLY called via systray.menuEntry.remove() !! + */ public void remove() { libgtk.gdk_threads_enter(); + removePrivate(); + + // have to rebuild the menu now... + systemTray.deleteMenu(); + systemTray.createMenu(); + + libgtk.gdk_threads_leave(); + } + + void removePrivate() { libgobject.g_signal_handler_disconnect(menuItem, nativeLong); libgtk.gtk_menu_shell_deactivate(parentMenu, menuItem); @@ -148,8 +159,6 @@ class GtkMenuEntry implements MenuEntry { libgtk.gtk_widget_destroy(image); } libgtk.gtk_widget_destroy(menuItem); - - libgtk.gdk_threads_leave(); } @Override diff --git a/src/dorkbox/util/tray/linux/GtkSystemTray.java b/src/dorkbox/util/tray/linux/GtkSystemTray.java index 0466ee5..5af2555 100644 --- a/src/dorkbox/util/tray/linux/GtkSystemTray.java +++ b/src/dorkbox/util/tray/linux/GtkSystemTray.java @@ -15,6 +15,7 @@ */ package dorkbox.util.tray.linux; +import com.sun.jna.NativeLong; import com.sun.jna.Pointer; import dorkbox.util.jna.linux.Gobject; import dorkbox.util.jna.linux.Gtk; @@ -27,11 +28,13 @@ import dorkbox.util.jna.linux.GtkSupport; */ public class GtkSystemTray extends GtkTypeSystemTray { - private volatile Pointer trayIcon; + private Pointer trayIcon; // have to make this a field, to prevent GC on this object @SuppressWarnings("FieldCanBeLocal") - private Gobject.GEventCallback gtkCallback; + private final Gobject.GEventCallback gtkCallback; + private NativeLong button_press_event; + private volatile Pointer menu; public GtkSystemTray(String iconName) { @@ -41,23 +44,24 @@ class GtkSystemTray extends GtkTypeSystemTray { final Pointer trayIcon = libgtk.gtk_status_icon_new(); libgtk.gtk_status_icon_set_title(trayIcon, "SystemTray@Dorkbox"); + libgtk.gtk_status_icon_set_name(trayIcon, "SystemTray"); this.trayIcon = trayIcon; libgtk.gtk_status_icon_set_from_file(trayIcon, iconPath(iconName)); - this.menu = libgtk.gtk_menu_new(); this.gtkCallback = new Gobject.GEventCallback() { @Override public - void callback(Pointer system_tray, final Gtk.GdkEventButton event) { + void callback(Pointer notUsed, final Gtk.GdkEventButton event) { // BUTTON_PRESS only (any mouse click) if (event.type == 4) { - libgtk.gtk_menu_popup(menu, null, null, Gtk.gtk_status_icon_position_menu, system_tray, 0, event.time); + libgtk.gtk_menu_popup(menu, null, null, Gtk.gtk_status_icon_position_menu, trayIcon, 0, event.time); } } }; - libgobject.g_signal_connect_data(trayIcon, "button_press_event", gtkCallback, menu, null, 0); + button_press_event = libgobject.g_signal_connect_data(trayIcon, "button_press_event", gtkCallback, null, null, 0); + libgtk.gtk_status_icon_set_visible(trayIcon, true); libgtk.gdk_threads_leave(); @@ -65,9 +69,17 @@ class GtkSystemTray extends GtkTypeSystemTray { GtkSupport.startGui(); } + /** + * Called inside the gdk_threads block + */ + protected + void onMenuAdded(final Pointer menu) { + this.menu = menu; + } + @SuppressWarnings("FieldRepeatedlyAccessedInMethod") @Override - public + public synchronized void shutdown() { libgtk.gdk_threads_enter(); @@ -83,7 +95,7 @@ class GtkSystemTray extends GtkTypeSystemTray { } @Override - public + public synchronized void setIcon(final String iconName) { libgtk.gdk_threads_enter(); libgtk.gtk_status_icon_set_from_file(trayIcon, iconPath(iconName)); diff --git a/src/dorkbox/util/tray/linux/GtkTypeSystemTray.java b/src/dorkbox/util/tray/linux/GtkTypeSystemTray.java index 5da117e..4cbc557 100644 --- a/src/dorkbox/util/tray/linux/GtkTypeSystemTray.java +++ b/src/dorkbox/util/tray/linux/GtkTypeSystemTray.java @@ -21,12 +21,9 @@ import dorkbox.util.NamedThreadFactory; import dorkbox.util.jna.linux.Gobject; import dorkbox.util.jna.linux.Gtk; import dorkbox.util.jna.linux.GtkSupport; -import dorkbox.util.tray.MenuEntry; import dorkbox.util.tray.SystemTray; import dorkbox.util.tray.SystemTrayMenuAction; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -37,38 +34,14 @@ class GtkTypeSystemTray extends SystemTray { final static ExecutorService callbackExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory("SysTrayExecutor", false)); - protected volatile Pointer menu; + private Pointer menu; + private Pointer connectionStatusItem; - private volatile Pointer connectionStatusItem; - - // need to hang on to these to prevent gc - private final List widgets = new ArrayList(4); - - - @Override + @Override synchronized public void shutdown() { // libgtk.gdk_threads_enter(); called by implementation - for (Pointer widget : this.widgets) { - libgtk.gtk_widget_destroy(widget); - } - - // GC it - this.widgets.clear(); - - // unrefs the children too - // GTK menu needs a "ref_sink" - libgobject.g_object_ref_sink(this.menu); - this.menu = null; - - synchronized (this.menuEntries) { - for (MenuEntry menuEntry : this.menuEntries) { - menuEntry.remove(); - } - this.menuEntries.clear(); - } - - this.connectionStatusItem = null; + obliterateMenu(); GtkSupport.shutdownGui(); libgtk.gdk_threads_leave(); @@ -77,70 +50,171 @@ class GtkTypeSystemTray extends SystemTray { } @Override - public + public synchronized void setStatus(String infoString) { - synchronized (this.menuEntries) { - libgtk.gdk_threads_enter(); + libgtk.gdk_threads_enter(); - if (this.connectionStatusItem == null && infoString != null && !infoString.isEmpty()) { - this.connectionStatusItem = libgtk.gtk_menu_item_new_with_label(""); + if (this.connectionStatusItem == null && infoString != null && !infoString.isEmpty()) { + deleteMenu(); + + this.connectionStatusItem = libgtk.gtk_menu_item_new_with_label(""); + libgobject.g_object_ref(connectionStatusItem); // so it matches with 'createMenu' + + // evil hacks abound... + Pointer label = libgtk.gtk_bin_get_child(connectionStatusItem); + libgtk.gtk_label_set_use_markup(label, Gtk.TRUE); + Pointer markup = libgobject.g_markup_printf_escaped("%s", infoString); + libgtk.gtk_label_set_markup(label, markup); + libgobject.g_free(markup); + + + libgtk.gtk_widget_set_sensitive(this.connectionStatusItem, Gtk.FALSE); + + createMenu(); + } + else { + if (infoString == null || infoString.isEmpty()) { + deleteMenu(); + libgtk.gtk_widget_destroy(connectionStatusItem); + connectionStatusItem = null; + + createMenu(); + } + else { + // set bold instead + // libgtk.gtk_menu_item_set_label(this.connectionStatusItem, infoString); // evil hacks abound... Pointer label = libgtk.gtk_bin_get_child(connectionStatusItem); libgtk.gtk_label_set_use_markup(label, Gtk.TRUE); - Pointer markup = libgobject.g_markup_printf_escaped ("%s", infoString); - libgtk.gtk_label_set_markup (label, markup); - libgobject.g_free (markup); + Pointer markup = libgobject.g_markup_printf_escaped("%s", infoString); + libgtk.gtk_label_set_markup(label, markup); + libgobject.g_free(markup); - - this.widgets.add(this.connectionStatusItem); - - libgtk.gtk_widget_set_sensitive(this.connectionStatusItem, Gtk.FALSE); - libgtk.gtk_menu_shell_prepend(this.menu, this.connectionStatusItem); + libgtk.gtk_widget_show_all(menu); } - else { - if (infoString == null || infoString.isEmpty()) { - libgtk.gtk_menu_shell_deactivate(menu, connectionStatusItem); - libgtk.gtk_widget_destroy(connectionStatusItem); - } - else { - // set bold instead - // libgtk.gtk_menu_item_set_label(this.connectionStatusItem, infoString); + } - // evil hacks abound... - Pointer label = libgtk.gtk_bin_get_child(connectionStatusItem); - libgtk.gtk_label_set_use_markup(label, Gtk.TRUE); - Pointer markup = libgobject.g_markup_printf_escaped ("%s", infoString); - libgtk.gtk_label_set_markup (label, markup); - libgobject.g_free (markup); - } + libgtk.gdk_threads_leave(); + } + + /** + * Called inside the gdk_threads block + */ + protected abstract + void onMenuAdded(final Pointer menu); + + + // UNSAFE. must be protected inside synchronized, and inside threads_enter/exit + + /** + * Completely obliterates the menu, no possible way to reconstruct it. + */ + private + void obliterateMenu() { + if (menu != null) { + // have to remove status from menu + if (connectionStatusItem != null) { + libgtk.gtk_widget_destroy(connectionStatusItem); + connectionStatusItem = null; } - libgtk.gtk_widget_show_all(menu); - libgtk.gdk_threads_leave(); + // have to remove all other menu entries + for (int i = 0; i < menuEntries.size(); i++) { + GtkMenuEntry menuEntry__ = (GtkMenuEntry) menuEntries.get(i); + + menuEntry__.removePrivate(); + } + menuEntries.clear(); + + // GTK menu needs a "ref_sink" + libgobject.g_object_ref_sink(menu); } } + /** + * Deletes the menu, and unreferences everything in it. ALSO recreates ONLY the menu object. + */ + void deleteMenu() { + if (menu != null) { + // have to remove status from menu + if (connectionStatusItem != null) { + libgobject.g_object_ref(connectionStatusItem); + + libgtk.gtk_container_remove(menu, connectionStatusItem); + } + + // have to remove all other menu entries + for (int i = 0; i < menuEntries.size(); i++) { + GtkMenuEntry menuEntry__ = (GtkMenuEntry) menuEntries.get(i); + + libgobject.g_object_ref(menuEntry__.menuItem); + libgtk.gtk_container_remove(menu, menuEntry__.menuItem); + } + + // GTK menu needs a "ref_sink" + libgobject.g_object_ref_sink(menu); + + // have to 'blip' the thread, so that the state can catch up. Stupid, i know, and I am open to suggestions to fixing bizzare + // race conditions with GTK... + libgtk.gdk_threads_leave(); + libgtk.gdk_threads_enter(); + } + + menu = libgtk.gtk_menu_new(); + } + + // UNSAFE. must be protected inside synchronized, and inside threads_enter/exit + void createMenu() { + // now add status + if (connectionStatusItem != null) { + libgtk.gtk_menu_shell_append(this.menu, this.connectionStatusItem); + libgobject.g_object_unref(connectionStatusItem); + } + + // now add back other menu entries + for (int i = 0; i < menuEntries.size(); i++) { + GtkMenuEntry menuEntry__ = (GtkMenuEntry) menuEntries.get(i); + + // will also get: gsignal.c:2516: signal 'child-added' is invalid for instance '0x7f1df8244080' of type 'GtkMenu' + libgtk.gtk_menu_shell_append(this.menu, menuEntry__.menuItem); + libgobject.g_object_unref(menuEntry__.menuItem); + } + + onMenuAdded(menu); + libgtk.gtk_widget_show_all(menu); + } + @Override - public + public synchronized void addMenuEntry(String menuText, final String imagePath, final SystemTrayMenuAction 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"); } - synchronized (this.menuEntries) { - MenuEntry menuEntry = getMenuEntry(menuText); + GtkMenuEntry menuEntry = (GtkMenuEntry) getMenuEntry(menuText); - if (menuEntry != null) { - throw new IllegalArgumentException("Menu entry already exists for given label '" + menuText + "'"); - } - else { - libgtk.gdk_threads_enter(); - menuEntry = new GtkMenuEntry(menu, menuText, imagePath, callback, this); - libgtk.gdk_threads_leave(); + if (menuEntry != null) { + throw new IllegalArgumentException("Menu entry already exists for given label '" + menuText + "'"); + } + else { + libgtk.gdk_threads_enter(); - this.menuEntries.add(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 one is added. + deleteMenu(); + + menuEntry = new GtkMenuEntry(menu, menuText, imagePath, callback, this); + + libgobject.g_object_ref(menuEntry.menuItem); // so it matches with 'createMenu' + this.menuEntries.add(menuEntry); + + createMenu(); + + libgtk.gdk_threads_leave(); } } } diff --git a/src/dorkbox/util/tray/swing/SwingSystemTray.java b/src/dorkbox/util/tray/swing/SwingSystemTray.java index e22d481..9895ea8 100644 --- a/src/dorkbox/util/tray/swing/SwingSystemTray.java +++ b/src/dorkbox/util/tray/swing/SwingSystemTray.java @@ -129,16 +129,17 @@ class SwingSystemTray extends dorkbox.util.tray.SystemTray { @Override public void run() { - SwingSystemTray.this.tray.remove(SwingSystemTray.this.trayIcon); + SwingSystemTray tray = SwingSystemTray.this; + synchronized (tray) { + tray.tray.remove(tray.trayIcon); - synchronized (SwingSystemTray.this.menuEntries) { - for (MenuEntry menuEntry : SwingSystemTray.this.menuEntries) { - menuEntry.remove(); + for (MenuEntry menuEntry : tray.menuEntries) { + menuEntry.remove(); + } + tray.menuEntries.clear(); + + tray.connectionStatusItem = null; } - SwingSystemTray.this.menuEntries.clear(); - } - - SwingSystemTray.this.connectionStatusItem = null; } }); } @@ -150,19 +151,22 @@ class SwingSystemTray extends dorkbox.util.tray.SystemTray { @Override public void run() { - if (SwingSystemTray.this.connectionStatusItem == null) { - final JMenuItem connectionStatusItem = new JMenuItem(infoString); - Font font = connectionStatusItem.getFont(); - Font font1 = font.deriveFont(Font.BOLD); - connectionStatusItem.setFont(font1); + SwingSystemTray tray = SwingSystemTray.this; + synchronized (tray) { + if (tray.connectionStatusItem == null) { + final JMenuItem connectionStatusItem = new JMenuItem(infoString); + Font font = connectionStatusItem.getFont(); + Font font1 = font.deriveFont(Font.BOLD); + connectionStatusItem.setFont(font1); - connectionStatusItem.setEnabled(false); - SwingSystemTray.this.menu.add(connectionStatusItem); + connectionStatusItem.setEnabled(false); + tray.menu.add(connectionStatusItem); - SwingSystemTray.this.connectionStatusItem = connectionStatusItem; - } - else { - SwingSystemTray.this.connectionStatusItem.setText(infoString); + tray.connectionStatusItem = connectionStatusItem; + } + else { + tray.connectionStatusItem.setText(infoString); + } } } }); @@ -175,11 +179,14 @@ class SwingSystemTray extends dorkbox.util.tray.SystemTray { @Override public void run() { - String iconPath = iconPath(iconName); - Image trayImage = new ImageIcon(iconPath).getImage() - .getScaledInstance(TRAY_SIZE, TRAY_SIZE, Image.SCALE_SMOOTH); - trayImage.flush(); - SwingSystemTray.this.trayIcon.setImage(trayImage); + SwingSystemTray tray = SwingSystemTray.this; + synchronized (tray) { + String iconPath = iconPath(iconName); + Image trayImage = new ImageIcon(iconPath).getImage() + .getScaledInstance(TRAY_SIZE, TRAY_SIZE, Image.SCALE_SMOOTH); + trayImage.flush(); + tray.trayIcon.setImage(trayImage); + } } }); } @@ -198,14 +205,15 @@ class SwingSystemTray extends dorkbox.util.tray.SystemTray { @Override public void run() { - synchronized (menuEntries) { + SwingSystemTray tray = SwingSystemTray.this; + synchronized (tray) { MenuEntry menuEntry = getMenuEntry(menuText); if (menuEntry != null) { throw new IllegalArgumentException("Menu entry already exists for given label '" + menuText + "'"); } else { - menuEntry = new SwingMenuEntry(menu, menuText, imagePath, callback, SwingSystemTray.this); + menuEntry = new SwingMenuEntry(menu, menuText, imagePath, callback, tray); menuEntries.add(menuEntry); } }