diff --git a/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeEditorPane.java b/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeEditorPane.java index 8aa40543..99387bb2 100644 --- a/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeEditorPane.java +++ b/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeEditorPane.java @@ -21,7 +21,9 @@ import java.awt.Font; import java.io.IOException; import java.nio.charset.StandardCharsets; import javax.swing.JPanel; +import org.fife.ui.rsyntaxtextarea.AbstractTokenMakerFactory; import org.fife.ui.rsyntaxtextarea.FileLocation; +import org.fife.ui.rsyntaxtextarea.TokenMakerFactory; import org.fife.ui.rtextarea.RTextScrollPane; import com.formdev.flatlaf.util.UIScale; @@ -33,14 +35,21 @@ import com.formdev.flatlaf.util.UIScale; class FlatThemeEditorPane extends JPanel { + private static final String FLATLAF_STYLE = "text/flatlaf"; + private final RTextScrollPane scrollPane; private final FlatSyntaxTextArea textArea; FlatThemeEditorPane() { super( new BorderLayout() ); + // register FlatLaf token maker + AbstractTokenMakerFactory tmf = (AbstractTokenMakerFactory) TokenMakerFactory.getDefaultInstance(); + tmf.putMapping( FLATLAF_STYLE, FlatThemeTokenMaker.class.getName() ); + // create text area textArea = new FlatSyntaxTextArea(); + textArea.setSyntaxEditingStyle( FLATLAF_STYLE ); // create scroll pane scrollPane = new RTextScrollPane( textArea ); diff --git a/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeFileEditor.java b/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeFileEditor.java index 71f1e2b7..18e453aa 100644 --- a/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeFileEditor.java +++ b/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeFileEditor.java @@ -35,7 +35,7 @@ public class FlatThemeFileEditor public static void main( String[] args ) { File file = new File( args.length > 0 ? args[0] - : "../flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLightLaf.properties" ); // TODO + : "theme-editor-test.properties" ); // TODO SwingUtilities.invokeLater( () -> { FlatLightLaf.install(); diff --git a/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeTokenMaker.java b/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeTokenMaker.java new file mode 100644 index 00000000..47a51831 --- /dev/null +++ b/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeTokenMaker.java @@ -0,0 +1,191 @@ +/* + * Copyright 2020 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.themeeditor; + +import org.fife.ui.rsyntaxtextarea.RSyntaxUtilities; +import org.fife.ui.rsyntaxtextarea.Token; +import org.fife.ui.rsyntaxtextarea.TokenMap; +import org.fife.ui.rsyntaxtextarea.TokenTypes; +import org.fife.ui.rsyntaxtextarea.modes.PropertiesFileTokenMaker; + +/** + * Token maker for FlatLaf properties files. + *

+ * Lets the super class parse the properties file and modify the added tokens. + * The super class uses {@link TokenTypes#RESERVED_WORD} for property keys and + * {@link TokenTypes#LITERAL_STRING_DOUBLE_QUOTE} for property values. + * + * @author Karl Tauber + */ +public class FlatThemeTokenMaker + extends PropertiesFileTokenMaker +{ + private static final int TOKEN_PROPERTY = Token.IDENTIFIER; + private static final int TOKEN_VARIABLE = Token.VARIABLE; + private static final int TOKEN_NUMBER = Token.LITERAL_NUMBER_DECIMAL_INT; + private static final int TOKEN_COLOR = Token.LITERAL_NUMBER_HEXADECIMAL; + private static final int TOKEN_STRING = Token.LITERAL_STRING_DOUBLE_QUOTE; + private static final int TOKEN_FUNCTION = Token.FUNCTION; + private static final int TOKEN_TYPE = Token.DATA_TYPE; + + private final TokenMap tokenMap = new TokenMap(); + + public FlatThemeTokenMaker() { + // null, false, true + tokenMap.put( "null", Token.RESERVED_WORD ); + tokenMap.put( "false", Token.LITERAL_BOOLEAN ); + tokenMap.put( "true", Token.LITERAL_BOOLEAN ); + + // functions + tokenMap.put( "rgb", TOKEN_FUNCTION ); + tokenMap.put( "rgba", TOKEN_FUNCTION ); + tokenMap.put( "hsl", TOKEN_FUNCTION ); + tokenMap.put( "hsla", TOKEN_FUNCTION ); + tokenMap.put( "lighten", TOKEN_FUNCTION ); + tokenMap.put( "darken", TOKEN_FUNCTION ); + tokenMap.put( "lazy", TOKEN_FUNCTION ); + + // function options + tokenMap.put( "relative", Token.RESERVED_WORD ); + tokenMap.put( "autoInverse", Token.RESERVED_WORD ); + } + + /** + * This method is only invoked from the super class. + */ + @Override + public void addToken( char[] array, int start, int end, int tokenType, int startOffset, boolean hyperlink ) { +// debugInputToken( array, start, end, tokenType, startOffset, hyperlink ); + + // ignore invalid token + if( end < start ) + return; + + if( tokenType == Token.RESERVED_WORD ) { + // key + int newTokenType = (array[start] == '@') ? TOKEN_VARIABLE : TOKEN_PROPERTY; + super.addToken( array, start, end, newTokenType, startOffset, hyperlink ); + } else if( tokenType == Token.LITERAL_STRING_DOUBLE_QUOTE ) { + // value + tokenizeValue( array, start, end, startOffset ); + } else if( tokenType == Token.VARIABLE ) { + // '{variable}' + super.addToken( array, start, end, TOKEN_TYPE, startOffset, hyperlink ); + } else { + // comments or operators + super.addToken( array, start, end, tokenType, startOffset, hyperlink ); + } + } + + private void tokenizeValue( char[] array, int start, int end, int startOffset ) { + int newStartOffset = startOffset - start; + + int currentTokenStart = start; + int currentTokenType = Token.NULL; + + for( int i = start; i <= end; i++ ) { + int newTokenType; + char ch = array[i]; + if( ch <= ' ' ) + newTokenType = Token.WHITESPACE; + else if( ch == '#' || (currentTokenType == TOKEN_COLOR && RSyntaxUtilities.isHexCharacter( ch )) ) + newTokenType = TOKEN_COLOR; + else if( ch == '$' || (currentTokenType == TOKEN_PROPERTY && isPropertyChar( ch )) ) + newTokenType = TOKEN_PROPERTY; + else if( ch == '@' || (currentTokenType == TOKEN_VARIABLE && isPropertyChar( ch )) ) + newTokenType = TOKEN_VARIABLE; + else if( currentTokenType != TOKEN_STRING && (RSyntaxUtilities.isDigit( ch ) || (currentTokenType == TOKEN_NUMBER && ch == '.')) ) + newTokenType = TOKEN_NUMBER; + else if( ch == ',' || ch == '(' || ch == ')' || ch == '"' || ch == '%' ) + newTokenType = TokenTypes.OPERATOR; + else + newTokenType = TOKEN_STRING; + + if( currentTokenType == Token.NULL ) + currentTokenType = newTokenType; + else if( newTokenType != currentTokenType ) { + addTokenImpl( array, currentTokenStart, i - 1, currentTokenType, newStartOffset + currentTokenStart ); + currentTokenType = newTokenType; + currentTokenStart = i; + } + } + + if( currentTokenType != Token.NULL ) + addTokenImpl( array, currentTokenStart, end, currentTokenType, newStartOffset + currentTokenStart ); + } + + private void addTokenImpl( char[] array, int start, int end, int tokenType, int startOffset ) { + if( tokenType == TOKEN_STRING ) { + int type = tokenMap.get( array, start, end ); + if( type != -1 ) + tokenType = type; + } + +// debugOutputToken( array, start, end, tokenType ); + super.addToken( array, start, end, tokenType, startOffset, false ); + } + + private boolean isPropertyChar( char ch ) { + return RSyntaxUtilities.isLetterOrDigit( ch ) || ch == '.' || ch == '_' || ch == '-'; + } + +/*debug + private java.util.HashMap tokenTypeStrMap; + + private void debugInputToken( char[] array, int start, int end, int tokenType, int startOffset, boolean hyperlink ) { + if( tokenTypeStrMap == null ) { + tokenTypeStrMap = new java.util.HashMap<>(); + for( java.lang.reflect.Field f : TokenTypes.class.getFields() ) { + try { + tokenTypeStrMap.put( (Integer) f.get( null ), f.getName() ); + } catch( IllegalArgumentException | IllegalAccessException ex ) { + ex.printStackTrace(); + } + } + } + + String tokenTypeStr = tokenTypeStrMap.computeIfAbsent( tokenType, t -> { + return "(unknown " + t + ")"; + } ); + + System.out.printf( "%d-%d (%d) %-30s '%s'\n", + start, end, end - start, tokenTypeStr, new String( array, start, end - start + 1 ) ); + } + + private void debugOutputToken( char[] array, int start, int end, int tokenType ) { + String tokenTypeStr = null; + switch( tokenType ) { + case TOKEN_PROPERTY: tokenTypeStr = "PROPERTY"; break; + case TOKEN_VARIABLE: tokenTypeStr = "VARIABLE"; break; + case TOKEN_NUMBER: tokenTypeStr = "NUMBER"; break; + case TOKEN_COLOR: tokenTypeStr = "COLOR"; break; + case TOKEN_STRING: tokenTypeStr = "STRING"; break; + case TOKEN_FUNCTION: tokenTypeStr = "FUNCTION"; break; + case TOKEN_TYPE: tokenTypeStr = "TYPE"; break; + case TokenTypes.OPERATOR: tokenTypeStr = "OPERATOR"; break; + case TokenTypes.WHITESPACE: tokenTypeStr = "WHITESPACE"; break; + case TokenTypes.LITERAL_BOOLEAN: tokenTypeStr = "BOOLEAN"; break; + case TokenTypes.RESERVED_WORD: tokenTypeStr = "RESERVED_WORD"; break; + default: + throw new IllegalArgumentException( String.valueOf( tokenType ) ); + } + + System.out.printf( " %d-%d (%d) %-15s '%s'\n", + start, end, end - start, tokenTypeStr, new String( array, start, end - start + 1 ) ); + } +debug*/ +} diff --git a/flatlaf-theme-editor/theme-editor-test.properties b/flatlaf-theme-editor/theme-editor-test.properties new file mode 100644 index 00000000..18bf541b --- /dev/null +++ b/flatlaf-theme-editor/theme-editor-test.properties @@ -0,0 +1,40 @@ +# +# some comment +# + +ButtonUI=com.formdev.flatlaf.ui.FlatButtonUI + CheckBoxUI = com.formdev.flatlaf.ui.FlatCheckBoxUI + +Prop.string=some string +Prop.string2="some string 123 #f80" +Prop.char1=x +Prop.char2=\u2022 +Prop.integer=123 +Prop.float=456.123 +Prop.color1=#f80 +Prop.color2=#ff8800 +Prop.color3=#f804 +Prop.color4=#ff880044 +Prop.border=1,2,3,4 + +Prop.scaledInteger={scaledInteger}123 + +Prop.null=null +Prop.false=false +Prop.true=true + +@var1=123 +Prop.var=@var1 +Prop.ref=$Prop.string + +Prop.lazy=lazy(Prop.string) + +Prop.colorFunc1=rgb(12,34,56) +Prop.colorFunc2=rgba(12,34,56,78) +Prop.colorFunc3=hsl(12,34,56) +Prop.colorFunc4=hsla(12,34,56,78) + +Prop.colorFunc5=lighten(#fe1289,20%) +Prop.colorFunc6=darken(#fe1289,20%) +Prop.colorFunc7=lighten($Prop.colorFunc4,20%,relative autoInverse) +Prop.colorFunc8=lighten(Prop.colorFunc4,20%,lazy)