From a7c6a881b3a11d920a1ab7497941db6ef2149b15 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 19 Dec 2020 16:00:57 +0100 Subject: [PATCH] Extras: FlatTriStateCheckBox reworked --- CHANGELOG.md | 3 +- .../components/FlatTriStateCheckBox.java | 177 ++++++++++++++---- .../flatlaf/testing/FlatContainerTest.java | 2 +- .../testing/extras/FlatExtrasTest.java | 55 ++++-- .../flatlaf/testing/extras/FlatExtrasTest.jfd | 55 ++++-- 5 files changed, 220 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e2d448b..4ad339ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ FlatLaf Change Log `com.formdev.flatlaf.extras.components`). - Extras: Renamed tri-state check box class from `com.formdev.flatlaf.extras.TriStateCheckBox` to - `com.formdev.flatlaf.extras.components.FlatTriStateCheckBox`. + `com.formdev.flatlaf.extras.components.FlatTriStateCheckBox`. Also + changed/improved API and added javadoc. - Extras: Renamed SVG utility class from `com.formdev.flatlaf.extras.SVGUtils` to `com.formdev.flatlaf.extras.FlatSVGUtils`. - IntelliJ Themes: Added flag whether a theme is dark to diff --git a/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/components/FlatTriStateCheckBox.java b/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/components/FlatTriStateCheckBox.java index 781fac03..90964dbd 100644 --- a/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/components/FlatTriStateCheckBox.java +++ b/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/components/FlatTriStateCheckBox.java @@ -28,11 +28,31 @@ import com.formdev.flatlaf.FlatLaf; /** * A tri-state check box. *

+ * The initial state is {@link State#INDETERMINATE}. + *

+ * By default the third state is allowed and clicking on the checkbox cycles thru all + * three states. If you want that the user can cycle only thru two states, disallow + * intermediate state using {@link #setAllowIndeterminate(boolean)}. Then you can still + * set the indeterminate state via API if necessary, but the user can not. + *

+ * The default state cycle order is {@link State#UNSELECTED} to {@link State#INDETERMINATE} + * to {@link State#SELECTED}. + * This is the same order as used by macOS, win32, IntelliJ IDEA and on the web as recommended by W3C in + * Tri-State Checkbox Example). + *

+ * If {@link #isAltStateCycleOrder()} returns {@code true}, + * the state cycle order is {@link State#UNSELECTED} to {@link State#SELECTED} + * to {@link State#INDETERMINATE}. This order is used by Windows 10 UWP apps. + *

+ * If you prefer the alternative state cycle order for all tri-state check boxes, enable it using: + *

+ * UIManager.put( "FlatTriStateCheckBox.altStateCycleOrder", true );
+ * 
+ *

* To display the third state, this component requires an LaF that supports painting * the indeterminate state if client property {@code "JButton.selectedState"} has the * value {@code "indeterminate"}. - *

- * FlatLaf and Mac Aqua LaF support the third state. + * FlatLaf and macOS Aqua LaF support the third state. * For other LaFs a magenta rectangle is painted around the component for the third state. * * @author Karl Tauber @@ -40,10 +60,11 @@ import com.formdev.flatlaf.FlatLaf; public class FlatTriStateCheckBox extends JCheckBox { - public enum State { INDETERMINATE, SELECTED, UNSELECTED } + public enum State { UNSELECTED, INDETERMINATE, SELECTED } private State state; - private boolean thirdStateEnabled = true; + private boolean allowIndeterminate = true; + private boolean altStateCycleOrder = UIManager.getBoolean( "FlatTriStateCheckBox.altStateCycleOrder" ); public FlatTriStateCheckBox() { this( null ); @@ -64,11 +85,7 @@ public class FlatTriStateCheckBox @Override public void setSelected( boolean b ) { - switch( state ) { - case INDETERMINATE: setState( State.SELECTED ); break; - case SELECTED: setState( State.UNSELECTED ); break; - case UNSELECTED: setState( thirdStateEnabled ? State.INDETERMINATE : State.SELECTED ); break; - } + setState( nextState( state ) ); fireStateChanged(); fireItemStateChanged( new ItemEvent( this, ItemEvent.ITEM_STATE_CHANGED, this, @@ -79,10 +96,19 @@ public class FlatTriStateCheckBox setState( initialState ); } + /** + * Returns the state as {@link State} enum. + *

+ * Alternatively you can use {@link #getChecked()} to get all three states as {@link Boolean} + * or {@link #isIndeterminate()} to check only for indeterminate state. + */ public State getState() { return state; } + /** + * Sets the state as {@link State} enum. + */ public void setState( State state ) { if( this.state == state ) return; @@ -90,34 +116,58 @@ public class FlatTriStateCheckBox State oldState = this.state; this.state = state; - putClientProperty( SELECTED_STATE, state == State.INDETERMINATE ? SELECTED_STATE_INDETERMINATE : null ); + putClientProperty( SELECTED_STATE, (state == State.INDETERMINATE) ? SELECTED_STATE_INDETERMINATE : null ); firePropertyChange( "state", oldState, state ); repaint(); } - public Boolean getValue() { - switch( state ) { - default: - case INDETERMINATE: return null; - case SELECTED: return true; - case UNSELECTED: return false; + /** + * Returns the next state that follows the given state, depending on + * {@link #isAllowIndeterminate()} and {@link #isAltStateCycleOrder()}. + */ + protected State nextState( State state ) { + if( !altStateCycleOrder ) { + // default cycle order: UNSELECTED --> INDETERMINATE --> SELECTED + switch( state ) { + default: + case UNSELECTED: return allowIndeterminate ? State.INDETERMINATE : State.SELECTED; + case INDETERMINATE: return State.SELECTED; + case SELECTED: return State.UNSELECTED; + } + } else { + // alternative cycle order: INDETERMINATE --> UNSELECTED --> SELECTED + switch( state ) { + default: + case UNSELECTED: return State.SELECTED; + case INDETERMINATE: return State.UNSELECTED; + case SELECTED: return allowIndeterminate ? State.INDETERMINATE : State.UNSELECTED; + } } } - public void setValue( Boolean value ) { - setState( value == null ? State.INDETERMINATE : (value ? State.SELECTED : State.UNSELECTED) ); + /** + * Returns the state as {@link Boolean}. + * Returns {@code null} if the state is {@link State#INDETERMINATE}. + *

+ * Alternatively you can use {@link #getState()} to get state as {@link State} enum + * or {@link #isIndeterminate()} to check only for indeterminate state. + */ + public Boolean getChecked() { + switch( state ) { + default: + case UNSELECTED: return false; + case INDETERMINATE: return null; + case SELECTED: return true; + } } - public boolean isThirdStateEnabled() { - return thirdStateEnabled; - } - - public void setThirdStateEnabled( boolean thirdStateEnabled ) { - this.thirdStateEnabled = thirdStateEnabled; - - if( state == State.INDETERMINATE ) - setState( State.UNSELECTED ); + /** + * Sets the state as {@link Boolean}. + * Passing {@code null} sets state to {@link State#INDETERMINATE}. + */ + public void setChecked( Boolean value ) { + setState( (value == null) ? State.INDETERMINATE : (value ? State.SELECTED : State.UNSELECTED) ); } @Override @@ -125,17 +175,80 @@ public class FlatTriStateCheckBox setState( b ? State.SELECTED : State.UNSELECTED ); } + /** + * Returns whether state is indeterminate. + */ + public boolean isIndeterminate() { + return state == State.INDETERMINATE; + } + + /** + * Sets indeterminate state. + */ + public void setIndeterminate( boolean indeterminate ) { + if( indeterminate ) + setState( State.INDETERMINATE ); + else if( state == State.INDETERMINATE ) + setState( State.UNSELECTED ); + } + + /** + * Returns whether indeterminate state is allowed. + *

+ * This affects only the user when clicking on the checkbox. + * Setting state to indeterminate via API is always allowed. + */ + public boolean isAllowIndeterminate() { + return allowIndeterminate; + } + + /** + * Sets whether indeterminate state is allowed. + *

+ * This affects only the user when clicking on the checkbox. + * Setting state to indeterminate via API is always allowed. + */ + public void setAllowIndeterminate( boolean allowIndeterminate ) { + this.allowIndeterminate = allowIndeterminate; + } + + /** + * Returns whether alternative state cycle order should be used. + */ + public boolean isAltStateCycleOrder() { + return altStateCycleOrder; + } + + /** + * Sets whether alternative state cycle order should be used. + */ + public void setAltStateCycleOrder( boolean altStateCycleOrder ) { + this.altStateCycleOrder = altStateCycleOrder; + } + @Override protected void paintComponent( Graphics g ) { super.paintComponent( g ); - if( state == State.INDETERMINATE && !isThirdStateSupported() ) { - g.setColor( Color.magenta ); - g.drawRect( 0, 0, getWidth() - 1, getHeight() - 1 ); - } + if( state == State.INDETERMINATE && !isIndeterminateStateSupported() ) + paintIndeterminateState( g ); } - private boolean isThirdStateSupported() { + /** + * Paints the indeterminate state if the current LaF does not support displaying + * the indeterminate state. + * The default implementation draws a magenta rectangle around the component. + */ + protected void paintIndeterminateState( Graphics g ) { + g.setColor( Color.magenta ); + g.drawRect( 0, 0, getWidth() - 1, getHeight() - 1 ); + } + + /** + * Returns whether the current LaF supports displaying the indeterminate state. + * Returns {@code true} for FlatLaf and macOS Aqua. + */ + protected boolean isIndeterminateStateSupported() { LookAndFeel laf = UIManager.getLookAndFeel(); return laf instanceof FlatLaf || laf.getClass().getName().equals( "com.apple.laf.AquaLookAndFeel" ); } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatContainerTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatContainerTest.java index 5dc283ed..bf32a30b 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatContainerTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatContainerTest.java @@ -359,7 +359,7 @@ public class FlatContainerTest } private void secondTabClosableChanged() { - Boolean closable = secondTabClosableCheckBox.getValue(); + Boolean closable = secondTabClosableCheckBox.getChecked(); for( FlatTabbedPane tabbedPane : allTabbedPanes ) { if( tabbedPane.getTabCount() > 1 ) { diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/extras/FlatExtrasTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/extras/FlatExtrasTest.java index 098ae845..143cf6cb 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/extras/FlatExtrasTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/extras/FlatExtrasTest.java @@ -38,8 +38,8 @@ public class FlatExtrasTest public FlatExtrasTest() { initComponents(); - triStateLabel1.setText( triStateCheckBox1.getState().toString() ); - triStateLabel2.setText( triStateCheckBox2.getState().toString() ); + triStateCheckBox1Changed(); + triStateCheckBox2Changed(); addSVGIcon( "actions/copy.svg" ); addSVGIcon( "actions/colors.svg" ); @@ -90,6 +90,10 @@ public class FlatExtrasTest triStateLabel2.setText( triStateCheckBox2.getState().toString() ); } + private void triStateCheckBox3Changed() { + triStateLabel3.setText( triStateCheckBox3.getState().toString() ); + } + private void disabledChanged() { boolean enabled = !disabledCheckBox.isSelected(); @@ -120,6 +124,8 @@ public class FlatExtrasTest label1 = new JLabel(); triStateCheckBox1 = new FlatTriStateCheckBox(); triStateLabel1 = new JLabel(); + triStateCheckBox3 = new FlatTriStateCheckBox(); + triStateLabel3 = new JLabel(); triStateCheckBox2 = new FlatTriStateCheckBox(); triStateLabel2 = new JLabel(); label2 = new JLabel(); @@ -166,20 +172,31 @@ public class FlatExtrasTest triStateLabel1.setEnabled(false); add(triStateLabel1, "cell 2 0,gapx 30"); + //---- triStateCheckBox3 ---- + triStateCheckBox3.setText("alt state cycle order"); + triStateCheckBox3.setAltStateCycleOrder(true); + triStateCheckBox3.addActionListener(e -> triStateCheckBox3Changed()); + add(triStateCheckBox3, "cell 1 1"); + + //---- triStateLabel3 ---- + triStateLabel3.setText("text"); + triStateLabel3.setEnabled(false); + add(triStateLabel3, "cell 2 1,gapx 30"); + //---- triStateCheckBox2 ---- triStateCheckBox2.setText("third state disabled"); - triStateCheckBox2.setThirdStateEnabled(false); + triStateCheckBox2.setAllowIndeterminate(false); triStateCheckBox2.addActionListener(e -> triStateCheckBox2Changed()); - add(triStateCheckBox2, "cell 1 1"); + add(triStateCheckBox2, "cell 1 2"); //---- triStateLabel2 ---- triStateLabel2.setText("text"); triStateLabel2.setEnabled(false); - add(triStateLabel2, "cell 2 1,gapx 30"); + add(triStateLabel2, "cell 2 2,gapx 30"); //---- label2 ---- label2.setText("SVG Icons:"); - add(label2, "cell 0 2"); + add(label2, "cell 0 3"); //======== svgIconsPanel ======== { @@ -190,50 +207,50 @@ public class FlatExtrasTest // rows "[grow,center]")); } - add(svgIconsPanel, "cell 1 2 2 1"); + add(svgIconsPanel, "cell 1 3 2 1"); //---- label3 ---- label3.setText("The icons may change colors when switching to another theme."); - add(label3, "cell 1 3 2 1"); + add(label3, "cell 1 4 2 1"); //---- label4 ---- label4.setText("Disabled SVG Icons:"); - add(label4, "cell 0 4"); + add(label4, "cell 0 5"); //---- disabledLabel ---- disabledLabel.setText("label"); - add(disabledLabel, "cell 1 4 2 1"); + add(disabledLabel, "cell 1 5 2 1"); //---- disabledButton ---- disabledButton.setText("button"); - add(disabledButton, "cell 1 4 2 1"); - add(disabledTabbedPane, "cell 1 4 2 1"); + add(disabledButton, "cell 1 5 2 1"); + add(disabledTabbedPane, "cell 1 5 2 1"); //---- label5 ---- label5.setText("only setIcon()"); label5.setEnabled(false); - add(label5, "cell 1 4 2 1,gapx 20"); + add(label5, "cell 1 5 2 1,gapx 20"); //---- disabledCheckBox ---- disabledCheckBox.setText("disabled"); disabledCheckBox.setSelected(true); disabledCheckBox.setMnemonic('D'); disabledCheckBox.addActionListener(e -> disabledChanged()); - add(disabledCheckBox, "cell 0 5,alignx left,growx 0"); + add(disabledCheckBox, "cell 0 6,alignx left,growx 0"); //---- disabledLabel2 ---- disabledLabel2.setText("label"); - add(disabledLabel2, "cell 1 5 2 1"); + add(disabledLabel2, "cell 1 6 2 1"); //---- disabledButton2 ---- disabledButton2.setText("button"); - add(disabledButton2, "cell 1 5 2 1"); - add(disabledTabbedPane2, "cell 1 5 2 1"); + add(disabledButton2, "cell 1 6 2 1"); + add(disabledTabbedPane2, "cell 1 6 2 1"); //---- label6 ---- label6.setText("setIcon() and setDisabledIcon()"); label6.setEnabled(false); - add(label6, "cell 1 5 2 1,gapx 20"); + add(label6, "cell 1 6 2 1,gapx 20"); // JFormDesigner - End of component initialization //GEN-END:initComponents } @@ -241,6 +258,8 @@ public class FlatExtrasTest private JLabel label1; private FlatTriStateCheckBox triStateCheckBox1; private JLabel triStateLabel1; + private FlatTriStateCheckBox triStateCheckBox3; + private JLabel triStateLabel3; private FlatTriStateCheckBox triStateCheckBox2; private JLabel triStateLabel2; private JLabel label2; diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/extras/FlatExtrasTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/extras/FlatExtrasTest.jfd index 0f0c2222..2853a30d 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/extras/FlatExtrasTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/extras/FlatExtrasTest.jfd @@ -1,4 +1,4 @@ -JFDML JFormDesigner: "7.0.2.0.298" Java: "15" encoding: "UTF-8" +JFDML JFormDesigner: "7.0.3.1.342" Java: "15" encoding: "UTF-8" new FormModel { contentType: "form/swing" @@ -30,25 +30,40 @@ new FormModel { "value": "cell 2 0,gapx 30" } ) add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { - name: "triStateCheckBox2" - "text": "third state disabled" - "thirdStateEnabled": false - addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "triStateCheckBox2Changed", false ) ) + name: "triStateCheckBox3" + "text": "alt state cycle order" + "altStateCycleOrder": true + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "triStateCheckBox3Changed", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 1" } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "triStateLabel3" + "text": "text" + "enabled": false + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 1,gapx 30" + } ) + add( new FormComponent( "com.formdev.flatlaf.extras.components.FlatTriStateCheckBox" ) { + name: "triStateCheckBox2" + "text": "third state disabled" + "allowIndeterminate": false + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "triStateCheckBox2Changed", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "triStateLabel2" "text": "text" "enabled": false }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 2 1,gapx 30" + "value": "cell 2 2,gapx 30" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "label2" "text": "SVG Icons:" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 2" + "value": "cell 0 3" } ) add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "insets 0,hidemode 3" @@ -57,43 +72,43 @@ new FormModel { } ) { name: "svgIconsPanel" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 2 2 1" + "value": "cell 1 3 2 1" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "label3" "text": "The icons may change colors when switching to another theme." }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 3 2 1" + "value": "cell 1 4 2 1" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "label4" "text": "Disabled SVG Icons:" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 4" + "value": "cell 0 5" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "disabledLabel" "text": "label" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 4 2 1" + "value": "cell 1 5 2 1" } ) add( new FormComponent( "javax.swing.JButton" ) { name: "disabledButton" "text": "button" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 4 2 1" + "value": "cell 1 5 2 1" } ) add( new FormContainer( "javax.swing.JTabbedPane", new FormLayoutManager( class javax.swing.JTabbedPane ) ) { name: "disabledTabbedPane" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 4 2 1" + "value": "cell 1 5 2 1" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "label5" "text": "only setIcon()" "enabled": false }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 4 2 1,gapx 20" + "value": "cell 1 5 2 1,gapx 20" } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "disabledCheckBox" @@ -102,35 +117,35 @@ new FormModel { "mnemonic": 68 addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "disabledChanged", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 5,alignx left,growx 0" + "value": "cell 0 6,alignx left,growx 0" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "disabledLabel2" "text": "label" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 5 2 1" + "value": "cell 1 6 2 1" } ) add( new FormComponent( "javax.swing.JButton" ) { name: "disabledButton2" "text": "button" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 5 2 1" + "value": "cell 1 6 2 1" } ) add( new FormContainer( "javax.swing.JTabbedPane", new FormLayoutManager( class javax.swing.JTabbedPane ) ) { name: "disabledTabbedPane2" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 5 2 1" + "value": "cell 1 6 2 1" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "label6" "text": "setIcon() and setDisabledIcon()" "enabled": false }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 5 2 1,gapx 20" + "value": "cell 1 6 2 1,gapx 20" } ) }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 0 ) - "size": new java.awt.Dimension( 595, 300 ) + "size": new java.awt.Dimension( 595, 470 ) } ) } }