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}