Bladeren bron

Merge pull request #1416 from cryptomator/feature/#1228-forcedUnmountDialog

Feature/#1228 forced unmount dialog
Armin Schrenk 4 jaren geleden
bovenliggende
commit
c48c9b1568

+ 2 - 0
main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java

@@ -11,6 +11,8 @@ public enum FxmlFile {
 	CHANGEPASSWORD("/fxml/changepassword.fxml"), //
 	ERROR("/fxml/error.fxml"), //
 	FORGET_PASSWORD("/fxml/forget_password.fxml"), //
+	LOCK_FORCED("/fxml/lock_forced.fxml"), //
+	LOCK_FAILED("/fxml/lock_failed.fxml"), //
 	MAIN_WINDOW("/fxml/main_window.fxml"), //
 	MIGRATION_CAPABILITY_ERROR("/fxml/migration_capability_error.fxml"), //
 	MIGRATION_IMPOSSIBLE("/fxml/migration_impossible.fxml"),

+ 1 - 0
main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java

@@ -65,6 +65,7 @@ public class VaultService {
 	public Task<Vault> createLockTask(Vault vault, boolean forced) {
 		Task<Vault> task = new LockVaultTask(vault, forced);
 		task.setOnSucceeded(evt -> LOG.info("Locked {}", vault.getDisplayName()));
+		task.setOnFailed(evt -> LOG.info("Failed to lock {}.", vault.getDisplayName(), evt.getSource().getException()));
 		return task;
 	}
 

+ 11 - 1
main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java

@@ -11,6 +11,7 @@ import org.cryptomator.integrations.uiappearance.UiAppearanceException;
 import org.cryptomator.integrations.uiappearance.UiAppearanceListener;
 import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
 import org.cryptomator.ui.common.VaultService;
+import org.cryptomator.ui.lock.LockComponent;
 import org.cryptomator.ui.mainwindow.MainWindowComponent;
 import org.cryptomator.ui.preferences.PreferencesComponent;
 import org.cryptomator.ui.preferences.SelectedPreferencesTab;
@@ -42,6 +43,7 @@ public class FxApplication extends Application {
 	private final Lazy<PreferencesComponent> preferencesWindow;
 	private final Lazy<QuitComponent> quitWindow;
 	private final Provider<UnlockComponent.Builder> unlockWindowBuilderProvider;
+	private final Provider<LockComponent.Builder> lockWindowBuilderProvider;
 	private final Optional<TrayIntegrationProvider> trayIntegration;
 	private final Optional<UiAppearanceProvider> appearanceProvider;
 	private final VaultService vaultService;
@@ -51,11 +53,12 @@ public class FxApplication extends Application {
 	private final UiAppearanceListener systemInterfaceThemeListener = this::systemInterfaceThemeChanged;
 
 	@Inject
-	FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Provider<UnlockComponent.Builder> unlockWindowBuilderProvider, Lazy<QuitComponent> quitWindow, Optional<TrayIntegrationProvider> trayIntegration, Optional<UiAppearanceProvider> appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder) {
+	FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Provider<UnlockComponent.Builder> unlockWindowBuilderProvider, Provider<LockComponent.Builder> lockWindowBuilderProvider, Lazy<QuitComponent> quitWindow, Optional<TrayIntegrationProvider> trayIntegration, Optional<UiAppearanceProvider> appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder) {
 		this.settings = settings;
 		this.mainWindow = mainWindow;
 		this.preferencesWindow = preferencesWindow;
 		this.unlockWindowBuilderProvider = unlockWindowBuilderProvider;
+		this.lockWindowBuilderProvider = lockWindowBuilderProvider;
 		this.quitWindow = quitWindow;
 		this.trayIntegration = trayIntegration;
 		this.appearanceProvider = appearanceProvider;
@@ -110,6 +113,13 @@ public class FxApplication extends Application {
 		});
 	}
 
+	public void startLockWorkflow(Vault vault, Optional<Stage> owner) {
+		Platform.runLater(() -> {
+			lockWindowBuilderProvider.get().vault(vault).owner(owner).build().startLockWorkflow();
+			LOG.debug("Start lock workflow for {}", vault.getDisplayName());
+		});
+	}
+
 	public void showQuitWindow(QuitResponse response) {
 		Platform.runLater(() -> {
 			quitWindow.get().showQuitWindow(response);

+ 2 - 1
main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java

@@ -11,6 +11,7 @@ import dagger.Provides;
 import org.apache.commons.lang3.SystemUtils;
 import org.cryptomator.ui.common.ErrorComponent;
 import org.cryptomator.ui.common.StageFactory;
+import org.cryptomator.ui.lock.LockComponent;
 import org.cryptomator.ui.mainwindow.MainWindowComponent;
 import org.cryptomator.ui.preferences.PreferencesComponent;
 import org.cryptomator.ui.quit.QuitComponent;
@@ -27,7 +28,7 @@ import java.io.UncheckedIOException;
 import java.util.Collections;
 import java.util.List;
 
-@Module(includes = {UpdateCheckerModule.class}, subcomponents = {MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class, QuitComponent.class, ErrorComponent.class})
+@Module(includes = {UpdateCheckerModule.class}, subcomponents = {MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class, LockComponent.class, QuitComponent.class, ErrorComponent.class})
 abstract class FxApplicationModule {
 
 	@Provides

+ 39 - 0
main/ui/src/main/java/org/cryptomator/ui/lock/LockComponent.java

@@ -0,0 +1,39 @@
+package org.cryptomator.ui.lock;
+
+import dagger.BindsInstance;
+import dagger.Subcomponent;
+import org.cryptomator.common.vaults.Vault;
+
+import javax.inject.Named;
+import javafx.stage.Stage;
+import java.util.Optional;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+
+@LockScoped
+@Subcomponent(modules = {LockModule.class})
+public interface LockComponent {
+
+	ExecutorService defaultExecutorService();
+
+	LockWorkflow lockWorkflow();
+
+	default Future<Void> startLockWorkflow() {
+		LockWorkflow workflow = lockWorkflow();
+		defaultExecutorService().submit(workflow);
+		return workflow;
+	}
+
+	@Subcomponent.Builder
+	interface Builder {
+
+		@BindsInstance
+		LockComponent.Builder vault(@LockWindow Vault vault);
+
+		@BindsInstance
+		LockComponent.Builder owner(@Named("lockWindowOwner") Optional<Stage> owner);
+
+		LockComponent build();
+	}
+}

+ 31 - 0
main/ui/src/main/java/org/cryptomator/ui/lock/LockFailedController.java

@@ -0,0 +1,31 @@
+package org.cryptomator.ui.lock;
+
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.ui.common.FxController;
+
+import javax.inject.Inject;
+import javafx.fxml.FXML;
+import javafx.stage.Stage;
+
+@LockScoped
+public class LockFailedController implements FxController {
+
+	private final Stage window;
+	private final Vault vault;
+
+	@Inject
+	public LockFailedController(@LockWindow Stage window, @LockWindow Vault vault) {
+		this.window = window;
+		this.vault = vault;
+	}
+
+	@FXML
+	public void close() {
+		window.close();
+	}
+
+	// ----- Getter & Setter -----
+	public String getVaultName() {
+		return vault.getDisplayName();
+	}
+}

+ 57 - 0
main/ui/src/main/java/org/cryptomator/ui/lock/LockForcedController.java

@@ -0,0 +1,57 @@
+package org.cryptomator.ui.lock;
+
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.common.UserInteractionLock;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import javafx.fxml.FXML;
+import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
+
+@LockScoped
+public class LockForcedController implements FxController {
+
+	private static final Logger LOG = LoggerFactory.getLogger(LockForcedController.class);
+
+	private final Stage window;
+	private final Vault vault;
+	private final UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock;
+
+	@Inject
+	public LockForcedController(@LockWindow Stage window, @LockWindow Vault vault, UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock) {
+		this.window = window;
+		this.vault = vault;
+		this.forceLockDecisionLock = forceLockDecisionLock;
+		this.window.setOnHiding(this::windowClosed);
+	}
+
+	@FXML
+	public void cancel() {
+		forceLockDecisionLock.interacted(LockModule.ForceLockDecision.CANCEL);
+		window.close();
+	}
+
+	@FXML
+	public void confirmForcedLock() {
+		forceLockDecisionLock.interacted(LockModule.ForceLockDecision.FORCE);
+		window.close();
+	}
+
+	private void windowClosed(WindowEvent windowEvent) {
+		// if not already interacted, set the decision to CANCEL
+		if (forceLockDecisionLock.awaitingInteraction().get()) {
+			LOG.debug("Lock canceled in force-lock-phase by user.");
+			forceLockDecisionLock.interacted(LockModule.ForceLockDecision.CANCEL);
+		}
+	}
+
+	// ----- Getter & Setter -----
+
+	public String getVaultName() {
+		return vault.getDisplayName();
+	}
+
+}

+ 89 - 0
main/ui/src/main/java/org/cryptomator/ui/lock/LockModule.java

@@ -0,0 +1,89 @@
+package org.cryptomator.ui.lock;
+
+import dagger.Binds;
+import dagger.Module;
+import dagger.Provides;
+import dagger.multibindings.IntoMap;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.ui.common.DefaultSceneFactory;
+import org.cryptomator.ui.common.FXMLLoaderFactory;
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.common.FxControllerKey;
+import org.cryptomator.ui.common.FxmlFile;
+import org.cryptomator.ui.common.FxmlScene;
+import org.cryptomator.ui.common.StageFactory;
+import org.cryptomator.ui.common.UserInteractionLock;
+
+import javax.inject.Named;
+import javax.inject.Provider;
+import javafx.scene.Scene;
+import javafx.stage.Modality;
+import javafx.stage.Stage;
+import java.util.Map;
+import java.util.Optional;
+import java.util.ResourceBundle;
+
+@Module
+abstract class LockModule {
+
+	enum ForceLockDecision {
+		CANCEL,
+		FORCE;
+	}
+
+	@Provides
+	@LockScoped
+	static UserInteractionLock<LockModule.ForceLockDecision> provideForceLockDecisionLock() {
+		return new UserInteractionLock<>(null);
+	}
+
+	@Provides
+	@LockWindow
+	@LockScoped
+	static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
+		return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle);
+	}
+
+	@Provides
+	@LockWindow
+	@LockScoped
+	static Stage provideWindow(StageFactory factory, @LockWindow Vault vault, @Named("lockWindowOwner") Optional<Stage> owner) {
+		Stage stage = factory.create();
+		stage.setTitle(vault.getDisplayName());
+		stage.setResizable(false);
+		if (owner.isPresent()) {
+			stage.initOwner(owner.get());
+			stage.initModality(Modality.WINDOW_MODAL);
+		} else {
+			stage.initModality(Modality.APPLICATION_MODAL);
+		}
+		return stage;
+	}
+
+	@Provides
+	@FxmlScene(FxmlFile.LOCK_FORCED)
+	@LockScoped
+	static Scene provideForceLockScene(@LockWindow FXMLLoaderFactory fxmlLoaders) {
+		return fxmlLoaders.createScene("/fxml/lock_forced.fxml");
+	}
+
+	@Provides
+	@FxmlScene(FxmlFile.LOCK_FAILED)
+	@LockScoped
+	static Scene provideLockFailedScene(@LockWindow FXMLLoaderFactory fxmlLoaders) {
+		return fxmlLoaders.createScene("/fxml/lock_failed.fxml");
+	}
+
+	// ------------------
+
+	@Binds
+	@IntoMap
+	@FxControllerKey(LockForcedController.class)
+	abstract FxController bindLockForcedController(LockForcedController controller);
+
+	@Binds
+	@IntoMap
+	@FxControllerKey(LockFailedController.class)
+	abstract FxController bindLockFailedController(LockFailedController controller);
+
+}

+ 13 - 0
main/ui/src/main/java/org/cryptomator/ui/lock/LockScoped.java

@@ -0,0 +1,13 @@
+package org.cryptomator.ui.lock;
+
+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 LockScoped {
+
+}

+ 14 - 0
main/ui/src/main/java/org/cryptomator/ui/lock/LockWindow.java

@@ -0,0 +1,14 @@
+package org.cryptomator.ui.lock;
+
+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 LockWindow {
+
+}

+ 105 - 0
main/ui/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java

@@ -0,0 +1,105 @@
+package org.cryptomator.ui.lock;
+
+import dagger.Lazy;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.common.vaults.VaultState;
+import org.cryptomator.common.vaults.Volume;
+import org.cryptomator.ui.common.FxmlFile;
+import org.cryptomator.ui.common.FxmlScene;
+import org.cryptomator.ui.common.UserInteractionLock;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import javafx.application.Platform;
+import javafx.concurrent.Task;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+import javafx.stage.Window;
+
+/**
+ * The sequence of actions performed and checked during lock of a vault.
+ * <p>
+ * This class implements the Task interface, sucht that it can run in the background with some possible forground operations/requests to the ui, without blocking the main app.
+ * If the task state is
+ * <li>succeeded, the vault was successfully locked;</li>
+ * <li>canceled, the lock was canceled;</li>
+ * <li>failed, the lock failed due to an exception.</li>
+ */
+public class LockWorkflow extends Task<Void> {
+
+	private static final Logger LOG = LoggerFactory.getLogger(LockWorkflow.class);
+
+	private final Stage lockWindow;
+	private final Vault vault;
+	private final UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock;
+	private final Lazy<Scene> lockForcedScene;
+	private final Lazy<Scene> lockFailedScene;
+
+	@Inject
+	public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy<Scene> lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy<Scene> lockFailedScene) {
+		this.lockWindow = lockWindow;
+		this.vault = vault;
+		this.forceLockDecisionLock = forceLockDecisionLock;
+		this.lockForcedScene = lockForcedScene;
+		this.lockFailedScene = lockFailedScene;
+	}
+
+	@Override
+	protected Void call() throws Volume.VolumeException, InterruptedException {
+		try {
+			vault.lock(false);
+		} catch (Volume.VolumeException e) {
+			LOG.debug("Regular lock of {} failed.", vault.getDisplayName(), e);
+			var decision = askUserForAction();
+			switch (decision) {
+				case FORCE -> vault.lock(true);
+				case CANCEL -> cancel(false);
+			}
+		}
+		return null;
+	}
+
+	private LockModule.ForceLockDecision askUserForAction() throws InterruptedException {
+		// show forcedLock dialogue ...
+		Platform.runLater(() -> {
+			lockWindow.setScene(lockForcedScene.get());
+			lockWindow.show();
+			Window owner = lockWindow.getOwner();
+			if (owner != null) {
+				lockWindow.setX(owner.getX() + (owner.getWidth() - lockWindow.getWidth()) / 2);
+				lockWindow.setY(owner.getY() + (owner.getHeight() - lockWindow.getHeight()) / 2);
+			} else {
+				lockWindow.centerOnScreen();
+			}
+		});
+		// ... and wait for answer
+		return forceLockDecisionLock.awaitInteraction();
+	}
+
+	@Override
+	protected void scheduled() {
+		vault.setState(VaultState.PROCESSING);
+	}
+
+	@Override
+	protected void succeeded() {
+		LOG.info("Lock of {} succeeded.", vault.getDisplayName());
+		vault.setState(VaultState.LOCKED);
+	}
+
+	@Override
+	protected void failed() {
+		LOG.warn("Failed to lock {}.", vault.getDisplayName());
+		vault.setState(VaultState.UNLOCKED);
+		lockWindow.setScene(lockFailedScene.get());
+		lockWindow.show();
+	}
+
+	@Override
+	protected void cancelled() {
+		LOG.debug("Lock of {} canceled.", vault.getDisplayName());
+		vault.setState(VaultState.UNLOCKED);
+	}
+
+}

+ 9 - 3
main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java

@@ -6,25 +6,32 @@ import com.google.common.cache.LoadingCache;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.common.VaultService;
+import org.cryptomator.ui.fxapp.FxApplication;
 import org.cryptomator.ui.stats.VaultStatisticsComponent;
 
 import javax.inject.Inject;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.ReadOnlyObjectProperty;
 import javafx.fxml.FXML;
+import javafx.stage.Stage;
+import java.util.Optional;
 
 @MainWindowScoped
 public class VaultDetailUnlockedController implements FxController {
 
 	private final ReadOnlyObjectProperty<Vault> vault;
+	private final FxApplication application;
 	private final VaultService vaultService;
+	private final Stage mainWindow;
 	private final LoadingCache<Vault, VaultStatisticsComponent> vaultStats;
 	private final VaultStatisticsComponent.Builder vaultStatsBuilder;
 
 	@Inject
-	public VaultDetailUnlockedController(ObjectProperty<Vault> vault, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder) {
+	public VaultDetailUnlockedController(ObjectProperty<Vault> vault, FxApplication application, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, @MainWindow Stage mainWindow) {
 		this.vault = vault;
+		this.application = application;
 		this.vaultService = vaultService;
+		this.mainWindow = mainWindow;
 		this.vaultStats = CacheBuilder.newBuilder().weakValues().build(CacheLoader.from(this::buildVaultStats));
 		this.vaultStatsBuilder = vaultStatsBuilder;
 	}
@@ -40,8 +47,7 @@ public class VaultDetailUnlockedController implements FxController {
 
 	@FXML
 	public void lock() {
-		vaultService.lock(vault.get(), false);
-		// TODO count lock attempts, and allow forced lock
+		application.startLockWorkflow(vault.get(), Optional.of(mainWindow));
 	}
 
 	@FXML

+ 1 - 1
main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java

@@ -108,7 +108,7 @@ class TrayMenuController {
 	}
 
 	private void lockVault(Vault vault) {
-		fxApplicationStarter.get(true).thenAccept(app -> app.getVaultService().lock(vault, false));
+		fxApplicationStarter.get(true).thenAccept(app -> app.startLockWorkflow(vault, Optional.empty()));
 	}
 
 	private void lockAllVaults(ActionEvent actionEvent) {

+ 37 - 0
main/ui/src/main/resources/fxml/lock_failed.fxml

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
+<?import org.cryptomator.ui.controls.FormattedLabel?>
+<?import javafx.geometry.Insets?>
+<?import javafx.scene.control.Button?>
+<?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?>
+<VBox xmlns:fx="http://javafx.com/fxml"
+	  xmlns="http://javafx.com/javafx"
+	  fx:controller="org.cryptomator.ui.lock.LockFailedController"
+	  minWidth="400"
+	  maxWidth="400"
+	  minHeight="145"
+	  spacing="12">
+	<padding>
+		<Insets topRightBottomLeft="12"/>
+	</padding>
+	<children>
+		<HBox spacing="12" alignment="CENTER_LEFT" VBox.vgrow="ALWAYS">
+			<StackPane alignment="CENTER" HBox.hgrow="NEVER">
+				<Circle styleClass="glyph-icon-red" radius="24"/>
+				<FontAwesome5IconView styleClass="glyph-icon-white" glyph="TIMES" glyphSize="24"/>
+			</StackPane>
+			<VBox spacing="6">
+				<Label styleClass="label-large" text="%lock.fail.heading"/>
+				<FormattedLabel format="%lock.fail.message" arg1="${controller.vaultName}" wrapText="true"/>
+			</VBox>
+		</HBox>
+		<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
+			<Button text="OK" defaultButton="false" VBox.vgrow="ALWAYS" cancelButton="true" onAction="#close"/>
+		</VBox>
+	</children>
+</VBox>

+ 45 - 0
main/ui/src/main/resources/fxml/lock_forced.fxml

@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
+<?import org.cryptomator.ui.controls.FormattedLabel?>
+<?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?>
+<VBox xmlns:fx="http://javafx.com/fxml"
+	  xmlns="http://javafx.com/javafx"
+	  fx:controller="org.cryptomator.ui.lock.LockForcedController"
+	  minWidth="400"
+	  maxWidth="400"
+	  minHeight="145"
+	  spacing="12">
+	<padding>
+		<Insets topRightBottomLeft="12"/>
+	</padding>
+	<children>
+		<HBox spacing="12" alignment="CENTER_LEFT" VBox.vgrow="ALWAYS">
+			<StackPane alignment="CENTER" HBox.hgrow="NEVER">
+				<Circle styleClass="glyph-icon-orange" radius="24"/>
+				<FontAwesome5IconView styleClass="glyph-icon-white" glyph="EXCLAMATION" glyphSize="24"/>
+			</StackPane>
+			<VBox spacing="6">
+				<Label styleClass="label-large" text="%lock.forced.heading"/>
+				<FormattedLabel format="%lock.forced.message" arg1="${controller.vaultName}" wrapText="true"/>
+			</VBox>
+		</HBox>
+
+		<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
+			<ButtonBar buttonMinWidth="120" buttonOrder="+CI">
+				<buttons>
+					<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" defaultButton="true" cancelButton="true" onAction="#cancel"/>
+					<!-- TODO: third button with retry? -->
+					<Button text="%lock.forced.confirmBtn" ButtonBar.buttonData="FINISH" onAction="#confirmForcedLock"/>
+				</buttons>
+			</ButtonBar>
+		</VBox>
+	</children>
+</VBox>

+ 9 - 0
main/ui/src/main/resources/i18n/strings.properties

@@ -108,6 +108,15 @@ unlock.error.heading=Unable to unlock vault
 unlock.error.invalidMountPoint.notExisting=Mount point "%s" is not a directory, not empty or does not exist.
 unlock.error.invalidMountPoint.existing=Mount point "%s" already exists or parent folder is missing.
 
+# Lock
+## Force
+lock.forced.heading=Forcefully lock vault?
+lock.forced.message=The vault "%s" cannot be locked due to open files or pending operations. You can enforce locking, but unsaved data will be lost and pending read/write operations aborted.
+lock.forced.confirmBtn=Force locking
+## Failure
+lock.fail.heading=Locking vault failed.
+lock.fail.message=Vault "%s" could not be locked. Ensure unsaved work is saved elsewhere and important Read/Write operations are finished. In order to close the vault, kill the Cryptomator process.
+
 # Migration
 migration.title=Upgrade Vault
 ## Start