I spend a lot of time designing and creating graphical user interfaces. Hence, the bulk of my work is not to design object oriented architectures or think of complicated algorithms. Instead, I mostly tweak values: colors, opacities, gradients, rendering hints and so forth. Creating nice mockups in an image manipulation program is unfortunately not enough and I have to adapt the design to Java SE limitations, my liking and colleagues suggestions. As you can imagine, tuning a value for the umpteenth time in the day to rerun the application and check the result can become somewhat tedious. Unfortunately, this is exactly what I have been doing lately for an internal project at Sun. When the project started I knew I would soon beg for mercy or eat my keyboard out of despair if I didn't find something to ease my pain.
This is why I created Fuse a small library designed to change GUI resources easily. It has other advantages like the ability to reload values at runtime and decoupling the resource loading code from the GUI. (To sound smart and wise I say it's a resource injection library; but one day I'll have the same gray hair as Chet and I'll naturally look smart and wise even though I'll still be a fool - like Chet :) Anyway, Sun let me open source the project which will become a sub-project of SwingLabs as soon as I get the approval from java.net.
"Ye gods! Cut to the chase and show us some code!", are you probably yelling behind your screen by now. Before delving into this matter, I'd like to warn you the API is far from being finalized yet. The names are likely to change. (hitherto I was using the notion of "themes" and "theme resources" instead of the current broader definition) One last thing: you might hate the apparent trickery introduced by the use of Fuse. You will need two things: a Java class and a plain text file. The first step is to identify the resource you want to inject in your instances:
// A component showing a title on a colored background
class TitleComponent extends JComponent {
@InjectedResource
private Color foreground, background;
@InjectedResource
private Font font;
}
Each field marked by the annotation @InjectedResource
will be handled by Fuse. The second step is to inject values into those resources:
public static void main(String... args) {
TitleComponent t = new TitleComponent();
ResourceInjecter.inject("classic_theme", t);
}
The call to inject()
will browse the annotated fields from instance t
and give them a value found in a properties file called /resource/classic_theme.uitheme
(this will change, but this is how it works in the project from wich Fuse is born). As you can see, there is some black magic at work here since Fuse will assign a value to the private fields. Many might despise that, I think it's ok. You can also perform the injection in the component's constructor if you need the values right away. Now, here is the content of the properties file:
TitleComponent.background=#FFFFFF
TitleComponent.foreground=#000000
TitleComponent.font=Arial-BOLD-42
Depending on the type of each resource, Fuse will invoke a specific TypeLoader
which role is to create the appropriate value from the plain text. Each value is identified by the field name prefixed by the class name. The library ships with many type loaders: boolean, byte, character, color, composite (and alpha composite), double, file, float, font, gradient paint, image icon, image (and buffered image), insets, int, long, rendering hints, short, string, stroke (and basic stroke) and URL. You can also easily create your own type loaders.
The good thing with these type loaders is they allow you to define values in an easier way. For instance you can define a font with the format Face-Style-Size
but if Face is a TrueType font file, Fuse will load it for you. Another example is the rendering hints: if you want to use the value VALUE_ANTIALIASING_ON
for the key KEY_ANTIALIASING
, you can write one of the following:
MyComponent.hints=KEY_ANTIALIASING=VALUE_ANTIALIASING_ON
MyComponent.hints=key antialiasing=value antialiasing on
MyComponent.hints=antialiasing=antialiasing on
MyComponent.hints=antialiasing=on
Fuse will try to be smart and infer hints names (for instance a key "antialiasing" is actually KEY_ANTIALIASING, and its value "on" must be VALUE_ANTIALIASING_ON). Very often in a UI design you will reuse the same values over and over and Fuse makes this easy by allowing references:
Common.darkColor=#101010
MyComponent.shadowColor={Common.darkColor}
MyComponent.gradient=0,0 | 0,400 | {MyComponent.shadowColor} | #FFFFFF
A key surrounded by curly braces is interpreted as a reference. The library will detect circular references (for instance if Common.darkColor
was defined by {MyComponent.shadowColor}
) and will warn you about them. Speaking of which, every type loading can raise a TypeLoadingException
which tells exactly which property is wrong and why. (and you don't have to catch this exception, but you can)
Another nicety is the ability for a single type loader to inject a value into different types of a same class hierarchy. Take these declarations for instance:
@InjectedResource
private Image smallLogo;
@InjectedResource
private BufferedImage bigLogo;
Both these values will be injected by ImageTypeLoader
. Finally, you can reinject the values any time you want, and this is why I first named the ResourceInjecter
a Theme
, by calling inject()
. This really saves quite some time when you just want to adjust a value without having to create some annoying GUI for debug purpose only.
I have many ideas for Fuse. For instance, it'd be nice if it let you define the naming pattern of your properties files. (so that they don't have to be /resource/*.uitheme
) I also would like to introduce an optionnal component hive that would keep track of every component in which you injected resource. The hive would then be able to reinject values at runtime without you having to go through your instances.
No matter what, you will see this library in my future demos :)
As a bonus, here is an example of a real component I created for the aforementionned project:
class Footer extends JComponent {
@InjectedResource
private LinearGradientPaint backgroundGradient;
@InjectedResource
private int preferredHeight;
@InjectedResource
private Color lightColor;
@InjectedResource
private Color shadowColor;
Footer(final Channel channel) {
ResourceInjecter.inject(channel, this);
}
@Override
public Dimension getPreferredSize() {
Dimension size = super.getPreferredSize();
size.height = preferredHeight;
return size;
}
@Override
public Dimension getMaximumSize() {
Dimension size = super.getMaximumSize();
size.height = preferredHeight;
return size;
}
@Override
protected void paintComponent(Graphics g) {
if (!isVisible()) {
return;
}
Graphics2D g2 = (Graphics2D) g;
Paint paint = g2.getPaint();
g2.setPaint(backgroundGradient);
Rectangle clip = g2.getClipBounds();
clip = clip.intersection(new Rectangle(0, 2, getWidth(), getHeight()));
g2.fillRect(clip.x, clip.y, clip.width, clip.height);
g2.setPaint(paint);
g2.setColor(lightColor);
g2.drawLine(0, 0, getWidth(), 0);
g2.setColor(shadowColor);
g2.drawLine(0, 1, getWidth(), 1);
}
}
And the properties file:
Common.shadowOpacity=0.7
Common.shadowColor=#000000
Common.shadowDirection=60
Common.lightColor=#4B5461
Common.darkColor=#202737
Footer.preferredHeight=17
Footer.lightColor={Common.lightColor}
Footer.shadowColor={Common.darkColor}
Footer.backgroundGradient=0,0 | 0,15 | 0.0,#666F7F | 1.0,#202737