JavaFX

JavaFX is an open source, next generation client application platform for desktop, mobile and embedded systems based on JavaSE. It is a collaborative effort by many individuals and companies with the goal of producing a modern, efficient, and fully featured toolkit for developing rich client applications.

JavaFX is used on JabRef for the user interface.

Resources

Resources of historical interest

Architecture: Model - View - (Controller) - ViewModel (MV(C)VM)

The goal of the MVVM architecture is to separate the state/behavior from the appearance of the ui. This is archived by dividing JabRef into different layers, each having a clear responsibility.

  • The Model contains the business logic and data structures. These aspects are again encapsulated in the logic and model package, respectively.
  • The View controls the appearance and structure of the UI. It is usually defined in a FXML file.
  • View model converts the data from logic and model in a form that is easily usable in the gui. Thus it controls the state of the View. Moreover, the ViewModel contains all the logic needed to change the current state of the UI or perform an action. These actions are usually passed down to the logic package, after some data validation. The important aspect is that the ViewModel contains all the ui-related logic but does not have direct access to the controls defined in the View. Hence, the ViewModel can easily be tested by unit tests.
  • The Controller initializes the view model and binds it to the view. In an ideal world all the binding would already be done directly in the FXML. But JavaFX’s binding expressions are not yet powerful enough to accomplish this. It is important to keep in mind that the Controller should be as minimalistic as possible. Especially one should resist the temptation to validate inputs in the controller. The ViewModel should handle data validation! It is often convenient to load the FXML file directly from the controller.

The only class which access model and logic classes is the ViewModel. Controller and View have only access the ViewModel and never the backend. The ViewModel does not know the Controller or View.

More details about the MVVM pattern can be found in an article by Microsoft and in an article focusing on the implementation with JavaFX.

Example

ViewModel

  • The ViewModel should derive from AbstractViewModel
public class MyDialogViewModel extends AbstractViewModel {
}
private final ReadOnlyStringWrapper heading = new ReadOnlyStringWrapper();

public ReadOnlyStringProperty headingProperty() {
    return heading.getReadOnlyProperty();
}

public String getHeading() {
    return heading.get();
}
  • Create constructor which initializes the fields to their default values. Write tests to ensure that everything works as expected!
public MyDialogViewModel(@NonNull Dependency dependency) {
    this.dependency = dependency;
    heading.set("Hello " + dependency.getUserName());
}
  • Add methods which allow interaction. Again, don’t forget to write tests!
public void shutdown() {
    heading.set("Goodbye!");
}

View - Controller

  • The “code-behind” part of the view, which binds the View to the ViewModel.
  • The usual convention is that the controller ends on the suffix *View. Dialogs should derive from BaseDialog.
public class AboutDialogView extends BaseDialog<Void>
  • You get access to nodes in the FXML file by declaring them with the @FXML annotation.
@FXML protected Button helloButton;
@FXML protected ImageView iconImage;
  • Dependencies can easily be injected into the controller using the @Inject annotation.
@Inject private DialogService dialogService;
  • It is convenient to load the FXML-view directly from the controller class.

    The FXML file is loaded using ViewLoader based on the name of the class passed to view. To make this convention-over-configuration approach work, both the FXML file and the View class should have the same name and should be located in the same package.

    Note that fields annotated with @FXML or @Inject only become accessible after ViewLoader.load() is called.

    a View class that loads the FXML file.

private Dependency dependency;

public AboutDialogView(Dependency dependency) {
        this.dependency = dependency;

        this.setTitle(Localization.lang("About JabRef"));

        ViewLoader.view(this)
                .load()
                .setAsDialogPane(this);
}
  • Dialogs should use setResultConverter to convert the data entered in the dialog to the desired result. This conversion should be done by the view model and not the controller.
setResultConverter(button -> {
    if (button == ButtonType.OK) {
        return viewModel.getData();
    }
    return null;
});
  • The initialize method may use data-binding to connect the ui-controls and the ViewModel. However, it is recommended to do as much binding as possible directly in the FXML-file.
@FXML
private void initialize() {
    viewModel = new AboutDialogViewModel(dialogService, dependency, ...);

    helloLabel.textProperty().bind(viewModel.helloMessageProperty());
}
  • calling the view model:
@FXML
private void openJabrefWebsite() {
    viewModel.openJabrefWebsite();
}

View - FXML

The view consists a FXML file MyDialog.fxml which defines the structure and the layout of the UI. Moreover, the FXML file may be accompanied by a style file that should have the same name as the FXML file but with a css ending, e.g., MyDialog.css. It is recommended to use a graphical design tools like SceneBuilder to edit the FXML file. The tool Scenic View is very helpful in debugging styling issues.

Node ids exist for the walkthrough. A walkthrough step finds the control it highlights by id, so:

  • Give every major view, dialog and panel a stable id, and resolve walkthrough steps with NodeResolver.fxId(...) wherever such a node exists, rather than by class name, node type or visible text. Steps that target a virtualized cell — a row of the entry table, the groups tree or a preferences tab list — have no stable node to name and still match on text.
  • Keep every walkthrough id in WalkthroughNodeIds and set it from there. One list makes an id reusable, shows which controls the walkthroughs depend on, and stops one being deleted by accident. WalkthroughNodeIdsTest fails when a constant no longer names a node.
  • Style with styleClass, not with an id. Styling by id works and is occasionally unavoidable — the ids inside JavaFX’s own custom-color dialog are the remaining case — but keeping it rare is the point: renaming an id should never change the look, and restyling should never break a walkthrough.
  • Write ids in kebab case (entry-editor), the same way style classes are written. An fx:id has to stay a Java identifier because a controller field is named after it, so give such a node an explicit id attribute as well — FXML applies that one, and the fx:id keeps injecting.

CSS style classes and themes

The appearance of JabRef is the job of a theme. Themes live at https://themes.jabref.org/ (checked out as the submodule jabgui/src/main/themes.jabref.org, so JabRef bundles every theme that covers both color schemes); a theme sets the -color-* tokens and the rules of its style guide. A new look is a new theme there, not new CSS in JabRef.

What remains in JabRef is jabgui/src/main/resources/org/jabref/gui/theme/internal/jabref-base.css, loaded with every theme. It holds the utility classes for padding, gaps, alignment, font size and color (padding-12, gap-8, align-center-left, h3, text-accent) and the structure of the shared controls and views: borders, radii, paddings and layout of buttons, tabs, the entry editor, the main table and so on, all expressed in -color-* tokens so that a theme only has to pick colors. The drivers, from https://github.com/JabRef/jabref/issues/16042, https://github.com/JabRef/jabref/issues/16787 and https://github.com/JabRef/jabref/issues/15721:

  • Do not introduce a CSS class. Every class is a lookup for the next reader, and one used by a single view is usually a padding or a font size a utility class already offers. When the utilities cannot express what a view needs, one class named after the view (welcome-main-container) is the trade-off between few classes and a UI that still looks right; a second step on the spacing scale is not.
  • The utility classes form one fixed scale in em, so all views share the same few distances and grow with the user’s font size. Do not add padding-10 because one dialog looked better with it.
  • The spacing between the children of a VBox, HBox or GridPane goes into the constructor: new VBox(12), new GridPane(24, 24).
  • No inline styles: no setStyle(..), no styleProperty() binding, no -fx-* string in Java. An inline style beats every stylesheet, so a theme could not change it.
  • Colors only through the -color-* tokens, never as literals, so every theme keeps working.
  • Style with styleClass, not with an id; see the node id rules above.

FXML

The following expressions can be used in FXML attributes, according to the official documentation

Type Expression Value point to Remark
Location @image.png path relative to the current FXML file  
Resource %textToBeTranslated key in ResourceBundle  
Attribute variable $idOfControl or $variable named control or variable in controller (may be path in the namespace) resolved only once at load time
Expression binding ${expression} expression, for example textField.text changes to source are propagated
Bidirectional expression binding #{expression} expression changes are propagated in both directions (not yet implemented in JavaFX, see feature request)
Event handler #nameOfEventHandler name of the event handler method in the controller  
Constant <text><Strings fx:constant="MYSTRING"/></text> constant (here MYSTRING in the Strings class)  

JavaFX Radio Buttons Example

All radio buttons that should be grouped together need to have a ToggleGroup defined in the FXML code Example:

<VBox>
            <fx:define>
                <ToggleGroup fx:id="citeToggleGroup"/>
            </fx:define>
            <children>
                <RadioButton fx:id="inPar" minWidth="-Infinity" mnemonicParsing="false"
                             text="%Cite selected entries between parenthesis" toggleGroup="$citeToggleGroup"/>
                <RadioButton fx:id="inText" minWidth="-Infinity" mnemonicParsing="false"
                             text="%Cite selected entries with in-text citation" toggleGroup="$citeToggleGroup"/>
                <Label minWidth="-Infinity" text="%Extra information (e.g. page number)"/>
                <TextField fx:id="pageInfo"/>
            </children>
</VBox>

JavaFX Dialogs

All dialogs should be displayed to the user via DialogService interface methods. DialogService provides methods to display various dialogs (including custom ones) to the user. It also ensures the displayed dialog opens on the correct window via initOwner() (for cases where the user has multiple screens). The following code snippet demonstrates how a custom dialog is displayed to the user:

dialogService.showCustomDialog(new DocumentViewerView());

If an instance of DialogService is unavailable within current class/scope in which the dialog needs to be displayed, DialogService can be instantiated via the code snippet shown as follows:

DialogService dialogService = Injector.instantiateModelOrService(DialogService.class);

Properties and Bindings

JabRef makes heavy use of Properties and Bindings. These are wrappers around Observables. A good explanation on the concept can be found here: JavaFX Bindings and Properties

Features missing in JavaFX