diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXWebDavMounter.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXWebDavMounter.java index e7f3d5e2d..981d2f6f3 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXWebDavMounter.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXWebDavMounter.java @@ -72,8 +72,6 @@ final class MacOsXWebDavMounter implements WebDavMounterStrategy { String activateAppleScript = String.format("tell application \"Finder\" to activate \"%s\"", volumeIdentifier); String ejectAppleScript = String.format("tell application \"Finder\" to if \"%s\" exists then eject \"%s\"", volumeIdentifier, volumeIdentifier); - System.err.println("open: " + openAppleScript + "\nactivate: " + activateAppleScript + "\neject: " + ejectAppleScript); - this.revealCommand = new ProcessBuilder("/usr/bin/osascript", "-e", openAppleScript, "-e", activateAppleScript); this.unmountCommand = new ProcessBuilder("/usr/bin/osascript", "-e", ejectAppleScript); } diff --git a/main/pom.xml b/main/pom.xml index 5f52f26cc..3b770293b 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -170,6 +170,13 @@ commons-httpclient ${commons-httpclient.version} + + + + org.fxmisc.easybind + easybind + 1.0.3 + diff --git a/main/ui/pom.xml b/main/ui/pom.xml index b04a80729..e2b0b99b0 100644 --- a/main/ui/pom.xml +++ b/main/ui/pom.xml @@ -50,6 +50,12 @@ org.cryptomator frontend-webdav + + + + org.fxmisc.easybind + easybind + diff --git a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java index 32883b5a1..bed077a24 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java +++ b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java @@ -70,7 +70,7 @@ public class MainApplication extends Application { mainCtrl.initStage(primaryStage); final ResourceBundle rb = ResourceBundle.getBundle("localization"); - primaryStage.setTitle(rb.getString("app.name")); + primaryStage.titleProperty().bind(mainCtrl.windowTitle()); primaryStage.setResizable(false); primaryStage.getIcons().add(new Image(MainApplication.class.getResourceAsStream("/window_icon.png"))); primaryStage.show(); diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/AbstractFXMLViewController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/AbstractFXMLViewController.java index d74c64610..91de748ef 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/AbstractFXMLViewController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/AbstractFXMLViewController.java @@ -11,6 +11,9 @@ package org.cryptomator.ui.controllers; import java.io.IOException; import java.net.URL; import java.util.ResourceBundle; +import java.util.concurrent.atomic.AtomicReference; + +import org.cryptomator.common.LazyInitializer; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; @@ -23,7 +26,7 @@ import javafx.stage.Stage; */ abstract class AbstractFXMLViewController implements Initializable { - private Parent fxmlRoot; + private final AtomicReference fxmlRoot = new AtomicReference<>(); /** * URL from #initialize(URL, ResourceBundle) @@ -80,16 +83,15 @@ abstract class AbstractFXMLViewController implements Initializable { * * @return Parent view element. */ - protected final synchronized Parent loadFxml() { - if (fxmlRoot == null) { + protected final Parent loadFxml() { + return LazyInitializer.initializeLazily(fxmlRoot, () -> { final FXMLLoader loader = createFxmlLoader(); try { - fxmlRoot = loader.load(); + return loader.load(); } catch (IOException e) { throw new IllegalStateException("Could not load FXML file from location: " + loader.getLocation(), e); } - } - return fxmlRoot; + }); } /** diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java index 7dbdb5b0b..5d9367702 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java @@ -9,6 +9,7 @@ package org.cryptomator.ui.controllers; import java.io.IOException; +import java.io.UncheckedIOException; import java.net.URL; import java.util.Optional; import java.util.ResourceBundle; @@ -17,6 +18,7 @@ import javax.inject.Inject; import javax.inject.Singleton; import org.cryptomator.crypto.engine.InvalidPassphraseException; +import org.cryptomator.crypto.engine.UnsupportedVaultFormatException; import org.cryptomator.ui.controls.SecPasswordField; import org.cryptomator.ui.model.Vault; import org.slf4j.Logger; @@ -24,7 +26,9 @@ import org.slf4j.LoggerFactory; import javafx.application.Application; import javafx.application.Platform; -import javafx.beans.value.ObservableValue; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Button; @@ -36,8 +40,14 @@ public class ChangePasswordController extends AbstractFXMLViewController { private static final Logger LOG = LoggerFactory.getLogger(ChangePasswordController.class); + private final Application app; + final ObjectProperty vault = new SimpleObjectProperty<>(); private Optional listener = Optional.empty(); - private Vault vault; + + @Inject + public ChangePasswordController(Application app) { + this.app = app; + } @FXML private SecPasswordField oldPasswordField; @@ -57,12 +67,12 @@ public class ChangePasswordController extends AbstractFXMLViewController { @FXML private Hyperlink downloadsPageLink; - private final Application app; - - @Inject - public ChangePasswordController(Application app) { - super(); - this.app = app; + @Override + public void initialize() { + BooleanBinding oldPasswordIsEmpty = oldPasswordField.textProperty().isEmpty(); + BooleanBinding newPasswordIsEmpty = newPasswordField.textProperty().isEmpty(); + BooleanBinding passwordsDiffer = newPasswordField.textProperty().isNotEqualTo(retypePasswordField.textProperty()); + changePasswordButton.disableProperty().bind(oldPasswordIsEmpty.or(newPasswordIsEmpty.or(passwordsDiffer))); } @Override @@ -75,24 +85,6 @@ public class ChangePasswordController extends AbstractFXMLViewController { return ResourceBundle.getBundle("localization"); } - @Override - public void initialize() { - oldPasswordField.textProperty().addListener(this::passwordFieldsDidChange); - newPasswordField.textProperty().addListener(this::passwordFieldsDidChange); - retypePasswordField.textProperty().addListener(this::passwordFieldsDidChange); - } - - // **************************************** - // Password fields - // **************************************** - - private void passwordFieldsDidChange(ObservableValue property, String oldValue, String newValue) { - boolean oldPasswordIsEmpty = oldPasswordField.getText().isEmpty(); - boolean newPasswordIsEmpty = newPasswordField.getText().isEmpty(); - boolean passwordsAreEqual = newPasswordField.getText().equals(retypePasswordField.getText()); - changePasswordButton.setDisable(oldPasswordIsEmpty || newPasswordIsEmpty || !passwordsAreEqual); - } - // **************************************** // Downloads link // **************************************** @@ -110,31 +102,23 @@ public class ChangePasswordController extends AbstractFXMLViewController { private void didClickChangePasswordButton(ActionEvent event) { downloadsPageLink.setVisible(false); try { - vault.changePassphrase(oldPasswordField.getCharacters(), newPasswordField.getCharacters()); + vault.get().changePassphrase(oldPasswordField.getCharacters(), newPasswordField.getCharacters()); messageText.setText(resourceBundle.getString("changePassword.infoMessage.success")); listener.ifPresent(this::invokeListenerLater); } catch (InvalidPassphraseException e) { messageText.setText(resourceBundle.getString("changePassword.errorMessage.wrongPassword")); - newPasswordField.swipe(); - retypePasswordField.swipe(); Platform.runLater(oldPasswordField::requestFocus); - return; - } catch (IOException ex) { + } catch (UncheckedIOException | IOException ex) { messageText.setText(resourceBundle.getString("changePassword.errorMessage.decryptionFailed")); LOG.error("Decryption failed for technical reasons.", ex); - newPasswordField.swipe(); - retypePasswordField.swipe(); - return; - // } catch (UnsupportedVaultException e) { - // downloadsPageLink.setVisible(true); - // if (e.isVaultOlderThanSoftware()) { - // messageText.setText(resourceBundle.getString("changePassword.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " "); - // } else if (e.isSoftwareOlderThanVault()) { - // messageText.setText(resourceBundle.getString("changePassword.errorMessage.unsupportedVersion.softwareOlderThanVault") + " "); - // } - // newPasswordField.swipe(); - // retypePasswordField.swipe(); - // return; + } catch (UnsupportedVaultFormatException e) { + downloadsPageLink.setVisible(true); + LOG.warn("Unable to unlock vault: " + e.getMessage()); + if (e.isVaultOlderThanSoftware()) { + messageText.setText(resourceBundle.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " "); + } else if (e.isSoftwareOlderThanVault()) { + messageText.setText(resourceBundle.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " "); + } } finally { oldPasswordField.swipe(); newPasswordField.swipe(); @@ -144,14 +128,6 @@ public class ChangePasswordController extends AbstractFXMLViewController { /* Getter/Setter */ - public Vault getVault() { - return vault; - } - - public void setVault(Vault vault) { - this.vault = vault; - } - public ChangePasswordListener getListener() { return listener.orElse(null); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java index 9a256b944..4a47b9d36 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java @@ -9,6 +9,7 @@ package org.cryptomator.ui.controllers; import java.io.IOException; +import java.io.UncheckedIOException; import java.net.URL; import java.nio.file.FileAlreadyExistsException; import java.util.Optional; @@ -23,7 +24,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javafx.application.Platform; -import javafx.beans.value.ObservableValue; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Button; @@ -34,9 +37,13 @@ public class InitializeController extends AbstractFXMLViewController { private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class); - private Vault vault; + final ObjectProperty vault = new SimpleObjectProperty<>(); private Optional listener = Optional.empty(); + @Inject + public InitializeController() { + } + @FXML private SecPasswordField passwordField; @@ -49,8 +56,11 @@ public class InitializeController extends AbstractFXMLViewController { @FXML private Label messageLabel; - @Inject - public InitializeController() { + @Override + public void initialize() { + BooleanBinding passwordIsEmpty = passwordField.textProperty().isEmpty(); + BooleanBinding passwordsDiffer = passwordField.textProperty().isNotEqualTo(retypePasswordField.textProperty()); + okButton.disableProperty().bind(passwordIsEmpty.or(passwordsDiffer)); } @Override @@ -63,60 +73,29 @@ public class InitializeController extends AbstractFXMLViewController { return ResourceBundle.getBundle("localization"); } - @Override - public void initialize() { - passwordField.textProperty().addListener(this::passwordFieldsDidChange); - retypePasswordField.textProperty().addListener(this::passwordFieldsDidChange); - } - - // **************************************** - // Password fields - // **************************************** - - private void passwordFieldsDidChange(ObservableValue property, String oldValue, String newValue) { - boolean passwordIsEmpty = passwordField.getText().isEmpty(); - boolean passwordsAreEqual = passwordField.getText().equals(retypePasswordField.getText()); - okButton.setDisable(passwordIsEmpty || !passwordsAreEqual); - } - // **************************************** // OK button // **************************************** @FXML protected void initializeVault(ActionEvent event) { - setControlsDisabled(true); final CharSequence passphrase = passwordField.getCharacters(); try { - vault.create(passphrase); + vault.get().create(passphrase); listener.ifPresent(this::invokeListenerLater); } catch (FileAlreadyExistsException ex) { messageLabel.setText(resourceBundle.getString("initialize.messageLabel.alreadyInitialized")); - } catch (IOException ex) { + } catch (UncheckedIOException | IOException ex) { LOG.error("I/O Exception", ex); + messageLabel.setText(resourceBundle.getString("initialize.messageLabel.initializationFailed")); } finally { - setControlsDisabled(false); passwordField.swipe(); retypePasswordField.swipe(); } } - private void setControlsDisabled(boolean disable) { - passwordField.setDisable(disable); - retypePasswordField.setDisable(disable); - okButton.setDisable(disable); - } - /* Getter/Setter */ - public Vault getVault() { - return vault; - } - - public void setVault(Vault vault) { - this.vault = vault; - } - public InitializationListener getListener() { return listener.orElse(null); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/MacWarningsController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/MacWarningsController.java index 6af12e497..ed2148881 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/MacWarningsController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/MacWarningsController.java @@ -19,8 +19,10 @@ import org.cryptomator.ui.model.Vault; import javafx.application.Application; import javafx.beans.Observable; import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyStringWrapper; import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.beans.value.WeakChangeListener; @@ -38,33 +40,23 @@ import javafx.util.StringConverter; public class MacWarningsController extends AbstractFXMLViewController { - @FXML - private ListView warningsList; - - @FXML - private Button whitelistButton; - private final Application application; private final ObservableList warnings = FXCollections.observableArrayList(); private final ListChangeListener unauthenticatedResourcesChangeListener = this::unauthenticatedResourcesDidChange; private final ChangeListener stageVisibilityChangeListener = this::windowVisibilityDidChange; + final ObjectProperty vault = new SimpleObjectProperty<>(); private Stage stage; - private Vault vault; @Inject public MacWarningsController(Application application) { this.application = application; } - @Override - protected URL getFxmlResourceUrl() { - return getClass().getResource("/fxml/mac_warnings.fxml"); - } + @FXML + private ListView warningsList; - @Override - protected ResourceBundle getFxmlResourceBundle() { - return ResourceBundle.getBundle("localization"); - } + @FXML + private Button whitelistButton; @Override public void initialize() { @@ -85,6 +77,16 @@ public class MacWarningsController extends AbstractFXMLViewController { })); } + @Override + protected URL getFxmlResourceUrl() { + return getClass().getResource("/fxml/mac_warnings.fxml"); + } + + @Override + protected ResourceBundle getFxmlResourceBundle() { + return ResourceBundle.getBundle("localization"); + } + @Override public void initStage(Stage stage) { super.initStage(stage); @@ -96,8 +98,8 @@ public class MacWarningsController extends AbstractFXMLViewController { private void didClickWhitelistButton(ActionEvent event) { warnings.filtered(w -> w.isSelected()).stream().forEach(w -> { final String resourceToBeWhitelisted = w.getName(); - vault.getWhitelistedResourcesWithInvalidMac().add(resourceToBeWhitelisted); - vault.getNamesOfResourcesWithInvalidMac().remove(resourceToBeWhitelisted); + vault.get().getWhitelistedResourcesWithInvalidMac().add(resourceToBeWhitelisted); + vault.get().getNamesOfResourcesWithInvalidMac().remove(resourceToBeWhitelisted); }); warnings.removeIf(w -> w.isSelected()); } @@ -125,12 +127,12 @@ public class MacWarningsController extends AbstractFXMLViewController { private void windowVisibilityDidChange(ObservableValue observable, Boolean oldValue, Boolean newValue) { if (Boolean.TRUE.equals(newValue)) { - stage.setTitle(String.format(resourceBundle.getString("macWarnings.windowTitle"), vault.getName())); - warnings.addAll(vault.getNamesOfResourcesWithInvalidMac().stream().map(Warning::new).collect(Collectors.toList())); - vault.getNamesOfResourcesWithInvalidMac().addListener(this.unauthenticatedResourcesChangeListener); + stage.setTitle(String.format(resourceBundle.getString("macWarnings.windowTitle"), vault.get().getName())); + warnings.addAll(vault.get().getNamesOfResourcesWithInvalidMac().stream().map(Warning::new).collect(Collectors.toList())); + vault.get().getNamesOfResourcesWithInvalidMac().addListener(this.unauthenticatedResourcesChangeListener); } else { - vault.getNamesOfResourcesWithInvalidMac().clear(); - vault.getNamesOfResourcesWithInvalidMac().removeListener(this.unauthenticatedResourcesChangeListener); + vault.get().getNamesOfResourcesWithInvalidMac().clear(); + vault.get().getNamesOfResourcesWithInvalidMac().removeListener(this.unauthenticatedResourcesChangeListener); } } @@ -138,10 +140,6 @@ public class MacWarningsController extends AbstractFXMLViewController { whitelistButton.setDisable(warnings.filtered(w -> w.isSelected()).isEmpty()); } - public void setVault(Vault vault) { - this.vault = vault; - } - private class Warning { private final ReadOnlyStringWrapper name = new ReadOnlyStringWrapper(); diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java index e468c0519..85f6eb55f 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java @@ -13,10 +13,10 @@ import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.ResourceBundle; -import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Provider; @@ -26,13 +26,19 @@ import org.cryptomator.ui.controls.DirectoryListCell; import org.cryptomator.ui.model.Vault; import org.cryptomator.ui.model.VaultFactory; import org.cryptomator.ui.settings.Settings; +import org.cryptomator.ui.util.Listeners; +import org.fxmisc.easybind.EasyBind; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import dagger.Lazy; import javafx.application.Platform; +import javafx.beans.binding.Binding; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; -import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; @@ -47,13 +53,42 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.stage.FileChooser; import javafx.stage.Stage; -import javafx.stage.WindowEvent; @Singleton public class MainController extends AbstractFXMLViewController { private static final Logger LOG = LoggerFactory.getLogger(MainController.class); + private final VaultFactory vaultFactoy; + private final Lazy welcomeController; + private final Lazy initializeController; + private final Lazy unlockController; + private final Provider unlockedControllerProvider; + private final Lazy changePasswordController; + private final Lazy settingsController; + private final ObjectProperty activeController = new SimpleObjectProperty<>(); + private final ObservableList vaults; + private final ObjectProperty selectedVault = new SimpleObjectProperty<>(); + private final Binding isSelectedVaultUnlocked = EasyBind.select(selectedVault).selectObject(Vault::unlockedProperty); + private final BooleanBinding isShowingSettings; + private final Map unlockedVaults = new HashMap<>(); + + @Inject + public MainController(Settings settings, VaultFactory vaultFactoy, Lazy welcomeController, Lazy initializeController, Lazy unlockController, + Provider unlockedControllerProvider, Lazy changePasswordController, Lazy settingsController) { + this.vaultFactoy = vaultFactoy; + this.welcomeController = welcomeController; + this.initializeController = initializeController; + this.unlockController = unlockController; + this.unlockedControllerProvider = unlockedControllerProvider; + this.changePasswordController = changePasswordController; + this.settingsController = settingsController; + this.vaults = FXCollections.observableList(settings.getDirectories()); + + // derived bindings: + this.isShowingSettings = activeController.isEqualTo(settingsController.get()); + } + private Stage stage; @FXML @@ -74,28 +109,24 @@ public class MainController extends AbstractFXMLViewController { @FXML private Button removeVaultButton; + @FXML + private ToggleButton settingsButton; + @FXML private Pane contentPane; - private final Settings settings; - private final VaultFactory vaultFactoy; - private final Lazy welcomeController; - private final Lazy initializeController; - private final Lazy unlockController; - private final Provider unlockedController; - private final Lazy changePasswordController; - - @Inject - public MainController(Settings settings, VaultFactory vaultFactoy, Lazy welcomeController, Lazy initializeController, Lazy unlockController, - Provider unlockedController, Lazy changePasswordController) { - super(); - this.settings = settings; - this.vaultFactoy = vaultFactoy; - this.welcomeController = welcomeController; - this.initializeController = initializeController; - this.unlockController = unlockController; - this.unlockedController = unlockedController; - this.changePasswordController = changePasswordController; + @Override + public void initialize() { + activeController.addListener(this::activeControllerDidChange); + activeController.set(welcomeController.get()); + vaultList.setItems(vaults); + vaultList.setCellFactory(this::createDirecoryListCell); + selectedVault.addListener(this::selectedVaultDidChange); + selectedVault.bind(vaultList.getSelectionModel().selectedItemProperty()); + addVaultContextMenu.showingProperty().addListener(Listeners.withNewValue(addVaultButton::setSelected)); + removeVaultButton.disableProperty().bind(selectedVault.isNull()); + isShowingSettings.addListener(Listeners.withNewValue(settingsButton::setSelected)); + isSelectedVaultUnlocked.addListener(Listeners.withNewValue(this::selectedVaultUnlockedDidChange)); } @Override @@ -108,14 +139,10 @@ public class MainController extends AbstractFXMLViewController { return ResourceBundle.getBundle("localization"); } - @Override - public void initialize() { - final ObservableList items = FXCollections.observableList(settings.getDirectories()); - vaultList.setItems(items); - vaultList.setCellFactory(this::createDirecoryListCell); - vaultList.getSelectionModel().getSelectedItems().addListener(this::selectedVaultDidChange); - removeVaultButton.disableProperty().bind(vaultList.getSelectionModel().selectedItemProperty().isNull()); - this.showWelcomeView(); + private ListCell createDirecoryListCell(ListView param) { + final DirectoryListCell cell = new DirectoryListCell(); + cell.setVaultContextMenu(vaultListCellContextMenu); + return cell; } @Override @@ -133,16 +160,6 @@ public class MainController extends AbstractFXMLViewController { } } - @FXML - private void willShowAddVaultContextMenu(WindowEvent event) { - addVaultButton.setSelected(true); - } - - @FXML - private void didHideAddVaultContextMenu(WindowEvent event) { - addVaultButton.setSelected(false); - } - @FXML private void didClickCreateNewVault(ActionEvent event) { final FileChooser fileChooser = new FileChooser(); @@ -201,143 +218,117 @@ public class MainController extends AbstractFXMLViewController { } final Vault vault = vaultFactoy.createVault(vaultPath); - if (!vaultList.getItems().contains(vault)) { - vaultList.getItems().add(vault); + if (!vaults.contains(vault)) { + vaults.add(vault); } vaultList.getSelectionModel().select(vault); } - private ListCell createDirecoryListCell(ListView param) { - final DirectoryListCell cell = new DirectoryListCell(); - cell.setVaultContextMenu(vaultListCellContextMenu); - return cell; - } - - private void selectedVaultDidChange(ListChangeListener.Change change) { - final Vault selectedVault = vaultList.getSelectionModel().getSelectedItem(); - if (selectedVault == null) { - stage.setTitle(resourceBundle.getString("app.name")); - showWelcomeView(); - } else if (!Files.isDirectory(selectedVault.getPath())) { - Platform.runLater(() -> { - vaultList.getItems().remove(selectedVault); - vaultList.getSelectionModel().clearSelection(); - }); - stage.setTitle(resourceBundle.getString("app.name")); - showWelcomeView(); - } else { - stage.setTitle(selectedVault.getName()); - showVault(selectedVault); - } - } - @FXML private void didClickRemoveSelectedEntry(ActionEvent e) { - final Vault selectedVault = vaultList.getSelectionModel().getSelectedItem(); - vaultList.getItems().remove(selectedVault); - vaultList.getSelectionModel().clearSelection(); + vaults.remove(selectedVault.get()); } @FXML private void didClickChangePassword(ActionEvent e) { - final Vault selectedVault = vaultList.getSelectionModel().getSelectedItem(); - showChangePasswordView(selectedVault); + showChangePasswordView(); + } + + @FXML + private void didClickShowSettings(ActionEvent e) { + if (settingsController.get().equals(activeController.get())) { + activeController.set(welcomeController.get()); + } else { + activeController.set(settingsController.get()); + } + vaultList.getSelectionModel().clearSelection(); + } + + // **************************************** + // Bindings and Property Listeners + // **************************************** + + private void activeControllerDidChange(ObservableValue property, AbstractFXMLViewController oldValue, AbstractFXMLViewController newValue) { + final Parent root = newValue.loadFxml(); + contentPane.getChildren().clear(); + contentPane.getChildren().add(root); + } + + private void selectedVaultDidChange(ObservableValue property, Vault oldValue, Vault newValue) { + if (newValue == null) { + return; + } + if (newValue.isUnlocked()) { + this.showUnlockedView(newValue); + } else if (newValue.isValidVaultDirectory()) { + this.showUnlockView(); + } else { + this.showInitializeView(); + } + } + + private void selectedVaultUnlockedDidChange(Boolean unlocked) { + if (unlocked == null) { + // no vault selected -> no-op + } else if (unlocked) { + Platform.setImplicitExit(false); + this.showUnlockedView(selectedVault.get()); + } else { + this.showUnlockView(); + } + } + + public Binding windowTitle() { + return EasyBind.monadic(selectedVault).map(Vault::getName).orElse(resourceBundle.getString("app.name")); } // **************************************** // Subcontroller for right panel // **************************************** - private void showVault(Vault vault) { - if (vault.isUnlocked()) { - this.showUnlockedView(vault); - } else if (vault.isValidVaultDirectory()) { - this.showUnlockView(vault); - } else { - this.showInitializeView(vault); - } - } - - private void showWelcomeView() { - final Parent root = welcomeController.get().loadFxml(); - contentPane.getChildren().clear(); - contentPane.getChildren().add(root); - } - - private void showInitializeView(Vault vault) { + private void showInitializeView() { final InitializeController ctrl = initializeController.get(); - final Parent root = ctrl.loadFxml(); - contentPane.getChildren().clear(); - contentPane.getChildren().add(root); - ctrl.setVault(vault); + ctrl.vault.bind(selectedVault); ctrl.setListener(this::didInitialize); + activeController.set(ctrl); } public void didInitialize(InitializeController ctrl) { - showUnlockView(ctrl.getVault()); + showUnlockView(); } - private void showUnlockView(Vault vault) { + private void showUnlockView() { final UnlockController ctrl = unlockController.get(); - final Parent root = ctrl.loadFxml(); - contentPane.getChildren().clear(); - contentPane.getChildren().add(root); - ctrl.setVault(vault); - ctrl.setListener(this::didUnlock); - } - - public void didUnlock(UnlockController ctrl) { - showUnlockedView(ctrl.getVault()); - Platform.setImplicitExit(false); + ctrl.vault.bind(selectedVault); + activeController.set(ctrl); } private void showUnlockedView(Vault vault) { - final UnlockedController ctrl = unlockedController.get(); - final Parent root = ctrl.loadFxml(); - contentPane.getChildren().clear(); - contentPane.getChildren().add(root); + final UnlockedController ctrl = unlockedVaults.computeIfAbsent(vault, k -> { + return unlockedControllerProvider.get(); + }); ctrl.setVault(vault); ctrl.setListener(this::didLock); + activeController.set(ctrl); } public void didLock(UnlockedController ctrl) { - showUnlockView(ctrl.getVault()); - if (getUnlockedVaults().isEmpty()) { + unlockedVaults.remove(ctrl.getVault()); + showUnlockView(); + if (vaults.stream().anyMatch(Vault::isUnlocked)) { Platform.setImplicitExit(true); } } - private void showChangePasswordView(Vault vault) { + private void showChangePasswordView() { final ChangePasswordController ctrl = changePasswordController.get(); - final Parent root = ctrl.loadFxml(); - contentPane.getChildren().clear(); - contentPane.getChildren().add(root); - ctrl.setVault(vault); + ctrl.vault.bind(selectedVault); ctrl.setListener(this::didChangePassword); + activeController.set(ctrl); } public void didChangePassword(ChangePasswordController ctrl) { - showUnlockView(ctrl.getVault()); - } - - /* Convenience */ - - public Collection getVaults() { - return vaultList.getItems(); - } - - public Collection getUnlockedVaults() { - return getVaults().stream().filter(d -> d.isUnlocked()).collect(Collectors.toSet()); - } - - /* public Getter/Setter */ - - public Stage getStage() { - return stage; - } - - public void setStage(Stage stage) { - this.stage = stage; + showUnlockView(); } /** diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java new file mode 100644 index 000000000..fd433fc34 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright (c) 2014, 2016 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 + ******************************************************************************/ +package org.cryptomator.ui.controllers; + +import java.net.URL; +import java.util.ResourceBundle; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.cryptomator.ui.settings.Settings; +import org.fxmisc.easybind.EasyBind; + +import javafx.application.Application; +import javafx.fxml.FXML; +import javafx.scene.control.CheckBox; + +@Singleton +public class SettingsController extends AbstractFXMLViewController { + + private final Application app; + private final Settings settings; + + @Inject + public SettingsController(Application app, Settings settings) { + this.app = app; + this.settings = settings; + } + + @FXML + private CheckBox checkForUpdatesCheckbox; + + @Override + public void initialize() { + checkForUpdatesCheckbox.setSelected(settings.isCheckForUpdatesEnabled()); + EasyBind.subscribe(checkForUpdatesCheckbox.selectedProperty(), settings::setCheckForUpdatesEnabled); + } + + @Override + protected URL getFxmlResourceUrl() { + return getClass().getResource("/fxml/settings.fxml"); + } + + @Override + protected ResourceBundle getFxmlResourceBundle() { + return ResourceBundle.getBundle("localization"); + } + + // private boolean areUpdatesManagedExternally() { + // return Boolean.parseBoolean(System.getProperty("cryptomator.updatesManagedExternally", "false")); + // } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java index f9e4c2109..10692f163 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java @@ -10,10 +10,8 @@ package org.cryptomator.ui.controllers; import java.net.URL; import java.util.Comparator; -import java.util.Optional; import java.util.ResourceBundle; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; import javax.inject.Inject; @@ -26,12 +24,14 @@ import org.cryptomator.frontend.FrontendCreationFailedException; import org.cryptomator.frontend.webdav.mount.WindowsDriveLetters; import org.cryptomator.ui.controls.SecPasswordField; import org.cryptomator.ui.model.Vault; -import org.cryptomator.ui.util.FXThreads; +import org.fxmisc.easybind.EasyBind; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javafx.application.Application; import javafx.application.Platform; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; @@ -51,8 +51,18 @@ public class UnlockController extends AbstractFXMLViewController { private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class); - private Optional listener = Optional.empty(); - private Vault vault; + private final ExecutorService exec; + private final Application app; + private final WindowsDriveLetters driveLetters; + private final ChangeListener driveLetterChangeListener = this::winDriveLetterDidChange; + final ObjectProperty vault = new SimpleObjectProperty<>(); + + @Inject + public UnlockController(Application app, ExecutorService exec, WindowsDriveLetters driveLetters) { + this.app = app; + this.exec = exec; + this.driveLetters = driveLetters; + } @FXML private SecPasswordField passwordField; @@ -84,16 +94,22 @@ public class UnlockController extends AbstractFXMLViewController { @FXML private GridPane advancedOptions; - private final ExecutorService exec; - private final Application app; - private final WindowsDriveLetters driveLetters; - private final ChangeListener driveLetterChangeListener = this::winDriveLetterDidChange; + @Override + public void initialize() { + advancedOptions.managedProperty().bind(advancedOptions.visibleProperty()); + mountName.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents); + mountName.textProperty().addListener(this::mountNameDidChange); + if (SystemUtils.IS_OS_WINDOWS) { + winDriveLetter.setConverter(new WinDriveLetterLabelConverter()); + } else { + winDriveLetterLabel.setVisible(false); + winDriveLetterLabel.setManaged(false); + winDriveLetter.setVisible(false); + winDriveLetter.setManaged(false); + } + unlockButton.disableProperty().bind(passwordField.textProperty().isEmpty()); - @Inject - public UnlockController(Application app, ExecutorService exec, WindowsDriveLetters driveLetters) { - this.app = app; - this.exec = exec; - this.driveLetters = driveLetters; + EasyBind.subscribe(vault, this::vaultChanged); } @Override @@ -106,25 +122,11 @@ public class UnlockController extends AbstractFXMLViewController { return ResourceBundle.getBundle("localization"); } - @Override - public void initialize() { - passwordField.textProperty().addListener(this::passwordFieldsDidChange); - advancedOptions.managedProperty().bind(advancedOptions.visibleProperty()); - mountName.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents); - mountName.textProperty().addListener(this::mountNameDidChange); - if (SystemUtils.IS_OS_WINDOWS) { - winDriveLetter.setConverter(new WinDriveLetterLabelConverter()); - } else { - winDriveLetterLabel.setVisible(false); - winDriveLetterLabel.setManaged(false); - winDriveLetter.setVisible(false); - winDriveLetter.setManaged(false); + private void vaultChanged(Vault newVault) { + if (newVault == null) { + return; } - } - - private void resetView() { passwordField.clear(); - unlockButton.setDisable(true); advancedOptions.setVisible(false); advancedOptionsButton.setText(resourceBundle.getString("unlock.button.advancedOptions.show")); progressIndicator.setVisible(false); @@ -138,15 +140,10 @@ public class UnlockController extends AbstractFXMLViewController { } downloadsPageLink.setVisible(false); messageText.setText(null); - } - - // **************************************** - // Password field - // **************************************** - - private void passwordFieldsDidChange(ObservableValue property, String oldValue, String newValue) { - boolean passwordIsEmpty = passwordField.getText().isEmpty(); - unlockButton.setDisable(passwordIsEmpty); + mountName.setText(newVault.getMountName()); + if (SystemUtils.IS_OS_WINDOWS) { + chooseSelectedDriveLetter(); + } } // **************************************** @@ -183,14 +180,14 @@ public class UnlockController extends AbstractFXMLViewController { } private void mountNameDidChange(ObservableValue property, String oldValue, String newValue) { - if (vault == null) { + if (vault.get() == null) { return; } // newValue is guaranteed to be a-z0-9_, see #filterAlphanumericKeyEvents if (newValue.isEmpty()) { - mountName.setText(vault.getMountName()); + mountName.setText(vault.get().getMountName()); } else { - vault.setMountName(newValue); + vault.get().setMountName(newValue); } } @@ -237,99 +234,19 @@ public class UnlockController extends AbstractFXMLViewController { } private void winDriveLetterDidChange(ObservableValue property, Character oldValue, Character newValue) { - if (vault == null) { + if (vault.get() == null) { return; } - vault.setWinDriveLetter(newValue); - } - - // **************************************** - // Unlock button - // **************************************** - - @FXML - private void didClickUnlockButton(ActionEvent event) { - setControlsDisabled(true); - progressIndicator.setVisible(true); - downloadsPageLink.setVisible(false); - final CharSequence password = passwordField.getCharacters(); - try { - vault.activateFrontend(password); - Future futureMount = exec.submit(vault::mount); - FXThreads.runOnMainThreadWhenFinished(exec, futureMount, this::unlockAndMountFinished); - } catch (InvalidPassphraseException e) { - setControlsDisabled(false); - progressIndicator.setVisible(false); - messageText.setText(resourceBundle.getString("unlock.errorMessage.wrongPassword")); - Platform.runLater(passwordField::requestFocus); - } catch (UnsupportedVaultFormatException e) { - setControlsDisabled(false); - progressIndicator.setVisible(false); - downloadsPageLink.setVisible(true); - LOG.warn("Unable to unlock vault: " + e.getMessage()); - if (e.isVaultOlderThanSoftware()) { - messageText.setText(resourceBundle.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " "); - } else if (e.isSoftwareOlderThanVault()) { - messageText.setText(resourceBundle.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " "); - } - } catch (FrontendCreationFailedException ex) { - setControlsDisabled(false); - progressIndicator.setVisible(false); - messageText.setText(resourceBundle.getString("unlock.errorMessage.decryptionFailed")); - LOG.error("Decryption failed for technical reasons.", ex); - } finally { - passwordField.swipe(); - } - } - - private void setControlsDisabled(boolean disable) { - passwordField.setDisable(disable); - mountName.setDisable(disable); - unlockButton.setDisable(disable); - advancedOptionsButton.setDisable(disable); - } - - private void unlockAndMountFinished(boolean mountSuccess) { - progressIndicator.setVisible(false); - setControlsDisabled(false); - if (vault.isUnlocked() && !mountSuccess) { - exec.submit(vault::deactivateFrontend); - } else if (vault.isUnlocked() && mountSuccess) { - exec.submit(() -> { - try { - vault.reveal(); - } catch (CommandFailedException e) { - LOG.error("Failed to reveal mounted vault", e); - } - }); - } - if (mountSuccess) { - listener.ifPresent(this::invokeListenerLater); - } - } - - /* Getter/Setter */ - - public Vault getVault() { - return vault; - } - - public void setVault(Vault vault) { - this.resetView(); - this.vault = vault; - this.mountName.setText(vault.getMountName()); - if (SystemUtils.IS_OS_WINDOWS) { - chooseSelectedDriveLetter(); - } + vault.get().setWinDriveLetter(newValue); } private void chooseSelectedDriveLetter() { assert SystemUtils.IS_OS_WINDOWS; // if the vault prefers a drive letter, that is currently occupied, this is our last chance to reset this: - if (driveLetters.getOccupiedDriveLetters().contains(vault.getWinDriveLetter())) { - vault.setWinDriveLetter(null); + if (driveLetters.getOccupiedDriveLetters().contains(vault.get().getWinDriveLetter())) { + vault.get().setWinDriveLetter(null); } - final Character letter = vault.getWinDriveLetter(); + final Character letter = vault.get().getWinDriveLetter(); if (letter == null) { // first option is known to be 'auto-assign' due to #WinDriveLetterComparator. this.winDriveLetter.getSelectionModel().selectFirst(); @@ -338,25 +255,55 @@ public class UnlockController extends AbstractFXMLViewController { } } - public UnlockListener getListener() { - return listener.orElse(null); + // **************************************** + // Unlock button + // **************************************** + + @FXML + private void didClickUnlockButton(ActionEvent event) { + mountName.setDisable(true); + advancedOptionsButton.setDisable(true); + progressIndicator.setVisible(true); + downloadsPageLink.setVisible(false); + CharSequence password = passwordField.getCharacters(); + exec.submit(() -> this.unlock(password)); + } - public void setListener(UnlockListener listener) { - this.listener = Optional.ofNullable(listener); - } - - /* callback */ - - private void invokeListenerLater(UnlockListener listener) { - Platform.runLater(() -> { - listener.didUnlock(this); - }); - } - - @FunctionalInterface - interface UnlockListener { - void didUnlock(UnlockController ctrl); + private void unlock(CharSequence password) { + try { + vault.get().activateFrontend(password); + vault.get().reveal(); + } catch (InvalidPassphraseException e) { + Platform.runLater(() -> { + messageText.setText(resourceBundle.getString("unlock.errorMessage.wrongPassword")); + passwordField.requestFocus(); + }); + } catch (UnsupportedVaultFormatException e) { + LOG.warn("Unable to unlock vault: " + e.getMessage()); + Platform.runLater(() -> { + downloadsPageLink.setVisible(true); + if (e.isVaultOlderThanSoftware()) { + messageText.setText(resourceBundle.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " "); + } else if (e.isSoftwareOlderThanVault()) { + messageText.setText(resourceBundle.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " "); + } + }); + } catch (FrontendCreationFailedException e) { + LOG.error("Decryption failed for technical reasons.", e); + Platform.runLater(() -> { + messageText.setText(resourceBundle.getString("unlock.errorMessage.decryptionFailed")); + }); + } catch (CommandFailedException e) { + LOG.error("Failed to reveal mounted vault", e); + } finally { + Platform.runLater(() -> { + passwordField.swipe(); + mountName.setDisable(false); + advancedOptionsButton.setDisable(false); + progressIndicator.setVisible(false); + }); + } } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java index 8a6c61ee9..b1f43f32e 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java @@ -19,11 +19,14 @@ import javax.inject.Provider; import org.cryptomator.frontend.CommandFailedException; import org.cryptomator.ui.model.Vault; import org.cryptomator.ui.util.ActiveWindowStyleSupport; +import org.fxmisc.easybind.EasyBind; import javafx.animation.Animation; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.application.Platform; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.collections.ListChangeListener; import javafx.event.ActionEvent; import javafx.event.EventHandler; @@ -40,10 +43,22 @@ public class UnlockedController extends AbstractFXMLViewController { private static final int IO_SAMPLING_STEPS = 100; private static final double IO_SAMPLING_INTERVAL = 0.25; + + private final Stage macWarningsWindow = new Stage(); + private final MacWarningsController macWarningsController; + private final ExecutorService exec; + private final ObjectProperty vault = new SimpleObjectProperty<>(); private Optional listener = Optional.empty(); - private Vault vault; private Timeline ioAnimation; + @Inject + public UnlockedController(Provider macWarningsControllerProvider, ExecutorService exec) { + this.macWarningsController = macWarningsControllerProvider.get(); + this.exec = exec; + + macWarningsController.vault.bind(this.vault); + } + @FXML private Label messageLabel; @@ -53,14 +68,12 @@ public class UnlockedController extends AbstractFXMLViewController { @FXML private NumberAxis xAxis; - private final Stage macWarningsWindow = new Stage(); - private final MacWarningsController macWarningsController; - private final ExecutorService exec; + @Override + public void initialize() { + macWarningsController.initStage(macWarningsWindow); + ActiveWindowStyleSupport.startObservingFocus(macWarningsWindow); - @Inject - public UnlockedController(Provider macWarningsControllerProvider, ExecutorService exec) { - this.macWarningsController = macWarningsControllerProvider.get(); - this.exec = exec; + EasyBind.subscribe(vault, this::vaultChanged); } @Override @@ -73,17 +86,30 @@ public class UnlockedController extends AbstractFXMLViewController { return ResourceBundle.getBundle("localization"); } - @Override - public void initialize() { - macWarningsController.initStage(macWarningsWindow); - ActiveWindowStyleSupport.startObservingFocus(macWarningsWindow); + private void vaultChanged(Vault newVault) { + if (newVault == null) { + return; + } + + // listen to MAC warnings as long as this vault is unlocked: + final ListChangeListener macWarningsListener = this::macWarningsDidChange; + newVault.getNamesOfResourcesWithInvalidMac().addListener(macWarningsListener); + newVault.unlockedProperty().addListener((observable, oldValue, newValue) -> { + if (Boolean.FALSE.equals(newValue)) { + newVault.getNamesOfResourcesWithInvalidMac().removeListener(macWarningsListener); + } + }); + + // (re)start throughput statistics: + stopIoSampling(); + startIoSampling(); } @FXML private void didClickRevealVault(ActionEvent event) { exec.submit(() -> { try { - vault.reveal(); + vault.get().reveal(); } catch (CommandFailedException e) { Platform.runLater(() -> { messageLabel.setText(resourceBundle.getString("unlocked.label.revealFailed")); @@ -96,14 +122,14 @@ public class UnlockedController extends AbstractFXMLViewController { private void didClickCloseVault(ActionEvent event) { exec.submit(() -> { try { - vault.unmount(); + vault.get().unmount(); } catch (CommandFailedException e) { Platform.runLater(() -> { messageLabel.setText(resourceBundle.getString("unlocked.label.unmountFailed")); }); return; } - vault.deactivateFrontend(); + vault.get().deactivateFrontend(); listener.ifPresent(this::invokeListenerLater); }); } @@ -166,7 +192,7 @@ public class UnlockedController extends AbstractFXMLViewController { public void handle(ActionEvent event) { step++; - final long decBytes = vault.pollBytesRead(); + final long decBytes = vault.get().pollBytesRead(); final double smoothedDecBytes = oldDecBytes + SMOOTHING_FACTOR * (decBytes - oldDecBytes); final double smoothedDecMb = smoothedDecBytes * BYTES_TO_MEGABYTES_FACTOR; oldDecBytes = smoothedDecBytes > EFFECTIVELY_ZERO ? (long) smoothedDecBytes : 0l; @@ -175,7 +201,7 @@ public class UnlockedController extends AbstractFXMLViewController { decryptedBytes.getData().remove(0); } - final long encBytes = vault.pollBytesWritten(); + final long encBytes = vault.get().pollBytesWritten(); final double smoothedEncBytes = oldEncBytes + SMOOTHING_FACTOR * (encBytes - oldEncBytes); final double smoothedEncMb = smoothedEncBytes * BYTES_TO_MEGABYTES_FACTOR; oldEncBytes = smoothedEncBytes > EFFECTIVELY_ZERO ? (long) smoothedEncBytes : 0l; @@ -192,27 +218,15 @@ public class UnlockedController extends AbstractFXMLViewController { /* Getter/Setter */ public Vault getVault() { - return vault; + return this.vault.get(); } public void setVault(Vault vault) { - this.vault = vault; - macWarningsController.setVault(vault); - - // listen to MAC warnings as long as this vault is unlocked: - final ListChangeListener macWarningsListener = this::macWarningsDidChange; - vault.getNamesOfResourcesWithInvalidMac().addListener(macWarningsListener); - vault.unlockedProperty().addListener((observable, oldValue, newValue) -> { - if (Boolean.FALSE.equals(newValue)) { - vault.getNamesOfResourcesWithInvalidMac().removeListener(macWarningsListener); - } - }); - - // (re)start throughput statistics: - stopIoSampling(); - startIoSampling(); + this.vault.set(vault); } + /* callback */ + public LockListener getListener() { return listener.orElse(null); } @@ -221,8 +235,6 @@ public class UnlockedController extends AbstractFXMLViewController { this.listener = Optional.ofNullable(listener); } - /* callback */ - private void invokeListenerLater(LockListener listener) { Platform.runLater(() -> { listener.didLock(this); diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java index 9d969682a..4e9bcc02f 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java @@ -39,11 +39,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import javafx.application.Application; import javafx.application.Platform; -import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.Node; -import javafx.scene.control.CheckBox; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.control.ProgressIndicator; @@ -61,9 +59,6 @@ public class WelcomeController extends AbstractFXMLViewController { @FXML private Node checkForUpdatesContainer; - @FXML - private CheckBox checkForUpdatesCheckbox; - @FXML private Label checkForUpdatesStatus; @@ -99,17 +94,11 @@ public class WelcomeController extends AbstractFXMLViewController { @Override public void initialize() { botImageView.setImage(new Image(getClass().getResource("/bot_welcome.png").toString())); - checkForUpdatesCheckbox.setSelected(settings.isCheckForUpdatesEnabled()); - checkForUpdatesCheckbox.selectedProperty().addListener(this::checkForUpdatesChanged); if (areUpdatesManagedExternally()) { checkForUpdatesContainer.setVisible(false); checkForUpdatesContainer.setManaged(false); - } else { - checkForUpdatesCheckbox.setSelected(settings.isCheckForUpdatesEnabled()); - checkForUpdatesCheckbox.selectedProperty().addListener(this::checkForUpdatesChanged); - if (settings.isCheckForUpdatesEnabled()) { - executor.execute(this::checkForUpdates); - } + } else if (settings.isCheckForUpdatesEnabled()) { + executor.execute(this::checkForUpdates); } } @@ -117,14 +106,6 @@ public class WelcomeController extends AbstractFXMLViewController { // Check for updates // **************************************** - private void checkForUpdatesChanged(ObservableValue observable, Boolean oldValue, Boolean newValue) { - assert newValue != null; - settings.setCheckForUpdatesEnabled(newValue); - if (newValue) { - executor.execute(this::checkForUpdates); - } - } - private boolean areUpdatesManagedExternally() { return Boolean.parseBoolean(System.getProperty("cryptomator.updatesManagedExternally", "false")); } @@ -134,7 +115,6 @@ public class WelcomeController extends AbstractFXMLViewController { return; } Platform.runLater(() -> { - checkForUpdatesCheckbox.setVisible(false); checkForUpdatesStatus.setText(resourceBundle.getString("welcome.checkForUpdates.label.currentlyChecking")); checkForUpdatesIndicator.setVisible(true); }); @@ -157,8 +137,7 @@ public class WelcomeController extends AbstractFXMLViewController { // no error handling required. Maybe next time the version check is successful. } finally { Platform.runLater(() -> { - checkForUpdatesCheckbox.setVisible(true); - checkForUpdatesStatus.setText(resourceBundle.getString("welcome.checkForUpdates.label.checkboxLabel")); + checkForUpdatesStatus.setText(""); checkForUpdatesIndicator.setVisible(false); }); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controls/SecPasswordField.java b/main/ui/src/main/java/org/cryptomator/ui/controls/SecPasswordField.java index f551149bb..c7c6fc8fa 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controls/SecPasswordField.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controls/SecPasswordField.java @@ -26,7 +26,7 @@ public class SecPasswordField extends PasswordField { * Imagine the following example with pass being the password, x being the swipe char and ' being the offset of the char array: *
    *
  1. Append filling chars to the end of the password: passxxxx'
  2. - *
  3. Delete first 4 chars. Internal implementation will then copy the following chars to the position, where the deletion occured: xxxx'xxxx
  4. + *
  5. Delete first 4 chars. Internal implementation will then copy subsequent chars to the position, where the deletion occured: xxxx'xxxx
  6. *
  7. Delete first 4 chars again, as we appended 4 chars in step 1: 'xxxxxx
  8. *
*/ @@ -37,8 +37,8 @@ public class SecPasswordField extends PasswordField { this.getContent().insert(pwLength, new String(fillingChars), false); this.getContent().delete(0, pwLength, true); this.getContent().delete(0, pwLength, true); - // previous text has now been overwritten. still we need to update the text to trigger some property bindings: - this.setText(""); + // previous text has now been overwritten. but we still need to update the text to trigger some property bindings: + this.clear(); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java index a1ded7f3d..ba7ca9cc9 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java @@ -46,8 +46,9 @@ import org.cryptomator.ui.util.FXThreads; import com.google.common.collect.ImmutableMap; import dagger.Lazy; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; +import javafx.application.Platform; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -62,7 +63,7 @@ public class Vault implements Serializable, CryptoFileSystemDelegate { private final DeferredCloser closer; private final ShorteningFileSystemFactory shorteningFileSystemFactory; private final CryptoFileSystemFactory cryptoFileSystemFactory; - private final ObjectProperty unlocked = new SimpleObjectProperty(this, "unlocked", Boolean.FALSE); + private final BooleanProperty unlocked = new SimpleBooleanProperty(); private final ObservableList namesOfResourcesWithInvalidMac = FXThreads.observableListOnMainThread(FXCollections.observableArrayList()); private final Set whitelistedResourcesWithInvalidMac = new HashSet<>(); private final AtomicReference nioFileSystem = new AtomicReference<>(); @@ -118,6 +119,7 @@ public class Vault implements Serializable, CryptoFileSystemDelegate { } public synchronized void activateFrontend(CharSequence passphrase) throws FrontendCreationFailedException { + boolean success = false; try { FileSystem fs = getNioFileSystem(); FileSystem shorteningFs = shorteningFileSystemFactory.get(fs); @@ -127,16 +129,21 @@ public class Vault implements Serializable, CryptoFileSystemDelegate { String contextPath = StringUtils.prependIfMissing(mountName, "/"); Frontend frontend = frontendFactory.get().create(statsFs, contextPath); filesystemFrontend = closer.closeLater(frontend); - unlocked.set(true); - } catch (UncheckedIOException e) { + frontend.mount(getMountParams()); + success = true; + } catch (UncheckedIOException | CommandFailedException e) { throw new FrontendCreationFailedException(e); + } finally { + // unlocked is a observable property and should only be changed by the FX application thread + final boolean finalSuccess = success; + Platform.runLater(() -> unlocked.set(finalSuccess)); } } public synchronized void deactivateFrontend() { filesystemFrontend.close(); statsFileSystem = Optional.empty(); - unlocked.set(false); + Platform.runLater(() -> unlocked.set(false)); } private Map> getMountParams() { @@ -146,17 +153,6 @@ public class Vault implements Serializable, CryptoFileSystemDelegate { ); } - public Boolean mount() { - try { - Optionals.ifPresent(filesystemFrontend.get(), f -> { - f.mount(getMountParams()); - }); - return true; - } catch (CommandFailedException e) { - return false; - } - } - public void reveal() throws CommandFailedException { Optionals.ifPresent(filesystemFrontend.get(), Frontend::reveal); } @@ -213,7 +209,7 @@ public class Vault implements Serializable, CryptoFileSystemDelegate { } } - public ObjectProperty unlockedProperty() { + public BooleanProperty unlockedProperty() { return unlocked; } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/FXThreads.java b/main/ui/src/main/java/org/cryptomator/ui/util/FXThreads.java index d9b4cefe1..089979139 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/FXThreads.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/FXThreads.java @@ -13,6 +13,7 @@ package org.cryptomator.ui.util; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; +import java.util.function.Consumer; import javafx.application.Platform; import javafx.collections.ObservableList; @@ -42,14 +43,14 @@ import javafx.collections.ObservableSet; * // when done, update text label: * runOnMainThreadWhenFinished(futureBookName, (bookName) -> { * myLabel.setText(bookName); - * }, (exception) -> { + * } , (exception) -> { * myLabel.setText("An exception occured: " + exception.getMessage()); * }); * */ public final class FXThreads { - private static final CallbackWhenTaskFailed DUMMY_EXCEPTION_CALLBACK = (e) -> { + private static final Consumer DUMMY_EXCEPTION_CALLBACK = (e) -> { // ignore. }; @@ -65,11 +66,11 @@ public final class FXThreads { * }); * * - * @param executor + * @param executor The service to execute the background task on * @param task The task to wait for. * @param successCallback The action to perform, when the task finished. */ - public static void runOnMainThreadWhenFinished(ExecutorService executor, Future task, CallbackWhenTaskFinished successCallback) { + public static void runOnMainThreadWhenFinished(ExecutorService executor, Future task, Consumer successCallback) { runOnMainThreadWhenFinished(executor, task, successCallback, DUMMY_EXCEPTION_CALLBACK); } @@ -82,7 +83,7 @@ public final class FXThreads { * * runOnMainThreadWhenFinished(futureBookNamePossiblyFailing, (bookName) -> { * myLabel.setText(bookName); - * }, (exception) -> { + * } , (exception) -> { * myLabel.setText("An exception occured: " + exception.getMessage()); * }); * @@ -92,7 +93,7 @@ public final class FXThreads { * @param successCallback The action to perform, when the task finished. * @param exceptionCallback */ - public static void runOnMainThreadWhenFinished(ExecutorService executor, Future task, CallbackWhenTaskFinished successCallback, CallbackWhenTaskFailed exceptionCallback) { + public static void runOnMainThreadWhenFinished(ExecutorService executor, Future task, Consumer successCallback, Consumer exceptionCallback) { Objects.requireNonNull(task, "task must not be null."); Objects.requireNonNull(successCallback, "successCallback must not be null."); Objects.requireNonNull(exceptionCallback, "exceptionCallback must not be null."); @@ -100,24 +101,16 @@ public final class FXThreads { try { final T result = task.get(); Platform.runLater(() -> { - successCallback.taskFinished(result); + successCallback.accept(result); }); } catch (Exception e) { Platform.runLater(() -> { - exceptionCallback.taskFailed(e); + exceptionCallback.accept(e); }); } }); } - public interface CallbackWhenTaskFinished { - void taskFinished(T result); - } - - public interface CallbackWhenTaskFailed { - void taskFailed(Throwable t); - } - public static ObservableSet observableSetOnMainThread(ObservableSet set) { return new ObservableSetOnMainThread(set); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/Listeners.java b/main/ui/src/main/java/org/cryptomator/ui/util/Listeners.java new file mode 100644 index 000000000..76db45b9d --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/util/Listeners.java @@ -0,0 +1,25 @@ +package org.cryptomator.ui.util; + +import java.util.function.Consumer; + +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; + +public final class Listeners { + + private Listeners() { + } + + /** + * Invokes the given consumer with the new value as soon as a change is reported by an {@link ObservableValue}. + * + * @param consumer The function to call with the changed value. + * @return A listener that can i.e. be used in {@link ObservableValue#addListener(ChangeListener)}. + */ + public static ChangeListener withNewValue(Consumer consumer) { + return (ObservableValue observable, T oldValue, T newValue) -> { + consumer.accept(newValue); + }; + } + +} diff --git a/main/ui/src/main/resources/fxml/main.fxml b/main/ui/src/main/resources/fxml/main.fxml index 1dd8997d0..ca5da150c 100644 --- a/main/ui/src/main/resources/fxml/main.fxml +++ b/main/ui/src/main/resources/fxml/main.fxml @@ -31,7 +31,7 @@ - + @@ -49,7 +49,7 @@