From c78a4aa241ec7ae9043ba0965855a213a374bb17 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 11 Jul 2016 22:14:35 +0200 Subject: [PATCH 01/27] updated travis config [ci skip] --- .travis.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3016c1cb8..82b77045e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ env: global: - secure: "Lgj042RD0X3rB8VZVZLWP1GetLhjd3PqI5JbJMlzgHJpDI6RkFIBLN9SWAGmkLPCehIp2zA5tu9+UVy0NNMxm9xz6SyjMCaxS28/fnYEXaNmwwDSF6O6gLUbdxyzoYIFPYOPmFxpzhebqnNIsxaM29oZpgRgUGqosCczQxiB+Ng=" #coveralls - secure: "IfYURwZaDWuBDvyn47n0k1Zod/IQw1FF+CS5nnV08Q+NfC3vGGJMwV8m59XnbfwnWGxwvCaAbk4qP6s6+ijgZNKkvgfFMo3rfTok5zt43bIqgaFOANYV+OC/1c59gYD6ZUxhW5iNgMgU3qdsRtJuwSmfkVv/jKyLGfAbS4kN8BA=" #coverity + - secure: "lV9OwUbHMrMpLUH1CY+Z4puLDdFXytudyPlG1eGRsesdpuG6KM3uQVz6uAtf6lrU8DRbMM/T7ML+PmvQ4UoPPYLdLxESLLBat2qUPOIVBOhTSlCc7I0DmGy04CSvkeMy8dPaQC0ukgNiR7zwoNzfcpGRN/U9S8tziDruuHoZSrg=" #bintray before_install: "curl -L --cookie 'oraclelicense=accept-securebackup-cookie;' http://download.oracle.com/otn-pub/java/jce/8/jce_policy-8.zip -o /tmp/policy.zip && sudo unzip -j -o /tmp/policy.zip *.jar -d `jdk_switcher home oraclejdk8`/jre/lib/security && rm /tmp/policy.zip" @@ -42,7 +43,7 @@ addons: branch_pattern: release.* deploy: - provider: releases +- provider: releases prerelease: false api_key: secure: "ZjE1j93v3qbPIe2YbmhS319aCbMdLQw0HuymmluTurxXsZtn9D4t2+eTr99vBVxGRuB5lzzGezPR5zjk5W7iHF7xhwrawXrFzr2rPJWzWFt0aM+Ry2njU1ROTGGXGTbv4anWeBlgMxLEInTAy/9ytOGNJlec83yc0THpOY2wxnk=" @@ -53,3 +54,13 @@ deploy: on: repo: cryptomator/cryptomator tags: true +- provider: script + script: curl -X POST -u cryptobot:${BINTRAY_API_KEY} -H "Content-Type: application/json" -d '{"name": "${TRAVIS_TAG}", "vcs_tag": "${TRAVIS_TAG}"}' https://api.bintray.com/packages/cryptomator/cryptomator/cryptomator-win/versions + on: + repo: cryptomator/cryptomator + tags: true +- provider: script + script: curl -X POST -u cryptobot:${BINTRAY_API_KEY} -H "Content-Type: application/json" -d '{"name": "${TRAVIS_TAG}", "vcs_tag": "${TRAVIS_TAG}"}' https://api.bintray.com/packages/cryptomator/cryptomator/cryptomator-osx/versions + on: + repo: cryptomator/cryptomator + tags: true From 9a2f602d6caf3f24df9fdcd271223de23d16f883 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 13 Jul 2016 12:37:40 +0200 Subject: [PATCH 02/27] fixes #270 --- .../java/org/cryptomator/ui/controllers/UnlockController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java index ad8cd5cac..759227123 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java @@ -246,6 +246,7 @@ public class UnlockController extends LocalizedFXMLViewController { return; } vault.get().setWinDriveLetter(newValue); + settings.save(); } private void chooseSelectedDriveLetter() { From 7022a80c954018d37f36ef62f76ccb78e0b60ea4 Mon Sep 17 00:00:00 2001 From: Markus Kreusch Date: Thu, 14 Jul 2016 13:58:17 +0200 Subject: [PATCH 03/27] Improved error handling * Created AsyncTaskService to build async UI operations which always log uncaught exceptions * Changed all executor service invocations in the UI to invocations of AsyncTaskService * Improved error handling in some other places, especially try-with-resources * Unlocking read/write locks in NioFile when opening of a channel fails --- .../common/ConsumerThrowingException.java | 2 +- .../common/RunnableThrowingException.java | 2 +- .../org/cryptomator/common/StackTrace.java | 56 ++++++ .../common/SupplierThrowingException.java | 8 + .../java/org/cryptomator/io/FileContents.java | 5 +- .../filesystem/crypto/Masterkeys.java | 13 +- .../inmem/InMemoryReadableFile.java | 6 +- .../cryptomator/filesystem/nio/NioFile.java | 28 ++- .../filesystem/nio/NioFileTest.java | 21 ++- .../cryptomator/ui/CryptomatorComponent.java | 4 + .../ui/controllers/UnlockController.java | 11 +- .../ui/controllers/UnlockedController.java | 46 ++--- .../ui/controllers/UpgradeController.java | 41 ++--- .../ui/controllers/WelcomeController.java | 41 ++--- .../cryptomator/ui/util/AsyncTaskService.java | 174 ++++++++++++++++++ 15 files changed, 351 insertions(+), 107 deletions(-) create mode 100644 main/commons/src/main/java/org/cryptomator/common/StackTrace.java create mode 100644 main/commons/src/main/java/org/cryptomator/common/SupplierThrowingException.java create mode 100644 main/ui/src/main/java/org/cryptomator/ui/util/AsyncTaskService.java diff --git a/main/commons/src/main/java/org/cryptomator/common/ConsumerThrowingException.java b/main/commons/src/main/java/org/cryptomator/common/ConsumerThrowingException.java index 649a2b6b6..242c48572 100644 --- a/main/commons/src/main/java/org/cryptomator/common/ConsumerThrowingException.java +++ b/main/commons/src/main/java/org/cryptomator/common/ConsumerThrowingException.java @@ -1,7 +1,7 @@ package org.cryptomator.common; @FunctionalInterface -public interface ConsumerThrowingException { +public interface ConsumerThrowingException { void accept(T t) throws E; diff --git a/main/commons/src/main/java/org/cryptomator/common/RunnableThrowingException.java b/main/commons/src/main/java/org/cryptomator/common/RunnableThrowingException.java index ec453b886..6af091bf1 100644 --- a/main/commons/src/main/java/org/cryptomator/common/RunnableThrowingException.java +++ b/main/commons/src/main/java/org/cryptomator/common/RunnableThrowingException.java @@ -1,7 +1,7 @@ package org.cryptomator.common; @FunctionalInterface -public interface RunnableThrowingException { +public interface RunnableThrowingException { void run() throws T; diff --git a/main/commons/src/main/java/org/cryptomator/common/StackTrace.java b/main/commons/src/main/java/org/cryptomator/common/StackTrace.java new file mode 100644 index 000000000..8d7b64c8a --- /dev/null +++ b/main/commons/src/main/java/org/cryptomator/common/StackTrace.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * Copyright (c) 2016 Markus Kreusch and others. + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Markus Kreusch - initial implementation + *******************************************************************************/ +package org.cryptomator.common; + +import java.util.stream.Stream; + +/** + * Utility to print stack traces while analyzing issues. + * + * @author Markus Kreusch + */ +public class StackTrace { + + public static void print(String message) { + Thread thread = Thread.currentThread(); + System.err.println(stackTraceFor(message, thread)); + } + + private static String stackTraceFor(String message, Thread thread) { + StringBuilder result = new StringBuilder(); + appendMessageAndThreadName(result, message, thread); + appendStackTrace(thread, result); + return result.toString(); + } + + private static void appendStackTrace(Thread thread, StringBuilder result) { + Stream.of(thread.getStackTrace()) // + .skip(4) // + .forEach(stackTraceElement -> append(stackTraceElement, result)); + } + + private static void appendMessageAndThreadName(StringBuilder result, String message, Thread thread) { + result // + .append('[') // + .append(thread.getName()) // + .append("] ") // + .append(message); + } + + private static void append(StackTraceElement stackTraceElement, StringBuilder result) { + String className = stackTraceElement.getClassName(); + String methodName = stackTraceElement.getMethodName(); + String fileName = stackTraceElement.getFileName(); + int lineNumber = stackTraceElement.getLineNumber(); + result.append('\n') // + .append(className).append(':').append(methodName) // + .append(" (").append(fileName).append(':').append(lineNumber).append(')'); + } + +} diff --git a/main/commons/src/main/java/org/cryptomator/common/SupplierThrowingException.java b/main/commons/src/main/java/org/cryptomator/common/SupplierThrowingException.java new file mode 100644 index 000000000..ed73a51d0 --- /dev/null +++ b/main/commons/src/main/java/org/cryptomator/common/SupplierThrowingException.java @@ -0,0 +1,8 @@ +package org.cryptomator.common; + +@FunctionalInterface +public interface SupplierThrowingException { + + T get() throws E; + +} diff --git a/main/filesystem-api/src/main/java/org/cryptomator/io/FileContents.java b/main/filesystem-api/src/main/java/org/cryptomator/io/FileContents.java index 6152f65cb..32e5adac7 100644 --- a/main/filesystem-api/src/main/java/org/cryptomator/io/FileContents.java +++ b/main/filesystem-api/src/main/java/org/cryptomator/io/FileContents.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.io.Reader; import java.io.UncheckedIOException; import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -28,7 +29,9 @@ public final class FileContents { * @return The file's content interpreted in this FileContents' charset. */ public String readContents(File file) { - try (Reader reader = Channels.newReader(file.openReadable(), charset.newDecoder(), -1)) { + try ( // + ReadableByteChannel channel = file.openReadable(); // + Reader reader = Channels.newReader(channel, charset.newDecoder(), -1)) { return IOUtils.toString(reader); } catch (IOException e) { throw new UncheckedIOException(e); diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/Masterkeys.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/Masterkeys.java index c58c31384..9e8578dc2 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/Masterkeys.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/Masterkeys.java @@ -16,6 +16,7 @@ import java.io.InputStream; import java.io.UncheckedIOException; import java.nio.ByteBuffer; import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; import javax.inject.Inject; import javax.inject.Provider; @@ -52,10 +53,14 @@ class Masterkeys { public Cryptor decrypt(Folder vaultLocation, CharSequence passphrase) throws InvalidPassphraseException { File masterkeyFile = vaultLocation.file(MASTERKEY_FILENAME); Cryptor cryptor = cryptorProvider.get(); + boolean success = false; try { readMasterKey(masterkeyFile, cryptor, passphrase); - } catch (UncheckedIOException e) { - cryptor.destroy(); + success = true; + } finally { + if (!success) { + cryptor.destroy(); + } } return cryptor; } @@ -86,7 +91,9 @@ class Masterkeys { /* I/O */ private static void readMasterKey(File file, Cryptor cryptor, CharSequence passphrase) throws UncheckedIOException, InvalidPassphraseException { - try (InputStream in = Channels.newInputStream(file.openReadable())) { + try ( // + ReadableByteChannel channel = file.openReadable(); // + InputStream in = Channels.newInputStream(channel)) { final byte[] fileContents = IOUtils.toByteArray(in); cryptor.readKeysFromMasterkeyFile(fileContents, passphrase); } catch (IOException e) { diff --git a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryReadableFile.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryReadableFile.java index 6ba8d326b..90eab31f3 100644 --- a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryReadableFile.java +++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryReadableFile.java @@ -64,8 +64,10 @@ class InMemoryReadableFile implements ReadableFile { @Override public void close() throws UncheckedIOException { - open.set(false); - readLock.unlock(); + if (open.get()) { + open.set(false); + readLock.unlock(); + } } } diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFile.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFile.java index 5e38a8e25..e1afc2718 100644 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFile.java +++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFile.java @@ -30,13 +30,21 @@ class NioFile extends NioNode implements File { @Override public ReadableFile openReadable() throws UncheckedIOException { if (lock.getWriteHoldCount() > 0) { - throw new IllegalStateException("Current thread is currently writing this file"); + throw new IllegalStateException("Current thread is currently writing " + path); } if (lock.getReadHoldCount() > 0) { - throw new IllegalStateException("Current thread is already reading this file"); + throw new IllegalStateException("Current thread is already reading " + path); } lock.readLock().lock(); - return instanceFactory.readableNioFile(path, sharedChannel, this::unlockReadLock); + ReadableFile result = null; + try { + result = instanceFactory.readableNioFile(path, sharedChannel, this::unlockReadLock); + } finally { + if (result == null) { + unlockReadLock(); + } + } + return result; } private void unlockReadLock() { @@ -46,13 +54,21 @@ class NioFile extends NioNode implements File { @Override public WritableFile openWritable() throws UncheckedIOException { if (lock.getWriteHoldCount() > 0) { - throw new IllegalStateException("Current thread is already writing this file"); + throw new IllegalStateException("Current thread is already writing " + path); } if (lock.getReadHoldCount() > 0) { - throw new IllegalStateException("Current thread is currently reading this file"); + throw new IllegalStateException("Current thread is currently reading " + path); } lockWriteLock(); - return instanceFactory.writableNioFile(fileSystem(), path, sharedChannel, this::unlockWriteLock); + WritableFile result = null; + try { + result = instanceFactory.writableNioFile(fileSystem(), path, sharedChannel, this::unlockWriteLock); + } finally { + if (result == null) { + unlockWriteLock(); + } + } + return result; } // visible for testing diff --git a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileTest.java b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileTest.java index 489849f24..ec4269fc9 100644 --- a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileTest.java +++ b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileTest.java @@ -99,10 +99,11 @@ public class NioFileTest { @Test public void testOpenReadableInvokedBeforeInvokingAfterCloseOperationThrowsIllegalStateException() { + when(instanceFactory.readableNioFile(same(path), same(channel), any())).thenReturn(mock(ReadableNioFile.class)); inTest.openReadable(); thrown.expect(IllegalStateException.class); - thrown.expectMessage("already reading this file"); + thrown.expectMessage("already reading " + path); inTest.openReadable(); } @@ -111,7 +112,7 @@ public class NioFileTest { public void testOpenReadableInvokedAfterAfterCloseOperationCreatesNewReadableFile() { ReadableNioFile readableNioFile = mock(ReadableNioFile.class); ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); - when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(null, readableNioFile); + when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(mock(ReadableNioFile.class), readableNioFile); inTest.openReadable(); captor.getValue().run(); @@ -122,10 +123,11 @@ public class NioFileTest { @Test public void testOpenReadableInvokedBeforeInvokingAfterCloseOperationOfOpenWritableThrowsIllegalStateException() { + when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), any())).thenReturn(mock(WritableNioFile.class)); inTest.openWritable(); thrown.expect(IllegalStateException.class); - thrown.expectMessage("currently writing this file"); + thrown.expectMessage("currently writing " + path); inTest.openReadable(); } @@ -133,7 +135,7 @@ public class NioFileTest { @Test public void testOpenReadableInvokedAfterInvokingAfterCloseOperationWorks() { ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); - when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(null); + when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(mock(WritableNioFile.class)); inTest.openWritable(); captor.getValue().run(); @@ -154,7 +156,7 @@ public class NioFileTest { public void testOpenWritableInvokedAfterAfterCloseOperationCreatesNewWritableFile() { WritableNioFile writableNioFile = mock(WritableNioFile.class); ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); - when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(null, writableNioFile); + when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(mock(WritableNioFile.class), writableNioFile); inTest.openWritable(); captor.getValue().run(); @@ -165,28 +167,31 @@ public class NioFileTest { @Test public void testOpenWritableInvokedBeforeInvokingAfterCloseOperationThrowsIllegalStateException() { + when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), any())).thenReturn(mock(WritableNioFile.class)); inTest.openWritable(); thrown.expect(IllegalStateException.class); - thrown.expectMessage("already writing this file"); + thrown.expectMessage("already writing " + path); inTest.openWritable(); } @Test public void testOpenWritableInvokedBeforeInvokingAfterCloseOperationFromOpenReadableThrowsIllegalStateException() { + when(instanceFactory.readableNioFile(same(path), same(channel), any())).thenReturn(mock(ReadableNioFile.class)); inTest.openReadable(); thrown.expect(IllegalStateException.class); - thrown.expectMessage("currently reading this file"); + thrown.expectMessage("currently reading " + path); inTest.openWritable(); } @Test public void testOpenWritableInvokedAfterInvokingAfterCloseOperationWorks() { + ReadableNioFile readableNioFile = mock(ReadableNioFile.class); ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); - when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(null); + when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(readableNioFile); inTest.openReadable(); captor.getValue().run(); diff --git a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java index b32935193..095caeb89 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java +++ b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java @@ -14,6 +14,7 @@ import javax.inject.Singleton; import org.cryptomator.ui.controllers.MainController; import org.cryptomator.ui.settings.Localization; +import org.cryptomator.ui.util.AsyncTaskService; import org.cryptomator.ui.util.DeferredCloser; import dagger.Component; @@ -21,6 +22,9 @@ import dagger.Component; @Singleton @Component(modules = CryptomatorModule.class) interface CryptomatorComponent { + + AsyncTaskService asyncTaskService(); + ExecutorService executorService(); DeferredCloser deferredCloser(); diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java index 759227123..d17175dc7 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java @@ -11,7 +11,6 @@ package org.cryptomator.ui.controllers; import java.net.URL; import java.util.Comparator; import java.util.Optional; -import java.util.concurrent.ExecutorService; import javax.inject.Inject; @@ -27,6 +26,7 @@ import org.cryptomator.ui.controls.SecPasswordField; import org.cryptomator.ui.model.Vault; import org.cryptomator.ui.settings.Localization; import org.cryptomator.ui.settings.Settings; +import org.cryptomator.ui.util.AsyncTaskService; import org.fxmisc.easybind.EasyBind; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,7 +56,7 @@ public class UnlockController extends LocalizedFXMLViewController { private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class); private final Application app; - private final ExecutorService exec; + private final AsyncTaskService asyncTaskService; private final Lazy frontendFactory; private final Settings settings; private final WindowsDriveLetters driveLetters; @@ -65,10 +65,10 @@ public class UnlockController extends LocalizedFXMLViewController { private Optional listener = Optional.empty(); @Inject - public UnlockController(Application app, Localization localization, ExecutorService exec, Lazy frontendFactory, Settings settings, WindowsDriveLetters driveLetters) { + public UnlockController(Application app, Localization localization, AsyncTaskService asyncTaskService, Lazy frontendFactory, Settings settings, WindowsDriveLetters driveLetters) { super(localization); this.app = app; - this.exec = exec; + this.asyncTaskService = asyncTaskService; this.frontendFactory = frontendFactory; this.settings = settings; this.driveLetters = driveLetters; @@ -275,8 +275,7 @@ public class UnlockController extends LocalizedFXMLViewController { progressIndicator.setVisible(true); downloadsPageLink.setVisible(false); CharSequence password = passwordField.getCharacters(); - exec.submit(() -> this.unlock(vault.get(), password)); - + asyncTaskService.asyncTaskOf(() -> this.unlock(vault.get(), password)).run(); } private void unlock(Vault vault, CharSequence password) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java index 9cc58ecf1..c0e671d8e 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java @@ -10,7 +10,6 @@ package org.cryptomator.ui.controllers; import java.net.URL; import java.util.Optional; -import java.util.concurrent.ExecutorService; import javax.inject.Inject; import javax.inject.Provider; @@ -19,6 +18,7 @@ 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.cryptomator.ui.util.AsyncTaskService; import org.fxmisc.easybind.EasyBind; import javafx.animation.Animation; @@ -52,16 +52,16 @@ public class UnlockedController extends LocalizedFXMLViewController { private final Stage macWarningsWindow = new Stage(); private final MacWarningsController macWarningsController; - private final ExecutorService exec; + private final AsyncTaskService asyncTaskService; private final ObjectProperty vault = new SimpleObjectProperty<>(); private Optional listener = Optional.empty(); private Timeline ioAnimation; @Inject - public UnlockedController(Localization localization, Provider macWarningsControllerProvider, ExecutorService exec) { + public UnlockedController(Localization localization, Provider macWarningsControllerProvider, AsyncTaskService asyncTaskService) { super(localization); this.macWarningsController = macWarningsControllerProvider.get(); - this.exec = exec; + this.asyncTaskService = asyncTaskService; macWarningsController.vault.bind(this.vault); } @@ -116,18 +116,14 @@ public class UnlockedController extends LocalizedFXMLViewController { @FXML private void didClickLockVault(ActionEvent event) { - exec.submit(() -> { - try { - vault.get().unmount(); - } catch (CommandFailedException e) { - Platform.runLater(() -> { - messageLabel.setText(localization.getString("unlocked.label.unmountFailed")); - }); - return; - } + asyncTaskService.asyncTaskOf(() -> { + vault.get().unmount(); vault.get().deactivateFrontend(); - listener.ifPresent(this::invokeListenerLater); - }); + }).onError(CommandFailedException.class, () -> { + messageLabel.setText(localization.getString("unlocked.label.unmountFailed")); + }).andFinally(() -> { + listener.ifPresent(listener -> listener.didLock(this)); + }).run(); } @FXML @@ -142,15 +138,11 @@ public class UnlockedController extends LocalizedFXMLViewController { @FXML private void didClickRevealVault(ActionEvent event) { - exec.submit(() -> { - try { - vault.get().reveal(); - } catch (CommandFailedException e) { - Platform.runLater(() -> { - messageLabel.setText(localization.getString("unlocked.label.revealFailed")); - }); - } - }); + asyncTaskService.asyncTaskOf(() -> { + vault.get().reveal(); + }).onError(CommandFailedException.class, () -> { + messageLabel.setText(localization.getString("unlocked.label.revealFailed")); + }).run(); } @FXML @@ -258,12 +250,6 @@ public class UnlockedController extends LocalizedFXMLViewController { this.listener = Optional.ofNullable(listener); } - private void invokeListenerLater(LockListener listener) { - Platform.runLater(() -> { - listener.didLock(this); - }); - } - @FunctionalInterface interface LockListener { void didLock(UnlockedController ctrl); diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java index db92d7df4..729f3fdf9 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java @@ -3,7 +3,6 @@ package org.cryptomator.ui.controllers; import java.net.URL; import java.util.Objects; import java.util.Optional; -import java.util.concurrent.ExecutorService; import javax.inject.Inject; @@ -13,11 +12,9 @@ import org.cryptomator.ui.model.UpgradeStrategy; import org.cryptomator.ui.model.UpgradeStrategy.UpgradeFailedException; import org.cryptomator.ui.model.Vault; import org.cryptomator.ui.settings.Localization; +import org.cryptomator.ui.util.AsyncTaskService; import org.fxmisc.easybind.EasyBind; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import javafx.application.Platform; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.event.ActionEvent; @@ -28,19 +25,17 @@ import javafx.scene.control.ProgressIndicator; public class UpgradeController extends LocalizedFXMLViewController { - private static final Logger LOG = LoggerFactory.getLogger(UpgradeController.class); - final ObjectProperty vault = new SimpleObjectProperty<>(); final ObjectProperty> strategy = new SimpleObjectProperty<>(); private final UpgradeStrategies strategies; - private final ExecutorService exec; + private final AsyncTaskService asyncTaskService; private Optional listener = Optional.empty(); @Inject - public UpgradeController(Localization localization, UpgradeStrategies strategies, ExecutorService exec) { + public UpgradeController(Localization localization, UpgradeStrategies strategies, AsyncTaskService asyncTaskService) { super(localization); this.strategies = strategies; - this.exec = exec; + this.asyncTaskService = asyncTaskService; } @FXML @@ -103,26 +98,22 @@ public class UpgradeController extends LocalizedFXMLViewController { Vault v = Objects.requireNonNull(vault.getValue()); passwordField.setDisable(true); progressIndicator.setVisible(true); - exec.submit(() -> { - if (!instruction.isApplicable(v)) { - LOG.error("No upgrade needed for " + v.path().getValue()); - throw new IllegalStateException("No ugprade needed for " + v.path().getValue()); - } - try { - instruction.upgrade(v, passwordField.getCharacters()); - Platform.runLater(this::showNextUpgrade); - } catch (UpgradeFailedException e) { - Platform.runLater(() -> { + asyncTaskService // + .asyncTaskOf(() -> { + if (!instruction.isApplicable(v)) { + throw new IllegalStateException("No ugprade needed for " + v.path().getValue()); + } + instruction.upgrade(v, passwordField.getCharacters()); + }) // + .onSuccess(this::showNextUpgrade) // + .onError(UpgradeFailedException.class, e -> { errorLabel.setText(e.getLocalizedMessage()); - }); - } finally { - Platform.runLater(() -> { + }) // + .andFinally(() -> { progressIndicator.setVisible(false); passwordField.setDisable(false); passwordField.swipe(); - }); - } - }); + }).run(); } private void showNextUpgrade() { diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java index b7c41776a..143a43ef9 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java @@ -8,14 +8,12 @@ ******************************************************************************/ package org.cryptomator.ui.controllers; -import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.Optional; -import java.util.concurrent.ExecutorService; import javax.inject.Inject; import javax.inject.Named; @@ -31,6 +29,7 @@ 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.cryptomator.ui.util.AsyncTaskService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,15 +53,15 @@ public class WelcomeController extends LocalizedFXMLViewController { private final Application app; private final Settings settings; private final Comparator semVerComparator; - private final ExecutorService executor; + private final AsyncTaskService asyncTaskService; @Inject - public WelcomeController(Application app, Localization localization, Settings settings, @Named("SemVer") Comparator semVerComparator, ExecutorService executor) { + public WelcomeController(Application app, Localization localization, Settings settings, @Named("SemVer") Comparator semVerComparator, AsyncTaskService asyncTaskService) { super(localization); this.app = app; this.settings = settings; this.semVerComparator = semVerComparator; - this.executor = executor; + this.asyncTaskService = asyncTaskService; } @FXML @@ -82,7 +81,7 @@ public class WelcomeController extends LocalizedFXMLViewController { if (areUpdatesManagedExternally()) { checkForUpdatesContainer.setVisible(false); } else if (settings.isCheckForUpdatesEnabled()) { - executor.execute(this::checkForUpdates); + this.checkForUpdates(); } } @@ -100,16 +99,14 @@ public class WelcomeController extends LocalizedFXMLViewController { } private void checkForUpdates() { - Platform.runLater(() -> { - checkForUpdatesStatus.setText(localization.getString("welcome.checkForUpdates.label.currentlyChecking")); - checkForUpdatesIndicator.setVisible(true); - }); - final HttpClient client = new HttpClient(); - final HttpMethod method = new GetMethod("https://cryptomator.org/downloads/latestVersion.json"); - client.getParams().setParameter(HttpClientParams.USER_AGENT, "Cryptomator VersionChecker/" + applicationVersion().orElse("SNAPSHOT")); - client.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES); - client.getParams().setConnectionManagerTimeout(5000); - try { + checkForUpdatesStatus.setText(localization.getString("welcome.checkForUpdates.label.currentlyChecking")); + checkForUpdatesIndicator.setVisible(true); + asyncTaskService.asyncTaskOf(() -> { + final HttpClient client = new HttpClient(); + final HttpMethod method = new GetMethod("https://cryptomator.org/downloads/latestVersion.json"); + client.getParams().setParameter(HttpClientParams.USER_AGENT, "Cryptomator VersionChecker/" + applicationVersion().orElse("SNAPSHOT")); + client.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES); + client.getParams().setConnectionManagerTimeout(5000); client.executeMethod(method); final InputStream responseBodyStream = method.getResponseBodyAsStream(); if (method.getStatusCode() == HttpStatus.SC_OK && responseBodyStream != null) { @@ -121,14 +118,10 @@ public class WelcomeController extends LocalizedFXMLViewController { this.compareVersions(map); } } - } catch (IOException e) { - // no error handling required. Maybe next time the version check is successful. - } finally { - Platform.runLater(() -> { - checkForUpdatesStatus.setText(""); - checkForUpdatesIndicator.setVisible(false); - }); - } + }).andFinally(() -> { + checkForUpdatesStatus.setText(""); + checkForUpdatesIndicator.setVisible(false); + }).run(); } private Optional applicationVersion() { diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/AsyncTaskService.java b/main/ui/src/main/java/org/cryptomator/ui/util/AsyncTaskService.java new file mode 100644 index 000000000..e61a5b774 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/util/AsyncTaskService.java @@ -0,0 +1,174 @@ +package org.cryptomator.ui.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.cryptomator.common.ConsumerThrowingException; +import org.cryptomator.common.RunnableThrowingException; +import org.cryptomator.common.SupplierThrowingException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javafx.application.Platform; + +@Singleton +public class AsyncTaskService { + + private static final Logger LOG = LoggerFactory.getLogger(AsyncTaskService.class); + + private final ExecutorService executor; + + @Inject + public AsyncTaskService(ExecutorService executor) { + this.executor = executor; + } + + public AsyncTaskWithoutSuccessHandler asyncTaskOf(RunnableThrowingException task) { + return new AsyncTaskImpl<>(() -> { + task.run(); + return null; + }); + } + + public AsyncTaskWithoutSuccessHandler asyncTaskOf(SupplierThrowingException task) { + return new AsyncTaskImpl<>(task); + } + + private class AsyncTaskImpl implements AsyncTaskWithoutSuccessHandler { + + private final SupplierThrowingException task; + + private ConsumerThrowingException successHandler = value -> { + }; + private List> errorHandlers = new ArrayList<>(); + private RunnableThrowingException finallyHandler = () -> { + }; + + public AsyncTaskImpl(SupplierThrowingException task) { + this.task = task; + } + + @Override + public AsyncTaskWithoutErrorHandler onSuccess(ConsumerThrowingException handler) { + successHandler = handler; + return this; + } + + @Override + public AsyncTaskWithoutErrorHandler onSuccess(RunnableThrowingException handler) { + return onSuccess(result -> handler.run()); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public AsyncTaskWithoutErrorHandler onError(Class type, ConsumerThrowingException handler) { + errorHandlers.add((ErrorHandler) new ErrorHandler<>(type, handler)); + return this; + } + + @Override + public AsyncTaskWithoutErrorHandler onError(Class type, RunnableThrowingException handler) { + return onError(type, error -> handler.run()); + } + + @Override + public AsyncTask andFinally(RunnableThrowingException handler) { + finallyHandler = handler; + return this; + } + + @Override + public void run() { + errorHandlers.add(ErrorHandler.LOGGING_HANDLER); + executor.execute(() -> logExceptions(() -> { + try { + ResultType result = task.get(); + Platform.runLater(() -> { + try { + successHandler.accept(result); + } catch (Throwable e) { + LOG.error("Uncaught exception", e); + } + }); + } catch (Throwable e) { + ErrorHandler errorHandler = errorHandlerFor(e); + Platform.runLater(toRunnableLoggingException(() -> errorHandler.accept(e))); + } finally { + Platform.runLater(toRunnableLoggingException(finallyHandler)); + } + })); + } + + private ErrorHandler errorHandlerFor(Throwable e) { + return errorHandlers.stream().filter(handler -> handler.handles(e)).findFirst().get(); + } + + } + + private static Runnable toRunnableLoggingException(RunnableThrowingException delegate) { + return () -> logExceptions(delegate); + } + + private static void logExceptions(RunnableThrowingException delegate) { + try { + delegate.run(); + } catch (Throwable e) { + LOG.error("Uncaught exception", e); + } + } + + private static class ErrorHandler implements ConsumerThrowingException { + + public static final ErrorHandler LOGGING_HANDLER = new ErrorHandler(Throwable.class, error -> { + LOG.error("Uncaught exception", error); + }); + + private final Class type; + private final ConsumerThrowingException delegate; + + public ErrorHandler(Class type, ConsumerThrowingException delegate) { + this.type = type; + this.delegate = delegate; + } + + public boolean handles(Throwable error) { + return type.isInstance(error); + } + + @Override + public void accept(ErrorType error) throws Throwable { + delegate.accept(error); + } + + } + + public interface AsyncTaskWithoutSuccessHandler extends AsyncTaskWithoutErrorHandler { + + AsyncTaskWithoutErrorHandler onSuccess(ConsumerThrowingException handler); + + AsyncTaskWithoutErrorHandler onSuccess(RunnableThrowingException handler); + + } + + public interface AsyncTaskWithoutErrorHandler extends AsyncTaskWithoutFinallyHandler { + + AsyncTaskWithoutErrorHandler onError(Class type, ConsumerThrowingException handler); + + AsyncTaskWithoutErrorHandler onError(Class type, RunnableThrowingException handler); + + } + + public interface AsyncTaskWithoutFinallyHandler extends AsyncTask { + + AsyncTask andFinally(RunnableThrowingException handler); + + } + + public interface AsyncTask extends Runnable { + } + +} From a8ad335aed1371dcb02f84cbc01ebb0fc6179bbf Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 18 Jul 2016 12:18:47 +0200 Subject: [PATCH 04/27] Update README.md [ci skip] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index fe4ff3890..1b79ec1bf 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Build Status](https://travis-ci.org/cryptomator/cryptomator.svg?branch=master)](https://travis-ci.org/cryptomator/cryptomator) [![Coverity Scan Build Status](https://scan.coverity.com/projects/cryptomator-cryptomator/badge.svg?flat=1)](https://scan.coverity.com/projects/cryptomator-cryptomator) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/2a0adf3cec6a4143b91035d3924178f1)](https://www.codacy.com/app/cryptomator/cryptomator?utm_source=github.com&utm_medium=referral&utm_content=cryptomator/cryptomator&utm_campaign=Badge_Grade) [![Coverage Status](https://coveralls.io/repos/github/cryptomator/cryptomator/badge.svg?branch=master)](https://coveralls.io/github/cryptomator/cryptomator?branch=master) [![Join the chat at https://gitter.im/cryptomator/cryptomator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/cryptomator/cryptomator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Twitter](https://img.shields.io/badge/twitter-@Cryptomator-blue.svg?style=flat)](http://twitter.com/Cryptomator) From f071efe1b95b27858cb8f2000659aee8a06ce3ad Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 25 Jul 2016 10:08:21 +0200 Subject: [PATCH 05/27] allow user to specify whether to use dav:// or webdav:// scheme for Linux GVFS mounts. Fixes #307 --- .../org/cryptomator/frontend/Frontend.java | 7 +- .../frontend/webdav/WebDavFrontend.java | 2 +- .../frontend/webdav/WebDavModule.java | 11 ++ .../webdav/mount/FallbackWebDavMounter.java | 2 +- .../webdav/mount/LinuxGvfsDavMounter.java | 94 ++++++++++++++++ .../webdav/mount/LinuxGvfsWebDavMounter.java | 16 +-- .../mount/MacOsXAppleScriptWebDavMounter.java | 2 +- .../mount/MacOsXShellScriptWebDavMounter.java | 2 +- .../webdav/mount/MountStrategies.java | 105 ------------------ .../webdav/mount/WebDavMounterModule.java | 31 ++++++ .../webdav/mount/WebDavMounterProvider.java | 29 ++--- .../webdav/mount/WebDavMounterStrategy.java | 7 +- .../webdav/mount/WindowsWebDavMounter.java | 8 +- .../frontend/webdav/WebDavComponent.java | 4 +- .../org/cryptomator/ui/CryptomatorModule.java | 11 +- .../ui/controllers/SettingsController.java | 18 +++ .../java/org/cryptomator/ui/model/Vault.java | 3 +- .../org/cryptomator/ui/settings/Settings.java | 14 ++- .../ui/src/main/resources/css/linux_theme.css | 26 +++++ main/ui/src/main/resources/fxml/settings.fxml | 6 + .../ui/src/main/resources/localization/en.txt | 1 + 21 files changed, 245 insertions(+), 154 deletions(-) create mode 100644 main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavModule.java create mode 100644 main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/LinuxGvfsDavMounter.java delete mode 100644 main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MountStrategies.java create mode 100644 main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterModule.java rename main/frontend-webdav/src/{main => test}/java/org/cryptomator/frontend/webdav/WebDavComponent.java (86%) diff --git a/main/frontend-api/src/main/java/org/cryptomator/frontend/Frontend.java b/main/frontend-api/src/main/java/org/cryptomator/frontend/Frontend.java index 2be80c778..25d0290ca 100644 --- a/main/frontend-api/src/main/java/org/cryptomator/frontend/Frontend.java +++ b/main/frontend-api/src/main/java/org/cryptomator/frontend/Frontend.java @@ -14,7 +14,12 @@ import java.util.Optional; public interface Frontend extends AutoCloseable { public enum MountParam { - MOUNT_NAME, HOSTNAME, WIN_DRIVE_LETTER + MOUNT_NAME, HOSTNAME, WIN_DRIVE_LETTER, + + /** + * "dav" or "webdav" + */ + PREFERRED_GVFS_SCHEME } void mount(Map> map) throws CommandFailedException; diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavFrontend.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavFrontend.java index 87ca8abee..50081c846 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavFrontend.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavFrontend.java @@ -45,7 +45,7 @@ class WebDavFrontend implements Frontend { @Override public void mount(Map> mountParams) throws CommandFailedException { - mount = webdavMounterProvider.get().mount(uri, mountParams); + mount = webdavMounterProvider.chooseMounter(mountParams).mount(uri, mountParams); } @Override diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavModule.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavModule.java new file mode 100644 index 000000000..7603d867e --- /dev/null +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavModule.java @@ -0,0 +1,11 @@ +package org.cryptomator.frontend.webdav; + +import org.cryptomator.common.CommonsModule; +import org.cryptomator.frontend.webdav.mount.WebDavMounterModule; + +import dagger.Module; + +@Module(includes = {CommonsModule.class, WebDavMounterModule.class}) +public class WebDavModule { + +} diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/FallbackWebDavMounter.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/FallbackWebDavMounter.java index 7d83f10af..a05d19bd1 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/FallbackWebDavMounter.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/FallbackWebDavMounter.java @@ -23,7 +23,7 @@ import org.cryptomator.frontend.Frontend.MountParam; final class FallbackWebDavMounter implements WebDavMounterStrategy { @Override - public boolean shouldWork() { + public boolean shouldWork(Map> mountParams) { return true; } diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/LinuxGvfsDavMounter.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/LinuxGvfsDavMounter.java new file mode 100644 index 000000000..d8c5224f6 --- /dev/null +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/LinuxGvfsDavMounter.java @@ -0,0 +1,94 @@ +/******************************************************************************* + * Copyright (c) 2014, 2016 Sebastian Stenzel, Markus Kreusch + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Sebastian Stenzel - initial API and implementation + * Markus Kreusch - Refactored WebDavMounter to use strategy pattern + * Mohit Raju - Added fallback schema-name "webdav" when opening file managers + ******************************************************************************/ +package org.cryptomator.frontend.webdav.mount; + +import java.net.URI; +import java.util.Map; +import java.util.Optional; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.frontend.CommandFailedException; +import org.cryptomator.frontend.Frontend.MountParam; +import org.cryptomator.frontend.webdav.mount.command.Script; + +@Singleton +final class LinuxGvfsDavMounter implements WebDavMounterStrategy { + + @Inject + LinuxGvfsDavMounter() { + } + + @Override + public boolean shouldWork(Map> mountParams) { + if (SystemUtils.IS_OS_LINUX) { + Optional prefScheme = mountParams.getOrDefault(MountParam.PREFERRED_GVFS_SCHEME, Optional.empty()); + boolean prefSchemeIsUnspecifiedOrDav = !prefScheme.isPresent() || prefScheme.get().equalsIgnoreCase("dav"); + final Script checkScripts = Script.fromLines("which gvfs-mount xdg-open"); + try { + checkScripts.execute(); + return prefSchemeIsUnspecifiedOrDav; + } catch (CommandFailedException e) { + return false; + } + } else { + return false; + } + } + + @Override + public void warmUp(int serverPort) { + // no-op + } + + @Override + public WebDavMount mount(URI uri, Map> mountParams) throws CommandFailedException { + final Script mountScript = Script.fromLines("set -x", "gvfs-mount \"dav:$DAV_SSP\"").addEnv("DAV_SSP", uri.getRawSchemeSpecificPart()); + mountScript.execute(); + return new LinuxGvfsDavMount(uri); + } + + private static class LinuxGvfsDavMount extends AbstractWebDavMount { + private final URI webDavUri; + private final Script testMountStillExistsScript; + private final Script unmountScript; + + private LinuxGvfsDavMount(URI webDavUri) { + this.webDavUri = webDavUri; + this.testMountStillExistsScript = Script.fromLines("set -x", "test `gvfs-mount --list | grep \"$DAV_SSP\" | wc -l` -eq 1").addEnv("DAV_SSP", webDavUri.getRawSchemeSpecificPart()); + this.unmountScript = Script.fromLines("set -x", "gvfs-mount -u \"dav:$DAV_SSP\"").addEnv("DAV_SSP", webDavUri.getRawSchemeSpecificPart()); + } + + @Override + public void unmount() throws CommandFailedException { + boolean mountStillExists; + try { + testMountStillExistsScript.execute(); + mountStillExists = true; + } catch (CommandFailedException e) { + mountStillExists = false; + } + // only attempt unmount if user didn't unmount manually: + if (mountStillExists) { + unmountScript.execute(); + } + } + + @Override + public void reveal() throws CommandFailedException { + Script.fromLines("set -x", "xdg-open \"webdav:$DAV_SSP\"").addEnv("DAV_SSP", webDavUri.getRawSchemeSpecificPart()).execute(); + } + + } + +} diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/LinuxGvfsWebDavMounter.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/LinuxGvfsWebDavMounter.java index 1feab0d2d..cbeffd5b2 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/LinuxGvfsWebDavMounter.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/LinuxGvfsWebDavMounter.java @@ -30,12 +30,14 @@ final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy { } @Override - public boolean shouldWork() { + public boolean shouldWork(Map> mountParams) { if (SystemUtils.IS_OS_LINUX) { + Optional prefScheme = mountParams.getOrDefault(MountParam.PREFERRED_GVFS_SCHEME, Optional.empty()); + boolean prefSchemeIsUnspecifiedOrWebDav = !prefScheme.isPresent() || prefScheme.get().equalsIgnoreCase("webdav"); final Script checkScripts = Script.fromLines("which gvfs-mount xdg-open"); try { checkScripts.execute(); - return true; + return prefSchemeIsUnspecifiedOrWebDav; } catch (CommandFailedException e) { return false; } @@ -84,15 +86,7 @@ final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy { @Override public void reveal() throws CommandFailedException { - try { - openMountWithWebdavUri("dav:" + webDavUri.getRawSchemeSpecificPart()).execute(); - } catch (CommandFailedException exception) { - openMountWithWebdavUri("webdav:" + webDavUri.getRawSchemeSpecificPart()).execute(); - } - } - - private Script openMountWithWebdavUri(String webdavUri) { - return Script.fromLines("set -x", "xdg-open \"$DAV_URI\"").addEnv("DAV_URI", webdavUri); + Script.fromLines("set -x", "xdg-open \"webdav:$DAV_SSP\"").addEnv("DAV_SSP", webDavUri.getRawSchemeSpecificPart()).execute(); } } diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXAppleScriptWebDavMounter.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXAppleScriptWebDavMounter.java index f381e2e6f..0bd477541 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXAppleScriptWebDavMounter.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXAppleScriptWebDavMounter.java @@ -38,7 +38,7 @@ final class MacOsXAppleScriptWebDavMounter implements WebDavMounterStrategy { } @Override - public boolean shouldWork() { + public boolean shouldWork(Map> mountParams) { return SystemUtils.IS_OS_MAC_OSX && semVerComparator.compare(SystemUtils.OS_VERSION, "10.10") >= 0; } diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXShellScriptWebDavMounter.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXShellScriptWebDavMounter.java index 89e8a86a2..8e834821a 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXShellScriptWebDavMounter.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXShellScriptWebDavMounter.java @@ -37,7 +37,7 @@ final class MacOsXShellScriptWebDavMounter implements WebDavMounterStrategy { } @Override - public boolean shouldWork() { + public boolean shouldWork(Map> mountParams) { return SystemUtils.IS_OS_MAC_OSX && semVerComparator.compare(SystemUtils.OS_VERSION, "10.10") < 0; } diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MountStrategies.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MountStrategies.java deleted file mode 100644 index 3cd1b2a08..000000000 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MountStrategies.java +++ /dev/null @@ -1,105 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2016 Sebastian Stenzel and others. - * This file is licensed under the terms of the MIT license. - * See the LICENSE.txt file for more info. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - *******************************************************************************/ -package org.cryptomator.frontend.webdav.mount; - -import static java.util.Arrays.asList; -import static java.util.Collections.unmodifiableList; - -import java.util.Collection; -import java.util.Iterator; - -import javax.inject.Inject; -import javax.inject.Singleton; - -@Singleton -class MountStrategies implements Collection { - - private final Collection delegate; - - @Inject - MountStrategies(LinuxGvfsWebDavMounter linuxMounter, MacOsXAppleScriptWebDavMounter osxAppleScriptMounter, MacOsXShellScriptWebDavMounter osxShellScriptMounter, WindowsWebDavMounter winMounter) { - delegate = unmodifiableList(asList(linuxMounter, osxAppleScriptMounter, osxShellScriptMounter, winMounter)); - } - - @Override - public int size() { - return delegate.size(); - } - - @Override - public boolean isEmpty() { - return delegate.isEmpty(); - } - - @Override - public boolean contains(Object o) { - return delegate.contains(o); - } - - @Override - public Iterator iterator() { - return delegate.iterator(); - } - - @Override - public Object[] toArray() { - return delegate.toArray(); - } - - @Override - public T[] toArray(T[] a) { - return delegate.toArray(a); - } - - @Override - public boolean add(WebDavMounterStrategy e) { - return delegate.add(e); - } - - @Override - public boolean remove(Object o) { - return delegate.remove(o); - } - - @Override - public boolean containsAll(Collection c) { - return delegate.containsAll(c); - } - - @Override - public boolean addAll(Collection c) { - return delegate.addAll(c); - } - - @Override - public boolean removeAll(Collection c) { - return delegate.removeAll(c); - } - - @Override - public boolean retainAll(Collection c) { - return delegate.retainAll(c); - } - - @Override - public void clear() { - delegate.clear(); - } - - @Override - public boolean equals(Object o) { - return delegate.equals(o); - } - - @Override - public int hashCode() { - return delegate.hashCode(); - } - -} diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterModule.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterModule.java new file mode 100644 index 000000000..c42151f40 --- /dev/null +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterModule.java @@ -0,0 +1,31 @@ +package org.cryptomator.frontend.webdav.mount; + +import java.util.Set; + +import javax.inject.Named; +import javax.inject.Singleton; + +import com.google.common.collect.Sets; + +import dagger.Module; +import dagger.Provides; +import dagger.multibindings.ElementsIntoSet; + +@Module +public class WebDavMounterModule { + + @Provides + @ElementsIntoSet + static Set provideMounters(LinuxGvfsWebDavMounter linuxWebDavMounter, LinuxGvfsDavMounter linuxDavMounter, MacOsXAppleScriptWebDavMounter osxAppleScriptMounter, + MacOsXShellScriptWebDavMounter osxShellScriptMounter, WindowsWebDavMounter winMounter) { + return Sets.newHashSet(linuxWebDavMounter, linuxDavMounter, osxAppleScriptMounter, osxShellScriptMounter, winMounter); + } + + @Provides + @Singleton + @Named("fallback") + static WebDavMounterStrategy provideFallbackStrategy() { + return new FallbackWebDavMounter(); + } + +} diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterProvider.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterProvider.java index ae5341f9d..a1bdf1745 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterProvider.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterProvider.java @@ -10,34 +10,35 @@ package org.cryptomator.frontend.webdav.mount; import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import javax.inject.Inject; -import javax.inject.Provider; +import javax.inject.Named; import javax.inject.Singleton; +import org.cryptomator.frontend.Frontend.MountParam; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Singleton -public class WebDavMounterProvider implements Provider { +public class WebDavMounterProvider { private static final Logger LOG = LoggerFactory.getLogger(WebDavMounterProvider.class); - private final WebDavMounterStrategy choosenStrategy; + private final Collection availableStrategies; + private final WebDavMounterStrategy fallbackStrategy; @Inject - public WebDavMounterProvider(MountStrategies availableStrategies) { - this.choosenStrategy = getStrategyWhichShouldWork(availableStrategies); + public WebDavMounterProvider(Set availableStrategies, @Named("fallback") WebDavMounterStrategy fallbackStrategy) { + this.availableStrategies = availableStrategies; + this.fallbackStrategy = fallbackStrategy; } - @Override - public WebDavMounter get() { - return this.choosenStrategy; - } - - private WebDavMounterStrategy getStrategyWhichShouldWork(Collection availableStrategies) { - WebDavMounterStrategy strategy = availableStrategies.stream().filter(WebDavMounterStrategy::shouldWork).findFirst().orElse(new FallbackWebDavMounter()); - LOG.info("Using {}", strategy.getClass().getSimpleName()); - return strategy; + public WebDavMounter chooseMounter(Map> mountParams) { + WebDavMounterStrategy result = availableStrategies.stream().filter(strategy -> strategy.shouldWork(mountParams)).findFirst().orElse(fallbackStrategy); + LOG.info("Using {}", result.getClass().getSimpleName()); + return result; } } diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterStrategy.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterStrategy.java index 99ec00162..ae5d6b687 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterStrategy.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WebDavMounterStrategy.java @@ -9,6 +9,11 @@ ******************************************************************************/ package org.cryptomator.frontend.webdav.mount; +import java.util.Map; +import java.util.Optional; + +import org.cryptomator.frontend.Frontend.MountParam; + /** * A strategy able to mount a webdav share and display it to the user. * @@ -19,7 +24,7 @@ interface WebDavMounterStrategy extends WebDavMounter { /** * @return {@code false} if this {@code WebDavMounterStrategy} can not work on the local machine, {@code true} if it could work */ - boolean shouldWork(); + boolean shouldWork(Map> mountParams); /** * Invoked when mounting strategy gets chosen. On some operating systems (we don't want to tell names here) mounting might be faster, diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsWebDavMounter.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsWebDavMounter.java index ee8fd5b96..89268e864 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsWebDavMounter.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsWebDavMounter.java @@ -61,7 +61,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy { } @Override - public boolean shouldWork() { + public boolean shouldWork(Map> mountParams) { return SystemUtils.IS_OS_WINDOWS; } @@ -102,7 +102,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy { mountScript.addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\')); return mountScript.execute(MOUNT_TIMEOUT_SECONDS, TimeUnit.SECONDS); } - + private void addProxyOverrides(URI uri) throws IOException, CommandFailedException { try { // get existing value for ProxyOverride key from reqistry: @@ -110,7 +110,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy { Process queryCmd = query.start(); String queryStdOut = IOUtils.toString(queryCmd.getInputStream(), StandardCharsets.UTF_8); int queryResult = queryCmd.waitFor(); - + // determine new value for ProxyOverride key: Set overrides = new HashSet<>(); Matcher matcher = REG_QUERY_PROXY_OVERRIDES_PATTERN.matcher(queryStdOut); @@ -122,7 +122,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy { overrides.add(""); overrides.add(uri.getHost()); overrides.add(uri.getHost() + ":" + uri.getPort()); - + // set new value: String overridesStr = StringUtils.join(overrides, ';'); ProcessBuilder add = new ProcessBuilder("reg", "add", "\"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\"", "/v", "ProxyOverride", "/d", "\"" + overridesStr + "\"", "/f"); diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavComponent.java b/main/frontend-webdav/src/test/java/org/cryptomator/frontend/webdav/WebDavComponent.java similarity index 86% rename from main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavComponent.java rename to main/frontend-webdav/src/test/java/org/cryptomator/frontend/webdav/WebDavComponent.java index 68f1a43e7..cbeae7914 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavComponent.java +++ b/main/frontend-webdav/src/test/java/org/cryptomator/frontend/webdav/WebDavComponent.java @@ -10,12 +10,10 @@ package org.cryptomator.frontend.webdav; import javax.inject.Singleton; -import org.cryptomator.common.CommonsModule; - import dagger.Component; @Singleton -@Component(modules = {CommonsModule.class}) +@Component(modules = {WebDavModule.class}) public interface WebDavComponent { WebDavServer server(); diff --git a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java index 3740b566d..457cd6d71 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java @@ -17,9 +17,8 @@ import javax.inject.Singleton; import org.cryptomator.common.CommonsModule; import org.cryptomator.crypto.engine.impl.CryptoEngineModule; import org.cryptomator.frontend.FrontendFactory; +import org.cryptomator.frontend.webdav.WebDavModule; import org.cryptomator.frontend.webdav.WebDavServer; -import org.cryptomator.frontend.webdav.mount.WebDavMounter; -import org.cryptomator.frontend.webdav.mount.WebDavMounterProvider; import org.cryptomator.ui.model.VaultObjectMapperProvider; import org.cryptomator.ui.settings.Settings; import org.cryptomator.ui.settings.SettingsProvider; @@ -32,7 +31,7 @@ import dagger.Provides; import javafx.application.Application; import javafx.stage.Stage; -@Module(includes = {CryptoEngineModule.class, CommonsModule.class}) +@Module(includes = {CryptoEngineModule.class, CommonsModule.class, WebDavModule.class}) class CryptomatorModule { private final Application application; @@ -83,12 +82,6 @@ class CryptomatorModule { return closer.closeLater(Executors.newCachedThreadPool(), ExecutorService::shutdown).get().orElseThrow(IllegalStateException::new); } - @Provides - @Singleton - WebDavMounter provideWebDavMounter(WebDavMounterProvider webDavMounterProvider) { - return webDavMounterProvider.get(); - } - @Provides @Singleton FrontendFactory provideFrontendFactory(DeferredCloser closer, WebDavServer webDavServer, Settings settings) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java index 7c1532775..34ef59b0d 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java @@ -22,6 +22,7 @@ import org.fxmisc.easybind.EasyBind; import javafx.fxml.FXML; import javafx.scene.control.CheckBox; +import javafx.scene.control.ChoiceBox; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.input.KeyEvent; @@ -52,6 +53,12 @@ public class SettingsController extends LocalizedFXMLViewController { @FXML private Label versionLabel; + @FXML + private Label prefGvfsSchemeLabel; + + @FXML + private ChoiceBox prefGvfsScheme; + @Override public void initialize() { checkForUpdatesCheckbox.setDisable(areUpdatesManagedExternally()); @@ -62,10 +69,16 @@ public class SettingsController extends LocalizedFXMLViewController { useIpv6Checkbox.setVisible(SystemUtils.IS_OS_WINDOWS); useIpv6Checkbox.setSelected(SystemUtils.IS_OS_WINDOWS && settings.shouldUseIpv6()); versionLabel.setText(String.format(localization.getString("settings.version.label"), applicationVersion().orElse("SNAPSHOT"))); + prefGvfsSchemeLabel.setVisible(SystemUtils.IS_OS_LINUX); + prefGvfsScheme.setVisible(SystemUtils.IS_OS_LINUX); + prefGvfsScheme.getItems().add("dav"); + prefGvfsScheme.getItems().add("webdav"); + prefGvfsScheme.setValue(settings.getPreferredGvfsScheme()); EasyBind.subscribe(checkForUpdatesCheckbox.selectedProperty(), this::checkForUpdateDidChange); EasyBind.subscribe(portField.textProperty(), this::portDidChange); EasyBind.subscribe(useIpv6Checkbox.selectedProperty(), this::useIpv6DidChange); + EasyBind.subscribe(prefGvfsScheme.valueProperty(), this::prefGvfsSchemeDidChange); } @Override @@ -101,6 +114,11 @@ public class SettingsController extends LocalizedFXMLViewController { settings.save(); } + private void prefGvfsSchemeDidChange(String newValue) { + settings.setPreferredGvfsScheme(newValue); + settings.save(); + } + private void filterNumericKeyEvents(KeyEvent t) { if (t.getCharacter() == null || t.getCharacter().length() == 0) { return; diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java index d38f9152c..6868574ac 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java @@ -154,7 +154,8 @@ public class Vault implements CryptoFileSystemDelegate { return ImmutableMap.of( // MountParam.MOUNT_NAME, Optional.ofNullable(mountName), // MountParam.WIN_DRIVE_LETTER, Optional.ofNullable(CharUtils.toString(winDriveLetter)), // - MountParam.HOSTNAME, Optional.of(hostname) // + MountParam.HOSTNAME, Optional.of(hostname), // + MountParam.PREFERRED_GVFS_SCHEME, Optional.ofNullable(settings.getPreferredGvfsScheme()) // ); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java b/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java index 6b00aeb41..f9323bf99 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java +++ b/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java @@ -18,7 +18,7 @@ import org.cryptomator.ui.model.Vault; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; -@JsonPropertyOrder(value = {"directories", "checkForUpdatesEnabled", "port", "useIpv6", "numTrayNotifications"}) +@JsonPropertyOrder(value = {"directories", "checkForUpdatesEnabled", "port", "useIpv6", "numTrayNotifications", "preferredGvfsScheme"}) public class Settings implements Serializable { private static final long serialVersionUID = 7609959894417878744L; @@ -27,6 +27,7 @@ public class Settings implements Serializable { public static final int DEFAULT_PORT = 42427; public static final boolean DEFAULT_USE_IPV6 = false; public static final Integer DEFAULT_NUM_TRAY_NOTIFICATIONS = 3; + public static final String DEFAULT_GVFS_SCHEME = "dav"; private final Consumer saveCmd; @@ -45,6 +46,9 @@ public class Settings implements Serializable { @JsonProperty("numTrayNotifications") private Integer numTrayNotifications; + @JsonProperty("preferredGvfsScheme") + private String preferredGvfsScheme; + /** * Package-private constructor; use {@link SettingsProvider}. */ @@ -113,4 +117,12 @@ public class Settings implements Serializable { this.numTrayNotifications = numTrayNotifications; } + public String getPreferredGvfsScheme() { + return preferredGvfsScheme == null ? DEFAULT_GVFS_SCHEME : preferredGvfsScheme; + } + + public void setPreferredGvfsScheme(String preferredGvfsScheme) { + this.preferredGvfsScheme = preferredGvfsScheme; + } + } diff --git a/main/ui/src/main/resources/css/linux_theme.css b/main/ui/src/main/resources/css/linux_theme.css index cb3c407f0..38f453feb 100644 --- a/main/ui/src/main/resources/css/linux_theme.css +++ b/main/ui/src/main/resources/css/linux_theme.css @@ -325,6 +325,32 @@ -fx-background-color: COLOR_TEXT; } +/******************************************************************************* + * * + * ChoiceBox * + * * + ******************************************************************************/ + +.choice-box { + -fx-background-color: COLOR_BORDER_DARK, COLOR_BACKGROUND; + -fx-background-insets: 0, 1; + -fx-background-radius: 0, 0; + -fx-padding: 0.1em 0.6em 0.1em 0.6em; + -fx-text-fill: COLOR_TEXT; +} + +.choice-box > .open-button > .arrow { + -fx-background-color: transparent, COLOR_TEXT; + -fx-background-insets: 0 0 -1 0, 0; + -fx-padding: 0.166667em 0.333333em 0.166667em 0.333333em; /* 2 4 2 4 */ + -fx-shape: "M 0 0 h 7 l -3.5 4 z"; +} + +.choice-box .context-menu { + -fx-background-color: COLOR_BORDER, #FFF; + -fx-background-insets: 0, 1; +} + /**************************************************************************** * * * ProgressIndicator * diff --git a/main/ui/src/main/resources/fxml/settings.fxml b/main/ui/src/main/resources/fxml/settings.fxml index 73f26630f..41f770be2 100644 --- a/main/ui/src/main/resources/fxml/settings.fxml +++ b/main/ui/src/main/resources/fxml/settings.fxml @@ -15,6 +15,7 @@ +