/*******************************************************************************
* Copyright (c) 2014, 2017 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Jean-Noël Charon - confirmation dialog on vault removal
******************************************************************************/
package org.cryptomator.ui.controllers;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.ui.ExitUtil;
import org.cryptomator.ui.controls.DirectoryListCell;
import org.cryptomator.ui.l10n.Localization;
import org.cryptomator.ui.model.AutoUnlocker;
import org.cryptomator.ui.model.UpgradeStrategies;
import org.cryptomator.ui.model.UpgradeStrategy;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.model.VaultFactory;
import org.cryptomator.ui.model.VaultList;
import org.cryptomator.ui.util.DialogBuilderUtil;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import org.fxmisc.easybind.monadic.MonadicBinding;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.geometry.Side;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ToggleButton;
import javafx.scene.image.Image;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.text.Font;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
@Singleton
public class MainController implements ViewController {
private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
private static final String ACTIVE_WINDOW_STYLE_CLASS = "active-window";
private static final String INACTIVE_WINDOW_STYLE_CLASS = "inactive-window";
private final Stage mainWindow;
private final ExitUtil exitUtil;
private final Localization localization;
private final ExecutorService executorService;
private final BlockingQueue<Path> fileOpenRequests;
private final VaultFactory vaultFactoy;
private final ViewControllerLoader viewControllerLoader;
private final ObjectProperty<ViewController> activeController = new SimpleObjectProperty<>();
private final ObservableList<Vault> vaults;
private final BooleanBinding areAllVaultsLocked;
private final ObjectProperty<Vault> selectedVault = new SimpleObjectProperty<>();
private final BooleanExpression isSelectedVaultUnlocked = BooleanExpression.booleanExpression(EasyBind.select(selectedVault).selectObject(Vault::unlockedProperty).orElse(false));
private final BooleanExpression isSelectedVaultValid = BooleanExpression.booleanExpression(EasyBind.monadic(selectedVault).map(Vault::isValidVaultDirectory).orElse(false));
private final BooleanExpression canEditSelectedVault = selectedVault.isNotNull().and(isSelectedVaultUnlocked.not());
private final MonadicBinding<UpgradeStrategy> upgradeStrategyForSelectedVault;
private final BooleanBinding isShowingSettings;
private final Map<Vault, UnlockedController> unlockedVaults = new HashMap<>();
private Subscription subs = Subscription.EMPTY;
@Inject
public MainController(@Named("mainWindow") Stage mainWindow, ExecutorService executorService, @Named("fileOpenRequests") BlockingQueue<Path> fileOpenRequests, ExitUtil exitUtil, Localization localization,
VaultFactory vaultFactoy, ViewControllerLoader viewControllerLoader, UpgradeStrategies upgradeStrategies, VaultList vaults, AutoUnlocker autoUnlocker) {
this.mainWindow = mainWindow;
this.executorService = executorService;
this.fileOpenRequests = fileOpenRequests;
this.exitUtil = exitUtil;
this.localization = localization;
this.vaultFactoy = vaultFactoy;
this.viewControllerLoader = viewControllerLoader;
this.vaults = vaults;
// derived bindings:
this.isShowingSettings = Bindings.equal(SettingsController.class, EasyBind.monadic(activeController).map(ViewController::getClass));
this.upgradeStrategyForSelectedVault = EasyBind.monadic(selectedVault).map(upgradeStrategies::getUpgradeStrategy);
this.areAllVaultsLocked = Bindings.isEmpty(FXCollections.observableList(vaults, Vault::observables).filtered(Vault::isUnlocked));
EasyBind.subscribe(areAllVaultsLocked, Platform::setImplicitExit);
autoUnlocker.unlockAllSilently();
}
@FXML
private ContextMenu vaultListCellContextMenu;
@FXML
private MenuItem changePasswordMenuItem;
@FXML
private ContextMenu addVaultContextMenu;
@FXML
private HBox root;
@FXML
private ListView<Vault> vaultList;
@FXML
private ToggleButton addVaultButton;
@FXML
private Button removeVaultButton;
@FXML
private ToggleButton settingsButton;
@FXML
private Pane contentPane;
@FXML
private Pane emptyListInstructions;
@Override
public void initialize() {
vaultList.setItems(vaults);
vaultList.setCellFactory(this::createDirecoryListCell);
activeController.set(viewControllerLoader.load("/fxml/welcome.fxml"));
selectedVault.bind(vaultList.getSelectionModel().selectedItemProperty());
removeVaultButton.disableProperty().bind(canEditSelectedVault.not());
emptyListInstructions.visibleProperty().bind(Bindings.isEmpty(vaults));
changePasswordMenuItem.visibleProperty().bind(isSelectedVaultValid.and(Bindings.isNull(upgradeStrategyForSelectedVault)));
subs = subs.and(EasyBind.subscribe(selectedVault, this::selectedVaultDidChange));
subs = subs.and(EasyBind.subscribe(activeController, this::activeControllerDidChange));
subs = subs.and(EasyBind.subscribe(isShowingSettings, settingsButton::setSelected));
subs = subs.and(EasyBind.subscribe(addVaultContextMenu.showingProperty(), addVaultButton::setSelected));
}
@Override
public Parent getRoot() {
return root;
}
public void initStage(Stage stage) {
stage.setScene(new Scene(getRoot()));
stage.sizeToScene();
stage.titleProperty().bind(windowTitle());
stage.setResizable(false);
loadFont("/css/ionicons.ttf");
loadFont("/css/fontawesome-webfont.ttf");
if (SystemUtils.IS_OS_MAC_OSX) {
subs = subs.and(EasyBind.includeWhen(mainWindow.getScene().getRoot().getStyleClass(), ACTIVE_WINDOW_STYLE_CLASS, mainWindow.focusedProperty()));
subs = subs.and(EasyBind.includeWhen(mainWindow.getScene().getRoot().getStyleClass(), INACTIVE_WINDOW_STYLE_CLASS, mainWindow.focusedProperty().not()));
Application.setUserAgentStylesheet(getClass().getResource("/css/mac_theme.css").toString());
} else if (SystemUtils.IS_OS_LINUX) {
Application.setUserAgentStylesheet(getClass().getResource("/css/linux_theme.css").toString());
} else if (SystemUtils.IS_OS_WINDOWS) {
stage.getIcons().add(new Image(getClass().getResourceAsStream("/window_icon.png")));
Application.setUserAgentStylesheet(getClass().getResource("/css/win_theme.css").toString());
}
exitUtil.initExitHandler(this::gracefulShutdown);
listenToFileOpenRequests(stage);
}
private void gracefulShutdown() {
vaults.filtered(Vault::isUnlocked).forEach(Vault::prepareForShutdown);
Platform.runLater(Platform::exit);
}
private void loadFont(String resourcePath) {
try (InputStream in = getClass().getResourceAsStream(resourcePath)) {
Font.loadFont(in, 12.0);
} catch (IOException e) {
LOG.warn("Error loading font from path: " + resourcePath, e);
}
}
private void listenToFileOpenRequests(Stage stage) {
executorService.submit(() -> {
while (!Thread.interrupted()) {
try {
final Path path = fileOpenRequests.take();
Platform.runLater(() -> {
addVault(path, true);
stage.setIconified(false);
stage.show();
stage.toFront();
stage.requestFocus();
});
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
}
private ListCell<Vault> createDirecoryListCell(ListView<Vault> param) {
final DirectoryListCell cell = new DirectoryListCell();
cell.setVaultContextMenu(vaultListCellContextMenu);
return cell;
}
// ****************************************
// UI Events
// ****************************************
@FXML
private void didClickAddVault(ActionEvent event) {
if (addVaultContextMenu.isShowing()) {
addVaultContextMenu.hide();
} else {
addVaultContextMenu.show(addVaultButton, Side.BOTTOM, 0.0, 0.0);
}
}
@FXML
private void didClickCreateNewVault(ActionEvent event) {
final FileChooser fileChooser = new FileChooser();
final File file = fileChooser.showSaveDialog(mainWindow);
if (file == null) {
return;
}
try {
final Path vaultDir = file.toPath();
if (!Files.exists(vaultDir)) {
Files.createDirectory(vaultDir);
}
addVault(vaultDir, true);
} catch (IOException e) {
LOG.error("Unable to create vault", e);
}
}
@FXML
private void didClickAddExistingVaults(ActionEvent event) {
final FileChooser fileChooser = new FileChooser();
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
final List<File> files = fileChooser.showOpenMultipleDialog(mainWindow);
if (files != null) {
for (final File file : files) {
addVault(file.toPath(), true);
}
}
}
/**
* adds the given directory or selects it if it is already in the list of directories.
*
* @param path to a vault directory or masterkey file
*/
public void addVault(final Path path, boolean select) {
final Path vaultPath;
if (path != null && Files.isDirectory(path)) {
vaultPath = path;
} else if (path != null && Files.isRegularFile(path)) {
vaultPath = path.getParent();
} else {
LOG.warn("Ignoring attempt to add vault with invalid path: {}", path);
return;
}
final Vault vault = vaults.stream().filter(v -> v.getPath().equals(vaultPath)).findAny().orElseGet(() -> {
VaultSettings vaultSettings = VaultSettings.withRandomId();
vaultSettings.path().set(vaultPath);
return vaultFactoy.get(vaultSettings);
});
if (!vaults.contains(vault)) {
vaults.add(vault);
}
if (select) {
vaultList.getSelectionModel().select(vault);
}
}
@FXML
private void didClickRemoveSelectedEntry(ActionEvent e) {
Alert confirmDialog = DialogBuilderUtil.buildConfirmationDialog( //
localization.getString("main.directoryList.remove.confirmation.title"), //
localization.getString("main.directoryList.remove.confirmation.header"), //
localization.getString("main.directoryList.remove.confirmation.content"), //
SystemUtils.IS_OS_MAC_OSX ? ButtonType.CANCEL : ButtonType.OK);
Optional<ButtonType> choice = confirmDialog.showAndWait();
if (ButtonType.OK.equals(choice.get())) {
vaults.remove(selectedVault.get());
if (vaults.isEmpty()) {
activeController.set(viewControllerLoader.load("/fxml/welcome.fxml"));
}
}
}
@FXML
private void didClickChangePassword(ActionEvent e) {
showChangePasswordView();
}
@FXML
private void didClickShowSettings(ActionEvent e) {
if (isShowingSettings.get()) {
activeController.set(viewControllerLoader.load("/fxml/welcome.fxml"));
} else {
activeController.set(viewControllerLoader.load("/fxml/settings.fxml"));
}
vaultList.getSelectionModel().clearSelection();
}
// ****************************************
// Binding Listeners
// ****************************************
private void activeControllerDidChange(ViewController newValue) {
final Parent root = newValue.getRoot();
contentPane.getChildren().clear();
contentPane.getChildren().add(root);
}
private void selectedVaultDidChange(Vault newValue) {
if (newValue == null) {
return;
}
if (newValue.isUnlocked()) {
this.showUnlockedView(newValue);
} else if (!newValue.doesVaultDirectoryExist()) {
this.showNotFoundView();
} else if (newValue.isValidVaultDirectory() && upgradeStrategyForSelectedVault.isPresent()) {
this.showUpgradeView();
} else if (newValue.isValidVaultDirectory()) {
this.showUnlockView();
} else {
this.showInitializeView();
}
}
// ****************************************
// Public Bindings
// ****************************************
public Binding<String> windowTitle() {
return EasyBind.monadic(selectedVault).flatMap(Vault::name).orElse(localization.getString("app.name"));
}
// ****************************************
// Subcontroller for right panel
// ****************************************
private void showNotFoundView() {
activeController.set(viewControllerLoader.load("/fxml/notfound.fxml"));
}
private void showInitializeView() {
final InitializeController ctrl = viewControllerLoader.load("/fxml/initialize.fxml");
ctrl.setVault(selectedVault.get());
ctrl.setListener(this::didInitialize);
activeController.set(ctrl);
}
public void didInitialize() {
showUnlockView();
}
private void showUpgradeView() {
final UpgradeController ctrl = viewControllerLoader.load("/fxml/upgrade.fxml");
ctrl.setVault(selectedVault.get());
ctrl.setListener(this::didUpgrade);
activeController.set(ctrl);
}
public void didUpgrade() {
showUnlockView();
}
private void showUnlockView() {
final UnlockController ctrl = viewControllerLoader.load("/fxml/unlock.fxml");
ctrl.setVault(selectedVault.get());
ctrl.setListener(this::didUnlock);
activeController.set(ctrl);
}
public void didUnlock(Vault vault) {
if (vault.equals(selectedVault.getValue())) {
this.showUnlockedView(vault);
}
}
private void showUnlockedView(Vault vault) {
final UnlockedController ctrl = unlockedVaults.computeIfAbsent(vault, k -> {
return viewControllerLoader.load("/fxml/unlocked.fxml");
});
ctrl.setVault(vault);
ctrl.setListener(this::didLock);
activeController.set(ctrl);
}
public void didLock(UnlockedController ctrl) {
unlockedVaults.remove(ctrl.getVault());
showUnlockView();
}
private void showChangePasswordView() {
final ChangePasswordController ctrl = viewControllerLoader.load("/fxml/change_password.fxml");
ctrl.setVault(selectedVault.get());
ctrl.setListener(this::didChangePassword);
activeController.set(ctrl);
}
public void didChangePassword() {
showUnlockView();
}
}