diff --git a/CHANGELOG.md b/CHANGELOG.md index 920fa4a6..58709069 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ FlatLaf Change Log - Table: Hide grid and changed intercell spacing to zero. - List and Tree: Paint cell focus indicator (black rectangle) only if more than one item is selected. +- Support basic color functions in `.properties` files: `rgb(red,green,blue)`, + `rgba(red,green,blue,alpha)`, `hsl(hue,saturation,lightness)`, + `hsla(hue,saturation,lightness,alpha)`, `lighten(color,amount[,options])` and + `darken(color,amount[,options])`. - Fixed link color (in HTML text) and separator color in IntelliJ platform themes. diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/UIDefaultsLoader.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/UIDefaultsLoader.java index be889a40..aae60c2b 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/UIDefaultsLoader.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/UIDefaultsLoader.java @@ -40,6 +40,7 @@ import com.formdev.flatlaf.ui.FlatEmptyBorder; import com.formdev.flatlaf.ui.FlatLineBorder; import com.formdev.flatlaf.util.ColorFunctions; import com.formdev.flatlaf.util.DerivedColor; +import com.formdev.flatlaf.util.HSLColor; import com.formdev.flatlaf.util.StringUtils; import com.formdev.flatlaf.util.SystemInfo; import com.formdev.flatlaf.util.UIScale; @@ -266,14 +267,14 @@ class UIDefaultsLoader case ICON: return parseInstance( value, addonClassLoaders ); case INSETS: return parseInsets( value ); case DIMENSION: return parseDimension( value ); - case COLOR: return parseColorOrFunction( value, true ); + case COLOR: return parseColorOrFunction( value, resolver, true ); case SCALEDINTEGER: return parseScaledInteger( value ); case INSTANCE: return parseInstance( value, addonClassLoaders ); case CLASS: return parseClass( value, addonClassLoaders ); case UNKNOWN: default: // colors - ColorUIResource color = parseColorOrFunction( value, false ); + ColorUIResource color = parseColorOrFunction( value, resolver, false ); if( color != null ) return color; @@ -290,10 +291,10 @@ class UIDefaultsLoader private static Object parseBorder( String value, Function resolver, List addonClassLoaders ) { if( value.indexOf( ',' ) >= 0 ) { // top,left,bottom,right[,lineColor] - List parts = StringUtils.split( value, ',' ); + List parts = split( value, ',' ); Insets insets = parseInsets( value ); ColorUIResource lineColor = (parts.size() == 5) - ? parseColorOrFunction( resolver.apply( parts.get( 4 ) ), true ) + ? parseColorOrFunction( resolver.apply( parts.get( 4 ) ), resolver, true ) : null; return (LazyValue) t -> { @@ -346,7 +347,7 @@ class UIDefaultsLoader } private static Insets parseInsets( String value ) { - List numbers = StringUtils.split( value, ',' ); + List numbers = split( value, ',' ); try { return new InsetsUIResource( Integer.parseInt( numbers.get( 0 ) ), @@ -359,7 +360,7 @@ class UIDefaultsLoader } private static Dimension parseDimension( String value ) { - List numbers = StringUtils.split( value, ',' ); + List numbers = split( value, ',' ); try { return new DimensionUIResource( Integer.parseInt( numbers.get( 0 ) ), @@ -369,9 +370,9 @@ class UIDefaultsLoader } } - private static ColorUIResource parseColorOrFunction( String value, boolean reportError ) { + private static ColorUIResource parseColorOrFunction( String value, Function resolver, boolean reportError ) { if( value.endsWith( ")" ) ) - return parseColorFunctions( value, reportError ); + return parseColorFunctions( value, resolver, reportError ); return parseColor( value, reportError ); } @@ -439,7 +440,7 @@ class UIDefaultsLoader : (((n >> 8) & 0xffffff) | ((n & 0xff) << 24)); // move alpha from lowest to highest byte } - private static ColorUIResource parseColorFunctions( String value, boolean reportError ) { + private static ColorUIResource parseColorFunctions( String value, Function resolver, boolean reportError ) { int paramsStart = value.indexOf( '(' ); if( paramsStart < 0 ) { if( reportError ) @@ -448,37 +449,88 @@ class UIDefaultsLoader } String function = value.substring( 0, paramsStart ).trim(); - List params = StringUtils.split( value.substring( paramsStart + 1, value.length() - 1 ), ',' ); + List params = splitFunctionParams( value.substring( paramsStart + 1, value.length() - 1 ), ',' ); if( params.isEmpty() ) throw new IllegalArgumentException( "missing parameters in function '" + value + "'" ); switch( function ) { - case "lighten": return parseColorLightenOrDarken( true, params, reportError ); - case "darken": return parseColorLightenOrDarken( false, params, reportError ); + case "rgb": return parseColorRgbOrRgba( false, params ); + case "rgba": return parseColorRgbOrRgba( true, params ); + case "hsl": return parseColorHslOrHsla( false, params ); + case "hsla": return parseColorHslOrHsla( true, params ); + case "lighten": return parseColorLightenOrDarken( true, params, resolver, reportError ); + case "darken": return parseColorLightenOrDarken( false, params, resolver, reportError ); } throw new IllegalArgumentException( "unknown color function '" + value + "'" ); } /** - * Syntax: lighten(amount[,options]) or darken(amount[,options]) + * Syntax: rgb(red,green,blue) or rgba(red,green,blue,alpha) + * - red: an integer 0-255 + * - green: an integer 0-255 + * - blue: an integer 0-255 + * - alpha: an integer 0-255 + */ + private static ColorUIResource parseColorRgbOrRgba( boolean hasAlpha, List params ) { + int red = parseInteger( params.get( 0 ), 0, 255 ); + int green = parseInteger( params.get( 1 ), 0, 255 ); + int blue = parseInteger( params.get( 2 ), 0, 255 ); + int alpha = hasAlpha ? parseInteger( params.get( 3 ), 0, 255 ) : 255; + + return hasAlpha + ? new ColorUIResource( new Color( red, green, blue, alpha ) ) + : new ColorUIResource( red, green, blue ); + } + + /** + * Syntax: hsl(hue,saturation,lightness) or hsla(hue,saturation,lightness,alpha) + * - hue: an integer 0-360 representing degrees + * - saturation: a percentage 0-100% + * - lightness: a percentage 0-100% + * - alpha: a percentage 0-100% + */ + private static ColorUIResource parseColorHslOrHsla( boolean hasAlpha, List params ) { + int hue = parseInteger( params.get( 0 ), 0, 360 ); + int saturation = parsePercentage( params.get( 1 ) ); + int lightness = parsePercentage( params.get( 2 ) ); + int alpha = hasAlpha ? parsePercentage( params.get( 3 ) ) : 100; + + float[] hsl = new float[] { hue, saturation, lightness }; + return new ColorUIResource( HSLColor.toRGB( hsl, alpha / 100f ) ); + } + + /** + * Syntax: lighten([color,]amount[,options]) or darken([color,]amount[,options]) + * - color: a color (e.g. #f00) or a color function * - amount: percentage 0-100% * - options: [relative] [autoInverse] */ - private static ColorUIResource parseColorLightenOrDarken( boolean lighten, List params, boolean reportError ) { - int amount = parsePercentage( params.get( 0 ) ); + private static ColorUIResource parseColorLightenOrDarken( boolean lighten, List params, + Function resolver, boolean reportError ) + { + boolean isDerived = params.get( 0 ).endsWith( "%" ); + String colorStr = isDerived ? null : params.get( 0 ); + int nextParam = isDerived ? 0 : 1; + int amount = parsePercentage( params.get( nextParam++ ) ); boolean relative = false; boolean autoInverse = false; - if( params.size() >= 2 ) { - String options = params.get( 1 ); + if( params.size() > nextParam ) { + String options = params.get( nextParam++ ); relative = options.contains( "relative" ); autoInverse = options.contains( "autoInverse" ); } - return new DerivedColor( lighten + ColorFunctions.ColorFunction function = lighten ? new ColorFunctions.Lighten( amount, relative, autoInverse ) - : new ColorFunctions.Darken( amount, relative, autoInverse ) ); + : new ColorFunctions.Darken( amount, relative, autoInverse ); + + if( isDerived ) + return new DerivedColor( function ); + + ColorUIResource color = parseColorOrFunction( resolver.apply( colorStr ), resolver, reportError ); + return new ColorUIResource( ColorFunctions.applyFunctions( color, function ) ); } private static int parsePercentage( String value ) { @@ -497,6 +549,13 @@ class UIDefaultsLoader return val; } + private static Integer parseInteger( String value, int min, int max ) { + Integer integer = parseInteger( value, true ); + if( integer.intValue() < min || integer.intValue() > max ) + throw new NumberFormatException( "integer '" + value + "' out of range (" + min + '-' + max + ')' ); + return integer; + } + private static Integer parseInteger( String value, boolean reportError ) { try { return Integer.parseInt( value ); @@ -513,4 +572,43 @@ class UIDefaultsLoader return UIScale.scale( val ); }; } + + /** + * Split string and trim parts. + */ + private static List split( String str, char delim ) { + List result = StringUtils.split( str, delim ); + + // trim strings + int size = result.size(); + for( int i = 0; i < size; i++ ) + result.set( i, result.get( i ).trim() ); + + return result; + } + + /** + * Splits function parameters and allows using functions as parameters. + * In other words: Delimiters surrounded by '(' and ')' are ignored. + */ + private static List splitFunctionParams( String str, char delim ) { + ArrayList strs = new ArrayList<>(); + int nestLevel = 0; + int start = 0; + int strlen = str.length(); + for( int i = 0; i < strlen; i++ ) { + char ch = str.charAt( i ); + if( ch == '(' ) + nestLevel++; + else if( ch == ')' ) + nestLevel--; + else if( nestLevel == 0 && ch == delim ) { + strs.add( str.substring( start, i ).trim() ); + start = i + 1; + } + } + strs.add( str.substring( start ).trim() ); + + return strs; + } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ColorFunctions.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ColorFunctions.java index c246e9e5..00f60561 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ColorFunctions.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ColorFunctions.java @@ -25,7 +25,7 @@ import java.awt.Color; */ public class ColorFunctions { - public static Color applyFunctions( Color color, ColorFunction[] functions ) { + public static Color applyFunctions( Color color, ColorFunction... functions ) { float[] hsl = HSLColor.fromRGB( color ); float alpha = color.getAlpha() / 255f;