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