001package jmri.util.swing; 002 003import java.awt.*; 004import java.awt.event.*; 005 006import javax.annotation.Nonnull; 007import javax.swing.*; 008 009import org.slf4j.Logger; 010import org.slf4j.LoggerFactory; 011 012/** 013 * JPanel containing 014 * Extension of JCheckBox allowing a partial state to be displayed. 015 * 016 * The partial state is in the form of a small square, a style similar to Google Earth. 017 * 018 * If Checkbox is pressed when unchecked, state changes to checked. 019 * If Checkbox is pressed when checked, state changes to unchecked. 020 * If Checkbox is pressed when partial, state changes to unchecked. 021 * 022 * User can only check / un-check the checkbox, 023 * the partial state is only available programatically. 024 * 025 * State can be set by enum or an array of boolean values. 026 * 027 * The enum of the actual state can be obtained, 028 * isSelected() returns false when in a partial state. 029 * 030 * <p> 031 * Inspired by postings of 032 * 843806 https://community.oracle.com/tech/developers/discussion/1354306/is-there-a-tri-state-check-box-in-swing 033 * s1w_ https://stackoverflow.com/questions/1263323/tristate-checkboxes-in-java 034 * 035 * 036 * @author Steve Young Copyright (c) 2021 037 */ 038public class TriStateJCheckBox extends JPanel { 039 040 private final JPanel contentContainer; 041 private final JCheckBox checkBox; 042 private final JLabel label; 043 private TriStateModel model; 044 private String text; 045 046 /** 047 * Enum of TriStateJCheckBox state values. 048 */ 049 public static enum State { 050 CHECKED, UNCHECKED, PARTIAL 051 } 052 053 /** 054 * Creates a check box. 055 * Defaults to unchecked state. 056 * 057 */ 058 public TriStateJCheckBox () { 059 this(null); 060 } 061 062 /** 063 * Creates a check box with text. 064 * Defaults to unchecked state. 065 * 066 * @param labelText the text of the check box. 067 */ 068 public TriStateJCheckBox (String labelText) { 069 super(); 070 text = labelText; 071 072 contentContainer = new JPanel(new BorderLayout(0, 0)); 073 label = new JLabel(); 074 checkBox = new TriCheckBox(); 075 initProperties(); 076 } 077 078 private void initProperties(){ 079 080 log.debug("started TriStateJCheckBox"); 081 082 setLayout(new GridBagLayout()); 083 084 model = new TriStateModel(State.UNCHECKED); 085 checkBox.setModel(model); 086 087 // INITIALIZE COMPONENTS 088 checkBox.setOpaque(false); 089 label.setOpaque(false); 090 contentContainer.setOpaque(false); 091 setOpaque(false); 092 setText(text); 093 094 label.setLabelFor(checkBox); 095 096 // LAYOUT COMPONENTS 097 contentContainer.add(checkBox, BorderLayout.WEST); 098 contentContainer.add(label, BorderLayout.CENTER); 099 final GridBagConstraints gbc = new GridBagConstraints(); 100 gbc.weightx = 1; 101 gbc.weighty = 1; 102 this.add(contentContainer, gbc); 103 104 final MouseListener labelAndPanelListener = new MouseAdapter() { 105 106 @Override 107 public void mousePressed(final MouseEvent e) { 108 if (e.getButton() == JmriMouseEvent.BUTTON1) { 109 model.setPressed(true); 110 } 111 } 112 113 @Override 114 public void mouseReleased(final MouseEvent e) { 115 if (e.getButton() == JmriMouseEvent.BUTTON1) { 116 model.setPressed(false); 117 } 118 } 119 120 @Override 121 public void mouseEntered(final MouseEvent e) { 122 model.setRollover(true); 123 } 124 125 @Override 126 public void mouseExited(final MouseEvent e) { 127 model.setRollover(false); 128 } 129 130 }; 131 132 label.addMouseListener(labelAndPanelListener); 133 134 } 135 136 /** 137 * Set the new state to either CHECKED, PARTIAL or UNCHECKED. 138 * @param state enum of new state. 139 */ 140 public void setState(@Nonnull State state) { 141 model.setState(state); 142 checkBox.repaint(); 143 } 144 145 /** 146 * Set the new state using values within a boolean array. 147 * 148 * boolean[]{false,false} = UNCHECKED 149 * boolean[]{true,true} = CHECKED 150 * boolean[]{true,false} = PARTIAL 151 * 152 * @param booleanArray boolean values to compare 153 */ 154 public void setState(boolean[] booleanArray){ 155 if (arrayDoesNotContainsTrueOrFalse(booleanArray,true)){ 156 setState(State.UNCHECKED); 157 } else if (arrayDoesNotContainsTrueOrFalse(booleanArray,false)){ 158 setState(State.CHECKED); 159 } else { 160 setState(State.PARTIAL); 161 } 162 } 163 164 private boolean arrayDoesNotContainsTrueOrFalse(boolean[] booleanArray, boolean condition){ 165 for(boolean value: booleanArray){ 166 if ( value == condition ) { 167 return false; 168 } 169 } 170 return true; 171 } 172 173 /** 174 * Return the current state, which is determined by the selection status of 175 * the model. 176 * @return enum of current state. 177 */ 178 @Nonnull 179 public State getState() { 180 return model.getState(); 181 } 182 183 /** 184 * Set the CheckBox to Selected or Unselected. 185 * @param selected true for selected, else false. 186 */ 187 public void setSelected(boolean selected) { 188 checkBox.setSelected(selected); 189 } 190 191 /** 192 * Is the CheckBox currently fully selected? 193 * @return true if CHECKED, false for UNCHECKED and PARTIAL. 194 */ 195 public boolean isSelected(){ 196 return checkBox.isSelected(); 197 } 198 199 /** 200 * Set the CheckBox enabled or disabled. 201 * @param enabled true for enabled, false disabled. 202 */ 203 @Override 204 public void setEnabled(final boolean enabled) { 205 checkBox.setEnabled(enabled); 206 } 207 208 /** 209 * Set the CheckBox Label Text if different from constructor. 210 * @param newText New Text Label. 211 */ 212 public void setText(final String newText) { 213 if (newText == null || newText.trim().isEmpty()) { 214 text = null; 215 } else { 216 text = newText; 217 } 218 label.setText(text); 219 } 220 221 /** 222 * Add an ActionListener to the JCheckBox. 223 * @param al the ActionListener to add. 224 */ 225 public void addActionListener(ActionListener al) { 226 checkBox.addActionListener(al); 227 } 228 229 @Override 230 public void setToolTipText(String s){ 231 checkBox.setToolTipText(s); 232 label.setToolTipText(s); 233 contentContainer.setToolTipText(s); 234 super.setToolTipText(s); 235 } 236 237 /** 238 * Model for checkbox 239 */ 240 private class TriStateModel extends JToggleButton.ToggleButtonModel { 241 242 protected State state; 243 244 public TriStateModel(State state) { 245 this.state = state; 246 } 247 248 /** 249 * {@inheritDoc} 250 */ 251 @Override 252 public boolean isSelected() { 253 return state == State.CHECKED; 254 } 255 256 public State getState() { 257 return state; 258 } 259 260 public void setState(State state) { 261 this.state = state; 262 fireStateChanged(); 263 } 264 265 /** 266 * {@inheritDoc} 267 */ 268 @Override 269 public void setPressed(boolean pressed) { 270 log.debug("setPressed {}",pressed); 271 if (pressed && isEnabled()) { 272 state = ( state==State.UNCHECKED ? State.CHECKED : State.UNCHECKED ); 273 fireStateChanged(); 274 fireActionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED,getActionCommand())); 275 } 276 } 277 278 /** 279 * {@inheritDoc} 280 */ 281 @Override 282 public void setSelected(boolean selected) { 283 this.state = ( selected ? State.CHECKED : State.UNCHECKED ); 284 super.setSelected(selected); 285 } 286 } 287 288 private static class TriCheckBox extends JCheckBox { 289 290 private static final int DOT_SIZE = 2; 291 292 protected TriCheckBox() { 293 super(); 294 } 295 296 @Override 297 protected void paintComponent(final Graphics g) { 298 super.paintComponent(g); 299 300 if ((((TriStateModel) model).getState() == TriStateJCheckBox.State.PARTIAL)){ 301 final int w = getWidth(); 302 final int h = getHeight(); 303 final int wOdd = w % 2; 304 final int hOdd = h % 2; 305 final int centerX = w / 2; 306 final int centerY = h / 2; 307 final int rw = DOT_SIZE * 2 + wOdd; 308 final int rh = DOT_SIZE * 2 + hOdd; 309 310 g.setColor(isEnabled() ? new Color(51, 51, 51) : new Color(122, 138, 153)); 311 g.fillRect(centerX - DOT_SIZE, centerY - DOT_SIZE, rw, rh); 312 } 313 } 314 } 315 316 private final static Logger log = LoggerFactory.getLogger(TriStateJCheckBox.class); 317 318}