All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dorkbox.systemTray.ui.gtk._AppIndicatorNativeTray Maven / Gradle / Ivy

Go to download

Cross-platform SystemTray support for Swing/AWT, GtkStatusIcon, and AppIndicator on Java 8+

There is a newer version: 4.4
Show newest version
/*
 * 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;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy