From 976c754360f4bf98ca8680f173c80ef0a72cf7df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20L=C3=B6ffler?= <22102800+Oliver-Loeffler@users.noreply.github.com> Date: Fri, 29 Mar 2024 08:14:04 +0100 Subject: [PATCH] fix: SceneBuilder will not load missing files from recent projects (#585) --- .../scenebuilder/app/SceneBuilderApp.java | 132 ++++++++++++------ .../WelcomeDialogWindowController.java | 127 ++++++++++++++--- .../app/i18n/SceneBuilderApp.properties | 14 +- .../app/i18n/SceneBuilderApp_ja.properties | 4 - .../app/i18n/SceneBuilderApp_zh_CN.properties | 4 - .../scenebuilder/app/JfxInitializer.java | 64 +++++++++ .../WelcomeDialogWindowControllerTest.java | 113 +++++++++++++++ kit/pom.xml | 17 --- .../editor/panel/util/dialog/ErrorDialog.java | 13 +- .../util/dialog/AbstractModalDialogW.fxml | 6 +- pom.xml | 17 +++ 11 files changed, 417 insertions(+), 94 deletions(-) create mode 100644 app/src/test/java/com/oracle/javafx/scenebuilder/app/JfxInitializer.java create mode 100644 app/src/test/java/com/oracle/javafx/scenebuilder/app/welcomedialog/WelcomeDialogWindowControllerTest.java diff --git a/app/src/main/java/com/oracle/javafx/scenebuilder/app/SceneBuilderApp.java b/app/src/main/java/com/oracle/javafx/scenebuilder/app/SceneBuilderApp.java index f9630b1ce..351a99732 100644 --- a/app/src/main/java/com/oracle/javafx/scenebuilder/app/SceneBuilderApp.java +++ b/app/src/main/java/com/oracle/javafx/scenebuilder/app/SceneBuilderApp.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2023, Gluon and/or its affiliates. + * Copyright (c) 2016, 2024, Gluon and/or its affiliates. * Copyright (c) 2012, 2014, Oracle and/or its affiliates. * All rights reserved. Use is subject to license terms. * @@ -82,9 +82,16 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDate; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -94,6 +101,8 @@ */ public class SceneBuilderApp extends Application implements AppPlatform.AppNotificationHandler { + private static final Logger LOGGER = Logger.getLogger(SceneBuilderApp.class.getName()); + public enum ApplicationControlAction { ABOUT, CHECK_UPDATES, @@ -496,8 +505,25 @@ private void createEmptyDocumentWindow() { newWindow.updateWithDefaultContent(); } + /** + * By default all necessary actions to open a single file or a group of files take place in this method. + * If it is required to perform certain actions after successfully loading all files, please use {@code handleOpenFilesAction(List files, Runnable onSuccess)} instead. + * + * All error handling takes place here within, there is no way yet to access exceptional results and to work with them. + */ @Override public void handleOpenFilesAction(List files) { + handleOpenFilesAction(files, () -> { /* no operation in this case */ }); + } + + /** + * As file loading errors are handled within this method (all exceptions are handled within), it can be helpful to be able to run a certain action after successful file loading (e.g. closing a certain stage). + * For this case this method offers the argument {@code Runnable onSuccess} which will be executed after successful file open activity. The {@code Runnable onSuccess} is only ran once, despite how many files have been loaded. + * + * @param files List of Strings denoting file paths to be opened + * @param onSuccess {@link Runnable} to be executed after all files have been opened successfully + */ + public void handleOpenFilesAction(List files, Runnable onSuccess) { assert files != null; assert files.isEmpty() == false; @@ -507,24 +533,60 @@ public void handleOpenFilesAction(List files) { } EditorController.updateNextInitialDirectory(fileObjs.get(0)); - + + Consumer> onError = errors -> showFileOpenErrors(errors, + () -> WelcomeDialogWindowController.getInstance().getStage()); + // Fix for #45 if (userLibrary.isFirstExplorationCompleted()) { - performOpenFiles(fileObjs); + performOpenFiles(fileObjs, onError, onSuccess); } else { // open files only after the first exploration has finished userLibrary.firstExplorationCompletedProperty().addListener(new InvalidationListener() { @Override public void invalidated(Observable observable) { if (userLibrary.isFirstExplorationCompleted()) { - performOpenFiles(fileObjs); userLibrary.firstExplorationCompletedProperty().removeListener(this); + performOpenFiles(fileObjs, onError, onSuccess); } } }); } } + /** + * For each file open error (when opened through the welcome dialog), the file + * name and the related exception text are presented to the user to confirm. + * For an empty collection, no dialog is displayed. + * + * @param errors A {@link Map} with having the file to be opened as the key and + * the occurred {@link Exception} as value. exceptions. + * @param owner Owner Supplier function to obtain the owners {@link Stage} + */ + private void showFileOpenErrors(Map errors, Supplier owner) { + if (errors.isEmpty()) { + return; + } + + for (Entry error : errors.entrySet()) { + final File fxmlFile = error.getKey(); + final Exception x = error.getValue(); + final ErrorDialog errorDialog = new ErrorDialog(owner.get()); + errorDialog.setMessage(I18N.getString("alert.open.failure1.message", displayName(fxmlFile.getPath()))); + errorDialog.setDetails(I18N.getString("alert.open.failure1.details")); + errorDialog.setDebugInfoWithThrowable(x); + errorDialog.setTitle(I18N.getString("alert.open.failure.title")); + errorDialog.setDetailsTitle(I18N.getString("alert.open.failure.title") + ": " + fxmlFile.getName()); + errorDialog.showAndWait(); + } + } + + private Supplier getOwnerWindow() { + return () -> findFirstUnusedDocumentWindowController() + .map(DocumentWindowController::getStage) + .orElse(WelcomeDialogWindowController.getInstance().getStage()); + } + @Override public void handleMessageBoxFailure(Exception x) { final ErrorDialog errorDialog = new ErrorDialog(null); @@ -657,11 +719,18 @@ public DocumentWindowController getFrontDocumentWindow() { } private void performOpenFiles(List fxmlFiles) { + performOpenFiles(fxmlFiles, r -> showFileOpenErrors(r, getOwnerWindow()), () -> { /* no action here */ } ); + } + + private void performOpenFiles(List fxmlFiles, Consumer> onError, Runnable onSuccess) { assert fxmlFiles != null; assert fxmlFiles.isEmpty() == false; - final Map exceptions = new HashMap<>(); + LOGGER.log(Level.FINE, "Opening {0} files...", fxmlFiles.size()); + final Map exceptionsPerFile = new HashMap<>(); + final List openedFiles = new ArrayList<>(); for (File fxmlFile : fxmlFiles) { + LOGGER.log(Level.FINE, "Attempting to open file {0}", fxmlFile); try { final DocumentWindowController dwc = lookupDocumentWindowControllers(fxmlFile.toURI().toURL()); @@ -673,46 +742,27 @@ private void performOpenFiles(List fxmlFiles) { var hostWindow = findFirstUnusedDocumentWindowController().orElse(makeNewWindow()); hostWindow.loadFromFile(fxmlFile); hostWindow.openWindow(); + openedFiles.add(fxmlFile); + LOGGER.log(Level.INFO, "Successfully opened file {0}", fxmlFile); } } catch (Exception xx) { - exceptions.put(fxmlFile, xx); + LOGGER.log(Level.WARNING, "Failed to open file: %s".formatted(fxmlFile), xx); + exceptionsPerFile.put(fxmlFile, xx); } } + + // Update recent items with opened files + if (!openedFiles.isEmpty()) { + final PreferencesController pc = PreferencesController.getSingleton(); + pc.getRecordGlobal().addRecentItems(openedFiles); + } - switch (exceptions.size()) { - case 0: { // Good - // Update recent items with opened files - final PreferencesController pc = PreferencesController.getSingleton(); - pc.getRecordGlobal().addRecentItems(fxmlFiles); - break; - } - case 1: { - final File fxmlFile = exceptions.keySet().iterator().next(); - final Exception x = exceptions.get(fxmlFile); - final ErrorDialog errorDialog = new ErrorDialog(null); - errorDialog.setMessage(I18N.getString("alert.open.failure1.message", displayName(fxmlFile.getPath()))); - errorDialog.setDetails(I18N.getString("alert.open.failure1.details")); - errorDialog.setDebugInfoWithThrowable(x); - errorDialog.setTitle(I18N.getString("alert.title.open")); - errorDialog.showAndWait(); - break; - } - default: { - final ErrorDialog errorDialog = new ErrorDialog(null); - if (exceptions.size() == fxmlFiles.size()) { - // Open operation has failed for all the files - errorDialog.setMessage(I18N.getString("alert.open.failureN.message")); - errorDialog.setDetails(I18N.getString("alert.open.failureN.details")); - } else { - // Open operation has failed for some files - errorDialog.setMessage(I18N.getString("alert.open.failureMofN.message", - exceptions.size(), fxmlFiles.size())); - errorDialog.setDetails(I18N.getString("alert.open.failureMofN.details")); - } - errorDialog.setTitle(I18N.getString("alert.title.open")); - errorDialog.showAndWait(); - break; - } + if (exceptionsPerFile.isEmpty()) { + LOGGER.log(Level.FINE, "Successfully opened all files."); + onSuccess.run(); + } else { + LOGGER.log(Level.WARNING, "Failed to open {0} of {1} files!", new Object[] {exceptionsPerFile.size(), fxmlFiles.size()}); + onError.accept(exceptionsPerFile); } } diff --git a/app/src/main/java/com/oracle/javafx/scenebuilder/app/welcomedialog/WelcomeDialogWindowController.java b/app/src/main/java/com/oracle/javafx/scenebuilder/app/welcomedialog/WelcomeDialogWindowController.java index aaac1d51f..deee8cdb7 100644 --- a/app/src/main/java/com/oracle/javafx/scenebuilder/app/welcomedialog/WelcomeDialogWindowController.java +++ b/app/src/main/java/com/oracle/javafx/scenebuilder/app/welcomedialog/WelcomeDialogWindowController.java @@ -1,5 +1,5 @@ /* - * 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: @@ -32,14 +32,27 @@ package com.oracle.javafx.scenebuilder.app.welcomedialog; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + import com.oracle.javafx.scenebuilder.app.SceneBuilderApp; import com.oracle.javafx.scenebuilder.app.i18n.I18N; import com.oracle.javafx.scenebuilder.app.preferences.PreferencesController; 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.template.Template; import com.oracle.javafx.scenebuilder.kit.template.TemplatesBaseWindowController; + import javafx.application.Platform; import javafx.event.ActionEvent; import javafx.fxml.FXML; @@ -52,15 +65,13 @@ import javafx.scene.layout.VBox; import javafx.stage.FileChooser; import javafx.stage.Modality; +import javafx.stage.Stage; import javafx.stage.WindowEvent; -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - public class WelcomeDialogWindowController extends TemplatesBaseWindowController { + private static final Logger LOGGER = Logger.getLogger(WelcomeDialogWindowController.class.getName()); + @FXML private BorderPane contentPane; @@ -80,7 +91,7 @@ public class WelcomeDialogWindowController extends TemplatesBaseWindowController private final SceneBuilderApp sceneBuilderApp; - private WelcomeDialogWindowController() { + WelcomeDialogWindowController() { super(WelcomeDialogWindowController.class.getResource("WelcomeWindow.fxml"), //NOI18N I18N.getBundle(), null); // We want it to be a top level window so we're setting the owner to null. @@ -168,7 +179,7 @@ private void loadAndPopulateRecentItemsInBackground() { } public static WelcomeDialogWindowController getInstance() { - if (instance == null){ + if (instance == null) { instance = new WelcomeDialogWindowController(); var stage = instance.getStage(); stage.setMinWidth(800); @@ -201,7 +212,6 @@ private void openDocument() { new FileChooser.ExtensionFilter(I18N.getString("file.filter.label.fxml"), "*.fxml") ); fileChooser.setInitialDirectory(EditorController.getNextInitialDirectory()); - List fxmlFiles = fileChooser.showOpenMultipleDialog(getStage()); // no file was selected, so nothing to do @@ -215,17 +225,101 @@ private void openDocument() { handleOpen(paths); } + + protected static AlertDialog questionMissingFilesCleanup(Stage stage, List missingFiles) { + String withPath = missingFiles.stream() + .collect(Collectors.joining(System.lineSeparator())); + + AlertDialog question = new AlertDialog(stage); + question.setDefaultButtonID(ButtonID.CANCEL); + question.setShowDefaultButton(true); + question.setOKButtonTitle(I18N.getString("alert.welcome.file.not.found.okay")); + question.setTitle(I18N.getString("alert.welcome.file.not.found.title")); + question.setMessage(I18N.getString("alert.welcome.file.not.found.question")); + question.setCancelButtonTitle(I18N.getString("alert.welcome.file.not.found.no")); + question.setDetails(I18N.getString("alert.welcome.file.not.found.message") + withPath); + return question; + } + + boolean filePathExists(String filePath) { + return Files.exists(Path.of(filePath)); + } + + /** + * Attempts to open files in filePaths. Scene Builder will only attempt to load + * files which exist. If a file does not exist, Scene Builder will ask the user + * to remove this file from recent files. + * + * @param filePaths List of file paths to project files to be opened by Scene + * Builder. + */ + private void handleOpen(List filePaths) { + handleOpen(filePaths, + this::askUserToRemoveMissingRecentFiles, + this::attemptOpenExistingFiles); + } + + private void askUserToRemoveMissingRecentFiles(List missingFiles) { + if (!missingFiles.isEmpty()) { + var questionDialog = questionMissingFilesCleanup(getStage(), missingFiles); + if (questionDialog.showAndWait() == AlertDialog.ButtonID.OK) { + removeMissingFilesFromPrefs(missingFiles); + loadAndPopulateRecentItemsInBackground(); + } + } + } - private void handleOpen(List paths) { + private void attemptOpenExistingFiles(List paths) { if (sceneBuilderApp.startupTasksFinishedBinding().get()) { - sceneBuilderApp.handleOpenFilesAction(paths); - getStage().hide(); + openFilesAndHideStage(paths); } else { - showMasker(() -> { - sceneBuilderApp.handleOpenFilesAction(paths); - getStage().hide(); - }); + showMasker(() -> openFilesAndHideStage(paths)); + } + } + + private void openFilesAndHideStage(List files) { + sceneBuilderApp.handleOpenFilesAction(files, () -> getStage().hide()); + } + + /** + * Attempts to open files in filePaths. + * In case of files are missing, a special procedure is applied to handle missing files. + * + * @param filePaths List of file paths to project files to be opened by Scene Builder. + * @param missingFilesHandler Determines how missing files are handled. + * @param fileLoader Determines how files are loaded. + */ + void handleOpen(List filePaths, + Consumer> missingFilesHandler, + Consumer> fileLoader) { + + if (filePaths.isEmpty()) { + return; } + + List existingFiles = new ArrayList<>(); + List missingFiles = new ArrayList<>(); + filePaths.forEach(file -> { + if (filePathExists(file)) { + existingFiles.add(file); + } else { + missingFiles.add(file); + } + }); + + missingFilesHandler.accept(missingFiles); + + if (existingFiles.isEmpty()) { + return; + } + + fileLoader.accept(existingFiles); + } + + private void removeMissingFilesFromPrefs(List missingFiles) { + missingFiles.forEach(fxmlFileName -> LOGGER.log(Level.INFO, "Removing missing file from recent items: {0}", fxmlFileName)); + PreferencesRecordGlobal preferencesRecordGlobal = PreferencesController.getSingleton().getRecordGlobal(); + preferencesRecordGlobal.removeRecentItems(missingFiles); } private void showMasker(Runnable onEndAction) { @@ -239,7 +333,6 @@ private void showMasker(Runnable onEndAction) { if (isFinished) { Platform.runLater(() -> { onEndAction.run(); - // restore state in case welcome dialog is opened again contentPane.setDisable(false); masker.setVisible(false); diff --git a/app/src/main/resources/com/oracle/javafx/scenebuilder/app/i18n/SceneBuilderApp.properties b/app/src/main/resources/com/oracle/javafx/scenebuilder/app/i18n/SceneBuilderApp.properties index 98c136064..90350b145 100644 --- a/app/src/main/resources/com/oracle/javafx/scenebuilder/app/i18n/SceneBuilderApp.properties +++ b/app/src/main/resources/com/oracle/javafx/scenebuilder/app/i18n/SceneBuilderApp.properties @@ -1,4 +1,4 @@ -# Copyright (c) 2016, 2023, Gluon and/or its affiliates. +# Copyright (c) 2016, 2024, Gluon and/or its affiliates. # Copyright (c) 2012, 2014, Oracle and/or its affiliates. # All rights reserved. Use is subject to license terms. # @@ -397,12 +397,9 @@ alert.title.copy = Copy alert.title.start = Startup alert.title.messagebox = External Open +alert.open.failure.title = File Open Error alert.open.failure1.message = Could not open ''{0}'' alert.open.failure1.details = Open operation has failed. Make sure that the chosen file is a valid FXML document. -alert.open.failureN.message = Could not open the specified files -alert.open.failureN.details = Open operation has failed. Make sure that those files are valid FXML documents. -alert.open.failureMofN.message = Could not open {0} files -alert.open.failureMofN.details = Open operation has failed for some files. Make sure that those files are valid FXML documents. alert.review.question.message = {0} of your documents contain unsaved changes. Do you want to review them before exiting ? alert.review.question.details = Your changes will be lost if you don't review them. alert.overwrite.message = The file ''{0}'' was modified externally. Do you want to overwrite it ? @@ -436,7 +433,12 @@ alert.save.noextension.savewith = Save with '.fxml' alert.save.noextension.savewithout = Save without '.fxml' alert.open.failure.charset.not.found = The given charset could not be set. alert.open.failure.charset.not.found.details = It may be due to the encoding in your document. - +alert.welcome.file.not.found.question = One or more project files were not found. +alert.welcome.file.not.found.message = If those are located on a removable or network drive, please make sure the drive is connected.\n\n +alert.welcome.file.not.found.title = Project file(s) not found +alert.welcome.file.not.found.okay = Remove From List +alert.welcome.file.not.found.no = OK + # ----------------------------------------------------------------------------- # Log Messages # ----------------------------------------------------------------------------- diff --git a/app/src/main/resources/com/oracle/javafx/scenebuilder/app/i18n/SceneBuilderApp_ja.properties b/app/src/main/resources/com/oracle/javafx/scenebuilder/app/i18n/SceneBuilderApp_ja.properties index 280754a22..131863bdc 100644 --- a/app/src/main/resources/com/oracle/javafx/scenebuilder/app/i18n/SceneBuilderApp_ja.properties +++ b/app/src/main/resources/com/oracle/javafx/scenebuilder/app/i18n/SceneBuilderApp_ja.properties @@ -394,10 +394,6 @@ alert.title.messagebox = 外部プログラムで開く alert.open.failure1.message = ''{0}''を開けませんでした alert.open.failure1.details = 開くのに失敗しました。選択されたファイルが有効なFXMLドキュメントであることを確認してください。 -alert.open.failureN.message = 指定されたファイルを開けませんでした -alert.open.failureN.details = 開くのに失敗しました。それらのファイルが有効なFXMLドキュメントであることを確認してください。 -alert.open.failureMofN.message = {0}個のファイルを開けませんでした -alert.open.failureMofN.details = いくつかのファイルを開くのに失敗しました。それらのファイルが有効なFXMLドキュメントであることを確認してください。 alert.review.question.message = ドキュメントのうち{0}個に未保存の変更があります。終了する前にそれらをレビューしますか。 alert.review.question.details = レビューしないと、変更は失われます。 alert.overwrite.message = ファイル''{0}''は外部で変更されました。上書きしますか。 diff --git a/app/src/main/resources/com/oracle/javafx/scenebuilder/app/i18n/SceneBuilderApp_zh_CN.properties b/app/src/main/resources/com/oracle/javafx/scenebuilder/app/i18n/SceneBuilderApp_zh_CN.properties index 39a31e900..0764eba7b 100644 --- a/app/src/main/resources/com/oracle/javafx/scenebuilder/app/i18n/SceneBuilderApp_zh_CN.properties +++ b/app/src/main/resources/com/oracle/javafx/scenebuilder/app/i18n/SceneBuilderApp_zh_CN.properties @@ -399,10 +399,6 @@ alert.title.messagebox = 外部打开 alert.open.failure1.message = 无法打开 ''{0}'' alert.open.failure1.details = 打开操作失败。确保所选文件是有效的 FXML 文档。 -alert.open.failureN.message = 无法打开指定的文件 -alert.open.failureN.details = 打开操作失败。请确保这些文件是有效的 FXML 文档。 -alert.open.failureMofN.message = 无法打开 {0} 文件 -alert.open.failureMofN.details = 某些文件的打开操作失败。请确保这些文件是有效的 FXML 文档。 alert.review.question.message = {0} 文档包含未保存的更改。您想在退出之前查看它们吗? alert.review.question.details = 如果您不查看更改,则更改将丢失。 alert.overwrite.message = 文件 ''{0}'' 已在外部修改。你想覆盖它吗? diff --git a/app/src/test/java/com/oracle/javafx/scenebuilder/app/JfxInitializer.java b/app/src/test/java/com/oracle/javafx/scenebuilder/app/JfxInitializer.java new file mode 100644 index 000000000..78a9cb1ca --- /dev/null +++ b/app/src/test/java/com/oracle/javafx/scenebuilder/app/JfxInitializer.java @@ -0,0 +1,64 @@ +/* + * 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; + +import javafx.application.Application; +import javafx.stage.Stage; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class JfxInitializer { + + private static final AtomicBoolean initialized = new AtomicBoolean(false); + + public static void initialize() { + if (initialized.compareAndSet(false, true)) { + Thread t = new Thread("JavaFX Init Thread") { + + @Override + public void run() { + Application.launch(DummyApp.class); + } + }; + t.setDaemon(true); + t.start(); + } + } + + public static class DummyApp extends Application { + + @Override + public void start(Stage primaryStage) { + // noop + } + } +} diff --git a/app/src/test/java/com/oracle/javafx/scenebuilder/app/welcomedialog/WelcomeDialogWindowControllerTest.java b/app/src/test/java/com/oracle/javafx/scenebuilder/app/welcomedialog/WelcomeDialogWindowControllerTest.java new file mode 100644 index 000000000..81666f9e7 --- /dev/null +++ b/app/src/test/java/com/oracle/javafx/scenebuilder/app/welcomedialog/WelcomeDialogWindowControllerTest.java @@ -0,0 +1,113 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +import com.oracle.javafx.scenebuilder.app.JfxInitializer; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class WelcomeDialogWindowControllerTest { + + private final WelcomeDialogWindowController classUnderTest = new WelcomeDialogWindowController(); + + @BeforeAll + public static void initialize() { + JfxInitializer.initialize(); + } + + @Test + void that_missing_files_are_detected_and_handled_and_existing_files_are_loaded() throws Exception { + String expectedExistingFile = getResource("WelcomeWindow.fxml").toString(); + List filesToLoad = List.of( + "k:/folder/test/notExisting.fxml", + expectedExistingFile); + + List filesMissing = new ArrayList<>(); + List filesLoaded = new ArrayList<>(); + + Consumer> missingFilesHandler = missing -> filesMissing.addAll(missing); + Consumer> existingFilesHandler = existing -> filesLoaded.addAll(existing); + + assertDoesNotThrow(() -> classUnderTest.handleOpen(filesToLoad, + missingFilesHandler, + existingFilesHandler)); + + assertEquals(1, filesMissing.size()); + assertEquals(1, filesLoaded.size()); + assertTrue(filesLoaded.contains(expectedExistingFile)); + } + + @Test + void that_no_actions_are_performed_on_empty_list() { + List filesToLoad = Collections.emptyList(); + + Set actionsPerformed = new HashSet<>(); + Consumer> filesHandler = listOffiles -> actionsPerformed.add("some action performed"); + classUnderTest.handleOpen(filesToLoad, filesHandler, filesHandler); + + assertTrue(actionsPerformed.isEmpty()); + } + + @Test + void that_file_loader_is_not_called_when_all_files_are_missing() { + List filesToLoad = List.of( + "k:/folder/test/notExisting.fxml", + "o:\\otherLocation\\another_missing.fxml"); + + List filesMissing = new ArrayList<>(); + List filesLoaded = new ArrayList<>(); + + Consumer> missingFilesHandler = missing -> filesMissing.addAll(missing); + Consumer> existingFilesHandler = existing -> filesLoaded.addAll(existing); + classUnderTest.handleOpen(filesToLoad, missingFilesHandler, existingFilesHandler); + + assertTrue(filesLoaded.isEmpty()); + assertEquals(2, filesMissing.size()); + } + + private Path getResource(String resourceName) throws Exception { + return Path.of(getClass().getResource(resourceName).toURI()); + } +} diff --git a/kit/pom.xml b/kit/pom.xml index ee9161424..c205135b6 100644 --- a/kit/pom.xml +++ b/kit/pom.xml @@ -91,14 +91,6 @@ 1.0.4 runtime - - - - org.assertj - assertj-core - 3.19.0 - test - @@ -117,15 +109,6 @@ - - org.apache.maven.plugins - maven-surefire-plugin - 3.0.0-M5 - - false - 1 - - diff --git a/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/panel/util/dialog/ErrorDialog.java b/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/panel/util/dialog/ErrorDialog.java index 2696e8518..8ee7f5080 100644 --- a/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/panel/util/dialog/ErrorDialog.java +++ b/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/panel/util/dialog/ErrorDialog.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2014, Oracle and/or its affiliates. + * Copyright (c) 2012, 2024, Oracle and/or its affiliates. * All rights reserved. Use is subject to license terms. * * This file is available and licensed under the following license: @@ -43,6 +43,7 @@ public class ErrorDialog extends AlertDialog { private String debugInfo; + private String detailsTitle; public ErrorDialog(Window owner) { super(owner); @@ -80,7 +81,10 @@ public void setDebugInfoWithThrowable(Throwable t) { setDebugInfo(info); } - + public void setDetailsTitle(String detailsTitle) { + this.detailsTitle = detailsTitle; + } + /* * Private */ @@ -90,8 +94,11 @@ private void updateActionButtonVisibility() { } private void showDetailsDialog() { - final TextViewDialog detailDialog = new TextViewDialog(null); + final TextViewDialog detailDialog = new TextViewDialog(this.getStage()); detailDialog.setText(debugInfo); + if (detailsTitle != null) { + detailDialog.setTitle(detailsTitle); + } detailDialog.showAndWait(); } } diff --git a/kit/src/main/resources/com/oracle/javafx/scenebuilder/kit/editor/panel/util/dialog/AbstractModalDialogW.fxml b/kit/src/main/resources/com/oracle/javafx/scenebuilder/kit/editor/panel/util/dialog/AbstractModalDialogW.fxml index c847ccc9f..84284225a 100644 --- a/kit/src/main/resources/com/oracle/javafx/scenebuilder/kit/editor/panel/util/dialog/AbstractModalDialogW.fxml +++ b/kit/src/main/resources/com/oracle/javafx/scenebuilder/kit/editor/panel/util/dialog/AbstractModalDialogW.fxml @@ -1,4 +1,5 @@ + - + + @@ -55,7 +57,7 @@ - + diff --git a/pom.xml b/pom.xml index 99dba32f5..b50ac0059 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,12 @@ junit-jupiter-engine test + + org.assertj + assertj-core + 3.19.0 + test + @@ -134,6 +140,17 @@ maven-resources-plugin 3.2.0 + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + false + 1 + +