tentackle-fx — The Extended JavaFX Layer¶
Overview and Motivation¶
tentackle-fx is Tentackle's desktop UI foundation. It wraps the standard
JavaFX controls and containers in an extended FX layer that integrates
seamlessly with the rest of the framework: the binding
API, validation,
PDOs, internationalization,
theming, and SPI-based service discovery.
The module lives in package org.tentackle.fx (JPMS module org.tentackle.fx)
and requires transitive org.tentackle.core together with the JavaFX modules
(javafx.fxml, javafx.web, javafx.controls, …) and the Ikonli icon packs.
Why an extended FX layer?¶
Plain JavaFX gives you controls, FXML, properties, and a controller hook. What it does not give you is a way to connect those controls to a domain model without hand-writing glue per field, nor a unified notion of "changeable", "mandatory", "error" or "value of the model's type". Tentackle adds exactly that:
- Every control is a first-class, bindable component.
FxTextField,FxComboBox,FxTableView, … each implementFxComponentand carry a model type, a value translator, mandatory/changeable/error state, and popup support — on top of the JavaFX control they extend. - Controllers are wired automatically. An
@FxControllerService-annotated controller is discovered, its FXML/CSS/resource bundle located by convention, its@FXMLfields injected, and its components bound by name to the model — notextProperty().bindBidirectional(...)boilerplate. - Model values, not strings. A component exposes
getViewValue()/setViewValue()in the model's own type (BigDecimal,LocalDate,BMoney, an enum, a PDO …). A pluggableValueTranslatorconverts to and from the on-screen representation. - Toolkit-agnostic model side. The binding and validation contracts live in
tentackle-coreand know nothing about JavaFX.tentackle-fxis merely the reference implementation of those contracts for the JavaFX toolkit. - Modular and non-modular. Components, translators, configurators,
controllers, and table-cell types are all located through the
ServiceFinderSPI, so the same code runs on the module path or the class path.
Design principles¶
- Wrap, don't replace. Each Tentackle component extends its JavaFX
counterpart, so anything you can do with a
TextFieldyou can still do with anFxTextField. The extras are additive. - Delegate over inheritance. Because a component must already extend a
concrete JavaFX class, the shared
FxComponentbehavior cannot also be inherited from a common base class. Instead, each component holds a delegate that implements the behavior. This emulates the multiple inheritance that the PDO layer achieves with proxies. - Configure by convention, override by service. Defaults come from naming conventions (FXML file next to the controller, bundle by class name) and from per-type configurators; applications override by contributing their own annotated services.
- No state in shared singletons. Factories, configurators, and cell-type factories are stateless singletons located once and reused across all instances.
Key Concepts¶
FxControl, FxComponent, and FxContainer¶
The control hierarchy mirrors JavaFX but adds the Tentackle contract:
FxControl— the common parent of everything in the layer. It defines the changeable abstraction (setChangeable/isChangeable/changeableProperty), which is translated tosetEditableorsetDisabledepending on the underlying control, plus the container-changeable propagation that lets a whole form be switched read-only at once.FxComponent extends FxControl— a leaf control that carries a value: text fields, combo boxes, check boxes, pickers, tables, etc. It adds the model type (setType/getType, plus a generic type), the value translator, thegetViewValue()/setViewValue()value access in the model's type, the mandatory flag, and — viaErrorPopupSupportedandInfoPopupSupported— error and info popups.FxContainer extends FxControl— a layout node holding components:FxVBox,FxGridPane,FxTabPane, … It knows itsFxController, can walk the controller hierarchy (getController(Class)), and offers form-wide operations such asclearErrors(),updateView()andupdateViewNonFocused().
The delegate pattern¶
Every component must extend a concrete JavaFX class (FxTextField extends
TextField), so the cross-cutting FxComponent behavior cannot live in a
shared superclass. Tentackle therefore keeps that behavior in a delegate:
public class FxTextField extends TextField implements FxTextComponent {
private FxTextFieldDelegate delegate;
protected FxTextFieldDelegate createDelegate() {
return new FxTextFieldDelegate(this);
}
@Override
public FxTextFieldDelegate getDelegate() {
if (delegate == null) {
setDelegate(createDelegate());
}
return delegate;
}
// ... thin forwarding methods generated by a wurblet ...
}
The component is a thin shell that extends the JavaFX class and forwards to its
delegate; the delegate (FxTextFieldDelegate → AbstractTextFieldDelegate →
FxTextComponentDelegate → FxComponentDelegate → FxControlDelegate) holds the real logic — the type,
the translator, the mandatory/error/info state, and the toolkit-specific
getViewObject()/setViewObject() that read and write the actual widget. The
forwarding methods are generated by a wurblet (@wurblet delegate Include), so
the boilerplate is never hand-maintained. setDelegate(...) lets an application
substitute custom behavior. The delegate base also defines the standard CSS
style classes tt-mandatory, tt-error and tt-info.
Controllers¶
A controller drives a view. FxController is the contract; AbstractFxController
is the usual base class (and AbstractValidateableFxController adds
Validateable for controllers that validate themselves rather than their
bindables). A controller:
- holds its
view(aParent, usually also anFxContainer) and its top-level container; - owns an
FxComponentBinder(getBinder()) connecting components to the model; - tracks its
@FXML-annotated fields and methods, andvalidateInjections()verifies that everyfx:idactually resolved to a non-null field (the FXML loader silently ignores mismatches) — the controller tests oftentackle-test-fx-rdcinvoke it for all controllers; - runs
configure()as the final initialization step.
Controllers are discovered, not hand-instantiated, through the
@FxControllerService annotation (see below).
@FxControllerService — declaring a controller¶
@FxControllerService marks a class as a Tentackle FX controller and is an
SPI @Service(FxController.class), so FxFactory can enumerate and create all
controllers. By convention the loader resolves, next to the controller class:
| Attribute | Default / convention |
|---|---|
url() |
<ControllerName>.fxml (set FXML_NONE to skip the FXML loader) |
resources() |
<ControllerName>.properties (set RESOURCES_NONE for none) |
css() |
<ControllerName>.css (missing is fine) |
cssDarkAware() |
append -dark to the CSS file name in dark mode |
binding() |
BINDING.YES; also NO, COMPONENT_INHERITED, BINDABLE_INHERITED, ALL_INHERITED |
view() |
a view class to use instead of FXML (see no-FXML below) |
test() |
whether the controller is created and bound in unit tests |
FXML, CSS, and
.propertiesfiles are co-located with the controller insrc/main/java(notsrc/main/resources) and copied as resources at build time, so SceneBuilder resolves them by relative path. See the FXML/FX conventions inCLAUDE.md.
FxFactory — the central factory¶
FxFactory.getInstance() is the singleton entry point. It enumerates the
registered controller classes, supplies the BuilderFactory used by the FXML
loader, hands out per-class Configurators, creates ValueTranslators for a
given model/view type pair, creates configured Stages and Scenes, graphics,
notifications and table configurations, and creates controllers
(createController(...)). The default implementation is DefaultFxFactory.
Fx — the convenience facade¶
Fx is a static helper covering the common operations so application code stays
terse:
- Loading:
Fx.load(url),Fx.load(url, resources),Fx.load(controllerClass). - Stages and scenes:
createStage(...),createScene(root),show(stage),getStage(node),isModal(stage). - Creating components:
Fx.create(TextField.class)returns the configured Tentackle implementation (here anFxTextField) via the builder factory, just as the FXML loader does. - Dialogs:
info,warning,error,question,yes,no— each taking an owner, a message, and an optional callback. - Icons:
createGraphic(realm, name)/createGraphic(name)via theGraphicProviderSPI (Ikonli or image based). - Misc:
copyToClipboard(text),terminate().
Value translators¶
A ValueTranslator<M,V> converts between the model type M and the view
type V (Tentackle says translator, not converter, to avoid confusion with
JavaFX StringConverter). It exposes a toViewFunction() and a
toModelFunction(), a lenient flag for parsing, and an incomplete-mapping
mechanism (isMappingIncomplete() / saveModelValue()) for types — like
I18NText — where the view shows only part of the model and the rest must be
remembered to detect changes.
Translators are located by model/view type through FxFactory.createValueTranslator(...)
and registered with @ValueTranslatorService. The org.tentackle.fx.translate
package ships a large built-in set: numeric (IntegerStringTranslator,
LongStringTranslator, DoubleStringTranslator, BMoneyStringTranslator,
FractionNumberStringTranslator, …), temporal (LocalDateStringTranslator,
InstantStringTranslator, ZonedDateTimeStringTranslator, …), EnumStringTranslator,
I18NTextStringTranslator, ColorColorTranslator, identity/collection helpers,
and the AbstractValueTranslator base.
Tables¶
FxTableView, FxTreeTableView and TotalsTableView are the table components,
with FxTableColumn / FxTreeTableColumn and matching cells. Columns are
populated from a table configuration (TableConfiguration /
TableColumnConfiguration, default implementations and a
TableConfigurationProvider discovered via @TableConfigurationProviderService).
Per-value rendering is handled by table cell types: a TableCellType knows
how to display and edit one value class, produced by a TableCellTypeFactory
and registered with @TableCellTypeService. The org.tentackle.fx.table.type
package supplies cell types for strings, numbers, booleans, enums, money,
I18NText and the full range of date/time classes. Tables bind through the
dedicated FxTableBinder / FxTableBinding (see the binding doc).
For the full picture — including preferences-backed layouts, totals, and the
PDO-aware RDC layer — see tables.md.
Showing validation results¶
Validation results describe problems in the model (invoice.lineList[3].price),
not on screen. An InteractiveError links a ValidationResult to the concrete
FxControl it belongs to, adding the message, an optional error code, and
whether it is an error or just a warning. InteractiveErrorFactory (a
replaceable @Service) resolves the control by mapping the validation path to a
binding path and looking up the FxComponentBinding.
AbstractValidateableFxController.validateForm() ties it together: it validates,
shows the collected messages in a dialog, and marks the offending components with
setError / setInfo. See interactive-error.md.
Graphics and icons¶
Icons never appear as file names or icon-pack names in framework code. A
GraphicProvider maps a symbolic name to a JavaFX Node
(Fx.createGraphic("save")), and providers are registered per realm with
@GraphicProviderService — the framework uses the default realm, applications
should use their own. DefaultGraphicProvider serves the default realm from the
Ikonli materialdesign2 pack; IkonliGraphicProvider and ImageGraphicProvider
are the base classes for font-icon and image-file providers. Size and color come
from CSS, so icons follow the theme. See
graphic-provider.md.
Auto-completion¶
Any FxTextComponent can propose matching values while the user types:
setAutoCompletion(...) takes a Function<String, List<List<SubString>>> mapping
the current input to the matching candidates, each described by its matching text
segments. The org.tentackle.fx.component.auto package supplies the controller
and ready-made suggestion generators (simple substring, camel-case, separated
segments, regular expression), and the matched segments are rendered in bold in a
popup below the component. Matching runs in the background, so large choice lists
don't block the FX thread. For generators, styling and threading rules see
auto-completion.md.
Expandable one-line text areas¶
A text area displayed as a one-liner (prefRowCount of 1, e.g. from the
LINES=1 binding option) is a compromise: it saves space in the form, but the
text does not fit. FxTextArea can therefore show an expand button at its
top right corner. Clicking it pops up a multi-line editor that covers the text area,
aligned to its upper left corner, so that the first line stays where it is and
the editor grows downwards. It collapses again as soon as it loses the focus, or on ESCAPE, or
via the collapse button in its upper right corner.
The feature is off by default and enabled per component, regardless of its number of rows:
textArea.setExpandable(true); // opt in
textArea.setExpandedRowCount(10); // 0 = FxUtilities.getDefaultExpandedRowCount(), which is 6
textArea.expand(); // programmatically, e.g. from a menu item
Expanding is only a different view on the same component. The editor's text is
written back to the text area on each keystroke, so binding, validation, and the
modification tracking behave exactly as if the user had typed into the text area
itself and the text area keeps the focus in its own scene while the popup is
showing — the model is updated by its regular focus handling. The editor also
gets a TextFormatter with the component's delegate as its filter, so valid
chars, case conversion and maxColumns apply to the expanded view as well.
ENTER inserts a newline there instead of navigating, since entering more than
one line is the very reason to expand; TAB collapses and navigates as usual.
The button lives in FxTextAreaSkin, which lays out the text in the space left
of it, and the editor in ExpandedTextPopup. Styling hooks:
| Hook | Kind | Applies to |
|---|---|---|
.tt-expand-button |
style class | both the expand- and the collapse button |
.tt-expanded-text |
style class | the popup |
.tt-expanded-text-pane |
style class | the popup's StackPane |
Input alerts¶
Some input is rejected silently: text typed or pasted beyond maxColumns is cut
off by the delegate's TextFormatter filter, and BACKSPACE at the beginning of
the text has nothing to delete. InputAlert gives the user feedback in both
cases, acoustically and/or visually. java.awt.Toolkit.beep() is no option,
since it beeps the system speaker instead of the sound system, and JavaFX has no
API to access the system alerts.
- The visual alert briefly (150 ms) sets the CSS pseudo-class
:tt-alerton the control. The stylesheets map it to the color variable-tt-alert-color, which overrides the mandatory, error and info colors for the duration of the flash. Since JavaFX weighs a style class higher than any number of pseudo-classes (unlike the W3C rules), the stylesheets combine each state class with the pseudo-class (.tt-mandatory:tt-alertetc.). Application stylesheets that color controls by their own style classes must do the same, e.g..my-state:tt-alert, or the flash will not show on those controls. - The acoustic alert plays a short, soft bump (the bundled
org/tentackle/fx/input-alert.wav) viajavax.sound.sampled, i.e. through the sound system. An alert is not played again while the previous one is still running, so holding down a key does not stack tones. If no audio device is available, a warning is logged once and the alert stays silent.
The alerts are triggered by all text components (FxTextField, FxTextArea,
FxPasswordField, the editors of FxComboBox and FxDatePicker, and the
expanded text popup). Truncation while the view is updated from the model does
not alert, since the user did not type anything.
By default, only the visual alert is enabled. Both can be changed at runtime, e.g. from a settings dialog, since they are JavaFX properties:
InputAlert alert = InputAlert.getInstance();
alert.setSoundEnabled(true);
alert.setVisualEnabled(false);
alert.setSound(MyApp.class.getResource("bump.wav")); // wav, aiff or au; null = default sound
soundCheckBox.selectedProperty().bindBidirectional(alert.soundEnabledProperty());
A single control opts out with
control.getProperties().put(InputAlert.DISABLED, Boolean.TRUE). To change the
behavior altogether, e.g. to alert on other occasions via InputAlert.Reason,
replace DefaultInputAlert with an implementation annotated
@Service(InputAlert.class).
Keyboard handling¶
The component configurators install a consistent keyboard model, geared to fast data entry:
| Key | Effect |
|---|---|
ENTER / Shift-ENTER |
Move the focus to the next / previous component, like TAB / Shift-TAB. Not in table cell editors. |
Ctrl-ENTER / Ctrl-TAB |
In a text area: insert a newline / tab, since ENTER navigates there as well, unless FxTextArea.setNavigateViaEnterEnabled(false) restores the JavaFX behavior. |
Shift-BACKSPACE |
Clear a text field, the editor of a date picker or an editable combo box. |
Ctrl-Z |
Restore the saved value of the component — usually the value loaded from the database (see View-modified tracking). |
Ctrl-Y |
Bring back the value the user entered, i.e. undo the Ctrl-Z. The two keys toggle between the old and the new value. |
DELETE / BACKSPACE |
In a non-editable combo box or a choice box: clear the selection, if allowed (see Combo and choice boxes). |
F1 |
Show the online help of the component. |
Ctrl-C / Shift-Ctrl-C |
In a table or tree table: copy the selected cells to the clipboard, in a table rows and columns swapped with Shift (see Utilities). |
Ctrl-Shift + right click |
Log the component hierarchy under the mouse pointer and, if BundleMonkey is running, navigate to the component's bundle. |
With tentackle-fx-rdc, F2 and F3 edit and search the PDO shown in a component, see
PDO-bound components.
When a component gains the focus, it updates its view from the model, shows its info or error note
(see Notes and notifications) and, if autoSelect
is set, selects its text. When it loses the focus, it updates the model — this is the moment the
binding transfers the value and validation runs. The notes are also shown while the mouse hovers
over the component.
The key handling is implemented by FxUtilities.remapKeys, filterKeys and setupFocusHandling,
invoked by the component configurators. An application changes it by overriding
these methods in its own FxUtilities service, or per type with a configurator.
View-modified tracking¶
Every control remembers the value it showed after the last update from the model: saveView()
takes a copy, invalidateSavedView() drops it. As soon as the user types, triggerViewModified()
compares the current view with the saved one and sets the viewModified property. The flag bubbles up
the container hierarchy, so the top-level container of a form knows whether anything was changed.
This is how the RDC's PdoCrud decides whether to ask "save, discard or cancel?" before closing, and
it is what Ctrl-Z restores. The saved view is only replaced by saveView(), not by updateView():
call saveView() on the container after loading a new object to start over.
Online help¶
Every control may carry a help URL (setHelpUrl(...)). F1, or showHelp(), opens the URL of the
control in the system browser — or, if the control has none, the URL of the nearest container that
has one, so a single URL on a form's top container covers all its fields. The URLs are relative to
a prefix set once for the application:
FxUtilities.getInstance().setHelpURL("https://help.example.com/myapp/");
invoiceNoField.setHelpUrl("invoice.html#number");
Text input properties¶
Text components (FxTextField, FxTextArea, FxPasswordField and the editors of combo boxes and date
pickers) implement FxTextComponent, which controls how the input is accepted and formatted:
| Property | Effect |
|---|---|
columns / maxColumns |
The visible width in characters / the maximum length of the input (longer input is cut off with an input alert). |
caseConversion |
UPPER_CASE or LOWER_CASE while typing. |
validChars / invalidChars |
The characters accepted / rejected while typing. |
filler / stripMode |
A filler character (default blank) and which fillers are stripped from the input: NONE, LEFT, RIGHT or BOTH (default). |
textConverter |
A Function<String,String> applied to the typed text before it is shown. |
pattern |
The format pattern of numbers and dates, overriding the defaults of the value translator. |
scale |
The number of fraction digits of a decimal number. |
unsigned |
Reject negative numbers. |
utc |
Interpret a timestamp as UTC. |
lenient |
A hint for application-specific value translators to parse leniently. The built-in translators don't evaluate it; dates accept abbreviated forms anyway (see SmartDateTimeParser). |
autoSelect |
Select the whole text when the component gains the focus, handy for numeric fields. |
textAlignment |
The alignment of the text; numbers are right-aligned by default. |
autoCompletion |
See Auto-completion. |
Usually, these properties are not set in code, but derived from the model: the binding applies the
binding options of the bound
attribute (the @Bindable options generated from the model) to the component.
| Binding option | Property |
|---|---|
uc / lc |
caseConversion |
maxcols=<n> (and size for the model size) |
maxColumns |
cols=<n> |
columns |
lines=<n> |
the row count of a text area |
scale=<n> |
scale |
filler=<c> |
filler |
strip, nostrip, lstrip, rstrip |
stripMode |
digits |
validChars = the digits |
unsigned |
unsigned |
utc |
utc |
autoselect |
autoSelect |
In tables, the options uc/lc, autoselect, unsigned, digits, maxcols and scale configure
the column; cols, lines, filler and the strip modes are ignored there.
An application that wants money to be entered cash-register style overrides
FxUtilities.isLenientMoneyInput(component) to return true (default false): a decimal separator
missing in the input is then inserted according to the scale, so 1234 becomes 12.34.
Date and time input¶
Text components bound to a java.time type parse the input with the
SmartDateTimeParser,
so users may type shortcuts such as . for now, +3d or 0103. The legacy types (java.util.Date,
java.sql.Timestamp, java.sql.Time and their org.tentackle.common counterparts) are translated via
their java.time equivalents. TemporalStringTranslator
adds a few component properties on top:
- Reference date. An input starting with
@is replaced by the date of aSupplierstored underTemporalStringTranslator.REFERENCE_DATE_SUPPLIER, typically the value of a related field. For a range,connectAsReferenceDateSuppliers(fromField, untilField)lets both bound fields refer to each other, so@in one field takes the date of the other. - Sliding windows. The number shortcut completes a year, month or day given with fewer digits
according to a sliding window around the current date. The windows are set per component with the
IntegerpropertiesYEAR_WINDOW(default 50 years),MONTH_WINDOWandDAY_WINDOW(default-1, disabled).
dueField.getProperties().put(TemporalStringTranslator.YEAR_WINDOW, 10);
TemporalStringTranslator.connectAsReferenceDateSuppliers(fromField, untilField);
A parse error sets a temporary error on the component, positions the caret at the error offset, and keeps the focus in the component.
Multilingual texts¶
A text component bound to an I18NText
shows and edits the translation for the current locale and is marked with the style class tt-i18n.
F2 or the context menu item edit translations opens the
I18NEditor, a dialog with one editor per
language of the LocaleProvider's effective locales, the fallback language on top. Since the component shows only one of the
translations, the view-modified tracking compares the whole I18NText
as well, so changing a hidden translation marks the form modified, too.
Secure password input¶
FxPasswordField bound to a char[] never turns the password into a String. getPassword()
returns the characters and overwrites the field's internal buffer with blanks, and the previous model
value is blanked when the model is updated; the application is responsible for clearing the returned
array. Access to the internal buffer requires
--add-opens javafx.controls/javafx.scene.control=org.tentackle.fx in modular mode. Otherwise, the field
logs a warning and falls back to a less secure mode. With a char[] model, the text processing such as
maxColumns or filtering is disabled, and standard FX bindings or listeners on the text must not be used.
Notes and notifications¶
Two kinds of popups give feedback without blocking the user:
- A
Noteis a small speech bubble linked to a component. Tentackle uses notes for the error message of a component (e.g. after a parse error) and for its info message, shown to the right of the component while it has the focus or the mouse hovers over it. A new note is centered on its component unless another position is set. There are three types —ERROR,INFOandSTICKY— and five positions (RIGHT,LEFT,TOP,BOTTOM,CENTER), which map to the style classes of the same name in lower case on the style classnote. Applications use notes for hints of their own:
Note note = Fx.create(Note.class);
note.setType(Note.Type.STICKY);
note.setText("check the delivery address");
note.show(addressField);
- A notification is a lightweight replacement for a JavaFX
Alertdialog: a popup centered above its owner window, built by aNotificationBuilderfromFxFactory.createNotificationBuilder(). The dialogs of theFxfacade —Fx.info,warning,error,question— are notifications. UnlikeAlert.showAndWait(), they do not block the event queue: the methods return immediately and deliver the answer to a callback. In a question,0answers no and1answers yes. A custom notification looks like this:
Popup popup = FxFactory.getInstance().createNotificationPopup();
Parent notification = FxFactory.getInstance().createNotificationBuilder()
.type(NotificationBuilder.Type.WARNING) // NONE, INFORMATION, WARNING, ERROR, CONFIRMATION, QUESTION
.title("Import")
.text("3 lines skipped")
.details(log) // or .content(node)
.duration(5000) // close automatically after 5 s
.hide(popup::hide)
.build();
FxUtilities.getInstance().showNotification(owner, popup, notification, null);
button(...) adds buttons with actions, closeButton(...) controls the close button, fadeIn and
fadeOut animate it, and css(url) replaces the default stylesheet org/tentackle/fx/notification.css.
The notification pane carries the style class tt-notification.
Combo and choice boxes¶
FxComboBox and FxChoiceBox add a few conveniences to their JavaFX counterparts:
- Prefix selection. In a non-editable box, typing selects the first item starting with the typed
characters; the prefix is reset after a pause of
FxUtilities.getPrefixSelectionTimeout()(750 ms). The match is case-sensitive only if the first character is upper case. Setting the control propertyPrefixSelectionFeature.ENABLEDtoPrefixSelectionFeature.CAMEL_CASEmatches camel-case prefixes instead, e.g.COselectsCustomerOrder; removing the property turns the feature off. For shortcuts that don't appear in the displayed text, aNavigationLetterStringConverterdefines per item the letters to navigate by. - Deselection.
DELETEorBACKSPACEclears the selection of a non-editable combo box or a choice box, unless its type is primitive or the component is mandatory.setDeselectAllowed(Boolean)overrides this;nullrestores the automatic decision. - Short and long text. Items implementing
ShortLongText(such as code tables) are shown in two columns in the popup list, the short text and the long text.setShortLongTextEnabled(false)shows the plaintoString()instead. The cell factory is theShortLongTextCellFactory; the RDC replaces it by one that respects the view permission of PDOs.
Themes and dark mode¶
ThemeUtilities is the replaceable
@Service managing the look of the application: the theme, dark mode, the stylesheets applied to each
new scene, and the default font. The base implementation supports JavaFX's own Modena theme;
tentackle-fx-atlanta replaces it by one
supporting the AtlantaFX themes.
ThemeUtilities themes = ThemeUtilities.getInstance();
themes.setTheme(true, "Nord"); // dark mode and theme at once
themes.darkModeProperty().addListener(...); // react on changes
List<String> names = themes.getSupportedThemes();
Changing the theme or the mode re-applies the stylesheets to all scenes. Controllers declared with
@FxControllerService(cssDarkAware = true) load their -dark stylesheet when the view is created in
dark mode; for that reason, the RDC drops its cached dialogs when the mode changes. The user's choice is
persisted in the user preferences with savePreferences(appClass, new ThemePreferences(darkMode, themeName))
and restored with loadPreferences(appClass) and applyThemePreferences(...); a null component of
ThemePreferences means "default" — for dark mode, the setting of the desktop. The RDC's
DesktopApplication does this at startup, also accepting darkMode and themeName as application
properties on the command line. The application generated by the
archetype has a
dialog to change the theme.
Utilities¶
A few more features of the components and of FxUtilities,
the replaceable @Service behind many of them:
- Copy to clipboard.
FxTableViewandFxTreeTableViewcopy the selected cells withCtrl-C— the texts as rendered, one CSV line per row, quoted only where necessary — andFxTableViewwith rows and columns swapped withShift-Ctrl-C; the copied text is shown in a note whileCtrlis held down.setCopyToClipboardEnabled(...)turns it off, or on for anFxListView, which copies the selected items one per line, or as a single comma-separated line withShift-Ctrl-C. - Scrolling.
scrollToCentered(row)orscrollToCentered(object)of table, tree table and list views scroll so that the row is in the middle of the visible rows, not at the edge. - Stage placement.
determinePreferredStageLocation(stage)centers a new stage above its owner and keeps it on the screen; the RDC uses it for its CRUD and search dialogs. - Buttons. Buttons are not focus traversable by default. Clicking one would therefore not move
the focus away from the component the user is editing, so a non-traversable
FxButton(except a cancel button) updates the model of the focused component before firing its action.doClick()clicks a button programmatically, keeping it visually pressed for a moment. - Labels.
FxLabel.setContextMenuEnabled(true)adds a context menu to copy the label's text to the clipboard or to a scratchpad, where parts of it can be selected. - Diagnostics.
dumpComponentHierarchy(node)describes the stage and the chain of containers down to a node, with their controllers and resource bundles — the text logged byCtrl-Shift+ right click, which tells a translator which bundle holds a label.
Binding integration¶
tentackle-fx is the JavaFX implementation of the toolkit-neutral binding
contracts from tentackle-core. The package org.tentackle.fx.bind provides:
FxBindingFactory/DefaultFxBindingFactory— the@Servicefactory;FxComponentBinder/DefaultFxComponentBinder— scans a controller and binds itsFxComponents to model members by name, optionally including inherited components or bindables;FxComponentBinding,FxTableBinder,FxTableBinding— the individual links.
Because the binding moves values in the model's type, the binder uses the
component's ValueTranslator to render and parse, and the component's mandatory
and validation state flows back into the view (the tt-mandatory / tt-error
styles and the error popups). For the full transfer/veto/validation mechanics
see binding.md.
Low-level listeners¶
Besides the binding, a control accepts plain listeners — a low-level alternative for views that are not bound to a model, or for reacting to a change without a model property:
- a
ViewToModelListener(addViewToModelListener) is invoked after the view was transferred to the model, i.e. when the user changed the value; - a
ModelToViewListener(addModelToViewListener) is invoked before the model is transferred to the view.
Registered on a container, the listener fires for the updates of any of its components. By default,
the listeners are invoked on every transfer, even if the value did not change;
setListenerSuppressedIfModelUnchanged(true) and setListenerSuppressedIfViewUnchanged(true) of a
component suppress these invocations.
Changeable forms¶
setChangeable(false) on a container makes all its controls read-only at once — a form in view
mode. A control that must stay usable nevertheless, e.g. a button to open a detail view, is exempted
with setContainerChangeableIgnored(true).
Configurators¶
A Configurator<T> applies cross-cutting setup to every instance of an FX type
T (key handling, focus handling, default behavior). Configurators are stateless,
one per type, and their inheritance hierarchy mirrors the FX class hierarchy, so
a subclass configurator extends the configuration of its superclass via super.configure(...).
FxFactory.getConfigurator(clazz) resolves the configurator registered for the nearest class
along the superclass hierarchy. The builders and Fx.create(...) apply it to each new instance.
Configurators are registered via
@ConfiguratorService and live in org.tentackle.fx.component.config and
org.tentackle.fx.container.config.
Building views without FXML¶
Sometimes building a view programmatically is simpler than maintaining an FXML
file. Set @FxControllerService(view = MyView.class) and the framework creates
the view and controller without FXMLLoader at all. Fields in the view
annotated with @NoFXML are injected into the controller exactly as @FXML
would inject them; the NoFXMLLinker performs the wiring. The controller code
is unchanged, and switching back to FXML is just an annotation change. See
org.tentackle.fx.nofxml.
Annotation processors¶
The org.tentackle.fx.apt package contains compile-time annotation processors
(FxControllerServiceAnnotationProcessor, ValueTranslatorServiceAnnotationProcessor,
TableCellTypeServiceAnnotationProcessor) that verify the annotated classes: the
implemented interface and the constructors the factories rely on. The SPI registration
metadata under META-INF is generated by the tentackle-maven-plugin, so discovery works on
both the module path and the class path.
How It Fits Together¶
A typical screen comes to life like this:
- An
@FxControllerServicecontroller class is discovered byFxFactory. Fx.load(MyController.class)(orFxFactory.createController(...)) locates the FXML, resource bundle, and CSS by convention and loads the view — using the factory'sBuilderFactory, which instantiates Tentackle components (FxTextField,FxComboBox, …) instead of plain JavaFX ones.- As each node is built, the per-type
Configuratorruns against it. - The controller's
@FXMLfields are injected (validateInjections()verifies that none were silently missed). - The controller's
FxComponentBinderscans the components and binds them, by name, to the model's@Bindablemembers; each component gets the rightValueTranslatorfor its model type. Finally, the controller'sconfigure()runs. - At runtime, binding transfers move values both ways in the model's type, running validation and reflecting mandatory/error state through CSS classes and popups.
Package Reference¶
| Package | Role |
|---|---|
org.tentackle.fx |
Core contracts (FxControl, FxComponent, FxContainer, FxController), the Fx facade, FxFactory, ValueTranslator, Configurator, controller base classes, graphic/notification/interactive-error support |
org.tentackle.fx.component |
Extended components: FxTextField, FxTextArea, FxComboBox, FxChoiceBox, FxCheckBox, FxRadioButton, FxToggleButton, FxButton, FxLabel, FxDatePicker, FxColorPicker, FxPasswordField, FxListView, FxTableView, FxTreeView, FxTreeTableView, FxHTMLEditor, Note, AutoCompletionPopup, ExpandedTextPopup |
org.tentackle.fx.component.delegate |
Component delegates holding the behavior (AbstractTextFieldDelegate, per-component delegates; the base classes FxComponentDelegate and FxTextComponentDelegate live in org.tentackle.fx) |
org.tentackle.fx.component.config |
Per-component Configurators |
org.tentackle.fx.component.build |
Component builders used by the FXML builder factory |
org.tentackle.fx.component.auto |
Auto-completion (popup, controller, suggestion generators: camel-case, regex, separator, …) |
org.tentackle.fx.component.skin |
Custom skins: FxTextAreaSkin (expand button), NoteSkin, and the skins of the auto-completion, expanded text and filter popups |
org.tentackle.fx.container |
Extended containers: FxVBox, FxHBox, FxGridPane, FxBorderPane, FxAnchorPane, FxFlowPane, FxTilePane, FxStackPane, FxSplitPane, FxScrollPane, FxTabPane, FxTab, FxTitledPane, FxAccordion, FxToolBar, FxButtonBar, FxDialogPane, FxPane, FxTextFlow |
org.tentackle.fx.container.delegate / .config / .build |
Container delegates, configurators and builders |
org.tentackle.fx.bind |
JavaFX binding implementation (FxComponentBinder, FxComponentBinding, FxTableBinder, FxTableBinding, FxBindingFactory) |
org.tentackle.fx.translate |
Built-in ValueTranslators for numeric, temporal, enum, money, color, I18NText, collection and string types |
org.tentackle.fx.table |
Table configuration and cell-type infrastructure (TableConfiguration, TableColumnConfiguration, TableConfigurationProvider, TableCellType, TableCellTypeFactory, FxTableColumn/FxTableCell and tree variants, TotalsTableView) |
org.tentackle.fx.table.type |
Concrete TableCellTypes per value class |
org.tentackle.fx.table.filter |
Column filters (autofilter): TableFilterSupport, TableFilter, TableColumnFilter, TableFilterPopup, TreeItemFilter |
org.tentackle.fx.nofxml |
Build a view/controller without FXMLLoader (NoFXML, NoFXMLLinker) |
org.tentackle.fx.apt |
Annotation processors validating the contracts of controllers, translators and cell types at compile time (see Annotation Processors) |
org.tentackle.fx.service |
Module hook (Hook) registered as a ModuleHook |
See also: binding.md for the model↔view binding mechanics, validation.md for how mandatory/changeable and validation messages reach the view, and pdo.md for the domain objects most often displayed and edited.