diff --git a/CHANGELOG.md b/CHANGELOG.md index a6a367a8..20c63384 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ FlatLaf Change Log ``, `` and `` in HTML text for components Button, CheckBox, RadioButton, MenuItem (and subclasses), JideLabel, JideButton, JXBusyLabel and 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 `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 diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatBorder.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatBorder.java index 7af299d7..6afcd0ee 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatBorder.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatBorder.java @@ -135,7 +135,7 @@ public class FlatBorder Paint borderColor = (outlineColor != null) ? outlineColor : getBorderColor( c ); FlatUIUtils.paintOutlinedComponent( g2, x, y, width, height, focusWidth, 1, focusInnerWidth, borderWidth, arc, - focusColor, borderColor, null ); + focusColor, borderColor, null, c instanceof JScrollPane ); } finally { g2.dispose(); } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatSliderUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatSliderUI.java index 790b5693..c0a0c49b 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatSliderUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatSliderUI.java @@ -422,7 +422,7 @@ debug*/ Color thumbColor, Color thumbBorderColor, Color focusedColor, float thumbBorderWidth, int focusWidth ) { 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% HiDPIUtils.paintAtScale1x( (Graphics2D) g, thumbRect.x, thumbRect.y, thumbRect.width, thumbRect.height, (g2d, x2, y2, width2, height2, scaleFactor) -> { diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatUIUtils.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatUIUtils.java index 17707c76..115185ca 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatUIUtils.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatUIUtils.java @@ -601,28 +601,55 @@ public class FlatUIUtils public 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 ) + { + 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 ); - 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% HiDPIUtils.paintAtScale1x( g, x, y, width, height, (g2d, x2, y2, width2, height2, scaleFactor) -> { paintOutlinedComponentImpl( g2d, x2, y2, width2, height2, (float) (focusWidth * scaleFactor), focusWidthFraction, (float) (focusInnerWidth * scaleFactor), (float) (borderWidth * scaleFactor), (float) (arc * scaleFactor), - focusColor, borderColor, background ); + focusColor, borderColor, background, scrollPane, scaleFactor ); } ); return; } 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, 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 float x1 = x + focusWidth; float y1 = y + focusWidth; @@ -780,7 +807,7 @@ public class FlatUIUtils if( arcTopLeft > 0 || arcTopRight > 0 || arcBottomLeft > 0 || arcBottomRight > 0 ) { 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% HiDPIUtils.paintAtScale1x( g, x, y, width, height, (g2d, x2, y2, width2, height2, scaleFactor) -> { diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatPaintingHiDPITest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatPaintingHiDPITest.java index deb0b9d9..d0e5eaaa 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatPaintingHiDPITest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatPaintingHiDPITest.java @@ -40,10 +40,15 @@ public class FlatPaintingHiDPITest FlatPaintingHiDPITest() { initComponents(); - reset(); sliderChanged(); } + @Override + public void addNotify() { + super.addNotify(); + reset(); + } + private void sliderChanged() { painter.originX = originXSlider.getValue(); painter.originY = originYSlider.getValue(); @@ -212,7 +217,7 @@ public class FlatPaintingHiDPITest scaleXSlider.setPaintTicks(true); scaleXSlider.setMajorTickSpacing(50); scaleXSlider.setSnapToTicks(true); - scaleXSlider.setMinorTickSpacing(10); + scaleXSlider.setMinorTickSpacing(5); scaleXSlider.setMinimum(-100); scaleXSlider.addChangeListener(e -> sliderChanged()); add(scaleXSlider, "cell 1 4"); @@ -228,7 +233,7 @@ public class FlatPaintingHiDPITest scaleYSlider.setPaintLabels(true); scaleYSlider.setMajorTickSpacing(50); scaleYSlider.setSnapToTicks(true); - scaleYSlider.setMinorTickSpacing(10); + scaleYSlider.setMinorTickSpacing(5); scaleYSlider.setMinimum(-100); scaleYSlider.addChangeListener(e -> sliderChanged()); add(scaleYSlider, "cell 1 5"); diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatPaintingHiDPITest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatPaintingHiDPITest.jfd index 2e989af3..8e9d773f 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatPaintingHiDPITest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatPaintingHiDPITest.jfd @@ -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 { contentType: "form/swing" @@ -108,7 +108,7 @@ new FormModel { "paintTicks": true "majorTickSpacing": 50 "snapToTicks": true - "minorTickSpacing": 10 + "minorTickSpacing": 5 "minimum": -100 auxiliary() { "JavaCodeGenerator.variableLocal": false @@ -131,7 +131,7 @@ new FormModel { "paintLabels": true "majorTickSpacing": 50 "snapToTicks": true - "minorTickSpacing": 10 + "minorTickSpacing": 5 "minimum": -100 auxiliary() { "JavaCodeGenerator.variableLocal": false diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatRoundedScrollPaneTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatRoundedScrollPaneTest.java index ebea0428..221a5bde 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatRoundedScrollPaneTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatRoundedScrollPaneTest.java @@ -17,14 +17,19 @@ package com.formdev.flatlaf.testing; import java.awt.Color; +import java.awt.Component; import java.awt.Dimension; 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.Arrays; import java.util.stream.Collectors; import javax.swing.*; import javax.swing.border.Border; import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; import javax.swing.border.MatteBorder; import javax.swing.table.AbstractTableModel; 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() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents splitPane2 = new JSplitPane(); @@ -265,6 +302,7 @@ public class FlatRoundedScrollPaneTest viewportBorderCheckBox = new JCheckBox(); rowHeaderCheckBox = new JCheckBox(); verticalScrollBarCheckBox = new JCheckBox(); + emptyViewportCheckBox = new JCheckBox(); //======== this ======== setLayout(new MigLayout( @@ -420,6 +458,7 @@ public class FlatRoundedScrollPaneTest "[]", // rows "[]" + + "[]" + "[]")); //---- arcLabel ---- @@ -468,6 +507,11 @@ public class FlatRoundedScrollPaneTest verticalScrollBarCheckBox.setSelected(true); verticalScrollBarCheckBox.addActionListener(e -> verticalScrollBarChanged()); 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"); // JFormDesigner - End of component initialization //GEN-END:initComponents @@ -509,6 +553,7 @@ public class FlatRoundedScrollPaneTest private JCheckBox viewportBorderCheckBox; private JCheckBox rowHeaderCheckBox; private JCheckBox verticalScrollBarCheckBox; + private JCheckBox emptyViewportCheckBox; // JFormDesigner - End of variables declaration //GEN-END:variables //---- class Corner ------------------------------------------------------- @@ -525,4 +570,29 @@ public class FlatRoundedScrollPaneTest // 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 ); + } + } + } } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatRoundedScrollPaneTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatRoundedScrollPaneTest.jfd index ffaf2a6a..db0669f6 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatRoundedScrollPaneTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatRoundedScrollPaneTest.jfd @@ -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 { contentType: "form/swing" @@ -169,7 +169,7 @@ new FormModel { add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "hidemode 3" "$columnConstraints": "[fill][grow,fill]para[][][]" - "$rowConstraints": "[][]" + "$rowConstraints": "[][][]" } ) { name: "panel3" add( new FormComponent( "javax.swing.JLabel" ) { @@ -238,6 +238,13 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "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 ) { "value": "cell 0 1" } )