forked from dorkbox/SystemTray
Fixed issues with menu's not showing up with certain versions of libappindicator. (Any time the menu is edited, it is recreated).
This commit is contained in:
parent
60fbd6306d
commit
c1933e6763
32
README.md
32
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+
|
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:
|
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
|
Note: Gnome-shell users will experience an extension install to support this
|
||||||
functionality. Additionally, a shell restart is necessary for the extension
|
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,
|
to be registered by the shell. You can disable the restart behavior if you
|
||||||
and the 'system tray' functionality will be picked up on log out/in, or a
|
like, and the 'system tray' functionality will be picked up on log out/in,
|
||||||
system restart.
|
or a system restart.
|
||||||
|
|
||||||
Also, screw you gnome-project leads, for making it such a pain-in-the-ass
|
Also, screw you gnome-project leads, for making it such a pain-in-the-ass
|
||||||
to do something so incredibly simple and basic.
|
to do something so incredibly simple and basic.
|
||||||
|
|
||||||
Note: Some desktop environments might use a dated version of libappindicator, when
|
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.
|
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
|
This library will try to load a GTK indicator instead when it can, or will
|
||||||
to load libappindicator1 first. Thank you RedHat for putting it back.
|
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
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ class SystemTray {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if ("XFCE".equalsIgnoreCase(XDG)) {
|
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 {
|
try {
|
||||||
trayType = GtkSystemTray.class;
|
trayType = GtkSystemTray.class;
|
||||||
} catch (Throwable ignored) {
|
} catch (Throwable ignored) {
|
||||||
@ -107,6 +107,12 @@ class SystemTray {
|
|||||||
} catch (Throwable ignored) {
|
} catch (Throwable ignored) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if ("KDE".equalsIgnoreCase(XDG)) {
|
||||||
|
try {
|
||||||
|
trayType = AppIndicatorTray.class;
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
else if ("GNOME".equalsIgnoreCase(XDG)) {
|
else if ("GNOME".equalsIgnoreCase(XDG)) {
|
||||||
// check other DE
|
// check other DE
|
||||||
String GDM = System.getenv("GDMSESSION");
|
String GDM = System.getenv("GDMSESSION");
|
||||||
@ -331,9 +337,8 @@ class SystemTray {
|
|||||||
* @param origMenuText the original menu text
|
* @param origMenuText the original menu text
|
||||||
* @param newMenuText the new menu text (this will replace 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) {
|
void updateMenuEntry_Text(String origMenuText, String newMenuText) {
|
||||||
synchronized (this.menuEntries) {
|
|
||||||
MenuEntry menuEntry = getMenuEntry(origMenuText);
|
MenuEntry menuEntry = getMenuEntry(origMenuText);
|
||||||
|
|
||||||
if (menuEntry == null) {
|
if (menuEntry == null) {
|
||||||
@ -343,7 +348,6 @@ class SystemTray {
|
|||||||
menuEntry.setText(newMenuText);
|
menuEntry.setText(newMenuText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -352,9 +356,8 @@ class SystemTray {
|
|||||||
* @param origMenuText the original menu text
|
* @param origMenuText the original menu text
|
||||||
* @param imagePath the new path for the image to use. Null to remove an image.
|
* @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) {
|
void updateMenuEntry_Image(String origMenuText, String imagePath) {
|
||||||
synchronized (this.menuEntries) {
|
|
||||||
MenuEntry menuEntry = getMenuEntry(origMenuText);
|
MenuEntry menuEntry = getMenuEntry(origMenuText);
|
||||||
|
|
||||||
if (menuEntry == null) {
|
if (menuEntry == null) {
|
||||||
@ -364,7 +367,6 @@ class SystemTray {
|
|||||||
menuEntry.setImage(imagePath);
|
menuEntry.setImage(imagePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -373,9 +375,8 @@ class SystemTray {
|
|||||||
* @param origMenuText the original menu text
|
* @param origMenuText the original menu text
|
||||||
* @param newCallback the new callback (this will replace the original callback)
|
* @param newCallback the new callback (this will replace the original callback)
|
||||||
*/
|
*/
|
||||||
public final
|
public final synchronized
|
||||||
void updateMenuEntry_Callback(String origMenuText, SystemTrayMenuAction newCallback) {
|
void updateMenuEntry_Callback(String origMenuText, SystemTrayMenuAction newCallback) {
|
||||||
synchronized (this.menuEntries) {
|
|
||||||
MenuEntry menuEntry = getMenuEntry(origMenuText);
|
MenuEntry menuEntry = getMenuEntry(origMenuText);
|
||||||
|
|
||||||
if (menuEntry != null) {
|
if (menuEntry != null) {
|
||||||
@ -385,7 +386,6 @@ class SystemTray {
|
|||||||
throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'");
|
throw new NullPointerException("No menu entry exists for string '" + origMenuText + "'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -395,9 +395,8 @@ class SystemTray {
|
|||||||
* @param newMenuText the new menu text (this will replace the original menu text)
|
* @param newMenuText the new menu text (this will replace the original menu text)
|
||||||
* @param newCallback the new callback (this will replace the original callback)
|
* @param newCallback the new callback (this will replace the original callback)
|
||||||
*/
|
*/
|
||||||
public final
|
public final synchronized
|
||||||
void updateMenuEntry(String origMenuText, String newMenuText, SystemTrayMenuAction newCallback) {
|
void updateMenuEntry(String origMenuText, String newMenuText, SystemTrayMenuAction newCallback) {
|
||||||
synchronized (this.menuEntries) {
|
|
||||||
MenuEntry menuEntry = getMenuEntry(origMenuText);
|
MenuEntry menuEntry = getMenuEntry(origMenuText);
|
||||||
|
|
||||||
if (menuEntry == null) {
|
if (menuEntry == null) {
|
||||||
@ -408,7 +407,6 @@ class SystemTray {
|
|||||||
menuEntry.setCallback(newCallback);
|
menuEntry.setCallback(newCallback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -416,13 +414,12 @@ class SystemTray {
|
|||||||
*
|
*
|
||||||
* @param menuEntry This is the menu entry to remove
|
* @param menuEntry This is the menu entry to remove
|
||||||
*/
|
*/
|
||||||
public final
|
public final synchronized
|
||||||
void removeMenuEntry(final MenuEntry menuEntry) {
|
void removeMenuEntry(final MenuEntry menuEntry) {
|
||||||
if (menuEntry == null) {
|
if (menuEntry == null) {
|
||||||
throw new NullPointerException("No menu entry exists for menuEntry");
|
throw new NullPointerException("No menu entry exists for menuEntry");
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized (this.menuEntries) {
|
|
||||||
final String label = menuEntry.getText();
|
final String label = menuEntry.getText();
|
||||||
|
|
||||||
for (Iterator<MenuEntry> iterator = menuEntries.iterator(); iterator.hasNext(); ) {
|
for (Iterator<MenuEntry> iterator = menuEntries.iterator(); iterator.hasNext(); ) {
|
||||||
@ -431,13 +428,13 @@ class SystemTray {
|
|||||||
.equals(label)) {
|
.equals(label)) {
|
||||||
iterator.remove();
|
iterator.remove();
|
||||||
|
|
||||||
|
// this will also reset the menu
|
||||||
menuEntry.remove();
|
menuEntry.remove();
|
||||||
return;
|
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,9 +442,8 @@ class SystemTray {
|
|||||||
*
|
*
|
||||||
* @param menuText This is the label for the menu entry to remove
|
* @param menuText This is the label for the menu entry to remove
|
||||||
*/
|
*/
|
||||||
public final
|
public final synchronized
|
||||||
void removeMenuEntry(final String menuText) {
|
void removeMenuEntry(final String menuText) {
|
||||||
synchronized (this.menuEntries) {
|
|
||||||
MenuEntry menuEntry = getMenuEntry(menuText);
|
MenuEntry menuEntry = getMenuEntry(menuText);
|
||||||
|
|
||||||
if (menuEntry == null) {
|
if (menuEntry == null) {
|
||||||
@ -457,10 +453,11 @@ class SystemTray {
|
|||||||
removeMenuEntry(menuEntry);
|
removeMenuEntry(menuEntry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* UNSAFE. must be called inside sync
|
||||||
|
*
|
||||||
* appIndicator/gtk require strings (which is the path)
|
* appIndicator/gtk require strings (which is the path)
|
||||||
* swing version loads as an image (which can be stream or path, we use path)
|
* swing version loads as an image (which can be stream or path, we use path)
|
||||||
*/
|
*/
|
||||||
|
@ -32,18 +32,14 @@ public
|
|||||||
class AppIndicatorTray extends GtkTypeSystemTray {
|
class AppIndicatorTray extends GtkTypeSystemTray {
|
||||||
private static final AppIndicator libappindicator = AppIndicator.INSTANCE;
|
private static final AppIndicator libappindicator = AppIndicator.INSTANCE;
|
||||||
|
|
||||||
private volatile AppIndicator.AppIndicatorInstanceStruct appIndicator;
|
private AppIndicator.AppIndicatorInstanceStruct appIndicator;
|
||||||
|
|
||||||
public
|
public
|
||||||
AppIndicatorTray(String iconName) {
|
AppIndicatorTray(String iconName) {
|
||||||
libgtk.gdk_threads_enter();
|
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);
|
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);
|
libappindicator.app_indicator_set_status(this.appIndicator, AppIndicator.STATUS_ACTIVE);
|
||||||
|
|
||||||
libgtk.gdk_threads_leave();
|
libgtk.gdk_threads_leave();
|
||||||
@ -52,13 +48,12 @@ class AppIndicatorTray extends GtkTypeSystemTray {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public
|
public synchronized
|
||||||
void shutdown() {
|
void shutdown() {
|
||||||
libgtk.gdk_threads_enter();
|
libgtk.gdk_threads_enter();
|
||||||
|
|
||||||
// this hides the indicator
|
// this hides the indicator
|
||||||
libappindicator.app_indicator_set_status(this.appIndicator, AppIndicator.STATUS_PASSIVE);
|
libappindicator.app_indicator_set_status(this.appIndicator, AppIndicator.STATUS_PASSIVE);
|
||||||
this.appIndicator.write();
|
|
||||||
Pointer p = this.appIndicator.getPointer();
|
Pointer p = this.appIndicator.getPointer();
|
||||||
libgobject.g_object_unref(p);
|
libgobject.g_object_unref(p);
|
||||||
|
|
||||||
@ -70,10 +65,19 @@ class AppIndicatorTray extends GtkTypeSystemTray {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public
|
public synchronized
|
||||||
void setIcon(final String iconName) {
|
void setIcon(final String iconName) {
|
||||||
libgtk.gdk_threads_enter();
|
libgtk.gdk_threads_enter();
|
||||||
libappindicator.app_indicator_set_icon(this.appIndicator, iconPath(iconName));
|
libappindicator.app_indicator_set_icon(this.appIndicator, iconPath(iconName));
|
||||||
libgtk.gdk_threads_leave();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,6 @@ import dorkbox.util.jna.linux.Gobject;
|
|||||||
import dorkbox.util.jna.linux.Gobject.GCallback;
|
import dorkbox.util.jna.linux.Gobject.GCallback;
|
||||||
import dorkbox.util.jna.linux.Gtk;
|
import dorkbox.util.jna.linux.Gtk;
|
||||||
import dorkbox.util.tray.MenuEntry;
|
import dorkbox.util.tray.MenuEntry;
|
||||||
import dorkbox.util.tray.SystemTray;
|
|
||||||
import dorkbox.util.tray.SystemTrayMenuAction;
|
import dorkbox.util.tray.SystemTrayMenuAction;
|
||||||
|
|
||||||
class GtkMenuEntry implements MenuEntry {
|
class GtkMenuEntry implements MenuEntry {
|
||||||
@ -29,17 +28,18 @@ class GtkMenuEntry implements MenuEntry {
|
|||||||
private static final Gobject libgobject = Gobject.INSTANCE;
|
private static final Gobject libgobject = Gobject.INSTANCE;
|
||||||
|
|
||||||
private final GCallback gtkCallback;
|
private final GCallback gtkCallback;
|
||||||
private final Pointer menuItem;
|
final Pointer menuItem;
|
||||||
private final Pointer parentMenu;
|
private final Pointer parentMenu;
|
||||||
private final SystemTray systemTray;
|
final GtkTypeSystemTray systemTray;
|
||||||
private final NativeLong nativeLong;
|
private final NativeLong nativeLong;
|
||||||
|
|
||||||
|
// these have to be volatile, because they can be changed from any thread
|
||||||
private volatile String text;
|
private volatile String text;
|
||||||
private volatile SystemTrayMenuAction callback;
|
private volatile SystemTrayMenuAction callback;
|
||||||
private volatile Pointer image;
|
private volatile Pointer image;
|
||||||
|
|
||||||
GtkMenuEntry(final Pointer parentMenu, final String label, final String imagePath, final SystemTrayMenuAction callback,
|
GtkMenuEntry(final Pointer parentMenu, final String label, final String imagePath, final SystemTrayMenuAction callback,
|
||||||
final SystemTray systemTray) {
|
final GtkTypeSystemTray systemTray) {
|
||||||
this.parentMenu = parentMenu;
|
this.parentMenu = parentMenu;
|
||||||
this.text = label;
|
this.text = label;
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
@ -55,7 +55,6 @@ class GtkMenuEntry implements MenuEntry {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
menuItem = libgtk.gtk_image_menu_item_new_with_label(label);
|
menuItem = libgtk.gtk_image_menu_item_new_with_label(label);
|
||||||
|
|
||||||
if (imagePath != null && !imagePath.isEmpty()) {
|
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);
|
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
|
private
|
||||||
@ -117,7 +113,10 @@ class GtkMenuEntry implements MenuEntry {
|
|||||||
if (image != null) {
|
if (image != null) {
|
||||||
libgtk.gtk_widget_destroy(image);
|
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);
|
image = libgtk.gtk_image_new_from_file(imagePath);
|
||||||
|
|
||||||
libgtk.gtk_image_menu_item_set_image(menuItem, image);
|
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
|
// must always re-set always-show after setting the image
|
||||||
libgtk.gtk_image_menu_item_set_always_show_image(menuItem, Gtk.TRUE);
|
libgtk.gtk_image_menu_item_set_always_show_image(menuItem, Gtk.TRUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
libgtk.gtk_widget_show_all(parentMenu);
|
libgtk.gtk_widget_show_all(parentMenu);
|
||||||
|
|
||||||
libgtk.gdk_threads_leave();
|
libgtk.gdk_threads_leave();
|
||||||
@ -137,10 +135,23 @@ class GtkMenuEntry implements MenuEntry {
|
|||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is ONLY called via systray.menuEntry.remove() !!
|
||||||
|
*/
|
||||||
public
|
public
|
||||||
void remove() {
|
void remove() {
|
||||||
libgtk.gdk_threads_enter();
|
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);
|
libgobject.g_signal_handler_disconnect(menuItem, nativeLong);
|
||||||
libgtk.gtk_menu_shell_deactivate(parentMenu, menuItem);
|
libgtk.gtk_menu_shell_deactivate(parentMenu, menuItem);
|
||||||
|
|
||||||
@ -148,8 +159,6 @@ class GtkMenuEntry implements MenuEntry {
|
|||||||
libgtk.gtk_widget_destroy(image);
|
libgtk.gtk_widget_destroy(image);
|
||||||
}
|
}
|
||||||
libgtk.gtk_widget_destroy(menuItem);
|
libgtk.gtk_widget_destroy(menuItem);
|
||||||
|
|
||||||
libgtk.gdk_threads_leave();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package dorkbox.util.tray.linux;
|
package dorkbox.util.tray.linux;
|
||||||
|
|
||||||
|
import com.sun.jna.NativeLong;
|
||||||
import com.sun.jna.Pointer;
|
import com.sun.jna.Pointer;
|
||||||
import dorkbox.util.jna.linux.Gobject;
|
import dorkbox.util.jna.linux.Gobject;
|
||||||
import dorkbox.util.jna.linux.Gtk;
|
import dorkbox.util.jna.linux.Gtk;
|
||||||
@ -27,11 +28,13 @@ import dorkbox.util.jna.linux.GtkSupport;
|
|||||||
*/
|
*/
|
||||||
public
|
public
|
||||||
class GtkSystemTray extends GtkTypeSystemTray {
|
class GtkSystemTray extends GtkTypeSystemTray {
|
||||||
private volatile Pointer trayIcon;
|
private Pointer trayIcon;
|
||||||
|
|
||||||
// have to make this a field, to prevent GC on this object
|
// have to make this a field, to prevent GC on this object
|
||||||
@SuppressWarnings("FieldCanBeLocal")
|
@SuppressWarnings("FieldCanBeLocal")
|
||||||
private Gobject.GEventCallback gtkCallback;
|
private final Gobject.GEventCallback gtkCallback;
|
||||||
|
private NativeLong button_press_event;
|
||||||
|
private volatile Pointer menu;
|
||||||
|
|
||||||
public
|
public
|
||||||
GtkSystemTray(String iconName) {
|
GtkSystemTray(String iconName) {
|
||||||
@ -41,23 +44,24 @@ class GtkSystemTray extends GtkTypeSystemTray {
|
|||||||
|
|
||||||
final Pointer trayIcon = libgtk.gtk_status_icon_new();
|
final Pointer trayIcon = libgtk.gtk_status_icon_new();
|
||||||
libgtk.gtk_status_icon_set_title(trayIcon, "SystemTray@Dorkbox");
|
libgtk.gtk_status_icon_set_title(trayIcon, "SystemTray@Dorkbox");
|
||||||
|
libgtk.gtk_status_icon_set_name(trayIcon, "SystemTray");
|
||||||
|
|
||||||
this.trayIcon = trayIcon;
|
this.trayIcon = trayIcon;
|
||||||
|
|
||||||
libgtk.gtk_status_icon_set_from_file(trayIcon, iconPath(iconName));
|
libgtk.gtk_status_icon_set_from_file(trayIcon, iconPath(iconName));
|
||||||
|
|
||||||
this.menu = libgtk.gtk_menu_new();
|
|
||||||
this.gtkCallback = new Gobject.GEventCallback() {
|
this.gtkCallback = new Gobject.GEventCallback() {
|
||||||
@Override
|
@Override
|
||||||
public
|
public
|
||||||
void callback(Pointer system_tray, final Gtk.GdkEventButton event) {
|
void callback(Pointer notUsed, final Gtk.GdkEventButton event) {
|
||||||
// BUTTON_PRESS only (any mouse click)
|
// BUTTON_PRESS only (any mouse click)
|
||||||
if (event.type == 4) {
|
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.gtk_status_icon_set_visible(trayIcon, true);
|
||||||
|
|
||||||
libgtk.gdk_threads_leave();
|
libgtk.gdk_threads_leave();
|
||||||
@ -65,9 +69,17 @@ class GtkSystemTray extends GtkTypeSystemTray {
|
|||||||
GtkSupport.startGui();
|
GtkSupport.startGui();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called inside the gdk_threads block
|
||||||
|
*/
|
||||||
|
protected
|
||||||
|
void onMenuAdded(final Pointer menu) {
|
||||||
|
this.menu = menu;
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("FieldRepeatedlyAccessedInMethod")
|
@SuppressWarnings("FieldRepeatedlyAccessedInMethod")
|
||||||
@Override
|
@Override
|
||||||
public
|
public synchronized
|
||||||
void shutdown() {
|
void shutdown() {
|
||||||
libgtk.gdk_threads_enter();
|
libgtk.gdk_threads_enter();
|
||||||
|
|
||||||
@ -83,7 +95,7 @@ class GtkSystemTray extends GtkTypeSystemTray {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public
|
public synchronized
|
||||||
void setIcon(final String iconName) {
|
void setIcon(final String iconName) {
|
||||||
libgtk.gdk_threads_enter();
|
libgtk.gdk_threads_enter();
|
||||||
libgtk.gtk_status_icon_set_from_file(trayIcon, iconPath(iconName));
|
libgtk.gtk_status_icon_set_from_file(trayIcon, iconPath(iconName));
|
||||||
|
@ -21,12 +21,9 @@ import dorkbox.util.NamedThreadFactory;
|
|||||||
import dorkbox.util.jna.linux.Gobject;
|
import dorkbox.util.jna.linux.Gobject;
|
||||||
import dorkbox.util.jna.linux.Gtk;
|
import dorkbox.util.jna.linux.Gtk;
|
||||||
import dorkbox.util.jna.linux.GtkSupport;
|
import dorkbox.util.jna.linux.GtkSupport;
|
||||||
import dorkbox.util.tray.MenuEntry;
|
|
||||||
import dorkbox.util.tray.SystemTray;
|
import dorkbox.util.tray.SystemTray;
|
||||||
import dorkbox.util.tray.SystemTrayMenuAction;
|
import dorkbox.util.tray.SystemTrayMenuAction;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
@ -37,38 +34,14 @@ class GtkTypeSystemTray extends SystemTray {
|
|||||||
|
|
||||||
final static ExecutorService callbackExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory("SysTrayExecutor", false));
|
final static ExecutorService callbackExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory("SysTrayExecutor", false));
|
||||||
|
|
||||||
protected volatile Pointer menu;
|
private Pointer menu;
|
||||||
|
private Pointer connectionStatusItem;
|
||||||
|
|
||||||
private volatile Pointer connectionStatusItem;
|
@Override synchronized
|
||||||
|
|
||||||
// need to hang on to these to prevent gc
|
|
||||||
private final List<Pointer> widgets = new ArrayList<Pointer>(4);
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public
|
public
|
||||||
void shutdown() {
|
void shutdown() {
|
||||||
// libgtk.gdk_threads_enter(); called by implementation
|
// libgtk.gdk_threads_enter(); called by implementation
|
||||||
for (Pointer widget : this.widgets) {
|
obliterateMenu();
|
||||||
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;
|
|
||||||
GtkSupport.shutdownGui();
|
GtkSupport.shutdownGui();
|
||||||
|
|
||||||
libgtk.gdk_threads_leave();
|
libgtk.gdk_threads_leave();
|
||||||
@ -77,13 +50,15 @@ class GtkTypeSystemTray extends SystemTray {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public
|
public synchronized
|
||||||
void setStatus(String infoString) {
|
void setStatus(String infoString) {
|
||||||
synchronized (this.menuEntries) {
|
|
||||||
libgtk.gdk_threads_enter();
|
libgtk.gdk_threads_enter();
|
||||||
|
|
||||||
if (this.connectionStatusItem == null && infoString != null && !infoString.isEmpty()) {
|
if (this.connectionStatusItem == null && infoString != null && !infoString.isEmpty()) {
|
||||||
|
deleteMenu();
|
||||||
|
|
||||||
this.connectionStatusItem = libgtk.gtk_menu_item_new_with_label("");
|
this.connectionStatusItem = libgtk.gtk_menu_item_new_with_label("");
|
||||||
|
libgobject.g_object_ref(connectionStatusItem); // so it matches with 'createMenu'
|
||||||
|
|
||||||
// evil hacks abound...
|
// evil hacks abound...
|
||||||
Pointer label = libgtk.gtk_bin_get_child(connectionStatusItem);
|
Pointer label = libgtk.gtk_bin_get_child(connectionStatusItem);
|
||||||
@ -93,15 +68,17 @@ class GtkTypeSystemTray extends SystemTray {
|
|||||||
libgobject.g_free(markup);
|
libgobject.g_free(markup);
|
||||||
|
|
||||||
|
|
||||||
this.widgets.add(this.connectionStatusItem);
|
|
||||||
|
|
||||||
libgtk.gtk_widget_set_sensitive(this.connectionStatusItem, Gtk.FALSE);
|
libgtk.gtk_widget_set_sensitive(this.connectionStatusItem, Gtk.FALSE);
|
||||||
libgtk.gtk_menu_shell_prepend(this.menu, this.connectionStatusItem);
|
|
||||||
|
createMenu();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (infoString == null || infoString.isEmpty()) {
|
if (infoString == null || infoString.isEmpty()) {
|
||||||
libgtk.gtk_menu_shell_deactivate(menu, connectionStatusItem);
|
deleteMenu();
|
||||||
libgtk.gtk_widget_destroy(connectionStatusItem);
|
libgtk.gtk_widget_destroy(connectionStatusItem);
|
||||||
|
connectionStatusItem = null;
|
||||||
|
|
||||||
|
createMenu();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// set bold instead
|
// set bold instead
|
||||||
@ -113,34 +90,131 @@ class GtkTypeSystemTray extends SystemTray {
|
|||||||
Pointer markup = libgobject.g_markup_printf_escaped("<b>%s</b>", infoString);
|
Pointer markup = libgobject.g_markup_printf_escaped("<b>%s</b>", infoString);
|
||||||
libgtk.gtk_label_set_markup(label, markup);
|
libgtk.gtk_label_set_markup(label, markup);
|
||||||
libgobject.g_free(markup);
|
libgobject.g_free(markup);
|
||||||
|
|
||||||
|
libgtk.gtk_widget_show_all(menu);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
libgtk.gtk_widget_show_all(menu);
|
|
||||||
libgtk.gdk_threads_leave();
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
@Override
|
||||||
public
|
public synchronized
|
||||||
void addMenuEntry(String menuText, final String imagePath, final SystemTrayMenuAction callback) {
|
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) {
|
if (menuText == null) {
|
||||||
throw new NullPointerException("Menu text cannot be null");
|
throw new NullPointerException("Menu text cannot be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized (this.menuEntries) {
|
GtkMenuEntry menuEntry = (GtkMenuEntry) getMenuEntry(menuText);
|
||||||
MenuEntry menuEntry = getMenuEntry(menuText);
|
|
||||||
|
|
||||||
if (menuEntry != null) {
|
if (menuEntry != null) {
|
||||||
throw new IllegalArgumentException("Menu entry already exists for given label '" + menuText + "'");
|
throw new IllegalArgumentException("Menu entry already exists for given label '" + menuText + "'");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
libgtk.gdk_threads_enter();
|
libgtk.gdk_threads_enter();
|
||||||
menuEntry = new GtkMenuEntry(menu, menuText, imagePath, callback, this);
|
|
||||||
libgtk.gdk_threads_leave();
|
|
||||||
|
|
||||||
|
// 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);
|
this.menuEntries.add(menuEntry);
|
||||||
}
|
|
||||||
|
createMenu();
|
||||||
|
|
||||||
|
libgtk.gdk_threads_leave();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -129,16 +129,17 @@ class SwingSystemTray extends dorkbox.util.tray.SystemTray {
|
|||||||
@Override
|
@Override
|
||||||
public
|
public
|
||||||
void run() {
|
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 : tray.menuEntries) {
|
||||||
for (MenuEntry menuEntry : SwingSystemTray.this.menuEntries) {
|
|
||||||
menuEntry.remove();
|
menuEntry.remove();
|
||||||
}
|
}
|
||||||
SwingSystemTray.this.menuEntries.clear();
|
tray.menuEntries.clear();
|
||||||
}
|
|
||||||
|
|
||||||
SwingSystemTray.this.connectionStatusItem = null;
|
tray.connectionStatusItem = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -150,19 +151,22 @@ class SwingSystemTray extends dorkbox.util.tray.SystemTray {
|
|||||||
@Override
|
@Override
|
||||||
public
|
public
|
||||||
void run() {
|
void run() {
|
||||||
if (SwingSystemTray.this.connectionStatusItem == null) {
|
SwingSystemTray tray = SwingSystemTray.this;
|
||||||
|
synchronized (tray) {
|
||||||
|
if (tray.connectionStatusItem == null) {
|
||||||
final JMenuItem connectionStatusItem = new JMenuItem(infoString);
|
final JMenuItem connectionStatusItem = new JMenuItem(infoString);
|
||||||
Font font = connectionStatusItem.getFont();
|
Font font = connectionStatusItem.getFont();
|
||||||
Font font1 = font.deriveFont(Font.BOLD);
|
Font font1 = font.deriveFont(Font.BOLD);
|
||||||
connectionStatusItem.setFont(font1);
|
connectionStatusItem.setFont(font1);
|
||||||
|
|
||||||
connectionStatusItem.setEnabled(false);
|
connectionStatusItem.setEnabled(false);
|
||||||
SwingSystemTray.this.menu.add(connectionStatusItem);
|
tray.menu.add(connectionStatusItem);
|
||||||
|
|
||||||
SwingSystemTray.this.connectionStatusItem = connectionStatusItem;
|
tray.connectionStatusItem = connectionStatusItem;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
SwingSystemTray.this.connectionStatusItem.setText(infoString);
|
tray.connectionStatusItem.setText(infoString);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -175,11 +179,14 @@ class SwingSystemTray extends dorkbox.util.tray.SystemTray {
|
|||||||
@Override
|
@Override
|
||||||
public
|
public
|
||||||
void run() {
|
void run() {
|
||||||
|
SwingSystemTray tray = SwingSystemTray.this;
|
||||||
|
synchronized (tray) {
|
||||||
String iconPath = iconPath(iconName);
|
String iconPath = iconPath(iconName);
|
||||||
Image trayImage = new ImageIcon(iconPath).getImage()
|
Image trayImage = new ImageIcon(iconPath).getImage()
|
||||||
.getScaledInstance(TRAY_SIZE, TRAY_SIZE, Image.SCALE_SMOOTH);
|
.getScaledInstance(TRAY_SIZE, TRAY_SIZE, Image.SCALE_SMOOTH);
|
||||||
trayImage.flush();
|
trayImage.flush();
|
||||||
SwingSystemTray.this.trayIcon.setImage(trayImage);
|
tray.trayIcon.setImage(trayImage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -198,14 +205,15 @@ class SwingSystemTray extends dorkbox.util.tray.SystemTray {
|
|||||||
@Override
|
@Override
|
||||||
public
|
public
|
||||||
void run() {
|
void run() {
|
||||||
synchronized (menuEntries) {
|
SwingSystemTray tray = SwingSystemTray.this;
|
||||||
|
synchronized (tray) {
|
||||||
MenuEntry menuEntry = getMenuEntry(menuText);
|
MenuEntry menuEntry = getMenuEntry(menuText);
|
||||||
|
|
||||||
if (menuEntry != null) {
|
if (menuEntry != null) {
|
||||||
throw new IllegalArgumentException("Menu entry already exists for given label '" + menuText + "'");
|
throw new IllegalArgumentException("Menu entry already exists for given label '" + menuText + "'");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
menuEntry = new SwingMenuEntry(menu, menuText, imagePath, callback, SwingSystemTray.this);
|
menuEntry = new SwingMenuEntry(menu, menuText, imagePath, callback, tray);
|
||||||
menuEntries.add(menuEntry);
|
menuEntries.add(menuEntry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user