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
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.
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;
}
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.