Procházet zdrojové kódy

Add new material control: MFXButton

PAlex404 před 5 roky
rodič
revize
a16d2431a5

+ 14 - 0
.gitignore

@@ -0,0 +1,14 @@
+# IntelliJ Idea
+.idea/
+build/
+out/
+
+# Gradle
+.gradle
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+# Others
+demo/src/test
+materialfx/src/test

+ 55 - 0
build.gradle

@@ -0,0 +1,55 @@
+plugins {
+    id 'java'
+    id 'org.openjfx.javafxplugin' version '0.0.9' apply false
+    id 'biz.aQute.bnd.builder' version '5.1.2' apply false
+}
+
+group 'it.paprojects.materialfx'
+version '0.1-ALPHA'
+
+repositories {
+    mavenCentral()
+    jcenter()
+}
+
+subprojects {
+    if (name.endsWith('demo')) {
+        apply plugin: 'application'
+    }
+    if (name.endsWith("materialfx")) {
+        apply plugin: 'biz.aQute.bnd.builder'
+    }
+    apply plugin: 'org.openjfx.javafxplugin'
+
+    javafx {
+        version = "14.0.2.1"
+        modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.web' ]
+    }
+
+    version '0.1-ALPHA'
+
+    javadoc {
+        excludes = ['**/*.html', 'META-INF/**']
+
+        options.use = true
+        options.splitIndex = true
+        options.encoding = 'UTF-8'
+        options.author = true
+        options.version = true
+        options.windowTitle = "$project.name $project.version API"
+        options.docTitle = "$project.name $project.version API"
+        options.links = ['https://docs.oracle.com/en/java/javase/11/docs/api',
+                         'https://openjfx.io/javadoc/14']
+    }
+}
+
+jar {
+    manifest {
+        attributes(
+                'Bundle-Name': project.name,
+                'Bundle-Description': "Material controls for JavaFX",
+                'Bundle-SymbolicName': 'it.paprojects',
+                'Export-Package': 'it.paprojects.materialfx.*, it.paprojects.materialfx.demo.*'
+        )
+    }
+}

+ 17 - 0
demo/build.gradle

@@ -0,0 +1,17 @@
+plugins {
+    id 'application'
+}
+
+group 'it.paprojects.materialfx.demo'
+
+repositories {
+    mavenCentral()
+    jcenter()
+}
+
+dependencies {
+    implementation "fr.brouillard.oss:cssfx:11.4.0"
+    implementation project(':materialfx')
+}
+
+mainClassName = 'it.paprojects.materialfx.demo.Demo'

+ 44 - 0
demo/src/main/java/it/paprojects/materialfx/demo/Demo.java

@@ -0,0 +1,44 @@
+package it.paprojects.materialfx.demo;
+
+import fr.brouillard.oss.cssfx.CSSFX;
+import it.paprojects.materialfx.MFXResources;
+import it.paprojects.materialfx.controls.MFXButton;
+import it.paprojects.materialfx.utils.ButtonType;
+import javafx.application.Application;
+import javafx.geometry.Pos;
+import javafx.scene.Scene;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.stage.Stage;
+import javafx.util.Duration;
+
+public class Demo extends Application {
+
+    @Override
+    public void start(Stage primaryStage) {
+        CSSFX.start();
+
+        StackPane pane = new StackPane();
+        pane.getStylesheets().add(MFXResources.load("css/mfx-button.css").toString());
+
+        MFXButton button = new MFXButton("MFXButton");
+        button.setPrefWidth(500);
+        button.setPrefHeight(200);
+        button.setButtonType(ButtonType.RAISED);
+        pane.getChildren().add(button);
+        StackPane.setAlignment(button, Pos.CENTER);
+
+        button.setRippleColor(Color.rgb(10, 120, 200));
+        button.setRippleRadius(255);
+        button.setRippleInDuration(Duration.millis(600));
+
+        primaryStage.setTitle("HELLO THERE");
+        primaryStage.setScene(new Scene(pane, 800, 600));
+        primaryStage.show();
+    }
+
+    public static void main(String[] args) {
+        launch(args);
+    }
+
+}

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

@@ -0,0 +1,8 @@
+module MaterialFX.demo.main {
+    requires MaterialFX.materialfx.main;
+
+    requires fr.brouillard.oss.cssfx;
+    requires javafx.graphics;
+    requires javafx.controls;
+    exports it.paprojects.materialfx.demo;
+}

binární
gradle/wrapper/gradle-wrapper.jar


+ 5 - 0
gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 185 - 0
gradlew

@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=`expr $i + 1`
+    done
+    case $i in
+        0) set -- ;;
+        1) set -- "$args0" ;;
+        2) set -- "$args0" "$args1" ;;
+        3) set -- "$args0" "$args1" "$args2" ;;
+        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"

+ 104 - 0
gradlew.bat

@@ -0,0 +1,104 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 16 - 0
materialfx/build.gradle

@@ -0,0 +1,16 @@
+group 'it.paprojects.materialfx'
+
+repositories {
+    mavenCentral()
+    jcenter()
+}
+
+compileJava   {
+    // This is necessary since Gluon devs are assholes :)
+    sourceCompatibility = '11'
+    targetCompatibility = '11'
+}
+
+dependencies {
+    testImplementation('junit:junit:4.13')
+}

+ 16 - 0
materialfx/src/main/java/it/paprojects/materialfx/MFXResources.java

@@ -0,0 +1,16 @@
+package it.paprojects.materialfx;
+
+import java.net.URL;
+
+/**
+ * Utility class which manages the access to this project's assets.
+ * Helps keeping the assets files structure organized.
+ */
+public class MFXResources {
+
+    private MFXResources() {}
+
+    public static URL load(String path) {
+        return MFXResources.class.getResource(path);
+    }
+}

+ 241 - 0
materialfx/src/main/java/it/paprojects/materialfx/controls/MFXButton.java

@@ -0,0 +1,241 @@
+package it.paprojects.materialfx.controls;
+
+import it.paprojects.materialfx.MFXResources;
+import it.paprojects.materialfx.effects.DepthLevel;
+import it.paprojects.materialfx.effects.RippleGenerator;
+import it.paprojects.materialfx.skins.MFXButtonSkin;
+import it.paprojects.materialfx.utils.ButtonType;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.css.*;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.Skin;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+import javafx.util.Duration;
+
+import java.util.List;
+
+/**
+ * This is the implementation of a button following Google's material design guidelines in JavaFX.
+ * <p>
+ * Extends {@code Button}, redefines the style class to "mfx-button" for usage in CSS and
+ * includes a {@code RippleGenerator} to generate ripple effect on click.
+ */
+public class MFXButton extends Button {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private static final StyleablePropertyFactory<MFXButton> FACTORY = new StyleablePropertyFactory<>(Button.getClassCssMetaData());
+    private final String STYLE_CLASS = "mfx-button";
+    private final String STYLESHEET = MFXResources.load("css/mfx-button.css").toString();
+    private final RippleGenerator rippleGenerator = new RippleGenerator(this);
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXButton() {
+        setText("Button");
+        init();
+    }
+
+    public MFXButton(String text) {
+        super(text);
+        init();
+    }
+
+    public MFXButton(String text, Node graphic) {
+        super(text, graphic);
+        init();
+    }
+
+    private void init() {
+        getStyleClass().add(STYLE_CLASS);
+        setAlignment(Pos.CENTER);
+        setBindings();
+    }
+
+    //================================================================================
+    // Ripple properties
+    //================================================================================
+
+    /**
+     * Specifies the ripples color of this control.
+     * @see Color
+     */
+    private final ObjectProperty<Paint> rippleColor = new SimpleObjectProperty<>();
+
+    /**
+     * Specifies the ripples radius of this control.
+     */
+    private final DoubleProperty rippleRadius = new SimpleDoubleProperty();
+
+    /**
+     * Specifies the ripples in animation duration of this control.
+     * @see Duration
+     */
+    private final ObjectProperty<Duration> rippleInDuration = new SimpleObjectProperty<>();
+
+    /**
+     * Specifies the ripples out animation duration of this control.
+     * @see Duration
+     */
+    private final ObjectProperty<Duration> rippleOutDuration = new SimpleObjectProperty<>();
+
+    public final ObjectProperty<Paint> rippleColorProperty() {
+        return this.rippleColor;
+    }
+
+    public final Paint getRippleColor() {
+        return rippleColor.get();
+    }
+
+    public final void setRippleColor(Paint rippleColor) {
+        rippleGenerator.setRippleColor((Color) rippleColor);
+    }
+
+    public double getRippleRadius() {
+        return rippleRadius.get();
+    }
+
+    public DoubleProperty rippleRadiusProperty() {
+        return rippleRadius;
+    }
+
+    public void setRippleRadius(double rippleRadius) {
+        rippleGenerator.setRippleRadius(rippleRadius);
+    }
+
+    public Duration getRippleInDuration() {
+        return rippleInDuration.get();
+    }
+
+    public ObjectProperty<Duration> rippleInDurationProperty() {
+        return rippleInDuration;
+    }
+
+    public void setRippleInDuration(Duration rippleInDuration) {
+        rippleGenerator.setInDuration(rippleInDuration);
+    }
+
+    public Duration getRippleOutDuration() {
+        return rippleOutDuration.get();
+    }
+
+    public ObjectProperty<Duration> rippleOutDurationProperty() {
+        return rippleOutDuration;
+    }
+
+    public void setRippleOutDuration(Duration rippleOutDuration) {
+        rippleGenerator.setOutDuration(rippleOutDuration);
+    }
+
+    private void setBindings() {
+        rippleColor.bind(rippleGenerator.rippleColorProperty());
+        rippleRadius.bind(rippleGenerator.rippleRadiusProperty());
+        rippleInDuration.bind(rippleGenerator.inDurationProperty());
+        rippleOutDuration.bind(rippleGenerator.outDurationProperty());
+    }
+
+    //================================================================================
+    // Stylesheet properties
+    //================================================================================
+
+    /**
+     * Specifies how intense is the {@code DropShadow} effect applied to this control.
+     * <p>
+     * The {@code DropShadow} effect is used to make the control appear {@code RAISED}.
+     */
+    private final ObjectProperty<DepthLevel> depthLevel = new SimpleObjectProperty<>(DepthLevel.LEVEL2);
+
+    /**
+     * Specifies the appearance of this control. According to material design there are two types of buttons:
+     * <p>
+     * - {@code FLAT}
+     * <p>
+     * - {@code RAISED}
+     */
+    private final StyleableObjectProperty<ButtonType> buttonType = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.BUTTON_TYPE,
+            this,
+            "buttonType",
+            ButtonType.FLAT
+    );
+
+    public ButtonType getButtonType() {
+        return buttonType.get();
+    }
+
+    public StyleableObjectProperty<ButtonType> buttonTypeProperty() {
+        return buttonType;
+    }
+
+    public void setButtonType(ButtonType buttonType) {
+        this.buttonType.set(buttonType);
+    }
+
+    public DepthLevel getDepthLevel() {
+        return depthLevel.get();
+    }
+
+    public ObjectProperty<DepthLevel> depthLevelProperty() {
+        return depthLevel;
+    }
+
+    public void setDepthLevel(DepthLevel depthLevel) {
+        this.depthLevel.set(depthLevel);
+    }
+
+    //================================================================================
+    // CssMetaData
+    //================================================================================
+    private static class StyleableProperties {
+        private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+        private static final CssMetaData<MFXButton, ButtonType> BUTTON_TYPE =
+                FACTORY.createEnumCssMetaData(
+                        ButtonType.class,
+                        "-mfx-button-type",
+                        MFXButton::buttonTypeProperty,
+                        ButtonType.FLAT);
+
+        static {
+            cssMetaDataList = List.of(BUTTON_TYPE);
+        }
+
+    }
+
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+        return StyleableProperties.cssMetaDataList;
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        MFXButtonSkin skin = new MFXButtonSkin(this, depthLevel.get());
+        this.getChildren().add(0, rippleGenerator);
+        this.setOnMousePressed(event -> {
+            rippleGenerator.setGeneratorCenterX(event.getX());
+            rippleGenerator.setGeneratorCenterY(event.getY());
+            rippleGenerator.createRipple();
+        });
+        return skin;
+    }
+
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+
+    @Override
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+        return this.getControlCssMetaDataList();
+    }
+}

+ 58 - 0
materialfx/src/main/java/it/paprojects/materialfx/effects/DepthLevel.java

@@ -0,0 +1,58 @@
+package it.paprojects.materialfx.effects;
+
+import javafx.scene.paint.Color;
+
+/**
+ * Enumerator which defines 5 levels of {@code DropShadow} effects from {@code LEVEL1} to {@code LEVEL5}.
+ */
+public enum DepthLevel {
+    //LEVEL0(Color.rgb(0, 0, 0, 0), 0, 0, 0, 0),
+    LEVEL1(Color.rgb(0, 0, 0, 0.20), 10, 0.12, -1, 2),
+    LEVEL2(Color.rgb(0, 0, 0, 0.20), 15, 0.16, 0, 4),
+    LEVEL3(Color.rgb(0, 0, 0, 0.20), 20, 0.19, 0, 6),
+    LEVEL4(Color.rgb(0, 0, 0, 0.20), 25, 0.25, 0, 8),
+    LEVEL5(Color.rgb(0, 0, 0, 0.20), 30, 0.30, 0, 10);
+
+    private final Color color;
+    private final double radius;
+    private final double spread;
+    private final double offsetX;
+    private final double offsetY;
+    private static final DepthLevel[] valuesArr = values();
+
+    DepthLevel(Color color, double radius, double spread, double offsetX, double offsetY) {
+        this.color = color;
+        this.radius = radius;
+        this.spread = spread;
+        this.offsetX = offsetX;
+        this.offsetY = offsetY;
+    }
+
+    public Color getColor() {
+        return color;
+    }
+
+    public double getRadius() {
+        return radius;
+    }
+
+    public double getSpread() {
+        return spread;
+    }
+
+    public double getOffsetX() {
+        return offsetX;
+    }
+
+    public double getOffsetY() {
+        return offsetY;
+    }
+
+    /**
+     * Retrieves the next {@code DepthLevel} associated with {@code this} enumerator.
+     * @return The next {@code DepthLevel}
+     */
+    public DepthLevel next() {
+        return valuesArr[(this.ordinal()+1) % valuesArr.length];
+    }
+}

+ 60 - 0
materialfx/src/main/java/it/paprojects/materialfx/effects/MFXDepthManager.java

@@ -0,0 +1,60 @@
+package it.paprojects.materialfx.effects;
+
+import javafx.scene.effect.BlurType;
+import javafx.scene.effect.DropShadow;
+
+/**
+ * Utility class which manages a preset number of {@code DropShadow} effects ordered by {@code DepthLevel}.
+ * <p></p>
+ * {@link DepthLevel}
+ */
+public class MFXDepthManager {
+
+    /**
+     * Retrieves the {@code DropShadow} associated with the specified {@code DepthLevel}.
+     * @param level The desired {@code DepthLevel} between 1 and 5
+     * @return The desired {@code DropShadow} effect
+     */
+    public static DropShadow shadowOf(DepthLevel level) {
+        return new DropShadow(
+                BlurType.GAUSSIAN,
+                level.getColor(),
+                level.getRadius(),
+                level.getSpread(),
+                level.getOffsetX(),
+                level.getOffsetY()
+        );
+    }
+
+    /**
+     * Retrieves the {@code DropShadow} associated with the specified {@code DepthLevel} added to delta.
+     * <p></p>
+     * Example 1: for a depth level equal to 3 and a delta equal to 2, the returned {@code DropShadow} effect is
+     * the effected associated to a depth level of 5.
+     * <p></p>
+     * Example 2: for a depth level equal to 5 and a delta equal to whatever integer, the returned {@code DropShadow} effect is
+     * the effected associated to a depth level of 5.
+     * @param level The desired {@code DepthLevel} between 1 and 5
+     * @param delta The number of levels to shift
+     * @return The final {@code DropShadow} effect}
+     * <p></p>
+     * {@link #nextLevel(DepthLevel)}
+     */
+    public static DropShadow shadowOf(DepthLevel level, int delta) {
+        DepthLevel endLevel = level;
+        for (int i = 0; i < delta; i++) {
+            endLevel = nextLevel(endLevel);
+        }
+        return shadowOf(endLevel);
+    }
+
+    /**
+     * From a starting {@code DepthLevel} retrieves the {@code DropShadow} effect associated to the next {@code DepthLevel}.
+     * @param startLevel The starting {@code DepthLevel}
+     * @return The {@code DropShadow} effect associated to the next {@code DepthLevel}
+     * @see DepthLevel
+     */
+    private static DepthLevel nextLevel(DepthLevel startLevel) {
+        return !(startLevel.equals(DepthLevel.LEVEL5)) ? startLevel.next() : DepthLevel.LEVEL5;
+    }
+}

+ 221 - 0
materialfx/src/main/java/it/paprojects/materialfx/effects/RippleGenerator.java

@@ -0,0 +1,221 @@
+package it.paprojects.materialfx.effects;
+
+import it.paprojects.materialfx.controls.MFXButton;
+import javafx.animation.*;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.css.*;
+import javafx.scene.Group;
+import javafx.scene.control.Control;
+import javafx.scene.effect.DropShadow;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Circle;
+import javafx.scene.shape.Rectangle;
+import javafx.util.Duration;
+
+import java.util.List;
+
+import static it.paprojects.materialfx.effects.MFXDepthManager.shadowOf;
+
+public class RippleGenerator extends Group {
+    private final String STYLE_CLASS = "rippleGenerator";
+    private static final StyleablePropertyFactory<RippleGenerator> FACTORY = new StyleablePropertyFactory<>(Group.getClassCssMetaData());
+
+    private final Control control;
+
+    private final Interpolator rippleInterpolator = Interpolator.SPLINE(0.0825, 0.3025, 0.0875, 0.9975);
+    //private final Interpolator rippleInterpolator = Interpolator.SPLINE(0.1, 0.50, 0.3, 0.85);
+    private final Interpolator easeIn = Interpolator.EASE_IN;
+    private final Interpolator easeOut = Interpolator.EASE_OUT;
+    private final Interpolator easeBoth = Interpolator.EASE_BOTH;
+    private  ObjectProperty<Color> rippleColor = new StyleableObjectProperty<>(Color.ROYALBLUE)
+    {
+        @Override public CssMetaData getCssMetaData() { return StyleableProperties.RIPPLE_COLOR; }
+        @Override public Object getBean() { return this; }
+        @Override public String getName() { return "ledColor"; }
+    };
+    private final DoubleProperty rippleRadius = new SimpleDoubleProperty(10.0);
+    private final ObjectProperty<Duration> inDuration = new SimpleObjectProperty<>(Duration.millis(700));
+    private final ObjectProperty<Duration> outDuration = new SimpleObjectProperty<>(inDuration.get().divide(2));
+
+    private double generatorCenterX = 100.0;
+    private double generatorCenterY = 100.0;
+
+    public RippleGenerator(Control control) {
+        this.control = control;
+        getStyleClass().add(STYLE_CLASS);
+        inDuration.addListener((observable, oldValue, newValue) -> {
+            if (!newValue.equals(oldValue)) {
+                outDuration.set(newValue.divide(2));
+            }
+        });
+        outDuration.addListener((observable, oldValue, newValue) -> {
+            if (!newValue.equals(oldValue)) {
+                inDuration.set(newValue.multiply(2));
+            }
+        });
+    }
+
+    public void createRipple() {
+        final Ripple ripple = new Ripple(generatorCenterX, generatorCenterY);
+        getChildren().add(ripple);
+
+        Rectangle fillRect = new Rectangle(control.getWidth(), control.getHeight());
+        fillRect.setFill(rippleColor.get());
+        fillRect.setOpacity(0);
+        getChildren().add(0, fillRect);
+
+        KeyValue keyValueIn = new KeyValue(fillRect.opacityProperty(), 0.3);
+        KeyValue keyValueOut = new KeyValue(fillRect.opacityProperty(), 0);
+        KeyFrame keyFrameIn = new KeyFrame(inDuration.get(), keyValueIn);
+        KeyFrame keyFrameOut = new KeyFrame(outDuration.get(), keyValueOut);
+        ripple.inAnimation.getKeyFrames().add(keyFrameIn);
+        ripple.outAnimation.getKeyFrames().add(keyFrameOut);
+
+        ripple.parallelTransition.setOnFinished(event -> getChildren().remove(ripple));
+        ripple.parallelTransition.play();
+
+    }
+
+    public void setGeneratorCenterX(double generatorCenterX) {
+        this.generatorCenterX = generatorCenterX;
+    }
+
+    public void setGeneratorCenterY(double generatorCenterY) {
+        this.generatorCenterY = generatorCenterY;
+    }
+
+    public Color getRippleColor() {
+        return rippleColor.get();
+    }
+
+    public final ObjectProperty<Color> rippleColorProperty() {
+        return rippleColor;
+    }
+
+    public void setRippleColor(Color rippleColor) {
+        this.rippleColor.set(rippleColor);
+    }
+
+    public double getRippleRadius() {
+        return rippleRadius.get();
+    }
+
+    public DoubleProperty rippleRadiusProperty() {
+        return rippleRadius;
+    }
+
+    public void setRippleRadius(double rippleRadius) {
+        this.rippleRadius.set(rippleRadius);
+    }
+
+    public Duration getInDuration() {
+        return inDuration.get();
+    }
+
+    public ObjectProperty<Duration> inDurationProperty() {
+        return inDuration;
+    }
+
+    public void setInDuration(Duration inDuration) {
+        this.inDuration.set(inDuration);
+    }
+
+    public Duration getOutDuration() {
+        return outDuration.get();
+    }
+
+    public ObjectProperty<Duration> outDurationProperty() {
+        return outDuration;
+    }
+
+    public void setOutDuration(Duration outDuration) {
+        this.outDuration.set(outDuration);
+    }
+
+    private class Ripple extends Circle {
+        private final int shadowDelta = 2;
+
+        private final Timeline inAnimation = new Timeline();
+        private final Timeline outAnimation = new Timeline();
+        private final Timeline shadowAnimation = new Timeline();
+        private final SequentialTransition sequentialTransition = new SequentialTransition();
+        private final ParallelTransition parallelTransition = new ParallelTransition();
+
+        private Ripple(double centerX, double centerY) {
+            super(centerX, centerY, 0, Color.TRANSPARENT);
+            setFill(rippleColor.get());
+            Rectangle rectangle = new Rectangle(control.getWidth(), control.getHeight());
+            setClip(rectangle);
+            buildAnimation();
+        }
+
+        private void buildAnimation() {
+            DropShadow buttonShadow = (DropShadow) ((MFXButton) control.getSkin().getSkinnable()).getEffect();
+            DepthLevel depthLevel = ((MFXButton) control).getDepthLevel();
+            DropShadow startShadow = shadowOf(depthLevel);
+            DropShadow endShadow = shadowOf(depthLevel, shadowDelta);
+
+            KeyValue keyValue1 = new KeyValue(radiusProperty(), rippleRadius.get());
+            KeyValue keyValue2 = new KeyValue(opacityProperty(), 1.0);
+            KeyFrame keyFrame1 = new KeyFrame(inDuration.get(), keyValue1);
+            KeyFrame keyFrame2 = new KeyFrame(inDuration.get(), keyValue2);
+            inAnimation.getKeyFrames().addAll(keyFrame1, keyFrame2);
+
+            KeyValue keyValue3 = new KeyValue(radiusProperty(), rippleRadius.get() * 2);
+            KeyValue keyValue4 = new KeyValue(opacityProperty(), 0.0);
+            KeyFrame keyFrame3 = new KeyFrame(outDuration.get(), keyValue3);
+            KeyFrame keyFrame4 = new KeyFrame(outDuration.get(), keyValue4);
+            outAnimation.getKeyFrames().addAll(keyFrame3, keyFrame4);
+
+            // Button shadow
+            // Spread
+            KeyValue keyValue5 = new KeyValue(buttonShadow.spreadProperty(), endShadow.getSpread(), easeBoth);
+            KeyValue keyValue6 = new KeyValue(buttonShadow.spreadProperty(), startShadow.getSpread(), easeBoth);
+            //Radius
+            KeyValue keyValue7 = new KeyValue(buttonShadow.radiusProperty(), endShadow.getRadius(), easeBoth);
+            KeyValue keyValue8 = new KeyValue(buttonShadow.radiusProperty(), startShadow.getRadius(), easeBoth);
+            // Offsets
+            KeyValue keyValue9 = new KeyValue(buttonShadow.offsetXProperty(), endShadow.getOffsetX(), easeBoth);
+            KeyValue keyValue10 = new KeyValue(buttonShadow.offsetXProperty(), startShadow.getOffsetX(), easeBoth);
+            KeyValue keyValue11 = new KeyValue(buttonShadow.offsetYProperty(), endShadow.getOffsetY(), easeBoth);
+            KeyValue keyValue12 = new KeyValue(buttonShadow.offsetYProperty(), startShadow.getOffsetY(), easeBoth);
+            KeyFrame keyFrame5 = new KeyFrame(Duration.ZERO, keyValue5, keyValue7, keyValue9, keyValue11);
+            KeyFrame keyFrame6 = new KeyFrame(inDuration.get(), keyValue6, keyValue8, keyValue10, keyValue12);
+            shadowAnimation.getKeyFrames().addAll(keyFrame5, keyFrame6);
+
+            sequentialTransition.getChildren().addAll(inAnimation, outAnimation);
+            parallelTransition.setInterpolator(rippleInterpolator);
+            parallelTransition.getChildren().addAll(shadowAnimation, sequentialTransition);
+        }
+    }
+
+    //================================================================================
+    // Stylesheet properties
+    //================================================================================
+    private static class StyleableProperties {
+        private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+        private static final CssMetaData<RippleGenerator, Color> RIPPLE_COLOR =
+                FACTORY.createColorCssMetaData(
+                        "-mfx-ripple-color",
+                        rippleGenerator -> (StyleableProperty<Color>) rippleGenerator.rippleColorProperty()
+                );
+
+        static {
+            cssMetaDataList = List.of(RIPPLE_COLOR);
+        }
+
+    }
+
+    public List<CssMetaData<? extends Styleable, ?>> getGroupCssMetaDataList() {
+        return RippleGenerator.StyleableProperties.cssMetaDataList;
+    }
+
+    @Override
+    public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
+        return this.getGroupCssMetaDataList();
+    }
+}

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

@@ -0,0 +1,42 @@
+package it.paprojects.materialfx.skins;
+
+import it.paprojects.materialfx.controls.MFXButton;
+import it.paprojects.materialfx.effects.DepthLevel;
+import it.paprojects.materialfx.effects.MFXDepthManager;
+import javafx.scene.control.skin.ButtonSkin;
+
+/**
+ *  This is the implementation of the {@code Skin} associated with every {@code MFXButton}.
+ */
+public class MFXButtonSkin extends ButtonSkin {
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXButtonSkin(MFXButton button, DepthLevel depthLevel) {
+        super(button);
+
+
+        button.buttonTypeProperty().addListener(
+                ((observable, oldValue, newValue) -> updateButtonType(button, depthLevel)));
+
+        updateButtonType(button, depthLevel);
+    }
+
+    //================================================================================
+    // Private Methods
+    //================================================================================
+    private void updateButtonType(MFXButton button, DepthLevel depthLevel) {
+        switch (button.getButtonType()) {
+            case RAISED: {
+                getSkinnable().setEffect(MFXDepthManager.shadowOf(depthLevel));
+                getSkinnable().setPickOnBounds(false);
+                break;
+            }
+            case FLAT: {
+                getSkinnable().setEffect(null);
+                getSkinnable().setPickOnBounds(true);
+                break;
+            }
+        }
+    }
+}

+ 6 - 0
materialfx/src/main/java/it/paprojects/materialfx/utils/ButtonType.java

@@ -0,0 +1,6 @@
+package it.paprojects.materialfx.utils;
+
+public enum ButtonType {
+    FLAT,
+    RAISED
+}

+ 12 - 0
materialfx/src/main/java/module-info.java

@@ -0,0 +1,12 @@
+module MaterialFX.materialfx.main {
+    requires javafx.base;
+    requires javafx.controls;
+    requires javafx.graphics;
+    requires java.desktop;
+
+    exports it.paprojects.materialfx.controls;
+    exports it.paprojects.materialfx.effects;
+    exports it.paprojects.materialfx.skins;
+    exports it.paprojects.materialfx.utils;
+    exports it.paprojects.materialfx;
+}

+ 12 - 0
materialfx/src/main/resources/it/paprojects/materialfx/css/mfx-button.css

@@ -0,0 +1,12 @@
+.mfx-button:armed,
+.mfx-button:hover,
+.mfx-button:focused,
+.mfx-button {
+    -fx-background-radius: 3px;
+    -fx-background-insets: 0px;
+}
+
+.mfx-button,
+.mfx-button:armed {
+    -fx-background-color: white;
+}

+ 4 - 0
settings.gradle

@@ -0,0 +1,4 @@
+rootProject.name = 'MaterialFX'
+include 'materialfx'
+include 'demo'
+