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
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.
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With