001: package com.jidesoft.swing;
002:
003: import javax.swing.*;
004: import javax.swing.event.ChangeListener;
005: import javax.swing.plaf.ActionMapUIResource;
006: import java.awt.event.*;
007:
008: /**
009: * Maintenance tip - There were some tricks to getting this code
010: * working:
011: * <p/>
012: * 1. You have to overwite addMouseListener() to do nothing
013: * 2. You have to add a mouse event on mousePressed by calling
014: * super.addMouseListener()
015: * 3. You have to replace the UIActionMap for the keyboard event
016: * "pressed" with your own one.
017: * 4. You have to remove the UIActionMap for the keyboard event
018: * "released".
019: * 5. You have to grab focus when the next state is entered,
020: * otherwise clicking on the component won't get the focus.
021: * 6. You have to make a TristateDecorator as a button model that
022: * wraps the original button model and does state management.
023: *
024: * @author Dr. Heinz M. Kabutz
025: */
026: public class TristateCheckBox extends JCheckBox {
027: /**
028: * This is a type-safe enumerated type
029: */
030: public static class State {
031: private State() {
032: }
033: }
034:
035: public static final State NOT_SELECTED = new State();
036: public static final State SELECTED = new State();
037: public static final State DONT_CARE = new State();
038:
039: private final TristateDecorator model;
040:
041: public TristateCheckBox(String text, Icon icon, State initial) {
042: super (text, icon);
043: // Add a listener for when the mouse is pressed
044: super .addMouseListener(new MouseAdapter() {
045: @Override
046: public void mousePressed(MouseEvent e) {
047: grabFocus();
048: model.setState(getNextState(model.getState()));
049: }
050: });
051: // Reset the keyboard action map
052: ActionMap map = new ActionMapUIResource();
053: map.put("pressed", new AbstractAction() {
054: public void actionPerformed(ActionEvent e) {
055: grabFocus();
056: model.setState(getNextState(model.getState()));
057: }
058: });
059: map.put("released", null);
060: SwingUtilities.replaceUIActionMap(this , map);
061: // set the model to the adapted model
062: model = new TristateDecorator(getModel());
063: setModel(model);
064: setState(initial);
065: }
066:
067: public TristateCheckBox(String text, State initial) {
068: this (text, null, initial);
069: }
070:
071: public TristateCheckBox(String text) {
072: this (text, DONT_CARE);
073: }
074:
075: public TristateCheckBox() {
076: this (null);
077: }
078:
079: /**
080: * No one may add mouse listeners, not even Swing!
081: */
082: @Override
083: public void addMouseListener(MouseListener l) {
084: }
085:
086: /**
087: * Set the new state to either SELECTED, NOT_SELECTED or
088: * DONT_CARE. If state == null, it is treated as DONT_CARE.
089: */
090: public void setState(State state) {
091: model.setState(state);
092: }
093:
094: /**
095: * Return the current state, which is determined by the
096: * selection status of the model.
097: */
098: public State getState() {
099: return model.getState();
100: }
101:
102: @Override
103: public void setSelected(boolean b) {
104: if (b) {
105: setState(SELECTED);
106: } else {
107: setState(NOT_SELECTED);
108: }
109: }
110:
111: /**
112: * Exactly which Design Pattern is this? Is it an Adapter,
113: * a Proxy or a Decorator? In this case, my vote lies with the
114: * Decorator, because we are extending functionality and
115: * "decorating" the original model with a more powerful model.
116: */
117: private class TristateDecorator implements ButtonModel {
118: private final ButtonModel other;
119:
120: private TristateDecorator(ButtonModel other) {
121: this .other = other;
122: }
123:
124: private void setState(State state) {
125: if (state == NOT_SELECTED) {
126: other.setArmed(false);
127: setPressed(false);
128: setSelected(false);
129: } else if (state == SELECTED) {
130: other.setArmed(false);
131: setPressed(false);
132: setSelected(true);
133: } else { // either "null" or DONT_CARE
134: other.setArmed(true);
135: setPressed(true);
136: setSelected(true);
137: }
138: }
139:
140: /**
141: * The current state is embedded in the selection / armed
142: * state of the model.
143: * <p/>
144: * We return the SELECTED state when the checkbox is selected
145: * but not armed, DONT_CARE state when the checkbox is
146: * selected and armed (grey) and NOT_SELECTED when the
147: * checkbox is deselected.
148: */
149: private State getState() {
150: if (isSelected() && !isArmed()) {
151: // normal black tick
152: return SELECTED;
153: } else if (isSelected() && isArmed()) {
154: // don't care grey tick
155: return DONT_CARE;
156: } else {
157: // normal deselected
158: return NOT_SELECTED;
159: }
160: }
161:
162: /**
163: * Filter: No one may change the armed status except us.
164: */
165: public void setArmed(boolean b) {
166: }
167:
168: /**
169: * We disable focusing on the component when it is not
170: * enabled.
171: */
172: public void setEnabled(boolean b) {
173: setFocusable(b);
174: other.setEnabled(b);
175: }
176:
177: /**
178: * All these methods simply delegate to the "other" model
179: * that is being decorated.
180: */
181: public boolean isArmed() {
182: return other.isArmed();
183: }
184:
185: public boolean isSelected() {
186: return other.isSelected();
187: }
188:
189: public boolean isEnabled() {
190: return other.isEnabled();
191: }
192:
193: public boolean isPressed() {
194: return other.isPressed();
195: }
196:
197: public boolean isRollover() {
198: return other.isRollover();
199: }
200:
201: public void setSelected(boolean b) {
202: other.setSelected(b);
203: }
204:
205: public void setPressed(boolean b) {
206: other.setPressed(b);
207: }
208:
209: public void setRollover(boolean b) {
210: other.setRollover(b);
211: }
212:
213: public void setMnemonic(int key) {
214: other.setMnemonic(key);
215: }
216:
217: public int getMnemonic() {
218: return other.getMnemonic();
219: }
220:
221: public void setActionCommand(String s) {
222: other.setActionCommand(s);
223: }
224:
225: public String getActionCommand() {
226: return other.getActionCommand();
227: }
228:
229: public void setGroup(ButtonGroup group) {
230: other.setGroup(group);
231: }
232:
233: public void addActionListener(ActionListener l) {
234: other.addActionListener(l);
235: }
236:
237: public void removeActionListener(ActionListener l) {
238: other.removeActionListener(l);
239: }
240:
241: public void addItemListener(ItemListener l) {
242: other.addItemListener(l);
243: }
244:
245: public void removeItemListener(ItemListener l) {
246: other.removeItemListener(l);
247: }
248:
249: public void addChangeListener(ChangeListener l) {
250: other.addChangeListener(l);
251: }
252:
253: public void removeChangeListener(ChangeListener l) {
254: other.removeChangeListener(l);
255: }
256:
257: public Object[] getSelectedObjects() {
258: return other.getSelectedObjects();
259: }
260: }
261:
262: /**
263: * We rotate between NOT_SELECTED, SELECTED and DONT_CARE. Subclass can
264: * override this method to tell the check box what next state is. Here is
265: * the default implementation.
266: * <code><pre>
267: * if (current == NOT_SELECTED) {
268: * return SELECTED;
269: * }
270: * else if (current == SELECTED) {
271: * return DONT_CARE;
272: * }
273: * else {
274: * return NOT_SELECTED;
275: * }
276: * </code></pre>
277: */
278: protected State getNextState(State current) {
279: if (current == NOT_SELECTED) {
280: return SELECTED;
281: } else if (current == SELECTED) {
282: return DONT_CARE;
283: } else /*if (current == DONT_CARE)*/{
284: return NOT_SELECTED;
285: }
286: }
287: }
|