diff --git a/CHANGELOG.md b/CHANGELOG.md index 48ccf45c..e15d2137 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ FlatLaf Change Log - Added more color functions to class `ColorFunctions` for easy use in applications: `lighten()`, `darken()`, `saturate()`, `desaturate()`, `spin()`, `tint()`, `shade()` and `luma()`. +- Support defining fonts in FlatLaf properties files. (issue #384) #### Fixed bugs 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 d816d7be..cf36cf5e 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java @@ -569,7 +569,7 @@ public abstract class FlatLaf // use active value for all fonts to allow changing fonts in all components // (similar as in Nimbus L&F) with: // UIManager.put( "defaultFont", myFont ); - Object activeFont = new ActiveFont( 1 ); + Object activeFont = new ActiveFont( null, -1, 0, 0, 0, 0 ); // override fonts for( Object key : defaults.keySet() ) { @@ -577,9 +577,6 @@ public abstract class FlatLaf defaults.put( key, activeFont ); } - // use smaller font for progress bar - defaults.put( "ProgressBar.font", new ActiveFont( 0.85f ) ); - // set default font defaults.put( "defaultFont", uiFont ); } @@ -594,7 +591,7 @@ public abstract class FlatLaf /** @since 1.1 */ public static ActiveValue createActiveFontValue( float scaleFactor ) { - return new ActiveFont( scaleFactor ); + return new ActiveFont( null, -1, 0, 0, 0, scaleFactor ); } /** @@ -1162,17 +1159,38 @@ public abstract class FlatLaf //---- class ActiveFont --------------------------------------------------- - private static class ActiveFont + static class ActiveFont implements ActiveValue { - private final float scaleFactor; + private final List families; + private final int style; + private final int styleChange; + private final int absoluteSize; + private final int relativeSize; + private final float scaleSize; - // cache (scaled) font + // cache (scaled/derived) font private Font font; private Font lastDefaultFont; - ActiveFont( float scaleFactor ) { - this.scaleFactor = scaleFactor; + /** + * @param families list of font families, or {@code null} + * @param style new style of font, or {@code -1} + * @param styleChange derive style of base font; or {@code 0} + * (the lower 16 bits are added; the upper 16 bits are removed) + * @param absoluteSize new size of font, or {@code 0} + * @param relativeSize added to size of base font, or {@code 0} + * @param scaleSize multiply size of base font, or {@code 0} + */ + ActiveFont( List families, int style, int styleChange, + int absoluteSize, int relativeSize, float scaleSize ) + { + this.families = families; + this.style = style; + this.styleChange = styleChange; + this.absoluteSize = absoluteSize; + this.relativeSize = relativeSize; + this.scaleSize = scaleSize; } @Override @@ -1186,20 +1204,57 @@ public abstract class FlatLaf if( lastDefaultFont != defaultFont ) { lastDefaultFont = defaultFont; - if( scaleFactor != 1 ) { - // scale font - int newFontSize = Math.round( defaultFont.getSize() * scaleFactor ); - font = new FontUIResource( defaultFont.deriveFont( (float) newFontSize ) ); - } else { - // make sure that font is a UIResource for LaF switching - font = (defaultFont instanceof UIResource) - ? defaultFont - : new FontUIResource( defaultFont ); - } + font = derive( defaultFont ); + + // make sure that font is a UIResource for LaF switching + if( !(font instanceof UIResource) ) + font = new FontUIResource( font ); } return font; } + + private Font derive( Font baseFont ) { + int baseStyle = baseFont.getStyle(); + int baseSize = baseFont.getSize(); + + // new style + int newStyle = (style != -1) + ? style + : (styleChange != 0) + ? baseStyle & ~((styleChange >> 16) & 0xffff) | (styleChange & 0xffff) + : baseStyle; + + // new size + int newSize = (absoluteSize > 0) + ? UIScale.scale( absoluteSize ) + : (relativeSize != 0) + ? (baseSize + UIScale.scale( relativeSize )) + : (scaleSize > 0) + ? Math.round( baseSize * scaleSize ) + : baseSize; + if( newSize <= 0 ) + newSize = 1; + + // create font for family + if( families != null && !families.isEmpty() ) { + for( String family : families ) { + Font font = createCompositeFont( family, newStyle, newSize ); + if( !isFallbackFont( font ) || family.equalsIgnoreCase( Font.DIALOG ) ) + return font; + } + } + + // derive font + if( newStyle != baseStyle || newSize != baseSize ) + return baseFont.deriveFont( newStyle, newSize ); + else + return baseFont; + } + + private boolean isFallbackFont( Font font ) { + return Font.DIALOG.equalsIgnoreCase( font.getFamily() ); + } } //---- class ImageIconUIResource ------------------------------------------ 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 47de5c37..aaa9e618 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/UIDefaultsLoader.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/UIDefaultsLoader.java @@ -18,11 +18,14 @@ package com.formdev.flatlaf; import java.awt.Color; import java.awt.Dimension; +import java.awt.Font; import java.awt.Insets; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.StreamTokenizer; +import java.io.StringReader; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -309,7 +312,7 @@ class UIDefaultsLoader throw new IllegalArgumentException( "property value type '" + newValue.getClass().getName() + "' not supported in references" ); } - enum ValueType { UNKNOWN, STRING, BOOLEAN, CHARACTER, INTEGER, FLOAT, BORDER, ICON, INSETS, DIMENSION, COLOR, + enum ValueType { UNKNOWN, STRING, BOOLEAN, CHARACTER, INTEGER, FLOAT, BORDER, ICON, INSETS, DIMENSION, COLOR, FONT, SCALEDINTEGER, SCALEDFLOAT, SCALEDINSETS, SCALEDDIMENSION, INSTANCE, CLASS, GRAYFILTER, NULL, LAZY } private static ValueType[] tempResultValueType = new ValueType[1]; @@ -371,6 +374,7 @@ class UIDefaultsLoader javaValueTypes.put( Insets.class, ValueType.INSETS ); javaValueTypes.put( Dimension.class, ValueType.DIMENSION ); javaValueTypes.put( Color.class, ValueType.COLOR ); + javaValueTypes.put( Font.class, ValueType.FONT ); } // map java value type to parser value type @@ -428,6 +432,8 @@ class UIDefaultsLoader (key.endsWith( ".background" ) || key.endsWith( "Background" ) || key.equals( "background" ) || key.endsWith( ".foreground" ) || key.endsWith( "Foreground" ) || key.equals( "foreground" ))) ) valueType = ValueType.COLOR; + else if( key.endsWith( ".font" ) || key.endsWith( "Font" ) || key.equals( "font" ) ) + valueType = ValueType.FONT; else if( key.endsWith( ".border" ) || key.endsWith( "Border" ) || key.equals( "border" ) ) valueType = ValueType.BORDER; else if( key.endsWith( ".icon" ) || key.endsWith( "Icon" ) || key.equals( "icon" ) ) @@ -461,6 +467,7 @@ class UIDefaultsLoader case INSETS: return parseInsets( value ); case DIMENSION: return parseDimension( value ); case COLOR: return parseColorOrFunction( value, resolver, true ); + case FONT: return parseFont( value ); case SCALEDINTEGER: return parseScaledInteger( value ); case SCALEDFLOAT: return parseScaledFloat( value ); case SCALEDINSETS: return parseScaledInsets( value ); @@ -984,6 +991,94 @@ class UIDefaultsLoader return new ColorUIResource( newColor ); } + /** + * Syntax: [normal] [bold|+bold|-bold] [italic|+italic|-italic] [|+|-|%] [family[, family]] + */ + private static Object parseFont( String value ) { + int style = -1; + int styleChange = 0; + int absoluteSize = 0; + int relativeSize = 0; + float scaleSize = 0; + List families = null; + + // use StreamTokenizer to split string because it supports quoted strings + StreamTokenizer st = new StreamTokenizer( new StringReader( value ) ); + st.resetSyntax(); + st.wordChars( ' ' + 1, 255 ); + st.whitespaceChars( 0, ' ' ); + st.whitespaceChars( ',', ',' ); // ignore ',' + st.quoteChar( '"' ); + st.quoteChar( '\'' ); + + try { + while( st.nextToken() != StreamTokenizer.TT_EOF ) { + String param = st.sval; + switch( param ) { + // font style + case "normal": + style = 0; + break; + + case "bold": + if( style == -1 ) + style = 0; + style |= Font.BOLD; + break; + + case "italic": + if( style == -1 ) + style = 0; + style |= Font.ITALIC; + break; + + case "+bold": styleChange |= Font.BOLD; break; + case "-bold": styleChange |= Font.BOLD << 16; break; + case "+italic": styleChange |= Font.ITALIC; break; + case "-italic": styleChange |= Font.ITALIC << 16; break; + + default: + char firstChar = param.charAt( 0 ); + if( Character.isDigit( firstChar ) || firstChar == '+' || firstChar == '-' ) { + // font size + if( absoluteSize != 0 || relativeSize != 0 || scaleSize != 0 ) + throw new IllegalArgumentException( "size specified more than once in '" + value + "'" ); + + if( firstChar == '+' || firstChar == '-' ) + relativeSize = parseInteger( param, true ); + else if( param.endsWith( "%" ) ) + scaleSize = parseInteger( param.substring( 0, param.length() - 1 ), true ) / 100f; + else + absoluteSize = parseInteger( param, true ); + } else { + // font family + if( families == null ) + families = Collections.singletonList( param ); + else { + if( families.size() == 1 ) + families = new ArrayList<>( families ); + families.add( param ); + } + } + break; + } + } + } catch( IOException ex ) { + throw new IllegalArgumentException( ex ); + } + + if( style != -1 && styleChange != 0 ) + throw new IllegalArgumentException( "can not mix absolute style (e.g. 'bold') with derived style (e.g. '+italic') in '" + value + "'" ); + if( styleChange != 0 ) { + if( (styleChange & Font.BOLD) != 0 && (styleChange & (Font.BOLD << 16)) != 0 ) + throw new IllegalArgumentException( "can not use '+bold' and '-bold' in '" + value + "'" ); + if( (styleChange & Font.ITALIC) != 0 && (styleChange & (Font.ITALIC << 16)) != 0 ) + throw new IllegalArgumentException( "can not use '+italic' and '-italic' in '" + value + "'" ); + } + + return new FlatLaf.ActiveFont( families, style, styleChange, absoluteSize, relativeSize, scaleSize ); + } + private static int parsePercentage( String value ) { if( !value.endsWith( "%" ) ) throw new NumberFormatException( "invalid percentage '" + value + "'" ); diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties index 7ebbb61a..ebbb9296 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties @@ -449,6 +449,7 @@ ProgressBar.horizontalSize = 146,4 ProgressBar.verticalSize = 4,146 ProgressBar.cycleTime = 4000 ProgressBar.repaintInterval = 15 +ProgressBar.font = -2 #---- RadioButton ---- diff --git a/flatlaf-core/src/test/java/com/formdev/flatlaf/TestUIDefaultsLoader.java b/flatlaf-core/src/test/java/com/formdev/flatlaf/TestUIDefaultsLoader.java index b940debe..eb9de8e7 100644 --- a/flatlaf-core/src/test/java/com/formdev/flatlaf/TestUIDefaultsLoader.java +++ b/flatlaf-core/src/test/java/com/formdev/flatlaf/TestUIDefaultsLoader.java @@ -19,7 +19,12 @@ package com.formdev.flatlaf; import static org.junit.jupiter.api.Assertions.assertEquals; import java.awt.Color; import java.awt.Dimension; +import java.awt.Font; import java.awt.Insets; +import javax.swing.UIManager; +import javax.swing.UIDefaults.ActiveValue; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; /** @@ -27,6 +32,16 @@ import org.junit.jupiter.api.Test; */ public class TestUIDefaultsLoader { + @BeforeAll + static void setup() { + System.setProperty( FlatSystemProperties.UI_SCALE_ENABLED, "false" ); + } + + @AfterAll + static void cleanup() { + System.clearProperty( FlatSystemProperties.UI_SCALE_ENABLED ); + } + @Test void parseValue() { assertEquals( null, UIDefaultsLoader.parseValue( "dummy", "null", null ) ); @@ -71,4 +86,56 @@ public class TestUIDefaultsLoader assertEquals( new Dimension( 2,2 ), UIDefaultsLoader.parseValue( "dummy", "2,2", Dimension.class ) ); assertEquals( new Color( 0xff0000 ), UIDefaultsLoader.parseValue( "dummy", "#f00", Color.class ) ); } + + @Test + void parseFonts() { + // style + UIManager.put( "defaultFont", new Font( Font.DIALOG, Font.PLAIN, 10 ) ); + assertFontEquals( Font.DIALOG, Font.PLAIN, 10, "" ); + assertFontEquals( Font.DIALOG, Font.PLAIN, 10, "normal" ); + assertFontEquals( Font.DIALOG, Font.BOLD, 10, "bold" ); + assertFontEquals( Font.DIALOG, Font.ITALIC, 10, "italic" ); + assertFontEquals( Font.DIALOG, Font.BOLD|Font.ITALIC, 10, "bold italic" ); + + // derived style + assertFontEquals( Font.DIALOG, Font.BOLD, 10, "+bold" ); + assertFontEquals( Font.DIALOG, Font.ITALIC, 10, "+italic" ); + assertFontEquals( Font.DIALOG, Font.BOLD|Font.ITALIC, 10, "+bold +italic" ); + UIManager.put( "defaultFont", new Font( Font.DIALOG, Font.BOLD|Font.ITALIC, 10 ) ); + assertFontEquals( Font.DIALOG, Font.ITALIC, 10, "-bold" ); + assertFontEquals( Font.DIALOG, Font.BOLD, 10, "-italic" ); + assertFontEquals( Font.DIALOG, Font.PLAIN, 10, "-bold -italic" ); + UIManager.put( "defaultFont", new Font( Font.DIALOG, Font.BOLD, 10 ) ); + assertFontEquals( Font.DIALOG, Font.ITALIC, 10, "-bold +italic" ); + + // size + UIManager.put( "defaultFont", new Font( Font.DIALOG, Font.PLAIN, 10 ) ); + assertFontEquals( Font.DIALOG, Font.PLAIN, 12, "12" ); + assertFontEquals( Font.DIALOG, Font.PLAIN, 13, "+3" ); + assertFontEquals( Font.DIALOG, Font.PLAIN, 6, "-4" ); + assertFontEquals( Font.DIALOG, Font.PLAIN, 15, "150%" ); + + // family + assertFontEquals( Font.MONOSPACED, Font.PLAIN, 10, "Monospaced" ); + assertFontEquals( Font.MONOSPACED, Font.PLAIN, 10, "Monospaced, Dialog" ); + assertFontEquals( Font.DIALOG, Font.PLAIN, 10, "Dialog, Monospaced" ); + + // unknown family + assertFontEquals( Font.MONOSPACED, Font.PLAIN, 12, "normal 12 UnknownFamily, Monospaced" ); + assertFontEquals( Font.DIALOG, Font.PLAIN, 12, "normal 12 UnknownFamily, Dialog" ); + assertFontEquals( Font.DIALOG, Font.PLAIN, 12, "normal 12 UnknownFamily, 'Another unknown family'" ); + + // all + assertFontEquals( Font.MONOSPACED, Font.BOLD, 13, "bold 13 Monospaced" ); + assertFontEquals( Font.DIALOG, Font.ITALIC, 14, "italic 14 Dialog" ); + assertFontEquals( Font.DIALOG, Font.BOLD|Font.ITALIC, 15, "bold italic 15 Dialog" ); + + UIManager.put( "defaultFont", null ); + } + + private void assertFontEquals( String name, int style, int size, String actualStyle ) { + assertEquals( + new Font( name, style, size ), + ((ActiveValue)UIDefaultsLoader.parseValue( "dummyFont", actualStyle, null )).createValue( null ) ); + } } diff --git a/flatlaf-core/src/test/java/com/formdev/flatlaf/ui/TestFlatStyling.java b/flatlaf-core/src/test/java/com/formdev/flatlaf/ui/TestFlatStyling.java index 7af01268..9902ed1b 100644 --- a/flatlaf-core/src/test/java/com/formdev/flatlaf/ui/TestFlatStyling.java +++ b/flatlaf-core/src/test/java/com/formdev/flatlaf/ui/TestFlatStyling.java @@ -243,6 +243,7 @@ public class TestFlatStyling ui.applyStyle( b, "background: #fff" ); ui.applyStyle( b, "foreground: #fff" ); ui.applyStyle( b, "border: 2,2,2,2,#f00" ); + ui.applyStyle( b, "font: italic 12 monospaced" ); // AbstractButton properties ui.applyStyle( b, "margin: 2,2,2,2" ); @@ -295,6 +296,7 @@ public class TestFlatStyling ui.applyStyle( "background: #fff" ); ui.applyStyle( "foreground: #fff" ); ui.applyStyle( "border: 2,2,2,2,#f00" ); + ui.applyStyle( "font: italic 12 monospaced" ); } @Test @@ -311,6 +313,7 @@ public class TestFlatStyling ui.applyStyle( "background: #fff" ); ui.applyStyle( "foreground: #fff" ); ui.applyStyle( "border: 2,2,2,2,#f00" ); + ui.applyStyle( "font: italic 12 monospaced" ); // JTextComponent properties ui.applyStyle( "caretColor: #fff" ); @@ -363,6 +366,7 @@ public class TestFlatStyling ui.applyStyle( c, "background: #fff" ); ui.applyStyle( c, "foreground: #fff" ); ui.applyStyle( c, "border: 2,2,2,2,#f00" ); + ui.applyStyle( c, "font: italic 12 monospaced" ); // JLabel properties ui.applyStyle( c, "icon: com.formdev.flatlaf.icons.FlatTreeExpandedIcon" ); @@ -387,6 +391,7 @@ public class TestFlatStyling ui.applyStyle( "background: #fff" ); ui.applyStyle( "foreground: #fff" ); ui.applyStyle( "border: 2,2,2,2,#f00" ); + ui.applyStyle( "font: italic 12 monospaced" ); // JList properties ui.applyStyle( "visibleRowCount: 20" ); @@ -410,6 +415,7 @@ public class TestFlatStyling ui.applyStyle( "background: #fff" ); ui.applyStyle( "foreground: #fff" ); ui.applyStyle( "border: 2,2,2,2,#f00" ); + ui.applyStyle( "font: italic 12 monospaced" ); } @Test @@ -467,6 +473,7 @@ public class TestFlatStyling applyStyle.accept( "background: #fff" ); applyStyle.accept( "foreground: #fff" ); applyStyle.accept( "border: 2,2,2,2,#f00" ); + applyStyle.accept( "font: italic 12 monospaced" ); // AbstractButton properties applyStyle.accept( "margin: 2,2,2,2" ); @@ -557,6 +564,7 @@ public class TestFlatStyling ui.applyStyle( "background: #fff" ); ui.applyStyle( "foreground: #fff" ); ui.applyStyle( "border: 2,2,2,2,#f00" ); + ui.applyStyle( "font: italic 12 monospaced" ); } @Test @@ -578,6 +586,7 @@ public class TestFlatStyling ui.applyStyle( b, "background: #fff" ); ui.applyStyle( b, "foreground: #fff" ); ui.applyStyle( b, "border: 2,2,2,2,#f00" ); + ui.applyStyle( b, "font: italic 12 monospaced" ); // AbstractButton properties ui.applyStyle( b, "margin: 2,2,2,2" ); @@ -714,6 +723,7 @@ public class TestFlatStyling ui.applyStyle( "background: #fff" ); ui.applyStyle( "foreground: #fff" ); ui.applyStyle( "border: 2,2,2,2,#f00" ); + ui.applyStyle( "font: italic 12 monospaced" ); ui.applyStyle( "minimum: 0" ); ui.applyStyle( "maximum: 50" ); ui.applyStyle( "value: 20" ); @@ -760,6 +770,7 @@ public class TestFlatStyling ui.applyStyle( "background: #fff" ); ui.applyStyle( "foreground: #fff" ); ui.applyStyle( "border: 2,2,2,2,#f00" ); + ui.applyStyle( "font: italic 12 monospaced" ); } @Test @@ -856,6 +867,7 @@ public class TestFlatStyling ui.applyStyle( "background: #fff" ); ui.applyStyle( "foreground: #fff" ); ui.applyStyle( "border: 2,2,2,2,#f00" ); + ui.applyStyle( "font: italic 12 monospaced" ); } @Test @@ -878,6 +890,7 @@ public class TestFlatStyling ui.applyStyle( "background: #fff" ); ui.applyStyle( "foreground: #fff" ); ui.applyStyle( "border: 2,2,2,2,#f00" ); + ui.applyStyle( "font: italic 12 monospaced" ); // JTable properties ui.applyStyle( "fillsViewportHeight: true" ); @@ -909,6 +922,7 @@ public class TestFlatStyling ui.applyStyle( "background: #fff" ); ui.applyStyle( "foreground: #fff" ); ui.applyStyle( "border: 2,2,2,2,#f00" ); + ui.applyStyle( "font: italic 12 monospaced" ); } @Test @@ -925,6 +939,7 @@ public class TestFlatStyling ui.applyStyle( "background: #fff" ); ui.applyStyle( "foreground: #fff" ); ui.applyStyle( "border: 2,2,2,2,#f00" ); + ui.applyStyle( "font: italic 12 monospaced" ); // JTextComponent properties ui.applyStyle( "caretColor: #fff" ); @@ -958,6 +973,7 @@ public class TestFlatStyling ui.applyStyle( "background: #fff" ); ui.applyStyle( "foreground: #fff" ); ui.applyStyle( "border: 2,2,2,2,#f00" ); + ui.applyStyle( "font: italic 12 monospaced" ); // JTextComponent properties ui.applyStyle( "caretColor: #fff" ); @@ -980,6 +996,7 @@ public class TestFlatStyling ui.applyStyle( "background: #fff" ); ui.applyStyle( "foreground: #fff" ); ui.applyStyle( "border: 2,2,2,2,#f00" ); + ui.applyStyle( "font: italic 12 monospaced" ); // JTextComponent properties ui.applyStyle( "caretColor: #fff" ); @@ -1063,6 +1080,7 @@ public class TestFlatStyling ui.applyStyle( "background: #fff" ); ui.applyStyle( "foreground: #fff" ); ui.applyStyle( "border: 2,2,2,2,#f00" ); + ui.applyStyle( "font: italic 12 monospaced" ); // JTree properties ui.applyStyle( "rootVisible: true" );