Abstract: 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
https://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.
javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.
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.
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.
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.
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.
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()); } frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.pack(); frame.setVisible(true); } private static JPanel makePanel() { 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.
We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)
We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.