├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── .travis.yml ├── LICENSE ├── README.md ├── mvnw ├── pom.xml ├── readme ├── color-color.png ├── color-context.png ├── color-derive.png ├── color-lineargradient.png ├── font.png ├── highlight.png ├── key.png ├── shape-svg.png ├── suggest-common.png ├── suggest-current-id.png └── suggest-key.png └── src └── main ├── java └── xdean │ └── css │ └── editor │ ├── FxCssApplication.java │ ├── FxCssEntrance.java │ ├── context │ ├── Config.java │ ├── Context.java │ └── setting │ │ ├── DefaultValue.java │ │ ├── EditActions.java │ │ ├── FileActions.java │ │ ├── HelpActions.java │ │ ├── OptionBindProcessor.java │ │ ├── OtherSettings.java │ │ ├── PreferenceSettings.java │ │ ├── RawSettingKeys.java │ │ ├── SettingKeys.java │ │ └── model │ │ ├── CssEditorActionKeyOption.java │ │ ├── CssEditorKeyOption.java │ │ └── option │ │ ├── BooleanOption.java │ │ ├── ConstraintOption.java │ │ ├── IntegerOption.java │ │ ├── Option.java │ │ ├── OptionGroup.java │ │ ├── RangeOption.java │ │ ├── SimpleConstraintOption.java │ │ ├── SimpleOption.java │ │ ├── StringOption.java │ │ └── ValueOption.java │ ├── control │ ├── CssEditor.java │ └── CssEditorEvent.java │ ├── controller │ ├── CssEditorTab.java │ ├── MainFrameController.java │ ├── MainFrameModel.java │ ├── MenuBarController.java │ ├── OptionsController.java │ ├── SearchBarController.java │ ├── StatusBarController.java │ └── ToolBarController.java │ ├── feature │ ├── CommentFeature.java │ ├── CssAppFeature.java │ ├── CssEditorFeature.java │ ├── HighLightFeature.java │ ├── LastFileFeature.java │ ├── PreviewFeature.java │ ├── SuggestionFeature.java │ ├── highlight │ │ ├── CssHighLight.java │ │ ├── CssHighLightConfig.java │ │ ├── RegexHighLight.java │ │ └── SimpleHighLight.java │ ├── preview │ │ ├── BorderPreviewer.java │ │ ├── CssElementPreviewer.java │ │ ├── PaintPreviewer.java │ │ └── SvgPreviewer.java │ └── suggestion │ │ ├── CssSuggestionConfig.java │ │ ├── CssSuggestionFilter.java │ │ ├── CssSuggestionService.java │ │ ├── CssSuggestionsFilter.java │ │ ├── RegexSuggestion.java │ │ └── SimpleSuggestion.java │ ├── model │ ├── CssContext.java │ ├── FileWrapper.java │ ├── FunctionParsedValue.java │ └── SimpleParsedValue.java │ └── service │ ├── ContextService.java │ ├── DialogService.java │ ├── MessageService.java │ ├── RecentFileService.java │ └── SkinService.java └── resources ├── application.properties ├── css ├── css-highlighting.css ├── global.css └── skin │ ├── classic.css │ ├── default.css │ ├── metal.css │ └── pink.css ├── fontawesome.ttf ├── fxml ├── MainFrame.fxgraph ├── MainFrame.fxml ├── MenuBar.fxgraph ├── MenuBar.fxml ├── Options.fxgraph ├── Options.fxml ├── SearchBar.fxgraph ├── SearchBar.fxml ├── StatusBar.fxgraph ├── StatusBar.fxml ├── ToolBar.fxgraph └── ToolBar.fxml ├── image └── icon.jpg ├── logback.xml └── message └── settings.properties /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | hs_err_pid* 4 | 5 | ### Eclipse ### 6 | .settings/ 7 | target/ 8 | bin/ 9 | .classpath 10 | .project 11 | .factorypath 12 | .apt_generated 13 | .springBeans 14 | rebel.xml 15 | /javadoc/ 16 | 17 | !.mvn/wrapper/maven-wrapper.jar 18 | !**/src/main/**/target/ 19 | !**/src/test/**/target/ 20 | 21 | ### IntelliJ IDEA ### 22 | .idea 23 | *.iws 24 | *.iml 25 | *.ipr 26 | 27 | ### NetBeans ### 28 | nbproject/private/ 29 | build/ 30 | nbbuild/ 31 | dist/ 32 | nbdist/ 33 | .nb-gradle/ 34 | 35 | ### other ### 36 | .svn/ 37 | .*.md.html 38 | dependency-reduced-pom.xml -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XDean/CSS-Editor-FX/9fb891b71fdfa46b675a4d1d31eb694795d2c586/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: java 3 | dist: trusty 4 | cache: 5 | directories: 6 | - $HOME/.m2 7 | jdk: 8 | - oraclejdk8 9 | install: true 10 | addons: 11 | sonarcloud: 12 | organization: xdean-github 13 | script: 14 | - mvn clean package sonar:sonar -P jacoco -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSS Editor FX # 2 | 3 | A CSS editor written in JavaFX 8 4 | 5 | **This is a study project in 2017. Not maintain now.** 6 | 7 | **Hope it inspires you about using Spring Boot + JavaFX together** 8 | 9 | ## Features ## 10 | - [Code Highlight](#code-highlight) 11 | - [Code Assist](#code-assist) 12 | - [JavaFX Context](#javafx-context) 13 | - [Current Context](#current-context) 14 | - *[Specify Context](#specify-context)* 15 | - [Preview](#preview) 16 | - [Color](#color) 17 | - [RGB](#rgb) 18 | - [Color Function](#function) 19 | - [Gradient](#gradient) 20 | - *[Context Color Paser](#context-color-paser)* 21 | - [Shape](#shape) 22 | - [Other Text Editor Common Features](#other) 23 | - *[Code Format](#code-format)* 24 | - [Toggle Comment](#toggle-comment) 25 | - [Custom Font](#custom-font) 26 | - [Custom Shortcut](#custom-shortcut) 27 | - [Known Bugs](#known-bugs) 28 | 29 | ## Code Highlight ## 30 | ![highlight.png](readme/highlight.png) 31 | 32 | ## Code Assist ## 33 | ### JavaFX Context ### 34 | ![suggest-common.png](readme/suggest-common.png) 35 | ### Current Context ### 36 | ![suggest-current-id.png](readme/suggest-current-id.png) 37 | ### Specify Context ### 38 | *Not implement yet* 39 | 40 | ## Preview ## 41 | ### Color ### 42 | #### RGB #### 43 | ![color-color.png](readme/color-color.png) 44 | #### Function #### 45 | ![color-derive.png](readme/color-derive.png) 46 | #### Gradient #### 47 | ![color-lineargradient.png](readme/color-lineargradient.png) 48 | #### Context Color Paser #### 49 | ![color-context.png](readme/color-context.png) 50 | ### Shape ### 51 | ![shape-svg.png](readme/shape-svg.png) 52 | 53 | ## Other ## 54 | ### Code Format ### 55 | *Not implement yet* 56 | ### Toggle Comment ### 57 | 58 | ### Custom Font ### 59 | ![font.png](readme/font.png) 60 | ### Custom Shortcut ### 61 | ![key.png](readme/key.png) 62 | 63 | ## Known Bugs ## 64 | - richtextfx codeArea undo leads IllegalArgumentException when merge changes. See [https://github.com/TomasMikula/RichTextFX/pull/402](https://github.com/TomasMikula/RichTextFX/pull/402 "github"), it may resolve soon. 65 | - close tab will not correctly refresh currentTab property. It lead tab color and scrollbars still. -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /usr/local/etc/mavenrc ] ; then 40 | . /usr/local/etc/mavenrc 41 | fi 42 | 43 | if [ -f /etc/mavenrc ] ; then 44 | . /etc/mavenrc 45 | fi 46 | 47 | if [ -f "$HOME/.mavenrc" ] ; then 48 | . "$HOME/.mavenrc" 49 | fi 50 | 51 | fi 52 | 53 | # OS specific support. $var _must_ be set to either true or false. 54 | cygwin=false; 55 | darwin=false; 56 | mingw=false 57 | case "`uname`" in 58 | CYGWIN*) cygwin=true ;; 59 | MINGW*) mingw=true;; 60 | Darwin*) darwin=true 61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 63 | if [ -z "$JAVA_HOME" ]; then 64 | if [ -x "/usr/libexec/java_home" ]; then 65 | export JAVA_HOME="`/usr/libexec/java_home`" 66 | else 67 | export JAVA_HOME="/Library/Java/Home" 68 | fi 69 | fi 70 | ;; 71 | esac 72 | 73 | if [ -z "$JAVA_HOME" ] ; then 74 | if [ -r /etc/gentoo-release ] ; then 75 | JAVA_HOME=`java-config --jre-home` 76 | fi 77 | fi 78 | 79 | if [ -z "$M2_HOME" ] ; then 80 | ## resolve links - $0 may be a link to maven's home 81 | PRG="$0" 82 | 83 | # need this for relative symlinks 84 | while [ -h "$PRG" ] ; do 85 | ls=`ls -ld "$PRG"` 86 | link=`expr "$ls" : '.*-> \(.*\)$'` 87 | if expr "$link" : '/.*' > /dev/null; then 88 | PRG="$link" 89 | else 90 | PRG="`dirname "$PRG"`/$link" 91 | fi 92 | done 93 | 94 | saveddir=`pwd` 95 | 96 | M2_HOME=`dirname "$PRG"`/.. 97 | 98 | # make it fully qualified 99 | M2_HOME=`cd "$M2_HOME" && pwd` 100 | 101 | cd "$saveddir" 102 | # echo Using m2 at $M2_HOME 103 | fi 104 | 105 | # For Cygwin, ensure paths are in UNIX format before anything is touched 106 | if $cygwin ; then 107 | [ -n "$M2_HOME" ] && 108 | M2_HOME=`cygpath --unix "$M2_HOME"` 109 | [ -n "$JAVA_HOME" ] && 110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 111 | [ -n "$CLASSPATH" ] && 112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 113 | fi 114 | 115 | # For Mingw, ensure paths are in UNIX format before anything is touched 116 | if $mingw ; then 117 | [ -n "$M2_HOME" ] && 118 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 119 | [ -n "$JAVA_HOME" ] && 120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 121 | fi 122 | 123 | if [ -z "$JAVA_HOME" ]; then 124 | javaExecutable="`which javac`" 125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 126 | # readlink(1) is not available as standard on Solaris 10. 127 | readLink=`which readlink` 128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 129 | if $darwin ; then 130 | javaHome="`dirname \"$javaExecutable\"`" 131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 132 | else 133 | javaExecutable="`readlink -f \"$javaExecutable\"`" 134 | fi 135 | javaHome="`dirname \"$javaExecutable\"`" 136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 137 | JAVA_HOME="$javaHome" 138 | export JAVA_HOME 139 | fi 140 | fi 141 | fi 142 | 143 | if [ -z "$JAVACMD" ] ; then 144 | if [ -n "$JAVA_HOME" ] ; then 145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 146 | # IBM's JDK on AIX uses strange locations for the executables 147 | JAVACMD="$JAVA_HOME/jre/sh/java" 148 | else 149 | JAVACMD="$JAVA_HOME/bin/java" 150 | fi 151 | else 152 | JAVACMD="`\\unset -f command; \\command -v java`" 153 | fi 154 | fi 155 | 156 | if [ ! -x "$JAVACMD" ] ; then 157 | echo "Error: JAVA_HOME is not defined correctly." >&2 158 | echo " We cannot execute $JAVACMD" >&2 159 | exit 1 160 | fi 161 | 162 | if [ -z "$JAVA_HOME" ] ; then 163 | echo "Warning: JAVA_HOME environment variable is not set." 164 | fi 165 | 166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 167 | 168 | # traverses directory structure from process work directory to filesystem root 169 | # first directory with .mvn subdirectory is considered project base directory 170 | find_maven_basedir() { 171 | 172 | if [ -z "$1" ] 173 | then 174 | echo "Path not specified to find_maven_basedir" 175 | return 1 176 | fi 177 | 178 | basedir="$1" 179 | wdir="$1" 180 | while [ "$wdir" != '/' ] ; do 181 | if [ -d "$wdir"/.mvn ] ; then 182 | basedir=$wdir 183 | break 184 | fi 185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 186 | if [ -d "${wdir}" ]; then 187 | wdir=`cd "$wdir/.."; pwd` 188 | fi 189 | # end of workaround 190 | done 191 | echo "${basedir}" 192 | } 193 | 194 | # concatenates all lines of a file 195 | concat_lines() { 196 | if [ -f "$1" ]; then 197 | echo "$(tr -s '\n' ' ' < "$1")" 198 | fi 199 | } 200 | 201 | BASE_DIR=`find_maven_basedir "$(pwd)"` 202 | if [ -z "$BASE_DIR" ]; then 203 | exit 1; 204 | fi 205 | 206 | ########################################################################################## 207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 208 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 209 | ########################################################################################## 210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Found .mvn/wrapper/maven-wrapper.jar" 213 | fi 214 | else 215 | if [ "$MVNW_VERBOSE" = true ]; then 216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 217 | fi 218 | if [ -n "$MVNW_REPOURL" ]; then 219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 220 | else 221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 222 | fi 223 | while IFS="=" read key value; do 224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 225 | esac 226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 227 | if [ "$MVNW_VERBOSE" = true ]; then 228 | echo "Downloading from: $jarUrl" 229 | fi 230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 231 | if $cygwin; then 232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 233 | fi 234 | 235 | if command -v wget > /dev/null; then 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Found wget ... using wget" 238 | fi 239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 241 | else 242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 243 | fi 244 | elif command -v curl > /dev/null; then 245 | if [ "$MVNW_VERBOSE" = true ]; then 246 | echo "Found curl ... using curl" 247 | fi 248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 249 | curl -o "$wrapperJarPath" "$jarUrl" -f 250 | else 251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 252 | fi 253 | 254 | else 255 | if [ "$MVNW_VERBOSE" = true ]; then 256 | echo "Falling back to using Java to download" 257 | fi 258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 259 | # For Cygwin, switch paths to Windows format before running javac 260 | if $cygwin; then 261 | javaClass=`cygpath --path --windows "$javaClass"` 262 | fi 263 | if [ -e "$javaClass" ]; then 264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 265 | if [ "$MVNW_VERBOSE" = true ]; then 266 | echo " - Compiling MavenWrapperDownloader.java ..." 267 | fi 268 | # Compiling the Java class 269 | ("$JAVA_HOME/bin/javac" "$javaClass") 270 | fi 271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 272 | # Running the downloader 273 | if [ "$MVNW_VERBOSE" = true ]; then 274 | echo " - Running MavenWrapperDownloader.java ..." 275 | fi 276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 277 | fi 278 | fi 279 | fi 280 | fi 281 | ########################################################################################## 282 | # End of extension 283 | ########################################################################################## 284 | 285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 286 | if [ "$MVNW_VERBOSE" = true ]; then 287 | echo $MAVEN_PROJECTBASEDIR 288 | fi 289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 290 | 291 | # For Cygwin, switch paths to Windows format before running java 292 | if $cygwin; then 293 | [ -n "$M2_HOME" ] && 294 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 295 | [ -n "$JAVA_HOME" ] && 296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 297 | [ -n "$CLASSPATH" ] && 298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 299 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 301 | fi 302 | 303 | # Provide a "standardized" way to retrieve the CLI args that will 304 | # work with both Windows and non-Windows executions. 305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 306 | export MAVEN_CMD_LINE_ARGS 307 | 308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 309 | 310 | exec "$JAVACMD" \ 311 | $MAVEN_OPTS \ 312 | $MAVEN_DEBUG_OPTS \ 313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 314 | "-Dmaven.home=${M2_HOME}" \ 315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 317 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | com.github.XDean 6 | oss-parent 7 | 1.1 8 | 9 | JavaFX-CSS-Editor 10 | 0.0.1-SNAPSHOT 11 | 12 | 13 | 14 | 15 | io.reactivex.rxjava2 16 | rxjava 17 | 2.1.13 18 | 19 | 20 | 21 | 22 | 23 | 24 | com.github.XDean 25 | auto-message 26 | 0.1.6 27 | provided 28 | 29 | 30 | com.github.XDean 31 | auto-spring-factories 32 | 0.1.2 33 | provided 34 | 35 | 36 | com.github.XDean 37 | javafx-spring-boot 38 | 0.1-SNAPSHOT 39 | 40 | 41 | com.github.XDean 42 | rx2-nullable 43 | 1.0 44 | 45 | 46 | ch.qos.logback 47 | logback-classic 48 | 1.1.3 49 | 50 | 51 | ch.qos.logback 52 | logback-core 53 | 1.1.3 54 | 55 | 56 | org.slf4j 57 | slf4j-api 58 | 1.7.5 59 | 60 | 61 | junit 62 | junit 63 | 4.11 64 | 65 | 66 | io.reactivex.rxjava2 67 | rxjavafx 68 | 2.2.2 69 | 70 | 71 | org.controlsfx 72 | controlsfx 73 | 8.40.12 74 | 75 | 76 | org.fxmisc.richtext 77 | richtextfx 78 | 0.7-M2 79 | 80 | 81 | com.github.XDean 82 | JavaFX-EX 83 | 0.1.0-SNAPSHOT 84 | 85 | 86 | 87 | 88 | 89 | 90 | org.apache.maven.plugins 91 | maven-compiler-plugin 92 | 3.1 93 | 94 | 95 | ${sun.boot.class.path}${path.separator}${java.home}/lib/jfxrt.jar 96 | 97 | 98 | 99 | 100 | org.springframework.boot 101 | spring-boot-maven-plugin 102 | 2.2.7.RELEASE 103 | 104 | true 105 | 106 | 107 | 108 | 109 | repackage 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /readme/color-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XDean/CSS-Editor-FX/9fb891b71fdfa46b675a4d1d31eb694795d2c586/readme/color-color.png -------------------------------------------------------------------------------- /readme/color-context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XDean/CSS-Editor-FX/9fb891b71fdfa46b675a4d1d31eb694795d2c586/readme/color-context.png -------------------------------------------------------------------------------- /readme/color-derive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XDean/CSS-Editor-FX/9fb891b71fdfa46b675a4d1d31eb694795d2c586/readme/color-derive.png -------------------------------------------------------------------------------- /readme/color-lineargradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XDean/CSS-Editor-FX/9fb891b71fdfa46b675a4d1d31eb694795d2c586/readme/color-lineargradient.png -------------------------------------------------------------------------------- /readme/font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XDean/CSS-Editor-FX/9fb891b71fdfa46b675a4d1d31eb694795d2c586/readme/font.png -------------------------------------------------------------------------------- /readme/highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XDean/CSS-Editor-FX/9fb891b71fdfa46b675a4d1d31eb694795d2c586/readme/highlight.png -------------------------------------------------------------------------------- /readme/key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XDean/CSS-Editor-FX/9fb891b71fdfa46b675a4d1d31eb694795d2c586/readme/key.png -------------------------------------------------------------------------------- /readme/shape-svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XDean/CSS-Editor-FX/9fb891b71fdfa46b675a4d1d31eb694795d2c586/readme/shape-svg.png -------------------------------------------------------------------------------- /readme/suggest-common.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XDean/CSS-Editor-FX/9fb891b71fdfa46b675a4d1d31eb694795d2c586/readme/suggest-common.png -------------------------------------------------------------------------------- /readme/suggest-current-id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XDean/CSS-Editor-FX/9fb891b71fdfa46b675a4d1d31eb694795d2c586/readme/suggest-current-id.png -------------------------------------------------------------------------------- /readme/suggest-key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XDean/CSS-Editor-FX/9fb891b71fdfa46b675a4d1d31eb694795d2c586/readme/suggest-key.png -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/FxCssApplication.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor; 2 | 3 | import static xdean.jfxex.bean.BeanUtil.map; 4 | import static xdean.jfxex.event.EventHandlers.consume; 5 | 6 | import java.util.Collections; 7 | import java.util.List; 8 | 9 | import javax.inject.Inject; 10 | import javax.inject.Named; 11 | 12 | import org.springframework.stereotype.Component; 13 | 14 | import javafx.beans.property.ObjectProperty; 15 | import javafx.collections.FXCollections; 16 | import javafx.collections.ObservableList; 17 | import javafx.scene.Parent; 18 | import javafx.scene.Scene; 19 | import javafx.scene.image.Image; 20 | import javafx.stage.Stage; 21 | import xdean.css.editor.context.setting.FileActions; 22 | import xdean.css.editor.control.CssEditor; 23 | import xdean.css.editor.controller.MainFrameController; 24 | import xdean.css.editor.feature.CssAppFeature; 25 | import xdean.css.editor.service.ContextService; 26 | import xdean.css.editor.service.SkinService; 27 | import xdean.jfx.spring.FxApplication; 28 | import xdean.jfx.spring.FxmlResult; 29 | import xdean.jfx.spring.context.FxContext; 30 | import xdean.jfxex.bean.annotation.CheckNull; 31 | import xdean.jfxex.bean.property.ObjectPropertyEX; 32 | 33 | @Component 34 | public class FxCssApplication implements FxApplication, ContextService { 35 | 36 | private @Inject FxmlResult mainFrame; 37 | private @Inject SkinService skinService; 38 | private @Inject FileActions fileActions; 39 | private @Inject List features = Collections.emptyList(); 40 | 41 | private final Stage stage; 42 | private final ObservableList editors = FXCollections.observableArrayList(); 43 | private final ObjectPropertyEX<@CheckNull CssEditor> activeEditor = new ObjectPropertyEX(this, "editor") 44 | .in(editors, false); 45 | 46 | @Inject 47 | public FxCssApplication(@Named(FxContext.FX_PRIMARY_STAGE) Stage stage) { 48 | this.stage = stage; 49 | activeEditor.addListener((ob, o, n) -> { 50 | if (o != null) { 51 | o.activeProperty().set(false); 52 | } 53 | if (n != null) { 54 | n.activeProperty().set(true); 55 | } 56 | }); 57 | } 58 | 59 | @Override 60 | public void start(Stage stage) throws Exception { 61 | Scene scene = skinService.bind(new Scene(mainFrame.getRoot())); 62 | scene.getStylesheets().add("/css/global.css"); 63 | stage.setScene(scene); 64 | 65 | features.forEach(f -> f.bind(stage)); 66 | 67 | stage.titleProperty() 68 | .bind(map(activeEditorProperty(), e -> (e == null ? "" : e.fileProperty().get().getFileName()) + " - CSS Editor FX")); 69 | stage.setOnCloseRequest(consume(e -> fire(fileActions.exit()))); 70 | stage.setMaximized(true); 71 | stage.show(); 72 | 73 | stage.getIcons().add(new Image(FxCssApplication.class.getClassLoader().getResourceAsStream("image/icon.jpg"))); 74 | } 75 | 76 | @Override 77 | public ObservableList editorList() { 78 | return editors; 79 | } 80 | 81 | @Override 82 | public ObjectProperty<@CheckNull CssEditor> activeEditorProperty() { 83 | return activeEditor; 84 | } 85 | 86 | @Override 87 | public Stage stage() { 88 | return stage; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/FxCssEntrance.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | 5 | import xdean.jfx.spring.annotation.Splash; 6 | import xdean.jfx.spring.annotation.SpringFxApplication; 7 | 8 | @Splash 9 | @SpringFxApplication 10 | public class FxCssEntrance { 11 | public static void main(String[] args) { 12 | SpringApplication.run(FxCssEntrance.class, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/Config.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | import java.nio.file.Paths; 7 | import java.util.Optional; 8 | import java.util.Properties; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | import javax.annotation.PostConstruct; 12 | import javax.inject.Inject; 13 | 14 | import org.springframework.stereotype.Service; 15 | 16 | import io.reactivex.subjects.Subject; 17 | import io.reactivex.subjects.UnicastSubject; 18 | import xdean.jex.log.Logable; 19 | import xdean.jfx.spring.splash.PreloadReporter; 20 | import xdean.jfx.spring.splash.PreloadReporter.SubReporter; 21 | 22 | @Service 23 | public class Config implements Logable { 24 | private static final Path CONFIG_FILE = Context.HOME_PATH.resolve("config.properties"); 25 | private static final Path DEFAULT_CONFIG_PATH = Paths.get("/default_config.properties"); 26 | 27 | private final Properties properties = new Properties(); 28 | private final Subject saveSubject = UnicastSubject.create(); 29 | private @Inject PreloadReporter preload; 30 | 31 | @PostConstruct 32 | public void init() { 33 | try { 34 | SubReporter sub = preload.load("Loading settings..."); 35 | sub.setCount(3); 36 | sub.load("Check user setting"); 37 | if (Files.notExists(CONFIG_FILE)) { 38 | if (Files.exists(DEFAULT_CONFIG_PATH)) { 39 | Files.copy(DEFAULT_CONFIG_PATH, CONFIG_FILE); 40 | } else { 41 | Files.createFile(CONFIG_FILE); 42 | } 43 | } 44 | sub.load("Loading settings file"); 45 | properties.load(Files.newBufferedReader(CONFIG_FILE)); 46 | debug("Load last config: " + properties.toString()); 47 | sub.load("Apply settings"); 48 | saveSubject 49 | .debounce(1000, TimeUnit.MILLISECONDS) 50 | .subscribe(e -> saveToFile()); 51 | } catch (IOException e) { 52 | error("IOException", e); 53 | } 54 | } 55 | 56 | public Optional getProperty(String key) { 57 | return Optional.ofNullable(properties.getProperty(key)); 58 | } 59 | 60 | public Optional getProperty(Object key) { 61 | return getProperty(key.toString()); 62 | } 63 | 64 | public String getProperty(Object key, String defaultValue) { 65 | return getProperty(key.toString(), defaultValue); 66 | } 67 | 68 | public String getProperty(String key, String defaultValue) { 69 | return getProperty(key).orElse(defaultValue); 70 | } 71 | 72 | public void setProperty(Object key, String value) { 73 | setProperty(key.toString(), value); 74 | } 75 | 76 | public void setProperty(String key, String value) { 77 | properties.setProperty(key, value); 78 | saveSubject.onNext(key); 79 | } 80 | 81 | public void setIfAbsent(Object key, String value) { 82 | setIfAbsent(key.toString(), value); 83 | } 84 | 85 | public void setIfAbsent(String key, String value) { 86 | if (getProperty(key).isPresent() == false) { 87 | setProperty(key, value); 88 | } 89 | } 90 | 91 | public synchronized void saveToFile() { 92 | try { 93 | properties.store(Files.newOutputStream(CONFIG_FILE), ""); 94 | } catch (IOException e) { 95 | error(e); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/Context.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context; 2 | 3 | import java.io.IOException; 4 | import java.lang.Thread.UncaughtExceptionHandler; 5 | import java.nio.file.Path; 6 | import java.nio.file.Paths; 7 | 8 | import javax.inject.Inject; 9 | 10 | import org.controlsfx.glyphfont.FontAwesome; 11 | import org.controlsfx.glyphfont.GlyphFontRegistry; 12 | import org.springframework.boot.context.event.ApplicationPreparedEvent; 13 | import org.springframework.context.ApplicationListener; 14 | import org.springframework.context.annotation.Configuration; 15 | 16 | import com.sun.javafx.util.Logging; 17 | 18 | import javafx.application.Platform; 19 | import sun.util.logging.PlatformLogger.Level; 20 | import xdean.css.editor.service.DialogService; 21 | import xdean.jex.log.Logable; 22 | import xdean.jex.util.file.FileUtil; 23 | import xdean.jfx.spring.context.FxContextPostProcessor; 24 | import xdean.spring.auto.AutoSpringFactories; 25 | 26 | @Configuration 27 | @AutoSpringFactories(ApplicationListener.class) 28 | public class Context implements FxContextPostProcessor, ApplicationListener, 29 | UncaughtExceptionHandler, Logable { 30 | public static final Path HOME_PATH = Paths.get(System.getProperty("user.home"), ".xdean", "css"); 31 | public static final Path TEMP_PATH = HOME_PATH.resolve("temp"); 32 | public static final Path LAST_FILE_PATH = Context.TEMP_PATH.resolve("last"); 33 | 34 | private @Inject DialogService messageService; 35 | 36 | @Override 37 | public void onApplicationEvent(ApplicationPreparedEvent event) { 38 | debug("Setup Css Editor Environment"); 39 | // create directories 40 | try { 41 | FileUtil.createDirectory(HOME_PATH); 42 | FileUtil.createDirectory(TEMP_PATH); 43 | } catch (IOException e) { 44 | error("Create home path fail.", e); 45 | } 46 | // close CSS logger 47 | Logging.getCSSLogger().setLevel(Level.OFF); 48 | 49 | // To ensure font awesome loaded 50 | GlyphFontRegistry.register(new FontAwesome(getClass().getResourceAsStream("/fontawesome.ttf"))); 51 | GlyphFontRegistry.font("FontAwesome"); 52 | } 53 | 54 | @Override 55 | public void beforeStart() { 56 | // handle uncaught exception 57 | Thread.setDefaultUncaughtExceptionHandler(this); 58 | Platform.runLater(() -> Thread.currentThread().setUncaughtExceptionHandler(this)); 59 | } 60 | 61 | @Override 62 | public void uncaughtException(Thread t, Throwable e) { 63 | error("Uncaught exception", e); 64 | if (e instanceof Error) { 65 | System.exit(1); 66 | } 67 | if (Platform.isFxApplicationThread()) { 68 | handleError(e); 69 | } else { 70 | Platform.runLater(() -> handleError(e)); 71 | } 72 | } 73 | 74 | private void handleError(Throwable e) { 75 | messageService.errorDialog(e).show(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/setting/DefaultValue.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context.setting; 2 | 3 | import java.nio.charset.Charset; 4 | import java.util.Locale; 5 | 6 | import xdean.jex.util.string.StringUtil; 7 | import xdean.jfxex.support.skin.SkinStyle; 8 | 9 | public interface DefaultValue { 10 | int DEFAULT_FONT_SIZE = 16; 11 | int MIN_FONT_SIZE = 8; 12 | int MAX_FONT_SIZE = 36; 13 | String DEFAULT_FONT_FAMILY = "Monospaced"; 14 | Locale DEFAULT_LOCALE = Locale.ENGLISH; 15 | Charset DEFAULT_CHARSET = Charset.defaultCharset(); 16 | String[] DEF_FONT_FAMILIES = { 17 | "Consolas", 18 | "DejaVu Sans Mono", 19 | "Lucida Sans Typewriter", 20 | "Lucida Console", 21 | }; 22 | 23 | enum DefaultSkin implements SkinStyle { 24 | CLASSIC("classic"), 25 | DEFAULT("default"), 26 | METAL("metal"), 27 | PINK("pink"); 28 | 29 | private static final String CSS_PATH = "/css/skin/"; 30 | 31 | private String path; 32 | 33 | private DefaultSkin(String name) { 34 | try { 35 | this.path = DefaultSkin.class.getResource(CSS_PATH + name + ".bss").toExternalForm(); 36 | } catch (NullPointerException e) {// If the resource not exist 37 | this.path = DefaultSkin.class.getResource(CSS_PATH + name + ".css").toExternalForm(); 38 | } 39 | } 40 | 41 | @Override 42 | public String getURL() { 43 | return path; 44 | } 45 | 46 | @Override 47 | public String getName() { 48 | return StringUtil.upperFirst(toString()); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/setting/EditActions.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context.setting; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | import javafx.scene.input.KeyCode; 7 | import javafx.scene.input.KeyCodeCombination; 8 | import javafx.scene.input.KeyCombination; 9 | import xdean.css.editor.context.setting.SettingKeys.Edit; 10 | import xdean.css.editor.context.setting.model.CssEditorActionKeyOption; 11 | 12 | @Configuration 13 | public class EditActions { 14 | 15 | @Bean(Edit.UNDO) 16 | public CssEditorActionKeyOption undo() { 17 | return new CssEditorActionKeyOption(Edit.UNDO, new KeyCodeCombination(KeyCode.Z, KeyCombination.CONTROL_DOWN)); 18 | } 19 | 20 | @Bean(Edit.REDO) 21 | public CssEditorActionKeyOption redo() { 22 | return new CssEditorActionKeyOption(Edit.REDO, new KeyCodeCombination(KeyCode.Y, KeyCombination.CONTROL_DOWN)); 23 | } 24 | 25 | @Bean(Edit.SUGGEST) 26 | public CssEditorActionKeyOption suggest() { 27 | return new CssEditorActionKeyOption(Edit.SUGGEST, new KeyCodeCombination(KeyCode.SLASH, KeyCombination.ALT_DOWN)); 28 | } 29 | 30 | @Bean(Edit.FORMAT) 31 | public CssEditorActionKeyOption format() { 32 | return new CssEditorActionKeyOption(Edit.FORMAT, 33 | new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN)); 34 | } 35 | 36 | @Bean(Edit.COMMENT) 37 | public CssEditorActionKeyOption comment() { 38 | return new CssEditorActionKeyOption(Edit.COMMENT, new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN)); 39 | } 40 | 41 | @Bean(Edit.FIND) 42 | public CssEditorActionKeyOption find() { 43 | return new CssEditorActionKeyOption(Edit.FIND, new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/setting/FileActions.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context.setting; 2 | 3 | import java.nio.file.Path; 4 | 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | import javafx.scene.input.KeyCode; 9 | import javafx.scene.input.KeyCodeCombination; 10 | import javafx.scene.input.KeyCombination; 11 | import xdean.css.editor.context.setting.SettingKeys.File; 12 | import xdean.css.editor.context.setting.model.CssEditorActionKeyOption; 13 | import xdean.css.editor.context.setting.model.CssEditorKeyOption; 14 | 15 | @Configuration 16 | public class FileActions { 17 | 18 | @Bean(File.NEW) 19 | public CssEditorActionKeyOption newFile() { 20 | return new CssEditorActionKeyOption(File.NEW, new KeyCodeCombination(KeyCode.N, KeyCombination.CONTROL_DOWN)); 21 | } 22 | 23 | @Bean(File.OPEN) 24 | public CssEditorKeyOption open() { 25 | return new CssEditorKeyOption<>(File.OPEN, new KeyCodeCombination(KeyCode.O, KeyCombination.CONTROL_DOWN)); 26 | } 27 | 28 | @Bean(File.SAVE) 29 | public CssEditorActionKeyOption save() { 30 | return new CssEditorActionKeyOption(File.SAVE, new KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN)); 31 | } 32 | 33 | @Bean(File.SAVE_AS) 34 | public CssEditorKeyOption saveAs() { 35 | return new CssEditorKeyOption<>(File.SAVE_AS, 36 | new KeyCodeCombination(KeyCode.S, KeyCombination.ALT_DOWN, KeyCombination.CONTROL_DOWN)); 37 | } 38 | 39 | @Bean(File.CLOSE) 40 | public CssEditorActionKeyOption close() { 41 | return new CssEditorActionKeyOption(File.CLOSE, new KeyCodeCombination(KeyCode.W, KeyCombination.CONTROL_DOWN)); 42 | } 43 | 44 | @Bean(File.REVERT) 45 | public CssEditorActionKeyOption revert() { 46 | return new CssEditorActionKeyOption(File.REVERT, KeyCodeCombination.NO_MATCH); 47 | } 48 | 49 | @Bean(File.EXIT) 50 | public CssEditorActionKeyOption exit() { 51 | return new CssEditorActionKeyOption(File.EXIT, KeyCodeCombination.NO_MATCH); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/setting/HelpActions.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context.setting; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | import javafx.scene.input.KeyCodeCombination; 7 | import xdean.css.editor.context.setting.SettingKeys.Help; 8 | import xdean.css.editor.context.setting.model.CssEditorActionKeyOption; 9 | 10 | @Configuration 11 | public class HelpActions { 12 | 13 | @Bean(Help.SETTINGS) 14 | public CssEditorActionKeyOption settings() { 15 | return new CssEditorActionKeyOption(Help.SETTINGS, KeyCodeCombination.NO_MATCH); 16 | } 17 | @Bean(Help.ABOUT) 18 | public CssEditorActionKeyOption about() { 19 | return new CssEditorActionKeyOption(Help.ABOUT, KeyCodeCombination.NO_MATCH); 20 | } 21 | @Bean(Help.HELP) 22 | public CssEditorActionKeyOption help() { 23 | return new CssEditorActionKeyOption(Help.HELP, KeyCodeCombination.NO_MATCH); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/setting/OptionBindProcessor.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context.setting; 2 | 3 | import javax.inject.Inject; 4 | 5 | import org.springframework.beans.BeansException; 6 | import org.springframework.beans.factory.config.BeanPostProcessor; 7 | import org.springframework.stereotype.Component; 8 | 9 | import xdean.css.editor.context.Config; 10 | import xdean.css.editor.context.setting.model.option.Option; 11 | 12 | @Component 13 | public class OptionBindProcessor implements BeanPostProcessor { 14 | 15 | @Inject 16 | Config config; 17 | 18 | @Override 19 | public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { 20 | if (bean instanceof Option) { 21 | ((Option) bean).bind(config); 22 | } 23 | return bean; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/setting/OtherSettings.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context.setting; 2 | 3 | import static xdean.css.editor.context.setting.SettingKeys.LANGUAGE; 4 | import static xdean.css.editor.context.setting.SettingKeys.RECENT_LOC; 5 | import static xdean.css.editor.context.setting.SettingKeys.SKIN; 6 | 7 | import java.util.Locale; 8 | 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | import xdean.css.editor.context.setting.DefaultValue.DefaultSkin; 13 | import xdean.css.editor.context.setting.SettingKeys.Find; 14 | import xdean.css.editor.context.setting.model.option.BooleanOption; 15 | import xdean.css.editor.context.setting.model.option.Option; 16 | import xdean.css.editor.context.setting.model.option.SimpleOption; 17 | import xdean.css.editor.context.setting.model.option.StringOption; 18 | import xdean.jfxex.util.StringConverters; 19 | 20 | @Configuration 21 | public class OtherSettings { 22 | 23 | @Bean(LANGUAGE) 24 | public Option locale() { 25 | return new SimpleOption<>(LANGUAGE, DefaultValue.DEFAULT_LOCALE, StringConverters.create(Locale::forLanguageTag)); 26 | } 27 | 28 | @Bean(SKIN) 29 | public StringOption skin() { 30 | return new StringOption(SKIN, DefaultSkin.CLASSIC.getName()); 31 | } 32 | 33 | @Bean(RECENT_LOC) 34 | public StringOption recent() { 35 | return new StringOption(RECENT_LOC, ""); 36 | } 37 | 38 | @Bean(Find.WRAP_SEARCH) 39 | public BooleanOption wrapSearch() { 40 | return new BooleanOption(Find.WRAP_SEARCH, true); 41 | } 42 | 43 | @Bean(Find.REGEX) 44 | public BooleanOption regexSearch() { 45 | return new BooleanOption(Find.REGEX, false); 46 | } 47 | 48 | @Bean(Find.CASE_SENSITIVE) 49 | public BooleanOption caseSensitive() { 50 | return new BooleanOption(Find.CASE_SENSITIVE, false); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/setting/PreferenceSettings.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context.setting; 2 | 3 | import static xdean.css.editor.context.setting.SettingKeys.GENERAL; 4 | 5 | import java.nio.charset.Charset; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | import com.google.common.collect.BoundType; 13 | 14 | import javafx.scene.text.Font; 15 | import xdean.css.editor.context.setting.SettingKeys.General; 16 | import xdean.css.editor.context.setting.model.option.BooleanOption; 17 | import xdean.css.editor.context.setting.model.option.IntegerOption; 18 | import xdean.css.editor.context.setting.model.option.OptionGroup; 19 | import xdean.css.editor.context.setting.model.option.ValueOption; 20 | import xdean.jfxex.util.StringConverters; 21 | 22 | @Configuration 23 | public class PreferenceSettings { 24 | 25 | @Bean(GENERAL) 26 | public OptionGroup general() { 27 | OptionGroup general = new OptionGroup(GENERAL); 28 | general.add(common()); 29 | general.add(text()); 30 | return general; 31 | } 32 | 33 | @Bean(General.COMMON) 34 | public OptionGroup common() { 35 | OptionGroup common = new OptionGroup(General.COMMON); 36 | common.add(autoSuggest()); 37 | common.add(showLineNo()); 38 | common.add(openLast()); 39 | common.add(charset()); 40 | return common; 41 | } 42 | 43 | @Bean(General.Common.AUTO_SUGGEST) 44 | public BooleanOption autoSuggest() { 45 | return new BooleanOption(General.Common.AUTO_SUGGEST, true); 46 | } 47 | 48 | @Bean(General.Common.SHOW_LINE) 49 | public BooleanOption showLineNo() { 50 | return new BooleanOption(General.Common.SHOW_LINE, true); 51 | } 52 | 53 | @Bean(General.Common.OPEN_LAST) 54 | public BooleanOption openLast() { 55 | return new BooleanOption(General.Common.OPEN_LAST, true); 56 | } 57 | 58 | @Bean(General.Common.CHARSET) 59 | public ValueOption charset() { 60 | ValueOption charset = new ValueOption<>(General.Common.CHARSET, DefaultValue.DEFAULT_CHARSET, 61 | StringConverters.create(this::safeCharSet)); 62 | charset.values.setAll(Charset.availableCharsets().values()); 63 | return charset; 64 | } 65 | 66 | @Bean(General.TEXT) 67 | public OptionGroup text() { 68 | OptionGroup text = new OptionGroup(General.TEXT); 69 | text.add(fontFamily()); 70 | text.add(fontSize()); 71 | text.add(wrapText()); 72 | return text; 73 | } 74 | 75 | @Bean(General.Text.FONT_FAMILY) 76 | public ValueOption fontFamily() { 77 | ValueOption fontFamily = new ValueOption<>(General.Text.FONT_FAMILY, DefaultValue.DEFAULT_FONT_FAMILY, 78 | StringConverters.create(this::safeFontFamily)); 79 | fontFamily.values.setAll(Arrays.asList(DefaultValue.DEF_FONT_FAMILIES)); 80 | fontFamily.values.addAll(Font.getFamilies()); 81 | return fontFamily; 82 | } 83 | 84 | @Bean(General.Text.FONT_SIZE) 85 | public IntegerOption fontSize() { 86 | IntegerOption fontSize = new IntegerOption(General.Text.FONT_SIZE, DefaultValue.DEFAULT_FONT_SIZE); 87 | fontSize.setRange(DefaultValue.MIN_FONT_SIZE, BoundType.CLOSED, DefaultValue.MAX_FONT_SIZE, BoundType.CLOSED); 88 | return fontSize; 89 | } 90 | 91 | @Bean(General.Text.WRAP_TEXT) 92 | public BooleanOption wrapText() { 93 | return new BooleanOption(General.Text.WRAP_TEXT, true); 94 | } 95 | 96 | /** 97 | * Check whether font family is null or invalid (family not available on system) and search for an 98 | * available family. 99 | */ 100 | private String safeFontFamily(String fontFamily) { 101 | List fontFamilies = Font.getFamilies(); 102 | if (fontFamily != null && fontFamilies.contains(fontFamily)) { 103 | return fontFamily; 104 | } 105 | 106 | for (String family : DefaultValue.DEF_FONT_FAMILIES) { 107 | if (fontFamilies.contains(family)) { 108 | return family; 109 | } 110 | } 111 | return DefaultValue.DEFAULT_FONT_FAMILY; 112 | } 113 | 114 | private Charset safeCharSet(String charsetName) { 115 | if (Charset.isSupported(charsetName)) { 116 | return Charset.forName(charsetName); 117 | } else { 118 | return Charset.defaultCharset(); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/setting/RawSettingKeys.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context.setting; 2 | 3 | import java.lang.String; 4 | import javax.annotation.Generated; 5 | 6 | @Generated("xdean.auto.message.AutoMessageProcessor") 7 | public interface RawSettingKeys { 8 | String OPTION_GENERAL = "option.general"; 9 | 10 | String OPTION_GENERAL_COMMON = "option.general.common"; 11 | 12 | String OPTION_GENERAL_COMMON_AUTO_SUGGEST = "option.general.common.auto-suggest"; 13 | 14 | String OPTION_GENERAL_COMMON_SHOW_LINE = "option.general.common.show-line"; 15 | 16 | String OPTION_GENERAL_COMMON_OPEN_LAST = "option.general.common.open-last"; 17 | 18 | String OPTION_GENERAL_COMMON_CHARSET = "option.general.common.charset"; 19 | 20 | String OPTION_GENERAL_TEXT = "option.general.text"; 21 | 22 | String OPTION_GENERAL_TEXT_FONT_FAMILY = "option.general.text.font-family"; 23 | 24 | String OPTION_GENERAL_TEXT_FONT_SIZE = "option.general.text.font-size"; 25 | 26 | String OPTION_GENERAL_TEXT_WRAP_TEXT = "option.general.text.wrap-text"; 27 | 28 | String OPTION_KEY = "option.key"; 29 | 30 | String OPTION_FIND_REGEX = "option.find.regex"; 31 | 32 | String OPTION_FIND_WRAP_SEARCH = "option.find.wrap-search"; 33 | 34 | String OPTION_FIND_CASE_SENSITIVE = "option.find.case-sensitive"; 35 | 36 | String OPTION_LANGUAGE = "option.language"; 37 | 38 | String OPTION_SKIN = "option.skin"; 39 | 40 | String OPTION_RECENT_LOCATION = "option.recent.location"; 41 | 42 | String EDIT_UNDO = "edit.undo"; 43 | 44 | String EDIT_REDO = "edit.redo"; 45 | 46 | String EDIT_SUGGEST = "edit.suggest"; 47 | 48 | String EDIT_FORMAT = "edit.format"; 49 | 50 | String EDIT_COMMENT = "edit.comment"; 51 | 52 | String EDIT_FIND = "edit.find"; 53 | 54 | String FILE_NEW = "file.new"; 55 | 56 | String FILE_OPEN = "file.open"; 57 | 58 | String FILE_SAVE = "file.save"; 59 | 60 | String FILE_SAVEAS = "file.saveAs"; 61 | 62 | String FILE_CLOSE = "file.close"; 63 | 64 | String FILE_REVERT = "file.revert"; 65 | 66 | String FILE_EXIT = "file.exit"; 67 | 68 | String HELP_SETTINGS = "help.settings"; 69 | 70 | String HELP_ABOUT = "help.about"; 71 | 72 | String HELP_HELP = "help.help"; 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/setting/SettingKeys.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context.setting; 2 | 3 | import static xdean.css.editor.context.setting.RawSettingKeys.*; 4 | 5 | import xdean.auto.message.AutoMessage; 6 | 7 | @AutoMessage(path = "/message/settings.properties", generatedName = "RawSettingKeysStub") 8 | public interface SettingKeys { 9 | String GENERAL = OPTION_GENERAL; 10 | 11 | interface General { 12 | 13 | String COMMON = OPTION_GENERAL_COMMON; 14 | 15 | interface Common { 16 | String AUTO_SUGGEST = OPTION_GENERAL_COMMON_AUTO_SUGGEST; 17 | String SHOW_LINE = OPTION_GENERAL_COMMON_SHOW_LINE; 18 | String OPEN_LAST = OPTION_GENERAL_COMMON_OPEN_LAST; 19 | String CHARSET = OPTION_GENERAL_COMMON_CHARSET; 20 | } 21 | 22 | String TEXT = OPTION_GENERAL_TEXT; 23 | 24 | interface Text { 25 | String FONT_FAMILY = OPTION_GENERAL_TEXT_FONT_FAMILY; 26 | String FONT_SIZE = OPTION_GENERAL_TEXT_FONT_SIZE; 27 | String WRAP_TEXT = OPTION_GENERAL_TEXT_WRAP_TEXT; 28 | } 29 | } 30 | 31 | interface Find { 32 | String REGEX = OPTION_FIND_REGEX; 33 | String WRAP_SEARCH = OPTION_FIND_WRAP_SEARCH; 34 | String CASE_SENSITIVE = OPTION_FIND_CASE_SENSITIVE; 35 | } 36 | 37 | String LANGUAGE = OPTION_LANGUAGE; 38 | String SKIN = OPTION_SKIN; 39 | String RECENT_LOC = OPTION_RECENT_LOCATION; 40 | 41 | interface File { 42 | String NEW = FILE_NEW; 43 | String OPEN = FILE_OPEN; 44 | String SAVE = FILE_SAVE; 45 | String SAVE_AS = FILE_SAVEAS; 46 | String CLOSE = FILE_CLOSE; 47 | String REVERT = FILE_REVERT; 48 | String EXIT = FILE_EXIT; 49 | } 50 | 51 | interface Edit { 52 | String UNDO = EDIT_UNDO; 53 | String REDO = EDIT_REDO; 54 | String SUGGEST = EDIT_SUGGEST; 55 | String FORMAT = EDIT_FORMAT; 56 | String COMMENT = EDIT_COMMENT; 57 | String FIND = EDIT_FIND; 58 | } 59 | 60 | interface Help { 61 | String SETTINGS = HELP_SETTINGS; 62 | String ABOUT = HELP_ABOUT; 63 | String HELP = HELP_HELP; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/setting/model/CssEditorActionKeyOption.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context.setting.model; 2 | 3 | import javafx.scene.input.KeyCombination; 4 | 5 | public class CssEditorActionKeyOption extends CssEditorKeyOption { 6 | public CssEditorActionKeyOption(String key, KeyCombination defaultValue) { 7 | super(key, defaultValue); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/setting/model/CssEditorKeyOption.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context.setting.model; 2 | 3 | import static xdean.jfxex.event.EventHandlers.consumeIf; 4 | 5 | import io.reactivex.rxjavafx.observables.JavaFxObservable; 6 | import javafx.beans.property.BooleanProperty; 7 | import javafx.beans.property.Property; 8 | import javafx.beans.property.SimpleBooleanProperty; 9 | import javafx.event.EventType; 10 | import javafx.scene.input.KeyCombination; 11 | import javafx.scene.input.KeyEvent; 12 | import javafx.util.StringConverter; 13 | import xdean.css.editor.context.setting.model.option.Option; 14 | import xdean.css.editor.control.CssEditor; 15 | import xdean.css.editor.control.CssEditorEvent; 16 | import xdean.jfxex.bean.property.ObjectPropertyEX; 17 | import xdean.jfxex.util.StringConverters; 18 | 19 | public class CssEditorKeyOption implements Option { 20 | 21 | private static final StringConverter CONVERTER = StringConverters.create(KeyCombination::valueOf); 22 | 23 | private final String key; 24 | private final EventType> eventType; 25 | private final ObjectPropertyEX value = new ObjectPropertyEX<>(this, "value"); 26 | private final KeyCombination defaultValue; 27 | private final BooleanProperty disable = new SimpleBooleanProperty(this, "disable", false); 28 | 29 | public CssEditorKeyOption(String key, KeyCombination defaultValue) { 30 | this.key = key; 31 | this.defaultValue = defaultValue; 32 | this.eventType = new EventType<>(CssEditorEvent.ANY, key); 33 | this.value.defaultForNull(KeyCombination.NO_MATCH); 34 | } 35 | 36 | @Override 37 | public String getKey() { 38 | return key; 39 | } 40 | 41 | public EventType> getEventType() { 42 | return eventType; 43 | } 44 | 45 | public BooleanProperty disableProperty() { 46 | return disable; 47 | } 48 | 49 | @Override 50 | public Property valueProperty() { 51 | return value; 52 | } 53 | 54 | @Override 55 | public KeyCombination getDefaultValue() { 56 | return defaultValue; 57 | } 58 | 59 | @Override 60 | public StringConverter getConverter() { 61 | return CONVERTER; 62 | } 63 | 64 | public void bind(CssEditor editor) { 65 | editor.addEventFilter(eventType, consumeIf(e -> disable.get())); 66 | JavaFxObservable.eventsOf(editor, KeyEvent.KEY_PRESSED) 67 | .filter(e -> getValue().match(e)) 68 | .doOnNext(KeyEvent::consume) 69 | .subscribe(e -> editor.fireEvent(getEvent(editor))); 70 | } 71 | 72 | public CssEditorEvent getEvent(CssEditor editor) { 73 | return new CssEditorEvent<>(editor, getEventType()); 74 | } 75 | 76 | public CssEditorEvent getEvent(CssEditor editor, T data) { 77 | return new CssEditorEvent<>(editor, getEventType(), data); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/setting/model/option/BooleanOption.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context.setting.model.option; 2 | 3 | import static xdean.jex.util.cache.CacheUtil.cache; 4 | 5 | import javafx.beans.property.BooleanProperty; 6 | import xdean.jfxex.bean.BeanConvertUtil; 7 | import xdean.jfxex.util.StringConverters; 8 | 9 | public class BooleanOption extends SimpleOption { 10 | public BooleanOption(String key, boolean defaultValue) { 11 | super(key, defaultValue, StringConverters.create(Boolean::valueOf)); 12 | } 13 | 14 | public BooleanProperty booleanProperty() { 15 | return cache(this, () -> BeanConvertUtil.toBoolean(valueProperty())); 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/setting/model/option/ConstraintOption.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context.setting.model.option; 2 | 3 | public interface ConstraintOption extends Option { 4 | boolean isValid(T t); 5 | 6 | @Override 7 | public default void setValue(T t) { 8 | if (isValid(t)) { 9 | valueProperty().setValue(t); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/setting/model/option/IntegerOption.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context.setting.model.option; 2 | 3 | import static xdean.jex.util.cache.CacheUtil.cache; 4 | import static xdean.jfxex.bean.BeanConvertUtil.toInteger; 5 | 6 | import javafx.beans.property.IntegerProperty; 7 | import xdean.jfxex.util.StringConverters; 8 | 9 | public class IntegerOption extends RangeOption { 10 | 11 | public IntegerOption(String key, int defaultValue) { 12 | super(key, defaultValue, StringConverters.forInteger(defaultValue)); 13 | } 14 | 15 | public IntegerProperty intProperty() { 16 | return cache(this, () -> toInteger(valueProperty())); 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/setting/model/option/Option.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context.setting.model.option; 2 | 3 | import javafx.beans.property.Property; 4 | import javafx.util.StringConverter; 5 | import xdean.css.editor.context.Config; 6 | import xdean.jex.extra.tryto.Try; 7 | 8 | public interface Option { 9 | 10 | Property valueProperty(); 11 | 12 | default T getValue() { 13 | return valueProperty().getValue(); 14 | } 15 | 16 | default void setValue(T t) { 17 | valueProperty().setValue(t); 18 | } 19 | 20 | T getDefaultValue(); 21 | 22 | String getKey(); 23 | 24 | StringConverter getConverter(); 25 | 26 | default void bind(Config config) { 27 | StringConverter converter = getConverter(); 28 | valueProperty() 29 | .setValue(Try.to(() -> config.getProperty(getKey()).map(converter::fromString).get()).getOrElse(getDefaultValue())); 30 | valueProperty().addListener((ob, o, n) -> config.setProperty(getKey(), converter.toString(n))); 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/setting/model/option/OptionGroup.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context.setting.model.option; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.stream.Collectors; 7 | 8 | import xdean.jex.extra.collection.Either; 9 | import xdean.jex.util.cache.CacheUtil; 10 | 11 | public class OptionGroup { 12 | 13 | List, OptionGroup>> list; 14 | String key; 15 | 16 | public OptionGroup(String key) { 17 | this.key = key; 18 | list = new ArrayList<>(); 19 | } 20 | 21 | public > T add(T o) { 22 | list.add(Either.left(o)); 23 | return o; 24 | } 25 | 26 | public OptionGroup add(OptionGroup og) { 27 | list.add(Either.right(og)); 28 | return og; 29 | } 30 | 31 | public List, OptionGroup>> getChildren() { 32 | return CacheUtil.cache(this, () -> Collections.unmodifiableList(list)); 33 | } 34 | 35 | @SuppressWarnings("unchecked") 36 | public List> getChildrenWithValueType(Class clz) { 37 | return list.stream() 38 | .filter(e -> e.isLeft()) 39 | .map(e -> e.getLeft()) 40 | .filter(o -> clz.isInstance(o.getValue())) 41 | .map(o -> (Option) o) 42 | .collect(Collectors.toList()); 43 | } 44 | 45 | @SuppressWarnings("unchecked") 46 | public > List getChildrenWithType(Class clz) { 47 | return list.stream() 48 | .filter(e -> e.isLeft()) 49 | .map(e -> e.getLeft()) 50 | .filter(o -> clz.isInstance(o)) 51 | .map(o -> (T) o) 52 | .collect(Collectors.toList()); 53 | } 54 | 55 | public String getKey() { 56 | return key; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/setting/model/option/RangeOption.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context.setting.model.option; 2 | 3 | import com.google.common.collect.BoundType; 4 | import com.google.common.collect.Range; 5 | 6 | import javafx.util.StringConverter; 7 | 8 | public abstract class RangeOption> extends SimpleConstraintOption { 9 | 10 | Range range; 11 | 12 | RangeOption(String key, T defaultValue, StringConverter converter) { 13 | super(key, defaultValue, converter); 14 | range = Range.all(); 15 | } 16 | 17 | @Override 18 | public boolean isValid(T t) { 19 | return range.contains(t); 20 | } 21 | 22 | public Range getRange() { 23 | return range; 24 | } 25 | 26 | public T getMin() { 27 | return range.lowerEndpoint(); 28 | } 29 | 30 | public void setMin(T min, BoundType type) { 31 | range = Range.range(min, type, range.upperEndpoint(), range.upperBoundType()); 32 | } 33 | 34 | public T getMax() { 35 | return range.upperEndpoint(); 36 | } 37 | 38 | public void setMax(T max, BoundType type) { 39 | range = Range.range(range.lowerEndpoint(), range.lowerBoundType(), max, type); 40 | } 41 | 42 | public void setRange(T min, BoundType minType, T max, BoundType maxType) { 43 | range = Range.range(min, minType, max, maxType); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/setting/model/option/SimpleConstraintOption.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context.setting.model.option; 2 | 3 | import javafx.util.StringConverter; 4 | 5 | public abstract class SimpleConstraintOption extends SimpleOption implements ConstraintOption { 6 | 7 | SimpleConstraintOption(String key, T defaultValue, StringConverter conveter) { 8 | super(key, defaultValue, conveter); 9 | } 10 | 11 | @Override 12 | public void setValue(T t) { 13 | if (isValid(t)) { 14 | valueProperty().setValue(t); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/setting/model/option/SimpleOption.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context.setting.model.option; 2 | 3 | import javafx.util.StringConverter; 4 | import xdean.jfxex.bean.property.ObjectPropertyEX; 5 | 6 | public class SimpleOption implements Option { 7 | 8 | private final String key; 9 | private final ObjectPropertyEX value = new ObjectPropertyEX<>(this, "value"); 10 | private final T defaultValue; 11 | private final StringConverter converter; 12 | 13 | public SimpleOption(String key, T defaultValue, StringConverter converter) { 14 | this.defaultValue = defaultValue; 15 | this.key = key; 16 | this.converter = converter; 17 | 18 | value.defaultForNull(defaultValue); 19 | } 20 | 21 | @Override 22 | public String getKey() { 23 | return key; 24 | } 25 | 26 | @Override 27 | public ObjectPropertyEX valueProperty() { 28 | return value; 29 | } 30 | 31 | @Override 32 | public T getDefaultValue() { 33 | return defaultValue; 34 | } 35 | 36 | @Override 37 | public StringConverter getConverter() { 38 | return converter; 39 | } 40 | 41 | @Override 42 | public String toString() { 43 | return "Option [property=" + value + ", defaultValue=" + defaultValue + ", key=" + key + "]"; 44 | } 45 | } -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/setting/model/option/StringOption.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context.setting.model.option; 2 | 3 | import java.util.function.UnaryOperator; 4 | 5 | import javafx.beans.property.StringProperty; 6 | import xdean.jfxex.bean.BeanConvertUtil; 7 | import xdean.jfxex.util.StringConverters; 8 | 9 | public class StringOption extends SimpleOption { 10 | 11 | public StringOption(String key, String defaultValue) { 12 | super(key, defaultValue, StringConverters.create(UnaryOperator.identity())); 13 | } 14 | 15 | public StringProperty stringProperty() { 16 | return BeanConvertUtil.toString(valueProperty()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/context/setting/model/option/ValueOption.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.context.setting.model.option; 2 | 3 | import javafx.collections.FXCollections; 4 | import javafx.collections.ObservableList; 5 | import javafx.util.StringConverter; 6 | 7 | public class ValueOption extends SimpleConstraintOption { 8 | 9 | public final ObservableList values = FXCollections.observableArrayList(); 10 | 11 | public ValueOption(String key, T defaultValue, StringConverter conveter) { 12 | super(key, defaultValue, conveter); 13 | } 14 | 15 | @Override 16 | public boolean isValid(T t) { 17 | return values.contains(t); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/control/CssEditorEvent.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.control; 2 | 3 | import java.util.Optional; 4 | 5 | import javax.annotation.Nullable; 6 | 7 | import javafx.event.Event; 8 | import javafx.event.EventType; 9 | 10 | public class CssEditorEvent extends Event { 11 | 12 | public static final EventType> ANY = new EventType<>(Event.ANY, "css-editor"); 13 | 14 | private final Optional editor; 15 | private final Optional data; 16 | 17 | public CssEditorEvent(CssEditor editor, EventType> eventType, @Nullable T data) { 18 | super(editor, editor, eventType); 19 | this.editor = Optional.ofNullable(editor); 20 | this.data = Optional.ofNullable(data); 21 | } 22 | 23 | public CssEditorEvent(CssEditor editor, EventType> eventType) { 24 | this(editor, eventType, null); 25 | } 26 | 27 | public CssEditorEvent(EventType> eventType) { 28 | this(null, eventType); 29 | } 30 | 31 | public Optional getEditor() { 32 | return editor; 33 | } 34 | 35 | public Optional getData() { 36 | return data; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/controller/CssEditorTab.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.controller; 2 | 3 | import static xdean.jex.util.lang.ExceptionUtil.uncatch; 4 | import static xdean.jfxex.event.EventHandlers.consume; 5 | 6 | import javax.inject.Inject; 7 | 8 | import org.controlsfx.glyphfont.FontAwesome; 9 | import org.controlsfx.glyphfont.Glyph; 10 | import org.springframework.util.Assert; 11 | 12 | import javafx.beans.binding.Bindings; 13 | import javafx.beans.binding.StringBinding; 14 | import javafx.css.PseudoClass; 15 | import javafx.scene.control.Tab; 16 | import xdean.css.editor.context.setting.FileActions; 17 | import xdean.css.editor.control.CssEditor; 18 | import xdean.jex.util.cache.CacheUtil; 19 | import xdean.jex.util.task.If; 20 | import xdean.jfx.spring.annotation.FxNode; 21 | 22 | @FxNode 23 | public class CssEditorTab extends Tab { 24 | 25 | public static CssEditorTab get(CssEditor editor) { 26 | return (CssEditorTab) editor.getProperties().get(CssEditorTab.class); 27 | } 28 | 29 | private @Inject FileActions fileActions; 30 | private CssEditor editor; 31 | 32 | public CssEditorTab bind(CssEditor editor) { 33 | Assert.isNull(this.editor, "Can't bind twice."); 34 | this.editor = editor; 35 | editor.getProperties().put(CssEditorTab.class, this); 36 | 37 | setOnCloseRequest(consume(e -> editor.fire(fileActions.close()))); 38 | 39 | this.tabPaneProperty().addListener((ob, o, n) -> { 40 | if (n != null) { 41 | editor.activeProperty().softBind(n.getSelectionModel().selectedItemProperty().isEqualTo(this)); 42 | editor.activeProperty().addListener((obe, oe, ne) -> If.that(ne).todo(() -> n.getSelectionModel().select(this))); 43 | } 44 | }); 45 | 46 | textProperty().bind(editor.nameBinding()); 47 | // graphics 48 | Glyph icon = new Glyph(); 49 | icon.getStyleClass().add("tab-icon"); 50 | icon.setIcon(FontAwesome.Glyph.SAVE); 51 | setGraphic(icon); 52 | // Hold the object in cache to avoid gc 53 | StringBinding state = CacheUtil.cache(this, "state", 54 | () -> Bindings.when(editor.activeProperty().normalize()) 55 | .then(Bindings.when(editor.modifiedProperty()) 56 | .then("selected-modified") 57 | .otherwise("selected")) 58 | .otherwise(Bindings.when(editor.modifiedProperty()) 59 | .then("modified") 60 | .otherwise(""))); 61 | state.addListener((ob, o, n) -> { 62 | uncatch(() -> icon.pseudoClassStateChanged(PseudoClass.getPseudoClass(o), false)); 63 | uncatch(() -> icon.pseudoClassStateChanged(PseudoClass.getPseudoClass(n), true)); 64 | }); 65 | 66 | setContent(editor); 67 | return this; 68 | } 69 | 70 | public CssEditor getEditor() { 71 | return editor; 72 | } 73 | } -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/controller/MainFrameModel.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.controller; 2 | 3 | import static xdean.jfxex.bean.BeanUtil.mapList; 4 | 5 | import javax.inject.Inject; 6 | import javax.inject.Provider; 7 | 8 | import org.springframework.stereotype.Component; 9 | 10 | import javafx.collections.FXCollections; 11 | import javafx.collections.ObservableList; 12 | import javafx.scene.control.Tab; 13 | import xdean.css.editor.control.CssEditor; 14 | import xdean.css.editor.model.FileWrapper; 15 | import xdean.jfxex.bean.annotation.CheckNull; 16 | import xdean.jfxex.bean.property.ObjectPropertyEX; 17 | 18 | @Component 19 | public class MainFrameModel { 20 | 21 | private @Inject Provider editorFactory; 22 | private @Inject Provider editorTabFactory; 23 | 24 | final ObservableList editors = FXCollections.observableArrayList(); 25 | final ObservableList tabs = mapList(editors, e -> editorTabFactory.get().bind(e)); 26 | final ObjectPropertyEX<@CheckNull CssEditor> activeEditor = new ObjectPropertyEX<>(this, "currentEditor"); 27 | 28 | public CssEditor newTab(FileWrapper file) { 29 | CssEditor editor = editorFactory.get(); 30 | editors.add(editor); 31 | editor.fileProperty().set(file); 32 | return editor; 33 | } 34 | 35 | public int nextNewOrder() { 36 | for (int i = 1;; i++) { 37 | int ii = i; 38 | if (editors.stream() 39 | .map(t -> t.fileProperty().get()) 40 | .filter(f -> f.isNewFile()) 41 | .map(f -> f.getNewOrder().get()) 42 | .allMatch(n -> n != ii)) { 43 | return i; 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/controller/MenuBarController.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.controller; 2 | 3 | import static xdean.jfxex.bean.BeanUtil.mapList; 4 | 5 | import javax.inject.Inject; 6 | 7 | import javafx.beans.binding.Bindings; 8 | import javafx.fxml.FXML; 9 | import javafx.scene.control.Menu; 10 | import javafx.scene.control.MenuItem; 11 | import javafx.scene.control.RadioMenuItem; 12 | import javafx.scene.control.ToggleGroup; 13 | import xdean.css.editor.context.setting.EditActions; 14 | import xdean.css.editor.context.setting.FileActions; 15 | import xdean.css.editor.context.setting.HelpActions; 16 | import xdean.css.editor.context.setting.model.CssEditorKeyOption; 17 | import xdean.css.editor.service.ContextService; 18 | import xdean.css.editor.service.RecentFileService; 19 | import xdean.css.editor.service.SkinService; 20 | import xdean.jfx.spring.FxInitializable; 21 | import xdean.jfx.spring.annotation.FxController; 22 | import xdean.jfxex.support.skin.SkinStyle; 23 | 24 | @FxController(fxml = "/fxml/MenuBar.fxml") 25 | public class MenuBarController implements FxInitializable { 26 | 27 | private @FXML Menu recentMenu; 28 | private @FXML Menu skinMenu; 29 | private @FXML MenuItem newItem; 30 | private @FXML MenuItem openItem; 31 | private @FXML MenuItem closeItem; 32 | private @FXML MenuItem saveItem; 33 | private @FXML MenuItem saveAsItem; 34 | private @FXML MenuItem revertItem; 35 | 36 | private @FXML MenuItem undoItem; 37 | private @FXML MenuItem redoItem; 38 | private @FXML MenuItem findItem; 39 | private @FXML MenuItem commentItem; 40 | private @FXML MenuItem formatItem; 41 | 42 | private @FXML MenuItem optionItem; 43 | private @FXML MenuItem aboutItem; 44 | private @FXML MenuItem helpItem; 45 | 46 | private @Inject RecentFileService recentSupport; 47 | private @Inject SkinService skinManager; 48 | private @Inject ContextService contextService; 49 | private @Inject EditActions editActions; 50 | private @Inject FileActions fileActions; 51 | private @Inject HelpActions helpActions; 52 | 53 | @Override 54 | public void initAfterFxSpringReady() { 55 | initRecentMenu(); 56 | initSkinMenu(); 57 | 58 | bind(newItem, fileActions.newFile()); 59 | bind(openItem, fileActions.open()); 60 | bind(closeItem, fileActions.close()); 61 | bind(saveItem, fileActions.save()); 62 | bind(saveAsItem, fileActions.saveAs()); 63 | bind(revertItem, fileActions.revert()); 64 | 65 | bind(undoItem, editActions.undo()); 66 | bind(redoItem, editActions.redo()); 67 | // bind(suggestItem, editActions.suggest()); 68 | bind(findItem, editActions.find()); 69 | bind(commentItem, editActions.comment()); 70 | bind(formatItem, editActions.format()); 71 | 72 | bind(optionItem, helpActions.settings()); 73 | bind(aboutItem, helpActions.about()); 74 | bind(helpItem, helpActions.help()); 75 | } 76 | 77 | private void initRecentMenu() { 78 | Bindings.bindContent(recentMenu.getItems(), mapList(recentSupport.getRecentFiles(), p -> { 79 | MenuItem item = new MenuItem(); 80 | item.setText(p.toString()); 81 | item.setOnAction(e -> contextService.fire(fileActions.open().getEvent(null, p))); 82 | return item; 83 | })); 84 | } 85 | 86 | private void initSkinMenu() { 87 | ToggleGroup group = new ToggleGroup(); 88 | for (SkinStyle style : skinManager.getSkinList()) { 89 | RadioMenuItem item = new RadioMenuItem(style.getName()); 90 | item.setToggleGroup(group); 91 | item.setOnAction(e -> skinManager.changeSkin(style)); 92 | if (skinManager.currentSkin() == style) { 93 | item.setSelected(true); 94 | } 95 | skinMenu.getItems().add(item); 96 | } 97 | } 98 | 99 | private void bind(MenuItem item, CssEditorKeyOption key) { 100 | item.acceleratorProperty().bind(key.valueProperty()); 101 | item.disableProperty().bind(key.disableProperty()); 102 | item.setOnAction(e -> contextService.fire(key)); 103 | } 104 | 105 | @FXML 106 | public void exit() { 107 | } 108 | 109 | @FXML 110 | public void option() { 111 | } 112 | 113 | @FXML 114 | public void about() { 115 | } 116 | 117 | @FXML 118 | public void help() { 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/controller/OptionsController.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.controller; 2 | 3 | import static xdean.jex.util.cache.CacheUtil.cache; 4 | import static xdean.jfxex.bean.ListenerUtil.on; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | import javax.inject.Inject; 10 | 11 | import com.sun.javafx.binding.ContentBinding; 12 | 13 | import javafx.beans.property.Property; 14 | import javafx.beans.property.SimpleObjectProperty; 15 | import javafx.beans.property.SimpleStringProperty; 16 | import javafx.event.ActionEvent; 17 | import javafx.fxml.FXML; 18 | import javafx.geometry.Insets; 19 | import javafx.scene.Node; 20 | import javafx.scene.control.ButtonType; 21 | import javafx.scene.control.CheckBox; 22 | import javafx.scene.control.ComboBox; 23 | import javafx.scene.control.Dialog; 24 | import javafx.scene.control.DialogPane; 25 | import javafx.scene.control.Label; 26 | import javafx.scene.control.ListCell; 27 | import javafx.scene.control.Spinner; 28 | import javafx.scene.control.SpinnerValueFactory; 29 | import javafx.scene.control.TableCell; 30 | import javafx.scene.control.TableColumn; 31 | import javafx.scene.control.TableView; 32 | import javafx.scene.control.TextField; 33 | import javafx.scene.input.KeyCode; 34 | import javafx.scene.input.KeyCodeCombination; 35 | import javafx.scene.input.KeyCombination; 36 | import javafx.scene.input.KeyCombination.ModifierValue; 37 | import javafx.scene.input.KeyEvent; 38 | import javafx.scene.layout.HBox; 39 | import javafx.scene.layout.Priority; 40 | import javafx.scene.layout.Region; 41 | import javafx.scene.layout.VBox; 42 | import javafx.scene.text.Font; 43 | import javafx.stage.Stage; 44 | import xdean.css.editor.context.setting.HelpActions; 45 | import xdean.css.editor.context.setting.PreferenceSettings; 46 | import xdean.css.editor.context.setting.model.option.BooleanOption; 47 | import xdean.css.editor.context.setting.model.option.IntegerOption; 48 | import xdean.css.editor.context.setting.model.option.Option; 49 | import xdean.css.editor.context.setting.model.option.OptionGroup; 50 | import xdean.css.editor.context.setting.model.option.ValueOption; 51 | import xdean.css.editor.feature.CssAppFeature; 52 | import xdean.css.editor.service.MessageService; 53 | import xdean.jex.log.Logable; 54 | import xdean.jex.util.string.StringUtil; 55 | import xdean.jex.util.task.TaskUtil; 56 | import xdean.jfx.spring.FxInitializable; 57 | import xdean.jfx.spring.annotation.FxController; 58 | 59 | @FxController(fxml = "/fxml/Options.fxml") 60 | public class OptionsController implements FxInitializable, Logable, CssAppFeature { 61 | private @FXML DialogPane root; 62 | private @FXML VBox generalPane; 63 | private @FXML TableView> keyTable; 64 | private @FXML TableColumn, String> commandColumn; 65 | private @FXML TableColumn, KeyCombination> bindingColumn; 66 | 67 | private @Inject PreferenceSettings preference; 68 | private @Inject List> keyOptions; 69 | private @Inject MessageService messageService; 70 | private @Inject HelpActions help; 71 | 72 | private int nowTab = 0; 73 | private final List onSubmit = new ArrayList<>(); 74 | 75 | @Override 76 | public void bind(Stage stage) { 77 | stage.addEventHandler(help.settings().getEventType(), e -> open(stage)); 78 | } 79 | 80 | @Override 81 | public void initAfterFxSpringReady() { 82 | initGeneral(); 83 | initKey(); 84 | 85 | root.lookupButton(ButtonType.FINISH).addEventHandler(ActionEvent.ACTION, e -> onSubmit.forEach(Runnable::run)); 86 | } 87 | 88 | public void open(Stage stage) { 89 | Dialog dialog = new Dialog<>(); 90 | dialog.initOwner(stage); 91 | dialog.setTitle("Option"); 92 | dialog.setDialogPane(root); 93 | dialog.show(); 94 | } 95 | 96 | private void initGeneral() { 97 | nowTab = -1; 98 | preference.general().getChildren().forEach(e -> e.exec(this::handle, this::handle)); 99 | } 100 | 101 | private void initKey() { 102 | commandColumn.setCellValueFactory(cdf -> new SimpleStringProperty(messageService.getMessage(cdf.getValue().getKey()))); 103 | bindingColumn.setCellValueFactory(cdf -> cache(this, 104 | cdf.getValue(), () -> new SimpleObjectProperty<>(cdf.getValue().getValue()))); 105 | 106 | bindingColumn.setEditable(true); 107 | bindingColumn.setCellFactory(column -> new KeyEditField()); 108 | 109 | keyTable.getItems().setAll(keyOptions); 110 | onSubmit.add(() -> keyTable.getItems().forEach(key -> key.setValue(bindingColumn.getCellData(key)))); 111 | keyOptions.forEach(option -> option.valueProperty() 112 | .addListener((ob, o, n) -> ((Property) bindingColumn.getCellObservableValue(option)).setValue(n))); 113 | } 114 | 115 | private void handle(OptionGroup og) { 116 | nowTab++; 117 | Label label = new Label(messageService.getMessage(og.getKey())); 118 | Font font = label.getFont(); 119 | label.setFont(Font.font(font.getSize() + 10 - nowTab * 3)); 120 | add(label); 121 | og.getChildren().forEach(e -> e.exec(this::handle, this::handle)); 122 | nowTab--; 123 | } 124 | 125 | private void handle(Option o) { 126 | nowTab++; 127 | if (special(o)) { 128 | } else if (o instanceof BooleanOption) { 129 | add((BooleanOption) o); 130 | } else if (o instanceof IntegerOption) { 131 | add((IntegerOption) o); 132 | } else if (o instanceof ValueOption) { 133 | add((ValueOption) o); 134 | } else { 135 | error("Can't handle this option: " + o); 136 | } 137 | nowTab--; 138 | } 139 | 140 | private boolean special(Option o) { 141 | if (o == preference.fontFamily()) { 142 | ComboBox box = add(preference.fontFamily()); 143 | box.setCellFactory(p -> new FontListCell()); 144 | return true; 145 | } 146 | return false; 147 | } 148 | 149 | private ComboBox add(ValueOption vo) { 150 | ComboBox cb = new ComboBox<>(); 151 | ContentBinding.bind(cb.getItems(), vo.values); 152 | cb.getSelectionModel().select(vo.getValue()); 153 | onSubmit.add(() -> vo.setValue(cb.getSelectionModel().getSelectedItem())); 154 | vo.valueProperty().addListener((ob, o, n) -> cb.getSelectionModel().select(n)); 155 | add(wrapWithText(cb, messageService.getMessage(vo.getKey()))); 156 | return cb; 157 | } 158 | 159 | private Spinner add(IntegerOption ro) { 160 | Spinner s = new Spinner<>( 161 | new SpinnerValueFactory.IntegerSpinnerValueFactory(ro.getMin(), ro.getMax(), ro.getValue())); 162 | onSubmit.add(() -> ro.setValue(s.getValue())); 163 | ro.valueProperty().addListener((ob, o, n) -> s.getValueFactory().setValue(n)); 164 | add(wrapWithText(s, messageService.getMessage(ro.getKey()))); 165 | return s; 166 | } 167 | 168 | private CheckBox add(BooleanOption bo) { 169 | CheckBox cb = new CheckBox(messageService.getMessage(bo.getKey())); 170 | cb.setSelected(bo.getValue()); 171 | onSubmit.add(() -> bo.setValue(cb.isSelected())); 172 | bo.valueProperty().addListener((ob, o, n) -> cb.setSelected(n)); 173 | add(cb); 174 | return cb; 175 | } 176 | 177 | private Node wrapWithText(Node node, String text) { 178 | Label label = new Label(text); 179 | label.setMinWidth(Region.USE_PREF_SIZE); 180 | HBox hb = new HBox(label, node); 181 | HBox.setHgrow(node, Priority.ALWAYS); 182 | hb.setSpacing(5); 183 | return hb; 184 | } 185 | 186 | private void add(Node node) { 187 | VBox.setMargin(node, new Insets(0, 0, 0, 15 * nowTab)); 188 | generalPane.getChildren().add(node); 189 | } 190 | 191 | private static class FontListCell extends ListCell { 192 | @Override 193 | protected void updateItem(String fontFamily, boolean empty) { 194 | super.updateItem(fontFamily, empty); 195 | if (!empty) { 196 | setText(fontFamily); 197 | setFont(Font.font(fontFamily)); 198 | } else { 199 | setText(null); 200 | } 201 | } 202 | } 203 | 204 | private static class KeyEditField extends TableCell, KeyCombination> implements Logable { 205 | 206 | private TextField field; 207 | 208 | @Override 209 | public void startEdit() { 210 | super.startEdit(); 211 | if (field == null) { 212 | field = createField(); 213 | } 214 | field.setText(getString()); 215 | setText(null); 216 | setGraphic(field); 217 | field.requestFocus(); 218 | } 219 | 220 | @Override 221 | public void cancelEdit() { 222 | super.cancelEdit(); 223 | setText(getString()); 224 | setGraphic(null); 225 | } 226 | 227 | @Override 228 | protected void updateItem(KeyCombination item, boolean empty) { 229 | super.updateItem(item, empty); 230 | if (empty) { 231 | setText(null); 232 | setGraphic(null); 233 | } else { 234 | if (isEditing()) { 235 | setText(null); 236 | setGraphic(field); 237 | } else { 238 | setText(getString()); 239 | setGraphic(null); 240 | } 241 | } 242 | } 243 | 244 | private TextField createField() { 245 | TextField field = new TextField(getString()); 246 | field.setEditable(false); 247 | field.addEventFilter(KeyEvent.KEY_PRESSED, e -> { 248 | if (e.getCode() == KeyCode.ENTER) { 249 | commit(field.getText()); 250 | } else if (e.getCode() == KeyCode.ESCAPE) { 251 | cancelEdit(); 252 | } else if (e.getCode() == KeyCode.BACK_SPACE) { 253 | field.setText(null); 254 | } else { 255 | field.setText(convert(e).toString()); 256 | } 257 | e.consume(); 258 | }); 259 | field.focusedProperty().addListener(on(false, () -> commit(field.getText()))); 260 | return field; 261 | } 262 | 263 | public void commit(String text) { 264 | try { 265 | commitEdit(StringUtil.isEmpty(text) ? KeyCombination.NO_MATCH : KeyCombination.valueOf(text)); 266 | } catch (Exception ee) { 267 | debug(ee); 268 | cancelEdit(); 269 | } 270 | } 271 | 272 | private String convert(KeyEvent e) { 273 | return TaskUtil.firstSuccess( 274 | () -> new KeyCodeCombination(e.getCode(), 275 | e.isShiftDown() ? ModifierValue.DOWN : ModifierValue.UP, 276 | e.isControlDown() || !(e.isAltDown() || e.isShiftDown() || e.isMetaDown()) ? ModifierValue.DOWN : ModifierValue.UP, 277 | e.isAltDown() ? ModifierValue.DOWN : ModifierValue.UP, 278 | e.isMetaDown() ? ModifierValue.DOWN : ModifierValue.UP, 279 | ModifierValue.UP).toString(), 280 | () -> { 281 | KeyCodeCombination key = new KeyCodeCombination(KeyCode.A, 282 | e.isShiftDown() ? ModifierValue.DOWN : ModifierValue.UP, 283 | e.isControlDown() ? ModifierValue.DOWN : ModifierValue.UP, 284 | e.isAltDown() ? ModifierValue.DOWN : ModifierValue.UP, 285 | e.isMetaDown() ? ModifierValue.DOWN : ModifierValue.UP, 286 | ModifierValue.UP); 287 | String name = key.getName(); 288 | return name.substring(0, name.length() - key.getCode().getName().length()); 289 | }); 290 | } 291 | 292 | private String getString() { 293 | return getItem() == null ? "" : getItem().toString(); 294 | } 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/controller/SearchBarController.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.controller; 2 | 3 | import static xdean.jfxex.bean.ListenerUtil.on; 4 | 5 | import java.util.regex.Matcher; 6 | import java.util.regex.Pattern; 7 | 8 | import javax.inject.Inject; 9 | 10 | import org.controlsfx.control.textfield.TextFields; 11 | import org.fxmisc.richtext.CodeArea; 12 | 13 | import javafx.fxml.FXML; 14 | import javafx.scene.Node; 15 | import javafx.scene.control.Button; 16 | import javafx.scene.control.CheckBox; 17 | import javafx.scene.control.TextField; 18 | import javafx.scene.layout.HBox; 19 | import xdean.css.editor.context.setting.EditActions; 20 | import xdean.css.editor.context.setting.OtherSettings; 21 | import xdean.css.editor.control.CssEditor; 22 | import xdean.css.editor.feature.CssEditorFeature; 23 | import xdean.css.editor.service.ContextService; 24 | import xdean.jex.extra.function.Func3; 25 | import xdean.jex.util.string.StringUtil; 26 | import xdean.jfx.spring.FxInitializable; 27 | import xdean.jfx.spring.annotation.FxController; 28 | import xdean.jfxex.bean.property.BooleanPropertyEX; 29 | 30 | @FxController(fxml = "/fxml/SearchBar.fxml") 31 | public class SearchBarController implements FxInitializable, CssEditorFeature { 32 | 33 | private @FXML HBox root; 34 | private @FXML HBox textContainer; 35 | private @FXML Button findButton; 36 | private @FXML CheckBox caseSensitive; 37 | private @FXML CheckBox regex; 38 | private @FXML CheckBox wrapSearch; 39 | private @Inject OtherSettings otherSettings; 40 | private @Inject ContextService contextService; 41 | private @Inject EditActions editActions; 42 | 43 | private TextField findField; 44 | private final BooleanPropertyEX visible = new BooleanPropertyEX(this, "visible", false); 45 | 46 | @Override 47 | public void initAfterFxSpringReady() { 48 | findField = TextFields.createClearableTextField(); 49 | textContainer.getChildren().add(findField); 50 | 51 | regex.selectedProperty().bindBidirectional(otherSettings.regexSearch().valueProperty()); 52 | caseSensitive.selectedProperty().bindBidirectional(otherSettings.caseSensitive().valueProperty()); 53 | wrapSearch.selectedProperty().bindBidirectional(otherSettings.wrapSearch().valueProperty()); 54 | visible.and(contextService.activeEditorProperty().isNotNull()); 55 | 56 | root.visibleProperty().addListener(on(true, findField::requestFocus) 57 | .on(false, () -> contextService.getActiveEditorSafe().ifPresent(Node::requestFocus))); 58 | root.visibleProperty().bind(visible); 59 | root.managedProperty().bind(root.visibleProperty()); 60 | findField.setOnAction(e -> find()); 61 | } 62 | 63 | @Override 64 | public void bind(CssEditor editor) { 65 | editor.addEventHandler(editActions.find().getEventType(), e -> toggle()); 66 | } 67 | 68 | @FXML 69 | public void find() { 70 | if (findFrom(contextService.getActiveEditor().getCaretPosition()) == false && wrapSearch.isSelected()) { 71 | findFrom(0); 72 | } 73 | } 74 | 75 | private boolean findFrom(int offset) { 76 | CodeArea area = contextService.getActiveEditor(); 77 | String findText = findField.getText(); 78 | if (regex.isSelected()) { 79 | return regexFind(area, findText, offset); 80 | } else { 81 | return simpleFind(area, findText, offset); 82 | } 83 | } 84 | 85 | private boolean simpleFind(CodeArea area, String text, int offset) { 86 | Func3 func = caseSensitive.isSelected() ? String::indexOf 87 | : StringUtil::indexOfIgnoreCase; 88 | int index = func.call(area.getText(), text, offset); 89 | if (index != -1) { 90 | area.selectRange(index, index + text.length()); 91 | return true; 92 | } 93 | return false; 94 | } 95 | 96 | private boolean regexFind(CodeArea area, String regex, int offset) { 97 | if (caseSensitive.isSelected() && regex.startsWith("(?i)")) { 98 | regex = "?i" + regex; 99 | } 100 | Matcher matcher = Pattern.compile(regex).matcher(area.getText().substring(offset)); 101 | if (matcher.find()) { 102 | area.selectRange(matcher.start() + offset, matcher.end() + offset); 103 | return true; 104 | } 105 | return false; 106 | } 107 | 108 | public void toggle() { 109 | visible.set(!visible.get()); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/controller/StatusBarController.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.controller; 2 | 3 | import static xdean.jex.util.lang.ExceptionUtil.uncatch; 4 | import static xdean.jfxex.bean.BeanUtil.mapToString; 5 | import static xdean.jfxex.bean.BeanUtil.nestBooleanValue; 6 | import static xdean.jfxex.bean.BeanUtil.nestValue; 7 | 8 | import java.util.function.Function; 9 | 10 | import javax.inject.Inject; 11 | 12 | import org.controlsfx.validation.ValidationSupport; 13 | import org.controlsfx.validation.Validator; 14 | 15 | import javafx.beans.binding.Bindings; 16 | import javafx.beans.binding.StringBinding; 17 | import javafx.beans.property.BooleanProperty; 18 | import javafx.beans.property.ObjectProperty; 19 | import javafx.beans.value.ObservableValue; 20 | import javafx.fxml.FXML; 21 | import javafx.scene.control.Label; 22 | import javafx.scene.control.TextField; 23 | import javafx.scene.control.TextInputDialog; 24 | import javafx.scene.input.MouseEvent; 25 | import xdean.css.editor.context.setting.PreferenceSettings; 26 | import xdean.css.editor.control.CssEditor; 27 | import xdean.css.editor.service.ContextService; 28 | import xdean.jex.util.calc.MathUtil; 29 | import xdean.jfx.spring.FxInitializable; 30 | import xdean.jfx.spring.annotation.FxController; 31 | 32 | @FxController(fxml = "/fxml/StatusBar.fxml") 33 | public class StatusBarController implements FxInitializable { 34 | 35 | private @FXML Label length; 36 | private @FXML Label lines; 37 | private @FXML Label caretLine; 38 | private @FXML Label caretCol; 39 | private @FXML Label select; 40 | private @FXML Label charset; 41 | private @FXML Label inputType; 42 | private @Inject PreferenceSettings options; 43 | private @Inject ContextService contextService; 44 | 45 | @Override 46 | public void initAfterFxSpringReady() { 47 | ObjectProperty editor = contextService.activeEditorProperty(); 48 | lines.textProperty().bind(map(nestValue(editor, c -> c.textProperty()), t -> countLine(t))); 49 | length.textProperty().bind(map(nestValue(editor, c -> c.textProperty()), t -> t.length())); 50 | caretCol.textProperty().bind(map(nestValue(editor, c -> c.caretColumnProperty()), t -> t)); 51 | caretLine.textProperty().bind(map(nestValue(editor, c -> c.caretPositionProperty()), 52 | t -> countLine(editor.getValue().getText().substring(0, t)))); 53 | select.textProperty() 54 | .bind(map(nestValue(editor, c -> c.selectedTextProperty()), t -> t.length() + " | " + countLine(t))); 55 | charset.textProperty().bind(map(options.charset().valueProperty(), t -> t.toString())); 56 | inputType.textProperty() 57 | .bind(Bindings.when(nestBooleanValue(editor, e -> e.overrideProperty())).then("Override").otherwise("Insert")); 58 | } 59 | 60 | @FXML 61 | public void toggleType(MouseEvent e) { 62 | BooleanProperty override = contextService.getActiveEditor().overrideProperty(); 63 | override.set(!override.get()); 64 | } 65 | 66 | @FXML 67 | public void jumpLine(MouseEvent e) { 68 | if (e.getClickCount() == 2) { 69 | showLineJumpDialog(); 70 | } 71 | } 72 | 73 | private void showLineJumpDialog() { 74 | CssEditor editor = contextService.activeEditorProperty().getValue(); 75 | TextInputDialog dialog = new TextInputDialog(); 76 | dialog.setTitle("Goto Line"); 77 | dialog.getDialogPane().setContentText("Input Line Number: "); 78 | dialog.initOwner(editor.getScene().getWindow()); 79 | TextField tf = dialog.getEditor(); 80 | 81 | int lines = countLine(editor.getText()); 82 | ValidationSupport vs = new ValidationSupport(); 83 | vs.registerValidator(tf, Validator. createPredicateValidator( 84 | s -> uncatch(() -> MathUtil.inRange(Integer.valueOf(s), 1, lines)) == Boolean.TRUE, 85 | String.format("Line number must be in [%d,%d]", 1, lines))); 86 | 87 | dialog.showAndWait().ifPresent(s -> { 88 | if (vs.isInvalid() == false) { 89 | editor.moveTo(Integer.valueOf(s) - 1, 0); 90 | } 91 | }); 92 | } 93 | 94 | private static int countLine(String str) { 95 | return (str + " ").split("\\R").length; 96 | } 97 | 98 | private static StringBinding map(ObservableValue v, Function func) { 99 | return mapToString(v, t -> t == null ? "" : func.apply(t).toString()); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/controller/ToolBarController.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.controller; 2 | 3 | import javax.inject.Inject; 4 | 5 | import javafx.fxml.FXML; 6 | import javafx.scene.control.Button; 7 | import xdean.css.editor.context.setting.EditActions; 8 | import xdean.css.editor.context.setting.FileActions; 9 | import xdean.css.editor.context.setting.model.CssEditorKeyOption; 10 | import xdean.css.editor.service.ContextService; 11 | import xdean.jfx.spring.FxInitializable; 12 | import xdean.jfx.spring.annotation.FxController; 13 | 14 | @FxController(fxml = "/fxml/ToolBar.fxml") 15 | public class ToolBarController implements FxInitializable { 16 | 17 | private @FXML Button newButton; 18 | private @FXML Button openButton; 19 | private @FXML Button saveButton; 20 | private @FXML Button undoButton; 21 | private @FXML Button redoButton; 22 | 23 | private @Inject ContextService contextService; 24 | private @Inject FileActions fileActions; 25 | private @Inject EditActions editActions; 26 | 27 | @Override 28 | public void initAfterFxSpringReady() { 29 | bind(newButton, fileActions.newFile()); 30 | bind(openButton, fileActions.open()); 31 | bind(saveButton, fileActions.save()); 32 | bind(undoButton, editActions.undo()); 33 | bind(redoButton, editActions.redo()); 34 | } 35 | 36 | private void bind(Button button, CssEditorKeyOption key) { 37 | button.disableProperty().bind(key.disableProperty()); 38 | button.setOnAction(e -> contextService.fire(key)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/feature/CommentFeature.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.feature; 2 | 3 | import java.util.stream.Stream; 4 | 5 | import javax.inject.Inject; 6 | 7 | import org.fxmisc.richtext.CodeArea; 8 | import org.fxmisc.richtext.model.NavigationActions.SelectionPolicy; 9 | import org.springframework.stereotype.Service; 10 | 11 | import javafx.scene.control.IndexRange; 12 | import xdean.css.editor.context.setting.EditActions; 13 | import xdean.css.editor.control.CssEditor; 14 | 15 | @Service 16 | public class CommentFeature implements CssEditorFeature { 17 | 18 | private static final String LINE_COMMENT_PATTERN = "^\\s*/\\*.*\\*/\\s*$"; 19 | 20 | @Inject 21 | EditActions actions; 22 | 23 | @Override 24 | public void bind(CssEditor editor) { 25 | editor.addEventHandler(actions.comment().getEventType(), e -> onAction((CssEditor) e.getSource())); 26 | } 27 | 28 | private void onAction(CssEditor editor) { 29 | selectLines(editor); 30 | String selectedText = editor.getSelectedText(); 31 | IndexRange selection = editor.getSelection(); 32 | editor.getUndoManager().preventMerge(); 33 | editor.replaceSelection(CommentFeature.toggleComment(selectedText)); 34 | editor.getUndoManager().preventMerge(); 35 | editor.moveTo(selection.getStart(), SelectionPolicy.EXTEND); 36 | } 37 | 38 | private static void selectLines(CodeArea area) { 39 | IndexRange origin = area.getSelection(); 40 | 41 | area.moveTo(origin.getStart()); 42 | area.lineStart(SelectionPolicy.CLEAR); 43 | int start = area.getCaretPosition(); 44 | 45 | area.moveTo(origin.getEnd()); 46 | area.lineEnd(SelectionPolicy.CLEAR); 47 | int end = area.getCaretPosition(); 48 | 49 | area.selectRange(start, end); 50 | } 51 | 52 | private static String toggleComment(String text) { 53 | String[] split = text.split("\\R"); 54 | boolean allMatch = Stream.of(split).allMatch(s -> s.matches(LINE_COMMENT_PATTERN)); 55 | if (allMatch) { 56 | return Stream.of(split).map(s -> clearComment(s)) 57 | .reduce((s1, s2) -> String.join(System.lineSeparator(), s1, s2)).orElse(text); 58 | } else { 59 | return Stream.of(split).map(s -> addComment(s)) 60 | .reduce((s1, s2) -> String.join(System.lineSeparator(), s1, s2)).orElse(text); 61 | } 62 | } 63 | 64 | private static String addComment(String line) { 65 | return "/* " + line + " */"; 66 | } 67 | 68 | private static String clearComment(String line) { 69 | String trim = line.trim(); 70 | String replace = trim.replaceAll("^/\\* ?", "").replaceAll(" ?\\*/$", ""); 71 | return line.replace(trim, replace); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/feature/CssAppFeature.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.feature; 2 | 3 | import javafx.stage.Stage; 4 | 5 | public interface CssAppFeature { 6 | void bind(Stage stage); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/feature/CssEditorFeature.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.feature; 2 | 3 | import xdean.css.editor.control.CssEditor; 4 | 5 | public interface CssEditorFeature { 6 | void bind(CssEditor editor); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/feature/HighLightFeature.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.feature; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | import javax.inject.Inject; 6 | 7 | import org.springframework.stereotype.Service; 8 | 9 | import io.reactivex.rxjavafx.observables.JavaFxObservable; 10 | import io.reactivex.rxjavafx.schedulers.JavaFxScheduler; 11 | import xdean.css.editor.control.CssEditor; 12 | import xdean.css.editor.feature.highlight.CssHighLight; 13 | 14 | @Service 15 | public class HighLightFeature implements CssEditorFeature { 16 | 17 | @Inject 18 | CssHighLight cssHighLight; 19 | 20 | @Override 21 | public void bind(CssEditor cssEditor) { 22 | JavaFxObservable.valuesOf(cssEditor.textProperty()) 23 | .debounce(300, TimeUnit.MILLISECONDS) 24 | .observeOn(JavaFxScheduler.platform()) 25 | .subscribe(e -> cssEditor.setStyleSpans(0, cssHighLight.compute(e))); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/feature/LastFileFeature.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.feature; 2 | 3 | import static xdean.css.editor.context.Context.LAST_FILE_PATH; 4 | import static xdean.jex.util.lang.ExceptionUtil.uncheck; 5 | 6 | import java.io.IOException; 7 | import java.nio.file.DirectoryStream; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.nio.file.Paths; 11 | import java.util.List; 12 | 13 | import javax.inject.Inject; 14 | import javax.inject.Provider; 15 | 16 | import org.springframework.stereotype.Component; 17 | 18 | import javafx.stage.Stage; 19 | import xdean.css.editor.context.setting.FileActions; 20 | import xdean.css.editor.context.setting.PreferenceSettings; 21 | import xdean.css.editor.control.CssEditor; 22 | import xdean.css.editor.model.FileWrapper; 23 | import xdean.css.editor.service.ContextService; 24 | import xdean.css.editor.service.DialogService; 25 | import xdean.jex.extra.tryto.Try; 26 | import xdean.jex.util.collection.ListUtil; 27 | import xdean.jex.util.file.FileUtil; 28 | 29 | @Component 30 | public class LastFileFeature implements CssAppFeature { 31 | 32 | private @Inject Provider editorFactory; 33 | private @Inject ContextService contextService; 34 | private @Inject PreferenceSettings options; 35 | private @Inject FileActions fileActions; 36 | private @Inject DialogService dialogService; 37 | 38 | @Override 39 | public void bind(Stage stage) { 40 | stage.addEventHandler(fileActions.exit().getEventType(), e -> save()); 41 | if (options.openLast().getValue()) { 42 | open(); 43 | } 44 | } 45 | 46 | public void open() { 47 | try { 48 | FileUtil.createDirectory(LAST_FILE_PATH); 49 | try (DirectoryStream stream = Files.newDirectoryStream(LAST_FILE_PATH, "*.tmp")) { 50 | stream.forEach(p -> uncheck(() -> { 51 | List lines = Files.readAllLines(p, options.charset().getValue()); 52 | if (lines.isEmpty()) { 53 | return; 54 | } 55 | String head = lines.get(0); 56 | CssEditor editor = editorFactory.get(); 57 | editor.fileProperty().set(Try.to(() -> Integer.valueOf(head)).map(i -> FileWrapper.newFile(i)) 58 | .getOrElse(() -> FileWrapper.existFile(Paths.get(head)))); 59 | lines.stream() 60 | .skip(1) 61 | .reduce((a, b) -> String.join(System.lineSeparator(), a, b)) 62 | .ifPresent(t -> { 63 | editor.replaceText(t); 64 | editor.getUndoManager().forgetHistory(); 65 | }); 66 | contextService.editorList().add(editor); 67 | })); 68 | } 69 | } catch (IOException e) { 70 | dialogService.errorDialog(e).content("Fail to open last files.").show(); 71 | } 72 | } 73 | 74 | private void save() { 75 | if (options.openLast().getValue()) { 76 | uncheck(() -> FileUtil.createDirectory(LAST_FILE_PATH)); 77 | uncheck(() -> Files.newDirectoryStream(LAST_FILE_PATH, "*.tmp").forEach(p -> uncheck(() -> Files.delete(p)))); 78 | ListUtil.forEach(contextService.editorList(), (e, i) -> { 79 | String nameString = e.fileProperty().get().fileOrNew.unify(p -> p.toString(), n -> n.toString()); 80 | String text = e.modifiedProperty().get() ? e.getText() : ""; 81 | Path path = LAST_FILE_PATH.resolve(String.format("%s.tmp", i)); 82 | try { 83 | Files.write(path, String.join("\n", nameString, text).getBytes(options.charset().getValue())); 84 | } catch (IOException e1) { 85 | dialogService.errorDialog(e1).content("Fail to save last files.").show(); 86 | } 87 | }); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/feature/PreviewFeature.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.feature; 2 | 3 | import java.util.Collections; 4 | import java.util.List; 5 | import java.util.concurrent.TimeUnit; 6 | 7 | import org.fxmisc.richtext.PopupAlignment; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | 11 | import io.reactivex.Observable; 12 | import io.reactivex.functions.Consumer; 13 | import io.reactivex.rxjavafx.observables.JavaFxObservable; 14 | import io.reactivex.rxjavafx.schedulers.JavaFxScheduler; 15 | import io.reactivex.schedulers.Schedulers; 16 | import javafx.scene.canvas.Canvas; 17 | import javafx.scene.canvas.GraphicsContext; 18 | import javafx.scene.control.PopupControl; 19 | import javafx.scene.layout.AnchorPane; 20 | import javafx.scene.layout.Background; 21 | import javafx.scene.layout.BackgroundFill; 22 | import javafx.scene.layout.Region; 23 | import javafx.scene.paint.Color; 24 | import javafx.stage.PopupWindow; 25 | import xdean.css.editor.control.CssEditor; 26 | import xdean.css.editor.feature.preview.CssElementPreviewer; 27 | import xdean.jex.extra.collection.Pair; 28 | import xdean.jfxex.support.DragSupport; 29 | import xdean.jfxex.util.LayoutUtil; 30 | 31 | @Service 32 | @SuppressWarnings("rawtypes") 33 | public class PreviewFeature implements CssEditorFeature { 34 | 35 | @Autowired(required = false) 36 | List previewers = Collections.emptyList(); 37 | 38 | @Override 39 | public void bind(CssEditor cssEditor) { 40 | new InnerController(cssEditor); 41 | } 42 | 43 | private static String extractValue(String text) { 44 | String str = text.trim(); 45 | int colon = str.indexOf(':'); 46 | int semicolon = str.indexOf(';'); 47 | if (semicolon != -1 && semicolon == str.length() - 1) { 48 | str = str.substring(0, str.length() - 1); 49 | } 50 | if (colon != -1) { 51 | str = str.substring(colon + 1); 52 | } 53 | return str; 54 | } 55 | 56 | private class InnerController { 57 | 58 | private double width = 80, height = 50, line = 2; 59 | 60 | CssEditor editor; 61 | PopupControl popup = new PopupControl(); 62 | AnchorPane contentPane = new AnchorPane(); 63 | Canvas canvas = new Canvas(width, height); 64 | Region region = new Region(); 65 | 66 | @SuppressWarnings("unchecked") 67 | InnerController(CssEditor area) { 68 | this.editor = area; 69 | 70 | contentPane.setBorder(LayoutUtil.getSimpleBorder(Color.BLACK, line)); 71 | contentPane.setPrefWidth(width + 2 * line); 72 | contentPane.setPrefHeight(height + 2 * line); 73 | contentPane.getChildren().setAll(canvas); 74 | 75 | LayoutUtil.setAnchorZero(canvas); 76 | LayoutUtil.setAnchorZero(region); 77 | 78 | region.setPrefSize(width, height); 79 | region.setBackground(new Background(new BackgroundFill(Color.WHITE, null, null))); 80 | 81 | popup.setConsumeAutoHidingEvents(false); 82 | popup.setAutoHide(true); 83 | popup.setAutoFix(true); 84 | popup.setHideOnEscape(true); 85 | 86 | popup.getScene().setRoot(contentPane); 87 | 88 | area.setPopupWindow(popup); 89 | area.setPopupAlignment(PopupAlignment.CARET_BOTTOM); 90 | 91 | DragSupport.bind(popup); 92 | 93 | JavaFxObservable.valuesOf(editor.selectedTextProperty()) 94 | .debounce(300, TimeUnit.MILLISECONDS) 95 | .map(t -> extractValue(t)) 96 | .map(String::trim) 97 | .filter(s -> !s.isEmpty()) 98 | .observeOn(Schedulers.computation()) 99 | .> switchMapMaybe(text -> Observable.fromIterable(previewers) 100 | .flatMapMaybe(p -> p.parse(editor.context, text) 101 | .subscribeOn(Schedulers.computation()) 102 | .map(o -> Pair.of(p, o))) 103 | .firstElement()) 104 | .observeOn(JavaFxScheduler.platform()) 105 | .doOnNext((Consumer>) (p -> show(p.getLeft(), p.getRight()))) 106 | .subscribe(); 107 | } 108 | 109 | void showPopup() { 110 | PopupWindow popupWindow = editor.getPopupWindow(); 111 | if (popupWindow != popup) { 112 | popupWindow.hide(); 113 | editor.setPopupWindow(popup); 114 | } 115 | popup.show(editor.getScene().getWindow()); 116 | } 117 | 118 | void show(CssElementPreviewer previewer, T value) { 119 | GraphicsContext graphics = canvas.getGraphicsContext2D(); 120 | graphics.clearRect(0, 0, width, height); 121 | 122 | previewer.preview(graphics, value, width, height); 123 | 124 | showPopup(); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/feature/SuggestionFeature.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.feature; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collection; 5 | import java.util.concurrent.TimeUnit; 6 | 7 | import javax.inject.Inject; 8 | 9 | import org.fxmisc.richtext.CodeArea; 10 | import org.fxmisc.richtext.PopupAlignment; 11 | import org.springframework.stereotype.Service; 12 | 13 | import impl.org.controlsfx.skin.AutoCompletePopup; 14 | import impl.org.controlsfx.skin.AutoCompletePopupSkin; 15 | import io.reactivex.rxjavafx.observables.JavaFxObservable; 16 | import io.reactivex.rxjavafx.schedulers.JavaFxScheduler; 17 | import javafx.scene.control.IndexRange; 18 | import javafx.scene.control.ListView; 19 | import javafx.scene.control.Skin; 20 | import javafx.scene.input.KeyCode; 21 | import javafx.scene.input.KeyCodeCombination; 22 | import javafx.scene.input.KeyCombination; 23 | import javafx.scene.input.KeyEvent; 24 | import javafx.stage.PopupWindow; 25 | import xdean.css.editor.context.setting.EditActions; 26 | import xdean.css.editor.control.CssEditor; 27 | import xdean.css.editor.feature.suggestion.CssSuggestionService; 28 | import xdean.css.editor.model.CssContext; 29 | 30 | @Service 31 | public class SuggestionFeature implements CssEditorFeature { 32 | 33 | @Inject 34 | CssSuggestionService cssSuggestion; 35 | 36 | @Inject 37 | EditActions keys; 38 | 39 | private final Collection legalPrefix = Arrays.asList( 40 | new KeyCodeCombination(KeyCode.PERIOD), 41 | new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.SHIFT_DOWN), 42 | new KeyCodeCombination(KeyCode.MINUS)); 43 | 44 | @Override 45 | public void bind(CssEditor cssEditor) { 46 | new InnerController(cssEditor); 47 | } 48 | 49 | private boolean shouldSuggest(KeyEvent e) { 50 | if (legalPrefix.stream().filter(c -> c.match(e)).count() > 0) { 51 | return true; 52 | } else if (keys.suggest().getValue().match(e)) { 53 | return true; 54 | } else { 55 | return false; 56 | } 57 | } 58 | 59 | private class InnerController { 60 | 61 | CodeArea editor; 62 | CssContext context; 63 | 64 | AutoCompletePopup popup = new AutoCompletePopup<>(); 65 | 66 | public InnerController(CssEditor cssEditor) { 67 | this.editor = cssEditor; 68 | this.context = cssEditor.context; 69 | 70 | editor.textProperty().addListener((ob, o, n) -> { 71 | if (editor.isFocused() && popup.isShowing()) { 72 | showPopup(); 73 | } 74 | }); 75 | editor.focusedProperty().addListener((ob, o, n) -> { 76 | if (n == false) { 77 | hidePopup(); 78 | } 79 | }); 80 | 81 | editor.setPopupWindow(popup); 82 | editor.setPopupAlignment(PopupAlignment.CARET_BOTTOM); 83 | 84 | popup.setOnSuggestion(sce -> { 85 | completeUserInput(sce.getSuggestion()); 86 | hidePopup(); 87 | }); 88 | 89 | JavaFxObservable.eventsOf(editor, KeyEvent.KEY_PRESSED) 90 | .debounce(100, TimeUnit.MILLISECONDS) 91 | .filter(e -> shouldSuggest(e)) 92 | .observeOn(JavaFxScheduler.platform()) 93 | .subscribe(e -> showPopup()); 94 | } 95 | 96 | public void showPopup() { 97 | Collection suggestions = cssSuggestion.getSuggestion(editor.getText(), editor.getCaretPosition(), context); 98 | if (suggestions.isEmpty()) { 99 | hidePopup(); 100 | } else { 101 | PopupWindow popupWindow = editor.getPopupWindow(); 102 | if (popupWindow != popup) { 103 | popupWindow.hide(); 104 | editor.setPopupWindow(popup); 105 | } 106 | popup.getSuggestions().setAll(suggestions); 107 | selectFirstSuggestion(popup); 108 | if (popup.isShowing() == false) { 109 | popup.show(editor.getScene().getWindow()); 110 | } 111 | } 112 | } 113 | 114 | public void hidePopup() { 115 | popup.hide(); 116 | } 117 | 118 | private void completeUserInput(String suggestion) { 119 | IndexRange range = cssSuggestion.getReplaceRange(editor.getText(), editor.getCaretPosition(), context); 120 | editor.deleteText(range); 121 | editor.insertText(range.getStart(), suggestion); 122 | editor.moveTo(range.getStart() + suggestion.length()); 123 | } 124 | 125 | private void selectFirstSuggestion(AutoCompletePopup autoCompletionPopup) { 126 | Skin skin = autoCompletionPopup.getSkin(); 127 | if (skin instanceof AutoCompletePopupSkin) { 128 | AutoCompletePopupSkin au = (AutoCompletePopupSkin) skin; 129 | ListView li = (ListView) au.getNode(); 130 | if (li.getItems() != null && !li.getItems().isEmpty()) { 131 | li.getSelectionModel().select(0); 132 | } 133 | } 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/feature/highlight/CssHighLight.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.feature.highlight; 2 | 3 | import java.util.Collection; 4 | 5 | import org.fxmisc.richtext.model.StyleSpans; 6 | 7 | public interface CssHighLight { 8 | 9 | public interface Element { 10 | String CSS = "css"; 11 | String SELECTOR = "selector"; 12 | String SELECTOR_ID = "selector-id"; 13 | String SELECTOR_CLASS = "selector-class"; 14 | String SELECTOR_STATE = "selector-state"; 15 | String SELECTOR_JAVACLASS = "selector-java-class"; 16 | String ENTRIES = "entries"; 17 | String KEY = "key"; 18 | String VALUE = "value"; 19 | String MUTI_COMMENT = "muticomment"; 20 | String ENTRY = "entry"; 21 | String LEFT_BRACE = "leftbrace"; 22 | String RIGHT_BRACE = "rightbrace"; 23 | String COLON = "colon"; 24 | String SEMICOLON = "semicolon"; 25 | } 26 | 27 | StyleSpans> compute(String text); 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/feature/highlight/CssHighLightConfig.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.feature.highlight; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | @Configuration 7 | public class CssHighLightConfig { 8 | 9 | @Bean 10 | public CssHighLight impl() { 11 | return new RegexHighLight(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/feature/highlight/RegexHighLight.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.feature.highlight; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.Collections; 6 | import java.util.List; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | 10 | import org.fxmisc.richtext.model.StyleSpans; 11 | import org.fxmisc.richtext.model.StyleSpansBuilder; 12 | 13 | public class RegexHighLight implements CssHighLight { 14 | 15 | private static final String CSS_REGEX = "(([^\\{\\}/])*)(\\{)(([^}])*)(\\})"; 16 | private static final String SELECTOR_REGEX = "([.#: ]|\\n)([A-Za-z0-9-_]+)"; 17 | private static final String MUTI_COMMENT_REGEX = "/\\*+([^*]|(\\*+[^*/]))*\\*+/"; 18 | private static final String ENTRY_REGEX = "([A-Za-z0-9-_]+)(\\s*:\\s*)(([^;]|\\R)*)(;)"; 19 | 20 | private static final Pattern CSS_PATTERN = Pattern.compile(String.format( 21 | "(?<%s>%s)|(?<%s>%s)", Element.MUTI_COMMENT, MUTI_COMMENT_REGEX, Element.CSS, CSS_REGEX)); 22 | private static final Pattern ENTRY_PATTERN = Pattern.compile(String.format( 23 | "(?<%s>%s)|(?<%s>%s)", Element.MUTI_COMMENT, MUTI_COMMENT_REGEX, Element.ENTRY, ENTRY_REGEX)); 24 | private static final Pattern SELECTOR_PATTERN = Pattern.compile(SELECTOR_REGEX); 25 | 26 | private static final int GROUP_BEFORE = 5; 27 | 28 | private static final int GROUP_KEY = GROUP_BEFORE + 0; 29 | private static final int GROPU_COLON = GROUP_BEFORE + 1; 30 | private static final int GROUP_VALUE = GROUP_BEFORE + 2; 31 | private static final int GROUP_SEMICOLON = GROUP_BEFORE + 4; 32 | 33 | private static final int GROUP_SELECTOR = GROUP_BEFORE + 0; 34 | private static final int GROUP_LEFT_BRACE = GROUP_BEFORE + 2; 35 | private static final int GROUP_VALUES = GROUP_BEFORE + 3; 36 | private static final int GROUP_RIGHT_BRACE = GROUP_BEFORE + 5; 37 | 38 | @Override 39 | public StyleSpans> compute(String text) { 40 | StyleSpansBuilder> spansBuilder = new StyleSpansBuilder<>(); 41 | Matcher cssMatcher = CSS_PATTERN.matcher(text); 42 | int lastKwEnd = 0; 43 | while (cssMatcher.find()) { 44 | spansBuilder.add(Collections.emptyList(), cssMatcher.start() - lastKwEnd); 45 | if (cssMatcher.group(Element.MUTI_COMMENT) != null) { 46 | spansBuilder.add(Collections.singleton(Element.MUTI_COMMENT), cssMatcher.end() - cssMatcher.start()); 47 | } else { 48 | if (cssMatcher.group(Element.CSS) != null) { 49 | String selectorText = cssMatcher.group(GROUP_SELECTOR); 50 | if (!selectorText.isEmpty()) { 51 | lastKwEnd = 0; 52 | Matcher selectorMatcher = SELECTOR_PATTERN.matcher(selectorText); 53 | while (selectorMatcher.find()) { 54 | spansBuilder.add(Collections.emptyList(), selectorMatcher.start() - lastKwEnd); 55 | spansBuilder.add(Collections.emptyList(), selectorMatcher.end(1) - selectorMatcher.start(1)); 56 | String prefix = selectorMatcher.group(1); 57 | List list = new ArrayList<>(); 58 | switch (prefix) { 59 | case "#": 60 | list.add(Element.SELECTOR_ID); 61 | break; 62 | case ".": 63 | list.add(Element.SELECTOR_CLASS); 64 | break; 65 | case ":": 66 | list.add(Element.SELECTOR_STATE); 67 | break; 68 | case " ": 69 | case "\n": 70 | list.add(Element.SELECTOR_JAVACLASS); 71 | break; 72 | } 73 | spansBuilder.add(list, selectorMatcher.end(2) - selectorMatcher.start(2)); 74 | lastKwEnd = selectorMatcher.end(); 75 | } 76 | if (selectorText.length() > lastKwEnd) { 77 | spansBuilder.add(Collections.emptyList(), selectorText.length() - lastKwEnd); 78 | } 79 | } 80 | spansBuilder.add(Collections.singleton(Element.LEFT_BRACE), 81 | cssMatcher.end(GROUP_LEFT_BRACE) - cssMatcher.start(GROUP_LEFT_BRACE)); 82 | String entriesText = cssMatcher.group(GROUP_VALUES); 83 | if (!entriesText.isEmpty()) { 84 | lastKwEnd = 0; 85 | Matcher entryMatcher = ENTRY_PATTERN.matcher(entriesText); 86 | while (entryMatcher.find()) { 87 | spansBuilder.add(Collections.emptyList(), entryMatcher.start() - lastKwEnd); 88 | if (entryMatcher.group(Element.MUTI_COMMENT) != null) { 89 | spansBuilder 90 | .add(Collections.singleton(Element.MUTI_COMMENT), entryMatcher.end() - entryMatcher.start()); 91 | } else { 92 | if (entryMatcher.group(Element.ENTRY) != null) { 93 | spansBuilder.add(Collections.singleton(Element.KEY), 94 | entryMatcher.end(GROUP_KEY) - entryMatcher.start(GROUP_KEY)); 95 | spansBuilder.add(Collections.singleton(Element.COLON), 96 | entryMatcher.end(GROPU_COLON) - entryMatcher.start(GROPU_COLON)); 97 | spansBuilder.add(Collections.singleton(Element.VALUE), 98 | entryMatcher.end(GROUP_VALUE) - entryMatcher.start(GROUP_VALUE)); 99 | spansBuilder.add(Collections.singleton(Element.SEMICOLON), 100 | entryMatcher.end(GROUP_SEMICOLON) - entryMatcher.start(GROUP_SEMICOLON)); 101 | } 102 | } 103 | lastKwEnd = entryMatcher.end(); 104 | } 105 | if (entriesText.length() > lastKwEnd) { 106 | spansBuilder.add(Collections.emptyList(), entriesText.length() - lastKwEnd); 107 | } 108 | } 109 | 110 | lastKwEnd = cssMatcher.end(GROUP_VALUES); 111 | 112 | spansBuilder.add(Collections.singleton(Element.RIGHT_BRACE), cssMatcher.end(GROUP_RIGHT_BRACE) - lastKwEnd); 113 | } 114 | } 115 | lastKwEnd = cssMatcher.end(); 116 | } 117 | spansBuilder.add(Collections.emptyList(), text.length() - lastKwEnd); 118 | return spansBuilder.create(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/feature/highlight/SimpleHighLight.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.feature.highlight; 2 | 3 | import static xdean.jex.util.lang.ExceptionUtil.uncatch; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collection; 7 | import java.util.Collections; 8 | import java.util.Iterator; 9 | import java.util.Map; 10 | 11 | import org.fxmisc.richtext.model.StyleSpans; 12 | import org.fxmisc.richtext.model.StyleSpansBuilder; 13 | 14 | import com.google.common.collect.Range; 15 | import com.google.common.collect.RangeMap; 16 | import com.google.common.collect.TreeRangeMap; 17 | 18 | import xdean.jex.util.string.StringUtil; 19 | 20 | public class SimpleHighLight implements CssHighLight { 21 | 22 | @Override 23 | public StyleSpans> compute(String text) { 24 | StyleSpansBuilder> spansBuilder = new StyleSpansBuilder<>(); 25 | RangeMap> map = TreeRangeMap.create(); 26 | int offset = 0; 27 | while (offset < text.length()) { 28 | Range comment = findComment(text, offset); 29 | if (comment == null) { 30 | break; 31 | } 32 | map.put(Range.closed(comment.lowerEndpoint(), comment.upperEndpoint()), Collections.singleton(Element.MUTI_COMMENT)); 33 | offset = comment.upperEndpoint(); 34 | } 35 | offset = 0; 36 | Iterator> iterator = new ArrayList<>(map.asMapOfRanges().keySet()).iterator(); 37 | Range comment = uncatch(() -> iterator.next()); 38 | Range css = findCSS(text, offset); 39 | while (offset < text.length()) { 40 | if (css == null) { 41 | break; 42 | } 43 | while (comment != null && comment.upperEndpoint() < css.lowerEndpoint()) { 44 | comment = uncatch(() -> iterator.next()); 45 | } 46 | if (comment != null) { 47 | if (comment.contains(css.lowerEndpoint())) { 48 | // [COMMEN{CST]S} 49 | offset = comment.upperEndpoint() + 1; 50 | css = findCSS(text, offset); 51 | continue; 52 | } 53 | if (comment.contains(css.upperEndpoint())) { 54 | // {CS[CS}OMMENT] 55 | int newEnd = text.indexOf('}', comment.upperEndpoint()); 56 | if (newEnd == -1) { 57 | break; 58 | } 59 | css = Range.closed(css.lowerEndpoint(), newEnd + 1); 60 | continue; 61 | } 62 | if (css.encloses(comment)) { 63 | // {C[COMMENT]S[COMMES}NT] 64 | if (map.get(css.upperEndpoint()) != null) { 65 | int newEnd = text.indexOf('}', map.getEntry(css.upperEndpoint()).getKey().upperEndpoint()); 66 | if (newEnd == -1) { 67 | break; 68 | } 69 | css = Range.closed(css.lowerEndpoint(), newEnd + 1); 70 | continue; 71 | } 72 | // {C[COMMENT]S[COMMENT]S} 73 | int lower = css.lowerEndpoint(); 74 | while (comment != null && comment.upperEndpoint() < css.upperEndpoint()) { 75 | map.put(Range.closed(lower, comment.lowerEndpoint()), Collections.singleton(Element.SELECTOR_CLASS)); 76 | lower = comment.upperEndpoint(); 77 | comment = uncatch(() -> iterator.next()); 78 | } 79 | map.put(Range.closed(lower, css.upperEndpoint()), Collections.singleton(Element.SELECTOR_CLASS)); 80 | } else { 81 | // {CSS}[COMMENT] 82 | map.put(css, Collections.singleton(Element.SELECTOR_CLASS)); 83 | } 84 | } else { 85 | // {CSS} no comment 86 | map.put(css, Collections.singleton(Element.SELECTOR_CLASS)); 87 | } 88 | offset = css.upperEndpoint(); 89 | css = findCSS(text, offset); 90 | } 91 | offset = 0; 92 | Map, Collection> rangeMap = map.asMapOfRanges(); 93 | for (Range range : rangeMap.keySet()) { 94 | spansBuilder.add(Collections.emptyList(), range.lowerEndpoint() - offset); 95 | spansBuilder.add(rangeMap.get(range), range.upperEndpoint() - range.lowerEndpoint()); 96 | offset = range.upperEndpoint(); 97 | } 98 | spansBuilder.add(Collections.emptyList(), text.length() - offset); 99 | return spansBuilder.create(); 100 | } 101 | 102 | private Range findComment(String text, int offset) { 103 | int start = text.indexOf("/*", offset); 104 | int end = text.indexOf("*/", start); 105 | if (start == -1 || end == -1) { 106 | return null; 107 | } 108 | return Range.closed(start, end + 2); 109 | } 110 | 111 | private Range findCSS(String text, int offset) { 112 | int start = text.indexOf("{", offset); 113 | int end = text.indexOf("}", start); 114 | if (start > end || start == -1 || end == -1) { 115 | return null; 116 | } 117 | start = offset + StringUtil.lastIndexOf(text.substring(offset, start), "{", "}") + 1; 118 | return Range.closed(start, end + 1); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/feature/preview/BorderPreviewer.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.feature.preview; 2 | 3 | import io.reactivex.Maybe; 4 | import javafx.scene.canvas.GraphicsContext; 5 | import javafx.scene.layout.Border; 6 | import xdean.css.editor.model.CssContext; 7 | 8 | // TODO 9 | // @Service 10 | public class BorderPreviewer implements CssElementPreviewer { 11 | 12 | @Override 13 | public Maybe parse(CssContext context, String text) { 14 | // TODO Auto-generated method stub 15 | return null; 16 | } 17 | 18 | @Override 19 | public void preview(GraphicsContext gc, Border value, double width, double height) { 20 | // TODO Auto-generated method stub 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/feature/preview/CssElementPreviewer.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.feature.preview; 2 | 3 | import io.reactivex.Maybe; 4 | import javafx.scene.canvas.GraphicsContext; 5 | import xdean.css.editor.model.CssContext; 6 | 7 | public interface CssElementPreviewer { 8 | Maybe parse(CssContext context, String text); 9 | 10 | void preview(GraphicsContext gc, T value, double width, double height); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/feature/preview/PaintPreviewer.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.feature.preview; 2 | 3 | import static xdean.jex.util.lang.ExceptionUtil.uncatch; 4 | 5 | import org.springframework.stereotype.Service; 6 | 7 | import com.sun.javafx.css.Stylesheet; 8 | import com.sun.javafx.css.converters.PaintConverter.LinearGradientConverter; 9 | import com.sun.javafx.css.converters.PaintConverter.RadialGradientConverter; 10 | import com.sun.javafx.css.parser.CSSParser; 11 | import com.sun.javafx.css.parser.DeriveColorConverter; 12 | import com.sun.javafx.css.parser.LadderConverter; 13 | 14 | import io.reactivex.Maybe; 15 | import io.reactivex.MaybeSource; 16 | import io.reactivex.Observable; 17 | import io.reactivex.functions.Function; 18 | import io.reactivex.schedulers.Schedulers; 19 | import javafx.css.ParsedValue; 20 | import javafx.scene.Group; 21 | import javafx.scene.Node; 22 | import javafx.scene.canvas.GraphicsContext; 23 | import javafx.scene.paint.Color; 24 | import javafx.scene.paint.LinearGradient; 25 | import javafx.scene.paint.Paint; 26 | import javafx.scene.paint.RadialGradient; 27 | import javafx.scene.text.Font; 28 | import javafx.scene.transform.Affine; 29 | import xdean.css.editor.model.CssContext; 30 | 31 | @Service 32 | @SuppressWarnings({ "rawtypes", "unchecked" }) 33 | public class PaintPreviewer implements CssElementPreviewer { 34 | 35 | private final Node inlineNode = new Group(); 36 | 37 | @Override 38 | public Maybe parse(CssContext context, String str) { 39 | return Observable.> just( 40 | text -> Color.web(text, 1), 41 | text -> LinearGradient.valueOf(text), 42 | text -> RadialGradient.valueOf(text), 43 | text -> context.lookup(text), 44 | text -> DeriveColorConverter.getInstance().convert(resolve(context, text), Font.getDefault()), 45 | text -> LadderConverter.getInstance().convert(resolve(context, text), Font.getDefault()), 46 | text -> LinearGradientConverter.getInstance().convert(resolve(context, text), Font.getDefault()), 47 | text -> RadialGradientConverter.getInstance().convert(resolve(context, text), Font.getDefault())) 48 | .observeOn(Schedulers.computation()) 49 | . flatMapMaybe((Function, MaybeSource>) (t -> Maybe 50 | .fromCallable(() -> uncatch(() -> t.apply(str))) 51 | .onErrorComplete())) 52 | .firstElement(); 53 | } 54 | 55 | @Override 56 | public void preview(GraphicsContext graphics, Paint paint, double width, double height) { 57 | graphics.setTransform(new Affine()); 58 | graphics.setFill(paint); 59 | graphics.fillRect(0, 0, width, height); 60 | } 61 | 62 | private ParsedValue resolve(CssContext context, String text) { 63 | inlineNode.setStyle("-fx-color:" + text); 64 | Stylesheet css = new CSSParser().parseInlineStyle(inlineNode); 65 | ParsedValue parsedValue = uncatch(() -> css.getRules().get(0).getDeclarations().get(0).getParsedValue()); 66 | return context.resolve(parsedValue); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/feature/preview/SvgPreviewer.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.feature.preview; 2 | 3 | import static xdean.jex.util.lang.ExceptionUtil.uncatch; 4 | 5 | import org.springframework.stereotype.Service; 6 | 7 | import com.sun.javafx.geom.Path2D; 8 | 9 | import io.reactivex.Maybe; 10 | import javafx.scene.canvas.GraphicsContext; 11 | import javafx.scene.paint.Color; 12 | import javafx.scene.shape.SVGPath; 13 | import javafx.scene.transform.Affine; 14 | import xdean.css.editor.model.CssContext; 15 | import xdean.jex.util.string.StringUtil; 16 | 17 | @Service 18 | public class SvgPreviewer implements CssElementPreviewer { 19 | 20 | public static boolean verify(String svg) { 21 | if (StringUtil.isEmpty(svg)) { 22 | return false; 23 | } 24 | return uncatch(() -> new Path2D().appendSVGPath(svg)); 25 | } 26 | 27 | @Override 28 | public Maybe parse(CssContext context, String svg) { 29 | svg = StringUtil.unWrap(svg, "\"", "\""); 30 | try { 31 | new Path2D().appendSVGPath(svg); 32 | } catch (Exception e) { 33 | return Maybe.empty(); 34 | } 35 | return Maybe.just(svg); 36 | } 37 | 38 | @Override 39 | public void preview(GraphicsContext graphics, String svg, double width, double height) { 40 | SVGPath svgPath = new SVGPath(); 41 | svgPath.setContent(svg); 42 | double scaleRate = Math.min(width / svgPath.getBoundsInLocal().getWidth(), height / svgPath.getBoundsInLocal().getHeight()); 43 | 44 | graphics.setTransform(new Affine(Affine.scale(scaleRate, scaleRate))); 45 | graphics.setFill(Color.BLACK); 46 | graphics.beginPath(); 47 | graphics.appendSVGPath(svg); 48 | graphics.closePath(); 49 | graphics.fill(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/feature/suggestion/CssSuggestionConfig.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.feature.suggestion; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | @Configuration 7 | public class CssSuggestionConfig { 8 | 9 | @Bean 10 | public CssSuggestionService service() { 11 | return new SimpleSuggestion(); 12 | } 13 | 14 | @Bean 15 | public CssSuggestionFilter filterByEqual() { 16 | return CssSuggestionFilter.create(0, (suggestion, input) -> suggestion.equalsIgnoreCase(input)); 17 | } 18 | 19 | @Bean 20 | public CssSuggestionFilter filterByStart() { 21 | return CssSuggestionFilter.create(1, (suggestion, input) -> suggestion.toLowerCase().startsWith(input)); 22 | } 23 | 24 | @Bean 25 | public CssSuggestionFilter filterByContain() { 26 | return CssSuggestionFilter.create(2, (suggestion, input) -> suggestion.toLowerCase().contains(input)); 27 | } 28 | 29 | @Bean 30 | public CssSuggestionFilter filterByInclude() { 31 | return CssSuggestionFilter.create(3, (suggestion, input) -> { 32 | suggestion = suggestion.toLowerCase();// lower can be option 33 | int index = 0; 34 | for (char c : input.toCharArray()) { 35 | index = suggestion.indexOf(c); 36 | if (index == -1) { 37 | break; 38 | } 39 | suggestion = suggestion.substring(index); 40 | } 41 | return index != -1; 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/feature/suggestion/CssSuggestionFilter.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.feature.suggestion; 2 | 3 | import java.util.function.BiPredicate; 4 | 5 | import org.springframework.core.Ordered; 6 | 7 | /** 8 | * Suggestion Filter. High order filter will be first execute and the accepted 9 | * suggestion also has high order. 10 | * 11 | * 12 | * @author Dean Xu (XDean@github.com) 13 | */ 14 | public interface CssSuggestionFilter extends Ordered { 15 | /** 16 | * Filter the suggestion. 17 | * 18 | * @param suggestion the candidate suggestion 19 | * @param input the input text 20 | * @return return true to accept the suggestion, return false to skip the 21 | * suggestion (to next suggestion). 22 | */ 23 | boolean filter(String suggestion, String input); 24 | 25 | static CssSuggestionFilter create(int order, BiPredicate filter) { 26 | return new CssSuggestionFilter() { 27 | @Override 28 | public int getOrder() { 29 | return order; 30 | } 31 | 32 | @Override 33 | public boolean filter(String suggestion, String input) { 34 | return filter.test(suggestion, input); 35 | } 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/feature/suggestion/CssSuggestionService.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.feature.suggestion; 2 | 3 | import java.util.Collection; 4 | 5 | import javafx.scene.control.IndexRange; 6 | import xdean.css.editor.model.CssContext; 7 | 8 | public interface CssSuggestionService { 9 | Collection getSuggestion(String text, int caretPos, CssContext context); 10 | 11 | IndexRange getReplaceRange(String text, int caretPos, CssContext context); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/feature/suggestion/CssSuggestionsFilter.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.feature.suggestion; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.List; 6 | 7 | import javax.inject.Inject; 8 | 9 | import org.springframework.stereotype.Service; 10 | 11 | import io.reactivex.Observable; 12 | 13 | @Service 14 | public class CssSuggestionsFilter { 15 | @Inject 16 | Collection filters; 17 | 18 | public Collection filter(Collection suggestions, 19 | String keyWord) { 20 | String input = keyWord.toLowerCase();// XXX lower can be option 21 | List list = new ArrayList<>(); 22 | Observable.fromIterable(suggestions) 23 | .distinct() 24 | .groupBy(s -> filters.stream() 25 | .filter(f -> f.filter(s, input)) 26 | .findFirst()) 27 | .filter(g -> g.getKey().isPresent()) 28 | .sorted((g1, g2) -> g1.getKey().get().getOrder() - g2.getKey().get().getOrder()) 29 | .subscribe(g -> g.forEach(list::add)); 30 | return list; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/feature/suggestion/RegexSuggestion.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.feature.suggestion; 2 | 3 | import java.util.Collection; 4 | import java.util.Collections; 5 | import java.util.regex.Matcher; 6 | import java.util.regex.Pattern; 7 | 8 | import javax.inject.Inject; 9 | 10 | import xdean.css.editor.model.CssContext; 11 | import xdean.jex.util.string.StringUtil; 12 | 13 | public class RegexSuggestion extends SimpleSuggestion { 14 | private static Pattern pattern = Pattern.compile("([^a-zA-Z0-9-_]*)([a-zA-Z0-9-_]+)"); 15 | 16 | @Inject 17 | CssSuggestionsFilter filter; 18 | 19 | @Override 20 | public Collection getSuggestion(String text, int caretPos, CssContext context) { 21 | String prePart = text.substring(0, caretPos); 22 | int leftIndex = prePart.lastIndexOf('{'); 23 | int rightIndex = prePart.lastIndexOf('}'); 24 | if (leftIndex == rightIndex) { 25 | return Collections.emptyList(); 26 | } 27 | prePart = prePart.substring(Math.max(leftIndex, rightIndex) + 1); 28 | 29 | Matcher matcher = pattern.matcher(prePart); 30 | String lastMatch1 = ""; 31 | String lastMatch2 = null; 32 | while (matcher.find()) { 33 | lastMatch1 = matcher.group(1); 34 | lastMatch2 = matcher.group(2); 35 | } 36 | final String keyWord = lastMatch2; 37 | 38 | if (StringUtil.notEmpty(keyWord) && prePart.endsWith(keyWord)) { 39 | if (leftIndex > rightIndex) { 40 | if (lastMatch1.indexOf(':') > -1) { 41 | // TODO value suggestion 42 | } else { 43 | return filter.filter(context.getKeys(), keyWord); 44 | } 45 | } else { 46 | if (lastMatch1.indexOf('.') > -1) { 47 | return filter.filter(context.getClasses(), keyWord); 48 | } else if (lastMatch1.indexOf('#') > -1) { 49 | return filter.filter(context.getIds(), keyWord); 50 | } else if (lastMatch1.indexOf(':') > -1) { 51 | return filter.filter(context.getStates(), keyWord); 52 | } else { 53 | return filter.filter(context.getJavaClasses(), keyWord); 54 | } 55 | } 56 | } 57 | return Collections.emptyList(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/feature/suggestion/SimpleSuggestion.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.feature.suggestion; 2 | 3 | import java.util.Collection; 4 | import java.util.Collections; 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | 8 | import javax.inject.Inject; 9 | 10 | import javafx.scene.control.IndexRange; 11 | import xdean.css.editor.model.CssContext; 12 | 13 | public class SimpleSuggestion implements CssSuggestionService { 14 | 15 | protected static final Set LEGAL_CHARS; 16 | 17 | static { 18 | Set set = new HashSet<>(); 19 | for (char c = '0'; c <= '9'; c++) { 20 | set.add(c); 21 | } 22 | for (char c = 'a'; c <= 'z'; c++) { 23 | set.add(c); 24 | set.add((char) (c - 'a' + 'A')); 25 | } 26 | set.add('-'); 27 | set.add('_'); 28 | LEGAL_CHARS = Collections.unmodifiableSet(set); 29 | } 30 | 31 | @Inject 32 | CssSuggestionsFilter filter; 33 | 34 | @Override 35 | public Collection getSuggestion(String text, int caretPos, CssContext context) { 36 | String prePart = text.substring(0, caretPos); 37 | IndexRange replaceRange = getReplaceRange(text, caretPos, context); 38 | char c = ' '; 39 | int index = replaceRange.getStart(); 40 | while (index > 0) { 41 | index--; 42 | c = text.charAt(index); 43 | if (c != ' ' && c != '\t') { 44 | break; 45 | } 46 | } 47 | String keyWord = text.substring(replaceRange.getStart(), caretPos); 48 | 49 | boolean inBrace = prePart.lastIndexOf('{') > prePart.lastIndexOf('}'); 50 | if (inBrace) { 51 | if (c == ':') { 52 | // XXX Can be smarter? 53 | // return filterSuggestion(CSSRepository.GLOBAL.getKeys(), keyWord); 54 | return Collections.emptyList(); 55 | } else if (c == ' ' || c == '\t' || c == '\n' | c == '{') { 56 | return filter.filter(context.getKeys(), keyWord); 57 | } else { 58 | return Collections.emptyList(); 59 | } 60 | } else { 61 | if (c == '.') { 62 | return filter.filter(context.getClasses(), keyWord); 63 | } else if (c == '#') { 64 | return filter.filter(context.getIds(), keyWord); 65 | } else if (c == ':') { 66 | return filter.filter(context.getStates(), keyWord); 67 | } else { 68 | return filter.filter(context.getJavaClasses(), keyWord); 69 | } 70 | } 71 | } 72 | 73 | @Override 74 | public IndexRange getReplaceRange(String text, int caretPos, CssContext context) { 75 | int start = caretPos - 1; 76 | int end = caretPos; 77 | while (start > -1 && LEGAL_CHARS.contains(text.charAt(start))) { 78 | start--; 79 | } 80 | 81 | while (end < text.length() && LEGAL_CHARS.contains(text.charAt(end))) { 82 | end++; 83 | } 84 | 85 | return new IndexRange(start + 1, end); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/model/CssContext.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.model; 2 | 3 | import java.io.IOException; 4 | import java.net.URL; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | 10 | import com.google.common.collect.LinkedHashMultiset; 11 | import com.google.common.collect.LinkedListMultimap; 12 | import com.google.common.collect.Multimap; 13 | import com.google.common.collect.Multiset; 14 | import com.sun.javafx.css.ParsedValueImpl; 15 | import com.sun.javafx.css.Selector; 16 | import com.sun.javafx.css.Stylesheet; 17 | import com.sun.javafx.css.parser.CSSParser; 18 | 19 | import io.reactivex.Observable; 20 | import javafx.css.ParsedValue; 21 | import javafx.scene.paint.Paint; 22 | import javafx.scene.text.Font; 23 | import xdean.jex.log.LogFactory; 24 | import xdean.jex.util.collection.ListUtil; 25 | import xdean.jex.util.task.TaskUtil; 26 | 27 | public class CssContext { 28 | private static final CssContext MODENA = new CssContext(); 29 | private static final String ROOT = "*.root";; 30 | private static final Font FONT = Font.font(1); 31 | static { 32 | try { 33 | MODENA.load(CssContext.class.getResource( 34 | "/com/sun/javafx/scene/control/skin/modena/modena.css")); 35 | } catch (IOException e) { 36 | LogFactory.from(CssContext.class).error("Load modena.css fail!", e); 37 | throw new RuntimeException(e); 38 | } 39 | } 40 | 41 | public static CssContext createByDefault() { 42 | return new CssContext(MODENA); 43 | } 44 | 45 | final Multiset selectors; 46 | final Multiset classes; 47 | final Multiset javaClasses; 48 | final Multiset ids; 49 | final Multiset states; 50 | final LinkedListMultimap> entries; 51 | 52 | // Values 53 | final LinkedListMultimap> paints; 54 | 55 | Pattern classPattern = Pattern.compile("\\.[a-zA-Z][A-Za-z0-9-_]*"); 56 | Pattern idPattern = Pattern.compile("#[a-zA-Z][A-Za-z0-9-_]*"); 57 | Pattern statePattern = Pattern.compile(":[a-zA-Z][A-Za-z0-9-_]*"); 58 | Pattern javaPattern = Pattern.compile("([^.#:A-Za-z0-9-_]|(?^))[a-zA-Z][A-Za-z0-9-_]*"); 59 | 60 | public CssContext() { 61 | selectors = LinkedHashMultiset.create(); 62 | classes = LinkedHashMultiset.create(); 63 | javaClasses = LinkedHashMultiset.create(); 64 | ids = LinkedHashMultiset.create(); 65 | states = LinkedHashMultiset.create(); 66 | entries = LinkedListMultimap.create(); 67 | paints = LinkedListMultimap.create(); 68 | } 69 | 70 | public CssContext(String text) { 71 | this(); 72 | load(text); 73 | } 74 | 75 | public CssContext(CssContext context) { 76 | this(); 77 | add(context); 78 | } 79 | 80 | public void add(CssContext cr) { 81 | this.selectors.addAll(cr.selectors); 82 | this.classes.addAll(cr.classes); 83 | this.javaClasses.addAll(cr.javaClasses); 84 | this.ids.addAll(cr.ids); 85 | this.states.addAll(cr.states); 86 | this.entries.putAll(cr.entries); 87 | this.paints.putAll(cr.paints); 88 | } 89 | 90 | public void remove(CssContext cr) { 91 | this.selectors.removeAll(cr.selectors); 92 | this.classes.removeAll(cr.classes); 93 | this.javaClasses.removeAll(cr.javaClasses); 94 | this.ids.removeAll(cr.ids); 95 | this.states.removeAll(cr.states); 96 | cr.entries.entries().forEach(e -> this.entries.remove(e.getKey(), e.getValue())); 97 | cr.paints.entries().forEach(e -> this.paints.remove(e.getKey(), e.getValue())); 98 | } 99 | 100 | public void load(URL url) throws IOException { 101 | load(new CSSParser().parse(url)); 102 | } 103 | 104 | public void load(String styleText) { 105 | load(new CSSParser().parse(styleText)); 106 | } 107 | 108 | private void load(Stylesheet css) { 109 | loadSelector(css); 110 | loadDeclaration(css); 111 | resolveRoot(css); 112 | } 113 | 114 | private void loadSelector(Stylesheet css) { 115 | Observable.just(css) 116 | .flatMap(s -> Observable.fromIterable(s.getRules())) 117 | .flatMap(r -> Observable.fromIterable(r.getSelectors())) 118 | .map(Selector::toString) 119 | .map(s -> s.replace("*", "")) 120 | .subscribe(s -> { 121 | selectors.add(s); 122 | classes.addAll(loadClass(s)); 123 | ids.addAll(loadId(s)); 124 | states.addAll(loadState(s)); 125 | javaClasses.addAll(loadJavaClass(s)); 126 | }); 127 | } 128 | 129 | private void loadDeclaration(Stylesheet css) { 130 | css.getRules().stream() 131 | .flatMap(r -> r.getDeclarations().stream()) 132 | .forEach(d -> entries.put(d.getProperty(), d.getParsedValue())); 133 | } 134 | 135 | private void resolveRoot(Stylesheet css) { 136 | css.getRules().stream() 137 | .filter(r -> r.getSelectors().stream() 138 | .map(Selector::toString) 139 | .map(String::trim) 140 | .filter(s -> s.equals(ROOT)) 141 | .findAny() 142 | .isPresent()) 143 | .flatMap(r -> r.getDeclarations().stream()) 144 | .forEach(d -> { 145 | ParsedValue pv = d.getParsedValue(); 146 | ParsedValue resolve = resolve(pv); 147 | if (classifyValue(resolve.getValue(), d.getProperty(), d.getParsedValue()) == false) { 148 | classifyValue(resolve.convert(FONT), d.getProperty(), new FunctionParsedValue<>(() -> resolve(pv).convert(FONT))); 149 | } 150 | }); 151 | } 152 | 153 | @SuppressWarnings("unchecked") 154 | private boolean classifyValue(Object classObject, String key, ParsedValue value) { 155 | if (classObject instanceof Paint) { 156 | paints.put(key, (ParsedValue) value); 157 | return true; 158 | } 159 | return false; 160 | } 161 | 162 | private List loadClass(String selector) { 163 | List ids = new ArrayList<>(); 164 | Matcher matcher = classPattern.matcher(selector); 165 | while (matcher.find()) { 166 | ids.add(selector.substring(matcher.start() + 1, matcher.end())); 167 | } 168 | return ids; 169 | } 170 | 171 | private List loadJavaClass(String selector) { 172 | List javaClasses = new ArrayList<>(); 173 | Matcher matcher = javaPattern.matcher(selector); 174 | while (matcher.find()) { 175 | javaClasses.add(selector.substring(matcher.start() + (matcher.group("START") == null ? 1 : 0), matcher.end())); 176 | } 177 | return javaClasses; 178 | } 179 | 180 | private List loadId(String selector) { 181 | List ids = new ArrayList<>(); 182 | Matcher matcher = idPattern.matcher(selector); 183 | while (matcher.find()) { 184 | ids.add(selector.substring(matcher.start() + 1, matcher.end())); 185 | } 186 | return ids; 187 | } 188 | 189 | private List loadState(String selector) { 190 | List state = new ArrayList<>(); 191 | Matcher matcher = statePattern.matcher(selector); 192 | while (matcher.find()) { 193 | state.add(selector.substring(matcher.start() + 1, matcher.end())); 194 | } 195 | return state; 196 | } 197 | 198 | @SuppressWarnings({ "rawtypes", "unchecked" }) 199 | public ParsedValue resolve(ParsedValue pv) { 200 | try { 201 | if (pv instanceof ParsedValueImpl) { 202 | ParsedValueImpl pvi = (ParsedValueImpl) pv; 203 | if (pvi.isContainsLookups() == false) { 204 | return pv; 205 | } else if (pvi.isLookup()) { 206 | T lookup = lookup(pv.getValue()); 207 | // System.out.printf("lookup %s find %s\n", pv.getValue(), lookup); 208 | return new SimpleParsedValue<>(lookup); 209 | } else { 210 | V value = pv.getValue(); 211 | if (value instanceof ParsedValue) { 212 | return new ParsedValueImpl<>((V) resolve((ParsedValue) value), pv.getConverter()); 213 | } else if (value instanceof ParsedValue[]) { 214 | ParsedValue[] originPvs = (ParsedValue[]) value; 215 | ParsedValue[] pvs = new ParsedValue[originPvs.length]; 216 | for (int i = 0; i < pvs.length; i++) { 217 | pvs[i] = resolve(originPvs[i]); 218 | } 219 | return new ParsedValueImpl<>((V) pvs, pv.getConverter()); 220 | } 221 | } 222 | } 223 | } catch (ClassCastException e) { 224 | return pv; 225 | } 226 | return pv; 227 | } 228 | 229 | public T lookup(Object o) { 230 | return TaskUtil.firstSuccess( 231 | () -> _lookup(o), 232 | () -> this == MODENA ? null : MODENA._lookup(o)); 233 | } 234 | 235 | @SuppressWarnings("unchecked") 236 | private T _lookup(Object o) { 237 | return TaskUtil.firstSuccess( 238 | () -> (T) ListUtil.lastGet(paints.get(o.toString().trim().toLowerCase()), 0).convert(FONT)); 239 | } 240 | 241 | public Multiset getSelectors() { 242 | return selectors; 243 | } 244 | 245 | public Multiset getClasses() { 246 | return classes; 247 | } 248 | 249 | public Multiset getIds() { 250 | return ids; 251 | } 252 | 253 | public Multiset getStates() { 254 | return states; 255 | } 256 | 257 | public Multiset getKeys() { 258 | return entries.keys(); 259 | } 260 | 261 | public Multimap> getEntries() { 262 | return entries; 263 | } 264 | 265 | public Multiset getJavaClasses() { 266 | return javaClasses; 267 | } 268 | 269 | public LinkedListMultimap> getPaints() { 270 | return paints; 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/model/FileWrapper.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.model; 2 | 3 | import java.nio.file.Path; 4 | import java.util.Optional; 5 | 6 | import xdean.jex.extra.collection.Either; 7 | 8 | public class FileWrapper { 9 | 10 | public static FileWrapper existFile(Path path) { 11 | return new FileWrapper(Either.left(path)); 12 | } 13 | 14 | public static FileWrapper newFile(int i) { 15 | return new FileWrapper(Either.right(i)); 16 | } 17 | 18 | public final Either fileOrNew; 19 | 20 | public FileWrapper(Either fileOrNewOrder) { 21 | this.fileOrNew = fileOrNewOrder; 22 | } 23 | 24 | public boolean isExistFile() { 25 | return fileOrNew.isLeft(); 26 | } 27 | 28 | public boolean isNewFile() { 29 | return fileOrNew.isRight(); 30 | } 31 | 32 | public Optional getExistFile() { 33 | return fileOrNew.asLeft(); 34 | } 35 | 36 | public Optional getNewOrder() { 37 | return fileOrNew.asRight(); 38 | } 39 | 40 | public String getFileName() { 41 | return fileOrNew.unify(p -> p.getFileName().toString(), i -> newFileName(i)); 42 | } 43 | 44 | public static String newFileName(int i) { 45 | return "new " + i; 46 | } 47 | 48 | @Override 49 | public String toString() { 50 | return getFileName(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/model/FunctionParsedValue.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.model; 2 | 3 | import com.google.common.base.Supplier; 4 | 5 | import javafx.css.ParsedValue; 6 | import javafx.scene.text.Font; 7 | 8 | class FunctionParsedValue extends ParsedValue { 9 | Supplier result; 10 | 11 | FunctionParsedValue(Supplier actualResult) { 12 | super(null, null); 13 | result = actualResult; 14 | } 15 | 16 | @Override 17 | public T convert(Font font) { 18 | return result.get(); 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/model/SimpleParsedValue.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.model; 2 | 3 | import javafx.css.ParsedValue; 4 | import javafx.scene.text.Font; 5 | 6 | class SimpleParsedValue extends ParsedValue { 7 | T result; 8 | 9 | SimpleParsedValue(T actualResult) { 10 | super(null, null); 11 | result = actualResult; 12 | } 13 | 14 | @Override 15 | public T convert(Font font) { 16 | return result; 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/service/ContextService.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.service; 2 | 3 | import java.util.Optional; 4 | 5 | import javax.annotation.Nullable; 6 | 7 | import javafx.beans.property.ObjectProperty; 8 | import javafx.collections.ObservableList; 9 | import javafx.event.Event; 10 | import javafx.stage.Stage; 11 | import xdean.css.editor.context.setting.model.CssEditorKeyOption; 12 | import xdean.css.editor.control.CssEditor; 13 | import xdean.jfxex.bean.annotation.CheckNull; 14 | 15 | public interface ContextService { 16 | 17 | ObservableList editorList(); 18 | 19 | ObjectProperty<@CheckNull CssEditor> activeEditorProperty(); 20 | 21 | @Nullable 22 | default CssEditor getActiveEditor() { 23 | return activeEditorProperty().getValue(); 24 | } 25 | 26 | default Optional getActiveEditorSafe() { 27 | return Optional.ofNullable(getActiveEditor()); 28 | } 29 | 30 | Stage stage(); 31 | 32 | default void fire(@Nullable CssEditor editor, Event event) { 33 | if (editor == null) { 34 | stage().fireEvent(event); 35 | } else { 36 | editor.fireEvent(event); 37 | } 38 | } 39 | 40 | default void fire(@Nullable CssEditor editor, CssEditorKeyOption keyOption) { 41 | fire(editor, keyOption.getEvent(editor)); 42 | } 43 | 44 | default void fire(Event event) { 45 | fire(getActiveEditor(), event); 46 | } 47 | 48 | default void fire(CssEditorKeyOption keyOption) { 49 | fire(getActiveEditor(), keyOption); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/service/DialogService.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.service; 2 | 3 | import javax.inject.Inject; 4 | import javax.inject.Named; 5 | 6 | import org.controlsfx.control.Notifications; 7 | import org.controlsfx.dialog.ExceptionDialog; 8 | import org.springframework.stereotype.Service; 9 | 10 | import javafx.geometry.Pos; 11 | import javafx.scene.control.Alert; 12 | import javafx.scene.control.Alert.AlertType; 13 | import javafx.scene.control.ButtonType; 14 | import javafx.scene.image.ImageView; 15 | import javafx.stage.Stage; 16 | import javafx.util.Duration; 17 | import xdean.jfx.spring.annotation.FxReady; 18 | import xdean.jfx.spring.context.FxContext; 19 | import xdean.jfxex.extra.FluentDialog; 20 | 21 | @Service 22 | @FxReady 23 | public class DialogService { 24 | private @Inject @Named(FxContext.FX_PRIMARY_STAGE) Stage owner; 25 | 26 | public FluentDialog infoDialog() { 27 | return FluentDialog.create(new Alert(AlertType.INFORMATION)) 28 | .owner(owner) 29 | .button(ButtonType.OK); 30 | } 31 | 32 | public FluentDialog warnDialog(Throwable e) { 33 | return FluentDialog.create(new Alert(AlertType.WARNING)) 34 | .owner(owner) 35 | .button(ButtonType.OK); 36 | } 37 | 38 | public FluentDialog errorDialog(Throwable e) { 39 | return FluentDialog.create(new ExceptionDialog(e)) 40 | .owner(owner); 41 | } 42 | 43 | public FluentDialog confirmDialog() { 44 | return FluentDialog.create(new Alert(AlertType.CONFIRMATION)) 45 | .owner(owner) 46 | .button(ButtonType.OK, ButtonType.CANCEL); 47 | } 48 | 49 | public FluentDialog confirmCancelDialog() { 50 | return FluentDialog.create(new Alert(AlertType.CONFIRMATION)) 51 | .owner(owner) 52 | .button(ButtonType.YES, ButtonType.NO, ButtonType.CANCEL); 53 | } 54 | 55 | public Notifications infoNotification() { 56 | return createNotification() 57 | .graphic(new ImageView(Notifications.class.getResource("/org/controlsfx/dialog/dialog-information.png") 58 | .toExternalForm())); // $NON-NLS-1$ 59 | } 60 | 61 | public Notifications warningNotification() { 62 | return createNotification() 63 | .graphic(new ImageView(Notifications.class.getResource("/org/controlsfx/dialog/dialog-warning.png") 64 | .toExternalForm())); // $NON-NLS-1$ 65 | } 66 | 67 | public Notifications errorNotification(Throwable e) { 68 | return createNotification() 69 | .graphic(new ImageView(Notifications.class.getResource("/org/controlsfx/dialog/dialog-error.png") 70 | .toExternalForm())) // $NON-NLS-1$ 71 | .onAction(event -> errorDialog(e).dialog(d -> d.getDialogPane().setExpanded(false)).showAndWait()); 72 | } 73 | 74 | private Notifications createNotification() { 75 | return Notifications.create() 76 | .owner(owner) 77 | .hideAfter(Duration.seconds(5)) 78 | .position(Pos.BOTTOM_RIGHT); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/service/MessageService.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.service; 2 | 3 | import java.text.MessageFormat; 4 | import java.util.Locale; 5 | 6 | import javax.inject.Inject; 7 | 8 | import org.springframework.context.MessageSource; 9 | import org.springframework.context.NoSuchMessageException; 10 | import org.springframework.stereotype.Service; 11 | 12 | import xdean.css.editor.context.setting.model.option.Option; 13 | 14 | @Service 15 | public class MessageService { 16 | 17 | private @Inject MessageSource messageSource; 18 | private @Inject Option localeOption; 19 | 20 | public String getMessage(String code, Object... args) throws NoSuchMessageException { 21 | return messageSource.getMessage(code, args, localeOption.getValue()); 22 | } 23 | 24 | public String getMessageDefault(String defaultMessage, String code, Object... args) { 25 | return messageSource.getMessage(code, args, defaultMessage, localeOption.getValue()); 26 | } 27 | 28 | public MessageFormat getMessageFormat(String code) { 29 | return new MessageFormat(getMessage(code), localeOption.getValue()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/service/RecentFileService.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.service; 2 | 3 | import static xdean.jex.util.function.Predicates.isEquals; 4 | import static xdean.jfxex.bean.ListenerUtil.addListenerAndInvoke; 5 | import static xdean.jfxex.bean.ListenerUtil.list; 6 | 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import java.nio.file.Paths; 10 | import java.util.Arrays; 11 | import java.util.Optional; 12 | import java.util.stream.Collectors; 13 | 14 | import javax.annotation.PostConstruct; 15 | import javax.inject.Inject; 16 | import javax.inject.Named; 17 | 18 | import org.springframework.stereotype.Service; 19 | 20 | import javafx.collections.FXCollections; 21 | import javafx.collections.ObservableList; 22 | import xdean.css.editor.context.setting.SettingKeys; 23 | import xdean.css.editor.context.setting.model.option.StringOption; 24 | import xdean.css.editor.control.CssEditor; 25 | import xdean.css.editor.feature.CssEditorFeature; 26 | import xdean.jex.log.Logable; 27 | 28 | @Service 29 | public class RecentFileService implements Logable, CssEditorFeature { 30 | 31 | @Inject 32 | @Named(SettingKeys.RECENT_LOC) 33 | private StringOption recent; 34 | 35 | private final ObservableList recentFiles = FXCollections.observableArrayList(); 36 | 37 | @PostConstruct 38 | private void init() { 39 | recentFiles.addListener(list(b -> b.onAdd(c -> save()))); 40 | addListenerAndInvoke(recent.valueProperty(), (ob, o, n) -> load()); 41 | } 42 | 43 | @Override 44 | public void bind(CssEditor editor) { 45 | editor.fileProperty().addListener((ob, o, n) -> { 46 | if (n.isExistFile()) { 47 | setLatestFile(n.getExistFile().get()); 48 | } 49 | }); 50 | } 51 | 52 | public ObservableList getRecentFiles() { 53 | return recentFiles; 54 | } 55 | 56 | public void load() { 57 | try { 58 | recentFiles.setAll(Arrays.asList(recent.getValue().split(",")) 59 | .stream() 60 | .map(String::trim) 61 | .filter(s -> !s.isEmpty()) 62 | .map(Paths::get) 63 | .filter(Files::exists) 64 | .collect(Collectors.toList())); 65 | } catch (Exception e) { 66 | error().log("Error to load recent location", e); 67 | } 68 | } 69 | 70 | public void save() { 71 | recent.setValue(String.join(", ", recentFiles.stream() 72 | .filter(Files::exists) 73 | .map(Path::toString) 74 | .collect(Collectors.toList()))); 75 | } 76 | 77 | public Optional getLatestFile() { 78 | return recentFiles.stream().findFirst(); 79 | } 80 | 81 | public void setLatestFile(Path path) { 82 | recentFiles.removeIf(isEquals(path)); 83 | recentFiles.add(0, path); 84 | } 85 | 86 | public void clear() { 87 | recentFiles.clear(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/xdean/css/editor/service/SkinService.java: -------------------------------------------------------------------------------- 1 | package xdean.css.editor.service; 2 | 3 | import java.nio.file.DirectoryStream; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | 7 | import javax.annotation.PostConstruct; 8 | import javax.inject.Inject; 9 | import javax.inject.Named; 10 | 11 | import org.springframework.stereotype.Service; 12 | 13 | import io.reactivex.rxjavafx.observables.JavaFxObservable; 14 | import xdean.css.editor.context.Context; 15 | import xdean.css.editor.context.setting.DefaultValue.DefaultSkin; 16 | import xdean.css.editor.context.setting.SettingKeys; 17 | import xdean.css.editor.context.setting.model.option.Option; 18 | import xdean.jex.log.Logable; 19 | import xdean.jex.util.string.StringUtil; 20 | import xdean.jfx.spring.annotation.FxReady; 21 | import xdean.jfx.spring.splash.PreloadReporter; 22 | import xdean.jfx.spring.splash.PreloadReporter.SubReporter; 23 | import xdean.jfxex.support.skin.SkinManager; 24 | import xdean.jfxex.support.skin.SkinStyle; 25 | 26 | @Service 27 | @FxReady 28 | public class SkinService extends SkinManager implements Logable { 29 | @Inject 30 | @Named(SettingKeys.SKIN) 31 | private Option skinOption; 32 | 33 | @Inject 34 | private DialogService dialogService; 35 | 36 | @Inject 37 | private PreloadReporter preload; 38 | 39 | @PostConstruct 40 | public void init() throws Exception { 41 | SubReporter sub = preload.load("Loading skins..."); 42 | // load default skins 43 | for (SkinStyle ss : DefaultSkin.values()) { 44 | sub.load("Loading " + ss.getName()); 45 | addSkin(ss); 46 | } 47 | // load skin files in /skin 48 | Path skinFolder = Context.HOME_PATH.resolve("skin"); 49 | try { 50 | if (Files.notExists(skinFolder)) { 51 | Files.createDirectory(skinFolder); 52 | } else { 53 | try (DirectoryStream stream = Files.newDirectoryStream(skinFolder)) { 54 | stream.forEach(path -> { 55 | String fileName = path.getFileName().toString(); 56 | if (!Files.isDirectory(path) && (fileName.endsWith(".css") || fileName.endsWith(".bss"))) { 57 | String url = path.toUri().toString(); 58 | String name = StringUtil.upperFirst(fileName.substring(0, fileName.length() - 4)); 59 | sub.load("Loading " + name); 60 | addSkin(new SkinStyle() { 61 | @Override 62 | public String getURL() { 63 | return url; 64 | } 65 | 66 | @Override 67 | public String getName() { 68 | return name; 69 | } 70 | }); 71 | } 72 | }); 73 | } 74 | } 75 | throw new Exception(); 76 | } catch (Exception e) { 77 | dialogService.errorNotification(e) 78 | .text("Load skins failed.") 79 | .show(); 80 | } 81 | getSkinList().stream().map(SkinStyle::getURL).map(s -> "loaded skin: " + s).forEach(this::debug); 82 | 83 | sub.load("Read skin setting"); 84 | String configSkin = skinOption.getValue(); 85 | if (configSkin != null) { 86 | getSkinList().stream() 87 | .filter(s -> s.getName().equals(configSkin)) 88 | .findAny() 89 | .ifPresent(s -> changeSkin(s)); 90 | } else { 91 | changeSkin(DefaultSkin.CLASSIC); 92 | } 93 | sub.load("Apply skin setting"); 94 | JavaFxObservable.valuesOf(skinProperty()) 95 | .subscribe(skin -> skinOption.setValue(skin.getName())); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | logging.level.root=INFO 2 | logging.level.xdean=DEBUG 3 | 4 | spring.messages.basename=message/settings -------------------------------------------------------------------------------- /src/main/resources/css/css-highlighting.css: -------------------------------------------------------------------------------- 1 | .selector { 2 | -fx-fill: red; 3 | } 4 | 5 | .selector-id { 6 | -fx-fill: #4444ff; 7 | } 8 | 9 | .selector-class { 10 | -fx-fill: #ff4444; 11 | } 12 | 13 | .selector-state { 14 | -fx-fill: #ff69b4; 15 | } 16 | 17 | .selector-java-class { 18 | -fx-font-weight: bold; 19 | -fx-fill: #222222; 20 | } 21 | 22 | .key { 23 | -fx-fill: #666666; 24 | } 25 | 26 | .value { 27 | -fx-font-weight: bold; 28 | -fx-fill: #333333; 29 | } 30 | 31 | .muticomment { 32 | -fx-fill: #008800; 33 | } -------------------------------------------------------------------------------- /src/main/resources/css/global.css: -------------------------------------------------------------------------------- 1 | .code-area { 2 | -fx-background-color: white; 3 | } 4 | 5 | .lineno { 6 | -fx-background-color: white; 7 | } 8 | 9 | .status-bar { 10 | -fx-padding: 4px; 11 | -fx-pref-height: 22px; 12 | -fx-background-color: white; 13 | -fx-background-insets: 0, 1; 14 | } 15 | 16 | .glyph-font { 17 | -fx-text-fill: -fx-text-base-color; 18 | -fx-font-family : "FontAwesome"; 19 | } 20 | 21 | .tab-icon { 22 | -fx-text-fill: DEEPSKYBLUE; 23 | } 24 | 25 | .tab-icon:selected { 26 | -fx-text-fill: BLUE; 27 | } 28 | 29 | .tab-icon:modified { 30 | -fx-text-fill: PURPLE; 31 | } 32 | 33 | .tab-icon:selected-modified { 34 | -fx-text-fill: RED; 35 | } -------------------------------------------------------------------------------- /src/main/resources/css/skin/classic.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 Karl Tauber 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * o Redistributions of source code must retain the above copyright 9 | * notice, this list of conditions and the following disclaimer. 10 | * 11 | * o Redistributions in binary form must reproduce the above copyright 12 | * notice, this list of conditions and the following disclaimer in the 13 | * documentation and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | /*---- menubar ----*/ 29 | 30 | .menu-bar { 31 | -fx-background-color: -fx-outer-border, white; 32 | -fx-background-insets: 0 0 0 0, 0 0 1 0; 33 | } 34 | 35 | 36 | /*---- toolbar ----*/ 37 | 38 | .tool-bar { 39 | -fx-spacing: 0; 40 | -fx-background-color: derive(-fx-background,25%); 41 | } 42 | 43 | .tool-bar .button, 44 | .tool-bar .toggle-button, 45 | .tool-bar .choice-box { 46 | -fx-background-color: transparent; 47 | } 48 | 49 | .tool-bar .button:hover, 50 | .tool-bar .toggle-button:hover, 51 | .tool-bar .choice-box:hover { 52 | -fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color; 53 | -fx-color: -fx-hover-base; 54 | } 55 | 56 | .tool-bar .button:armed, 57 | .tool-bar .toggle-button:armed { 58 | -fx-color: -fx-pressed-base; 59 | } 60 | 61 | .tool-bar .toggle-button:selected { 62 | -fx-background-color: -fx-pressed-base, -fx-outer-border, -fx-inner-border, -fx-body-color; 63 | } 64 | 65 | 66 | /*---- main tab pane ----*/ 67 | 68 | .main .tab-pane { 69 | -mwfx-tab-background: derive(-fx-background,25%); 70 | -mwfx-tab-active: #f47806; 71 | 72 | -fx-tab-max-width: 10em; 73 | } 74 | 75 | .main .tab-pane > .tab-header-area { 76 | -fx-padding: 0 0.25em 0 0; 77 | } 78 | 79 | .main .tab-pane > .tab-header-area > .tab-header-background { 80 | -fx-background-color: -fx-outer-border, transparent, -mwfx-tab-background; 81 | -fx-background-insets: 1 1 0 1, 0 0 1 0, 1; 82 | } 83 | 84 | .main .tab-pane > .tab-header-area > .headers-region > .tab { 85 | -fx-background-color: -mwfx-tab-background; 86 | -fx-background-insets: 0 1 1 0; 87 | -fx-background-radius: 0; 88 | -fx-padding: 0.5em 0.25em 0.5em 1em; 89 | } 90 | 91 | /* orange marker for selected/hover tab */ 92 | .main .tab-pane > .tab-header-area > .headers-region > .tab:selected { 93 | -fx-background-color: -mwfx-tab-active, -mwfx-tab-background; 94 | -fx-background-insets: 0 1 0 0, 0 1 0.333333em 0; 95 | } 96 | .main .tab-pane > .tab-header-area > .headers-region > .tab:hover { 97 | -fx-background-color: derive(-mwfx-tab-active,35%), -mwfx-tab-background; 98 | -fx-background-insets: 0 1 0 0, 0 1 0.333333em 0; 99 | } 100 | 101 | /* make tab texts brighter, except selected tab */ 102 | .main .tab-pane .tab-label { 103 | -fx-text-fill: derive(-fx-text-base-color,35%); 104 | } 105 | .main .tab-pane .tab:selected .tab-label, 106 | .main .tab-pane .tab:hover .tab-label { 107 | -fx-text-fill: -fx-text-base-color; 108 | } 109 | 110 | .main .tab-pane .focus-indicator { 111 | visibility: hidden; 112 | } 113 | 114 | 115 | /* hide tab-close-button by default; show it only for selected tab and on hover */ 116 | .main .tab-pane > .tab-header-area > .headers-region > .tab > .tab-container > .tab-close-button { 117 | -fx-background-color: transparent; 118 | } 119 | .main .tab-pane > .tab-header-area > .headers-region > .tab:selected > .tab-container > .tab-close-button, 120 | .main .tab-pane > .tab-header-area > .headers-region > .tab:hover > .tab-container > .tab-close-button { 121 | -fx-background-color: -fx-mark-color; 122 | -fx-opacity: 0.4; 123 | } 124 | .main .tab-pane > .tab-header-area > .headers-region > .tab > .tab-container > .tab-close-button:hover { 125 | -fx-background-color: red; 126 | -fx-opacity: 1.0; 127 | } 128 | 129 | 130 | /*---- scroll bars ----*/ 131 | 132 | .scroll-bar { 133 | -fx-background-color: -fx-base; 134 | -fx-background-insets: 0; 135 | } 136 | 137 | /* make horizontal increment/decrement buttons square */ 138 | .scroll-bar:horizontal > .increment-button, 139 | .scroll-bar:horizontal > .decrement-button { 140 | -fx-padding: 0.25em 0.416667em; /* 3 5 */ 141 | -fx-background-insets: 0; 142 | } 143 | 144 | /* make vertical increment/decrement buttons square */ 145 | .scroll-bar:vertical > .increment-button, 146 | .scroll-bar:vertical > .decrement-button { 147 | -fx-padding: 0.416667em 0.25em; /* 5 3 */ 148 | -fx-background-insets: 0; 149 | } 150 | 151 | .scroll-bar > .increment-button:hover, 152 | .scroll-bar > .decrement-button:hover { 153 | -fx-background-color: #ddd; 154 | } 155 | .scroll-bar > .increment-button:pressed, 156 | .scroll-bar > .decrement-button:pressed { 157 | -fx-background-color: #bbb; 158 | } 159 | 160 | /* make thumb flat */ 161 | .scroll-bar > .thumb { 162 | -fx-background-color: #00000022; 163 | -fx-background-insets: 0; 164 | -fx-background-radius: 0; 165 | } 166 | .scroll-bar > .thumb:hover { 167 | -fx-background-color: #00000044; 168 | } 169 | .scroll-bar > .thumb:pressed { 170 | -fx-background-color: #00000066; 171 | } 172 | 173 | 174 | /*---- main window ----*/ 175 | 176 | /* 1px width split pane */ 177 | .main .split-pane > .split-pane-divider { 178 | -fx-background-color: #aaa; 179 | -fx-padding: 0 1 0 0; 180 | } 181 | /* make (invisible) split pane grabber larger */ 182 | .main .split-pane:horizontal .split-pane-divider .horizontal-grabber, 183 | .main .split-pane:vertical .split-pane-divider .vertical-grabber { 184 | -fx-padding: 0.25em; 185 | } 186 | 187 | /* remove scroll pane borders */ 188 | .main .scroll-pane { 189 | -fx-padding: 0; 190 | } 191 | 192 | /* remove text area round edges */ 193 | .main .preview-pane .text-input { 194 | -fx-background-insets: 0; 195 | -fx-background-radius: 0; 196 | } 197 | 198 | 199 | /*---- find/replace ----*/ 200 | 201 | .find-replace { 202 | -fx-padding: 0.25em; 203 | -fx-background-color: -mwfx-tab-background; 204 | -fx-border-color: -fx-outer-border; 205 | -fx-border-width: 1 0 0 0; 206 | } 207 | 208 | .find-replace .text-field:error { 209 | -fx-control-inner-background: #ffebee; 210 | } 211 | .find-replace .info { 212 | -fx-text-fill: #c62828; 213 | } 214 | 215 | .find-replace .custom-text-field .left-pane { 216 | -fx-padding: 0 0.25em 0 0.5em; 217 | } 218 | .find-replace .custom-text-field .right-pane { 219 | -fx-padding: 0 0.5em 0 0.25em; 220 | } 221 | .find-replace .custom-text-field .glyph-icon { 222 | -fx-fill: #aaa; 223 | } 224 | .find-replace .custom-text-field .label { 225 | -fx-text-fill: #888; 226 | -fx-font-size: 0.9em; 227 | } 228 | 229 | .find-replace .flat-button { 230 | -fx-background-color: transparent; 231 | } 232 | .find-replace .flat-button:hover { 233 | -fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color; 234 | -fx-color: -fx-hover-base; 235 | } 236 | .find-replace .flat-button:armed { 237 | -fx-color: -fx-pressed-base; 238 | } 239 | .find-replace .flat-button:selected { 240 | -fx-background-color: -fx-outer-border; 241 | } 242 | 243 | .markdown-editor .hit { 244 | -rtfx-background-color: #FFF59D !important; 245 | } 246 | .markdown-editor .hit-active { 247 | -rtfx-background-color: #F9A825 !important; 248 | } 249 | 250 | .scroll-bar > .track { 251 | -mwfx-hit: #FBC02D; 252 | } 253 | -------------------------------------------------------------------------------- /src/main/resources/css/skin/default.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XDean/CSS-Editor-FX/9fb891b71fdfa46b675a4d1d31eb694795d2c586/src/main/resources/css/skin/default.css -------------------------------------------------------------------------------- /src/main/resources/fontawesome.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XDean/CSS-Editor-FX/9fb891b71fdfa46b675a4d1d31eb694795d2c586/src/main/resources/fontawesome.ttf -------------------------------------------------------------------------------- /src/main/resources/fxml/MainFrame.fxgraph: -------------------------------------------------------------------------------- 1 | package fxml 2 | import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView 3 | import javafx.geometry.Insets 4 | import javafx.scene.control.Button 5 | import javafx.scene.control.Menu 6 | import javafx.scene.control.MenuBar 7 | import javafx.scene.control.MenuItem 8 | import javafx.scene.control.ScrollBar 9 | import javafx.scene.control.Separator 10 | import javafx.scene.control.SeparatorMenuItem 11 | import javafx.scene.control.SplitPane 12 | import javafx.scene.control.Tab 13 | import javafx.scene.control.TabPane 14 | import javafx.scene.control.ToolBar 15 | import javafx.scene.control.Tooltip 16 | import javafx.scene.input.KeyCodeCombination 17 | import javafx.scene.input.KeyCodeCombination.Modifier 18 | import javafx.scene.layout.BorderPane 19 | import javafx.scene.layout.VBox 20 | import org.controlsfx.control.StatusBar 21 | import org.fxmisc.richtext.CodeArea 22 | import org.controlsfx.glyphfont.Glyph 23 | 24 | component MainFrame controlledby xdean.css.editor.controller.MainFrameController styledwith "../css/global.css"{ 25 | VBox { 26 | prefHeight : 600.0, 27 | prefWidth : 800.0, 28 | stylesheets : location "../css/global.css", 29 | padding : Insets { 30 | bottom : 1.0 31 | }, 32 | children : [ 33 | include fxml.MenuBar as menuBar, 34 | include fxml.ToolBar as toolBar, 35 | SplitPane { 36 | call VBox#vgrow : "ALWAYS", 37 | items : [ 38 | BorderPane { 39 | right : ScrollBar id verticalScrollBar { 40 | orientation : "VERTICAL", 41 | prefWidth : 15.0, 42 | call BorderPane#alignment : "CENTER" 43 | }, 44 | bottom : ScrollBar id horizontalScrollBar { 45 | prefHeight : 15.0, 46 | call BorderPane#alignment : "CENTER" 47 | }, 48 | center : TabPane id tabPane { 49 | call BorderPane#alignment : "CENTER", 50 | onDragDropped : controllermethod onDragDrop, 51 | onDragOver : controllermethod onDragOver, 52 | preview tabs : [ 53 | Tab { 54 | content : CodeArea { 55 | wrapText : true 56 | }, 57 | graphic : Glyph { 58 | icon : "SAVE" 59 | } 60 | } 61 | ] 62 | } 63 | } 64 | ] 65 | }, 66 | VBox id bottomExtraPane { 67 | children : [ 68 | include fxml.SearchBar as searchBar 69 | ] 70 | }, 71 | include fxml.StatusBar as statusBar 72 | ] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/resources/fxml/MainFrame.fxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 |
35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 |
46 | -------------------------------------------------------------------------------- /src/main/resources/fxml/MenuBar.fxgraph: -------------------------------------------------------------------------------- 1 | package fxml 2 | import javafx.geometry.Insets 3 | import javafx.scene.control.Button 4 | import javafx.scene.control.Menu 5 | import javafx.scene.control.MenuBar 6 | import javafx.scene.control.MenuItem 7 | import javafx.scene.control.ScrollBar 8 | import javafx.scene.control.Separator 9 | import javafx.scene.control.SeparatorMenuItem 10 | import javafx.scene.control.SplitPane 11 | import javafx.scene.control.Tab 12 | import javafx.scene.control.TabPane 13 | import javafx.scene.control.ToolBar 14 | import javafx.scene.control.Tooltip 15 | import javafx.scene.input.KeyCodeCombination 16 | import javafx.scene.input.KeyCodeCombination.Modifier 17 | import javafx.scene.layout.BorderPane 18 | import javafx.scene.layout.VBox 19 | import org.controlsfx.control.StatusBar 20 | import org.fxmisc.richtext.CodeArea 21 | import xdean.css.editor.controller.MenuBarController 22 | import org.controlsfx.glyphfont.Glyph 23 | import javafx.scene.paint.Color 24 | 25 | component MenuBar controlledby MenuBarController styledwith "../css/global.css"{ 26 | MenuBar { 27 | menus : [ 28 | Menu { 29 | text : "File", 30 | items : [ 31 | MenuItem id newItem { 32 | text : "New", 33 | graphic : Glyph { 34 | icon : "FILE_ALT" 35 | } 36 | }, 37 | MenuItem id openItem { 38 | text : "Open...", 39 | graphic : Glyph { 40 | icon : "FOLDER_OPEN_ALT" 41 | } 42 | }, 43 | Menu id recentMenu { 44 | text : "Open Recent" 45 | }, 46 | SeparatorMenuItem, 47 | MenuItem id closeItem { 48 | text : "Close", 49 | graphic : Glyph { 50 | icon : "CLOSE" 51 | } 52 | }, 53 | MenuItem id saveItem { 54 | text : "Save", 55 | graphic : Glyph { 56 | icon : "SAVE" 57 | } 58 | }, 59 | MenuItem id saveAsItem { 60 | text : "Save as..." 61 | }, 62 | MenuItem id revertItem { 63 | text : "Revert" 64 | }, 65 | SeparatorMenuItem, 66 | MenuItem { 67 | text : "Exit", 68 | onAction : controllermethod exit 69 | } 70 | ] 71 | }, 72 | Menu { 73 | text : "Edit", 74 | items : [ 75 | MenuItem id undoItem { 76 | text : "Undo", 77 | graphic : Glyph { 78 | icon : "UNDO" 79 | } 80 | }, 81 | MenuItem id redoItem { 82 | text : "Redo", 83 | graphic : Glyph { 84 | icon : "REPEAT" 85 | } 86 | }, 87 | SeparatorMenuItem, 88 | MenuItem id findItem { 89 | text : "Find" 90 | }, 91 | MenuItem id commentItem { 92 | text : "Toggle Comment" 93 | }, 94 | MenuItem id formatItem { 95 | text : "Format" 96 | } 97 | ] 98 | }, 99 | Menu { 100 | text : "Help", 101 | items : [ 102 | MenuItem id optionItem { 103 | text : "Options", 104 | graphic : Glyph { 105 | icon : "GEAR" 106 | } 107 | }, 108 | SeparatorMenuItem, 109 | Menu id skinMenu { 110 | text : "Skin" 111 | }, 112 | MenuItem id aboutItem { 113 | text : "About", 114 | graphic : Glyph { 115 | icon : "EXCLAMATION_CIRCLE" 116 | } 117 | }, 118 | MenuItem id helpItem { 119 | text : "Help", 120 | graphic : Glyph { 121 | icon : "QUESTION_CIRCLE" 122 | } 123 | } 124 | ] 125 | } 126 | ] 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/main/resources/fxml/MenuBar.fxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/main/resources/fxml/Options.fxgraph: -------------------------------------------------------------------------------- 1 | package fxml 2 | 3 | import javafx.geometry.Insets 4 | import javafx.scene.control.ButtonType 5 | import javafx.scene.control.DialogPane 6 | import javafx.scene.control.Tab 7 | import javafx.scene.control.TabPane 8 | import javafx.scene.control.TableColumn 9 | import javafx.scene.control.TableView 10 | import javafx.scene.layout.VBox 11 | import javafx.scene.input.KeyCombination 12 | import xdean.css.editor.context.setting.model.option.Option 13 | 14 | component Options controlledby xdean.css.editor.controller.OptionsController { 15 | DialogPane id root { 16 | expanded : true, 17 | buttonTypes : const ButtonType#FINISH, 18 | buttonTypes : const ButtonType#CANCEL, 19 | content : TabPane { 20 | styleClass : "floating", 21 | tabClosingPolicy : "UNAVAILABLE", 22 | tabs : [ 23 | Tab { 24 | text : "General", 25 | content : VBox id generalPane { 26 | spacing : 5.0, 27 | padding : Insets { 28 | topRightBottomLeft : 5.0 29 | } 30 | } 31 | }, 32 | Tab { 33 | text : "Key", 34 | content : TableView < Option < KeyCombination > > id keyTable { 35 | editable : true, 36 | columns : TableColumn < Option < KeyCombination >, String > id commandColumn { 37 | editable : false, 38 | prefWidth : 75.0, 39 | text : "Command" 40 | }, 41 | columns : TableColumn < Option < KeyCombination >, KeyCombination > id bindingColumn { 42 | prefWidth : 75.0, 43 | text : "Binding" 44 | }, 45 | columnResizePolicy : const TableView < ? >#CONSTRAINED_RESIZE_POLICY 46 | } 47 | } 48 | ] 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/resources/fxml/Options.fxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/main/resources/fxml/SearchBar.fxgraph: -------------------------------------------------------------------------------- 1 | package fxml 2 | 3 | import javafx.scene.layout.HBox 4 | import javafx.scene.control.Button 5 | import javafx.scene.control.CheckBox 6 | import xdean.css.editor.controller.SearchBarController 7 | import javafx.geometry.Insets 8 | 9 | component SearchBar controlledby SearchBarController { 10 | HBox id root{ 11 | alignment : "CENTER_LEFT", 12 | padding : Insets { 13 | topRightBottomLeft : 5 14 | }, 15 | spacing : 5, 16 | children : [ 17 | HBox id textContainer, // Because it is created by TextFields.createClearableTextField(); 18 | Button id findButton { 19 | text : "Find", 20 | onAction : controllermethod find 21 | }, 22 | CheckBox id caseSensitive { 23 | text : "Case Sensitive" 24 | }, 25 | CheckBox id regex { 26 | text : "Regex" 27 | }, 28 | CheckBox id wrapSearch { 29 | text : "Wrap Search" 30 | } 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/resources/fxml/SearchBar.fxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 26 | 34 | 42 | 43 | 51 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/main/resources/image/icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XDean/CSS-Editor-FX/9fb891b71fdfa46b675a4d1d31eb694795d2c586/src/main/resources/image/icon.jpg -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | ${APP_NAME} 8 | 9 | 10 | 11 | ${ENCODER_PATTERN} 12 | 13 | 14 | 15 | 17 | 18 | ${LOG_HOME}/output.%d{yyyy-MM-dd}.log 19 | 20 | 7 21 | 22 | 23 | ${ENCODER_PATTERN} 24 | 25 | 26 | 27 | 29 | 30 | ${LOG_HOME}/error.%d{yyyy-MM-dd}.log 31 | 32 | 7 33 | 34 | 35 | ${ENCODER_PATTERN} 36 | 37 | 38 | WARN 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/main/resources/message/settings.properties: -------------------------------------------------------------------------------- 1 | option.general=General 2 | option.general.common=Common 3 | option.general.common.auto-suggest=Auto Suggest 4 | option.general.common.show-line=Show Line Number 5 | option.general.common.open-last=Open Last Files 6 | option.general.common.charset=Charset 7 | option.general.text=Text 8 | option.general.text.font-family=Font 9 | option.general.text.font-size=Font Size 10 | option.general.text.wrap-text=Wrap Text 11 | option.key=Key 12 | option.find.regex=Regex Search 13 | option.find.wrap-search=Wrap Search 14 | option.find.case-sensitive=Case Sensitive 15 | option.language=Language 16 | option.skin=Skin 17 | option.recent.location=Recent Location 18 | 19 | edit.undo=Undo 20 | edit.redo=Redo 21 | edit.suggest=Suggest 22 | edit.format=Format 23 | edit.comment=Comment 24 | edit.find=Find 25 | 26 | file.new=New 27 | file.open=Open 28 | file.save=Save 29 | file.saveAs=Save As... 30 | file.close=Close 31 | file.revert=Revert 32 | file.exit=Exit 33 | 34 | help.settings=Settings... 35 | help.about=About 36 | help.help=Help --------------------------------------------------------------------------------