What is this?
swing-extras is a library of components for Java Swing applications. This collection has been in development
since around 2012, but was not publicly available until 2025. This documentation guide covers the possibilities
that swing-extras offers that allow you to quickly and easily add useful functionality to your Java Swing
applications.
This guide covers version 2.4.0 of swing-extras from 2025-09-01
The library jar includes a built-in demo application that offers a brief preview of some of the features and
components of swing-extras:

How do I get it?
swing-extras is in the Maven central repository. So, you can simply list it as a dependency:
<dependencies>
<dependency>
<groupId>ca.corbett</groupId>
<artifactId>swing-extras</artifactId>
<version>2.4.0</version>
</dependency>
</dependencies>
At the time of this writing, 2.4.0 is the latest version. You can verify the latest available
version on the GitHub project page:
- https://github.com/scorbo2/swing-extras
- Browse the javadocs online
- Version history and release notes
License
swing-extras is made available under the MIT license. This means that you can do as you wish with the source code, provided the copyright notices remain intact. Have fun!
Suggestions and bug reports
Bug reports and feature requests are submitted via GitHub issues. Feel free to create a ticket there!
swing-forms
The swing-forms library was initially developed and maintained as a separate library, but
has been absorbed into swing-extras as of the 2.0.0 release, to make maintenance and
extension much easier.
swing-forms is greatly useful in saving you from having to write a lot of manual layout
code for input forms, particularly with GridBagLayout, which can be complicated and
tedious to use. With swing-forms, you can very quickly stand up an input form with
optional validation rules and with optional actions attached to each form field. You can
also easily extend swing-forms by writing your own custom form field implementations.
What can I do with swing-forms?
The swing-forms library wraps most of the common input components into easy-to-use wrapper classes and hides much of the complexity of using them. Included example form field implementations are:
- Checkbox
- Color picker (with support for solid colors and for color gradients)
- Combo box (editable and non-editable variants)
- File chooser
- Directory chooser
- Static label fields
- Number pickers (spinners)
- Text input fields (single-line and multiline supported)
- Multi-select list fields
- Panel fields (for rendering custom stuff)
In the next few sections, we'll take a tour of the features provided by swing-forms, and we
will be making use of these features in most of the rest of this documentation, because many
of the other features included in swing-extras build on top of swing-forms.
swing-forms: the basics

Most of the common Swing components are wrapped in swing-forms so that you can very quickly stand
up an input form without writing any UI code. In the screenshot above, we can see examples of some
of the more basic form fields. This requires comparatively MUCH less code than trying to lay everything
out with GridBagLayout.
Example code for a simple text input
The code to generate a form with a simple text input field is quite simple:
FormPanel formPanel = new FormPanel();
formPanel.add(new ShortTextField("Label:", 15));
The ShortTextField constructor takes the following parameters:
- Text to show in the field label
- Number of character columns for the text field (field width)
Optionally, you can set some extra parameters at creation time:
FormPanel formPanel = new FormPanel();
formPanel.add(new ShortTextField("Label:", 15)
.setAllowBlank(false) // field will reject empty values when validating
.setText("initial text") // set an initial value for the field
);
Most FormField implementing classes use fluent-style setter methods to
allow easy method chaining as shown above. Of course, you can also initialize
a field in a non-fluent fashion. The below code is equivalent to the above code:
FormPanel formPanel = new FormPanel();
ShortTextField shortTextField = new ShortTextField("Label:", 15);
shortTextField.setAllowBlank(false);
shortTextField.setText("initial text");
formPanel.add(shortTextField);
However you create the ShortTextField, it can then be added to a FormPanel via the add() method. And you don't have to write any manual layout code!
Retrieving field values
Of course, just adding a new field to a FormPanel won't help you retrieve the
value of it once the form is submitted. For this purpose, you can either add an
action to the form field using addValueChangedListener() to listen for updates
on the text field itself, or you can query for the field later by giving it
a unique identifier.
Listening for change events on the field
Let's say we want to receive updates as the user enters text on the field.
We can do this by supplying a custom ValueChangedListener to the field:
shortTextField.addValueChangedListener(new ValueChangedListener() {
@Override
public void formFieldValueChanged(FormField field) {
String currentValue = ((ShortTextField)field).getText();
// Do something with the current value...
}
});
The drawback of this approach is that our action will be triggered every time the user adds, deletes, or edits text within the field, even before the form has been submitted. We likely don't want to do this (although there are many scenarios where listening to the form field for changes can be useful, as we will cover in the Actions section later). What we likely want to do instead is to retrieve the value of the text field AFTER the user has hit the OK or Submit button.
Retrieving a specific field from a FormPanel
We do this by setting a unique identifier for the field when we create it. We can then
query the FormPanel for this field later:
formPanel.add(new ShortTextField("Enter some text:", 12)
.setIdentifier("textField1"));
Then, later, when the form is submitted, we can find that field:
ShortTextField textField = (ShortTextField)formPanel.getFormField("textField1");
String value = textField.getText();
// do something with the value
There's an even easier way...
Later, when we discuss Properties, we'll find that there's an even easier
way of retrieving the values from submitted forms, without having to talk to the FormPanel or
any of the form fields directly.
But first, let's continue covering the basics provided by swing-forms...
Form validation
Form validation is optional, but is very easy to do with swing-forms. Often you want
to restrict certain fields so that they only allow certain values, or so that the
values in one field are only valid if some value in some other field is within a
certain range, etc. These rules are very easy to apply in swing-forms.
In the previous section, we saw a code example of one possible built-in
validation available with text fields, and that is the setAllowBlank() method. If you
tell the ShortTextField not to allow blank values, then it will automatically add
a FieldValidator with that rule to itself. But you are not limited to built-in
validation capabilities with these fields! You can add new validation rules
very easily. First, let's use the built-in validation rule to tell the ShortTextField
to not allow blank values:
ShortTextField textField = new TextField("Can't be blank:", 15);
textField.setAllowBlank(false);
What happens now when we try to validate the form with a blank value in that field?

We see that the field fails validation, and we get a helpful tooltip message from the red validation marker. The ShortTextField itself added that FieldValidator on our behalf because of the way we instantiated it.
But what if we want to add custom validation? This is quite easy!
textField.addFieldValidator(new FieldValidator<ShortTextField>() {
@Override
public ValidationResult validate(ShortTextField fieldToValidate) {
if (fieldToValidate.getText().length() < 3) {
return ValidationResult.invalid("Text must be at least three characters!");
}
return ValidationResult.valid();
}
});

Now we see our custom validation message is triggered if our own validation logic determines that the field is invalid.
FormFields can have multiple FieldValidators attached to them. In this case, all FieldValidators must report that the field is valid, otherwise the field will be marked as invalid. If more than one FieldValidator reports a validation failure, the validation messages will be concatenated together, like this:

Here we see both the built-in FieldValidator and our custom FieldValidator have both reported a validation failure for the field in question. In this particular case, the two messages are redundant. But, you can see how easy it is to apply multiple validation rules to a FormField and have the FormPanel itself manage validating each field and displaying messages as appropriate.
Validating a form
FormPanel offers two equivalent methods for form validation:
isFormValid()will validate the form and return a boolean indicating validation success.validateForm()will simply validate the form and return nothing.
Both of these methods will cause the validation success (green checkmark) or failure (X marker) to appear beside each validatable field. Note that some fields, like LabelField or CheckBoxField, do not subject themselves to validation by default, as their contents are quite simple.
Manually enabling or disabling validation on a field
All FormField implementations have a hasValidationLabel() method which (usually)
returns true. Some fields, for example LabelField, will return false from this
method unless one or more FieldValidators have been added to it. This is because
labels do not allow user input, so it (usually) does not make sense to validate them.
Let's try overriding this default behavior by adding a FieldValidator to a LabelField:
LabelField labelField = new LabelField("Labels usually don't validate...");
labelField.addFieldValidator(new FieldValidator<LabelField>(labelField) {
@Override
public ValidationResult validate(LabelField fieldToValidate) {
if (!fieldToValidate.getText().isBlank()) {
return ValidationResult.invalid("How dare you have a label with text!");
}
return ValidationResult.valid();
}
});
When we validate the form, we see that the label has allowed itself to be validated:

Later, when we look at custom FormField implementations, we can think
about whether it makes sense for our new custom field to respond to validation or not.
But for the built-in FormField implementations that come with swing-forms, the
default behavior is usually what you want. You can override the default behavior
as shown above if you need to.
Form field actions
Often, it is useful to be able to perform some Action when the value in a form field changes. For example, to show or hide other form fields depending on the value in a combo box, or to perform some additional logic as soon as a field value changes. This is also quite easy to achieve in swing-forms!
Let's start by defining a ComboField that has some basic options:
List<String> options = new ArrayList<>();
options.add("This option has no extra settings");
options.add("This option has 1 extra setting");
options.add("This option has lot of extra settings");
ComboField<String> comboField = new ComboField<>("Show/hide extra fields:", options, 0);
formPanel.add(comboField);
Then we can define some extra fields and hide them by default:
CheckBoxField extraField1 = new CheckBoxField("Extra setting", false);
extraField1.setVisible(false);
formPanel.add(extraField1);
ShortTextField extraField2 = new ShortTextField("Extra text field 1:", 10);
extraField2.setVisible(false);
formPanel.add(extraField2);
ShortTextField extraField3 = new ShortTextField("Extra text field 2:", 10);
extraField3.setVisible(false);
formPanel.add(extraField3);
ShortTextField extraField4 = new ShortTextField("Extra text field 3:", 10);
extraField4.setVisible(false);
formPanel.add(extraField4);
Now, we can add a custom Action onto our ComboField to show or hide the extra fields depending on which combo option is selected:
comboField.addValueChangedListener(new ValueChangedListener() {
@Override
public void formFieldValueChanged(FormField field){
int selectedIndex = ((ComboField)field).getSelectedIndex();
extraField1.setVisible(selectedIndex == 1);
extraField2.setVisible(selectedIndex == 2);
extraField3.setVisible(selectedIndex == 2);
extraField4.setVisible(selectedIndex == 2);
}
});
The end result is that the "extra" fields will be shown or hidden as needed at runtime, based on what you pick in the dropdown:

Above we see the difference between selecting the different combo options. The "extra" fields that we have defined appear or disappear based on our combo box selection. We can simply use the FormField's setVisible() method in our custom Action to accomplish this!
Other uses for custom actions
We can hook a custom action onto almost any form field, in order to drive behaviour elsewhere on the form. Common uses of this are:
- Showing/hiding components depending on input (as shown above)
- Enabling/disabling components depending on input
- Pre-filling fields based on the values in other fields
- Loading additional options or data as values are selected
With an action wired up to every field on a form, you could even build a "live update" control panel that does not require an "OK" or "Submit" button, but which rather updates something elsewhere in your application as soon as the user makes a selection.
Custom form fields
The included example FormField implementations will cover the most basic form
input requirements. But inevitably, you may require some new type of FormField
to capture data that the built-in FormFields simply can't. Fortunately, swing-forms
is built with extensibility in mind. The abstract FormField class can fairly easily
be extended to create a new field type.
Custom form field walkthrough - let's build a Font chooser
Let's walk through the process of building a FontField form field that allows
the user to choose a font, along with style parameters like bold or italics,
and also optional foreground/background color selection. Can we create such
a field with swing-forms? Yes we can!

We start by extending the FormField class and adding all the class properties that we will need:
public final class FontField extends FormField {
private final JLabel sampleLabel;
private final JButton button;
private final JPanel wrapperPanel;
private ActionListener actionListener;
private Font selectedFont;
private Color textColor;
private Color bgColor;
// ...
}
We can add some overloaded constructors to allow optionally setting an initial font, and optionally specifying a starting text color and background color. If the color properties aren't specified, we'll omit them from our font dialog and those properties won't be editable.
We also need to create our font chooser popup dialog. This is actually fairly easy because it is in fact just another FormPanel! We can use existing FormFields to create it, such as ComboField, ListField, and LabelField.
Okay, so we have an empty Font list... how do we populate it with the list of fonts?
switch (typeField.getSelectedIndex()) {
case 0: // built-in fonts
fontListModel.addAll(List.of(Font.SERIF, Font.SANS_SERIF, Font.MONOSPACED, Font.DIALOG, Font.DIALOG_INPUT));
// ...
break;
case 1: // System fonts
fontListModel.addAll(Arrays.asList(GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames()));
// ...
break;
}
The Java built-in fonts are those guaranteed to use by the JRE. These are the "safe" fonts that we can simply hard-code here with assurance from Java that they will resolve to actual system fonts at runtime.
The system-installed fonts we can retrieve from the local graphics environment. This list may vary greatly from system to system and is beyond our control, but we can enumerate them and present them to our users in the font list field.
Setting the field component
The next important step when creating a custom FormField implementation is to set something
called the fieldComponent. Let's look at part of the constructor of our FontField:
//...
wrapperPanel = new JPanel();
wrapperPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
sampleLabel = new JLabel();
sampleLabel.setOpaque(true);
sampleLabel.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
updateSampleLabel();
fieldComponent = wrapperPanel;
wrapperPanel.add(sampleLabel);
wrapperPanel.add(button);
//...
We create a wrapperPanel and add both our sample label to it, and also our JButton for launching the font
selection dialog. Then, this wrapper panel is set as our field component:
fieldComponent = wrapperPanel;
Anatomy of a FormField
There are four main components within every FormField:
- The field label (optional in some components).
- The field component - this is the key part of the FormField.
- The help label (optional - only shown if help text is available)
- The validation label (optional - only shown when the form is validated)
We can see these pieces in the screenshot below:

When we create a custom FormField, we'll typically set the user-interactable component as our fieldComponent.
But what if we require more than one UI component in our form field?
Now we can see why our FontField implementation creates a wrapperPanel and adds more than one UI element
to it. It's because the FormField parent class expects a single fieldComponent. Using a wrapper panel
to group several UI components together into one fieldComponent can be a great way to develop complex,
multi-component FormFields.
Allowing callers to listen for changes
When designing a new FormField implementation, we should publish change events whenever the
content of our field has been modified. This allows callers to respond to those change events
if needed. In our case, we need to notify callers whenever one of our properties changes:
the selected font, the selected font color, or the selected font background color. How
do we do this? Fortunately, the parent FormField class makes this easy for us! It gives
us a fireValueChangedEvent() method that we can invoke whenever we detect any change. This
method handles notifying all listeners, if any are registered.
So, we simply need to include this in our setter methods. For example:
public FontField setSelectedFont(Font font) {
if (Objects.equals(selectedFont, font)) {
return this; // don't accept no-op changes
}
selectedFont = font;
updateSampleLabel();
fireValueChangedEvent();
return this;
}
We first check to make sure that the call to setSelectedFont() will actually result in a change.
(That is, if you invoke setSelectedFont() with the same font that is already selected, nothing happens).
Next, we accept the new font, update our sample label, and invoke fireValueChangedEvent() in the parent
class to notify any listeners. Finally, we return this to allow for fluent-style method chaining, as
we do with all our setter methods.
It's not just a documentation example
The FontField form field that we've built above is not just a theoretical example that
was cooked up for this documentation. It actually works! The full source is included
in the swing-extras library and you can use it in your applications. Here's an example
of it being used in the musicplayer application:

Help tooltips
Most FormField implementations (including your own custom ones, if you want) support
the concept of a helpful informational tooltip that can be displayed next to the field.
It looks like this:

To enable this, you can invoke setHelpText() on the field in question and supply
some non-blank value (setting null or an empty string will disable the tooltip and
prevent the information icon from appearing).
ShortTextField textField = new ShortTextField("Text:", 12);
textField.setHelpText("Help icons show up whenever a field has help text");
panel.add(textField);
When the user hovers the mouse over the informational icon, the tooltip appears:

To support this in custom form field implementations, you don't actually have to do
anything at all, as the code for this is handled by the abstract FormField class
and by the FormPanel class (which handles the rendering of the icon).
You can prevent the help icon from showing up in your custom FormField implementation
by overriding the hasHelpLabel() method in your implementing class to return false.
Let's look at the default implementation of this method in the parent FormField class:
public boolean hasHelpLabel() {
return helpLabel.getToolTipText() != null && !helpLabel.getToolTipText().isBlank();
}
This default implementation is generally what you want: the help label will automatically
show up if your form field has help text, and will hide itself if not. If you're writing
a custom FormField implementation that, for whatever reason, never requires a help
label, you could override that method like this:
@Override
public boolean hasHelpLabel() {
return false; // help label will not show up even if help text is set
}
List rendering
Both 'ListField' and 'ComboField' support custom cell renderers, if the items you are displaying are more than mere String values, or if the items in your list require some custom rendering.
For a particularly ugly example of this powerful feature, consider this screenshot:

Here we see simple String items being rendered with a custom cell renderer that gives each item a gradient background that varies depending on whether the item is selected or not. The font for each item also varies depending on item selection status.
That example is pretty visually horrible, but it does give you an idea of what you can accomplish with the
setCellRenderer() method. The above screenshot was generated with this code:
List<String> items = List.of("Item 1", "Item 2", "Item 3", "Item 4",
"Item 5", "Item 6", "Item 7", "Item 8",
"Item 9");
ListField<String> listField = new ListField<>("List:", items);
listField.setFixedCellWidth(80);
listField.setCellRenderer(new MyAwfulRenderer(90,25));
listField.setVisibleRowCount(4);
listField.getMargins().setBottom(12);
formPanel.add(listField);
ComboField<String> comboField = new ComboField<>("Combo box:", items, 0);
comboField.setCellRenderer(new MyAwfulRenderer(140, 25));
formPanel.add(comboField);
And the implementation of the MyAwfulRenderer is pretty straightforward:
private static class MyAwfulRenderer implements ListCellRenderer<String> {
private final Map<String, ImagePanel> unselectedCells = new HashMap<>();
private final Map<String, ImagePanel> selectedCells = new HashMap<>();
private final int cellWidth;
private final int cellHeight;
public MyAwfulRenderer(int width, int height) {
cellWidth = width;
cellHeight = height;
}
@Override
public Component getListCellRendererComponent(JList<? extends String> list, String value, int index, boolean isSelected, boolean cellHasFocus) {
ImagePanel iPanel = isSelected ? selectedCells.get(value) : unselectedCells.get(value);
if (iPanel == null) {
iPanel = createImagePanel(value, isSelected);
}
return iPanel;
}
private ImagePanel createImagePanel(String value, boolean isSelected) {
ImagePanel panel = new ImagePanel(ImagePanelConfig.createSimpleReadOnlyProperties());
panel.stretchImage();
Gradient gradient = new Gradient(GradientType.HORIZONTAL_STRIPE,
isSelected ? Color.GREEN : Color.BLACK,
isSelected ? Color.BLUE : Color.GREEN);
LogoProperty config = new LogoProperty(value);
config.setLogoWidth(cellWidth);
config.setLogoHeight(cellHeight);
config.setBgColorType(LogoProperty.ColorType.GRADIENT);
config.setBgGradient(gradient);
config.setAutoSize(true);
config.setFont(new Font(Font.MONOSPACED, Font.BOLD, 11));
config.setTextColor(isSelected ? Color.YELLOW : Color.WHITE);
config.setBorderWidth(0);
panel.setImage(LogoGenerator.generateImage(value, config));
if (isSelected) {
selectedCells.put(value, panel);
}
else {
unselectedCells.put(value, panel);
}
panel.setPreferredSize(new Dimension(cellWidth, cellHeight));
return panel;
}
}
Basically, it lazily creates the image for each cell based on selection status, and then uses a pair of HashMaps to cache the generated images for later retrieval.
This example is admittedly ugly, but the point of it is that you can supply whatever renderer you can think up for rendering your list items. Hopefully with help from a good graphic designer! :)
Properties handling
For the purposes of this documentation, "properties" refers to any kind of name/value pair of data that you wish to persist in your application. This could be application settings, user preferences, application state (window size and position, etc) that you wish to automatically load and use on the next startup, and so on. Java provides some built-in ways of solving this problem, but as we'll see, the built-in Java approach is limiting and sometimes frustrating.
java.util.Properties
The built-in java.util.Properties class is great at dealing with simple String-based name/value
pairs of data. It even comes with store() and load() methods that allow you to write data out
to a file and read it back in later.
Advantages of java.util.Properties:
- extremely simple API
- handles i/o for you
However, there are some drawbacks:
- values are
Stringonly- That means, if you want to store some custom type, you have to handle conversion to/from
String
- That means, if you want to store some custom type, you have to handle conversion to/from
- this means that the simple API actually works against you (you have to write custom code here)
java.util.prefs.Preferences
The built-in java.util.prefs.Preferences seems at first glance as though it solves the problems
presented by the java.util.Properties class:
- primitive types, such as
boolean,int,float, anddoubleare wrapped - can write to a "user node" (negates the need to manually pick a file save location)
For example, to use java.util.prefs.Preferences:
Preferences prefs = Preferences.userNodeForPackage(ca.corbett.Example.class);
prefs.put("someImportantPref", "somevalue");
On my system (linux-based), this automatically creates a file:
/home/scorbett/.java/.userPrefs/ca/corbett/prefs.xml with the following content:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE map SYSTEM "http://java.sun.com/dtd/preferences.dtd">
<map MAP_XML_VERSION="1.0">
<entry key="someImportantPref" value="someValue"/>
</map>
The nice part about this is that it was very easy for us to use, and also that we didn't have to
worry about selecting a save location - this is done automatically for us by java.util.prefs.Preferences.
The not-so-nice part about this is that the exact location of the output varies from system to system,
and on some systems, may not even be stored in a file at all, but rather in some other OS-supplied
storage mechanism, such as a system registry. This can make troubleshooting difficult, and also makes
it difficult to transport application settings easily from one machine to another.
Another drawback of this approach is that, if each of your classes uses its own class as the parameter
for userNodeForPackage, then your application preferences may end up spread across multiple files
in multiple directories, making it difficult or impossible to view them all at once (again, in a
troubleshooting type of scenario where you're trying to figure out why things aren't saving/loading
correctly).
Wouldn't it be nice if there was a portable way to consistently handle properties, resulting in all application properties going to the same location, which can then be transported as needed to another machine, or even checked into source control? Well, there is!
The ca.corbett.extras.properties package
First, let's say hello to the ca.corbett.extras.properties.Properties class. It has the following benefits:
- wraps not just primitives, but also
ColorandFontobjects - the
FileBasedPropertiessubclass can handle reading/writing to a file - keeps all related properties in the same place.
- tightly integrates with
swing-formsfor generating form fields - is extensible by design, so you can build your own custom property types
If it ended there, this would be a somewhat underwhelming offering. But wait, there's more!
PropertiesManager and PropertiesDialog
The PropertiesManager and PropertiesDialog classes help your application manage settings, not just
in a "please persist these properties" kind of way, but also in a "hey, while you're at it, could you
please generate a nice UI for my users to view and edit those properties" kind of way.
The power of these classes is considerable in saving you from having to write code for managing and displaying properties to the user. Let's take a closer look at these features!
Relation to swing-forms
If we look into ca.corbett.extras.properties package, we'll find many different
Property implementations... but implementations of what? They all extend a class
called AbstractProperty, which is well worth a closer look. Let's zoom in and
take a look at four of the abstract methods in this class:
public abstract void saveToProps(Properties props);
public abstract void loadFromProps(Properties props);
public abstract FormField generateFormFieldImpl();
public abstract void loadFromFormField(FormField field);
(Javadoc omitted for brevity). We can see that all implementations of AbstractProperty
have to do at least two basic tasks:
- load and save themselves from and to a
ca.corbett.extras.properties.Propertiesinstance - generate a
FormFieldfrom themselves and then later load the value from thatFormField
This, in theory, allows us to support data in any custom format that we want.
A simple example: BooleanProperty
Let's start by looking at the easiest example: BooleanProperty extends AbstractProperty
to allow handling of boolean properties:
@Override
public void saveToProps(Properties props) {
props.setBoolean(fullyQualifiedName, value);
}
@Override
public void loadFromProps(Properties props) {
value = props.getBoolean(fullyQualifiedName, value);
}
The saveToProps() and loadFromProps() methods are pretty much exactly what we might expect.
We save or load a single boolean value, and we're done. But we must also implement
generateFormFieldImpl() and loadFromFormField() as well. What kind of form field would be
best for representing a boolean value? Why, a CheckBoxField of course!
@Override
public FormField generateFormFieldImpl() {
return new CheckBoxField(propertyLabel, value);
}
@Override
public void loadFromFormField(FormField field) {
if (field.getIdentifier() == null
|| !field.getIdentifier().equals(fullyQualifiedName)
|| !(field instanceof CheckBoxField)) {
logger.log(Level.SEVERE, "BooleanProperty.loadFromFormField: received the wrong field \"{0}\"",
field.getIdentifier());
return;
}
value = ((CheckBoxField)field).isChecked();
}
We see that the generateFormFieldImpl() method simply creates and returns a simple checkbox field.
This is a template method that is invoked as needed by the parent class - this is important, as we'll
see later, because the parent class will do certain things with our generated FormField, such as
assigning it a unique identifier. We'll discuss property identifiers in much more detail later.
The loadFromFormField() method does some basic error handling to make sure the field was wired
up correctly, and then just reads the value from the checkbox.
So far, so good. But this is barely scratching the surface of what we can do here.
A complex example: FontProperty
Let's look at a property that isn't quite so straightforward. How do we store a Font? Well,
a Font can have a name (SansSerif, Monospaced, Serif, etc), but also style information,
such as bold or italics. In swing-extras, fonts can also optionally have color information
for their foreground color and background color. How can we store so much data in one single
property? Don't we have to map everything to a single name/value pair on the back end?
No, we don't. We can split it into multiple properties that we will manage behind the scenes.
@Override
public void saveToProps(Properties props) {
props.setString(fullyQualifiedName + ".name", font.getFamily());
props.setBoolean(fullyQualifiedName + ".isBold", font.isBold());
props.setBoolean(fullyQualifiedName + ".isItalic", font.isItalic());
props.setInteger(fullyQualifiedName + ".pointSize", font.getSize());
if (textColor != null) {
props.setColor(fullyQualifiedName + ".textColor", textColor);
}
else {
props.remove(fullyQualifiedName + ".textColor");
}
if (bgColor != null) {
props.setColor(fullyQualifiedName + ".bgColor", bgColor);
}
else {
props.remove(fullyQualifiedName + ".bgColor");
}
props.setBoolean(fullyQualifiedName + ".allowSizeSelection", allowSizeSelection);
}
@Override
public void loadFromProps(Properties props) {
String fontName = props.getString(fullyQualifiedName + ".name", font.getFamily());
boolean isBold = props.getBoolean(fullyQualifiedName + ".isBold", font.isBold());
boolean isItalic = props.getBoolean(fullyQualifiedName + ".isItalic", font.isItalic());
int pointSize = props.getInteger(fullyQualifiedName + ".pointSize", font.getSize());
font = Properties.createFontFromAttributes(fontName, isBold, isItalic, pointSize);
textColor = props.getColor(fullyQualifiedName + ".textColor", textColor);
bgColor = props.getColor(fullyQualifiedName + ".bgColor", bgColor);
allowSizeSelection = props.getBoolean(fullyQualifiedName + ".allowSizeSelection", allowSizeSelection);
}
Whoah, there's a lot going on here! Saving a single FontProperty to a Properties object
actually ends up creating a bunch of property entries! But how will we keep them all grouped
together? We use the fullyQualifiedName of the property and then append sub-names for all
of our individual attributes. So, a single FontProperty with a fullyQualifiedName of
my.amazing.font will end up creating the following entries:
my.amazing.font.allowSizeSelection=true
my.amazing.font.isBold=false
my.amazing.font.isItalic=false
my.amazing.font.name=SansSerif
my.amazing.font.pointSize=22
my.amazing.font.textColor=0x00000000
my.amazing.font.bgColor=0xffffffff
This is transparent to callers of this class - they don't need to know or care about the storage details
of the property in question. It also means there's effectively no limit to how complicated our AbstractProperty
implementations can get. Because we're not limited to a single name/value pair for our properties, we can
design property wrappers around even very complex data types, and still save them with one line of code!
Let's see now what happens when we ask a FontProperty to generate a form field, and to load itself
from a form field:
@Override
protected FormField generateFormFieldImpl() {
FontField field = new FontField(propertyLabel, getFont(), textColor, bgColor);
field.setShowSizeField(allowSizeSelection);
return field;
}
@Override
public void loadFromFormField(FormField field) {
if (field.getIdentifier() == null
|| !field.getIdentifier().equals(fullyQualifiedName)
|| !(field instanceof FontField)) {
logger.log(Level.SEVERE, "FontProperty.loadFromFormField: received the wrong field \"{0}\"",
field.getIdentifier());
return;
}
FontField fontField = (FontField)field;
font = ((FontField)field).getSelectedFont();
textColor = fontField.getTextColor();
bgColor = fontField.getBgColor();
}
We see that there's a very close relationship between FontProperty and the matching swing-forms class FontField.
This is because the intention with most properties is that you're eventually going to want to expose them
to the user for viewing and/or editing. This integration between the ca.corbett.extras.properties package
and the swing-forms classes is what not only enables this, but also makes things MUCH easier when it comes
time to generating and showing that UI. As it turns out, our calling code won't have to worry about
generating FormPanel instances at all...
Working with enums
It's worth taking a moment to look more closely at a very common use case with properties in general:
I want my application to have a property whose available options are determined with an enum. How can I
do that? You could of course use the swing-extras class ComboProperty, which specifically lets
you store a multi-choice option as a property. It looks like this:
List<String> options = new ArrayList<>();
for (MyEnum value : MyEnum.values()) {
options.add(value.toString());
}
comboProperty = new ComboProperty("fieldname", "Choose:", options, 0, false);
There are several problems with this approach:
- I have to write boilerplate code to get all the values out of the enum
- By using
toString()to load the value into the combo, I have to parse the String value that comes out of the combo box. - If the
toString()changes over time (or gets localized to another language), my properties file breaks.
Alternatively, I could use value.name() instead of value.toString() to populate the combo box, but
then the user has to look at the internal names of my enum values, which are often written in ALL_CAPS.
Wouldn't it be nice if I could just say "this property should use values from this enum"?
Using EnumProperty
This is the problem that EnumProperty was designed to solve. Let's look at a better way of doing the above code:
EnumProperty<MyEnum> enumProp = new EnumProperty<>("fieldname", "Choose:" MyEnum.VALUE1);
That's it! The EnumProperty is smart enough to interrogate the given enum and extract its values, using
toString() to populate the combo box, but using name() to store items into the Properties object.
This gives you the best of both worlds.
You also have the choice of using the useNamesInsteadOfLabels option with EnumProperty if you actually
want to display the enum name() instead of its toString() in the combo box. The default value is
to use toString() as it is usually the user-friendlier option.
EnumProperty example
The swing-extras demo app contains a quick demo of the EnumProperty class. Let's look at the definition of
our simple example enum:
public enum TestEnum {
VALUE1("This is value 1"),
VALUE2("This is value 2"),
VALUE3("This is value 3");
final String label;
TestEnum(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
We see a straightforward enum definition with three values, each of which defines a user-friendly label that we
return in the toString() implementation. Now, the demo app can create an instance of EnumProperty to handle
the display and user interaction with this enum:
new EnumProperty<TestEnum>("Enums.Enums.enumField1", "Choose:", TestEnum.VALUE1);
The result looks like this:

We also have the option of using the useNamesInsteadOfLabels option if we wish to usee the enum value names
instead of the toString value for some reason. The result of that is shown below:

The code is otherwise identical, and your code doesn't ever need to populate the combobox directly, or read values from it directly. Your code always deals with instances of your enum, which is much easier!
Custom properties
Like most things in swing-extras, the properties handling is designed with extensibility in mind.
It's fairly straightforward to implement a new AbstractProperty to store whatever custom data
your application needs to worry about. Usually this will also mean developing a new FormField
implementation to represent that data to the user and allow viewing/editing of it, but not
necessarily. For example, in the previous section, we saw how EnumProperty was able to back
itself onto the existing ComboField by just presenting a friendlier interface to it. Your custom
property might be able to make use of an existing FormField in the same way.
In order to implement the saveToProps() and loadFromProps() successfully, though, it's
vitally important to understand how and why properties are given a fullyQualifiedName.
And that is an excellent segue to talk about the most powerful feature of the
ca.corbett.extras.properties package...
PropertiesDialog
There's a pair of very powerful classes hiding in the ca.corbett.extras.properties package, and they
go very well together. They are PropertiesManager and PropertiesDialog. These two classes can
save you a LOT of coding.
Let's imagine we want to generate a properties form that looks like this:

How much UI code would we need to write? Would you have guess "absolutely none"?
PropertiesManager, and naming properties carefully
Let's start by looking at how we would define the properties for the above properties form:
List<AbstractProperty> props = new ArrayList<>();
// Simple label properties don't allow user input.
// But, they can be handy for organizing and explaining the input form.
props.add(new LabelProperty("Intro.Overview.label1", "All of the props on this dialog were generated in code."));
props.add(new LabelProperty("Intro.Overview.label2", "No UI code was required to generate this dialog!"));
// The available property types correspond to form field types in swing-forms!
// That means we can do checkboxes and combo boxes and all the usual stuff:
props.add(new BooleanProperty("Intro.Overview.checkbox1", "Property types correspond to form field types"));
List<String> options = new ArrayList<>();
options.add("Option 1");
options.add("Option 2 (default)");
options.add("Option 3");
props.add(new ComboProperty<>("Intro.Overview.combo1", "ComboProperty:", options, 1, false));
// Label styling options are available:
props.add(new LabelProperty("Intro.Labels.someLabelProperty", "You can add labels, too!"));
LabelProperty testLabel = new LabelProperty("Intro.Labels.someLabelProperty2", "You can set label font properties");
testLabel.setFont(new Font("Monospaced", Font.ITALIC, 14));
testLabel.setColor(Color.BLUE);
props.add(testLabel);
props.add(new LabelProperty("Intro.Labels.label3", "You can also add hidden properties."));
// Color properties can accept solid colors, color gradients, or both:
props.add(new ColorProperty("Colors.someSolidColor", "Solid color:", ColorSelectionType.SOLID).setSolidColor(Color.RED));
props.add(new ColorProperty("Colors.someGradient", "Gradient:", ColorSelectionType.GRADIENT));
props.add(new ColorProperty("Colors.someMultiColor", "Both:", ColorSelectionType.EITHER));
// File properties can accept directories or files:
props.add(new DirectoryProperty("Files.someDirProperty", "Directory:"));
props.add(new FileProperty("Files.someFileProperty", "File:"));
// Text properties can be single-line or multi-line:
props.add(new ShortTextProperty("Text.Single line.someTextProp1", "Text property1:", "hello"));
props.add(new ShortTextProperty("Text.Single line.someTextProp2", "Text property2:", ""));
props.add(LongTextProperty.ofFixedSizeMultiLine("Text.Multi line.someMultiLineTextProp", "Text entry:", 4, 40)
.setValue("You can support long text as well.\n\nPop-out editing is optional.")
.setAllowPopoutEditing(true));
// Properties can be "hidden".
// They are readable and settable by the client application.
// But they won't appear in the properties dialog!
// This is great for application state like window size/dimensions and etc.
IntegerProperty hiddenProp = new IntegerProperty("Hidden.someHiddenProp", "hiddenProp", 77);
hiddenProp.setExposed(false);
props.add(hiddenProp);
Okay, so far so good. But why do these properties have such long internal names? Let's take a quick
look at the fullyQualifiedName of our properties:
AbstractProperty.fullyQualifiedName
The format of this fully qualified name is as follows:
[category.[subcategory.]]propertyName
If category name is not specified, a default name of "General" will be used.
If subcategory is not specified, a default name of "General" will be used
Some examples:
- UI.windowState creates a property called "windowState" belonging to an implied subcategory of "General" within the "UI" category.
- UI.window.state creates a property called "state" in the subcategory of "window" within the top-level category of "UI".
- windowState - creates a property called "windowState" in an implied top-level category of "General" with an implied subcategory of "General"
- UI.window.state.isMaximized - creates a property called "state.isMaximized" within the "window" subcategory in the "UI" top-level category. Note that further dots after the second one are basically ignored and are considered part of the property name. So, you can't have sub-sub-categories.
So, let's look a little closer at one of the properties we created earlier:
new LabelProperty("Intro.Overview.label1", "All of the props on this dialog were generated in code.");
This creates a top-level category called "Intro" and a subcategory called "Overview".
The actual field name is called label1.
Okay, this seems like a long way to go just to uniquely identify each property. What purpose do these top-level and subcategory names serve?
Generating a PropertiesDialog
The PropertiesManager class has a method called generateDialog() that will return an
instance of PropertiesDialog. The beautiful part of all of this is that, because PropertiesManager
knows all about our properties, and because each property knows how to generate a FormField
from itself, we actually don't need to write a single line of UI code to generate the
PropertiesDialog. Our PropertiesManager can not only create a dialog for us, but it
can handle creating all the FormPanel and FormField instances that it needs.
Referring to our screenshot at the top of this page, you can see how the top-level categories in our fully qualified names were turned into tab headers, and the subcategory names were used to generate section header labels. The forms organize themselves and we don't have to lay out a single UI element!
A real-world example: musicplayer
For an example of what's possible with these properties classes in swing-extras, I refer you
to my own musicplayer application:

This entire dialog was generated using mechanisms similar to that described above (but not exactly
the same... we'll discuss this more in a later section about application extensions). Absolutely
no UI code was required in the musicplayer application itself! The PropertiesManager and
PropertiesDialog class manage it all! And, the application code doesn't have to worry about
saving or loading these properties, because each property intrinsically knows how to save
and load itself, and PropertiesManager can wrap that up for you as well!
Custom form logic
It's often a requirement to add custom form logic to the properties dialog, such that certain controls are only visible or enabled when some other control has a particular value. For example, in the MusicPlayer application, we want to have the custom waveform style controls disabled, unless the user opts to override the application theme settings. When the properties dialog first comes up, the custom style fields should be disabled, like this:

And then the user selects the override option in the combo box, the form fields underneath it should be enabled automatically, like this:

How do we accomplish this?
Setting initial visibility state
Setting the initial state is quite easy. There are methods in AbstractProperty that can help with this:
public AbstractProperty setInitiallyEditable(boolean initiallyEditable) { ... }
public AbstractProperty setInitiallyVisible(boolean initiallyVisible) { ... }
When we create a new property, we can easily set its initial state based on whatever application logic
we have. And, because all setter methods in AbstractProperty use the fluent style (returning a reference
to themselves), we can chain this call quite easily:
BooleanProperty myProp = new BooleanProperty("id", "label", true).setInitiallyEditable(false);
Here we are creating a new property and setting its initial editability in one line. We could also choose to have the control initially invisible, and show it later.
Implementing custom form logic
Setting the initial state is fine, but we also need to implement logic to toggle the editability and/or visibility of a field based on changes elsewhere on the properties dialog. How do we do that?
Fortunately, this is quite easy, thanks to the addFormFieldChangeListener method in AbstractProperty.
Let's look at the actual change listener from the MusicPlayer example screenshots above:
// Make sure we respond to change events properly, to enable or disable the override fields:
overrideAppThemeWaveform.addFormFieldChangeListener(event -> {
boolean shouldEnable = ((ComboField<String>)event.formField()).getSelectedIndex() == 1;
FormPanel fp = event.formPanel();
fp.getFormField(waveformBgColor.getFullyQualifiedName()).setEnabled(shouldEnable);
fp.getFormField(waveformFillColor.getFullyQualifiedName()).setEnabled(shouldEnable);
fp.getFormField(waveformOutlineColor.getFullyQualifiedName()).setEnabled(shouldEnable);
fp.getFormField(waveformOutlineThickness.getFullyQualifiedName()).setEnabled(shouldEnable);
});
The PropertyFormFieldChangeListener interface provides a valueChanged method that accepts a
PropertyFormFieldValueChangedEvent as a parameter. This event parameter contains the FormField that
triggered the change, the FormPanel in which the field is located, and even the AbstractProperty that
owns the field in question. Using this information, and in particular using the getFormField method
in FormPanel to look up other form fields by their fully qualified name, we are able to look up
whatever fields we need to adjust, and invoke their setEnabled or setVisible methods as needed.
Naming properties is important!
In the previous section, we talked about the importance of giving each AbstractProperty
a meaningful fully qualified name. As we see in the example code above, this fully qualified name is passed
on to each FormField on the generated properties dialog. This is what allows you to look up a form field
when you need to modify its state.
app-extensions
The ca.corbett.extensions package contains some powerful classes that you can use to dynamically
change the functionality of your application, without changing the code of your application itself.
Because app-extensions builds upon both swing-forms and the properties handling classes contained
within swing-extras, it is strongly recommended that you read the sections on
swing-forms and properties handling first.
How does it all work?
When designing your application, you identify certain "extension points" - that is, parts of the code where an extension could either change the way that something is done, or add additional functionality that the application does not do out of the box.
For example, let's say we're writing an application that accepts input in the form of Xml files, extracts information from those files, and writes it to a database. The loading of the input files is an obvious extension point. Could we write an extension that would allow the application to accept input in other formats? Json or CSV, for example?
For a more complex example, let's say we are writing an image editing application that supports certain image edit operations out of the box: rotate, flip, resize, and so on. Could we write an extension that dynamically adds a completely new image editing operation to our application, without changing the code of our application itself?
The app-extensions code provides a way to handle both of the example scenarios,
and many more. In the next sections, we'll look at this in detail.
General design
Designing an application with extensions in mind
The first and most important step is to identify possible extension points in your code.
Let's start with the simpler of our two examples: we want our application to be
able to accept file input in other file formats. Okay, let's start by extending the
AppExtension abstract class for our fictional application:
public abstract class MyExtension extends AppExtension {
public abstract boolean isFormatSupported(File inputFile);
}
We've created an abstract class for our application and extended the base AppExtension
abstract class. We then add a new abstract method called isFormatSupported that accepts
a candidate file and returns either true or false based on that file's format. But
where is the logic for this method? Why not just implement right here?
Because we don't want the application to know what additional formats are supported.
The whole point of offloading this logic into an extension class is that the extension class might not exist yet. In fact, the file format that we eventually want to support might also not exist yet. We're coding for a future that hasn't happened yet. Putting the logic into the application would make the application inflexible about what formats it supports, and would require a new version of the application code down the road when we want to add support for a new file format later. The whole point of this library is to avoid that.
We will also need a way for the extension to load data from the input file and return it, presumably in the form of some model object that our application deals with. Let's do that next:
public abstract class MyExtension extends AppExtension {
public abstract boolean isFormatSupported(File inputFile);
public abstract MyModelObject loadFromFile(File inputFile)
throws InputFormatNotSupportedException, InputProcessingException;
}
Okay, now we have an abstract method that our application code can invoke when loading data from input files. But the logic for this method does not exist yet and in fact won't ever exist within our application's codebase. In theory, our application can support any format we want, via extensions.
But we're not done yet. We have to write our application code to consider the existence
and the capabilities of any extensions that may be present at runtime. Let's extend the
ExtensionManager class and add some logic to it:
public class MyExtensionManager extends ExtensionManager<MyExtension> {
// ...
// skipping some boilerplate for now
// ...
public boolean isFormatSupported(File inputFile) {
for (MyExtension extension : getAllLoadedExtensions()) {
if (extension.isFormatSupported(inputFile)) {
return true;
}
}
return false;
}
public MyModelObject loadFromFile(File inputFile)
throws InputFormatNotSupportedException, InputProcessingException {
for (MyExtension extension : getAllLoadedExtensions()) {
if (extension.isFormatSupported(inputFile)) {
return extension.loadFromFile(inputFile);
}
}
return null;
}
}
The methods we add here wrap the functionality offered by our currently loaded extensions (if any). Now we in theory have what we need to modify our application's loading code to take extensions into consideration:
// ... somewhere in our application code ...
public MyModelObject loadInputFile(File inputFile)
throws InputFormatNotSupportedException, InputProcessingException {
// If it's an xml file, we can handle it natively:
if (isXmlFile(inputFile)) {
return loadXmlInputFile(inputFile);
}
// Otherwise, see if any of our extensions know how to handle this file type:
if (MyExtensionManager.getInstance().isFormatSupported(inputFile)) {
return MyExtensionManager.getInstance().loadFromFile(inputFile);
}
throw InputFormatNotSupportedException("Input file is not in a supported format.");
}
First, we check to see if the input file is in a format recognized by our application code itself. If so, we can handle it natively, the same way we would have before adding extension support to our application. But, if the format is unknown to us, instead of simply giving up, we can ask our extension manager if there are any registered extensions that know how to process that file type. If so, we can get that extension to handle the loading of the input file, even without our application knowing or caring about the particulars of what format it's in.
Using this same basic pattern, we can augment our application in any number of ways, allowing extensions to either enhance existing processing, or even to add new processing options that the original application codebase didn't even think of.
ExtensionManager
We should take a look at the ExtensionManager base class and its intended purpose.
This class contains methods that can scan a directory for compatible jar files and
load them dynamically (we'll discuss what we mean by "compatible" in the next section).
Once loaded, the ExtensionManager is the central authority that tracks which extensions
have been loaded, and which of them are currently enabled. Extensions can be enabled
or disabled at will by the user at runtime, and this can have drastic effects on the
UI of the application in question. If an extension is disabled, the facilities that it
provided are gone. This might mean that menu options, buttons, popup menus, and
application configuration properties related to that extension have to be hidden.
The previous example of adding support for a new file format was a fairly trivial one.
Let's look at a more complex example that involves UI changes, and we'll see how
ExtensionManager can work together with PropertiesManager to provide a consistent
and logical look and feel to your applications.
A more complex example - adding new functionality
Let's look at a more complex hypothetical scenario - we're writing an image editing application where the user can bring up an image and do certain manipulations to it: resize it, rotate it, mirror it, and etc. Many of these basic operations can be provided by the application code itself. But what if we want to support the idea that an extension could offer a brand-new image editing operation that we didn't even think of when writing the application? Can we do it? Sure!
Let's again start by extending the AppExtension abstract class, but this time let's
keep it vague instead of focusing on specific actions like loading an input file:
public abstract class MyExtension extends AppExtension {
public abstract List<AbstractAction> getImageEditActions();
}
This abstract method allows an extension to return a list of AbstractAction
instances related to image editing. Let's again add a wrapper around this
method to our extension manager class:
public class MyExtensionManager extends ExtensionManager<MyExtension> {
public List<AbstractAction> getImageEditActions() {
List<AbstractAction> allActions = new ArrayList<>();
for (MyExtension extension : getAllLoadedExtensions()) {
List<AbstractAction> actions = extension.getImageEditActions();
if (actions != null && ! actions.isEmpty()) {
allActions.addAll(actions);
}
}
return allActions;
}
}
With this in place, our application can offer additional functionality wherever we are building up a list of possible image edit actions. For example, when building our edit menu:
private JMenu buildImageEditMenu() {
JMenu menu = new JMenu("Edit");
// Add the ones we know about natively:
menu.add(new JMenuItem(imageRotateAction));
menu.add(new JMenuItem(imageResizeAction));
menu.add(new JMenuItem(imageFlipAction));
// Now add any provided by our extensions:
for (AbstractAction action : MyExtensionManager.getInstance().getImageEditActions()) {
menu.add(new JMenuItem(action));
}
}
When we initially release our application, there will not yet be any extensions for it, so the menu will only show the image edit actions that shipped with the application. But over time, as extensions are written and published, our image edit menu will automatically expand to include them.

But what happens when one of those extension actions is invoked? What can an extension actually do? Well, pretty much whatever you want. Let's look at extension code for our hypothetical image editing application:
public class AddImageBorderExtension extends MyExtension {
@Override
public List<AbstractAction> getImageEditActions() {
List<AbstractAction> actionList = new ArrayList<>();
actionList.add(new AddImageBorderAction());
return actionList;
}
}
And the code for our custom action is similarly simple:
public class AddImageBorderAction extends AbstractAction {
public AddImageBorderAction() {
super("Add border...");
}
@Override
public void actionPerformed(ActionEvent e) {
if (MainWindow.getInstance().getSelectedImage() == null) {
JOptionPane.showMessageDialog(MainWindow.getInstance(), "Nothing selected.");
}
new AddImageBorderDialog(MainWindow.getInstance()).setVisible(true);
}
}
Our extension code can reference whatever it needs from the application codebase,
because extensions are built using that code. So, as long as our application provides
ways for extensions to query for things (like MainWindow.getInstance() and getSelectedImage()),
then they will be able to interact with those things.
So now, when we register our AddImageBorderExtension, our new menu item will
show up automatically, and the new image editing functionality supplied by our
extension is added to the application.
But, how do we register and load our extension?
Registering an extension
Extensions have to package up a file called extInfo.json in their jar file.
The file can live anywhere, but a fully-qualified package name directory structure
in the jar resources is a good convention. This file contains information about our
extension:
{
"name": "Add image border",
"author": "steve@corbett.ca",
"version": "1.0",
"targetAppName": "MyAmazingImageEditor",
"targetAppVersion": "1.0",
"shortDescription": "Add a configurable border to selected image",
"longDescription": "This is a test of the app extension code.\nThis will go in the README somewhere.",
"customFields": {
"Custom field 1": "Hello"
}
}
We can specify a name and description of our extension, but more importantly, we can specify an application name and version, which we'll use in a moment when loading the extension. We can also specify custom fields in the form of name/value pairs of additional information for this extension. Let's try that out just to see what happens.
Okay, so, how do we tell our application about our new extension? And how does the application
actually load them? Well, that's what the ExtensionManager class is for. Somewhere
in our application startup, we should ask our extension manager to load all extensions
from jars in some known location (perhaps in an "extensions" folder in our application
install directory, or by prompting the user for it):
File extensionDir = new File("...some directory with extension jars...");
MyExtensionManager.getInstance().loadExtensions(extensionDir, MyAppExtension.class, "MyAmazingImageEditor", "1.0");
The loadExtensions method takes some parameters to help it find jar files that are appropriate
for this application. Specifically, we have to pass in the class of our AppExtension
implementation. We also provide an application name and version. This is so that extension
manager can weed out extension jars that target some other application, or extension jars
that target the wrong version of this application. This is the "compatibility" check that
we mentioned earlier. The ExtensionManager is smart enough (with the help of this extInfo.json
file supplied by each extension jar) to know which jars can be loaded and which cannot.
If an extension jar does not package an extInfo.json file in its resources, it will
not be considered for loading.
On startup, we now see that our application has registered and our menu item appears:

And when we click this new menu item, we see our border dialog comes up:

Great! Our extension is loaded and the functionality it provides has been added to our application's functionality, even without our application code knowing anything at all about the new function! We can continue now to add extensions to our application this way, building it up over time until it does many more things than it was originally designed to do.
Your application is a framework
Heavy use of ExtensionManager requires you to think of your application as more of a framework.
You need to provide access to the core parts of what your application does, and add extension points
all throughout your code, so that extensions can step in as needed to augment or even replace
parts of what your application code does. If you design your application with this in mind, writing
extensions later will be much easier, and your application might grow far beyond what you
originally envisioned for it.
But, we have a slight problem - how do we know when an application extension has been enabled or disabled? How do we know when to reload our UI?
ExtensionManagerDialog
Managing extensions, and enabling/disabling them at runtime
Wouldn't it be nice if we could provide our users with a user-friendly view
of all loaded extensions, and maybe provide a way to enable and disable them
at runtime? Well, for that we have ExtensionManagerDialog!

When we launch the ExtensionManagerDialog from our application code, we can get
a visual listing of all currently loaded extensions and their metadata. Here,
we see not only the information that we supplied about our extension, but also the
custom field that we added to our json is also present. Here, the user has the
ability to enable or disable extensions. This requires a bit more work in our application
code! In our case, if the user disables our AddImageBorder extension, we no longer
want the menu item or the dialog to show up. So, we would have to regenerate our
main menu bar. Other applications may have to re-render their UI after every time
the ExtensionManagerDialog is shown, in order to show or hide controls
provided by those extensions.
The best part about ExtensionManagerDialog is that we don't have to write a single
line of UI code in our application - we get it for free from the swing-extras library!
Also, the logic for enabling/disabling extensions is wrapped up in the
ExtensionManager base class, so our derived class simply inherits it.
Extension types
You may notice in the ExtensionManagerDialog that there is a field called Type. There
are three possible values for this field:
Application built-in: these are extensions that are provided by the Application in question. They can be disabled but they cannot be removed from the list.System extension: these are dynamically-loaded extensions from jar files located in any read-only directory (e.g./opt/YourApplication/extensions). These are intended to be shared with all users on a given system.User extension: these extensions are loaded from jar files in a read/write directory, typically in the users home directory (e.g./home/YourUsername/YourApplication/extensions). These are visible only to the user who owns them.
The application doesn't treat System extensions any differently than User extensions. This information is presented in this dialog just for informational purposes. Extension config is stored together with application config, so each user on the same system can access the same System extension, each with their own configuration. The extension code in swing-extras manages this for you.
Custom properties for extensions
What if our extension has many options, and we want to be able to expose those
options to the user in a friendly way so that they can be persisted? It turns out
that the app-extensions code builds on the swing-extras properties code, which
does a lot of this for us. We can modify our ExtensionManager implementation
to accommodate this. In fact, one of the methods present in the AppExtension
interface is createConfigProperties:
public class AddImageBorderExtension implements MyAppExtension {
@Override
protected List<AbstractProperty> createConfigProperties() {
// ...
}
}
This method gives extensions a way of returning a list of configuration properties.
You can take a look at some of the facilities in the swing-extras library to see
what's possible here, specifically the PropertiesManager.
Let's modify our extension to return some custom config properties:
public class AddImageBorderExtension implements MyAppExtension {
@Override
protected List<AbstractProperty> createConfigProperties() {
List<AbstractProperty> props = new ArrayList<>();
props.add(new ColorProperty("UI.Borders.color", "Default border color:", ColorType.SOLID, Color.RED));
props.add(new IntegerProperty("UI.Borders.thickness", "Default border width:", 4,0,100,1));
LabelProperty label = new LabelProperty("UI.Borders.label",
"This properties tab was added by an extension!");
label.setExtraMargins(12,12);
label.setColor(Color.BLUE);
props.add(label);
return props;
}
}
And in our application startup code, we can gather all of our properties
and all of our extension properties together when creating our PropertiesManager:
private void loadConfig() {
List<AbstractProperty> props = new ArrayList<>();
// Load the properties that our application natively knows about:
props.addAll(getGeneralProps());
props.addAll(getDiskProps());
// Also ask our ExtensionManager for any extension-supplied props:
props.addAll(MyExtensionManager.getInstance().getAllEnabledExtensionProperties());
propsManager = new PropertiesManager(PROPS_FILE, props, "MyAmazingImageEditor");
propsManager.load();
}
Now, when we generate and launch the PropertiesDialog from our application code,
we see that our extension's properties have been added to a new tab:

And we didn't have to write any UI code at all to support this!
But it gets even better
Now that we've covered all the groundwork, let's look at arguably the most important class in the whole system...
AppProperties
Using AppProperties to combine PropertiesManager and ExtensionManager
To make things even easier, the app-extensions code also includes a
wrapper class AppProperties, which combines a custom ExtensionManager
together with a PropertiesManager instance. This allows those two classes
to co-ordinate things like the enabled/disabled status of extensions, and makes
the client code just a little easier to manage. Basically, once our AppProperties
instance exists, we can invoke the save() and load() methods as needed to
load properties and extension enabled status to and from our config file.
Setting up a new AppProperties instance is fairly straightforward:
public class MyAppProps extends AppProperties<MyAppExtension> {
public MyAppProps() {
super("MyAmazingImageEditor", new File("mypropsfile"), MyExtensionManager.getInstance());
}
@Override
protected List<AbstractProperty> createInternalProperties() {
List<AbstractProperty> props = new ArrayList<>();
// Load the properties that our application natively knows about:
props.addAll(getGeneralProps());
props.addAll(getDiskProps());
return props;
}
}
Basically, we just need to provide the location on disk where our configuration should
be saved and loaded, and then override the createInternalProperties method
to specify the properties that our application knows about natively.
But wait... where do we load the properties from our extensions? We had to do that
manually earlier, didn't we? It turns out the AppProperties class can do this for us,
because it contains both a PropertiesManager and an ExtensionManager together.
So, our application code can be simplified a bit.
There are convenience methods in AppProperties that make generating and showing
the properties dialog and the extension manager dialog very simple:
// Show the application properties dialog:
if (myAppProps.showPropertiesDialog(MainWindow.getInstance())) {
// User okayed the properties dialog...
// reload our UI to reflect the new property values
}
// Show the extension manager:
if (myAppProps.showExtensionDialog(MainWindow.getInstance())) {
// User okayed the extension manager dialog...
// reload our UI to show/hide extension-related options
}
Great! Having the configuration properties managed by PropertiesManager and the
extension enabled/disabled status managed by ExtensionManager makes our code so
much easier. Combining those two classes together into a custom AppProperties class
makes it even easier!
Real-world example: musicplayer
For a real-world example of app-extensions being used to enhance an application,
I refer you to my own musicplayer application:

The extensions shown above are the "built-in" extensions that come with the musicplayer application.
These "built-in" extensions are added not dynamically loaded from jar files, but rather programmatically
added directly to the ExtensionManager:
// Load all the built-in extensions:
addExtension(new ExtraThemes(), true);
addExtension(new ExtraAnimations(), true);
addExtension(new ExtraVisualizers(), true);
addExtension(new QuickLoadExtension(), true);
Supplying "built-in" extensions like this can be a great way of testing our your extension points with actual extensions (this often reveals places where more extension points are needed), and also provides your users with a template they can follow to write their own extensions.
We can also see lots of dynamically loaded configuration properties:

The visualizer tab that we're looking at was added by the ExtraVisualizers extension. Disabling this extension
results in this tab being hidden and the option for the "rolling waves" visualizer disappears from the application.
Extensions can greatly enhance your application
There are effectively no limits to what an application designed with extensibility in mind can do. With enough extension points, it's possible that your application could eventually be capable of doing really cool things that you never thought of when you wrote it - and you can do often it without even changing the application code itself!
Extension load order
Most of the time, the order in which extensions are loaded from disk doesn't matter, and so the default load order (alphabetically by jar file name) is fine. However, there are some circumstances where you want to ensure that extension A loads before extension B. Consider the following hypothetical method in our application's ExtensionManager implementation:
public boolean handleKeyEvent(KeyEvent keyEvent) {
for (MyAppExtension extension : getEnabledLoadedExtensions()) {
if (extension.handleKeyEvent(keyEvent)) {
return true;
}
}
return false;
}
When a KeyEvent is received, our extension manager will iterate through all of our enabled and loaded
extensions in the order in which they were loaded, asking each one if it wants to handle that particular
key event. The first extension that handles the event terminates the search. So what happens if there
are two extensions that can both handle the same KeyEvent? Well, whichever one was loaded first will
be able to process it, and the other one will not be called upon.
One possible approach to solve this is to continue making the KeyEvent available to all other extensions,
even after an extension has handled it. But perhaps we only want the KeyEvent to be handled by one single
extension. This same general pattern can appear whenever there is some kind of event within the application
that might be handled by an extension - sometimes, we only want ONE extension to handle it, and no more.
But how can we control which one?
The default sort order
By default, all extension jars within the extension jar directory are loaded in alphabetical order. So, we can control load order by naming our jars carefully. For example:
0001_important_extension.jar
0002_less_important_extension.jar
9999_really_unimportant_extension.jar
This will ensure that the 0001_important_extension.jar is loaded before the others. The ExtensionManager will
always query extensions in the order they were loaded, so in this case, the 0001 extension will always get
first kick at the cat.
But this approach requires manual filename manipulation, and is therefore a little clunky and fragile. Is there a better way?
A better way: ext-load-order.txt
ExtensionManager will automatically look for an optional file called ext-load-order.txt in the extension
jar directory. If present, this file can specify the order of extensions by naming one jar file per line
within the file. For example:
# ext-load-order.txt
#
# Blank lines and lines starting with # are ignored
important_extension.jar
less_important_extension.jar
Any jars named in this file will be loaded in the order specified BEFORE the rest of the jars in the extension jar directory are loaded. So, if a jar file is not explicitly mentioned in this text file, it will be loaded after any jar that is mentioned.
If the file is not present, load order will revert to the default load order described above.
Verifying extension load order
ExtensionManager will output log information as extensions are loaded. This can be used to easily verify
that extensions are being loaded in the expected order. For example:
2025-06-11 09:04:54 P.M. [INFO] Extension loaded externally: Important Extension
2025-06-11 09:04:54 P.M. [INFO] Extension loaded externally: Less important extension
2025-06-11 09:04:54 P.M. [INFO] Extension loaded externally: Really unimportant extension
Here we see the expected results. You can increase the log level for the ca.corbett.extensions package to FINE
if you wish to see more information about the extensions that are being scanned and loaded, but the default
INFO log level will show you basic information that is sufficient for verifying load order.
Extension versioning
As your application goes through normal development and extension over time, you may find that it becomes
a bit cumbersome to match which versions of your extensions were written for which version of your application.
Of course, each extension publishes an extInfo.json as described in the section Registering extensions.
This json file explicitly says which version of the app that extension targets. But, this is not immediately obvious
from looking at the extension jar directory. For example:
app-extension-name-1.0.jar
app-extension-name-1.1.jar
app-extension-name-1.2.jar
app-extension-name-1.3.jar
We see we've gone through a few versions of the extension, and there's no way to tell at a glance what app version each of these extensions intended to target.
A versioning convention
For this reason, the following convention is suggested (nothing in the code enforces this - you can version your extensions and your application however you like... but this might avoid headaches):
- Extensions should use the major.minor version of the application
- Extensions can add a patch version for multiple releases for a specific app version
This convention allows us to easily determine compatibility just by looking at the jar files:
app-extension-name-2.1.0.jar (targets app version 2.1)
app-extension-name-2.1.1.jar (targets app version 2.1)
app-extension-name-2.2.0.jar (targets app version 2.2)
app-extension-name-3.0.0.jar (targets app version 3.0)
And so on. Now the guesswork is removed and we know which jar files apply to the version of the application we're using.
Extension resources
It's a fairly common use case for extensions to package some resource that needs to be loaded. For example,
an icon to display on a button, or an audio file to play when certain actions happen. Of course, these resources
can be packaged into the jar file and then loaded via getClass().getResourceAsStream().
However! There's an odd bug that requires this to be done only in the constructor of the extension.
Attempting to invoke getResourceAsStream() from any method other than the extension constructor will
simply return null. See the comments on swing-extras issue 34
for a little more details.
This also applies to loading the extInfo.json file as a resource in your extension. For example:
public class MyExtension extends MyAppExtension {
private final AppExtensionInfo extInfo;
private final BufferedImage someIcon;
public MyExtension() {
extInfo = AppExtensionInfo.fromExtensionJar(getClass(), "/path/to/extInfo.json");
if (extInfo == null) {
throw new RuntimeException("MyExtension: can't load extInfo.json!");
}
try {
someIcon = ImageUtil.loadFromResource(getClass(), "/path/to/icon.png", 32, 32);
}
catch (IOException ioe) {
throw new RuntimeException("MyExtension: can't load icon resource!", ioe);
}
}
}
As long as all resources are loaded in the extension constructor, as depicted above, they will be loaded as normal. Attempting to lazy-load any resources after construction will result in those resources failing to load.
The cause of this is currently unknown. Pull requests welcome! :)
Sharing extension config
Generally, extensions that expose config properties will expose properties that are specific to that extension. This is enforced by specifying a unique fully-qualified property name, as described in the properties section. For example:
// In our custom extension class:
@Override
protected List<AbstractProperty> createConfigProperties() {
return List.of(new BooleanProperty("CustomExtension.CustomExtension.fieldName", "My checkbox"));
}
This will create a new tab on the properties dialog called CustomExtension with a checkbox labeled "My checkbox".
Not only is the name unique, but this property will be physically separated by the others by placing it into
a new tab just for this extension.
But, sometimes, we have a set of related extensions that might share some common configuration properties. For example, there might be multiple extensions for a document-processing application. These related extensions might each add a different kind of annotation or footnote to the current document. The font size, face, and style of these annotations can be controlled with config properties. But, we don't want to clutter the properties dialog with multiple properties for font selection. Can we set it up so that all of our related annotation extensions use the same font property? Yes we can!
The solution is for each extension to specify a property with the same fully qualified name. ExtensionManager is
smart enough to filter out duplicates from the combined list. This means that only one property will be created
for each unique name, regardless of how many extensions expose that property. To illustrate the above scenario,
each of our annotation extensions could have the same implementation of createConfigProperties:
@Override
protected List<AbstractProperty> createConfigProperties() {
return List.of(new FontProperty("Annotations.Font.fontSelector", "Annotation font:"));
}
Now only one font control will be added to the properties dialog, and all extensions that registered a config property with that name will be able to query the value of it. This allows the user to set the annotation font settings just once, and all annotation extensions (regardless of how many there are!) will use those settings. Even if a new annotation extension is added later, if it also registers a property with the same name, then it will also be able to immediately make use of the user's preferred annotation font setting.
It's just that easy!
Real-life examples
The app-extensions library is not just hypothetical code - it's been used in several applications to great effect! Here are just a few examples of actual extensions:
MusicPlayer
The MusicPlayer application has had extension capabilities for several versions now. Extensions can provide additional functionality not provided by the application itself. In particular, extensions can provide new fullscreen visualization effects to show while music is playing.
The Scenery extension for MusicPlayer is an interesting example. It shows gently scrolling landscape scenery images, with programmable "tour guides" that can appear at configured intervals to offer commentary either on the currently playing track, or on the currently shown scenery image:

ImageViewer
The ImageViewer application was also designed with extension in mind. Almost all built-in functionality within the app is extensible, allowing the creation of extensions that can do almost any kind of manipulation on the currently shown image. Here are some example extensions:
- Image Transform - rotate or flip an image
- Image Resize - resize an image or a directory of images
- Image Converter - convert an image or a directory of images between jpg and png formats
- Full Screen - presents a directory of images as a fullscreen slideshow
It starts with extensible application design
ImageViewer is actually a good example of an application that was designed with extensibility in mind. Take a look in particular at the ImageViewerExtension abstract class to see what kind of functionality is exposed to ImageViewer extensions. The ImageViewerExtensionManager class handles interrogation of currently loaded and enabled extensions to see what features they offer.
It's difficult, but not impossible, to add extension capabilities to an application as an afterthought. It's therefore recommended to think about possible extension points early in the application's development!
Image handling
The ca.corbett.extras.image package contains some utility classes and components related
to image handling. This includes not only wrappers around Java's built-in image IO facilities,
but also extra features and conveniences for making images much easier to deal with in
your application.
ImageUtil
The ImageUtil utility class provides convenient wrappers around some functionality in Java's image IO library.
Loading and saving images
We get the following utility methods (javadoc omitted for brevity):
public static ImageIcon loadImageIcon(final File file) {...}
public static ImageIcon loadImageIcon(final URL url) {...}
public static BufferedImage loadImage(final URL url) throws IOException {...}
public static BufferedImage loadImage(final File file) throws IOException {...}
public static void saveImage(final BufferedImage image, final File file) throws IOException {...}
public static void saveImage(final BufferedImage image, final File file, float compressionQuality) {...}
public static void saveImage(final BufferedImage image, final File file, ImageWriteParam writeParam) {...}
public static void saveImage(final BufferedImage image, final File file, ImageWriter writer, ImageWriteParam writeParam) {...}
We see that we have numerous options for easily loading and saving images in the following formats:
- jpeg, with configurable compression options
- png for lossless image storage, includes support for transparency
- gif, including support for animated gifs
Generating thumbnail images
ImageUtil also contains facilities for easily generating "thumbnail" images (these are scaled-down versions
of larger images, suitable for use as an image preview):
public static BufferedImage generateThumbnail(final File image,
final int width,
final int height) throws IOException {...}
public static BufferedImage generateThumbnail(final BufferedImage sourceImage,
final int width,
final int height) {...}
public static BufferedImage generateThumbnailWithTransparency(final File image,
final int width,
final int height) throws IOException {...}
public static BufferedImage generateThumbnailWithTransparency(final BufferedImage sourceImage,
final int width,
final int height) {...}
We see that it can work either with images already in memory (in the form of a BufferedImage), or with
images saved on disk (sparing you from having to do the I/O).
ImagePanel
The ImagePanel is a custom JPanel implementation that can display an image with many options
for determining how the image is to be displayed. All images types are supported - even animated GIF
images can be displayed!

Here we see that the image to be displayed does not perfectly fit into the ImagePanel. That's not
a problem - we select the BEST_FIT display option, and the image will be scaled down to fit inside
the panel, without stretching or distorting the image in the process. This may result in empty space
either above and below the image (as seen above), or on the left and right sides of the image,
depending on whether the image to be displayed is too wide or too tall to fit inside the panel.
We can change the display option to STRETCH if we want the image to completely fill the panel, but
this will result in distorting the image to make it fit:

We also have options for allowing the mouse wheel to zoom in and out of the image, and allowing
to click+drag the mouse when zoomed in, to scroll the image around in the panel. ImagePanel
also offers performance-related options for deciding whether to prioritize image quality versus
speed when doing things such as scaling and zooming.
Animated GIFs
The ImagePanel class offers native support for animated GIF images! This is actually supplied
by the built-in Java class ImageIcon.
if (imageFile.getName().toLowerCase().endsWith(".gif")) {
ImageIcon icon = new ImageIcon(imageFile.getAbsolutePath());
imagePanel.setImageIcon(icon);
}
else {
BufferedImage image = ImageUtil.loadImage(imageFile);
imagePanel.setImage(image);
}
The above example code will invoke the correct ImagePanel method depending on whether
the image is a GIF image or not. Generally speaking, ImagePanel prefers dealing with
BufferedImage instances, because we can store the entire raster of pixels in memory
to make it easier to handle things like scaling or stretching. But, animated GIF images
must be represented by an ImageIcon instance, so slightly different handling is required.
All of the same display options still work! You can stretch, scale, and zoom animated GIF images even as the animation is playing. This can either be done programmatically or via user input (via mouse clicks or mouse wheel, if those options are enabled in the image panel).
ImageTextUtil
With the ImageTextUtil utility class, we can easily render text onto an image, with many
configuration options:

Let's look at the methods that we have available to us here (javadoc omitted for brevity):
public static void drawText(BufferedImage image, String text) {...}
public static void drawText(BufferedImage image, String text, Font font, Color outlineColor, Color fillColor) {...}
public static void drawText(BufferedImage image, String text, Font font, Color outlineColor,
Color fillColor, TextAlign align) {...}
public static void drawText(BufferedImage image, String text, int lineLength, Font font,
TextAlign align, Color outlineColor, float outlineWidthFactor, Color fillColor) {...}
public static void drawText(BufferedImage image, String text, int lineLength, Font font,
TextAlign align, Color outlineColor, float outlineWidthFactor,
Color fillColor, BufferedImage fillTexture, Rectangle rect) {...}
We see that the simplest option simply takes the BufferedImage onto which we should render the text,
and the text itself. There are more and more complicated overloads of this method to expose further
configuration options for rendering the text.
We can use the lineLength parameter to determine at what point (based on character count) line-wrapping
should occur. If not specified, the default line length is 30 characters.
We can also control the positioning of the text within the image using the TextAlign enum:
public enum TextAlign {
TOP_LEFT("Top left"),
TOP_CENTER("Top center"),
TOP_RIGHT("Top right"),
CENTER_LEFT("Center left"),
CENTER("Dead center"),
CENTER_RIGHT("Center right"),
BOTTOM_LEFT("Bottom left"),
BOTTOM_CENTER("Bottom center"),
BOTTOM_RIGHT("Bottom right");
//...
}
Gradients
The ca.corbett.extras.gradient package contains some neat utilities and components related
to color gradients. Let's start with a visual demonstration of the capabilities, and then take
a look at the code.

We can also render text with a color gradient:

Integration with swing-forms
The ColorField in swing-forms understands both solid colors or color gradients, and allows
you to present a form field for user selection of both or of either. You can also use the
GradientColorChooser to allow your users to configure a color gradient interactively if you
are not working with swing-forms.
Integration with properties
The ColorProperty property in ca.corbett.extras.properties understands both solid colors
or color gradients, and allows you to create a property that allows the user to choose either
one, or restricts them to one or the other.
Other integrations
If your application uses a JDesktopPane, you may want to consider upgrading to the
swing-extras class CustomizableDesktopPane, which allows you
to use a gradient as a background.
Of course, the gradient images generated by the GradientUtil are compatible with
ImageUtil, so you can easily generate gradient images and save them.
How do I generate gradients?
Everything is driven from the Gradient record, which is very easy to set up:
public record Gradient(GradientType type, Color color1, Color color2) {
public static Gradient createDefault() {
return new Gradient(GradientType.VERTICAL_STRIPE, Color.WHITE, Color.BLACK);
}
}}
This record contains a static factory method for creating a "default" gradient quickly
and easily, or you can create a new one by specifying the start and end colors for the gradient,
and then selecting a GradientType:
public enum GradientType {
/**
* Describes a single gradient that progresses linearly from left to right, color 1 to color 2.
*/
HORIZONTAL_LINEAR("Horizontal linear"),
/**
* Describes a single gradient that progresses linearly from top to bottom, color 1 to color 2.
*/
VERTICAL_LINEAR("Vertical linear"),
/**
* Describes a two-part gradient that progresses linearly from top to center, color 1 to
* color 2, and then center to bottom, color 2 to color 1. The end result is a gradient that
* looks like a horizontal stripe running along the center of the image.
*/
HORIZONTAL_STRIPE("Horizontal stripe"),
/**
* Describes a two-part gradient that progresses linearly from left to center, color 1 to
* color 2, and then center to right, color 2 to color 1. The end result is a gradient that
* looks like a vertical stripe running up the center of the image.
*/
VERTICAL_STRIPE("Vertical stripe"),
/**
* Represents a single gradient that progresses linearly from top left to bottom right,
* color 1 to color 2.
*/
DIAGONAL1("Diagonal 1"),
/**
* Represents a single gradient that progresses linearly from bottom left to top right,
* color 1 to color 2.
*/
DIAGONAL2("Diagonal 2"),
/**
* Describes a four-part gradient that progresses linearly from each corner of the image
* towards the center, color 1 to color 2.
*/
STAR("Star");
// ...
}
From there, we can talk to the GradientUtil class, which has methods for actually rendering the gradient:
public static BufferedImage createGradientImage(Gradient conf, int width, int height) {...}
public static void fill(Gradient conf, Graphics2D graphics, int x1, int y1, int width, int height) {...}
public static void drawRect(Gradient conf, Graphics2D graphics, int x1, int y1, int width, int height) {...}
public static void drawString(Gradient conf, Graphics2D graphics, int textX, int textY, String string) {...}
public static void drawString(Gradient conf, Graphics2D graphics, int gradientX1, int gradientY1,
int gradientX2, int gradientY2, int textX, int textY, String string) {...}
We see that every method requires a Gradient record which describes the gradient to be rendered, and then there
are options to control the size and shape of the gradient. Very easy to use, and can produce quite nice results!
LogoGenerator
It's worth mentioning an older class that's still around in swing-extras, and that is LogoGenerator.
This is the class that ultimately led to the creation of ImageTextUtil.
LogoGenerator has been used for years as a way to very quickly and easily generate very small,
text-based images, which can be suitable for use for application logo images or banner images
to be shown in an application. It can also be used to generate small square images suitable for
an application icon.

Generator logo images
We start by populating a LogoProperty object describing the image to be generated:
public final class LogoProperty extends AbstractProperty {
// ...
private ColorType bgColorType;
private Color bgColor;
private Gradient bgGradient;
private ColorType borderColorType;
private Color borderColor;
private Gradient borderGradient;
private ColorType textColorType;
private Color textColor;
private Gradient textGradient;
private int borderWidth;
private boolean hasBorder;
private boolean autoSize;
private Font font;
private int logoWidth;
private int logoHeight;
private int yTweak;
// ... getters and setters omitted...
}
Once we have configured the properties we want, we can talk to the LogoGenerator class:
public final class LogoGenerator {
// ...
public static BufferedImage generateImage(String text, LogoProperty preset) {...}
public static void generateAndSaveImage(String text, LogoProperty preset, File outputFile) throws IOException {...}}
}
We see that the API here is quite simple. Just specify the text to render and your LogoProperty object,
and decide whether you want to receive a BufferedImage generated in memory, or whether you want to save
the resulting image directly to disk. Easy!
Animation
There are some animation classes in swing-extras that are well-suited for fullscreen applications!
AnimatedTextRenderer

The AnimatedTextRenderer can be easily wired into an animation loop to type out a text message
with configurable styling at a configurable rate (in characters typed per second). Line wrap
is handled automatically and the animation can be paused/restarted at any point. An optional
cursor can be shown as the text is typed out, and the cursor can be optionally made to blink
as it moves.
ImageScroller

ImageScroller is more intended for fullscreen applications rather than Swing desktop applications. It can scroll an oversized image, either horizontally or vertically, with a configurable "bounce" when a scroll limit is reached. This "bounce" causes the image scrolling to slow as a scroll limit is reached, and then speed up again gradually after the scrolling reverses direction.
For an actual example of this, I refer you to the Scenery extension for MusicPlayer.
Logging utilities
Almost every application wants to keep a log of things that it does or problems that it encounters
along the way. Usually, this log output is dumped to the console or to a log file somewhere.
But within swing-extras we have some extra logging options which can make log output
a little easier to parse through.
LogConsole
You can very easily wire up a LogConsole instance to give your application a way to present log information
to the user. This saves them the trouble of hunting down the log file or keeping the console handy so that
they can read it:

We enable this by modifying our logging.properties file to include our custom log handler:
handlers=java.util.logging.ConsoleHandler,ca.corbett.extras.logging.LogConsoleHandler
Here we see we are writing both to the console and also to our own LogConsoleHandler. What does this
do for us? Well, at runtime, we can summon the LogConsole:
LogConsole.getInstance().setVisible(true);
This gives the users a way to visually see all log output up to this point. Leaving the LogConsole window open (it's not modal, so it can sit in the background while you work with the application) can allow you to keep an eye on it.
But this is arguably no different than just tailing a log file. So what's the use of this?
Custom styling and log themes
LogConsole allows us to set up LogConsoleStyle and LogConsoleTheme objects. A theme is just a collection
of styles, so let's look at LogConsoleStyle:
public final class LogConsoleStyle {
// ...
public void setLogLevel(Level logLevel) {...}
public void setLogToken(String logToken, boolean isCaseSensitive) {...}
public void setFontColor(Color fontColor) {...}
public void setFontBgColor(Color fontBgColor) {...}
public void setFontFamilyName(String family) {...}
public void setIsBold(boolean isBold) {...}
public void setIsItalic(boolean isItalic) {...}
public void setIsUnderline(boolean isUnderline) {...}
public void setFontPointSize(int fontPointSize) {...}
// ...
}
What is it that we're setting up here, exactly? Well, LogConsole can scan log messages in real time and apply
styling rules to them based either on the log level (INFO, WARNING, SEVERE, and etc), or based on the content
of the log messages themselves. In the screenshot at the top of the page, we saw that LogConsole out of the
box automatically applies a different styling to WARNING or SEVERE messages, to make them stand out. We can
not only override that behaviour, but we can in fact extend it by adding our own styles. You can then make
use of this in your application by having certain log messages contain certain string tokens that might
be more relevant to your users than other log messages.
For example:
- I want messages that contain "mysql" to appear in blue text so I can see my database calls
- I want messages that contain "removed" or "deleted" to appear in orange so I can see those operations
- I want messages that contain an "@" to appear bold so I can spot log messages that include email addresses
- etc etc
This is very easy to do in LogConsole, and it might look something like this:

We've configured it so that any log message that contains the string "myToken" will be formatted in a different
color (and optionally a different font or style if you wish). This helps them visually stand out in the LogConsole
in a way that they wouldn't in a console if you were just tailing a log file.
Matching styles to log messages
The LogConsole will ask the current theme for a matching LogConsoleStyle to use for each log message that comes in. Style matching is done either by matching a log Level, or by matching a string token that appears in a log message, or both.
- If a log message matches a style's log token and its log level, that style is considered a strong match for that log message.
- If a log message matches a style's log token, but the style doesn't specify a log level, it is still considered a strong match.
- If a log message matches a style's log level, but the style doesn't specify a log token, then this is considered a weak match. That means that this style will match only if no other style matches the log message.
- If no style matches, either by log token or by log level, then the default style from the current theme will be used for that log message.
LogConsole is not a substitute for file logging!
Of course, your application should also be logging to a log file, because LogConsole is only available while
your application is running and makes no attempt to persist its contents. So, if your application crashes
unexpectedly, you will have lost your log information.
LogConsole is intended to supplement your usual log process, NOT replace it. It's just a handy way to
view log information at runtime.
Stopwatch
Another handy utility class in swing-extras is the Stopwatch class.
When performance testing your code, it's often the case that you want to know how long a particular section of code takes to execute, so you can measure the success (or failure) of your optimization efforts. A fairly standard approach is to do something like this:
long startTimeMs = System.currentTimeMillis();
// Do something that might eat a few CPU cycles...
long elapsedTimeMs = System.currentTimeMillis() - startTimeMs;
logger.info("That took "+elapsedTime+"ms.");
This approach is usually "good enough" but isn't very pretty. We can make it somewhat easier to work
with by using the Stopwatch class:
Stopwatch.start("myTimer");
// Do something that might eat a few CPU cycles...
Stopwatch.stop("myTimer");
logger.info("That took "+Stopwatch.reportFormatted("myTimer"));
This code is functionally equivalent to what we had before, with a couple of important distinctions:
- We can have multiple timers running concurrently by giving them unique names.
- We can invoke
report()to get the raw millisecond count, orreportFormatted()to get a human-readable version.
A more complex example:
Stopwatch.start("totalProcess");
for (int i = 0; i < someHighNumber; i++) {
doSomething();
doSomethingElse();
Stopwatch.start("criticalInnerSection");
doSomethingCritical();
Stopwatch.stop("criticalInnerSection");
logger.fine("Critical inner section took "+Stopwatch.report("criticalInnerSection")+"ms.");
}
Stopwatch.stop("totalProcess");
logger.info("Total process took "+Stopwatch.reportFormatted());
We can start and stop as many timers as we need and they can run concurrently without interfering with one another.
General components
Let's look at some miscellaneous features in swing-extras that can make your
development activities a little easier!
Look and Feel
By default, Java Swing applications have access to a small number of "look and feels" that can change
how components are rendered and which colors are used to display them. The swing-extras library
packages additional look and feels from FlatLaf and JTattoo that go beyond basic customization and
offer a wide variety of options.
We can use the LookAndFeelManager and LookAndFeelProperty classes to expose these options to the user.
Option 1: set a Look and Feel programmatically
On startup, you can simply pick one of the available options and set your application to use it:
public static void main(String[] args) {
LookAndFeelManager.installExtraLafs();
LookAndFeelManager.switchLaf(FlatLafDark.class.getName());
// Now initialize and show your main window...
}
This is the simplest option, and the users of your application will always have the same consistent Look and Feel.
Option 2: Allow the user to select look and feel
You can optionally expose a LookAndFeelProperty to your users to allow them to pick a look and feel.
LookAndFeelProperty prop = new LookAndFeelProperty("UI.LookAndFeel", "Look and feel:", FlatLafLight.class.getName());
The above property defaults to FlatLafLight, but gives your users a dropdown to choose from all available options:

To switch to the selected look and feel, we simply need to call upon LookAndFeelManager:
LookAndFeelManager.switchLaf(prop.getSelectedLafClass());
The LookAndFeelManager will handle updating the UI to reflect the new selection.
Try it out!
You can try this in the swing-extras demo app, on the introduction panel!

AboutPanel
Most applications want to show basic information about themselves, such as what version they are
and when they were published, etc. Rather than rewriting the UI code for this for each new application,
wouldn't it be nice if you could simply populate an instance of an AboutInfo POJO and hand it to
some utility that would generate the UI for you in a consistent way? Well, now there is!
Basic usage
AboutInfo contains many fields, most of which are optional. If an optional
field is not specified, it will not be included in the resulting dialog or panel.
Here's an example of setting up an AboutInfo instance for a fictional application:
AboutInfo aboutInfo = new AboutInfo();
aboutInfo.license = "https://opensource.org/license/mit";
aboutInfo.copyright = "Copyright © 2025 Your Name Here";
aboutInfo.projectUrl = "https://example.com/MyAmazingProject";
aboutInfo.applicationName = "My Amazing Project";
aboutInfo.applicationVersion = "1.0.0";
aboutInfo.shortDescription = "This project will change the world...";
Now, we can hand this AboutInfo object to an AboutPanel or to an AboutDialog:
new AboutDialog(myMainWindow, aboutInfo).setVisible(true);

We see a logo image was automatically generated for us, because we didn't specify one, and we see that the links to our project URL and license URL were automatically made clickable (if the JRE supports desktop browsing). We also note that JVM memory usage stats were added automatically.
But this dialog looks a little plain. Can we improve it a bit? Yes, we can...
Customizing the logo image
The default generated logo image looks a bit bland, but we're seeing that because
we didn't specify one of our own. Let's hire a graphic designer to come up with
something more visually interesting, and then we can specify it in our AboutInfo:
aboutInfo.logoImageLocation = "/myapp/images/logo.jpg";

Looks better! The logo image is recommended to be around 480x90 pixels, but
you can experiment to see what works. You can also look at AboutInfo.LogoDisplayMode
to learn about how the logo image can be positioned on the form.
Let's continue customizing the dialog for our application:
Specifying release notes
Another common use of an About dialog is to show the application release notes.
With AboutInfo, you can either specify these as a text file in your resources
somewhere, or just as a text string:
// Option 1: inline text:
String releaseNotes = "RELEASE NOTES:\n\n"
+ "v1.0.0 - Initial release!\n";
aboutInfo.releaseNotesText = releaseNotes;
// OR Option 2: specify a resource file:
aboutInfo.releaseNotesLocation = "/myapp/releaseNotes.txt";

If both options are specified, releaseNotesText will be used and releaseNotesLocation
will be ignored. If either option is specified, a read-only text area will
appear on the dialog, with a scrollbar if needed. Line wrapping is done automatically
as needed, or "\n" can be used for manual newlines.
Adding custom fields
Often it's nice to be able to supply custom information to the About dialog. This can be done in the form of name:value pairs of Strings:
aboutInfo.addCustomField("Messages received:", "10");
aboutInfo.addCustomField("Messages sent:", "4");

We now see that our custom fields Messages sent and Messages received appear
on the About dialog.
But these fields represent dynamic values that will change during the lifetime of the application. Can we change them once they are set?
aboutInfo.updateCustomField("Messages received:", "99");
aboutInfo.updateCustomField("Messages sent:", "50");

We see that the values have been updated. We can call updateCustomField whenever
our custom field values change. Any AboutPanel or AboutDialog that is currently
showing will be updated immediately, and any panel or dialog that is shown with
that AboutInfo object from that point on will also reflect the latest values.
Panel vs Dialog
Sometimes, you may not want to show this information in the form of a popup
dialog, but rather embedded somewhere within one of your application windows.
You can use AboutPanel for this purpose instead of AboutDialog:
AboutPanel aboutPanel = new AboutPanel(aboutInfo);
myContainerPanel.add(aboutPanel);
Now your AboutPanel can be embedded wherever you like within your application.
Behind the scenes, AboutDialog also uses an AboutPanel instance, so the
look and feel, and the contents are guaranteed to be the same.
AudioUtil
AudioUtil provides convenience methods for loading and playing audio clips in wav format.
Java unfortunately stopped supporting the mp3 format out of the box, but you can download
a 3rd party library to add support for that format if you wish. See my
musicplayer application for an example.
Let's look at some of the handy methods in AudioUtil (javadocs omitted for brevity):
public static PlaybackThread play(File audioFile, PlaybackListener listener) {...}
public static PlaybackThread play(BufferedInputStream inStream, PlaybackListener listener) {...}
public static PlaybackThread play(int[][] audioData, PlaybackListener listener) {...}
public static PlaybackThread play(File audioFile, long offset, long limit, PlaybackListener listener) {...}
public static PlaybackThread play(BufferedInputStream inStream, long offset, long limit, PlaybackListener listener) {...}
public static PlaybackThread play(int[][] audioData, long offset, long limit, PlaybackListener listener) {...}
public static int[][] parseAudioFile(File file) throws UnsupportedAudioFileException, IOException {...}
public static int[][] parseAudioStream(BufferedInputStream inStream) {...}
public static void saveAudioFile(File file, int[][] audioData) throws IOException {...}
public static AudioInputStream getAudioInputStream(int[][] audioData) {...}
public static BufferedImage generateWaveform(File file) throws UnsupportedAudioFileException, IOException {...}
public static BufferedImage generateWaveform(BufferedInputStream audioStream) {...}
public static BufferedImage generateWaveform(File file, WaveformConfig prefs) {...}
public static BufferedImage generateWaveform(BufferedInputStream audioStream, WaveformConfig prefs) {...}
public static BufferedImage generateWaveform(int[][] audioData, WaveformConfig prefs) {...}
Full javadocs are available online: http://www.corbett.ca/swing-extras-javadocs
We can see that we have many options for loading and playing audio clips, and also for generating a "waveform image". But what is a waveform image?
Generating an audio waveform image and editing it
The WaveformConfig and AudioWaveformPanel classes allow you to
visualize an audio waveform by generating a visual representation
of the audio waveform data. Once you have an audio clip loaded into an
AudioWaveformPanel, you also optionally have controls to manipulate it:

The controls on the left allow you to play the current clip, stop playing, or record a new clip to replace the clip currently being displayed. The controls on the right allow you to cut, copy, or paste based on mouse selections you make within the panel. The position and size of these controls are customizable:
// Make the controls x-small and put them in top-left position:
audioWaveformPanel.setControlPanelPosition(ControlPanelPosition.TOP_LEFT);
audioWaveformPanel.setControlPanelSize(ControlPanelSize.XSMALL);

You also have the ability to disable recording within the panel, to force it to be for playback only:
// Make the controls normal size, put them bottom-center, and disable recording:
audioWaveformPanel.setControlPanelPosition(ControlPanelPosition.BOTTOM_CENTER);
audioWaveformPanel.setControlPanelSize(ControlPanelSize.NORMAL);
audioWaveformPanel.setRecordingAllowed(false);

And you can disable the cut/copy/paste functionality to make the panel truly read-only:
// Make the controls large in bottom-left, and truly read-only:
audioWaveformPanel.setControlPanelPosition(ControlPanelPosition.BOTTOM_LEFT);
audioWaveformPanel.setControlPanelSize(ControlPanelSize.LARGE);
audioWaveformPanel.setRecordingAllowed(false);
audioWaveformPanel.setEditingAllowed(false);

If editing is enabled, you can click and drag in the panel to select a portion of the audio to be cut or copied:

And you can left-click in the panel to set an insertion point for paste operations:

Customizing the display
Of course, the default grey and black and white display may be a bit drab and boring.
Naturally, it's fully customizable, using the WaveformConfig class:
WaveformConfig waveformConfig = new WaveformConfig();
waveformConfig.setBgColor(Color.BLACK);
waveformConfig.setFillColor(Color.BLUE);
waveformConfig.setOutlineColor(Color.GREEN);
audioWaveformPanel.setWaveformPreferences(waveformConfig);

Fine-tuning waveform image generation
If the input audio clip is very long, this may result in very wide images,
as by default AudioUtil does not limit the output image width. You can control
this with setXLimit:
// Scale the waveform down so it will fit into 1024 horizontal pixels:
waveformConfig.setXLimit(1024);
You also have options for controlling the x and y scaling values that AudioUtil will use when generating the resulting image, but this relies on knowing how much audio data will be present in the input clip:
waveformConfig.setXScale(4096); // default is 1024 but let's make it smaller
waveformConfig.setYScale(32); // default is 64 but let's make it taller
By adjusting xLimit, xScale, and yScale, you can create images of different
shapes and sizes to represent your audio data.
Real-world example
For a real-world example of how AudioUtil can be used in an application, I refer
you to my own musicplayer application:

DesktopPane
If your application uses JDesktopPane, you may be frustrated with the lack of options for
customization. Meet the CustomizableDesktopPane!

The idea here is that you can add a color gradient background (using the GradientConfig which we will discuss later in this guide), and also display a custom logo in some corner of the desktop.
You can programmatically change the positioning and the transparency of the logo image:

This is purely cosmetic, of course, but it can be accomplished with only a few lines of code, and immediately
makes your JDesktopPane-based application look a lot nicer (and much more customizable for your users,
if you decide to expose the gradient configuration to them).
Real-world example
For a real-world example of the use of this component, I refer you to my own Snotes application:

DirTree
DirTree is a component that gives you a read-only view onto a file system, with the optional ability to "lock" the view (chroot-style) to a specific directory.

Right-clicking on the tree will allow you to lock the tree to the given node:

This has an effect similar to chroot, in that the DirTree component now
views that directory as the root of the filesystem, and can only see directories
underneath it:

You can then right-click again to unlock the tree.
Locking and unlocking can be allowed or disallowed programmatically:
// Create a DirTree for my home directory and disallow locking/unlocking:
DirTree myDirTree = DirTree.createDirTree(new File("/home/scorbett"));
myDirTree.setAllowLock(false);
myDirTree.setAllowUnlock(false);
Detecting and responding to events
Of course, displaying a read-only view of a file system is not terribly useful without the ability to detect user selection events:
myDirTree.addDirTreeListener(new DirTreeListener() {
@Override
public void selectionChanged(DirTree source, File selectedDir) {
// The selection has changed
}
@Override
public void treeLocked(DirTree source, File lockDir) {
// Tree has been locked to lockDir
}
@Override
public void treeUnlocked(DirTree source) {
// Tree has been unlocked
}
});
Now you can respond to selection changes by, for example, displaying a list of files in the current directory in some other part of your application UI.
Programmatic selection
You can also programmatically lock the DirTree to any
particular directory, regardless of whether locking is enabled or disabled
for the user:
myDirTree.lock(new File("/some/other/dir"));
And you can select a particular directory, and cause the DirTree to scroll
if necessary so that it is visible in the view:
myDirTree.selectAndScrollTo(new File("/some/nested/dir/somewhere/else"));
Real-world example
For a real-world usage of the DirTree component, I refer you to my own imageviewer application:

Progress utilities
Java comes with a couple of useful classes out of the box:
ProgressMonitoris useful for showing a very simple progress dialogSplashScreenis useful for showing an image as your application starts up
But both of these are very limited in what they can do. Can we improve upon them? Yes we can!
MultiProgressDialog
Often it's useful not just to show progress as a linear list of steps (eg. processing file 3 of 10), but rather as a list of major and minor progress steps. For example, when processing a large number of files recursively in a large directory tree, it might be useful to have a major progress bar to show the number of directories to be processed, and a minor progress bar to show the number of files in the directory that is currently being processed. Let's look at MultiProgressDialog:

Setting up a MultiProgressDialog is reasonably straightforward. We start by extending
the MultiProgressWorker class and implementing the run method:
public class MyWorker extends MultiProgressWorker {
@Override
public void run() {
// worker logic goes here
}
};
We can use the various fire... methods in the parent class to let listeners know
what we're up to. This is very important, as we'll see later:
@Override
public void run() {
fireProgressBegins(majorStepCount);
boolean wasCanceled = false;
boolean isComplete = false;
while (!wasCanceled && !isComplete) {
wasCanceled = !fireMajorProgressUpdate(...);
for (int i = 0; (i < minorStepsCount) && !wasCanceled; i++) {
wasCanceled = !fireMinorProgressUpdate(...);
// Do some work here...
}
if (noMoreMajorStepsToDo()) {
isComplete = true;
}
}
if (wasCanceled) {
fireProgressCanceled();
} else {
fireProgressComplete();
}
}
The basic skeleton of our run method fires a progressBegins message when we
start processing, a majorProgressUpdate message as we start to process each
major work step, and a minorProgressUpdate message as we start to process
each minor work step. Upon completion (or user cancellation via the Cancel button),
we fire either a progressCanceled message or a progressComplete message.
So, what is listening to all these messages? Our MultiProgressDialog, of course!
(You can also add your own listener to handle things like logging of each
work step or whatnot).
Once our MultiProgressWorker is ready to go, we can hand it to the
MultiProgressDialog to begin the work:
MultiProgressDialog dialog = new MultiProgressDialog(myMainWindow, "Progress");
dialog.runWorker(worker, true);
The dialog will show itself, update itself as the work progresses, and then
close itself when complete. If you attached a listener, you can receive notification
when the work is complete. The dialog presents a Cancel button which is checked
every time one of the fire... methods is invoked. If the method returns false,
the user clicked the Cancel button and processing should stop.
Showing only a single progress bar
Of course, not all tasks have subtasks that require a second progress bar. Sometimes,
you just want a simple, single-bar progress dialog. To accomplish this, you can create
a new SimpleProgressWorker instead of a MultiProgressWorker. The rest of the example
code above is the same! Just pass your SimpleProgressWorker into the runWorker method
as we did in the previous example, and the dialog will reconfigure itself to show
only one progress bar instead of two:

SplashProgressWindow
The built-in Java SplashScreen class is handy for showing a static image briefly
on the screen as your application starts up. But perhaps your application has to do
some expensive loading or preparation work as it starts up, and simply showing a static
image to the user is not very enlightening. Wouldn't it be nice if we could show
a progress bar with our splash screen so that the user can visually see the application
starting up? Let's look at SplashProgressWindow!

Everything you see is customizable. If you have a static image you wish to show, you can supply it to the constructor:
mySplashWindow = new SplashProgressWindow(Color.WHITE, Color.BLACK, myImage);
mySplashWindow.runWorker(myWorker);
The process for creating a worker thread is very similar to what we saw with
MultiProgressWorker above, except now we will extend the SimpleProgressWorker
class instead, which allows one single progress bar to show progress:
public class MyWorker extends SimpleProgressWorker {
@Override
public void run() {
boolean shouldContinue;
shouldContinue = fireProgressBegins(steps);
for (int i = 0; (i < steps) && shouldContinue; i++) {
shouldContinue = fireProgressUpdate(i, "");
// Do some loading work here
}
fireProgressComplete();
if (action != null) {
action.actionPerformed(new ActionEvent(thisWindow, 0, "Complete"));
}
}
}
Again we rely on the fire... methods in the parent class to fire off events so
that the progress bar can be updated as we do work. Since there is no Cancel button
on the splash screen, you may wonder why we still check the return value from these
methods. The reason is that other listeners attached to the worker have the option
of returning false from a message notification, indicating that processing should
stop. This gives you a way of programmatically canceling the dialog if some
condition fails during startup. As with MultiProgressDialog, the splash progress
window will automatically close itself when the loading thread completes or
when processing is canceled.
Generating the splash image dynamically
We can use the LogoConfig class from the LogoGenerator code to
generate a splash image programmatically if we don't have one premade. The example screenshot
above was in fact generated programmatically using LogoConfig:
String appName = "swing-extras";
LogoConfig config = new LogoConfig(appName);
// Set up background color gradient:
config.setBgColorType(LogoConfig.ColorType.GRADIENT);
GradientConfig gradient = new GradientConfig();
gradient.setColor1(Color.BLACK);
gradient.setColor2(Color.BLUE);
gradient.setGradientType(GradientUtil.GradientType.HORIZONTAL_STRIPE);
config.setBgGradient(gradient);
// Set text and border color for progress bar:
config.setTextColorType(LogoConfig.ColorType.SOLID);
config.setTextColor(Color.CYAN);
config.setBorderColorType(LogoConfig.ColorType.SOLID);
config.setBorderColor(Color.CYAN);
// Set border thickness and image dimensions:
config.setBorderWidth(2);
config.setLogoWidth(400);
config.setLogoHeight(100);
new SplashProgressWindow(myMainWindow, appName, config).runWorker(myWorker);
The progress image is generated for us and we don't have to hire a graphic designer:

MessageUtil
Often, your application will need to show an informational, warning, or error message to the user. Usually,
you will want to log that message also. This can of course be done in two steps, but wouldn't it be nice
if there were a handy utility to bring those two things together? Meet MessageUtil!
MessageUtil needs a parent component (for displaying informational dialogs) and a Logger instance (for
writing log messages). For example:
MessageUtil messageUtil = new MessageUtil(MainWindow.getInstance(), Logger.getLogger(getClass().getName()));
Now you can easily display informational, warning, or error messages, and have them simultaneously go out to the log file:
messageUtil.error("Load error", "Error loading data!", exception);
If an exception is supplied, it will be included in the log output (But not in the displayed message).
The interface to MessageUtil is pretty straightforward:
public void error(String message) { ... }
public void error(String title, String message) { ... }
public void error(String message, Throwable ex) { ... }
public void error(String title, String message, Throwable ex) { ... }
public void info(String message) { ... }
public void info(String title, String message) { ... }
public void warning(String message) { ... }
public void warning(String title, String message) { ... }
This utility saves a small amount of code every time you need to display a message to the user and log it at the same time.
Conclusion
In this guide, we've looked at some of the many possibilities offered by the swing-extras library
for your Java Swing applications. This library provides opportunities to speed up and/or simplify
your own application development efforts!
For real-world examples of usages of this library, I invite you view my other projects, some of which are available on my GitHub page: https://github.com/scorbo2
Snotes

ImageViewer

MusicPlayer

TaskTracker
![]()
Contributors
swing-extras and all documentation was written by Steve Corbett in Calgary, Alberta, Canada.
Email: steve@corbett.ca
GitHub: https://github.com/scorbo2
This documentation was generated with mdbook (not my project), which is amazingly easy to use.