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}