How to allow users to customize the UI

Idea

Take advantage of the declarative design pattern of JavafX/FXML and allow users to customize a certain view without any coding just by opening it with e.g. SceneBuilder to re-arrange the layout or add new controls or even change the style according to the users needs.

The FXML file + CSS can be basically be placed whereever they are reachable via a URL.
The user must only know the interface/methods of the assigned controller class inside the FXML.

RemoteController

Assuming this simple demo controller class provides methods to remotely control devices and to send MQTT messages, a user is able to customize his own remote control.

public class RemoteController{

    @FXML
    public void onTest(){
        Alert alert = new Alert(Alert.AlertType.INFORMATION);
        alert.setContentText("");
        alert.setHeaderText("WORKS!");
        alert.show();
    }
    
    public void onTest(String value){
        Alert alert = new Alert(Alert.AlertType.INFORMATION);
        alert.setHeaderText("WORKS!");
        alert.setContentText(value);
        alert.show();
    }
    
    public void onSwitch(String houseCode, int groudId, int deviceId, String command){
        Alert alert = new Alert(Alert.AlertType.INFORMATION);
        alert.setHeaderText("Switch!");
        alert.setContentText(String.format("Command: send %s %d %d %s", houseCode, groudId, deviceId, command));
        alert.show();
    }
}

remote.fxml and remote.css

Note the referenced de.jensd.shichimifx.demo.ext.RemoteController and remote.css.
So basically controller actions can be called via

onAction="#onTest".

Nice:
If you add

<?language javascript?>

to FXML, it’s also possible to pass parameters by a JavaScript call via the controller-instance.

onAction=controller.onTest('OFF')
onAction=controller.onSwitch('a',1,1,'ON')

Unfortunately I can’t find more documentation about this feature than -> this, but somehow it magically it works ;-). Its even possible to pass different types of parameters.

<?xml version="1.0" encoding="UTF-8"?>

<?language javascript?>
<?import javafx.geometry.*?>
<?import java.lang.*?>
<?import java.net.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<VBox alignment="TOP_CENTER" prefHeight="400.0" prefWidth="600.0" spacing="20.0" styleClass="main-pane" stylesheets="@remote.css" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="de.jensd.shichimifx.demo.ext.RemoteController">
   <children>
      <Label styleClass="title-label" text="Universal Remote" />
      <HBox alignment="CENTER_RIGHT" spacing="20.0">
         <children>
            <Label layoutX="228.0" layoutY="96.0" styleClass="sub-title-label" text="Light Frontdoor" />
            <Button layoutX="43.0" layoutY="86.0" mnemonicParsing="false" onAction="#onTest" prefWidth="150.0" styleClass="button-on" text="ON" />
            <Button layoutX="411.0" layoutY="86.0" mnemonicParsing="false" onAction="#onTest" prefWidth="150.0" styleClass="button-off" text="OFF" />
         </children>
         <padding>
            <Insets left="10.0" right="10.0" />
         </padding>
      </HBox>
      <HBox alignment="CENTER_RIGHT" spacing="20.0">
         <children>
            <Label layoutX="228.0" layoutY="96.0" styleClass="sub-title-label" text="Light Garden" />
            <Button layoutX="43.0" layoutY="86.0" mnemonicParsing="false" onAction="controller.onTest('ON')" prefWidth="150.0" styleClass="button-on" text="ON" />
            <Button layoutX="411.0" layoutY="86.0" mnemonicParsing="false" onAction="controller.onTest('OFF')" prefWidth="150.0" styleClass="button-off" text="OFF" />
         </children>
         <padding>
            <Insets left="10.0" right="10.0" />
         </padding>
      </HBox>
      <HBox alignment="CENTER_RIGHT" spacing="20.0">
         <children>
            <Label layoutX="228.0" layoutY="96.0" styleClass="sub-title-label" text="Light Garden" />
            <Button layoutX="43.0" layoutY="86.0" mnemonicParsing="false" onAction="controller.onSwitch('a', 1,1,'ON')" prefWidth="150.0" styleClass="button-on" text="ON" />
            <Button layoutX="411.0" layoutY="86.0" mnemonicParsing="false" onAction="controller.onTest('OFF')" prefWidth="150.0" styleClass="button-off" text="OFF" />
         </children>
         <padding>
            <Insets left="10.0" right="10.0" />
         </padding>
      </HBox>
   </children>
   <padding>
      <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
   </padding>
</VBox>

Based on this example a user is able to simple open the FXMl with SceneBuilder and to add new Button calling the controller.onSwitch() method to control different/new devices installed for home automation.

FxmlUtils
The next release of ShichimiFX will contain a new Utilily class to load FXML as shown in the ExternalFXMLDemoController. Note that the loaded Pane is added to the center of the externalPane (BorderPane) of the Demo-Application via onLoadExternalFxml():

public class ExternalFXMLDemoController {

    @FXML
    private ResourceBundle resources;

    @FXML
    private BorderPane externalPane;

    @FXML
    private TextField fxmlFileNameTextField;

    @FXML
    private Button chooseFxmlFileButton;

    @FXML
    private Button loadFxmlFileButton;

    private StringProperty fxmlFileName;

    public void initialize() {
        fxmlFileNameTextField.textProperty().bindBidirectional(fxmlFileNameProperty());
        loadFxmlFileButton.disableProperty().bind(fxmlFileNameProperty().isEmpty());
    }

    public StringProperty fxmlFileNameProperty() {
        if (fxmlFileName == null) {
            fxmlFileName = new SimpleStringProperty("");
        }
        return fxmlFileName;
    }

    public String getFxmlFileName() {
        return fxmlFileNameProperty().getValue();
    }

    public void setFxmlFileName(String fxmlFileName) {
        this.fxmlFileNameProperty().setValue(fxmlFileName);
    }

    @FXML
    public void chooseFxmlFile() {
        FileChooser chooser = new FileChooser();
        chooser.setTitle("Choose FXML file to load");
        if (getFxmlFileName().isEmpty()) {
            chooser.setInitialDirectory(new File(System.getProperty("user.home")));
        } else {
            chooser.setInitialDirectory(new File(getFxmlFileName()).getParentFile());
        }

        File file = chooser.showOpenDialog(chooseFxmlFileButton.getScene().getWindow());
        if (file != null) {
            setFxmlFileName(file.getAbsolutePath());
        }
    }

    @FXML
    public void onLoadExternalFxml() {
        try {
            Optional<URL> url = FxmlUtils.getFxmlUrl(Paths.get(getFxmlFileName()));
            if (url.isPresent()) {
                Pane pane = FxmlUtils.loadFxmlPane(url.get(), resources);
                externalPane.setCenter(pane);
            } else {
                Alert alert = new Alert(Alert.AlertType.WARNING);
                alert.setContentText(getFxmlFileName() + " could not be found!");
                alert.show();
            }
        } catch (IOException ex) {
            Dialogs.create().showException(ex);
        }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

5 thoughts on “How to allow users to customize the UI”

  1. Thanks for the very nice work.
    What if we want to add a new field with a new event in the controller which doesn’t exist yet in the controller. How can we do it ?
    Thank
    Tkp