diff --git a/CHANGELOG.md b/CHANGELOG.md index f32b44a9..5c22fdfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ #### Improvements * Updated **JPro** to version `2024.3.3`. +#### Features +* Register an `extensionFilters` listener to the native (desktop) implementation FileSavePicker in the `jpro-file` +module. The listener can be used to filter the files that are displayed in the file save picker dialog based on the file +extension. + #### Bugfixes * Fixed the binding of the port in the local server implementation inside the `jpro-core` module to occur only when necessary, rather than during server creation. diff --git a/jpro-file/example/src/main/java/one/jpro/platform/file/example/editor/TextEditorSample.java b/jpro-file/example/src/main/java/one/jpro/platform/file/example/editor/TextEditorSample.java index c17a33b5..d53365d4 100644 --- a/jpro-file/example/src/main/java/one/jpro/platform/file/example/editor/TextEditorSample.java +++ b/jpro-file/example/src/main/java/one/jpro/platform/file/example/editor/TextEditorSample.java @@ -57,9 +57,10 @@ */ public class TextEditorSample extends Application { - private static final Logger logger = LoggerFactory.getLogger(TextEditorSample.class); + private static final Logger LOGGER = LoggerFactory.getLogger(TextEditorSample.class); + private static final PseudoClass FILES_DRAG_OVER_PSEUDO_CLASS = PseudoClass.getPseudoClass("files-drag-over"); - private static final ExtensionFilter textExtensionFilter = ExtensionFilter.of("Text files", ".txt", ".srt", ".md", ".csv"); + private static final ExtensionFilter TEXT_EXTENSION_FILTER = ExtensionFilter.of("Text files", ".txt", ".srt", ".md", ".csv"); private final ObjectProperty lastOpenedFile = new SimpleObjectProperty<>(this, "lastOpenedFile"); @Override @@ -76,7 +77,7 @@ public void start(Stage stage) { } public Parent createRoot(Stage stage) { - Label dropLabel = new Label("Drop " + textExtensionFilter.description().toLowerCase() + " here!"); + Label dropLabel = new Label("Drop " + TEXT_EXTENSION_FILTER.description().toLowerCase() + " here!"); StackPane dropPane = new StackPane(dropLabel); dropPane.getStyleClass().add("drop-pane"); @@ -84,7 +85,7 @@ public Parent createRoot(Stage stage) { StackPane contentPane = new StackPane(textArea, dropPane); FileDropper fileDropper = FileDropper.create(contentPane); - fileDropper.setExtensionFilter(textExtensionFilter); + fileDropper.setExtensionFilter(TEXT_EXTENSION_FILTER); fileDropper.setOnDragEntered(event -> { dropPane.pseudoClassStateChanged(FILES_DRAG_OVER_PSEUDO_CLASS, true); contentPane.getChildren().setAll(textArea, dropPane); @@ -105,7 +106,7 @@ public Parent createRoot(Stage stage) { Button openButton = new Button("Open", new FontIcon(Material2AL.FOLDER_OPEN)); FileOpenPicker fileOpenPicker = FileOpenPicker.create(openButton); - fileOpenPicker.setSelectedExtensionFilter(textExtensionFilter); + fileOpenPicker.setSelectedExtensionFilter(TEXT_EXTENSION_FILTER); fileOpenPicker.setOnFilesSelected(fileSources -> { openFile(fileSources, textArea); contentPane.getChildren().setAll(textArea); @@ -133,7 +134,7 @@ public Parent createRoot(Stage stage) { fileSavePicker.initialFileNameProperty().bind(lastOpenedFile.map(file -> FilenameUtils.getName(file.getName())).orElse("subtitle")); fileSavePicker.initialDirectoryProperty().bind(lastOpenedFile.map(File::getParentFile)); - fileSavePicker.setSelectedExtensionFilter(ExtensionFilter.of("Subtitle format (.srt)", ".srt")); + fileSavePicker.setSelectedExtensionFilter(TEXT_EXTENSION_FILTER); fileSavePicker.setOnFileSelected(file -> saveToFile(textArea).apply(file)); BorderPane rootPane = new BorderPane(contentPane); @@ -151,16 +152,16 @@ public Parent createRoot(Stage stage) { private void openFile(List fileSources, TextArea textArea) { fileSources.stream().findFirst().ifPresentOrElse(fileSource -> // Set the last opened file fileSource.uploadFileAsync() - .thenCompose(file -> { - try { - final String fileContent = new String(Files.readAllBytes(file.toPath())); - Platform.runLater(() -> textArea.setText(fileContent)); - return CompletableFuture.completedFuture(file); - } catch (IOException ex) { - logger.error("Error reading file: " + ex.getMessage(), ex); - return CompletableFuture.failedFuture(ex); - } - }).thenAccept(lastOpenedFile::set), () -> logger.warn("No file selected")); + .thenCompose(file -> { + try { + final String fileContent = new String(Files.readAllBytes(file.toPath())); + Platform.runLater(() -> textArea.setText(fileContent)); + return CompletableFuture.completedFuture(file); + } catch (IOException ex) { + LOGGER.error("Error reading file: {}", file.getAbsolutePath(), ex); + return CompletableFuture.failedFuture(ex); + } + }).thenAccept(lastOpenedFile::set), () -> LOGGER.warn("No file selected")); } /** @@ -174,10 +175,10 @@ private Function> saveToFile(TextArea textArea) { try (FileOutputStream fos = new FileOutputStream(file)) { fos.write(textArea.getText().getBytes()); } catch (IOException ex) { - logger.error("Error writing file: " + ex.getMessage(), ex); + LOGGER.error("Error writing file: {}", file.getAbsolutePath(), ex); } lastOpenedFile.set(file); - System.out.println("Saved file: " + file.getAbsolutePath()); + LOGGER.info("Saved to file: {}", file.getAbsolutePath()); }); } } diff --git a/jpro-file/src/main/java/one/jpro/platform/file/picker/NativeFileOpenPicker.java b/jpro-file/src/main/java/one/jpro/platform/file/picker/NativeFileOpenPicker.java index 32d9a2fa..64f2ae77 100644 --- a/jpro-file/src/main/java/one/jpro/platform/file/picker/NativeFileOpenPicker.java +++ b/jpro-file/src/main/java/one/jpro/platform/file/picker/NativeFileOpenPicker.java @@ -30,7 +30,7 @@ public class NativeFileOpenPicker extends BaseFileOpenPicker { private final FileChooser fileChooser = new FileChooser(); private List nativeFileSources = List.of(); - private final ListChangeListener extensionListFiltersListener = change -> { + private final ListChangeListener extensionFiltersListChangeListener = change -> { while (change.next()) { if (change.wasAdded()) { for (ExtensionFilter extensionFilter : change.getAddedSubList()) { @@ -57,7 +57,7 @@ public NativeFileOpenPicker(Node node) { // Wrap the listener into a WeakListChangeListener to avoid memory leaks, // that can occur if observers are not unregistered from observed objects after use. - getExtensionFilters().addListener(new WeakListChangeListener<>(extensionListFiltersListener)); + getExtensionFilters().addListener(new WeakListChangeListener<>(extensionFiltersListChangeListener)); // Define the action that should be performed when the user clicks on the node. node.addEventHandler(MouseEvent.MOUSE_CLICKED, actionEvent -> { diff --git a/jpro-file/src/main/java/one/jpro/platform/file/picker/NativeFileSavePicker.java b/jpro-file/src/main/java/one/jpro/platform/file/picker/NativeFileSavePicker.java index 11ca7a4d..3c6fde20 100644 --- a/jpro-file/src/main/java/one/jpro/platform/file/picker/NativeFileSavePicker.java +++ b/jpro-file/src/main/java/one/jpro/platform/file/picker/NativeFileSavePicker.java @@ -4,6 +4,8 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; +import javafx.collections.ListChangeListener; +import javafx.collections.WeakListChangeListener; import javafx.scene.Node; import javafx.scene.Scene; import javafx.stage.FileChooser; @@ -23,11 +25,30 @@ */ public class NativeFileSavePicker extends BaseFileSavePicker { - private final FileChooser fileChooser; + private final FileChooser fileChooser = new FileChooser(); + private final ListChangeListener extensionFiltersListChangeListener = change -> { + while (change.next()) { + if (change.wasAdded()) { + for (ExtensionFilter extensionFilter : change.getAddedSubList()) { + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter( + extensionFilter.description(), extensionFilter.extensions().stream() + .map(ext -> "*" + ext).toList())); + } + } else if (change.wasRemoved()) { + for (ExtensionFilter extensionFilter : change.getRemoved()) { + fileChooser.getExtensionFilters().removeIf(filter -> + filter.getDescription().equals(extensionFilter.description())); + } + } + } + }; public NativeFileSavePicker(Node node) { super(node); - fileChooser = new FileChooser(); + + // Wrap the listener into a WeakListChangeListener to avoid memory leaks, + // that can occur if observers are not unregistered from observed objects after use. + getExtensionFilters().addListener(new WeakListChangeListener<>(extensionFiltersListChangeListener)); } @Override