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