Add label position support to toggle switch

This commit is contained in:
mkpaz 2023-02-23 16:24:40 +04:00
parent dca4d7c21e
commit c40ec4e3b3
6 changed files with 227 additions and 73 deletions

@ -12,13 +12,17 @@
### Improvements ### Improvements
- (Base) Improved Javadoc. See full API reference in [docs](https://mkpaz.github.io/atlantafx/apidocs/atlantafx.base/module-summary.html).
- (Base) `ToggleSwitch` label position support (left or right).
- (CSS) `Button` shadow support (`-color-button-shadow`). Only for themes compiled with the `button.$use-shadow` flag enabled. - (CSS) `Button` shadow support (`-color-button-shadow`). Only for themes compiled with the `button.$use-shadow` flag enabled.
- (CSS) Looked-up color variables support: `Separator`. - (CSS) Looked-up color variables support: `Separator`.
- (CSS) Added border radius/shadow to popup menu for `ComboBox` (and all `ComboBox`-based) controls.
### Bugfixes ### Bugfixes
- (CSS) Added border radius/shadow to popup menu for `ComboBox` (and `ComboBox`-based) controls. - (Base) Fixed incorrect `Slider` progress track length calculation.
- (CSS) Fixed `Popover` arrow background color. - (CSS) Fixed `Popover` arrow background color.
- (CSS) Fixed `ListView` with `.bordered` class displays borders on empty cells.
## [1.2.0] - 2023-02-11 ## [1.2.0] - 2023-02-11

@ -249,8 +249,9 @@ public class RingProgressIndicatorSkin extends SkinBase<RingProgressIndicator> {
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
static { static {
final List<CssMetaData<? extends Styleable, ?>> styleables = final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(
new ArrayList<>(SkinBase.getClassCssMetaData()); SkinBase.getClassCssMetaData()
);
styleables.add(INDETERMINATE_ANIMATION_TIME); styleables.add(INDETERMINATE_ANIMATION_TIME);
STYLEABLES = Collections.unmodifiableList(styleables); STYLEABLES = Collections.unmodifiableList(styleables);
} }

@ -29,10 +29,21 @@
package atlantafx.base.controls; package atlantafx.base.controls;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.BooleanPropertyBase; import javafx.beans.property.BooleanPropertyBase;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.WritableValue;
import javafx.css.CssMetaData;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
import javafx.css.Styleable;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.css.converter.EnumConverter;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.geometry.HorizontalDirection;
import javafx.scene.control.Labeled; import javafx.scene.control.Labeled;
import javafx.scene.control.Skin; import javafx.scene.control.Skin;
@ -41,6 +52,7 @@ public class ToggleSwitch extends Labeled {
protected static final String DEFAULT_STYLE_CLASS = "toggle-switch"; protected static final String DEFAULT_STYLE_CLASS = "toggle-switch";
protected static final PseudoClass PSEUDO_CLASS_SELECTED = PseudoClass.getPseudoClass("selected"); protected static final PseudoClass PSEUDO_CLASS_SELECTED = PseudoClass.getPseudoClass("selected");
protected static final PseudoClass PSEUDO_CLASS_RIGHT = PseudoClass.getPseudoClass("right");
/** /**
* Creates a toggle switch with empty string for its label. * Creates a toggle switch with empty string for its label.
@ -67,36 +79,22 @@ public class ToggleSwitch extends Labeled {
// Properties // // Properties //
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
/*
* Indicates whether this switch is selected.
*/
private BooleanProperty selected; private BooleanProperty selected;
/**
* Sets the selected value.
*/
public final void setSelected(boolean value) { public final void setSelected(boolean value) {
selectedProperty().set(value); selectedProperty().set(value);
} }
/**
* Returns whether this Toggle Switch is selected.
*/
public final boolean isSelected() { public final boolean isSelected() {
return selected != null && selected.get(); return selected != null && selected.get();
} }
/** /**
* Returns the selected property. * Returns whether this Toggle Switch is selected.
*/ */
public final BooleanProperty selectedProperty() { public final BooleanProperty selectedProperty() {
if (selected == null) { if (selected == null) {
selected = new BooleanPropertyBase() { selected = new BooleanPropertyBase() {
@Override
protected void invalidated() {
final boolean v = get();
pseudoClassStateChanged(PSEUDO_CLASS_SELECTED, v);
}
@Override @Override
public Object getBean() { public Object getBean() {
@ -107,12 +105,67 @@ public class ToggleSwitch extends Labeled {
public String getName() { public String getName() {
return "selected"; return "selected";
} }
@Override
protected void invalidated() {
final boolean v = get();
pseudoClassStateChanged(PSEUDO_CLASS_SELECTED, v);
}
}; };
} }
return selected; return selected;
} }
// ~
private ObjectProperty<HorizontalDirection> labelPosition;
public final void setLabelPosition(HorizontalDirection pos) {
labelPositionProperty().setValue(pos);
}
/**
* Returns whether this Toggle Switch is selected.
*/
public final HorizontalDirection getLabelPosition() {
return labelPosition == null ? HorizontalDirection.LEFT : labelPosition.getValue();
}
/**
* Specifies the side where {@link #textProperty()} values should be placed.
* Default is {@link HorizontalDirection#LEFT}.
*/
public final ObjectProperty<HorizontalDirection> labelPositionProperty() {
if (labelPosition == null) {
labelPosition = new StyleableObjectProperty<>(HorizontalDirection.LEFT) {
@Override
public Object getBean() {
return ToggleSwitch.this;
}
@Override
public String getName() {
return "labelPosition";
}
@Override
protected void invalidated() {
final HorizontalDirection v = get();
pseudoClassStateChanged(ToggleSwitch.PSEUDO_CLASS_RIGHT, v == HorizontalDirection.RIGHT);
}
@Override
public CssMetaData<ToggleSwitch, HorizontalDirection> getCssMetaData() {
return StyleableProperties.LABEL_POSITION;
}
};
}
return labelPosition;
}
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Methods // // Methods //
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@ -135,4 +188,43 @@ public class ToggleSwitch extends Labeled {
protected Skin<?> createDefaultSkin() { protected Skin<?> createDefaultSkin() {
return new ToggleSwitchSkin(this); return new ToggleSwitchSkin(this);
} }
/**
* {@inheritDoc}
*/
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return StyleableProperties.STYLEABLES;
}
///////////////////////////////////////////////////////////////////////////
// Styleable Properties //
///////////////////////////////////////////////////////////////////////////
private static class StyleableProperties {
private static final CssMetaData<ToggleSwitch, HorizontalDirection> LABEL_POSITION = new CssMetaData<>(
"-fx-label-position", new EnumConverter<>(HorizontalDirection.class), HorizontalDirection.LEFT
) {
@Override
public boolean isSettable(ToggleSwitch c) {
return c.labelPositionProperty() == null || !c.labelPositionProperty().isBound();
}
@Override
public StyleableProperty<HorizontalDirection> getStyleableProperty(ToggleSwitch c) {
var val = (WritableValue<HorizontalDirection>) c.labelPositionProperty();
return (StyleableProperty<HorizontalDirection>) val;
}
};
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Labeled.getClassCssMetaData());
styleables.add(LABEL_POSITION);
STYLEABLES = Collections.unmodifiableList(styleables);
}
}
} }

@ -41,6 +41,7 @@ import javafx.css.Styleable;
import javafx.css.StyleableDoubleProperty; import javafx.css.StyleableDoubleProperty;
import javafx.css.StyleableProperty; import javafx.css.StyleableProperty;
import javafx.css.converter.SizeConverter; import javafx.css.converter.SizeConverter;
import javafx.geometry.HorizontalDirection;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.SkinBase; import javafx.scene.control.SkinBase;
@ -49,6 +50,8 @@ import javafx.util.Duration;
public class ToggleSwitchSkin extends SkinBase<ToggleSwitch> { public class ToggleSwitchSkin extends SkinBase<ToggleSwitch> {
protected static final Duration DEFAULT_ANIMATION_TIME = Duration.millis(200);
protected final StackPane thumb; protected final StackPane thumb;
protected final StackPane thumbArea; protected final StackPane thumbArea;
protected final Label label; protected final Label label;
@ -59,20 +62,24 @@ public class ToggleSwitchSkin extends SkinBase<ToggleSwitch> {
super(control); super(control);
thumb = new StackPane(); thumb = new StackPane();
thumb.getStyleClass().setAll("thumb");
thumbArea = new StackPane(); thumbArea = new StackPane();
thumbArea.getStyleClass().setAll("thumb-area");
label = new Label(); label = new Label();
labelContainer = new StackPane(); labelContainer = new StackPane();
labelContainer.getStyleClass().add("label-container"); labelContainer.getStyleClass().add("label-container");
transition = new TranslateTransition(Duration.millis(getThumbMoveAnimationTime()), thumb);
transition = new TranslateTransition(DEFAULT_ANIMATION_TIME, thumb);
transition.setFromX(0.0); transition.setFromX(0.0);
label.textProperty().bind(control.textProperty()); label.textProperty().bind(control.textProperty());
getChildren().addAll(labelContainer, thumbArea, thumb);
labelContainer.getChildren().addAll(label);
StackPane.setAlignment(label, Pos.CENTER_LEFT); StackPane.setAlignment(label, Pos.CENTER_LEFT);
thumb.getStyleClass().setAll("thumb"); labelContainer.getChildren().addAll(label);
thumbArea.getStyleClass().setAll("thumb-area"); getChildren().addAll(labelContainer, thumbArea, thumb);
thumbArea.setOnMouseReleased(event -> mousePressedOnToggleSwitch(control)); thumbArea.setOnMouseReleased(event -> mousePressedOnToggleSwitch(control));
thumb.setOnMouseReleased(event -> mousePressedOnToggleSwitch(control)); thumb.setOnMouseReleased(event -> mousePressedOnToggleSwitch(control));
@ -109,7 +116,7 @@ public class ToggleSwitchSkin extends SkinBase<ToggleSwitch> {
private DoubleProperty thumbMoveAnimationTimeProperty() { private DoubleProperty thumbMoveAnimationTimeProperty() {
if (thumbMoveAnimationTime == null) { if (thumbMoveAnimationTime == null) {
thumbMoveAnimationTime = new StyleableDoubleProperty(200) { thumbMoveAnimationTime = new StyleableDoubleProperty(DEFAULT_ANIMATION_TIME.toMillis()) {
@Override @Override
public Object getBean() { public Object getBean() {
@ -121,22 +128,28 @@ public class ToggleSwitchSkin extends SkinBase<ToggleSwitch> {
return "thumbMoveAnimationTime"; return "thumbMoveAnimationTime";
} }
@Override
protected void invalidated() {
// update duration value
transition.setDuration(Duration.millis(getValue()));
}
@Override @Override
public CssMetaData<ToggleSwitch, Number> getCssMetaData() { public CssMetaData<ToggleSwitch, Number> getCssMetaData() {
return THUMB_MOVE_ANIMATION_TIME; return StyleableProperties.THUMB_MOVE_ANIMATION_TIME;
} }
}; };
} }
return thumbMoveAnimationTime; return thumbMoveAnimationTime;
} }
protected double getThumbMoveAnimationTime() { /**
return thumbMoveAnimationTime == null ? 200 : thumbMoveAnimationTime.get(); * {@inheritDoc}
} */
@Override @Override
protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) { protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
ToggleSwitch toggleSwitch = getSkinnable(); ToggleSwitch c = getSkinnable();
double thumbWidth = snapSizeX(thumb.prefWidth(-1)); double thumbWidth = snapSizeX(thumb.prefWidth(-1));
double thumbHeight = snapSizeX(thumb.prefHeight(-1)); double thumbHeight = snapSizeX(thumb.prefHeight(-1));
thumb.resize(thumbWidth, thumbHeight); thumb.resize(thumbWidth, thumbHeight);
@ -144,14 +157,18 @@ public class ToggleSwitchSkin extends SkinBase<ToggleSwitch> {
double thumbAreaWidth = snapSizeX(thumbArea.prefWidth(-1)); double thumbAreaWidth = snapSizeX(thumbArea.prefWidth(-1));
double thumbAreaHeight = snapSizeX(thumbArea.prefHeight(-1)); double thumbAreaHeight = snapSizeX(thumbArea.prefHeight(-1));
double thumbAreaY = snapPositionX(contentY + (contentHeight / 2) - (thumbAreaHeight / 2)); double thumbAreaY = snapPositionX(contentY + (contentHeight / 2) - (thumbAreaHeight / 2));
double labelContainerWidth = label.getText() != null && !label.getText().isEmpty()
? contentWidth - thumbAreaWidth : 0; double labelWidth = label.getText() != null && !label.getText().isEmpty() ? contentWidth - thumbAreaWidth : 0;
double labelX = c.getLabelPosition() == HorizontalDirection.RIGHT ? thumbAreaWidth : 0;
double thumbAreaX = c.getLabelPosition() == HorizontalDirection.RIGHT ? 0 : labelWidth;
thumbArea.resize(thumbAreaWidth, thumbAreaHeight); thumbArea.resize(thumbAreaWidth, thumbAreaHeight);
thumbArea.setLayoutX(labelContainerWidth); thumbArea.setLayoutX(thumbAreaX);
thumbArea.setLayoutY(thumbAreaY); thumbArea.setLayoutY(thumbAreaY);
labelContainer.resize(labelContainerWidth, thumbAreaHeight); labelContainer.resize(labelWidth, thumbAreaHeight);
labelContainer.setLayoutX(labelX);
labelContainer.setLayoutY(thumbAreaY); labelContainer.setLayoutY(thumbAreaY);
// layout the thumb on the "unselected" position // layout the thumb on the "unselected" position
@ -169,84 +186,104 @@ public class ToggleSwitchSkin extends SkinBase<ToggleSwitch> {
transition.playFrom(currentTime); transition.playFrom(currentTime);
} else { } else {
// if the transition is not running, simply apply the translateX value // if the transition is not running, simply apply the translateX value
thumb.setTranslateX(toggleSwitch.isSelected() ? thumbTarget : 0.0); thumb.setTranslateX(c.isSelected() ? thumbTarget : 0.0);
} }
} }
/**
* {@inheritDoc}
*/
@Override @Override
protected double computeMinWidth(double height, double topInset, double rightInset, protected double computeMinWidth(double height, double topInset, double rightInset,
double bottomInset, double leftInset) { double bottomInset, double leftInset) {
return leftInset + label.prefWidth(-1) + thumbArea.prefWidth(-1) + rightInset; return leftInset + label.prefWidth(-1) + thumbArea.prefWidth(-1) + rightInset;
} }
/**
* {@inheritDoc}
*/
@Override @Override
protected double computeMinHeight(double width, double topInset, double rightInset, protected double computeMinHeight(double width, double topInset, double rightInset,
double bottomInset, double leftInset) { double bottomInset, double leftInset) {
return topInset + Math.max(thumb.prefHeight(-1), label.prefHeight(-1)) + bottomInset; return topInset + Math.max(thumb.prefHeight(-1), label.prefHeight(-1)) + bottomInset;
} }
/**
* {@inheritDoc}
*/
@Override @Override
protected double computePrefWidth(double height, double topInset, double rightInset, protected double computePrefWidth(double height, double topInset, double rightInset,
double bottomInset, double leftInset) { double bottomInset, double leftInset) {
return leftInset + label.prefWidth(-1) + 1 + thumbArea.prefWidth(-1) + rightInset; return leftInset + label.prefWidth(-1) + 1 + thumbArea.prefWidth(-1) + rightInset;
} }
/**
* {@inheritDoc}
*/
@Override @Override
protected double computePrefHeight(double width, double topInset, double rightInset, protected double computePrefHeight(double width, double topInset, double rightInset,
double bottomInset, double leftInset) { double bottomInset, double leftInset) {
return computeMinHeight(width, topInset, rightInset, bottomInset, leftInset); return computeMinHeight(width, topInset, rightInset, bottomInset, leftInset);
} }
/**
* {@inheritDoc}
*/
@Override @Override
protected double computeMaxWidth(double height, double topInset, double rightInset, protected double computeMaxWidth(double height, double topInset, double rightInset,
double bottomInset, double leftInset) { double bottomInset, double leftInset) {
return getSkinnable().prefWidth(height); return getSkinnable().prefWidth(height);
} }
/**
* {@inheritDoc}
*/
@Override @Override
protected double computeMaxHeight(double width, double topInset, double rightInset, protected double computeMaxHeight(double width, double topInset, double rightInset,
double bottomInset, double leftInset) { double bottomInset, double leftInset) {
return getSkinnable().prefHeight(width); return getSkinnable().prefHeight(width);
} }
private static final CssMetaData<ToggleSwitch, Number> THUMB_MOVE_ANIMATION_TIME =
new CssMetaData<>("-fx-thumb-move-animation-time", SizeConverter.getInstance(), 200) {
@Override
public boolean isSettable(ToggleSwitch toggleSwitch) {
final ToggleSwitchSkin skin = (ToggleSwitchSkin) toggleSwitch.getSkin();
return skin.thumbMoveAnimationTime == null || skin.thumbMoveAnimationTime.isBound();
}
@Override
@SuppressWarnings("RedundantCast")
public StyleableProperty<Number> getStyleableProperty(ToggleSwitch toggleSwitch) {
final ToggleSwitchSkin skin = (ToggleSwitchSkin) toggleSwitch.getSkin();
return (StyleableProperty<Number>) (WritableValue<Number>) skin.thumbMoveAnimationTimeProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(SkinBase.getClassCssMetaData());
styleables.add(THUMB_MOVE_ANIMATION_TIME);
STYLEABLES = Collections.unmodifiableList(styleables);
}
/**
* Returns the CssMetaData associated with this class, which may include the
* CssMetaData of its super classes.
*/
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return STYLEABLES;
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
@Override @Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return getClassCssMetaData(); return ToggleSwitchSkin.StyleableProperties.STYLEABLES;
}
///////////////////////////////////////////////////////////////////////////
// Styleable Properties //
///////////////////////////////////////////////////////////////////////////
static class StyleableProperties {
private static final CssMetaData<ToggleSwitch, Number> THUMB_MOVE_ANIMATION_TIME = new CssMetaData<>(
"-fx-thumb-move-animation-time", SizeConverter.getInstance(), DEFAULT_ANIMATION_TIME.toMillis()
) {
@Override
public boolean isSettable(ToggleSwitch toggleSwitch) {
final var skin = (ToggleSwitchSkin) toggleSwitch.getSkin();
return skin.thumbMoveAnimationTime == null || !skin.thumbMoveAnimationTime.isBound();
}
@Override
@SuppressWarnings("RedundantCast")
public StyleableProperty<Number> getStyleableProperty(ToggleSwitch toggleSwitch) {
final var skin = (ToggleSwitchSkin) toggleSwitch.getSkin();
return (StyleableProperty<Number>) (WritableValue<Number>) skin.thumbMoveAnimationTimeProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(
SkinBase.getClassCssMetaData()
);
styleables.add(THUMB_MOVE_ANIMATION_TIME);
STYLEABLES = Collections.unmodifiableList(styleables);
}
} }
} }

@ -6,7 +6,9 @@ import atlantafx.base.controls.ToggleSwitch;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.Page; import atlantafx.sampler.page.Page;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.SampleBlock;
import javafx.geometry.HorizontalDirection;
import javafx.scene.layout.FlowPane; import javafx.scene.layout.FlowPane;
import javafx.scene.layout.VBox;
public class ToggleSwitchPage extends AbstractPage { public class ToggleSwitchPage extends AbstractPage {
@ -26,9 +28,19 @@ public class ToggleSwitchPage extends AbstractPage {
} }
private SampleBlock basicSample() { private SampleBlock basicSample() {
var toggle = new ToggleSwitch(); var leftToggle = new ToggleSwitch("Enable");
toggle.selectedProperty().addListener((obs, old, val) -> toggle.setText(val ? "Disable" : "Enable")); leftToggle.selectedProperty().addListener(
toggle.setSelected(true); (obs, old, val) -> leftToggle.setText(val ? "Enabled" : "Disabled")
return new SampleBlock("Basic", toggle); );
leftToggle.setSelected(true);
var rightToggle = new ToggleSwitch("Disable");
rightToggle.selectedProperty().addListener(
(obs, old, val) -> rightToggle.setText(val ? "Enabled" : "Disabled")
);
rightToggle.setLabelPosition(HorizontalDirection.RIGHT);
rightToggle.setSelected(false);
return new SampleBlock("Basic", new VBox(SampleBlock.BLOCK_VGAP, leftToggle, rightToggle));
} }
} }

@ -62,6 +62,14 @@ $thumb-area-padding: 0.85em 1.4em 0.85em 1.4em !default;
} }
} }
&:right {
>.label-container {
>.label {
-fx-padding: cfg.$checkbox-label-padding 0 cfg.$checkbox-label-padding cfg.$graphic-gap;
}
}
}
&:disabled { &:disabled {
-fx-opacity: cfg.$opacity-disabled; -fx-opacity: cfg.$opacity-disabled;
} }