I'm trying to create a JavaFX application that can work with plugins, those plugins are other jars loaded on runtime and openened to look for the implementation of an specific interface I created, I'm able to load the jar and to find the specific class but some styles that the loaded jar can't be loaded, let me explain what I did:
I created three maven projects, these projects are the following:
Core: Has an interface the the plugin should implement(TestPlugin.java), and a interface that the main program should implement(TestSceneHandler.java)
TestPlugin.java
public interface TestPlugin {
void init(TestSceneHandler sceneHandler);
}
TestSceneHandler.java
import javafx.scene.Parent;
public interface TestSceneHandler {
void setView(Parent node);
}
Plugin: Has the Core as a dependency and a class that implements TestPlugin.java, I left the Main.java so it can work on both modes, sigle app and plugin but it's not really necsessary
MyTestViewController.java
import javafx.fxml.FXMLLoader;
import javafx.scene.layout.GridPane;
import java.io.IOException;
public class MyTestViewController extends GridPane {
public MyTestViewController() {
FXMLLoader fxmlLoader = new FXMLLoader(this.getClass().getResource("/pluginView.fxml"));
fxmlLoader.setClassLoader(getClass().getClassLoader());
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}
}
TestPluginImpl.java
package sample;
public class TestPluginImpl implements TestPlugin {
#Override
public void init(TestSceneHandler testSceneHandler) {
testSceneHandler.setView(new MyTestViewController());
}
}
Main.java
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception{
primaryStage.setTitle("Hello World");
primaryStage.setScene(new Scene(new MyTestViewController(), 300, 275));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
pluginView.fxml
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.control.Label?>
<fx:root xmlns:fx="http://javafx.com/fxml/1"
type="javafx.scene.layout.GridPane"
alignment="center" hgap="10" vgap="10" stylesheets="style.css">
<Label>
Hello world
</Label>
</fx:root>
style.css
.root {
-fx-background-color: red;
}
App: Has the Core as a dependency and a class that implements TestSceneHandler.java
Main.java
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception{
primaryStage.setTitle("Hello World");
primaryStage.setScene(new Scene(new TestScene(), 300, 275));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
sample.fxml
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.Label?>
<fx:root xmlns:fx="http://javafx.com/fxml/1"
type="javafx.scene.layout.BorderPane">
<top>
<HBox style="-fx-background-color: orange;">
<children>
<Label>
This is the header
</Label>
</children>
</HBox>
</top>
</fx:root>
TestScene.java
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.layout.BorderPane;
import java.io.*;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipFile;
public class TestScene extends BorderPane implements TestSceneHandler {
private static final String ROOT_FOLDER = "Zamba";
private static final String PLUGIN_FOLDER = "plugins";
private static final String USER_HOME = System.getProperty("user.home");
public TestScene() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/sample.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
File pluginFolder = initFolder();
readPlugins(pluginFolder);
}
private File initFolder() {
final String ROOT_FOLDER_PATH = USER_HOME + "/" + ROOT_FOLDER;
final String PLUGIN_FOLDER_PATH = ROOT_FOLDER_PATH + "/" + PLUGIN_FOLDER;
File appFolder = new File(ROOT_FOLDER_PATH);
if(!appFolder.exists()) {
appFolder.mkdir();
}
File pluginFolder = new File(PLUGIN_FOLDER_PATH);
if(!pluginFolder.exists()) {
pluginFolder.mkdir();
}
return pluginFolder;
}
/**
* Determine whether a file is a JAR File.
*/
public static boolean isJarFile(File file) throws IOException {
if (!isZipFile(file)) {
return false;
}
ZipFile zip = new ZipFile(file);
boolean manifest = zip.getEntry("META-INF/MANIFEST.MF") != null;
zip.close();
return manifest;
}
/**
* Determine whether a file is a ZIP File.
*/
public static boolean isZipFile(File file) throws IOException {
if(file.isDirectory()) {
return false;
}
if(!file.canRead()) {
throw new IOException("Cannot read file "+file.getAbsolutePath());
}
if(file.length() < 4) {
return false;
}
DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(file)));
int test = in.readInt();
in.close();
return test == 0x504b0304;
}
private void readPlugins(File pluginFolder) {
File[] pluginFolderFiles = pluginFolder.listFiles();
Arrays.asList(pluginFolderFiles).forEach(file -> {
System.out.println("Filee: " + file.getAbsolutePath());
try {
if(isJarFile(file)) {
JarFile jarFile = new JarFile(file);
Enumeration<JarEntry> e = jarFile.entries();
URL[] urls = { new URL("jar:file:" + file.getAbsolutePath()+"!/") };
URLClassLoader cl = URLClassLoader.newInstance(urls);
while (e.hasMoreElements()) {
JarEntry je = e.nextElement();
if(je.isDirectory() || !je.getName().endsWith(".class")){
continue;
}
// -6 because of .class
String className = je.getName().substring(0,je.getName().length()-6);
className = className.replace('/', '.');
Class c = cl.loadClass(className);
if(TestPlugin.class.isAssignableFrom(c) && c != TestPlugin.class) {
System.out.println("Plugin found!!!");
TestPlugin plugin = (TestPlugin)c.newInstance();
plugin.init(this);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
#Override
public void setView(Parent parent) {
setCenter(parent);
}
}
When executing the project Plugin as an standalone app this is the result:
But when it is executed through the App project the result is the following:
As you can see the styles are gone and I have an error on the console:
Plugin found!!!
dic 30, 2018 5:41:30 PM com.sun.javafx.css.StyleManager loadStylesheetUnPrivileged
WARNING: Resource "style.css" not found.
The problem is caused by a classpath issue since you are creating your own ClassLoader to load your plugins. The value of the stylesheet attribute in your FXML is style.css. This is the same as doing:
GridPane pane = new GridPane();
pane.getStylesheets().add("style.css");
Which will look for a resource named style.css relative to the root of the classpath. This is because there is no scheme; see the documentation for details. The problem is this behavior uses the ClassLoader for the JavaFX classes to load the resource. Unfortunately, your resource is not visible to that ClassLoader but rather the ClassLoader you created to load the plugin.
The fix for this is to provide the full URL for the CSS resource file. This is done by using # (see Introduction to FXML). When using # the location is relative to the FXML file. For example, if your FXML file is /fxml/PluginView.fxml and your CSS file is /styles/style.css, you'd have:
<?import javafx.scene.control.Label?>
<fx:root xmlns:fx="http://javafx.com/fxml/1"
type="javafx.scene.layout.GridPane"
alignment="center" hgap="10" vgap="10" stylesheets="#../styles/style.css">
<Label>
Hello world
</Label>
</fx:root>
This will call getStylesheets().add(...) with something like:
jar:file:///path/to/plugin/file.jar!/styles/style.css
Which can be located regardless of the ClassLoader used.
Another likely issue is in your style.css file. You use .root {} but the root style-class is only automatically added to roots of a Scene. What you probably want to do is set your own style-class and use that in the CSS file.
Also, you are writing your own plugin-discovery-and-load code. Not saying you can't keep doing what you're doing but I just want to let you know you don't have to reinvent the wheel. Java has java.util.ServiceLoader for exactly this sort of thing.
Related
I am working on coding a modular soundboard in Java and JavaFX using FXML. One of key aspects is the ability to load an arbitrary class that implements the Plugin abstract class at runtime.
The plugin abstract class looks like this:
import javafx.scene.control.Tab;
import java.io.File;
import java.io.FileReader;
import java.net.URLClassLoader;
import java.util.Properties;
public abstract class Plugin {
protected Tab mainTab;
protected Tab settingsTab;
protected Properties propertyFile = new Properties();
protected File fileName;
protected URLClassLoader loader;
public Plugin(){
mainTab = new Tab();
}
public Plugin(String tabName){
mainTab = new Tab(tabName);
settingsTab = new Tab(tabName);
}
public abstract void save();
public Tab getMainTab(){
return mainTab;
}
public Tab getSettingsTab(){
return settingsTab;
}
public void initPropertyFile(String filename){
try{
File file = new File(System.getenv("APPDATA")+"\\Testing\\"+filename+".properties");
if(file.exists())
propertyFile.load(new FileReader(file));
else{
file.createNewFile();
propertyFile.load(new FileReader(file));
}
}catch(Exception e){
e.printStackTrace();
}
}
public void setClassLoader(URLClassLoader loader){
this.loader = loader;
}
public abstract void initController();
public Properties getPropertyFile(){
return propertyFile;
}
}
I use a URLClassLoader to load any classes found in a specified JAR file, and then I instantiate any instances of Plugin and collect them inside a list. Where I am having an issue is creating an FXML controller to use for these classes that I instantiate at runtime. Because both these classes and the controllers are loaded at runtime using a custom URLClassLoader I am unable to just specify a FXML controller inside my FXML files. If I try to, I get an error saying that the class I am using for a controller could not be found. If I don't specify a controller inside the FXML file, it is able to load the UI components, but I can't specify any actions inside it. I can set a controller using the FXMLLoader.setController() method, but when I load a controller like this though, none of the buttons I am using have any actions. I've tried using the initialize() method with FXML annotations, but this method isn't automatically called by the FXMLLoader. If I call initialize myself then it seems like the FXML hasn't been injected yet, but the UI components specified by the FXML file appear with the formatting specified by the FXML file on my screen.
import java.net.URL;
public class Plugin3 extends Plugin {
FXMLLoader mainTabLoader;
URL testURL;
public Plugin3(){
//Calls Plugin Constructor to initialize mainTab and settingsTab, and set the UI names of the tabs
super("Plugin3");
//Try creating FXMLLoader to setup the UI of the plugin
try {
mainTabLoader = new FXMLLoader();
//mainTabLoader.setController(this);
Plugin3 thisObj = mainTabLoader.getController();
testURL = getClass().getResource("testFXML.fxml");
//Load the FXML file
} catch (Exception e) {
e.printStackTrace();
}
}
#Override
public void save() {
}
//Attempts to set the controller of the FXMLLoader mainTabLoader and call the initialize method of that controller
#Override
public void initController() {
try {
String classPath = "com.example.plugindevelopment.Plugin3Controller";
mainTabLoader.setController(Class.forName(classPath, true, loader).getConstructor().newInstance());
getMainTab().setContent(mainTabLoader.load(testURL));
mainTabLoader.getController().getClass().getDeclaredMethod("initialize").invoke(mainTabLoader.getController());
}catch(Exception e){
e.printStackTrace();
}
}
}
This is the controller:
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.layout.VBox;
public class Plugin3Controller
{
#FXML
VBox root;
#FXML
Label label;
#FXML
Slider slider;
#FXML
Button button1;
protected void onButtonClick(){
System.out.println("Hello");
}
#FXML
public void initialize(){
System.out.println("Initializing Plugin3Controller");
button1 = new Button("Hello");
button1.setOnAction(event -> onButtonClick());
}
}
and this is the FXML file:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1">
<children>
<VBox fx:id="root" prefHeight="200.0" prefWidth="100.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<Label fx:id="label" prefHeight="137.2" text="text" />
<Slider fx:id="slider">
<VBox.margin>
<Insets top="100.0" />
</VBox.margin>
<padding>
<Insets top="50.0" />
</padding></Slider>
<Button fx:id="button1" mnemonicParsing="false" text="Click Me" />
</children></VBox>
</children>
</AnchorPane>
I would appreciate any insight anyone can provide, thank you.
Because both these classes and the controllers are loaded at runtime using a custom URLClassLoader I am unable to just specify a FXML controller inside my FXML files. If I try to, I get an error saying that the class I am using for a controller could not be found.
Note that you can set the Class Loader to be used by the FXMLLoader.
The solution you attempt in which you set the controller manually has a number of errors. Most importantly:
String classPath = "com.example.plugindevelopment.Plugin3Controller";
mainTabLoader.setController(Class.forName(classPath, true, loader).getConstructor().newInstance());
getMainTab().setContent(mainTabLoader.load(testURL));
Here mainTabLoader.load(testURL) invokes the static method FXMLLoader.load(URL). Since this is a static method, it doesn't reference the FXMLLoader instance mainTabLoader at all, and in particular won't be aware of the controller instance.
You should use
String classPath = "com.example.plugindevelopment.Plugin3Controller";
mainTabLoader.setController(Class.forName(classPath, true, loader).getConstructor().newInstance());
mainTabLoader.setLocation(testURL);
getMainTab().setContent(mainTabLoader.load());
Note the call to load() has no arguments, invoking the instance method FXMLLoader.load().
Once the FXMLLoader is properly referencing a controller instance, it will invoke initialize() automatically. You should remove the line
mainTabLoader.getController().getClass().getDeclaredMethod("initialize").invoke(mainTabLoader.getController());
You also have an error in your controller implementation (I'm guessing this may have been a desperate attempt to fix the problems caused by the error above).
It is always a mistake to instantiate objects and assign them to #FXML-annotated fields, as you do with
button1 = new Button("Hello");
Remove this line. It causes the event handler to be set on a button that is not displayed in the UI, instead of on the button that is declared in the FXML file.
In all you should have
public class Plugin3Controller
{
#FXML
VBox root;
#FXML
Label label;
#FXML
Slider slider;
#FXML
Button button1;
protected void onButtonClick(){
System.out.println("Hello");
}
#FXML
public void initialize(){
System.out.println("Initializing Plugin3Controller");
button1.setOnAction(event -> onButtonClick());
}
}
and
public class Plugin3 extends Plugin {
FXMLLoader mainTabLoader;
URL testURL;
public Plugin3(){
//Calls Plugin Constructor to initialize mainTab and settingsTab, and set the UI names of the tabs
super("Plugin3");
//Try creating FXMLLoader to setup the UI of the plugin
mainTabLoader = new FXMLLoader();
//mainTabLoader.setController(this);
// This line is patently nonsense: remove it:
// Plugin3 thisObj = mainTabLoader.getController();
testURL = getClass().getResource("testFXML.fxml");
//Load the FXML file
}
#Override
public void save() {
}
//Attempts to set the controller of the FXMLLoader mainTabLoader and call the initialize method of that controller
#Override
public void initController() {
try {
String classPath = "com.example.plugindevelopment.Plugin3Controller";
mainTabLoader.setController(Class.forName(classPath, true, loader).getConstructor().newInstance());
mailTabLoader.setLocation(testURL);
getMainTab().setContent(mainTabLoader.load());
}catch(Exception e){
e.printStackTrace();
}
}
}
This question already has answers here:
How to reference javafx fxml files in resource folder?
(3 answers)
How do I determine the correct path for FXML files, CSS files, Images, and other resources needed by my JavaFX Application?
(1 answer)
Javafx - Can application class be the controller class
(2 answers)
Closed 1 year ago.
Let's say I have three files:
Controller.java
Creator.java
Scene.fxml
Scene.fxml's controller is set to Controller.java. Controller.java calls a method from Creator.java which creates a new scene using FXMLLoader.load( ) method, then passes this scene to newly created stage and returns this stage. Controller.java calls .show() method on that returned stage. Everything is great so far, the window did pop up, but the issue is that I can not access any node from Scene.fxml in Controller.java. I naturally have "fx:id" stuff in Scene.fxml and I've also created all those nodes in Controller.java with the exact name and #FXML annotation. Anyway, they are all set to NULL.
I assume that the issue might be connected with FXMLLoader.load() method being called from s class that is not the class controller. So the question is: am I right? i
If I am, is there any way to actually make it work the way I want?
If the explanation is not good enough I will create a minimal reproducible example.
EDIT:
Controller.java:
package Issue;
import javafx.application.Application;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.stage.Stage;
public class Controller extends Application {
#FXML
private Label label;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
stage = Creator.createStage(stage);
stage.show();
System.out.println("Is label null?");
if(label == null){
System.out.println("yes");
} else {
System.out.println("no");
}
}
}
Creator.java:
package Issue;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
public class Creator {
public static Stage createStage(Stage stage) {
Parent root = null;
String pathToProject = System.getProperty("user.dir");
Path path = Paths.get(pathToProject, "src", "main", "java", "Issue", "Scene.fxml");
try{
URL url = new URL("file", "", path.toString());
root = FXMLLoader.load(url);
} catch (IOException | NullPointerException e) {
System.out.println("wrong url to fxml");
}
stage.setScene(new Scene(root, 400, 400));
return stage;
}
}
Scene.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<BorderPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="Issue.Controller"
prefHeight="400.0" prefWidth="600.0">
<center>
<Label fx:id="label" text="hey stackoverflow"/>
</center>
</BorderPane>
module-info.java:
module Main {
requires javafx.controls;
requires javafx.fxml;
opens Issue to javafx.fxml;
exports Issue;
}
I've created standard Maven projects and created "Issue" package with those files in src/main/java path. module-info.java is in src/main/java.
Normally I handle my exceptions by showing some custom Alert (JavaFX) with details, but JavaFX runtime is not initialized at all when the static initializer of my class runs.
Is there any way to handle such exception without printing its content to output like an animal?
public class MyStaticInitializedClass {
static {
try {
//do the things that may throw exception
} catch(Exception ex) {
ExceptionHandler.showException(ex);
}
}
}
public class ExceptionHandler {
public static void showException(Exception ex) {
//constructs JavaFX alert with exception details
alert.show();
}
}
First consder if you shouldn't let the application simply crash and log the reason. A failure in a static initializer typically means there's something seriously wrong with the environment, which is not likely something you can recover from. Also, as far as I know, once a class fails to load it can't ever be loaded by the same ClassLoader again.
That said, if you want to show errors to your user in an alert, even if the error occurs before the JavaFX runtime has been initialized, then you need to save the error somewhere. Then, once you launch JavaFX, check wherever you stored the error(s) and show them. For example:
Main.java:
import javafx.application.Application;
public class Main {
public static void main(String[] args) {
// an "error" before JavaFX is launched
App.notifyUserOfError(new RuntimeException("OOPS!"));
Application.launch(App.class, args);
}
}
App.java:
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayDeque;
import java.util.Objects;
import java.util.Queue;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
public class App extends Application {
private static Queue<Throwable> errorQueue;
private static App appInstance;
public static synchronized void notifyUserOfError(Throwable throwable) {
Objects.requireNonNull(throwable);
if (appInstance == null) {
if (errorQueue == null) {
errorQueue = new ArrayDeque<>();
}
errorQueue.add(throwable);
} else {
if (Platform.isFxApplicationThread()) {
appInstance.showErrorAlert(throwable);
} else {
Platform.runLater(() -> appInstance.showErrorAlert(throwable));
}
}
}
private static synchronized Queue<Throwable> setAppInstance(App instance) {
if (appInstance != null) {
throw new IllegalStateException();
}
appInstance = instance;
var queue = errorQueue;
errorQueue = null; // no longer needed
return queue;
}
private Stage primaryStage;
#Override
public void start(Stage primaryStage) {
this.primaryStage = primaryStage;
var scene = new Scene(new StackPane(new Label("Hello, World!")), 600, 400);
primaryStage.setScene(scene);
primaryStage.show();
var errors = setAppInstance(this);
if (errors != null) {
// if non-null then should be non-empty
do {
showErrorAlert(errors.remove());
} while (!errors.isEmpty());
// possibly exit the application if you can't recover
}
}
private void showErrorAlert(Throwable error) {
var alert = new Alert(AlertType.ERROR);
alert.initOwner(primaryStage);
alert.setContentText(error.toString());
var sw = new StringWriter();
error.printStackTrace(new PrintWriter(sw));
var area = new TextArea(sw.toString());
area.setEditable(false);
area.setFont(Font.font("Monospaced", 12));
var details = new VBox(5, new Label("Stack trace:"), area);
VBox.setVgrow(area, Priority.ALWAYS);
alert.getDialogPane().setExpandableContent(details);
alert.showAndWait();
}
}
The above puts the error in a queue if JavaFX has not been initialized yet. At the end of the start method the queue is checked for any errors and they're displayed to the user one after the other. If JavaFX has already been initialized then the error is immediately shown to the user.
I am working with the Javafx GUI but i also require the same level of functionality from the command line. I am wondering what the best way to make a main class which has functionality for both command line and Javafx at the same time so you can do one thing on the GUI and then do the next thing on command line. Command line would also update the GUI display.
(Really, this question is off-topic, as it is too broad. It was interesting enough, though, for me to try a proof of concept of the approach that seemed natural to me, so I answered it anyway.)
You essentially need two things here:
Use an MVC approach, with the model containing the data. You can share the same model instance with both the command line interface and the UI, so both update the same data. The UI, as usual, will observe the model and update if the data change.
Launch the CLI from the JavaFX application's start() method, running it in a background thread so that it doesn't block the UI. You just need to make sure there that the model makes updates on the correct (i.e. the FX Application) thread.
Here's a simple example, which just computes the total of a list of integers. Here's the model, which stores the list and the total. It has methods to add a new value, or clear the list. Note how those methods execute their changes on the UI thread:
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class AddingModel {
private final ObservableList<Integer> values = FXCollections.observableArrayList();
private final ReadOnlyIntegerWrapper total = new ReadOnlyIntegerWrapper();
public AddingModel() {
total.bind(Bindings.createIntegerBinding(() ->
values.stream().collect(Collectors.summingInt(Integer::intValue)),
values));
}
private void ensureFXThread(Runnable action) {
if (Platform.isFxApplicationThread()) {
action.run();
} else {
Platform.runLater(action);
}
}
public void clear() {
ensureFXThread(values::clear);
}
public void addValue(int value) {
ensureFXThread(() -> values.add(value));
}
public final ReadOnlyIntegerProperty totalProperty() {
return this.total.getReadOnlyProperty();
}
public final int getTotal() {
return this.totalProperty().get();
}
public ObservableList<Integer> getValues() {
return values ;
}
}
Here's the UI code. First a view, in FXML:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="AddingController">
<top>
<HBox spacing="5">
<TextField fx:id="valueField" onAction="#addValue"/>
<Button text="Clear" onAction="#clearValues"/>
</HBox>
</top>
<center>
<ListView fx:id="values"/>
</center>
<bottom>
<Label fx:id="sum"/>
</bottom>
</BorderPane>
and a controller, which observes and updates the model:
import java.util.function.UnaryOperator;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
public class AddingController {
private final AddingModel model ;
#FXML
private TextField valueField ;
#FXML
private ListView<Integer> values ;
#FXML
private Label sum ;
public AddingController(AddingModel model) {
this.model = model ;
}
#FXML
private void initialize() {
values.setItems(model.getValues());
sum.textProperty().bind(model.totalProperty().asString("Total = %,d"));
// Allow only integer values in the text field:
UnaryOperator<TextFormatter.Change> filter = c -> {
if (c.getControlNewText().matches("-?[0-9]*")) {
return c;
} else {
return null ;
}
};
valueField.setTextFormatter(new TextFormatter<>(filter));
}
#FXML
private void addValue() {
String text = valueField.getText();
if (! text.isEmpty()) {
int value = Integer.parseInt(text);
model.addValue(value);
valueField.clear();
}
}
#FXML
private void clearValues() {
model.clear();
}
}
Now a simple command line interpreter, which reads from the command line and references the model. It supports either integer entry (add value to the model), or the commands total, show, or clear:
import java.util.List;
import java.util.Scanner;
import java.util.regex.Pattern;
public class AddingCLI {
private final AddingModel model ;
private final Pattern intPattern = Pattern.compile("-?[0-9]+");
public AddingCLI(AddingModel model) {
this.model = model ;
}
public void processCommandLine() {
try (Scanner in = new Scanner(System.in)) {
while (true) {
String input = in.next().trim().toLowerCase();
if (intPattern.matcher(input).matches()) {
int value = Integer.parseInt(input);
model.addValue(value);
} else if ("show".equals(input)) {
outputValues();
} else if ("clear".equals(input)) {
model.clear();
System.out.println("Values cleared");
} else if ("total".equals(input)) {
System.out.println("Total = "+model.getTotal());
}
}
}
}
private void outputValues() {
List<Integer> values = model.getValues();
if (values.isEmpty()) {
System.out.println("No values");
} else {
values.forEach(System.out::println);
}
}
}
Finally, the JavaFX application which assembles all these. Note that the same model instance is passed to both the CLI and the UI controller, so both are updating the same data. You can enter some values in the text field, then type "show" in the command line, and you'll see the values. Type "clear" in the command line, and the values will be removed from the UI, etc.
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class AddingApp extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
AddingModel model = new AddingModel();
AddingController controller = new AddingController(model);
FXMLLoader loader = new FXMLLoader(AddingController.class.getResource("ValueTotaler.fxml"));
loader.setControllerFactory(type -> {
if (type == AddingController.class) {
return controller ;
} else {
throw new IllegalArgumentException("Unexpected controller type: "+type);
}
});
Parent root = loader.load();
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
AddingCLI cli = new AddingCLI(model);
Thread cliThread = new Thread(cli::processCommandLine);
cliThread.setDaemon(true);
cliThread.start();
}
public static void main(String[] args) {
launch(args);
}
}
You can, of course, just create the UI without the CLI, or create the CLI without the UI; both are independent of each other (they just both depend on the model).
I think this is borderline to be too broad. One part of that is: your requirements are unclear. Do you intend to you use the command line like:
java -jar whatever.jar -command A
java -jar whatever.jar -command B
java -jar whatever.jar -command C
So - you invoke java repeatedly, and whatever.jar basically is a client that takes to some "server" that does the real work, or do you envision
java -jar whatever.jar
> Type your command:
> A
... ran command A
> Type your command:
Obviously, that makes a huge difference here.
But in the end, it also tells us where a solution is: by de-coupling these clients from the actual execution.
Meaning: you should do two things
define the functionality aka services that some server has to provide
then you can look into ways of creating different clients that make use of these services
Avoid baking all of these different aspects into one single main() method!
Everything on the GUI is Event based. This means that methods get called when you press a button or interact with a JavaFX Window in another way like selecting an item on a list.
I suggest keeping your internal logic and GUI logic seperated.
When clicking on a button you call a handleButton(ActionEvent actionEvent) method that is linked to the button. This method should call a method in one of your other classes that actually contains the logic.
You can get user input through the command line with a scanner:
public String getUserInput() {
Scanner scan = new Scanner(System.in);
String s = scan.next();
return s
}
You can now check this user input string and connect the corresponding method with a switch(s) statement.
I'm not sure WHEN you want to get this input through the command line, but I suggest adding a developer button in your Stage that calls getUserInput() when pressed.
I found a way to open a link on default browser using HostServices.
getHostServices().showDocument("http://www.google.com");
Is there any way to open a media in default media player?
Is there any way to launch a specific File or Application?
Generally speaking, you can use Desktop#open(file) to open a file natively as next:
final Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null;
if (desktop != null && desktop.isSupported(Desktop.Action.OPEN)) {
desktop.open(file);
} else {
throw new UnsupportedOperationException("Open action not supported");
}
Launches the associated application to open the file. If the specified
file is a directory, the file manager of the current platform is
launched to open it.
More specifically, in case of a browser you can use directly Desktop#browse(uri), as next:
final Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null;
if (desktop != null && desktop.isSupported(Desktop.Action.BROWSE)) {
desktop.browse(uri);
} else {
throw new UnsupportedOperationException("Browse action not supported");
}
Launches the default browser to display a URI. If the default browser
is not able to handle the specified URI, the application registered
for handling URIs of the specified type is invoked. The application is
determined from the protocol and path of the URI, as defined by the
URI class. If the calling thread does not have the necessary
permissions, and this is invoked from within an applet,
AppletContext.showDocument() is used. Similarly, if the calling does
not have the necessary permissions, and this is invoked from within a
Java Web Started application, BasicService.showDocument() is used.
If you want to either open a URL which has an http: scheme in the browser, or open a file using the default application for that file type, the HostServices.showDocument(...) method you referenced provides a "pure JavaFX" way to do this. Note that you can't use this (as far as I can tell) to download a file from a web server and open it with the default application.
To open a file with the default application, you must convert the file to the string representation of the file: URL. Here is a simple example:
import java.io.File;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
public class OpenResourceNatively extends Application {
#Override
public void start(Stage primaryStage) {
TextField textField = new TextField("http://stackoverflow.com/questions/39898704");
Button openURLButton = new Button("Open URL");
EventHandler<ActionEvent> handler = e -> open(textField.getText());
textField.setOnAction(handler);
openURLButton.setOnAction(handler);
FileChooser fileChooser = new FileChooser();
Button openFileButton = new Button("Open File...");
openFileButton.setOnAction(e -> {
File file = fileChooser.showOpenDialog(primaryStage);
if (file != null) {
open(file.toURI().toString());
}
});
VBox root = new VBox(5,
new HBox(new Label("URL:"), textField, openURLButton),
new HBox(openFileButton)
);
root.setPadding(new Insets(20));
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
private void open(String resource) {
getHostServices().showDocument(resource);
}
public static void main(String[] args) {
launch(args);
}
}
Only the solution with java.awt.Desktop worked for me to open a file from JavaFX.
However, at first, my application got stuck and I had to figure out that it is necessary to call Desktop#open(File file) from a new thread. Calling the method from the current thread or the JavaFX application thread Platform#runLater(Runnable runnable) resulted in the application to hang indefinitely without an exception being thrown.
This is a small sample JavaFX application with the working file open solution:
import java.awt.Desktop;
import java.io.File;
import java.io.IOException;
import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
public class FileOpenDemo extends Application {
#Override
public void start(Stage primaryStage) {
final Button button = new Button("Open file");
button.setOnAction(event -> {
final FileChooser fileChooser = new FileChooser();
final File file = fileChooser.showOpenDialog(primaryStage.getOwner());
if (file == null)
return;
System.out.println("File selected: " + file.getName());
if (!Desktop.isDesktopSupported()) {
System.out.println("Desktop not supported");
return;
}
if (!Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
System.out.println("File opening not supported");
return;
}
final Task<Void> task = new Task<Void>() {
#Override
public Void call() throws Exception {
try {
Desktop.getDesktop().open(file);
} catch (IOException e) {
System.err.println(e.toString());
}
return null;
}
};
final Thread thread = new Thread(task);
thread.setDaemon(true);
thread.start();
});
primaryStage.setScene(new Scene(button));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The other proposed solution with javafx.application.HostServices did not work at all. I am using OpenJFX 8u141 on Ubuntu 17.10 amd64 and I got the following exception when invoking HostServices#showDocument(String uri):
java.lang.ClassNotFoundException: com.sun.deploy.uitoolkit.impl.fx.HostServicesFactory
Obviously, JavaFX HostServices is not yet properly implemented on all platforms. On this topic see also: https://github.com/Qabel/qabel-desktop/issues/420