Abstract: The ordinary Swing JCheckBox has only two states. We present a new component that is based on the old JCheckBox, but which has three states.
For a revised version of this newsletter that works for Java 5 upwards, please refer to TristateCheckBox Revisited.
Welcome to the 82nd edition of The Java(tm) Specialists' Newsletter. My last newsletter caused some subscribers to come forward, offering to translate to their language. The most interesting one, in my opinion, is our Zulu translation. Zulu is spoken by approximately 9 million people in Southern Africa. It is one of the 11 official languages in South Africa. I am particularly grateful to Mondli Mabaso for sacrificing his time and bringing us the translation.
In addition to Zulu, we have also been approached with Estonian, Polish and Bulgarian. Thank you very much in advance! Please send us an email if you would like to translate the newsletter into your language.
My uncle Karl-Heinz is one of many relatives who is on the subscriber list. Having a large family is an advantage with electronic newsletters, since you immediately have a captive audience, who dare not unsubscribe for fear of mortally offending you *grin*. Karl-Heinz and my aunt Gunhild visited us in November from Germany, and one of the reasons the newsletters have been so scarce is because they had me chase a little white ball across the grass (or rather the bushes) in Somerset West :-)
javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.
I wrote this component for a customer a few years ago and it has been running happily in production since then. Thank you for letting me publish this! I have found and removed a bug at the same time :-)
Writing custom components in Swing can be tricky, especially when you are trying to change the behaviour in a Look & Feel independent way. A real challenge was this checkbox with three states: selected, not selected and neither (i.e. don't care). You see this type of checkbox in configuration GUIs. My challenge was that it should support any Look & Feel.
After scratching my head for a while, I noticed that the ordinary JCheckBox also had three states: selected/unarmed, selected/armed, deselected/unarmed. The selected/armed state looked exactly like the "don't care" state that I wanted. [There is actually a fourth state: deselected/armed, but I did not find a use for that] The armed state is set when you have pressed the mouse button on the control, but have not released it yet.
It took me a while to get it working, and it was a long time ago. Fortunately, since I love useful comments, I wrote a comment describing the steps needed to get this to work:
I also wanted to use decent enumerated types, rather than just an int, so used my "static inner class with private constructor" trick.
Let's look at the code:
import javax.swing.*; import javax.swing.event.ChangeListener; import javax.swing.plaf.ActionMapUIResource; import java.awt.event.*; /** * Maintenance tip - There were some tricks to getting this code * working: * * 1. You have to overwite addMouseListener() to do nothing * 2. You have to add a mouse event on mousePressed by calling * super.addMouseListener() * 3. You have to replace the UIActionMap for the keyboard event * "pressed" with your own one. * 4. You have to remove the UIActionMap for the keyboard event * "released". * 5. You have to grab focus when the next state is entered, * otherwise clicking on the component won't get the focus. * 6. You have to make a TristateDecorator as a button model that * wraps the original button model and does state management. */ public class TristateCheckBox extends JCheckBox { /** This is a type-safe enumerated type */ public static class State { private State() { } } public static final State NOT_SELECTED = new State(); public static final State SELECTED = new State(); public static final State DONT_CARE = new State(); private final TristateDecorator model; public TristateCheckBox(String text, Icon icon, State initial){ super(text, icon); // Add a listener for when the mouse is pressed super.addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { grabFocus(); model.nextState(); } }); // Reset the keyboard action map ActionMap map = new ActionMapUIResource(); map.put("pressed", new AbstractAction() { public void actionPerformed(ActionEvent e) { grabFocus(); model.nextState(); } }); map.put("released", null); SwingUtilities.replaceUIActionMap(this, map); // set the model to the adapted model model = new TristateDecorator(getModel()); setModel(model); setState(initial); } public TristateCheckBox(String text, State initial) { this(text, null, initial); } public TristateCheckBox(String text) { this(text, DONT_CARE); } public TristateCheckBox() { this(null); } /** No one may add mouse listeners, not even Swing! */ public void addMouseListener(MouseListener l) { } /** * Set the new state to either SELECTED, NOT_SELECTED or * DONT_CARE. If state == null, it is treated as DONT_CARE. */ public void setState(State state) { model.setState(state); } /** Return the current state, which is determined by the * selection status of the model. */ public State getState() { return model.getState(); } public void setSelected(boolean b) { if (b) { setState(SELECTED); } else { setState(NOT_SELECTED); } } /** * Exactly which Design Pattern is this? Is it an Adapter, * a Proxy or a Decorator? In this case, my vote lies with the * Decorator, because we are extending functionality and * "decorating" the original model with a more powerful model. */ private class TristateDecorator implements ButtonModel { private final ButtonModel other; private TristateDecorator(ButtonModel other) { this.other = other; } private void setState(State state) { if (state == NOT_SELECTED) { other.setArmed(false); setPressed(false); setSelected(false); } else if (state == SELECTED) { other.setArmed(false); setPressed(false); setSelected(true); } else { // either "null" or DONT_CARE other.setArmed(true); setPressed(true); setSelected(true); } } /** * The current state is embedded in the selection / armed * state of the model. * * We return the SELECTED state when the checkbox is selected * but not armed, DONT_CARE state when the checkbox is * selected and armed (grey) and NOT_SELECTED when the * checkbox is deselected. */ private State getState() { if (isSelected() && !isArmed()) { // normal black tick return SELECTED; } else if (isSelected() && isArmed()) { // don't care grey tick return DONT_CARE; } else { // normal deselected return NOT_SELECTED; } } /** We rotate between NOT_SELECTED, SELECTED and DONT_CARE.*/ private void nextState() { State current = getState(); if (current == NOT_SELECTED) { setState(SELECTED); } else if (current == SELECTED) { setState(DONT_CARE); } else if (current == DONT_CARE) { setState(NOT_SELECTED); } } /** Filter: No one may change the armed status except us. */ public void setArmed(boolean b) { } /** We disable focusing on the component when it is not * enabled. */ public void setEnabled(boolean b) { setFocusable(b); other.setEnabled(b); } /** All these methods simply delegate to the "other" model * that is being decorated. */ public boolean isArmed() { return other.isArmed(); } public boolean isSelected() { return other.isSelected(); } public boolean isEnabled() { return other.isEnabled(); } public boolean isPressed() { return other.isPressed(); } public boolean isRollover() { return other.isRollover(); } public void setSelected(boolean b) { other.setSelected(b); } public void setPressed(boolean b) { other.setPressed(b); } public void setRollover(boolean b) { other.setRollover(b); } public void setMnemonic(int key) { other.setMnemonic(key); } public int getMnemonic() { return other.getMnemonic(); } public void setActionCommand(String s) { other.setActionCommand(s); } public String getActionCommand() { return other.getActionCommand(); } public void setGroup(ButtonGroup group) { other.setGroup(group); } public void addActionListener(ActionListener l) { other.addActionListener(l); } public void removeActionListener(ActionListener l) { other.removeActionListener(l); } public void addItemListener(ItemListener l) { other.addItemListener(l); } public void removeItemListener(ItemListener l) { other.removeItemListener(l); } public void addChangeListener(ChangeListener l) { other.addChangeListener(l); } public void removeChangeListener(ChangeListener l) { other.removeChangeListener(l); } public Object[] getSelectedObjects() { return other.getSelectedObjects(); } } }
Here is some sample code that uses the TristateCheckBox:
import javax.swing.*; import java.awt.*; public class TristateCheckBoxTest { public static void main(String args[]) throws Exception { JFrame frame = new JFrame("TristateCheckBoxTest"); frame.getContentPane().setLayout(new GridLayout(0, 1, 5, 5)); final TristateCheckBox swingBox = new TristateCheckBox( "Testing the tristate checkbox"); swingBox.setMnemonic('T'); frame.getContentPane().add(swingBox); frame.getContentPane().add(new JCheckBox( "The normal checkbox")); UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName()); final TristateCheckBox winBox = new TristateCheckBox( "Testing the tristate checkbox", TristateCheckBox.SELECTED); frame.getContentPane().add(winBox); final JCheckBox winNormal = new JCheckBox( "The normal checkbox"); frame.getContentPane().add(winNormal); // wait for 3 seconds, then enable all check boxes new Thread() { {start();} public void run() { try { winBox.setEnabled(false); winNormal.setEnabled(false); Thread.sleep(3000); winBox.setEnabled(true); winNormal.setEnabled(true); } catch (InterruptedException ex) { } } }; frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.pack(); frame.show(); } }
Tri it out!
Kind regards
Heinz
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.