From 32d5cee186d2f24e8e6ae63939225cd446b9c909 Mon Sep 17 00:00:00 2001 From: palexdev Date: Sun, 24 Sep 2023 22:48:28 +0200 Subject: [PATCH] :boom: Huge Update Part 3/3 [components, resources] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Components Module] - Behaviors Package :recycle: MFXCheckboxBehavior: replace ifs with EnumUtils.next(...) utility. Also now the states order has been changed as shown by M3 guidelines: UNSELECTED -> SELECTED -> INDETERMINATE - Controls Package :sparkles: MFXIconButton: added support for animated icon switching :sparkles: MFXCheckbox: added support for animated state change as shown by M3 guidelines. Now the icon of SELECTED and INDETERMINATE state needs to be changed through two new properties since the skin has been modified as well for this - Skins Package :sparkles: MFXCheckboxSkin: implemented animations as shown by M3 guidelines. Now the icon is wrapped in a MFXIconWrapper :sparkles: MFXIconButtonSkin: the icon is now contained by a MFXIconWrapper which is also used to play the icon switch animation :adhesive_bandage: MFXCheckboxSkin and MFXIconButtonSkin: re-enabled ripple generation, oversight of recent update to Skin and Behavior APIs [Resources Module] - Fonts Package :adhesive_bandage: MFXIconWrapper: for how it handles icons add/remove it may happen to cause an IllegalStateException if the change is not done on the application thread, this was not necessary before. Simply ensure we are on FX thread otherwise runLater - Assets/Themes :lipstick: _checkbox.scss: M3 guidelines show a CLIP animation when switching states. Also remove '-mfx-description' properties for the changes mentioned above [Misc] :memo: Added some TODOs as remainders Signed-off-by: palexdev --- .idea/inspectionProfiles/Project_Default.xml | 6 + TODO.md | 7 +- .../behaviors/MFXCheckboxBehavior.java | 18 +-- .../controls/buttons/MFXIconButton.java | 34 +++++- .../controls/checkbox/MFXCheckbox.java | 106 +++++++++++++++++- .../mfxcomponents/skins/MFXCheckboxSkin.java | 79 ++++++++++++- .../skins/MFXIconButtonSkin.java | 76 +++++-------- .../components/src/test/java/app/Sandbox.java | 31 ++--- .../src/test/java/app/Showcase.java | 72 ++++++------ .../test/java/interactive/TestCheckboxes.java | 12 +- .../interactive/TestLayoutStrategies.java | 4 +- .../mfxresources/fonts/MFXIconWrapper.java | 11 +- .../components/checkbox/_checkbox.scss | 6 +- .../themes/material/md-indigo-dark.css | 6 +- .../themes/material/md-indigo-light.css | 6 +- .../themes/material/md-purple-dark.css | 6 +- .../themes/material/md-purple-light.css | 6 +- .../src/test/java/app/IconAnimation.java | 5 + .../src/test/java/interactive/IconsTests.java | 31 +++-- 19 files changed, 359 insertions(+), 163 deletions(-) diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 8bbe75e0..4598de81 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -877,6 +877,7 @@ + @@ -908,6 +909,7 @@ + @@ -1494,6 +1496,7 @@ + + diff --git a/TODO.md b/TODO.md index eda5f2c2..53f1eb1f 100644 --- a/TODO.md +++ b/TODO.md @@ -55,4 +55,9 @@ - [ ] TimePicker (and combined with date) - [ ] Check Combo-boxes - [ ] Split Button -- [ ] Progress Button \ No newline at end of file +- [ ] Progress Button + +#### Misc/Remainders + +- StyleableProperties should be split. The ones dependent on the skin from the ones dependent on the control +- Resolve icon provider from a string description (through prefix) \ No newline at end of file diff --git a/modules/components/src/main/java/io/github/palexdev/mfxcomponents/behaviors/MFXCheckboxBehavior.java b/modules/components/src/main/java/io/github/palexdev/mfxcomponents/behaviors/MFXCheckboxBehavior.java index dc417ed5..81e50ec1 100644 --- a/modules/components/src/main/java/io/github/palexdev/mfxcomponents/behaviors/MFXCheckboxBehavior.java +++ b/modules/components/src/main/java/io/github/palexdev/mfxcomponents/behaviors/MFXCheckboxBehavior.java @@ -21,6 +21,7 @@ import io.github.palexdev.mfxcomponents.controls.checkbox.MFXCheckbox; import io.github.palexdev.mfxcomponents.controls.checkbox.TriState; import io.github.palexdev.mfxcore.selection.SelectionProperty; +import io.github.palexdev.mfxcore.utils.EnumUtils; import javafx.event.ActionEvent; /** @@ -53,7 +54,7 @@ public MFXCheckboxBehavior(MFXCheckbox button) { *

2) The checkbox is {@code indeterminate}, sets the state to {@code selected} *

3) The checkbox is not selected, sets the state to {@code indeterminate} *

- * In short, the cycle is: UNSELECTED -> INDETERMINATE (if allowed) -> SELECTED + * In short, the cycle is: UNSELECTED -> SELECTED -> INDETERMINATE (if allowed) *

* Note: this method will not invoke {@link MFXCheckbox#fire()}, as it is handled by the checkbox' {@link SelectionProperty}, * this is done to make {@link ActionEvent}s work also when the property is bound. I've not yet decided if this will @@ -65,17 +66,10 @@ protected void handleSelection() { if (checkBox.stateProperty().isBound()) return; TriState oldState = checkBox.getState(); - if (checkBox.isAllowIndeterminate()) { - if (oldState == TriState.INDETERMINATE) { - checkBox.setState(TriState.SELECTED); - return; - } - if (oldState == TriState.UNSELECTED) { - checkBox.setState(TriState.INDETERMINATE); - return; - } - } - checkBox.setState(oldState == TriState.UNSELECTED ? TriState.SELECTED : TriState.UNSELECTED); + TriState newState = EnumUtils.next(TriState.class, oldState); + if (newState == TriState.INDETERMINATE && !checkBox.isAllowIndeterminate()) + newState = EnumUtils.next(TriState.class, newState); + checkBox.setState(newState); // fire() is handled by the state property, to make bindings work too } } diff --git a/modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/buttons/MFXIconButton.java b/modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/buttons/MFXIconButton.java index 983e288d..b8c6c9d8 100644 --- a/modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/buttons/MFXIconButton.java +++ b/modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/buttons/MFXIconButton.java @@ -13,6 +13,7 @@ import io.github.palexdev.mfxresources.base.properties.IconProperty; import io.github.palexdev.mfxresources.fonts.IconProvider; import io.github.palexdev.mfxresources.fonts.MFXFontIcon; +import io.github.palexdev.mfxresources.fonts.MFXIconWrapper; import javafx.css.CssMetaData; import javafx.css.Styleable; import javafx.css.StyleablePropertyFactory; @@ -136,6 +137,13 @@ public MFXIconButton removeVariants(IconButtonVariants... variants) { //================================================================================ // Styleable Properties //================================================================================ + private final StyleableBooleanProperty animated = new StyleableBooleanProperty( + StyleableProperties.ANIMATED, + this, + "animated", + true + ); + private final StyleableBooleanProperty selectable = new StyleableBooleanProperty( StyleableProperties.SELECTABLE, this, @@ -155,6 +163,23 @@ protected void invalidated() { 40.0 ); + public boolean isAnimated() { + return animated.get(); + } + + /** + * Specifies whether to play an animation when switching icons. + *

+ * In the default skin the animation is built and played by the {@link MFXIconWrapper} that contains the icons. + */ + public StyleableBooleanProperty animatedProperty() { + return animated; + } + + public void setAnimated(boolean animated) { + this.animated.set(animated); + } + public boolean isSelectable() { return selectable.get(); } @@ -198,6 +223,13 @@ 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", + MFXIconButton::animatedProperty, + true + ); + private static final CssMetaData SELECTABLE = FACTORY.createBooleanCssMetaData( "-mfx-selectable", @@ -215,7 +247,7 @@ private static class StyleableProperties { static { cssMetaDataList = StyleUtils.cssMetaDataList( MFXSelectable.getClassCssMetaData(), - SELECTABLE, SIZE + ANIMATED, SELECTABLE, SIZE ); } } diff --git a/modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/checkbox/MFXCheckbox.java b/modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/checkbox/MFXCheckbox.java index ba0a31fe..45cccca5 100644 --- a/modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/checkbox/MFXCheckbox.java +++ b/modules/components/src/main/java/io/github/palexdev/mfxcomponents/controls/checkbox/MFXCheckbox.java @@ -24,10 +24,15 @@ 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; @@ -73,6 +78,12 @@ *

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 { @@ -158,6 +169,13 @@ protected void sceneBuilderIntegration() { //================================================================================ // Styleable Properties //================================================================================ + private final StyleableBooleanProperty animated = new StyleableBooleanProperty( + StyleableProperties.ANIMATED, + this, + "animated", + true + ); + private final StyleableBooleanProperty allowIndeterminate = new StyleableBooleanProperty( StyleableProperties.ALLOW_INDETERMINATE, this, @@ -177,6 +195,35 @@ protected void invalidated() { } }; + 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(); } @@ -196,6 +243,42 @@ 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 //================================================================================ @@ -203,6 +286,13 @@ 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", @@ -210,10 +300,24 @@ private static class StyleableProperties { 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(), - ALLOW_INDETERMINATE + ANIMATED, ALLOW_INDETERMINATE, SELECTED_ICON, INDETERMINATE_ICON ); } } diff --git a/modules/components/src/main/java/io/github/palexdev/mfxcomponents/skins/MFXCheckboxSkin.java b/modules/components/src/main/java/io/github/palexdev/mfxcomponents/skins/MFXCheckboxSkin.java index c5e4dbc8..6b291d6e 100644 --- a/modules/components/src/main/java/io/github/palexdev/mfxcomponents/skins/MFXCheckboxSkin.java +++ b/modules/components/src/main/java/io/github/palexdev/mfxcomponents/skins/MFXCheckboxSkin.java @@ -21,12 +21,20 @@ import io.github.palexdev.mfxcomponents.behaviors.MFXCheckboxBehavior; import io.github.palexdev.mfxcomponents.controls.MaterialSurface; import io.github.palexdev.mfxcomponents.controls.checkbox.MFXCheckbox; +import io.github.palexdev.mfxcomponents.controls.checkbox.TriState; import io.github.palexdev.mfxcomponents.skins.base.MFXLabeledSkin; import io.github.palexdev.mfxcore.utils.fx.LayoutUtils; +import io.github.palexdev.mfxeffects.animations.Animations.KeyFrames; +import io.github.palexdev.mfxeffects.animations.Animations.SequentialBuilder; +import io.github.palexdev.mfxeffects.animations.Animations.TimelineBuilder; +import io.github.palexdev.mfxeffects.animations.motion.M3Motion; import io.github.palexdev.mfxeffects.beans.Position; import io.github.palexdev.mfxeffects.ripple.MFXRippleGenerator; import io.github.palexdev.mfxresources.fonts.MFXFontIcon; import io.github.palexdev.mfxresources.fonts.MFXIconWrapper; +import javafx.animation.Animation; +import javafx.animation.Interpolator; +import javafx.animation.Timeline; import javafx.geometry.Bounds; import javafx.geometry.HPos; import javafx.geometry.VPos; @@ -34,9 +42,11 @@ import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; import javafx.scene.paint.Color; +import javafx.util.Duration; import static io.github.palexdev.mfxcore.events.WhenEvent.intercept; import static io.github.palexdev.mfxcore.observables.When.onChanged; +import static io.github.palexdev.mfxcore.observables.When.onInvalidated; /** * Default skin implementation for {@link MFXCheckbox} components, extends {@link MFXLabeledSkin}. @@ -57,6 +67,7 @@ public class MFXCheckboxSkin extends MFXLabeledSkin - A listener on the {@link MFXCheckbox#contentDisplayProperty()} to add/remove the label node - * when the values is/is not {@link ContentDisplay#GRAPHIC_ONLY}. + * when the value is/is not {@link ContentDisplay#GRAPHIC_ONLY}. */ private void addListeners() { MFXCheckbox checkBox = getSkinnable(); @@ -105,10 +117,65 @@ private void addListeners() { return; } getChildren().add(label); + }), + + onInvalidated(checkBox.stateProperty()) + .then(s -> { + if (s == TriState.SELECTED) { + icon.setIcon(checkBox.getSelectedIcon()); + return; + } + if (s == TriState.INDETERMINATE) { + icon.setIcon(checkBox.getIndeterminateIcon()); + return; + } + icon.setIcon((MFXFontIcon) null); }) + .executeNow(), + + onInvalidated(checkBox.selectedIconProperty()) + .condition(s -> checkBox.isSelected()) + .then(icon::setIcon), + + onInvalidated(checkBox.indeterminateIconProperty()) + .condition(s -> checkBox.isIndeterminate()) + .then(icon::setIcon) ); } + /** + * This is responsible for scaling down and up the box node when the state transitions from: + *

- UNSELECTED to SELECTED (and vice-versa) + *

- INDETERMINATE to UNSELECTED (if indeterminate enabled) + */ + protected void scale() { + MFXCheckbox checkbox = getSkinnable(); + TriState state = checkbox.getState(); + if (state == TriState.SELECTED || state == TriState.UNSELECTED) { + if (scaleAnimation == null) { + Duration d = Duration.millis(125); + Interpolator i = M3Motion.LINEAR; + Timeline down = TimelineBuilder.build() + .add(KeyFrames.of(d, icon.scaleXProperty(), 0.9, i)) + .add(KeyFrames.of(d, icon.scaleYProperty(), 0.9, i)) + .getAnimation(); + Timeline up = TimelineBuilder.build() + .add(KeyFrames.of(d, icon.scaleXProperty(), 1.0, i)) + .add(KeyFrames.of(d, icon.scaleYProperty(), 1.0, i)) + .getAnimation(); + scaleAnimation = SequentialBuilder.build() + .add(down) + .add(up) + .setOnFinished(e -> { + icon.setScaleX(1.0); + icon.setScaleY(1.0); + }) + .getAnimation(); + } + scaleAnimation.playFromStart(); + } + } + //================================================================================ // Overridden Methods //================================================================================ @@ -125,16 +192,16 @@ protected void initBehavior(MFXCheckboxBehavior behavior) { behavior.init(); events( intercept(checkBox, MouseEvent.MOUSE_PRESSED) - .process(behavior::mousePressed), + .process(e -> behavior.mousePressed(e, c -> rg.generate(e))), intercept(checkBox, MouseEvent.MOUSE_RELEASED) - .process(behavior::mouseReleased), + .process(e -> behavior.mouseReleased(e, c -> rg.release())), intercept(checkBox, MouseEvent.MOUSE_CLICKED) - .process(behavior::mouseClicked), + .process(e -> behavior.mouseClicked(e, c -> scale())), intercept(checkBox, MouseEvent.MOUSE_EXITED) - .process(behavior::mouseExited), + .process(e -> behavior.mouseExited(e, c -> rg.release())), intercept(checkBox, KeyEvent.KEY_PRESSED) .process(e -> behavior.keyPressed(e, c -> { diff --git a/modules/components/src/main/java/io/github/palexdev/mfxcomponents/skins/MFXIconButtonSkin.java b/modules/components/src/main/java/io/github/palexdev/mfxcomponents/skins/MFXIconButtonSkin.java index d8e679c3..3f1688e3 100644 --- a/modules/components/src/main/java/io/github/palexdev/mfxcomponents/skins/MFXIconButtonSkin.java +++ b/modules/components/src/main/java/io/github/palexdev/mfxcomponents/skins/MFXIconButtonSkin.java @@ -4,8 +4,10 @@ import io.github.palexdev.mfxcomponents.controls.MaterialSurface; import io.github.palexdev.mfxcomponents.controls.base.MFXSkinBase; import io.github.palexdev.mfxcomponents.controls.buttons.MFXIconButton; -import io.github.palexdev.mfxcore.utils.fx.LayoutUtils; -import io.github.palexdev.mfxresources.fonts.MFXFontIcon; +import io.github.palexdev.mfxcore.builders.bindings.DoubleBindingBuilder; +import io.github.palexdev.mfxeffects.ripple.MFXRippleGenerator; +import io.github.palexdev.mfxresources.fonts.MFXIconWrapper; +import javafx.geometry.Bounds; import javafx.geometry.HPos; import javafx.geometry.VPos; import javafx.scene.input.KeyEvent; @@ -13,7 +15,7 @@ import javafx.scene.paint.Color; import static io.github.palexdev.mfxcore.events.WhenEvent.intercept; -import static io.github.palexdev.mfxcore.observables.When.onChanged; +import static io.github.palexdev.mfxcore.observables.When.onInvalidated; /** * Default skin implementation for {@link MFXIconButton}s. Doesn't extend {@link MFXButtonSkin} as one may expect since @@ -25,12 +27,12 @@ * {@link MaterialSurface} responsible for showing the various interaction states (applying an overlay background) * and generating ripple effects. */ -// TODO maybe one day the icon could be wrapped in a MFXIconWrapper allowing the icon switch to be animated too public class MFXIconButtonSkin extends MFXSkinBase { //================================================================================ // Properties //================================================================================ private final MaterialSurface surface; + private final MFXIconWrapper icon; //================================================================================ // Constructors @@ -38,14 +40,20 @@ public class MFXIconButtonSkin extends MFXSkinBase Math.max(button.getWidth(), button.getHeight())) + .addSources(button.widthProperty(), button.heightProperty()) + .get()); + // Init surface surface = new MaterialSurface(button) .initRipple(rg -> rg.setRippleColor(Color.web("#d7d1e7"))); // Finalize init - MFXFontIcon icon = button.getIcon(); - getChildren().add(surface); - if (icon != null) getChildren().add(icon); + getChildren().addAll(surface, icon); addListeners(); } @@ -60,11 +68,9 @@ public MFXIconButtonSkin(MFXIconButton button) { private void addListeners() { MFXIconButton button = getSkinnable(); listeners( - onChanged(button.iconProperty()) - .then((o, n) -> { - if (o != null) getChildren().remove(o); - if (n != null) getChildren().add(n); - }) + onInvalidated(button.iconProperty()) + .then(icon::setIcon) + .executeNow() ); } @@ -80,22 +86,26 @@ private void addListeners() { @Override protected void initBehavior(MFXIconButtonBehavior behavior) { MFXIconButton button = getSkinnable(); + MFXRippleGenerator rg = surface.getRippleGenerator(); behavior.init(); events( intercept(button, MouseEvent.MOUSE_PRESSED) - .process(behavior::mousePressed), + .process(e -> behavior.mousePressed(e, c -> rg.generate(e))), intercept(button, MouseEvent.MOUSE_RELEASED) - .process(behavior::mouseReleased), + .process(e -> behavior.mouseReleased(e, c -> rg.release())), intercept(button, MouseEvent.MOUSE_CLICKED) .process(behavior::mouseClicked), intercept(button, MouseEvent.MOUSE_EXITED) - .process(behavior::mouseExited), + .process(e -> behavior.mouseExited(e, c -> rg.release())), intercept(button, KeyEvent.KEY_PRESSED) - .process(behavior::keyPressed) + .process(e -> behavior.keyPressed(e, c -> { + Bounds b = button.getLayoutBounds(); + rg.generate(b.getCenterX(), b.getCenterY()); + })) ); } @@ -111,36 +121,12 @@ public double computeMinHeight(double width, double topInset, double rightInset, @Override public double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { - MFXIconButton button = getSkinnable(); - MFXFontIcon icon = button.getIcon(); - double size = button.getSize(); - double val; - if (icon == null) { - val = size; - } else { - val = Math.max(size, Math.max( - LayoutUtils.boundWidth(icon), - LayoutUtils.boundHeight(icon) - )); - } - return leftInset + val + rightInset; + return leftInset + getSkinnable().getSize() + rightInset; } @Override public double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { - MFXIconButton button = getSkinnable(); - MFXFontIcon icon = button.getIcon(); - double size = button.getSize(); - double val; - if (icon == null) { - val = size; - } else { - val = Math.max(size, Math.max( - LayoutUtils.boundWidth(icon), - LayoutUtils.boundHeight(icon) - )); - } - return leftInset + val + rightInset; + return topInset + getSkinnable().getSize() + bottomInset; } @Override @@ -156,11 +142,9 @@ public double computeMaxHeight(double width, double topInset, double rightInset, @Override protected void layoutChildren(double x, double y, double w, double h) { MFXIconButton button = getSkinnable(); - MFXFontIcon icon = button.getIcon(); - surface.resizeRelocate(0, 0, button.getWidth(), button.getHeight()); - if (icon == null) return; - layoutInArea(icon, x, y, w, h, 0, HPos.CENTER, VPos.CENTER); + icon.autosize(); + positionInArea(icon, x, y, w, h, 0, HPos.CENTER, VPos.CENTER); } @Override diff --git a/modules/components/src/test/java/app/Sandbox.java b/modules/components/src/test/java/app/Sandbox.java index 721a03d9..28b5c8d0 100644 --- a/modules/components/src/test/java/app/Sandbox.java +++ b/modules/components/src/test/java/app/Sandbox.java @@ -19,15 +19,15 @@ package app; import io.github.palexdev.mfxcomponents.controls.buttons.MFXButton; -import io.github.palexdev.mfxcomponents.controls.fab.MFXFab; +import io.github.palexdev.mfxcomponents.controls.buttons.MFXIconButton; +import io.github.palexdev.mfxcomponents.controls.checkbox.MFXCheckbox; import io.github.palexdev.mfxcomponents.theming.MaterialThemes; import io.github.palexdev.mfxcore.builders.InsetsBuilder; -import io.github.palexdev.mfxresources.fonts.MFXFontIcon; +import io.github.palexdev.mfxcore.utils.fx.CSSFragment; import io.github.palexdev.mfxresources.fonts.fontawesome.FontAwesomeSolid; import javafx.application.Application; import javafx.geometry.Pos; import javafx.scene.Scene; -import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.stage.Stage; import org.scenicview.ScenicView; @@ -40,19 +40,22 @@ public void start(Stage stage) throws Exception { pane.setAlignment(Pos.CENTER); pane.setPadding(InsetsBuilder.all(10)); - MFXFab fab = new MFXFab("Floating Action Button", new MFXFontIcon(FontAwesomeSolid.CALCULATOR)); - fab.setExtended(true); + MFXCheckbox cb = new MFXCheckbox("Check"); + cb.setSelected(true); + cb.setAllowIndeterminate(true); - MFXButton btn = new MFXButton("Extend"); - btn.setOnAction(e -> fab.setExtended(!fab.isExtended())); + MFXIconButton ib = new MFXIconButton().asToggle().outlined(); + CSSFragment.Builder.build() + .addSelector(".mfx-icon-wrapper") + .border("red") + .addStyle("-mfx-round: true") + .addStyle("-mfx-animation-preset: SLIDE_RIGHT") + .closeSelector() + .applyOn(ib); + MFXButton b = new MFXButton("Change icon"); + b.setOnAction(e -> ib.setIcon(FontAwesomeSolid.random())); - MFXButton btn2 = new MFXButton("Change Icon"); - btn2.setOnAction(e -> fab.setIcon(FontAwesomeSolid.random())); - - HBox box = new HBox(30, btn, btn2); - box.setAlignment(Pos.CENTER); - - pane.getChildren().addAll(fab, box); + pane.getChildren().addAll(cb, ib, b); Scene scene = new Scene(pane, 600, 600); MaterialThemes.INDIGO_LIGHT.applyOn(scene); stage.setScene(scene); diff --git a/modules/components/src/test/java/app/Showcase.java b/modules/components/src/test/java/app/Showcase.java index 4110df74..c8714e41 100644 --- a/modules/components/src/test/java/app/Showcase.java +++ b/modules/components/src/test/java/app/Showcase.java @@ -36,6 +36,7 @@ import io.github.palexdev.mfxcore.selection.SelectionGroup; import io.github.palexdev.mfxcore.utils.fx.CSSFragment; import io.github.palexdev.mfxresources.fonts.MFXFontIcon; +import io.github.palexdev.mfxresources.fonts.fontawesome.FontAwesomeSolid; import javafx.application.Application; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; @@ -60,9 +61,6 @@ import java.util.function.BiFunction; import java.util.function.Supplier; -import static io.github.palexdev.mfxresources.fonts.IconsProviders.FONTAWESOME_SOLID; -import static io.github.palexdev.mfxresources.fonts.IconsProviders.randomIcon; - public class Showcase extends Application implements MultipleViewApp { //================================================================================ // Properties @@ -254,8 +252,8 @@ private Node createButtonsView(String title, double length, BiFunction { - btn8.setIcon(randomIcon(FONTAWESOME_SOLID)); + btn8.setIcon(FontAwesomeSolid.random()); e.consume(); }); @@ -317,15 +315,15 @@ private Node createExtendedFabView(String title, double length, BiFunction btn6.setExtended(!btn6.isExtended())); - btn7.setOnAction(e -> btn7.setIcon(randomIcon(FONTAWESOME_SOLID))); + btn7.setOnAction(e -> btn7.setIcon(FontAwesomeSolid.random())); defTfp.add(btn0, btn1, btn2, btn3, btn4, btn5, btn6, btn7, btn8); return defTfp; @@ -358,11 +356,11 @@ private Node createIconButtonsView(String title, double length, BiFunction { fab.setJavaFXLayoutStrategy(); root.getChildren().setAll(fab); diff --git a/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/MFXIconWrapper.java b/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/MFXIconWrapper.java index cc6a35f4..5cc7bf6f 100644 --- a/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/MFXIconWrapper.java +++ b/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/MFXIconWrapper.java @@ -30,6 +30,7 @@ import javafx.animation.Animation; import javafx.animation.Interpolator; import javafx.animation.Timeline; +import javafx.application.Platform; import javafx.collections.ObservableList; import javafx.css.*; import javafx.event.EventHandler; @@ -197,7 +198,7 @@ public MFXIconWrapper enableRippleGenerator(boolean enable, Function super.getChildren().add(child)); + return; + } super.getChildren().add(child); } @@ -304,6 +309,10 @@ protected void addChild(Node child) { * Convenience method for calling {@code super.getChildren().remove(child)}. */ protected void removeChild(Node child) { + if (!Platform.isFxApplicationThread()) { + Platform.runLater(() -> super.getChildren().remove(child)); + return; + } super.getChildren().remove(child); } diff --git a/modules/resources/src/main/resources/io/github/palexdev/mfxresources/themes/material/components/checkbox/_checkbox.scss b/modules/resources/src/main/resources/io/github/palexdev/mfxresources/themes/material/components/checkbox/_checkbox.scss index eebc10dc..f5578222 100644 --- a/modules/resources/src/main/resources/io/github/palexdev/mfxresources/themes/material/components/checkbox/_checkbox.scss +++ b/modules/resources/src/main/resources/io/github/palexdev/mfxresources/themes/material/components/checkbox/_checkbox.scss @@ -44,10 +44,10 @@ $ripple-color: Ripple.Ripple('primary') !default; -fx-border-radius: $icon-wrapper-radius; -fx-border-width: $icon-wrapper-bd-width; -mfx-size: $icon-wrapper-size; + -mfx-animation-preset: CLIP; } .mfx-checkbox > .mfx-icon-wrapper > .mfx-font-icon { - -mfx-description: "fas-check"; -mfx-size: $icon-size; visibility: hidden; } @@ -78,10 +78,6 @@ $icon-color: Theme.GetSchemeColor('on-primary') !default; -mfx-color: $icon-color; } -.mfx-checkbox:indeterminate > .mfx-icon-wrapper > .mfx-font-icon { - -mfx-description: "fas-minus"; -} - // Component-level tokens (error) $surface-tint-error: Theme.GetSchemeColor('error') !default; $icon-color-error: Theme.GetSchemeColor('on-error') !default; diff --git a/modules/resources/src/main/resources/io/github/palexdev/mfxresources/themes/material/md-indigo-dark.css b/modules/resources/src/main/resources/io/github/palexdev/mfxresources/themes/material/md-indigo-dark.css index ebf19919..2714762e 100644 --- a/modules/resources/src/main/resources/io/github/palexdev/mfxresources/themes/material/md-indigo-dark.css +++ b/modules/resources/src/main/resources/io/github/palexdev/mfxresources/themes/material/md-indigo-dark.css @@ -391,10 +391,10 @@ -fx-border-radius: 2px; -fx-border-width: 2px; -mfx-size: 18px; + -mfx-animation-preset: CLIP; } .mfx-checkbox > .mfx-icon-wrapper > .mfx-font-icon { - -mfx-description: "fas-check"; -mfx-size: 12px; visibility: hidden; } @@ -415,10 +415,6 @@ -mfx-color: #08218a; } -.mfx-checkbox:indeterminate > .mfx-icon-wrapper > .mfx-font-icon { - -mfx-description: "fas-minus"; -} - .mfx-checkbox:error > .surface > .mfx-ripple-generator { -mfx-ripple-color: rgba(255, 180, 171, 0.12); } diff --git a/modules/resources/src/main/resources/io/github/palexdev/mfxresources/themes/material/md-indigo-light.css b/modules/resources/src/main/resources/io/github/palexdev/mfxresources/themes/material/md-indigo-light.css index 4b2ee105..7c08b258 100644 --- a/modules/resources/src/main/resources/io/github/palexdev/mfxresources/themes/material/md-indigo-light.css +++ b/modules/resources/src/main/resources/io/github/palexdev/mfxresources/themes/material/md-indigo-light.css @@ -391,10 +391,10 @@ -fx-border-radius: 2px; -fx-border-width: 2px; -mfx-size: 18px; + -mfx-animation-preset: CLIP; } .mfx-checkbox > .mfx-icon-wrapper > .mfx-font-icon { - -mfx-description: "fas-check"; -mfx-size: 12px; visibility: hidden; } @@ -415,10 +415,6 @@ -mfx-color: #ffffff; } -.mfx-checkbox:indeterminate > .mfx-icon-wrapper > .mfx-font-icon { - -mfx-description: "fas-minus"; -} - .mfx-checkbox:error > .surface > .mfx-ripple-generator { -mfx-ripple-color: rgba(186, 26, 26, 0.12); } diff --git a/modules/resources/src/main/resources/io/github/palexdev/mfxresources/themes/material/md-purple-dark.css b/modules/resources/src/main/resources/io/github/palexdev/mfxresources/themes/material/md-purple-dark.css index cd1b60ee..0490c4ae 100644 --- a/modules/resources/src/main/resources/io/github/palexdev/mfxresources/themes/material/md-purple-dark.css +++ b/modules/resources/src/main/resources/io/github/palexdev/mfxresources/themes/material/md-purple-dark.css @@ -391,10 +391,10 @@ -fx-border-radius: 2px; -fx-border-width: 2px; -mfx-size: 18px; + -mfx-animation-preset: CLIP; } .mfx-checkbox > .mfx-icon-wrapper > .mfx-font-icon { - -mfx-description: "fas-check"; -mfx-size: 12px; visibility: hidden; } @@ -415,10 +415,6 @@ -mfx-color: #381E72; } -.mfx-checkbox:indeterminate > .mfx-icon-wrapper > .mfx-font-icon { - -mfx-description: "fas-minus"; -} - .mfx-checkbox:error > .surface > .mfx-ripple-generator { -mfx-ripple-color: rgba(242, 184, 181, 0.12); } diff --git a/modules/resources/src/main/resources/io/github/palexdev/mfxresources/themes/material/md-purple-light.css b/modules/resources/src/main/resources/io/github/palexdev/mfxresources/themes/material/md-purple-light.css index 6107fc43..3c47cf9b 100644 --- a/modules/resources/src/main/resources/io/github/palexdev/mfxresources/themes/material/md-purple-light.css +++ b/modules/resources/src/main/resources/io/github/palexdev/mfxresources/themes/material/md-purple-light.css @@ -391,10 +391,10 @@ -fx-border-radius: 2px; -fx-border-width: 2px; -mfx-size: 18px; + -mfx-animation-preset: CLIP; } .mfx-checkbox > .mfx-icon-wrapper > .mfx-font-icon { - -mfx-description: "fas-check"; -mfx-size: 12px; visibility: hidden; } @@ -415,10 +415,6 @@ -mfx-color: #FFFFFF; } -.mfx-checkbox:indeterminate > .mfx-icon-wrapper > .mfx-font-icon { - -mfx-description: "fas-minus"; -} - .mfx-checkbox:error > .surface > .mfx-ripple-generator { -mfx-ripple-color: rgba(179, 38, 30, 0.12); } diff --git a/modules/resources/src/test/java/app/IconAnimation.java b/modules/resources/src/test/java/app/IconAnimation.java index d7622199..e1e4d919 100644 --- a/modules/resources/src/test/java/app/IconAnimation.java +++ b/modules/resources/src/test/java/app/IconAnimation.java @@ -54,12 +54,17 @@ public void start(Stage primaryStage) { //wrapper.setBackground(Background.fill(Color.PURPLE)); String css = """ + .mfx-icon-wrapper { + -fx-background-color: blue; + } + .mfx-icon-wrapper .mfx-ripple-generator { -mfx-ripple-color: lightgrey; -mfx-ripple-radius: 64px; } .mfx-icon-wrapper .mfx-font-icon { + -mfx-color: white; -mfx-size: 64px; } """; diff --git a/modules/resources/src/test/java/interactive/IconsTests.java b/modules/resources/src/test/java/interactive/IconsTests.java index 5cf3af46..8a56e2e3 100644 --- a/modules/resources/src/test/java/interactive/IconsTests.java +++ b/modules/resources/src/test/java/interactive/IconsTests.java @@ -20,6 +20,7 @@ import io.github.palexdev.mfxeffects.enums.RippleState; import io.github.palexdev.mfxeffects.ripple.MFXRippleGenerator; +import io.github.palexdev.mfxeffects.ripple.base.RippleGenerator; import io.github.palexdev.mfxresources.builders.IconWrapperBuilder; import io.github.palexdev.mfxresources.fonts.IconsProviders; import io.github.palexdev.mfxresources.fonts.MFXFontIcon; @@ -29,6 +30,7 @@ import io.github.palexdev.mfxresources.fonts.fontawesome.FontAwesomeSolid; import io.github.palexdev.mfxresources.utils.EnumUtils; import javafx.beans.binding.Bindings; +import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; @@ -91,13 +93,13 @@ void testConstructors(FxRobot robot) throws InterruptedException { assertEquals(64.0, icon.get().getSize()); Thread.sleep(sleep); - icon.set(IconsProviders.FONTAWESOME_SOLID.randomIcon(64.0, Color.RED)); + icon.set(FontAwesomeSolid.random(Color.RED, 64.0)); robot.interact(() -> root.getChildren().setAll(icon.get())); assertEquals(64.0, icon.get().getFont().getSize()); assertEquals(64.0, icon.get().getSize()); Thread.sleep(sleep); - icon.set(IconsProviders.FONTAWESOME_BRANDS.randomIcon(64.0, Color.RED)); + icon.set(FontAwesomeSolid.random(Color.RED, 64.0)); robot.interact(() -> root.getChildren().setAll(icon.get())); assertEquals(64.0, icon.get().getFont().getSize()); assertEquals(64.0, icon.get().getSize()); @@ -203,16 +205,11 @@ void testWrap(FxRobot robot) throws InterruptedException { .setDescription(EnumUtils.randomEnum(FontAwesomeBrands.class).getDescription()); Thread.sleep(sleep); - robot.interact(() -> wrapper.setIcon( - IconsProviders.randomIcon( - IconsProviders.FONTAWESOME_REGULAR, - 64.0, - Color.web("#454545") - ) - )); + robot.interact(() -> wrapper.setIcon(FontAwesomeSolid.random(Color.web("#454545"), 64.0))); Thread.sleep(sleep); robot.interact(() -> { + wrapper.getIcon().setIconsProvider(IconsProviders.FONTAWESOME_REGULAR); wrapper.getIcon().setDescription(FontAwesomeRegular.SQUARE.getDescription()); wrapper.setStyle("-mfx-enable-ripple: true;\n-mfx-round: true;\n-mfx-ripple-pref-size: \"128.0 128.0\""); }); @@ -240,7 +237,13 @@ void testWrapperEnableRipple(FxRobot robot) { .enableRippleGenerator(true) .get(); robot.interact(() -> root.getChildren().setAll(icon)); - assertTrue(icon.getChildren().get(0) instanceof MFXRippleGenerator); + for (Node child : icon.getChildren()) { + if (child instanceof RippleGenerator) { + assertEquals(0, child.getViewOrder()); + continue; + } + assertEquals(1, child.getViewOrder()); + } } @Test @@ -251,7 +254,13 @@ void testWrapperEnableDisableRipple(FxRobot robot) { .enableRippleGenerator(true) .get(); robot.interact(() -> root.getChildren().setAll(icon)); - assertTrue(icon.getChildren().get(0) instanceof MFXRippleGenerator); + for (Node child : icon.getChildren()) { + if (child instanceof RippleGenerator) { + assertEquals(0, child.getViewOrder()); + continue; + } + assertEquals(1, child.getViewOrder()); + } robot.interact(() -> icon.enableRippleGenerator(false)); assertTrue(icon.getChildren().size() == 1 && !(icon.getChildren().get(0) instanceof MFXRippleGenerator));