~exprez135/cryptomator-libre

c48c9b1568c1ba86b3527e9c646ea2277416f640 — Armin Schrenk 9 months ago 3b73544 + 6da7eae
Merge pull request #1416 from cryptomator/feature/#1228-forcedUnmountDialog

Feature/#1228 forced unmount dialog
M main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java => main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java +2 -0
@@ 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"),

M main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java => main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java +1 -0
@@ 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;
	}


M main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java => main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java +11 -1
@@ 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);

M main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java => main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java +2 -1
@@ 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

A main/ui/src/main/java/org/cryptomator/ui/lock/LockComponent.java => main/ui/src/main/java/org/cryptomator/ui/lock/LockComponent.java +39 -0
@@ 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();
	}
}

A main/ui/src/main/java/org/cryptomator/ui/lock/LockFailedController.java => main/ui/src/main/java/org/cryptomator/ui/lock/LockFailedController.java +31 -0
@@ 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();
	}
}

A main/ui/src/main/java/org/cryptomator/ui/lock/LockForcedController.java => main/ui/src/main/java/org/cryptomator/ui/lock/LockForcedController.java +57 -0
@@ 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();
	}

}

A main/ui/src/main/java/org/cryptomator/ui/lock/LockModule.java => main/ui/src/main/java/org/cryptomator/ui/lock/LockModule.java +89 -0
@@ 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);

}

A main/ui/src/main/java/org/cryptomator/ui/lock/LockScoped.java => main/ui/src/main/java/org/cryptomator/ui/lock/LockScoped.java +13 -0
@@ 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 {

}

A main/ui/src/main/java/org/cryptomator/ui/lock/LockWindow.java => main/ui/src/main/java/org/cryptomator/ui/lock/LockWindow.java +14 -0
@@ 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 {

}

A main/ui/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java => main/ui/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java +105 -0
@@ 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);
	}

}

M main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java => main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java +9 -3
@@ 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

M main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java => main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java +1 -1
@@ 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) {

A main/ui/src/main/resources/fxml/lock_failed.fxml => main/ui/src/main/resources/fxml/lock_failed.fxml +37 -0
@@ 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>

A main/ui/src/main/resources/fxml/lock_forced.fxml => main/ui/src/main/resources/fxml/lock_forced.fxml +45 -0
@@ 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>

M main/ui/src/main/resources/i18n/strings.properties => main/ui/src/main/resources/i18n/strings.properties +9 -0
@@ 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