diff --git a/README.md b/README.md index b5ac57c91..d59c37696 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,10 @@ See the [documentation](http://docs.gluonhq.com/scenebuilder/) about the new fea For community support, go to [StackOverflow](https://stackoverflow.com/questions/tagged/scenebuilder). +### Requirements on Linux ### + +On Linux systems, Scene Builder uses the `xdg-open` command to reveal files in the system's default file system browser or to open URLs in the default web browser. Most modern Linux Desktop Environments already provide the [xdg-utils](https://freedesktop.org/wiki/Software/xdg-utils/) package. If it is missing, it can be installed using the respective Linux package management tool such as `yum`, `apt-get`, `dnf` or `pacman`. The `xdg-utils` package is usually available on KDE based systems, some Arch based systems may require manual installations. + ## Issues and Contributions ## Issues can be reported to the [Issue tracker](https://github.com/gluonhq/scenebuilder/issues/) diff --git a/app/src/main/java/com/oracle/javafx/scenebuilder/app/DocumentWindowController.java b/app/src/main/java/com/oracle/javafx/scenebuilder/app/DocumentWindowController.java index 1ec055b5b..8fcb64064 100644 --- a/app/src/main/java/com/oracle/javafx/scenebuilder/app/DocumentWindowController.java +++ b/app/src/main/java/com/oracle/javafx/scenebuilder/app/DocumentWindowController.java @@ -47,6 +47,7 @@ import com.oracle.javafx.scenebuilder.kit.editor.EditorController.ControlAction; import com.oracle.javafx.scenebuilder.kit.editor.EditorController.EditAction; import com.oracle.javafx.scenebuilder.kit.editor.EditorPlatform; +import com.oracle.javafx.scenebuilder.kit.editor.FileBrowserRevealException; import com.oracle.javafx.scenebuilder.kit.editor.job.Job; import com.oracle.javafx.scenebuilder.kit.editor.panel.content.ContentPanelController; import com.oracle.javafx.scenebuilder.kit.editor.panel.css.CssPanelController; @@ -94,6 +95,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; import javafx.beans.InvalidationListener; import javafx.beans.binding.Bindings; @@ -184,6 +187,8 @@ public enum ActionStatus { DONE } + private static final Logger LOGGER = Logger.getLogger(DocumentWindowController.class.getName()); + private final EditorController editorController = new EditorController(); private final MenuBarController menuBarController = new MenuBarController(this); private final ContentPanelController contentPanelController = new ContentPanelController(editorController); @@ -1467,14 +1472,13 @@ void onLibraryImportSelection(ActionEvent event) { @FXML void onLibraryRevealCustomFolder(ActionEvent event) { String userLibraryPath = ((UserLibrary) getEditorController().getLibrary()).getPath(); + + // ensure that there is no mixup of forward and backward slashes. + File libraryPath = Paths.get(userLibraryPath).normalize().toFile(); try { - EditorPlatform.revealInFileBrowser(new File(userLibraryPath)); - } catch(IOException x) { - final ErrorDialog errorDialog = new ErrorDialog(null); - errorDialog.setMessage(I18N.getString("alert.reveal.failure.message", getStage().getTitle())); - errorDialog.setDetails(I18N.getString("alert.reveal.failure.details")); - errorDialog.setDebugInfoWithThrowable(x); - errorDialog.showAndWait(); + EditorPlatform.revealInFileBrowser(libraryPath); + } catch (Exception revealError) { + handleRevealFolderException(revealError, String.valueOf(libraryPath)); } } @@ -2185,26 +2189,33 @@ ActionStatus performCloseAction() { return closeConfirmed ? ActionStatus.DONE : ActionStatus.CANCELLED; } - private void performRevealAction() { assert editorController.getFxomDocument() != null; assert editorController.getFxomDocument().getLocation() != null; - + final URL location = editorController.getFxomDocument().getLocation(); - + + File fxmlFile = null; + try { + /* + * Using Path.normalize().toAbsolutePath() ensures that forward and backward slashes are not mixed + * and the path matches the platform requirements. It also ensures, that the file:/ prefix is + * removed from paths and users can directly use the path in their attempt to investigate the error. + */ + fxmlFile = Path.of(location.toURI()).normalize().toAbsolutePath().toFile(); + } catch (URISyntaxException e) { + handleRevealResourceException(e, String.valueOf(location)); + } + try { - final File fxmlFile = new File(location.toURI()); EditorPlatform.revealInFileBrowser(fxmlFile); - } catch(IOException | URISyntaxException x) { - final ErrorDialog errorDialog = new ErrorDialog(null); - errorDialog.setMessage(I18N.getString("alert.reveal.failure.message", getStage().getTitle())); - errorDialog.setDetails(I18N.getString("alert.reveal.failure.details")); - errorDialog.setDebugInfoWithThrowable(x); - errorDialog.showAndWait(); + } catch (FileBrowserRevealException re) { + handleRevealFileException(re, String.valueOf(fxmlFile)); + } catch (IOException x) { + handleRevealResourceException(x, String.valueOf(fxmlFile)); } } - private void updateLoadFileTime() { final URL fxmlURL = editorController.getFxmlLocation(); @@ -2275,17 +2286,64 @@ private void performHelp() { private void openURL(String url) { try { + LOGGER.log(Level.FINE, "Attempting to open URL: {0}", url); EditorPlatform.open(url); } catch (IOException ioe) { - handleErrorWhenOpeningURL(ioe, url); + LOGGER.log(Level.WARNING, "Error during attempt to open URL!", ioe); + handleRevealResourceException(ioe, url); } } - private void handleErrorWhenOpeningURL(IOException ioe, String url) { - final ErrorDialog errorDialog = new ErrorDialog(null); - errorDialog.setMessage(I18N.getString("alert.help.failure.message", url)); - errorDialog.setDetails(I18N.getString("alert.messagebox.failure.details")); - errorDialog.setDebugInfoWithThrowable(ioe); + /** + * When an FXML file is to be revealed and an error occurs, the file and the error are displayed in this dialog. + * The error type and its details are displayed within the details window. The dialog itself only + * notifies that there was an error. + * + * @param fileRevealException {@link FileBrowserRevealException} + * @param locationToBeRevealed {@link String} + */ + private void handleRevealFileException(FileBrowserRevealException fileRevealException, String locationToBeRevealed) { + final ErrorDialog errorDialog = new ErrorDialog(this.getStage()); + errorDialog.setTitle(I18N.getString("alert.error.file.reveal.title")); + errorDialog.setMessage(I18N.getString("alert.error.file.reveal.message")); + errorDialog.setDetails(I18N.getString("alert.error.file.reveal.details", locationToBeRevealed)); + errorDialog.setDetailsTitle(I18N.getString("alert.error.file.reveal.details.title")); + errorDialog.setDebugInfoWithThrowable(fileRevealException); + errorDialog.showAndWait(); + } + + /** + * + * Resources may be related to files included with Scene Builder or with arbitrary files (e.g. CSS sheets) + * co-located to a given FXML file. In case of an error opening a resource, a custom dialog is shown. + * + * @param revealException {@link Exception} + * @param locationToBeRevealed {@link String} + */ + private void handleRevealResourceException(Exception revealException, String locationToBeRevealed) { + final ErrorDialog errorDialog = new ErrorDialog(this.getStage()); + errorDialog.setTitle(I18N.getString("alert.error.resource.reveal.title")); + errorDialog.setMessage(I18N.getString("alert.error.resource.reveal.message")); + errorDialog.setDetails(I18N.getString("alert.error.resource.reveal.details", locationToBeRevealed)); + errorDialog.setDetailsTitle(I18N.getString("alert.error.resource.reveal.details.title")); + errorDialog.setDebugInfoWithThrowable(revealException); + errorDialog.showAndWait(); + } + + /** + * + * In some cases the resource to be revealed or opened is a directory. Hence the UI dialog should reflect this. + * + * @param revealException {@link Exception} + * @param directoryToBeRevealed {@link String} + */ + private void handleRevealFolderException(Exception revealException, String directoryToBeRevealed) { + final ErrorDialog errorDialog = new ErrorDialog(this.getStage()); + errorDialog.setTitle(I18N.getString("alert.error.directory.reveal.title")); + errorDialog.setMessage(I18N.getString("alert.error.directory.reveal.message")); + errorDialog.setDetails(I18N.getString("alert.error.directory.reveal.details", directoryToBeRevealed)); + errorDialog.setDetailsTitle(I18N.getString("alert.error.directory.reveal.details.title")); + errorDialog.setDebugInfoWithThrowable(revealException); errorDialog.showAndWait(); } diff --git a/app/src/main/java/com/oracle/javafx/scenebuilder/app/ResourceController.java b/app/src/main/java/com/oracle/javafx/scenebuilder/app/ResourceController.java index 86bc3615e..e63489cd6 100644 --- a/app/src/main/java/com/oracle/javafx/scenebuilder/app/ResourceController.java +++ b/app/src/main/java/com/oracle/javafx/scenebuilder/app/ResourceController.java @@ -35,6 +35,7 @@ import com.oracle.javafx.scenebuilder.app.i18n.I18N; import com.oracle.javafx.scenebuilder.kit.editor.EditorController; import com.oracle.javafx.scenebuilder.kit.editor.EditorPlatform; +import com.oracle.javafx.scenebuilder.kit.editor.FileBrowserRevealException; import com.oracle.javafx.scenebuilder.kit.editor.panel.util.dialog.ErrorDialog; import javafx.stage.FileChooser; @@ -97,10 +98,11 @@ public void performRevealResource() { try { EditorPlatform.revealInFileBrowser(resourceFile); } catch (IOException ioe) { - final ErrorDialog errorDialog = new ErrorDialog(null); - errorDialog.setTitle(I18N.getString("error.file.reveal.title")); - errorDialog.setMessage(I18N.getString("error.file.reveal.message")); - errorDialog.setDetails(I18N.getString("error.filesystem.details")); + final ErrorDialog errorDialog = new ErrorDialog(documentWindowController.getStage()); + errorDialog.setTitle(I18N.getString("alert.error.file.reveal.title")); + errorDialog.setMessage(I18N.getString("alert.error.file.reveal.message")); + errorDialog.setDetails(I18N.getString("alert.error.file.reveal.details", resourceFile)); + errorDialog.setDetailsTitle(I18N.getString("alert.error.file.reveal.details.title")); errorDialog.setDebugInfoWithThrowable(ioe); errorDialog.showAndWait(); } diff --git a/app/src/main/java/com/oracle/javafx/scenebuilder/app/SceneStyleSheetMenuController.java b/app/src/main/java/com/oracle/javafx/scenebuilder/app/SceneStyleSheetMenuController.java index e8c977fc6..d8dce99a9 100644 --- a/app/src/main/java/com/oracle/javafx/scenebuilder/app/SceneStyleSheetMenuController.java +++ b/app/src/main/java/com/oracle/javafx/scenebuilder/app/SceneStyleSheetMenuController.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2024, Gluon and/or its affiliates. * Copyright (c) 2012, 2014, Oracle and/or its affiliates. * All rights reserved. Use is subject to license terms. * @@ -102,10 +103,11 @@ public void performOpenSceneStyleSheet(File toOpen) { try { EditorPlatform.open(toOpen.getPath()); } catch (IOException ioe) { - final ErrorDialog errorDialog = new ErrorDialog(null); + final ErrorDialog errorDialog = new ErrorDialog(documentWindowController.getStage()); errorDialog.setTitle(I18N.getString("error.file.open.title")); - errorDialog.setMessage(I18N.getString("error.file.open.message")); + errorDialog.setMessage(I18N.getString("error.file.open.message", toOpen)); errorDialog.setDetails(I18N.getString("error.filesystem.details")); + errorDialog.setDetailsTitle(I18N.getString("error.file.open.title")); errorDialog.setDebugInfoWithThrowable(ioe); errorDialog.showAndWait(); } 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 f455f81a6..b3f4b97ac 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 @@ -417,8 +417,6 @@ alert.messagebox.failure.message = Could not process open request alert.messagebox.failure.details = An unexpected failure happened during open operation. Please report this failure to support. alert.revert.question.message = Do you want to revert to the last saved version of ''{0}'' ? alert.revert.question.details = Your current changes will be lost. -alert.reveal.failure.message = Could not reveal ''{0}'' -alert.reveal.failure.details = Reveal operation has failed. Check logs of the operating system. alert.copy.failure.message = Could not copy ''{0}'' alert.help.failure.message = Could not access to ''{0}'' alert.delete.fxid1of1.message = This component has an fx:id. Do you really want to delete it ? @@ -470,11 +468,29 @@ file.filter.label.image = Image Document file.filter.label.media = Media Document file.filter.label.video = Video Document error.file.open.title = File opening failed +error.file.open.details.title = File opening failed - Details # {0} is a file path error.file.open.message = File {0} cannot be opened -error.filesystem.details = Please check your file system. -error.file.reveal.title = File locating failed -error.file.reveal.message = File {0} cannot be found +error.filesystem.details = File: {0} + +# File Reveal Error Dialog +alert.error.file.reveal.title = Failed to reveal file ... +alert.error.file.reveal.message = Cannot reveal file in file system viewer due to an error. +alert.error.file.reveal.details = ''{0}'' +alert.error.file.reveal.details.title = File Reveal Error Details + +# Directory Reveal Error Dialog +alert.error.directory.reveal.title = Failed to reveal folder ... +alert.error.directory.reveal.message = Cannot reveal folder in file system viewer due to an error. +alert.error.directory.reveal.details = ''{0}'' +alert.error.directory.reveal.details.title = Folder Reveal Error Details + +# Resource Reveal Error Dialog +alert.error.resource.reveal.title = Failed to reveal resource ... +alert.error.resource.reveal.message = Cannot reveal resource in file system viewer due to an error. +alert.error.resource.reveal.details = ''{0}'' +alert.error.resource.reveal.details.title = Resource Reveal Error Details + # log.start = JavaFX Scene Builder started log.stop = JavaFX Scene Builder stopped 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 131863bdc..4a6650813 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 @@ -410,8 +410,6 @@ alert.messagebox.failure.message = 開く操作を処理できませんでした alert.messagebox.failure.details = 開く操作中に予期しない問題が起きました。この問題をサポートに報告してください。 alert.revert.question.message = 最後に保存した''{0}''に戻しますか。 alert.revert.question.details = 現在の変更は失われます。 -alert.reveal.failure.message = ''{0}''を表示できませんでした -alert.reveal.failure.details = 元に戻すのに失敗しました。オペレーティング・システムのログを確認してください。 alert.copy.failure.message = コピー alert.help.failure.message = ''{0}''にアクセスできませんでした alert.delete.fxid1of1.message = このコンポーネントにはfx:idがあります。このコンポーネントを削除しますか。 @@ -457,7 +455,7 @@ file.filter.label.fxml = FXMLドキュメント file.filter.label.image = イメージ・ドキュメント file.filter.label.media = メディア・ドキュメント file.filter.label.video = ビデオ・ドキュメント -error.file.open.title = ファイルを開くのに失敗しました + # {0} is a file path error.file.open.message = ファイル{0}を開けません error.filesystem.details = ファイル・システムを確認してください。 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 0764eba7b..2fd97b692 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 @@ -416,8 +416,6 @@ alert.messagebox.failure.message = 无法处理打开的请求 alert.messagebox.failure.details = 打开操作期间发生意外故障。请向支持部门报告此故障。 alert.revert.question.message = 是否要恢复到上次保存的 ''{0}'' 版本? alert.revert.question.details = 您当前的更改将丢失。 -alert.reveal.failure.message = 无法显示 ''{0}'' -alert.reveal.failure.details = 显示操作失败。检查操作系统的日志。 alert.copy.failure.message = 无法复制 ''{0}'' alert.help.failure.message = 无法访问 ''{0}'' alert.delete.fxid1of1.message = 此组件具有 fx:id,你真的要删除它吗? diff --git a/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/Cmd.java b/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/Cmd.java new file mode 100644 index 000000000..2712a60b2 --- /dev/null +++ b/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/Cmd.java @@ -0,0 +1,68 @@ +/* + * 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.kit.editor; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Cmd allows the execution of a given command line in a defined working directory. The execution is + * aborted after the timeout. + */ +final class Cmd { + + /** + * Executes a given command line using the working directory and timeout. + * + * @param cmd The command line to be executed defined as a {@link List} of {@link String} + * @param wDir The working directory, where the process shall be executed within. + * @param timeoutSec Duration in in seconds after which the execution should be stopped. + * @return exit code of the command line as an Integer + * + * @throws IOException - if the command was not found or the program execution exceeds the given timeout duration. + * @throws InterruptedException - if the current thread is interrupted while waiting + */ + public final Integer exec(List cmd, File wDir, long timeoutSec) throws IOException, + InterruptedException { + ProcessBuilder builder = new ProcessBuilder(cmd); + builder = builder.directory(wDir); + Process proc = builder.start(); + boolean completed = proc.waitFor(timeoutSec, TimeUnit.SECONDS); + if (completed) { + return proc.exitValue(); + } + throw new IOException("Process timed out after %s seconds!".formatted(timeoutSec)); + } + +} diff --git a/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/EditorController.java b/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/EditorController.java index 41073419a..0bb065252 100644 --- a/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/EditorController.java +++ b/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/EditorController.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023, Gluon and/or its affiliates. + * Copyright (c) 2017, 2024, Gluon and/or its affiliates. * Copyright (c) 2012, 2014, Oracle and/or its affiliates. * All rights reserved. Use is subject to license terms. * @@ -2331,12 +2331,13 @@ private void performEditIncludedFile() { assert includedFile != null; // Because of (1) try { EditorPlatform.open(includedFile.getAbsolutePath()); - } catch (IOException ioe) { - final ErrorDialog errorDialog = new ErrorDialog(null); - errorDialog.setTitle(I18N.getString("error.file.open.title")); - errorDialog.setMessage(I18N.getString("error.file.open.message", - includedFile.getAbsolutePath())); - errorDialog.setDebugInfoWithThrowable(ioe); + } catch (IOException re) { + final ErrorDialog errorDialog = new ErrorDialog(this.ownerWindow); + errorDialog.setTitle(I18N.getString("alert.error.file.open.title")); + errorDialog.setMessage(I18N.getString("alert.error.file.open.edit.message")); + errorDialog.setDetails(I18N.getString("alert.error.file.reveal.details", includedFile.getAbsolutePath())); + errorDialog.setDetailsTitle(I18N.getString("alert.error.file.open.edit.details.title")); + errorDialog.setDebugInfoWithThrowable(re); errorDialog.showAndWait(); } } @@ -2348,11 +2349,11 @@ private void performRevealIncludedFile() { try { EditorPlatform.revealInFileBrowser(includedFile); } catch (IOException ioe) { - final ErrorDialog errorDialog = new ErrorDialog(null); - errorDialog.setTitle(I18N.getString("error.file.reveal.title")); - errorDialog.setMessage(I18N.getString("error.file.reveal.message", - includedFile.getAbsolutePath())); - errorDialog.setDetails(I18N.getString("error.write.details")); + final ErrorDialog errorDialog = new ErrorDialog(this.ownerWindow); + errorDialog.setTitle(I18N.getString("alert.error.file.reveal.title")); + errorDialog.setMessage(I18N.getString("alert.error.file.reveal.message")); + errorDialog.setDetails(I18N.getString("alert.error.file.reveal.details", includedFile.getAbsolutePath())); + errorDialog.setDetailsTitle(I18N.getString("alert.error.file.reveal.details.title")); errorDialog.setDebugInfoWithThrowable(ioe); errorDialog.showAndWait(); } diff --git a/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/EditorPlatform.java b/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/EditorPlatform.java index 50e729735..40cf39e6d 100644 --- a/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/EditorPlatform.java +++ b/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/EditorPlatform.java @@ -39,11 +39,13 @@ import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import com.oracle.javafx.scenebuilder.kit.i18n.I18N; import javafx.scene.Node; @@ -58,6 +60,8 @@ */ public class EditorPlatform { + private static final Logger LOGGER = Logger.getLogger(EditorPlatform.class.getName()); + public enum OS { LINUX, MAC, WINDOWS; @@ -68,6 +72,10 @@ public static OS get() { private static final String osName = System.getProperty("os.name").toLowerCase(Locale.ROOT); //NOI18N + static { + LOGGER.log(Level.FINE, "Detected Operating System: {0}", osName); + } + /** * True if current platform is running Linux. */ @@ -210,7 +218,7 @@ public Color getColor() { } } } catch (IOException e) { - Logger.getLogger(getClass().getName()).log(Level.WARNING, "Failed to get color from stylesheet: ", e); + LOGGER.log(Level.WARNING, "Failed to get color from stylesheet: ", e); } } return color; @@ -306,9 +314,10 @@ public static boolean isGluonMobileDark(Theme theme) { * 'xdg-open'. On Mac, it runs 'open'. On Windows, it runs 'cmd /c start'. * * @param path path for the file to be opened - * @throws IOException if an error occurs + * @throws IOException in case the application called failed to open due to an error. + * @throws FileBrowserRevealException in case the application opened indicates an error (unexpected return code). */ - public static void open(String path) throws IOException { + public static void open(String path) throws IOException, FileBrowserRevealException { List args = new ArrayList<>(); if (EditorPlatform.IS_MAC) { args.add("open"); //NOI18N @@ -331,21 +340,22 @@ public static void open(String path) throws IOException { } if (!args.isEmpty()) { - executeDaemon(args, null); + executeDaemon(args, null, 0); } } - + /** - * Requests the underlying platform to "reveal" the specified folder. On - * Linux, it runs 'nautilus'. On Mac, it runs 'open'. On Windows, it runs - * 'explorer /select'. + * Requests the underlying platform to open the specified folder in its default file system viewer. This will reveal any file therein. + * On Linux, it runs {@code 'xdg-open'}. On Mac, it runs {@code 'open'} and on Windows it runs {@code 'explorer /select'}. * * @param filePath path for the folder to be revealed - * @throws IOException if an error occurs + * @throws FileBrowserRevealException This exception allows to catch exits codes != 0 from the called process. + * @throws IOException General IOExceptions are thrown by Java System Call Processes in case of an error. */ - public static void revealInFileBrowser(File filePath) throws IOException { + public static void revealInFileBrowser(File filePath) throws IOException, FileBrowserRevealException { List args = new ArrayList<>(); - String path = filePath.toURI().toURL().toExternalForm(); + String path = Paths.get(filePath.toURI()).normalize().toAbsolutePath().toString(); + int exitCodeOk = 0; if (EditorPlatform.IS_MAC) { args.add("open"); //NOI18N args.add("-R"); //NOI18N @@ -353,32 +363,21 @@ public static void revealInFileBrowser(File filePath) throws IOException { } else if (EditorPlatform.IS_WINDOWS) { args.add("explorer"); //NOI18N args.add("/select," + path); //NOI18N - } else if (EditorPlatform.IS_LINUX) { - // nautilus does fine on Ubuntu, which is a Debian. - // I've no idea how it does with other Linux flavors. - args.add("nautilus"); //NOI18N - // The nautilus that comes with Ubuntu up to 11.04 included doesn't - // take a file path as parameter (you get an error popup), you must - // provide a dir path. - // Starting with Ubuntu 11.10 (the first based on kernel 3.x) a - // file path is well managed. - int osVersionNumerical = Integer.parseInt(System.getProperty("os.version").substring(0, 1)); //NOI18N - if (osVersionNumerical < 3) { - // Case Ubuntu 10.04 to 11.04: What you provide to nautilus is - // the name of the directory containing the file you want to see - // listed. See DTL-5384. - path = filePath.getAbsoluteFile().getParent(); - if (path == null) { - path = "."; //NOI18N - } + exitCodeOk = 1; + } else if (EditorPlatform.IS_LINUX) { + args.add("xdg-open"); //NOI18N + path = filePath.getAbsoluteFile().getParent(); + if (path == null) { + path = "."; //NOI18N } args.add(path); } else { // Not Supported + LOGGER.log(Level.SEVERE, "Unsupported operating system! Cannot reveal location {0} in file browser.", path); } if (!args.isEmpty()) { - executeDaemon(args, null); + executeDaemon(args, null, exitCodeOk); } } @@ -410,18 +409,49 @@ public static boolean isNonContinousSelectKeyDown(MouseEvent e) { public static boolean isAssertionEnabled() { return EditorPlatform.class.desiredAssertionStatus(); } - - /* - * Private + + /** + * Executes a system process using the given cmd list as command line definition within the provided + * working directory. + * + * @param cmd Command line definition as {@link List} of {@link String} + * @param wDir Working Directory as {@link File} + * @param exitCodeOk Certain applications (e.g. Windows Explorer) do report exit code 1 in case + * everything is okay. Hence one can configure the expected exit code here. + * @throws IOException Any given runtime exception is collected and re-thrown as + * IOException. + * + * @throws FileBrowserRevealException This exception is only thrown if the exit code of the command + * line call is not 0. This allows to specifically react e.g. to + * invalid command line calls or to unsuccessful calls. Not every + * cmd call which ends with an error code != 0 is creating an + * exception. */ - private static void executeDaemon(List cmd, File wDir) throws IOException { + private static void executeDaemon(List cmd, File wDir, int exitCodeOk) + throws IOException, FileBrowserRevealException { + var cmdLine = cmd.stream().collect(Collectors.joining(" ")); + long timeoutSec = 5; try { - ProcessBuilder builder = new ProcessBuilder(cmd); - builder = builder.directory(wDir); - builder.start(); + int exitValue = new Cmd().exec(cmd, wDir, timeoutSec); + if (exitCodeOk != exitValue) { + LOGGER.log(Level.SEVERE, "Error during attempt to run: {0} in {1}", new Object[] { cmdLine, wDir }); + throw new FileBrowserRevealException( + "The command to reval the file exited with an error (exitValue=%s).\nCommand: %s\nWorking Dir: %s" + .formatted(Integer.toString(exitValue), cmdLine, wDir)); + } else { + LOGGER.log(Level.FINE, "Successfully executed command: {0} in {1}", new Object[] { cmdLine, wDir }); + } } catch (RuntimeException ex) { + LOGGER.log(Level.SEVERE, "Unknown error during attempt to run: {0} in {1}", new Object[] { cmdLine, wDir }); throw new IOException(ex); + } catch (InterruptedException e) { + LOGGER.log(Level.SEVERE, "Process timeout after {0}s: {1} in {2}", + new Object[] { timeoutSec, cmdLine, wDir }); + Thread.currentThread().interrupt(); + String msg = "The command to reval the file exited with an error after timeout.\nCommand: %s\nWorking Dir: %s\nTimeout (s):%s" + .formatted(cmdLine, wDir, timeoutSec); + String detailMsg = msg + "\n" + e.getMessage(); + throw new IOException(detailMsg); } } - } diff --git a/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/FileBrowserRevealException.java b/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/FileBrowserRevealException.java new file mode 100644 index 000000000..a4a3e78ab --- /dev/null +++ b/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/FileBrowserRevealException.java @@ -0,0 +1,44 @@ +/* + * 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.kit.editor; + +import java.io.IOException; + +public class FileBrowserRevealException extends IOException { + + private static final long serialVersionUID = -1682452255824949867L; + + public FileBrowserRevealException(String message) { + super(message); + } + +} diff --git a/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/panel/css/CssPanelController.java b/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/panel/css/CssPanelController.java index 109a32456..757bc2ffc 100644 --- a/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/panel/css/CssPanelController.java +++ b/kit/src/main/java/com/oracle/javafx/scenebuilder/kit/editor/panel/css/CssPanelController.java @@ -46,6 +46,7 @@ import com.oracle.javafx.scenebuilder.kit.editor.panel.css.NodeCssState.CssProperty; import com.oracle.javafx.scenebuilder.kit.editor.panel.css.SelectionPath.Item; import com.oracle.javafx.scenebuilder.kit.editor.panel.util.AbstractFxmlPanelController; +import com.oracle.javafx.scenebuilder.kit.editor.panel.util.dialog.ErrorDialog; import com.oracle.javafx.scenebuilder.kit.fxom.FXOMDocument; import com.oracle.javafx.scenebuilder.kit.fxom.FXOMObject; import com.oracle.javafx.scenebuilder.kit.editor.panel.css.SelectionPath.Path; @@ -107,6 +108,8 @@ * */ public class CssPanelController extends AbstractFxmlPanelController { + + private static final Logger LOGGER = Logger.getLogger(CssPanelController.class.getName()); @FXML private StackPane cssPanelHost; @@ -1449,7 +1452,14 @@ private void navigate(CssProperty item, PropertyState state, CssStyle style, Sty EditorPlatform.revealInFileBrowser(f); } } catch (URISyntaxException | IOException ex) { - System.out.println(ex.getMessage() + ": " + ex); + LOGGER.log(Level.SEVERE, "An unexpected error occured!", ex); + final ErrorDialog errorDialog = new ErrorDialog(editorController.getOwnerWindow()); + errorDialog.setTitle(I18N.getString("alert.error.file.reveal.title")); + errorDialog.setMessage(I18N.getString("alert.error.file.reveal.message")); + errorDialog.setDetails(I18N.getString("alert.error.file.reveal.details", path)); + errorDialog.setDetailsTitle(I18N.getString("alert.error.file.reveal.details.title")); + errorDialog.setDebugInfoWithThrowable(ex); + errorDialog.showAndWait(); } } } else { diff --git a/kit/src/main/resources/com/oracle/javafx/scenebuilder/kit/i18n/SceneBuilderKit.properties b/kit/src/main/resources/com/oracle/javafx/scenebuilder/kit/i18n/SceneBuilderKit.properties index decb59e0e..55de7bffa 100644 --- a/kit/src/main/resources/com/oracle/javafx/scenebuilder/kit/i18n/SceneBuilderKit.properties +++ b/kit/src/main/resources/com/oracle/javafx/scenebuilder/kit/i18n/SceneBuilderKit.properties @@ -20,7 +20,7 @@ # 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 NOlabel.action.edit.paste EVENT SHALL THE COPYRIGHT +# 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, @@ -430,10 +430,19 @@ error.file.create.title = File creation failed # {0} is a file path error.file.create.message = File {0} cannot be created error.file.open.message = File {0} cannot be opened -error.file.open.title = File opening failed error.write.details = Please check your file system. -error.file.reveal.title = File locating failed -error.file.reveal.message = File {0} cannot be found + +# Alert file open error +alert.error.file.open.title = File opening failed +alert.error.file.open.edit.message = Cannot open file for editing! +alert.error.file.open.edit.details = ''{0}'' +alert.error.file.open.edit.details.title = Details - File opening failed + +# Alert Dialog in case of revealing a file +alert.error.file.reveal.title = Failed to reveal file ... +alert.error.file.reveal.message = Cannot reveal file in file system viewer due to an error. +alert.error.file.reveal.details.title = File Reveal Error Details +alert.error.file.reveal.details = {0} #maven log.user.maven.installed = {0} installed in local repository diff --git a/kit/src/main/resources/com/oracle/javafx/scenebuilder/kit/i18n/SceneBuilderKit_ja.properties b/kit/src/main/resources/com/oracle/javafx/scenebuilder/kit/i18n/SceneBuilderKit_ja.properties index 3b62233f7..e6c5eba6a 100644 --- a/kit/src/main/resources/com/oracle/javafx/scenebuilder/kit/i18n/SceneBuilderKit_ja.properties +++ b/kit/src/main/resources/com/oracle/javafx/scenebuilder/kit/i18n/SceneBuilderKit_ja.properties @@ -406,8 +406,6 @@ error.file.create.message = ファイル{0}を作成できません error.file.open.message = ファイル{0}を開けません error.file.open.title = ファイルを開くのに失敗しました error.write.details = ファイル・システムを確認してください。 -error.file.reveal.title = ファイルを見つけるのに失敗しました -error.file.reveal.message = ファイル{0}が見つかりません #maven log.user.maven.installed = {0}はローカル・レポジトリにインストールされています diff --git a/kit/src/main/resources/com/oracle/javafx/scenebuilder/kit/i18n/SceneBuilderKit_zh_CN.properties b/kit/src/main/resources/com/oracle/javafx/scenebuilder/kit/i18n/SceneBuilderKit_zh_CN.properties index 65e3ae0e2..3e6b52b2a 100644 --- a/kit/src/main/resources/com/oracle/javafx/scenebuilder/kit/i18n/SceneBuilderKit_zh_CN.properties +++ b/kit/src/main/resources/com/oracle/javafx/scenebuilder/kit/i18n/SceneBuilderKit_zh_CN.properties @@ -432,8 +432,6 @@ error.file.create.message = 无法创建文件 {0} error.file.open.message = 无法打开文件 {0} error.file.open.title = 文件打开失败 error.write.details = 请检查您的文件系统。 -error.file.reveal.title = 文件定位失败 -error.file.reveal.message = 未找到 {0} 文件 #maven log.user.maven.installed = {0} 安装在本地存储库中 diff --git a/kit/src/test/java/com/oracle/javafx/scenebuilder/kit/editor/CmdTest.java b/kit/src/test/java/com/oracle/javafx/scenebuilder/kit/editor/CmdTest.java new file mode 100644 index 000000000..32f25cadb --- /dev/null +++ b/kit/src/test/java/com/oracle/javafx/scenebuilder/kit/editor/CmdTest.java @@ -0,0 +1,109 @@ +/* + * 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.kit.editor; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; + +public class CmdTest { + + private Cmd classUnderTest = new Cmd(); + + private final long timeoutSeconds = 2L; + + private Path workingDir = Path.of("./src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/") + .normalize() + .toAbsolutePath(); + + @Test + void that_exception_is_created_with_not_existing_command() { + var cmdLine = List.of("this", "command", "should", "not", "exist"); + + Throwable t = assertThrows(IOException.class, + () -> classUnderTest.exec(cmdLine, workingDir.toFile(), timeoutSeconds)); + + assertTrue(t.getMessage().startsWith("Cannot run program \"this\"")); + } + + @EnabledOnOs(value = {OS.WINDOWS}) + @Test + void that_exit_code_from_program_is_collected_on_Windows() { + var cmdLine = List.of(workingDir.resolve("exit-error.cmd").toString()); + + Integer result = assertDoesNotThrow(() -> classUnderTest.exec(cmdLine, workingDir.toFile(), timeoutSeconds)); + + assertEquals(1, result, "Exit code must be 1"); + } + + @EnabledOnOs(value = {OS.LINUX, OS.MAC}) + @Test + void that_exit_code_is_collected_on_Linux_and_Mac() { + var cmdLine = List.of("/bin/sh", workingDir.resolve("exit-error.sh").toString()); + + Integer result = assertDoesNotThrow(() -> classUnderTest.exec(cmdLine, workingDir.toFile(), timeoutSeconds)); + + assertEquals(1, result, "Exit code must be 1"); + } + + @EnabledOnOs(value = {OS.WINDOWS}) + @Test + void that_process_does_not_run_indefinitevely_on_Windows() { + var cmdLine = List.of(workingDir.resolve("timeout.cmd").toString()); + + Throwable t = assertThrows(IOException.class, + () -> classUnderTest.exec(cmdLine, workingDir.toFile(), timeoutSeconds)); + + assertEquals(t.getMessage(), "Process timed out after 2 seconds!"); + } + + @EnabledOnOs(value = {OS.LINUX, OS.MAC}) + @Test + void that_process_does_not_run_indefinitevely_on_Linux_and_Mac() { + var cmdLine = List.of("/bin/sh", workingDir.resolve("timeout.sh").toString()); + + Throwable t = assertThrows(IOException.class, + () -> classUnderTest.exec(cmdLine, workingDir.toFile(), timeoutSeconds)); + + assertEquals(t.getMessage(), "Process timed out after 2 seconds!"); + } + +} diff --git a/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/exit-error.cmd b/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/exit-error.cmd new file mode 100644 index 000000000..9e24ca57f --- /dev/null +++ b/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/exit-error.cmd @@ -0,0 +1,2 @@ +ECHO Programm Error! +EXIT 1 diff --git a/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/exit-error.sh b/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/exit-error.sh new file mode 100644 index 000000000..23817e8c9 --- /dev/null +++ b/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/exit-error.sh @@ -0,0 +1,2 @@ +echo Program exited with error. +exit 1 diff --git a/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/timeout.cmd b/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/timeout.cmd new file mode 100644 index 000000000..4f03ca5ca --- /dev/null +++ b/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/timeout.cmd @@ -0,0 +1,3 @@ +ECHO Waiting for 5sec +TIMEOUT /T 5 +EXIT 0 diff --git a/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/timeout.sh b/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/timeout.sh new file mode 100644 index 000000000..9b5b3b78e --- /dev/null +++ b/kit/src/test/resources/com/oracle/javafx/scenebuilder/kit/editor/timeout.sh @@ -0,0 +1,3 @@ +echo Waiting for 5sec +sleep 5 +exit 0