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...