Skip to content
Ralph Plawetzki edited this page Jul 4, 2026 · 9 revisions

Introduction

These are Java bindings for libayatana-appindicator and libappindicator-gtk3, created with jextract on Java 25. 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.1

A new method allows to set an entry in the tray menu insensitive:

Gtk.widgetSetSensitive(gtkMenuItem, false);

From release 1.4.0

A new public API eases use of the bindings:

packagecom.purejava.fxdemo;
importjavafx.application.Application;
importjavafx.fxml.FXMLLoader;
importjavafx.scene.Scene;
importjavafx.stage.Stage;
importorg.purejava.appindicator.AppIndicator;
importorg.purejava.appindicator.GObject;
importorg.purejava.appindicator.Gtk;
importorg.purejava.appindicator.GCallback;
importjava.io.IOException;
importjava.lang.foreign.Arena;
importstaticorg.purejava.appindicator.app_indicator_h.*;
publicclassHelloApplicationextendsApplication {
privatestaticfinalArenaARENA = Arena.global();
@Overridepublicvoidstart(Stagestage) throwsIOException {
FXMLLoaderfxmlLoader = newFXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
Scenescene = newScene(fxmlLoader.load(), 320, 240);
stage.setTitle("Hello!");
stage.setScene(scene);
stage.show();
}
publicstaticvoidmain(String[] args) {
try (vararena = Arena.ofConfined()) {
arenaAuto = Arena.ofAuto();
varindicator = AppIndicator.newIndicator("example-simple-client",
"indicator-message",
APP_INDICATOR_CATEGORY_APPLICATION_STATUS());
vargtkSeparator = Gtk.newMenuItem();
vargtkMenu = Gtk.newMenu();
vargtkSubmenu = Gtk.newMenu();
vargtkMenuItem = Gtk.newMenuItem();
Gtk.menuItemSetLabel(gtkMenuItem, "More");
vargtkSMenuItem = Gtk.newMenuItem();
Gtk.menuItemSetLabel(gtkSMenuItem, "Change icon");
vargtkSMenuItem1 = 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(newQuitCallback(), arenaAuto), gtkMenu, 0);
GObject.signalConnectObject(gtkSMenuItem, "activate", GCallback.allocate(newChangeIconCallback(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

packagecom.purejava.fxdemo;
importjavafx.application.Application;
importjavafx.fxml.FXMLLoader;
importjavafx.scene.Scene;
importjavafx.stage.Stage;
importorg.purejava.appindicator.GCallback;
importjava.io.IOException;
importjava.lang.foreign.Arena;
importstaticorg.purejava.appindicator.app_indicator_h.*;
publicclassHelloApplicationextendsApplication {
privatestaticfinalArenaARENA = Arena.global();
@Overridepublicvoidstart(Stagestage) throwsIOException {
FXMLLoaderfxmlLoader = newFXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
Scenescene = newScene(fxmlLoader.load(), 320, 240);
stage.setTitle("Hello!");
stage.setScene(scene);
stage.show();
}
publicstaticvoidmain(String[] args) {
try (vararena = Arena.ofConfined()) {
varindicator = app_indicator_new(arena.allocateUtf8String("example-simple-client"),
arena.allocateUtf8String("indicator-message"),
APP_INDICATOR_CATEGORY_APPLICATION_STATUS());
vargtkSeparator = gtk_menu_item_new();
vargtkMenu = gtk_menu_new();
vargtkSubmenu = gtk_menu_new();
vargtkMenuItem = gtk_menu_item_new();
gtk_menu_item_set_label(gtkMenuItem, arena.allocateUtf8String("More"));
vargtkSMenuItem = gtk_menu_item_new();
gtk_menu_item_set_label(gtkSMenuItem, arena.allocateUtf8String("Change icon"));
vargtkSMenuItem1 = 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(newQuitCallback(), ARENA), gtkMenu, 0);
g_signal_connect_object(gtkSMenuItem, arena.allocateUtf8String("activate"), GCallback.allocate(newChangeIconCallback(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

packagecom.purejava.fxdemo;
importjavafx.application.Application;
importjavafx.fxml.FXMLLoader;
importjavafx.scene.Scene;
importjavafx.stage.Stage;
importorg.purejava.appindicator.GCallback;
importjava.io.IOException;
importjava.lang.foreign.Arena;
importjava.lang.foreign.SegmentScope;
importstaticorg.purejava.appindicator.app_indicator_h.*;
publicclassHelloApplicationextendsApplication {
privatestaticfinalSegmentScopeSCOPE = SegmentScope.global();
@Overridepublicvoidstart(Stagestage) throwsIOException {
FXMLLoaderfxmlLoader = newFXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
Scenescene = newScene(fxmlLoader.load(), 320, 240);
stage.setTitle("Hello!");
stage.setScene(scene);
stage.show();
}
publicstaticvoidmain(String[] args) {
try (vararena = Arena.openConfined()) {
varindicator = app_indicator_new(arena.allocateUtf8String("example-simple-client"),
arena.allocateUtf8String("indicator-message"),
APP_INDICATOR_CATEGORY_APPLICATION_STATUS());
vargtkSeparator = gtk_menu_item_new();
vargtkMenu = gtk_menu_new();
vargtkSubmenu = gtk_menu_new();
vargtkMenuItem = gtk_menu_item_new();
gtk_menu_item_set_label(gtkMenuItem, arena.allocateUtf8String("More"));
vargtkSMenuItem = gtk_menu_item_new();
gtk_menu_item_set_label(gtkSMenuItem, arena.allocateUtf8String("Change icon"));
vargtkSMenuItem1 = 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(newQuitCallback(), SCOPE), gtkMenu, 0);
g_signal_connect_object(gtkSMenuItem, arena.allocateUtf8String("activate"), GCallback.allocate(newChangeIconCallback(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:

modulecom.purejava.fxdemo {
requiresjavafx.controls;
requiresjavafx.fxml;
requiresorg.kordamp.bootstrapfx.core;
requiresorg.slf4j;
requiresorg.purejava.appindicator;
openscom.purejava.fxdemotojavafx.fxml;
exportscom.purejava.fxdemo;
}

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

packagecom.purejava.fxdemo;
importorg.purejava.appindicator.GCallback;
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
publicclassQuitCallbackimplementsGCallback.Function {
privatestaticfinalLoggerLOG = LoggerFactory.getLogger(QuitCallback.class);
@Overridepublicvoidapply() {
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