14

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

Ben
  • 6,567
  • 10
  • 42
  • 64

4 Answers4

27

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.

James_D
  • 201,275
  • 16
  • 291
  • 322
  • the ugly static solution won't work if you do not name the getHostServices differently e.g. getStaticHostServices. It is the least effort solution in my use case. – Wolfgang Fahl Jul 18 '17 at 14:40
  • @WolfgangFahl Yeah, probably. Not sure why you would even try something I so strongly recommend against using anyway. – James_D Jul 18 '17 at 14:48
  • 1
    no worries I am using an interface now public interface Linker { public void browse(String url); } which hides the implementation - the static stuff is not needed that way – Wolfgang Fahl Jul 18 '17 at 18:35
7

If you want to open a url when you click on a button inside your app and you are using an fxml controller file then you can do the following...

First in your Main Application Startup file get a pointer to the HostServices object and add it to your stage such as...

stage.getProperties().put("hostServices", this.getHostServices());

Then in your fxml controller file get the hostServices object from the stage object and then execute the showDocument() method.

HostServices hostServices = (HostServices)this.getStage().getProperties().get("hostServices");
hostServices.showDocument("http://stackoverflow.com/");

I have a method in my contoller class called getStage()...

/**
 * @return the stage from my mainAnchor1 node.
 */
public Stage getStage() {
    if(this.stage==null)
        this.stage = (Stage) this.mainAnchor1.getScene().getWindow();
    return stage;
}
Peter
  • 81
  • 4
4

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.

Puce
  • 37,247
  • 13
  • 80
  • 152
1

While pursuing a more sustainable solution, consider either of these interim approaches:

  1. Let the class extend Application, giving it access to an instance of HostServices. It won't be the parent application's instance, but it may suffice.

     class MyController extends Application … {
         HostServices hostServices = this.getHostServices();
     }
    
  2. Let the class contain an Application from which it can obtain an instance of HostServices. Again, it won't be the parent application's instance.

     class MyController extends Application … {
         HostServices hostServices = new ServiceApp().getHostServices();
    
         private static class ServiceApp extends Application {
             @Override
             public void start(Stage stage) throws Exception {}
         }
     }
    

In this complete example, an early version of a controller simply extended Application. A later version of the same controller implements a suitable interface to effect the first solution outlined here. A more recent version uses a controller factory to provide the reference as shown in the second solution seen here.

Why bother? Conceptually, HostServices should be unique to each Application instance. This implies the singleton pattern, suggested here and in the fourth solution outlined here. Indeed, HostServices is final, and the source delegates to a private class seen here, which references the application.

trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • 1
    I think having a controller class or inner class extend an Application class is not a good idea. You don’t get good separation of concerns with such an approach and JavaFX Applications follow very strict lifecycle rules. – jewelsea Dec 25 '22 at 05:45