Compare commits

..

1 Commits

Author SHA1 Message Date
Bane Stojkovic 92a742d167 scaling 2025-12-25 16:19:25 +01:00
3 changed files with 353 additions and 387 deletions
@@ -33,7 +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.ThemeUtils;
import org.diylc.utils.DPIScalingUtils;
import com.thoughtworks.xstream.XStream;
import org.diylc.common.IPlugInPort;
import org.diylc.core.IView;
@@ -72,9 +72,6 @@ public class DIYLCStarter {
}
public static void runDIYLC(String[] args) {
// Apply OS dark theme settings before creating any UI components
ThemeUtils.applyOSDarkTheme();
// Initialize splash screen
DIYLCSplash splash = null;
Exception splashException = null;
@@ -141,8 +138,12 @@ public class DIYLCStarter {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
// Apply dark theme colors if dark mode is detected
ThemeUtils.applyDarkThemeColors();
// 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);
}
@@ -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 <http://www.gnu.org/licenses/>.
*
*/
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<Object> 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());
}
}
}
@@ -1,381 +0,0 @@
/*
*
* 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
* <http://www.gnu.org/licenses/>.
*/
package org.diylc.utils;
import java.awt.Color;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import javax.swing.UIManager;
import org.apache.log4j.Logger;
import org.diylc.appframework.miscutils.Utils;
/**
* Utility class for detecting and applying OS dark theme settings.
*
* Note: Modern Java (11+) with system Look and Feel should automatically
* respect OS dark theme settings, but this utility provides explicit detection
* and ensures compatibility with older Java versions.
*
* @author Branislav Stojkovic
*/
public class ThemeUtils {
private static final Logger LOG = Logger.getLogger(ThemeUtils.class);
/**
* Detects and applies OS dark theme settings.
* This must be called before creating any AWT/Swing components.
*/
public static void applyOSDarkTheme() {
try {
if (Utils.isMac()) {
applyMacOSDarkTheme();
} else if (Utils.isWindows()) {
detectWindowsDarkTheme();
} else {
detectLinuxDarkTheme();
}
} catch (Exception e) {
LOG.warn("Error detecting OS dark theme: " + e.getMessage(), e);
}
}
/**
* Applies dark theme colors to UIManager after the Look and Feel is set.
* This should be called after UIManager.setLookAndFeel().
*
* @return true if dark mode was detected and colors were applied, false otherwise
*/
public static boolean applyDarkThemeColors() {
try {
if (Utils.isMac()) {
return applyMacOSDarkThemeColors();
} else if (Utils.isWindows()) {
return applyWindowsDarkThemeColors();
} else {
return applyLinuxDarkThemeColors();
}
} catch (Exception e) {
LOG.warn("Error applying dark theme colors: " + e.getMessage(), e);
return false;
}
}
/**
* Detects and applies macOS dark theme settings.
*/
private static void applyMacOSDarkTheme() {
// macOS: Check if dark mode is enabled
// Modern Java (11+) should automatically detect this, but we'll check explicitly
String appearance = System.getProperty("apple.awt.application.appearance");
if (appearance == null) {
// Try to detect dark mode via system command (for older Java versions)
try {
Process process = Runtime.getRuntime().exec(
new String[]{"defaults", "read", "-g", "AppleInterfaceStyle"});
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
String result = reader.readLine();
if ("Dark".equals(result)) {
// Note: Setting this property after JVM start may not always work,
// but it's worth trying for older Java versions
System.setProperty("apple.awt.application.appearance", "NSAppearanceNameDarkAqua");
LOG.info("Detected macOS dark mode - attempting to apply dark theme");
} else {
LOG.debug("macOS light mode detected");
}
} catch (Exception e) {
// If detection fails, that's okay - modern Java will handle it automatically
LOG.debug("Could not detect macOS theme via defaults command: " + e.getMessage());
}
} else if ("NSAppearanceNameDarkAqua".equals(appearance)) {
LOG.info("macOS dark mode already detected by JVM");
} else {
LOG.debug("macOS appearance: " + appearance);
}
}
/**
* Detects Windows dark theme settings.
*/
private static void detectWindowsDarkTheme() {
// Windows: Try to detect dark theme via registry
try {
Process process = Runtime.getRuntime().exec(
new String[]{"reg", "query",
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
"/v", "AppsUseLightTheme"});
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("AppsUseLightTheme")) {
// If value is 0, dark theme is enabled
if (line.contains("0x0") || line.endsWith("0")) {
// For Windows, the system Look and Feel should automatically respect dark mode
// in Java 11+, but we can log it for debugging
LOG.info("Detected Windows dark mode");
}
break;
}
}
} catch (Exception e) {
LOG.debug("Could not detect Windows theme via registry: " + e.getMessage());
}
}
/**
* Detects Linux dark theme settings.
*/
private static void detectLinuxDarkTheme() {
// Linux: Check GTK theme or environment variables
try {
String gtkTheme = System.getenv("GTK_THEME");
if (gtkTheme != null && (gtkTheme.toLowerCase().contains("dark") ||
gtkTheme.toLowerCase().contains("Dark"))) {
LOG.info("Detected Linux dark theme: " + gtkTheme);
}
// Also check COLORFGBG environment variable (used by some terminals/desktops)
String colorFgBg = System.getenv("COLORFGBG");
if (colorFgBg != null) {
// Format is typically "15;0" where second number < 8 indicates dark background
String[] parts = colorFgBg.split(";");
if (parts.length > 1) {
try {
int bgColor = Integer.parseInt(parts[parts.length - 1]);
if (bgColor < 8) {
LOG.info("Detected Linux dark background via COLORFGBG");
}
} catch (NumberFormatException e) {
// Ignore
}
}
}
} catch (Exception e) {
LOG.debug("Could not detect Linux theme: " + e.getMessage());
}
}
/**
* Applies macOS dark theme colors to UIManager.
* @return true if dark mode is active
*/
private static boolean applyMacOSDarkThemeColors() {
boolean isDarkMode = false;
// Check if dark mode is active
String appearance = System.getProperty("apple.awt.application.appearance");
if (appearance == null) {
try {
Process process = Runtime.getRuntime().exec(
new String[]{"defaults", "read", "-g", "AppleInterfaceStyle"});
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
String result = reader.readLine();
isDarkMode = "Dark".equals(result);
} catch (Exception e) {
LOG.debug("Could not detect macOS theme: " + e.getMessage());
// Try to infer from current UI colors
Color menuBarBg = UIManager.getColor("MenuBar.background");
if (menuBarBg != null && isDarkColor(menuBarBg)) {
isDarkMode = true;
}
}
} else {
isDarkMode = "NSAppearanceNameDarkAqua".equals(appearance);
}
if (isDarkMode) {
LOG.info("Applying macOS dark theme colors");
applyDarkColors();
return true;
}
return false;
}
/**
* Applies Windows dark theme colors to UIManager.
* @return true if dark mode is active
*/
private static boolean applyWindowsDarkThemeColors() {
boolean isDarkMode = false;
try {
Process process = Runtime.getRuntime().exec(
new String[]{"reg", "query",
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
"/v", "AppsUseLightTheme"});
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("AppsUseLightTheme")) {
// If value is 0, dark theme is enabled
if (line.contains("0x0") || line.endsWith("0")) {
isDarkMode = true;
}
break;
}
}
} catch (Exception e) {
LOG.debug("Could not detect Windows theme: " + e.getMessage());
// Try to infer from current UI colors
Color menuBarBg = UIManager.getColor("MenuBar.background");
if (menuBarBg != null && isDarkColor(menuBarBg)) {
isDarkMode = true;
}
}
if (isDarkMode) {
LOG.info("Applying Windows dark theme colors");
applyDarkColors();
return true;
}
return false;
}
/**
* Applies Linux dark theme colors to UIManager.
* @return true if dark mode is active
*/
private static boolean applyLinuxDarkThemeColors() {
boolean isDarkMode = false;
try {
String gtkTheme = System.getenv("GTK_THEME");
if (gtkTheme != null && (gtkTheme.toLowerCase().contains("dark") ||
gtkTheme.toLowerCase().contains("Dark"))) {
isDarkMode = true;
}
// Also check COLORFGBG
if (!isDarkMode) {
String colorFgBg = System.getenv("COLORFGBG");
if (colorFgBg != null) {
String[] parts = colorFgBg.split(";");
if (parts.length > 1) {
try {
int bgColor = Integer.parseInt(parts[parts.length - 1]);
if (bgColor < 8) {
isDarkMode = true;
}
} catch (NumberFormatException e) {
// Ignore
}
}
}
}
// Try to infer from current UI colors
if (!isDarkMode) {
Color menuBarBg = UIManager.getColor("MenuBar.background");
if (menuBarBg != null && isDarkColor(menuBarBg)) {
isDarkMode = true;
}
}
} catch (Exception e) {
LOG.debug("Could not detect Linux theme: " + e.getMessage());
}
if (isDarkMode) {
LOG.info("Applying Linux dark theme colors");
applyDarkColors();
return true;
}
return false;
}
/**
* Applies dark theme colors to common UI components.
* Only overrides colors that would cause visibility issues (e.g., dark text on dark background).
*/
private static void applyDarkColors() {
// Menu bar and menu colors - fix text colors if background is dark
Color menuBarBg = UIManager.getColor("MenuBar.background");
Color menuBarFg = UIManager.getColor("MenuBar.foreground");
if (menuBarBg != null && isDarkColor(menuBarBg)) {
// Background is dark, ensure text is light
if (menuBarFg == null || isDarkColor(menuBarFg)) {
UIManager.put("MenuBar.foreground", Color.WHITE);
}
// Fix menu colors
Color menuBg = UIManager.getColor("Menu.background");
Color menuFg = UIManager.getColor("Menu.foreground");
if (menuBg == null || isDarkColor(menuBg)) {
UIManager.put("Menu.background", new Color(50, 50, 50));
}
if (menuFg == null || (menuBg != null && isDarkColor(menuBg) && isDarkColor(menuFg))) {
UIManager.put("Menu.foreground", Color.WHITE);
}
UIManager.put("Menu.selectionBackground", new Color(60, 60, 60));
UIManager.put("Menu.selectionForeground", Color.WHITE);
// Fix menu item colors
Color menuItemBg = UIManager.getColor("MenuItem.background");
Color menuItemFg = UIManager.getColor("MenuItem.foreground");
if (menuItemBg == null || isDarkColor(menuItemBg)) {
UIManager.put("MenuItem.background", new Color(50, 50, 50));
}
if (menuItemFg == null || (menuItemBg != null && isDarkColor(menuItemBg) && isDarkColor(menuItemFg))) {
UIManager.put("MenuItem.foreground", Color.WHITE);
}
UIManager.put("MenuItem.selectionBackground", new Color(60, 60, 60));
UIManager.put("MenuItem.selectionForeground", Color.WHITE);
}
// Popup menu colors
Color popupBg = UIManager.getColor("PopupMenu.background");
if (popupBg == null || isDarkColor(popupBg)) {
UIManager.put("PopupMenu.background", new Color(50, 50, 50));
UIManager.put("PopupMenu.foreground", Color.WHITE);
UIManager.put("PopupMenu.borderColor", new Color(70, 70, 70));
}
// Checkbox and radio button menu items
Color checkBoxMenuItemBg = UIManager.getColor("CheckBoxMenuItem.background");
if (checkBoxMenuItemBg == null || isDarkColor(checkBoxMenuItemBg)) {
UIManager.put("CheckBoxMenuItem.background", new Color(50, 50, 50));
UIManager.put("CheckBoxMenuItem.foreground", Color.WHITE);
UIManager.put("CheckBoxMenuItem.selectionBackground", new Color(60, 60, 60));
UIManager.put("CheckBoxMenuItem.selectionForeground", Color.WHITE);
}
Color radioMenuItemBg = UIManager.getColor("RadioButtonMenuItem.background");
if (radioMenuItemBg == null || isDarkColor(radioMenuItemBg)) {
UIManager.put("RadioButtonMenuItem.background", new Color(50, 50, 50));
UIManager.put("RadioButtonMenuItem.foreground", Color.WHITE);
UIManager.put("RadioButtonMenuItem.selectionBackground", new Color(60, 60, 60));
UIManager.put("RadioButtonMenuItem.selectionForeground", Color.WHITE);
}
}
/**
* Checks if a color is dark (suitable for dark theme).
*/
private static boolean isDarkColor(Color color) {
if (color == null) {
return false;
}
// Calculate luminance
double luminance = (0.299 * color.getRed() + 0.587 * color.getGreen() + 0.114 * color.getBlue()) / 255;
return luminance < 0.5;
}
}