From ec572436a979073ba6e0426adee5a498eddb2e11 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 25 Oct 2019 23:07:44 +0200 Subject: [PATCH] extracted properties file parsing to new class UIDefaultsLoader --- .../java/com/formdev/flatlaf/FlatLaf.java | 312 +--------------- .../com/formdev/flatlaf/UIDefaultsLoader.java | 338 ++++++++++++++++++ 2 files changed, 340 insertions(+), 310 deletions(-) create mode 100644 flatlaf-core/src/main/java/com/formdev/flatlaf/UIDefaultsLoader.java diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java index 19150829..211fd49a 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java @@ -19,10 +19,8 @@ package com.formdev.flatlaf; import java.awt.Color; import java.awt.Component; import java.awt.Container; -import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Font; -import java.awt.Insets; import java.awt.KeyEventPostProcessor; import java.awt.KeyboardFocusManager; import java.awt.Toolkit; @@ -30,16 +28,7 @@ import java.awt.Window; import java.awt.event.KeyEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Properties; -import java.util.ServiceLoader; -import java.util.function.Function; import javax.swing.AbstractButton; import javax.swing.JLabel; import javax.swing.JTabbedPane; @@ -48,16 +37,9 @@ import javax.swing.SwingUtilities; import javax.swing.UIDefaults; import javax.swing.UIManager; import javax.swing.UnsupportedLookAndFeelException; -import javax.swing.UIDefaults.LazyValue; -import javax.swing.plaf.ColorUIResource; -import javax.swing.plaf.DimensionUIResource; import javax.swing.plaf.FontUIResource; -import javax.swing.plaf.InsetsUIResource; import javax.swing.plaf.basic.BasicLookAndFeel; import javax.swing.plaf.metal.MetalLookAndFeel; -import com.formdev.flatlaf.ui.FlatEmptyBorder; -import com.formdev.flatlaf.ui.FlatLineBorder; -import com.formdev.flatlaf.util.ScaledNumber; import com.formdev.flatlaf.util.SystemInfo; import com.formdev.flatlaf.util.UIScale; @@ -69,13 +51,6 @@ import com.formdev.flatlaf.util.UIScale; public abstract class FlatLaf extends BasicLookAndFeel { - private static final String TYPE_PREFIX = "{"; - private static final String TYPE_PREFIX_END = "}"; - private static final String VARIABLE_PREFIX = "@"; - private static final String REF_PREFIX = VARIABLE_PREFIX + "@"; - private static final String OPTIONAL_PREFIX = "?"; - private static final String GLOBAL_PREFIX = "*."; - private BasicLookAndFeel base; private String desktopPropertyName; @@ -209,7 +184,7 @@ public abstract class FlatLaf Object aquaMenuBarUI = useScreenMenuBar ? defaults.get( "MenuBarUI" ) : null; initFonts( defaults ); - loadDefaultsFromProperties( defaults ); + UIDefaultsLoader.loadDefaultsFromProperties( getClass(), defaults ); // use Aqua MenuBarUI if Mac screen menubar is enabled if( useScreenMenuBar ) @@ -254,291 +229,8 @@ public abstract class FlatLaf defaults.put( "MenuItem.acceleratorFont", uiFont ); } - /** - * Load properties associated to Flat LaF classes and add to UI defaults. - * - * Each class that extend this class may have its own .properties file - * in the same package as the class. Properties from superclasses are loaded - * first to give subclasses a chance to override defaults. - * E.g. if running FlatDarkLaf, then the FlatLaf.properties is loaded first - * and FlatDarkLaf.properties loaded second. - */ - private void loadDefaultsFromProperties( UIDefaults defaults ) { - // determine classes in class hierarchy in reverse order - ArrayList> lafClasses = new ArrayList<>(); - for( Class lafClass = getClass(); - FlatLaf.class.isAssignableFrom( lafClass ); - lafClass = lafClass.getSuperclass() ) - { - lafClasses.add( 0, lafClass ); - } - - try { - // load properties files - Properties properties = new Properties(); - ServiceLoader addonLoader = ServiceLoader.load( FlatDefaultsAddon.class ); - for( Class lafClass : lafClasses ) { - // load core properties - String propertiesName = "/" + lafClass.getName().replace( '.', '/' ) + ".properties"; - try( InputStream in = lafClass.getResourceAsStream( propertiesName ) ) { - if( in != null ) - properties.load( in ); - } - - // load properties from addons - for( FlatDefaultsAddon addon : addonLoader ) { - try( InputStream in = addon.getDefaults( lafClass ) ) { - if( in != null ) - properties.load( in ); - } - } - } - - Function resolver = value -> { - return resolveValue( properties, value ); - }; - - // get globals, which override all other defaults that end with same suffix - HashMap globals = new HashMap<>(); - for( Map.Entry e : properties.entrySet() ) { - String key = (String) e.getKey(); - if( !key.startsWith( GLOBAL_PREFIX ) ) - continue; - - String value = resolveValue( properties, (String) e.getValue() ); - globals.put( key.substring( GLOBAL_PREFIX.length() ), parseValue( key, value, resolver ) ); - } - - // override UI defaults with globals - for( Object key : defaults.keySet() ) { - if( key instanceof String && ((String)key).contains( "." ) ) { - String skey = (String) key; - String globalKey = skey.substring( skey.lastIndexOf( '.' ) + 1 ); - Object globalValue = globals.get( globalKey ); - if( globalValue != null ) - defaults.put( key, globalValue ); - } - } - - // add non-global properties to UI defaults - for( Map.Entry e : properties.entrySet() ) { - String key = (String) e.getKey(); - if( key.startsWith( VARIABLE_PREFIX ) || key.startsWith( GLOBAL_PREFIX ) ) - continue; - - String value = resolveValue( properties, (String) e.getValue() ); - defaults.put( key, parseValue( key, value, resolver ) ); - } - } catch( IOException ex ) { - ex.printStackTrace(); - } - } - - private String resolveValue( Properties properties, String value ) { - if( !value.startsWith( VARIABLE_PREFIX ) ) - return value; - - if( value.startsWith( REF_PREFIX ) ) - value = value.substring( REF_PREFIX.length() ); - - boolean optional = false; - if( value.startsWith( OPTIONAL_PREFIX ) ) { - value = value.substring( OPTIONAL_PREFIX.length() ); - optional = true; - } - - String newValue = properties.getProperty( value ); - if( newValue == null ) { - if( optional ) - return "null"; - - System.err.println( "variable or reference '" + value + "' not found" ); - throw new IllegalArgumentException( value ); - } - - return resolveValue( properties, newValue ); - } - - private enum ValueType { UNKNOWN, STRING, INTEGER, BORDER, ICON, INSETS, SIZE, COLOR, SCALEDNUMBER } - - private Object parseValue( String key, String value, Function resolver ) { - value = value.trim(); - - // null, false, true - switch( value ) { - case "null": return null; - case "false": return false; - case "true": return true; - } - - ValueType valueType = ValueType.UNKNOWN; - - // check whether value type is specified in the value - if( value.startsWith( TYPE_PREFIX ) ) { - int end = value.indexOf( TYPE_PREFIX_END ); - if( end != -1 ) { - try { - String typeStr = value.substring( TYPE_PREFIX.length(), end ); - valueType = ValueType.valueOf( typeStr.toUpperCase( Locale.ENGLISH ) ); - - // remove type from value - value = value.substring( end + TYPE_PREFIX_END.length() ); - } catch( IllegalArgumentException ex ) { - // ignore - } - } - } - - // determine value type from key - if( valueType == ValueType.UNKNOWN ) { - if( key.endsWith( ".border" ) || key.endsWith( "Border" ) ) - valueType = ValueType.BORDER; - else if( key.endsWith( ".icon" ) || key.endsWith( "Icon" ) ) - valueType = ValueType.ICON; - else if( key.endsWith( ".margin" ) || key.endsWith( ".padding" ) || - key.endsWith( "Margins" ) || key.endsWith( "Insets" ) ) - valueType = ValueType.INSETS; - else if( key.endsWith( "Size" ) ) - valueType = ValueType.SIZE; - else if( key.endsWith( "Width" ) || key.endsWith( "Height" ) ) - valueType = ValueType.INTEGER; - } - - // parse value - switch( valueType ) { - case STRING: return value; - case INTEGER: return parseInteger( value, true ); - case BORDER: return parseBorder( value, resolver ); - case ICON: return parseInstance( value ); - case INSETS: return parseInsets( value ); - case SIZE: return parseSize( value ); - case COLOR: return parseColor( value, true ); - case SCALEDNUMBER: return parseScaledNumber( value ); - case UNKNOWN: - default: - // colors - ColorUIResource color = parseColor( value, false ); - if( color != null ) - return color; - - // integer - Integer integer = parseInteger( value, false ); - if( integer != null ) - return integer; - - // string - return value; - } - } - - private Object parseBorder( String value, Function resolver ) { - if( value.indexOf( ',' ) >= 0 ) { - // top,left,bottom,right[,lineColor] - List parts = split( value, ',' ); - Insets insets = parseInsets( value ); - ColorUIResource lineColor = (parts.size() == 5) - ? parseColor( resolver.apply( parts.get( 4 ) ), true ) - : null; - - return (LazyValue) t -> { - return (lineColor != null) - ? new FlatLineBorder( insets, lineColor ) - : new FlatEmptyBorder( insets ); - }; - } else - return parseInstance( value ); - } - - private Object parseInstance( String value ) { - return (LazyValue) t -> { - try { - return Class.forName( value ).newInstance(); - } catch( InstantiationException | IllegalAccessException | ClassNotFoundException ex ) { - ex.printStackTrace(); - return null; - } - }; - } - - private Insets parseInsets( String value ) { - List numbers = split( value, ',' ); - try { - return new InsetsUIResource( - Integer.parseInt( numbers.get( 0 ) ), - Integer.parseInt( numbers.get( 1 ) ), - Integer.parseInt( numbers.get( 2 ) ), - Integer.parseInt( numbers.get( 3 ) ) ); - } catch( NumberFormatException ex ) { - System.err.println( "invalid insets '" + value + "'" ); - throw ex; - } - } - - private Dimension parseSize( String value ) { - List numbers = split( value, ',' ); - try { - return new DimensionUIResource( - Integer.parseInt( numbers.get( 0 ) ), - Integer.parseInt( numbers.get( 1 ) ) ); - } catch( NumberFormatException ex ) { - System.err.println( "invalid size '" + value + "'" ); - throw ex; - } - } - - private ColorUIResource parseColor( String value, boolean reportError ) { - try { - int rgb = Integer.parseInt( value, 16 ); - if( value.length() == 6 ) - return new ColorUIResource( rgb ); - if( value.length() == 8 ) - return new ColorUIResource( new Color( rgb, true ) ); - - if( reportError ) - throw new NumberFormatException( value ); - } catch( NumberFormatException ex ) { - if( reportError ) { - System.err.println( "invalid color '" + value + "'" ); - throw ex; - } - // not a color --> ignore - } - return null; - } - - private Integer parseInteger( String value, boolean reportError ) { - try { - return Integer.parseInt( value ); - } catch( NumberFormatException ex ) { - if( reportError ) { - System.err.println( "invalid integer '" + value + "'" ); - throw ex; - } - } - return null; - } - - private ScaledNumber parseScaledNumber( String value ) { - try { - return new ScaledNumber( Integer.parseInt( value ) ); - } catch( NumberFormatException ex ) { - System.err.println( "invalid integer '" + value + "'" ); - throw ex; - } - } - public static List split( String str, char delim ) { - ArrayList strs = new ArrayList<>(); - int delimIndex = str.indexOf( delim ); - int index = 0; - while( delimIndex >= 0 ) { - strs.add( str.substring( index, delimIndex ) ); - index = delimIndex + 1; - delimIndex = str.indexOf( delim, index ); - } - strs.add( str.substring( index ) ); - - return strs; + return UIDefaultsLoader.split( str, delim ); } private static void reSetLookAndFeel() { diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/UIDefaultsLoader.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/UIDefaultsLoader.java new file mode 100644 index 00000000..853fa88b --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/UIDefaultsLoader.java @@ -0,0 +1,338 @@ +/* + * Copyright 2019 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 + * + * http://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; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Insets; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.ServiceLoader; +import java.util.function.Function; +import javax.swing.UIDefaults; +import javax.swing.UIDefaults.LazyValue; +import javax.swing.plaf.ColorUIResource; +import javax.swing.plaf.DimensionUIResource; +import javax.swing.plaf.InsetsUIResource; +import com.formdev.flatlaf.ui.FlatEmptyBorder; +import com.formdev.flatlaf.ui.FlatLineBorder; +import com.formdev.flatlaf.util.ScaledNumber; + +/** + * Load UI defaults from properties files associated to Flat LaF classes and add to UI defaults. + * + * Each class that extend the LaF class may have its own .properties file + * in the same package as the class. Properties from superclasses are loaded + * first to give subclasses a chance to override defaults. + * E.g. if running FlatDarkLaf, then the FlatLaf.properties is loaded first + * and FlatDarkLaf.properties loaded second. + * + * @author Karl Tauber + */ +class UIDefaultsLoader +{ + private static final String TYPE_PREFIX = "{"; + private static final String TYPE_PREFIX_END = "}"; + private static final String VARIABLE_PREFIX = "@"; + private static final String REF_PREFIX = VARIABLE_PREFIX + "@"; + private static final String OPTIONAL_PREFIX = "?"; + private static final String GLOBAL_PREFIX = "*."; + + static void loadDefaultsFromProperties( Class lookAndFeelClass, UIDefaults defaults ) { + // determine classes in class hierarchy in reverse order + ArrayList> lafClasses = new ArrayList<>(); + for( Class lafClass = lookAndFeelClass; + FlatLaf.class.isAssignableFrom( lafClass ); + lafClass = lafClass.getSuperclass() ) + { + lafClasses.add( 0, lafClass ); + } + + try { + // load properties files + Properties properties = new Properties(); + ServiceLoader addonLoader = ServiceLoader.load( FlatDefaultsAddon.class ); + for( Class lafClass : lafClasses ) { + // load core properties + String propertiesName = "/" + lafClass.getName().replace( '.', '/' ) + ".properties"; + try( InputStream in = lafClass.getResourceAsStream( propertiesName ) ) { + if( in != null ) + properties.load( in ); + } + + // load properties from addons + for( FlatDefaultsAddon addon : addonLoader ) { + try( InputStream in = addon.getDefaults( lafClass ) ) { + if( in != null ) + properties.load( in ); + } + } + } + + Function resolver = value -> { + return resolveValue( properties, value ); + }; + + // get globals, which override all other defaults that end with same suffix + HashMap globals = new HashMap<>(); + for( Map.Entry e : properties.entrySet() ) { + String key = (String) e.getKey(); + if( !key.startsWith( GLOBAL_PREFIX ) ) + continue; + + String value = resolveValue( properties, (String) e.getValue() ); + globals.put( key.substring( GLOBAL_PREFIX.length() ), parseValue( key, value, resolver ) ); + } + + // override UI defaults with globals + for( Object key : defaults.keySet() ) { + if( key instanceof String && ((String)key).contains( "." ) ) { + String skey = (String) key; + String globalKey = skey.substring( skey.lastIndexOf( '.' ) + 1 ); + Object globalValue = globals.get( globalKey ); + if( globalValue != null ) + defaults.put( key, globalValue ); + } + } + + // add non-global properties to UI defaults + for( Map.Entry e : properties.entrySet() ) { + String key = (String) e.getKey(); + if( key.startsWith( VARIABLE_PREFIX ) || key.startsWith( GLOBAL_PREFIX ) ) + continue; + + String value = resolveValue( properties, (String) e.getValue() ); + defaults.put( key, parseValue( key, value, resolver ) ); + } + } catch( IOException ex ) { + ex.printStackTrace(); + } + } + + private static String resolveValue( Properties properties, String value ) { + if( !value.startsWith( VARIABLE_PREFIX ) ) + return value; + + if( value.startsWith( REF_PREFIX ) ) + value = value.substring( REF_PREFIX.length() ); + + boolean optional = false; + if( value.startsWith( OPTIONAL_PREFIX ) ) { + value = value.substring( OPTIONAL_PREFIX.length() ); + optional = true; + } + + String newValue = properties.getProperty( value ); + if( newValue == null ) { + if( optional ) + return "null"; + + System.err.println( "variable or reference '" + value + "' not found" ); + throw new IllegalArgumentException( value ); + } + + return resolveValue( properties, newValue ); + } + + private enum ValueType { UNKNOWN, STRING, INTEGER, BORDER, ICON, INSETS, SIZE, COLOR, SCALEDNUMBER } + + private static Object parseValue( String key, String value, Function resolver ) { + value = value.trim(); + + // null, false, true + switch( value ) { + case "null": return null; + case "false": return false; + case "true": return true; + } + + ValueType valueType = ValueType.UNKNOWN; + + // check whether value type is specified in the value + if( value.startsWith( TYPE_PREFIX ) ) { + int end = value.indexOf( TYPE_PREFIX_END ); + if( end != -1 ) { + try { + String typeStr = value.substring( TYPE_PREFIX.length(), end ); + valueType = ValueType.valueOf( typeStr.toUpperCase( Locale.ENGLISH ) ); + + // remove type from value + value = value.substring( end + TYPE_PREFIX_END.length() ); + } catch( IllegalArgumentException ex ) { + // ignore + } + } + } + + // determine value type from key + if( valueType == ValueType.UNKNOWN ) { + if( key.endsWith( ".border" ) || key.endsWith( "Border" ) ) + valueType = ValueType.BORDER; + else if( key.endsWith( ".icon" ) || key.endsWith( "Icon" ) ) + valueType = ValueType.ICON; + else if( key.endsWith( ".margin" ) || key.endsWith( ".padding" ) || + key.endsWith( "Margins" ) || key.endsWith( "Insets" ) ) + valueType = ValueType.INSETS; + else if( key.endsWith( "Size" ) ) + valueType = ValueType.SIZE; + else if( key.endsWith( "Width" ) || key.endsWith( "Height" ) ) + valueType = ValueType.INTEGER; + } + + // parse value + switch( valueType ) { + case STRING: return value; + case INTEGER: return parseInteger( value, true ); + case BORDER: return parseBorder( value, resolver ); + case ICON: return parseInstance( value ); + case INSETS: return parseInsets( value ); + case SIZE: return parseSize( value ); + case COLOR: return parseColor( value, true ); + case SCALEDNUMBER: return parseScaledNumber( value ); + case UNKNOWN: + default: + // colors + ColorUIResource color = parseColor( value, false ); + if( color != null ) + return color; + + // integer + Integer integer = parseInteger( value, false ); + if( integer != null ) + return integer; + + // string + return value; + } + } + + private static Object parseBorder( String value, Function resolver ) { + if( value.indexOf( ',' ) >= 0 ) { + // top,left,bottom,right[,lineColor] + List parts = split( value, ',' ); + Insets insets = parseInsets( value ); + ColorUIResource lineColor = (parts.size() == 5) + ? parseColor( resolver.apply( parts.get( 4 ) ), true ) + : null; + + return (LazyValue) t -> { + return (lineColor != null) + ? new FlatLineBorder( insets, lineColor ) + : new FlatEmptyBorder( insets ); + }; + } else + return parseInstance( value ); + } + + private static Object parseInstance( String value ) { + return (LazyValue) t -> { + try { + return Class.forName( value ).newInstance(); + } catch( InstantiationException | IllegalAccessException | ClassNotFoundException ex ) { + ex.printStackTrace(); + return null; + } + }; + } + + private static Insets parseInsets( String value ) { + List numbers = split( value, ',' ); + try { + return new InsetsUIResource( + Integer.parseInt( numbers.get( 0 ) ), + Integer.parseInt( numbers.get( 1 ) ), + Integer.parseInt( numbers.get( 2 ) ), + Integer.parseInt( numbers.get( 3 ) ) ); + } catch( NumberFormatException ex ) { + System.err.println( "invalid insets '" + value + "'" ); + throw ex; + } + } + + private static Dimension parseSize( String value ) { + List numbers = split( value, ',' ); + try { + return new DimensionUIResource( + Integer.parseInt( numbers.get( 0 ) ), + Integer.parseInt( numbers.get( 1 ) ) ); + } catch( NumberFormatException ex ) { + System.err.println( "invalid size '" + value + "'" ); + throw ex; + } + } + + private static ColorUIResource parseColor( String value, boolean reportError ) { + try { + int rgb = Integer.parseInt( value, 16 ); + if( value.length() == 6 ) + return new ColorUIResource( rgb ); + if( value.length() == 8 ) + return new ColorUIResource( new Color( rgb, true ) ); + + if( reportError ) + throw new NumberFormatException( value ); + } catch( NumberFormatException ex ) { + if( reportError ) { + System.err.println( "invalid color '" + value + "'" ); + throw ex; + } + // not a color --> ignore + } + return null; + } + + private static Integer parseInteger( String value, boolean reportError ) { + try { + return Integer.parseInt( value ); + } catch( NumberFormatException ex ) { + if( reportError ) { + System.err.println( "invalid integer '" + value + "'" ); + throw ex; + } + } + return null; + } + + private static ScaledNumber parseScaledNumber( String value ) { + try { + return new ScaledNumber( Integer.parseInt( value ) ); + } catch( NumberFormatException ex ) { + System.err.println( "invalid integer '" + value + "'" ); + throw ex; + } + } + + static List split( String str, char delim ) { + ArrayList strs = new ArrayList<>(); + int delimIndex = str.indexOf( delim ); + int index = 0; + while( delimIndex >= 0 ) { + strs.add( str.substring( index, delimIndex ) ); + index = delimIndex + 1; + delimIndex = str.indexOf( delim, index ); + } + strs.add( str.substring( index ) ); + + return strs; + } +}