All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.palexdev.mfxcomponents.controls.checkbox.MFXCheckbox Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2023 Parisi Alessandro - [email protected]
 * This file is part of MaterialFX (https://github.com/palexdev/MaterialFX)
 *
 * MaterialFX is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 3 of the License,
 * or (at your option) any later version.
 *
 * MaterialFX is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with MaterialFX. If not, see .
 */

package io.github.palexdev.mfxcomponents.controls.checkbox;

import io.github.palexdev.mfxcomponents.behaviors.MFXCheckboxBehavior;
import io.github.palexdev.mfxcomponents.controls.base.MFXSelectable;
import io.github.palexdev.mfxcomponents.controls.base.MFXSkinBase;
import io.github.palexdev.mfxcomponents.skins.MFXCheckboxSkin;
import io.github.palexdev.mfxcomponents.theming.enums.PseudoClasses;
import io.github.palexdev.mfxcore.base.properties.styleable.StyleableBooleanProperty;
import io.github.palexdev.mfxcore.base.properties.styleable.StyleableStringProperty;
import io.github.palexdev.mfxcore.selection.Selectable;
import io.github.palexdev.mfxcore.selection.SelectionGroup;
import io.github.palexdev.mfxcore.utils.fx.SceneBuilderIntegration;
import io.github.palexdev.mfxeffects.utils.StyleUtils;
import io.github.palexdev.mfxresources.fonts.IconDescriptor;
import io.github.palexdev.mfxresources.fonts.MFXFontIcon;
import io.github.palexdev.mfxresources.fonts.MFXIconWrapper;
import io.github.palexdev.mfxresources.fonts.fontawesome.FontAwesomeSolid;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.CssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleablePropertyFactory;

import java.util.List;
import java.util.function.Supplier;

/**
 * Custom implementation of a checkbox which extends {@link MFXSelectable}, has its own skin
 * {@link MFXCheckboxSkin}.
 * 

* {@code Checkboxes} are special type of 'selectables' because of their special {@code indeterminate} state. * Such state is not allowed by default, set {@link #allowIndeterminateProperty()} to change this. *

* The default behavior type for all {@link MFXCheckbox} components is {@link MFXCheckboxBehavior}. *

* The default style class of this component is: '.mfx-checkbox'. *

* About this component *

* Not so long ago, I was developing a little tool for myself to help me perform boring actions in a bunch of clicks, with * a beautiful UI, so of course I used all of my libraries. While developing the app, I was struggling with checkboxes * because for necessity I needed in some cases their state to be bound to something else. Turns out, JavaFX checkboxes * are not very easy to use in such occasions because they split the {@code state} in two different properties (selected, indeterminate). *

* I understand why it was done like this, it's a common pattern for tri-states, but if it can be avoided then it should * always be. Booleans are not meant to represent more than two states, boolean algebra consists of true and false values. *

* So, what's the other option? In one word: {@code Enumerators}. *

* {@code MFXCheckbox} uses the {@link TriState} enumerator to represent its states. Because of this, the checkbox's code * is much simpler, using listeners and bindings is also more straightforward. *

* Important Notes *

* {@code MFXCheckbox} is a particular type of selectable component. Since it extends {@link MFXSelectable} (thus implementing * {@link Selectable} too), all the API used for common selectable components is inherited, but: *

1) Using the checkbox in a {@link SelectionGroup} will disable the {@link #allowIndeterminateProperty()} and you * won't be able to turn it back on until the checkbox is removed from the group. Selection groups are made to work with * selectables which are either selected/unselected, indeterminate states make no sense in a group. *

2) The {@link #selectedProperty()} is bound to the {@link #stateProperty()}. First of all, for this reason * you won't be able to set it directly, {@link #setSelected(boolean)} is also overridden to change the {@link #stateProperty()} * instead. Second, don't try to unbind it, it's just not meant to work like that. *

3) Previously the icon was changed according to the state by the theme. Now, since the checkbox is animated, the * icon's description/identifier has been moved from the themes to code for one reason. Every time the state changes, a * new icon is created and switched from the old one, this is because for the animation this uses the new API introduced * by {@link MFXIconWrapper}. That being said, I didn't want to hard code it, so I added to properties to specify the icons * in CSS: {@link #selectedIconProperty()} and {@link #indeterminateIconProperty()}. Also, keep in mind that you can even * disable the animations via {@link #animatedProperty()} or change the animation in CSS, again see {@link MFXIconWrapper}. */ // TODO introduce validator public class MFXCheckbox extends MFXSelectable { //================================================================================ // Properties //================================================================================ private final ObjectProperty state = new SimpleObjectProperty<>(TriState.UNSELECTED) { @Override public void set(TriState newValue) { if (newValue == null) newValue = TriState.UNSELECTED; TriState state = handleNewState(newValue); super.set(state); } @Override protected void invalidated() { TriState state = get(); PseudoClasses.INDETERMINATE.setOn(MFXCheckbox.this, state == TriState.INDETERMINATE); // ":selected" pseudo class is handled by the onSelectionChanged(boolean) method fire(); } }; //================================================================================ // Constructors //================================================================================ public MFXCheckbox() { initialize(); } public MFXCheckbox(String text) { super(text); initialize(); } //================================================================================ // Methods //================================================================================ private void initialize() { selectionGroupProperty().addListener(i -> { if (getSelectionGroup() != null) setAllowIndeterminate(false); }); selectedProperty().bind(stateProperty().isEqualTo(TriState.SELECTED)); } /** * Invoked when a state change is requested. This is responsible for returning a valid state when the checkbox is in * a {@link SelectionGroup}. If it is not, returns the given parameter untouched. * * @param requested the new requested state for the checkbox */ protected TriState handleNewState(TriState requested) { SelectionGroup sg = getSelectionGroup(); if (sg == null) return requested; boolean check = sg.check(this, requested == TriState.SELECTED); return check ? TriState.SELECTED : TriState.UNSELECTED; } //================================================================================ // Overridden Methods //================================================================================ @Override protected MFXSkinBase buildSkin() { return new MFXCheckboxSkin(this); } @Override public List defaultStyleClasses() { return List.of("mfx-checkbox"); } @Override public Supplier defaultBehaviorProvider() { return () -> new MFXCheckboxBehavior(this); } @Override protected void sceneBuilderIntegration() { super.sceneBuilderIntegration(); SceneBuilderIntegration.ifInSceneBuilder(() -> setText("Checkbox")); } //================================================================================ // Styleable Properties //================================================================================ private final StyleableBooleanProperty animated = new StyleableBooleanProperty( StyleableProperties.ANIMATED, this, "animated", true ); private final StyleableBooleanProperty allowIndeterminate = new StyleableBooleanProperty( StyleableProperties.ALLOW_INDETERMINATE, this, "allowIndeterminate", false ) { @Override public void set(boolean newValue) { SelectionGroup sg = getSelectionGroup(); super.set(sg == null && newValue); } @Override protected void invalidated() { boolean state = get(); if (!state && isIndeterminate()) setState(TriState.UNSELECTED); } }; private final StyleableStringProperty selectedIcon = new StyleableStringProperty( StyleableProperties.SELECTED_ICON, this, "selectedIcon", "fas-check" ); private final StyleableStringProperty indeterminateIcon = new StyleableStringProperty( StyleableProperties.INDETERMINATE_ICON, this, "indeterminateBean", "fas-minus" ); public boolean isAnimated() { return animated.get(); } /** * Specifies whether to play animations when the checkbox' state changes. */ public StyleableBooleanProperty animatedProperty() { return animated; } public void setAnimated(boolean animated) { this.animated.set(animated); } public boolean isAllowIndeterminate() { return allowIndeterminate.get(); } /** * Specifies whether the checkbox can also transition to the {@code indeterminate} state. *

* When turned off, the {@code indeterminate} state is also set to false. *

* Can be set in CSS via the property: '-mfx-allow-indeterminate'. */ public StyleableBooleanProperty allowIndeterminateProperty() { return allowIndeterminate; } public void setAllowIndeterminate(boolean allowIndeterminate) { this.allowIndeterminate.set(allowIndeterminate); } public String getSelectedIcon() { return selectedIcon.get(); } /** * Specifies the {@link IconDescriptor} as a String, used to build a new {@link MFXFontIcon} when the checkbox is * selected. *

* As of now, only {@link FontAwesomeSolid} are supported. */ public StyleableStringProperty selectedIconProperty() { return selectedIcon; } public void setSelectedIcon(String selectedIcon) { this.selectedIcon.set(selectedIcon); } public String getIndeterminateIcon() { return indeterminateIcon.get(); } /** * Specifies the {@link IconDescriptor} as a String, used to build a new {@link MFXFontIcon} when the checkbox is * indeterminate. *

* As of now, only {@link FontAwesomeSolid} are supported. */ public StyleableStringProperty indeterminateIconProperty() { return indeterminateIcon; } public void setIndeterminateIcon(String indeterminateIcon) { this.indeterminateIcon.set(indeterminateIcon); } //================================================================================ // CssMetaData //================================================================================ private static class StyleableProperties { private static final StyleablePropertyFactory FACTORY = new StyleablePropertyFactory<>(MFXSelectable.getClassCssMetaData()); private static final List> cssMetaDataList; private static final CssMetaData ANIMATED = FACTORY.createBooleanCssMetaData( "-mfx-animated", MFXCheckbox::animatedProperty, true ); private static final CssMetaData ALLOW_INDETERMINATE = FACTORY.createBooleanCssMetaData( "-mfx-allow-indeterminate", MFXCheckbox::allowIndeterminateProperty, false ); private static final CssMetaData SELECTED_ICON = FACTORY.createStringCssMetaData( "-mfx-selected-icon", MFXCheckbox::selectedIconProperty, "fas-check" ); private static final CssMetaData INDETERMINATE_ICON = FACTORY.createStringCssMetaData( "-mfx-indeterminate-icon", MFXCheckbox::indeterminateIconProperty, "fas-minus" ); static { cssMetaDataList = StyleUtils.cssMetaDataList( MFXSelectable.getClassCssMetaData(), ANIMATED, ALLOW_INDETERMINATE, SELECTED_ICON, INDETERMINATE_ICON ); } } public static List> getClassCssMetaData() { return StyleableProperties.cssMetaDataList; } @Override public List> getControlCssMetaData() { return getClassCssMetaData(); } //================================================================================ // Getters/Setters //================================================================================ /** * @return whether the {@link #stateProperty()} is {@link TriState#INDETERMINATE} */ public boolean isIndeterminate() { return getState() == TriState.INDETERMINATE; } public TriState getState() { return state.get(); } /** * Specifies the selection state of the checkbox as an enum constant. * * @see TriState */ public ObjectProperty stateProperty() { return state; } public void setState(TriState state) { this.state.set(state); } /** * Overridden since checkboxes are a particular type of {@link Selectable}. *

* First the given boolean value is converted to a {@link TriState} by using {@link TriState#from(Boolean)}, then * the resulting value is fed to {@link #handleNewState(TriState)} to ensure the new state is valid if the checkbox * is in a {@link SelectionGroup}. Finally, the result of the previous operations is given to {@link #setState(TriState)}. */ @Override public void setSelected(boolean selected) { setState(handleNewState(TriState.from(selected))); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy