Skip to content
Ralph Plawetzki edited this page Mar 15, 2024 · 7 revisions

Introduction

These are Java bindings for libayatana-appindicator and libappindicator-gtk3, created with jextract on Java 22. They make it possible to build appindicator tray icons for Java applications.

Bildschirmfoto 2023-04-11 um 18 43 00

The icons used for the tray icon itself can be SVG, that scale and should look ok on all desktop environments.

Requirements

Coding

After adding the Maven artifact to your code, you can simply define an appindicator tray icon like this:

From release 1.4.0

A new public API eases use of the bindings:

package com.purejava.fxdemo;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.purejava.appindicator.AppIndicator;
import org.purejava.appindicator.GObject;
import org.purejava.appindicator.Gtk;
import org.purejava.appindicator.GCallback;

import java.io.IOException;
import java.lang.foreign.Arena;

import static org.purejava.appindicator.app_indicator_h.*;

public class HelloApplication extends Application {
    private static final Arena ARENA = Arena.global();

    @Override
    public void start(Stage stage) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
        Scene scene = new Scene(fxmlLoader.load(), 320, 240);
        stage.setTitle("Hello!");
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        try (var arena = Arena.ofConfined()) {
            arenaAuto = Arena.ofAuto();
            var indicator = AppIndicator.newIndicator("example-simple-client",
                    "indicator-message",
                    APP_INDICATOR_CATEGORY_APPLICATION_STATUS());
            var gtkSeparator = Gtk.newMenuItem();
            var gtkMenu = Gtk.newMenu();
            var gtkSubmenu = Gtk.newMenu();
            var gtkMenuItem = Gtk.newMenuItem();
            Gtk.menuItemSetLabel(gtkMenuItem, "More");
            var gtkSMenuItem = Gtk.newMenuItem();
            Gtk.menuItemSetLabel(gtkSMenuItem, "Change icon");
            var gtkSMenuItem1 = Gtk.newMenuItem();
            Gtk.menuItemSetLabel(gtkSMenuItem1, "Quit");
            Gtk.menuShellAppend(gtkSubmenu, gtkSMenuItem);
            Gtk.menuShellAppend(gtkSubmenu, gtkSeparator);
            Gtk.menuShellAppend(gtkSubmenu, gtkSMenuItem1);
            Gtk.menuItemSetSubmenu(gtkMenuItem, gtkSubmenu);
            GObject.signalConnectObject(gtkSMenuItem1, "activate", GCallback.allocate(new QuitCallback(), arenaAuto), gtkMenu, 0);
            GObject.signalConnectObject(gtkSMenuItem, "activate", GCallback.allocate(new ChangeIconCallback(indicator), arenaAuto), gtkMenu, 0);
            Gtk.menuShellAppend(gtkMenu, gtkMenuItem);
            Gtk.widgetShowAll(gtkMenu);
            AppIndicator.setMenu(indicator, gtkMenu);
            AppIndicator.setAttentionIcon(indicator, "indicator-messages-new");
            AppIndicator.setStatus(indicator, APP_INDICATOR_STATUS_ACTIVE());
            launch();
        }
    }
}

From release 1.3.6

package com.purejava.fxdemo;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.purejava.appindicator.GCallback;

import java.io.IOException;
import java.lang.foreign.Arena;

import static org.purejava.appindicator.app_indicator_h.*;

public class HelloApplication extends Application {
    private static final Arena ARENA = Arena.global();

    @Override
    public void start(Stage stage) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
        Scene scene = new Scene(fxmlLoader.load(), 320, 240);
        stage.setTitle("Hello!");
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        try (var arena = Arena.ofConfined()) {
            var indicator = app_indicator_new(arena.allocateUtf8String("example-simple-client"),
                    arena.allocateUtf8String("indicator-message"),
                    APP_INDICATOR_CATEGORY_APPLICATION_STATUS());
            var gtkSeparator = gtk_menu_item_new();
            var gtkMenu = gtk_menu_new();
            var gtkSubmenu = gtk_menu_new();
            var gtkMenuItem = gtk_menu_item_new();
            gtk_menu_item_set_label(gtkMenuItem, arena.allocateUtf8String("More"));
            var gtkSMenuItem = gtk_menu_item_new();
            gtk_menu_item_set_label(gtkSMenuItem, arena.allocateUtf8String("Change icon"));
            var gtkSMenuItem1 = gtk_menu_item_new();
            gtk_menu_item_set_label(gtkSMenuItem1, arena.allocateUtf8String("Quit"));
            gtk_menu_shell_append(gtkSubmenu, gtkSMenuItem);
            gtk_menu_shell_append(gtkSubmenu, gtkSeparator);
            gtk_menu_shell_append(gtkSubmenu, gtkSMenuItem1);
            gtk_menu_item_set_submenu(gtkMenuItem, gtkSubmenu);
            g_signal_connect_object(gtkSMenuItem1, arena.allocateUtf8String("activate"), GCallback.allocate(new QuitCallback(), ARENA), gtkMenu, 0);
            g_signal_connect_object(gtkSMenuItem, arena.allocateUtf8String("activate"), GCallback.allocate(new ChangeIconCallback(indicator), ARENA), gtkMenu, 0);
            gtk_menu_shell_append(gtkMenu, gtkMenuItem);
            gtk_widget_show_all(gtkMenu);
            app_indicator_set_menu(indicator, gtkMenu);
            app_indicator_set_attention_icon(indicator, arena.allocateUtf8String("indicator-messages-new"));
            app_indicator_set_status(indicator, APP_INDICATOR_STATUS_ACTIVE());
            launch();
        }
    }
}

Up to release 1.3.5

package com.purejava.fxdemo;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.purejava.appindicator.GCallback;

import java.io.IOException;
import java.lang.foreign.Arena;
import java.lang.foreign.SegmentScope;

import static org.purejava.appindicator.app_indicator_h.*;

public class HelloApplication extends Application {
    private static final SegmentScope SCOPE = SegmentScope.global();

    @Override
    public void start(Stage stage) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
        Scene scene = new Scene(fxmlLoader.load(), 320, 240);
        stage.setTitle("Hello!");
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        try (var arena = Arena.openConfined()) {
            var indicator = app_indicator_new(arena.allocateUtf8String("example-simple-client"),
                    arena.allocateUtf8String("indicator-message"),
                    APP_INDICATOR_CATEGORY_APPLICATION_STATUS());
            var gtkSeparator = gtk_menu_item_new();
            var gtkMenu = gtk_menu_new();
            var gtkSubmenu = gtk_menu_new();
            var gtkMenuItem = gtk_menu_item_new();
            gtk_menu_item_set_label(gtkMenuItem, arena.allocateUtf8String("More"));
            var gtkSMenuItem = gtk_menu_item_new();
            gtk_menu_item_set_label(gtkSMenuItem, arena.allocateUtf8String("Change icon"));
            var gtkSMenuItem1 = gtk_menu_item_new();
            gtk_menu_item_set_label(gtkSMenuItem1, arena.allocateUtf8String("Quit"));
            gtk_menu_shell_append(gtkSubmenu, gtkSMenuItem);
            gtk_menu_shell_append(gtkSubmenu, gtkSeparator);
            gtk_menu_shell_append(gtkSubmenu, gtkSMenuItem1);
            gtk_menu_item_set_submenu(gtkMenuItem, gtkSubmenu);
            g_signal_connect_object(gtkSMenuItem1, arena.allocateUtf8String("activate"), GCallback.allocate(new QuitCallback(), SCOPE), gtkMenu, 0);
            g_signal_connect_object(gtkSMenuItem, arena.allocateUtf8String("activate"), GCallback.allocate(new ChangeIconCallback(indicator), SCOPE), gtkMenu, 0);
            gtk_menu_shell_append(gtkMenu, gtkMenuItem);
            gtk_widget_show_all(gtkMenu);
            app_indicator_set_menu(indicator, gtkMenu);
            app_indicator_set_attention_icon(indicator, arena.allocateUtf8String("indicator-messages-new"));
            app_indicator_set_status(indicator, APP_INDICATOR_STATUS_ACTIVE());
            launch();
        }
    }
}

Don't forget to add the appindicator module to your module-info.java:

module com.purejava.fxdemo {
    requires javafx.controls;
    requires javafx.fxml;

    requires org.kordamp.bootstrapfx.core;
    requires org.slf4j;
    requires org.purejava.appindicator;

    opens com.purejava.fxdemo to javafx.fxml;
    exports com.purejava.fxdemo;
}

For wiring a tray icon menu item entry to an action, use the GCallback:

package com.purejava.fxdemo;

import org.purejava.appindicator.GCallback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class QuitCallback implements GCallback.Function {
    private static final Logger LOG = LoggerFactory.getLogger(QuitCallback.class);
    @Override
    public void apply() {
        LOG.info("Hit Quit");
        System.exit(0);
    }
}

Software stack

Some notes on requirements to get appindicator support:

  • libayatana-appindicator is a native library, that needs to be installed on the system. It is on most recent Linux desktop environments though. On Ubuntu, the package is named libayatana-appindicator3-1, on Arch linux it's called libayatana-appindicator.
  • There is another native library, that's used as a fall back, if it is installed on the system: libappindicator. On Ubuntu, the package is named libappindicator3-1, on Arch linux it's called libappindicator-gtk3.
  • Recent GNOME desktop environments by default do not allow the SystemTray icon to be shown. It's necessary to install the Appindicator Support plugin to show an appindicator icon at all