From 3369401f1cf6e9eab38c35805079fb5a61d66f26 Mon Sep 17 00:00:00 2001 From: En-Jan Chou Date: Sun, 12 Jan 2020 05:09:21 -0500 Subject: [PATCH 1/2] Add minimize button and tray menu refactoring * Add minimize button and change close button behavior * close main window (minimize to tray) if system tray available, quit application otherwise. * show minimize button if system tray unavailable * Move some codes from TrayMenuController to FxApplication, includes: * Desktop integrations (shortcut, quit handlers...) * vaults change listener for sudden termination * public method showPreferenceWindow change to showPerferenceTab due to name conflict * public method quitApplication for both main window and system tray * shutdown hook for unmounting vaults on shutdown * Add a new i18n string: main.minimizeBtn.tooltip --- .../ui/controls/FontAwesome5Icon.java | 1 + .../cryptomator/ui/fxapp/FxApplication.java | 97 ++++++++++++++++++- .../mainwindow/MainWindowTitleController.java | 17 ++-- .../ui/traymenu/TrayMenuController.java | 91 +---------------- .../resources/fxml/main_window_title.fxml | 8 ++ .../main/resources/i18n/strings.properties | 1 + 6 files changed, 120 insertions(+), 95 deletions(-) diff --git a/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java b/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java index 742d29438..b63aa75e7 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java @@ -33,6 +33,7 @@ public enum FontAwesome5Icon { TIMES("\uF00D"), // USER_CROWN("\uF6A4"), // WRENCH("\uF0AD"), // + WINDOW_MINIMIZE("\uF2D1"), // ; private final String unicode; diff --git a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java index aca5650ea..acacd93c6 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java +++ b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java @@ -3,16 +3,21 @@ package org.cryptomator.ui.fxapp; import dagger.Lazy; import javafx.application.Application; import javafx.application.Platform; +import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.collections.ObservableSet; import javafx.stage.Stage; import org.cryptomator.common.LicenseHolder; +import org.cryptomator.common.ShutdownHook; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.UiTheme; import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultState; +import org.cryptomator.common.vaults.Volume; import org.cryptomator.jni.JniException; import org.cryptomator.jni.MacApplicationUiAppearance; import org.cryptomator.jni.MacApplicationUiState; @@ -27,13 +32,21 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; +import javax.inject.Named; +import java.awt.Desktop; import java.awt.desktop.QuitResponse; +import java.util.EnumSet; +import java.util.EventObject; import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; @FxApplicationScoped public class FxApplication extends Application { private static final Logger LOG = LoggerFactory.getLogger(FxApplication.class); + private static final Set STATES_ALLOWING_TERMINATION = EnumSet.of(VaultState.LOCKED, VaultState.NEEDS_MIGRATION, VaultState.MISSING, VaultState.ERROR); private final Settings settings; private final Lazy mainWindow; @@ -45,9 +58,13 @@ public class FxApplication extends Application { private final LicenseHolder licenseHolder; private final ObservableSet visibleStages = FXCollections.observableSet(); private final BooleanBinding hasVisibleStages = Bindings.isNotEmpty(visibleStages); + private final AtomicBoolean allowSuddenTermination; + private final CountDownLatch shutdownLatch; + private final ShutdownHook shutdownHook; + private final ObservableList vaults; @Inject - FxApplication(Settings settings, Lazy mainWindow, Lazy preferencesWindow, UnlockComponent.Builder unlockWindowBuilder, QuitComponent.Builder quitWindowBuilder, Optional macFunctions, VaultService vaultService, LicenseHolder licenseHolder) { + FxApplication(Settings settings, Lazy mainWindow, Lazy preferencesWindow, UnlockComponent.Builder unlockWindowBuilder, QuitComponent.Builder quitWindowBuilder, Optional macFunctions, VaultService vaultService, LicenseHolder licenseHolder, ObservableList vaults, @Named("shutdownLatch") CountDownLatch shutdownLatch, ShutdownHook shutdownHook) { this.settings = settings; this.mainWindow = mainWindow; this.preferencesWindow = preferencesWindow; @@ -56,6 +73,10 @@ public class FxApplication extends Application { this.macFunctions = macFunctions; this.vaultService = vaultService; this.licenseHolder = licenseHolder; + this.vaults = vaults; + this.shutdownLatch = shutdownLatch; + this.shutdownHook = shutdownHook; + this.allowSuddenTermination = new AtomicBoolean(true); } public void start() { @@ -66,6 +87,25 @@ public class FxApplication extends Application { settings.theme().addListener(this::themeChanged); loadSelectedStyleSheet(settings.theme().get()); + + vaults.addListener(this::vaultsChanged); + + shutdownHook.runOnShutdown(this::forceUnmountRemainingVaults); + + // register preferences shortcut + if (Desktop.getDesktop().isSupported(Desktop.Action.APP_PREFERENCES)) { + Desktop.getDesktop().setPreferencesHandler(this::showPreferencesWindow); + } + + // register quit handler + if (Desktop.getDesktop().isSupported(Desktop.Action.APP_QUIT_HANDLER)) { + Desktop.getDesktop().setQuitHandler(this::handleQuitRequest); + } + + // allow sudden termination + if (Desktop.getDesktop().isSupported(Desktop.Action.APP_SUDDEN_TERMINATION)) { + Desktop.getDesktop().enableSuddenTermination(); + } } @Override @@ -86,7 +126,7 @@ public class FxApplication extends Application { } } - public void showPreferencesWindow(SelectedPreferencesTab selectedTab) { + public void showPreferencesTab(SelectedPreferencesTab selectedTab) { Platform.runLater(() -> { Stage stage = preferencesWindow.get().showPreferencesWindow(selectedTab); addVisibleStage(stage); @@ -110,7 +150,7 @@ public class FxApplication extends Application { }); } - public void showQuitWindow(QuitResponse response) { + private void showQuitWindow(@SuppressWarnings("unused") EventObject actionEvent, QuitResponse response) { Platform.runLater(() -> { Stage stage = quitWindowBuilder.quitResponse(response).build().showQuitWindow(); addVisibleStage(stage); @@ -145,4 +185,55 @@ public class FxApplication extends Application { } } + public void quitApplication() { + handleQuitRequest(null, new QuitResponse() { + @Override + public void performQuit() { + shutdownLatch.countDown(); + } + + @Override + public void cancelQuit() { + // no-op + } + }); + } + + private void showPreferencesWindow(@SuppressWarnings("unused") EventObject actionEvent) { + showPreferencesTab(SelectedPreferencesTab.ANY); + } + + private void handleQuitRequest(EventObject e, QuitResponse response) { + if (allowSuddenTermination.get()) { + response.performQuit(); // really? + } else { + showQuitWindow(e, response); + } + } + + private void vaultsChanged(@SuppressWarnings("unused") Observable observable) { + boolean allVaultsAllowTermination = vaults.stream().map(Vault::getState).allMatch(STATES_ALLOWING_TERMINATION::contains); + boolean suddenTerminationChanged = allowSuddenTermination.compareAndSet(!allVaultsAllowTermination, allVaultsAllowTermination); + if (suddenTerminationChanged && Desktop.getDesktop().isSupported(Desktop.Action.APP_SUDDEN_TERMINATION)) { + if (allVaultsAllowTermination) { + Desktop.getDesktop().enableSuddenTermination(); + LOG.debug("sudden termination enabled"); + } else { + Desktop.getDesktop().disableSuddenTermination(); + LOG.debug("sudden termination disabled"); + } + } + } + + private void forceUnmountRemainingVaults() { + for (Vault vault : vaults) { + if (vault.isUnlocked()) { + try { + vault.lock(true); + } catch (Volume.VolumeException e) { + LOG.error("Failed to unmount vault " + vault.getPath(), e); + } + } + } + } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java index 49bd905f5..ed1d51c50 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java @@ -5,12 +5,10 @@ import javafx.fxml.FXML; import javafx.scene.layout.HBox; import javafx.stage.Stage; import org.cryptomator.common.LicenseHolder; -import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.fxapp.FxApplication; import org.cryptomator.ui.fxapp.UpdateChecker; import org.cryptomator.ui.preferences.SelectedPreferencesTab; -import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -63,18 +61,23 @@ public class MainWindowTitleController implements FxController { if (minimizeToSysTray) { window.close(); } else { - window.setIconified(true); + application.quitApplication(); } } + @FXML + public void minimize() { + window.setIconified(true); + } + @FXML public void showPreferences() { - application.showPreferencesWindow(SelectedPreferencesTab.ANY); + application.showPreferencesTab(SelectedPreferencesTab.ANY); } @FXML public void showDonationKeyPreferences() { - application.showPreferencesWindow(SelectedPreferencesTab.DONATION_KEY); + application.showPreferencesTab(SelectedPreferencesTab.DONATION_KEY); } /* Getter/Setter */ @@ -91,5 +94,7 @@ public class MainWindowTitleController implements FxController { return updateAvailable.get(); } - + public boolean isMinimizeToSysTray() { + return minimizeToSysTray; + } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java b/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java index bef3c441c..5220fd5c9 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java @@ -3,57 +3,33 @@ package org.cryptomator.ui.traymenu; import javafx.application.Platform; import javafx.beans.Observable; import javafx.collections.ObservableList; -import org.cryptomator.common.ShutdownHook; -import org.cryptomator.common.settings.Settings; import org.cryptomator.common.vaults.Vault; -import org.cryptomator.common.vaults.VaultState; -import org.cryptomator.common.vaults.Volume; -import org.cryptomator.ui.fxapp.FxApplication; import org.cryptomator.ui.launcher.FxApplicationStarter; import org.cryptomator.ui.preferences.SelectedPreferencesTab; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import javax.inject.Inject; -import javax.inject.Named; -import java.awt.Desktop; import java.awt.Menu; import java.awt.MenuItem; import java.awt.PopupMenu; -import java.awt.desktop.QuitResponse; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.util.EnumSet; import java.util.EventObject; import java.util.ResourceBundle; -import java.util.Set; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; @TrayMenuScoped class TrayMenuController { - - private static final Logger LOG = LoggerFactory.getLogger(TrayMenuController.class); - public static final Set STATES_ALLOWING_TERMINATION = EnumSet.of(VaultState.LOCKED, VaultState.NEEDS_MIGRATION, VaultState.MISSING, VaultState.ERROR); - private final ResourceBundle resourceBundle; private final FxApplicationStarter fxApplicationStarter; - private final CountDownLatch shutdownLatch; - private final ShutdownHook shutdownHook; private final ObservableList vaults; private final PopupMenu menu; - private final AtomicBoolean allowSuddenTermination; @Inject - TrayMenuController(ResourceBundle resourceBundle, FxApplicationStarter fxApplicationStarter, @Named("shutdownLatch") CountDownLatch shutdownLatch, ShutdownHook shutdownHook, ObservableList vaults) { + TrayMenuController(ResourceBundle resourceBundle, FxApplicationStarter fxApplicationStarter, ObservableList vaults) { this.resourceBundle = resourceBundle; this.fxApplicationStarter = fxApplicationStarter; - this.shutdownLatch = shutdownLatch; - this.shutdownHook = shutdownHook; this.vaults = vaults; this.menu = new PopupMenu(); - this.allowSuddenTermination = new AtomicBoolean(true); } public PopupMenu getMenu() { @@ -64,38 +40,11 @@ class TrayMenuController { vaults.addListener(this::vaultListChanged); rebuildMenu(); - - // register preferences shortcut - if (Desktop.getDesktop().isSupported(Desktop.Action.APP_PREFERENCES)) { - Desktop.getDesktop().setPreferencesHandler(this::showPreferencesWindow); - } - - // register quit handler - if (Desktop.getDesktop().isSupported(Desktop.Action.APP_QUIT_HANDLER)) { - Desktop.getDesktop().setQuitHandler(this::handleQuitRequest); - } - shutdownHook.runOnShutdown(this::forceUnmountRemainingVaults); - - // allow sudden termination - if (Desktop.getDesktop().isSupported(Desktop.Action.APP_SUDDEN_TERMINATION)) { - Desktop.getDesktop().enableSuddenTermination(); - } } private void vaultListChanged(@SuppressWarnings("unused") Observable observable) { assert Platform.isFxApplicationThread(); rebuildMenu(); - boolean allVaultsAllowTermination = vaults.stream().map(Vault::getState).allMatch(STATES_ALLOWING_TERMINATION::contains); - boolean suddenTerminationChanged = allowSuddenTermination.compareAndSet(!allVaultsAllowTermination, allVaultsAllowTermination); - if (suddenTerminationChanged && Desktop.getDesktop().isSupported(Desktop.Action.APP_SUDDEN_TERMINATION)) { - if (allVaultsAllowTermination) { - Desktop.getDesktop().enableSuddenTermination(); - LOG.debug("sudden termination enabled"); - } else { - Desktop.getDesktop().disableSuddenTermination(); - LOG.debug("sudden termination disabled"); - } - } } private void rebuildMenu() { @@ -166,45 +115,15 @@ class TrayMenuController { fxApplicationStarter.get(true).thenAccept(app -> app.getVaultService().reveal(vault)); } - void showMainWindow(@SuppressWarnings("unused") ActionEvent actionEvent) { + public 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)); + fxApplicationStarter.get(true).thenAccept(app -> app.showPreferencesTab(SelectedPreferencesTab.ANY)); } - private void handleQuitRequest(EventObject e, QuitResponse response) { - if (allowSuddenTermination.get()) { - response.performQuit(); // really? - } else { - fxApplicationStarter.get(true).thenAccept(app -> app.showQuitWindow(response)); - } - } - - private void quitApplication(EventObject actionEvent) { - handleQuitRequest(actionEvent, new QuitResponse() { - @Override - public void performQuit() { - shutdownLatch.countDown(); - } - - @Override - public void cancelQuit() { - // no-op - } - }); - } - - private void forceUnmountRemainingVaults() { - for (Vault vault : vaults) { - if (vault.isUnlocked()) { - try { - vault.lock(true); - } catch (Volume.VolumeException e) { - LOG.error("Failed to unmount vault " + vault.getPath(), e); - } - } - } + private void quitApplication(@SuppressWarnings("unused") EventObject actionEvent) { + fxApplicationStarter.get(true).thenAccept(app -> app.quitApplication()); } } diff --git a/main/ui/src/main/resources/fxml/main_window_title.fxml b/main/ui/src/main/resources/fxml/main_window_title.fxml index 3765146bd..968f706bc 100644 --- a/main/ui/src/main/resources/fxml/main_window_title.fxml +++ b/main/ui/src/main/resources/fxml/main_window_title.fxml @@ -43,6 +43,14 @@ +