-
-
Notifications
You must be signed in to change notification settings - Fork 321
Description
Hey there,
I noticed this bug the other day and created a SSCCE (see HeaderRendererTest.java below) and a screen capture, demonstrating the problem.
After looking into it, I figured I'd need help, so I had Claude analyze this for me.
Claude seems to have found the problem and I have verified that it is indeed fixed, but I decided to not create a PR, but rather a comprehensive issue with a patch, since this is quite a subtle bug in a pretty complex component, so me and Claude may definitely be missing something 😄.
Below you'll find the issue description Claude created for me, hope it's helpful.
Summary
header-renderer-bug.webm
When using a custom table header renderer that sets a background color on specific columns, the custom background stops being painted after moving the mouse back and forth between columns a few times. Once the bug triggers, the custom background "never comes back" until the component is recreated.
Environment
- FlatLaf version: 3.7.1-SNAPSHOT (and likely earlier versions)
- Java version: 25.0.1 (also reproduced on other versions)
- OS: Linux (likely affects all platforms)
Steps to Reproduce
- Run the SSCCE below
- Click on a row to select it
- Click on the first column ("One") to select it - the column header should turn RED
- Move the mouse cursor back and forth between the column headers several times
- Observe that the RED background stops appearing and never comes back
Important: The bug specifically manifests when the first column (index 0) is selected. It may take a few back-and-forth mouse movements to trigger.
SSCCE
package com.formdev.flatlaf.testing;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;
import com.formdev.flatlaf.FlatDarkLaf;
public class HeaderRendererTest
{
static void show() {
try {
UIManager.setLookAndFeel( FlatDarkLaf.class.getName() );
} catch( Exception ex ) {
throw new RuntimeException( ex );
}
TableCellRenderer headerRenderer = ( table, value, isSelected, hasFocus, row, column ) -> {
TableCellRenderer defaultRenderer = table.getTableHeader().getDefaultRenderer();
Component component = defaultRenderer.getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column );
if( !table.getSelectionModel().isSelectionEmpty() ) {
int selectedColumn = table.getColumnModel().getSelectionModel().getLeadSelectionIndex();
if( column == selectedColumn ) {
component.setBackground( Color.RED );// indicates the selected column
}
}
return component;
};
DefaultTableModel tableModel = new DefaultTableModel();
tableModel.addColumn( "One" );
tableModel.addColumn( "Two" );
tableModel.addRow( new Object[] { "1", "2" } );
tableModel.addRow( new Object[] { "3", "4" } );
JTable table = new JTable( tableModel );
table.getColumnModel().getSelectionModel().addListSelectionListener( e -> table.getTableHeader().repaint() );
table.getColumn( "One" ).setHeaderRenderer( headerRenderer );
table.getColumn( "Two" ).setHeaderRenderer( headerRenderer );
JScrollPane scrollPane = new JScrollPane( table );
scrollPane.setPreferredSize( new Dimension(300, 100 ) );
JOptionPane.showOptionDialog( null, scrollPane, "Test",
JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, null, null );
}
public static void main( String[] args ) {
SwingUtilities.invokeLater( HeaderRendererTest::show );
}
}Root Cause Analysis
The bug is in FlatTableHeaderUI.FlatTableHeaderCellRendererPane.paintComponent().
The Problem
The same JLabel instance (from the default renderer) is reused for rendering ALL table header columns. When hover/pressed background is applied, the code:
- Saves
oldOpaque = l.isOpaque() - Sets
l.setOpaque(true)for painting with hover background - After painting, restores
l.setOpaque(oldOpaque)
The issue is that oldOpaque captures the JLabel's opaque state at the start of paintComponent, which may have been corrupted by a previous column's restore operation.
Detailed Flow
- Column 1's default renderer returns the JLabel with
opaque=false - When hover is applied to column 1:
- Saves
oldOpaque=false - Sets
opaque=truefor painting - Restores
opaque=falseafter painting
- Saves
- Now the shared JLabel instance has
opaque=false - When column 0 is painted next (with custom RED background but no hover):
- The JLabel inherits
opaque=falsefrom column 1's restore - The
background != nullcheck fails (no hover), so no save/restore happens - The RED background is set but doesn't show because
opaque=false
- The JLabel inherits
The custom background color IS being set correctly (verified with debug output), but the JLabel's opaque property becomes permanently false, preventing the background from being painted.
Proposed Fix
Remove the save/restore of oldOpaque. When hover is applied, setting opaque=true is correct for painting backgrounds. We should not restore it to a potentially corrupted value from a previous column's render.
Patch
diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTableHeaderUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTableHeaderUI.java
index e02d44c2..1ff8a71d 100644
--- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTableHeaderUI.java
+++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTableHeaderUI.java
@@ -357,7 +357,6 @@ public class FlatTableHeaderUI
JLabel l = (JLabel) c;
Color oldBackground = null;
Color oldForeground = null;
- boolean oldOpaque = false;
Icon oldIcon = null;
int oldHorizontalTextPosition = -1;
@@ -377,7 +376,6 @@ public class FlatTableHeaderUI
}
if( background != null ) {
oldBackground = l.getBackground();
- oldOpaque = l.isOpaque();
l.setBackground( FlatUIUtils.deriveColor( background, header.getBackground() ) );
l.setOpaque( true );
}
@@ -416,10 +414,8 @@ public class FlatTableHeaderUI
}
// restore modified renderer component properties
- if( background != null ) {
+ if( background != null )
l.setBackground( oldBackground );
- l.setOpaque( oldOpaque );
- }
if( foreground != null )
l.setForeground( oldForeground );
if( oldIcon != null )Why This Fix Works
- When hover IS applied: We set
opaque=true(correct for painting backgrounds), and we no longer restore it to a corrupted value - When hover is NOT applied: The renderer controls the opaque state as before
- The renderer is called before each
paintComponent, so it has the opportunity to set the correct opaque state for each column
The fix removes 4 lines of code related to oldOpaque handling.
Additional Notes
- The bug is timing/interaction dependent - it requires specific mouse movement patterns to trigger
- The bug specifically affects column 0 because of how the opaque state propagates between column renders
- A video demonstration is attached showing the bug in action