Bladeren bron

refactored "add vault" functionality, which fixes #14
removed some dependencies
refactored Main/MainApplication, which fixes #16

Sebastian Stenzel 10 jaren geleden
bovenliggende
commit
5e0ebab587
22 gewijzigde bestanden met toevoegingen van 324 en 351 verwijderingen
  1. 0 83
      main/core/src/main/java/org/cryptomator/files/EncryptingFileVisitor.java
  2. 0 4
      main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/FileNamingConventions.java
  3. 2 2
      main/ui/pom.xml
  4. 9 75
      main/ui/src/main/java/org/cryptomator/ui/InitializeController.java
  5. 92 19
      main/ui/src/main/java/org/cryptomator/ui/Main.java
  6. 29 68
      main/ui/src/main/java/org/cryptomator/ui/MainApplication.java
  7. 75 26
      main/ui/src/main/java/org/cryptomator/ui/MainController.java
  8. 4 5
      main/ui/src/main/java/org/cryptomator/ui/UnlockController.java
  9. 4 4
      main/ui/src/main/java/org/cryptomator/ui/UnlockedController.java
  10. 4 4
      main/ui/src/main/java/org/cryptomator/ui/controls/DirectoryListCell.java
  11. 21 23
      main/ui/src/main/java/org/cryptomator/ui/model/Directory.java
  12. 3 3
      main/ui/src/main/java/org/cryptomator/ui/model/DirectoryDeserializer.java
  13. 2 2
      main/ui/src/main/java/org/cryptomator/ui/model/DirectorySerializer.java
  14. 4 4
      main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java
  15. 7 0
      main/ui/src/main/resources/css/linux_theme.css
  16. 24 1
      main/ui/src/main/resources/css/mac_theme.css
  17. 7 0
      main/ui/src/main/resources/css/win_theme.css
  18. 14 4
      main/ui/src/main/resources/fxml/main.fxml
  19. 3 3
      main/ui/src/main/resources/fxml/welcome.fxml
  20. 2 0
      main/ui/src/main/resources/localization.properties
  21. 0 21
      main/ui/src/test/java/org/cryptomator/ui/model/DirectoryTest.java
  22. 18 0
      main/ui/src/test/java/org/cryptomator/ui/model/VaultTest.java

+ 0 - 83
main/core/src/main/java/org/cryptomator/files/EncryptingFileVisitor.java

@@ -1,83 +0,0 @@
-package org.cryptomator.files;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.channels.SeekableByteChannel;
-import java.nio.file.FileVisitResult;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.StandardCopyOption;
-import java.nio.file.StandardOpenOption;
-import java.nio.file.attribute.BasicFileAttributes;
-
-import org.cryptomator.crypto.Cryptor;
-import org.cryptomator.crypto.CryptorIOSupport;
-
-public class EncryptingFileVisitor extends SimpleFileVisitor<Path> implements CryptorIOSupport {
-
-	private final Path rootDir;
-	private final Cryptor cryptor;
-	private final EncryptionDecider encryptionDecider;
-	private Path currentDir;
-
-	public EncryptingFileVisitor(Path rootDir, Cryptor cryptor, EncryptionDecider encryptionDecider) {
-		this.rootDir = rootDir;
-		this.cryptor = cryptor;
-		this.encryptionDecider = encryptionDecider;
-	}
-
-	@Override
-	public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
-		if (rootDir.equals(dir) || encryptionDecider.shouldEncrypt(dir)) {
-			this.currentDir = dir;
-			return FileVisitResult.CONTINUE;
-		} else {
-			return FileVisitResult.SKIP_SUBTREE;
-		}
-	}
-
-	@Override
-	public FileVisitResult visitFile(Path plaintextFile, BasicFileAttributes attrs) throws IOException {
-		if (encryptionDecider.shouldEncrypt(plaintextFile)) {
-			final String plaintextName = plaintextFile.getFileName().toString();
-			final String encryptedName = cryptor.encryptPath(plaintextName, '/', '/', this);
-			final Path encryptedPath = plaintextFile.resolveSibling(encryptedName);
-			final InputStream plaintextIn = Files.newInputStream(plaintextFile, StandardOpenOption.READ);
-			final SeekableByteChannel ciphertextOut = Files.newByteChannel(encryptedPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
-			cryptor.encryptFile(plaintextIn, ciphertextOut);
-			Files.delete(plaintextFile);
-		}
-		return FileVisitResult.CONTINUE;
-	}
-
-	@Override
-	public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
-		if (encryptionDecider.shouldEncrypt(dir)) {
-			final String plaintext = dir.getFileName().toString();
-			final String encrypted = cryptor.encryptPath(plaintext, '/', '/', this);
-			final Path newPath = dir.resolveSibling(encrypted);
-			Files.move(dir, newPath, StandardCopyOption.ATOMIC_MOVE);
-		}
-		return FileVisitResult.CONTINUE;
-	}
-
-	@Override
-	public void writePathSpecificMetadata(String metadataFile, byte[] encryptedMetadata) throws IOException {
-		final Path path = currentDir.resolve(metadataFile);
-		Files.write(path, encryptedMetadata, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
-	}
-
-	@Override
-	public byte[] readPathSpecificMetadata(String metadataFile) throws IOException {
-		final Path path = currentDir.resolve(metadataFile);
-		return Files.readAllBytes(path);
-	}
-
-	/* callback */
-
-	public interface EncryptionDecider {
-		boolean shouldEncrypt(Path path);
-	}
-
-}

+ 0 - 4
main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/FileNamingConventions.java

@@ -63,8 +63,4 @@ interface FileNamingConventions {
 	 */
 	PathMatcher ENCRYPTED_FILE_GLOB_MATCHER = FileSystems.getDefault().getPathMatcher("glob:**/*{" + BASIC_FILE_EXT + "," + LONG_NAME_FILE_EXT + "}");
 
-	/**
-	 * On OSX, folders with this extension are treated as a package.
-	 */
-	String FOLDER_EXTENSION = ".cryptomator";
 }

+ 2 - 2
main/ui/pom.xml

@@ -50,7 +50,7 @@
 			<artifactId>commons-lang3</artifactId>
 		</dependency>
 		
-		<!-- UI -->
+		<!-- UI
 		<dependency>
 			<groupId>org.controlsfx</groupId>
 			<artifactId>controlsfx</artifactId>
@@ -60,7 +60,7 @@
 		<dependency>
 			<groupId>com.github.axet</groupId>
 			<artifactId>desktop</artifactId>
-		</dependency>
+		</dependency> -->
 	</dependencies>
 
 	<build>

+ 9 - 75
main/ui/src/main/java/org/cryptomator/ui/InitializeController.java

@@ -11,39 +11,28 @@ package org.cryptomator.ui;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.net.URL;
-import java.nio.file.DirectoryStream;
 import java.nio.file.FileAlreadyExistsException;
-import java.nio.file.FileVisitor;
 import java.nio.file.Files;
 import java.nio.file.InvalidPathException;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
-import java.util.Optional;
 import java.util.ResourceBundle;
-import java.util.concurrent.Future;
 
 import javafx.beans.value.ObservableValue;
 import javafx.event.ActionEvent;
 import javafx.fxml.FXML;
 import javafx.fxml.Initializable;
-import javafx.scene.control.Alert;
-import javafx.scene.control.Alert.AlertType;
 import javafx.scene.control.Button;
-import javafx.scene.control.ButtonType;
 import javafx.scene.control.Label;
-import javafx.scene.control.ProgressIndicator;
 import javafx.scene.control.TextField;
 import javafx.scene.input.KeyEvent;
 
-import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.CharUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.cryptomator.crypto.aes256.Aes256Cryptor;
-import org.cryptomator.files.EncryptingFileVisitor;
 import org.cryptomator.ui.controls.ClearOnDisableListener;
 import org.cryptomator.ui.controls.SecPasswordField;
-import org.cryptomator.ui.model.Directory;
-import org.cryptomator.ui.util.FXThreads;
+import org.cryptomator.ui.model.Vault;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -53,7 +42,7 @@ public class InitializeController implements Initializable {
 	private static final int MAX_USERNAME_LENGTH = 250;
 
 	private ResourceBundle localization;
-	private Directory directory;
+	private Vault directory;
 	private InitializationListener listener;
 
 	@FXML
@@ -68,9 +57,6 @@ public class InitializeController implements Initializable {
 	@FXML
 	private Button okButton;
 
-	@FXML
-	private ProgressIndicator progressIndicator;
-
 	@FXML
 	private Label messageLabel;
 
@@ -130,43 +116,25 @@ public class InitializeController implements Initializable {
 	@FXML
 	protected void initializeVault(ActionEvent event) {
 		setControlsDisabled(true);
-		if (!isDirectoryEmpty() && !shouldEncryptExistingFiles()) {
-			return;
-		}
 		final String masterKeyFileName = usernameField.getText() + Aes256Cryptor.MASTERKEY_FILE_EXT;
 		final Path masterKeyPath = directory.getPath().resolve(masterKeyFileName);
 		final CharSequence password = passwordField.getCharacters();
-		OutputStream masterKeyOutputStream = null;
-		try {
-			progressIndicator.setVisible(true);
-			masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
+		try (OutputStream masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) {
 			directory.getCryptor().encryptMasterKey(masterKeyOutputStream, password);
-			final Future<?> futureDone = FXThreads.runOnBackgroundThread(this::encryptExistingContents);
-			FXThreads.runOnMainThreadWhenFinished(futureDone, (result) -> {
-				progressIndicator.setVisible(false);
-				progressIndicator.setVisible(false);
-				directory.getCryptor().swipeSensitiveData();
-				if (listener != null) {
-					listener.didInitialize(this);
-				}
-			});
+			if (listener != null) {
+				listener.didInitialize(this);
+			}
 		} catch (FileAlreadyExistsException ex) {
-			setControlsDisabled(false);
-			progressIndicator.setVisible(false);
 			messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
 		} catch (InvalidPathException ex) {
-			setControlsDisabled(false);
-			progressIndicator.setVisible(false);
 			messageLabel.setText(localization.getString("initialize.messageLabel.invalidPath"));
 		} catch (IOException ex) {
-			setControlsDisabled(false);
-			progressIndicator.setVisible(false);
 			LOG.error("I/O Exception", ex);
 		} finally {
+			setControlsDisabled(false);
 			usernameField.setText(null);
 			passwordField.swipe();
 			retypePasswordField.swipe();
-			IOUtils.closeQuietly(masterKeyOutputStream);
 		}
 	}
 
@@ -177,47 +145,13 @@ public class InitializeController implements Initializable {
 		okButton.setDisable(disable);
 	}
 
-	private boolean isDirectoryEmpty() {
-		try {
-			final DirectoryStream<Path> dirContents = Files.newDirectoryStream(directory.getPath());
-			return !dirContents.iterator().hasNext();
-		} catch (IOException e) {
-			LOG.error("Failed to analyze directory.", e);
-			throw new IllegalStateException(e);
-		}
-	}
-
-	private boolean shouldEncryptExistingFiles() {
-		final Alert alert = new Alert(AlertType.CONFIRMATION);
-		alert.setTitle(localization.getString("initialize.alert.directoryIsNotEmpty.title"));
-		alert.setHeaderText(null);
-		alert.setContentText(localization.getString("initialize.alert.directoryIsNotEmpty.content"));
-
-		final Optional<ButtonType> result = alert.showAndWait();
-		return ButtonType.OK.equals(result.get());
-	}
-
-	private void encryptExistingContents() {
-		try {
-			final FileVisitor<Path> visitor = new EncryptingFileVisitor(directory.getPath(), directory.getCryptor(), this::shouldEncryptExistingFile);
-			Files.walkFileTree(directory.getPath(), visitor);
-		} catch (IOException ex) {
-			LOG.error("I/O Exception", ex);
-		}
-	}
-
-	private boolean shouldEncryptExistingFile(Path path) {
-		final String name = path.getFileName().toString();
-		return !directory.getPath().equals(path) && !name.endsWith(Aes256Cryptor.BASIC_FILE_EXT) && !name.endsWith(Aes256Cryptor.METADATA_FILE_EXT) && !name.endsWith(Aes256Cryptor.MASTERKEY_FILE_EXT);
-	}
-
 	/* Getter/Setter */
 
-	public Directory getDirectory() {
+	public Vault getDirectory() {
 		return directory;
 	}
 
-	public void setDirectory(Directory directory) {
+	public void setDirectory(Vault directory) {
 		this.directory = directory;
 	}
 

+ 92 - 19
main/ui/src/main/java/org/cryptomator/ui/Main.java

@@ -5,11 +5,17 @@
  * 
  * Contributors:
  *     Tillmann Gaida - initial implementation
+ *     Sebastian Stenzel - refactoring
  ******************************************************************************/
 package org.cryptomator.ui;
 
 import java.io.File;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.function.Consumer;
 
@@ -18,35 +24,40 @@ import javafx.application.Application;
 import org.apache.commons.lang3.SystemUtils;
 import org.cryptomator.ui.util.SingleInstanceManager;
 import org.cryptomator.ui.util.SingleInstanceManager.RemoteInstance;
+import org.eclipse.jetty.util.ConcurrentHashSet;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.github.axet.desktop.os.mac.AppleHandlers;
-
 public class Main {
 	public static final Logger LOG = LoggerFactory.getLogger(MainApplication.class);
 
 	public static final CompletableFuture<Consumer<File>> OPEN_FILE_HANDLER = new CompletableFuture<>();
 
+	private static final Set<Runnable> SHUTDOWN_TASKS = new ConcurrentHashSet<>();
+	private static final CleanShutdownPerformer CLEAN_SHUTDOWN_PERFORMER = new CleanShutdownPerformer();
+
 	public static void main(String[] args) {
 		if (SystemUtils.IS_OS_MAC_OSX) {
 			/*
-			 * On OSX we're in an awkward position. We need to register a
-			 * handler in the main thread of this application. However, we can't
-			 * even pass objects to the application, so we're forced to use a
-			 * static CompletableFuture for the handler, which actually opens
+			 * On OSX we're in an awkward position. We need to register a handler in the main thread of this application. However, we can't
+			 * even pass objects to the application, so we're forced to use a static CompletableFuture for the handler, which actually opens
 			 * the file in the application.
+			 * 
+			 * Code taken from https://github.com/axet/desktop/blob/master/src/main/java/com/github/axet/desktop/os/mac/AppleHandlers.java
 			 */
 			try {
-				AppleHandlers.getAppleHandlers().addOpenFileListener(file -> {
-					try {
-						OPEN_FILE_HANDLER.get().accept(file);
-					} catch (Exception e) {
-						LOG.error("exception handling file open event", e);
-						throw new RuntimeException(e);
-					}
-				});
-			} catch (RuntimeException e) {
+				final Class<?> applicationClass = Class.forName("com.apple.eawt.Application");
+				final Class<?> openFilesHandlerClass = Class.forName("com.apple.eawt.OpenFilesHandler");
+				final Method getApplication = applicationClass.getMethod("getApplication");
+				final Object application = getApplication.invoke(null);
+				final Method setOpenFileHandler = applicationClass.getMethod("setOpenFileHandler", openFilesHandlerClass);
+
+				final ClassLoader openFilesHandlerClassLoader = openFilesHandlerClass.getClassLoader();
+				final OpenFilesHandlerClassHandler openFilesHandlerHandler = new OpenFilesHandlerClassHandler();
+				final Object openFilesHandlerObject = Proxy.newProxyInstance(openFilesHandlerClassLoader, new Class<?>[] {openFilesHandlerClass}, openFilesHandlerHandler);
+
+				setOpenFileHandler.invoke(application, openFilesHandlerObject);
+			} catch (ReflectiveOperationException | RuntimeException e) {
 				// Since we're trying to call OS-specific code, we'll just have
 				// to hope for the best.
 				LOG.error("exception adding OSX file open handler", e);
@@ -54,9 +65,13 @@ public class Main {
 		}
 
 		/*
-		 * Before starting the application, we check if there is already an
-		 * instance running on this computer. If so, we send our command line
-		 * arguments to that instance and quit.
+		 * Perform certain things on VM termination.
+		 */
+		Runtime.getRuntime().addShutdownHook(CLEAN_SHUTDOWN_PERFORMER);
+
+		/*
+		 * Before starting the application, we check if there is already an instance running on this computer. If so, we send our command
+		 * line arguments to that instance and quit.
 		 */
 		final Optional<RemoteInstance> remoteInstance = SingleInstanceManager.getRemoteInstance(MainApplication.APPLICATION_KEY);
 
@@ -64,7 +79,7 @@ public class Main {
 			try (RemoteInstance instance = remoteInstance.get()) {
 				LOG.info("An instance of Cryptomator is already running at {}.", instance.getRemotePort());
 				for (int i = 0; i < args.length; i++) {
-					remoteInstance.get().sendMessage(args[i], 1000);
+					remoteInstance.get().sendMessage(args[i], 100);
 				}
 			} catch (Exception e) {
 				LOG.error("Error forwarding arguments to remote instance", e);
@@ -73,4 +88,62 @@ public class Main {
 			Application.launch(MainApplication.class, args);
 		}
 	}
+
+	public static void addShutdownTask(Runnable r) {
+		SHUTDOWN_TASKS.add(r);
+	}
+
+	public static void removeShutdownTask(Runnable r) {
+		SHUTDOWN_TASKS.remove(r);
+	}
+
+	private static class CleanShutdownPerformer extends Thread {
+		@Override
+		public void run() {
+			LOG.debug("Shutting down");
+			SHUTDOWN_TASKS.forEach(r -> {
+				try {
+					r.run();
+				} catch (RuntimeException e) {
+					LOG.error("exception while shutting down", e);
+				}
+			});
+			SHUTDOWN_TASKS.clear();
+		}
+	}
+
+	private static void handleOpenFileRequest(File file) {
+		try {
+			OPEN_FILE_HANDLER.get().accept(file);
+		} catch (Exception e) {
+			LOG.error("exception handling file open event for file " + file.getAbsolutePath(), e);
+			throw new RuntimeException(e);
+		}
+	}
+
+	/**
+	 * Handler class taken from https://github.com/axet/desktop/blob/master/src/main/java/com/github/axet/desktop/os/mac/AppleHandlers.java
+	 */
+	private static class OpenFilesHandlerClassHandler implements InvocationHandler {
+		@Override
+		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+			if (method.getName().equals("openFiles")) {
+				final Class<?> openFilesEventClass = Class.forName("com.apple.eawt.AppEvent$OpenFilesEvent");
+				final Method getFiles = openFilesEventClass.getMethod("getFiles");
+				Object e = args[0];
+				try {
+					@SuppressWarnings("unchecked")
+					final List<File> ff = (List<File>) getFiles.invoke(e);
+					for (File f : ff) {
+						handleOpenFileRequest(f);
+					}
+				} catch (RuntimeException ee) {
+					throw ee;
+				} catch (Exception ee) {
+					throw new RuntimeException(ee);
+				}
+			}
+			return null;
+		}
+	}
 }

+ 29 - 68
main/ui/src/main/java/org/cryptomator/ui/MainApplication.java

@@ -13,7 +13,6 @@ import java.nio.file.FileSystems;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ResourceBundle;
-import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
@@ -24,24 +23,19 @@ import javafx.scene.Parent;
 import javafx.scene.Scene;
 import javafx.stage.Stage;
 
-import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.SystemUtils;
-import org.cryptomator.crypto.aes256.Aes256Cryptor;
+import org.cryptomator.ui.model.Vault;
 import org.cryptomator.ui.settings.Settings;
 import org.cryptomator.ui.util.ActiveWindowStyleSupport;
 import org.cryptomator.ui.util.SingleInstanceManager;
 import org.cryptomator.ui.util.SingleInstanceManager.LocalInstance;
 import org.cryptomator.ui.util.TrayIconUtil;
 import org.cryptomator.webdav.WebDavServer;
-import org.eclipse.jetty.util.ConcurrentHashSet;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 public class MainApplication extends Application {
 
-	private static final Set<Runnable> SHUTDOWN_TASKS = new ConcurrentHashSet<>();
-	private static final CleanShutdownPerformer CLEAN_SHUTDOWN_PERFORMER = new CleanShutdownPerformer();
-
 	public static final String APPLICATION_KEY = "CryptomatorGUI";
 
 	private static final Logger LOG = LoggerFactory.getLogger(MainApplication.class);
@@ -53,21 +47,15 @@ public class MainApplication extends Application {
 		ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
 		Platform.runLater(() -> {
 			/*
-			 * This fixes a bug on OSX where the magic file open handler leads
-			 * to no context class loader being set in the AppKit (event) thread
-			 * if the application is not started opening a file.
+			 * This fixes a bug on OSX where the magic file open handler leads to no context class loader being set in the AppKit (event)
+			 * thread if the application is not started opening a file.
 			 */
 			if (Thread.currentThread().getContextClassLoader() == null) {
 				Thread.currentThread().setContextClassLoader(contextClassLoader);
 			}
 		});
 
-		Runtime.getRuntime().addShutdownHook(MainApplication.CLEAN_SHUTDOWN_PERFORMER);
-
 		executorService = Executors.newCachedThreadPool();
-		addShutdownTask(() -> {
-			executorService.shutdown();
-		});
 
 		WebDavServer.getInstance().start();
 		chooseNativeStylesheet();
@@ -91,40 +79,42 @@ public class MainApplication extends Application {
 			handleCommandLineArg(ctrl, arg);
 		}
 
-		if (org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.OSX)) {
+		if (SystemUtils.IS_OS_MAC_OSX) {
 			Main.OPEN_FILE_HANDLER.complete(file -> handleCommandLineArg(ctrl, file.getAbsolutePath()));
 		}
 
-		LocalInstance cryptomatorGuiInstance = SingleInstanceManager.startLocalInstance(APPLICATION_KEY, executorService);
-		addShutdownTask(() -> {
+		final LocalInstance cryptomatorGuiInstance = SingleInstanceManager.startLocalInstance(APPLICATION_KEY, executorService);
+		cryptomatorGuiInstance.registerListener(arg -> handleCommandLineArg(ctrl, arg));
+
+		Main.addShutdownTask(() -> {
 			cryptomatorGuiInstance.close();
+			Settings.save();
+			executorService.shutdown();
 		});
-
-		cryptomatorGuiInstance.registerListener(arg -> handleCommandLineArg(ctrl, arg));
 	}
 
 	void handleCommandLineArg(final MainController ctrl, String arg) {
-		Path file = FileSystems.getDefault().getPath(arg);
-		if (!Files.exists(file)) {
-			try {
-				if (!Files.isDirectory(Files.createDirectories(file))) {
-					return;
-				}
-			} catch (IOException e) {
-				return;
-			}
-			// directory created.
-		} else if (Files.isRegularFile(file)) {
-			if (StringUtils.endsWithIgnoreCase(file.getFileName().toString(), Aes256Cryptor.MASTERKEY_FILE_EXT)) {
-				file = file.getParent();
-			} else {
-				// is a file, but not a masterkey file
-				return;
-			}
+		// only open files with our file extension:
+		if (!arg.endsWith(Vault.VAULT_FILE_EXTENSION)) {
+			LOG.warn("Invalid vault path %s", arg);
+			return;
 		}
-		Path f = file;
+
+		// find correct location:
+		final Path path = FileSystems.getDefault().getPath(arg);
+		final Path vaultPath;
+		if (Files.isDirectory(path)) {
+			vaultPath = path;
+		} else if (Files.isRegularFile(path) && path.getParent().getFileName().toString().endsWith(Vault.VAULT_FILE_EXTENSION)) {
+			vaultPath = path.getParent();
+		} else {
+			LOG.warn("Invalid vault path %s", arg);
+			return;
+		}
+
+		// add vault to ctrl:
 		Platform.runLater(() -> {
-			ctrl.addDirectory(f);
+			ctrl.addVault(vaultPath, true);
 			ctrl.toFront();
 		});
 	}
@@ -142,39 +132,10 @@ public class MainApplication extends Application {
 	private void quit() {
 		Platform.runLater(() -> {
 			WebDavServer.getInstance().stop();
-			CLEAN_SHUTDOWN_PERFORMER.run();
 			Settings.save();
 			Platform.exit();
 			System.exit(0);
 		});
 	}
 
-	@Override
-	public void stop() {
-		CLEAN_SHUTDOWN_PERFORMER.run();
-		Settings.save();
-	}
-
-	public static void addShutdownTask(Runnable r) {
-		SHUTDOWN_TASKS.add(r);
-	}
-
-	public static void removeShutdownTask(Runnable r) {
-		SHUTDOWN_TASKS.remove(r);
-	}
-
-	private static class CleanShutdownPerformer extends Thread {
-		@Override
-		public void run() {
-			SHUTDOWN_TASKS.forEach(r -> {
-				try {
-					r.run();
-				} catch (RuntimeException e) {
-					LOG.error("exception while shutting down", e);
-				}
-			});
-			SHUTDOWN_TASKS.clear();
-		}
-	}
-
 }

+ 75 - 26
main/ui/src/main/java/org/cryptomator/ui/MainController.java

@@ -14,6 +14,7 @@ import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Collection;
+import java.util.List;
 import java.util.ResourceBundle;
 import java.util.stream.Collectors;
 
@@ -25,20 +26,23 @@ import javafx.event.ActionEvent;
 import javafx.fxml.FXML;
 import javafx.fxml.FXMLLoader;
 import javafx.fxml.Initializable;
+import javafx.geometry.Side;
 import javafx.scene.Parent;
 import javafx.scene.control.ContextMenu;
 import javafx.scene.control.ListCell;
 import javafx.scene.control.ListView;
+import javafx.scene.control.ToggleButton;
 import javafx.scene.layout.HBox;
 import javafx.scene.layout.Pane;
-import javafx.stage.DirectoryChooser;
+import javafx.stage.FileChooser;
 import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
 
 import org.cryptomator.ui.InitializeController.InitializationListener;
 import org.cryptomator.ui.UnlockController.UnlockListener;
 import org.cryptomator.ui.UnlockedController.LockListener;
 import org.cryptomator.ui.controls.DirectoryListCell;
-import org.cryptomator.ui.model.Directory;
+import org.cryptomator.ui.model.Vault;
 import org.cryptomator.ui.settings.Settings;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -52,11 +56,17 @@ public class MainController implements Initializable, InitializationListener, Un
 	@FXML
 	private ContextMenu directoryContextMenu;
 
+	@FXML
+	private ContextMenu addVaultContextMenu;
+
 	@FXML
 	private HBox rootPane;
 
 	@FXML
-	private ListView<Directory> directoryList;
+	private ListView<Vault> directoryList;
+
+	@FXML
+	private ToggleButton addVaultButton;
 
 	@FXML
 	private Pane contentPane;
@@ -67,44 +77,83 @@ public class MainController implements Initializable, InitializationListener, Un
 	public void initialize(URL url, ResourceBundle rb) {
 		this.rb = rb;
 
-		final ObservableList<Directory> items = FXCollections.observableList(Settings.load().getDirectories());
+		final ObservableList<Vault> items = FXCollections.observableList(Settings.load().getDirectories());
 		directoryList.setItems(items);
 		directoryList.setCellFactory(this::createDirecoryListCell);
 		directoryList.getSelectionModel().getSelectedItems().addListener(this::selectedDirectoryDidChange);
 	}
 
 	@FXML
-	private void didClickAddDirectory(ActionEvent event) {
-		final DirectoryChooser dirChooser = new DirectoryChooser();
-		final File file = dirChooser.showDialog(stage);
-		if (file != null) {
-			addDirectory(file.toPath());
+	private void didClickAddVault(ActionEvent event) {
+		if (addVaultContextMenu.isShowing()) {
+			addVaultContextMenu.hide();
+		} else {
+			addVaultContextMenu.show(addVaultButton, Side.RIGHT, 0.0, 0.0);
+		}
+	}
+
+	@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();
+		fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator vault", "*.cryptomator"));
+		final File file = fileChooser.showSaveDialog(stage);
+		try {
+			if (file != null) {
+				final Path vaultDir = Files.createDirectory(file.toPath());
+				final Path vaultShortcutFile = vaultDir.resolve(vaultDir.getFileName());
+				Files.createFile(vaultShortcutFile);
+				addVault(vaultDir, true);
+			}
+		} catch (IOException e) {
+			LOG.error("Unable to create vault", e);
+		}
+	}
+
+	@FXML
+	private void didClickAddExistingVaults(ActionEvent event) {
+		final FileChooser fileChooser = new FileChooser();
+		fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator vault", "*.cryptomator"));
+		final List<File> files = fileChooser.showOpenMultipleDialog(stage);
+		if (files != null) {
+			for (final File file : files) {
+				addVault(file.toPath(), false);
+			}
 		}
 	}
 
 	/**
 	 * adds the given directory or selects it if it is already in the list of directories.
 	 * 
-	 * @param file non-null, writable, existing directory
+	 * @param dir non-null, writable, existing directory
 	 */
-	void addDirectory(final Path file) {
-		if (file != null && Files.isWritable(file)) {
-			final Directory dir = new Directory(file);
-			if (!directoryList.getItems().contains(dir)) {
-				directoryList.getItems().add(dir);
+	void addVault(final Path dir, boolean select) {
+		if (dir != null && Files.isWritable(dir)) {
+			final Vault vault = new Vault(dir);
+			if (!directoryList.getItems().contains(vault)) {
+				directoryList.getItems().add(vault);
 			}
-			directoryList.getSelectionModel().select(dir);
+			directoryList.getSelectionModel().select(vault);
 		}
 	}
 
-	private ListCell<Directory> createDirecoryListCell(ListView<Directory> param) {
+	private ListCell<Vault> createDirecoryListCell(ListView<Vault> param) {
 		final DirectoryListCell cell = new DirectoryListCell();
 		cell.setContextMenu(directoryContextMenu);
 		return cell;
 	}
 
-	private void selectedDirectoryDidChange(ListChangeListener.Change<? extends Directory> change) {
-		final Directory selectedDir = directoryList.getSelectionModel().getSelectedItem();
+	private void selectedDirectoryDidChange(ListChangeListener.Change<? extends Vault> change) {
+		final Vault selectedDir = directoryList.getSelectionModel().getSelectedItem();
 		if (selectedDir == null) {
 			stage.setTitle(rb.getString("app.name"));
 			showWelcomeView();
@@ -116,7 +165,7 @@ public class MainController implements Initializable, InitializationListener, Un
 
 	@FXML
 	private void didClickRemoveSelectedEntry(ActionEvent e) {
-		final Directory selectedDir = directoryList.getSelectionModel().getSelectedItem();
+		final Vault selectedDir = directoryList.getSelectionModel().getSelectedItem();
 		directoryList.getItems().remove(selectedDir);
 		directoryList.getSelectionModel().clearSelection();
 	}
@@ -125,7 +174,7 @@ public class MainController implements Initializable, InitializationListener, Un
 	// Subcontroller for right panel
 	// ****************************************
 
-	private void showDirectory(Directory directory) {
+	private void showDirectory(Vault directory) {
 		try {
 			if (directory.isUnlocked()) {
 				this.showUnlockedView(directory);
@@ -155,7 +204,7 @@ public class MainController implements Initializable, InitializationListener, Un
 		this.showView("/fxml/welcome.fxml");
 	}
 
-	private void showInitializeView(Directory directory) {
+	private void showInitializeView(Vault directory) {
 		final InitializeController ctrl = showView("/fxml/initialize.fxml");
 		ctrl.setDirectory(directory);
 		ctrl.setListener(this);
@@ -166,7 +215,7 @@ public class MainController implements Initializable, InitializationListener, Un
 		showUnlockView(ctrl.getDirectory());
 	}
 
-	private void showUnlockView(Directory directory) {
+	private void showUnlockView(Vault directory) {
 		final UnlockController ctrl = showView("/fxml/unlock.fxml");
 		ctrl.setDirectory(directory);
 		ctrl.setListener(this);
@@ -178,7 +227,7 @@ public class MainController implements Initializable, InitializationListener, Un
 		Platform.setImplicitExit(false);
 	}
 
-	private void showUnlockedView(Directory directory) {
+	private void showUnlockedView(Vault directory) {
 		final UnlockedController ctrl = showView("/fxml/unlocked.fxml");
 		ctrl.setDirectory(directory);
 		ctrl.setListener(this);
@@ -194,11 +243,11 @@ public class MainController implements Initializable, InitializationListener, Un
 
 	/* Convenience */
 
-	public Collection<Directory> getDirectories() {
+	public Collection<Vault> getDirectories() {
 		return directoryList.getItems();
 	}
 
-	public Collection<Directory> getUnlockedDirectories() {
+	public Collection<Vault> getUnlockedDirectories() {
 		return getDirectories().stream().filter(d -> d.isUnlocked()).collect(Collectors.toSet());
 	}
 

+ 4 - 5
main/ui/src/main/java/org/cryptomator/ui/UnlockController.java

@@ -37,10 +37,9 @@ import org.cryptomator.crypto.exceptions.DecryptFailedException;
 import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
 import org.cryptomator.crypto.exceptions.WrongPasswordException;
 import org.cryptomator.ui.controls.SecPasswordField;
-import org.cryptomator.ui.model.Directory;
+import org.cryptomator.ui.model.Vault;
 import org.cryptomator.ui.util.FXThreads;
 import org.cryptomator.ui.util.MasterKeyFilter;
-import org.cryptomator.webdav.WebDavServer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -50,7 +49,7 @@ public class UnlockController implements Initializable {
 
 	private ResourceBundle rb;
 	private UnlockListener listener;
-	private Directory directory;
+	private Vault directory;
 
 	@FXML
 	private ComboBox<String> usernameBox;
@@ -186,11 +185,11 @@ public class UnlockController implements Initializable {
 
 	/* Getter/Setter */
 
-	public Directory getDirectory() {
+	public Vault getDirectory() {
 		return directory;
 	}
 
-	public void setDirectory(Directory directory) {
+	public void setDirectory(Vault directory) {
 		this.directory = directory;
 		this.findExistingUsernames();
 		this.checkIntegrity.setSelected(directory.shouldVerifyFileIntegrity());

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

@@ -26,7 +26,7 @@ import javafx.scene.control.Label;
 import javafx.util.Duration;
 
 import org.cryptomator.crypto.CryptorIOSampling;
-import org.cryptomator.ui.model.Directory;
+import org.cryptomator.ui.model.Vault;
 import org.cryptomator.webdav.WebDavServer;
 
 public class UnlockedController implements Initializable {
@@ -35,7 +35,7 @@ public class UnlockedController implements Initializable {
 	private static final double IO_SAMPLING_INTERVAL = 0.25;
 	private ResourceBundle rb;
 	private LockListener listener;
-	private Directory directory;
+	private Vault directory;
 	private Timeline ioAnimation;
 
 	@FXML
@@ -118,11 +118,11 @@ public class UnlockedController implements Initializable {
 
 	/* Getter/Setter */
 
-	public Directory getDirectory() {
+	public Vault getDirectory() {
 		return directory;
 	}
 
-	public void setDirectory(Directory directory) {
+	public void setDirectory(Vault directory) {
 		this.directory = directory;
 		final String msg = String.format(rb.getString("unlocked.messageLabel.runningOnPort"), WebDavServer.getInstance().getPort());
 		messageLabel.setText(msg);

+ 4 - 4
main/ui/src/main/java/org/cryptomator/ui/controls/DirectoryListCell.java

@@ -8,9 +8,9 @@ import javafx.scene.paint.Color;
 import javafx.scene.paint.Paint;
 import javafx.scene.shape.Circle;
 
-import org.cryptomator.ui.model.Directory;
+import org.cryptomator.ui.model.Vault;
 
-public class DirectoryListCell extends DraggableListCell<Directory> implements ChangeListener<Boolean> {
+public class DirectoryListCell extends DraggableListCell<Vault> implements ChangeListener<Boolean> {
 
 	// fill: #FD4943, stroke: #E1443F
 	private static final Color RED_FILL = Color.rgb(253, 73, 67);
@@ -29,8 +29,8 @@ public class DirectoryListCell extends DraggableListCell<Directory> implements C
 	}
 
 	@Override
-	protected void updateItem(Directory item, boolean empty) {
-		final Directory oldItem = super.getItem();
+	protected void updateItem(Vault item, boolean empty) {
+		final Vault oldItem = super.getItem();
 		if (oldItem != null) {
 			oldItem.unlockedProperty().removeListener(this);
 		}

+ 21 - 23
main/ui/src/main/java/org/cryptomator/ui/model/Directory.java

@@ -14,7 +14,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.cryptomator.crypto.Cryptor;
 import org.cryptomator.crypto.SamplingDecorator;
 import org.cryptomator.crypto.aes256.Aes256Cryptor;
-import org.cryptomator.ui.MainApplication;
+import org.cryptomator.ui.Main;
 import org.cryptomator.ui.util.MasterKeyFilter;
 import org.cryptomator.ui.util.mount.CommandFailedException;
 import org.cryptomator.ui.util.mount.WebDavMount;
@@ -27,31 +27,34 @@ import org.slf4j.LoggerFactory;
 import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
 import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 
-@JsonSerialize(using = DirectorySerializer.class)
-@JsonDeserialize(using = DirectoryDeserializer.class)
-public class Directory implements Serializable {
+@JsonSerialize(using = VaultSerializer.class)
+@JsonDeserialize(using = VaultDeserializer.class)
+public class Vault implements Serializable {
 
 	private static final long serialVersionUID = 3754487289683599469L;
-	private static final Logger LOG = LoggerFactory.getLogger(Directory.class);
+	private static final Logger LOG = LoggerFactory.getLogger(Vault.class);
+
+	public static final String VAULT_FILE_EXTENSION = ".cryptomator";
+
 	private final Cryptor cryptor = SamplingDecorator.decorate(new Aes256Cryptor());
 	private final ObjectProperty<Boolean> unlocked = new SimpleObjectProperty<Boolean>(this, "unlocked", Boolean.FALSE);
 	private final Runnable shutdownTask = new ShutdownTask();
 	private final Path path;
 	private boolean verifyFileIntegrity;
-	private String mountName = "Cryptomator";
+	private String mountName;
 	private ServletLifeCycleAdapter webDavServlet;
 	private WebDavMount webDavMount;
 
-	public Directory(final Path path) {
-		if (!Files.isDirectory(path)) {
-			throw new IllegalArgumentException("Not a directory: " + path);
+	public Vault(final Path vaultDirectoryPath) {
+		if (!Files.isDirectory(vaultDirectoryPath) || !vaultDirectoryPath.getFileName().toString().endsWith(VAULT_FILE_EXTENSION)) {
+			throw new IllegalArgumentException("Not a valid vault directory: " + vaultDirectoryPath);
 		}
-		this.path = path;
+		this.path = vaultDirectoryPath;
 
 		try {
 			setMountName(getName());
 		} catch (IllegalArgumentException e) {
-
+			// mount name needs to be set by the user explicitly later
 		}
 	}
 
@@ -65,7 +68,7 @@ public class Directory implements Serializable {
 		}
 		webDavServlet = WebDavServer.getInstance().createServlet(path, verifyFileIntegrity, cryptor, getMountName());
 		if (webDavServlet.start()) {
-			MainApplication.addShutdownTask(shutdownTask);
+			Main.addShutdownTask(shutdownTask);
 			return true;
 		} else {
 			return false;
@@ -74,7 +77,7 @@ public class Directory implements Serializable {
 
 	public void stopServer() {
 		if (webDavServlet != null && webDavServlet.isRunning()) {
-			MainApplication.removeShutdownTask(shutdownTask);
+			Main.removeShutdownTask(shutdownTask);
 			this.unmount();
 			webDavServlet.stop();
 			cryptor.swipeSensitiveData();
@@ -122,14 +125,10 @@ public class Directory implements Serializable {
 	}
 
 	/**
-	 * @return Directory name without preceeding path components
+	 * @return Directory name without preceeding path components and file extension
 	 */
 	public String getName() {
-		String name = path.getFileName().toString();
-		if (StringUtils.endsWithIgnoreCase(name, Aes256Cryptor.FOLDER_EXTENSION)) {
-			name = name.substring(0, name.length() - Aes256Cryptor.FOLDER_EXTENSION.length());
-		}
-		return name;
+		return StringUtils.removeEnd(path.getFileName().toString(), VAULT_FILE_EXTENSION);
 	}
 
 	public Cryptor getCryptor() {
@@ -182,8 +181,7 @@ public class Directory implements Serializable {
 	 * sets the mount name while normalizing it
 	 * 
 	 * @param mountName
-	 * @throws IllegalArgumentException
-	 *             if the name is empty after normalization
+	 * @throws IllegalArgumentException if the name is empty after normalization
 	 */
 	public void setMountName(String mountName) throws IllegalArgumentException {
 		mountName = normalize(mountName);
@@ -202,8 +200,8 @@ public class Directory implements Serializable {
 
 	@Override
 	public boolean equals(Object obj) {
-		if (obj instanceof Directory) {
-			final Directory other = (Directory) obj;
+		if (obj instanceof Vault) {
+			final Vault other = (Vault) obj;
 			return this.path.equals(other.path);
 		} else {
 			return false;

+ 3 - 3
main/ui/src/main/java/org/cryptomator/ui/model/DirectoryDeserializer.java

@@ -10,14 +10,14 @@ import com.fasterxml.jackson.databind.DeserializationContext;
 import com.fasterxml.jackson.databind.JsonDeserializer;
 import com.fasterxml.jackson.databind.JsonNode;
 
-public class DirectoryDeserializer extends JsonDeserializer<Directory> {
+public class VaultDeserializer extends JsonDeserializer<Vault> {
 
 	@Override
-	public Directory deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
+	public Vault deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
 		final JsonNode node = jp.readValueAsTree();
 		final String pathStr = node.get("path").asText();
 		final Path path = FileSystems.getDefault().getPath(pathStr);
-		final Directory dir = new Directory(path);
+		final Vault dir = new Vault(path);
 		final boolean verifyFileIntegrity = node.has("checkIntegrity") ? node.get("checkIntegrity").asBoolean() : false;
 		dir.setVerifyFileIntegrity(verifyFileIntegrity);
 		if (node.has("mountName")) {

+ 2 - 2
main/ui/src/main/java/org/cryptomator/ui/model/DirectorySerializer.java

@@ -7,10 +7,10 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonSerializer;
 import com.fasterxml.jackson.databind.SerializerProvider;
 
-public class DirectorySerializer extends JsonSerializer<Directory> {
+public class VaultSerializer extends JsonSerializer<Vault> {
 
 	@Override
-	public void serialize(Directory value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
+	public void serialize(Vault value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
 		jgen.writeStartObject();
 		jgen.writeStringField("path", value.getPath().toString());
 		jgen.writeBooleanField("checkIntegrity", value.shouldVerifyFileIntegrity());

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

@@ -21,7 +21,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 import org.apache.commons.lang3.SystemUtils;
-import org.cryptomator.ui.model.Directory;
+import org.cryptomator.ui.model.Vault;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -54,7 +54,7 @@ public class Settings implements Serializable {
 		}
 	}
 
-	private List<Directory> directories;
+	private List<Vault> directories;
 
 	private Settings() {
 		// private constructor
@@ -95,14 +95,14 @@ public class Settings implements Serializable {
 
 	/* Getter/Setter */
 
-	public List<Directory> getDirectories() {
+	public List<Vault> getDirectories() {
 		if (directories == null) {
 			directories = new ArrayList<>();
 		}
 		return directories;
 	}
 
-	public void setDirectories(List<Directory> directories) {
+	public void setDirectories(List<Vault> directories) {
 		this.directories = directories;
 	}
 

+ 7 - 0
main/ui/src/main/resources/css/linux_theme.css

@@ -775,6 +775,13 @@ is being used to size a border should also be in pixels.
     -fx-orientation: horizontal;    
 }
 
+.tool-bar.list-related-toolbar {
+	-fx-background-color: transparent;
+    -fx-padding: 0.1em 0;
+    -fx-spacing: 0;
+    -fx-alignment: CENTER_LEFT;
+}
+
 /*******************************************************************************
  *                                                                             *
  * Slider                                                                      *

+ 24 - 1
main/ui/src/main/resources/css/mac_theme.css

@@ -206,7 +206,6 @@
 }
 .button:armed,
 .button:default:armed,
-.toggle-button:armed,
 .menu-button:armed,
 .split-menu-button:armed > .label,
 .split-menu-button > .arrow-button:pressed,
@@ -362,6 +361,30 @@
     -fx-orientation: vertical;
 }
 
+.tool-bar.list-related-toolbar {
+	-fx-background-color: #B4B4B4, #F7F7F7;
+    -fx-background-insets: 0, 0 1 1 1;
+    -fx-padding: 0;
+    -fx-spacing: 0;
+    -fx-alignment: CENTER_LEFT;
+}
+
+.tool-bar.list-related-toolbar .button,
+.tool-bar.list-related-toolbar .toggle-button {
+	-fx-background-color: transparent;
+	-fx-background-insets: 0;
+	-fx-background-radius: 0;
+	-fx-border-color: transparent #B4B4B4 transparent transparent;
+    -fx-border-width: 1;
+}
+
+.tool-bar.list-related-toolbar .button:armed,
+.tool-bar.list-related-toolbar .toggle-button:armed,
+.tool-bar.list-related-toolbar .toggle-button:selected {
+	-fx-background-color: linear-gradient(to bottom, #C0C0C0 0%, #ADADAD 100%);
+}
+
+
 /*******************************************************************************
  *                                                                             *
  * ScrollBar                                                                   *

+ 7 - 0
main/ui/src/main/resources/css/win_theme.css

@@ -358,6 +358,13 @@
     -fx-orientation: vertical;
 }
 
+.tool-bar.list-related-toolbar {
+	-fx-background-color: transparent;
+    -fx-padding: 0.1em 0;
+    -fx-spacing: 0;
+    -fx-alignment: CENTER_LEFT;
+}
+
 /*******************************************************************************
  *                                                                             *
  * ScrollBar                                                                   *

+ 14 - 4
main/ui/src/main/resources/fxml/main.fxml

@@ -13,11 +13,15 @@
 <?import javafx.scene.control.ListView?>
 <?import javafx.scene.layout.Pane?>
 <?import javafx.scene.control.ToolBar?>
-<?import javafx.scene.control.Button?>
+<?import javafx.scene.control.ToggleButton?>
 <?import javafx.scene.control.ContextMenu?>
 <?import javafx.scene.control.MenuItem?>
+<?import javafx.scene.control.Separator?>
+<?import javafx.geometry.Insets?>
 
-<HBox fx:id="rootPane" prefHeight="400.0" prefWidth="600.0" fx:controller="org.cryptomator.ui.MainController" xmlns:fx="http://javafx.com/fxml">
+<HBox fx:id="rootPane" prefHeight="440.0" prefWidth="640.0" fx:controller="org.cryptomator.ui.MainController" xmlns:fx="http://javafx.com/fxml">
+
+	<padding><Insets top="20" right="20" bottom="20" left="20.0"/></padding>
 
 	<fx:define>
 		<fx:include fx:id="welcomeView" source="welcome.fxml" />
@@ -29,15 +33,21 @@
 				<MenuItem text="%main.directoryList.contextMenu.changePassword" disable="true" />
 			</items>
 		</ContextMenu>
+		<ContextMenu fx:id="addVaultContextMenu" onShowing="#willShowAddVaultContextMenu" onHidden="#didHideAddVaultContextMenu">
+			<items>
+				<MenuItem text="%main.addDirectory.contextMenu.new" onAction="#didClickCreateNewVault" />
+				<MenuItem text="%main.addDirectory.contextMenu.open" onAction="#didClickAddExistingVaults" />
+			</items>
+		</ContextMenu>
 	</fx:define>
 
 	<children>
 		<VBox prefWidth="200.0">
 			<children>
 				<ListView fx:id="directoryList" VBox.vgrow="ALWAYS" focusTraversable="false" />
-				<ToolBar VBox.vgrow="NEVER">
+				<ToolBar VBox.vgrow="NEVER" styleClass="list-related-toolbar">
 					<items>
-						<Button text="+" onAction="#didClickAddDirectory" />
+						<ToggleButton text="+" fx:id="addVaultButton" onAction="#didClickAddVault" focusTraversable="false"/>
 					</items>
 				</ToolBar>
 			</children>

+ 3 - 3
main/ui/src/main/resources/fxml/welcome.fxml

@@ -25,9 +25,9 @@
 		<Label AnchorPane.leftAnchor="100.0" AnchorPane.topAnchor="50.0" style="-fx-font-size: 1.5em;" text="%welcome.welcomeLabel"/>
 		<Label AnchorPane.leftAnchor="120.0" AnchorPane.topAnchor="280.0" text="%welcome.addButtonInstructionLabel"/>
 		
-		<QuadCurve AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="300.0" startX="200.0" startY="0.0" endX="0.0" endY="80.0" controlX="180.0" controlY="80.0" fill="TRANSPARENT" stroke="BLACK" strokeWidth="2.0"/>
-		<Line AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="370.0" startX="0.0" endX="10.0" startY="10.0" endY="0.0" strokeWidth="2.0"/>
-		<Line AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="380.0" startX="0.0" endX="10.0" startY="0.0" endY="10.0" strokeWidth="2.0"/>
+		<QuadCurve AnchorPane.leftAnchor="4.0" AnchorPane.topAnchor="300.0" startX="200.0" startY="0.0" endX="0.0" endY="80.0" controlX="180.0" controlY="80.0" fill="TRANSPARENT" stroke="BLACK" strokeWidth="2.0"/>
+		<Line AnchorPane.leftAnchor="4.0" AnchorPane.topAnchor="370.0" startX="0.0" endX="10.0" startY="10.0" endY="0.0" strokeWidth="2.0"/>
+		<Line AnchorPane.leftAnchor="4.0" AnchorPane.topAnchor="380.0" startX="0.0" endX="10.0" startY="0.0" endY="10.0" strokeWidth="2.0"/>
 	</children>
 	
 </AnchorPane>

+ 2 - 0
main/ui/src/main/resources/localization.properties

@@ -13,6 +13,8 @@ app.name=Cryptomator
 main.directoryList.contextMenu.remove=Remove from list
 main.directoryList.contextMenu.addUser=Add user
 main.directoryList.contextMenu.changePassword=Change password
+main.addDirectory.contextMenu.new=Create new vault
+main.addDirectory.contextMenu.open=Add existing vault
 
 
 # welcome.fxml

+ 0 - 21
main/ui/src/test/java/org/cryptomator/ui/model/DirectoryTest.java

@@ -1,21 +0,0 @@
-package org.cryptomator.ui.model;
-
-import static org.junit.Assert.*;
-
-import org.cryptomator.ui.model.Directory;
-import org.junit.Test;
-
-public class DirectoryTest {
-	@Test
-	public void testNormalize() throws Exception {
-		assertEquals("_", Directory.normalize(" "));
-
-		assertEquals("a", Directory.normalize("ä"));
-
-		assertEquals("C", Directory.normalize("Ĉ"));
-
-		assertEquals("_", Directory.normalize(":"));
-
-		assertEquals("", Directory.normalize("汉语"));
-	}
-}

+ 18 - 0
main/ui/src/test/java/org/cryptomator/ui/model/VaultTest.java

@@ -0,0 +1,18 @@
+package org.cryptomator.ui.model;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+public class VaultTest {
+
+	@Test
+	public void testNormalize() throws Exception {
+		assertEquals("_", Vault.normalize(" "));
+		assertEquals("a", Vault.normalize("ä"));
+		assertEquals("C", Vault.normalize("Ĉ"));
+		assertEquals("_", Vault.normalize(":"));
+		assertEquals("", Vault.normalize("汉语"));
+	}
+
+}