From 92a742d1676b55479718044fd14e59895dbfc7df Mon Sep 17 00:00:00 2001 From: Bane Stojkovic Date: Thu, 25 Dec 2025 16:19:25 +0100 Subject: [PATCH] scaling --- .../src/main/java/org/diylc/DIYLCStarter.java | 7 + .../java/org/diylc/utils/DPIScalingUtils.java | 346 ++++++++++++++++++ 2 files changed, 353 insertions(+) create mode 100644 diylc/diylc-swing/src/main/java/org/diylc/utils/DPIScalingUtils.java diff --git a/diylc/diylc-swing/src/main/java/org/diylc/DIYLCStarter.java b/diylc/diylc-swing/src/main/java/org/diylc/DIYLCStarter.java index fb02f9d0..5849545c 100644 --- a/diylc/diylc-swing/src/main/java/org/diylc/DIYLCStarter.java +++ b/diylc/diylc-swing/src/main/java/org/diylc/DIYLCStarter.java @@ -33,6 +33,7 @@ import org.diylc.appframework.miscutils.PropertyInjector; import org.diylc.appframework.miscutils.Utils; import org.diylc.swingframework.export.DrawingExporter; import org.diylc.swingframework.fonts.FontOptimizer; +import org.diylc.utils.DPIScalingUtils; import com.thoughtworks.xstream.XStream; import org.diylc.common.IPlugInPort; import org.diylc.core.IView; @@ -137,6 +138,12 @@ public class DIYLCStarter { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + + // Scale all UIManager font defaults for high DPI displays + // This applies globally to all Swing components, fixing font scaling + // issues on Windows with high DPI displays where HTML rendering may + // not properly scale fonts. + DPIScalingUtils.scaleUIManagerFonts(); } catch (Exception e) { LOG.error("Could not set Look&Feel", e); } diff --git a/diylc/diylc-swing/src/main/java/org/diylc/utils/DPIScalingUtils.java b/diylc/diylc-swing/src/main/java/org/diylc/utils/DPIScalingUtils.java new file mode 100644 index 00000000..47e973b0 --- /dev/null +++ b/diylc/diylc-swing/src/main/java/org/diylc/utils/DPIScalingUtils.java @@ -0,0 +1,346 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.utils; + +import java.awt.Font; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.Toolkit; +import java.util.Enumeration; +import javax.swing.UIDefaults; +import javax.swing.UIManager; + +/** + * Utility class for handling DPI scaling on high-resolution displays. + * + * This is particularly important on Windows where HTML rendering in Swing + * components (like JLabel) can fail to properly scale fonts on high DPI displays. + * + * @author Branislav Stojkovic + */ +public class DPIScalingUtils { + + private static Double dpiScaleFactor = null; + private static final double SCALING_THRESHOLD = 0.85; // 85% threshold to avoid double-scaling + private static final double OVERSCALE_THRESHOLD = 1.3; // 130% - if font is this much larger than expected, it's over-scaled + private static final float BASE_FONT_SIZE_96DPI = 12.0f; // Standard font size at 96 DPI + private static final float MINIMUM_FONT_SIZE = 10.0f; + private static final float MAXIMUM_REASONABLE_FONT_SIZE = 20.0f; + + /** + * Gets the DPI scale factor for the current display. + * This is particularly important on Windows with high DPI displays where + * HTML rendering in Swing components can fail to properly scale fonts. + * + * @return The DPI scale factor (1.0 for normal displays, > 1.0 for high DPI) + */ + public static double getDpiScaleFactor() { + if (dpiScaleFactor == null) { + try { + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice gd = ge.getDefaultScreenDevice(); + GraphicsConfiguration gc = gd.getDefaultConfiguration(); + + // Get the transform scale factor from GraphicsConfiguration + // This is the most reliable way to detect DPI scaling + double scaleX = gc.getDefaultTransform().getScaleX(); + double scaleY = gc.getDefaultTransform().getScaleY(); + double transformScale = Math.max(scaleX, scaleY); + + // On Windows, Java's DPI awareness can be inconsistent, especially with HTML rendering + // Check if we're on Windows and if the transform indicates scaling + if (System.getProperty("os.name", "").toLowerCase().contains("windows")) { + // If transform scale is > 1.0, we have DPI scaling + // HTML rendering in Swing on Windows may not respect this, so we need to compensate + if (transformScale > 1.0) { + dpiScaleFactor = transformScale; + } else { + // Fallback: check screen resolution as a secondary indicator + int screenResolution = Toolkit.getDefaultToolkit().getScreenResolution(); + if (screenResolution > 96) { + // High DPI display detected via resolution + dpiScaleFactor = screenResolution / 96.0; + } else { + dpiScaleFactor = 1.0; + } + } + } else { + // On non-Windows systems, trust the transform scale + dpiScaleFactor = Math.max(1.0, transformScale); + } + } catch (Exception e) { + // Fallback to 1.0 if there's any error + dpiScaleFactor = 1.0; + } + } + return dpiScaleFactor; + } + + /** + * Calculates the expected font size for a given base size at the current DPI. + * + * @param baseSize The base font size (typically at 96 DPI) + * @return The expected font size at the current DPI + */ + private static float getExpectedFontSize(float baseSize) { + double scaleFactor = getDpiScaleFactor(); + return (float) (baseSize * scaleFactor); + } + + /** + * Scales a font to match the DPI scaling of the display. + * This is useful for components that use HTML rendering (like JLabel with HTML text), + * which may not properly scale fonts on Windows with high DPI displays. + * + * @param font The font to scale + * @return A scaled font if high DPI is detected and scaling is needed, otherwise the original font + */ + public static Font scaleFontForDPI(Font font) { + if (font == null) { + return null; + } + + double scaleFactor = getDpiScaleFactor(); + + // Only apply scaling when high DPI is detected + if (scaleFactor <= 1.0) { + return font; + } + + float currentSize = font.getSize(); + + // Calculate what the base size would be (assuming current size is at 96 DPI) + float assumedBaseSize = currentSize; + + // Calculate what the expected size should be at this DPI + float expectedSize = getExpectedFontSize(assumedBaseSize); + + // Calculate the ratio of current to expected size + double sizeRatio = currentSize / expectedSize; + + // Only scale if the current size is significantly smaller than expected + // This prevents double-scaling on systems that already handle it correctly + if (sizeRatio < SCALING_THRESHOLD) { + // Scale to expected size, but ensure minimum readability + float scaledSize = Math.max(expectedSize, MINIMUM_FONT_SIZE); + return font.deriveFont(scaledSize); + } + + return font; + } + + /** + * Downscales a font that appears to be over-scaled. + * This calculates what the font size should be based on the actual DPI + * and scales it down proportionally if it's significantly larger than expected. + * + * @param font The font to downscale + * @return A downscaled font if needed, otherwise the original font + */ + private static Font downscaleFontForDPI(Font font) { + if (font == null) { + return font; + } + + float currentSize = font.getSize(); + double scaleFactor = getDpiScaleFactor(); + + // Calculate what the base size would be if we reverse the DPI scaling + // This assumes the font was scaled from a base size at 96 DPI + float estimatedBaseSize = (float) (currentSize / scaleFactor); + + // Calculate what the expected size should be at this DPI + float expectedSize = getExpectedFontSize(BASE_FONT_SIZE_96DPI); + + // Calculate the ratio of current to expected size + double sizeRatio = currentSize / expectedSize; + + // If the font is significantly larger than expected (over-scaled), downscale it + if (sizeRatio > OVERSCALE_THRESHOLD) { + // Calculate target size: use the base size if reasonable, otherwise use expected size + float targetSize; + if (estimatedBaseSize >= MINIMUM_FONT_SIZE && estimatedBaseSize <= MAXIMUM_REASONABLE_FONT_SIZE) { + // Use the calculated base size if it's reasonable + targetSize = estimatedBaseSize; + } else { + // Otherwise, use the expected size for the current DPI + targetSize = expectedSize; + } + + // Ensure target is within reasonable bounds + targetSize = Math.max(MINIMUM_FONT_SIZE, Math.min(targetSize, MAXIMUM_REASONABLE_FONT_SIZE)); + + // Only apply if the reduction is significant enough to matter (>10% change) + if (Math.abs(targetSize - currentSize) / currentSize > 0.1) { + return font.deriveFont(targetSize); + } + } + + return font; + } + + /** + * Scales a font size value to match the DPI scaling of the display. + * + * @param fontSize The base font size to scale + * @return The scaled font size if high DPI is detected, otherwise the original size + */ + public static float scaleFontSizeForDPI(float fontSize) { + double scaleFactor = getDpiScaleFactor(); + if (scaleFactor > 1.0) { + return (float) (fontSize * scaleFactor); + } + return fontSize; + } + + /** + * Scales an integer font size value to match the DPI scaling of the display. + * + * @param fontSize The base font size to scale + * @return The scaled font size if high DPI is detected, otherwise the original size + */ + public static int scaleFontSizeForDPI(int fontSize) { + return Math.round(scaleFontSizeForDPI((float) fontSize)); + } + + /** + * Resets the cached DPI scale factor. This is useful if the display configuration + * changes at runtime (e.g., moving the window to a different display). + */ + public static void resetCache() { + dpiScaleFactor = null; + } + + /** + * Checks if the current display is a high DPI display. + * + * @return true if the DPI scale factor is greater than 1.0 + */ + public static boolean isHighDPIDisplay() { + return getDpiScaleFactor() > 1.0; + } + + /** + * Scales all UIManager font defaults to match the DPI scaling of the display. + * This applies globally to all Swing components, ensuring consistent font scaling + * across the entire application without needing to modify individual components. + * + * This should be called once during application initialization, after setting the + * Look and Feel but before creating any UI components. + * + * On Windows: Fixes HTML rendering issues where fonts don't scale properly. + * On Linux: May downscale fonts if they appear to be over-scaled by the system. + * On macOS: Does nothing (macOS handles DPI scaling correctly). + * + * The scaling is conservative - it only scales fonts that appear to need adjustment, + * preventing double-scaling on systems that already handle it correctly. + */ + public static void scaleUIManagerFonts() { + String osName = System.getProperty("os.name", "").toLowerCase(); + boolean isWindows = osName.contains("windows"); + boolean isLinux = osName.contains("linux"); + boolean isMac = osName.contains("mac"); + + // On macOS, Java/Swing handles DPI scaling perfectly, so do nothing + if (isMac) { + return; + } + + double scaleFactor = getDpiScaleFactor(); + + try { + UIDefaults defaults = UIManager.getDefaults(); + Enumeration keys = defaults.keys(); + + // Get a reference font size to check if fonts are already scaled + // Typical default font size is 12-13pt at 96 DPI + Font defaultFont = UIManager.getFont("Label.font"); + if (defaultFont == null) { + defaultFont = UIManager.getFont("Button.font"); + } + + if (defaultFont == null) { + return; + } + + float defaultSize = defaultFont.getSize(); + + // Calculate expected font size at current DPI (assuming base size of 12pt at 96 DPI) + float expectedSize = getExpectedFontSize(BASE_FONT_SIZE_96DPI); + + // Calculate the ratio of actual to expected size + double sizeRatio = defaultSize / expectedSize; + + // Determine if we need to scale up or down based on the ratio + boolean needsUpScaling = false; + boolean needsDownScaling = false; + + if (isWindows && scaleFactor > 1.0) { + // On Windows with high DPI, fonts may be too small due to HTML rendering issues + // If font is significantly smaller than expected, it needs up-scaling + if (sizeRatio < SCALING_THRESHOLD) { + needsUpScaling = true; + } + } else if (isLinux) { + // On Linux, fonts may be over-scaled by the system + // If font is significantly larger than expected, it needs down-scaling + if (sizeRatio > OVERSCALE_THRESHOLD) { + needsDownScaling = true; + } + } + + // If no scaling is needed, return early + if (!needsUpScaling && !needsDownScaling) { + return; + } + + while (keys.hasMoreElements()) { + Object key = keys.nextElement(); + Object value = defaults.get(key); + + // Check if this is a font property + if (value instanceof Font) { + Font font = (Font) value; + Font adjustedFont = font; + + if (needsUpScaling) { + // Scale up for Windows (fix HTML rendering issues) + adjustedFont = scaleFontForDPI(font); + } else if (needsDownScaling) { + // Scale down for Linux (fix over-scaling) + adjustedFont = downscaleFontForDPI(font); + } + + // Only update if the font was actually adjusted + if (adjustedFont != font) { + defaults.put(key, adjustedFont); + } + } + } + } catch (Exception e) { + // Log error but don't fail - font scaling is a nice-to-have feature + System.err.println("Warning: Could not scale UIManager fonts: " + e.getMessage()); + } + } +} +