WIP AppIndicators using swing menu

This commit is contained in:
nathan 2016-10-09 20:20:23 +02:00
parent b936a4cd76
commit fbf528d0ca
8 changed files with 396 additions and 120 deletions

View File

@ -34,8 +34,6 @@ class AppIndicator {
public static boolean isVersion3 = false;
private static boolean isLoaded = false;
private static final boolean VERBOSE_DEBUG = false;
/**
* Loader 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/API set is. We just try until we find one that work, and are able to map the
@ -69,11 +67,8 @@ class AppIndicator {
isLoaded = true;
}
} catch (Throwable e) {
if (VERBOSE_DEBUG) {
logger.debug("Error loading library: {}", "appindicator1", e);
}
else if (SystemTray.DEBUG) {
logger.debug("Error loading GTK2 explicit appindicator1");
if (SystemTray.DEBUG) {
logger.debug("Error loading GTK2 explicit appindicator1. {}", e.getMessage());
}
}
}
@ -104,7 +99,7 @@ class AppIndicator {
isLoaded = true;
} catch (Throwable e) {
if (SystemTray.DEBUG) {
logger.debug("Error loading library: '{}'", nameToCheck1, e);
logger.debug("Error loading library: '{}'. \n{}", nameToCheck1, e.getMessage());
}
}
}
@ -141,7 +136,7 @@ class AppIndicator {
break;
} catch (Throwable e) {
if (SystemTray.DEBUG) {
logger.debug("Error loading library: '{}'", "appindicator" + i, e);
logger.debug("Error loading library: '{}'. \n{}", "appindicator" + i, e.getMessage());
}
}
}
@ -168,7 +163,7 @@ class AppIndicator {
break;
} catch (Throwable e) {
if (SystemTray.DEBUG) {
logger.debug("Error loading library: '{}'", "appindicator" + i, e);
logger.debug("Error loading library: '{}'. \n{}", "appindicator" + i, e.getMessage());
}
}
}
@ -196,7 +191,7 @@ class AppIndicator {
isLoaded = true;
} catch (Throwable e) {
if (SystemTray.DEBUG) {
logger.debug("Error loading library: '{}'", nameToCheck1, e);
logger.debug("Error loading library: '{}'. \n{}", nameToCheck1, e.getMessage());
}
}
}
@ -208,7 +203,7 @@ class AppIndicator {
isLoaded = true;
} catch (Throwable e) {
if (SystemTray.DEBUG) {
logger.debug("Error loading library: '{}'", nameToCheck2, e);
logger.debug("Error loading library: '{}'. \n{}", nameToCheck2, e.getMessage());
}
}
}
@ -233,4 +228,5 @@ class AppIndicator {
public static native void app_indicator_set_status(AppIndicatorInstanceStruct self, int status);
public static native void app_indicator_set_menu(AppIndicatorInstanceStruct self, Pointer menu);
public static native void app_indicator_set_icon(AppIndicatorInstanceStruct self, String icon_name);
public static native void app_indicator_set_label(AppIndicatorInstanceStruct self, String label, String notused);
}

View File

@ -18,6 +18,7 @@ package dorkbox.systemTray.linux.jna;
import com.sun.jna.Callback;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
import com.sun.jna.ptr.PointerByReference;
/**
* bindings for libgobject-2.0
@ -32,6 +33,8 @@ class Gobject {
}
public static native void g_object_get(Pointer object, String objectName, PointerByReference objectVal, Pointer nullValue);
public static native void g_free(Pointer object);
public static native void g_object_unref(Pointer object);

View File

@ -55,6 +55,7 @@ class Gtk {
// there is ONLY a single thread EVER setting this value!!
private static volatile boolean isDispatch = false;
public static boolean isKDE = false;
// 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

View File

@ -0,0 +1,291 @@
/*
* Copyright 2014 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.systemTray.swing;
import static dorkbox.systemTray.SystemTray.TIMEOUT;
import java.awt.Dimension;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Rectangle;
import java.io.File;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.JPopupMenu;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
import com.sun.jna.ptr.PointerByReference;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.linux.jna.AppIndicator;
import dorkbox.systemTray.linux.jna.AppIndicatorInstanceStruct;
import dorkbox.systemTray.linux.jna.GEventCallback;
import dorkbox.systemTray.linux.jna.GdkEventButton;
import dorkbox.systemTray.linux.jna.Gobject;
import dorkbox.systemTray.linux.jna.Gtk;
import dorkbox.systemTray.util.ImageUtils;
import dorkbox.util.ScreenUtil;
import dorkbox.util.SwingUtil;
/**
* Class for handling all system tray interactions.
* specialization for using app indicators in ubuntu unity
*
* Derived from
* Lantern: https://github.com/getlantern/lantern/ Apache 2.0 License Copyright 2010 Brave New Software Project, Inc.
*
* AppIndicators DO NOT support anything other than plain gtk-menus, because of how they use dbus so no tooltips AND no custom widgets
*
*
*
* As a result of this decision by Canonical, we have to resort to hacks to get it to do what we want. BY NO MEANS IS THIS PERFECT.
*
*
* We still cannot have tooltips, but we *CAN* have custom widgets in the menu (because it's our swing menu now...)
*
*
* It would be too much work to re-implement AppIndicators, or even to use LD_PRELOAD + restart service to do what we want.
*
* As a result, we have some wicked little hacks which are rather effective (but have a small side-effect of very briefly
* showing a blank menu)
*
* // What are AppIndicators?
* http://unity.ubuntu.com/projects/appindicators/
*
*
* // Entry-point into appindicators
* http://bazaar.launchpad.net/~unity-team/unity/trunk/view/head:/services/panel-main.c
*
*
* // The idiocy of appindicators
* https://bugs.launchpad.net/screenlets/+bug/522152
*
* // Code of how the dbus menus work
* http://bazaar.launchpad.net/~dbusmenu-team/libdbusmenu/trunk.16.10/view/head:/libdbusmenu-gtk/client.c
* https://developer.ubuntu.com/api/devel/ubuntu-12.04/c/dbusmenugtk/index.html
*
* // more info about trying to put widgets into GTK menus
* http://askubuntu.com/questions/16431/putting-an-arbitrary-gtk-widget-into-an-appindicator-indicator
*
* // possible idea on how to get GTK widgets into GTK menus
* https://launchpad.net/ido
* http://bazaar.launchpad.net/~canonical-dx-team/ido/trunk/view/head:/src/idoentrymenuitem.c
* http://bazaar.launchpad.net/~ubuntu-desktop/ido/gtk3/files
*/
public
class AppIndicatorTray extends SwingGenericTray {
private AppIndicatorInstanceStruct appIndicator;
private boolean isActive = false;
// This is required if we have JavaFX or SWT shutdown hooks (to prevent us from shutting down twice...)
private AtomicBoolean shuttingDown = new AtomicBoolean();
private volatile NativeLong nativeLong;
private volatile GEventCallback gtkCallback;
private Pointer dummyMenu;
private final Runnable popupRunnable;
public
AppIndicatorTray(final SystemTray systemTray) {
super(systemTray,null, new SwingSystemTrayMenuPopup());
if (SystemTray.FORCE_TRAY_TYPE == SystemTray.TYPE_GTK_STATUSICON) {
// if we force GTK type system tray, don't attempt to load AppIndicator libs
throw new IllegalArgumentException("Unable to start AppIndicator if 'SystemTray.FORCE_TRAY_TYPE' is set to GtkStatusIcon");
}
JPopupMenu popupMenu = (JPopupMenu) _native;
popupMenu.pack();
popupMenu.setFocusable(true);
popupRunnable = new Runnable() {
@Override
public
void run() {
Dimension size = _native.getPreferredSize();
Point point = MouseInfo.getPointerInfo()
.getLocation();
Rectangle bounds = ScreenUtil.getScreenBoundsAt(point);
int x = point.x;
int y = point.y;
if (y < bounds.y) {
y = bounds.y;
}
else if (y + size.height > bounds.y + bounds.height) {
// our menu cannot have the top-edge snap to the mouse
// so we make the bottom-edge snap to the mouse
y -= size.height; // snap to edge of mouse
}
if (x < bounds.x) {
x = bounds.x;
x -= 32; // display over the stupid appindicator menu (which has to show, this is a major hack!)
}
else if (x + size.width > bounds.x + bounds.width) {
// our menu cannot have the left-edge snap to the mouse
// so we make the right-edge snap to the mouse
x -= size.width; // snap to edge of mouse
x += 32; // display over the stupid appindicator menu (which has to show, this is a major hack!)
}
SwingSystemTrayMenuPopup popupMenu = (SwingSystemTrayMenuPopup) _native;
popupMenu.doShow(x, y);
// Such ugly hacks to get AppIndicator support properly working. This is so horrible I am ashamed.
Gtk.dispatch(new Runnable() {
@Override
public
void run() {
createAppIndicatorMenu();
hookMenuOpen();
}
});
}
};
// appindicators DO NOT support anything other than PLAIN gtk-menus
// they ALSO do not support tooltips, so we cater to the lowest common denominator
// trayIcon.setToolTip(SwingSystemTray.this.appName);
Gtk.startGui();
Gtk.dispatch(new Runnable() {
@Override
public
void run() {
// we initialize with a blank image
File image = ImageUtils.getTransparentImage(ImageUtils.ENTRY_SIZE);
String id = System.nanoTime() + "DBST";
appIndicator = AppIndicator.app_indicator_new(id, image.getAbsolutePath(), AppIndicator.CATEGORY_APPLICATION_STATUS);
createAppIndicatorMenu();
}
});
Gtk.waitForStartup();
}
private
void hookMenuOpen() {
// now we have to setup a way for us to catch the "activation" click on this menu. Must be after the menu is set
PointerByReference menuServer = new PointerByReference();
PointerByReference rootMenuItem = new PointerByReference();
Gobject.g_object_get(appIndicator.getPointer(), "dbus-menu-server", menuServer, null);
Gobject.g_object_get(menuServer.getValue(), "root-node", rootMenuItem, null);
gtkCallback = new GEventCallback() {
@Override
public
void callback(Pointer notUsed, final GdkEventButton event) {
Gtk.gtk_widget_destroy(dummyMenu);
SwingUtil.invokeLater(popupRunnable);
}
};
nativeLong = Gobject.g_signal_connect_object(rootMenuItem.getValue(), "about-to-show", gtkCallback, null, 0);
}
private void createAppIndicatorMenu() {
dummyMenu = Gtk.gtk_menu_new();
Pointer item = Gtk.gtk_image_menu_item_new_with_mnemonic("");
Gtk.gtk_menu_shell_append(dummyMenu, item);
Gtk.gtk_widget_show_all(item);
AppIndicator.app_indicator_set_menu(appIndicator, dummyMenu);
}
public
void shutdown() {
if (!shuttingDown.getAndSet(true)) {
final CountDownLatch countDownLatch = new CountDownLatch(1);
Gtk.dispatch(new Runnable() {
@Override
public
void run() {
try {
// STATUS_PASSIVE hides the indicator
AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_PASSIVE);
Pointer p = appIndicator.getPointer();
Gobject.g_object_unref(p);
appIndicator = null;
} finally {
countDownLatch.countDown();
}
}
});
// this is slightly different than how swing does it. We have a timeout here so that we can make sure that updates on the GUI
// thread occur in REASONABLE time-frames, and alert the user if not.
try {
if (!countDownLatch.await(TIMEOUT, TimeUnit.SECONDS)) {
SystemTray.logger.error("Event dispatch queue took longer than " + TIMEOUT + " seconds to shutdown. Please adjust " +
"`SystemTray.TIMEOUT` to a value which better suites your environment.");
}
} catch (InterruptedException e) {
SystemTray.logger.error("Error waiting for shutdown dispatch to complete.", new Exception());
}
Gtk.shutdownGui();
// uses EDT
super.remove();
}
}
public
void setImage_(final File imageFile) {
dispatch(new Runnable() {
@Override
public
void run() {
AppIndicator.app_indicator_set_icon(appIndicator, imageFile.getAbsolutePath());
if (!isActive) {
isActive = true;
AppIndicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_ACTIVE);
// kindof lame, but necessary for KDE
if (Gtk.isKDE) {
AppIndicator.app_indicator_set_label(appIndicator, "SystemTray", null);
}
// now we have to setup a way for us to catch the "activation" click on this menu. Must be after the menu is set
hookMenuOpen();
}
}
});
dispatch(new Runnable() {
@Override
public
void run() {
((SwingSystemTrayMenuPopup) _native).setTitleBarImage(imageFile);
}
});
}
}

View File

@ -33,13 +33,11 @@ import javax.swing.JPopupMenu;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
import dorkbox.systemTray.MenuEntry;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.linux.jna.GEventCallback;
import dorkbox.systemTray.linux.jna.GdkEventButton;
import dorkbox.systemTray.linux.jna.Gobject;
import dorkbox.systemTray.linux.jna.Gtk;
import dorkbox.systemTray.util.ImageUtils;
import dorkbox.util.ScreenUtil;
/**
@ -49,7 +47,7 @@ import dorkbox.util.ScreenUtil;
* swing menu popup INSTEAD of GTK menu popups. The "golden standard" is our swing menu popup, since we have 100% control over it.
*/
public
class GtkStatusIconTray extends SwingMenu {
class GtkStatusIconTray extends SwingGenericTray {
private volatile Pointer trayIcon;
// http://code.metager.de/source/xref/gnome/Platform/gtk%2B/gtk/deprecated/gtkstatusicon.c
@ -72,7 +70,6 @@ class GtkStatusIconTray extends SwingMenu {
throw new IllegalArgumentException("Unable to start GtkStatusIcon if 'SystemTray.FORCE_TRAY_TYPE' is set to AppIndicator");
}
JPopupMenu popupMenu = (JPopupMenu) _native;
popupMenu.pack();
popupMenu.setFocusable(true);
@ -117,7 +114,6 @@ class GtkStatusIconTray extends SwingMenu {
// they ALSO do not support tooltips, so we cater to the lowest common denominator
// trayIcon.setToolTip(SwingSystemTray.this.appName);
ImageUtils.determineIconSize();
Gtk.startGui();
Gtk.dispatch(new Runnable() {
@ -131,6 +127,7 @@ class GtkStatusIconTray extends SwingMenu {
@Override
public
void callback(Pointer notUsed, final GdkEventButton event) {
// show the swing menu on the EDT
// BUTTON_PRESS only (any mouse click)
if (event.type == 4) {
// show the swing menu on the EDT
@ -214,6 +211,7 @@ class GtkStatusIconTray extends SwingMenu {
Gtk.shutdownGui();
// uses EDT
super.remove();
}
}
@ -232,53 +230,12 @@ class GtkStatusIconTray extends SwingMenu {
}
}
});
}
public
String getStatus() {
synchronized (menuEntries) {
MenuEntry menuEntry = menuEntries.get(0);
if (menuEntry instanceof SwingEntryStatus) {
return menuEntry.getText();
}
}
return null;
}
@SuppressWarnings("Duplicates")
public
void setStatus(final String statusText) {
dispatch(new Runnable() {
@Override
public
void run() {
synchronized (menuEntries) {
// status is ALWAYS at 0 index...
SwingEntry menuEntry = null;
if (!menuEntries.isEmpty()) {
menuEntry = (SwingEntry) menuEntries.get(0);
}
if (menuEntry instanceof SwingEntryStatus) {
// set the text or delete...
if (statusText == null) {
// delete
remove(menuEntry);
}
else {
// set text
menuEntry.setText(statusText);
}
} else {
// create a new one
menuEntry = new SwingEntryStatus(GtkStatusIconTray.this, statusText);
// status is ALWAYS at 0 index...
menuEntries.add(0, menuEntry);
}
}
((SwingSystemTrayMenuPopup) _native).setTitleBarImage(iconFile);
}
});
}

View File

@ -0,0 +1,77 @@
package dorkbox.systemTray.swing;
import javax.swing.JComponent;
import dorkbox.systemTray.Menu;
import dorkbox.systemTray.MenuEntry;
import dorkbox.systemTray.SystemTray;
import dorkbox.systemTray.util.ImageUtils;
/**
*
*/
public abstract
class SwingGenericTray extends SwingMenu {
/**
* Called in the EDT
*
* @param systemTray
* the system tray (which is the object that sits in the system tray)
* @param parent
* @param _native
*/
SwingGenericTray(final SystemTray systemTray, final Menu parent, final JComponent _native) {
super(systemTray, parent, _native);
ImageUtils.determineIconSize();
}
public
String getStatus() {
synchronized (menuEntries) {
MenuEntry menuEntry = menuEntries.get(0);
if (menuEntry instanceof SwingEntryStatus) {
return menuEntry.getText();
}
}
return null;
}
public
void setStatus(final String statusText) {
final SwingMenu _this = this;
dispatchAndWait(new Runnable() {
@Override
public
void run() {
synchronized (menuEntries) {
// status is ALWAYS at 0 index...
SwingEntry menuEntry = null;
if (!menuEntries.isEmpty()) {
menuEntry = (SwingEntry) menuEntries.get(0);
}
if (menuEntry instanceof SwingEntryStatus) {
// set the text or delete...
if (statusText == null) {
// delete
remove(menuEntry);
}
else {
// set text
menuEntry.setText(statusText);
}
} else {
// create a new one
menuEntry = new SwingEntryStatus(_this, statusText);
// status is ALWAYS at 0 index...
menuEntries.add(0, menuEntry);
}
}
}
});
}
}

View File

@ -30,7 +30,6 @@ import javax.swing.ImageIcon;
import javax.swing.JPopupMenu;
import dorkbox.systemTray.MenuEntry;
import dorkbox.systemTray.util.ImageUtils;
import dorkbox.util.ScreenUtil;
/**
@ -43,7 +42,7 @@ import dorkbox.util.ScreenUtil;
*/
@SuppressWarnings({"SynchronizationOnLocalVariableOrMethodParameter", "WeakerAccess"})
public
class SwingSystemTray extends SwingMenu {
class SwingSystemTray extends SwingGenericTray {
volatile SystemTray tray;
volatile TrayIcon trayIcon;
@ -52,8 +51,6 @@ class SwingSystemTray extends SwingMenu {
SwingSystemTray(final dorkbox.systemTray.SystemTray systemTray) {
super(systemTray, null, new SwingSystemTrayMenuPopup());
ImageUtils.determineIconSize();
SwingSystemTray.this.tray = SystemTray.getSystemTray();
}
@ -77,55 +74,6 @@ class SwingSystemTray extends SwingMenu {
});
}
public
String getStatus() {
synchronized (menuEntries) {
MenuEntry menuEntry = menuEntries.get(0);
if (menuEntry instanceof SwingEntryStatus) {
return menuEntry.getText();
}
}
return null;
}
@SuppressWarnings("Duplicates")
public
void setStatus(final String statusText) {
dispatch(new Runnable() {
@Override
public
void run() {
synchronized (menuEntries) {
// status is ALWAYS at 0 index...
SwingEntry menuEntry = null;
if (!menuEntries.isEmpty()) {
menuEntry = (SwingEntry) menuEntries.get(0);
}
if (menuEntry instanceof SwingEntryStatus) {
// set the text or delete...
if (statusText == null) {
// delete
remove(menuEntry);
}
else {
// set text
menuEntry.setText(statusText);
}
} else {
// create a new one
menuEntry = new SwingEntryStatus(SwingSystemTray.this, statusText);
// status is ALWAYS at 0 index...
menuEntries.add(0, menuEntry);
}
}
}
});
}
public
void setImage_(final File iconFile) {
dispatch(new Runnable() {
@ -185,14 +133,14 @@ class SwingSystemTray extends SwingMenu {
try {
tray.add(trayIcon);
((SwingSystemTrayMenuPopup) _native).setIcon(iconFile);
} catch (AWTException e) {
dorkbox.systemTray.SystemTray.logger.error("TrayIcon could not be added.", e);
}
} else {
((SwingSystemTrayMenuPopup) _native).setIcon(iconFile);
trayIcon.setImage(trayImage);
}
((SwingSystemTrayMenuPopup) _native).setTitleBarImage(iconFile);
}
});
}

View File

@ -39,6 +39,7 @@ import dorkbox.util.OS;
*
* This is our "golden standard" since we have 100% control over it.
*/
public
class SwingSystemTrayMenuPopup extends JPopupMenu {
private static final long serialVersionUID = 1L;
@ -47,6 +48,7 @@ class SwingSystemTrayMenuPopup extends JPopupMenu {
private volatile File iconFile;
@SuppressWarnings("unchecked")
public
SwingSystemTrayMenuPopup() {
super();
setFocusable(true);
@ -113,24 +115,25 @@ class SwingSystemTrayMenuPopup extends JPopupMenu {
}
/**
* Sets the icon for the title-bar, so IF it shows in the task-bar, it will have the corresponding icon as the SystemTray icon
* Sets the image for the title-bar, so IF it shows in the task-bar, it will have the corresponding image as the SystemTray image
*/
void setIcon(final File iconFile) {
if (this.iconFile == null || !this.iconFile.equals(iconFile)) {
this.iconFile = iconFile;
void setTitleBarImage(final File imageFile) {
if (this.iconFile == null || !this.iconFile.equals(imageFile)) {
this.iconFile = imageFile;
try {
Image image = new ImageIcon(ImageIO.read(iconFile)).getImage();
Image image = new ImageIcon(ImageIO.read(imageFile)).getImage();
image.flush();
// we set the dialog window to have the same icon as what is on the system tray
hiddenDialog.setIconImage(image);
} catch (IOException e) {
SystemTray.logger.error("Error setting the icon for the popup menu task tray dialog");
SystemTray.logger.error("Error setting the title-bar image for the popup menu task tray dialog");
}
}
}
public
void doShow(final int x, final int y) {
// critical to get the keyboard listeners working for the popup menu
setInvoker(hiddenDialog.getContentPane());