|
The Java Specialists' Newsletter
Issue 082 2003-12-02
Category:
GUI
Java version: TristateCheckBox based on the Swing JCheckBoxby Dr. Heinz M. Kabutz
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 :-)
Would you like to really understand Java concurrency? Join us for an
in-depth study of how threading works in Java. During the course,
you will learn how to write correct and fast multi-threaded Java code.
Please
click here if you would like to learn more. TristateCheckBox based on the Swing JCheckBox
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:
- You have to overwite addMouseListener() to do
nothing
- You have to add a mouse event on mousePressed by calling
super.addMouseListener()
- You have to replace the UIActionMap for the keyboard
event "pressed" with your own one.
- You have to remove the UIActionMap for the keyboard event
"released".
- You have to grab focus when the next state is entered,
otherwise clicking on the component won't get the
focus.
- You have to make a TristateDecorator as a button model
that wraps the original button model and does state
management.
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
GUI Articles
Related Java Course
Discuss at The Java Specialist Club
|