|
The Java Specialists' Newsletter
Issue 145 2007-05-25
Category:
GUI
Java version: 5+ TristateCheckBox Revisitedby Dr. Heinz M. KabutzAbstract: The Tristate Checkbox is widely used to
represent an undetermined state of a check box. In this
newsletter, we present a new version of this popular
control, retrofitted to Java 5 and 6.
Welcome to the 145th issue of The Java(tm) Specialists' Newsletter sent from Korakies,
Chania! When we moved to the
Island of Crete from South Africa, we brought our Ed Seiler
upright piano along. You should have seen the movers carry
that baby up the stairs! We had intended to let it settle
for a month or two, then tune it. Helene spoke to Maxi's
piano teacher yesterday and discovered that the piano tuner
is coming to the island in October. That's right,
October! He visits here from Athens, tunes all the
pianos and then scurries back home. So for the next, um,
five months, we will have to endure an out-of-tune piano!
As mentioned in the previous newsletter, from July 2007, I
will be offering Java code reviews
for your team to get an expert's viewpoint of your Java
system. Please take note and speak to your manager about
this opportunity.
This last week, I added local search functionality to our
newsletter archive. With over 157 newsletters and follow-ups,
it is becoming increasingly difficult to find back issues,
many of which I still dig out when I cannot figure something
out. The search facility is available on the
main archive
page, together with the abstracts and titles of
all our 157 newsletters and followups.
Unfortunately, due to my ambivalence in choosing domain names
- we went from www.smotricz.com/kabutz to
www.javaspecialists.co.za to www.cretesoft.com and now
finally to www.javaspecialists.EU - poor Google
is having a hard time keeping up. I have resubmitted a
sitemap, but it will likely take a few days before the site
is correctly indexed.
Then I did something else a few months ago, which was really
really dumb. I have a mapper class in my website that
automatically translates URLs of the format
http://www.javaspecialists.eu/archive/Issue###.html into the
servlet URL that is needed to display this. It saves me from
having to write the mapping into the web.xml file. It also
allows me to support older versions of URLs with minimal
effort. However, I forgot to change the status code of the
result, which at that point had been set to 404 (page not
found). This would have been invisible to humans reading the
pages, but web crawlers would have a serious issue with that.
Upcoming Java Specialist Master Courses:
- please click here to sign up.
As from May 2010, we are also offering this course on the island of Crete. We
only accept 6 students per class in Crete, due to the size of our conference
room. Please book early to avoid disappointment!
San Jose CA, Mar 16-19 2010, $3500 Ottawa, Canada, Mar 22-25 2010, $3500 Oslo, Norway, Apr 13-16 2010, Kr 24500 Montreal, Canada, Apr 20-23 2010, $3500 Toronto, Canada, May 17-20 2010, $3500 Chania, Crete, May 25-28, Jun 29-Jul 2 or Aug 24-27 2010, €2500
In-house courses if these dates or locations do not suit you - click here for more information. TristateCheckBox Revisited
Have you ever written a piece of software and not received
bug reports for it? If so, then make sure that your clients
are actually running the code!
Three and a half years ago, I
published a
TristateCheckBox that you can use
in Swing applications and applets. Unlike a normal check box
it had three states, SELECTED, DESELECTED and INDETERMINATE.
Even though it is just a small little GUI class, I have had
requests from no less than eight institutes, including
a Fortune 500 company and a major university, asking whether
they could use the class. In addition, several readers have
sent corrections and additions to the class.
The most recent was by Mark Zellers from Adaptive
Planning, informing me that there was a problem with
the TristateCheckBox. His description of the bug struck me
as odd, so I ran my old code again. It looks like the
behaviour changed in the move over to Java 5. The check box
did not retain its INDETERMINATE state when it lost focus.
Unfortunately, none of the proposed fixes covered all the
issues that needed to be addressed. Even after spending the
entire day reading through related emails and coming up with
a better solution, I am convinced that there are still snafus
that I have not thought of yet.
The best solution was sent by David Wright, who is working on
the Superficial
framework for GUIs. In his last email to me, David wrote:
Trying to produce a really robust TristateCheckBox has
turned out to be quite a challenge and I'm still not sure
I've covered all the bases. However, I would be delighted
if you wanted to publish this or a further improved
version.
Several readers also sent me minor corrections to do with
using enums for the states, so let us start with the states.
Here, we have a very simple state machine that indicates
what the next state would be. This works in a chain of
Selected -> Indeterminate -> Deselected -> Selected.
package eu.javaspecialists.tjsn.gui;
public enum TristateState {
SELECTED {
public TristateState next() {
return INDETERMINATE;
}
},
INDETERMINATE {
public TristateState next() {
return DESELECTED;
}
},
DESELECTED {
public TristateState next() {
return SELECTED;
}
};
public abstract TristateState next();
}
In David Wright's approach, he subclassed the
TristateButtonModel from
ToggleButtonModel, rather than try to decorate
the model. This allows us to reuse the model for other
components, for example, a TristateRadioButton
(not shown here).
A point worth mentioning is that the itemChanged event is
typically only fired with the checkbox is either selected
or deselected (not during the intermediate states). We have
to manually fire off the events in the
setState() method.
package eu.javaspecialists.tjsn.gui;
import javax.swing.JToggleButton.ToggleButtonModel;
import java.awt.event.ItemEvent;
public class TristateButtonModel extends ToggleButtonModel {
private TristateState state = TristateState.DESELECTED;
public TristateButtonModel(TristateState state) {
setState(state);
}
public TristateButtonModel() {
this(TristateState.DESELECTED);
}
public void setIndeterminate() {
setState(TristateState.INDETERMINATE);
}
public boolean isIndeterminate() {
return state == TristateState.INDETERMINATE;
}
// Overrides of superclass methods
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
// Restore state display
displayState();
}
public void setSelected(boolean selected) {
setState(selected ?
TristateState.SELECTED : TristateState.DESELECTED);
}
// Empty overrides of superclass methods
public void setArmed(boolean b) {
}
public void setPressed(boolean b) {
}
void iterateState() {
setState(state.next());
}
private void setState(TristateState state) {
//Set internal state
this.state = state;
displayState();
if (state == TristateState.INDETERMINATE && isEnabled()) {
// force the events to fire
// Send ChangeEvent
fireStateChanged();
// Send ItemEvent
int indeterminate = 3;
fireItemStateChanged(new ItemEvent(
this, ItemEvent.ITEM_STATE_CHANGED, this,
indeterminate));
}
}
private void displayState() {
super.setSelected(state != TristateState.DESELECTED);
super.setArmed(state == TristateState.INDETERMINATE);
super.setPressed(state == TristateState.INDETERMINATE);
}
public TristateState getState() {
return state;
}
}
We reference this model from within the TristateCheckbox
class. Users can either figure out the state using the
isSelected() and isIndeterminate() methods or by calling the
getState() method.
package eu.javaspecialists.tjsn.gui;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.plaf.ActionMapUIResource;
import java.awt.*;
import java.awt.event.*;
public final class TristateCheckBox extends JCheckBox {
// Listener on model changes to maintain correct focusability
private final ChangeListener enableListener =
new ChangeListener() {
public void stateChanged(ChangeEvent e) {
TristateCheckBox.this.setFocusable(
getModel().isEnabled());
}
};
public TristateCheckBox(String text) {
this(text, null, TristateState.DESELECTED);
}
public TristateCheckBox(String text, Icon icon,
TristateState initial) {
super(text, icon);
//Set default single model
setModel(new TristateButtonModel(initial));
// override action behaviour
super.addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
TristateCheckBox.this.iterateState();
}
});
ActionMap actions = new ActionMapUIResource();
actions.put("pressed", new AbstractAction() {
public void actionPerformed(ActionEvent e) {
TristateCheckBox.this.iterateState();
}
});
actions.put("released", null);
SwingUtilities.replaceUIActionMap(this, actions);
}
// Next two methods implement new API by delegation to model
public void setIndeterminate() {
getTristateModel().setIndeterminate();
}
public boolean isIndeterminate() {
return getTristateModel().isIndeterminate();
}
public TristateState getState() {
return getTristateModel().getState();
}
//Overrides superclass method
public void setModel(ButtonModel newModel) {
super.setModel(newModel);
//Listen for enable changes
if (model instanceof TristateButtonModel)
model.addChangeListener(enableListener);
}
//Empty override of superclass method
public void addMouseListener(MouseListener l) {
}
// Mostly delegates to model
private void iterateState() {
//Maybe do nothing at all?
if (!getModel().isEnabled()) return;
grabFocus();
// Iterate state
getTristateModel().iterateState();
// Fire ActionEvent
int modifiers = 0;
AWTEvent currentEvent = EventQueue.getCurrentEvent();
if (currentEvent instanceof InputEvent) {
modifiers = ((InputEvent) currentEvent).getModifiers();
} else if (currentEvent instanceof ActionEvent) {
modifiers = ((ActionEvent) currentEvent).getModifiers();
}
fireActionPerformed(new ActionEvent(this,
ActionEvent.ACTION_PERFORMED, getText(),
System.currentTimeMillis(), modifiers));
}
//Convenience cast
public TristateButtonModel getTristateModel() {
return (TristateButtonModel) super.getModel();
}
}
We also have a test case for this class, using the various
look and feels installed on your system. Note that this
approach does not work well on the Motif L&F.
package eu.javaspecialists.tjsn.gui;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class TristateCheckBoxTest {
public static void main(String args[]) throws Exception {
JFrame frame = new JFrame("TristateCheckBoxTest");
frame.setLayout(new GridLayout(0, 1, 15, 15));
UIManager.LookAndFeelInfo[] lfs =
UIManager.getInstalledLookAndFeels();
for (UIManager.LookAndFeelInfo lf : lfs) {
System.out.println("Look&Feel " + lf.getName());
UIManager.setLookAndFeel(lf.getClassName());
frame.add(makePanel(lf));
}
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
private static JPanel makePanel(UIManager.LookAndFeelInfo lf) {
final TristateCheckBox tristateBox = new TristateCheckBox(
"Tristate checkbox");
tristateBox.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent e) {
switch(tristateBox.getState()) {
case SELECTED:
System.out.println("Selected"); break;
case DESELECTED:
System.out.println("Not Selected"); break;
case INDETERMINATE:
System.out.println("Tristate Selected"); break;
}
}
});
tristateBox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println(e);
}
});
final JCheckBox normalBox = new JCheckBox("Normal checkbox");
normalBox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println(e);
}
});
final JCheckBox enabledBox = new JCheckBox("Enable", true);
enabledBox.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent e) {
tristateBox.setEnabled(enabledBox.isSelected());
normalBox.setEnabled(enabledBox.isSelected());
}
});
JPanel panel = new JPanel(new GridLayout(0, 1, 5, 5));
panel.add(new JLabel(UIManager.getLookAndFeel().getName()));
panel.add(tristateBox);
panel.add(normalBox);
panel.add(enabledBox);
return panel;
}
}
Please try it out and let me know if it causes any problems
or unexpected behaviour on your system.
Kind regards from Crete
Heinz
P.S. The next few weeks will be quite busy as Helene and our
baby Evangeline go to England. I have the task of keeping
Maximilian and Constance well fed and clean until her return.
Don't expect any newsletters in the next three weeks :-) We
already have plans to stay up late, eat crisps, watch movies,
bunk school, catch fish, etc. Maybe we will even go camping.
GUI Articles
Related Java Course
|