Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaFx 8: open a link in a browser without reference to Application

Have got a Hyperlink. When clicked I want a link to be opened in an external browser.

The usual method cited on the web seems to be:

final Hyperlink hyperlink = new Hyperlink("http://www.google.com");
hyperlink.setOnAction(t -> {
    application.getHostServices().showDocument(hyperlink.getText());
});

However I don't have a reference to Application. The link is opened from Dialog, which is opened from a Controller, which is opened via an fxml file, so getting a reference to the Application object would be quite painful.

Does anyone know an easy way of doing this?

Cheers

like image 266
Ben Avatar asked Oct 13 '15 05:10

Ben


People also ask

Can JavaFX run in browser?

Yes, JavaFX applications can be deployed so that they run inside a web browser hosted html web page. The technology which allows this to occur is the Java Plugin. This plugin is currently a NPAPI based browser plugin solution. The Java Plugin is shipped with the Oracle Java 7 Runtime Standard Environment.

How do I display JavaFX in my browser?

The recommended way to embed a JavaFX application into a web page or launch it from inside a web browser is to use the Deployment Toolkit library. The Deployment Toolkit provides a JavaScript API to simplify web deployment of JavaFX applications and improve the end user experience with getting the application to start.

Is JavaFX free to use?

Yes JavaFX is free to download. If you are building an embedded or consumer device and would like to include Java or JavaFX, please contact Oracle for more information on including these technologies in your device.


2 Answers

Solution 1: Pass a reference to the HostServices down through your application.

This is probably similar to the "quite painful" approach you are anticipating. But basically you would do something like:

public void start(Stage primaryStage) throws Exception {

    FXMLLoader loader = new FXMLLoader(getClass().getResource("main.fxml"));
    Parent root = loader.load();
    MainController controller = loader.getController();
    controller.setHostServices(getHostServices());
    primaryStage.setScene(new Scene(root));
    primaryStage.show();

}

and then in MainController:

public class MainController {

    private HostServices hostServices ;

    public HostServices getHostServices() {
        return hostServices ;
    }

    public void setHostServices(HostServices hostServices) {
        this.hostServices = hostServices ;
    }

    @FXML
    private void showDialog() {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("dialog.fxml"));
        Parent dialogRoot = loader.load();
        DialogController dialogController = loader.getController();
        dialogController.setHostServices(hostServices);
        Stage dialog = new Stage();
        dialog.setScene(new Scene(dialogRoot));
        dialog.show();
    }
}

and of course DialogController looks like:

public class DialogController {

    @FXML
    private Hyperlink hyperlink ;

    private HostServices hostServices ;

    public HostServices getHostServices() {
        return hostServices ;
    }

    public void setHostServices(HostServices hostServices) {
        this.hostServices = hostServices ;
    }

    @FXML
    private void openURL() {
        hostServices.openDocument(hyperlink.getText());
    }
}

Solution 2: Use a controller factory to push the host services to the controllers.

This is a cleaner version of the above. Instead of getting the controllers and calling a method to initialize them, you configure the creation of them via a controllerFactory and create controllers by passing a HostServices object to the controller's constructor, if it has a suitable constructor:

public class HostServicesControllerFactory implements Callback<Class<?>,Object> {

    private final HostServices hostServices ;

    public HostServicesControllerFactory(HostServices hostServices) {
        this.hostServices = hostServices ;
    }

    @Override
    public Object call(Class<?> type) {
        try {
            for (Constructor<?> c : type.getConstructors()) {
                if (c.getParameterCount() == 1 && c.getParameterTypes()[0] == HostServices.class) {
                    return c.newInstance(hostServices) ;
                }
            }
            return type.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

Now use the controller factory when you load the FXML:

public void start(Stage primaryStage) throws Exception {
    FXMLLoader loader = new FXMLLoader(getClass().getResource("main.fxml"));
    loader.setControllerFactory(new HostServicesControllerFactory(getHostServices()));
    Parent root = loader.load();
    primaryStage.setScene(new Scene(root));
    primaryStage.show();
}

and define your controllers to take HostServices as a constructor parameter:

public class MainController {

    private final HostServices hostServices ;

    public MainController(HostServices hostServices) {
        this.hostServices = hostServices ;
    }

    @FXML
    private void showDialog() {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("dialog.fxml"));
        loader.setControllerFactory(new HostServicesControllerFactory(hostServices));
        Parent dialogRoot = loader.load();
        Stage dialog = new Stage();
        dialog.setScene(new Scene(dialogRoot));
        dialog.show();
    }    
}

and of course

public class DialogController {

    @FXML
    private Hyperlink hyperlink ;

    private final HostServices hostServices ;

    public DialogController(HostServices hostServices) {
        this.hostServices = hostServices ;
    }

    @FXML
    private void openURL() {
        hostServices.openDocument(hyperlink.getText());
    }
}

Solution 3: This is a miserably ugly solution, and I strongly recommend against using it. I just wanted to include it so I could express that without offending someone else when they posted it. Store the host services in a static field.

public class MainApp extends Application {

    private static HostServices hostServices ;

    public static HostServices getHostServices() {
        return hostServices ;
    }

    public void start(Stage primaryStage) throws Exception {

        hostServices = getHostServices();

        Parent root = FXMLLoader.load(getClass().getResource("main.fxml"));
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }
}

Then you just do

MainApp.getHostServices().showDocument(hyperlink.getText());

anywhere you need. One of the problems here is that you introduce a dependency on your application type for all controllers that need access to the host services.


Solution 4 Define a singleton HostServicesProvider. This is better than solution 3, but still not a good solution imo.

public enum HostServicesProvider {

    INSTANCE ;

    private HostServices hostServices ;
    public void init(HostServices hostServices) {
        if (this.hostServices != null) {
            throw new IllegalStateException("Host services already initialized");
        }
        this.hostServices = hostServices ;
    }
    public HostServices getHostServices() {
        if (hostServices == null) {
            throw new IllegalStateException("Host services not initialized");
        }
        return hostServices ;
    }
}

Now you just need

public void start(Stage primaryStage) throws Exception {
    HostServicesProvider.INSTANCE.init(getHostServices());
    // just load and show main app...
}

and

public class DialogController {

    @FXML
    private Hyperlink hyperlink ;

    @FXML
    private void openURL() {
        HostServicesProvider.INSTANCE.getHostServices().showDocument(hyperlink.getText());
    }
}

Solution 5 Use a dependency injection framework. This probably doesn't apply to your current use case, but might give you an idea how powerful these (relatively simple) frameworks can be.

For example, if you are using afterburner.fx, you just need to do

Injector.setModelOrService(HostServices.class, getHostServices());

in your application start() or init() method, and then

public class DialogPresenter {

    @Inject
    private HostServices hostServices ;

    @FXML
    private Hyperlink hyperlink ;

    @FXML
    private void showURL() {
        hostServices.showDocument(hyperlink.getText());
    }
}

An example using Spring is here.

like image 149
James_D Avatar answered Sep 22 '22 14:09

James_D


Another way would be to use java.awt.Desktop

Try (untested):

URI uri = ...;
if (Desktop.isDesktopSupported()){
    Desktop desktop = Desktop.getDesktop();
    if (desktop.isSupported(Desktop.Action.BROWSE)){
        desktop.browse(uri);
    }
}

Note however that this will introduce a dependency to the AWT stack. This is probably not an issue if you're working with the full JRE, but it might become an issue if you want to work with a tailored JRE (Java SE 9 & Jigsaw) or if you want to run your application on mobile device (javafxports).

There is an open issue to support Desktop in JavaFX in the future.

like image 43
Puce Avatar answered Sep 19 '22 14:09

Puce