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