Added checkbox in settings to start without a tray icon

references #1113, #1078, #1079, #1344
This commit is contained in:
Sebastian Stenzel
2020-12-16 16:53:49 +01:00
parent 96bb97d50a
commit 45c714a123
12 changed files with 89 additions and 66 deletions

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

@@ -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 org.cryptomator.common.settings.Settings;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.fxapp.FxApplicationComponent;
import org.slf4j.Logger;
@@ -8,6 +9,7 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import javafx.application.Platform;
import java.awt.SystemTray;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutorService;
@@ -22,23 +24,25 @@ public class FxApplicationStarter {
private final ExecutorService executor;
private final AtomicBoolean started;
private final CompletableFuture<FxApplication> future;
private final boolean hasTrayIcon;
@Inject
public FxApplicationStarter(FxApplicationComponent.Builder fxAppComponent, ExecutorService executor) {
public FxApplicationStarter(FxApplicationComponent.Builder fxAppComponent, ExecutorService executor, Settings settings) {
this.fxAppComponent = fxAppComponent;
this.executor = executor;
this.started = new AtomicBoolean();
this.future = new CompletableFuture<>();
this.hasTrayIcon = SystemTray.isSupported() && settings.showTrayIcon().get();
}
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(() -> {
@@ -50,5 +54,4 @@ public class FxApplicationStarter {
});
});
}
}

View File

@@ -40,44 +40,50 @@ public class UiLauncher {
}
public void launch() {
final boolean hasTrayIcon;
if (SystemTray.isSupported()) {
boolean hidden = settings.startHidden().get();
if (SystemTray.isSupported() && settings.showTrayIcon().get()) {
trayComponent.build().addIconToSystemTray();
hasTrayIcon = true;
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

@@ -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

@@ -28,7 +28,7 @@ public class MainWindowTitleController implements FxController {
private final AppLifecycleListener appLifecycle;
private final Stage window;
private final FxApplication application;
private final boolean minimizeToSysTray;
private final boolean isTrayIconPresent;
private final UpdateChecker updateChecker;
private final BooleanBinding updateAvailable;
private final LicenseHolder licenseHolder;
@@ -39,11 +39,11 @@ public class MainWindowTitleController implements FxController {
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, @Named("trayMenuSupported") boolean isTrayIconPresent, UpdateChecker updateChecker, LicenseHolder licenseHolder, Settings settings) {
this.appLifecycle = appLifecycle;
this.window = window;
this.application = application;
this.minimizeToSysTray = minimizeToSysTray;
this.isTrayIconPresent = isTrayIconPresent;
this.updateChecker = updateChecker;
this.updateAvailable = updateChecker.latestVersionProperty().isNotNull();
this.licenseHolder = licenseHolder;
@@ -71,7 +71,7 @@ public class MainWindowTitleController implements FxController {
@FXML
public void close() {
if (minimizeToSysTray) {
if (isTrayIconPresent) {
window.close();
} else {
appLifecycle.quit();
@@ -112,8 +112,8 @@ public class MainWindowTitleController implements FxController {
return updateAvailable.get();
}
public boolean isMinimizeToSysTray() {
return minimizeToSysTray;
public boolean isTrayIconPresent() {
return isTrayIconPresent;
}
public BooleanBinding debugModeEnabledProperty() {

View File

@@ -27,6 +27,7 @@ import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import java.awt.SystemTray;
import java.util.Arrays;
import java.util.Optional;
import java.util.ResourceBundle;
@@ -41,7 +42,6 @@ public class GeneralPreferencesController implements FxController {
private final Stage window;
private final Settings settings;
private final boolean trayMenuSupported;
private final Optional<AutoStartProvider> autoStartProvider;
private final ObjectProperty<SelectedPreferencesTab> selectedTabProperty;
private final LicenseHolder licenseHolder;
@@ -53,6 +53,7 @@ public class GeneralPreferencesController implements FxController {
private final ErrorComponent.Builder errorComponent;
public ChoiceBox<UiTheme> themeChoiceBox;
public ChoiceBox<KeychainBackend> keychainBackendChoiceBox;
public CheckBox showTrayIconCheckbox;
public CheckBox startHiddenCheckbox;
public CheckBox debugModeCheckbox;
public CheckBox autoStartCheckbox;
@@ -61,10 +62,9 @@ public class GeneralPreferencesController implements FxController {
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, Optional<AutoStartProvider> autoStartProvider, Set<KeychainAccessProvider> keychainAccessProviders, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ExecutorService executor, ResourceBundle resourceBundle, Application application, Environment environment, ErrorComponent.Builder errorComponent) {
this.window = window;
this.settings = settings;
this.trayMenuSupported = trayMenuSupported;
this.autoStartProvider = autoStartProvider;
this.keychainAccessProviders = keychainAccessProviders;
this.selectedTabProperty = selectedTabProperty;
@@ -85,6 +85,8 @@ public class GeneralPreferencesController implements FxController {
themeChoiceBox.valueProperty().bindBidirectional(settings.theme());
themeChoiceBox.setConverter(new UiThemeConverter(resourceBundle));
showTrayIconCheckbox.selectedProperty().bindBidirectional(settings.showTrayIcon());
startHiddenCheckbox.selectedProperty().bindBidirectional(settings.startHidden());
debugModeCheckbox.selectedProperty().bindBidirectional(settings.debugMode());
@@ -106,7 +108,7 @@ public class GeneralPreferencesController implements FxController {
}
public boolean isTrayMenuSupported() {
return this.trayMenuSupported;
return SystemTray.isSupported();
}
public boolean isAutoStartSupported() {

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.trayIconPresent}" managed="${!controller.trayIconPresent}">
<graphic>
<FontAwesome5IconView glyph="WINDOW_MINIMIZE" glyphSize="12"/>
</graphic>

View File

@@ -32,7 +32,9 @@
<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="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,7 @@ preferences.general.theme.automatic=Automatic
preferences.general.theme.light=Light
preferences.general.theme.dark=Dark
preferences.general.unlockThemes=Unlock dark mode
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