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 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 * @return current state 161 */ 162 private State getState() { 163 if (isSelected() && !isArmed()) { 164 // normal black tick 165 return State.SELECTED; 166 } else if (isSelected() && isArmed()) { 167 // don't care grey tick 168 return State.PARTIAL; 169 } else if (!isSelected() && !isArmed()) { 170 return State.NOT_SELECTED; 171 } else { 172 return State.UNSET; 173 } 174 } 175 176 /** Rotate to the next allowed state.*/ 177 private void nextState() { 178 State current = getState(); 179 for (int i = 0; i < allowed.length; i++) { 180 if (allowed[i] == current) { 181 setState((i == allowed.length-1) ? allowed[0] : allowed[i+1]); 182 break; 183 } 184 } 185 } 186 187 // ---------------------------------------------------------------------- 188 // Filter: No one may change the armed/selected/pressed status except us. 189 // ---------------------------------------------------------------------- 190 191 @Override 192 public void setArmed(boolean b) { } 193 194 @Override 195 public void setSelected(boolean b) { } 196 197 @Override 198 public void setPressed(boolean b) { } 199 200 /** We disable focusing on the component when it is not enabled. */ 201 @Override 202 public void setEnabled(boolean b) { 203 setFocusable(b); 204 other.setEnabled(b); 205 } 206 207 // ------------------------------------------------------------------------------- 208 // All these methods simply delegate to the "other" model that is being decorated. 209 // ------------------------------------------------------------------------------- 210 211 @Override 212 public boolean isArmed() { 213 return other.isArmed(); 214 } 215 216 @Override 217 public boolean isSelected() { 218 return other.isSelected(); 219 } 220 221 @Override 222 public boolean isEnabled() { 223 return other.isEnabled(); 224 } 225 226 @Override 227 public boolean isPressed() { 228 return other.isPressed(); 229 } 230 231 @Override 232 public boolean isRollover() { 233 return other.isRollover(); 234 } 235 236 @Override 237 public void setRollover(boolean b) { 238 other.setRollover(b); 239 } 240 241 @Override 242 public void setMnemonic(int key) { 243 other.setMnemonic(key); 244 } 245 246 @Override 247 public int getMnemonic() { 248 return other.getMnemonic(); 249 } 250 251 @Override 252 public void setActionCommand(String s) { 253 other.setActionCommand(s); 254 } 255 256 @Override public String getActionCommand() { 257 return other.getActionCommand(); 258 } 259 260 @Override public void setGroup(ButtonGroup group) { 261 other.setGroup(group); 262 } 263 264 @Override public void addActionListener(ActionListener l) { 265 other.addActionListener(l); 266 } 267 268 @Override public void removeActionListener(ActionListener l) { 269 other.removeActionListener(l); 270 } 271 272 @Override public void addItemListener(ItemListener l) { 273 other.addItemListener(l); 274 } 275 276 @Override public void removeItemListener(ItemListener l) { 277 other.removeItemListener(l); 278 } 279 280 @Override public void addChangeListener(ChangeListener l) { 281 other.addChangeListener(l); 282 } 283 284 @Override public void removeChangeListener(ChangeListener l) { 285 other.removeChangeListener(l); 286 } 287 288 @Override public Object[] getSelectedObjects() { 289 return other.getSelectedObjects(); 290 } 291 } 292}