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

MQTT.fx 0.0.7 released

A new version of MQTT.fx is available featuring:

  • Scripting support
  • Setup/Re-configuration tool
  • Reviewed Connection Profile Editor
  • Reviewed UI and style

007_start

Download latest binaries HERE.

Scripting support

I have added support for scripts executed by the Nashorn Engine.
Thought it might be helpful for testing a MQTT message flow of a certain setup, e.g. simulate sensors sending temperature value messages. All controlled via the UI.

007_executescript

Adding new scripts

New scripts can be easily made available for execution via the UI. The app is scanning a folder named “scripts” located at the MQTT.fx working directory:
OS X: [USER_HOME]/Library/Application Support/MQTT-FX/scripts
Windows: [USER_HOME]\AppData\Local\MQTT-FX\scripts
Linux: [USER_HOME]/MQTT-FX/scripts
If this folder does not exists it is created by MQTT.fx containing a demo script.

007_workingdir

Naming convention:

[INDEX]__[NAME].js
01__Switch_Fountain_Test.js

[INDEX] - controls the order of the entry in the drop down menu
__ - delimiter between index and name
[NAME] - the name of the menu entry (words are delimit by '_')

Hint:
If you name a script “separator”, e.g. 02__separator.js a separator is added to the menu at the “index” position.

007_scripting

Example script:


var Thread = Java.type(“java.lang.Thread”);
var System = Java.type(‘java.lang.System’);

function execute(action) {
out(“Test Script: ” + action.getName());
for (var i = 0; i < 10; i++) { switchON(); Thread.sleep(500); switchOFF(); Thread.sleep(500); } action.setExitCode(0); action.setResultText("done."); out("Test Script: Done"); return action; } function switchON() { out("fountain ON"); mqttManager.publish("home/garden/fountain", "ON"); } function switchOFF() { out("fountain OFF"); mqttManager.publish("home/garden/fountain", "OFF"); } function out(message){ System.out.println(message); } [/javascript] The entry point of execution is the function execute(action).
During script execution System.out is hooked by MQTT.fx to print output to the console. Thus with System.out.println() messaged can be printed to the UI-console.

The interface to the broker-connection is the bound mqttManager. Messages can be published via the established broker connection.

Setup/Re-configuration Tool

If no configuration can be found, the app assumes its first start. The user is promoted with a setup dialog to create e new configuration.

Also if a configuration has been found but it seems to be corrupt or the format doesn’t fit a newer version also a new configuration can be created or the exiting config can be opened in an external editor (assumed an editor has been assigned to open *.xml-files.

007_setup

Reviewed Connection Profile Editor
Now the Connection Profile Editor is recognising wether the chosen profile has been changed (buttons are enabled/disabled accordingly). Changes can be reverted as not already applied.

007_newprofile

Reviewed UI and Style

Greetings from Yosemity! 😉