KickstartFX LogoKickstartFX Docs

GUI components

The how and why of compound components

About

While KickstartFX obviously uses JavaFX for the use your interface, there are still a lot of possibilities on how exactly any kind of node/control should be instantiated and handled in the codebase.

KickstartFX creates and manages these JavaFX nodes via comps (compound components). Essentially, a Comp is just a factory/supplier of Region instances. So when creating a comp, you are only configuring the factory for a Region. To actually create the JavaFX nodes, you will have to call the creator method.

Comps are a very basic system to create components for your user interface. If you don't like it are you something else, you can do so without problem. It's just that the existing pages like the preferences page uses them. Unless you want to heavily modify this page, there isn't any issue here.

Principles

Comps are based on a few principles that were discovered to be useful when developing, compared to just creating and using raw JavaFX nodes. You can see these principles in action with the few included Comp implementations in the comp package.

A comp should produce a Region instead of a Node

In practice, working with the very abstract Node class comes with its fair share of limitations. It is much easier to work with Region instances, as they have various width and height properties. Since pretty much every Node is also a Region, the main focus of Comps are Regions. In case you are dealing with Nodes that are not Regions, like an ImageView or WebView, you can still wrap them inside something like a StackPane to obtain a Region that you can work with.

A comp is a Region factory, not just another fancy wrapper for existing classes

It is advantageous to define a certain component to be a factory that can create an instance of a JavaFX Node each time it is called.

This essentially lazy initialization of your user inferface will prevent any kind of unintentional platform loading. The first call to any Node class, e.g. new Node() will trigger the loading of all JavaFX native libraries and toolkit dependencies. If you are building an application like KickstartFX that has strictly defined loading processes, it can happen that the platform is loaded accidentally when it shouldn't be loaded yet. With Comps, you can define all of your user interface layout, while not accidentally initializing the platform as this will only be done when you call the creator method.

By using this factory architecture, the creation process can be customized for each instance. The Comp class provides various methods to quickly perform the customizations. E.g. the Comp class contains method like prefWidth or maxHeight to quickly chain together common method calls for sizing. There is also support for controlling the visibility, setting tooptips, and more.

A comp should produce a transparent representation of all nodes that make up the control

In JavaFX, using skins allows for flexibility when generating the look and feel for a control. One limitation of this approach is that the generated node tree is not very transparent for developers who are especially interested in styling it. This is caused by the fact that a skin does not expose the information required to style it completely or even alter it without creating a new Skin class.

A comp should be designed to allow developers to easily expose as much information about the produced node tree structure using the CompStructure class.

In case you don't want to expose the detailed structure of your comp, you can also just use a SimpleComp or return a SimpleCompStructure.

The generation process of a comp can be augmented

As Comps are factories, any changes that should be applied to all produced Region instances must be integrated into the factory pipeline.

This can be achieved with the Augment class, which allows you to alter the produced Region after the base factory has finished.

This way you can reuse common augments between Comps to not duplicate code. It also makes your factories more flexible as augmentations can be handled via Augments and not via the Comp itself.

Properties used by Comps should be managed by the user, not the Comp itself

This allows Comps to only be a thin wrapper around already existing Observables/Properties and gives the user the ability to complete control the handling of Properties.

This is in contrast to JavaFX controls which instantiate their own properties. As a result, you always have to create bindings to the properties of controls. With Comps, you instantiate your own property instance and pass it to the Comp instead. This gives you more freedom when handling any kind of Properties. Also see the next point.

A comp should not break when used Observables are updated from a thread that is not the platform thread

One common limitation of using JavaFX is that many things break when calling any method or changing a property from another thread that is not the platform thread.

While in many cases these issues can be mitigated by wrapping a problematic call in a Platform.runLater(...), some problematic instances are harder to fix, for example, Observable bindings. In JavaFX, there is currently no way to propagate changes of an Observable to other bound Observables only using the platform thread, when the original change was made from a different thread.

KickstartFX provides a solution with the PlatformThread.sync() and PlatformThread.runLaterIfNeeded() methods and strongly encourages that Comps make use of these methods in combination with user-managed properties to allow for value changes for Observables from any thread without issue.

If you create a Comp with some kind of Property, you can freely modify it from any thread without any issue. The Comp will see that the change is propagated to the platform on the correct thread without you having to worry about it.

Not using comps

If you don't want to use Comps at all, but some methods of KickstartFX still take Comps as a parameter, you can create Comps inline as follows:

var comp = Comp.of(() -> new Label());
var region = comp.createRegion();

You can also use Comps in a minimal fashion by just extending the SimpleComp class:

public class MyComp extends SimpleComp {
 
    @Override
    protected Region createSimple() {
        var stack = new StackPane();
        // ... Do your stuff
        return stack;
    }
}