A Java library to facilitate building user interfaces for configuring algorithms.
Config-UI provides a declarative way to define algorithm parameters, and automatically generates Swing-based configuration dialogs from a single configuration class.
This library was initially developed to support the extension of TrackMate, a Fiji plugin for tracking particles in microscopy images. TrackMate allows third-party developers to contribute modules for spot detection, object segmentation, and tracking algorithms, each requiring custom configuration parameters.
The development bottleneck was consistently the creation of custom Swing configuration panels. Config-UI automates this step: define your parameters once, and the library generates the UI, handles serialization, and provides utilities for validation and persistence.
The library is built around two core concepts:
Configurator– A base class you extend to declare your algorithm's parameters using a fluent builder API.ParameterVisitors – Utilities that consume aConfiguratorto produce different outputs: Swing GUIs, JSON files, maps, string representations, or persistent preferences.
Add the following dependency to your pom.xml:
<dependency>
<groupId>org.scijava</groupId>
<artifactId>config-ui</artifactId>
<version>0.0.1</version>
</dependency>The library depends on:
- SciJava commons – For
CancelableandPreviewableinterfaces - Gson – For JSON serialization
- FontChooser – For font selection dialogs
Extend Configurator and declare parameters in the constructor:
import org.scijava.ui.config.Configurator;
import org.scijava.ui.config.Parameters.*;
public class MyAlgorithmConfig extends Configurator {
public final DoubleParam threshold;
public final IntParam maxIterations;
public final BooleanParam useAdvancedMode;
public final ChoiceParam method;
public MyAlgorithmConfig() {
super("My Algorithm", "Configure the parameters for my algorithm.");
// Bounded double parameter with slider
this.threshold = addDoubleParameter()
.key("THRESHOLD")
.name("Threshold")
.help("The detection threshold. Higher values are more strict.")
.defaultValue(0.5)
.min(0.0)
.max(1.0)
.get();
// Integer parameter with bounds
this.maxIterations = addIntParameter()
.key("MAX_ITER")
.name("Max iterations")
.help("Maximum number of iterations.")
.defaultValue(100)
.min(1)
.max(1000)
.units("iterations")
.get();
// Boolean flag
this.useAdvancedMode = addBooleanParameter()
.key("ADVANCED")
.name("Advanced mode")
.help("Enable advanced processing options.")
.defaultValue(false)
.get();
// Choice from discrete values
this.method = addChoiceParameter()
.key("METHOD")
.name("Processing method")
.help("Select the algorithm to use.")
.addChoice("FAST", "Fast but less accurate")
.addChoice("ACCURATE", "Slower but more accurate")
.addChoice("BALANCED", "A balance between speed and accuracy")
.defaultValue("BALANCED")
.get();
}
}import org.scijava.ui.config.visitors.gui.FrameBuilder;
import org.scijava.ui.config.visitors.gui.FrameBuilder.UserTask;
import org.scijava.ui.config.visitors.gui.FrameBuilder.ConfigFrame;
// Create config and default values instances
MyAlgorithmConfig config = new MyAlgorithmConfig();
MyAlgorithmConfig defaultValues = new MyAlgorithmConfig();
// Define the task to execute
UserTask task = progress -> {
progress.indeterminate(false, "Processing...");
for (int i = 0; i <= config.maxIterations.getValue(); i++) {
if (progress.isCanceled()) return;
// Do work...
progress.set((double) i / config.maxIterations.getValue(), "Step " + i);
Thread.sleep(50);
}
progress.message("Done!");
};
// Build and show the frame
ConfigFrame frame = FrameBuilder.build(config, task, defaultValues);
frame.setVisible(true);| Type | Builder Method | Description |
|---|---|---|
BooleanParam |
addBooleanParameter() |
A checkbox flag |
IntParam |
addIntParameter() |
Integer with optional min/max bounds (uses slider if bounded, spinner otherwise) |
DoubleParam |
addDoubleParameter() |
Double with optional min/max bounds (uses slider if bounded, text field otherwise) |
StringParam |
addStringParameter() |
Free text input |
PathParam |
addPathParameter() |
File or directory path (with browse button in GUI) |
ChoiceParam |
addChoiceParameter() |
Dropdown selection from discrete choices |
EnumParam<E> |
addEnumParameter(Class<E>) |
Dropdown selection from Java enum values |
All parameter builders support these methods:
| Method | Description |
|---|---|
.key(String) |
Required. Unique identifier for serialization and map conversion |
.name(String) |
Display name in the UI |
.help(String) |
Help text shown via the help button (can be a URL) |
.defaultValue(T) |
Default value when not set |
.units(String) |
Physical units (displayed next to the input in GUI) |
.visible(boolean) |
Whether to show in GUI (default: true) |
.updateListener(UpdateListener) |
Callback when value changes |
Bounded parameters (IntParam, DoubleParam) additionally support:
| Method | Description |
|---|---|
.min(T) |
Minimum allowed value |
.max(T) |
Maximum allowed value |
The addChoiceParameter() builder accepts pairs of values:
.addChoice("FAST", "Fast but less accurate")- First argument (
"FAST") – The internal value stored in the parameter and used in serialization - Second argument (
"Fast but less accurate") – The human-readable string displayed in the GUI dropdown
If only one argument is provided, it is used for both:
.addChoice("Simple Choice")For simple use cases where you only need the parameter panel:
import org.scijava.ui.config.visitors.gui.GuiBuilder;
import org.scijava.ui.config.visitors.gui.GuiBuilder.ConfigPanel;
ConfigPanel panel = GuiBuilder.build(config);
JFrame frame = new JFrame("Algorithm Configuration");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(panel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);The gray ? buttons will display the help text of a parameter when clicked.
FrameBuilder creates a full-featured dialog with action buttons, but you need to provide a UserTask that will be executed when the user presses the run button:
import org.scijava.ui.config.visitors.gui.FrameBuilder;
class MyTask implements UserTask
{
/* We will use it later in the cancelable example. */
protected final AtomicBoolean cancelRequested = new AtomicBoolean( false );
@Override
public void run( final Progress progress ) throws Exception
{
cancelRequested.set( false );
progress.indeterminate( false, "Processing..." );
for ( int i = 0; i <= config.maxIterations.getValue(); i++ )
{
if ( cancelRequested.get() )
{
System.out.println( "I have been canceled!" );
return;
}
// Do work...
progress.set( ( double ) i / config.maxIterations.getValue(), "Step " + i );
Thread.sleep( 50 );
}
progress.message( "Done!" );
}
};
ConfigFrame frame = FrameBuilder.build(config, task, defaultValues);
frame.setVisible(true);The frame includes:
- Run button
– Executes the task in a background thread with progress reporting - Stop button
– Appears during execution for cancelable tasks, and calls the cancelation method (see below) - Preview button
– For previewable tasks (quick preview without full execution) - Store button
– Saves configuration to preferences - Reload button
– Loads configuration from preferences - Reset button
– Restores default values - Help button
– Opens the help URL or shows help text - Text button
- Outputs the current config to text - A Progress bar – Shows task progress with status messages
If you provide a user task that implements Cancelable and/or Previewable, new buttons and behaviors will appear on the UI:
import org.scijava.Cancelable;
import org.scijava.command.Previewable;
import java.util.concurrent.atomic.AtomicBoolean;
class MyCancelableAndPreviewableTask extends MyTask implements Cancelable, Previewable
{
@Override
public void preview()
{
// Implement preview logic here, e.g., update a preview display
// based on current config values.
System.out.println( "Previewing with threshold: " + config.threshold.getValue() +
", max iterations: " + config.maxIterations.getValue() +
", advanced mode: " + config.useAdvancedMode.getValue() +
", method: " + config.method.getValue() );
}
@Override
public void cancel( final String reason )
{
cancelRequested.set( true );
}
@Override
public boolean isCanceled()
{
return cancelRequested.get();
}
@Override
public void cancel()
{
System.out.println( "Canceling preview" );
}
@Override
public String getCancelReason()
{
return "User requested cancellation.";
}
}Organize parameters into collapsible groups for cleaner UIs:
public class MyAdvancedConfig extends MyAlgorithmConfig {
public final BooleanParam enableLogging;
public final IntParam logLevel;
public MyAdvancedConfig() {
super("My Advanced Algorithm", "Configure with logging options.");
this.enableLogging = addBooleanParameter()
.key("ENABLE_LOGGING")
.name("Enable Logging")
.help("Enable detailed logging.")
.defaultValue(false)
.get();
this.logLevel = addIntParameter()
.key("LOG_LEVEL")
.name("Log Level")
.help("Set the logging level (0-5).")
.defaultValue(3)
.min(0)
.max(5)
.get();
// Create a collapsible group
addGroup("Logging Options")
.add(enableLogging)
.add(logLevel)
.collapsed(false) // Start expanded
.get();
}
}Use SelectableParameters to create radio-button groups where only one option can be active at a time:
public class MyModelConfig extends Configurator {
public final EnumParam<ModelType> builtinModel;
public final PathParam customModelPath;
public final SelectableParameters modelSelection;
enum ModelType {
MODEL_A, MODEL_B, MODEL_C
}
public MyModelConfig() {
super("Model Selection", "Choose a model to use.");
// Step 1: Define the parameters
this.builtinModel = addEnumParameter(ModelType.class)
.key("BUILTIN_MODEL")
.name("Built-in model")
.get();
this.customModelPath = addPathParameter()
.key("CUSTOM_PATH")
.name("Custom model path")
.get();
// Step 2: Group them as mutually exclusive
this.modelSelection = addSelectableParameters()
.key("MODEL_SOURCE")
.add(builtinModel)
.add(customModelPath)
.get();
}
}In the GUI, this creates radio buttons. Only the selected parameter's value is used when iterating over parameters or serializing.
Transform values for display while storing the original internally. This is useful for showing physical units when the parameter stores pixels:
public class MyMeasurementConfig extends Configurator {
public final DoubleParam diameter;
public MyMeasurementConfig() {
super("Measurement Config", "Configure measurements.");
this.diameter = addDoubleParameter()
.key("DIAMETER")
.name("Diameter")
.units("µm")
.defaultValue(30.0)
.get();
// Store pixels internally, display in physical units
double pixelSize = 0.2; // µm/pixel
setDisplayTranslator(
diameter,
v -> v * pixelSize, // Display: pixels -> µm
v -> v / pixelSize // Store: µm -> pixels
);
}
}When the user enters 13 µm, the parameter internally stores 65.0 (pixels). The table representation shows the translated value:
┌────────────────────────────┐
│ My translated algorithm │
├────────────────────────────┤
│Threshold │ 0.5 │
│Max iterations │ 100 │
│Advanced mode │ false │
│Processing method │ BALANCED│
│Diameter │ 65.0 │ <- properly stored in pixel units
└────────────────────────────┘
The visitors package provides utilities to consume configurations in different ways:
Save and load configurations to/from JSON files:
import org.scijava.ui.config.visitors.JSon;
// Save to file
JSon.serialize("/path/to/config.json", config);
// Load from file
JSon.deserialize("/path/to/config.json", config);
// Get as JSON string
String json = JSon.toJson(config);JSON format:
{
"MyAlgorithmConfig": {
"THRESHOLD": 0.5,
"MAX_ITER": 100,
"ADVANCED": false,
"METHOD": "BALANCED"
}
}Convert to/from a Map<String, Object> for programmatic manipulation:
import org.scijava.ui.config.visitors.Maps;
// Config → Map
Map<String, Object> map = Maps.toMap(config);
// Map → Config (populates existing instance)
Maps.fromMap(map, config);
// Modify values programmatically
map.put("THRESHOLD", 0.75);
Maps.fromMap(map, config); // config now has threshold = 0.75Pretty-print configurations for logging or debugging:
import org.scijava.ui.config.visitors.Strings;
// Simple list format
String list = Strings.toString(config);
// Output:
// My Algorithm:
// Threshold=0.5
// Max iterations=100
// ...
// Boxed table format (recommended)
String table = Strings.echo(config);
// Output:
// ┌───────────────────────────┐
// │ My Algorithm │
// ├───────────────────────────┤
// │ Threshold │ 0.5 │
// │ Max iterations │ 100 │
// │ Advanced mode │ false │
// │ Processing method│BALANCED│
// └───────────────────────────┘Save and reload configurations from user preferences:
import org.scijava.ui.config.visitors.Prefs;
// Store in preferences
Prefs.serialize(config);
// Reload from preferences
Prefs.deserialize(config);Configurator (your class)
│
├── Parameter<T, O> – Base type for all parameters
│ ├── BooleanParam
│ ├── IntParam
│ ├── DoubleParam
│ ├── StringParam
│ ├── PathParam
│ ├── ChoiceParam
│ └── EnumParam<E>
│
└── ParameterVisitor – Consumes parameters
├── GuiBuilder – Swing UI generation
│ └── FrameBuilder – Complete dialog with buttons
├── JSon – JSON (de)serialization
├── Maps – Map conversion
├── Strings – Text representation
└── Prefs – Preferences storage
See the demo classes in src/test/java:
DemoSimple.java– Minimal example with a single integer parameterDemoDoc.java– Example used in this READMEDemo.java– Full-featured Cellpose 3 configuration (real-world example)Cellpose3Config.java– Example standalone configuration class for Cellpose 3
BSD-3-Clause. See the LICENSE file for details.
Jean-Yves Tinevez, based on what Tobias Pietzsch taught me while working on Mastodon, and code he created there (notably the StyleElements).





