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