Skip to content

Commit

Permalink
feat: Enabled loading of FXML files via drag&drop to WelcomeDialog. (g…
Browse files Browse the repository at this point in the history
  • Loading branch information
Oliver-Loeffler committed Aug 26, 2024
1 parent d36d9f6 commit 3a27bd7
Show file tree
Hide file tree
Showing 5 changed files with 327 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright (c) 2024, Gluon and/or its affiliates.
* All rights reserved. Use is subject to license terms.
*
* This file is available and licensed under the following license:
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the distribution.
* - Neither the name of Oracle Corporation and Gluon nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package com.oracle.javafx.scenebuilder.app.welcomedialog;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;

final class WelcomeDialogFilesDropHandler {

private static final Logger LOGGER = Logger.getLogger(WelcomeDialogFilesDropHandler.class.getName());

private final List<File> droppedFiles;
private final List<String> toOpen;
private final List<String> unsupportedItems;
private Consumer<List<String>> openFiles;
private Consumer<List<String>> handleUnsupported;

WelcomeDialogFilesDropHandler(List<File> droppedFiles) {
this.droppedFiles = Objects.requireNonNull(droppedFiles);
this.toOpen = new ArrayList<>(droppedFiles.size());
this.unsupportedItems = new ArrayList<>(droppedFiles.size());
}

final WelcomeDialogFilesDropHandler withSupportedFiles(Consumer<List<String>> handleOpen) {
this.openFiles = handleOpen;
return this;
}

final WelcomeDialogFilesDropHandler withUnsupportedFiles(Consumer<List<String>> unsupportedHandler) {
this.handleUnsupported = unsupportedHandler;
return this;
}

final void run() {
analyzeDroppedItems();
handleDropResult();
}

final void handleDropResult() {
if (this.openFiles == null) {
throw new IllegalStateException("Please configure a dropped file handling action using the withSupportedFiles(...) method.");
}
if (this.handleUnsupported == null) {
throw new IllegalStateException("Please configure an action for handling of unsupported files using withUnsupportedFiles(...) method.");
}

if (!toOpen.isEmpty()) {
LOGGER.log(Level.INFO, "Received drop event to open files...");
openFiles.accept(toOpen);
} else {
LOGGER.log(Level.INFO, "Dropped object does not contain any loadable FXML files.");
handleUnsupported.accept(unsupportedItems);
}
}

final void analyzeDroppedItems() {
if (droppedFiles.isEmpty()) {
return;
}

for (var file : droppedFiles) {
if (file.isDirectory()) {
File[] children = file.listFiles();
List<String> inDir = new ArrayList<>(children.length);
for (var child : children) {
if (isFxml(child)) {
inDir.add(child.getAbsolutePath());
}
}
if (inDir.isEmpty()) {
unsupportedItems.add(file.getAbsolutePath());
} else {
toOpen.addAll(inDir);
}
} else {
if (isFxml(file)) {
toOpen.add(file.getAbsolutePath());
} else {
unsupportedItems.add(file.getAbsolutePath());
}
}
}
}

final boolean isFxml(File file) {
if (file.isDirectory()) {
return false;
}
return file.toString()
.toLowerCase()
.endsWith(".fxml");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@
import com.oracle.javafx.scenebuilder.app.preferences.PreferencesRecordGlobal;
import com.oracle.javafx.scenebuilder.app.util.AppSettings;
import com.oracle.javafx.scenebuilder.kit.editor.EditorController;
import com.oracle.javafx.scenebuilder.kit.editor.panel.util.dialog.AlertDialog;
import com.oracle.javafx.scenebuilder.kit.editor.panel.util.dialog.AbstractModalDialog.ButtonID;
import com.oracle.javafx.scenebuilder.kit.editor.panel.util.dialog.AlertDialog;
import com.oracle.javafx.scenebuilder.kit.editor.panel.util.dialog.ErrorDialog;
import com.oracle.javafx.scenebuilder.kit.template.Template;
import com.oracle.javafx.scenebuilder.kit.template.TemplatesBaseWindowController;

import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
Expand All @@ -61,6 +61,8 @@
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.Tooltip;
import javafx.scene.input.DragEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
Expand Down Expand Up @@ -116,6 +118,33 @@ protected void controllerDidCreateStage() {
getStage().setTitle(I18N.getString("welcome.title"));
getStage().initModality(Modality.APPLICATION_MODAL);
}

@FXML
void handleFileDraggedOver(DragEvent event) {
if (event.getDragboard().hasFiles()) {
event.acceptTransferModes(TransferMode.ANY);
}
}

@FXML
void handleDroppedFiles(DragEvent event) {
if (event.getDragboard().hasFiles()) {
new WelcomeDialogFilesDropHandler(event.getDragboard().getFiles())
.withSupportedFiles(fileNames->Platform.runLater(()->handleOpen(fileNames)))
.withUnsupportedFiles(unsupported->notifyUserWhenDroppedUnsupportedFiles(unsupported))
.run();
}
}

private void notifyUserWhenDroppedUnsupportedFiles(List<String> unsupported) {
ErrorDialog dialog = new ErrorDialog(getStage());
dialog.setTitle(I18N.getString("welcome.loading.when.dropped.error.title"));
dialog.setMessage(I18N.getString("welcome.loading.when.dropped.error.message"));
String detail = unsupported.stream()
.collect(Collectors.joining(System.lineSeparator()));
dialog.setDetails(detail);
Platform.runLater(()->dialog.showAndWait());
}

@Override
protected void controllerDidLoadFxml() {
Expand Down Expand Up @@ -262,7 +291,7 @@ private void handleOpen(List<String> filePaths) {
private void askUserToRemoveMissingRecentFiles(List<String> missingFiles) {
if (!missingFiles.isEmpty()) {
var questionDialog = questionMissingFilesCleanup(getStage(), missingFiles);
if (questionDialog.showAndWait() == AlertDialog.ButtonID.OK) {
if (questionDialog.showAndWait() == ButtonID.OK) {
removeMissingFilesFromPrefs(missingFiles);
loadAndPopulateRecentItemsInBackground();
}
Expand Down Expand Up @@ -292,7 +321,7 @@ private void openFilesAndHideStage(List<String> files) {
void handleOpen(List<String> filePaths,
Consumer<List<String>> missingFilesHandler,
Consumer<List<String>> fileLoader) {

LOGGER.log(Level.INFO, "Attempting to open files: {0}", filePaths);
if (filePaths.isEmpty()) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,9 @@ welcome.recent.items.loading = Loading recent projects...
welcome.recent.items.no.recent.items = no recent projects
welcome.open.project.label = Open Project
welcome.loading.label = Loading Components...
welcome.loading.when.dropped.error.title=Unsupported file format or empty directory
welcome.loading.when.dropped.error.message=The dropped object is either not a JavaFX FXML file or does not contain any FXML files to be loaded.

# -- Template (this keys are replicated from SceneBuilderKit.properties)
template.new.project.label = New Project from Template
template.title.header.desktop = Desktop
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>

<!--
Copyright (c) 2017, 2022, Gluon and/or its affiliates.
Copyright (c) 2017, 2024, Gluon and/or its affiliates.
All rights reserved. Use is subject to license terms.
This file is available and licensed under the following license:
Expand Down Expand Up @@ -43,7 +43,7 @@
<?import javafx.scene.text.Font?>

<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="650.0" prefWidth="1024.0" stylesheets="@WelcomeWindow.css" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1">
<BorderPane fx:id="contentPane">
<BorderPane fx:id="contentPane" onDragDropped="#handleDroppedFiles" onDragOver="#handleFileDraggedOver">
<left>
<VBox styleClass="left-pane">
<children>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Copyright (c) 2024, Gluon and/or its affiliates.
* All rights reserved. Use is subject to license terms.
*
* This file is available and licensed under the following license:
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the distribution.
* - Neither the name of Oracle Corporation and Gluon nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package com.oracle.javafx.scenebuilder.app.welcomedialog;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;

import static org.junit.jupiter.api.Assertions.*;

class WelcomeDialogFilesDropHandlerTest {

private WelcomeDialogFilesDropHandler classUnderTest;

@Test
void that_fxml_file_is_detected_properly(@TempDir Path directory) {
List<File> droppedFiles = List.of();
classUnderTest = new WelcomeDialogFilesDropHandler(droppedFiles);

assertFalse(classUnderTest.isFxml(directory.toFile()), "directories are not FXML");
assertFalse(classUnderTest.isFxml(new File("SomeImage.png")), "FXML files must have fxml file name extension");
assertTrue(classUnderTest.isFxml(new File("View.fxml")), "FXML files must have fxml file name extension");
assertTrue(classUnderTest.isFxml(new File("View.FxMl")), "FXML extension detection must not be case sensitive");
}

@Test
void that_exception_is_raised_with_incomplete_configuration() {
classUnderTest = new WelcomeDialogFilesDropHandler(List.of());
assertThrows(IllegalStateException.class, ()->classUnderTest.run());

classUnderTest.withSupportedFiles(files->System.out.println(files));
assertThrows(IllegalStateException.class, ()->classUnderTest.run());

classUnderTest.withUnsupportedFiles(unsupported->System.out.println(unsupported));
assertDoesNotThrow(()->classUnderTest.run());
}

@Test
void that_list_of_fxml_files_will_passed_to_open_action() {
List<File> droppedFiles = List.of(new File("MainView.fxml"), new File("SubView.fxml"));

// Action handler for opening files
List<String> fileOpenResults = new ArrayList<>();
Consumer<List<String>> openFilesAction = files->{
for (String file : files) {
fileOpenResults.add("opened " + new File(file).getName());
}
};

// Action handler to notify user on unsupported items
List<String> unsupportedFiles = new ArrayList<>();
Consumer<List<String>> unsupportedFileHandling = unsupported->unsupportedFiles.addAll(unsupported);

classUnderTest = new WelcomeDialogFilesDropHandler(droppedFiles)
.withSupportedFiles(openFilesAction)
.withUnsupportedFiles(unsupportedFileHandling);

assertDoesNotThrow(()->classUnderTest.run());
assertEquals(2, fileOpenResults.size());
assertEquals("opened MainView.fxml", fileOpenResults.get(0));
assertTrue(unsupportedFiles.isEmpty());
}

@Test
void that_an_attempt_to_handle_unsupported_files_triggers_appropriate_action(@TempDir Path emptyDir) throws Exception {
List<File> droppedFiles = List.of(new File("Image.png"), emptyDir.toFile());

// Action handler for opening files
List<String> fileOpenResults = new ArrayList<>();
Consumer<List<String>> openFilesAction = files->fileOpenResults.addAll(files);

// Action handler to notify user on unsupported items
List<String> unsupportedFiles = new ArrayList<>();
Consumer<List<String>> unsupportedFileHandling = unsupported->{
for (String file : unsupported) {
var item = new File(file);
if (item.isDirectory()) {
unsupportedFiles.add(new File(file).getName() + "(dir is empty)");
} else {
unsupportedFiles.add(new File(file).getName());
}
}
};

classUnderTest = new WelcomeDialogFilesDropHandler(droppedFiles)
.withSupportedFiles(openFilesAction)
.withUnsupportedFiles(unsupportedFileHandling);
classUnderTest.run();

assertTrue(fileOpenResults.isEmpty());
assertEquals(2, unsupportedFiles.size());
assertEquals("Image.png", unsupportedFiles.get(0));
assertTrue(unsupportedFiles.get(1).endsWith("(dir is empty)"));
}

@Test
void that_dropped_subdirectories_are_searched_for_fxml_in_first_level(@TempDir Path fxmlDir) {
List<File> droppedFiles = List.of(new File("src/main/resources/com/oracle/javafx/scenebuilder/app/welcomedialog"),
new File("src/main/resources/com/oracle/javafx/scenebuilder/app/DocumentWindow.fxml"),
new File("src/main/resources/com/oracle/javafx/scenebuilder/app/SceneBuilderLogo_32.png"));

// Action handler for opening files
Set<String> fileOpenResults = new HashSet<>();
Consumer<List<String>> openFilesAction = files->{
for (String file : files) {
fileOpenResults.add("opened " + new File(file).getName());
}
};

// Action handler to notify user on unsupported items
List<String> unsupportedFiles = new ArrayList<>();
Consumer<List<String>> unsupportedFileHandling = unsupported->unsupportedFiles.addAll(unsupported);

classUnderTest = new WelcomeDialogFilesDropHandler(droppedFiles)
.withSupportedFiles(openFilesAction)
.withUnsupportedFiles(unsupportedFileHandling);

classUnderTest.run();
assertTrue(fileOpenResults.contains("opened WelcomeWindow.fxml"), "FXML from dropped directory");
assertTrue(fileOpenResults.contains("opened DocumentWindow.fxml"), "FXML file dropped");
assertTrue(unsupportedFiles.isEmpty());
}

}

0 comments on commit 3a27bd7

Please sign in to comment.