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