diff --git a/pom.xml b/pom.xml
index b682f693d..36e19df5b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,10 +28,10 @@
2.4.1
- 1.1.0-beta1
- 1.0.0
- 1.0.0
- 1.0.1
+ 1.1.0
+ 1.1.0
+ 1.1.0
+ 1.1.0
1.3.3
1.3.3
1.2.7
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 90125d7cc..dd3d7680b 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -1,7 +1,5 @@
-import org.cryptomator.integrations.autostart.AutoStartProvider;
-import org.cryptomator.integrations.keychain.KeychainAccessProvider;
-import org.cryptomator.integrations.tray.TrayIntegrationProvider;
-import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
+import org.cryptomator.integrations.tray.TrayMenuController;
+import org.cryptomator.ui.traymenu.AwtTrayMenuController;
module org.cryptomator.desktop {
requires static org.jetbrains.annotations;
@@ -31,10 +29,8 @@ module org.cryptomator.desktop {
requires logback.classic;
requires logback.core;
- uses AutoStartProvider;
- uses KeychainAccessProvider;
- uses TrayIntegrationProvider;
- uses UiAppearanceProvider;
+ exports org.cryptomator.ui.traymenu to org.cryptomator.integrations.api;
+ provides TrayMenuController with AwtTrayMenuController;
opens org.cryptomator.common.settings to com.google.gson;
diff --git a/src/main/java/org/cryptomator/common/PluginClassLoader.java b/src/main/java/org/cryptomator/common/PluginClassLoader.java
deleted file mode 100644
index 16932923b..000000000
--- a/src/main/java/org/cryptomator/common/PluginClassLoader.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package org.cryptomator.common;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.URLClassLoader;
-import java.nio.file.FileVisitOption;
-import java.nio.file.FileVisitResult;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.util.ArrayList;
-import java.util.EnumSet;
-import java.util.List;
-
-@Singleton
-public class PluginClassLoader extends URLClassLoader {
-
- private static final Logger LOG = LoggerFactory.getLogger(PluginClassLoader.class);
- private static final String NAME = "PluginClassLoader";
- private static final String JAR_SUFFIX = ".jar";
-
- @Inject
- public PluginClassLoader(Environment env) {
- super(NAME, env.getPluginDir().map(PluginClassLoader::findJars).orElse(new URL[0]), PluginClassLoader.class.getClassLoader());
- }
-
- private static URL[] findJars(Path path) {
- if (!Files.isDirectory(path)) {
- return new URL[0];
- } else {
- try {
- var visitor = new JarVisitor();
- Files.walkFileTree(path, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, visitor);
- return visitor.urls.toArray(URL[]::new);
- } catch (IOException e) {
- LOG.warn("Failed to scan plugin dir " + path, e);
- return new URL[0];
- }
- }
- }
-
- private static final class JarVisitor extends SimpleFileVisitor {
-
- private final List urls = new ArrayList<>();
-
- @Override
- public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
- if (attrs.isRegularFile() && file.getFileName().toString().toLowerCase().endsWith(JAR_SUFFIX)) {
- try {
- urls.add(file.toUri().toURL());
- } catch (MalformedURLException e) {
- LOG.warn("Failed to create URL for jar file {}", file);
- }
- }
- return FileVisitResult.CONTINUE;
- }
- }
-
-}
diff --git a/src/main/java/org/cryptomator/common/keychain/KeychainManager.java b/src/main/java/org/cryptomator/common/keychain/KeychainManager.java
index d6adadfe4..48b0a0ed5 100644
--- a/src/main/java/org/cryptomator/common/keychain/KeychainManager.java
+++ b/src/main/java/org/cryptomator/common/keychain/KeychainManager.java
@@ -43,12 +43,6 @@ public class KeychainManager implements KeychainAccessProvider {
return getClass().getName();
}
- @Override
- public void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
- getKeychainOrFail().storePassphrase(key, passphrase);
- setPassphraseStored(key, true);
- }
-
@Override
public void storePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException {
getKeychainOrFail().storePassphrase(key, displayName, passphrase);
@@ -68,14 +62,6 @@ public class KeychainManager implements KeychainAccessProvider {
setPassphraseStored(key, false);
}
- @Override
- public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
- if (isPassphraseStored(key)) {
- getKeychainOrFail().changePassphrase(key, passphrase);
- setPassphraseStored(key, true);
- }
- }
-
@Override
public void changePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException {
if (isPassphraseStored(key)) {
diff --git a/src/main/java/org/cryptomator/common/keychain/KeychainModule.java b/src/main/java/org/cryptomator/common/keychain/KeychainModule.java
index 6356c4966..63749b445 100644
--- a/src/main/java/org/cryptomator/common/keychain/KeychainModule.java
+++ b/src/main/java/org/cryptomator/common/keychain/KeychainModule.java
@@ -2,42 +2,30 @@ package org.cryptomator.common.keychain;
import dagger.Module;
import dagger.Provides;
-import org.cryptomator.common.PluginClassLoader;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
import javax.inject.Singleton;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectExpression;
-import java.util.ServiceLoader;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.List;
@Module
public class KeychainModule {
@Provides
@Singleton
- static Set> provideAvailableKeychainAccessProviderFactories(PluginClassLoader classLoader) {
- return ServiceLoader.load(KeychainAccessProvider.class, classLoader).stream().collect(Collectors.toUnmodifiableSet());
+ static List provideSupportedKeychainAccessProviders() {
+ return KeychainAccessProvider.get().toList();
}
@Provides
@Singleton
- static Set provideSupportedKeychainAccessProviders(Set> availableFactories) {
- return availableFactories.stream() //
- .map(ServiceLoader.Provider::get) //
- .filter(KeychainAccessProvider::isSupported) //
- .collect(Collectors.toUnmodifiableSet());
- }
-
- @Provides
- @Singleton
- static ObjectExpression provideKeychainAccessProvider(Settings settings, Set providers) {
+ static ObjectExpression provideKeychainAccessProvider(Settings settings, List providers) {
return Bindings.createObjectBinding(() -> {
var selectedProviderClass = settings.keychainProvider().get();
var selectedProvider = providers.stream().filter(provider -> provider.getClass().getName().equals(selectedProviderClass)).findAny();
- var fallbackProvider = providers.stream().findAny().orElse(null);
+ var fallbackProvider = providers.stream().findFirst().orElse(null);
return selectedProvider.orElse(fallbackProvider);
}, settings.keychainProvider());
}
diff --git a/src/main/java/org/cryptomator/launcher/CryptomatorModule.java b/src/main/java/org/cryptomator/launcher/CryptomatorModule.java
index e6aab0309..42e908df2 100644
--- a/src/main/java/org/cryptomator/launcher/CryptomatorModule.java
+++ b/src/main/java/org/cryptomator/launcher/CryptomatorModule.java
@@ -2,7 +2,6 @@ package org.cryptomator.launcher;
import dagger.Module;
import dagger.Provides;
-import org.cryptomator.common.PluginClassLoader;
import org.cryptomator.integrations.autostart.AutoStartProvider;
import org.cryptomator.integrations.tray.TrayIntegrationProvider;
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
@@ -12,7 +11,6 @@ import javax.inject.Named;
import javax.inject.Singleton;
import java.util.Optional;
import java.util.ResourceBundle;
-import java.util.ServiceLoader;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
@@ -32,25 +30,22 @@ class CryptomatorModule {
return new ArrayBlockingQueue<>(10);
}
- // TODO: still needed after integrations-api 1.1.0?
-
@Provides
@Singleton
- static Optional provideAppearanceProvider(PluginClassLoader classLoader) {
- return ServiceLoader.load(UiAppearanceProvider.class, classLoader).findFirst();
+ static Optional provideAppearanceProvider() {
+ return UiAppearanceProvider.get();
}
@Provides
@Singleton
- static Optional provideAutostartProvider(PluginClassLoader classLoader) {
- return ServiceLoader.load(AutoStartProvider.class, classLoader).findFirst();
+ static Optional provideAutostartProvider() {
+ return AutoStartProvider.get();
}
@Provides
@Singleton
- static Optional provideTrayIntegrationProvider(PluginClassLoader classLoader) {
- return ServiceLoader.load(TrayIntegrationProvider.class, classLoader).findFirst();
+ static Optional provideTrayIntegrationProvider() {
+ return TrayIntegrationProvider.get();
}
-
}
diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java
index fd9326c3d..3ddb7cba6 100644
--- a/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java
+++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java
@@ -9,11 +9,6 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.application.Platform;
-import javafx.stage.Stage;
-import javafx.stage.StageStyle;
-import java.awt.SystemTray;
-import java.io.IOException;
-import java.io.UncheckedIOException;
@FxApplicationScoped
public class FxApplication {
@@ -49,7 +44,7 @@ public class FxApplication {
// init system tray
final boolean hasTrayIcon;
- if (SystemTray.isSupported() && settings.showTrayIcon().get()) {
+ if (settings.showTrayIcon().get() && trayMenu.get().isSupported()) {
trayMenu.get().initializeTrayIcon();
Platform.setImplicitExit(false); // don't quit when closing all windows
hasTrayIcon = true;
diff --git a/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java
index f2b7ef3b7..d33e919b6 100644
--- a/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java
+++ b/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java
@@ -19,8 +19,8 @@ import javafx.scene.control.ChoiceBox;
import javafx.scene.control.ToggleGroup;
import javafx.stage.Stage;
import javafx.util.StringConverter;
+import java.util.List;
import java.util.Optional;
-import java.util.Set;
@PreferencesScoped
public class GeneralPreferencesController implements FxController {
@@ -32,7 +32,7 @@ public class GeneralPreferencesController implements FxController {
private final Optional autoStartProvider;
private final Application application;
private final Environment environment;
- private final Set keychainAccessProviders;
+ private final List keychainAccessProviders;
private final FxApplicationWindows appWindows;
public ChoiceBox keychainBackendChoiceBox;
public CheckBox startHiddenCheckbox;
@@ -41,7 +41,7 @@ public class GeneralPreferencesController implements FxController {
public ToggleGroup nodeOrientation;
@Inject
- GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, Optional autoStartProvider, Set keychainAccessProviders, Application application, Environment environment, FxApplicationWindows appWindows) {
+ GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, Optional autoStartProvider, List keychainAccessProviders, Application application, Environment environment, FxApplicationWindows appWindows) {
this.window = window;
this.settings = settings;
this.autoStartProvider = autoStartProvider;
@@ -115,9 +115,9 @@ public class GeneralPreferencesController implements FxController {
private static class KeychainProviderClassNameConverter extends StringConverter {
- private final Set keychainAccessProviders;
+ private final List keychainAccessProviders;
- public KeychainProviderClassNameConverter(Set keychainAccessProviders) {
+ public KeychainProviderClassNameConverter(List keychainAccessProviders) {
this.keychainAccessProviders = keychainAccessProviders;
}
diff --git a/src/main/java/org/cryptomator/ui/traymenu/AwtTrayMenuController.java b/src/main/java/org/cryptomator/ui/traymenu/AwtTrayMenuController.java
new file mode 100644
index 000000000..79f1dd628
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/traymenu/AwtTrayMenuController.java
@@ -0,0 +1,79 @@
+package org.cryptomator.ui.traymenu;
+
+import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.integrations.common.CheckAvailability;
+import org.cryptomator.integrations.common.Priority;
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.awt.AWTException;
+import java.awt.Menu;
+import java.awt.MenuItem;
+import java.awt.PopupMenu;
+import java.awt.SystemTray;
+import java.awt.Toolkit;
+import java.awt.TrayIcon;
+import java.util.List;
+
+@CheckAvailability
+@Priority(Priority.FALLBACK)
+public class AwtTrayMenuController implements TrayMenuController {
+
+ private static final Logger LOG = LoggerFactory.getLogger(AwtTrayMenuController.class);
+
+ private final PopupMenu menu = new PopupMenu();
+
+ @CheckAvailability
+ public static boolean isAvailable() {
+ return SystemTray.isSupported();
+ }
+
+ @Override
+ public void showTrayIcon(byte[] rawImageData, Runnable defaultAction, String tooltip) throws TrayMenuException {
+ var image = Toolkit.getDefaultToolkit().createImage(rawImageData);
+ var trayIcon = new TrayIcon(image, tooltip, menu);
+
+ trayIcon.setImageAutoSize(true);
+ if (SystemUtils.IS_OS_WINDOWS) {
+ trayIcon.addActionListener(evt -> defaultAction.run());
+ }
+
+ try {
+ SystemTray.getSystemTray().add(trayIcon);
+ LOG.debug("initialized tray icon");
+ } catch (AWTException e) {
+ throw new TrayMenuException("Failed to add icon to system tray.", e);
+ }
+ }
+
+ @Override
+ public void updateTrayMenu(List items) {
+ menu.removeAll();
+ addChildren(menu, items);
+ }
+
+ private void addChildren(Menu menu, List items) {
+ for (var item : items) {
+ // TODO: use Pattern Matching for switch, once available
+ if (item instanceof ActionItem a) {
+ var menuItem = new MenuItem(a.title());
+ menuItem.addActionListener(evt -> a.action().run());
+ menuItem.setEnabled(a.enabled());
+ menu.add(menuItem);
+ } else if (item instanceof SeparatorItem) {
+ menu.addSeparator();
+ } else if (item instanceof SubMenuItem s) {
+ var submenu = new Menu(s.title());
+ addChildren(submenu, s.items());
+ menu.add(submenu);
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/ui/traymenu/TrayIconController.java b/src/main/java/org/cryptomator/ui/traymenu/TrayIconController.java
deleted file mode 100644
index 2c176df76..000000000
--- a/src/main/java/org/cryptomator/ui/traymenu/TrayIconController.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package org.cryptomator.ui.traymenu;
-
-import com.google.common.base.Preconditions;
-import org.apache.commons.lang3.SystemUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.inject.Inject;
-import java.awt.AWTException;
-import java.awt.SystemTray;
-import java.awt.TrayIcon;
-
-@TrayMenuScoped
-public class TrayIconController {
-
- private static final Logger LOG = LoggerFactory.getLogger(TrayIconController.class);
-
- private final TrayMenuController trayMenuController;
- private final TrayIcon trayIcon;
- private volatile boolean initialized;
-
- @Inject
- TrayIconController(TrayImageFactory imageFactory, TrayMenuController trayMenuController) {
- this.trayMenuController = trayMenuController;
- this.trayIcon = new TrayIcon(imageFactory.loadImage(), "Cryptomator", trayMenuController.getMenu());
- }
-
- public synchronized void initializeTrayIcon() throws IllegalStateException {
- Preconditions.checkState(!initialized);
-
- trayIcon.setImageAutoSize(true);
- if (SystemUtils.IS_OS_WINDOWS) {
- trayIcon.addActionListener(trayMenuController::showMainWindow);
- }
-
- try {
- SystemTray.getSystemTray().add(trayIcon);
- LOG.debug("initialized tray icon");
- } catch (AWTException e) {
- LOG.error("Error adding tray icon", e);
- }
-
- trayMenuController.initTrayMenu();
-
- this.initialized = true;
- }
-
- public boolean isInitialized() {
- return initialized;
- }
-}
diff --git a/src/main/java/org/cryptomator/ui/traymenu/TrayImageFactory.java b/src/main/java/org/cryptomator/ui/traymenu/TrayImageFactory.java
deleted file mode 100644
index aa55ca766..000000000
--- a/src/main/java/org/cryptomator/ui/traymenu/TrayImageFactory.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package org.cryptomator.ui.traymenu;
-
-import org.apache.commons.lang3.SystemUtils;
-import org.cryptomator.integrations.uiappearance.Theme;
-import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
-
-import javax.inject.Inject;
-import java.awt.Image;
-import java.awt.Toolkit;
-import java.util.Optional;
-
-@TrayMenuScoped
-class TrayImageFactory {
-
- private final Optional appearanceProvider;
-
- @Inject
- TrayImageFactory(Optional appearanceProvider) {
- this.appearanceProvider = appearanceProvider;
- }
-
- public Image loadImage() {
- String resourceName = SystemUtils.IS_OS_MAC_OSX ? getMacResourceName() : getWinOrLinuxResourceName();
- return Toolkit.getDefaultToolkit().getImage(getClass().getResource(resourceName));
- }
-
- private String getMacResourceName() {
- return "/img/tray_icon_mac.png";
- }
-
- private String getWinOrLinuxResourceName() {
- return "/img/tray_icon.png";
- }
-
-}
diff --git a/src/main/java/org/cryptomator/ui/traymenu/TrayMenuBuilder.java b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuBuilder.java
new file mode 100644
index 000000000..ea8599b51
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuBuilder.java
@@ -0,0 +1,151 @@
+package org.cryptomator.ui.traymenu;
+
+import com.google.common.base.Preconditions;
+import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.common.vaults.Vault;
+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.cryptomator.ui.common.VaultService;
+import org.cryptomator.ui.fxapp.FxApplicationTerminator;
+import org.cryptomator.ui.fxapp.FxApplicationWindows;
+import org.cryptomator.ui.preferences.SelectedPreferencesTab;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import javafx.application.Platform;
+import javafx.beans.Observable;
+import javafx.collections.ObservableList;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.ResourceBundle;
+
+@TrayMenuScoped
+public class TrayMenuBuilder {
+
+ private static final Logger LOG = LoggerFactory.getLogger(TrayMenuBuilder.class);
+ private static final String TRAY_ICON_MAC = "/img/tray_icon_mac.png";
+ private static final String TRAY_ICON = "/img/tray_icon.png";
+
+ private final ResourceBundle resourceBundle;
+ private final VaultService vaultService;
+ private final FxApplicationWindows appWindows;
+ private final FxApplicationTerminator appTerminator;
+ private final ObservableList vaults;
+ private final TrayMenuController trayMenu;
+
+ private volatile boolean initialized;
+
+ @Inject
+ TrayMenuBuilder(ResourceBundle resourceBundle, VaultService vaultService, FxApplicationWindows appWindows, FxApplicationTerminator appTerminator, ObservableList vaults, Optional trayMenu) {
+ this.resourceBundle = resourceBundle;
+ this.vaultService = vaultService;
+ this.appWindows = appWindows;
+ this.appTerminator = appTerminator;
+ this.vaults = vaults;
+ this.trayMenu = trayMenu.orElse(null);
+ }
+
+ public synchronized void initTrayMenu() {
+ Preconditions.checkState(!initialized, "tray icon already initialized");
+
+ vaults.addListener(this::vaultListChanged);
+ vaults.forEach(v -> {
+ v.displayNameProperty().addListener(this::vaultListChanged);
+ });
+
+ try (var image = getClass().getResourceAsStream(SystemUtils.IS_OS_MAC_OSX ? TRAY_ICON_MAC : TRAY_ICON)) {
+ trayMenu.showTrayIcon(image.readAllBytes(), this::showMainWindow, "Cryptomator");
+ rebuildMenu();
+ initialized = true;
+ } catch (IOException e) {
+ throw new UncheckedIOException("Failed to load embedded resource", e);
+ } catch (TrayMenuException e) {
+ LOG.error("Adding tray icon failed", e);
+ }
+ }
+
+ public boolean isInitialized() {
+ return initialized;
+ }
+
+ private void vaultListChanged(@SuppressWarnings("unused") Observable observable) {
+ assert Platform.isFxApplicationThread();
+ rebuildMenu();
+ }
+
+ private void rebuildMenu() {
+ List menu = new ArrayList<>();
+
+ menu.add(new ActionItem(resourceBundle.getString("traymenu.showMainWindow"), this::showMainWindow));
+ menu.add(new ActionItem(resourceBundle.getString("traymenu.showPreferencesWindow"), this::showPreferencesWindow));
+ menu.add(new SeparatorItem());
+ for (Vault vault : vaults) {
+ List submenu = buildSubmenu(vault);
+ var label = vault.isUnlocked() ? "* ".concat(vault.getDisplayName()) : vault.getDisplayName();
+ menu.add(new SubMenuItem(label, submenu));
+ }
+ menu.add(new SeparatorItem());
+ menu.add(new ActionItem(resourceBundle.getString("traymenu.lockAllVaults"), this::lockAllVaults, vaults.stream().anyMatch(Vault::isUnlocked)));
+ menu.add(new ActionItem(resourceBundle.getString("traymenu.quitApplication"), this::quitApplication));
+
+ try {
+ trayMenu.updateTrayMenu(menu);
+ } catch (TrayMenuException e) {
+ LOG.error("Updating tray menu failed", e);
+ }
+ }
+
+ private List buildSubmenu(Vault vault) {
+ if (vault.isLocked()) {
+ return List.of( //
+ new ActionItem(resourceBundle.getString("traymenu.vault.unlock"), () -> this.unlockVault(vault)) //
+ );
+ } else if (vault.isUnlocked()) {
+ return List.of( //
+ new ActionItem(resourceBundle.getString("traymenu.vault.lock"), () -> this.lockVault(vault)), //
+ new ActionItem(resourceBundle.getString("traymenu.vault.reveal"), () -> this.revealVault(vault)) //
+ );
+ } else {
+ return List.of();
+ }
+ }
+
+ /* action listeners: */
+
+ private void quitApplication() {
+ appTerminator.terminate();
+ }
+
+ private void unlockVault(Vault vault) {
+ appWindows.startUnlockWorkflow(vault, null);
+ }
+
+ private void lockVault(Vault vault) {
+ appWindows.startLockWorkflow(vault, null);
+ }
+
+ private void lockAllVaults() {
+ vaultService.lockAll(vaults.filtered(Vault::isUnlocked), false);
+ }
+
+ private void revealVault(Vault vault) {
+ vaultService.reveal(vault);
+ }
+
+ void showMainWindow() {
+ appWindows.showMainWindow();
+ }
+
+ private void showPreferencesWindow() {
+ appWindows.showPreferencesWindow(SelectedPreferencesTab.ANY);
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/ui/traymenu/TrayMenuComponent.java b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuComponent.java
index c4cbfd456..02bf2aabd 100644
--- a/src/main/java/org/cryptomator/ui/traymenu/TrayMenuComponent.java
+++ b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuComponent.java
@@ -5,38 +5,44 @@
*******************************************************************************/
package org.cryptomator.ui.traymenu;
-import dagger.Lazy;
+import com.google.common.base.Preconditions;
import dagger.Subcomponent;
-import java.awt.SystemTray;
+import org.cryptomator.integrations.tray.TrayMenuController;
+
+import java.util.Optional;
@TrayMenuScoped
-@Subcomponent
+@Subcomponent(modules = {TrayMenuModule.class})
public interface TrayMenuComponent {
- Lazy trayIconController();
+ Optional trayMenuController();
+
+ TrayMenuBuilder trayMenuBuilder();
/**
* @return true if a tray icon can be installed
*/
default boolean isSupported() {
- return SystemTray.isSupported();
+ return trayMenuController().isPresent();
}
/**
* @return true if a tray icon has been installed
*/
default boolean isInitialized() {
- return isSupported() && trayIconController().get().isInitialized();
+ return isSupported() && trayMenuBuilder().isInitialized();
}
/**
* Installs a tray icon to the system tray.
*
- * @throws IllegalStateException If already added
+ * @throws IllegalStateException If not {@link #isSupported() supported}
*/
default void initializeTrayIcon() throws IllegalStateException {
- assert isSupported();
- trayIconController().get().initializeTrayIcon();
+ Preconditions.checkState(isSupported(), "system tray not supported");
+ if (!trayMenuBuilder().isInitialized()) {
+ trayMenuBuilder().initTrayMenu();
+ }
}
@Subcomponent.Builder
diff --git a/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java
deleted file mode 100644
index 32f6cbc52..000000000
--- a/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java
+++ /dev/null
@@ -1,141 +0,0 @@
-package org.cryptomator.ui.traymenu;
-
-import org.cryptomator.common.vaults.Vault;
-import org.cryptomator.ui.common.VaultService;
-import org.cryptomator.ui.fxapp.FxApplicationTerminator;
-import org.cryptomator.ui.fxapp.FxApplicationWindows;
-import org.cryptomator.ui.preferences.SelectedPreferencesTab;
-
-import javax.inject.Inject;
-import javafx.application.Platform;
-import javafx.beans.Observable;
-import javafx.collections.ObservableList;
-import java.awt.Menu;
-import java.awt.MenuItem;
-import java.awt.PopupMenu;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.util.EventObject;
-import java.util.ResourceBundle;
-import java.util.function.Consumer;
-
-@TrayMenuScoped
-class TrayMenuController {
-
- private final ResourceBundle resourceBundle;
- private final VaultService vaultService;
- private final FxApplicationWindows appWindows;
- private final FxApplicationTerminator appTerminator;
- private final ObservableList vaults;
- private final PopupMenu menu;
-
- @Inject
- TrayMenuController(ResourceBundle resourceBundle, VaultService vaultService, FxApplicationWindows appWindows, FxApplicationTerminator appTerminator, ObservableList vaults) {
- this.resourceBundle = resourceBundle;
- this.vaultService = vaultService;
- this.appWindows = appWindows;
- this.appTerminator = appTerminator;
- this.vaults = vaults;
- this.menu = new PopupMenu();
- }
-
- public PopupMenu getMenu() {
- return menu;
- }
-
- public void initTrayMenu() {
- vaults.addListener(this::vaultListChanged);
- vaults.forEach(v -> {
- v.displayNameProperty().addListener(this::vaultListChanged);
- });
- rebuildMenu();
- }
-
- private void vaultListChanged(@SuppressWarnings("unused") Observable observable) {
- assert Platform.isFxApplicationThread();
- rebuildMenu();
- }
-
- private void rebuildMenu() {
- menu.removeAll();
-
- MenuItem showMainWindowItem = new MenuItem(resourceBundle.getString("traymenu.showMainWindow"));
- showMainWindowItem.addActionListener(this::showMainWindow);
- menu.add(showMainWindowItem);
-
- MenuItem showPreferencesItem = new MenuItem(resourceBundle.getString("traymenu.showPreferencesWindow"));
- showPreferencesItem.addActionListener(this::showPreferencesWindow);
- menu.add(showPreferencesItem);
-
- menu.addSeparator();
- for (Vault v : vaults) {
- MenuItem submenu = buildSubmenu(v);
- menu.add(submenu);
- }
- menu.addSeparator();
-
- MenuItem lockAllItem = new MenuItem(resourceBundle.getString("traymenu.lockAllVaults"));
- lockAllItem.addActionListener(this::lockAllVaults);
- lockAllItem.setEnabled(!vaults.filtered(Vault::isUnlocked).isEmpty());
- menu.add(lockAllItem);
-
- MenuItem quitApplicationItem = new MenuItem(resourceBundle.getString("traymenu.quitApplication"));
- quitApplicationItem.addActionListener(this::quitApplication);
- menu.add(quitApplicationItem);
- }
-
- private Menu buildSubmenu(Vault vault) {
- Menu submenu = new Menu(vault.getDisplayName());
-
- if (vault.isLocked()) {
- MenuItem unlockItem = new MenuItem(resourceBundle.getString("traymenu.vault.unlock"));
- unlockItem.addActionListener(createActionListenerForVault(vault, this::unlockVault));
- submenu.add(unlockItem);
- } else if (vault.isUnlocked()) {
- submenu.setLabel("* ".concat(submenu.getLabel()));
-
- MenuItem lockItem = new MenuItem(resourceBundle.getString("traymenu.vault.lock"));
- lockItem.addActionListener(createActionListenerForVault(vault, this::lockVault));
- submenu.add(lockItem);
-
- MenuItem revealItem = new MenuItem(resourceBundle.getString("traymenu.vault.reveal"));
- revealItem.addActionListener(createActionListenerForVault(vault, this::revealVault));
- submenu.add(revealItem);
- }
-
- return submenu;
- }
-
- private ActionListener createActionListenerForVault(Vault vault, Consumer consumer) {
- return actionEvent -> consumer.accept(vault);
- }
-
- private void quitApplication(EventObject actionEvent) {
- appTerminator.terminate();
- }
-
- private void unlockVault(Vault vault) {
- appWindows.startUnlockWorkflow(vault, null);
- }
-
- private void lockVault(Vault vault) {
- appWindows.startLockWorkflow(vault, null);
- }
-
- private void lockAllVaults(ActionEvent actionEvent) {
- vaultService.lockAll(vaults.filtered(Vault::isUnlocked), false);
- }
-
- private void revealVault(Vault vault) {
- vaultService.reveal(vault);
- }
-
- void showMainWindow(@SuppressWarnings("unused") ActionEvent actionEvent) {
- appWindows.showMainWindow();
- }
-
- private void showPreferencesWindow(@SuppressWarnings("unused") EventObject actionEvent) {
- appWindows.showPreferencesWindow(SelectedPreferencesTab.ANY);
- }
-
-}
diff --git a/src/main/java/org/cryptomator/ui/traymenu/TrayMenuModule.java b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuModule.java
new file mode 100644
index 000000000..3110be883
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuModule.java
@@ -0,0 +1,18 @@
+package org.cryptomator.ui.traymenu;
+
+import dagger.Module;
+import dagger.Provides;
+import org.cryptomator.integrations.tray.TrayMenuController;
+
+import java.util.Optional;
+
+@Module
+public class TrayMenuModule {
+
+ @Provides
+ @TrayMenuScoped
+ static Optional provideSupportedKeychainAccessProviders() {
+ return TrayMenuController.get();
+ }
+
+}
diff --git a/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java b/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java
index aa8b6e1f3..abf803e1e 100644
--- a/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java
+++ b/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java
@@ -2,17 +2,16 @@ package org.cryptomator.common.keychain;
import org.cryptomator.integrations.keychain.KeychainAccessException;
-import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
+import java.time.Duration;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -23,7 +22,7 @@ public class KeychainManagerTest {
@Test
public void testStoreAndLoad() throws KeychainAccessException {
KeychainManager keychainManager = new KeychainManager(new SimpleObjectProperty<>(new MapKeychainAccess()));
- keychainManager.storePassphrase("test", "asd");
+ keychainManager.storePassphrase("test", "Test", "asd");
Assertions.assertArrayEquals("asd".toCharArray(), keychainManager.loadPassphrase("test"));
}
@@ -42,9 +41,9 @@ public class KeychainManagerTest {
public void testPropertyChangesWhenStoringPassword() throws KeychainAccessException, InterruptedException {
KeychainManager keychainManager = new KeychainManager(new SimpleObjectProperty<>(new MapKeychainAccess()));
ReadOnlyBooleanProperty property = keychainManager.getPassphraseStoredProperty("test");
- Assertions.assertEquals(false, property.get());
+ Assertions.assertFalse(property.get());
- keychainManager.storePassphrase("test", "bar");
+ keychainManager.storePassphrase("test", null,"bar");
AtomicBoolean result = new AtomicBoolean(false);
CountDownLatch latch = new CountDownLatch(1);
@@ -52,8 +51,8 @@ public class KeychainManagerTest {
result.set(property.get());
latch.countDown();
});
- latch.await(1, TimeUnit.SECONDS);
- Assertions.assertEquals(true, result.get());
+ Assertions.assertTimeoutPreemptively(Duration.ofSeconds(1), () -> latch.await());
+ Assertions.assertTrue(result.get());
}
}
diff --git a/src/test/java/org/cryptomator/common/keychain/MapKeychainAccess.java b/src/test/java/org/cryptomator/common/keychain/MapKeychainAccess.java
index dbffae92e..c4baa96fa 100644
--- a/src/test/java/org/cryptomator/common/keychain/MapKeychainAccess.java
+++ b/src/test/java/org/cryptomator/common/keychain/MapKeychainAccess.java
@@ -20,7 +20,7 @@ class MapKeychainAccess implements KeychainAccessProvider {
}
@Override
- public void storePassphrase(String key, CharSequence passphrase) {
+ public void storePassphrase(String key, String displayName,CharSequence passphrase) {
char[] pw = new char[passphrase.length()];
for (int i = 0; i < passphrase.length(); i++) {
pw[i] = passphrase.charAt(i);
@@ -39,9 +39,9 @@ class MapKeychainAccess implements KeychainAccessProvider {
}
@Override
- public void changePassphrase(String key, CharSequence passphrase) {
+ public void changePassphrase(String key, String displayName, CharSequence passphrase) {
map.get(key);
- storePassphrase(key, passphrase);
+ storePassphrase(key, displayName, passphrase);
}
@Override