├── .gitignore ├── GradientPicker.png ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── license.txt ├── settings.gradle └── src └── main ├── java └── eu │ └── hansolo │ └── fx │ └── gradientpicker │ ├── Demo.java │ ├── GradientPicker.java │ ├── Handle.java │ ├── HandleType.java │ ├── event │ ├── GradientEvent.java │ ├── GradientEventType.java │ ├── GradientObserver.java │ ├── HandleEvent.java │ ├── HandleEventType.java │ └── HandleObserver.java │ └── tool │ ├── GradientLookup.java │ ├── Helper.java │ └── NumberTextField.java └── resources └── eu └── hansolo └── fx └── gradientpicker ├── gradientpicker.css └── opacitypattern.png /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.gradle 3 | .DS_Store 4 | /build 5 | /out 6 | -------------------------------------------------------------------------------- /GradientPicker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanSolo/gradientpicker/a664093558ef225c499ef9a9d8a11e8026b87889/GradientPicker.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## GradientPicker 2 | A JavaFX control to create color gradients. 3 | You can get the stops of the gradient as 4 | - JavaFX stops 5 | - JavaFX css 6 | 7 | ## Overview 8 | ![Overview](https://raw.githubusercontent.com/HanSolo/gradientpicker/master/GradientPicker.png) 9 | 10 | [![Demo](https://img.youtube.com/vi/XOjihYgXMpA/0.jpg)](https://www.youtube.com/watch?v=XOjihYgXMpA) -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'eu.hansolo.fx' 6 | version '1.0-SNAPSHOT' 7 | 8 | sourceCompatibility = 1.8 9 | 10 | repositories { 11 | mavenCentral() 12 | } 13 | 14 | dependencies { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanSolo/gradientpicker/a664093558ef225c499ef9a9d8a11e8026b87889/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-bin.zip 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'gradientpicker' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/fx/gradientpicker/Demo.java: -------------------------------------------------------------------------------- 1 | package eu.hansolo.fx.gradientpicker; 2 | 3 | import javafx.application.Application; 4 | import javafx.scene.Scene; 5 | import javafx.scene.layout.StackPane; 6 | import javafx.stage.Stage; 7 | 8 | 9 | public class Demo extends Application { 10 | private GradientPicker gradientPicker; 11 | 12 | @Override public void init() { 13 | gradientPicker = new GradientPicker(); 14 | } 15 | 16 | @Override public void start(Stage stage) { 17 | StackPane pane = new StackPane(gradientPicker); 18 | //pane.setPadding(new Insets(10)); 19 | 20 | Scene scene = new Scene(pane); 21 | 22 | stage.setTitle("GradientPicker"); 23 | stage.setScene(scene); 24 | stage.show(); 25 | } 26 | 27 | @Override public void stop() { 28 | System.exit(0); 29 | } 30 | 31 | public static void main(String[] args) { 32 | launch(args); 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/fx/gradientpicker/GradientPicker.java: -------------------------------------------------------------------------------- 1 | package eu.hansolo.fx.gradientpicker; 2 | 3 | import eu.hansolo.fx.gradientpicker.event.GradientEvent; 4 | import eu.hansolo.fx.gradientpicker.event.GradientEventType; 5 | import eu.hansolo.fx.gradientpicker.event.GradientObserver; 6 | import eu.hansolo.fx.gradientpicker.tool.GradientLookup; 7 | import eu.hansolo.fx.gradientpicker.tool.Helper; 8 | import eu.hansolo.fx.gradientpicker.tool.NumberTextField; 9 | import javafx.beans.DefaultProperty; 10 | import javafx.beans.binding.Bindings; 11 | import javafx.beans.binding.BooleanBinding; 12 | import javafx.collections.FXCollections; 13 | import javafx.collections.ListChangeListener; 14 | import javafx.collections.ObservableList; 15 | import javafx.event.EventHandler; 16 | import javafx.event.EventType; 17 | import javafx.geometry.Insets; 18 | import javafx.geometry.Pos; 19 | import javafx.scene.Node; 20 | import javafx.scene.Scene; 21 | import javafx.scene.control.ColorPicker; 22 | import javafx.scene.control.Label; 23 | import javafx.scene.control.Slider; 24 | import javafx.scene.control.Tooltip; 25 | import javafx.scene.effect.BlurType; 26 | import javafx.scene.effect.DropShadow; 27 | import javafx.scene.effect.InnerShadow; 28 | import javafx.scene.image.Image; 29 | import javafx.scene.input.MouseButton; 30 | import javafx.scene.input.MouseEvent; 31 | import javafx.scene.layout.HBox; 32 | import javafx.scene.layout.Pane; 33 | import javafx.scene.layout.Region; 34 | import javafx.scene.layout.StackPane; 35 | import javafx.scene.paint.Color; 36 | import javafx.scene.paint.CycleMethod; 37 | import javafx.scene.paint.ImagePattern; 38 | import javafx.scene.paint.LinearGradient; 39 | import javafx.scene.paint.Stop; 40 | import javafx.scene.shape.Rectangle; 41 | import javafx.scene.shape.Shape; 42 | import javafx.stage.Popup; 43 | import javafx.util.StringConverter; 44 | 45 | import java.util.Comparator; 46 | import java.util.List; 47 | import java.util.Locale; 48 | import java.util.concurrent.CopyOnWriteArrayList; 49 | import java.util.stream.Collectors; 50 | 51 | import static eu.hansolo.fx.gradientpicker.tool.Helper.clamp; 52 | 53 | 54 | /** 55 | * User: hansolo 56 | * Date: 13.07.18 57 | * Time: 12:29 58 | */ 59 | @DefaultProperty("children") 60 | public class GradientPicker extends Region { 61 | private static final double PREFERRED_WIDTH = 200; 62 | private static final double PREFERRED_HEIGHT = 70; 63 | private static final double MINIMUM_WIDTH = 50; 64 | private static final double MINIMUM_HEIGHT = 70; 65 | private static final double MAXIMUM_WIDTH = 1024; 66 | private static final double MAXIMUM_HEIGHT = 70; 67 | private static final double PADDING_LEFT = 13; 68 | private static final double PADDING_RIGHT = 2 * PADDING_LEFT; 69 | private static final double BOX_HEIGHT = 20; 70 | private static final double HANDLE_CENTER = Handle.HANDLE_SIZE * 0.5; 71 | private static final double DRAG_Y_OFFSET = Handle.HANDLE_SIZE; 72 | private static final double HANDLE_HEIGHT = Handle.HANDLE_SIZE; 73 | private final GradientEvent GRADIENT_CHANGED = new GradientEvent(GradientPicker.this, GradientEventType.GRADIENT_CHANGED); 74 | private BooleanBinding showing; 75 | private double width; 76 | private double height; 77 | private LinearGradient gradient; 78 | private Rectangle gradientBackground; 79 | private Rectangle gradientBox; 80 | private Pane pane; 81 | private ObservableList handles; 82 | private double dragX; 83 | private double dragY; 84 | private EventHandler mouseHandler; 85 | private Handle selectedHandle; 86 | private GradientLookup lookup; 87 | private ColorPicker colorPicker; 88 | private Popup alphaPopup; 89 | private StackPane alphaPopupPane; 90 | private Rectangle alphaPopupBounds; 91 | private Slider alphaSlider; 92 | private NumberTextField alphaTextField; 93 | private HBox alphaBox; 94 | private Popup positionPopup; 95 | private StackPane positionPopupPane; 96 | private Rectangle positionPopupBounds; 97 | private NumberTextField positionTextField; 98 | private HBox positionBox; 99 | private List observers; 100 | 101 | 102 | // ******************** Constructors ************************************** 103 | public GradientPicker() { 104 | getStylesheets().add(GradientPicker.class.getResource("gradientpicker.css").toExternalForm()); 105 | handles = FXCollections.observableArrayList(); 106 | mouseHandler = e -> { 107 | final EventType TYPE = e.getEventType(); 108 | final Handle handle = (Handle) e.getSource(); 109 | final HandleType handleType = handle.getType(); 110 | if (TYPE.equals(MouseEvent.MOUSE_PRESSED)) { 111 | focusHandle(handle); 112 | selectedHandle = handle; 113 | if (e.isAltDown() || e.isSecondaryButtonDown()) { 114 | if (HandleType.COLOR_HANDLE == handleType) { 115 | colorPicker.setValue(handle.getFill()); 116 | colorPicker.show(); 117 | } else { 118 | alphaSlider.setValue(handle.getAlpha()); 119 | showAlphaPopup(getScene(), e); 120 | } 121 | } else if (e.isControlDown()) { 122 | if (HandleType.COLOR_HANDLE == handleType) { showPositionPopup(getScene(), e); } 123 | } 124 | dragX = handle.getLayoutX() - e.getScreenX(); 125 | dragY = handle.getLayoutY() - e.getScreenY(); 126 | } else if (TYPE.equals(MouseEvent.MOUSE_DRAGGED)) { 127 | handle.setLayoutX(Helper.clamp(gradientBox.getX() - HANDLE_CENTER, gradientBox.getX() + gradientBox.getWidth() - HANDLE_CENTER, e.getScreenX() + dragX)); 128 | handle.setLayoutY(Helper.clamp(gradientBox.getY() + gradientBox.getHeight(), gradientBox.getY() + gradientBox.getHeight() + DRAG_Y_OFFSET, e.getScreenY() + dragY)); 129 | handle.setFraction((handle.getLayoutX() - gradientBox.getX() + HANDLE_CENTER) / gradientBox.getWidth()); 130 | updateGradient(); 131 | } else if (TYPE.equals(MouseEvent.MOUSE_RELEASED)) { 132 | if (handle.getLayoutY() >= gradientBox.getY() + gradientBox.getHeight() + DRAG_Y_OFFSET) { 133 | if (null != handle.getLinkedHandle()) { 134 | handle.getLinkedHandle().layoutXProperty().unbindBidirectional(handle.layoutXProperty()); 135 | handles.remove(handle.getLinkedHandle()); 136 | } 137 | handles.remove(handle); 138 | } else { 139 | handle.setLayoutY(gradientBox.getY() + gradientBox.getHeight()); 140 | } 141 | } 142 | }; 143 | lookup = new GradientLookup(); 144 | observers = new CopyOnWriteArrayList<>(); 145 | initGraphics(); 146 | initPopups(); 147 | registerListeners(); 148 | } 149 | 150 | 151 | // ******************** Initialization ************************************ 152 | private void initGraphics() { 153 | if (Double.compare(getPrefWidth(), 0.0) <= 0 || Double.compare(getPrefHeight(), 0.0) <= 0 || Double.compare(getWidth(), 0.0) <= 0 || 154 | Double.compare(getHeight(), 0.0) <= 0) { 155 | if (getPrefWidth() > 0 && getPrefHeight() > 0) { 156 | setPrefSize(getPrefWidth(), getPrefHeight()); 157 | } else { 158 | setPrefSize(PREFERRED_WIDTH, PREFERRED_HEIGHT); 159 | } 160 | } 161 | 162 | getStyleClass().add("gradient-picker"); 163 | 164 | gradient = new LinearGradient(0, 0, 1, 0, true, CycleMethod.NO_CYCLE); 165 | 166 | gradientBackground = new Rectangle(10, 10, 180, 20); 167 | gradientBackground.setFill(new ImagePattern(new Image(getClass().getResourceAsStream("opacitypattern.png")), 0, 5, 20, 20, false)); 168 | 169 | gradientBox = new Rectangle(10, 10, 180, 20); 170 | gradientBox.setFill(gradient); 171 | gradientBox.setStroke(Color.web("#353535")); 172 | 173 | Tooltip tooltip = new Tooltip("Double click to add a new stop"); 174 | Tooltip.install(gradientBox, tooltip); 175 | 176 | colorPicker = new ColorPicker(); 177 | colorPicker.getStyleClass().add("button"); 178 | colorPicker.setTranslateY(65); 179 | colorPicker.setVisible(false); 180 | colorPicker.setManaged(false); 181 | 182 | pane = new Pane(gradientBackground, gradientBox, colorPicker); 183 | 184 | getChildren().setAll(pane); 185 | } 186 | 187 | private void initPopups() { 188 | DropShadow shadow = new DropShadow(); 189 | shadow.setBlurType(BlurType.GAUSSIAN); 190 | shadow.setRadius(4); 191 | shadow.setOffsetY(2); 192 | 193 | /* 194 | colorPicker = new ColorPicker(); 195 | colorPicker.getStyleClass().add("button"); 196 | 197 | colorPickerPopupPane = new StackPane(); 198 | colorPickerPopupPane.setPadding(new Insets(10)); 199 | colorPickerPopupBounds = new Rectangle(225, 50); 200 | Shape colorPickerPopupShape = createPopupShape(colorPickerPopupBounds.getWidth(), colorPickerPopupBounds.getHeight()); 201 | colorPickerPopupPane.getChildren().setAll(colorPickerPopupShape, colorPicker); 202 | colorPickerPopupPane.setEffect(shadow); 203 | 204 | colorPickerPopup = new Popup(); 205 | colorPickerPopup.setHideOnEscape(true); 206 | colorPickerPopup.setAutoHide(true); 207 | colorPickerPopup.getContent().add(colorPicker); 208 | */ 209 | 210 | alphaSlider = new Slider(); 211 | alphaSlider.setMin(0.0); 212 | alphaSlider.setMax(1.0); 213 | alphaSlider.setValue(1.0); 214 | alphaSlider.setMinWidth(150); 215 | alphaSlider.setFocusTraversable(false); 216 | 217 | alphaTextField = new NumberTextField(); 218 | alphaTextField.setPrefWidth(55); 219 | alphaTextField.setDecimals(3); 220 | alphaTextField.setText("1.0"); 221 | alphaTextField.setFocusTraversable(false); 222 | 223 | alphaBox = new HBox(); 224 | alphaBox.setSpacing(10); 225 | alphaBox.setAlignment(Pos.CENTER); 226 | alphaBox.getChildren().addAll(alphaSlider, alphaTextField); 227 | 228 | Label positionLabel = new Label("Position:"); 229 | positionLabel.setTextFill(Color.WHITE); 230 | 231 | positionTextField = new NumberTextField(); 232 | positionTextField.setDecimals(3); 233 | positionTextField.setPrefWidth(55); 234 | positionTextField.setFocusTraversable(false); 235 | 236 | positionBox = new HBox(); 237 | positionBox.setSpacing(10); 238 | positionBox.setAlignment(Pos.CENTER); 239 | positionBox.getChildren().addAll(positionLabel, positionTextField); 240 | 241 | alphaPopupPane = new StackPane(); 242 | alphaPopupPane.setPadding(new Insets(10)); 243 | alphaPopupBounds = new Rectangle(225, 50); 244 | Shape alphaPopupShape = createPopupShape(alphaPopupBounds.getWidth(), alphaPopupBounds.getHeight()); 245 | alphaPopupPane.getChildren().setAll(alphaPopupShape, alphaBox); 246 | alphaPopupPane.setEffect(shadow); 247 | alphaPopup = new Popup(); 248 | alphaPopup.setHideOnEscape(true); 249 | alphaPopup.setAutoHide(true); 250 | alphaPopup.getContent().add(alphaPopupPane); 251 | 252 | positionPopupPane = new StackPane(); 253 | positionPopupPane.setPadding(new Insets(10)); 254 | positionPopupBounds = new Rectangle(135, 50); 255 | Shape positionPopupShape = createPopupShape(positionPopupBounds.getWidth(), positionPopupBounds.getHeight()); 256 | positionPopupPane.getChildren().setAll(positionPopupShape, positionBox); 257 | positionPopupPane.setEffect(shadow); 258 | positionPopup = new Popup(); 259 | positionPopup.setHideOnEscape(true); 260 | positionPopup.setAutoHide(true); 261 | positionPopup.getContent().add(positionPopupPane); 262 | } 263 | 264 | private void registerListeners() { 265 | widthProperty().addListener(o -> resize()); 266 | heightProperty().addListener(o -> resize()); 267 | handles.addListener((ListChangeListener) change -> { 268 | while (change.next()) { 269 | if (change.wasAdded()) { 270 | change.getAddedSubList().forEach(handle -> { 271 | if (HandleType.COLOR_HANDLE == handle.getType()) { 272 | handle.addEventFilter(MouseEvent.MOUSE_DRAGGED, mouseHandler); 273 | handle.addEventFilter(MouseEvent.MOUSE_RELEASED, mouseHandler); 274 | } 275 | handle.addEventFilter(MouseEvent.MOUSE_PRESSED, mouseHandler); 276 | }); 277 | updateHandles(); 278 | updateGradient(); 279 | } 280 | if (change.wasRemoved()) { 281 | change.getRemoved().forEach(handle -> { 282 | if (HandleType.COLOR_HANDLE == handle.getType()) { 283 | handle.removeEventFilter(MouseEvent.MOUSE_DRAGGED, mouseHandler); 284 | handle.removeEventFilter(MouseEvent.MOUSE_RELEASED, mouseHandler); 285 | } 286 | handle.removeEventFilter(MouseEvent.MOUSE_PRESSED, mouseHandler); 287 | }); 288 | updateHandles(); 289 | updateGradient(); 290 | } 291 | if (change.wasReplaced()) { 292 | updateHandles(); 293 | updateGradient(); 294 | } 295 | } 296 | }); 297 | gradientBox.setOnMousePressed(e -> { 298 | if (e.getButton().equals(MouseButton.PRIMARY)) { 299 | if (e.getClickCount() == 2) { addHandleAt(e); } 300 | } 301 | }); 302 | focusedProperty().addListener(o -> focusHandle(null)); 303 | 304 | colorPicker.setOnAction(e -> { 305 | selectedHandle.setFill(Color.color(colorPicker.getValue().getRed(), colorPicker.getValue().getGreen(), colorPicker.getValue().getBlue(), selectedHandle.getLinkedHandle().getAlpha())); 306 | colorPicker.hide(); 307 | updateGradient(); 308 | updateHandles(); 309 | }); 310 | 311 | positionTextField.setOnAction(actionEvent -> { 312 | if (!positionTextField.getText().isEmpty() && null != selectedHandle) { 313 | selectedHandle.setFraction(Double.parseDouble(positionTextField.getText())); 314 | updateHandles(); 315 | updateGradient(); 316 | positionPopup.hide(); 317 | } 318 | }); 319 | alphaSlider.valueProperty().addListener(o -> { 320 | if (null != selectedHandle) { 321 | double alpha = alphaSlider.getValue(); 322 | selectedHandle.setAlpha(alpha); 323 | selectedHandle.setFill(Color.rgb(0, 0, 0, alpha)); 324 | updateGradient(); 325 | } 326 | }); 327 | alphaSlider.setOnMouseReleased(mouseEvent -> alphaPopup.hide()); 328 | alphaTextField.setOnAction(actionEvent -> alphaPopup.hide()); 329 | alphaTextField.textProperty().bindBidirectional(alphaSlider.valueProperty(), new StringConverter() { 330 | @Override public String toString(Number number) { return String.format(Locale.US, "%.3f", number); } 331 | @Override public Number fromString(String text) { return (null == text || text.isEmpty()) ? 0.0 : Double.parseDouble(text); } 332 | }); 333 | 334 | if (null != getScene()) { 335 | setupBinding(); 336 | } else { 337 | sceneProperty().addListener((o1, ov1, nv1) -> { 338 | if (null == nv1) { return; } 339 | if (null != getScene().getWindow()) { 340 | setupBinding(); 341 | } else { 342 | sceneProperty().get().windowProperty().addListener((o2, ov2, nv2) -> { 343 | if (null == nv2) { return; } 344 | setupBinding(); 345 | }); 346 | } 347 | }); 348 | } 349 | } 350 | 351 | 352 | // ******************** Methods ******************************************* 353 | @Override public void layoutChildren() { 354 | super.layoutChildren(); 355 | } 356 | 357 | @Override protected double computeMinWidth(final double HEIGHT) { return MINIMUM_WIDTH; } 358 | @Override protected double computeMinHeight(final double WIDTH) { return MINIMUM_HEIGHT; } 359 | @Override protected double computePrefWidth(final double HEIGHT) { return super.computePrefWidth(HEIGHT); } 360 | @Override protected double computePrefHeight(final double WIDTH) { return super.computePrefHeight(WIDTH); } 361 | @Override protected double computeMaxWidth(final double HEIGHT) { return MAXIMUM_WIDTH; } 362 | @Override protected double computeMaxHeight(final double WIDTH) { return MAXIMUM_HEIGHT; } 363 | 364 | @Override public ObservableList getChildren() { return super.getChildren(); } 365 | 366 | public List getStops() { return gradient.getStops(); } 367 | public String getStopsAsString(final boolean CSS) { 368 | StringBuilder gradientString = new StringBuilder(); 369 | for (Stop stop : gradient.getStops()) { 370 | if (CSS) { 371 | gradientString.append("rgba(") 372 | .append((int) (stop.getColor().getRed() * 255)) 373 | .append(", ") 374 | .append((int) (stop.getColor().getGreen() * 255)) 375 | .append(", ") 376 | .append((int) (stop.getColor().getBlue() * 255)) 377 | .append(", ") 378 | .append(String.format(Locale.US, "%.3f", stop.getColor().getOpacity())) 379 | .append(") "); 380 | gradientString.append(Math.floor(stop.getOffset() * 100)).append("%"); 381 | } else { 382 | gradientString.append("new Stop(") 383 | .append(String.format(Locale.US, "%.3f", stop.getOffset())) 384 | .append(", Color.rgb(") 385 | .append((int) (stop.getColor().getRed() * 255)) 386 | .append(", ") 387 | .append((int) (stop.getColor().getGreen() * 255)) 388 | .append(", ") 389 | .append((int) (stop.getColor().getBlue() * 255)) 390 | .append(", ") 391 | .append(String.format(Locale.US, "%.2f", stop.getColor().getOpacity())) 392 | .append("))"); 393 | } 394 | if (stop.equals(gradient.getStops().get(gradient.getStops().size() - 1))) { 395 | gradientString.append("\n"); 396 | } else { 397 | gradientString.append(",\n"); 398 | } 399 | } 400 | return gradientString.toString(); 401 | } 402 | 403 | public boolean isShowing() { return null == showing ? false : showing.get(); } 404 | 405 | private void setupBinding() { 406 | showing = Bindings.createBooleanBinding(() -> { 407 | if (getScene() != null && getScene().getWindow() != null) { 408 | return getScene().getWindow().isShowing(); 409 | } else { 410 | return false; 411 | } 412 | }, sceneProperty(), getScene().windowProperty(), getScene().getWindow().showingProperty()); 413 | 414 | showing.addListener(o -> { 415 | if (showing.get()) { 416 | addHandle(new Handle(HandleType.COLOR_HANDLE, 0.0, Color.WHITE)); 417 | addHandle(new Handle(HandleType.COLOR_HANDLE, 1.0, Color.BLACK)); 418 | } 419 | }); 420 | } 421 | 422 | private void addHandleAt(final MouseEvent MOUSE_EVENT) { 423 | double fraction = (MOUSE_EVENT.getX() - gradientBox.getX()) / gradientBox.getWidth(); 424 | Color pickedColor = calculateColor(fraction); 425 | Handle colorHandle = new Handle(HandleType.COLOR_HANDLE, fraction, pickedColor); 426 | addHandle(colorHandle); 427 | } 428 | 429 | private void addHandle(final Handle HANDLE) { 430 | double fraction = HANDLE.getFraction(); 431 | double alpha = calculateOpacity(fraction); 432 | 433 | HANDLE.setLayoutX((fraction* gradientBox.getWidth())); 434 | HANDLE.setLayoutY(gradientBox.getY() + gradientBox.getHeight()); 435 | 436 | Handle alphaHandle = new Handle(HandleType.ALPHA_HANDLE, fraction, alpha); 437 | alphaHandle.setLayoutX((fraction * gradientBox.getWidth())); 438 | alphaHandle.setLayoutY(gradientBox.getY() - HANDLE_HEIGHT); 439 | 440 | HANDLE.setLinkedHandle(alphaHandle); 441 | 442 | focusHandle(HANDLE); 443 | handles.addAll(alphaHandle, HANDLE); 444 | } 445 | 446 | private void updateHandles() { 447 | pane.getChildren().setAll(gradientBackground, gradientBox, colorPicker); 448 | for (Handle handle : handles) { 449 | if (HandleType.COLOR_HANDLE == handle.getType()) { 450 | handle.setLayoutX(gradientBox.getX() + (handle.getFraction() * gradientBox.getWidth()) - HANDLE_CENTER); 451 | handle.setLayoutY(gradientBox.getY() + gradientBox.getHeight()); 452 | } else { 453 | handle.setLayoutY(gradientBox.getY() - HANDLE_HEIGHT); 454 | } 455 | 456 | pane.getChildren().addAll(handle); 457 | } 458 | } 459 | 460 | private void updateGradient() { 461 | List stops = handles.stream() 462 | .sorted(Comparator.naturalOrder()) 463 | .filter(handle -> HandleType.COLOR_HANDLE == handle.getType()) 464 | .map(handle -> new Stop(handle.getFraction(), Color.color(handle.getFill().getRed(), handle.getFill().getGreen(), handle.getFill().getBlue(), handle.getLinkedHandle().getAlpha()))) 465 | .collect(Collectors.toList()); 466 | gradient = new LinearGradient(0.0, 0.0, 1.0, 0.0, true, CycleMethod.NO_CYCLE, stops); 467 | gradientBox.setFill(gradient); 468 | fireGradientEvent(GRADIENT_CHANGED); 469 | } 470 | 471 | private Color calculateColor(final double FRACTION) { 472 | lookup.setStops(gradient.getStops()); 473 | return lookup.getColorAt(FRACTION); 474 | } 475 | 476 | private double calculateOpacity(final double FRACTION) { 477 | FXCollections.sort(handles); 478 | Handle lowerBound = null; 479 | Handle upperBound = null; 480 | for (Handle handle : handles) { 481 | if (FRACTION > handle.getFraction()) { 482 | lowerBound = handle; 483 | } else if (FRACTION < handle.getFraction()) { 484 | upperBound = handle; 485 | break; 486 | } 487 | } 488 | double alpha = 1.0; 489 | if (null != lowerBound && null != upperBound) { 490 | double deltaFraction = upperBound.getFraction() - lowerBound.getFraction(); 491 | //double deltaOpacity = Math.abs(upperBound.getOpacityHandle().getColorOpacity() - lowerBound.getOpacityHandle().getColorOpacity()); 492 | //double fractionFactor = 1.0 / deltaFraction * (FRACTION - lowerBound.getFraction()); 493 | //alpha = fractionFactor * deltaOpacity + lowerBound.getOpacityHandle().getColorOpacity(); 494 | alpha = 1.0; 495 | } 496 | return alpha; 497 | } 498 | 499 | private void focusHandle(final Handle HANDLE) { 500 | handles.forEach(handle -> handle.setFocus(false)); 501 | if (null == HANDLE) return; 502 | HANDLE.setFocus(true); 503 | } 504 | 505 | private Shape createPopupShape(final double WIDTH, final double HEIGHT) { 506 | Rectangle shape = new Rectangle(WIDTH, HEIGHT); 507 | shape.setArcWidth(10); 508 | shape.setArcHeight(10); 509 | 510 | LinearGradient gradient = 511 | new LinearGradient(0, shape.getLayoutBounds().getMinY(), 0, shape.getLayoutBounds().getMinY() + shape.getLayoutBounds().getHeight(), false, CycleMethod.NO_CYCLE, 512 | new Stop(0.0, Color.rgb(48, 48, 48, 0.8)), 513 | new Stop(1.0, Color.rgb(33, 33, 33, 0.8))); 514 | shape.setFill(gradient); 515 | 516 | InnerShadow innerShadow = new InnerShadow(); 517 | innerShadow.setRadius(4); 518 | innerShadow.setBlurType(BlurType.GAUSSIAN); 519 | innerShadow.setColor(Color.rgb(255, 255, 255, 0.6)); 520 | innerShadow.setOffsetX(0); 521 | innerShadow.setOffsetY(0); 522 | 523 | shape.setEffect(innerShadow); 524 | 525 | return shape; 526 | } 527 | 528 | private void showAlphaPopup(final Scene SCENE, final MouseEvent EVENT) { 529 | if (null == SCENE) return; 530 | final double popupX = EVENT.getScreenX() - HANDLE_CENTER - alphaPopupBounds.getWidth() * 0.5; 531 | final double popupY = EVENT.getScreenY() - alphaPopupBounds.getHeight() * 1.7; 532 | alphaPopup.show(SCENE.getWindow(), popupX, popupY); 533 | } 534 | 535 | private void showPositionPopup(final Scene SCENE, final MouseEvent EVENT) { 536 | if (null == SCENE) return; 537 | final double popupX = EVENT.getScreenX() - positionPopupBounds.getWidth() * 0.5; 538 | final double popupY = EVENT.getScreenY() + 10; 539 | positionTextField.setText(String.format(Locale.US, "%.3f", selectedHandle.getFraction())); 540 | positionPopup.show(SCENE.getWindow(), popupX, popupY); 541 | } 542 | 543 | 544 | // ******************** EventHandling ************************************* 545 | public void addGradientObserver(final GradientObserver OBSERVER) { if (!observers.contains(OBSERVER)) { observers.add(OBSERVER); } } 546 | public void removeGradientObserver(final GradientObserver OBSERVER) { if (observers.contains(OBSERVER)) { observers.remove(OBSERVER); } } 547 | 548 | private void fireGradientEvent(final GradientEvent EVT) { for (GradientObserver observer : observers) { observer.onGradientChanged(EVT); } } 549 | 550 | 551 | // ******************** Resizing ****************************************** 552 | private void resize() { 553 | width = getWidth() - getInsets().getLeft() - getInsets().getRight(); 554 | height = getHeight() - getInsets().getTop() - getInsets().getBottom(); 555 | 556 | if (width > 0 && height > 0) { 557 | pane.setMaxSize(width, height); 558 | pane.setPrefSize(width, height); 559 | pane.relocate((getWidth() - width) * 0.5, (getHeight() - height) * 0.5); 560 | 561 | gradientBackground.setWidth(width - PADDING_RIGHT); 562 | gradientBackground.setHeight(BOX_HEIGHT); 563 | gradientBackground.setX(PADDING_LEFT); 564 | gradientBackground.setY((height - BOX_HEIGHT) * 0.5); 565 | 566 | gradientBox.setWidth(width - PADDING_RIGHT); 567 | gradientBox.setHeight(BOX_HEIGHT); 568 | gradientBox.setX(PADDING_LEFT); 569 | gradientBox.setY((height - BOX_HEIGHT) * 0.5); 570 | 571 | updateHandles(); 572 | updateGradient(); 573 | } 574 | } 575 | } 576 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/fx/gradientpicker/Handle.java: -------------------------------------------------------------------------------- 1 | package eu.hansolo.fx.gradientpicker; 2 | 3 | 4 | import eu.hansolo.fx.gradientpicker.event.HandleEvent; 5 | import eu.hansolo.fx.gradientpicker.event.HandleObserver; 6 | import eu.hansolo.fx.gradientpicker.event.HandleEventType; 7 | import eu.hansolo.fx.gradientpicker.tool.Helper; 8 | import javafx.beans.DefaultProperty; 9 | import javafx.beans.property.DoubleProperty; 10 | import javafx.beans.property.DoublePropertyBase; 11 | import javafx.beans.property.ObjectProperty; 12 | import javafx.beans.property.ObjectPropertyBase; 13 | import javafx.collections.ObservableList; 14 | import javafx.geometry.Insets; 15 | import javafx.scene.Node; 16 | import javafx.scene.control.Tooltip; 17 | import javafx.scene.layout.Background; 18 | import javafx.scene.layout.BackgroundFill; 19 | import javafx.scene.layout.CornerRadii; 20 | import javafx.scene.layout.Region; 21 | import javafx.scene.paint.Color; 22 | import javafx.scene.shape.Circle; 23 | import javafx.scene.shape.ClosePath; 24 | import javafx.scene.shape.LineTo; 25 | import javafx.scene.shape.MoveTo; 26 | import javafx.scene.shape.Path; 27 | import javafx.scene.shape.StrokeType; 28 | 29 | import java.util.HashMap; 30 | import java.util.List; 31 | import java.util.Map; 32 | import java.util.concurrent.CopyOnWriteArrayList; 33 | 34 | import static eu.hansolo.fx.gradientpicker.tool.Helper.clamp; 35 | 36 | 37 | /** 38 | * User: hansolo 39 | * Date: 13.07.18 40 | * Time: 12:52 41 | */ 42 | @DefaultProperty("children") 43 | public class Handle extends Region implements Comparable{ 44 | public static final double HANDLE_SIZE = 12; 45 | private static final double PREFERRED_WIDTH = HANDLE_SIZE; 46 | private static final double PREFERRED_HEIGHT = HANDLE_SIZE; 47 | private static final double MINIMUM_WIDTH = HANDLE_SIZE; 48 | private static final double MINIMUM_HEIGHT = HANDLE_SIZE; 49 | private static final double MAXIMUM_WIDTH = HANDLE_SIZE; 50 | private static final double MAXIMUM_HEIGHT = HANDLE_SIZE; 51 | private static final double ASPECT_RATIO = PREFERRED_HEIGHT / PREFERRED_WIDTH; 52 | private final HandleEvent COLOR_EVENT = new HandleEvent(Handle.this, HandleEventType.COLOR); 53 | private final HandleEvent ALPHA_EVENT = new HandleEvent(Handle.this, HandleEventType.ALPHA); 54 | private final HandleEvent FRACTION_EVENT = new HandleEvent(Handle.this, HandleEventType.FRACTION); 55 | private final HandleEvent TYPE_EVENT = new HandleEvent(Handle.this, HandleEventType.TYPE); 56 | private List observers; 57 | private double size; 58 | private double width; 59 | private double height; 60 | private Circle path; 61 | private Color _fill; 62 | private ObjectProperty fill; 63 | private Color _stroke; 64 | private ObjectProperty stroke; 65 | private double _alpha; 66 | private DoubleProperty alpha; 67 | private double _fraction; 68 | private DoubleProperty fraction; 69 | private HandleType _type; 70 | private ObjectProperty type; 71 | private Color _focusColor; 72 | private ObjectProperty focusColor; 73 | private Handle linkedHandle; 74 | private Tooltip tooltip; 75 | private Map tooltipTexts; 76 | 77 | 78 | // ******************** Constructors ************************************** 79 | public Handle() { 80 | this(HandleType.COLOR_HANDLE, 0, Color.web("#282828"), 1.0, Color.web("#282828")); 81 | } 82 | public Handle(final HandleType TYPE, final double FRACTION, final Color FILL) { 83 | this(TYPE, FRACTION, FILL, 1.0, Color.web("#282828")); 84 | } 85 | public Handle(final HandleType TYPE, final double FRACTION, final double ALPHA) { 86 | this(TYPE, FRACTION, Color.web("#282828"), ALPHA, Color.web("#282828")); 87 | } 88 | public Handle(final HandleType TYPE, final double FRACTION, final Color FILL, final double ALPHA, final Color STROKE) { 89 | observers = new CopyOnWriteArrayList<>(); 90 | _type = TYPE; 91 | _fraction = FRACTION; 92 | _fill = FILL; 93 | _alpha = ALPHA; 94 | _stroke = STROKE; 95 | _focusColor = Color.web("#039ED3"); 96 | linkedHandle = null; 97 | tooltipTexts = new HashMap<>(2); 98 | tooltipTexts.put(HandleType.ALPHA_HANDLE, "Right mouse button to set alpha"); 99 | tooltipTexts.put(HandleType.COLOR_HANDLE, "Right mouse button to select color\nCTRL + left mouse button to set fraction\nDrag down to remove handle"); 100 | tooltip = new Tooltip(tooltipTexts.get(_type)); 101 | 102 | initGraphics(); 103 | registerListeners(); 104 | } 105 | 106 | 107 | // ******************** Initialization ************************************ 108 | private void initGraphics() { 109 | if (Double.compare(getPrefWidth(), 0.0) <= 0 || Double.compare(getPrefHeight(), 0.0) <= 0 || Double.compare(getWidth(), 0.0) <= 0 || 110 | Double.compare(getHeight(), 0.0) <= 0) { 111 | if (getPrefWidth() > 0 && getPrefHeight() > 0) { 112 | setPrefSize(getPrefWidth(), getPrefHeight()); 113 | } else { 114 | setPrefSize(PREFERRED_WIDTH, PREFERRED_HEIGHT); 115 | } 116 | } 117 | 118 | path = new Circle(HANDLE_SIZE * 0.5); 119 | path.setStrokeWidth(1); 120 | path.setFill(getFill()); 121 | path.setStroke(getStroke()); 122 | path.setStrokeType(StrokeType.INSIDE); 123 | 124 | setShape(path); 125 | 126 | setBackground(new Background(new BackgroundFill(getStroke(), CornerRadii.EMPTY, Insets.EMPTY), 127 | new BackgroundFill(Color.web("#dbdbdb"), CornerRadii.EMPTY, new Insets(1)), 128 | new BackgroundFill(getFill(), CornerRadii.EMPTY, new Insets(2)))); 129 | 130 | if (HandleType.COLOR_HANDLE == getType()) { setRotate(180); } 131 | 132 | Tooltip.install(Handle.this, tooltip); 133 | } 134 | 135 | private void registerListeners() { 136 | widthProperty().addListener(o -> resize()); 137 | heightProperty().addListener(o -> resize()); 138 | } 139 | 140 | 141 | // ******************** Methods ******************************************* 142 | @Override public void layoutChildren() { 143 | super.layoutChildren(); 144 | } 145 | 146 | @Override protected double computeMinWidth(final double HEIGHT) { return MINIMUM_WIDTH; } 147 | @Override protected double computeMinHeight(final double WIDTH) { return MINIMUM_HEIGHT; } 148 | @Override protected double computePrefWidth(final double HEIGHT) { return super.computePrefWidth(HEIGHT); } 149 | @Override protected double computePrefHeight(final double WIDTH) { return super.computePrefHeight(WIDTH); } 150 | @Override protected double computeMaxWidth(final double HEIGHT) { return MAXIMUM_WIDTH; } 151 | @Override protected double computeMaxHeight(final double WIDTH) { return MAXIMUM_HEIGHT; } 152 | 153 | @Override public ObservableList getChildren() { return super.getChildren(); } 154 | 155 | @Override public int compareTo(final Handle HANDLE) { return Double.compare(getFraction(), HANDLE.getFraction()); } 156 | 157 | public Color getFill() { return null == fill ? _fill : fill.get(); } 158 | public void setFill(final Color FILL) { 159 | if (null == fill) { 160 | _fill = FILL; 161 | updateBackground(); 162 | fireHandleEvent(COLOR_EVENT); 163 | } else { 164 | fill.set(FILL); 165 | } 166 | } 167 | public ObjectProperty fillProperty() { 168 | if (null == fill) { 169 | fill = new ObjectPropertyBase() { 170 | @Override protected void invalidated() { 171 | updateBackground(); 172 | fireHandleEvent(COLOR_EVENT); 173 | } 174 | @Override public Object getBean() { return Handle.this; } 175 | @Override public String getName() { return "fill"; } 176 | }; 177 | _fill = null; 178 | } 179 | return fill; 180 | } 181 | 182 | public Color getStroke() { return null == stroke ? _stroke : stroke.get(); } 183 | public void setStroke(final Color STROKE) { 184 | if (null == stroke) { 185 | _stroke = STROKE; 186 | updateBackground(); 187 | } else { 188 | stroke.set(STROKE); 189 | } 190 | } 191 | public ObjectProperty strokeProperty() { 192 | if (null == stroke) { 193 | stroke = new ObjectPropertyBase() { 194 | @Override protected void invalidated() { updateBackground(); } 195 | @Override public Object getBean() { return Handle.this; } 196 | @Override public String getName() { return "stroke"; } 197 | }; 198 | _stroke = null; 199 | } 200 | return stroke; 201 | } 202 | 203 | public double getAlpha() { return null == alpha ? _alpha : alpha.get(); } 204 | public void setAlpha(final double ALPHA) { 205 | if (null == alpha) { 206 | _alpha = Helper.clamp(0.0, 1.0, ALPHA); 207 | fireHandleEvent(ALPHA_EVENT); 208 | } else { 209 | alpha.set(ALPHA); 210 | } 211 | } 212 | public DoubleProperty alphaProperty() { 213 | if (null == alpha) { 214 | alpha = new DoublePropertyBase() { 215 | @Override protected void invalidated() { 216 | set(Helper.clamp(0.0, 1.0, get())); 217 | fireHandleEvent(ALPHA_EVENT); 218 | } 219 | @Override public Object getBean() { return Handle.this; } 220 | @Override public String getName() { return "alpha"; } 221 | }; 222 | } 223 | return alpha; 224 | } 225 | 226 | public double getFraction() { return null == fraction ? _fraction : fraction.get(); } 227 | public void setFraction(final double FRACTION) { 228 | if (null == fraction) { 229 | _fraction = Helper.clamp(0.0, 1.0, FRACTION); 230 | fireHandleEvent(FRACTION_EVENT); 231 | } else { 232 | fraction.set(FRACTION); 233 | } 234 | } 235 | public DoubleProperty fractionProperty() { 236 | if (null == fraction) { 237 | fraction = new DoublePropertyBase() { 238 | @Override protected void invalidated() { 239 | set(Helper.clamp(0.0, 1.0, get())); 240 | fireHandleEvent(FRACTION_EVENT); 241 | } 242 | @Override public Object getBean() { return Handle.this; } 243 | @Override public String getName() { return "fraction"; } 244 | }; 245 | } 246 | return fraction; 247 | } 248 | 249 | public HandleType getType() { return null == type ? _type : type.get(); } 250 | public void setType(final HandleType TYPE) { 251 | if (null == type) { 252 | _type = TYPE; 253 | setRotate(HandleType.COLOR_HANDLE == TYPE ? 180 : 0); 254 | fireHandleEvent(TYPE_EVENT); 255 | } else { 256 | type.set(TYPE); 257 | } 258 | } 259 | public ObjectProperty typeProperty() { 260 | if (null == type) { 261 | type = new ObjectPropertyBase() { 262 | @Override protected void invalidated() { 263 | setRotate(HandleType.COLOR_HANDLE == get() ? 180 : 0); 264 | fireHandleEvent(TYPE_EVENT); 265 | } 266 | @Override public Object getBean() { return Handle.this; } 267 | @Override public String getName() { return "type"; } 268 | }; 269 | _type = null; 270 | } 271 | return type; 272 | } 273 | 274 | public Color getFocusColor() { return null == focusColor ? _focusColor : focusColor.get(); } 275 | public void setFocusColor(final Color COLOR) { 276 | if (null == focusColor) { 277 | _focusColor = COLOR; 278 | } else { 279 | focusColor.set(COLOR); 280 | } 281 | } 282 | public ObjectProperty focusColorProperty() { 283 | if (null == focusColor) { 284 | focusColor = new ObjectPropertyBase() { 285 | @Override public Object getBean() { return Handle.this; } 286 | @Override public String getName() { return "focusColor"; } 287 | }; 288 | _focusColor = null; 289 | } 290 | return focusColor; 291 | } 292 | 293 | public String getTooltipText() { return tooltipTexts.get(getType()); } 294 | public void setTooltipText(final String TEXT) { tooltipTexts.put(getType(), TEXT); } 295 | 296 | public Handle getLinkedHandle() { return linkedHandle; } 297 | public void setLinkedHandle(final Handle HANDLE) { 298 | if (null == HANDLE) return; 299 | if (null != linkedHandle) { 300 | linkedHandle.layoutXProperty().unbindBidirectional(Handle.this.layoutXProperty()); 301 | } 302 | linkedHandle = HANDLE; 303 | linkedHandle.layoutXProperty().bindBidirectional(Handle.this.layoutXProperty()); 304 | 305 | } 306 | 307 | public void setFocus(final boolean FOCUSED) { path.setStroke(FOCUSED ? getFocusColor() : getStroke()); } 308 | 309 | private Color createColor() { 310 | Color fill = getFill(); 311 | return Color.color(fill.getRed(), fill.getGreen(), fill.getBlue(), getAlpha()); 312 | } 313 | 314 | private void updateBackground() { 315 | setBackground(new Background(new BackgroundFill(getStroke(), CornerRadii.EMPTY, Insets.EMPTY), 316 | new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, new Insets(1)), 317 | new BackgroundFill(createColor(), CornerRadii.EMPTY, new Insets(2)))); 318 | } 319 | 320 | 321 | // ******************** EventHandling ************************************* 322 | public void addHandleEventListener(final HandleObserver OBSERVER) { 323 | if (!observers.contains(OBSERVER)) { observers.add(OBSERVER); } 324 | } 325 | public void removeHandleEventListener(final HandleObserver OBSERVER) { 326 | if (observers.contains(OBSERVER)) { observers.remove(OBSERVER); } 327 | } 328 | public void fireHandleEvent(final HandleEvent EVENT) { 329 | observers.forEach(observer -> observer.onHandleEvent(EVENT)); 330 | } 331 | 332 | 333 | // ******************** Resizing ****************************************** 334 | private void resize() { 335 | width = getWidth() - getInsets().getLeft() - getInsets().getRight(); 336 | height = getHeight() - getInsets().getTop() - getInsets().getBottom(); 337 | size = width < height ? width : height; 338 | 339 | if (ASPECT_RATIO * width > height) { 340 | width = 1 / (ASPECT_RATIO / height); 341 | } else if (1 / (ASPECT_RATIO / height) > width) { 342 | height = ASPECT_RATIO * width; 343 | } 344 | 345 | if (width > 0 && height > 0) { 346 | setMaxSize(width, height); 347 | setPrefSize(width, height); 348 | } 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/fx/gradientpicker/HandleType.java: -------------------------------------------------------------------------------- 1 | package eu.hansolo.fx.gradientpicker; 2 | 3 | public enum HandleType { 4 | COLOR_HANDLE, ALPHA_HANDLE 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/fx/gradientpicker/event/GradientEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.fx.gradientpicker.event; 18 | 19 | import eu.hansolo.fx.gradientpicker.GradientPicker; 20 | 21 | 22 | public class GradientEvent { 23 | private final GradientPicker GRADIENT_PICKER; 24 | private final GradientEventType TYPE; 25 | 26 | 27 | // ******************** Constructors ************************************** 28 | public GradientEvent(final GradientPicker SRC, final GradientEventType TYPE) { 29 | this.GRADIENT_PICKER = SRC; 30 | this.TYPE = TYPE; 31 | } 32 | 33 | 34 | // ******************** Methods ******************************************* 35 | public GradientPicker getSource() { return GRADIENT_PICKER; } 36 | 37 | public GradientEventType getType() { return TYPE; } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/fx/gradientpicker/event/GradientEventType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.fx.gradientpicker.event; 18 | 19 | public enum GradientEventType { 20 | GRADIENT_CHANGED 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/fx/gradientpicker/event/GradientObserver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.fx.gradientpicker.event; 18 | 19 | @FunctionalInterface 20 | public interface GradientObserver { 21 | void onGradientChanged(final GradientEvent EVT); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/fx/gradientpicker/event/HandleEvent.java: -------------------------------------------------------------------------------- 1 | package eu.hansolo.fx.gradientpicker.event; 2 | 3 | import eu.hansolo.fx.gradientpicker.Handle; 4 | import java.util.EventObject; 5 | 6 | 7 | public class HandleEvent extends EventObject { 8 | private final HandleEventType TYPE; 9 | 10 | 11 | // ******************** Constructors ************************************** 12 | public HandleEvent(final Object SRC, final HandleEventType TYPE) { 13 | super(SRC); 14 | this.TYPE = TYPE; 15 | } 16 | 17 | 18 | // ******************** Methods ******************************************* 19 | public HandleEventType getType() { return TYPE; } 20 | 21 | public Handle getHandle() { return (Handle) getSource(); } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/fx/gradientpicker/event/HandleEventType.java: -------------------------------------------------------------------------------- 1 | package eu.hansolo.fx.gradientpicker.event; 2 | 3 | public enum HandleEventType { 4 | COLOR, ALPHA, FRACTION, TYPE 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/fx/gradientpicker/event/HandleObserver.java: -------------------------------------------------------------------------------- 1 | package eu.hansolo.fx.gradientpicker.event; 2 | 3 | import eu.hansolo.fx.gradientpicker.event.HandleEvent; 4 | 5 | import java.util.EventListener; 6 | 7 | 8 | @FunctionalInterface 9 | public interface HandleObserver extends EventListener { 10 | void onHandleEvent(final HandleEvent EVENT); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/fx/gradientpicker/tool/GradientLookup.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.fx.gradientpicker.tool; 18 | 19 | import javafx.scene.paint.Color; 20 | import javafx.scene.paint.Stop; 21 | 22 | import java.util.ArrayList; 23 | import java.util.Arrays; 24 | import java.util.Collections; 25 | import java.util.List; 26 | import java.util.Map; 27 | import java.util.Map.Entry; 28 | import java.util.TreeMap; 29 | 30 | 31 | public class GradientLookup { 32 | private Map stops; 33 | 34 | 35 | // ******************** Constructors ************************************** 36 | public GradientLookup () { 37 | this(new Stop[]{}); 38 | } 39 | public GradientLookup(final Stop... STOPS) { 40 | this(Arrays.asList(STOPS)); 41 | } 42 | public GradientLookup(final List STOPS) { 43 | stops = new TreeMap<>(); 44 | for (Stop stop : STOPS) { stops.put(stop.getOffset(), stop); } 45 | init(); 46 | } 47 | 48 | 49 | // ******************** Initialization ************************************ 50 | private void init() { 51 | if (stops.isEmpty()) return; 52 | 53 | double minFraction = Collections.min(stops.keySet()); 54 | double maxFraction = Collections.max(stops.keySet()); 55 | 56 | if (Double.compare(minFraction, 0) > 0) { stops.put(0.0, new Stop(0.0, stops.get(minFraction).getColor())); } 57 | if (Double.compare(maxFraction, 1) < 0) { stops.put(1.0, new Stop(1.0, stops.get(maxFraction).getColor())); } 58 | } 59 | 60 | 61 | // ******************** Methods ******************************************* 62 | public Color getColorAt(final double POSITION_OF_COLOR) { 63 | if (stops.isEmpty()) return Color.BLACK; 64 | 65 | final double POSITION = Helper.clamp(0.0, 1.0, POSITION_OF_COLOR); 66 | final Color COLOR; 67 | if (stops.size() == 1) { 68 | final Map ONE_ENTRY = (Map) stops.entrySet().iterator().next(); 69 | COLOR = stops.get(ONE_ENTRY.keySet().iterator().next()).getColor(); 70 | } else { 71 | Stop lowerBound = stops.get(0.0); 72 | Stop upperBound = stops.get(1.0); 73 | for (Double fraction : stops.keySet()) { 74 | if (Double.compare(fraction,POSITION) < 0) { 75 | lowerBound = stops.get(fraction); 76 | } 77 | if (Double.compare(fraction, POSITION) > 0) { 78 | upperBound = stops.get(fraction); 79 | break; 80 | } 81 | } 82 | COLOR = interpolateColor(lowerBound, upperBound, POSITION); 83 | } 84 | return COLOR; 85 | } 86 | 87 | public List getStops() { return new ArrayList<>(stops.values()); } 88 | public void setStops(final Stop... STOPS) { setStops(Arrays.asList(STOPS)); } 89 | public void setStops(final List STOPS) { 90 | stops.clear(); 91 | for (Stop stop : STOPS) { stops.put(stop.getOffset(), stop); } 92 | init(); 93 | } 94 | 95 | public Stop getStopAt(final double POSITION_OF_STOP) { 96 | if (stops.isEmpty()) { throw new IllegalArgumentException("GradientStop stops should not be empty"); }; 97 | 98 | final double POSITION = Helper.clamp(0.0, 1.0, POSITION_OF_STOP); 99 | 100 | Stop stop = null; 101 | double distance = Math.abs(stops.get(Double.valueOf(0)).getOffset() - POSITION); 102 | for(Entry entry : stops.entrySet()) { 103 | double cDistance = Math.abs(entry.getKey() - POSITION); 104 | if (cDistance < distance) { 105 | stop = stops.get(entry.getKey()); 106 | distance = cDistance; 107 | } 108 | } 109 | return stop; 110 | } 111 | 112 | public List getStopsBetween(final double MIN_OFFSET, final double MAX_OFFSET) { 113 | List selectedStops = new ArrayList<>(); 114 | for (Entry entry : stops.entrySet()) { 115 | if (entry.getValue().getOffset() >= MIN_OFFSET && entry.getValue().getOffset() <= MAX_OFFSET) { selectedStops.add(entry.getValue()); } 116 | } 117 | return selectedStops; 118 | } 119 | 120 | private Color interpolateColor(final Stop LOWER_BOUND, final Stop UPPER_BOUND, final double POSITION) { 121 | final double POS = (POSITION - LOWER_BOUND.getOffset()) / (UPPER_BOUND.getOffset() - LOWER_BOUND.getOffset()); 122 | 123 | final double DELTA_RED = (UPPER_BOUND.getColor().getRed() - LOWER_BOUND.getColor().getRed()) * POS; 124 | final double DELTA_GREEN = (UPPER_BOUND.getColor().getGreen() - LOWER_BOUND.getColor().getGreen()) * POS; 125 | final double DELTA_BLUE = (UPPER_BOUND.getColor().getBlue() - LOWER_BOUND.getColor().getBlue()) * POS; 126 | final double DELTA_OPACITY = (UPPER_BOUND.getColor().getOpacity() - LOWER_BOUND.getColor().getOpacity()) * POS; 127 | 128 | double red = Helper.clamp(0.0, 1.0, (LOWER_BOUND.getColor().getRed() + DELTA_RED)); 129 | double green = Helper.clamp(0.0, 1.0, (LOWER_BOUND.getColor().getGreen() + DELTA_GREEN)); 130 | double blue = Helper.clamp(0.0, 1.0, (LOWER_BOUND.getColor().getBlue() + DELTA_BLUE)); 131 | double opacity = Helper.clamp(0.0, 1.0, (LOWER_BOUND.getColor().getOpacity() + DELTA_OPACITY)); 132 | 133 | return Color.color(red, green, blue, opacity); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/fx/gradientpicker/tool/Helper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.fx.gradientpicker.tool; 18 | 19 | public class Helper { 20 | public static final double clamp(final double MIN, final double MAX, final double VALUE) { 21 | if (VALUE < MIN) return MIN; 22 | if (VALUE > MAX) return MAX; 23 | return VALUE; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/fx/gradientpicker/tool/NumberTextField.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.fx.gradientpicker.tool; 18 | 19 | import javafx.beans.value.ChangeListener; 20 | import javafx.scene.control.TextField; 21 | 22 | 23 | public class NumberTextField extends TextField { 24 | private int noBeforeDecimalPoint; 25 | private int decimals; 26 | private ChangeListener textListener; 27 | private String regex; 28 | 29 | 30 | // ******************** Constructors ************************************** 31 | public NumberTextField() { 32 | this("", 1, 3); 33 | } 34 | public NumberTextField(final String CONTENT) { 35 | this(CONTENT,1, 3 ); 36 | } 37 | public NumberTextField(final String CONTENT, final int NO_BEFORE_DECIMALPOINT, final int DECIMALS) { 38 | super(CONTENT); 39 | noBeforeDecimalPoint = NO_BEFORE_DECIMALPOINT; 40 | decimals = DECIMALS; 41 | regex = "\\d{0," + noBeforeDecimalPoint + "}([\\.]\\d{0," + decimals + "})?"; 42 | textListener = (o, ov, nv) -> { if (!nv.matches(regex)) { setText(ov); } }; 43 | registerListeners(); 44 | } 45 | 46 | private void registerListeners() { 47 | textProperty().addListener(textListener); 48 | } 49 | 50 | 51 | // ******************** Methods ******************************************* 52 | public int getNoBeforeDecimalPoint() { return noBeforeDecimalPoint; } 53 | public void setNoBeforeDecimalPoint(final int AMOUNT) { 54 | if (AMOUNT < 0) { throw new IllegalArgumentException("Amount of numbers cannot be negative"); } 55 | noBeforeDecimalPoint = AMOUNT; 56 | updateRegex(); 57 | } 58 | 59 | public int getDecimals() { return decimals; } 60 | public void setDecimals(final int DECIMALS) { 61 | if (DECIMALS < 0) { throw new IllegalArgumentException("No of decimals cannot be negative"); } 62 | decimals = DECIMALS; 63 | updateRegex(); 64 | } 65 | 66 | private void updateRegex() { 67 | regex = "\\d{0," + noBeforeDecimalPoint + "}([\\.]\\d{0," + decimals + "})?"; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/resources/eu/hansolo/fx/gradientpicker/gradientpicker.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | .gradient-picker { 18 | 19 | } 20 | 21 | .color-picker { 22 | -fx-color-label-visible: false; 23 | } -------------------------------------------------------------------------------- /src/main/resources/eu/hansolo/fx/gradientpicker/opacitypattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanSolo/gradientpicker/a664093558ef225c499ef9a9d8a11e8026b87889/src/main/resources/eu/hansolo/fx/gradientpicker/opacitypattern.png --------------------------------------------------------------------------------