001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.widgets;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.event.ActionEvent;
007import java.awt.event.ActionListener;
008import java.awt.event.ItemListener;
009import java.awt.event.MouseAdapter;
010import java.awt.event.MouseEvent;
011import java.awt.event.MouseListener;
012
013import javax.swing.AbstractAction;
014import javax.swing.ActionMap;
015import javax.swing.ButtonGroup;
016import javax.swing.ButtonModel;
017import javax.swing.Icon;
018import javax.swing.JCheckBox;
019import javax.swing.SwingUtilities;
020import javax.swing.event.ChangeListener;
021import javax.swing.plaf.ActionMapUIResource;
022
023import org.openstreetmap.josm.tools.Utils;
024
025/**
026 * A four-state checkbox. The states are enumerated in {@link State}.
027 * @since 591
028 */
029public class QuadStateCheckBox extends JCheckBox {
030
031    /**
032     * The 4 possible states of this checkbox.
033     */
034    public enum State {
035        /** Not selected: the property is explicitly switched off */
036        NOT_SELECTED,
037        /** Selected: the property is explicitly switched on */
038        SELECTED,
039        /** Unset: do not set this property on the selected objects */
040        UNSET,
041        /** Partial: different selected objects have different values, do not change */
042        PARTIAL
043    }
044
045    private final transient QuadStateDecorator model;
046    private State[] allowed;
047
048    /**
049     * Constructs a new {@code QuadStateCheckBox}.
050     * @param text the text of the check box
051     * @param icon the Icon image to display
052     * @param initial The initial state
053     * @param allowed The allowed states
054     */
055    public QuadStateCheckBox(String text, Icon icon, State initial, State[] allowed) {
056        super(text, icon);
057        this.allowed = Utils.copyArray(allowed);
058        // Add a listener for when the mouse is pressed
059        super.addMouseListener(new MouseAdapter() {
060            @Override public void mousePressed(MouseEvent e) {
061                grabFocus();
062                model.nextState();
063            }
064        });
065        // Reset the keyboard action map
066        ActionMap map = new ActionMapUIResource();
067        map.put("pressed", new AbstractAction() {
068            @Override
069            public void actionPerformed(ActionEvent e) {
070                grabFocus();
071                model.nextState();
072            }
073        });
074        map.put("released", null);
075        SwingUtilities.replaceUIActionMap(this, map);
076        // set the model to the adapted model
077        model = new QuadStateDecorator(getModel());
078        setModel(model);
079        setState(initial);
080    }
081
082    /**
083     * Constructs a new {@code QuadStateCheckBox}.
084     * @param text the text of the check box
085     * @param initial The initial state
086     * @param allowed The allowed states
087     */
088    public QuadStateCheckBox(String text, State initial, State[] allowed) {
089        this(text, null, initial, allowed);
090    }
091
092    /** Do not let anyone add mouse listeners */
093    @Override
094    public void addMouseListener(MouseListener l) { }
095
096    /**
097     * Sets a text describing this property in the tooltip text
098     * @param propertyText a description for the modelled property
099     */
100    public final void setPropertyText(final String propertyText) {
101        model.setPropertyText(propertyText);
102    }
103
104    /**
105     * Set the new state.
106     * @param state The new state
107     */
108    public final void setState(State state) {
109        model.setState(state);
110    }
111
112    /**
113     * Return the current state, which is determined by the selection status of the model.
114     * @return The current state
115     */
116    public State getState() {
117        return model.getState();
118    }
119
120    @Override
121    public void setSelected(boolean b) {
122        if (b) {
123            setState(State.SELECTED);
124        } else {
125            setState(State.NOT_SELECTED);
126        }
127    }
128
129    private final class QuadStateDecorator implements ButtonModel {
130        private final ButtonModel other;
131        private String propertyText;
132
133        private QuadStateDecorator(ButtonModel other) {
134            this.other = other;
135        }
136
137        private void setState(State state) {
138            if (state == State.NOT_SELECTED) {
139                other.setArmed(false);
140                other.setPressed(false);
141                other.setSelected(false);
142                setToolTipText(propertyText == null
143                        ? tr("false: the property is explicitly switched off")
144                        : tr("false: the property ''{0}'' is explicitly switched off", propertyText));
145            } else if (state == State.SELECTED) {
146                other.setArmed(false);
147                other.setPressed(false);
148                other.setSelected(true);
149                setToolTipText(propertyText == null
150                        ? tr("true: the property is explicitly switched on")
151                        : tr("true: the property ''{0}'' is explicitly switched on", propertyText));
152            } else if (state == State.PARTIAL) {
153                other.setArmed(true);
154                other.setPressed(true);
155                other.setSelected(true);
156                setToolTipText(propertyText == null
157                        ? tr("partial: different selected objects have different values, do not change")
158                        : tr("partial: different selected objects have different values for ''{0}'', do not change", propertyText));
159            } else {
160                other.setArmed(true);
161                other.setPressed(true);
162                other.setSelected(false);
163                setToolTipText(propertyText == null
164                        ? tr("unset: do not set this property on the selected objects")
165                        : tr("unset: do not set the property ''{0}'' on the selected objects", propertyText));
166            }
167        }
168
169        protected void setPropertyText(String propertyText) {
170            this.propertyText = propertyText;
171        }
172
173        /**
174         * The current state is embedded in the selection / armed
175         * state of the model.
176         *
177         * We return the SELECTED state when the checkbox is selected
178         * but not armed, PARTIAL state when the checkbox is
179         * selected and armed (grey) and NOT_SELECTED when the
180         * checkbox is deselected.
181         * @return current state
182         */
183        private State getState() {
184            if (isSelected() && !isArmed()) {
185                // normal black tick
186                return State.SELECTED;
187            } else if (isSelected() && isArmed()) {
188                // don't care grey tick
189                return State.PARTIAL;
190            } else if (!isSelected() && !isArmed()) {
191                return State.NOT_SELECTED;
192            } else {
193                return State.UNSET;
194            }
195        }
196
197        /** Rotate to the next allowed state.*/
198        private void nextState() {
199            State current = getState();
200            for (int i = 0; i < allowed.length; i++) {
201                if (allowed[i] == current) {
202                    setState((i == allowed.length-1) ? allowed[0] : allowed[i+1]);
203                    break;
204                }
205            }
206        }
207
208        // ----------------------------------------------------------------------
209        // Filter: No one may change the armed/selected/pressed status except us.
210        // ----------------------------------------------------------------------
211
212        @Override
213        public void setArmed(boolean b) { }
214
215        @Override
216        public void setSelected(boolean b) { }
217
218        @Override
219        public void setPressed(boolean b) { }
220
221        /** We disable focusing on the component when it is not enabled. */
222        @Override
223        public void setEnabled(boolean b) {
224            setFocusable(b);
225            other.setEnabled(b);
226        }
227
228        // -------------------------------------------------------------------------------
229        // All these methods simply delegate to the "other" model that is being decorated.
230        // -------------------------------------------------------------------------------
231
232        @Override
233        public boolean isArmed() {
234            return other.isArmed();
235        }
236
237        @Override
238        public boolean isSelected() {
239            return other.isSelected();
240        }
241
242        @Override
243        public boolean isEnabled() {
244            return other.isEnabled();
245        }
246
247        @Override
248        public boolean isPressed() {
249            return other.isPressed();
250        }
251
252        @Override
253        public boolean isRollover() {
254            return other.isRollover();
255        }
256
257        @Override
258        public void setRollover(boolean b) {
259            other.setRollover(b);
260        }
261
262        @Override
263        public void setMnemonic(int key) {
264            other.setMnemonic(key);
265        }
266
267        @Override
268        public int getMnemonic() {
269            return other.getMnemonic();
270        }
271
272        @Override
273        public void setActionCommand(String s) {
274            other.setActionCommand(s);
275        }
276
277        @Override public String getActionCommand() {
278            return other.getActionCommand();
279        }
280
281        @Override public void setGroup(ButtonGroup group) {
282            other.setGroup(group);
283        }
284
285        @Override public void addActionListener(ActionListener l) {
286            other.addActionListener(l);
287        }
288
289        @Override public void removeActionListener(ActionListener l) {
290            other.removeActionListener(l);
291        }
292
293        @Override public void addItemListener(ItemListener l) {
294            other.addItemListener(l);
295        }
296
297        @Override public void removeItemListener(ItemListener l) {
298            other.removeItemListener(l);
299        }
300
301        @Override public void addChangeListener(ChangeListener l) {
302            other.addChangeListener(l);
303        }
304
305        @Override public void removeChangeListener(ChangeListener l) {
306            other.removeChangeListener(l);
307        }
308
309        @Override public Object[] getSelectedObjects() {
310            return other.getSelectedObjects();
311        }
312    }
313}