001    // License: GPL. Copyright 2008 by Frederik Ramm and others
002    package org.openstreetmap.josm.gui;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.awt.event.ActionEvent;
007    import java.awt.event.ActionListener;
008    import java.awt.event.ItemListener;
009    import java.awt.event.MouseAdapter;
010    import java.awt.event.MouseEvent;
011    import java.awt.event.MouseListener;
012    
013    import javax.swing.AbstractAction;
014    import javax.swing.ActionMap;
015    import javax.swing.ButtonGroup;
016    import javax.swing.ButtonModel;
017    import javax.swing.Icon;
018    import javax.swing.JCheckBox;
019    import javax.swing.SwingUtilities;
020    import javax.swing.event.ChangeListener;
021    import javax.swing.plaf.ActionMapUIResource;
022    
023    public class QuadStateCheckBox extends JCheckBox {
024    
025        public enum State { NOT_SELECTED, SELECTED, UNSET, PARTIAL }
026    
027        private final QuadStateDecorator model;
028        private State[] allowed;
029    
030        public QuadStateCheckBox(String text, Icon icon, State initial, State[] allowed) {
031            super(text, icon);
032            this.allowed = allowed;
033            // Add a listener for when the mouse is pressed
034            super.addMouseListener(new MouseAdapter() {
035                @Override public void mousePressed(MouseEvent e) {
036                    grabFocus();
037                    model.nextState();
038                }
039            });
040            // Reset the keyboard action map
041            ActionMap map = new ActionMapUIResource();
042            map.put("pressed", new AbstractAction() {
043                public void actionPerformed(ActionEvent e) {
044                    grabFocus();
045                    model.nextState();
046                }
047            });
048            map.put("released", null);
049            SwingUtilities.replaceUIActionMap(this, map);
050            // set the model to the adapted model
051            model = new QuadStateDecorator(getModel());
052            setModel(model);
053            setState(initial);
054        }
055        public QuadStateCheckBox(String text, State initial, State[] allowed) {
056            this(text, null, initial, allowed);
057        }
058    
059        /** Do not let anyone add mouse listeners */
060        @Override public void addMouseListener(MouseListener l) { }
061        /**
062         * Set the new state.
063         */
064        public void setState(State state) { model.setState(state); }
065        /** Return the current state, which is determined by the
066         * selection status of the model. */
067        public State getState() { return model.getState(); }
068        @Override public void setSelected(boolean b) {
069            if (b) {
070                setState(State.SELECTED);
071            } else {
072                setState(State.NOT_SELECTED);
073            }
074        }
075    
076        private class QuadStateDecorator implements ButtonModel {
077            private final ButtonModel other;
078            private QuadStateDecorator(ButtonModel other) {
079                this.other = other;
080            }
081            private void setState(State state) {
082                if (state == State.NOT_SELECTED) {
083                    other.setArmed(false);
084                    other.setPressed(false);
085                    other.setSelected(false);
086                    setToolTipText(tr("false: the property is explicitly switched off"));
087                } else if (state == State.SELECTED) {
088                    other.setArmed(false);
089                    other.setPressed(false);
090                    other.setSelected(true);
091                    setToolTipText(tr("true: the property is explicitly switched on"));
092                } else if (state == State.PARTIAL) {
093                    other.setArmed(true);
094                    other.setPressed(true);
095                    other.setSelected(true);
096                    setToolTipText(tr("partial: different selected objects have different values, do not change"));
097                } else {
098                    other.setArmed(true);
099                    other.setPressed(true);
100                    other.setSelected(false);
101                    setToolTipText(tr("unset: do not set this property on the selected objects"));
102                }
103            }
104            /**
105             * The current state is embedded in the selection / armed
106             * state of the model.
107             *
108             * We return the SELECTED state when the checkbox is selected
109             * but not armed, PARTIAL state when the checkbox is
110             * selected and armed (grey) and NOT_SELECTED when the
111             * checkbox is deselected.
112             */
113            private State getState() {
114                if (isSelected() && !isArmed()) {
115                    // normal black tick
116                    return State.SELECTED;
117                } else if (isSelected() && isArmed()) {
118                    // don't care grey tick
119                    return State.PARTIAL;
120                } else if (!isSelected() && !isArmed()) {
121                    return State.NOT_SELECTED;
122                } else {
123                    return State.UNSET;
124                }
125            }
126            /** Rotate to the next allowed state.*/
127            private void nextState() {
128                State current = getState();
129                for (int i = 0; i < allowed.length; i++) {
130                    if (allowed[i] == current) {
131                        setState((i == allowed.length-1) ? allowed[0] : allowed[i+1]);
132                        break;
133                    }
134                }
135            }
136            /** Filter: No one may change the armed/selected/pressed status except us. */
137            public void setArmed(boolean b) { }
138            public void setSelected(boolean b) { }
139            public void setPressed(boolean b) { }
140            /** We disable focusing on the component when it is not
141             * enabled. */
142            public void setEnabled(boolean b) {
143                setFocusable(b);
144                other.setEnabled(b);
145            }
146            /** All these methods simply delegate to the "other" model
147             * that is being decorated. */
148            public boolean isArmed() { return other.isArmed(); }
149            public boolean isSelected() { return other.isSelected(); }
150            public boolean isEnabled() { return other.isEnabled(); }
151            public boolean isPressed() { return other.isPressed(); }
152            public boolean isRollover() { return other.isRollover(); }
153            public void setRollover(boolean b) { other.setRollover(b); }
154            public void setMnemonic(int key) { other.setMnemonic(key); }
155            public int getMnemonic() { return other.getMnemonic(); }
156            public void setActionCommand(String s) {
157                other.setActionCommand(s);
158            }
159            public String getActionCommand() {
160                return other.getActionCommand();
161            }
162            public void setGroup(ButtonGroup group) {
163                other.setGroup(group);
164            }
165            public void addActionListener(ActionListener l) {
166                other.addActionListener(l);
167            }
168            public void removeActionListener(ActionListener l) {
169                other.removeActionListener(l);
170            }
171            public void addItemListener(ItemListener l) {
172                other.addItemListener(l);
173            }
174            public void removeItemListener(ItemListener l) {
175                other.removeItemListener(l);
176            }
177            public void addChangeListener(ChangeListener l) {
178                other.addChangeListener(l);
179            }
180            public void removeChangeListener(ChangeListener l) {
181                other.removeChangeListener(l);
182            }
183            public Object[] getSelectedObjects() {
184                return other.getSelectedObjects();
185            }
186        }
187    }