Add AppindicatorTrayMenuController

This commit is contained in:
Ralph Plawetzki
2023-04-08 16:22:27 +02:00
parent aa03bd119a
commit 6da107f4db
10 changed files with 179 additions and 15 deletions

View File

@@ -1,6 +1,7 @@
import ch.qos.logback.classic.spi.Configurator;
import org.cryptomator.integrations.tray.TrayMenuController;
import org.cryptomator.logging.LogbackConfiguratorFactory;
import org.cryptomator.ui.traymenu.AppindicatorTrayMenuController;
import org.cryptomator.ui.traymenu.AwtTrayMenuController;
open module org.cryptomator.desktop {
@@ -31,12 +32,13 @@ open module org.cryptomator.desktop {
requires com.tobiasdiez.easybind;
requires dagger;
requires io.github.coffeelibs.tinyoauth2client;
requires libappindicator.gtk3.java.minimal;
requires org.slf4j;
requires org.apache.commons.lang3;
/* TODO: filename-based modules: */
requires static javax.inject; /* ugly dagger/guava crap */
provides TrayMenuController with AwtTrayMenuController;
provides TrayMenuController with AwtTrayMenuController, AppindicatorTrayMenuController;
provides Configurator with LogbackConfiguratorFactory;
}

View File

@@ -0,0 +1,21 @@
package org.cryptomator.ui.traymenu;
import org.cryptomator.integrations.tray.ActionItem;
import org.purejava.linux.GCallback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ActionItemCallback implements GCallback {
private static final Logger LOG = LoggerFactory.getLogger(ActionItemCallback.class);
private ActionItem actionItem;
public ActionItemCallback(ActionItem actionItem) {
this.actionItem = actionItem;
}
@Override
public void apply() {
LOG.debug("Hit tray menu action '{}'", actionItem.title());
actionItem.action().run();
}
}

View File

@@ -0,0 +1,110 @@
package org.cryptomator.ui.traymenu;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.integrations.common.CheckAvailability;
import org.cryptomator.integrations.tray.ActionItem;
import org.cryptomator.integrations.tray.SeparatorItem;
import org.cryptomator.integrations.tray.SubMenuItem;
import org.cryptomator.integrations.tray.TrayMenuController;
import org.cryptomator.integrations.tray.TrayMenuException;
import org.cryptomator.integrations.tray.TrayMenuItem;
import org.purejava.linux.MemoryAllocator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.lang.foreign.MemoryAddress;
import java.lang.foreign.MemorySession;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Paths;
import java.util.List;
import static org.purejava.linux.app_indicator_h.*;
@CheckAvailability
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();
@CheckAvailability
public static boolean isAvailable() {
return SystemUtils.IS_OS_LINUX && MemoryAllocator.isLoadedNativeLib();
}
@Override
public void showTrayIcon(URI uri, Runnable runnable, String s) throws TrayMenuException {
indicator = app_indicator_new(MemoryAllocator.ALLOCATE_FOR("org.cryptomator.Cryptomator"),
MemoryAllocator.ALLOCATE_FOR(getAbsolutePath(getPathString(uri))),
APP_INDICATOR_CATEGORY_APPLICATION_STATUS());
gtk_widget_show_all(menu);
app_indicator_set_menu(indicator, menu);
app_indicator_set_status(indicator, APP_INDICATOR_STATUS_ACTIVE());
}
@Override
public void updateTrayIcon(URI uri) {
app_indicator_set_icon(indicator, MemoryAllocator.ALLOCATE_FOR(getAbsolutePath(getPathString(uri))));
}
@Override
public void updateTrayMenu(List<TrayMenuItem> items) throws TrayMenuException {
menu = gtk_menu_new();
addChildren(menu, items);
gtk_widget_show_all(menu);
app_indicator_set_menu(indicator, menu);
}
@Override
public void onBeforeOpenMenu(Runnable runnable) {
}
private void addChildren(MemoryAddress menu, List<TrayMenuItem> items) {
for (var item : items) {
// TODO: use Pattern Matching for switch, once available
if (item instanceof ActionItem a) {
var gtkMenuItem = gtk_menu_item_new();
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),
menu,
0);
gtk_menu_shell_append(menu, gtkMenuItem);
} else if (item instanceof SeparatorItem) {
var gtkSeparator = gtk_menu_item_new();
gtk_menu_shell_append(menu, gtkSeparator);
} else if (item instanceof SubMenuItem s) {
var gtkMenuItem = gtk_menu_item_new();
var gtkSubmenu = gtk_menu_new();
gtk_menu_item_set_label(gtkMenuItem, MemoryAllocator.ALLOCATE_FOR(s.title()));
addChildren(gtkSubmenu, s.items());
gtk_menu_item_set_submenu(gtkMenuItem, gtkSubmenu);
gtk_menu_shell_append(menu, gtkMenuItem);
}
gtk_widget_show_all(menu);
}
}
private String getAbsolutePath(String iconName) {
var res = getClass().getClassLoader().getResource(iconName);
if (null == res) {
throw new IllegalArgumentException("Icon '" + iconName + "' cannot be found in resource folder");
}
File file = null;
try {
file = Paths.get(res.toURI()).toFile();
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Icon '" + iconName + "' cannot be converted to file", e);
}
return file.getAbsolutePath();
}
private String getPathString(URI uri) {
return uri.getPath().substring(1);
}
}

View File

@@ -22,25 +22,32 @@ import java.awt.Toolkit;
import java.awt.TrayIcon;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.net.URI;
import java.util.Base64;
import java.util.List;
/**
* Responsible to manage the tray icon on macOS and Windows using AWT.
* For Linux, we use {@link AppindicatorTrayMenuController}
*/
@CheckAvailability
@Priority(Priority.FALLBACK)
public class AwtTrayMenuController implements TrayMenuController {
private static final Logger LOG = LoggerFactory.getLogger(AwtTrayMenuController.class);
private static final String DATA_URI_SCHEME = "data:image/png;base64,";
private final PopupMenu menu = new PopupMenu();
private TrayIcon trayIcon;
@CheckAvailability
public static boolean isAvailable() {
return SystemTray.isSupported();
return !SystemUtils.IS_OS_LINUX && SystemTray.isSupported();
}
@Override
public void showTrayIcon(byte[] imageData, Runnable defaultAction, String tooltip) throws TrayMenuException {
var image = Toolkit.getDefaultToolkit().createImage(imageData);
public void showTrayIcon(URI uri, Runnable defaultAction, String tooltip) throws TrayMenuException {
var image = Toolkit.getDefaultToolkit().createImage(getImageBytes(uri));
trayIcon = new TrayIcon(image, tooltip, menu);
trayIcon.setImageAutoSize(true);
@@ -57,11 +64,8 @@ public class AwtTrayMenuController implements TrayMenuController {
}
@Override
public void updateTrayIcon(byte[] imageData) {
if (trayIcon == null) {
throw new IllegalStateException("Failed to update the icon as it has not yet been added");
}
var image = Toolkit.getDefaultToolkit().createImage(imageData);
public void updateTrayIcon(URI uri) {
var image = Toolkit.getDefaultToolkit().createImage(getImageBytes(uri));
trayIcon.setImage(image);
}
@@ -100,4 +104,8 @@ public class AwtTrayMenuController implements TrayMenuController {
}
}
private byte[] getImageBytes(URI uri) {
var data = uri.toString().split(DATA_URI_SCHEME)[1];
return Base64.getDecoder().decode(data);
}
}

View File

@@ -23,7 +23,9 @@ import javafx.beans.Observable;
import javafx.collections.ObservableList;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.ResourceBundle;
@@ -36,6 +38,10 @@ public class TrayMenuBuilder {
private static final String TRAY_ICON_UNLOCKED_MAC = "/img/tray_icon_unlocked_mac@2x.png";
private static final String TRAY_ICON = "/img/tray_icon.png";
private static final String TRAY_ICON_UNLOCKED = "/img/tray_icon_unlocked.png";
private static final String TRAY_ICON_SVG = "tray_icon.svg";
private static final String TRAY_ICON_UNLOCKED_SVG = "tray_icon_unlocked.svg";
private static final String DATA_URI_SCHEME = "data:image/png;base64,";
private static final String FILE_URI_SCHEME = "file:///";
private final ResourceBundle resourceBundle;
private final VaultService vaultService;
@@ -155,10 +161,16 @@ public class TrayMenuBuilder {
appWindows.showPreferencesWindow(SelectedPreferencesTab.ANY);
}
private byte[] getAppropriateTrayIconImage() {
private URI getAppropriateTrayIconImage() {
boolean isAnyVaultUnlocked = vaults.stream().anyMatch(Vault::isUnlocked);
String resourceName;
if (SystemUtils.IS_OS_LINUX) {
resourceName = isAnyVaultUnlocked ? TRAY_ICON_UNLOCKED_SVG : TRAY_ICON_SVG;
return URI.create(FILE_URI_SCHEME + resourceName);
}
if (SystemUtils.IS_OS_MAC_OSX) {
resourceName = isAnyVaultUnlocked ? TRAY_ICON_UNLOCKED_MAC : TRAY_ICON_MAC;
} else {
@@ -167,10 +179,11 @@ public class TrayMenuBuilder {
try (var image = getClass().getResourceAsStream(resourceName)) {
assert image != null;
return image.readAllBytes();
var imageBytes = image.readAllBytes();
var data = Base64.getEncoder().encodeToString(imageBytes);
return URI.create(DATA_URI_SCHEME + data);
} catch (IOException e) {
throw new UncheckedIOException("Failed to load tray icon image: " + resourceName, e);
}
}
}