/* * 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; } }