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 cbModel; 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 cbModel.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 cbModel.nextState(); 072 } 073 }); 074 map.put("released", null); 075 SwingUtilities.replaceUIActionMap(this, map); 076 // set the model to the adapted model 077 cbModel = new QuadStateDecorator(getModel()); 078 setModel(cbModel); 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 synchronized void addMouseListener(MouseListener l) { 095 // Do nothing 096 } 097 098 /** 099 * Sets a text describing this property in the tooltip text 100 * @param propertyText a description for the modelled property 101 */ 102 public final void setPropertyText(final String propertyText) { 103 cbModel.setPropertyText(propertyText); 104 } 105 106 /** 107 * Set the new state. 108 * @param state The new state 109 */ 110 public final void setState(State state) { 111 cbModel.setState(state); 112 } 113 114 /** 115 * Return the current state, which is determined by the selection status of the model. 116 * @return The current state 117 */ 118 public State getState() { 119 return cbModel.getState(); 120 } 121 122 @Override 123 public void setSelected(boolean b) { 124 if (b) { 125 setState(State.SELECTED); 126 } else { 127 setState(State.NOT_SELECTED); 128 } 129 } 130 131 /** 132 * Button model for the {@code QuadStateCheckBox}. 133 */ 134 private final class QuadStateDecorator implements ButtonModel { 135 private final ButtonModel other; 136 private String propertyText; 137 138 private QuadStateDecorator(ButtonModel other) { 139 this.other = other; 140 } 141 142 private void setState(State state) { 143 if (state == State.NOT_SELECTED) { 144 other.setArmed(false); 145 other.setPressed(false); 146 other.setSelected(false); 147 setToolTipText(propertyText == null 148 ? tr("false: the property is explicitly switched off") 149 : tr("false: the property ''{0}'' is explicitly switched off", propertyText)); 150 } else if (state == State.SELECTED) { 151 other.setArmed(false); 152 other.setPressed(false); 153 other.setSelected(true); 154 setToolTipText(propertyText == null 155 ? tr("true: the property is explicitly switched on") 156 : tr("true: the property ''{0}'' is explicitly switched on", propertyText)); 157 } else if (state == State.PARTIAL) { 158 other.setArmed(true); 159 other.setPressed(true); 160 other.setSelected(true); 161 setToolTipText(propertyText == null 162 ? tr("partial: different selected objects have different values, do not change") 163 : tr("partial: different selected objects have different values for ''{0}'', do not change", propertyText)); 164 } else { 165 other.setArmed(true); 166 other.setPressed(true); 167 other.setSelected(false); 168 setToolTipText(propertyText == null 169 ? tr("unset: do not set this property on the selected objects") 170 : tr("unset: do not set the property ''{0}'' on the selected objects", propertyText)); 171 } 172 } 173 174 private void setPropertyText(String propertyText) { 175 this.propertyText = propertyText; 176 } 177 178 /** 179 * The current state is embedded in the selection / armed 180 * state of the model. 181 * 182 * We return the SELECTED state when the checkbox is selected 183 * but not armed, PARTIAL state when the checkbox is 184 * selected and armed (grey) and NOT_SELECTED when the 185 * checkbox is deselected. 186 * @return current state 187 */ 188 private State getState() { 189 if (isSelected() && !isArmed()) { 190 // normal black tick 191 return State.SELECTED; 192 } else if (isSelected() && isArmed()) { 193 // don't care grey tick 194 return State.PARTIAL; 195 } else if (!isSelected() && !isArmed()) { 196 return State.NOT_SELECTED; 197 } else { 198 return State.UNSET; 199 } 200 } 201 202 /** Rotate to the next allowed state.*/ 203 private void nextState() { 204 State current = getState(); 205 for (int i = 0; i < allowed.length; i++) { 206 if (allowed[i] == current) { 207 setState((i == allowed.length-1) ? allowed[0] : allowed[i+1]); 208 break; 209 } 210 } 211 } 212 213 // ---------------------------------------------------------------------- 214 // Filter: No one may change the armed/selected/pressed status except us. 215 // ---------------------------------------------------------------------- 216 217 @Override 218 public void setArmed(boolean b) { 219 // Do nothing 220 } 221 222 @Override 223 public void setSelected(boolean b) { 224 // Do nothing 225 } 226 227 @Override 228 public void setPressed(boolean b) { 229 // Do nothing 230 } 231 232 /** We disable focusing on the component when it is not enabled. */ 233 @Override 234 public void setEnabled(boolean b) { 235 setFocusable(b); 236 if (other != null) { 237 other.setEnabled(b); 238 } 239 } 240 241 // ------------------------------------------------------------------------------- 242 // All these methods simply delegate to the "other" model that is being decorated. 243 // ------------------------------------------------------------------------------- 244 245 @Override 246 public boolean isArmed() { 247 return other.isArmed(); 248 } 249 250 @Override 251 public boolean isSelected() { 252 return other.isSelected(); 253 } 254 255 @Override 256 public boolean isEnabled() { 257 return other.isEnabled(); 258 } 259 260 @Override 261 public boolean isPressed() { 262 return other.isPressed(); 263 } 264 265 @Override 266 public boolean isRollover() { 267 return other.isRollover(); 268 } 269 270 @Override 271 public void setRollover(boolean b) { 272 other.setRollover(b); 273 } 274 275 @Override 276 public void setMnemonic(int key) { 277 other.setMnemonic(key); 278 } 279 280 @Override 281 public int getMnemonic() { 282 return other.getMnemonic(); 283 } 284 285 @Override 286 public void setActionCommand(String s) { 287 other.setActionCommand(s); 288 } 289 290 @Override public String getActionCommand() { 291 return other.getActionCommand(); 292 } 293 294 @Override public void setGroup(ButtonGroup group) { 295 other.setGroup(group); 296 } 297 298 @Override public void addActionListener(ActionListener l) { 299 other.addActionListener(l); 300 } 301 302 @Override public void removeActionListener(ActionListener l) { 303 other.removeActionListener(l); 304 } 305 306 @Override public void addItemListener(ItemListener l) { 307 other.addItemListener(l); 308 } 309 310 @Override public void removeItemListener(ItemListener l) { 311 other.removeItemListener(l); 312 } 313 314 @Override public void addChangeListener(ChangeListener l) { 315 other.addChangeListener(l); 316 } 317 318 @Override public void removeChangeListener(ChangeListener l) { 319 other.removeChangeListener(l); 320 } 321 322 @Override public Object[] getSelectedObjects() { 323 return other.getSelectedObjects(); 324 } 325 } 326}