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 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     * Set the new state.
098     * @param state The new state
099     */
100    public final void setState(State state) {
101        model.setState(state);
102    }
103    
104    /** 
105     * Return the current state, which is determined by the selection status of the model. 
106     * @return The current state 
107     */
108    public State getState() {
109        return model.getState();
110    }
111    
112    @Override
113    public void setSelected(boolean b) {
114        if (b) {
115            setState(State.SELECTED);
116        } else {
117            setState(State.NOT_SELECTED);
118        }
119    }
120
121    private final class QuadStateDecorator implements ButtonModel {
122        private final ButtonModel other;
123        
124        private QuadStateDecorator(ButtonModel other) {
125            this.other = other;
126        }
127        
128        private void setState(State state) {
129            if (state == State.NOT_SELECTED) {
130                other.setArmed(false);
131                other.setPressed(false);
132                other.setSelected(false);
133                setToolTipText(tr("false: the property is explicitly switched off"));
134            } else if (state == State.SELECTED) {
135                other.setArmed(false);
136                other.setPressed(false);
137                other.setSelected(true);
138                setToolTipText(tr("true: the property is explicitly switched on"));
139            } else if (state == State.PARTIAL) {
140                other.setArmed(true);
141                other.setPressed(true);
142                other.setSelected(true);
143                setToolTipText(tr("partial: different selected objects have different values, do not change"));
144            } else {
145                other.setArmed(true);
146                other.setPressed(true);
147                other.setSelected(false);
148                setToolTipText(tr("unset: do not set this property on the selected objects"));
149            }
150        }
151        
152        /**
153         * The current state is embedded in the selection / armed
154         * state of the model.
155         *
156         * We return the SELECTED state when the checkbox is selected
157         * but not armed, PARTIAL state when the checkbox is
158         * selected and armed (grey) and NOT_SELECTED when the
159         * checkbox is deselected.
160         */
161        private State getState() {
162            if (isSelected() && !isArmed()) {
163                // normal black tick
164                return State.SELECTED;
165            } else if (isSelected() && isArmed()) {
166                // don't care grey tick
167                return State.PARTIAL;
168            } else if (!isSelected() && !isArmed()) {
169                return State.NOT_SELECTED;
170            } else {
171                return State.UNSET;
172            }
173        }
174        /** Rotate to the next allowed state.*/
175        private void nextState() {
176            State current = getState();
177            for (int i = 0; i < allowed.length; i++) {
178                if (allowed[i] == current) {
179                    setState((i == allowed.length-1) ? allowed[0] : allowed[i+1]);
180                    break;
181                }
182            }
183        }
184        /** Filter: No one may change the armed/selected/pressed status except us. */
185        @Override public void setArmed(boolean b) { }
186        @Override public void setSelected(boolean b) { }
187        @Override public void setPressed(boolean b) { }
188        /** We disable focusing on the component when it is not enabled. */
189        @Override public void setEnabled(boolean b) {
190            setFocusable(b);
191            other.setEnabled(b);
192        }
193        /** All these methods simply delegate to the "other" model
194         * that is being decorated. */
195        @Override public boolean isArmed() { return other.isArmed(); }
196        @Override public boolean isSelected() { return other.isSelected(); }
197        @Override public boolean isEnabled() { return other.isEnabled(); }
198        @Override public boolean isPressed() { return other.isPressed(); }
199        @Override public boolean isRollover() { return other.isRollover(); }
200        @Override public void setRollover(boolean b) { other.setRollover(b); }
201        @Override public void setMnemonic(int key) { other.setMnemonic(key); }
202        @Override public int getMnemonic() { return other.getMnemonic(); }
203        @Override public void setActionCommand(String s) {
204            other.setActionCommand(s);
205        }
206        @Override public String getActionCommand() {
207            return other.getActionCommand();
208        }
209        @Override public void setGroup(ButtonGroup group) {
210            other.setGroup(group);
211        }
212        @Override public void addActionListener(ActionListener l) {
213            other.addActionListener(l);
214        }
215        @Override public void removeActionListener(ActionListener l) {
216            other.removeActionListener(l);
217        }
218        @Override public void addItemListener(ItemListener l) {
219            other.addItemListener(l);
220        }
221        @Override public void removeItemListener(ItemListener l) {
222            other.removeItemListener(l);
223        }
224        @Override public void addChangeListener(ChangeListener l) {
225            other.addChangeListener(l);
226        }
227        @Override public void removeChangeListener(ChangeListener l) {
228            other.removeChangeListener(l);
229        }
230        @Override public Object[] getSelectedObjects() {
231            return other.getSelectedObjects();
232        }
233    }
234}