FontAwesomeFX 8.0.13 released

FontAwesomeFX 8.0.13 comes with 40 new icons (FontAwesome 4.3.0)

Many thanks to Steffen Rachner for his pull request!

Download Binaries: Download
Sources available at Bitbucket

Maven:

<dependency>
  <groupId>de.jensd</groupId>
  <artifactId>fontawesomefx</artifactId>
  <version>8.0.13</version>
</dependency>

JFXPanel and FX Platform Thread pitfalls

The JFXPanel is a component to embed JavaFX content into (legacy ;-)) Swing applications.
Basically it makes it very easy to combine both tookits, but there are some pitfalls to master:
Both UI Toolkits are single threaded (Swing: EDT + JavaFX: FX Platform Thread). When used together you have to deal with these two threads, e.g.
javafx.embed.swing.SwingFXUtils.runOnFxThread(Runnable runnable)
or
javafx.embed.swing.SwingFXUtils.runOnEDT(Runnable runnable)

The FX Platform Thread is implicitly started in the constructor of the JFXPanel by initFx():

// Initialize FX runtime when the JFXPanel instance is constructed
private synchronized static void initFx() {
    // Note that calling PlatformImpl.startup more than once is OK
    PlatformImpl.startup(new Runnable() {
        @Override public void run() {
            // No need to do anything here
        }
    });
}

But (if I got it right) JFXPanel overrides addNotify() from Component where a finishListener is added to the FX-Platform ( PlatformImpl.addListener(finishListener)). Platform.exit is then called when the last JFXPanel “dies”.

This might lead into a weird situation:

When JFXPanel is used e.g. with a JDialog: The first call opens the Dialog with a new JFXPanel and all is going well.
But when this Dialog is closed the FX Platform Thread is exited and for some reasons it looks like the second call to open a new Dialog doesn’t start the FX Platform Thread again. So nothing happens on the JFXPanel!

Solution:

For me it worked to call (somewhere early in main())
Platform.setImplicitExit(false);
to prevent closing the FX Thread implicitly (its closed then by the System.exit()).

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.13 released

Download latest binaries at mqttfx.org.
Bugs and feature requests can be reported via the issue tracker.

New in this version:

Extended Scripting Features

  • fixed issues with starting external editor to edit scripts
  • support for subscribe/unsubscribe to Mqtt-Scripting interface


Redesinged and extended Settings Dialog

  • option to choose whether the default system editor should be use or a custom edit command
  • option to choose whether to check for updates on app start

new-settings

settings-script

settings-update

Build in Update Feature

  • Added support Refuel API for application updates

check-updates

updates-available

updates-load

KUDOs to my friend Dino Tsoumakis for his great work on his application update tool for Java 8 “Refuel” (view on github)!

ShichimiFX 1.0.5 released

I have released ShichimiFX 1.0.5.

Get more info and the code from my ShichimiFX Bitbucket Repo

Maven

<dependency>
  <groupId>de.jensd</groupId>
  <artifactId>shichimifx</artifactId>
  <version>1.0.5</version>
</dependency>

UI for Refuel

This release contains a Frontend for Refuel an application update API written by Dino Tsoumakis which is used with MQTT.fx 0.0.13.

updates-available

updates-load

ShichimiFX 1.0.4 released

I have released ShichimiFX 1.0.4.

Get the code from my ShichimiFX Bitbucket Repo

Maven

<dependency>
  <groupId>de.jensd</groupId>
  <artifactId>shichimifx</artifactId>
  <version>1.0.4</version>
</dependency>

This release contains 2 new utility classes I needed for MQTT.fx‘s script editing function Desktop and Shell.

By now I used to open a script for editing with the system default editor via java.awt.Desktop. So far this works well on my Mac but there were issues on Windows and Linux. To get rid of these issues I create my own Desktop class to open the script directly with the system shell:

Desktop

public class Desktop {

    public static boolean edit(String fileToEdit) {
        return openWithSystem(fileToEdit);
    }

    private static boolean openWithSystem(String fileToOpen) {
        if (OS.isLinux()) {
            if (Shell.canOpen("gnome-open", fileToOpen)) {
                return true;
            } else if (Shell.canOpen("kde-open", fileToOpen)) {
                return true;
            } else if (Shell.canOpen("xdg-open", fileToOpen)) {
                return true;
            }
        } else if (OS.isWindows()) {
            if (Shell.canOpen("explorer", fileToOpen)) {
                return true;
            }
        } else if (OS.isMac()) {
            if (Shell.canOpen("open", fileToOpen)) {
                return true;
            }
        }
        return false;
    }
}

Shell

public class Shell {

    public static boolean canOpen(String... command) {
        try {
            ProcessBuilder builder = new ProcessBuilder(command);
            Process process = builder.directory(new File(System.getProperty("user.home"))).start();
            if (process == null) {
                return false;
            }
            try {
                process.exitValue();
                // if this does not throw an IllegalThreadStateException the process is not running/has exited immediately
                return false;
            } catch (IllegalThreadStateException e) {
                // the process seems to be in a running state
                return true;
            }
        } catch (IOException ex) {
            return false;
        }
    }

    public static List<String> prepareOpenFileLine(String command, String fileToOpen) {
        List<String> commandList = new ArrayList<>();
        for (String s : command.split(" ")) {
            commandList.add(s.trim());
        }
        commandList.add(fileToOpen);
        return commandList;
    }

    public static Optional<Process> execute(List<String> command) throws IOException {
        return execute(new File(System.getProperty("user.home")), command);
    }

    public static Optional<Process> execute(File directory, String... command) throws IOException {
        return execute(directory, Arrays.asList(command));
    }

    public static Optional<Process> execute(String... command) throws IOException {
        return Shell.execute(new File(System.getProperty("user.home")), command);
    }

    public static Optional<Process> execute(File directory, List<String> command) throws IOException {
        ProcessBuilder builder = new ProcessBuilder(command);
        Process process = builder.directory(directory).start();
        return Optional.of(process);
    }
}