Compare commits

...

5 Commits

Author SHA1 Message Date
Karl Tauber
03b7a1c29e Styling: added missing unit tests; added missing FlatCapsLockIcon.getStyleableInfos()
Some checks failed
CI / build (push) Has been cancelled
CI / release (push) Has been cancelled
Error Prone / error-prone (push) Has been cancelled
2025-11-25 18:51:40 +01:00
Karl Tauber
60968f77eb Styling: replaced all occurrences of
`return new UnknownStyleException( key )`
with
  `throw new UnknownStyleException( key )`
2025-11-25 18:48:15 +01:00
Karl Tauber
8bafa37b4a Merge PR #1060: Styling of wrong icon in FlatRadioButtonUI 2025-11-25 18:24:06 +01:00
Karl Tauber
04602ac227 CheckBox and RadioButton:
- fixed styling of custom icon
- fixed focus width (and preferred size) if using custom icon
- added unit tests
2025-11-25 18:18:22 +01:00
daWoife
7ebc1b27c1 Update FlatRadioButtonUI.java
The method applyStyleProperty of class FlatRadioButtonUI currently styles the variable icon, a field of the parent class BasicRadioButtonUI and the default icon which is set with UIManager.getIcon in method installDefaults. 
So the wrong icon is styled if someone subclasses FlatRadioBoxIcon or FlatCheckBoxIcon and sets this as the new icon for a JRadioButton or JCheckBox instance. (An example why someone would do so is shown in issue #413).
With this change styling takes account of the current icon of a JRadioButton or JCheckBox.
2025-11-15 11:37:18 +01:00
10 changed files with 290 additions and 22 deletions

View File

@@ -11,6 +11,8 @@ FlatLaf Change Log
#### Fixed bugs
- CheckBox and RadioButton: Fixed styling of custom icon. Also fixed focus width
(and preferred size) if using custom icon. (PR #1060)
- TextField: Fixed wrong leading/trailing icon placement if border is set to
`null`. (issue #1047)
- Extras: UI defaults inspector: Exclude inspector window from being blocked by

View File

@@ -25,6 +25,8 @@ import java.awt.geom.Area;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.util.Collections;
import java.util.Map;
import javax.swing.UIManager;
import com.formdev.flatlaf.ui.FlatStylingSupport.UnknownStyleException;
import com.formdev.flatlaf.ui.FlatUIUtils;
@@ -54,6 +56,11 @@ public class FlatCapsLockIcon
}
}
/** @since 3.7 */
public Map<String, Class<?>> getStyleableInfos() throws IllegalArgumentException {
return Collections.singletonMap( "capsLockIconColor", Color.class );
}
/** @since 2.5 */
public Object getStyleableValue( String key ) {
switch( key ) {

View File

@@ -370,7 +370,7 @@ public class FlatButtonUI
protected Object applyStyleProperty( AbstractButton b, String key, Object value ) {
if( key.startsWith( "help." ) ) {
if( !(helpButtonIcon instanceof FlatHelpButtonIcon) )
return new UnknownStyleException( key );
throw new UnknownStyleException( key );
if( helpButtonIconShared ) {
helpButtonIcon = FlatStylingSupport.cloneIcon( helpButtonIcon );

View File

@@ -16,7 +16,6 @@
package com.formdev.flatlaf.ui;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Toolkit;
@@ -230,7 +229,8 @@ public class FlatPasswordFieldUI
@Override
public Map<String, Class<?>> getStyleableInfos( JComponent c ) {
Map<String, Class<?>> infos = super.getStyleableInfos( c );
infos.put( "capsLockIconColor", Color.class );
if( capsLockIcon instanceof FlatCapsLockIcon )
infos.putAll( ((FlatCapsLockIcon)capsLockIcon).getStyleableInfos() );
return infos;
}

View File

@@ -203,11 +203,12 @@ public class FlatRadioButtonUI
protected Object applyStyleProperty( AbstractButton b, String key, Object value ) {
// style icon
if( key.startsWith( "icon." ) ) {
Icon icon = getRealIcon( b );
if( !(icon instanceof FlatCheckBoxIcon) )
return new UnknownStyleException( key );
throw new UnknownStyleException( key );
if( iconShared ) {
icon = FlatStylingSupport.cloneIcon( icon );
if( icon == this.icon && iconShared ) {
this.icon = icon = FlatStylingSupport.cloneIcon( icon );
iconShared = false;
}
@@ -225,6 +226,7 @@ public class FlatRadioButtonUI
@Override
public Map<String, Class<?>> getStyleableInfos( JComponent c ) {
Map<String, Class<?>> infos = FlatStylingSupport.getAnnotatedStyleableInfos( this );
Icon icon = getRealIcon( c );
if( icon instanceof FlatCheckBoxIcon ) {
for( Map.Entry<String, Class<?>> e : ((FlatCheckBoxIcon)icon).getStyleableInfos().entrySet() )
infos.put( "icon.".concat( e.getKey() ), e.getValue() );
@@ -237,6 +239,7 @@ public class FlatRadioButtonUI
public Object getStyleableValue( JComponent c, String key ) {
// style icon
if( key.startsWith( "icon." ) ) {
Icon icon = getRealIcon( c );
return (icon instanceof FlatCheckBoxIcon)
? ((FlatCheckBoxIcon)icon).getStyleableValue( key.substring( "icon.".length() ) )
: null;
@@ -332,16 +335,18 @@ public class FlatRadioButtonUI
}
private int getIconFocusWidth( JComponent c ) {
AbstractButton b = (AbstractButton) c;
Icon icon = b.getIcon();
if( icon == null )
icon = getDefaultIcon();
Icon icon = getRealIcon( c );
return (icon instanceof FlatCheckBoxIcon)
? Math.round( UIScale.scale( ((FlatCheckBoxIcon)icon).getFocusWidth() ) )
: 0;
}
private Icon getRealIcon( JComponent c ) {
AbstractButton b = (AbstractButton) c;
Icon icon = b.getIcon();
return (icon != null) ? icon : getDefaultIcon();
}
@Override
public int getBaseline( JComponent c, int width, int height ) {
return FlatButtonUI.getBaselineImpl( c, width, height );

View File

@@ -671,7 +671,7 @@ public class FlatTabbedPaneUI
// close icon
if( key.startsWith( "close" ) ) {
if( !(closeIcon instanceof FlatTabbedPaneCloseIcon) )
return new UnknownStyleException( key );
throw new UnknownStyleException( key );
if( closeIconShared ) {
closeIcon = FlatStylingSupport.cloneIcon( closeIcon );

View File

@@ -30,6 +30,9 @@ import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import com.formdev.flatlaf.icons.*;
import com.formdev.flatlaf.ui.TestFlatStyling.CustomCheckBoxIcon;
import com.formdev.flatlaf.ui.TestFlatStyling.CustomIcon;
import com.formdev.flatlaf.ui.TestFlatStyling.CustomRadioButtonIcon;
/**
* @author Karl Tauber
@@ -144,7 +147,12 @@ public class TestFlatStyleableInfo
@Test
void checkBox() {
JCheckBox c = new JCheckBox();
checkBox( new JCheckBox() );
checkBox( new JCheckBox( new CustomIcon() ) );
checkBox( new JCheckBox( new CustomCheckBoxIcon() ) );
}
private void checkBox( JCheckBox c ) {
FlatCheckBoxUI ui = (FlatCheckBoxUI) c.getUI();
assertTrue( ui.getDefaultIcon() instanceof FlatCheckBoxIcon );
@@ -153,6 +161,11 @@ public class TestFlatStyleableInfo
Map<String, Class<?>> expected = new LinkedHashMap<>();
radioButton( expected );
// remove "icon." keys if check box has custom icon
Icon icon = c.getIcon();
if( icon != null && !(icon instanceof FlatCheckBoxIcon) )
expected.keySet().removeIf( key -> key.startsWith( "icon." ) );
assertMapEquals( expected, ui.getStyleableInfos( c ) );
}
@@ -492,7 +505,12 @@ public class TestFlatStyleableInfo
@Test
void radioButton() {
JRadioButton c = new JRadioButton();
radioButton( new JRadioButton() );
radioButton( new JRadioButton( new CustomIcon() ) );
radioButton( new JRadioButton( new CustomRadioButtonIcon() ) );
}
private void radioButton( JRadioButton c ) {
FlatRadioButtonUI ui = (FlatRadioButtonUI) c.getUI();
assertTrue( ui.getDefaultIcon() instanceof FlatRadioButtonIcon );
@@ -504,6 +522,11 @@ public class TestFlatStyleableInfo
"icon.centerDiameter", float.class
);
// remove "icon." keys if radio button has custom icon
Icon icon = c.getIcon();
if( icon != null && !(icon instanceof FlatRadioButtonIcon) )
expected.keySet().removeIf( key -> key.startsWith( "icon." ) );
assertMapEquals( expected, ui.getStyleableInfos( c ) );
}
@@ -1112,6 +1135,16 @@ public class TestFlatStyleableInfo
assertMapEquals( expected, border.getStyleableInfos() );
}
@Test
void flatScrollPaneBorder() {
FlatScrollPaneBorder border = new FlatScrollPaneBorder();
Map<String, Class<?>> expected = new LinkedHashMap<>();
flatScrollPaneBorder( expected );
assertMapEquals( expected, border.getStyleableInfos() );
}
@Test
void flatTextBorder() {
FlatTextBorder border = new FlatTextBorder();
@@ -1295,4 +1328,56 @@ public class TestFlatStyleableInfo
assertMapEquals( expected, icon.getStyleableInfos() );
}
@Test
void flatClearIcon() {
FlatClearIcon icon = new FlatClearIcon();
Map<String, Class<?>> expected = expectedMap(
"clearIconColor", Color.class,
"clearIconHoverColor", Color.class,
"clearIconPressedColor", Color.class
);
assertMapEquals( expected, icon.getStyleableInfos() );
}
@Test
void flatSearchIcon() {
FlatSearchIcon icon = new FlatSearchIcon();
Map<String, Class<?>> expected = new LinkedHashMap<>();
flatSearchIcon( expected );
assertMapEquals( expected, icon.getStyleableInfos() );
}
@Test
void flatSearchWithHistoryIcon() {
FlatSearchWithHistoryIcon icon = new FlatSearchWithHistoryIcon();
Map<String, Class<?>> expected = new LinkedHashMap<>();
flatSearchIcon( expected );
assertMapEquals( expected, icon.getStyleableInfos() );
}
private void flatSearchIcon( Map<String, Class<?>> expected ) {
expectedMap( expected,
"searchIconColor", Color.class,
"searchIconHoverColor", Color.class,
"searchIconPressedColor", Color.class
);
}
@Test
void flatCapsLockIcon() {
FlatCapsLockIcon icon = new FlatCapsLockIcon();
Map<String, Class<?>> expected = expectedMap(
"capsLockIconColor", Color.class
);
assertMapEquals( expected, icon.getStyleableInfos() );
}
}

View File

@@ -67,6 +67,7 @@ import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import com.formdev.flatlaf.icons.FlatCapsLockIcon;
import com.formdev.flatlaf.icons.FlatCheckBoxIcon;
import com.formdev.flatlaf.icons.FlatCheckBoxMenuItemIcon;
import com.formdev.flatlaf.icons.FlatClearIcon;
@@ -77,6 +78,10 @@ import com.formdev.flatlaf.icons.FlatRadioButtonMenuItemIcon;
import com.formdev.flatlaf.icons.FlatSearchIcon;
import com.formdev.flatlaf.icons.FlatSearchWithHistoryIcon;
import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI;
import com.formdev.flatlaf.ui.FlatStylingSupport.UnknownStyleException;
import com.formdev.flatlaf.ui.TestFlatStyling.CustomCheckBoxIcon;
import com.formdev.flatlaf.ui.TestFlatStyling.CustomIcon;
import com.formdev.flatlaf.ui.TestFlatStyling.CustomRadioButtonIcon;
/**
* @author Karl Tauber
@@ -269,11 +274,20 @@ public class TestFlatStyleableValue
@Test
void checkBox() {
JCheckBox c = new JCheckBox();
checkBox( new JCheckBox() );
checkBox( new JCheckBox( new CustomCheckBoxIcon() ) );
checkBox( new JCheckBox( new CustomIcon() ) );
}
private void checkBox( JCheckBox c ) {
FlatCheckBoxUI ui = (FlatCheckBoxUI) c.getUI();
// FlatCheckBoxUI extends FlatRadioButtonUI
radioButton( ui, c );
// necessary to clear FlatRadioButtonUI.oldStyleValues because
// ui.applyStyle(...) operates on shared instance
ui.uninstallUI( c );
}
@Test
@@ -536,14 +550,24 @@ public class TestFlatStyleableValue
@Test
void radioButton() {
JRadioButton c = new JRadioButton();
radioButton( new JRadioButton() );
radioButton( new JRadioButton( new CustomRadioButtonIcon() ) );
radioButton( new JRadioButton( new CustomIcon() ) );
}
private void radioButton( JRadioButton c ) {
FlatRadioButtonUI ui = (FlatRadioButtonUI) c.getUI();
assertTrue( ui.getDefaultIcon() instanceof FlatRadioButtonIcon );
radioButton( ui, c );
if( !(c.getIcon() instanceof CustomIcon) )
testFloat( c, ui, "icon.centerDiameter", 1.23f );
// necessary to clear FlatRadioButtonUI.oldStyleValues because
// ui.applyStyle(...) operates on shared instance
ui.uninstallUI( c );
}
private void radioButton( FlatRadioButtonUI ui, AbstractButton b ) {
@@ -551,6 +575,17 @@ public class TestFlatStyleableValue
//---- icon ----
if( b.getIcon() instanceof CustomIcon ) {
try {
ui.applyStyle( b, "icon.focusWidth: 1.23" );
assertTrue( false );
} catch( UnknownStyleException ex ) {
assertEquals( new UnknownStyleException( "icon.focusWidth" ).getMessage(), ex.getMessage() );
}
assertEquals( null, ui.getStyleableValue( b, "icon.focusWidth" ) );
return;
}
testFloat( b, ui, "icon.focusWidth", 1.23f );
testColor( b, ui, "icon.focusColor", 0x123456 );
testFloat( b, ui, "icon.borderWidth", 1.23f );
@@ -1290,6 +1325,13 @@ public class TestFlatStyleableValue
testValue( icon, "searchIconPressedColor", Color.WHITE );
}
@Test
void flatCapsLockIcon() {
FlatCapsLockIcon icon = new FlatCapsLockIcon();
testValue( icon, "capsLockIconColor", Color.WHITE );
}
//---- class TestIcon -----------------------------------------------------
@SuppressWarnings( "EqualsHashCode" ) // Error Prone

View File

@@ -19,10 +19,15 @@ package com.formdev.flatlaf.ui;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Insets;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import javax.swing.*;
import javax.swing.table.JTableHeader;
@@ -31,6 +36,7 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import com.formdev.flatlaf.FlatLaf;
import com.formdev.flatlaf.icons.*;
import com.formdev.flatlaf.ui.FlatStylingSupport.UnknownStyleException;
import com.formdev.flatlaf.util.ColorFunctions;
/**
@@ -47,6 +53,14 @@ public class TestFlatStyling
FlatLaf.setGlobalExtraDefaults( globalExtraDefaults );
TestUtils.setup( false );
Set<String> excludes = new HashSet<>();
Collections.addAll( excludes,
"parse", "parseIfFunction", "parseColorFunctions",
"parseReferences", "parseVariables", "parseRecursiveVariables",
"enumField", "enumProperty", "enumUIDefaults" );
TestUtils.checkImplementedTests( excludes, TestFlatStyling.class,
TestFlatStyleableValue.class, TestFlatStyleableInfo.class );
}
@AfterAll
@@ -294,13 +308,22 @@ public class TestFlatStyling
@Test
void checkBox() {
JCheckBox c = new JCheckBox();
checkBox( new JCheckBox() );
checkBox( new JCheckBox( new CustomIcon() ) );
checkBox( new JCheckBox( new CustomCheckBoxIcon() ) );
}
private void checkBox( JCheckBox c ) {
FlatCheckBoxUI ui = (FlatCheckBoxUI) c.getUI();
assertTrue( ui.getDefaultIcon() instanceof FlatCheckBoxIcon );
// FlatCheckBoxUI extends FlatRadioButtonUI
radioButton( ui, c );
// necessary to clear FlatRadioButtonUI.oldStyleValues because
// ui.applyStyle(...) operates on shared instance
ui.uninstallUI( c );
}
@Test
@@ -651,14 +674,24 @@ public class TestFlatStyling
@Test
void radioButton() {
JRadioButton c = new JRadioButton();
radioButton( new JRadioButton() );
radioButton( new JRadioButton( new CustomIcon() ) );
radioButton( new JRadioButton( new CustomRadioButtonIcon() ) );
}
private void radioButton( JRadioButton c ) {
FlatRadioButtonUI ui = (FlatRadioButtonUI) c.getUI();
assertTrue( ui.getDefaultIcon() instanceof FlatRadioButtonIcon );
radioButton( ui, c );
if( !(c.getIcon() instanceof CustomIcon) )
ui.applyStyle( c, "icon.centerDiameter: 8" );
// necessary to clear FlatRadioButtonUI.oldStyleValues because
// ui.applyStyle(...) operates on shared instance
ui.uninstallUI( c );
}
private void radioButton( FlatRadioButtonUI ui, AbstractButton b ) {
@@ -676,6 +709,16 @@ public class TestFlatStyling
//---- icon ----
if( b.getIcon() instanceof CustomIcon ) {
try {
ui.applyStyle( b, "icon.focusWidth: 1.5" );
assertTrue( false );
} catch( UnknownStyleException ex ) {
assertEquals( new UnknownStyleException( "icon.focusWidth" ).getMessage(), ex.getMessage() );
}
return;
}
ui.applyStyle( b, "icon.focusWidth: 1.5" );
ui.applyStyle( b, "icon.focusColor: #fff" );
ui.applyStyle( b, "icon.borderWidth: 1.5" );
@@ -1333,6 +1376,16 @@ public class TestFlatStyling
border.applyStyleProperty( "arc", 6 );
}
@Test
void flatScrollPaneBorder() {
FlatScrollPaneBorder border = new FlatScrollPaneBorder();
// FlatScrollPaneBorder extends FlatBorder
flatBorder( border );
border.applyStyleProperty( "arc", 6 );
}
@Test
void flatTextBorder() {
FlatTextBorder border = new FlatTextBorder();
@@ -1532,6 +1585,13 @@ public class TestFlatStyling
icon.applyStyleProperty( "searchIconPressedColor", Color.WHITE );
}
@Test
void flatCapsLockIcon() {
FlatCapsLockIcon icon = new FlatCapsLockIcon();
icon.applyStyleProperty( "capsLockIconColor", Color.WHITE );
}
//---- enums --------------------------------------------------------------
enum SomeEnum { enumValue1, enumValue2 }
@@ -1565,4 +1625,34 @@ public class TestFlatStyling
UIManager.put( "test.enum", null );
assertEquals( SomeEnum.enumValue1, FlatUIUtils.getUIEnum( "test.enum", SomeEnum.class, SomeEnum.enumValue1 ) );
}
//---- class CustomIcon ---------------------------------------------------
static class CustomIcon
implements Icon
{
@Override public void paintIcon( Component c, Graphics g, int x, int y ) {}
@Override public int getIconWidth() { return 1; }
@Override public int getIconHeight() { return 1; }
}
//---- class CustomCheckBoxIcon ----------------------------------------
static class CustomCheckBoxIcon
extends FlatCheckBoxIcon
{
CustomCheckBoxIcon() {
background = Color.green;
}
}
//---- class CustomRadioButtonIcon ----------------------------------------
static class CustomRadioButtonIcon
extends FlatRadioButtonIcon
{
CustomRadioButtonIcon() {
background = Color.green;
}
}
}

View File

@@ -17,9 +17,14 @@
package com.formdev.flatlaf.ui;
import java.awt.Font;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import javax.swing.UIManager;
import org.junit.jupiter.api.Test;
import org.opentest4j.AssertionFailedError;
import com.formdev.flatlaf.FlatIntelliJLaf;
import com.formdev.flatlaf.FlatLightLaf;
@@ -57,12 +62,44 @@ public class TestUtils
public static void assertMapEquals( Map<?, ?> expected, Map<?, ?> actual ) {
if( !Objects.equals( expected, actual ) ) {
String expectedStr = String.valueOf( expected ).replace( ", ", ",\n" );
String actualStr = String.valueOf( actual ).replace( ", ", ",\n" );
String expectedStr = String.valueOf( new TreeMap<>( expected ) ).replace( ", ", ",\n" );
String actualStr = String.valueOf( new TreeMap<>( actual ) ).replace( ", ", ",\n" );
String msg = String.format( "expected: <%s> but was: <%s>", expectedStr, actualStr );
// pass expected/actual strings to exception for nice diff in IDE
throw new AssertionFailedError( msg, expectedStr, actualStr );
}
}
public static void checkImplementedTests( Set<String> excludes, Class<?> baseClass, Class<?>... classes ) {
Set<String> expected = getTestMethods( baseClass );
for( Class<?> cls : classes ) {
Set<String> actual = getTestMethods( cls );
for( String methodName : expected ) {
if( !actual.contains( methodName ) && !excludes.contains( methodName ) ) {
throw new AssertionFailedError( "missing " + cls.getSimpleName() + '.' + methodName
+ "() for " + baseClass.getSimpleName() + '.' + methodName + "()" );
}
}
for( String methodName : actual ) {
if( !expected.contains( methodName ) && !excludes.contains( methodName ) ) {
throw new AssertionFailedError( "missing " + baseClass.getSimpleName() + '.' + methodName
+ "() for " + cls.getSimpleName() + '.' + methodName + "()" );
}
}
}
}
private static Set<String> getTestMethods( Class<?> cls ) {
HashSet<String> tests = new HashSet<>();
Method[] methods = cls.getDeclaredMethods();
for( Method m : methods ) {
if( m.isAnnotationPresent( Test.class ) )
tests.add( m.getName() );
}
return tests;
}
}