From 5ff99bd45e24fad241ae8c6d944a0c65a3849de3 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 5 Jul 2024 22:18:27 +0200 Subject: [PATCH] HiDPI: fixed incomplete component paintings at 125% or 175% scaling on Windows (issues #860 and #582) --- CHANGELOG.md | 3 + .../com/formdev/flatlaf/ui/FlatButtonUI.java | 22 +- .../formdev/flatlaf/ui/FlatComboBoxUI.java | 17 +- .../formdev/flatlaf/ui/FlatEditorPaneUI.java | 2 +- .../com/formdev/flatlaf/ui/FlatLabelUI.java | 2 +- .../com/formdev/flatlaf/ui/FlatListUI.java | 5 +- .../com/formdev/flatlaf/ui/FlatMenuUI.java | 3 +- .../com/formdev/flatlaf/ui/FlatPanelUI.java | 3 +- .../flatlaf/ui/FlatPasswordFieldUI.java | 5 +- .../formdev/flatlaf/ui/FlatProgressBarUI.java | 6 +- .../formdev/flatlaf/ui/FlatRadioButtonUI.java | 3 +- .../formdev/flatlaf/ui/FlatScrollBarUI.java | 7 +- .../formdev/flatlaf/ui/FlatScrollPaneUI.java | 15 +- .../formdev/flatlaf/ui/FlatSeparatorUI.java | 3 +- .../com/formdev/flatlaf/ui/FlatSliderUI.java | 40 +- .../com/formdev/flatlaf/ui/FlatSpinnerUI.java | 9 +- .../flatlaf/ui/FlatStylingSupport.java | 3 +- .../formdev/flatlaf/ui/FlatTabbedPaneUI.java | 21 +- .../formdev/flatlaf/ui/FlatTableHeaderUI.java | 5 +- .../com/formdev/flatlaf/ui/FlatTableUI.java | 7 +- .../formdev/flatlaf/ui/FlatTextFieldUI.java | 16 +- .../flatlaf/ui/FlatToggleButtonUI.java | 5 +- .../flatlaf/ui/FlatToolBarSeparatorUI.java | 3 +- .../com/formdev/flatlaf/ui/FlatToolBarUI.java | 3 +- .../com/formdev/flatlaf/ui/FlatTreeUI.java | 11 +- .../com/formdev/flatlaf/ui/FlatUIUtils.java | 4 +- .../com/formdev/flatlaf/util/HiDPIUtils.java | 162 ++++++++ .../flatlaf/testing/FlatHiDPITest.java | 386 ++++++++++++++++++ 28 files changed, 686 insertions(+), 85 deletions(-) create mode 100644 flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatHiDPITest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b9e2745..301c8f83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,9 @@ FlatLaf Change Log - Theme Editor: Fixed occasional empty window on startup on macOS. - FlatLaf window decorations: Fixed black line sometimes painted on top of (native) window border on Windows 11. (issue #852) +- HiDPI: Fixed incomplete component paintings at 125% or 175% scaling on Windows + where sometimes a 1px wide area at the right or bottom component edge is not + repainted. E.g. ScrollPane focus indicator border. (issues #860 and #582) #### Incompatibilities diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatButtonUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatButtonUI.java index 20101854..41691c1b 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatButtonUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatButtonUI.java @@ -29,6 +29,7 @@ import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Rectangle; +import java.awt.event.FocusEvent; import java.awt.geom.RoundRectangle2D; import java.beans.PropertyChangeEvent; import java.util.Map; @@ -61,6 +62,7 @@ import com.formdev.flatlaf.icons.FlatHelpButtonIcon; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; import com.formdev.flatlaf.ui.FlatStylingSupport.UnknownStyleException; +import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.LoggingFacade; import com.formdev.flatlaf.util.UIScale; @@ -312,11 +314,11 @@ public class FlatButtonUI case BUTTON_TYPE: b.revalidate(); - b.repaint(); + HiDPIUtils.repaint( b ); break; case OUTLINE: - b.repaint(); + HiDPIUtils.repaint( b ); break; case STYLE: @@ -328,7 +330,7 @@ public class FlatButtonUI } else installStyle( b ); b.revalidate(); - b.repaint(); + HiDPIUtils.repaint( b ); break; } } @@ -915,7 +917,7 @@ public class FlatButtonUI @Override public void stateChanged( ChangeEvent e ) { - super.stateChanged( e ); + HiDPIUtils.repaint( b ); // if button is in toolbar, repaint button groups AbstractButton b = (AbstractButton) e.getSource(); @@ -927,5 +929,17 @@ public class FlatButtonUI ((FlatToolBarUI)ui).repaintButtonGroup( b ); } } + + @Override + public void focusGained( FocusEvent e ) { + super.focusGained( e ); + HiDPIUtils.repaint( b ); + } + + @Override + public void focusLost( FocusEvent e ) { + super.focusLost( e ); + HiDPIUtils.repaint( b ); + } } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatComboBoxUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatComboBoxUI.java index 0d5039cd..863e5c0e 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatComboBoxUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatComboBoxUI.java @@ -78,6 +78,7 @@ import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableField; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableLookupProvider; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; +import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.LoggingFacade; import com.formdev.flatlaf.util.SystemInfo; @@ -220,7 +221,7 @@ public class FlatComboBoxUI private void repaintArrowButton() { if( arrowButton != null && !comboBox.isEditable() ) - arrowButton.repaint(); + HiDPIUtils.repaint( arrowButton ); } }; comboBox.addMouseListener( hoverListener ); @@ -351,15 +352,15 @@ public class FlatComboBoxUI @Override public void focusGained( FocusEvent e ) { super.focusGained( e ); - if( comboBox != null && comboBox.isEditable() ) - comboBox.repaint(); + if( comboBox != null ) + HiDPIUtils.repaint( comboBox ); } @Override public void focusLost( FocusEvent e ) { super.focusLost( e ); - if( comboBox != null && comboBox.isEditable() ) - comboBox.repaint(); + if( comboBox != null ) + HiDPIUtils.repaint( comboBox ); } }; } @@ -386,12 +387,12 @@ public class FlatComboBoxUI switch( propertyName ) { case PLACEHOLDER_TEXT: if( editor != null ) - editor.repaint(); + HiDPIUtils.repaint( editor ); break; case COMPONENT_ROUND_RECT: case OUTLINE: - comboBox.repaint(); + HiDPIUtils.repaint( comboBox ); break; case MINIMUM_WIDTH: @@ -402,7 +403,7 @@ public class FlatComboBoxUI case STYLE_CLASS: installStyle(); comboBox.revalidate(); - comboBox.repaint(); + HiDPIUtils.repaint( comboBox ); break; } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatEditorPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatEditorPaneUI.java index 2614ddee..cd94679c 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatEditorPaneUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatEditorPaneUI.java @@ -171,7 +171,7 @@ public class FlatEditorPaneUI case FlatClientProperties.STYLE_CLASS: installStyle.run(); c.revalidate(); - c.repaint(); + HiDPIUtils.repaint( c ); break; } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatLabelUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatLabelUI.java index 96fe1212..d898333d 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatLabelUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatLabelUI.java @@ -124,7 +124,7 @@ public class FlatLabelUI } else installStyle( label ); label.revalidate(); - label.repaint(); + HiDPIUtils.repaint( label ); } super.propertyChange( e ); diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatListUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatListUI.java index 57635398..00c18241 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatListUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatListUI.java @@ -43,6 +43,7 @@ import com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; import com.formdev.flatlaf.util.Graphics2DProxy; +import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.LoggingFacade; import com.formdev.flatlaf.util.UIScale; @@ -182,7 +183,7 @@ public class FlatListUI case FlatClientProperties.STYLE_CLASS: installStyle(); list.revalidate(); - list.repaint(); + HiDPIUtils.repaint( list ); break; } }; @@ -205,7 +206,7 @@ public class FlatListUI Rectangle r = getCellBounds( list, firstIndex, lastIndex ); if( r != null ) { int arc = (int) Math.ceil( UIScale.scale( selectionArc / 2f ) ); - list.repaint( r.x - arc, r.y - arc, r.width + (arc * 2), r.height + (arc * 2) ); + HiDPIUtils.repaint( list, r.x - arc, r.y - arc, r.width + (arc * 2), r.height + (arc * 2) ); } } }; diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatMenuUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatMenuUI.java index 2b7a8c5c..a3015341 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatMenuUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatMenuUI.java @@ -47,6 +47,7 @@ import javax.swing.plaf.basic.BasicMenuUI; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableField; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableLookupProvider; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; +import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.LoggingFacade; /** @@ -167,7 +168,7 @@ public class FlatMenuUI JMenu menu = (JMenu) e.getSource(); if( menu.isTopLevelMenu() && menu.isRolloverEnabled() ) { menu.getModel().setRollover( rollover ); - menu.repaint(); + HiDPIUtils.repaint( menu ); } } }; diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatPanelUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatPanelUI.java index 5b3cc55d..e1d311e8 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatPanelUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatPanelUI.java @@ -31,6 +31,7 @@ import javax.swing.plaf.basic.BasicPanelUI; import com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; +import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.LoggingFacade; import com.formdev.flatlaf.util.UIScale; @@ -111,7 +112,7 @@ public class FlatPanelUI } else installStyle( c ); c.revalidate(); - c.repaint(); + HiDPIUtils.repaint( c ); break; case FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_PLACEHOLDER: diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatPasswordFieldUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatPasswordFieldUI.java index ad0b5538..cf35ed96 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatPasswordFieldUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatPasswordFieldUI.java @@ -43,6 +43,7 @@ import javax.swing.text.View; import com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.icons.FlatCapsLockIcon; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; +import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.UIScale; /** @@ -163,7 +164,7 @@ public class FlatPasswordFieldUI } private void repaint( KeyEvent e ) { if( e.getKeyCode() == KeyEvent.VK_CAPS_LOCK ) { - e.getComponent().repaint(); + HiDPIUtils.repaint( e.getComponent() ); scrollCaretToVisible(); } } @@ -326,7 +327,7 @@ public class FlatPasswordFieldUI if( visible != revealButton.isVisible() ) { revealButton.setVisible( visible ); c.revalidate(); - c.repaint(); + HiDPIUtils.repaint( c ); if( !visible ) { revealButton.setSelected( false ); diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatProgressBarUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatProgressBarUI.java index 6e22376d..00940992 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatProgressBarUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatProgressBarUI.java @@ -133,14 +133,14 @@ public class FlatProgressBarUI case PROGRESS_BAR_LARGE_HEIGHT: case PROGRESS_BAR_SQUARE: progressBar.revalidate(); - progressBar.repaint(); + HiDPIUtils.repaint( progressBar ); break; case STYLE: case STYLE_CLASS: installStyle(); progressBar.revalidate(); - progressBar.repaint(); + HiDPIUtils.repaint( progressBar ); break; } }; @@ -294,6 +294,6 @@ public class FlatProgressBarUI // Only solution is to repaint whole progress bar. double systemScaleFactor = UIScale.getSystemScaleFactor( progressBar.getGraphicsConfiguration() ); if( (int) systemScaleFactor != systemScaleFactor ) - progressBar.repaint(); + HiDPIUtils.repaint( progressBar ); } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatRadioButtonUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatRadioButtonUI.java index 155c9fa1..73706178 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatRadioButtonUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatRadioButtonUI.java @@ -47,6 +47,7 @@ import com.formdev.flatlaf.icons.FlatCheckBoxIcon; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; import com.formdev.flatlaf.ui.FlatStylingSupport.UnknownStyleException; +import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.LoggingFacade; import com.formdev.flatlaf.util.UIScale; @@ -173,7 +174,7 @@ public class FlatRadioButtonUI } else installStyle( b ); b.revalidate(); - b.repaint(); + HiDPIUtils.repaint( b ); break; } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java index 6f6becc0..8e85860c 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java @@ -43,6 +43,7 @@ import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableField; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableLookupProvider; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; +import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.LoggingFacade; import com.formdev.flatlaf.util.SystemInfo; import com.formdev.flatlaf.util.UIScale; @@ -212,14 +213,14 @@ public class FlatScrollBarUI switch( e.getPropertyName() ) { case FlatClientProperties.SCROLL_BAR_SHOW_BUTTONS: scrollbar.revalidate(); - scrollbar.repaint(); + HiDPIUtils.repaint( scrollbar ); break; case FlatClientProperties.STYLE: case FlatClientProperties.STYLE_CLASS: installStyle(); scrollbar.revalidate(); - scrollbar.repaint(); + HiDPIUtils.repaint( scrollbar ); break; case "componentOrientation": @@ -492,7 +493,7 @@ public class FlatScrollBarUI private void repaint() { if( scrollbar.isEnabled() ) - scrollbar.repaint(); + HiDPIUtils.repaint( scrollbar ); } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java index f563af65..d851b23b 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java @@ -55,6 +55,7 @@ import javax.swing.plaf.basic.BasicScrollPaneUI; import com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; +import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.LoggingFacade; import com.formdev.flatlaf.util.UIScale; @@ -297,11 +298,11 @@ public class FlatScrollPaneUI JScrollBar hsb = scrollpane.getHorizontalScrollBar(); if( vsb != null ) { vsb.revalidate(); - vsb.repaint(); + HiDPIUtils.repaint( vsb ); } if( hsb != null ) { hsb.revalidate(); - hsb.repaint(); + HiDPIUtils.repaint( hsb ); } break; @@ -321,14 +322,14 @@ public class FlatScrollPaneUI break; case FlatClientProperties.OUTLINE: - scrollpane.repaint(); + HiDPIUtils.repaint( scrollpane ); break; case FlatClientProperties.STYLE: case FlatClientProperties.STYLE_CLASS: installStyle(); scrollpane.revalidate(); - scrollpane.repaint(); + HiDPIUtils.repaint( scrollpane ); break; case "border": @@ -339,7 +340,7 @@ public class FlatScrollPaneUI borderShared = null; installStyle(); scrollpane.revalidate(); - scrollpane.repaint(); + HiDPIUtils.repaint( scrollpane ); } break; } @@ -538,14 +539,14 @@ public class FlatScrollPaneUI public void focusGained( FocusEvent e ) { // necessary to update focus border if( scrollpane.getBorder() instanceof FlatBorder ) - scrollpane.repaint(); + HiDPIUtils.repaint( scrollpane ); } @Override public void focusLost( FocusEvent e ) { // necessary to update focus border if( scrollpane.getBorder() instanceof FlatBorder ) - scrollpane.repaint(); + HiDPIUtils.repaint( scrollpane ); } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatSeparatorUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatSeparatorUI.java index b93ce6b9..6334df53 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatSeparatorUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatSeparatorUI.java @@ -32,6 +32,7 @@ import javax.swing.plaf.basic.BasicSeparatorUI; import com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; +import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.LoggingFacade; /** @@ -134,7 +135,7 @@ public class FlatSeparatorUI } else installStyle( s ); s.revalidate(); - s.repaint(); + HiDPIUtils.repaint( s ); break; } } 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 c0a0c49b..7870f054 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 @@ -25,6 +25,8 @@ import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Rectangle; import java.awt.Shape; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; import java.awt.event.MouseEvent; import java.awt.geom.Ellipse2D; import java.awt.geom.Path2D; @@ -191,6 +193,23 @@ public class FlatSliderUI return new FlatTrackListener(); } + @Override + protected FocusListener createFocusListener( JSlider slider ) { + return new BasicSliderUI.FocusHandler() { + @Override + public void focusGained( FocusEvent e ) { + super.focusGained( e ); + HiDPIUtils.repaint( slider ); + } + + @Override + public void focusLost( FocusEvent e ) { + super.focusLost( e ); + HiDPIUtils.repaint( slider ); + } + }; + } + @Override protected PropertyChangeListener createPropertyChangeListener( JSlider slider ) { return FlatStylingSupport.createPropertyChangeListener( slider, this::installStyle, @@ -579,15 +598,15 @@ debug*/ @Override public void setThumbLocation( int x, int y ) { + // set new thumb location and compute union of old and new thumb bounds + Rectangle r = new Rectangle( thumbRect ); + thumbRect.setLocation( x, y ); + SwingUtilities.computeUnion( thumbRect.x, thumbRect.y, thumbRect.width, thumbRect.height, r ); + if( !isRoundThumb() ) { // the needle of the directional thumb is painted outside of thumbRect // --> must increase repaint rectangle - // set new thumb location and compute union of old and new thumb bounds - Rectangle r = new Rectangle( thumbRect ); - thumbRect.setLocation( x, y ); - SwingUtilities.computeUnion( thumbRect.x, thumbRect.y, thumbRect.width, thumbRect.height, r ); - // increase union rectangle for repaint int extra = (int) Math.ceil( UIScale.scale( focusWidth ) * 0.4142f ); if( slider.getOrientation() == JSlider.HORIZONTAL ) @@ -597,10 +616,9 @@ debug*/ if( !slider.getComponentOrientation().isLeftToRight() ) r.x -= extra; } + } - slider.repaint( r ); - } else - super.setThumbLocation( x, y ); + HiDPIUtils.repaint( slider, r ); } //---- class FlatTrackListener -------------------------------------------- @@ -688,21 +706,21 @@ debug*/ !UIManager.getBoolean( "Slider.snapToTicksOnReleased" ) ) { calculateThumbLocation(); - slider.repaint(); + HiDPIUtils.repaint( slider ); } } protected void setThumbHover( boolean hover ) { if( hover != thumbHover ) { thumbHover = hover; - slider.repaint( thumbRect ); + HiDPIUtils.repaint( slider, thumbRect ); } } protected void setThumbPressed( boolean pressed ) { if( pressed != thumbPressed ) { thumbPressed = pressed; - slider.repaint( thumbRect ); + HiDPIUtils.repaint( slider, thumbRect ); } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatSpinnerUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatSpinnerUI.java index d25ae66d..019f8691 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatSpinnerUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatSpinnerUI.java @@ -47,6 +47,7 @@ import javax.swing.plaf.basic.BasicSpinnerUI; import com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; +import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.LoggingFacade; /** @@ -586,7 +587,7 @@ public class FlatSpinnerUI @Override public void focusGained( FocusEvent e ) { // necessary to update focus border - spinner.repaint(); + HiDPIUtils.repaint( spinner ); // if spinner gained focus, transfer it to the editor text field if( e.getComponent() == spinner ) { @@ -599,7 +600,7 @@ public class FlatSpinnerUI @Override public void focusLost( FocusEvent e ) { // necessary to update focus border - spinner.repaint(); + HiDPIUtils.repaint( spinner ); } //---- interface PropertyChangeListener ---- @@ -614,7 +615,7 @@ public class FlatSpinnerUI case FlatClientProperties.COMPONENT_ROUND_RECT: case FlatClientProperties.OUTLINE: - spinner.repaint(); + HiDPIUtils.repaint( spinner ); break; case FlatClientProperties.MINIMUM_WIDTH: @@ -625,7 +626,7 @@ public class FlatSpinnerUI case FlatClientProperties.STYLE_CLASS: installStyle(); spinner.revalidate(); - spinner.repaint(); + HiDPIUtils.repaint( spinner ); break; } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatStylingSupport.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatStylingSupport.java index de35b6c3..e8c8bff8 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatStylingSupport.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatStylingSupport.java @@ -40,6 +40,7 @@ import javax.swing.UIManager; import javax.swing.border.Border; import com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.StringUtils; import com.formdev.flatlaf.util.SystemInfo; @@ -709,7 +710,7 @@ public class FlatStylingSupport case FlatClientProperties.STYLE_CLASS: installStyle.run(); c.revalidate(); - c.repaint(); + HiDPIUtils.repaint( c ); break; } }; diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java index 8f9a9093..0d65f17f 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java @@ -98,6 +98,7 @@ import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; import com.formdev.flatlaf.ui.FlatStylingSupport.UnknownStyleException; import com.formdev.flatlaf.util.Animator; import com.formdev.flatlaf.util.CubicBezierEasing; +import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.JavaCompatibility; import com.formdev.flatlaf.util.LoggingFacade; import com.formdev.flatlaf.util.StringUtils; @@ -895,7 +896,7 @@ public class FlatTabbedPaneUI } } - tabPane.repaint( r ); + HiDPIUtils.repaint( tabPane, r ); } private boolean inCalculateEqual; @@ -2581,19 +2582,19 @@ debug*/ @Override public void popupMenuWillBecomeVisible( PopupMenuEvent e ) { popupVisible = true; - repaint(); + HiDPIUtils.repaint( this ); } @Override public void popupMenuWillBecomeInvisible( PopupMenuEvent e ) { popupVisible = false; - repaint(); + HiDPIUtils.repaint( this ); } @Override public void popupMenuCanceled( PopupMenuEvent e ) { popupVisible = false; - repaint(); + HiDPIUtils.repaint( this ); } } @@ -3102,7 +3103,7 @@ debug*/ case TABBED_PANE_SHOW_TAB_SEPARATORS: case TABBED_PANE_TAB_TYPE: - tabPane.repaint(); + HiDPIUtils.repaint( tabPane ); break; case TABBED_PANE_SHOW_CONTENT_SEPARATOR: @@ -3125,14 +3126,14 @@ debug*/ case TABBED_PANE_TAB_ICON_PLACEMENT: case TABBED_PANE_TAB_CLOSABLE: tabPane.revalidate(); - tabPane.repaint(); + HiDPIUtils.repaint( tabPane ); break; case TABBED_PANE_LEADING_COMPONENT: uninstallLeadingComponent(); installLeadingComponent(); tabPane.revalidate(); - tabPane.repaint(); + HiDPIUtils.repaint( tabPane ); ensureSelectedTabIsVisibleLater(); break; @@ -3140,7 +3141,7 @@ debug*/ uninstallTrailingComponent(); installTrailingComponent(); tabPane.revalidate(); - tabPane.repaint(); + HiDPIUtils.repaint( tabPane ); ensureSelectedTabIsVisibleLater(); break; @@ -3148,7 +3149,7 @@ debug*/ case STYLE_CLASS: installStyle(); tabPane.revalidate(); - tabPane.repaint(); + HiDPIUtils.repaint( tabPane ); break; } } @@ -3172,7 +3173,7 @@ debug*/ case TABBED_PANE_TAB_ALIGNMENT: case TABBED_PANE_TAB_CLOSABLE: tabPane.revalidate(); - tabPane.repaint(); + HiDPIUtils.repaint( tabPane ); break; } } 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 91184b6f..e02d44c2 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 @@ -45,6 +45,7 @@ import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; +import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.LoggingFacade; import com.formdev.flatlaf.util.UIScale; @@ -234,8 +235,8 @@ public class FlatTableHeaderUI @Override protected void rolloverColumnUpdated( int oldColumn, int newColumn ) { - header.repaint( header.getHeaderRect( oldColumn ) ); - header.repaint( header.getHeaderRect( newColumn ) ); + HiDPIUtils.repaint( header, header.getHeaderRect( oldColumn ) ); + HiDPIUtils.repaint( header, header.getHeaderRect( newColumn ) ); } @Override 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 a7b032eb..b7c20bcd 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 @@ -56,6 +56,7 @@ import com.formdev.flatlaf.icons.FlatCheckBoxIcon; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; import com.formdev.flatlaf.util.Graphics2DProxy; +import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.LoggingFacade; import com.formdev.flatlaf.util.SystemInfo; import com.formdev.flatlaf.util.UIScale; @@ -257,7 +258,7 @@ public class FlatTableUI case FlatClientProperties.STYLE_CLASS: installStyle(); table.revalidate(); - table.repaint(); + HiDPIUtils.repaint( table ); break; } }; @@ -560,7 +561,7 @@ public class FlatTableUI public void componentHidden( ComponentEvent e ) { Container viewport = SwingUtilities.getUnwrappedParent( table ); if( viewport instanceof JViewport ) - viewport.repaint(); + HiDPIUtils.repaint( viewport ); } @Override @@ -579,7 +580,7 @@ public class FlatTableUI int viewportHeight = viewport.getHeight(); int tableHeight = table.getHeight(); if( tableHeight < viewportHeight ) - viewport.repaint( 0, tableHeight, viewport.getWidth(), viewportHeight - tableHeight ); + HiDPIUtils.repaint( viewport, 0, tableHeight, viewport.getWidth(), viewportHeight - tableHeight ); } } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextFieldUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextFieldUI.java index 4211da32..88a178ac 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextFieldUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextFieldUI.java @@ -239,7 +239,7 @@ public class FlatTextFieldUI case COMPONENT_ROUND_RECT: case OUTLINE: case TEXT_FIELD_PADDING: - c.repaint(); + HiDPIUtils.repaint( c ); break; case MINIMUM_WIDTH: @@ -250,38 +250,38 @@ public class FlatTextFieldUI case STYLE_CLASS: installStyle(); c.revalidate(); - c.repaint(); + HiDPIUtils.repaint( c ); break; case TEXT_FIELD_LEADING_ICON: leadingIcon = (e.getNewValue() instanceof Icon) ? (Icon) e.getNewValue() : null; - c.repaint(); + HiDPIUtils.repaint( c ); break; case TEXT_FIELD_TRAILING_ICON: trailingIcon = (e.getNewValue() instanceof Icon) ? (Icon) e.getNewValue() : null; - c.repaint(); + HiDPIUtils.repaint( c ); break; case TEXT_FIELD_LEADING_COMPONENT: uninstallLeadingComponent(); installLeadingComponent(); c.revalidate(); - c.repaint(); + HiDPIUtils.repaint( c ); break; case TEXT_FIELD_TRAILING_COMPONENT: uninstallTrailingComponent(); installTrailingComponent(); c.revalidate(); - c.repaint(); + HiDPIUtils.repaint( c ); break; case TEXT_FIELD_SHOW_CLEAR_BUTTON: uninstallClearButton(); installClearButton(); c.revalidate(); - c.repaint(); + HiDPIUtils.repaint( c ); break; case "enabled": @@ -815,7 +815,7 @@ debug*/ if( visible != clearButton.isVisible() ) { clearButton.setVisible( visible ); c.revalidate(); - c.repaint(); + HiDPIUtils.repaint( c ); } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatToggleButtonUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatToggleButtonUI.java index 9859b138..d6a97b61 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatToggleButtonUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatToggleButtonUI.java @@ -26,6 +26,7 @@ import javax.swing.*; import javax.swing.plaf.ComponentUI; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.UnknownStyleException; +import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.UIScale; /** @@ -159,14 +160,14 @@ public class FlatToggleButtonUI b.revalidate(); } - b.repaint(); + HiDPIUtils.repaint( b ); break; case TAB_BUTTON_UNDERLINE_PLACEMENT: case TAB_BUTTON_UNDERLINE_HEIGHT: case TAB_BUTTON_UNDERLINE_COLOR: case TAB_BUTTON_SELECTED_BACKGROUND: - b.repaint(); + HiDPIUtils.repaint( b ); break; } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatToolBarSeparatorUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatToolBarSeparatorUI.java index cae180bd..7eeaea32 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatToolBarSeparatorUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatToolBarSeparatorUI.java @@ -36,6 +36,7 @@ import javax.swing.plaf.basic.BasicToolBarSeparatorUI; import com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; +import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.LoggingFacade; /** @@ -131,7 +132,7 @@ public class FlatToolBarSeparatorUI } else installStyle( s ); s.revalidate(); - s.repaint(); + HiDPIUtils.repaint( s ); break; } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatToolBarUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatToolBarUI.java index 7bc13fb8..760202c3 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatToolBarUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatToolBarUI.java @@ -47,6 +47,7 @@ import javax.swing.plaf.basic.BasicToolBarUI; import com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; +import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.LoggingFacade; import com.formdev.flatlaf.util.UIScale; @@ -443,7 +444,7 @@ public class FlatToolBarUI // repaint button group if( gr != null ) - toolBar.repaint( gr ); + HiDPIUtils.repaint(toolBar, gr ); } private ButtonGroup getButtonGroup( AbstractButton b ) { diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTreeUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTreeUI.java index 31d898e2..6eecb751 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTreeUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTreeUI.java @@ -47,6 +47,7 @@ import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.TreePath; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; +import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.LoggingFacade; import com.formdev.flatlaf.util.UIScale; @@ -310,7 +311,7 @@ public class FlatTreeUI switch( e.getPropertyName() ) { case TREE_WIDE_SELECTION: case TREE_PAINT_SELECTION: - tree.repaint(); + HiDPIUtils.repaint( tree ); break; case "dropLocation": @@ -325,7 +326,7 @@ public class FlatTreeUI case STYLE_CLASS: installStyle(); tree.revalidate(); - tree.repaint(); + HiDPIUtils.repaint( tree ); break; case "enabled": @@ -353,7 +354,7 @@ public class FlatTreeUI Rectangle r = tree.getPathBounds( loc.getPath() ); if( r != null ) - tree.repaint( 0, r.y, tree.getWidth(), r.height ); + HiDPIUtils.repaint( tree, 0, r.y, tree.getWidth(), r.height ); } @Override @@ -370,14 +371,14 @@ public class FlatTreeUI { if( changedPaths.length > 4 ) { // same is done in BasicTreeUI.Handler.valueChanged() - tree.repaint(); + HiDPIUtils.repaint( tree ); } else { int arc = (int) Math.ceil( UIScale.scale( selectionArc / 2f ) ); for( TreePath path : changedPaths ) { Rectangle r = getPathBounds( tree, path ); if( r != null ) - tree.repaint( r.x, r.y - arc, r.width, r.height + (arc * 2) ); + HiDPIUtils.repaint( tree, r.x, r.y - arc, r.width, r.height + (arc * 2) ); } } } 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 115185ca..a6589189 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 @@ -1342,13 +1342,13 @@ debug*/ @Override public void focusGained( FocusEvent e ) { if( repaintCondition == null || repaintCondition.test( repaintComponent ) ) - repaintComponent.repaint(); + HiDPIUtils.repaint( repaintComponent ); } @Override public void focusLost( FocusEvent e ) { if( repaintCondition == null || repaintCondition.test( repaintComponent ) ) - repaintComponent.repaint(); + HiDPIUtils.repaint( repaintComponent ); } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/HiDPIUtils.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/HiDPIUtils.java index b4bccd64..4b6f8274 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/HiDPIUtils.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/HiDPIUtils.java @@ -16,9 +16,11 @@ package com.formdev.flatlaf.util; +import java.awt.Component; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; +import java.awt.Rectangle; import java.awt.font.GlyphVector; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; @@ -322,4 +324,164 @@ public class HiDPIUtils } }; } + + /** + * Repaints the given component. + *

+ * See {@link #repaint(Component, int, int, int, int)} for more details. + * + * @since 3.5 + */ + public static void repaint( Component c ) { + repaint( c, 0, 0, c.getWidth(), c.getHeight() ); + } + + /** + * Repaints the given component area. + *

+ * See {@link #repaint(Component, int, int, int, int)} for more details. + * + * @since 3.5 + */ + public static void repaint( Component c, Rectangle r ) { + repaint( c, r.x, r.y, r.width, r.height ); + } + + /** + * Repaints the given component area. + *

+ * Invokes {@link Component#repaint(int, int, int, int)} on the given component, + *

+ * Use this method, instead of {@code Component.repaint(...)}, + * to fix a problem in Swing when using scale factors that end on .25 or .75 + * (e.g. 1.25, 1.75, 2.25, etc) and repainting single components, which may not + * repaint right and/or bottom 1px edge of component. + *

+ * The problem may occur under following conditions: + *

  • using Java 9 or later + *
  • system scale factor is 125%, 175%, 225%, ... + * (Windows only; Java on macOS and Linux does not support fractional scale factors) + *
  • repaint whole component or right/bottom area of component + *
  • component is opaque; or component is contained in a opaque container + * that has same right/bottom bounds as component + *
  • component has bounds that Java/Swing scales different when repainting components + * + * + * @since 3.5 + */ + public static void repaint( Component c, int x, int y, int width, int height ) { + // repaint given component area + // Always invoke repaint() on given component, even if also invoked (below) + // on one of its ancestors, for the case that component overrides that method. + // Also RepaintManager "merges" the two repaints into one. + c.repaint( x, y, width, height ); + + // if necessary, also repaint given area in first ancestor that is larger than component + // to avoid clipping issue (see needsSpecialRepaint()) + if( needsSpecialRepaint( c, x, y, width, height ) ) { + int x2 = x + c.getX(); + int y2 = y + c.getY(); + for( Component p = c.getParent(); p != null; p = p.getParent() ) { + x2 += p.getX(); + y2 += p.getY(); + if( x2 + width < p.getWidth() && y2 + height < p.getHeight() ) { + p.repaint( x2, y2, width, height ); + break; + } + } + } + } + + /** + * There is a problem in Swing, when using scale factors that end on .25 or .75 + * (e.g. 1.25, 1.75, 2.25, etc) and repainting single components, which may not + * repaint right and/or bottom 1px edge of component. + *

    + * The component is first painted to an in-memory image, + * and then that image is copied to the screen. + * See {@code javax.swing.RepaintManager.PaintManager#paintDoubleBufferedFPScales()}. + *

    + * There are two clipping rectangles involved when copying the image to the screen: + * {@code sun.java2d.SunGraphics2D#devClip} and + * {@code sun.java2d.SunGraphics2D#usrClip}. + *

    + * {@code devClip} is the device clipping in physical pixels. + * It gets the bounds of the painting component, which is either the passed component, + * or if it is non-opaque, then the first opaque ancestor of the passed component. + * It is calculated in {@code sun.java2d.SunGraphics2D#constrain()} while + * getting a graphics context via {@link JComponent#getGraphics()}. + *

    + * {@code usrClip} is the user clipping, which is set via {@link Graphics} clipping methods. + * This is done in {@code javax.swing.RepaintManager.PaintManager#paintDoubleBufferedFPScales()}. + *

    + * The intersection of {@code devClip} and {@code usrClip} + * (computed in {@code sun.java2d.SunGraphics2D#validateCompClip()}) + * is used to copy the image to the screen. + *

    + * Unfortunately different scaling/rounding strategies are used to calculate + * the two clipping rectangles, which is the reason of the issue. + *

    + * {@code devClip} (see {@code sun.java2d.SunGraphics2D#constrain()}): + *

    {@code
    +	 * int devX = (int) (x * scale);
    +	 * int devWidth = Math.round( width * scale )
    +	 * }
    + * {@code usrClip} (see {@code javax.swing.RepaintManager.PaintManager#paintDoubleBufferedFPScales()}): + *
    {@code
    +	 * int usrX = (int) Math.ceil( (x * scale) - 0.5 );
    +	 * int usrWidth = ((int) Math.ceil( ((x + width) * scale) - 0.5 )) - usrX;
    +	 * }
    + * X/Y coordinates are always round down for {@code devClip}, but round up for {@code usrClip}. + * Width/height calculation is also different. + */ + private static boolean needsSpecialRepaint( Component c, int x, int y, int width, int height ) { + // no special repaint necessary for Java 8 or for macOS and Linux + // (Java on those platforms does not support fractional scale factors) + if( !SystemInfo.isJava_9_orLater || !SystemInfo.isWindows ) + return false; + + // check whether repaint area is empty or no component given + // (same checks as in javax.swing.RepaintManager.addDirtyRegion0()) + if( width <= 0 || height <= 0 || c == null ) + return false; + + // check whether component has zero size + // (same checks as in javax.swing.RepaintManager.addDirtyRegion0()) + int compWidth = c.getWidth(); + int compHeight = c.getHeight(); + if( compWidth <= 0 || compHeight <= 0 ) + return false; + + // check whether repaint area does span to right or bottom component edges + // (in this case, {@code devClip} is always larger than {@code usrClip}) + if( x + width < compWidth && y + height < compHeight ) + return false; + + // if component is not opaque, Swing uses the first opaque ancestor for painting + if( !c.isOpaque() ) { + int x2 = x; + int y2 = y; + for( Component p = c.getParent(); p != null; p = p.getParent() ) { + x2 += p.getX(); + y2 += p.getY(); + if( p.isOpaque() ) { + // check whether repaint area does span to right or bottom edges + // of the opaque ancestor component + // (in this case, {@code devClip} is always larger than {@code usrClip}) + if( x2 + width < p.getWidth() && y2 + height < p.getHeight() ) + return false; + break; + } + } + } + + // check whether Special repaint is necessary for current scale factor + // (doing this check late because it temporary allocates some memory) + double scaleFactor = UIScale.getSystemScaleFactor( c.getGraphicsConfiguration() ); + double fraction = scaleFactor - (int) scaleFactor; + if( fraction == 0 || fraction == 0.5 ) + return false; + + return true; + } } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatHiDPITest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatHiDPITest.java new file mode 100644 index 00000000..9154dfda --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatHiDPITest.java @@ -0,0 +1,386 @@ +/* + * Copyright 2024 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.testing; + +import java.awt.*; +import java.awt.event.*; +import java.awt.geom.Rectangle2D; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.function.Supplier; +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.FlatLightLaf; +import com.formdev.flatlaf.FlatSystemProperties; +import com.formdev.flatlaf.util.Graphics2DProxy; +import com.formdev.flatlaf.util.SystemInfo; + +/** + * @author Karl Tauber + */ +public class FlatHiDPITest +{ + private static final double scale = 1.25; + + private final JFrame frame; + private final JPanel testPanel; + + private final Insets frameInsets; + + public static void main( String[] args ) { + System.setProperty( FlatSystemProperties.USE_WINDOW_DECORATIONS, "false" ); + System.setProperty( "sun.java2d.uiScale", Double.toString( scale ) ); + + System.out.println( "Scale factor: " + scale ); + for( int x = 0; x <= 100; x++ ) { + int devX = devScaleXY( x, scale ); + int usrX = usrScaleXY( x, scale ); + if( usrX != devX ) + System.out.printf( "%d: %d != %d\n", x, devX, usrX ); + +/* + for( int w = 0; w <= 10; w++ ) { + int devW = devScaleWH( w, scale ); + int usrW = usrScaleWH( x, w, scale ); + if( usrW != devW ) + System.out.printf( " %d %d: %d != %d\n", x, w, devW, usrW ); + } +*/ + } + + SwingUtilities.invokeLater( () -> { + if( !SystemInfo.isJava_9_orLater ) { + JOptionPane.showMessageDialog( null, "Use Java 9+" ); + return; + } + + FlatLaf.setGlobalExtraDefaults( Collections.singletonMap( "@accentColor", "#f00" ) ); + FlatLightLaf.setup(); + + UIManager.put( "Button.pressedBorderColor", Color.blue ); + UIManager.put( "TextField.caretBlinkRate", 0 ); + UIManager.put( "FormattedTextField.caretBlinkRate", 0 ); + + new FlatHiDPITest(); + } ); + } + + FlatHiDPITest() { + frame = new JFrame( "FlatHiDPITest " + scale ) { + @Override + public Graphics getGraphics() { + return TestGraphics2D.install( super.getGraphics(), "JFrame" ); + } + }; + frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); + + // get frame insets + frame.addNotify(); + frameInsets = frame.getInsets(); + + testPanel = new JPanel( null ) { + @Override + public Graphics getGraphics() { + return TestGraphics2D.install( super.getGraphics(), "JPanel" ); + } + }; + + int y = 0; + addAtProblematicXY( 0, y, 40, 16, 48, "TestComp", TestComp::new ); + y += 20; + addAtProblematicXY( 0, y, 40, 16, 48, "JButton", () -> new JButton( "B" ) ); + y += 20; + addAtProblematicXY( 0, y, 40, 16, 48, "JTextField", () -> new JTextField( "Text" ) ); + y += 20; + addAtProblematicXY( 0, y, 40, 16, 48, "JComboBox", JComboBox::new ); + y += 20; + addAtProblematicXY( 0, y, 40, 16, 48, "JComboBox editable", () -> { + JComboBox c = new JComboBox<>(); + c.setEditable( true ); + return c; + } ); + y += 20; + addAtProblematicXY( 0, y, 40, 16, 48, "JSpinner", JSpinner::new ); + y += 20; + addAtProblematicXY( 0, y, 80, 16, 88, "JSlider", JSlider::new ); + y += 20; + addAtProblematicXY( 0, y, 80, 16, 88, "JScrollBar", () -> new JScrollBar( JScrollBar.HORIZONTAL ) ); + y += 20; + addAtProblematicXY( 0, y, 16, 40, 20, "JScrollBar", () -> new JScrollBar( JScrollBar.VERTICAL ) ); + y += 60; + addAtProblematicXY( 0, y, 80, 16, 88, "JProgressBar", () -> { + JProgressBar c = new JProgressBar(); + c.setValue( 60 ); + c.addMouseListener( new MouseAdapter() { + @Override + public void mousePressed( MouseEvent e ) { + int value = c.getValue(); + c.setValue( (value >= 20) ? value - 20 : 100 ); + } + } ); + return c; + } ); + + frame.getContentPane().add( testPanel ); + frame.setSize( 400, 300 ); + frame.setVisible( true ); + } + + private void addAtProblematicXY( int x, int y, int w, int h, int offset, String text, Supplier generator ) { + // plain component + addAtProblematicXY( x, y, w, h, generator.get() ); + + // component in (opaque) panel which has same bounds as component + addAtProblematicXY( x + offset, y, w, h, wrapInPanel( generator.get(), false ) ); + + // component in (opaque) panel which is 1px larger than component + addAtProblematicXY( x + (offset * 2), y, w + 1, h + 1, wrapInPanel( generator.get(), true ) ); + + JLabel l = new JLabel( text ); + testPanel.add( l ); + l.setLocation( x + (offset * 3) + 20, y ); + l.setSize( l.getPreferredSize() ); + } + + private void addAtProblematicXY( int x, int y, int w, int h, Component c ) { + int px = nextProblematicXY( x + frameInsets.left ) - frameInsets.left; + int py = nextProblematicXY( y + frameInsets.top ) - frameInsets.top; + testPanel.add( c ); + c.setBounds( px, py, w, h ); + } + + private Component wrapInPanel( Component c, boolean emptyBorder ) { + JPanel p = new JPanel( new BorderLayout() ) { + @Override + public Graphics getGraphics() { + return TestGraphics2D.install( super.getGraphics(), "wrapping JPanel" ); + } + }; + if( emptyBorder ) + p.setBorder( new EmptyBorder( 0, 0, 1, 1 ) ); + p.add( c, BorderLayout.CENTER ); + return p; + } + + private static int nextProblematicXY( int xy ) { + for( int i = xy; i < xy + 20; i++ ) { + if( devScaleXY( i, scale ) != usrScaleXY( i, scale ) ) + return i; + } + throw new IllegalArgumentException(); + } + + private static int devScaleXY( int xy, double scale ) { + return (int) (xy * scale); + } + + private static int usrScaleXY( int xy, double scale ) { + // see sun.java2d.pipe.Region.clipRound(double); + return (int) Math.ceil( (xy * scale) - 0.5 ); + } + + @SuppressWarnings( "unused" ) + private static int devScaleWH( int wh, double scale ) { + return (int) Math.round( wh * scale ); + } + + @SuppressWarnings( "unused" ) + private static int usrScaleWH( int xy, int wh, double scale ) { + int usrXY = usrScaleXY( xy, scale ); + return ((int) Math.ceil( ((xy + wh) * scale) - 0.5 )) - usrXY; + } + + //---- class TestComp ----------------------------------------------------- + + private static class TestComp + extends JComponent + implements FocusListener + { + // used to avoid repainting when window is deactivated and activated (for easier debugging) + private boolean permanentFocused; + + TestComp() { + setOpaque( true ); + setFocusable( true ); + + addFocusListener( this ); + addMouseListener( new MouseAdapter() { + @Override + public void mouseClicked( MouseEvent e ) { + requestFocusInWindow(); + } + } ); + } + + @Override + protected void paintComponent( Graphics g ) { + g.setColor( isFocusOwner() ? Color.green : Color.red ); + g.fillRect( 0, 0, getWidth(), getHeight() ); + } + + @Override + public void focusGained( FocusEvent e ) { + if( permanentFocused ) + return; + + if( !e.isTemporary() ) { + repaint(); + permanentFocused = true; + } + } + + @Override + public void focusLost( FocusEvent e ) { + if( !e.isTemporary() ) { + repaint(); + permanentFocused = false; + } + } + + @Override + public Graphics getGraphics() { + return TestGraphics2D.install( super.getGraphics(), "TestComp" ); + } + } + + //---- TestGraphics2D ----------------------------------------------------- + + private static class TestGraphics2D + extends Graphics2DProxy + { + private final Graphics2D delegate; + private final String id; + + static Graphics install( Graphics g, String id ) { + return wasInvokedFrom_safelyGetGraphics() + ? new TestGraphics2D( (Graphics2D) g, id ) + : g; + } + + private static boolean wasInvokedFrom_safelyGetGraphics() { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + StackTraceElement stackTraceElement = stackTrace[4]; + return "javax.swing.JComponent".equals( stackTraceElement.getClassName() ) && + "safelyGetGraphics".equals( stackTraceElement.getMethodName() ); + } + + private TestGraphics2D( Graphics2D delegate, String id ) { + super( delegate ); + this.delegate = delegate; + this.id = id; + + System.out.println(); + System.out.println( "---------------------------------------- " ); + System.out.println( id + ": construct" ); + printClipRects(); + } + + private void printClipRects() { + try { + Class sunGraphics2DClass = Class.forName( "sun.java2d.SunGraphics2D" ); + if( !sunGraphics2DClass.isInstance( delegate ) ) { + System.out.println( " not a SunGraphics2D: " + delegate.getClass().getName() ); + return; + } + + Rectangle devClip = region2rect( getFieldValue( sunGraphics2DClass, delegate, "devClip" ) ); + Shape usrClip = (Shape) getFieldValue( sunGraphics2DClass, delegate, "usrClip" ); + Rectangle clipRegion = region2rect( getFieldValue( sunGraphics2DClass, delegate, "clipRegion" ) ); + + printField( devClip, "devClip" ); + printField( usrClip, "usrClip" ); + printField( clipRegion, "clipRegion" ); + + if( (usrClip instanceof Rectangle && !devClip.contains( (Rectangle) usrClip )) || + (usrClip instanceof Rectangle2D && !devClip.contains( (Rectangle2D) usrClip )) ) + { + System.out.flush(); + System.err.println( "WARNING: devClip smaller than usrClip" ); + System.err.flush(); + } + } catch( Exception ex ) { + ex.printStackTrace(); + } + } + + private void printField( Object value, String name ) throws Exception { + System.out.printf( " %-16s", name ); + + if( value instanceof Rectangle ) { + Rectangle r = (Rectangle) value; + System.out.printf( "xy %3d %3d -> %3d %3d wh %3d %3d\n", + r.x, r.y, r.x + r.width, r.y + r.height, r.width, r.height ); + } else if( value instanceof Rectangle2D ) { + Rectangle2D r = (Rectangle2D) value; + System.out.printf( "xy %.2f %.2f -> %.2f %.2f wh %.2f %.2f\n", + r.getX(), r.getY(), r.getX() + r.getWidth(), r.getY() + r.getHeight(), r.getWidth(), r.getHeight() ); + } else + System.out.println( value ); + } + + private static Rectangle region2rect( Object region ) throws Exception { + Class regionClass = Class.forName( "sun.java2d.pipe.Region" ); + int loX = (int) getMethodValue( regionClass, region, "getLoX" ); + int loY = (int) getMethodValue( regionClass, region, "getLoY" ); + int hiX = (int) getMethodValue( regionClass, region, "getHiX" ); + int hiY = (int) getMethodValue( regionClass, region, "getHiY" ); + return new Rectangle( loX, loY, hiX - loX, hiY - loY ); + } + + private static Object getFieldValue( Class cls, Object object, String name ) throws Exception { + Field f = cls.getDeclaredField( name ); + f.setAccessible( true ); + return f.get( object ); + } + + private static Object getMethodValue( Class cls, Object object, String name ) throws Exception { + Method m = cls.getDeclaredMethod( name ); + m.setAccessible( true ); + return m.invoke( object ); + } + + @Override + public void clipRect( int x, int y, int width, int height ) { + System.out.printf( "\n%s: clipRect( %d, %d, %d, %d )\n", id, x, y, width, height ); + super.clipRect( x, y, width, height ); + printClipRects(); + } + + @Override + public void setClip( int x, int y, int width, int height ) { + System.out.printf( "\n%s: setClip( %d, %d, %d, %d )\n", id, x, y, width, height ); + super.setClip( x, y, width, height ); + printClipRects(); + } + + @Override + public void setClip( Shape clip ) { + System.out.printf( "\n%s: setClip( %s )\n", id, clip ); + super.setClip( clip ); + printClipRects(); + } + + @Override + public void clip( Shape s ) { + System.out.printf( "\n%s: clip( %s )\n", id, s ); + super.clip( s ); + printClipRects(); + } + } +}