diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatButtonBorder.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatButtonBorder.java index b9636bc5..209fc4cb 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatButtonBorder.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatButtonBorder.java @@ -25,6 +25,7 @@ import java.awt.Insets; import java.awt.Paint; import javax.swing.UIManager; import javax.swing.plaf.basic.BasicBorders; +import com.formdev.flatlaf.util.UIScale; /** * Border for {@link javax.swing.JButton}. @@ -42,7 +43,7 @@ public class FlatButtonBorder float focusWidth = getFocusWidth(); float lineWidth = getLineWidth(); - float arc = 6; //TODO + float arc = UIScale.scale( 6f ); //TODO g2.setPaint( getBorderColor( c ) ); FlatUIUtils.drawRoundRectangle( g2, x, y, width, height, focusWidth, lineWidth, arc ); @@ -66,7 +67,7 @@ public class FlatButtonBorder @Override public Insets getBorderInsets( Component c, Insets insets ) { - int w = Math.round( getFocusWidth() + getLineWidth() ); + int w = UIScale.round( getFocusWidth() + getLineWidth() ); insets = super.getBorderInsets( c, insets ); insets.top += w; @@ -78,11 +79,11 @@ public class FlatButtonBorder protected float getFocusWidth() { //TODO - return 2; + return UIScale.scale( 2f ); } protected float getLineWidth() { //TODO - return 1; + return UIScale.scale( 1f ); } } 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 98d9fd9c..60e67a57 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 @@ -28,6 +28,7 @@ import javax.swing.JComponent; import javax.swing.UIManager; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.basic.BasicButtonUI; +import com.formdev.flatlaf.util.UIScale; import sun.swing.SwingUtilities2; /** @@ -61,8 +62,8 @@ public class FlatButtonUI FlatUIUtils.setRenderingHints( g2 ); //TODO - float focusWidth = 2; - float arc = 6; + float focusWidth = UIScale.scale( 2f ); + float arc = UIScale.scale( 6f ); g2.setColor( getBackground( c ) ); FlatUIUtils.fillRoundRectangle( g2, 0, 0, c.getWidth(), c.getHeight(), focusWidth, arc ); diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java index 112cf147..5bff4fd1 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java @@ -17,6 +17,7 @@ package com.formdev.flatlaf.util; import java.util.Locale; +import java.util.StringTokenizer; /** * Provides information about the current system. @@ -29,11 +30,43 @@ public class SystemInfo public static final boolean IS_MAC; public static final boolean IS_LINUX; + public static final boolean IS_JAVA_9_OR_LATER; + + public static final boolean IS_JETBRAINS_JVM; + static { String osName = System.getProperty( "os.name" ).toLowerCase( Locale.ENGLISH ); IS_WINDOWS = osName.startsWith( "windows" ); IS_MAC = osName.startsWith( "mac" ); IS_LINUX = osName.startsWith( "linux" ); + + int javaVersion = scanVersion( System.getProperty( "java.version" ) ); + IS_JAVA_9_OR_LATER = (javaVersion >= toVersion( 9, 0, 0, 0 )); + + IS_JETBRAINS_JVM = System.getProperty( "java.vm.vendor", "Unknown" ) + .toLowerCase( Locale.ENGLISH ).contains( "jetbrains" ); + } + + private static int scanVersion( String version ) { + int major = 1; + int minor = 0; + int micro = 0; + int patch = 0; + try { + StringTokenizer st = new StringTokenizer( version, "._-+" ); + major = Integer.parseInt( st.nextToken() ); + minor = Integer.parseInt( st.nextToken() ); + micro = Integer.parseInt( st.nextToken() ); + patch = Integer.parseInt( st.nextToken() ); + } catch( Exception ex ) { + // ignore + } + + return toVersion( major, minor, micro, patch ); + } + + private static int toVersion( int major, int minor, int micro, int patch ) { + return (major << 24) + (minor << 16) + (micro << 8) + patch; } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/UIScale.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/UIScale.java new file mode 100644 index 00000000..1cfda364 --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/UIScale.java @@ -0,0 +1,188 @@ +/* + * Copyright 2019 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 + * + * http://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.util; + +import java.awt.Font; +import java.awt.GraphicsEnvironment; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.lang.reflect.Method; +import javax.swing.LookAndFeel; +import javax.swing.UIManager; + +/** + * Two scaling modes are supported for HiDPI displays: + * + * 1) system scaling mode + * + * This mode is supported since Java 9 on all platforms and in some Java 8 VMs + * (e.g. Apple and JetBrains). The JRE determines the scale factor per-display and + * adds a scaling transformation to the graphics object. + * E.g. invokes {@code java.awt.Graphics2D.scale( 1.5, 1.5 )} for 150%. + * So the JRE does the scaling itself. + * E.g. when you draw a 10px line, a 15px line is drawn on screen. + * The scale factor may be different for each connected display. + * + * 2) user scaling mode + * + * This mode is for Java 8 compatibility and can be removed when changing minimum + * required Java version to 9. + * The user scale factor is computed based on the used font. + * The JRE does not scale anything. + * So we have to invoke {@link #scale(float)} where necessary. + * There is only one user scale factor for all displays. + * + * @author Karl Tauber + */ +public class UIScale +{ + private static final boolean DEBUG = false; + + //---- system scaling (Java 9) -------------------------------------------- + + private static Boolean jreHiDPI; + + private static boolean isJreHiDPIEnabled() { + if( jreHiDPI != null ) + return jreHiDPI; + + jreHiDPI = false; + + if( SystemInfo.IS_JAVA_9_OR_LATER ) { + // Java 9 and later supports per-monitor scaling + jreHiDPI = true; + } else if( SystemInfo.IS_JETBRAINS_JVM ) { + // IntelliJ IDEA ships its own JetBrains Java 8 JRE that may supports per-monitor scaling + // see com.intellij.ui.JreHiDpiUtil.isJreHiDPIEnabled() + try { + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + Class sunGeClass = Class.forName( "sun.java2d.SunGraphicsEnvironment" ); + if( sunGeClass.isInstance( ge ) ) { + Method m = sunGeClass.getDeclaredMethod( "isUIScaleOn" ); + jreHiDPI = (Boolean) m.invoke( ge ); + } + } catch( Throwable ex ) { + // ignore + } + } + + return jreHiDPI; + } + + //---- user scaling (Java 8) ---------------------------------------------- + + private static float scaleFactor = 1; + + static { + if( isEnabled() ) { + // listener to update scale factor if LaF changed or if Label.font changed + // (e.g. option "Override default fonts" in IntelliJ IDEA) + PropertyChangeListener listener = new PropertyChangeListener() { + @Override + public void propertyChange( PropertyChangeEvent e ) { + String propName = e.getPropertyName(); + if( "lookAndFeel".equals( propName ) ) { + // it is not necessary (and possible) to remove listener of old LaF defaults + if( e.getNewValue() instanceof LookAndFeel ) + UIManager.getLookAndFeelDefaults().addPropertyChangeListener( this ); + updateScaleFactor(); + } else if( "Label.font".equals( propName ) ) + updateScaleFactor(); + } + }; + UIManager.addPropertyChangeListener( listener ); + UIManager.getLookAndFeelDefaults().addPropertyChangeListener( listener ); + + updateScaleFactor(); + } + } + + private static void updateScaleFactor() { + if( !isEnabled() ) + return; + + // use font size to calculate scale factor (instead of DPI) + // because even if we are on a HiDPI display it is not sure + // that a larger font size is set by the current LaF + // (e.g. can avoid large icons with small text) + Font font = UIManager.getFont( "Label.font" ); + + // default font size + float fontSizeDivider = 12f; + + if( SystemInfo.IS_WINDOWS ) { + // Windows LaF uses Tahoma font rather than the actual Windows system font (Segoe UI), + // and its size is always ca. 10% smaller than the actual system font size. + // Tahoma 11 is used at 100% + if( "Tahoma".equals( font.getFamily() ) ) + fontSizeDivider = 11f; + } else if( SystemInfo.IS_LINUX ) { + // default font size for Unity and Gnome is 15 + fontSizeDivider = 15f; + } + + setUserScaleFactor( font.getSize() / fontSizeDivider ); + } + + private static boolean isEnabled() { + if( isJreHiDPIEnabled() ) + return false; // disable user scaling if JRE scales + + // same as in IntelliJ IDEA + String hidpi = System.getProperty( "hidpi" ); + return (hidpi != null) ? Boolean.parseBoolean( hidpi ) : true; + } + + public static float getUserScaleFactor() { + return scaleFactor; + } + + private static void setUserScaleFactor( float scaleFactor ) { + if( scaleFactor <= 1f ) + scaleFactor = 1f; + else // round scale factor to 1/4 + scaleFactor = Math.round( scaleFactor * 4f ) / 4f; + + UIScale.scaleFactor = scaleFactor; + + if( DEBUG ) + System.out.println( "HiDPI scale factor " + scaleFactor ); + } + + public static float scale( float value ) { + return (scaleFactor == 1) ? value : (value * scaleFactor); + } + + public static int scale( int value ) { + return (scaleFactor == 1) ? value : round( value * scaleFactor ); + } + + public static float unscale( float value ) { + return (scaleFactor == 1f) ? value : (value / scaleFactor); + } + + public static int unscale( int value ) { + return (scaleFactor == 1f) ? value : round( value / scaleFactor ); + } + + public static int round( float value ) { + // subtracting 0.01 gives "better" results in some cases + // e.g. 2px * 125% = 2px instead of 3px + // or 1px * 150% = 1px instead of 2px + return Math.round( value - 0.01f ); + } +} diff --git a/flatlaf-core/src/test/java/com/formdev/flatlaf/FlatComponentsTest.java b/flatlaf-core/src/test/java/com/formdev/flatlaf/FlatComponentsTest.java index cf6a8352..dde6cea0 100644 --- a/flatlaf-core/src/test/java/com/formdev/flatlaf/FlatComponentsTest.java +++ b/flatlaf-core/src/test/java/com/formdev/flatlaf/FlatComponentsTest.java @@ -37,7 +37,7 @@ public class FlatComponentsTest JOptionPane.showMessageDialog( null, new FlatComponentsTest(), - "FlatComponentsTest", + "FlatComponentsTest (Java " + System.getProperty( "java.version" ) + ")", JOptionPane.PLAIN_MESSAGE ); }