Browse Source

implemented force quit dialog

Jan-Peter Klein 3 years ago
parent
commit
d10d8fb208

+ 1 - 0
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;

+ 1 - 0
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"), //

+ 8 - 1
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();
+	}
 }

+ 1 - 2
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 {

+ 8 - 1
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<MainWindowComponent> mainWindow;
 	private final Lazy<PreferencesComponent> preferencesWindow;
 	private final Lazy<QuitComponent> quitWindow;
+	private final Lazy<QuitForcedComponent> 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<Window> visibleWindows;
 
 	@Inject
-	public FxApplicationWindows(@PrimaryStage Stage primaryStage, Optional<TrayIntegrationProvider> trayIntegration, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Lazy<QuitComponent> quitWindow, UnlockComponent.Factory unlockWorkflowFactory, LockComponent.Factory lockWorkflowFactory, ErrorComponent.Factory errorWindowFactory, ExecutorService executor) {
+	public FxApplicationWindows(@PrimaryStage Stage primaryStage, Optional<TrayIntegrationProvider> trayIntegration, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Lazy<QuitComponent> quitWindow, Lazy<QuitForcedComponent> 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<Stage> showQuitForcedWindow(QuitResponse response) {
+		return CompletableFuture.supplyAsync(() -> quitForcedWindow.get().showQuitForcedWindow(response), Platform::runLater).whenComplete(this::reportErrors);
+	}
+
 	public CompletionStage<Void> startUnlockWorkflow(Vault vault, @Nullable Stage owner) {
 		return CompletableFuture.supplyAsync(() -> {
 					Preconditions.checkState(vault.stateProperty().transition(VaultState.Value.LOCKED, VaultState.Value.PROCESSING), "Vault not locked.");

+ 44 - 0
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> 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();
+	}
+
+}

+ 93 - 0
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<Vault> unlockedVaults;
+	private final ExecutorService executorService;
+	private final VaultService vaultService;
+	private final AtomicReference<QuitResponse> quitResponse = new AtomicReference<>();
+
+	/* FXML */
+	public Button forceLockAndQuitButton;
+
+	@Inject
+	QuitForcedController(@QuitForcedWindow Stage window, ObservableList<Vault> 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<QuitResponse> 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<Collection<Vault>> 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);
+	}
+
+}

+ 58 - 0
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<Class<? extends FxController>, Provider<FxController>> 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);
+
+}

+ 13 - 0
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 {
+
+}

+ 14 - 0
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 {
+
+}

+ 59 - 0
src/main/resources/fxml/quit_forced.fxml

@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
+<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
+<?import javafx.geometry.Insets?>
+<?import javafx.scene.control.Button?>
+<?import javafx.scene.control.ButtonBar?>
+<?import javafx.scene.control.Label?>
+<?import javafx.scene.layout.HBox?>
+<?import javafx.scene.layout.StackPane?>
+<?import javafx.scene.layout.VBox?>
+<?import javafx.scene.shape.Circle?>
+<?import javafx.scene.Group?>
+<?import javafx.scene.layout.Region?>
+<HBox xmlns:fx="http://javafx.com/fxml"
+	  xmlns="http://javafx.com/javafx"
+	  fx:controller="org.cryptomator.ui.quitforced.QuitForcedController"
+	  minWidth="400"
+	  maxWidth="400"
+	  minHeight="145"
+	  spacing="12"
+	  alignment="TOP_LEFT">
+	<padding>
+		<Insets topRightBottomLeft="12"/>
+	</padding>
+	<children>
+		<Group>
+			<StackPane>
+				<padding>
+					<Insets topRightBottomLeft="6"/>
+				</padding>
+				<Circle styleClass="glyph-icon-orange" radius="24"/>
+				<FontAwesome5IconView styleClass="glyph-icon-white" glyph="EXCLAMATION" glyphSize="24"/>
+			</StackPane>
+		</Group>
+
+		<VBox HBox.hgrow="ALWAYS">
+			<Label styleClass="label-large" text="%forcedQuit.message" wrapText="true" textAlignment="LEFT">
+				<padding>
+					<Insets bottom="6" top="6"/>
+				</padding>
+			</Label>
+
+			<Label text="%forcedQuit.description" wrapText="true" textAlignment="LEFT"/>
+
+			<Region VBox.vgrow="ALWAYS" minHeight="18"/>
+			<ButtonBar buttonMinWidth="120" buttonOrder="+CI">
+				<buttons>
+					<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" defaultButton="true" cancelButton="true" onAction="#cancel"/>
+					<Button fx:id="forceLockAndQuitButton" text="%forcedQuit.forceLockAndQuitBtn" ButtonBar.buttonData="FINISH" onAction="#forceLockAndQuit" contentDisplay="TEXT_ONLY">
+						<graphic>
+							<FontAwesome5Spinner glyphSize="12"/>
+						</graphic>
+					</Button>
+				</buttons>
+			</ButtonBar>
+		</VBox>
+	</children>
+</HBox>

+ 7 - 1
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
+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