Merge pull request #1472 from cryptomator/feature/minimize

Add Minimize Button
This commit is contained in:
Sebastian Stenzel
2020-12-18 15:58:02 +01:00
committed by GitHub
29 changed files with 237 additions and 117 deletions

View File

@@ -2,7 +2,7 @@
<configuration default="false" name="Cryptomator Linux" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="launcher" />
<option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath=&quot;~/.config/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/.config/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator/logs&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator/mnt&quot; -Xss20m -Xmx512m" />
<option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath=&quot;~/.config/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/.config/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator/logs&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator/mnt&quot; -Dcryptomator.showTrayIcon=true -Xss20m -Xmx512m" />
<method v="2">
<option name="Make" enabled="true" />
</method>

View File

@@ -2,7 +2,7 @@
<configuration default="false" name="Cryptomator Linux Dev" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="launcher" />
<option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath=&quot;~/.config/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/.config/Cryptomator-Dev/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator-Dev/logs&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator-Dev/mnt&quot; -Dfuse.experimental=&quot;true&quot; -Xss20m -Xmx512m" />
<option name="VM_PARAMETERS" value="-Djdk.gtk.version=2 -Duser.language=en -Dcryptomator.settingsPath=&quot;~/.config/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/.config/Cryptomator-Dev/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/.local/share/Cryptomator-Dev/logs&quot; -Dcryptomator.mountPointsDir=&quot;~/.local/share/Cryptomator-Dev/mnt&quot; -Dcryptomator.showTrayIcon=true -Dfuse.experimental=&quot;true&quot; -Xss20m -Xmx512m" />
<method v="2">
<option name="Make" enabled="true" />
</method>

View File

@@ -2,7 +2,7 @@
<configuration default="false" name="Cryptomator Windows" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="launcher" />
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/AppData/Roaming/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator&quot; -Dcryptomator.keychainPath=&quot;~/AppData/Roaming/Cryptomator/keychain.json&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator&quot; -Xss2m -Xmx512m" />
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/AppData/Roaming/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator&quot; -Dcryptomator.keychainPath=&quot;~/AppData/Roaming/Cryptomator/keychain.json&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" />
<method v="2">
<option name="Make" enabled="true" />
</method>

View File

@@ -2,7 +2,7 @@
<configuration default="false" name="Cryptomator Windows Dev" type="Application" factoryName="Application">
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="launcher" />
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/AppData/Roaming/Cryptomator-Dev/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator-Dev&quot; -Dcryptomator.keychainPath=&quot;~/AppData/Roaming/Cryptomator-Dev/keychain.json&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator-Dev&quot; -Dfuse.experimental=&quot;true&quot; -Xss2m -Xmx512m" />
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/AppData/Roaming/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/AppData/Roaming/Cryptomator-Dev/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/AppData/Roaming/Cryptomator-Dev&quot; -Dcryptomator.keychainPath=&quot;~/AppData/Roaming/Cryptomator-Dev/keychain.json&quot; -Dcryptomator.mountPointsDir=&quot;~/Cryptomator-Dev&quot; -Dfuse.experimental=&quot;true&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m" />
<method v="2">
<option name="Make" enabled="true" />
</method>

View File

@@ -5,7 +5,7 @@
</envs>
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="launcher" />
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/Library/Application Support/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator&quot; -Xss2m -Xmx512m -ea" />
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/Library/Application Support/Cryptomator/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
<method v="2">
<option name="Make" enabled="true" />
</method>

View File

@@ -5,7 +5,7 @@
</envs>
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
<module name="launcher" />
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/Library/Application Support/Cryptomator-Dev/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator-Dev&quot; -Xss2m -Xmx512m -ea" />
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath=&quot;~/Library/Application Support/Cryptomator-Dev/settings.json&quot; -Dcryptomator.ipcPortPath=&quot;~/Library/Application Support/Cryptomator-Dev/ipcPort.bin&quot; -Dcryptomator.logDir=&quot;~/Library/Logs/Cryptomator-Dev&quot; -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
<method v="2">
<option name="Make" enabled="true" />
</method>

View File

@@ -21,14 +21,13 @@ import java.util.stream.StreamSupport;
public class Environment {
private static final Logger LOG = LoggerFactory.getLogger(Environment.class);
private static final String USER_HOME = System.getProperty("user.home");
private static final Path RELATIVE_HOME_DIR = Paths.get("~");
private static final Path ABSOLUTE_HOME_DIR = Paths.get(USER_HOME);
private static final char PATH_LIST_SEP = ':';
private static final int DEFAULT_MIN_PW_LENGTH = 8;
@Inject
public Environment() {
LOG.debug("user.home: {}", System.getProperty("user.home"));
LOG.debug("java.library.path: {}", System.getProperty("java.library.path"));
LOG.debug("user.language: {}", System.getProperty("user.language"));
LOG.debug("user.region: {}", System.getProperty("user.region"));
@@ -40,6 +39,7 @@ public class Environment {
LOG.debug("cryptomator.mountPointsDir: {}", System.getProperty("cryptomator.mountPointsDir"));
LOG.debug("cryptomator.minPwLength: {}", System.getProperty("cryptomator.minPwLength"));
LOG.debug("cryptomator.buildNumber: {}", System.getProperty("cryptomator.buildNumber"));
LOG.debug("cryptomator.showTrayIcon: {}", System.getProperty("cryptomator.showTrayIcon"));
LOG.debug("fuse.experimental: {}", Boolean.getBoolean("fuse.experimental"));
}
@@ -75,6 +75,10 @@ public class Environment {
return getInt("cryptomator.minPwLength", DEFAULT_MIN_PW_LENGTH);
}
public boolean showTrayIcon() {
return Boolean.getBoolean("cryptomator.showTrayIcon");
}
@Deprecated // TODO: remove as soon as custom mount path works properly on Win+Fuse
public boolean useExperimentalFuse() {
return Boolean.getBoolean("fuse.experimental");
@@ -93,8 +97,13 @@ public class Environment {
String value = System.getProperty(propertyName);
return Optional.ofNullable(value).map(Paths::get);
}
// visible for testing
// visible for testing
Path getHomeDir() {
return getPath("user.home").orElseThrow();
}
// visible for testing
Stream<Path> getPaths(String propertyName) {
Stream<String> rawSettingsPaths = getRawList(propertyName, PATH_LIST_SEP);
return rawSettingsPaths.filter(Predicate.not(Strings::isNullOrEmpty)).map(Paths::get).map(this::replaceHomeDir);
@@ -102,7 +111,7 @@ public class Environment {
private Path replaceHomeDir(Path path) {
if (path.startsWith(RELATIVE_HOME_DIR)) {
return ABSOLUTE_HOME_DIR.resolve(RELATIVE_HOME_DIR.relativize(path));
return getHomeDir().resolve(RELATIVE_HOME_DIR.relativize(path));
} else {
return path;
}

View File

@@ -9,6 +9,7 @@
package org.cryptomator.common.settings;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Environment;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
@@ -39,7 +40,8 @@ public class Settings {
public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
public static final KeychainBackend DEFAULT_KEYCHAIN_BACKEND = SystemUtils.IS_OS_WINDOWS ? KeychainBackend.WIN_SYSTEM_KEYCHAIN : SystemUtils.IS_OS_MAC ? KeychainBackend.MAC_SYSTEM_KEYCHAIN : KeychainBackend.GNOME;
public static final NodeOrientation DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT;
private static final String DEFAULT_LICENSE_KEY = "";
public static final String DEFAULT_LICENSE_KEY = "";
public static final boolean DEFAULT_SHOW_MINIMIZE_BUTTON = false;
private final ObservableList<VaultSettings> directories = FXCollections.observableArrayList(VaultSettings::observables);
private final BooleanProperty askedForUpdateCheck = new SimpleBooleanProperty(DEFAULT_ASKED_FOR_UPDATE_CHECK);
@@ -54,13 +56,17 @@ public class Settings {
private final ObjectProperty<KeychainBackend> keychainBackend = new SimpleObjectProperty<>(DEFAULT_KEYCHAIN_BACKEND);
private final ObjectProperty<NodeOrientation> userInterfaceOrientation = new SimpleObjectProperty<>(DEFAULT_USER_INTERFACE_ORIENTATION);
private final StringProperty licenseKey = new SimpleStringProperty(DEFAULT_LICENSE_KEY);
private final BooleanProperty showMinimizeButton = new SimpleBooleanProperty(DEFAULT_SHOW_MINIMIZE_BUTTON);
private final BooleanProperty showTrayIcon;
private Consumer<Settings> saveCmd;
/**
* Package-private constructor; use {@link SettingsProvider}.
*/
Settings() {
Settings(Environment env) {
this.showTrayIcon = new SimpleBooleanProperty(env.showTrayIcon());
directories.addListener(this::somethingChanged);
askedForUpdateCheck.addListener(this::somethingChanged);
checkForUpdates.addListener(this::somethingChanged);
@@ -74,6 +80,8 @@ public class Settings {
keychainBackend.addListener(this::somethingChanged);
userInterfaceOrientation.addListener(this::somethingChanged);
licenseKey.addListener(this::somethingChanged);
showMinimizeButton.addListener(this::somethingChanged);
showTrayIcon.addListener(this::somethingChanged);
}
void setSaveCmd(Consumer<Settings> saveCmd) {
@@ -141,4 +149,12 @@ public class Settings {
public StringProperty licenseKey() {
return licenseKey;
}
public BooleanProperty showMinimizeButton() {
return showMinimizeButton;
}
public BooleanProperty showTrayIcon() {
return showTrayIcon;
}
}

View File

@@ -9,19 +9,29 @@ import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import org.cryptomator.common.Environment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import javafx.geometry.NodeOrientation;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@Singleton
public class SettingsJsonAdapter extends TypeAdapter<Settings> {
private static final Logger LOG = LoggerFactory.getLogger(SettingsJsonAdapter.class);
private final VaultSettingsJsonAdapter vaultSettingsJsonAdapter = new VaultSettingsJsonAdapter();
private final Environment env;
@Inject
public SettingsJsonAdapter(Environment env) {
this.env = env;
}
@Override
public void write(JsonWriter out, Settings value) throws IOException {
@@ -40,6 +50,8 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
out.name("uiOrientation").value(value.userInterfaceOrientation().get().name());
out.name("keychainBackend").value(value.keychainBackend().get().name());
out.name("licenseKey").value(value.licenseKey().get());
out.name("showMinimizeButton").value(value.showMinimizeButton().get());
out.name("showTrayIcon").value(value.showTrayIcon().get());
out.endObject();
}
@@ -53,7 +65,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
@Override
public Settings read(JsonReader in) throws IOException {
Settings settings = new Settings();
Settings settings = new Settings(env);
in.beginObject();
while (in.hasNext()) {
@@ -72,6 +84,8 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
case "keychainBackend" -> settings.keychainBackend().set(parseKeychainBackend(in.nextString()));
case "licenseKey" -> settings.licenseKey().set(in.nextString());
case "showMinimizeButton" -> settings.showMinimizeButton().set(in.nextBoolean());
case "showTrayIcon" -> settings.showTrayIcon().set(in.nextBoolean());
default -> {
LOG.warn("Unsupported vault setting found in JSON: " + name);
in.skipValue();
@@ -137,5 +151,4 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
in.endArray();
return result;
}
}

View File

@@ -49,13 +49,14 @@ public class SettingsProvider implements Supplier<Settings> {
private final AtomicReference<ScheduledFuture<?>> scheduledSaveCmd = new AtomicReference<>();
private final Supplier<Settings> settings = Suppliers.memoize(this::load);
private final SettingsJsonAdapter settingsJsonAdapter = new SettingsJsonAdapter();
private final SettingsJsonAdapter settingsJsonAdapter;
private final Environment env;
private final ScheduledExecutorService scheduler;
private final Gson gson;
@Inject
public SettingsProvider(Environment env, ScheduledExecutorService scheduler) {
public SettingsProvider(SettingsJsonAdapter settingsJsonAdapter, Environment env, ScheduledExecutorService scheduler) {
this.settingsJsonAdapter = settingsJsonAdapter;
this.env = env;
this.scheduler = scheduler;
this.gson = new GsonBuilder() //
@@ -70,7 +71,7 @@ public class SettingsProvider implements Supplier<Settings> {
}
private Settings load() {
Settings settings = env.getSettingsPath().flatMap(this::tryLoad).findFirst().orElse(new Settings());
Settings settings = env.getSettingsPath().flatMap(this::tryLoad).findFirst().orElse(new Settings(env));
settings.setSaveCmd(this::scheduleSave);
return settings;
}

View File

@@ -8,6 +8,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -20,14 +21,10 @@ class EnvironmentTest {
private Environment env;
@BeforeAll
static void init() {
System.setProperty("user.home", "/home/testuser");
}
@BeforeEach
void initEach() {
env = new Environment();
void init() {
env = Mockito.spy(new Environment());
Mockito.when(env.getHomeDir()).thenReturn(Path.of("/home/testuser"));
}
@Test

View File

@@ -5,16 +5,19 @@
*******************************************************************************/
package org.cryptomator.common.settings;
import org.cryptomator.common.Environment;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;
import java.io.IOException;
public class SettingsJsonAdapterTest {
private final SettingsJsonAdapter adapter = new SettingsJsonAdapter();
private final Environment env = Mockito.mock(Environment.class);
private final SettingsJsonAdapter adapter = new SettingsJsonAdapter(env);
@Test
public void testDeserialize() throws IOException {

View File

@@ -5,6 +5,7 @@
*******************************************************************************/
package org.cryptomator.common.settings;
import org.cryptomator.common.Environment;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@@ -14,8 +15,10 @@ public class SettingsTest {
@Test
public void testAutoSave() {
Environment env = Mockito.mock(Environment.class);
@SuppressWarnings("unchecked") Consumer<Settings> changeListener = Mockito.mock(Consumer.class);
Settings settings = new Settings();
Settings settings = new Settings(env);
settings.setSaveCmd(changeListener);
VaultSettings vaultSettings = VaultSettings.withRandomId();
Mockito.verify(changeListener, Mockito.times(0)).accept(settings);

View File

@@ -32,6 +32,8 @@ import javafx.stage.Stage;
import javafx.stage.Window;
import java.awt.desktop.QuitResponse;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
@FxApplicationScoped
public class FxApplication extends Application {
@@ -99,11 +101,14 @@ public class FxApplication extends Application {
});
}
public void showMainWindow() {
public CompletionStage<Stage> showMainWindow() {
CompletableFuture<Stage> future = new CompletableFuture<>();
Platform.runLater(() -> {
mainWindow.get().showMainWindow();
var win = mainWindow.get().showMainWindow();
LOG.debug("Showing MainWindow");
future.complete(win);
});
return future;
}
public void startUnlockWorkflow(Vault vault, Optional<Stage> owner) {

View File

@@ -5,11 +5,8 @@
*******************************************************************************/
package org.cryptomator.ui.fxapp;
import dagger.BindsInstance;
import dagger.Subcomponent;
import javax.inject.Named;
@FxApplicationScoped
@Subcomponent(modules = FxApplicationModule.class)
public interface FxApplicationComponent {
@@ -19,9 +16,6 @@ public interface FxApplicationComponent {
@Subcomponent.Builder
interface Builder {
@BindsInstance
Builder trayMenuSupported(@Named("trayMenuSupported") boolean trayMenuSupported);
FxApplicationComponent build();
}

View File

@@ -34,15 +34,15 @@ class AppLaunchEventHandler {
this.vaultListManager = vaultListManager;
}
public void startHandlingLaunchEvents(boolean hasTrayIcon) {
executorService.submit(() -> handleLaunchEvents(hasTrayIcon));
public void startHandlingLaunchEvents() {
executorService.submit(this::handleLaunchEvents);
}
private void handleLaunchEvents(boolean hasTrayIcon) {
private void handleLaunchEvents() {
try {
while (!Thread.interrupted()) {
AppLaunchEvent event = launchEventQueue.take();
handleLaunchEvent(hasTrayIcon, event);
handleLaunchEvent(event);
}
} catch (InterruptedException e) {
LOG.warn("Interrupted launch event handler.");
@@ -50,10 +50,10 @@ class AppLaunchEventHandler {
}
}
private void handleLaunchEvent(boolean hasTrayIcon, AppLaunchEvent event) {
private void handleLaunchEvent(AppLaunchEvent event) {
switch (event.getType()) {
case REVEAL_APP -> fxApplicationStarter.get(hasTrayIcon).thenAccept(FxApplication::showMainWindow);
case OPEN_FILE -> fxApplicationStarter.get(hasTrayIcon).thenRun(() -> {
case REVEAL_APP -> fxApplicationStarter.get().thenAccept(FxApplication::showMainWindow);
case OPEN_FILE -> fxApplicationStarter.get().thenRun(() -> {
Platform.runLater(() -> {
event.getPathsToOpen().forEach(this::addVault);
});

View File

@@ -83,7 +83,7 @@ public class AppLifecycleListener {
if (allowQuitWithoutPrompt.get()) {
decoratedQuitResponse.performQuit();
} else {
fxApplicationStarter.get(true).thenAccept(app -> app.showQuitWindow(decoratedQuitResponse));
fxApplicationStarter.get().thenAccept(app -> app.showQuitWindow(decoratedQuitResponse));
}
}
@@ -113,11 +113,11 @@ public class AppLifecycleListener {
}
private void showPreferencesWindow(@SuppressWarnings("unused") EventObject actionEvent) {
fxApplicationStarter.get(true).thenAccept(app -> app.showPreferencesWindow(SelectedPreferencesTab.ANY));
fxApplicationStarter.get().thenAccept(app -> app.showPreferencesWindow(SelectedPreferencesTab.ANY));
}
private void showAboutWindow(@SuppressWarnings("unused") AboutEvent aboutEvent) {
fxApplicationStarter.get(true).thenAccept(app -> app.showPreferencesWindow(SelectedPreferencesTab.ABOUT));
fxApplicationStarter.get().thenAccept(app -> app.showPreferencesWindow(SelectedPreferencesTab.ABOUT));
}
private void forceUnmountRemainingVaults() {

View File

@@ -1,5 +1,6 @@
package org.cryptomator.ui.launcher;
import dagger.Lazy;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.fxapp.FxApplicationComponent;
import org.slf4j.Logger;
@@ -18,37 +19,36 @@ public class FxApplicationStarter {
private static final Logger LOG = LoggerFactory.getLogger(FxApplicationStarter.class);
private final FxApplicationComponent.Builder fxAppComponent;
private final Lazy<FxApplicationComponent> fxAppComponent;
private final ExecutorService executor;
private final AtomicBoolean started;
private final CompletableFuture<FxApplication> future;
@Inject
public FxApplicationStarter(FxApplicationComponent.Builder fxAppComponent, ExecutorService executor) {
public FxApplicationStarter(Lazy<FxApplicationComponent> fxAppComponent, ExecutorService executor) {
this.fxAppComponent = fxAppComponent;
this.executor = executor;
this.started = new AtomicBoolean();
this.future = new CompletableFuture<>();
}
public CompletionStage<FxApplication> get(boolean hasTrayIcon) {
public CompletionStage<FxApplication> get() {
if (!started.getAndSet(true)) {
start(hasTrayIcon);
start();
}
return future;
}
private void start(boolean hasTrayIcon) {
private void start() {
executor.submit(() -> {
LOG.debug("Starting JavaFX runtime...");
Platform.startup(() -> {
assert Platform.isFxApplicationThread();
LOG.info("JavaFX Runtime started.");
FxApplication app = fxAppComponent.trayMenuSupported(hasTrayIcon).build().application();
FxApplication app = fxAppComponent.get().application();
app.start();
future.complete(app);
});
});
}
}

View File

@@ -1,5 +1,6 @@
package org.cryptomator.ui.launcher;
import dagger.Lazy;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.integrations.tray.TrayIntegrationProvider;
@@ -24,60 +25,66 @@ public class UiLauncher {
private final Settings settings;
private final ObservableList<Vault> vaults;
private final TrayMenuComponent.Builder trayComponent;
private final Lazy<TrayMenuComponent> trayMenu;
private final FxApplicationStarter fxApplicationStarter;
private final AppLaunchEventHandler launchEventHandler;
private final Optional<TrayIntegrationProvider> trayIntegration;
@Inject
public UiLauncher(Settings settings, ObservableList<Vault> vaults, TrayMenuComponent.Builder trayComponent, FxApplicationStarter fxApplicationStarter, AppLaunchEventHandler launchEventHandler, Optional<TrayIntegrationProvider> trayIntegration) {
public UiLauncher(Settings settings, ObservableList<Vault> vaults, Lazy<TrayMenuComponent> trayMenu, FxApplicationStarter fxApplicationStarter, AppLaunchEventHandler launchEventHandler, Optional<TrayIntegrationProvider> trayIntegration) {
this.settings = settings;
this.vaults = vaults;
this.trayComponent = trayComponent;
this.trayMenu = trayMenu;
this.fxApplicationStarter = fxApplicationStarter;
this.launchEventHandler = launchEventHandler;
this.trayIntegration = trayIntegration;
}
public void launch() {
final boolean hasTrayIcon;
if (SystemTray.isSupported()) {
trayComponent.build().addIconToSystemTray();
hasTrayIcon = true;
boolean hidden = settings.startHidden().get();
if (SystemTray.isSupported() && settings.showTrayIcon().get()) {
trayMenu.get().addIconToSystemTray();
launch(true, hidden);
} else {
hasTrayIcon = false;
launch(false, hidden);
}
}
// show window on start?
if (hasTrayIcon && settings.startHidden().get()) {
private void launch(boolean withTrayIcon, boolean hidden) {
// start hidden, minimized or normal?
if (withTrayIcon && hidden) {
LOG.debug("Hiding application...");
trayIntegration.ifPresent(TrayIntegrationProvider::minimizedToTray);
} else if (!withTrayIcon && hidden) {
LOG.debug("Minimizing application...");
showMainWindowAsync(true);
} else {
showMainWindowAsync(hasTrayIcon);
LOG.debug("Showing application...");
showMainWindowAsync(false);
}
// register app reopen listener
Desktop.getDesktop().addAppEventListener((AppReopenedListener) e -> showMainWindowAsync(hasTrayIcon));
Desktop.getDesktop().addAppEventListener((AppReopenedListener) e -> showMainWindowAsync(false));
// auto unlock
Collection<Vault> vaultsToAutoUnlock = vaults.filtered(this::shouldAttemptAutoUnlock);
if (!vaultsToAutoUnlock.isEmpty()) {
fxApplicationStarter.get(hasTrayIcon).thenAccept(app -> {
fxApplicationStarter.get().thenAccept(app -> {
for (Vault vault : vaultsToAutoUnlock) {
app.startUnlockWorkflow(vault, Optional.empty());
}
});
}
launchEventHandler.startHandlingLaunchEvents(hasTrayIcon);
launchEventHandler.startHandlingLaunchEvents();
}
private boolean shouldAttemptAutoUnlock(Vault vault) {
return vault.isLocked() && vault.getVaultSettings().unlockAfterStartup().get();
}
private void showMainWindowAsync(boolean hasTrayIcon) {
fxApplicationStarter.get(hasTrayIcon).thenAccept(FxApplication::showMainWindow);
private void showMainWindowAsync(boolean minimize) {
fxApplicationStarter.get().thenCompose(FxApplication::showMainWindow).thenAccept(win -> win.setIconified(minimize));
}
}

View File

@@ -19,6 +19,18 @@ import java.util.concurrent.BlockingQueue;
@Module(subcomponents = {TrayMenuComponent.class, FxApplicationComponent.class})
public abstract class UiLauncherModule {
@Provides
@Singleton
static TrayMenuComponent provideTrayMenuComponent(TrayMenuComponent.Builder builder) {
return builder.build();
}
@Provides
@Singleton
static FxApplicationComponent provideFxApplicationComponent(FxApplicationComponent.Builder builder) {
return builder.build();
}
@Provides
@Singleton
static Optional<UiAppearanceProvider> provideAppearanceProvider() {

View File

@@ -26,7 +26,6 @@ public interface MainWindowComponent {
default Stage showMainWindow() {
Stage stage = window();
stage.setScene(scene().get());
stage.setIconified(false);
stage.show();
stage.toFront();
stage.requestFocus();

View File

@@ -7,13 +7,14 @@ import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.fxapp.UpdateChecker;
import org.cryptomator.ui.launcher.AppLifecycleListener;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.cryptomator.ui.traymenu.TrayMenuComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.fxml.FXML;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
@@ -28,27 +29,28 @@ public class MainWindowTitleController implements FxController {
private final AppLifecycleListener appLifecycle;
private final Stage window;
private final FxApplication application;
private final boolean minimizeToSysTray;
private final boolean trayMenuInitialized;
private final UpdateChecker updateChecker;
private final BooleanBinding updateAvailable;
private final LicenseHolder licenseHolder;
private final Settings settings;
private final BooleanBinding debugModeEnabled;
private final BooleanBinding showMinimizeButton;
private double xOffset;
private double yOffset;
@Inject
MainWindowTitleController(AppLifecycleListener appLifecycle, @MainWindow Stage window, FxApplication application, @Named("trayMenuSupported") boolean minimizeToSysTray, UpdateChecker updateChecker, LicenseHolder licenseHolder, Settings settings) {
MainWindowTitleController(AppLifecycleListener appLifecycle, @MainWindow Stage window, FxApplication application, TrayMenuComponent trayMenu, UpdateChecker updateChecker, LicenseHolder licenseHolder, Settings settings) {
this.appLifecycle = appLifecycle;
this.window = window;
this.application = application;
this.minimizeToSysTray = minimizeToSysTray;
this.trayMenuInitialized = trayMenu.isInitialized();
this.updateChecker = updateChecker;
this.updateAvailable = updateChecker.latestVersionProperty().isNotNull();
this.licenseHolder = licenseHolder;
this.settings = settings;
this.debugModeEnabled = Bindings.createBooleanBinding(this::isDebugModeEnabled, settings.debugMode());
this.showMinimizeButton = Bindings.createBooleanBinding(this::isShowMinimizeButton, settings.showMinimizeButton(), settings.showTrayIcon());
}
@FXML
@@ -71,7 +73,7 @@ public class MainWindowTitleController implements FxController {
@FXML
public void close() {
if (minimizeToSysTray) {
if (trayMenuInitialized) {
window.close();
} else {
appLifecycle.quit();
@@ -112,15 +114,24 @@ public class MainWindowTitleController implements FxController {
return updateAvailable.get();
}
public boolean isMinimizeToSysTray() {
return minimizeToSysTray;
public boolean isTrayIconPresent() {
return trayMenuInitialized;
}
public BooleanBinding debugModeEnabledProperty() {
return debugModeEnabled;
public ReadOnlyBooleanProperty debugModeEnabledProperty() {
return settings.debugMode();
}
public boolean isDebugModeEnabled() {
return settings.debugMode().get();
return debugModeEnabledProperty().get();
}
public BooleanBinding showMinimizeButtonProperty() {
return showMinimizeButton;
}
public boolean isShowMinimizeButton() {
// always show the minimize button if no tray icon is present OR it is explicitily enabled
return !trayMenuInitialized || settings.showMinimizeButton().get();
}
}

View File

@@ -10,11 +10,11 @@ import org.cryptomator.integrations.autostart.ToggleAutoStartFailedException;
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.traymenu.TrayMenuComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue;
@@ -31,7 +31,6 @@ import java.util.Arrays;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
@PreferencesScoped
@@ -41,11 +40,11 @@ public class GeneralPreferencesController implements FxController {
private final Stage window;
private final Settings settings;
private final boolean trayMenuInitialized;
private final boolean trayMenuSupported;
private final Optional<AutoStartProvider> autoStartProvider;
private final ObjectProperty<SelectedPreferencesTab> selectedTabProperty;
private final LicenseHolder licenseHolder;
private final ExecutorService executor;
private final ResourceBundle resourceBundle;
private final Application application;
private final Environment environment;
@@ -53,6 +52,8 @@ public class GeneralPreferencesController implements FxController {
private final ErrorComponent.Builder errorComponent;
public ChoiceBox<UiTheme> themeChoiceBox;
public ChoiceBox<KeychainBackend> keychainBackendChoiceBox;
public CheckBox showMinimizeButtonCheckbox;
public CheckBox showTrayIconCheckbox;
public CheckBox startHiddenCheckbox;
public CheckBox debugModeCheckbox;
public CheckBox autoStartCheckbox;
@@ -60,16 +61,17 @@ public class GeneralPreferencesController implements FxController {
public RadioButton nodeOrientationLtr;
public RadioButton nodeOrientationRtl;
@Inject
GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional<AutoStartProvider> autoStartProvider, Set<KeychainAccessProvider> keychainAccessProviders, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ExecutorService executor, ResourceBundle resourceBundle, Application application, Environment environment, ErrorComponent.Builder errorComponent) {
GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, TrayMenuComponent trayMenu, Optional<AutoStartProvider> autoStartProvider, Set<KeychainAccessProvider> keychainAccessProviders, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ResourceBundle resourceBundle, Application application, Environment environment, ErrorComponent.Builder errorComponent) {
this.window = window;
this.settings = settings;
this.trayMenuSupported = trayMenuSupported;
this.trayMenuInitialized = trayMenu.isInitialized();
this.trayMenuSupported = trayMenu.isSupported();
this.autoStartProvider = autoStartProvider;
this.keychainAccessProviders = keychainAccessProviders;
this.selectedTabProperty = selectedTabProperty;
this.licenseHolder = licenseHolder;
this.executor = executor;
this.resourceBundle = resourceBundle;
this.application = application;
this.environment = environment;
@@ -85,6 +87,10 @@ public class GeneralPreferencesController implements FxController {
themeChoiceBox.valueProperty().bindBidirectional(settings.theme());
themeChoiceBox.setConverter(new UiThemeConverter(resourceBundle));
showMinimizeButtonCheckbox.selectedProperty().bindBidirectional(settings.showMinimizeButton());
showTrayIconCheckbox.selectedProperty().bindBidirectional(settings.showTrayIcon());
startHiddenCheckbox.selectedProperty().bindBidirectional(settings.startHidden());
debugModeCheckbox.selectedProperty().bindBidirectional(settings.debugMode());
@@ -105,8 +111,12 @@ public class GeneralPreferencesController implements FxController {
return Arrays.stream(KeychainBackend.values()).filter(value -> namesOfAvailableProviders.contains(value.getProviderClass())).toArray(KeychainBackend[]::new);
}
public boolean isTrayMenuInitialized() {
return trayMenuInitialized;
}
public boolean isTrayMenuSupported() {
return this.trayMenuSupported;
return trayMenuSupported;
}
public boolean isAutoStartSupported() {
@@ -175,6 +185,7 @@ public class GeneralPreferencesController implements FxController {
public UiTheme fromString(String string) {
throw new UnsupportedOperationException();
}
}
private static class KeychainBackendConverter extends StringConverter<KeychainBackend> {
@@ -194,6 +205,6 @@ public class GeneralPreferencesController implements FxController {
public KeychainBackend fromString(String string) {
throw new UnsupportedOperationException();
}
}
}
}

View File

@@ -1,5 +1,6 @@
package org.cryptomator.ui.traymenu;
import com.google.common.base.Preconditions;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.integrations.uiappearance.Theme;
import org.cryptomator.integrations.uiappearance.UiAppearanceException;
@@ -22,6 +23,7 @@ public class TrayIconController {
private final Optional<UiAppearanceProvider> appearanceProvider;
private final TrayMenuController trayMenuController;
private final TrayIcon trayIcon;
private volatile boolean initialized;
@Inject
TrayIconController(TrayImageFactory imageFactory, TrayMenuController trayMenuController, Optional<UiAppearanceProvider> appearanceProvider) {
@@ -31,7 +33,9 @@ public class TrayIconController {
this.trayIcon = new TrayIcon(imageFactory.loadImage(), "Cryptomator", trayMenuController.getMenu());
}
public void initializeTrayIcon() {
public synchronized void initializeTrayIcon() throws IllegalStateException {
Preconditions.checkState(!initialized);
appearanceProvider.ifPresent(appearanceProvider -> {
try {
appearanceProvider.addListener(this::systemInterfaceThemeChanged);
@@ -53,10 +57,15 @@ public class TrayIconController {
}
trayMenuController.initTrayMenu();
this.initialized = true;
}
private void systemInterfaceThemeChanged(Theme theme) {
trayIcon.setImage(imageFactory.loadImage()); // TODO refactor "theme" is re-queried in loadImage()
}
public boolean isInitialized() {
return initialized;
}
}

View File

@@ -15,8 +15,27 @@ public interface TrayMenuComponent {
TrayIconController trayIconController();
default void addIconToSystemTray() {
assert SystemTray.isSupported();
/**
* @return <code>true</code> if a tray icon can be installed
*/
default boolean isSupported() {
return SystemTray.isSupported();
}
/**
* @return <code>true</code> if a tray icon has been installed
*/
default boolean isInitialized() {
return trayIconController().isInitialized();
}
/**
* Installs a tray icon to the system tray.
*
* @throws IllegalStateException If already added
*/
default void addIconToSystemTray() throws IllegalStateException {
assert isSupported();
trayIconController().initializeTrayIcon();
}

View File

@@ -1,6 +1,7 @@
package org.cryptomator.ui.traymenu;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.launcher.AppLifecycleListener;
import org.cryptomator.ui.launcher.FxApplicationStarter;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
@@ -103,32 +104,36 @@ class TrayMenuController {
return actionEvent -> consumer.accept(vault);
}
private void unlockVault(Vault vault) {
fxApplicationStarter.get(true).thenAccept(app -> app.startUnlockWorkflow(vault, Optional.empty()));
}
private void lockVault(Vault vault) {
fxApplicationStarter.get(true).thenAccept(app -> app.startLockWorkflow(vault, Optional.empty()));
}
private void lockAllVaults(ActionEvent actionEvent) {
fxApplicationStarter.get(true).thenAccept(app -> app.getVaultService().lockAll(vaults.filtered(Vault::isUnlocked), false));
}
private void revealVault(Vault vault) {
fxApplicationStarter.get(true).thenAccept(app -> app.getVaultService().reveal(vault));
}
void showMainWindow(@SuppressWarnings("unused") ActionEvent actionEvent) {
fxApplicationStarter.get(true).thenAccept(app -> app.showMainWindow());
}
private void showPreferencesWindow(@SuppressWarnings("unused") EventObject actionEvent) {
fxApplicationStarter.get(true).thenAccept(app -> app.showPreferencesWindow(SelectedPreferencesTab.ANY));
}
private void quitApplication(EventObject actionEvent) {
appLifecycle.quit();
}
private void unlockVault(Vault vault) {
showMainAppAndThen(app -> app.startUnlockWorkflow(vault, Optional.empty()));
}
private void lockVault(Vault vault) {
showMainAppAndThen(app -> app.startLockWorkflow(vault, Optional.empty()));
}
private void lockAllVaults(ActionEvent actionEvent) {
showMainAppAndThen(app -> app.getVaultService().lockAll(vaults.filtered(Vault::isUnlocked), false));
}
private void revealVault(Vault vault) {
showMainAppAndThen(app -> app.getVaultService().reveal(vault));
}
void showMainWindow(@SuppressWarnings("unused") ActionEvent actionEvent) {
showMainAppAndThen(app -> app.showMainWindow());
}
private void showPreferencesWindow(@SuppressWarnings("unused") EventObject actionEvent) {
showMainAppAndThen(app -> app.showPreferencesWindow(SelectedPreferencesTab.ANY));
}
private void showMainAppAndThen(Consumer<FxApplication> action) {
fxApplicationStarter.get().thenAccept(action);
}
}

View File

@@ -54,7 +54,7 @@
<Tooltip text="%main.preferencesBtn.tooltip"/>
</tooltip>
</Button>
<Button contentDisplay="GRAPHIC_ONLY" mnemonicParsing="false" onAction="#minimize" focusTraversable="false" visible="${!controller.minimizeToSysTray}" managed="${!controller.minimizeToSysTray}">
<Button contentDisplay="GRAPHIC_ONLY" mnemonicParsing="false" onAction="#minimize" focusTraversable="false" visible="${controller.showMinimizeButton}" managed="${controller.showMinimizeButton}">
<graphic>
<FontAwesome5IconView glyph="WINDOW_MINIMIZE" glyphSize="12"/>
</graphic>

View File

@@ -32,7 +32,11 @@
<RadioButton fx:id="nodeOrientationRtl" text="%preferences.general.interfaceOrientation.rtl" alignment="CENTER_RIGHT" toggleGroup="${nodeOrientation}"/>
</HBox>
<CheckBox fx:id="startHiddenCheckbox" text="%preferences.general.startHidden" visible="${controller.trayMenuSupported}" managed="${controller.trayMenuSupported}"/>
<CheckBox fx:id="showMinimizeButtonCheckbox" text="%preferences.general.showMinimizeButton" visible="${controller.trayMenuInitialized}" managed="${controller.trayMenuInitialized}"/>
<CheckBox fx:id="showTrayIconCheckbox" text="%preferences.general.showTrayIcon" visible="${controller.trayMenuSupported}" managed="${controller.trayMenuSupported}"/>
<CheckBox fx:id="startHiddenCheckbox" text="%preferences.general.startHidden" />
<HBox spacing="6" alignment="CENTER_LEFT">
<CheckBox fx:id="debugModeCheckbox" text="%preferences.general.debugLogging"/>

View File

@@ -150,6 +150,8 @@ preferences.general.theme.automatic=Automatic
preferences.general.theme.light=Light
preferences.general.theme.dark=Dark
preferences.general.unlockThemes=Unlock dark mode
preferences.general.showMinimizeButton=Show minimize button
preferences.general.showTrayIcon=Show tray icon (requires restart)
preferences.general.startHidden=Hide window when starting Cryptomator
preferences.general.debugLogging=Enable debug logging
preferences.general.debugDirectory=Reveal log files