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