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 cbModel;
046    private State[] allowed;
047    private final transient MouseListener mouseAdapter = new MouseAdapter() {
048        @Override
049        public void mousePressed(MouseEvent e) {
050            grabFocus();
051            cbModel.nextState();
052        }
053    };
054
055    /**
056     * Constructs a new {@code QuadStateCheckBox}.
057     * @param text the text of the check box
058     * @param icon the Icon image to display
059     * @param initial The initial state
060     * @param allowed The allowed states
061     */
062    public QuadStateCheckBox(String text, Icon icon, State initial, State... allowed) {
063        super(text, icon);
064        this.allowed = Utils.copyArray(allowed);
065        // Add a listener for when the mouse is pressed
066        super.addMouseListener(mouseAdapter);
067        // Reset the keyboard action map
068        ActionMap map = new ActionMapUIResource();
069        map.put("pressed", new AbstractAction() {
070            @Override
071            public void actionPerformed(ActionEvent e) {
072                grabFocus();
073                cbModel.nextState();
074            }
075        });
076        map.put("released", null);
077        SwingUtilities.replaceUIActionMap(this, map);
078        // set the model to the adapted model
079        cbModel = new QuadStateDecorator(getModel());
080        setModel(cbModel);
081        setState(initial);
082    }
083
084    /**
085     * Constructs a new {@code QuadStateCheckBox}.
086     * @param text the text of the check box
087     * @param initial The initial state
088     * @param allowed The allowed states
089     */
090    public QuadStateCheckBox(String text, State initial, State... allowed) {
091        this(text, null, initial, allowed);
092    }
093
094    /** Do not let anyone add mouse listeners */
095    @Override
096    public synchronized void addMouseListener(MouseListener l) {
097        // Do nothing
098    }
099
100    /**
101     * Returns the internal mouse listener.
102     * @return the internal mouse listener
103     * @since 15437
104     */
105    public MouseListener getMouseAdapter() {
106        return mouseAdapter;
107    }
108
109    /**
110     * Sets a text describing this property in the tooltip text
111     * @param propertyText a description for the modelled property
112     */
113    public final void setPropertyText(final String propertyText) {
114        cbModel.setPropertyText(propertyText);
115    }
116
117    /**
118     * Set the new state.
119     * @param state The new state
120     */
121    public final void setState(State state) {
122        cbModel.setState(state);
123    }
124
125    /**
126     * Return the current state, which is determined by the selection status of the model.
127     * @return The current state
128     */
129    public State getState() {
130        return cbModel.getState();
131    }
132
133    @Override
134    public void setSelected(boolean b) {
135        if (b) {
136            setState(State.SELECTED);
137        } else {
138            setState(State.NOT_SELECTED);
139        }
140    }
141
142    /**
143     * Button model for the {@code QuadStateCheckBox}.
144     */
145    private final class QuadStateDecorator implements ButtonModel {
146        private final ButtonModel other;
147        private String propertyText;
148
149        private QuadStateDecorator(ButtonModel other) {
150            this.other = other;
151        }
152
153        private void setState(State state) {
154            if (state == State.NOT_SELECTED) {
155                other.setArmed(false);
156                other.setPressed(false);
157                other.setSelected(false);
158                setToolTipText(propertyText == null
159                        ? tr("false: the property is explicitly switched off")
160                        : tr("false: the property ''{0}'' is explicitly switched off", propertyText));
161            } else if (state == State.SELECTED) {
162                other.setArmed(false);
163                other.setPressed(false);
164                other.setSelected(true);
165                setToolTipText(propertyText == null
166                        ? tr("true: the property is explicitly switched on")
167                        : tr("true: the property ''{0}'' is explicitly switched on", propertyText));
168            } else if (state == State.PARTIAL) {
169                other.setArmed(true);
170                other.setPressed(true);
171                other.setSelected(true);
172                setToolTipText(propertyText == null
173                        ? tr("partial: different selected objects have different values, do not change")
174                        : tr("partial: different selected objects have different values for ''{0}'', do not change", propertyText));
175            } else {
176                other.setArmed(true);
177                other.setPressed(true);
178                other.setSelected(false);
179                setToolTipText(propertyText == null
180                        ? tr("unset: do not set this property on the selected objects")
181                        : tr("unset: do not set the property ''{0}'' on the selected objects", propertyText));
182            }
183        }
184
185        private void setPropertyText(String propertyText) {
186            this.propertyText = propertyText;
187        }
188
189        /**
190         * The current state is embedded in the selection / armed
191         * state of the model.
192         *
193         * We return the SELECTED state when the checkbox is selected
194         * but not armed, PARTIAL state when the checkbox is
195         * selected and armed (grey) and NOT_SELECTED when the
196         * checkbox is deselected.
197         * @return current state
198         */
199        private State getState() {
200            if (isSelected() && !isArmed()) {
201                // normal black tick
202                return State.SELECTED;
203            } else if (isSelected() && isArmed()) {
204                // don't care grey tick
205                return State.PARTIAL;
206            } else if (!isSelected() && !isArmed()) {
207                return State.NOT_SELECTED;
208            } else {
209                return State.UNSET;
210            }
211        }
212
213        /** Rotate to the next allowed state.*/
214        private void nextState() {
215            State current = getState();
216            for (int i = 0; i < allowed.length; i++) {
217                if (allowed[i] == current) {
218                    setState((i == allowed.length-1) ? allowed[0] : allowed[i+1]);
219                    break;
220                }
221            }
222        }
223
224        // ----------------------------------------------------------------------
225        // Filter: No one may change the armed/selected/pressed status except us.
226        // ----------------------------------------------------------------------
227
228        @Override
229        public void setArmed(boolean b) {
230            // Do nothing
231        }
232
233        @Override
234        public void setSelected(boolean b) {
235            // Do nothing
236        }
237
238        @Override
239        public void setPressed(boolean b) {
240            // Do nothing
241        }
242
243        /** We disable focusing on the component when it is not enabled. */
244        @Override
245        public void setEnabled(boolean b) {
246            setFocusable(b);
247            if (other != null) {
248                other.setEnabled(b);
249            }
250        }
251
252        // -------------------------------------------------------------------------------
253        // All these methods simply delegate to the "other" model that is being decorated.
254        // -------------------------------------------------------------------------------
255
256        @Override
257        public boolean isArmed() {
258            return other.isArmed();
259        }
260
261        @Override
262        public boolean isSelected() {
263            return other.isSelected();
264        }
265
266        @Override
267        public boolean isEnabled() {
268            return other.isEnabled();
269        }
270
271        @Override
272        public boolean isPressed() {
273            return other.isPressed();
274        }
275
276        @Override
277        public boolean isRollover() {
278            return other.isRollover();
279        }
280
281        @Override
282        public void setRollover(boolean b) {
283            other.setRollover(b);
284        }
285
286        @Override
287        public void setMnemonic(int key) {
288            other.setMnemonic(key);
289        }
290
291        @Override
292        public int getMnemonic() {
293            return other.getMnemonic();
294        }
295
296        @Override
297        public void setActionCommand(String s) {
298            other.setActionCommand(s);
299        }
300
301        @Override public String getActionCommand() {
302            return other.getActionCommand();
303        }
304
305        @Override public void setGroup(ButtonGroup group) {
306            other.setGroup(group);
307        }
308
309        @Override public void addActionListener(ActionListener l) {
310            other.addActionListener(l);
311        }
312
313        @Override public void removeActionListener(ActionListener l) {
314            other.removeActionListener(l);
315        }
316
317        @Override public void addItemListener(ItemListener l) {
318            other.addItemListener(l);
319        }
320
321        @Override public void removeItemListener(ItemListener l) {
322            other.removeItemListener(l);
323        }
324
325        @Override public void addChangeListener(ChangeListener l) {
326            other.addChangeListener(l);
327        }
328
329        @Override public void removeChangeListener(ChangeListener l) {
330            other.removeChangeListener(l);
331        }
332
333        @Override public Object[] getSelectedObjects() {
334            return other.getSelectedObjects();
335        }
336    }
337}