From d10d8fb208450efba1baedcedebf5ec5c812cb20 Mon Sep 17 00:00:00 2001 From: Jan-Peter Klein Date: Wed, 29 Jun 2022 15:12:43 +0200 Subject: [PATCH] implemented force quit dialog --- src/main/java/module-info.java | 1 + .../org/cryptomator/ui/common/FxmlFile.java | 1 + .../ui/fxapp/FxApplicationModule.java | 9 +- .../ui/fxapp/FxApplicationTerminator.java | 3 +- .../ui/fxapp/FxApplicationWindows.java | 9 +- .../ui/quitforced/QuitForcedComponent.java | 44 +++++++++ .../ui/quitforced/QuitForcedController.java | 93 +++++++++++++++++++ .../ui/quitforced/QuitForcedModule.java | 58 ++++++++++++ .../ui/quitforced/QuitForcedScoped.java | 13 +++ .../ui/quitforced/QuitForcedWindow.java | 14 +++ src/main/resources/fxml/quit_forced.fxml | 59 ++++++++++++ src/main/resources/i18n/strings.properties | 8 +- 12 files changed, 307 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/cryptomator/ui/quitforced/QuitForcedComponent.java create mode 100644 src/main/java/org/cryptomator/ui/quitforced/QuitForcedController.java create mode 100644 src/main/java/org/cryptomator/ui/quitforced/QuitForcedModule.java create mode 100644 src/main/java/org/cryptomator/ui/quitforced/QuitForcedScoped.java create mode 100644 src/main/java/org/cryptomator/ui/quitforced/QuitForcedWindow.java create mode 100644 src/main/resources/fxml/quit_forced.fxml diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index dd3d7680b..0d2c141b7 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -51,6 +51,7 @@ module org.cryptomator.desktop { opens org.cryptomator.ui.migration to javafx.fxml; opens org.cryptomator.ui.preferences to javafx.fxml; opens org.cryptomator.ui.quit to javafx.fxml; + opens org.cryptomator.ui.quitforced to javafx.fxml; opens org.cryptomator.ui.recoverykey to javafx.fxml; opens org.cryptomator.ui.removevault to javafx.fxml; opens org.cryptomator.ui.stats to javafx.fxml; diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java index bc952f9d1..e1912aad1 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -23,6 +23,7 @@ public enum FxmlFile { MIGRATION_SUCCESS("/fxml/migration_success.fxml"), // PREFERENCES("/fxml/preferences.fxml"), // QUIT("/fxml/quit.fxml"), // + QUIT_FORCED("/fxml/quit_forced.fxml"), // RECOVERYKEY_CREATE("/fxml/recoverykey_create.fxml"), // RECOVERYKEY_RECOVER("/fxml/recoverykey_recover.fxml"), // RECOVERYKEY_RESET_PASSWORD("/fxml/recoverykey_reset_password.fxml"), // diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java index 85e46dffa..007e62fd6 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java @@ -14,6 +14,7 @@ import org.cryptomator.ui.lock.LockComponent; import org.cryptomator.ui.mainwindow.MainWindowComponent; import org.cryptomator.ui.preferences.PreferencesComponent; import org.cryptomator.ui.quit.QuitComponent; +import org.cryptomator.ui.quitforced.QuitForcedComponent; import org.cryptomator.ui.traymenu.TrayMenuComponent; import org.cryptomator.ui.unlock.UnlockComponent; @@ -25,7 +26,7 @@ import java.io.UncheckedIOException; import java.util.Collections; import java.util.List; -@Module(includes = {UpdateCheckerModule.class}, subcomponents = {TrayMenuComponent.class, MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class, LockComponent.class, QuitComponent.class, ErrorComponent.class}) +@Module(includes = {UpdateCheckerModule.class}, subcomponents = {TrayMenuComponent.class, MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class, LockComponent.class, QuitComponent.class, QuitForcedComponent.class, ErrorComponent.class}) abstract class FxApplicationModule { private static Image createImageFromResource(String resourceName) throws IOException { @@ -57,4 +58,10 @@ abstract class FxApplicationModule { static QuitComponent provideQuitComponent(QuitComponent.Builder builder) { return builder.build(); } + + @Provides + @FxApplicationScoped + static QuitForcedComponent provideQuitForcedComponent(QuitForcedComponent.Builder builder) { + return builder.build(); + } } diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationTerminator.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationTerminator.java index cb34ae3d0..b38bd0d06 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationTerminator.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationTerminator.java @@ -115,8 +115,7 @@ public class FxApplicationTerminator { }); lockAllTask.setOnFailed(event -> { LOG.warn("Unable to lock all vaults."); - exitingResponse.cancelQuit(); - //TODO: notify user!?! + appWindows.showQuitForcedWindow(exitingResponse); }); lockAllTask.run(); } else { diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java index ba28f9bc4..d80cb6c5b 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java @@ -11,6 +11,7 @@ import org.cryptomator.ui.mainwindow.MainWindowComponent; import org.cryptomator.ui.preferences.PreferencesComponent; import org.cryptomator.ui.preferences.SelectedPreferencesTab; import org.cryptomator.ui.quit.QuitComponent; +import org.cryptomator.ui.quitforced.QuitForcedComponent; import org.cryptomator.ui.unlock.UnlockComponent; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -41,6 +42,7 @@ public class FxApplicationWindows { private final Lazy mainWindow; private final Lazy preferencesWindow; private final Lazy quitWindow; + private final Lazy quitForcedWindow; private final UnlockComponent.Factory unlockWorkflowFactory; private final LockComponent.Factory lockWorkflowFactory; private final ErrorComponent.Factory errorWindowFactory; @@ -48,12 +50,13 @@ public class FxApplicationWindows { private final FilteredList visibleWindows; @Inject - public FxApplicationWindows(@PrimaryStage Stage primaryStage, Optional trayIntegration, Lazy mainWindow, Lazy preferencesWindow, Lazy quitWindow, UnlockComponent.Factory unlockWorkflowFactory, LockComponent.Factory lockWorkflowFactory, ErrorComponent.Factory errorWindowFactory, ExecutorService executor) { + public FxApplicationWindows(@PrimaryStage Stage primaryStage, Optional trayIntegration, Lazy mainWindow, Lazy preferencesWindow, Lazy quitWindow, Lazy quitForcedWindow, UnlockComponent.Factory unlockWorkflowFactory, LockComponent.Factory lockWorkflowFactory, ErrorComponent.Factory errorWindowFactory, ExecutorService executor) { this.primaryStage = primaryStage; this.trayIntegration = trayIntegration; this.mainWindow = mainWindow; this.preferencesWindow = preferencesWindow; this.quitWindow = quitWindow; + this.quitForcedWindow = quitForcedWindow; this.unlockWorkflowFactory = unlockWorkflowFactory; this.lockWorkflowFactory = lockWorkflowFactory; this.errorWindowFactory = errorWindowFactory; @@ -108,6 +111,10 @@ public class FxApplicationWindows { return CompletableFuture.supplyAsync(() -> quitWindow.get().showQuitWindow(response), Platform::runLater).whenComplete(this::reportErrors); } + public CompletionStage showQuitForcedWindow(QuitResponse response) { + return CompletableFuture.supplyAsync(() -> quitForcedWindow.get().showQuitForcedWindow(response), Platform::runLater).whenComplete(this::reportErrors); + } + public CompletionStage startUnlockWorkflow(Vault vault, @Nullable Stage owner) { return CompletableFuture.supplyAsync(() -> { Preconditions.checkState(vault.stateProperty().transition(VaultState.Value.LOCKED, VaultState.Value.PROCESSING), "Vault not locked."); diff --git a/src/main/java/org/cryptomator/ui/quitforced/QuitForcedComponent.java b/src/main/java/org/cryptomator/ui/quitforced/QuitForcedComponent.java new file mode 100644 index 000000000..0a6ba5fb2 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/quitforced/QuitForcedComponent.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the accompanying LICENSE file. + *******************************************************************************/ +package org.cryptomator.ui.quitforced; + +import dagger.Lazy; +import dagger.Subcomponent; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; + +import javafx.scene.Scene; +import javafx.stage.Stage; +import java.awt.desktop.QuitResponse; + +@QuitForcedScoped +@Subcomponent(modules = {QuitForcedModule.class}) +public interface QuitForcedComponent { + + @QuitForcedWindow + Stage window(); + + @FxmlScene(FxmlFile.QUIT_FORCED) + Lazy scene(); + + QuitForcedController controller(); + + default Stage showQuitForcedWindow(QuitResponse response) { + controller().updateQuitRequest(response); + Stage stage = window(); + stage.setScene(scene().get()); + stage.show(); + stage.requestFocus(); + return stage; + } + + @Subcomponent.Builder + interface Builder { + + QuitForcedComponent build(); + } + +} diff --git a/src/main/java/org/cryptomator/ui/quitforced/QuitForcedController.java b/src/main/java/org/cryptomator/ui/quitforced/QuitForcedController.java new file mode 100644 index 000000000..60e758991 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/quitforced/QuitForcedController.java @@ -0,0 +1,93 @@ +package org.cryptomator.ui.quitforced; + +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.VaultService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javafx.collections.ObservableList; +import javafx.concurrent.Task; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.ContentDisplay; +import javafx.stage.Stage; +import java.awt.desktop.QuitResponse; +import java.util.Collection; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +@QuitForcedScoped +public class QuitForcedController implements FxController { + + private static final Logger LOG = LoggerFactory.getLogger(QuitForcedController.class); + + private final Stage window; + private final ObservableList unlockedVaults; + private final ExecutorService executorService; + private final VaultService vaultService; + private final AtomicReference quitResponse = new AtomicReference<>(); + + /* FXML */ + public Button forceLockAndQuitButton; + + @Inject + QuitForcedController(@QuitForcedWindow Stage window, ObservableList vaults, ExecutorService executorService, VaultService vaultService) { + this.window = window; + this.unlockedVaults = vaults.filtered(Vault::isUnlocked); + this.executorService = executorService; + this.vaultService = vaultService; + window.setOnCloseRequest(windowEvent -> cancel()); + } + + public void updateQuitRequest(QuitResponse newResponse) { + var oldResponse = quitResponse.getAndSet(newResponse); + if (oldResponse != null) { + oldResponse.cancelQuit(); + } + } + + private void respondToQuitRequest(Consumer action) { + var response = quitResponse.getAndSet(null); + if (response != null) { + action.accept(response); + } + } + + @FXML + public void cancel() { + LOG.info("Quitting application canceled by user."); + window.close(); + respondToQuitRequest(QuitResponse::cancelQuit); + } + + @FXML + public void forceLockAndQuit() { + forceLockAndQuitButton.setDisable(true); + forceLockAndQuitButton.setContentDisplay(ContentDisplay.LEFT); + + Task> lockAllTask = vaultService.createLockAllTask(unlockedVaults, true); // forced set to true + lockAllTask.setOnSucceeded(evt -> { + LOG.info("Locked {}", lockAllTask.getValue().stream().map(Vault::getDisplayName).collect(Collectors.joining(", "))); + if (unlockedVaults.isEmpty()) { + window.close(); + respondToQuitRequest(QuitResponse::performQuit); + } + }); + lockAllTask.setOnFailed(evt -> { + //TODO: what will happen if force lock and quit app fails? + + LOG.error("Forced locking failed", lockAllTask.getException()); + forceLockAndQuitButton.setDisable(false); + forceLockAndQuitButton.setContentDisplay(ContentDisplay.TEXT_ONLY); + + window.close(); + respondToQuitRequest(QuitResponse::cancelQuit); + }); + executorService.execute(lockAllTask); + } + +} diff --git a/src/main/java/org/cryptomator/ui/quitforced/QuitForcedModule.java b/src/main/java/org/cryptomator/ui/quitforced/QuitForcedModule.java new file mode 100644 index 000000000..b46de2cc5 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/quitforced/QuitForcedModule.java @@ -0,0 +1,58 @@ +package org.cryptomator.ui.quitforced; + +import dagger.Binds; +import dagger.Module; +import dagger.Provides; +import dagger.multibindings.IntoMap; +import org.cryptomator.ui.common.DefaultSceneFactory; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.FxControllerKey; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlLoaderFactory; +import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.ui.common.StageFactory; + +import javax.inject.Provider; +import javafx.scene.Scene; +import javafx.stage.Modality; +import javafx.stage.Stage; +import java.util.Map; +import java.util.ResourceBundle; + +@Module +abstract class QuitForcedModule { + + @Provides + @QuitForcedWindow + @QuitForcedScoped + static FxmlLoaderFactory provideFxmlLoaderFactory(Map, Provider> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { + return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle); + } + + @Provides + @QuitForcedWindow + @QuitForcedScoped + static Stage provideStage(StageFactory factory, ResourceBundle resourceBundle) { + Stage stage = factory.create(); + stage.setMinWidth(300); + stage.setMinHeight(100); + stage.initModality(Modality.APPLICATION_MODAL); + stage.setTitle(resourceBundle.getString("forcedQuit.title")); + return stage; + } + + @Provides + @FxmlScene(FxmlFile.QUIT_FORCED) + @QuitForcedScoped + static Scene provideQuitScene(@QuitForcedWindow FxmlLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene(FxmlFile.QUIT_FORCED); + } + + // ------------------ + + @Binds + @IntoMap + @FxControllerKey(QuitForcedController.class) + abstract FxController bindQuitController(QuitForcedController controller); + +} diff --git a/src/main/java/org/cryptomator/ui/quitforced/QuitForcedScoped.java b/src/main/java/org/cryptomator/ui/quitforced/QuitForcedScoped.java new file mode 100644 index 000000000..53bbc2209 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/quitforced/QuitForcedScoped.java @@ -0,0 +1,13 @@ +package org.cryptomator.ui.quitforced; + +import javax.inject.Scope; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Scope +@Documented +@Retention(RetentionPolicy.RUNTIME) +@interface QuitForcedScoped { + +} diff --git a/src/main/java/org/cryptomator/ui/quitforced/QuitForcedWindow.java b/src/main/java/org/cryptomator/ui/quitforced/QuitForcedWindow.java new file mode 100644 index 000000000..5026a4a5a --- /dev/null +++ b/src/main/java/org/cryptomator/ui/quitforced/QuitForcedWindow.java @@ -0,0 +1,14 @@ +package org.cryptomator.ui.quitforced; + +import javax.inject.Qualifier; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Qualifier +@Documented +@Retention(RUNTIME) +@interface QuitForcedWindow { + +} diff --git a/src/main/resources/fxml/quit_forced.fxml b/src/main/resources/fxml/quit_forced.fxml new file mode 100644 index 000000000..26f5e9251 --- /dev/null +++ b/src/main/resources/fxml/quit_forced.fxml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties index 8642be287..166e9b892 100644 --- a/src/main/resources/i18n/strings.properties +++ b/src/main/resources/i18n/strings.properties @@ -382,4 +382,10 @@ passwordStrength.messageLabel.4=Very strong # Quit quit.prompt=Quit application? There are unlocked vaults. -quit.lockAndQuit=Lock and Quit \ No newline at end of file +quit.lockAndQuit=Lock and Quit + +# Forced Quit +forcedQuit.title=Quit application +forcedQuit.message=Some vaults could not be locked +forcedQuit.description=Locking vaults was blocked by pending operations or open files. You can force lock remaining vaults, however interrupting I/O may result in the loss of unsaved data. +forcedQuit.forceLockAndQuitBtn=Force and Quit