Container editors

A container editor edits a property of an object stored in a WritableValue. The editor reads the property value from the object in the container and writes the new value back to the same object. When you put another object into the container, all editors bound to it show the properties of the new object.

Container editors require an SWT Composite, the parent widget that hosts the editor UI. The parent Composite is passed to the builder with the .parent(parent) method.

Add these bundles to Require-Bundle in the MANIFEST.MF of your plug-in:

com.amalgamasimulation.desktop.ui.editor
com.amalgamasimulation.desktop.binding
org.eclipse.core.databinding.observable
org.eclipse.swt
The complete source code for every example in this article can be found in the GitHub repository

1. How an editor works

Each editor is a section: a row with a label, an input control, and optional buttons.

Section view

Apart from that, container editors work like standalone editors. They are created by the same factory methods of the Sections class and use the same validation strategies, buttons, and properties. This article describes only the differences.

To add an editor:

  1. Call one of the factory methods of the Sections class: stringEditor(), integerEditor(), doubleEditor(), comboBoxEditor() and others. Each method accepts a WritableValue that holds the object being edited and returns a builder.

  2. Configure the editor step by step with the returned builder: set the parent Composite, the valueExtractor() function that reads the value from the object, the label, the validation strategy, and the value changed handler; optionally, add buttons and tooltips or set other properties.

  3. Call .create() to build and display the editor.

The section handles the value as follows:

  1. When the section is created, it reads the value of the object in the container and displays it.

  2. When the user presses Enter or the field loses focus, the editor validates the input.

  3. If the value is invalid, the editor shows an error icon, does not call the value changed handler, and restores the current value.

  4. If the value is valid but unchanged, the handler is not called.

  5. If the value is valid and different, the handler is called with the new value.

1.1. Refreshing an editor

When another object is put into the WritableValue, the section refreshes itself: it reads the value of the new object and recalculates the label, visibility, the enabled state and the buttons. The section does not track changes inside the object. When your code changes a value of the object in the container or a condition used by visible() or enabled(), refresh the section:

Method When to call

updateSection()

The value or the label text has changed. Also refreshes visibility, the enabled state and the buttons.

updateVisible()

Only the condition passed to visible() has changed.

updateEnabled()

Only the condition passed to enabled() has changed.

create() returns the section; store it in a field to refresh it later. In the following example, the total row displays a value computed from the object in the container and is refreshed by the handler of the quantity field:

WritableValue<Order> order = new WritableValue<>(new Order(2, 12.5), Order.class); // Order has a quantity and a price
ICustomSection totalSection;

Sections
	.integerEditor(order)
	.parent(parent)
	.valueExtractor(Order::getQuantity)
	.label("Quantity")
	.validationStrategy(ValidationStrategies.integerPositive())
	.handler((o, newValue) -> { o.setQuantity(newValue); totalSection.updateSection(); }) // the object in the container changed
	.create();

totalSection = Sections
	.customSectionBuilder(parent)
	.addLabel(LabelProperties.label().text("Total"))
	.addLabel(LabelProperties.label().text(() -> Formats.getDefaultFormats().customDecimals(order.getValue().getTotal(), 2)).font(boldFont)) // the text is a supplier
	.createSection();
Refreshing an editor

The examples in this article edit Truck objects. Any plain Java object with getters and setters works:

public class Truck {
	private String name;
	private double capacity;
	private boolean active;
	private Driver driver;

	public String getName() { return name; }
	public void setName(String name) { this.name = name; }
	public double getCapacity() { return capacity; }
	public void setCapacity(double capacity) { this.capacity = capacity; }
	public boolean isActive() { return active; }
	public void setActive(boolean active) { this.active = active; }
	public Driver getDriver() { return driver; }
	public void setDriver(Driver driver) { this.driver = driver; }
}

public class Driver {
	private String name;

	public String getName() { return name; }
	public void setName(String name) { this.name = name; }
}
List<Truck> trucks = ...; // the trucks to edit
List<Driver> drivers = ...; // the drivers to choose from
WritableValue<Truck> selectedTruck = new WritableValue<>(); // holds the object being edited

The examples below use these variables.

2. Switching the edited object

Unlike standalone editors, a form of container editors can edit different objects of the same type, one at a time. To switch, put another object into the container.

The form below edits the Truck selected by the radio buttons. The parent is any Composite with a GridLayout, such as a dialog area. The editors are created once, and the radio buttons set the selected Truck as the container’s value:

for (Truck truck : trucks) {
	Button radio = new Button(parent, SWT.RADIO);
	radio.setText(truck.getName());
	radio.addListener(SWT.Selection, e -> {
		if (radio.getSelection()) {
			selectedTruck.setValue(truck); // editors show the selected truck
		}
	});
}

SectionGroup group = new SectionGroup(parent, SWT.NONE); // aligns the labels

Sections
	.stringEditor(selectedTruck)
	.parent(group)
	.valueExtractor(Truck::getName)
	.label("Name")
	.validationStrategy(ValidationStrategies.stringIsNotEmpty())
	.handler(Truck::setName)
	.create();

Sections
	.doubleEditor(selectedTruck)
	.parent(group)
	.valueExtractor(Truck::getCapacity)
	.label("Capacity")
	.validationStrategy(ValidationStrategies.doublePositive())
	.handler(Truck::setCapacity)
	.create();
Editing the selected object

The handler writes the new value to the object as soon as the user commits it by pressing Enter or leaving the field. When the user selects the same Truck again, the editor shows the changed value. SectionGroup is described in the standalone editors article.

While the container holds null, for example before the user selects a Truck, the editors show empty fields, and anything typed into them is not saved.

3. Editors

Each editor’s configuration chain starts with a factory method of the Sections class that accepts the WritableValue with the object being edited. The table lists the required steps and settings of each editor. The steps are the same as for standalone editors, plus valueExtractor(). label(), visible() and enabled() also accept a function of the object in the container.

Editor Required steps Input settings Common settings

String
Double
Integer

parent()
valueExtractor()
label()
validationStrategy()
handler()

textProperties()

visible()
enabled()
toolTip()
labelProperties()
addButton()

ComboBox

parent()
valueExtractor()
label()
elements()
format()
handler()

comboBoxProperties()

canBeNull()
filter()
comparator()
visible()
enabled()
toolTip()
labelProperties()
addButton()

Autocomplete

parent()
valueExtractor()
label()
elements()
format()
handler()

textProperties()

canBeNull()
filter()
comparator()
visible()
enabled()
toolTip()
labelProperties()
addButton()

Enum

parent()
valueExtractor()
label()
elements()
format()
handler()

comboBoxProperties()

visible()
enabled()
toolTip()
labelProperties()
addButton()

CheckBox

parent()
valueExtractor()
label()
handler()

visible()
enabled()
toolTip()
labelProperties()

LocalDateTime
LocalTime
Color
Real distribution
Time Series

parent()
valueExtractor()
label()
handler()

textProperties()

canBeNull()
withoutDefaultButton()
validationStrategy()
visible()
enabled()
toolTip()
labelProperties()
addButton()

The combobox, autocomplete and enum editors need both types in the call, for example Sections.<Truck, Driver>comboBoxEditor(selectedTruck).

Creating a double editor for the capacity of the selected Truck:

Sections
	.doubleEditor(selectedTruck) // the container with the object being edited
	.parent(parent)
	.valueExtractor(Truck::getCapacity) // reads the value from the object
	.label("Capacity")
	.validationStrategy(ValidationStrategies.doublePositive())
	.handler(Truck::setCapacity) // gets the object and the new value
	.create();

handler() accepts a BiConsumer of the object in the container and the new value, so a setter reference fits here. label(), enabled() and visible() can also take a function of the object, see Fine-tuning.

If the user input does not pass the validation strategy, the value is not written to the object, and the input control returns to the current value when the user leaves it.

Each editor below has the same settings as its standalone counterpart.

3.1. Text-based editors

3.1.1. Double editor

Sections.doubleEditor(WritableValue<T>) edits a Double value. The call chain is shown in the example above. Validation strategies are listed in the validation strategies table of the standalone editors article. Other settings are described in the standalone double editor.

3.1.2. Integer editor

Sections.integerEditor(WritableValue<T>) edits an Integer value. Validation strategies are listed in the validation strategies table of the standalone editors article. Other settings are described in the standalone integer editor.

3.1.3. String editor

Sections.stringEditor(WritableValue<T>) edits a String value. Validation strategies are listed in the validation strategies table of the standalone editors article. Other settings are described in the standalone string editor.

3.2. ComboBox editor

Sections.comboBoxEditor(WritableValue<T>) selects the value from a list. Java cannot infer the element type from the container, so explicitly pass both the type of the container object and the type of the element being edited. The format function accepts the object from the container and a list element, and returns the text shown for that element:

Sections
	.<Truck, Driver>comboBoxEditor(selectedTruck) // the object type and the element type
	.parent(parent)
	.valueExtractor(Truck::getDriver)
	.label("Driver")
	.elements(drivers)
	.format((truck, driver) -> driver.getName()) // gets the object and the element
	.handler(Truck::setDriver)
	.create();

See the standalone combobox editor and its settings for filtering, sorting and canBeNull().

3.3. Autocomplete editor

Sections.autoCompleteEditor(WritableValue<T>) is set up like the combobox editor: explicit types, elements(List) and a format function of the object and the element. The rest is described in the standalone autocomplete editor.

3.4. Enum editor

Sections.enumEditor(WritableValue<T>) also needs explicit types. It takes an array in elements() and a format function of the object and the element. See the standalone enum editor.

3.5. CheckBox editor

Sections.checkBoxEditor(WritableValue<T>) edits a boolean value with a checkbox. It has no validation strategy. See the standalone checkbox editor.

3.6. LocalDateTime editor

Sections.localDateTimeEditor(WritableValue<T>) edits a LocalDateTime value. date(), newLocalDateTime(), withoutDefaultButton() and canBeNull() work as in the standalone LocalDateTime editor.

3.7. LocalTime editor

Sections.localTimeEditor(WritableValue<T>) edits a LocalTime value. newLocalTime(), withoutDefaultButton() and canBeNull() work as in the standalone LocalTime editor.

3.8. Color editor

Sections.colorEditor(WritableValue<T>) edits a color value. withoutDefaultButton() and canBeNull() work as in the standalone color editor.

3.9. Real distribution editor

Sections.realDistributionEditor(WritableValue<T>) edits a RealDistribution value. withoutDefaultButton() and canBeNull() work as in the standalone real distribution editor.

3.10. Time Series editor

Sections.timeSeriesEditor(WritableValue<T>) edits a TimeSeries value. withoutDefaultButton() and canBeNull() work as in the standalone time series editor.

4. Fine-tuning

label(), enabled() and visible() accept a function of the object in the container:

ITextSection<Double> capacitySection = Sections
	.doubleEditor(selectedTruck)
	.parent(parent)
	.valueExtractor(Truck::getCapacity)
	.label(truck -> truck == null ? "Capacity" : "Capacity of " + truck.getName())
	.validationStrategy(ValidationStrategies.doublePositive())
	.handler(Truck::setCapacity)
	.enabled(truck -> truck != null && truck.isActive()) // the field is enabled for active trucks only
	.create(); // returns the section

While the container is empty, the functions receive null.

toolTip(), labelProperties(), textProperties(), comboBoxProperties() and addButton() work as described in the Fine-tuning chapter of the standalone editors article.

5. Buttons

Besides the buttons described in the standalone editors article, two new button types are described below.

5.1. Button with your own action

Buttons.containerButton() creates a button with your own action, like Buttons.simpleButton() in standalone editors, but the action also receives the object in the container. The action() method accepts the SWT event, the object in the container and the current value:

Sections
	.doubleEditor(selectedTruck)
	.parent(parent)
	.valueExtractor(Truck::getCapacity)
	.label("Capacity")
	.validationStrategy(ValidationStrategies.doublePositive())
	.handler(Truck::setCapacity)
	.addButton(Buttons.<Truck, Double>containerButton() // the action gets the object and the value
		.action((event, truck, value) -> {
			MessageBox messageBox = new MessageBox(parent.getShell(), SWT.ICON_INFORMATION);
			messageBox.setMessage("Capacity of " + truck.getName() + " = " + value);
			messageBox.open();
		}))
	.create();
Custom button

5.2. Element selection button

Buttons.selectElementFromTable() creates a button that opens a dialog with a table of elements. The element the user selects becomes the new value of the editor:

Sections
	.<Truck, Driver>comboBoxEditor(selectedTruck)
	.parent(parent)
	.valueExtractor(Truck::getDriver)
	.label("Driver")
	.elements(drivers)
	.format((truck, driver) -> driver.getName())
	.handler(Truck::setDriver)
	.addButton(Buttons.<Truck, Driver>selectElementFromTable()
		.title("driver") // shown in the dialog title
		.values(() -> drivers) // elements in the table
		.columns(table -> table.column(Driver::getName).name("Name").width(200))) // table columns
	.create();
Select element from table

filter() and comparator() limit and sort the elements in the table. customAction((truck, driver) → …​) replaces the call to the editor’s handler when the user selects an element. The other button settings are described in the standalone editors article.

6. Common mistakes

Container editors share settings and buttons with standalone editors, so also check the common mistakes of standalone editors. The table below lists the mistakes specific to container editors.

Symptom Cause Fix

The fields are empty, and typed values are not saved.

The container holds null, for example new WritableValue<>() before the user selects an object.

Put an object into the container with setValue() or pass it to the WritableValue constructor.

The field still shows the old value after your code changes the object.

A section reacts only when another object is put into the container.

Call updateSection() on the section.

The fields do not change after the same object is put into the container again.

WritableValue notifies the editors only when the new object differs from the current one.

Call updateSection() on the sections.

The editors ignore new objects.

The code assigns a new WritableValue to the variable instead of calling setValue(), and the editors stay bound to the container passed to the factory method.

Keep one container and call setValue() on it.

NullPointerException in the function passed to label(), visible() or enabled().

While the container is empty, the functions receive null.

Check for null: truck → truck != null && truck.isActive().

NullPointerException in the action of Buttons.containerButton().

While the container is empty, the action receives null as the object and the value.

Check the object for null in the action.

A custom section keeps showing the old object after another object is put into the container.

Sections.customSectionBuilder() is not bound to the container.

Refresh it from a listener: selectedTruck.addValueChangeListener(e → section.updateSection()).

Compilation error List<Driver> cannot be converted to List<Object> in elements().

Java cannot infer the element type of the combobox, autocomplete or enum editor from the container.

Pass both types: Sections.<Truck, Driver>comboBoxEditor(selectedTruck).

The value does not change after the user selects an element in the dialog of Buttons.selectElementFromTable().

The button has customAction(), which is called instead of the handler of the editor, and it does not write the selected element.

Write the element in customAction() or remove customAction().

The dialog of Buttons.selectElementFromTable() shows an empty table.

The button has no values(), which sets the elements shown in the table.

Pass the elements: values(() → drivers).