Post

Implementing Tray Icon for Common Linux Environments - final part

Cryptomator issue #1645

Please find the first post of this story here.

Some revisions and changes

Changed icons

The feed back for the first PR I submitted included the request to use icons provided by the Cryptomator project, so I included these.

Different image data handling

Furthermore the devs wished, that the widening of the integrations-* API should implement an URI parameter, instead of adding the String. And in this case transporting the image as a byte array can be omitted, as URIs do support direct data transportation via the data scheme and file paths can be specified by the file scheme:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
From 17fbf493eace2e32cec3658fafab98d7d9311e37 Mon Sep 17 00:00:00 2001
From: Ralph Plawetzki <ralph@purejava.org>
Date: Sun, 23 Apr 2023 08:15:09 +0200
Subject: [PATCH 3/3] Change API to support svg icons

---
 .../integrations/tray/TrayMenuController.java         | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

diff --git a/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java b/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java
index 85f8c63..5cdd413 100644
--- a/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java
+++ b/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java
@@ -3,8 +3,7 @@
 import org.cryptomator.integrations.common.IntegrationsLoader;
 import org.jetbrains.annotations.ApiStatus;
 
-import java.io.IOException;
-import java.io.InputStream;
+import java.net.URI;
 import java.util.List;
 import java.util.Optional;
 
@@ -23,20 +22,20 @@ static Optional<TrayMenuController> get() {
  /**
   * Displays an icon on the system tray.
   *
-  * @param imageData     What image to show
+  * @param imageUri      What image to show
   * @param defaultAction Action to perform when interacting with the icon directly instead of its menu
   * @param tooltip       Text shown when hovering
   * @throws TrayMenuException thrown when adding the tray icon failed
   */
- void showTrayIcon(byte[] imageData, Runnable defaultAction, String tooltip) throws TrayMenuException;
+ void showTrayIcon(URI imageUri, Runnable defaultAction, String tooltip) throws TrayMenuException;
 
  /**
   * Updates the icon on the system tray.
   *
-  * @param imageData What image to show
+  * @param imageUri What image to show
   * @throws IllegalStateException thrown when called before an icon has been added
   */
- void updateTrayIcon(byte[] imageData);
+ void updateTrayIcon(URI imageUri);
 
  /**
   * Show the given options in the tray menu.

As proposed: JDK 19 -> JDK 20

The final step was to migrate my implementation to JDK 20. This required code changes, as the FFI has changed with JDK 20 (JEP 434). MemorySession was split into Arena and SegmentScope to promote sharing of segments across maintenance boundaries:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
diff --git a/src/main/java/org/cryptomator/ui/traymenu/AppindicatorTrayMenuController.java b/src/main/java/org/cryptomator/ui/traymenu/AppindicatorTrayMenuController.java
index 8f8e7653f3..4be9b690f9 100644
--- a/src/main/java/org/cryptomator/ui/traymenu/AppindicatorTrayMenuController.java
+++ b/src/main/java/org/cryptomator/ui/traymenu/AppindicatorTrayMenuController.java
@@ -13,8 +13,8 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.File;
-import java.lang.foreign.MemoryAddress;
-import java.lang.foreign.MemorySession;
+import java.lang.foreign.MemorySegment;
+import java.lang.foreign.SegmentScope;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.file.Paths;
@@ -27,9 +27,9 @@ public class AppindicatorTrayMenuController implements TrayMenuController {
 
  private static final Logger LOG = LoggerFactory.getLogger(AppindicatorTrayMenuController.class);
 
- private final MemorySession session = MemorySession.openShared();
- private MemoryAddress indicator;
- private MemoryAddress menu = gtk_menu_new();
+ private final SegmentScope scope = SegmentScope.auto();
+ private MemorySegment indicator;
+ private MemorySegment menu = gtk_menu_new();
 
  @CheckAvailability
  public static boolean isAvailable() {
@@ -64,7 +64,7 @@ public void onBeforeOpenMenu(Runnable runnable) {
 
  }
 
- private void addChildren(MemoryAddress menu, List<TrayMenuItem> items) {
+ private void addChildren(MemorySegment menu, List<TrayMenuItem> items) {
    for (var item : items) {
      // TODO: use Pattern Matching for switch, once available
      if (item instanceof ActionItem a) {
@@ -72,7 +72,7 @@ private void addChildren(MemoryAddress menu, List<TrayMenuItem> items) {
        gtk_menu_item_set_label(gtkMenuItem, MemoryAllocator.ALLOCATE_FOR(a.title()));
        g_signal_connect_object(gtkMenuItem,
            MemoryAllocator.ALLOCATE_FOR("activate"),
-           MemoryAllocator.ALLOCATE_CALLBACK_FOR(new ActionItemCallback(a), session),
+           MemoryAllocator.ALLOCATE_CALLBACK_FOR(new ActionItemCallback(a), scope),
            menu,
            0);
        gtk_menu_shell_append(menu, gtkMenuItem);

Review pending

All coding was submitted as two PRs, that are awaiting their review:

Polymorphic tray icon loading - wait, what?

After having changed the integrations-* API for two times, the Cryptomator devs came up with a further re-design, that allows to extend the API in the future.

The new design uses sealed classes and inversion of control, so that the implementation decides, which kind of icon it needs, e.g.:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class AppindicatorTrayMenuController implements TrayMenuController {
  // ...
  @Override
  public void updateTrayIcon(Consumer<TrayIconLoader> iconLoader) {
    TrayIconLoader.PngData callback = this::updateTrayIconWithSVG;
    iconLoader.accept(callback);
  }
    
  private void updateTrayIconWithSVG(String s) {
    try (var arena = Arena.openConfined()) {
      app_indicator_set_icon(indicator, arena.allocateUtf8String(s));
    }
  }
  // ...
}

On the consumer side, invocation looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TrayMenuBuilder {
  // ...
  private void vaultListChanged(@SuppressWarnings("unused") Observable observable) {
    assert Platform.isFxApplicationThread();
    trayMenu.updateTrayIcon(loader -> {
      switch (loader) {
        case TrayIconLoader.PngData l -> l.loadPng(getAppropriateTrayIconImage());
        case TrayIconLoader.FreedesktopIconName l -> l.lookupByName(getAppropriateFreedesktopIconName());
      }
    });
    rebuildMenu();
  }

  private String getAppropriateFreedesktopIconName() {
    boolean isAnyVaultUnlocked = vaults.stream().anyMatch(Vault::isUnlocked);

    return isAnyVaultUnlocked ? "org.cryptomator.Cryptomator-monochrome-unlocked" : "org.cryptomator.Cryptomator-monochrome";
  }
  // ...
}

I think, this is a quite sophisticated approach and implemented it.

Adding a module-info.java required further changes

As more and more Java projects are getting modularized, a module info was added for Cryptomator integrations-linux as well. This made a problem visible, that other libraries used with Cryptomator were using name spaces, that did not adhere to the standard and needed to be changed.

One of these is my own kdewallet and it caused a so called “split jar” problem. But this could be fixed easily by changing some packages names and add a module info there as well. In relese 1.3.0 of kdewallet this issue is fixed.

Icons for every Linux desktop environment

Making SVG icons available as tray icons for Cryptomator led to the question, how icons are displayed on various Linux desktop environments. Especially, when a desktop theme is changed - will the icon reflect this change and still be visible?

We are investigating this at the moment …

This post is licensed under CC BY 4.0 by the author.

Trending Tags