ScrollPane: fixed/improved border painting at 125% - 175% scaling to avoid different border thicknesses (issue #743)

This commit is contained in:
Karl Tauber
2024-06-14 18:13:17 +02:00
parent 2a494b1d60
commit 0c0d4bffbf
8 changed files with 126 additions and 15 deletions

View File

@@ -23,6 +23,8 @@ FlatLaf Change Log
`<big>`, `<small>` and `<samp>` in HTML text for components Button, CheckBox, `<big>`, `<small>` and `<samp>` in HTML text for components Button, CheckBox,
RadioButton, MenuItem (and subclasses), JideLabel, JideButton, JXBusyLabel and RadioButton, MenuItem (and subclasses), JideLabel, JideButton, JXBusyLabel and
JXHyperlink. Also fixed for Label and ToolTip if using Java 11+. JXHyperlink. Also fixed for Label and ToolTip if using Java 11+.
- ScrollPane: Fixed/improved border painting at 125% - 175% scaling to avoid
different border thicknesses. (issue #743)
- Table: Fixed painting of alternating rows below table if auto-resize mode is - Table: Fixed painting of alternating rows below table if auto-resize mode is
`JTable.AUTO_RESIZE_OFF` and table width is smaller than scroll pane (was not `JTable.AUTO_RESIZE_OFF` and table width is smaller than scroll pane (was not
updated when table width changed and was painted on wrong side in updated when table width changed and was painted on wrong side in

View File

@@ -135,7 +135,7 @@ public class FlatBorder
Paint borderColor = (outlineColor != null) ? outlineColor : getBorderColor( c ); Paint borderColor = (outlineColor != null) ? outlineColor : getBorderColor( c );
FlatUIUtils.paintOutlinedComponent( g2, x, y, width, height, FlatUIUtils.paintOutlinedComponent( g2, x, y, width, height,
focusWidth, 1, focusInnerWidth, borderWidth, arc, focusWidth, 1, focusInnerWidth, borderWidth, arc,
focusColor, borderColor, null ); focusColor, borderColor, null, c instanceof JScrollPane );
} finally { } finally {
g2.dispose(); g2.dispose();
} }

View File

@@ -422,7 +422,7 @@ debug*/
Color thumbColor, Color thumbBorderColor, Color focusedColor, float thumbBorderWidth, int focusWidth ) Color thumbColor, Color thumbBorderColor, Color focusedColor, float thumbBorderWidth, int focusWidth )
{ {
double systemScaleFactor = UIScale.getSystemScaleFactor( (Graphics2D) g ); double systemScaleFactor = UIScale.getSystemScaleFactor( (Graphics2D) g );
if( systemScaleFactor != 1 && systemScaleFactor != 2 ) { if( systemScaleFactor != (int) systemScaleFactor ) {
// paint at scale 1x to avoid clipping on right and bottom edges at 125%, 150% or 175% // paint at scale 1x to avoid clipping on right and bottom edges at 125%, 150% or 175%
HiDPIUtils.paintAtScale1x( (Graphics2D) g, thumbRect.x, thumbRect.y, thumbRect.width, thumbRect.height, HiDPIUtils.paintAtScale1x( (Graphics2D) g, thumbRect.x, thumbRect.y, thumbRect.width, thumbRect.height,
(g2d, x2, y2, width2, height2, scaleFactor) -> { (g2d, x2, y2, width2, height2, scaleFactor) -> {

View File

@@ -601,28 +601,55 @@ public class FlatUIUtils
public static void paintOutlinedComponent( Graphics2D g, int x, int y, int width, int height, public static void paintOutlinedComponent( Graphics2D g, int x, int y, int width, int height,
float focusWidth, float focusWidthFraction, float focusInnerWidth, float borderWidth, float arc, float focusWidth, float focusWidthFraction, float focusInnerWidth, float borderWidth, float arc,
Paint focusColor, Paint borderColor, Paint background ) Paint focusColor, Paint borderColor, Paint background )
{
paintOutlinedComponent( g, x, y, width, height, focusWidth, focusWidthFraction, focusInnerWidth,
borderWidth, arc, focusColor, borderColor, background, false );
}
static void paintOutlinedComponent( Graphics2D g, int x, int y, int width, int height,
float focusWidth, float focusWidthFraction, float focusInnerWidth, float borderWidth, float arc,
Paint focusColor, Paint borderColor, Paint background, boolean scrollPane )
{ {
double systemScaleFactor = UIScale.getSystemScaleFactor( g ); double systemScaleFactor = UIScale.getSystemScaleFactor( g );
if( systemScaleFactor != 1 && systemScaleFactor != 2 ) { if( (int) systemScaleFactor != systemScaleFactor ) {
// paint at scale 1x to avoid clipping on right and bottom edges at 125%, 150% or 175% // paint at scale 1x to avoid clipping on right and bottom edges at 125%, 150% or 175%
HiDPIUtils.paintAtScale1x( g, x, y, width, height, HiDPIUtils.paintAtScale1x( g, x, y, width, height,
(g2d, x2, y2, width2, height2, scaleFactor) -> { (g2d, x2, y2, width2, height2, scaleFactor) -> {
paintOutlinedComponentImpl( g2d, x2, y2, width2, height2, paintOutlinedComponentImpl( g2d, x2, y2, width2, height2,
(float) (focusWidth * scaleFactor), focusWidthFraction, (float) (focusInnerWidth * scaleFactor), (float) (focusWidth * scaleFactor), focusWidthFraction, (float) (focusInnerWidth * scaleFactor),
(float) (borderWidth * scaleFactor), (float) (arc * scaleFactor), (float) (borderWidth * scaleFactor), (float) (arc * scaleFactor),
focusColor, borderColor, background ); focusColor, borderColor, background, scrollPane, scaleFactor );
} ); } );
return; return;
} }
paintOutlinedComponentImpl( g, x, y, width, height, focusWidth, focusWidthFraction, focusInnerWidth, paintOutlinedComponentImpl( g, x, y, width, height, focusWidth, focusWidthFraction, focusInnerWidth,
borderWidth, arc, focusColor, borderColor, background ); borderWidth, arc, focusColor, borderColor, background, scrollPane, systemScaleFactor );
} }
@SuppressWarnings( "SelfAssignment" ) // Error Prone
private static void paintOutlinedComponentImpl( Graphics2D g, int x, int y, int width, int height, private static void paintOutlinedComponentImpl( Graphics2D g, int x, int y, int width, int height,
float focusWidth, float focusWidthFraction, float focusInnerWidth, float borderWidth, float arc, float focusWidth, float focusWidthFraction, float focusInnerWidth, float borderWidth, float arc,
Paint focusColor, Paint borderColor, Paint background ) Paint focusColor, Paint borderColor, Paint background, boolean scrollPane, double scaleFactor )
{ {
// Special handling for scrollpane and fractional scale factors (e.g. 1.25 - 1.75),
// where Swing scales one "logical" pixel (border insets) to either one or two physical pixels.
// Antialiasing is used to paint the border, which usually needs two physical pixels
// at small scale factors. 1px for the solid border and another 1px for antialiasing.
// But scrollpane view is painted over the border, which results in a painted border
// that is 1px thick at some sides and 2px thick at other sides.
if( scrollPane && scaleFactor != (int) scaleFactor ) {
if( focusWidth > 0 ) {
// reduce outer border thickness (focusWidth) so that inner side of
// component border (focusWidth + borderWidth) is at a full pixel
int totalWidth = (int) (focusWidth + borderWidth);
focusWidth = totalWidth - borderWidth;
} else {// if( scaleFactor > 1 && scaleFactor < 2 ) {
// reduce component border thickness (borderWidth) to full pixels
borderWidth = (int) borderWidth;
}
}
// outside bounds of the border and the background // outside bounds of the border and the background
float x1 = x + focusWidth; float x1 = x + focusWidth;
float y1 = y + focusWidth; float y1 = y + focusWidth;
@@ -780,7 +807,7 @@ public class FlatUIUtils
if( arcTopLeft > 0 || arcTopRight > 0 || arcBottomLeft > 0 || arcBottomRight > 0 ) { if( arcTopLeft > 0 || arcTopRight > 0 || arcBottomLeft > 0 || arcBottomRight > 0 ) {
double systemScaleFactor = UIScale.getSystemScaleFactor( g ); double systemScaleFactor = UIScale.getSystemScaleFactor( g );
if( systemScaleFactor != 1 && systemScaleFactor != 2 ) { if( systemScaleFactor != (int) systemScaleFactor ) {
// paint at scale 1x to avoid clipping on right and bottom edges at 125%, 150% or 175% // paint at scale 1x to avoid clipping on right and bottom edges at 125%, 150% or 175%
HiDPIUtils.paintAtScale1x( g, x, y, width, height, HiDPIUtils.paintAtScale1x( g, x, y, width, height,
(g2d, x2, y2, width2, height2, scaleFactor) -> { (g2d, x2, y2, width2, height2, scaleFactor) -> {

View File

@@ -40,10 +40,15 @@ public class FlatPaintingHiDPITest
FlatPaintingHiDPITest() { FlatPaintingHiDPITest() {
initComponents(); initComponents();
reset();
sliderChanged(); sliderChanged();
} }
@Override
public void addNotify() {
super.addNotify();
reset();
}
private void sliderChanged() { private void sliderChanged() {
painter.originX = originXSlider.getValue(); painter.originX = originXSlider.getValue();
painter.originY = originYSlider.getValue(); painter.originY = originYSlider.getValue();
@@ -212,7 +217,7 @@ public class FlatPaintingHiDPITest
scaleXSlider.setPaintTicks(true); scaleXSlider.setPaintTicks(true);
scaleXSlider.setMajorTickSpacing(50); scaleXSlider.setMajorTickSpacing(50);
scaleXSlider.setSnapToTicks(true); scaleXSlider.setSnapToTicks(true);
scaleXSlider.setMinorTickSpacing(10); scaleXSlider.setMinorTickSpacing(5);
scaleXSlider.setMinimum(-100); scaleXSlider.setMinimum(-100);
scaleXSlider.addChangeListener(e -> sliderChanged()); scaleXSlider.addChangeListener(e -> sliderChanged());
add(scaleXSlider, "cell 1 4"); add(scaleXSlider, "cell 1 4");
@@ -228,7 +233,7 @@ public class FlatPaintingHiDPITest
scaleYSlider.setPaintLabels(true); scaleYSlider.setPaintLabels(true);
scaleYSlider.setMajorTickSpacing(50); scaleYSlider.setMajorTickSpacing(50);
scaleYSlider.setSnapToTicks(true); scaleYSlider.setSnapToTicks(true);
scaleYSlider.setMinorTickSpacing(10); scaleYSlider.setMinorTickSpacing(5);
scaleYSlider.setMinimum(-100); scaleYSlider.setMinimum(-100);
scaleYSlider.addChangeListener(e -> sliderChanged()); scaleYSlider.addChangeListener(e -> sliderChanged());
add(scaleYSlider, "cell 1 5"); add(scaleYSlider, "cell 1 5");

View File

@@ -1,4 +1,4 @@
JFDML JFormDesigner: "8.0.0.0.122" Java: "17.0.2" encoding: "UTF-8" JFDML JFormDesigner: "8.2.3.0.386" Java: "21" encoding: "UTF-8"
new FormModel { new FormModel {
contentType: "form/swing" contentType: "form/swing"
@@ -108,7 +108,7 @@ new FormModel {
"paintTicks": true "paintTicks": true
"majorTickSpacing": 50 "majorTickSpacing": 50
"snapToTicks": true "snapToTicks": true
"minorTickSpacing": 10 "minorTickSpacing": 5
"minimum": -100 "minimum": -100
auxiliary() { auxiliary() {
"JavaCodeGenerator.variableLocal": false "JavaCodeGenerator.variableLocal": false
@@ -131,7 +131,7 @@ new FormModel {
"paintLabels": true "paintLabels": true
"majorTickSpacing": 50 "majorTickSpacing": 50
"snapToTicks": true "snapToTicks": true
"minorTickSpacing": 10 "minorTickSpacing": 5
"minimum": -100 "minimum": -100
auxiliary() { auxiliary() {
"JavaCodeGenerator.variableLocal": false "JavaCodeGenerator.variableLocal": false

View File

@@ -17,14 +17,19 @@
package com.formdev.flatlaf.testing; package com.formdev.flatlaf.testing;
import java.awt.Color; import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension; import java.awt.Dimension;
import java.awt.EventQueue; import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.Border; import javax.swing.border.Border;
import javax.swing.border.CompoundBorder; import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.MatteBorder; import javax.swing.border.MatteBorder;
import javax.swing.table.AbstractTableModel; import javax.swing.table.AbstractTableModel;
import javax.swing.tree.*; import javax.swing.tree.*;
@@ -227,6 +232,38 @@ public class FlatRoundedScrollPaneTest
} }
} }
private void emptyViewportChanged() {
boolean empty = emptyViewportCheckBox.isSelected();
for( JScrollPane scrollPane : allJScrollPanes ) {
JViewport viewport = scrollPane.getViewport();
Component view = viewport.getView();
if( empty ) {
scrollPane.putClientProperty( getClass().getName(), view );
JComponent emptyView = new JComponent() {
};
emptyView.setBorder( new EmptyViewBorder() );
emptyView.setFocusable( true );
emptyView.addMouseListener( new MouseAdapter() {
@Override
public void mousePressed( MouseEvent e ) {
emptyView.requestFocusInWindow();
}
} );
viewport.setView( emptyView );
} else {
Object oldView = scrollPane.getClientProperty( getClass().getName() );
scrollPane.putClientProperty( getClass().getName(), null );
if( oldView instanceof Component )
viewport.setView( (Component) oldView );
else
viewport.setView( null );
}
viewport.setOpaque( !empty );
scrollPane.revalidate();
scrollPane.repaint();
}
}
private void initComponents() { private void initComponents() {
// JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
splitPane2 = new JSplitPane(); splitPane2 = new JSplitPane();
@@ -265,6 +302,7 @@ public class FlatRoundedScrollPaneTest
viewportBorderCheckBox = new JCheckBox(); viewportBorderCheckBox = new JCheckBox();
rowHeaderCheckBox = new JCheckBox(); rowHeaderCheckBox = new JCheckBox();
verticalScrollBarCheckBox = new JCheckBox(); verticalScrollBarCheckBox = new JCheckBox();
emptyViewportCheckBox = new JCheckBox();
//======== this ======== //======== this ========
setLayout(new MigLayout( setLayout(new MigLayout(
@@ -420,6 +458,7 @@ public class FlatRoundedScrollPaneTest
"[]", "[]",
// rows // rows
"[]" + "[]" +
"[]" +
"[]")); "[]"));
//---- arcLabel ---- //---- arcLabel ----
@@ -468,6 +507,11 @@ public class FlatRoundedScrollPaneTest
verticalScrollBarCheckBox.setSelected(true); verticalScrollBarCheckBox.setSelected(true);
verticalScrollBarCheckBox.addActionListener(e -> verticalScrollBarChanged()); verticalScrollBarCheckBox.addActionListener(e -> verticalScrollBarChanged());
panel3.add(verticalScrollBarCheckBox, "cell 4 1"); panel3.add(verticalScrollBarCheckBox, "cell 4 1");
//---- emptyViewportCheckBox ----
emptyViewportCheckBox.setText("Empty viewport");
emptyViewportCheckBox.addActionListener(e -> emptyViewportChanged());
panel3.add(emptyViewportCheckBox, "cell 2 2");
} }
add(panel3, "cell 0 1"); add(panel3, "cell 0 1");
// JFormDesigner - End of component initialization //GEN-END:initComponents // JFormDesigner - End of component initialization //GEN-END:initComponents
@@ -509,6 +553,7 @@ public class FlatRoundedScrollPaneTest
private JCheckBox viewportBorderCheckBox; private JCheckBox viewportBorderCheckBox;
private JCheckBox rowHeaderCheckBox; private JCheckBox rowHeaderCheckBox;
private JCheckBox verticalScrollBarCheckBox; private JCheckBox verticalScrollBarCheckBox;
private JCheckBox emptyViewportCheckBox;
// JFormDesigner - End of variables declaration //GEN-END:variables // JFormDesigner - End of variables declaration //GEN-END:variables
//---- class Corner ------------------------------------------------------- //---- class Corner -------------------------------------------------------
@@ -525,4 +570,29 @@ public class FlatRoundedScrollPaneTest
// do not change background when checkbox "explicit colors" is selected // do not change background when checkbox "explicit colors" is selected
} }
} }
//---- class EmptyViewBorder ----------------------------------------------
private static class EmptyViewBorder
extends EmptyBorder
{
public EmptyViewBorder() {
super( 0, 0, 0, 0 );
}
@Override
public void paintBorder( Component c, Graphics g, int x, int y, int width, int height ) {
g.setColor( Color.red );
int x2 = x + width - 1;
int y2 = y + height - 1;
for( int px = x; px <= x2; px += 4 ) {
g.fillRect( px, y, 1, 1 );
g.fillRect( px, y2, 1, 1 );
}
for( int py = y; py <= y2; py += 4 ) {
g.fillRect( x, py, 1, 1 );
g.fillRect( x2, py, 1, 1 );
}
}
}
} }

View File

@@ -1,4 +1,4 @@
JFDML JFormDesigner: "8.1.1.0.298" Java: "19.0.2" encoding: "UTF-8" JFDML JFormDesigner: "8.2.3.0.386" Java: "21" encoding: "UTF-8"
new FormModel { new FormModel {
contentType: "form/swing" contentType: "form/swing"
@@ -169,7 +169,7 @@ new FormModel {
add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
"$layoutConstraints": "hidemode 3" "$layoutConstraints": "hidemode 3"
"$columnConstraints": "[fill][grow,fill]para[][][]" "$columnConstraints": "[fill][grow,fill]para[][][]"
"$rowConstraints": "[][]" "$rowConstraints": "[][][]"
} ) { } ) {
name: "panel3" name: "panel3"
add( new FormComponent( "javax.swing.JLabel" ) { add( new FormComponent( "javax.swing.JLabel" ) {
@@ -238,6 +238,13 @@ new FormModel {
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 4 1" "value": "cell 4 1"
} ) } )
add( new FormComponent( "javax.swing.JCheckBox" ) {
name: "emptyViewportCheckBox"
"text": "Empty viewport"
addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "emptyViewportChanged", false ) )
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 2 2"
} )
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 1" "value": "cell 0 1"
} ) } )