Преглед на файлове

Merge pull request #3327 from cryptomator/feature/3233-load-presets-background

Feature: Load LocationPresets in background and show indicator in UI
Armin Schrenk преди 1 година
родител
ревизия
251ad65344

+ 60 - 14
src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java

@@ -13,31 +13,37 @@ import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
 import javax.inject.Named;
+import javafx.application.Platform;
+import javafx.beans.binding.Bindings;
 import javafx.beans.binding.BooleanBinding;
 import javafx.beans.property.BooleanProperty;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleBooleanProperty;
 import javafx.beans.property.StringProperty;
 import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
 import javafx.fxml.FXML;
+import javafx.scene.Node;
 import javafx.scene.Scene;
 import javafx.scene.control.Label;
 import javafx.scene.control.RadioButton;
 import javafx.scene.control.Toggle;
 import javafx.scene.control.ToggleGroup;
+import javafx.scene.layout.HBox;
 import javafx.scene.layout.VBox;
 import javafx.stage.DirectoryChooser;
 import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
 import java.io.File;
 import java.io.IOException;
 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;
+import java.util.concurrent.ExecutorService;
 
 @AddVaultWizardScoped
 public class CreateNewVaultLocationController implements FxController {
@@ -49,19 +55,23 @@ public class CreateNewVaultLocationController implements FxController {
 	private final Stage window;
 	private final Lazy<Scene> chooseNameScene;
 	private final Lazy<Scene> chooseExpertSettingsScene;
-	private final List<RadioButton> locationPresetBtns;
 	private final ObjectProperty<Path> vaultPath;
 	private final StringProperty vaultName;
+	private final ExecutorService backgroundExecutor;
 	private final ResourceBundle resourceBundle;
 	private final ObservableValue<VaultPathStatus> vaultPathStatus;
 	private final ObservableValue<Boolean> validVaultPath;
 	private final BooleanProperty usePresetPath;
+	private final BooleanProperty loadingPresetLocations = new SimpleBooleanProperty(false);
+	private final ObservableList<Node> radioButtons;
+	private final ObservableList<Node> sortedRadioButtons;
 
 	private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH;
 
 	//FXML
 	public ToggleGroup locationPresetsToggler;
 	public VBox radioButtonVBox;
+	public HBox customLocationRadioBtn;
 	public RadioButton customRadioButton;
 	public Label locationStatusLabel;
 	public FontAwesome5IconView goodLocation;
@@ -73,25 +83,20 @@ public class CreateNewVaultLocationController implements FxController {
 									 @FxmlScene(FxmlFile.ADDVAULT_NEW_EXPERT_SETTINGS) Lazy<Scene> chooseExpertSettingsScene, //
 									 ObjectProperty<Path> vaultPath, //
 									 @Named("vaultName") StringProperty vaultName, //
-									 ResourceBundle resourceBundle) {
+									 ExecutorService backgroundExecutor, ResourceBundle resourceBundle) {
 		this.window = window;
 		this.chooseNameScene = chooseNameScene;
 		this.chooseExpertSettingsScene = chooseExpertSettingsScene;
 		this.vaultPath = vaultPath;
 		this.vaultName = vaultName;
+		this.backgroundExecutor = backgroundExecutor;
 		this.resourceBundle = resourceBundle;
 		this.vaultPathStatus = ObservableUtil.mapWithDefault(vaultPath, this::validatePath, new VaultPathStatus(false, "error.message"));
 		this.validVaultPath = ObservableUtil.mapWithDefault(vaultPathStatus, VaultPathStatus::valid, false);
 		this.vaultPathStatus.addListener(this::updateStatusLabel);
 		this.usePresetPath = new SimpleBooleanProperty();
-		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();
+		this.radioButtons = FXCollections.observableArrayList();
+		this.sortedRadioButtons = radioButtons.sorted(this::compareLocationPresets);
 	}
 
 	private VaultPathStatus validatePath(Path p) throws NullPointerException {
@@ -137,12 +142,45 @@ public class CreateNewVaultLocationController implements FxController {
 
 	@FXML
 	public void initialize() {
-		radioButtonVBox.getChildren().addAll(1, locationPresetBtns); //first item is the list header
-		locationPresetsToggler.getToggles().addAll(locationPresetBtns);
+		var task = backgroundExecutor.submit(this::loadLocationPresets);
+		window.addEventHandler(WindowEvent.WINDOW_HIDING, _ -> task.cancel(true));
 		locationPresetsToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation);
 		usePresetPath.bind(locationPresetsToggler.selectedToggleProperty().isNotEqualTo(customRadioButton));
+		radioButtons.add(customLocationRadioBtn);
+		Bindings.bindContent(radioButtonVBox.getChildren(), sortedRadioButtons); //to prevent garbage collection of the binding, we bind explicitly to the sorted list
 	}
 
+	private void loadLocationPresets() {
+		Platform.runLater(() -> loadingPresetLocations.set(true));
+		try {
+			LocationPresetsProvider.loadAll(LocationPresetsProvider.class) //
+					.flatMap(LocationPresetsProvider::getLocations) //we do not use sorted(), because it evaluates the stream elements, blocking until all elements are gathered
+					.forEach(this::createRadioButtonFor);
+		} finally {
+			Platform.runLater(() -> loadingPresetLocations.set(false));
+		}
+	}
+
+	private void createRadioButtonFor(LocationPreset preset) {
+		Platform.runLater(() -> {
+			var btn = new RadioButton(preset.name());
+			btn.setUserData(preset.path());
+			radioButtons.add(btn);
+			locationPresetsToggler.getToggles().add(btn);
+		});
+	}
+
+	private int compareLocationPresets(Node left, Node right) {
+		if (customLocationRadioBtn.getId().equals(left.getId())) {
+			return 1;
+		} else if (customLocationRadioBtn.getId().equals(right.getId())) {
+			return -1;
+		} else {
+			return ((RadioButton) left).getText().compareToIgnoreCase(((RadioButton) right).getText());
+		}
+	}
+
+
 	private void togglePredefinedLocation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
 		var storagePath = Optional.ofNullable((Path) newValue.getUserData()).orElse(customVaultPath);
 		vaultPath.set(storagePath.resolve(vaultName.get()));
@@ -200,6 +238,14 @@ public class CreateNewVaultLocationController implements FxController {
 		return validVaultPath.getValue();
 	}
 
+	public boolean isLoadingPresetLocations() {
+		return loadingPresetLocations.getValue();
+	}
+
+	public BooleanProperty loadingPresetLocationsProperty() {
+		return loadingPresetLocations;
+	}
+
 	public BooleanProperty usePresetPathProperty() {
 		return usePresetPath;
 	}

+ 22 - 12
src/main/resources/fxml/addvault_new_location.fxml

@@ -1,11 +1,13 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
 <?import org.cryptomator.ui.controls.FontAwesome5IconView?>
+<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
 <?import javafx.geometry.Insets?>
 <?import javafx.scene.control.Button?>
 <?import javafx.scene.control.ButtonBar?>
 <?import javafx.scene.control.Label?>
 <?import javafx.scene.control.RadioButton?>
+<?import javafx.scene.control.ScrollPane?>
 <?import javafx.scene.control.TextField?>
 <?import javafx.scene.control.ToggleGroup?>
 <?import javafx.scene.layout.HBox?>
@@ -29,18 +31,26 @@
 	<children>
 		<Region VBox.vgrow="ALWAYS"/>
 
-		<VBox fx:id="radioButtonVBox" spacing="6">
-			<Label wrapText="true" text="%addvaultwizard.new.locationInstruction"/>
-			<!-- PLACEHOLDER, more radio buttons are added programmatically via controller -->
-			<HBox spacing="12" alignment="CENTER_LEFT">
-				<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"/>
-					</graphic>
-				</Button>
-			</HBox>
-		</VBox>
+		<Label wrapText="true" text="%addvaultwizard.new.locationInstruction"/>
+		<ScrollPane hbarPolicy="NEVER">
+			<VBox fx:id="radioButtonVBox" spacing="6">
+				<!-- PLACEHOLDER, more radio buttons are added programmatically via controller -->
+				<HBox fx:id="customLocationRadioBtn" spacing="12" alignment="CENTER_LEFT">
+					<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"/>
+						</graphic>
+					</Button>
+				</HBox>
+			</VBox>
+		</ScrollPane>
+		<Region prefHeight="2"/>
+		<Label wrapText="true" text="%addvaultwizard.new.locationLoading" visible="${controller.loadingPresetLocations}" managed="${controller.loadingPresetLocations}" graphicTextGap="8">
+			<graphic>
+				<FontAwesome5Spinner/>
+			</graphic>
+		</Label>
 
 		<Region prefHeight="12" VBox.vgrow="NEVER"/>