Browse Source

2 new controls: MFXToggleButton, MFXToggleNode

Added missing documentation for MFXCheckbox and MFXButtonSkin

Signed-off-by: PAlex404 <alessandro.parisi406@gmail.com>
PAlex404 4 years ago
parent
commit
f446810e0c

+ 4 - 0
demo/build.gradle

@@ -11,8 +11,12 @@ repositories {
 
 dependencies {
     implementation "fr.brouillard.oss:cssfx:11.4.0"
+    implementation 'org.kordamp.ikonli:ikonli-core:11.5.0'
+    implementation 'org.kordamp.ikonli:ikonli-javafx:11.5.0'
+    implementation 'org.kordamp.ikonli:ikonli-fontawesome5-pack:11.5.0'
     implementation project(':materialfx')
 }
 application {
     mainClassName = 'MaterialFX.demo.main/it.paprojects.materialfx.demo.Demo'
+//    mainClassName = 'MaterialFX.demo.main/it.paprojects.materialfx.demo.TestDemo'
 }

+ 20 - 0
demo/src/main/java/it/paprojects/materialfx/demo/controllers/TogglesController.java

@@ -0,0 +1,20 @@
+package it.paprojects.materialfx.demo.controllers;
+
+import it.paprojects.materialfx.controls.MFXToggleButton;
+import javafx.fxml.FXML;
+import javafx.scene.paint.Color;
+
+import java.util.Random;
+
+public class TogglesController {
+    private final Random random = new Random(System.currentTimeMillis());
+
+    @FXML
+    private MFXToggleButton toggleButton;
+
+    @FXML
+    private void handleButtonClick() {
+        toggleButton.setToggleColor(Color.rgb(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
+        toggleButton.setSelected(false);
+    }
+}

+ 3 - 0
demo/src/main/java/module-info.java

@@ -2,8 +2,11 @@ module MaterialFX.demo.main {
     requires MaterialFX.materialfx.main;
 
     requires fr.brouillard.oss.cssfx;
+    requires org.kordamp.ikonli.javafx;
     requires javafx.graphics;
     requires javafx.controls;
     requires javafx.fxml;
+
+    opens it.paprojects.materialfx.demo.controllers;
     exports it.paprojects.materialfx.demo;
 }

+ 33 - 37
demo/src/main/resources/it/paprojects/materialfx/demo/checkboxes_demo.fxml

@@ -6,92 +6,88 @@
 <?import javafx.scene.layout.*?>
 <?import javafx.scene.paint.LinearGradient?>
 <?import javafx.scene.paint.Stop?>
-<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0"
-           prefWidth="600.0" stylesheets="@checkboxes_demo.css" xmlns="http://javafx.com/javafx/11.0.1">
-    <Label alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Checkboxes color"
-           StackPane.alignment="TOP_CENTER">
+<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" stylesheets="@checkboxes_demo.css" xmlns="http://javafx.com/javafx/11.0.1">
+    <Label alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Checkboxes color" StackPane.alignment="TOP_CENTER">
         <StackPane.margin>
-            <Insets top="20.0"/>
+            <Insets top="20.0" />
         </StackPane.margin>
     </Label>
     <MFXCheckbox allowIndeterminate="true" StackPane.alignment="TOP_CENTER">
         <StackPane.margin>
-            <Insets right="200.0" top="65.0"/>
+            <Insets right="200.0" top="65.0" />
         </StackPane.margin>
     </MFXCheckbox>
-    <MFXCheckbox allowIndeterminate="true" checkedColor="#b200ff" markType="VARIANT6" uncheckedColor="RED"
-                 StackPane.alignment="TOP_CENTER">
+    <MFXCheckbox allowIndeterminate="true" checkedColor="#b200ff" markType="VARIANT6" uncheckedColor="RED" StackPane.alignment="TOP_CENTER">
         <StackPane.margin>
-            <Insets top="65.0"/>
+            <Insets top="65.0" />
         </StackPane.margin>
     </MFXCheckbox>
     <MFXCheckbox allowIndeterminate="true" uncheckedColor="#008bff" StackPane.alignment="TOP_CENTER">
         <StackPane.margin>
-            <Insets left="200.0" top="65.0"/>
+            <Insets left="200.0" top="65.0" />
         </StackPane.margin>
         <checkedColor>
-            <LinearGradient endX="1.0" endY="1.0">
+            <LinearGradient>
                 <stops>
-                    <Stop color="#0098ff"/>
-                    <Stop color="#00ffcb" offset="0.8748091603053434"/>
-                    <Stop color="#00ffcb" offset="1.0"/>
+                    <Stop color="#0098ff" />
+                    <Stop color="#00ffcb" offset="0.8748091603053434" />
+                    <Stop color="#00ffcb" offset="1.0" />
                 </stops>
             </LinearGradient>
         </checkedColor>
     </MFXCheckbox>
-    <MFXCheckbox id="custom-ripple" prefHeight="30.0" prefWidth="164.0" text="CheckBox Custom Ripple"
-                 StackPane.alignment="TOP_CENTER">
+    <MFXCheckbox id="custom-ripple" prefHeight="30.0" prefWidth="164.0" text="CheckBox Custom Ripple" StackPane.alignment="TOP_CENTER">
         <StackPane.margin>
-            <Insets top="110.0"/>
+            <Insets top="110.0" />
         </StackPane.margin>
         <opaqueInsets>
-            <Insets/>
+            <Insets />
         </opaqueInsets>
     </MFXCheckbox>
-    <Label alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Checkboxes Mark Types"/>
-    <MFXCheckbox markType="CASPIAN">
+    <Label alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Checkboxes Mark Types" />
+    <MFXCheckbox markType="CASPIAN" text="Caspian">
         <StackPane.margin>
-            <Insets right="300.0" top="65.0"/>
+            <Insets right="300.0" top="65.0" />
         </StackPane.margin>
     </MFXCheckbox>
-    <MFXCheckbox>
+    <MFXCheckbox text="Modena">
         <StackPane.margin>
-            <Insets right="100.0" top="65.0"/>
+            <Insets right="100.0" top="65.0" />
         </StackPane.margin>
     </MFXCheckbox>
-    <MFXCheckbox markType="VARIANT3">
+    <MFXCheckbox markType="VARIANT3" text="Variant 3">
         <StackPane.margin>
-            <Insets left="100.0" top="65.0"/>
+            <Insets left="100.0" top="65.0" />
         </StackPane.margin>
     </MFXCheckbox>
-    <MFXCheckbox markType="VARIANT4">
+    <MFXCheckbox markType="VARIANT4" text="Variant 4">
         <StackPane.margin>
-            <Insets left="300.0" top="65.0"/>
+            <Insets left="300.0" top="65.0" />
         </StackPane.margin>
     </MFXCheckbox>
-    <MFXCheckbox markType="VARIANT5">
+    <MFXCheckbox markType="VARIANT5" text="Variant 5">
         <StackPane.margin>
-            <Insets right="300.0" top="130.0"/>
+            <Insets right="300.0" top="130.0" />
         </StackPane.margin>
     </MFXCheckbox>
-    <MFXCheckbox markType="VARIANT6">
+    <MFXCheckbox markType="VARIANT6" text="Variant 6">
         <StackPane.margin>
-            <Insets right="100.0" top="130.0"/>
+            <Insets right="100.0" top="130.0" />
         </StackPane.margin>
     </MFXCheckbox>
-    <MFXCheckbox markType="VARIANT7">
+    <MFXCheckbox markType="VARIANT7" text="Variant 7">
         <StackPane.margin>
-            <Insets left="100.0" top="130.0"/>
+            <Insets left="100.0" top="130.0" />
         </StackPane.margin>
     </MFXCheckbox>
-    <MFXCheckbox markType="VARIANT8">
+    <MFXCheckbox markType="VARIANT8" text="Variant 8">
         <StackPane.margin>
-            <Insets left="300.0" top="130.0"/>
+            <Insets left="300.0" top="130.0" />
         </StackPane.margin>
     </MFXCheckbox>
-    <MFXCheckbox markType="VARIANT9">
+    <MFXCheckbox markType="VARIANT9" text="Variant 9">
         <StackPane.margin>
-            <Insets top="195.0"/>
+            <Insets top="195.0" />
         </StackPane.margin>
     </MFXCheckbox>
 </StackPane>

+ 7 - 7
demo/src/main/resources/it/paprojects/materialfx/demo/main_demo.fxml

@@ -3,18 +3,18 @@
 <?import javafx.scene.control.Tab?>
 <?import javafx.scene.control.TabPane?>
 <?import javafx.scene.layout.*?>
-<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="360.0"
-            prefWidth="600.0" stylesheets="@main_demo.css" xmlns="http://javafx.com/javafx/11.0.1"
-            xmlns:fx="http://javafx.com/fxml/1">
-    <TabPane prefHeight="430.0" prefWidth="600.0" tabClosingPolicy="UNAVAILABLE" AnchorPane.bottomAnchor="0.0"
-             AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
+<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="360.0" prefWidth="600.0" stylesheets="@main_demo.css" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1">
+    <TabPane prefHeight="430.0" prefWidth="600.0" tabClosingPolicy="UNAVAILABLE" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
         <Tab text="Buttons demo">
             <StackPane prefHeight="258.0" prefWidth="600.0">
-                <fx:include maxWidth="-Infinity" source="buttons_demo.fxml" StackPane.alignment="CENTER"/>
+                <fx:include maxWidth="-Infinity" source="buttons_demo.fxml" StackPane.alignment="CENTER" />
             </StackPane>
         </Tab>
         <Tab text="Checkbox demo">
-            <fx:include source="checkboxes_demo.fxml"/>
+            <fx:include source="checkboxes_demo.fxml" />
+        </Tab>
+        <Tab text="Toggle Buttons">
+            <fx:include source="toggle_buttons_demo.fxml"/>
         </Tab>
     </TabPane>
 </AnchorPane>

+ 5 - 0
demo/src/main/resources/it/paprojects/materialfx/demo/toggle_buttons_demo.css

@@ -0,0 +1,5 @@
+@import url("common.css");
+
+#customRippleRadius .container .ripple-generator {
+    -mfx-ripple-radius: 15;
+}

+ 87 - 0
demo/src/main/resources/it/paprojects/materialfx/demo/toggle_buttons_demo.fxml

@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import it.paprojects.materialfx.controls.*?>
+<?import javafx.geometry.*?>
+<?import javafx.scene.control.Label?>
+<?import javafx.scene.layout.*?>
+<?import org.kordamp.ikonli.javafx.FontIcon?>
+<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0"
+           prefWidth="600.0" stylesheets="@toggle_buttons_demo.css" xmlns="http://javafx.com/javafx/11.0.1"
+           xmlns:fx="http://javafx.com/fxml/1"
+           fx:controller="it.paprojects.materialfx.demo.controllers.TogglesController">
+   <MFXToggleButton StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets right="320.0" top="60.0"/>
+      </StackPane.margin>
+   </MFXToggleButton>
+   <MFXToggleButton toggleColor="#008f1b" toggleLineColor="#aaff00" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets top="60.0"/>
+      </StackPane.margin>
+   </MFXToggleButton>
+   <MFXToggleButton toggleColor="#b200ff" toggleLineColor="#ff00f6" unToggleColor="#797979" unToggleLineColor="#bfbfbf"
+                    StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets left="320.0" top="60.0"/>
+      </StackPane.margin>
+   </MFXToggleButton>
+   <Label alignment="CENTER" maxWidth="266.0" prefHeight="26.0" text="Toggle Buttons" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets top="20.0"/>
+      </StackPane.margin>
+   </Label>
+   <MFXButton buttonType="RAISED" depthLevel="LEVEL1" onAction="#handleButtonClick" rippleColor="#0096ed"
+              rippleRadius="30.0" text="Change color">
+      <StackPane.margin>
+         <Insets bottom="135.0" right="370.0"/>
+      </StackPane.margin>
+   </MFXButton>
+   <MFXToggleButton fx:id="toggleButton" automaticColorAdjustment="true" text="Automatic Colors" toggleColor="#006aff"
+                    StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets right="320.0" top="140.0"/>
+      </StackPane.margin>
+   </MFXToggleButton>
+   <MFXToggleButton id="customRippleRadius" automaticColorAdjustment="true" text="RippleRadiusCss" toggleColor="#0095c2"
+                    StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets top="140.0"/>
+      </StackPane.margin>
+   </MFXToggleButton>
+   <MFXToggleButton id="customRippleRadius" automaticColorAdjustment="true" disable="true" text="Disabled"
+                    toggleColor="#0095c2" StackPane.alignment="TOP_CENTER">
+      <StackPane.margin>
+         <Insets left="320.0" top="140.0"/>
+      </StackPane.margin>
+   </MFXToggleButton>
+   <Label alignment="CENTER" maxWidth="266.0" prefHeight="26.0" text="Toggle Nodes">
+      <StackPane.margin>
+         <Insets top="20.0"/>
+      </StackPane.margin>
+   </Label>
+   <MFXToggleNode>
+      <StackPane.margin>
+         <Insets right="250.0" top="150.0"/>
+      </StackPane.margin>
+      <graphic>
+         <FontIcon iconColor="#2d7dd7" iconLiteral="fas-home" iconSize="25"/>
+      </graphic>
+   </MFXToggleNode>
+   <MFXToggleNode>
+      <StackPane.margin>
+         <Insets top="150.0"/>
+      </StackPane.margin>
+      <graphic>
+         <FontIcon iconColor="#e51010" iconLiteral="fas-heart" iconSize="25"/>
+      </graphic>
+   </MFXToggleNode>
+   <MFXToggleNode>
+      <StackPane.margin>
+         <Insets left="250.0" top="150.0"/>
+      </StackPane.margin>
+      <graphic>
+         <FontIcon iconColor="#e28000" iconLiteral="fas-cogs" iconSize="25" selectionEnd="0" selectionFill="RED"
+                   selectionStart="0" text=""/>
+      </graphic>
+   </MFXToggleNode>
+</StackPane>

+ 0 - 4
materialfx/build.gradle

@@ -10,7 +10,3 @@ compileJava   {
     sourceCompatibility = '11'
     targetCompatibility = '11'
 }
-
-dependencies {
-    testImplementation('junit:junit:4.13')
-}

+ 19 - 0
materialfx/src/main/java/it/paprojects/materialfx/controls/MFXCheckbox.java

@@ -11,6 +11,12 @@ import javafx.scene.paint.Paint;
 
 import java.util.List;
 
+/**
+ * This is the implementation of a checkbox following Google's material design guidelines in JavaFX.
+ * <p>
+ * Extends {@code CheckBox}, redefines the style class to "mfx-checkbox" for usage in CSS and
+ * includes a {@code RippleGenerator}(in the Skin) to generate ripple effect on click.
+ */
 public class MFXCheckbox extends CheckBox {
     //================================================================================
     // Properties
@@ -42,6 +48,11 @@ public class MFXCheckbox extends CheckBox {
     //================================================================================
     // Stylesheet properties
     //================================================================================
+
+    /**
+     * Specifies the color of the box when it's checked.
+     * @see Color
+     */
     private final StyleableObjectProperty<Paint> checkedColor = new SimpleStyleableObjectProperty<>(
             StyleableProperties.CHECKED_COLOR,
             this,
@@ -49,6 +60,10 @@ public class MFXCheckbox extends CheckBox {
             Color.rgb(15, 157, 88)
     );
 
+    /**
+     * Specifies the color of the box when it's unchecked.
+     * @see Color
+     */
     private final StyleableObjectProperty<Paint> uncheckedColor = new SimpleStyleableObjectProperty<>(
             StyleableProperties.UNCHECKED_COLOR,
             this,
@@ -56,6 +71,10 @@ public class MFXCheckbox extends CheckBox {
             Color.rgb(90, 90, 90)
     );
 
+    /**
+     * Specifies the SVG path(shape) of the mark from a predefined set.
+     * @see javafx.scene.shape.SVGPath
+     */
     private final StyleableObjectProperty<MarkType> markType = new SimpleStyleableObjectProperty<>(
             StyleableProperties.MARK_TYPE,
             this,

+ 274 - 0
materialfx/src/main/java/it/paprojects/materialfx/controls/MFXToggleButton.java

@@ -0,0 +1,274 @@
+package it.paprojects.materialfx.controls;
+
+import it.paprojects.materialfx.MFXResources;
+import it.paprojects.materialfx.skins.MFXToggleButtonSkin;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.css.*;
+import javafx.scene.Node;
+import javafx.scene.control.Skin;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+
+import java.util.List;
+
+/**
+ * This is the implementation of a toggle button following Google's material design guidelines in JavaFX.
+ * <p>
+ * Extends {@code ToggleButton}, redefines the style class to "mfx-toggle-button" for usage in CSS and
+ * includes a {@code RippleGenerator}(in the Skin) to generate ripple effect when toggled/untoggled.
+ */
+public class MFXToggleButton extends ToggleButton {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private static final StyleablePropertyFactory<MFXToggleButton> FACTORY = new StyleablePropertyFactory<>(ToggleButton.getClassCssMetaData());
+    private final String STYLE_CLASS = "mfx-toggle-button";
+    private final String STYLESHEET = MFXResources.load("css/mfx-togglebutton.css").toString();
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXToggleButton() {
+        setText("ToggleButton");
+        initialize();
+    }
+
+    public MFXToggleButton(String text) {
+        super(text);
+        initialize();
+    }
+
+    public MFXToggleButton(String text, Node graphic) {
+        super(text, graphic);
+        initialize();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        this.getStyleClass().add(STYLE_CLASS);
+
+        toggleColor.addListener((observable, oldValue, newValue) -> {
+            if (isAutomaticColorAdjustment()) {
+                Color color = ((Color) newValue).desaturate().desaturate().brighter();
+                toggleLineColor.set(color);
+            }
+        });
+    }
+
+    //================================================================================
+    // Styleable properties
+    //================================================================================
+
+    /**
+     * Specifies the color of the "circle" when toggled.
+     * @see Color
+     */
+    private final StyleableObjectProperty<Paint> toggleColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.TOGGLE_COLOR,
+            this,
+            "toggleColor",
+            Color.rgb(0, 150, 136)
+    );
+
+    /**
+     * Specifies the color of the "circle" when untoggled.
+     * @see Color
+     */
+    private final StyleableObjectProperty<Paint> unToggleColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.UNTOGGLE_COLOR,
+            this,
+            "unToggleColor",
+            Color.rgb(250, 250, 250)
+    );
+
+    /**
+     * Specifies the color of the "line" when toggled.
+     * @see Color
+     */
+    private final StyleableObjectProperty<Paint> toggleLineColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.TOGGLE_LINE_COLOR,
+            this,
+            "toggleLineColor",
+            Color.rgb(119, 194, 187)
+    );
+
+    /**
+     * Specifies the color of the line when untoggled.
+     * @see Color
+     */
+    private final StyleableObjectProperty<Paint> unToggleLineColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.UNTOGGLE_LINE_COLOR,
+            this,
+            "unToggleLineColor",
+            Color.rgb(153, 153, 153)
+    );
+
+    /**
+     * Specifies the size of the toggle button.
+     * <p>
+     * NOTE: Optimal values are from 8 upwards.
+     */
+    private final StyleableDoubleProperty size = new SimpleStyleableDoubleProperty(
+            StyleableProperties.SIZE,
+            this,
+            "size",
+            10.0
+    );
+
+    /**
+     * When this is set to true and toggle color is changed the untoggle color is automatically adjusted.
+     * <p>
+     * NOTE: This works only if changing toggle color, if untoggle color is changed the toggle color won't be automatically adjusted.
+     * You can see this behavior in the demo.
+     */
+    private final BooleanProperty automaticColorAdjustment = new SimpleBooleanProperty(false);
+
+    public Paint getToggleColor() {
+        return toggleColor.get();
+    }
+
+    public StyleableObjectProperty<Paint> toggleColorProperty() {
+        return toggleColor;
+    }
+
+    public void setToggleColor(Paint toggleColor) {
+        this.toggleColor.set(toggleColor);
+    }
+
+    public Paint getUnToggleColor() {
+        return unToggleColor.get();
+    }
+
+    public StyleableObjectProperty<Paint> unToggleColorProperty() {
+        return unToggleColor;
+    }
+
+    public void setUnToggleColor(Paint unToggleColor) {
+        this.unToggleColor.set(unToggleColor);
+    }
+
+    public Paint getToggleLineColor() {
+        return toggleLineColor.get();
+    }
+
+    public StyleableObjectProperty<Paint> toggleLineColorProperty() {
+        return toggleLineColor;
+    }
+
+    public void setToggleLineColor(Paint toggleLineColor) {
+        this.toggleLineColor.set(toggleLineColor);
+    }
+
+    public Paint getUnToggleLineColor() {
+        return unToggleLineColor.get();
+    }
+
+    public StyleableObjectProperty<Paint> unToggleLineColorProperty() {
+        return unToggleLineColor;
+    }
+
+    public void setUnToggleLineColor(Paint unToggleLineColor) {
+        this.unToggleLineColor.set(unToggleLineColor);
+    }
+
+    public double getSize() {
+        return size.get();
+    }
+
+    public StyleableDoubleProperty sizeProperty() {
+        return size;
+    }
+
+    public void setSize(double size) {
+        this.size.set(size);
+    }
+
+    public boolean isAutomaticColorAdjustment() {
+        return automaticColorAdjustment.get();
+    }
+
+    public BooleanProperty automaticColorAdjustmentProperty() {
+        return automaticColorAdjustment;
+    }
+
+    public void setAutomaticColorAdjustment(boolean automaticColorAdjustment) {
+        this.automaticColorAdjustment.set(automaticColorAdjustment);
+    }
+
+    //================================================================================
+    // CssMetaData
+    //================================================================================
+    private static class StyleableProperties {
+        private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+        private static final CssMetaData<MFXToggleButton, Paint> TOGGLE_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-toggle-color",
+                        MFXToggleButton::toggleColorProperty,
+                        Color.rgb(0, 150, 136)
+                );
+
+        private static final CssMetaData<MFXToggleButton, Paint> UNTOGGLE_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-untoggle-color",
+                        MFXToggleButton::unToggleColorProperty,
+                        Color.rgb(250, 250, 250)
+                );
+
+        private static final CssMetaData<MFXToggleButton, Paint> TOGGLE_LINE_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-toggle-line-color",
+                        MFXToggleButton::toggleLineColorProperty,
+                        Color.rgb(119, 194, 187)
+                );
+
+        private static final CssMetaData<MFXToggleButton, Paint> UNTOGGLE_LINE_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-untoggle-line-color",
+                        MFXToggleButton::unToggleLineColorProperty,
+                        Color.rgb(153, 153, 153)
+                );
+
+        private static final CssMetaData<MFXToggleButton, Number> SIZE =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-size",
+                        MFXToggleButton::sizeProperty,
+                        10.0
+                );
+
+        static {
+            cssMetaDataList = List.of(
+                    TOGGLE_COLOR, UNTOGGLE_COLOR,
+                    TOGGLE_LINE_COLOR, UNTOGGLE_LINE_COLOR,
+                    SIZE
+            );
+        }
+    }
+
+    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
+        return StyleableProperties.cssMetaDataList;
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new MFXToggleButtonSkin(this);
+    }
+
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+
+    @Override
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+        return getClassCssMetaData();
+    }
+}

+ 164 - 0
materialfx/src/main/java/it/paprojects/materialfx/controls/MFXToggleNode.java

@@ -0,0 +1,164 @@
+package it.paprojects.materialfx.controls;
+
+import it.paprojects.materialfx.MFXResources;
+import it.paprojects.materialfx.skins.MFXToggleNodeSkin;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.css.*;
+import javafx.scene.Node;
+import javafx.scene.control.Skin;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+
+import java.util.List;
+
+/**
+ * This control allows to create a toggle button with any {@code Node} set as graphic.
+ * <p>
+ * For example you can see in the demo this is used with Ikonli icons.
+ * <p>
+ * Extends {@code ToggleButton}, redefines the style class to "mfx-toggle-node" for usage in CSS and
+ * includes a {@code RippleGenerator}(in the Skin) to generate ripple effect when toggled/untoggled.
+ */
+public class MFXToggleNode extends ToggleButton {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private static final StyleablePropertyFactory<MFXToggleNode> FACTORY = new StyleablePropertyFactory<>(ToggleButton.getClassCssMetaData());
+    private final String STYLE_CLASS = "mfx-toggle-node";
+    private final String STYLESHEET = MFXResources.load("css/mfx-togglenode.css").toString();
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXToggleNode() {
+        initialize();
+    }
+
+    public MFXToggleNode(Node graphic) {
+        super("", graphic);
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        getStyleClass().add(STYLE_CLASS);
+    }
+
+    //================================================================================
+    // Styleable properties
+    //================================================================================
+
+    /**
+     * Specifies the background color when selected.
+     * @see Color
+     */
+    private final StyleableObjectProperty<Paint> selectedColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.SELECTED_COLOR,
+            this,
+            "selectedColor",
+            Color.rgb(0, 0, 0, 0.2)
+    );
+
+    /**
+     * Specifies the background color when unselected.
+     * @see Color
+     */
+    private final StyleableObjectProperty<Paint> unSelectedColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.UNSELECTED_COLOR,
+            this,
+            "unSelectedColor",
+            Color.TRANSPARENT
+    );
+
+    /**
+     * Specifies the border width of the control when selected.
+     */
+    private final DoubleProperty strokeWidth = new SimpleDoubleProperty(3.0);
+
+    public Paint getSelectedColor() {
+        return selectedColor.get();
+    }
+
+    public StyleableObjectProperty<Paint> selectedColorProperty() {
+        return selectedColor;
+    }
+
+    public void setSelectedColor(Paint selectedColor) {
+        this.selectedColor.set(selectedColor);
+    }
+
+    public Paint getUnSelectedColor() {
+        return unSelectedColor.get();
+    }
+
+    public StyleableObjectProperty<Paint> unSelectedColorProperty() {
+        return unSelectedColor;
+    }
+
+    public void setUnSelectedColor(Paint unSelectedColor) {
+        this.unSelectedColor.set(unSelectedColor);
+    }
+
+    public double getStrokeWidth() {
+        return strokeWidth.get();
+    }
+
+    public DoubleProperty strokeWidthProperty() {
+        return strokeWidth;
+    }
+
+    public void setStrokeWidth(double strokeWidth) {
+        this.strokeWidth.set(strokeWidth);
+    }
+
+    //================================================================================
+    // CssMetaData
+    //================================================================================
+    private static class StyleableProperties {
+        private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+        private static final CssMetaData<MFXToggleNode, Paint> SELECTED_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-selected-color",
+                        MFXToggleNode::selectedColorProperty,
+                        Color.rgb(0, 0, 0, 0.2)
+                );
+
+        private static final CssMetaData<MFXToggleNode, Paint> UNSELECTED_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-unselected-color",
+                        MFXToggleNode::unSelectedColorProperty,
+                        Color.TRANSPARENT
+                );
+
+        static {
+            cssMetaDataList = List.of(SELECTED_COLOR, UNSELECTED_COLOR);
+        }
+    }
+
+    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
+        return StyleableProperties.cssMetaDataList;
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new MFXToggleNodeSkin(this);
+    }
+
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+
+    @Override
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+        return getClassCssMetaData();
+    }
+}

+ 4 - 0
materialfx/src/main/java/it/paprojects/materialfx/skins/MFXButtonSkin.java

@@ -24,6 +24,10 @@ public class MFXButtonSkin extends ButtonSkin {
     //================================================================================
     // Methods
     //================================================================================
+
+    /**
+     * Changes the button type
+     */
     private void updateButtonType(MFXButton button, DepthLevel depthLevel) {
         switch (button.getButtonType()) {
             case RAISED: {

+ 0 - 4
materialfx/src/main/java/it/paprojects/materialfx/skins/MFXCheckboxSkin.java

@@ -175,10 +175,6 @@ public class MFXCheckboxSkin extends CheckBoxSkin {
         AnchorPane.setLeftAnchor(box, hInset);
     }
 
-    public Pane getRippleContainer() {
-        return rippleContainer;
-    }
-
     //================================================================================
     // Override Methods
     //================================================================================

+ 153 - 0
materialfx/src/main/java/it/paprojects/materialfx/skins/MFXToggleButtonSkin.java

@@ -0,0 +1,153 @@
+package it.paprojects.materialfx.skins;
+
+import it.paprojects.materialfx.controls.MFXToggleButton;
+import it.paprojects.materialfx.effects.DepthLevel;
+import it.paprojects.materialfx.effects.MFXDepthManager;
+import it.paprojects.materialfx.effects.RippleClipType;
+import it.paprojects.materialfx.effects.RippleGenerator;
+import javafx.animation.Interpolator;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.scene.Cursor;
+import javafx.scene.control.skin.ToggleButtonSkin;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Circle;
+import javafx.scene.shape.Line;
+import javafx.scene.shape.StrokeLineCap;
+import javafx.util.Duration;
+
+/**
+ *  This is the implementation of the {@code Skin} associated with every {@code MFXToggleButton}.
+ */
+public class MFXToggleButtonSkin extends ToggleButtonSkin {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final StackPane container;
+    private final Circle circle;
+    final double circleRadius;
+    private final Line line;
+    private final RippleGenerator rippleGenerator;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXToggleButtonSkin(MFXToggleButton control) {
+        super(control);
+
+         circleRadius = control.getSize();
+
+        line = new Line();
+        line.setStroke(control.isSelected() ? control.getToggleLineColor() : control.getUnToggleLineColor());
+        line.setStartX(0);
+        line.setStartY(0);
+        line.setEndX(circleRadius * 2 + 4);
+        line.setEndY(0);
+        line.setStrokeWidth(circleRadius * 1.5);
+        line.setStrokeLineCap(StrokeLineCap.ROUND);
+        line.setSmooth(true);
+
+        circle = new Circle(circleRadius);
+        circle.setFill(control.isSelected() ? control.getToggleColor() : control.getUnToggleColor());
+        circle.setTranslateX(-circleRadius);
+        circle.setSmooth(true);
+        circle.setEffect(MFXDepthManager.shadowOf(DepthLevel.LEVEL1));
+
+        container = new StackPane();
+        container.getStyleClass().setAll("container");
+        container.getChildren().addAll(line, circle);
+        container.setCursor(Cursor.HAND);
+        container.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+        container.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+        container.setPrefSize(50, 40);
+
+        rippleGenerator = new RippleGenerator(container, RippleClipType.NOCLIP);
+        rippleGenerator.setAnimateBackground(false);
+        rippleGenerator.setRippleColor((Color) (control.isSelected() ? control.getUnToggleLineColor() : control.getToggleLineColor()));
+        rippleGenerator.setRippleRadius(circleRadius * 1.2);
+        rippleGenerator.setInDuration(Duration.millis(400));
+        rippleGenerator.setTranslateX(-circleRadius);
+        container.getChildren().add(0, rippleGenerator);
+
+        control.setGraphic(container);
+
+        setListeners(control);
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+
+    /**
+     * Adds listeners for: selected, size and skin(workaround) properties.
+     * @param control The MFXToggleButton associated to this skin
+     */
+    private void setListeners(MFXToggleButton control) {
+        control.selectedProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue) {
+                line.setStroke(control.getToggleLineColor());
+                rippleGenerator.setRippleColor((Color) control.getToggleLineColor());
+                circle.setFill(control.getToggleColor());
+            } else {
+                line.setStroke(control.getUnToggleLineColor());
+                rippleGenerator.setRippleColor((Color) control.getUnToggleLineColor());
+                circle.setFill(control.getUnToggleColor());
+            }
+        });
+
+        control.selectedProperty().addListener((observable, oldValue, newValue) -> buildAndPlayAnimation(newValue));
+
+        control.sizeProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue.doubleValue() < oldValue.doubleValue()) {
+                double translateX = newValue.doubleValue() + oldValue.doubleValue();
+                circle.setTranslateX(translateX + 2);
+            }
+        });
+
+        /*
+         * Workaround
+         * When the control is created the Skin is still null, so if the ToggleButton is set
+         * to be selected the animation won't be played. To fix this add a listener to the
+         * control's skinProperty, when the skin is not null and the ToggleButton isSelected,
+         * play the animation.
+         */
+        control.skinProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue != null && control.isSelected()) {
+                buildAndPlayAnimation(true);
+            }
+        });
+    }
+
+    /**
+     * Re-builds and plays the translation animation every time the control is selected/unselected.
+     * @param isSelected The control's state
+     */
+    private void buildAndPlayAnimation(boolean isSelected) {
+        KeyValue circleTranslateXKey;
+        KeyValue rippleTranslateXKey;
+        KeyFrame circleTranslateXFrame;
+        KeyFrame rippleTranslateXFrame;
+        KeyFrame rippleAnimationFrame;
+
+        circleTranslateXKey = new KeyValue(circle.translateXProperty(), computeTranslateX(isSelected), Interpolator.EASE_BOTH);
+        rippleTranslateXKey = new KeyValue(rippleGenerator.translateXProperty(), computeTranslateX(isSelected), Interpolator.EASE_BOTH);
+
+        circleTranslateXFrame = new KeyFrame(Duration.millis(150), circleTranslateXKey);
+        rippleTranslateXFrame = new KeyFrame(Duration.millis(150), rippleTranslateXKey);
+        rippleAnimationFrame = new KeyFrame(Duration.ZERO, event -> rippleGenerator.createRipple());
+        Timeline timeline = new Timeline(circleTranslateXFrame, rippleTranslateXFrame, rippleAnimationFrame);
+        timeline.play();
+    }
+
+    /**
+     * Computes the final x coordinate of the translate animation.
+     * @param isSelected The control's state
+     * @return The final x coordinate.
+     */
+    private double computeTranslateX(boolean isSelected) {
+        return isSelected ? (line.getEndX() - circleRadius) : (-circleRadius);
+    }
+}

+ 154 - 0
materialfx/src/main/java/it/paprojects/materialfx/skins/MFXToggleNodeSkin.java

@@ -0,0 +1,154 @@
+package it.paprojects.materialfx.skins;
+
+import it.paprojects.materialfx.controls.MFXToggleNode;
+import it.paprojects.materialfx.effects.RippleClipType;
+import it.paprojects.materialfx.effects.RippleGenerator;
+import javafx.animation.Interpolator;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.scene.control.skin.ToggleButtonSkin;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Circle;
+import javafx.util.Duration;
+
+import java.util.Optional;
+
+/**
+ *  This is the implementation of the {@code Skin} associated with every {@code MFXToggleNode}.
+ */
+public class MFXToggleNodeSkin extends ToggleButtonSkin {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final StackPane container;
+    private final RippleGenerator rippleGenerator;
+    private final Circle circle;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXToggleNodeSkin(MFXToggleNode control) {
+        super(control);
+
+        container = new StackPane();
+        Optional.ofNullable(control.getGraphic()).ifPresent(node -> container.getChildren().add(node));
+
+        circle = new Circle();
+        circle.setOpacity(0.0);
+        circle.setFill(control.getUnSelectedColor());
+        circle.setStrokeWidth(control.getStrokeWidth());
+
+        rippleGenerator = new RippleGenerator(control, RippleClipType.NOCLIP);
+        rippleGenerator.setAnimateBackground(false);
+        rippleGenerator.setRippleColor(Color.GRAY);
+        rippleGenerator.setInDuration(Duration.millis(250));
+
+        updateChildren();
+        setListeners(control);
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+
+    /**
+     * Adds listeners for: mouse pressed, selected and skin(workaround) properties.
+     * @param control The MFXToggleButton associated to this skin
+     */
+    private void setListeners(MFXToggleNode control) {
+        control.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
+            rippleGenerator.setGeneratorCenterX(event.getX());
+            rippleGenerator.setGeneratorCenterY(event.getY());
+            rippleGenerator.createRipple();
+        });
+
+        control.selectedProperty().addListener((observable, oldValue, newValue) -> buildAndPlayAnimation(newValue));
+
+        /*
+         * Workaround
+         * When the control is created the Skin is still null, so if the ToggleNode is set
+         * to be selected the animation won't be played. To fix this add a listener to the
+         * control's skinProperty, when the skin is not null and the ToggleNode isSelected,
+         * play the animation.
+         */
+        control.skinProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue != null && control.isSelected()) {
+                buildAndPlayAnimation(true);
+            }
+        });
+    }
+
+    /**
+     * Re-builds and plays the background animation every time the control is selected/unselected.
+     * @param isSelected The control's state
+     */
+    private void buildAndPlayAnimation(boolean isSelected) {
+        MFXToggleNode control = (MFXToggleNode) getSkinnable();
+
+        final KeyValue keyValue1;
+        final KeyValue keyValue2;
+        final KeyValue keyValue3;
+        if (isSelected) {
+            keyValue1 = new KeyValue(circle.opacityProperty(), 0.3, Interpolator.EASE_IN);
+            keyValue2 = new KeyValue(circle.fillProperty(), control.getSelectedColor(), Interpolator.EASE_IN);
+            keyValue3 = new KeyValue(circle.strokeProperty(), ((Color) control.getSelectedColor()).darker(), Interpolator.EASE_IN);
+        } else {
+            keyValue1 = new KeyValue(circle.opacityProperty(), 0.0, Interpolator.EASE_OUT);
+            keyValue2 = new KeyValue(circle.fillProperty(), control.getUnSelectedColor(), Interpolator.EASE_OUT);
+            keyValue3 = new KeyValue(circle.strokeProperty(), Color.TRANSPARENT, Interpolator.EASE_OUT);
+        }
+
+        Timeline timeline = new Timeline(
+                new KeyFrame(Duration.millis(100), keyValue1, keyValue2, keyValue3)
+        );
+        timeline.play();
+    }
+
+    /**
+     * Computes the radius to be used for the circle. It's either half the width or height
+     * of the control depending on which of the two is the smaller.
+     * @return The circle radius
+     */
+    private double computeRadius() {
+        return Math.min((getSkinnable().getWidth() / 2), (getSkinnable().getHeight() / 2));
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+
+    /**
+     * Adds the circle and the ripple generator to the control as soon as they are not null.
+     */
+    @Override
+    protected void updateChildren() {
+        super.updateChildren();
+        if (circle != null && rippleGenerator != null) {
+            getChildren().addAll(circle, rippleGenerator);
+        }
+    }
+
+    /**
+     * Each time the control sizes change, recalculates the circle center coordinates and radius,
+     * the clip center coordinates and radius, the ripple generator radius.
+     */
+    @Override
+    protected void layoutChildren(double x, double y, double w, double h) {
+        super.layoutChildren(x, y, w, h);
+
+        circle.setCenterX(getSkinnable().getWidth() / 2);
+        circle.setCenterY(getSkinnable().getHeight() / 2);
+        circle.setRadius(computeRadius() * 1.3);
+
+        rippleGenerator.setRippleRadius(computeRadius() * 1.2);
+
+        Circle clip = new Circle();
+        clip.setCenterX(getSkinnable().getWidth() / 2);
+        clip.setCenterY(getSkinnable().getHeight() / 2);
+        clip.setRadius(computeRadius() * 1.4);
+        getSkinnable().setClip(clip);
+    }
+}

+ 32 - 0
materialfx/src/main/resources/it/paprojects/materialfx/css/mfx-togglebutton.css

@@ -0,0 +1,32 @@
+.mfx-toggle-button,
+.mfx-toggle-button:armed,
+.mfx-toggle-button:hover,
+.mfx-toggle-button:focused,
+.mfx-toggle-button:selected,
+.mfx-toggle-button:focused:selected {
+    -fx-background-color: TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT;
+    -fx-background-radius: 3px;
+    -fx-background-insets: 0px;
+
+    -mfx-toggle-color: #009688;
+    -mfx-untoggle-color: #FAFAFA;
+    -mfx-toggle-line-color: derive(-mfx-toggle-color, 65%);
+    -mfx-untoggle-line-color: #999999;
+    -mfx-size: 10px;
+}
+
+
+.mfx-toggle-button Line {
+    -fx-stroke: -mfx-untoggle-line-color;
+}
+.mfx-toggle-button:selected Line{
+    -fx-stroke: -mfx-toggle-line-color;
+}
+
+.mfx-toggle-button Circle{
+    -fx-fill: -mfx-untoggle-color;
+}
+
+.mfx-toggle-button:selected Circle{
+    -fx-fill: -mfx-toggle-color;
+}

+ 10 - 0
materialfx/src/main/resources/it/paprojects/materialfx/css/mfx-togglenode.css

@@ -0,0 +1,10 @@
+.mfx-toggle-node,
+.mfx-toggle-node:armed,
+.mfx-toggle-node:hover,
+.mfx-toggle-node:focused,
+.mfx-toggle-node:selected,
+.mfx-toggle-node:focused:selected{
+    -fx-background-color: TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT;
+    -fx-background-radius: 3px;
+    -fx-background-insets: 0px;
+}