From e75111696832cd953f7561d1cc3fd3d23cc191fd Mon Sep 17 00:00:00 2001 From: Tobias Nett Date: Sat, 14 Mar 2020 22:05:54 +0100 Subject: [PATCH] fix(footer): Use java.awt.Desktop as fallback for HostServices (#500) This introduces a wrapper around HostServices/Desktop, the `HostServicesWrapper`. This abstraction hides what technology is used to open links in the system browser. Co-authored-by: jdrueckert --- .../launcher/TerasologyLauncher.java | 36 ++++++---- .../gui/javafx/ApplicationController.java | 4 +- .../launcher/gui/javafx/FooterController.java | 25 +++---- .../launcher/util/HostServicesWrapper.java | 70 +++++++++++++++++++ 4 files changed, 104 insertions(+), 31 deletions(-) create mode 100644 src/main/java/org/terasology/launcher/util/HostServicesWrapper.java diff --git a/src/main/java/org/terasology/launcher/TerasologyLauncher.java b/src/main/java/org/terasology/launcher/TerasologyLauncher.java index 1994896b0..9de58a13a 100644 --- a/src/main/java/org/terasology/launcher/TerasologyLauncher.java +++ b/src/main/java/org/terasology/launcher/TerasologyLauncher.java @@ -46,6 +46,7 @@ import org.terasology.launcher.gui.javafx.ApplicationController; import org.terasology.launcher.log.TempLogFilePropertyDefiner; import org.terasology.launcher.util.BundleUtils; +import org.terasology.launcher.util.HostServicesWrapper; import org.terasology.launcher.util.Languages; import org.terasology.launcher.util.LauncherStartFailedException; import org.terasology.launcher.version.TerasologyLauncherVersionInfo; @@ -68,12 +69,32 @@ public final class TerasologyLauncher extends Application { private ProgressBar loadProgress; private Label progressText; private Stage mainStage; - private HostServices hostServices; + private HostServicesWrapper hostServices; public static void main(String[] args) { launch(args); } + /** + * Initialize the host service wrapper by attempting to use the JavaFX {@link HostServices}. + * @return the configured host service wrapper + */ + private HostServicesWrapper initHostServices() { + HostServices hs; + try { + // This may throw an exception on a different thread, but we cannot catch it here o.O + // In addition, `hostServices` will be initialized, but disfunctional. + // Thus, we have no idea whether we can use the services or not... + hs = getHostServices(); + // poor man's check: this will throw a NPE if the internal `hostServices.delegate` is not initialized + hs.getCodeBase(); + } catch (NullPointerException e) { + logger.warn("Host Services not available - won't be able to open hyperlinks in the system browser."); + hs = null; + } + return new HostServicesWrapper(hs); + } + @Override public void init() { ImageView splash = new ImageView(BundleUtils.getFxImage("splash")); @@ -85,18 +106,7 @@ public void init() { progressText.setAlignment(Pos.CENTER); splashLayout.getStylesheets().add(BundleUtils.getStylesheet("css_splash")); splashLayout.setEffect(new DropShadow()); - - try { - // This may throw an exception on a different thread, but we cannot catch it here o.O - // In addition, `hostServices` will be initialized, but disfunctional. - // Thus, we have no idea whether we can use the services or not... - hostServices = getHostServices(); - // poor man's check: this will throw a NPE if the internal `hostServices.deleagte` is not initialized - hostServices.getCodeBase(); - } catch (NullPointerException e) { - logger.warn("Host Services not available - won't be able to open hyperlinks in the system browser."); - hostServices = null; - } + hostServices = initHostServices(); } @Override diff --git a/src/main/java/org/terasology/launcher/gui/javafx/ApplicationController.java b/src/main/java/org/terasology/launcher/gui/javafx/ApplicationController.java index 59190c8cc..e2246705e 100644 --- a/src/main/java/org/terasology/launcher/gui/javafx/ApplicationController.java +++ b/src/main/java/org/terasology/launcher/gui/javafx/ApplicationController.java @@ -17,7 +17,6 @@ package org.terasology.launcher.gui.javafx; import javafx.animation.Transition; -import javafx.application.HostServices; import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.Property; @@ -62,6 +61,7 @@ import org.terasology.launcher.util.BundleUtils; import org.terasology.launcher.util.DownloadException; import org.terasology.launcher.util.GuiUtils; +import org.terasology.launcher.util.HostServicesWrapper; import org.terasology.launcher.util.Languages; import org.terasology.launcher.util.ProgressListener; @@ -376,7 +376,7 @@ protected void deleteAction() { } public void update(final Path newLauncherDirectory, final Path newDownloadDirectory, final Path newTempDirectory, final BaseLauncherSettings newLauncherSettings, - final PackageManager newPackageManager, final Stage newStage, final HostServices hostServices) { + final PackageManager newPackageManager, final Stage newStage, final HostServicesWrapper hostServices) { this.launcherDirectory = newLauncherDirectory; this.downloadDirectory = newDownloadDirectory; this.tempDirectory = newTempDirectory; diff --git a/src/main/java/org/terasology/launcher/gui/javafx/FooterController.java b/src/main/java/org/terasology/launcher/gui/javafx/FooterController.java index 8401a042e..6bcdb6fe2 100644 --- a/src/main/java/org/terasology/launcher/gui/javafx/FooterController.java +++ b/src/main/java/org/terasology/launcher/gui/javafx/FooterController.java @@ -17,7 +17,6 @@ package org.terasology.launcher.gui.javafx; import javafx.animation.Transition; -import javafx.application.HostServices; import javafx.beans.property.Property; import javafx.beans.property.ReadOnlyProperty; import javafx.beans.property.SimpleObjectProperty; @@ -30,9 +29,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.terasology.launcher.util.BundleUtils; +import org.terasology.launcher.util.HostServicesWrapper; import org.terasology.launcher.version.TerasologyLauncherVersionInfo; -import java.net.URI; import java.util.Optional; public class FooterController { @@ -43,7 +42,7 @@ public class FooterController { private Button warningButton; @FXML private Label versionInfo; - private HostServices hostServices; + private HostServicesWrapper hostServices; private Property> warningProperty; public FooterController() { @@ -59,7 +58,7 @@ private void updateLabels() { } } - public void setHostServices(HostServices hostServices) { + public void setHostServices(HostServicesWrapper hostServices) { this.hostServices = hostServices; } @@ -99,32 +98,32 @@ protected void handleSocialButtonMouseReleased(MouseEvent event) { @FXML protected void openFacebook() { - openUri(BundleUtils.getURI("terasology_facebook")); + hostServices.tryOpenUri(BundleUtils.getURI("terasology_facebook")); } @FXML protected void openGithub() { - openUri(BundleUtils.getURI("terasology_github")); + hostServices.tryOpenUri(BundleUtils.getURI("terasology_github")); } @FXML protected void openDiscord() { - openUri(BundleUtils.getURI("terasology_discord")); + hostServices.tryOpenUri(BundleUtils.getURI("terasology_discord")); } @FXML protected void openReddit() { - openUri(BundleUtils.getURI("terasology_reddit")); + hostServices.tryOpenUri(BundleUtils.getURI("terasology_reddit")); } @FXML protected void openTwitter() { - openUri(BundleUtils.getURI("terasology_twitter")); + hostServices.tryOpenUri(BundleUtils.getURI("terasology_twitter")); } @FXML protected void openYoutube() { - openUri(BundleUtils.getURI("terasology_youtube")); + hostServices.tryOpenUri(BundleUtils.getURI("terasology_youtube")); } @FXML @@ -133,12 +132,6 @@ protected void openLogs() { //contentTabPane.getSelectionModel().select(2); } - private void openUri(URI uri) { - if (uri != null && hostServices != null) { - hostServices.showDocument(uri.toString()); - } - } - private void updateWarningButton(Optional warning) { warningButton.setVisible(warning.isPresent()); warning.ifPresent(w -> { diff --git a/src/main/java/org/terasology/launcher/util/HostServicesWrapper.java b/src/main/java/org/terasology/launcher/util/HostServicesWrapper.java new file mode 100644 index 000000000..237cc8fa4 --- /dev/null +++ b/src/main/java/org/terasology/launcher/util/HostServicesWrapper.java @@ -0,0 +1,70 @@ +package org.terasology.launcher.util; + +import javafx.application.HostServices; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Desktop; +import java.io.IOException; +import java.net.URI; + +/** + * Provide capabilities to interact with the hosting system (OS). + *

+ * For instance, this allows to open a URL in the default browser on the system. + *

+ * This abstraction is necessary as {@link javafx.application.HostServices} may not be available and we want to use + * {@link java.awt.Desktop} as a fallback solution. + */ +public class HostServicesWrapper { + + private static final Logger logger = LoggerFactory.getLogger(HostServicesWrapper.class); + + private final Desktop desktop; + private final HostServices hostServices; + + /** + * Create a new {@link javafx.application.HostServices} wrapper. + *

+ * If the given host service instance is {@code null} + * + * @param hostServices a JavaFX host services instance or null + */ + public HostServicesWrapper(HostServices hostServices) { + this.hostServices = hostServices; + + if (hostServices == null && Desktop.isDesktopSupported()) { + Desktop d = Desktop.getDesktop(); + if (d.isSupported(Desktop.Action.BROWSE)) { + desktop = d; + } else { + desktop = null; + } + } else { + desktop = null; + } + } + + /** + * Attempt to open the given URI with the default browser. + *

+ * The preferred way to open the URI is via the {@link javafx.application.HostServices}, as a fallback + * {@link java.awt.Desktop} may be used internally. + *

+ * In case the URI cannot be opened (e.g., because neither HostServices nor Desktop are available) this method will + * fail silently. + * + * @param uri the URI to open + */ + public void tryOpenUri(URI uri) { + if (hostServices != null) { + hostServices.showDocument(uri.toString()); + } else if (desktop != null) { + try { + desktop.browse(uri); + } catch (IOException e) { + logger.warn("Unable to open URI with 'Browse' action", e); + } + } + } +}