浏览代码

refactor location ui in addVault workflow to new locationPreset framework

Armin Schrenk 1 年之前
父节点
当前提交
0af0a9e440

+ 0 - 64
src/main/java/org/cryptomator/common/LocationPreset.java

@@ -1,64 +0,0 @@
-package org.cryptomator.common;
-
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * Enum of common cloud providers and their default local storage location path.
- */
-public enum LocationPreset {
-
-	DROPBOX("Dropbox", "~/Library/CloudStorage/Dropbox", "~/Dropbox"),
-	ICLOUDDRIVE("iCloud Drive", "~/Library/Mobile Documents/com~apple~CloudDocs", "~/iCloudDrive"),
-	GDRIVE("Google Drive", "~/Google Drive/My Drive", "~/Google Drive"),
-	MEGA("MEGA", "~/MEGA"),
-	ONEDRIVE("OneDrive", "~/OneDrive"),
-	PCLOUD("pCloud", "~/pCloudDrive"),
-
-	LOCAL("local");
-
-	private final String name;
-	private final List<Path> candidates;
-
-	LocationPreset(String name, String... candidates) {
-		this.name = name;
-		this.candidates = Arrays.stream(candidates).map(UserHome::resolve).map(Path::of).toList();
-	}
-
-	/**
-	 * Checks for this LocationPreset if any of the associated paths exist.
-	 *
-	 * @return the first existing path or null, if none exists.
-	 */
-	public Path existingPath() {
-		return candidates.stream().filter(Files::isDirectory).findFirst().orElse(null);
-	}
-
-	public String getDisplayName() {
-		return name;
-	}
-
-	@Override
-	public String toString() {
-		return getDisplayName();
-	}
-
-	//this contruct is needed, since static members are initialized after every enum member is initialized
-	//TODO: refactor this to normal class and use this also in different parts of the project
-	private static class UserHome {
-
-		private static final String USER_HOME = System.getProperty("user.home");
-
-		private static String resolve(String path) {
-			if (path.startsWith("~/")) {
-				return UserHome.USER_HOME + path.substring(1);
-			} else {
-				return path;
-			}
-		}
-	}
-
-}
-

+ 73 - 0
src/main/java/org/cryptomator/common/locationpresets/LocationPresetsProvider.java

@@ -1,10 +1,22 @@
 package org.cryptomator.common.locationpresets;
 
+import org.cryptomator.integrations.common.CheckAvailability;
+import org.cryptomator.integrations.common.IntegrationsLoader;
+import org.cryptomator.integrations.common.OperatingSystem;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
 import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.ServiceLoader;
 import java.util.stream.Stream;
 
 public interface LocationPresetsProvider {
 
+	Logger LOG = LoggerFactory.getLogger(LocationPresetsProvider.class);
 	String USER_HOME = System.getProperty("user.home");
 
 	Stream<LocationPreset> getLocations();
@@ -17,4 +29,65 @@ public interface LocationPresetsProvider {
 		}
 	}
 
+	//copied from org.cryptomator.integrations.common.IntegrationsLoader
+	//TODO: delete, once migrated to integrations-api
+	static <T> Stream<T> loadAll(Class<T> clazz) {
+		return ServiceLoader.load(clazz)
+				.stream()
+				.filter(LocationPresetsProvider::isSupportedOperatingSystem)
+				.filter(LocationPresetsProvider::passesStaticAvailabilityCheck)
+				.map(ServiceLoader.Provider::get)
+				.peek(impl -> logServiceIsAvailable(clazz, impl.getClass()));
+	}
+
+
+	private static boolean isSupportedOperatingSystem(ServiceLoader.Provider<?> provider) {
+		var annotations = provider.type().getAnnotationsByType(OperatingSystem.class);
+		return annotations.length == 0 || Arrays.stream(annotations).anyMatch(OperatingSystem.Value::isCurrent);
+	}
+
+	private static boolean passesStaticAvailabilityCheck(ServiceLoader.Provider<?> provider) {
+		return passesStaticAvailabilityCheck(provider.type());
+	}
+
+	static boolean passesStaticAvailabilityCheck(Class<?> type) {
+		return passesAvailabilityCheck(type, null);
+	}
+
+	private static void logServiceIsAvailable(Class<?> apiType, Class<?> implType) {
+		if (LOG.isDebugEnabled()) {
+			LOG.debug("{}: Implementation is available: {}", apiType.getSimpleName(), implType.getName());
+		}
+	}
+
+	private static <T> boolean passesAvailabilityCheck(Class<? extends T> type, @Nullable T instance) {
+		if (!type.isAnnotationPresent(CheckAvailability.class)) {
+			return true; // if type is not annotated, skip tests
+		}
+		if (!type.getModule().isExported(type.getPackageName(), IntegrationsLoader.class.getModule())) {
+			LOG.error("Can't run @CheckAvailability tests for class {}. Make sure to export {} to {}!", type.getName(), type.getPackageName(), IntegrationsLoader.class.getPackageName());
+			return false;
+		}
+		return Arrays.stream(type.getMethods())
+				.filter(m -> isAvailabilityCheck(m, instance == null))
+				.allMatch(m -> passesAvailabilityCheck(m, instance));
+	}
+
+	private static boolean passesAvailabilityCheck(Method m, @Nullable Object instance) {
+		assert Boolean.TYPE.equals(m.getReturnType());
+		try {
+			return (boolean) m.invoke(instance);
+		} catch (ReflectiveOperationException e) {
+			LOG.warn("Failed to invoke @CheckAvailability test {}#{}", m.getDeclaringClass(), m.getName(), e);
+			return false;
+		}
+	}
+
+	private static boolean isAvailabilityCheck(Method m, boolean isStatic) {
+		return m.isAnnotationPresent(CheckAvailability.class)
+				&& Boolean.TYPE.equals(m.getReturnType())
+				&& m.getParameterCount() == 0
+				&& Modifier.isStatic(m.getModifiers()) == isStatic;
+	}
+
 }

+ 26 - 32
src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java

@@ -1,6 +1,8 @@
 package org.cryptomator.ui.addvaultwizard;
 
 import dagger.Lazy;
+import org.cryptomator.common.locationpresets.LocationPresetsProvider;
+import org.cryptomator.common.locationpresets.LocationPreset;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.common.FxmlFile;
 import org.cryptomator.ui.common.FxmlScene;
@@ -26,6 +28,7 @@ import javafx.scene.control.Label;
 import javafx.scene.control.RadioButton;
 import javafx.scene.control.Toggle;
 import javafx.scene.control.ToggleGroup;
+import javafx.scene.layout.VBox;
 import javafx.stage.DirectoryChooser;
 import javafx.stage.Stage;
 import java.io.File;
@@ -34,6 +37,9 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.StandardOpenOption;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
 import java.util.ResourceBundle;
 
 @AddVaultWizardScoped
@@ -46,7 +52,7 @@ public class CreateNewVaultLocationController implements FxController {
 	private final Stage window;
 	private final Lazy<Scene> chooseNameScene;
 	private final Lazy<Scene> choosePasswordScene;
-	private final ObservedLocationPresets locationPresets;
+	private final List<RadioButton> locationPresetBtns;
 	private final ObjectProperty<Path> vaultPath;
 	private final StringProperty vaultName;
 	private final ResourceBundle resourceBundle;
@@ -58,24 +64,18 @@ public class CreateNewVaultLocationController implements FxController {
 	private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH;
 
 	//FXML
-	public ToggleGroup predefinedLocationToggler;
-	public RadioButton iclouddriveRadioButton;
-	public RadioButton dropboxRadioButton;
-	public RadioButton gdriveRadioButton;
-	public RadioButton onedriveRadioButton;
-	public RadioButton megaRadioButton;
-	public RadioButton pcloudRadioButton;
+	public ToggleGroup locationPresetsToggler;
+	public VBox radioButtonVBox;
 	public RadioButton customRadioButton;
 	public Label vaultPathStatus;
 	public FontAwesome5IconView goodLocation;
 	public FontAwesome5IconView badLocation;
 
 	@Inject
-	CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, ObservedLocationPresets locationPresets, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
+	CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
 		this.window = window;
 		this.chooseNameScene = chooseNameScene;
 		this.choosePasswordScene = choosePasswordScene;
-		this.locationPresets = locationPresets;
 		this.vaultPath = vaultPath;
 		this.vaultName = vaultName;
 		this.resourceBundle = resourceBundle;
@@ -83,6 +83,14 @@ public class CreateNewVaultLocationController implements FxController {
 		this.usePresetPath = new SimpleBooleanProperty();
 		this.statusText = new SimpleStringProperty();
 		this.statusGraphic = new SimpleObjectProperty<>();
+		this.locationPresetBtns = LocationPresetsProvider.loadAll(LocationPresetsProvider.class) //
+				.flatMap(LocationPresetsProvider::getLocations) //
+				.sorted(Comparator.comparing(LocationPreset::name)) //
+				.map(preset -> { //
+					var btn = new RadioButton(preset.name());
+					btn.setUserData(preset.path());
+					return btn;
+				}).toList();
 	}
 
 	private boolean validateVaultPathAndSetStatus() {
@@ -127,26 +135,15 @@ public class CreateNewVaultLocationController implements FxController {
 
 	@FXML
 	public void initialize() {
-		predefinedLocationToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation);
-		usePresetPath.bind(predefinedLocationToggler.selectedToggleProperty().isNotEqualTo(customRadioButton));
+		radioButtonVBox.getChildren().addAll(1, locationPresetBtns); //first item is the list header
+		locationPresetsToggler.getToggles().addAll(locationPresetBtns);
+		locationPresetsToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation);
+		usePresetPath.bind(locationPresetsToggler.selectedToggleProperty().isNotEqualTo(customRadioButton));
 	}
 
 	private void togglePredefinedLocation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
-		if (iclouddriveRadioButton.equals(newValue)) {
-			vaultPath.set(locationPresets.getIclouddriveLocation().resolve(vaultName.get()));
-		} else if (dropboxRadioButton.equals(newValue)) {
-			vaultPath.set(locationPresets.getDropboxLocation().resolve(vaultName.get()));
-		} else if (gdriveRadioButton.equals(newValue)) {
-			vaultPath.set(locationPresets.getGdriveLocation().resolve(vaultName.get()));
-		} else if (onedriveRadioButton.equals(newValue)) {
-			vaultPath.set(locationPresets.getOnedriveLocation().resolve(vaultName.get()));
-		} else if (megaRadioButton.equals(newValue)) {
-			vaultPath.set(locationPresets.getMegaLocation().resolve(vaultName.get()));
-		} else if (pcloudRadioButton.equals(newValue)) {
-			vaultPath.set(locationPresets.getPcloudLocation().resolve(vaultName.get()));
-		} else if (customRadioButton.equals(newValue)) {
-			vaultPath.set(customVaultPath.resolve(vaultName.get()));
-		}
+		var storagePath = Optional.ofNullable((Path) newValue.getUserData()).orElse(customVaultPath);
+		vaultPath.set(storagePath.resolve(vaultName.get()));
 	}
 
 	@FXML
@@ -197,10 +194,6 @@ public class CreateNewVaultLocationController implements FxController {
 		return validVaultPath.get();
 	}
 
-	public ObservedLocationPresets getObservedLocationPresets() {
-		return locationPresets;
-	}
-
 	public BooleanProperty usePresetPathProperty() {
 		return usePresetPath;
 	}
@@ -210,7 +203,7 @@ public class CreateNewVaultLocationController implements FxController {
 	}
 
 	public BooleanBinding anyRadioButtonSelectedProperty() {
-		return predefinedLocationToggler.selectedToggleProperty().isNotNull();
+		return locationPresetsToggler.selectedToggleProperty().isNotNull();
 	}
 
 	public boolean isAnyRadioButtonSelected() {
@@ -232,4 +225,5 @@ public class CreateNewVaultLocationController implements FxController {
 	public Node getStatusGraphic() {
 		return statusGraphic.get();
 	}
+
 }

+ 0 - 141
src/main/java/org/cryptomator/ui/addvaultwizard/ObservedLocationPresets.java

@@ -1,141 +0,0 @@
-package org.cryptomator.ui.addvaultwizard;
-
-import org.cryptomator.common.LocationPreset;
-
-import javax.inject.Inject;
-import javafx.beans.binding.BooleanBinding;
-import javafx.beans.property.ReadOnlyObjectProperty;
-import javafx.beans.property.SimpleObjectProperty;
-import java.nio.file.Path;
-
-@AddVaultWizardScoped
-public class ObservedLocationPresets {
-
-	private final ReadOnlyObjectProperty<Path> iclouddriveLocation;
-	private final ReadOnlyObjectProperty<Path> dropboxLocation;
-	private final ReadOnlyObjectProperty<Path> gdriveLocation;
-	private final ReadOnlyObjectProperty<Path> onedriveLocation;
-	private final ReadOnlyObjectProperty<Path> megaLocation;
-	private final ReadOnlyObjectProperty<Path> pcloudLocation;
-	private final BooleanBinding foundIclouddrive;
-	private final BooleanBinding foundDropbox;
-	private final BooleanBinding foundGdrive;
-	private final BooleanBinding foundOnedrive;
-	private final BooleanBinding foundMega;
-	private final BooleanBinding foundPcloud;
-
-	@Inject
-	public ObservedLocationPresets() {
-		this.iclouddriveLocation = new SimpleObjectProperty<>(LocationPreset.ICLOUDDRIVE.existingPath());
-		this.dropboxLocation = new SimpleObjectProperty<>(LocationPreset.DROPBOX.existingPath());
-		this.gdriveLocation = new SimpleObjectProperty<>(LocationPreset.GDRIVE.existingPath());
-		this.onedriveLocation = new SimpleObjectProperty<>(LocationPreset.ONEDRIVE.existingPath());
-		this.megaLocation = new SimpleObjectProperty<>(LocationPreset.MEGA.existingPath());
-		this.pcloudLocation = new SimpleObjectProperty<>(LocationPreset.PCLOUD.existingPath());
-		this.foundIclouddrive = iclouddriveLocation.isNotNull();
-		this.foundDropbox = dropboxLocation.isNotNull();
-		this.foundGdrive = gdriveLocation.isNotNull();
-		this.foundOnedrive = onedriveLocation.isNotNull();
-		this.foundMega = megaLocation.isNotNull();
-		this.foundPcloud = pcloudLocation.isNotNull();
-	}
-
-	/* Observables */
-
-	public ReadOnlyObjectProperty<Path> iclouddriveLocationProperty() {
-		return iclouddriveLocation;
-	}
-
-	public Path getIclouddriveLocation() {
-		return iclouddriveLocation.get();
-	}
-
-	public BooleanBinding foundIclouddriveProperty() {
-		return foundIclouddrive;
-	}
-
-	public boolean isFoundIclouddrive() {
-		return foundIclouddrive.get();
-	}
-
-	public ReadOnlyObjectProperty<Path> dropboxLocationProperty() {
-		return dropboxLocation;
-	}
-
-	public Path getDropboxLocation() {
-		return dropboxLocation.get();
-	}
-
-	public BooleanBinding foundDropboxProperty() {
-		return foundDropbox;
-	}
-
-	public boolean isFoundDropbox() {
-		return foundDropbox.get();
-	}
-
-	public ReadOnlyObjectProperty<Path> gdriveLocationProperty() {
-		return gdriveLocation;
-	}
-
-	public Path getGdriveLocation() {
-		return gdriveLocation.get();
-	}
-
-	public BooleanBinding foundGdriveProperty() {
-		return foundGdrive;
-	}
-
-	public boolean isFoundGdrive() {
-		return foundGdrive.get();
-	}
-
-	public ReadOnlyObjectProperty<Path> onedriveLocationProperty() {
-		return onedriveLocation;
-	}
-
-	public Path getOnedriveLocation() {
-		return onedriveLocation.get();
-	}
-
-	public BooleanBinding foundOnedriveProperty() {
-		return foundOnedrive;
-	}
-
-	public boolean isFoundOnedrive() {
-		return foundOnedrive.get();
-	}
-
-	public ReadOnlyObjectProperty<Path> megaLocationProperty() {
-		return megaLocation;
-	}
-
-	public Path getMegaLocation() {
-		return megaLocation.get();
-	}
-
-	public BooleanBinding foundMegaProperty() {
-		return foundMega;
-	}
-
-	public boolean isFoundMega() {
-		return foundMega.get();
-	}
-
-	public ReadOnlyObjectProperty<Path> pcloudLocationProperty() {
-		return pcloudLocation;
-	}
-
-	public Path getPcloudLocation() {
-		return pcloudLocation.get();
-	}
-
-	public BooleanBinding foundPcloudProperty() {
-		return foundPcloud;
-	}
-
-	public boolean isFoundPcloud() {
-		return foundPcloud.get();
-	}
-
-}

+ 5 - 10
src/main/resources/fxml/addvault_new_location.fxml

@@ -19,7 +19,7 @@
 	  spacing="12"
 	  alignment="CENTER_LEFT">
 	<fx:define>
-		<ToggleGroup fx:id="predefinedLocationToggler"/>
+		<ToggleGroup fx:id="locationPresetsToggler"/>
 		<FontAwesome5IconView fx:id="badLocation" styleClass="glyph-icon-red" glyph="TIMES" />
 		<FontAwesome5IconView fx:id="goodLocation" styleClass="glyph-icon-primary" glyph="CHECK" />
 	</fx:define>
@@ -29,16 +29,11 @@
 	<children>
 		<Region VBox.vgrow="ALWAYS"/>
 
-		<VBox spacing="6">
+		<VBox fx:id="radioButtonVBox" spacing="6">
 			<Label wrapText="true" text="%addvaultwizard.new.locationInstruction"/>
-			<RadioButton fx:id="iclouddriveRadioButton" toggleGroup="${predefinedLocationToggler}" text="iCloud Drive" visible="${controller.observedLocationPresets.foundIclouddrive}" managed="${controller.observedLocationPresets.foundIclouddrive}"/>
-			<RadioButton fx:id="dropboxRadioButton" toggleGroup="${predefinedLocationToggler}" text="Dropbox" visible="${controller.observedLocationPresets.foundDropbox}" managed="${controller.observedLocationPresets.foundDropbox}"/>
-			<RadioButton fx:id="gdriveRadioButton" toggleGroup="${predefinedLocationToggler}" text="Google Drive" visible="${controller.observedLocationPresets.foundGdrive}" managed="${controller.observedLocationPresets.foundGdrive}"/>
-			<RadioButton fx:id="onedriveRadioButton" toggleGroup="${predefinedLocationToggler}" text="OneDrive" visible="${controller.observedLocationPresets.foundOnedrive}" managed="${controller.observedLocationPresets.foundOnedrive}"/>
-			<RadioButton fx:id="megaRadioButton" toggleGroup="${predefinedLocationToggler}" text="MEGA" visible="${controller.observedLocationPresets.foundMega}" managed="${controller.observedLocationPresets.foundMega}"/>
-			<RadioButton fx:id="pcloudRadioButton" toggleGroup="${predefinedLocationToggler}" text="pCloud" visible="${controller.observedLocationPresets.foundPcloud}" managed="${controller.observedLocationPresets.foundPcloud}"/>
+			<!-- PLACEHOLDER, more radio buttons are added programmatically via controller -->
 			<HBox spacing="12" alignment="CENTER_LEFT">
-				<RadioButton fx:id="customRadioButton" toggleGroup="${predefinedLocationToggler}" text="%addvaultwizard.new.directoryPickerLabel"/>
+				<RadioButton fx:id="customRadioButton" toggleGroup="${locationPresetsToggler}" text="%addvaultwizard.new.directoryPickerLabel"/>
 				<Button contentDisplay="LEFT" text="%addvaultwizard.new.directoryPickerButton" onAction="#chooseCustomVaultPath" disable="${controller.usePresetPath}">
 					<graphic>
 						<FontAwesome5IconView glyph="FOLDER_OPEN"/>
@@ -51,7 +46,7 @@
 
 		<VBox spacing="6">
 			<Label text="%addvaultwizard.new.locationLabel" labelFor="$locationTextField"/>
-			<TextField promptText="%addvaultwizard.new.locationPrompt" text="${controller.vaultPath}" editable="false" disable="${!controller.anyRadioButtonSelected}" HBox.hgrow="ALWAYS"/>
+			<TextField fx:id="locationTextField" promptText="%addvaultwizard.new.locationPrompt" text="${controller.vaultPath}" editable="false" disable="${!controller.anyRadioButtonSelected}" HBox.hgrow="ALWAYS"/>
 			<Label fx:id="vaultPathStatus" styleClass="label-muted" alignment="CENTER_RIGHT" wrapText="true" visible="${controller.anyRadioButtonSelected}" maxWidth="Infinity" graphicTextGap="6" text="${controller.statusText}" graphic="${controller.statusGraphic}" />
 		</VBox>