SystemTray/src/dorkbox/systemTray/ui/gtk/_AppIndicatorNativeTray.java

239 lines
9.3 KiB
Java

/*
* Copyright 2021 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.ui.gtk;
import java.io.File;
import java.util.concurrent.atomic.AtomicBoolean;
import com.sun.jna.Pointer;
import dorkbox.jna.linux.AppIndicator;
import dorkbox.jna.linux.GObject;
import dorkbox.jna.linux.GtkEventDispatch;
import dorkbox.jna.linux.structs.AppIndicatorInstanceStruct;
import dorkbox.systemTray.MenuItem;
import dorkbox.systemTray.Tray;
import dorkbox.systemTray.util.ImageResizeUtil;
import dorkbox.systemTray.util.SizeAndScalingUtil;
/**
* 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
*/
@SuppressWarnings("Duplicates")
public final
class _AppIndicatorNativeTray extends Tray {
public static boolean isLoaded = false;
private volatile 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 final AtomicBoolean shuttingDown = new AtomicBoolean();
// is the system tray visible or not.
private volatile boolean visible = true;
private volatile File imageFile;
// has the name already been set for the indicator?
private volatile boolean setName = false;
// appindicators DO NOT support anything other than PLAIN gtk-menus
// they ALSO do not support tooltips!!
// https://bugs.launchpad.net/indicator-application/+bug/527458/comments/12
public
_AppIndicatorNativeTray(final String trayName, final ImageResizeUtil imageResizeUtil, final Runnable onRemoveEvent) {
super(onRemoveEvent);
isLoaded = true;
// setup some GTK menu bits...
GtkBaseMenuItem.transparentIcon = imageResizeUtil.getTransparentImage();
GtkMenuItemCheckbox.uncheckedFile = imageResizeUtil.getTransparentImage().getAbsolutePath();
// we override various methods, because each tray implementation is SLIGHTLY different. This allows us customization.
final GtkMenu gtkMenu = new GtkMenu() {
/**
* MUST BE AFTER THE ITEM IS ADDED/CHANGED from the menu
*
* ALWAYS CALLED ON THE EDT
*/
@Override
protected final
void onMenuAdded(final Pointer menu) {
// see: https://code.launchpad.net/~mterry/libappindicator/fix-menu-leak/+merge/53247
appIndicator.app_indicator_set_menu(menu);
if (!setName) {
setName = true;
// in GNOME by default, the title/name of the tray icon is "java". We are the only java-based tray icon, so we just use that.
// If you change "SystemTray" to something else, make sure to change it in extension.js as well
// can cause (potentially)
// GLib-GIO-CRITICAL **: g_dbus_connection_emit_signal: assertion 'object_path != NULL && g_variant_is_object_path (object_path)' failed
// Gdk-CRITICAL **: IA__gdk_window_thaw_toplevel_updates_libgtk_only: assertion 'private->update_and_descendants_freeze_count > 0' failed
// necessary for gnome icon detection/placement because we move tray icons around by title. This is hardcoded
// in extension.js, so don't change it
// additionally, this is required to be set HERE (not somewhere else)
appIndicator.app_indicator_set_title(trayName);
}
}
@Override
public
void setEnabled(final MenuItem menuItem) {
GtkEventDispatch.dispatch(()->{
boolean enabled = menuItem.getEnabled();
if (visible && !enabled) {
// STATUS_PASSIVE hides the indicator
appIndicator.app_indicator_set_status(AppIndicator.STATUS_PASSIVE);
visible = false;
}
else if (!visible && enabled) {
appIndicator.app_indicator_set_status(AppIndicator.STATUS_ACTIVE);
visible = true;
}
});
}
@Override
public
void setImage(final MenuItem menuItem) {
imageFile = menuItem.getImage();
if (imageFile == null) {
return;
}
GtkEventDispatch.dispatch(()->{
appIndicator.app_indicator_set_icon(imageFile.getAbsolutePath());
if (!isActive) {
isActive = true;
appIndicator.app_indicator_set_status(AppIndicator.STATUS_ACTIVE);
}
});
}
@Override
public
void setText(final MenuItem menuItem) {
// no op.
}
@Override
public
void setShortcut(final MenuItem menuItem) {
// no op.
}
@Override
public
void setTooltip(final MenuItem menuItem) {
// no op. see https://bugs.launchpad.net/indicator-application/+bug/527458/comments/12
}
@Override
public
void remove() {
// This is required if we have JavaFX or SWT shutdown hooks (to prevent us from shutting down twice...)
if (!shuttingDown.getAndSet(true)) {
super.remove();
GtkEventDispatch.dispatch(()->{
// must happen asap, so our hook properly notices we are in shutdown mode
final AppIndicatorInstanceStruct savedAppIndicator = appIndicator;
appIndicator = null;
// STATUS_PASSIVE hides the indicator
savedAppIndicator.app_indicator_set_status(AppIndicator.STATUS_PASSIVE);
Pointer p = savedAppIndicator.getPointer();
GObject.g_object_unref(p);
GtkEventDispatch.shutdownGui();
});
}
}
};
GtkEventDispatch.dispatchAndWait(()->{
String id = "DBST" + System.nanoTime();
// we initialize with a blank image. Throws RuntimeException if not possible (this should never happen!)
// Ubuntu 17.10 REQUIRES this to be the correct tray image size, otherwise we get the error:
// GLib-GIO-CRITICAL **: g_dbus_proxy_new: assertion 'G_IS_DBUS_CONNECTION (connection)' failed
File image = imageResizeUtil.getTransparentImage(SizeAndScalingUtil.TRAY_MENU_SIZE);
appIndicator = AppIndicator.app_indicator_new(id, image.getAbsolutePath(), AppIndicator.CATEGORY_APPLICATION_STATUS);
});
GtkEventDispatch.waitForEventsToComplete();
bind(gtkMenu, null, imageResizeUtil);
}
@Override
public
boolean hasImage() {
return imageFile != null;
}
}