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 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 public void addMouseListener(MouseListener l) { } 094 095 /** 096 * Set the new state. 097 * @param state The new state 098 */ 099 public void setState(State state) { 100 model.setState(state); 101 } 102 103 /** 104 * Return the current state, which is determined by the selection status of the model. 105 * @return The current state 106 */ 107 public State getState() { 108 return model.getState(); 109 } 110 111 @Override 112 public void setSelected(boolean b) { 113 if (b) { 114 setState(State.SELECTED); 115 } else { 116 setState(State.NOT_SELECTED); 117 } 118 } 119 120 private final class QuadStateDecorator implements ButtonModel { 121 private final ButtonModel other; 122 123 private QuadStateDecorator(ButtonModel other) { 124 this.other = other; 125 } 126 127 private void setState(State state) { 128 if (state == State.NOT_SELECTED) { 129 other.setArmed(false); 130 other.setPressed(false); 131 other.setSelected(false); 132 setToolTipText(tr("false: the property is explicitly switched off")); 133 } else if (state == State.SELECTED) { 134 other.setArmed(false); 135 other.setPressed(false); 136 other.setSelected(true); 137 setToolTipText(tr("true: the property is explicitly switched on")); 138 } else if (state == State.PARTIAL) { 139 other.setArmed(true); 140 other.setPressed(true); 141 other.setSelected(true); 142 setToolTipText(tr("partial: different selected objects have different values, do not change")); 143 } else { 144 other.setArmed(true); 145 other.setPressed(true); 146 other.setSelected(false); 147 setToolTipText(tr("unset: do not set this property on the selected objects")); 148 } 149 } 150 151 /** 152 * The current state is embedded in the selection / armed 153 * state of the model. 154 * 155 * We return the SELECTED state when the checkbox is selected 156 * but not armed, PARTIAL state when the checkbox is 157 * selected and armed (grey) and NOT_SELECTED when the 158 * checkbox is deselected. 159 */ 160 private State getState() { 161 if (isSelected() && !isArmed()) { 162 // normal black tick 163 return State.SELECTED; 164 } else if (isSelected() && isArmed()) { 165 // don't care grey tick 166 return State.PARTIAL; 167 } else if (!isSelected() && !isArmed()) { 168 return State.NOT_SELECTED; 169 } else { 170 return State.UNSET; 171 } 172 } 173 /** Rotate to the next allowed state.*/ 174 private void nextState() { 175 State current = getState(); 176 for (int i = 0; i < allowed.length; i++) { 177 if (allowed[i] == current) { 178 setState((i == allowed.length-1) ? allowed[0] : allowed[i+1]); 179 break; 180 } 181 } 182 } 183 /** Filter: No one may change the armed/selected/pressed status except us. */ 184 @Override public void setArmed(boolean b) { } 185 @Override public void setSelected(boolean b) { } 186 @Override public void setPressed(boolean b) { } 187 /** We disable focusing on the component when it is not enabled. */ 188 @Override public void setEnabled(boolean b) { 189 setFocusable(b); 190 other.setEnabled(b); 191 } 192 /** All these methods simply delegate to the "other" model 193 * that is being decorated. */ 194 @Override public boolean isArmed() { return other.isArmed(); } 195 @Override public boolean isSelected() { return other.isSelected(); } 196 @Override public boolean isEnabled() { return other.isEnabled(); } 197 @Override public boolean isPressed() { return other.isPressed(); } 198 @Override public boolean isRollover() { return other.isRollover(); } 199 @Override public void setRollover(boolean b) { other.setRollover(b); } 200 @Override public void setMnemonic(int key) { other.setMnemonic(key); } 201 @Override public int getMnemonic() { return other.getMnemonic(); } 202 @Override public void setActionCommand(String s) { 203 other.setActionCommand(s); 204 } 205 @Override public String getActionCommand() { 206 return other.getActionCommand(); 207 } 208 @Override public void setGroup(ButtonGroup group) { 209 other.setGroup(group); 210 } 211 @Override public void addActionListener(ActionListener l) { 212 other.addActionListener(l); 213 } 214 @Override public void removeActionListener(ActionListener l) { 215 other.removeActionListener(l); 216 } 217 @Override public void addItemListener(ItemListener l) { 218 other.addItemListener(l); 219 } 220 @Override public void removeItemListener(ItemListener l) { 221 other.removeItemListener(l); 222 } 223 @Override public void addChangeListener(ChangeListener l) { 224 other.addChangeListener(l); 225 } 226 @Override public void removeChangeListener(ChangeListener l) { 227 other.removeChangeListener(l); 228 } 229 @Override public Object[] getSelectedObjects() { 230 return other.getSelectedObjects(); 231 } 232 } 233}