فهرست منبع

UI code cleanup

Sebastian Stenzel 9 سال پیش
والد
کامیت
f05440fe7a
17فایلهای تغییر یافته به همراه205 افزوده شده و 184 حذف شده
  1. 3 4
      main/ui/src/main/java/org/cryptomator/ui/Cryptomator.java
  2. 6 0
      main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java
  3. 18 13
      main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java
  4. 22 31
      main/ui/src/main/java/org/cryptomator/ui/MainApplication.java
  5. 0 12
      main/ui/src/main/java/org/cryptomator/ui/controllers/AbstractFXMLViewController.java
  6. 10 7
      main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java
  7. 7 4
      main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java
  8. 6 3
      main/ui/src/main/java/org/cryptomator/ui/controllers/MacWarningsController.java
  9. 13 23
      main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java
  10. 5 2
      main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java
  11. 14 11
      main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java
  12. 7 4
      main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java
  13. 7 4
      main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java
  14. 27 0
      main/ui/src/main/java/org/cryptomator/ui/settings/Localization.java
  15. 13 1
      main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java
  16. 20 24
      main/ui/src/main/java/org/cryptomator/ui/util/DeferredCloser.java
  17. 27 41
      main/ui/src/main/java/org/cryptomator/ui/util/TrayIconUtil.java

+ 3 - 4
main/ui/src/main/java/org/cryptomator/ui/Cryptomator.java

@@ -19,8 +19,6 @@ import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.function.Consumer;
 
-import javafx.application.Application;
-
 import org.apache.commons.lang3.SystemUtils;
 import org.cryptomator.ui.util.SingleInstanceManager;
 import org.cryptomator.ui.util.SingleInstanceManager.RemoteInstance;
@@ -28,11 +26,12 @@ import org.eclipse.jetty.util.ConcurrentHashSet;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javafx.application.Application;
+
 public class Cryptomator {
-	public static final Logger LOG = LoggerFactory.getLogger(MainApplication.class);
 
 	public static final CompletableFuture<Consumer<File>> OPEN_FILE_HANDLER = new CompletableFuture<>();
-
+	private static final Logger LOG = LoggerFactory.getLogger(Cryptomator.class);
 	private static final Set<Runnable> SHUTDOWN_TASKS = new ConcurrentHashSet<>();
 	private static final CleanShutdownPerformer CLEAN_SHUTDOWN_PERFORMER = new CleanShutdownPerformer();
 

+ 6 - 0
main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java

@@ -13,7 +13,9 @@ import java.util.concurrent.ExecutorService;
 import javax.inject.Singleton;
 
 import org.cryptomator.ui.controllers.MainController;
+import org.cryptomator.ui.settings.Localization;
 import org.cryptomator.ui.util.DeferredCloser;
+import org.cryptomator.ui.util.TrayIconUtil;
 
 import dagger.Component;
 
@@ -25,4 +27,8 @@ interface CryptomatorComponent {
 	DeferredCloser deferredCloser();
 
 	MainController mainController();
+
+	Localization localization();
+
+	TrayIconUtil trayIconUtil();
 }

+ 18 - 13
main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java

@@ -24,7 +24,6 @@ import org.cryptomator.ui.model.VaultObjectMapperProvider;
 import org.cryptomator.ui.settings.Settings;
 import org.cryptomator.ui.settings.SettingsProvider;
 import org.cryptomator.ui.util.DeferredCloser;
-import org.cryptomator.ui.util.DeferredCloser.Closer;
 import org.cryptomator.ui.util.SemVerComparator;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -32,16 +31,17 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import dagger.Module;
 import dagger.Provides;
 import javafx.application.Application;
+import javafx.stage.Stage;
 
 @Module(includes = CryptoEngineModule.class)
 class CryptomatorModule {
 
 	private final Application application;
-	private final DeferredCloser deferredCloser;
+	private final Stage mainWindow;
 
-	public CryptomatorModule(Application application) {
+	public CryptomatorModule(Application application, Stage mainWindow) {
 		this.application = application;
-		this.deferredCloser = new DeferredCloser();
+		this.mainWindow = mainWindow;
 	}
 
 	@Provides
@@ -50,10 +50,19 @@ class CryptomatorModule {
 		return application;
 	}
 
+	@Provides
+	@Singleton
+	@Named("mainWindow")
+	Stage provideMainWindow() {
+		return mainWindow;
+	}
+
 	@Provides
 	@Singleton
 	DeferredCloser provideDeferredCloser() {
-		return deferredCloser;
+		DeferredCloser closer = new DeferredCloser();
+		Cryptomator.addShutdownTask(closer::close);
+		return closer;
 	}
 
 	@Provides
@@ -78,8 +87,8 @@ class CryptomatorModule {
 
 	@Provides
 	@Singleton
-	ExecutorService provideExecutorService() {
-		return closeLater(Executors.newCachedThreadPool(), ExecutorService::shutdown);
+	ExecutorService provideExecutorService(DeferredCloser closer) {
+		return closer.closeLater(Executors.newCachedThreadPool(), ExecutorService::shutdown).get().orElseThrow(IllegalStateException::new);
 	}
 
 	@Provides
@@ -90,14 +99,10 @@ class CryptomatorModule {
 
 	@Provides
 	@Singleton
-	FrontendFactory provideFrontendFactory(WebDavServer webDavServer, Settings settings) {
+	FrontendFactory provideFrontendFactory(DeferredCloser closer, WebDavServer webDavServer, Settings settings) {
 		webDavServer.setPort(settings.getPort());
 		webDavServer.start();
-		return closeLater(webDavServer, WebDavServer::stop);
-	}
-
-	private <T> T closeLater(T object, Closer<T> closer) {
-		return deferredCloser.closeLater(object, closer).get().get();
+		return closer.closeLater(webDavServer, WebDavServer::stop).get().orElseThrow(IllegalStateException::new);
 	}
 
 }

+ 22 - 31
main/ui/src/main/java/org/cryptomator/ui/MainApplication.java

@@ -12,8 +12,6 @@ import java.io.IOException;
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.ResourceBundle;
-import java.util.concurrent.ExecutorService;
 
 import org.apache.commons.lang3.SystemUtils;
 import org.cryptomator.ui.controllers.MainController;
@@ -21,7 +19,6 @@ import org.cryptomator.ui.util.ActiveWindowStyleSupport;
 import org.cryptomator.ui.util.DeferredCloser;
 import org.cryptomator.ui.util.SingleInstanceManager;
 import org.cryptomator.ui.util.SingleInstanceManager.LocalInstance;
-import org.cryptomator.ui.util.TrayIconUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -35,24 +32,16 @@ import javafx.stage.Stage;
 public class MainApplication extends Application {
 
 	public static final String APPLICATION_KEY = "CryptomatorGUI";
-
 	private static final Logger LOG = LoggerFactory.getLogger(MainApplication.class);
 
-	private final ExecutorService executorService;
-	private final DeferredCloser closer;
-	private final MainController mainCtrl;
-
-	public MainApplication() {
-		final CryptomatorComponent comp = DaggerCryptomatorComponent.builder().cryptomatorModule(new CryptomatorModule(this)).build();
-		this.executorService = comp.executorService();
-		this.closer = comp.deferredCloser();
-		this.mainCtrl = comp.mainController();
-		Cryptomator.addShutdownTask(closer::close);
-	}
+	private DeferredCloser closer;
 
 	@Override
-	public void start(final Stage primaryStage) throws IOException {
-		Font.loadFont(getClass().getResourceAsStream("/css/ionicons.ttf"), 12.0);
+	public void start(Stage primaryStage) throws IOException {
+		final CryptomatorComponent comp = DaggerCryptomatorComponent.builder().cryptomatorModule(new CryptomatorModule(this, primaryStage)).build();
+		final MainController mainCtrl = comp.mainController();
+		closer = comp.deferredCloser();
+
 		ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
 		FXMLLoader.setDefaultClassLoader(contextClassLoader);
 		Platform.runLater(() -> {
@@ -65,36 +54,35 @@ public class MainApplication extends Application {
 			}
 		});
 
+		// Set stylesheets and initialize stage:
+		Font.loadFont(getClass().getResourceAsStream("/css/ionicons.ttf"), 12.0);
 		chooseNativeStylesheet();
-
 		mainCtrl.initStage(primaryStage);
-
-		final ResourceBundle rb = ResourceBundle.getBundle("localization");
 		primaryStage.titleProperty().bind(mainCtrl.windowTitle());
 		primaryStage.setResizable(false);
 		if (SystemUtils.IS_OS_WINDOWS) {
 			primaryStage.getIcons().add(new Image(MainApplication.class.getResourceAsStream("/window_icon.png")));
 		}
-		primaryStage.show();
 
+		// show window and start observing its focus:
+		primaryStage.show();
 		ActiveWindowStyleSupport.startObservingFocus(primaryStage);
-		TrayIconUtil.init(primaryStage, rb, () -> {
-			quit();
-		});
+		comp.trayIconUtil().initTrayIcon(this::quit);
 
+		// open files, if requested during startup:
 		for (String arg : getParameters().getUnnamed()) {
-			handleCommandLineArg(arg);
+			handleCommandLineArg(arg, primaryStage, mainCtrl);
 		}
-
 		if (SystemUtils.IS_OS_MAC_OSX) {
-			Cryptomator.OPEN_FILE_HANDLER.complete(file -> handleCommandLineArg(file.getAbsolutePath()));
+			Cryptomator.OPEN_FILE_HANDLER.complete(file -> handleCommandLineArg(file.getAbsolutePath(), primaryStage, mainCtrl));
 		}
 
-		LocalInstance cryptomatorGuiInstance = closer.closeLater(SingleInstanceManager.startLocalInstance(APPLICATION_KEY, executorService), LocalInstance::close).get().get();
-		cryptomatorGuiInstance.registerListener(arg -> handleCommandLineArg(arg));
+		// register this application instance as primary application, that other instances can send open file requests to:
+		LocalInstance cryptomatorGuiInstance = closer.closeLater(SingleInstanceManager.startLocalInstance(APPLICATION_KEY, comp.executorService()), LocalInstance::close).get().get();
+		cryptomatorGuiInstance.registerListener(arg -> handleCommandLineArg(arg, primaryStage, mainCtrl));
 	}
 
-	private void handleCommandLineArg(String arg) {
+	private void handleCommandLineArg(String arg, Stage primaryStage, MainController mainCtrl) {
 		// find correct location:
 		final Path path = FileSystems.getDefault().getPath(arg);
 		final Path vaultPath;
@@ -110,7 +98,10 @@ public class MainApplication extends Application {
 		// add vault to ctrl:
 		Platform.runLater(() -> {
 			mainCtrl.addVault(vaultPath, true);
-			mainCtrl.toFront();
+			primaryStage.setIconified(false);
+			primaryStage.show();
+			primaryStage.toFront();
+			primaryStage.requestFocus();
 		});
 	}
 

+ 0 - 12
main/ui/src/main/java/org/cryptomator/ui/controllers/AbstractFXMLViewController.java

@@ -28,16 +28,6 @@ abstract class AbstractFXMLViewController implements Initializable {
 
 	private final AtomicReference<Parent> fxmlRoot = new AtomicReference<>();
 
-	/**
-	 * URL from #initialize(URL, ResourceBundle)
-	 */
-	protected URL rootUrl;
-
-	/**
-	 * ResourceBundle from #initialize(URL, ResourceBundle)
-	 */
-	protected ResourceBundle resourceBundle;
-
 	/**
 	 * Gets the URL to the FXML file describing the view presented by this controller.<br/>
 	 * 
@@ -57,8 +47,6 @@ abstract class AbstractFXMLViewController implements Initializable {
 
 	@Override
 	public final void initialize(URL location, ResourceBundle resources) {
-		this.rootUrl = location;
-		this.resourceBundle = resources;
 		this.initialize();
 	}
 

+ 10 - 7
main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java

@@ -21,6 +21,7 @@ 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.cryptomator.ui.settings.Localization;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -41,12 +42,14 @@ public class ChangePasswordController extends AbstractFXMLViewController {
 	private static final Logger LOG = LoggerFactory.getLogger(ChangePasswordController.class);
 
 	private final Application app;
+	private final Localization localization;
 	final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
 	private Optional<ChangePasswordListener> listener = Optional.empty();
 
 	@Inject
-	public ChangePasswordController(Application app) {
+	public ChangePasswordController(Application app, Localization localization) {
 		this.app = app;
+		this.localization = localization;
 	}
 
 	@FXML
@@ -82,7 +85,7 @@ public class ChangePasswordController extends AbstractFXMLViewController {
 
 	@Override
 	protected ResourceBundle getFxmlResourceBundle() {
-		return ResourceBundle.getBundle("localization");
+		return localization;
 	}
 
 	// ****************************************
@@ -103,21 +106,21 @@ public class ChangePasswordController extends AbstractFXMLViewController {
 		downloadsPageLink.setVisible(false);
 		try {
 			vault.get().changePassphrase(oldPasswordField.getCharacters(), newPasswordField.getCharacters());
-			messageText.setText(resourceBundle.getString("changePassword.infoMessage.success"));
+			messageText.setText(localization.getString("changePassword.infoMessage.success"));
 			listener.ifPresent(this::invokeListenerLater);
 		} catch (InvalidPassphraseException e) {
-			messageText.setText(resourceBundle.getString("changePassword.errorMessage.wrongPassword"));
+			messageText.setText(localization.getString("changePassword.errorMessage.wrongPassword"));
 			Platform.runLater(oldPasswordField::requestFocus);
 		} catch (UncheckedIOException | IOException ex) {
-			messageText.setText(resourceBundle.getString("changePassword.errorMessage.decryptionFailed"));
+			messageText.setText(localization.getString("changePassword.errorMessage.decryptionFailed"));
 			LOG.error("Decryption failed for technical reasons.", ex);
 		} 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") + " ");
+				messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
 			} else if (e.isSoftwareOlderThanVault()) {
-				messageText.setText(resourceBundle.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
+				messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
 			}
 		} finally {
 			oldPasswordField.swipe();

+ 7 - 4
main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java

@@ -20,6 +20,7 @@ import javax.inject.Singleton;
 
 import org.cryptomator.ui.controls.SecPasswordField;
 import org.cryptomator.ui.model.Vault;
+import org.cryptomator.ui.settings.Localization;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -37,11 +38,13 @@ public class InitializeController extends AbstractFXMLViewController {
 
 	private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class);
 
+	private final Localization localization;
 	final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
 	private Optional<InitializationListener> listener = Optional.empty();
 
 	@Inject
-	public InitializeController() {
+	public InitializeController(Localization localization) {
+		this.localization = localization;
 	}
 
 	@FXML
@@ -70,7 +73,7 @@ public class InitializeController extends AbstractFXMLViewController {
 
 	@Override
 	protected ResourceBundle getFxmlResourceBundle() {
-		return ResourceBundle.getBundle("localization");
+		return localization;
 	}
 
 	// ****************************************
@@ -84,10 +87,10 @@ public class InitializeController extends AbstractFXMLViewController {
 			vault.get().create(passphrase);
 			listener.ifPresent(this::invokeListenerLater);
 		} catch (FileAlreadyExistsException ex) {
-			messageLabel.setText(resourceBundle.getString("initialize.messageLabel.alreadyInitialized"));
+			messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
 		} catch (UncheckedIOException | IOException ex) {
 			LOG.error("I/O Exception", ex);
-			messageLabel.setText(resourceBundle.getString("initialize.messageLabel.initializationFailed"));
+			messageLabel.setText(localization.getString("initialize.messageLabel.initializationFailed"));
 		} finally {
 			passwordField.swipe();
 			retypePasswordField.swipe();

+ 6 - 3
main/ui/src/main/java/org/cryptomator/ui/controllers/MacWarningsController.java

@@ -15,6 +15,7 @@ import java.util.stream.Collectors;
 import javax.inject.Inject;
 
 import org.cryptomator.ui.model.Vault;
+import org.cryptomator.ui.settings.Localization;
 
 import javafx.application.Application;
 import javafx.beans.Observable;
@@ -41,6 +42,7 @@ import javafx.util.StringConverter;
 public class MacWarningsController extends AbstractFXMLViewController {
 
 	private final Application application;
+	private final Localization localization;
 	private final ObservableList<Warning> warnings = FXCollections.observableArrayList();
 	private final ListChangeListener<String> unauthenticatedResourcesChangeListener = this::unauthenticatedResourcesDidChange;
 	private final ChangeListener<Boolean> stageVisibilityChangeListener = this::windowVisibilityDidChange;
@@ -48,8 +50,9 @@ public class MacWarningsController extends AbstractFXMLViewController {
 	private Stage stage;
 
 	@Inject
-	public MacWarningsController(Application application) {
+	public MacWarningsController(Application application, Localization localization) {
 		this.application = application;
+		this.localization = localization;
 	}
 
 	@FXML
@@ -84,7 +87,7 @@ public class MacWarningsController extends AbstractFXMLViewController {
 
 	@Override
 	protected ResourceBundle getFxmlResourceBundle() {
-		return ResourceBundle.getBundle("localization");
+		return localization;
 	}
 
 	@Override
@@ -127,7 +130,7 @@ public class MacWarningsController extends AbstractFXMLViewController {
 
 	private void windowVisibilityDidChange(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
 		if (Boolean.TRUE.equals(newValue)) {
-			stage.setTitle(String.format(resourceBundle.getString("macWarnings.windowTitle"), vault.get().getName()));
+			stage.setTitle(String.format(localization.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 {

+ 13 - 23
main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java

@@ -19,12 +19,14 @@ import java.util.Map;
 import java.util.ResourceBundle;
 
 import javax.inject.Inject;
+import javax.inject.Named;
 import javax.inject.Provider;
 import javax.inject.Singleton;
 
 import org.cryptomator.ui.controls.DirectoryListCell;
 import org.cryptomator.ui.model.Vault;
 import org.cryptomator.ui.model.VaultFactory;
+import org.cryptomator.ui.settings.Localization;
 import org.cryptomator.ui.settings.Settings;
 import org.fxmisc.easybind.EasyBind;
 import org.fxmisc.easybind.monadic.MonadicBinding;
@@ -58,6 +60,8 @@ public class MainController extends AbstractFXMLViewController {
 
 	private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
 
+	private final Stage mainWindow;
+	private final Localization localization;
 	private final VaultFactory vaultFactoy;
 	private final Lazy<WelcomeController> welcomeController;
 	private final Lazy<InitializeController> initializeController;
@@ -74,8 +78,11 @@ public class MainController extends AbstractFXMLViewController {
 	private final Map<Vault, UnlockedController> unlockedVaults = new HashMap<>();
 
 	@Inject
-	public MainController(Settings settings, VaultFactory vaultFactoy, Lazy<WelcomeController> welcomeController, Lazy<InitializeController> initializeController, Lazy<UnlockController> unlockController,
-			Provider<UnlockedController> unlockedControllerProvider, Lazy<ChangePasswordController> changePasswordController, Lazy<SettingsController> settingsController) {
+	public MainController(@Named("mainWindow") Stage mainWindow, Localization localization, Settings settings, VaultFactory vaultFactoy, Lazy<WelcomeController> welcomeController,
+			Lazy<InitializeController> initializeController, Lazy<UnlockController> unlockController, Provider<UnlockedController> unlockedControllerProvider, Lazy<ChangePasswordController> changePasswordController,
+			Lazy<SettingsController> settingsController) {
+		this.mainWindow = mainWindow;
+		this.localization = localization;
 		this.vaultFactoy = vaultFactoy;
 		this.welcomeController = welcomeController;
 		this.initializeController = initializeController;
@@ -89,8 +96,6 @@ public class MainController extends AbstractFXMLViewController {
 		this.isShowingSettings = activeController.isEqualTo(settingsController.get());
 	}
 
-	private Stage stage;
-
 	@FXML
 	private ContextMenu vaultListCellContextMenu;
 
@@ -137,7 +142,7 @@ public class MainController extends AbstractFXMLViewController {
 
 	@Override
 	protected ResourceBundle getFxmlResourceBundle() {
-		return ResourceBundle.getBundle("localization");
+		return localization;
 	}
 
 	private ListCell<Vault> createDirecoryListCell(ListView<Vault> param) {
@@ -146,12 +151,6 @@ public class MainController extends AbstractFXMLViewController {
 		return cell;
 	}
 
-	@Override
-	public void initStage(Stage stage) {
-		super.initStage(stage);
-		this.stage = stage;
-	}
-
 	// ****************************************
 	// UI Events
 	// ****************************************
@@ -169,7 +168,7 @@ public class MainController extends AbstractFXMLViewController {
 	private void didClickCreateNewVault(ActionEvent event) {
 		final FileChooser fileChooser = new FileChooser();
 		fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator vault", "*" + Vault.VAULT_FILE_EXTENSION));
-		final File file = fileChooser.showSaveDialog(stage);
+		final File file = fileChooser.showSaveDialog(mainWindow);
 		if (file == null) {
 			return;
 		}
@@ -194,7 +193,7 @@ public class MainController extends AbstractFXMLViewController {
 	private void didClickAddExistingVaults(ActionEvent event) {
 		final FileChooser fileChooser = new FileChooser();
 		fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator vault", "*" + Vault.VAULT_FILE_EXTENSION));
-		final List<File> files = fileChooser.showOpenMultipleDialog(stage);
+		final List<File> files = fileChooser.showOpenMultipleDialog(mainWindow);
 		if (files != null) {
 			for (final File file : files) {
 				addVault(file.toPath(), false);
@@ -288,7 +287,7 @@ public class MainController extends AbstractFXMLViewController {
 	// ****************************************
 
 	public Binding<String> windowTitle() {
-		return EasyBind.monadic(selectedVault).map(Vault::getName).orElse(resourceBundle.getString("app.name"));
+		return EasyBind.monadic(selectedVault).map(Vault::getName).orElse(localization.getString("app.name"));
 	}
 
 	// ****************************************
@@ -340,13 +339,4 @@ public class MainController extends AbstractFXMLViewController {
 		showUnlockView();
 	}
 
-	/**
-	 * Attempts to make the application window visible.
-	 */
-	public void toFront() {
-		stage.setIconified(false);
-		stage.show();
-		stage.toFront();
-	}
-
 }

+ 5 - 2
main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java

@@ -15,6 +15,7 @@ import javax.inject.Inject;
 import javax.inject.Singleton;
 
 import org.apache.commons.lang3.CharUtils;
+import org.cryptomator.ui.settings.Localization;
 import org.cryptomator.ui.settings.Settings;
 import org.fxmisc.easybind.EasyBind;
 
@@ -26,10 +27,12 @@ import javafx.scene.input.KeyEvent;
 @Singleton
 public class SettingsController extends AbstractFXMLViewController {
 
+	private final Localization localization;
 	private final Settings settings;
 
 	@Inject
-	public SettingsController(Settings settings) {
+	public SettingsController(Localization localization, Settings settings) {
+		this.localization = localization;
 		this.settings = settings;
 	}
 
@@ -57,7 +60,7 @@ public class SettingsController extends AbstractFXMLViewController {
 
 	@Override
 	protected ResourceBundle getFxmlResourceBundle() {
-		return ResourceBundle.getBundle("localization");
+		return localization;
 	}
 
 	private void portDidChange(String newValue) {

+ 14 - 11
main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java

@@ -25,6 +25,7 @@ import org.cryptomator.frontend.FrontendFactory;
 import org.cryptomator.frontend.webdav.mount.WindowsDriveLetters;
 import org.cryptomator.ui.controls.SecPasswordField;
 import org.cryptomator.ui.model.Vault;
+import org.cryptomator.ui.settings.Localization;
 import org.fxmisc.easybind.EasyBind;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -54,6 +55,7 @@ public class UnlockController extends AbstractFXMLViewController {
 	private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class);
 
 	private final Application app;
+	private final Localization localization;
 	private final ExecutorService exec;
 	private final Lazy<FrontendFactory> frontendFactory;
 	private final WindowsDriveLetters driveLetters;
@@ -61,8 +63,9 @@ public class UnlockController extends AbstractFXMLViewController {
 	final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
 
 	@Inject
-	public UnlockController(Application app, ExecutorService exec, Lazy<FrontendFactory> frontendFactory, WindowsDriveLetters driveLetters) {
+	public UnlockController(Application app, Localization localization, ExecutorService exec, Lazy<FrontendFactory> frontendFactory, WindowsDriveLetters driveLetters) {
 		this.app = app;
+		this.localization = localization;
 		this.exec = exec;
 		this.frontendFactory = frontendFactory;
 		this.driveLetters = driveLetters;
@@ -123,7 +126,7 @@ public class UnlockController extends AbstractFXMLViewController {
 
 	@Override
 	protected ResourceBundle getFxmlResourceBundle() {
-		return ResourceBundle.getBundle("localization");
+		return localization;
 	}
 
 	private void vaultChanged(Vault newVault) {
@@ -132,7 +135,7 @@ public class UnlockController extends AbstractFXMLViewController {
 		}
 		passwordField.clear();
 		advancedOptions.setVisible(false);
-		advancedOptionsButton.setText(resourceBundle.getString("unlock.button.advancedOptions.show"));
+		advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.show"));
 		progressIndicator.setVisible(false);
 		if (SystemUtils.IS_OS_WINDOWS) {
 			winDriveLetter.valueProperty().removeListener(driveLetterChangeListener);
@@ -167,9 +170,9 @@ public class UnlockController extends AbstractFXMLViewController {
 	private void didClickAdvancedOptionsButton(ActionEvent event) {
 		advancedOptions.setVisible(!advancedOptions.isVisible());
 		if (advancedOptions.isVisible()) {
-			advancedOptionsButton.setText(resourceBundle.getString("unlock.button.advancedOptions.hide"));
+			advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.hide"));
 		} else {
-			advancedOptionsButton.setText(resourceBundle.getString("unlock.button.advancedOptions.show"));
+			advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.show"));
 		}
 	}
 
@@ -203,7 +206,7 @@ public class UnlockController extends AbstractFXMLViewController {
 		@Override
 		public String toString(Character letter) {
 			if (letter == null) {
-				return resourceBundle.getString("unlock.choicebox.winDriveLetter.auto");
+				return localization.getString("unlock.choicebox.winDriveLetter.auto");
 			} else {
 				return Character.toString(letter) + ":";
 			}
@@ -211,7 +214,7 @@ public class UnlockController extends AbstractFXMLViewController {
 
 		@Override
 		public Character fromString(String string) {
-			if (resourceBundle.getString("unlock.choicebox.winDriveLetter.auto").equals(string)) {
+			if (localization.getString("unlock.choicebox.winDriveLetter.auto").equals(string)) {
 				return null;
 			} else {
 				return CharUtils.toCharacterObject(string);
@@ -280,7 +283,7 @@ public class UnlockController extends AbstractFXMLViewController {
 			vault.get().reveal();
 		} catch (InvalidPassphraseException e) {
 			Platform.runLater(() -> {
-				messageText.setText(resourceBundle.getString("unlock.errorMessage.wrongPassword"));
+				messageText.setText(localization.getString("unlock.errorMessage.wrongPassword"));
 				passwordField.requestFocus();
 			});
 		} catch (UnsupportedVaultFormatException e) {
@@ -288,15 +291,15 @@ public class UnlockController extends AbstractFXMLViewController {
 			Platform.runLater(() -> {
 				downloadsPageLink.setVisible(true);
 				if (e.isVaultOlderThanSoftware()) {
-					messageText.setText(resourceBundle.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
+					messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
 				} else if (e.isSoftwareOlderThanVault()) {
-					messageText.setText(resourceBundle.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
+					messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
 				}
 			});
 		} catch (FrontendCreationFailedException | CommandFailedException e) {
 			LOG.error("Decryption failed for technical reasons.", e);
 			Platform.runLater(() -> {
-				messageText.setText(resourceBundle.getString("unlock.errorMessage.mountingFailed"));
+				messageText.setText(localization.getString("unlock.errorMessage.mountingFailed"));
 			});
 		} finally {
 			Platform.runLater(() -> {

+ 7 - 4
main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java

@@ -18,6 +18,7 @@ import javax.inject.Provider;
 
 import org.cryptomator.frontend.CommandFailedException;
 import org.cryptomator.ui.model.Vault;
+import org.cryptomator.ui.settings.Localization;
 import org.cryptomator.ui.util.ActiveWindowStyleSupport;
 import org.fxmisc.easybind.EasyBind;
 
@@ -50,6 +51,7 @@ public class UnlockedController extends AbstractFXMLViewController {
 	private static final int IO_SAMPLING_STEPS = 100;
 	private static final double IO_SAMPLING_INTERVAL = 0.25;
 
+	private final Localization localization;
 	private final Stage macWarningsWindow = new Stage();
 	private final MacWarningsController macWarningsController;
 	private final ExecutorService exec;
@@ -58,7 +60,8 @@ public class UnlockedController extends AbstractFXMLViewController {
 	private Timeline ioAnimation;
 
 	@Inject
-	public UnlockedController(Provider<MacWarningsController> macWarningsControllerProvider, ExecutorService exec) {
+	public UnlockedController(Localization localization, Provider<MacWarningsController> macWarningsControllerProvider, ExecutorService exec) {
+		this.localization = localization;
 		this.macWarningsController = macWarningsControllerProvider.get();
 		this.exec = exec;
 
@@ -96,7 +99,7 @@ public class UnlockedController extends AbstractFXMLViewController {
 
 	@Override
 	protected ResourceBundle getFxmlResourceBundle() {
-		return ResourceBundle.getBundle("localization");
+		return localization;
 	}
 
 	private void vaultChanged(Vault newVault) {
@@ -125,7 +128,7 @@ public class UnlockedController extends AbstractFXMLViewController {
 				vault.get().unmount();
 			} catch (CommandFailedException e) {
 				Platform.runLater(() -> {
-					messageLabel.setText(resourceBundle.getString("unlocked.label.unmountFailed"));
+					messageLabel.setText(localization.getString("unlocked.label.unmountFailed"));
 				});
 				return;
 			}
@@ -151,7 +154,7 @@ public class UnlockedController extends AbstractFXMLViewController {
 				vault.get().reveal();
 			} catch (CommandFailedException e) {
 				Platform.runLater(() -> {
-					messageLabel.setText(resourceBundle.getString("unlocked.label.revealFailed"));
+					messageLabel.setText(localization.getString("unlocked.label.revealFailed"));
 				});
 			}
 		});

+ 7 - 4
main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java

@@ -30,6 +30,7 @@ import org.apache.commons.httpclient.methods.GetMethod;
 import org.apache.commons.httpclient.params.HttpClientParams;
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.ui.settings.Localization;
 import org.cryptomator.ui.settings.Settings;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -54,13 +55,15 @@ public class WelcomeController extends AbstractFXMLViewController {
 	private static final Logger LOG = LoggerFactory.getLogger(WelcomeController.class);
 
 	private final Application app;
+	private final Localization localization;
 	private final Settings settings;
 	private final Comparator<String> semVerComparator;
 	private final ExecutorService executor;
 
 	@Inject
-	public WelcomeController(Application app, Settings settings, @Named("SemVer") Comparator<String> semVerComparator, ExecutorService executor) {
+	public WelcomeController(Application app, Localization localization, Settings settings, @Named("SemVer") Comparator<String> semVerComparator, ExecutorService executor) {
 		this.app = app;
+		this.localization = localization;
 		this.settings = settings;
 		this.semVerComparator = semVerComparator;
 		this.executor = executor;
@@ -99,7 +102,7 @@ public class WelcomeController extends AbstractFXMLViewController {
 
 	@Override
 	protected ResourceBundle getFxmlResourceBundle() {
-		return ResourceBundle.getBundle("localization");
+		return localization;
 	}
 
 	// ****************************************
@@ -115,7 +118,7 @@ public class WelcomeController extends AbstractFXMLViewController {
 			return;
 		}
 		Platform.runLater(() -> {
-			checkForUpdatesStatus.setText(resourceBundle.getString("welcome.checkForUpdates.label.currentlyChecking"));
+			checkForUpdatesStatus.setText(localization.getString("welcome.checkForUpdates.label.currentlyChecking"));
 			checkForUpdatesIndicator.setVisible(true);
 		});
 		final HttpClient client = new HttpClient();
@@ -162,7 +165,7 @@ public class WelcomeController extends AbstractFXMLViewController {
 		final String currentVersion = applicationVersion().orElse(null);
 		LOG.debug("Current version: {}, lastest version: {}", currentVersion, latestVersion);
 		if (currentVersion != null && semVerComparator.compare(currentVersion, latestVersion) < 0) {
-			final String msg = String.format(resourceBundle.getString("welcome.newVersionMessage"), latestVersion, currentVersion);
+			final String msg = String.format(localization.getString("welcome.newVersionMessage"), latestVersion, currentVersion);
 			Platform.runLater(() -> {
 				this.updateLink.setText(msg);
 				this.updateLink.setVisible(true);

+ 27 - 0
main/ui/src/main/java/org/cryptomator/ui/settings/Localization.java

@@ -0,0 +1,27 @@
+package org.cryptomator.ui.settings;
+
+import java.util.Enumeration;
+import java.util.ResourceBundle;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+@Singleton
+public class Localization extends ResourceBundle {
+
+	@Inject
+	public Localization() {
+		this.parent = ResourceBundle.getBundle("localization");
+	}
+
+	@Override
+	protected Object handleGetObject(String key) {
+		return parent.getObject(key);
+	}
+
+	@Override
+	public Enumeration<String> getKeys() {
+		return parent.getKeys();
+	}
+
+}

+ 13 - 1
main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java

@@ -17,13 +17,14 @@ import org.cryptomator.ui.model.Vault;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonPropertyOrder;
 
-@JsonPropertyOrder(value = {"directories", "checkForUpdatesEnabled", "port"})
+@JsonPropertyOrder(value = {"directories", "checkForUpdatesEnabled", "port", "numTrayNotifications"})
 public class Settings implements Serializable {
 
 	private static final long serialVersionUID = 7609959894417878744L;
 	public static final int MIN_PORT = 1024;
 	public static final int MAX_PORT = 65535;
 	public static final int DEFAULT_PORT = 0;
+	public static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3;
 
 	@JsonProperty("directories")
 	private List<Vault> directories;
@@ -34,6 +35,9 @@ public class Settings implements Serializable {
 	@JsonProperty("port")
 	private Integer port;
 
+	@JsonProperty("numTrayNotifications")
+	private Integer numTrayNotifications;
+
 	/**
 	 * Package-private constructor; use {@link SettingsProvider}.
 	 */
@@ -82,4 +86,12 @@ public class Settings implements Serializable {
 		return port == DEFAULT_PORT || port >= MIN_PORT && port <= MAX_PORT;
 	}
 
+	public Integer getNumTrayNotifications() {
+		return numTrayNotifications == null ? DEFAULT_NUM_TRAY_NOTIFICATIONS : numTrayNotifications;
+	}
+
+	public void setNumTrayNotifications(Integer numTrayNotifications) {
+		this.numTrayNotifications = numTrayNotifications;
+	}
+
 }

+ 20 - 24
main/ui/src/main/java/org/cryptomator/ui/util/DeferredCloser.java

@@ -16,7 +16,7 @@ import java.util.concurrent.ConcurrentSkipListMap;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
 
-import org.cryptomator.ui.controllers.MainController;
+import org.cryptomator.common.ConsumerThrowingException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -41,23 +41,8 @@ import com.google.common.annotations.VisibleForTesting;
  * @author Tillmann Gaida
  */
 public class DeferredCloser implements AutoCloseable {
-	public static interface Closer<T> {
-		void close(T object) throws Exception;
-	}
-
-	static class EmptyResource<T> implements DeferredClosable<T> {
-		@Override
-		public Optional<T> get() {
-			return Optional.empty();
-		}
-
-		@Override
-		public void close() {
-
-		}
-	}
 
-	private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
+	private static final Logger LOG = LoggerFactory.getLogger(DeferredCloser.class);
 
 	@VisibleForTesting
 	final Map<Long, ManagedResource<?>> cleanups = new ConcurrentSkipListMap<>();
@@ -65,13 +50,13 @@ public class DeferredCloser implements AutoCloseable {
 	@VisibleForTesting
 	final AtomicLong counter = new AtomicLong();
 
-	public class ManagedResource<T> implements DeferredClosable<T> {
+	private class ManagedResource<T> implements DeferredClosable<T> {
 		private final long number = counter.incrementAndGet();
 
 		private final AtomicReference<T> object = new AtomicReference<>();
-		private final Closer<T> closer;
+		private final ConsumerThrowingException<T, Exception> closer;
 
-		ManagedResource(T object, Closer<T> closer) {
+		public ManagedResource(T object, ConsumerThrowingException<T, Exception> closer) {
 			super();
 			this.object.set(object);
 			this.closer = closer;
@@ -82,11 +67,10 @@ public class DeferredCloser implements AutoCloseable {
 			final T oldObject = object.getAndSet(null);
 			if (oldObject != null) {
 				cleanups.remove(number);
-
 				try {
-					closer.close(oldObject);
+					closer.accept(oldObject);
 				} catch (Exception e) {
-					LOG.error("exception closing resource", e);
+					LOG.error("Closing resource failed.", e);
 				}
 			}
 		}
@@ -109,7 +93,7 @@ public class DeferredCloser implements AutoCloseable {
 		}
 	}
 
-	public <T> DeferredClosable<T> closeLater(T object, Closer<T> closer) {
+	public <T> DeferredClosable<T> closeLater(T object, ConsumerThrowingException<T, Exception> closer) {
 		Objects.requireNonNull(object);
 		Objects.requireNonNull(closer);
 		final ManagedResource<T> resource = new ManagedResource<T>(object, closer);
@@ -130,4 +114,16 @@ public class DeferredCloser implements AutoCloseable {
 	public static <T> DeferredClosable<T> empty() {
 		return (DeferredClosable<T>) EMPTY_RESOURCE;
 	}
+
+	static class EmptyResource<T> implements DeferredClosable<T> {
+		@Override
+		public Optional<T> get() {
+			return Optional.empty();
+		}
+
+		@Override
+		public void close() {
+
+		}
+	}
 }

+ 27 - 41
main/ui/src/main/java/org/cryptomator/ui/util/TrayIconUtil.java

@@ -18,77 +18,67 @@ import java.awt.TrayIcon;
 import java.awt.TrayIcon.MessageType;
 import java.awt.event.ActionEvent;
 import java.io.IOException;
-import java.util.ResourceBundle;
 import java.util.concurrent.TimeUnit;
 
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
 import javax.script.ScriptEngine;
 import javax.script.ScriptEngineManager;
 import javax.script.ScriptException;
 import javax.swing.SwingUtilities;
 
 import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.ui.settings.Localization;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javafx.application.Platform;
 import javafx.stage.Stage;
 
+@Singleton
 public final class TrayIconUtil {
 
-	private static TrayIconUtil INSTANCE;
 	private static final Logger LOG = LoggerFactory.getLogger(TrayIconUtil.class);
 
-	private final Stage mainApplicationWindow;
-	private final ResourceBundle rb;
-	private final Runnable exitCommand;
+	private final Stage mainWindow;
+	private final Localization localization;
 
-	/**
-	 * This will add an icon to the system tray and modify the application shutdown procedure. Depending on
-	 * {@link Platform#isImplicitExit()} the application may still be running, allowing shutdown using the tray menu.
-	 */
-	public synchronized static void init(Stage mainApplicationWindow, ResourceBundle rb, Runnable exitCommand) {
-		if (INSTANCE == null && SystemTray.isSupported()) {
-			INSTANCE = new TrayIconUtil(mainApplicationWindow, rb, exitCommand);
-		}
-	}
-
-	private TrayIconUtil(Stage mainApplicationWindow, ResourceBundle rb, Runnable exitCommand) {
-		this.mainApplicationWindow = mainApplicationWindow;
-		this.rb = rb;
-		this.exitCommand = exitCommand;
-
-		initTrayIcon();
+	@Inject
+	public TrayIconUtil(@Named("mainWindow") Stage mainWindow, Localization localization) {
+		this.mainWindow = mainWindow;
+		this.localization = localization;
 	}
 
-	private void initTrayIcon() {
-		final TrayIcon trayIcon = createTrayIcon();
+	public void initTrayIcon(Runnable exitCommand) {
+		final TrayIcon trayIcon = createTrayIcon(exitCommand);
 		try {
 			SystemTray.getSystemTray().add(trayIcon);
-			mainApplicationWindow.setOnCloseRequest((e) -> {
+			mainWindow.setOnCloseRequest((e) -> {
 				if (Platform.isImplicitExit()) {
 					exitCommand.run();
 				} else {
-					mainApplicationWindow.close();
+					mainWindow.close();
 					this.showTrayNotification(trayIcon);
 				}
 			});
 		} catch (SecurityException | AWTException ex) {
 			// not working? then just go ahead and close the app
-			mainApplicationWindow.setOnCloseRequest((ev) -> {
+			mainWindow.setOnCloseRequest((ev) -> {
 				exitCommand.run();
 			});
 		}
 	}
 
-	private TrayIcon createTrayIcon() {
+	private TrayIcon createTrayIcon(Runnable exitCommand) {
 		final PopupMenu popup = new PopupMenu();
 
-		final MenuItem showItem = new MenuItem(rb.getString("tray.menu.open"));
+		final MenuItem showItem = new MenuItem(localization.getString("tray.menu.open"));
 		showItem.addActionListener(this::restoreFromTray);
 		popup.add(showItem);
 
-		final MenuItem exitItem = new MenuItem(rb.getString("tray.menu.quit"));
-		exitItem.addActionListener(this::quitFromTray);
+		final MenuItem exitItem = new MenuItem(localization.getString("tray.menu.quit"));
+		exitItem.addActionListener(e -> exitCommand.run());
 		popup.add(exitItem);
 
 		final Image image;
@@ -98,7 +88,7 @@ public final class TrayIconUtil {
 			image = Toolkit.getDefaultToolkit().getImage(TrayIconUtil.class.getResource("/tray_icon.png"));
 		}
 
-		return new TrayIcon(image, rb.getString("app.name"), popup);
+		return new TrayIcon(image, localization.getString("app.name"), popup);
 	}
 
 	/**
@@ -120,8 +110,8 @@ public final class TrayIconUtil {
 	private void showTrayNotification(TrayIcon trayIcon) {
 		final Runnable notificationCmd;
 		if (SystemUtils.IS_OS_MAC_OSX) {
-			final String title = rb.getString("tray.infoMsg.title");
-			final String msg = rb.getString("tray.infoMsg.msg.osx");
+			final String title = localization.getString("tray.infoMsg.title");
+			final String msg = localization.getString("tray.infoMsg.msg.osx");
 			final String notificationCenterAppleScript = String.format("display notification \"%s\" with title \"%s\"", msg, title);
 			notificationCmd = () -> {
 				try {
@@ -137,8 +127,8 @@ public final class TrayIconUtil {
 				}
 			};
 		} else {
-			final String title = rb.getString("tray.infoMsg.title");
-			final String msg = rb.getString("tray.infoMsg.msg");
+			final String title = localization.getString("tray.infoMsg.title");
+			final String msg = localization.getString("tray.infoMsg.msg");
 			notificationCmd = () -> {
 				trayIcon.displayMessage(title, msg, MessageType.INFO);
 			};
@@ -150,13 +140,9 @@ public final class TrayIconUtil {
 
 	private void restoreFromTray(ActionEvent event) {
 		Platform.runLater(() -> {
-			mainApplicationWindow.show();
-			mainApplicationWindow.requestFocus();
+			mainWindow.show();
+			mainWindow.requestFocus();
 		});
 	}
 
-	private void quitFromTray(ActionEvent event) {
-		exitCommand.run();
-	}
-
 }