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
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                cbModel.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                cbModel.nextState();
072            }
073        });
074        map.put("released", null);
075        SwingUtilities.replaceUIActionMap(this, map);
076        // set the model to the adapted model
077        cbModel = new QuadStateDecorator(getModel());
078        setModel(cbModel);
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 synchronized void addMouseListener(MouseListener l) {
095        // Do nothing
096    }
097
098    /**
099     * Sets a text describing this property in the tooltip text
100     * @param propertyText a description for the modelled property
101     */
102    public final void setPropertyText(final String propertyText) {
103        cbModel.setPropertyText(propertyText);
104    }
105
106    /**
107     * Set the new state.
108     * @param state The new state
109     */
110    public final void setState(State state) {
111        cbModel.setState(state);
112    }
113
114    /**
115     * Return the current state, which is determined by the selection status of the model.
116     * @return The current state
117     */
118    public State getState() {
119        return cbModel.getState();
120    }
121
122    @Override
123    public void setSelected(boolean b) {
124        if (b) {
125            setState(State.SELECTED);
126        } else {
127            setState(State.NOT_SELECTED);
128        }
129    }
130
131    /**
132     * Button model for the {@code QuadStateCheckBox}.
133     * It previously only implemented (and still could) the {@code ButtonModel} interface.
134     * But because of JDK-8182577 (Java 9 regression) it now extends {@code ToggleButtonModel} as a workaround.
135     * The previous implementation can be restored after Java 9 EOL (March 2018).
136     * See also https://bugs.openjdk.java.net/browse/JDK-8182695 - https://bugs.openjdk.java.net/browse/JDK-8182577
137     */
138    private final class QuadStateDecorator extends ToggleButtonModel {
139        private final ButtonModel other;
140        private String propertyText;
141
142        private QuadStateDecorator(ButtonModel other) {
143            this.other = other;
144        }
145
146        private void setState(State state) {
147            if (state == State.NOT_SELECTED) {
148                other.setArmed(false);
149                other.setPressed(false);
150                other.setSelected(false);
151                setToolTipText(propertyText == null
152                        ? tr("false: the property is explicitly switched off")
153                        : tr("false: the property ''{0}'' is explicitly switched off", propertyText));
154            } else if (state == State.SELECTED) {
155                other.setArmed(false);
156                other.setPressed(false);
157                other.setSelected(true);
158                setToolTipText(propertyText == null
159                        ? tr("true: the property is explicitly switched on")
160                        : tr("true: the property ''{0}'' is explicitly switched on", propertyText));
161            } else if (state == State.PARTIAL) {
162                other.setArmed(true);
163                other.setPressed(true);
164                other.setSelected(true);
165                setToolTipText(propertyText == null
166                        ? tr("partial: different selected objects have different values, do not change")
167                        : tr("partial: different selected objects have different values for ''{0}'', do not change", propertyText));
168            } else {
169                other.setArmed(true);
170                other.setPressed(true);
171                other.setSelected(false);
172                setToolTipText(propertyText == null
173                        ? tr("unset: do not set this property on the selected objects")
174                        : tr("unset: do not set the property ''{0}'' on the selected objects", propertyText));
175            }
176        }
177
178        private void setPropertyText(String propertyText) {
179            this.propertyText = propertyText;
180        }
181
182        /**
183         * The current state is embedded in the selection / armed
184         * state of the model.
185         *
186         * We return the SELECTED state when the checkbox is selected
187         * but not armed, PARTIAL state when the checkbox is
188         * selected and armed (grey) and NOT_SELECTED when the
189         * checkbox is deselected.
190         * @return current state
191         */
192        private State getState() {
193            if (isSelected() && !isArmed()) {
194                // normal black tick
195                return State.SELECTED;
196            } else if (isSelected() && isArmed()) {
197                // don't care grey tick
198                return State.PARTIAL;
199            } else if (!isSelected() && !isArmed()) {
200                return State.NOT_SELECTED;
201            } else {
202                return State.UNSET;
203            }
204        }
205
206        /** Rotate to the next allowed state.*/
207        private void nextState() {
208            State current = getState();
209            for (int i = 0; i < allowed.length; i++) {
210                if (allowed[i] == current) {
211                    setState((i == allowed.length-1) ? allowed[0] : allowed[i+1]);
212                    break;
213                }
214            }
215        }
216
217        // ----------------------------------------------------------------------
218        // Filter: No one may change the armed/selected/pressed status except us.
219        // ----------------------------------------------------------------------
220
221        @Override
222        public void setArmed(boolean b) {
223            // Do nothing
224        }
225
226        @Override
227        public void setSelected(boolean b) {
228            // Do nothing
229        }
230
231        @Override
232        public void setPressed(boolean b) {
233            // Do nothing
234        }
235
236        /** We disable focusing on the component when it is not enabled. */
237        @Override
238        public void setEnabled(boolean b) {
239            setFocusable(b);
240            if (other != null) {
241                other.setEnabled(b);
242            }
243        }
244
245        // -------------------------------------------------------------------------------
246        // All these methods simply delegate to the "other" model that is being decorated.
247        // -------------------------------------------------------------------------------
248
249        @Override
250        public boolean isArmed() {
251            return other.isArmed();
252        }
253
254        @Override
255        public boolean isSelected() {
256            return other.isSelected();
257        }
258
259        @Override
260        public boolean isEnabled() {
261            return other.isEnabled();
262        }
263
264        @Override
265        public boolean isPressed() {
266            return other.isPressed();
267        }
268
269        @Override
270        public boolean isRollover() {
271            return other.isRollover();
272        }
273
274        @Override
275        public void setRollover(boolean b) {
276            other.setRollover(b);
277        }
278
279        @Override
280        public void setMnemonic(int key) {
281            other.setMnemonic(key);
282        }
283
284        @Override
285        public int getMnemonic() {
286            return other.getMnemonic();
287        }
288
289        @Override
290        public void setActionCommand(String s) {
291            other.setActionCommand(s);
292        }
293
294        @Override public String getActionCommand() {
295            return other.getActionCommand();
296        }
297
298        @Override public void setGroup(ButtonGroup group) {
299            other.setGroup(group);
300        }
301
302        @Override public void addActionListener(ActionListener l) {
303            other.addActionListener(l);
304        }
305
306        @Override public void removeActionListener(ActionListener l) {
307            other.removeActionListener(l);
308        }
309
310        @Override public void addItemListener(ItemListener l) {
311            other.addItemListener(l);
312        }
313
314        @Override public void removeItemListener(ItemListener l) {
315            other.removeItemListener(l);
316        }
317
318        @Override public void addChangeListener(ChangeListener l) {
319            other.addChangeListener(l);
320        }
321
322        @Override public void removeChangeListener(ChangeListener l) {
323            other.removeChangeListener(l);
324        }
325
326        @Override public Object[] getSelectedObjects() {
327            return other.getSelectedObjects();
328        }
329    }
330}