mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-20 11:41:26 +00:00
Add AppindicatorTrayMenuController
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user