diff --git a/CHANGELOG.md b/CHANGELOG.md index 99aaf85d..4d2b276c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ FlatLaf Change Log #### Fixed bugs +- Table and TableHeader: Fixed missing right vertical grid line if using table + as row header in scroll pane. (issues #152 and #46) - TableHeader: Fixed position of column separators in right-to-left component orientation. - SwingX: Fixed striping background highlighting color (e.g. alternating table 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 a3ca46a7..50722d54 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 @@ -31,6 +31,7 @@ import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JScrollPane; import javax.swing.JTable; +import javax.swing.ScrollPaneConstants; import javax.swing.SwingConstants; import javax.swing.UIManager; import javax.swing.border.Border; @@ -148,6 +149,9 @@ public class FlatTableHeaderUI float bottomLineIndent = lineWidth * 3; TableColumnModel columnModel = header.getColumnModel(); int columnCount = columnModel.getColumnCount(); + int sepCount = columnCount; + if( hideLastVerticalLine() ) + sepCount--; Graphics2D g2 = (Graphics2D) g.create(); try { @@ -160,24 +164,30 @@ public class FlatTableHeaderUI // paint column separator lines g2.setColor( separatorColor ); - int sepCount = columnCount; - if( header.getTable() != null && header.getTable().getAutoResizeMode() != JTable.AUTO_RESIZE_OFF && !isVerticalScrollBarVisible() ) - sepCount--; + float y = topLineIndent; + float h = height - bottomLineIndent; if( header.getComponentOrientation().isLeftToRight() ) { int x = 0; for( int i = 0; i < sepCount; i++ ) { x += columnModel.getColumn( i ).getWidth(); - g2.fill( new Rectangle2D.Float( x - lineWidth, topLineIndent, lineWidth, height - bottomLineIndent ) ); + g2.fill( new Rectangle2D.Float( x - lineWidth, y, lineWidth, h ) ); } + + // paint trailing separator (on right side) + if( !hideTrailingVerticalLine() ) + g2.fill( new Rectangle2D.Float( header.getWidth() - lineWidth, y, lineWidth, h ) ); } else { Rectangle cellRect = header.getHeaderRect( 0 ); int x = cellRect.x + cellRect.width; for( int i = 0; i < sepCount; i++ ) { x -= columnModel.getColumn( i ).getWidth(); - g2.fill( new Rectangle2D.Float( x - (i < sepCount - 1 ? lineWidth : 0), - topLineIndent, lineWidth, height - bottomLineIndent ) ); + g2.fill( new Rectangle2D.Float( x - (i < sepCount - 1 ? lineWidth : 0), y, lineWidth, h ) ); } + + // paint trailing separator (on left side) + if( !hideTrailingVerticalLine() ) + g2.fill( new Rectangle2D.Float( 0, y, lineWidth, h ) ); } } finally { g2.dispose(); @@ -234,20 +244,30 @@ public class FlatTableHeaderUI return size; } - private boolean isVerticalScrollBarVisible() { - JScrollPane scrollPane = getScrollPane(); - return (scrollPane != null && scrollPane.getVerticalScrollBar() != null) - ? scrollPane.getVerticalScrollBar().isVisible() - : false; + protected boolean hideLastVerticalLine() { + Container viewport = header.getParent(); + Container viewportParent = (viewport != null) ? viewport.getParent() : null; + if( !(viewportParent instanceof JScrollPane) ) + return false; + + Rectangle cellRect = header.getHeaderRect( header.getColumnModel().getColumnCount() - 1 ); + + // using component orientation of scroll pane here because it is also used in FlatTableUI + JScrollPane scrollPane = (JScrollPane) viewportParent; + return scrollPane.getComponentOrientation().isLeftToRight() + ? cellRect.x + cellRect.width >= viewport.getWidth() + : cellRect.x <= 0; } - private JScrollPane getScrollPane() { - Container parent = header.getParent(); - if( parent == null ) - return null; + protected boolean hideTrailingVerticalLine() { + Container viewport = header.getParent(); + Container viewportParent = (viewport != null) ? viewport.getParent() : null; + if( !(viewportParent instanceof JScrollPane) ) + return false; - parent = parent.getParent(); - return (parent instanceof JScrollPane) ? (JScrollPane) parent : null; + JScrollPane scrollPane = (JScrollPane) viewportParent; + return viewport == scrollPane.getColumnHeader() && + scrollPane.getCorner( ScrollPaneConstants.UPPER_TRAILING_CORNER ) == null; } //---- class FlatTableCellHeaderRenderer ---------------------------------- diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTableUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTableUI.java index ea73f861..f436f195 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTableUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTableUI.java @@ -17,6 +17,7 @@ package com.formdev.flatlaf.ui; import java.awt.Color; +import java.awt.Container; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Graphics; @@ -26,8 +27,10 @@ import java.awt.event.FocusListener; import java.awt.geom.Rectangle2D; import javax.swing.JCheckBox; import javax.swing.JComponent; -import javax.swing.JTable; +import javax.swing.JScrollPane; +import javax.swing.JViewport; import javax.swing.LookAndFeel; +import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.basic.BasicTableUI; @@ -215,16 +218,11 @@ public class FlatTableUI boolean verticalLines = table.getShowVerticalLines(); if( horizontalLines || verticalLines ) { // fix grid painting issues in BasicTableUI - // - do not paint last vertical grid line if auto-resize mode is not off - // - in right-to-left component orientation, do not paint last vertical grid line - // in any auto-resize mode; can not paint on left side of table because - // cells are painted over left line + // - do not paint last vertical grid line if line is on right edge of scroll pane // - fix unstable grid line thickness when scaled at 125%, 150%, 175%, 225%, ... // which paints either 1px or 2px lines depending on location - boolean hideLastVerticalLine = - table.getAutoResizeMode() != JTable.AUTO_RESIZE_OFF || - !table.getComponentOrientation().isLeftToRight(); + boolean hideLastVerticalLine = hideLastVerticalLine(); int tableWidth = table.getWidth(); double systemScaleFactor = UIScale.getSystemScaleFactor( (Graphics2D) g ); @@ -281,4 +279,26 @@ public class FlatTableUI super.paint( g, c ); } + + protected boolean hideLastVerticalLine() { + Container viewport = SwingUtilities.getUnwrappedParent( table ); + Container viewportParent = (viewport != null) ? viewport.getParent() : null; + if( !(viewportParent instanceof JScrollPane) ) + return false; + + // do not hide last vertical line if table is smaller than viewport + if( table.getX() + table.getWidth() < viewport.getWidth() ) + return false; + + // in left-to-right: + // - do not hide last vertical line if table used as row header in scroll pane + // in right-to-left: + // - hide last vertical line if table used as row header in scroll pane + // - do not hide last vertical line if table is in center and scroll pane has row header + JScrollPane scrollPane = (JScrollPane) viewportParent; + JViewport rowHeader = scrollPane.getRowHeader(); + return scrollPane.getComponentOrientation().isLeftToRight() + ? (viewport != rowHeader) + : (viewport == rowHeader || rowHeader == null); + } } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatComponents2Test.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatComponents2Test.java index 996569cf..819fbe85 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatComponents2Test.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatComponents2Test.java @@ -17,6 +17,8 @@ package com.formdev.flatlaf.testing; import java.awt.Color; +import java.awt.Component; +import java.awt.ComponentOrientation; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.datatransfer.DataFlavor; @@ -29,10 +31,13 @@ import java.util.List; import java.util.Map; import java.util.Random; import javax.swing.*; +import javax.swing.event.TableModelEvent; +import javax.swing.event.TableModelListener; import javax.swing.table.*; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import com.formdev.flatlaf.icons.FlatMenuArrowIcon; +import com.formdev.flatlaf.util.UIScale; import net.miginfocom.swing.*; import org.jdesktop.swingx.JXTable; import org.jdesktop.swingx.JXTreeTable; @@ -53,6 +58,7 @@ public class FlatComponents2Test public static void main( String[] args ) { SwingUtilities.invokeLater( () -> { FlatTestFrame frame = FlatTestFrame.create( args, "FlatComponents2Test" ); + frame.useApplyComponentOrientation = true; frame.showFrame( FlatComponents2Test::new ); } ); } @@ -60,7 +66,9 @@ public class FlatComponents2Test private final TestListModel listModel; private final TestTreeModel treeModel; private final TestTableModel tableModel; - private final JTable[] allTables; + private final List allTables = new ArrayList<>(); + private final List allTablesInclRowHeader = new ArrayList<>(); + private JTable rowHeaderTable1; FlatComponents2Test() { initComponents(); @@ -96,7 +104,10 @@ public class FlatComponents2Test xTreeTable1.setTreeTableModel( new FileSystemModel( new File( "." ) ) ); xTreeTable1.setHighlighters( simpleStriping, magenta, rollover, shading ); - allTables = new JTable[] { table1, xTable1, xTreeTable1 }; + allTables.add( table1 ); + allTables.add( xTable1 ); + allTables.add( xTreeTable1 ); + allTablesInclRowHeader.addAll( allTables ); expandTree( tree1 ); expandTree( tree2 ); @@ -219,6 +230,7 @@ public class FlatComponents2Test JButton button = null; if( show ) { button = new JButton( new FlatMenuArrowIcon() ); + button.applyComponentOrientation( getComponentOrientation() ); button.addActionListener( e -> { JOptionPane.showMessageDialog( this, "hello" ); } ); @@ -237,25 +249,72 @@ public class FlatComponents2Test } private void showHorizontalLinesChanged() { - for( JTable table : allTables ) + for( JTable table : allTablesInclRowHeader ) table.setShowHorizontalLines( showHorizontalLinesCheckBox.isSelected() ); } private void showVerticalLinesChanged() { - for( JTable table : allTables ) + for( JTable table : allTablesInclRowHeader ) table.setShowVerticalLines( showVerticalLinesCheckBox.isSelected() ); } private void intercellSpacingChanged() { - for( JTable table : allTables ) + for( JTable table : allTablesInclRowHeader ) table.setIntercellSpacing( intercellSpacingCheckBox.isSelected() ? new Dimension( 1, 1 ) : new Dimension() ); } private void redGridColorChanged() { - for( JTable table : allTables ) + for( JTable table : allTablesInclRowHeader ) table.setGridColor( redGridColorCheckBox.isSelected() ? Color.red : UIManager.getColor( "Table.gridColor" ) ); } + private void rowHeaderChanged() { + if( rowHeaderCheckBox.isSelected() ) { + TestTableRowHeaderModel rowHeaderModel = new TestTableRowHeaderModel( tableModel ); + rowHeaderTable1 = new JTable( rowHeaderModel ); + rowHeaderTable1.setPreferredScrollableViewportSize( UIScale.scale( new Dimension( 50, 50 ) ) ); + rowHeaderTable1.setSelectionModel( table1.getSelectionModel() ); + + DefaultTableCellRenderer rowHeaderRenderer = new DefaultTableCellRenderer(); + rowHeaderRenderer.setHorizontalAlignment( JLabel.CENTER ); + rowHeaderTable1.setDefaultRenderer( Object.class, rowHeaderRenderer ); + table1ScrollPane.setRowHeaderView( rowHeaderTable1 ); + + JViewport headerViewport = new JViewport(); + headerViewport.setView( rowHeaderTable1.getTableHeader() ); + table1ScrollPane.setCorner( ScrollPaneConstants.UPPER_LEADING_CORNER, headerViewport ); + + table1ScrollPane.applyComponentOrientation( getComponentOrientation() ); + + allTablesInclRowHeader.add( rowHeaderTable1 ); + + showHorizontalLinesChanged(); + showVerticalLinesChanged(); + intercellSpacingChanged(); + redGridColorChanged(); + } else { + table1ScrollPane.setRowHeader( null ); + table1ScrollPane.setCorner( ScrollPaneConstants.UPPER_LEADING_CORNER, null ); + allTablesInclRowHeader.remove( rowHeaderTable1 ); + + ((TestTableRowHeaderModel)rowHeaderTable1.getModel()).dispose(); + rowHeaderTable1 = null; + } + } + + @Override + public void applyComponentOrientation( ComponentOrientation o ) { + super.applyComponentOrientation( o ); + + // swap upper right and left corners (other corners are not used in this app) + Component leftCorner = table1ScrollPane.getCorner( ScrollPaneConstants.UPPER_LEFT_CORNER ); + Component rightCorner = table1ScrollPane.getCorner( ScrollPaneConstants.UPPER_RIGHT_CORNER ); + table1ScrollPane.setCorner( ScrollPaneConstants.UPPER_LEFT_CORNER, null ); + table1ScrollPane.setCorner( ScrollPaneConstants.UPPER_RIGHT_CORNER, null ); + table1ScrollPane.setCorner( ScrollPaneConstants.UPPER_LEFT_CORNER, rightCorner ); + table1ScrollPane.setCorner( ScrollPaneConstants.UPPER_RIGHT_CORNER, leftCorner ); + } + @Override public void updateUI() { super.updateUI(); @@ -300,6 +359,7 @@ public class FlatComponents2Test JLabel autoResizeModeLabel = new JLabel(); autoResizeModeField = new JComboBox<>(); showHorizontalLinesCheckBox = new JCheckBox(); + rowHeaderCheckBox = new JCheckBox(); showVerticalLinesCheckBox = new JCheckBox(); intercellSpacingCheckBox = new JCheckBox(); redGridColorCheckBox = new JCheckBox(); @@ -461,7 +521,7 @@ public class FlatComponents2Test panel3.add(tableRowCountLabel, "cell 0 2"); //---- tableRowCountSpinner ---- - tableRowCountSpinner.setModel(new SpinnerNumberModel(20, 0, null, 10)); + tableRowCountSpinner.setModel(new SpinnerNumberModel(20, 0, null, 5)); tableRowCountSpinner.addChangeListener(e -> tableRowCountChanged()); panel3.add(tableRowCountSpinner, "cell 0 3"); } @@ -515,6 +575,11 @@ public class FlatComponents2Test showHorizontalLinesCheckBox.addActionListener(e -> showHorizontalLinesChanged()); tableOptionsPanel.add(showHorizontalLinesCheckBox, "cell 0 1"); + //---- rowHeaderCheckBox ---- + rowHeaderCheckBox.setText("row header"); + rowHeaderCheckBox.addActionListener(e -> rowHeaderChanged()); + tableOptionsPanel.add(rowHeaderCheckBox, "cell 1 1"); + //---- showVerticalLinesCheckBox ---- showVerticalLinesCheckBox.setText("show vertical lines"); showVerticalLinesCheckBox.addActionListener(e -> showVerticalLinesChanged()); @@ -588,6 +653,7 @@ public class FlatComponents2Test private JTable table1; private JComboBox autoResizeModeField; private JCheckBox showHorizontalLinesCheckBox; + private JCheckBox rowHeaderCheckBox; private JCheckBox showVerticalLinesCheckBox; private JCheckBox intercellSpacingCheckBox; private JCheckBox redGridColorCheckBox; @@ -885,4 +951,56 @@ public class FlatComponents2Test fireTableCellUpdated( rowIndex, columnIndex ); } } + + //---- TestTableRowHeaderModel -------------------------------------------- + + private class TestTableRowHeaderModel + extends AbstractTableModel + implements TableModelListener + { + private final TableModel model; + + TestTableRowHeaderModel( TableModel model ) { + this.model = model; + + model.addTableModelListener( this ); + } + + void dispose() { + model.removeTableModelListener( this ); + } + + @Override + public int getRowCount() { + return model.getRowCount(); + } + + @Override + public int getColumnCount() { + return 1; + } + + @Override + public String getColumnName( int columnIndex ) { + return "Row #"; + } + + @Override + public Object getValueAt( int rowIndex, int columnIndex ) { + return rowIndex + 1; + } + + @Override + public void tableChanged( TableModelEvent e ) { + switch( e.getType() ) { + case TableModelEvent.INSERT: + fireTableRowsInserted( e.getFirstRow(), e.getLastRow() ); + break; + + case TableModelEvent.DELETE: + fireTableRowsDeleted( e.getFirstRow(), e.getLastRow() ); + break; + } + } + } } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatComponents2Test.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatComponents2Test.jfd index 9e88c213..48b13c2f 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatComponents2Test.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatComponents2Test.jfd @@ -64,7 +64,7 @@ new FormModel { } ) add( new FormComponent( "javax.swing.JSpinner" ) { name: "listRowCountSpinner" - "model": &SpinnerNumberModel0 new javax.swing.SpinnerNumberModel { + "model": new javax.swing.SpinnerNumberModel { minimum: 0 stepSize: 10 value: 20 @@ -184,7 +184,11 @@ new FormModel { } ) add( new FormComponent( "javax.swing.JSpinner" ) { name: "tableRowCountSpinner" - "model": #SpinnerNumberModel0 + "model": new javax.swing.SpinnerNumberModel { + minimum: 0 + stepSize: 5 + value: 20 + } auxiliary() { "JavaCodeGenerator.variableLocal": false } @@ -251,6 +255,16 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 1" } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "rowHeaderCheckBox" + "text": "row header" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "rowHeaderChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "showVerticalLinesCheckBox" "text": "show vertical lines"