SwingTree.java

package swingtree;

import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import swingtree.api.IconDeclaration;
import swingtree.api.Painter;
import swingtree.style.StyleSheet;
import swingtree.threading.EventProcessor;

import javax.swing.ImageIcon;
import javax.swing.LookAndFeel;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import javax.swing.plaf.FontUIResource;
import javax.swing.plaf.UIResource;
import javax.swing.text.StyleContext;
import java.awt.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

/**
 *  A {@link SwingTree} is a singleton that holds global configuration context for the SwingTree library.
 *  This includes the {@link EventProcessor} that is used to process events, as well as the
 *  {@link StyleSheet} that is used to style components.
 *  <br>
 *  You may access the singleton instance of the {@link SwingTree} class through the {@link #get()} method.
 *
 * @author Daniel Nepp
 */
public final class SwingTree
{
    private static final Logger log = org.slf4j.LoggerFactory.getLogger(SwingTree.class);

    private static final String _DEFAULT_FONT = "defaultFont";

	private static @Nullable LazyRef<SwingTree> _INSTANCE = new LazyRef<>(SwingTree::new);

	/**
	 * Returns the singleton instance of the {@link SwingTree}.
	 * Note that this method will create the singleton if it does not exist.
	 * @return the singleton instance of the {@link SwingTree}.
	 */
	public static SwingTree get() {
        if ( _INSTANCE == null )
            initialize();
        if ( _INSTANCE == null )
            throw new IllegalStateException("Failed to initialize SwingTree singleton!");

        return _INSTANCE.get();
    }

    /**
     *  Clears the singleton instance of the {@link SwingTree}.
     *  This is useful for testing purposes, or if you want to
     *  reconfigure your application with a different {@link SwingTreeInitConfig}.
     *  (see {@link #initialiseUsing(SwingTreeConfigurator)}).
     */
    public static void clear() { _INSTANCE = null; }

    /**
     *  A lazy initialization of the singleton instance of the {@link SwingTree} class
     *  causing it to be recreated the next time it is requested through {@link #get()}.<br>
     *  This is useful for testing purposes, also in cases where
     *  the UI scale changes (through the reference font).<br>
     *  Also see {@link #initialiseUsing(SwingTreeConfigurator)}.
     */
    public static void initialize() {
        _INSTANCE = new LazyRef<>(SwingTree::new);
    }

    /**
     *  A lazy initialization of the singleton instance of the {@link SwingTree} class
     *  causing it to be recreated the next time it is requested through {@link #get()},<br>
     *  but with a {@link SwingTreeConfigurator} that is used
     *  to configure the {@link SwingTree} instance.<br>
     *  This is useful for testing purposes, but also in cases where
     *  the UI scale must be initialized or changed manually (through the reference font).<br>
     *  Also see {@link #initialize()}.
     *
     * @param configurator the {@link SwingTreeConfigurator} that is used
     *                     to configure the {@link SwingTree} instance.
     */
    public static void initialiseUsing( SwingTreeConfigurator configurator ) {
        _INSTANCE = new LazyRef<>(()->new SwingTree(configurator));
    }

    private SwingTreeInitConfig _config;

    private final LazyRef<UiScale> _uiScale;
    private final Map<IconDeclaration, ImageIcon> _iconCache = new HashMap<>();


    private SwingTree() { this(config -> config); }

	private SwingTree( SwingTreeConfigurator configurator ) {
        _config = _resolveConfiguration(configurator);
        _uiScale = new LazyRef<>( () -> new UiScale(_config) );
        _establishMainFont(_config);
    }

    private SwingTreeInitConfig _resolveConfiguration( SwingTreeConfigurator configurator ) {
        try {
            Objects.requireNonNull(configurator);
            SwingTreeInitConfig config = configurator.configure(SwingTreeInitConfig.standard());
            Objects.requireNonNull(config);
            return config;
        } catch (Exception ex) {
            log.error("Error resolving SwingTree configuration", ex);
            ex.printStackTrace();
            return SwingTreeInitConfig.standard();
        }
    }

    private static void _establishMainFont( SwingTreeInitConfig config ) {
        try {
            if (config.fontInstallation() == SwingTreeInitConfig.FontInstallation.HARD)
                config.defaultFont().ifPresent(font -> {
                    if (font instanceof FontUIResource)
                        _installFontInUIManager((FontUIResource) font);
                    else
                        _installFontInUIManager(new FontUIResource(font));
                });
        } catch (Exception ex) {
            log.error("Error installing font in UIManager", ex);
            ex.printStackTrace();
        }
    }

    private static void _installFontInUIManager(javax.swing.plaf.FontUIResource f){
        Enumeration<Object> keys = UIManager.getDefaults().keys();
        while ( keys.hasMoreElements() ) {
            Object key = keys.nextElement();
            Object value = UIManager.get (key);
            if ( value instanceof javax.swing.plaf.FontUIResource )
                UIManager.put(key, f);
        }
    }

    /**
     *  The icon cash is a hash map that uses an {@link IconDeclaration} as a key
     *  and an {@link ImageIcon} as a value. This is used to cache icons that are loaded
     *  from the file system using convenience methods like
     *  {@link swingtree.UI#findIcon(String)} and {@link swingtree.UI#findIcon(IconDeclaration)} or
     *  {@link swingtree.UI#findSvgIcon(String)}, {@link swingtree.UI#findSvgIcon(IconDeclaration)}.<br>
     *  Note that the map returned by this method is mutable and can be used to add or remove icons
     *  from the cache. <b>You may also want to consider this as a possible source of memory leaks.</b>
     *
     * @return The icon cache of this context, which is used to cache icons
     *         that are loaded from the file system.
     */
    public Map<IconDeclaration, ImageIcon> getIconCache() { return _iconCache; }

    /**
     * Returns the user scale factor is a scaling factor is used by SwingTree's
     * style engine to scale the UI during painting.
     * Note that this is different from the system/Graphics2D scale factor, which is
     * the scale factor that the JRE uses to scale everything through the
     * {@link java.awt.geom.AffineTransform} of the {@link Graphics2D}.
     * <p>
     * Use this scaling factor for painting operations that are not performed
     * by SwingTree's style engine, e.g. custom painting
     * (see {@link swingtree.style.ComponentStyleDelegate#painter(UI.Layer, Painter)}).
     * <p>
     * You can configure this scaling factor through the library initialization
     * method {@link SwingTree#initialiseUsing(SwingTreeConfigurator)},
     * or directly through the system property "swingtree.uiScale".
     *
     * @return The user scale factor.
     */
    public float getUiScaleFactor() {
        return _uiScale.get().getUserScaleFactor();
    }

    /**
     * Sets the user scale factor is a scaling factor that is used by SwingTree's
     * style engine to scale the UI during painting.
     * Note that this is different from the system/Graphics2D scale factor, which is
     * the scale factor that the JRE uses to scale everything through the
     * {@link java.awt.geom.AffineTransform} of the {@link Graphics2D}.
     * <p>
     * Use this scaling factor for painting operations that are not performed
     * by SwingTree's style engine, e.g. custom painting
     * (see {@link swingtree.style.ComponentStyleDelegate#painter(UI.Layer, Painter)}).
     * <p>
     * You can configure this scaling factor through the library initialization
     * method {@link SwingTree#initialiseUsing(SwingTreeConfigurator)},
     * or directly through the system property "swingtree.uiScale".
     *
     * @param scaleFactor The user scale factor.
     */
    public void setUiScaleFactor( float scaleFactor ) {
        log.debug("Changing UI scale factor from {} to {} now.", _uiScale.get().getUserScaleFactor(), scaleFactor);
        _uiScale.get().setUserScaleFactor(scaleFactor);
    }

    /**
     * Adds a property change listener to the user scale factor, so
     * when the user scale factor changes, the property "swingtree.uiScale" is fired.
     * The user scale factor is a scaling factor that is used by SwingTree's
     * style engine to scale the UI during painting.
     * Note that this is different from the system/Graphics2D scale factor, which is
     * the scale factor that the JRE uses to scale everything through the
     * {@link java.awt.geom.AffineTransform} of the {@link Graphics2D}.
     * <p>
     * Use this scaling factor for painting operations that are not performed
     * by SwingTree's style engine, e.g. custom painting
     * (see {@link swingtree.style.ComponentStyleDelegate#painter(UI.Layer, Painter)}).
     * <p>
     * You can configure this scaling factor through the library initialization
     * method {@link SwingTree#initialiseUsing(SwingTreeConfigurator)},
     * or directly through the system property "swingtree.uiScale".
     *
     * @param listener The property change listener to add.
     */
    public void addUiScaleChangeListener(PropertyChangeListener listener) {
        _uiScale.get().addPropertyChangeListener(listener);
    }

    /**
     * Removes the provided property change listener from the user scale factor
     * property with the name "swingtree.uiScale".
     * The user scale factor is a scaling factor that is used by SwingTree's
     * style engine to scale the UI during painting.
     * Note that this is different from the system/Graphics2D scale factor, which is
     * the scale factor that the JRE uses to scale everything through the
     * {@link java.awt.geom.AffineTransform} of the {@link Graphics2D}.
     * <p>
     * Use this scaling factor for painting operations that are not performed
     * by SwingTree's style engine, e.g. custom painting
     * (see {@link swingtree.style.ComponentStyleDelegate#painter(UI.Layer, Painter)}).
     * <p>
     * You can configure this scaling factor through the library initialization
     * method {@link SwingTree#initialiseUsing(SwingTreeConfigurator)},
     * or directly through the system property "swingtree.uiScale".
     *
     * @param listener The property change listener to remove.
     */
    public void removeUiScaleChangeListener(PropertyChangeListener listener) {
        _uiScale.get().removePropertyChangeListener(listener);
    }

    /**
     * Returns whether system scaling is enabled.
     * System scaling means that the JRE scales everything
     * through the {@link java.awt.geom.AffineTransform} of the {@link Graphics2D}.
     * If this is the case, then we do not have to do scaled painting
     * and can use the original size of icons, gaps, etc.
     * @return true if system scaling is enabled.
     */
    public boolean isSystemScalingEnabled() { return UiScale._isSystemScalingEnabled(); }

    /**
     * Returns the system scale factor for the given graphics context.
     * The system scale factor is the scale factor that the JRE uses
     * to scale everything (text, icons, gaps, etc).
     *
     * @param g The graphics context to get the system scale factor for.
     * @return The system scale factor for the given graphics context.
     */
    public double getSystemScaleFactorOf( Graphics2D g ) {
        return UiScale._getSystemScaleFactorOf(g);
    }

    /**
     * Returns the system scale factor.
     * The system scale factor is the scale factor that the JRE uses
     * to scale everything (text, icons, gaps, etc) irrespective of the
     * current look and feel, as this is the scale factor that is used
     * by the {@link java.awt.geom.AffineTransform} of the {@link Graphics2D}.
     *
     * @return The system scale factor.
     */
    public double getSystemScaleFactor() {
        return UiScale._getSystemScaleFactor();
    }

	/**
     *  The {@link EventProcessor} is a simple interface whose implementations
     *  delegate tasks to threads that are capable of processing GUI or application events.
     *  As part of this singleton, the SwingTree library maintains a global
     *  {@link EventProcessor} that is used consistently by all declarative builders.
     *
	 * @return The currently configured {@link EventProcessor} that is used to process
	 *         GUI and application events.
	 */
	public EventProcessor getEventProcessor() {
		return _config.eventProcessor();
	}

	/**
	 * Sets the {@link EventProcessor} that is used to process GUI and application events.
     * You may not pass null as an argument, because SwingTree requires an event processor to function.
     *
	 * @param eventProcessor the {@link EventProcessor} that is used to process GUI and application events.
     * @throws NullPointerException if eventProcessor is null!
	 */
	public void setEventProcessor( EventProcessor eventProcessor ) {
        try {
            _config = _config.eventProcessor(Objects.requireNonNull(eventProcessor));
        } catch (Exception ex) {
            log.error("Error setting event processor", ex);
            ex.printStackTrace();
        }
	}

	/**
     *  The {@link StyleSheet} is an abstract class whose extensions are used to declare
     *  component styles through a CSS like DSL API.
     *  As part of this singleton, the SwingTree library maintains a global
     *  {@link StyleSheet} that is used consistently by all declarative builders.
     *  Use this method to access this global style sheet.
     *
	 * @return The currently configured {@link StyleSheet} that is used to style components.
	 */
	public StyleSheet getStyleSheet() {
        return _config.styleSheet();
	}

	/**
	 * Sets the {@link StyleSheet} that is used to style components.
     * Use {@link StyleSheet#none()} instead of null to switch off global styling.
	 * @param styleSheet The {@link StyleSheet} that is used to style components.
     * @throws NullPointerException if styleSheet is null!
	 */
	public void setStyleSheet( StyleSheet styleSheet ) {
        try {
            _config = _config.styleSheet(Objects.requireNonNull(styleSheet));
        } catch ( Exception ex ) {
            log.error("Error setting style sheet", ex);
            ex.printStackTrace();
        }
	}

    /**
     *  Returns the default animation interval in milliseconds
     *  which is a property that
     *  determines the delay between two consecutive animation steps.
     *  You can think of it as the time between the heartbeats of the animation.
     *  The smaller the interval, the higher the refresh rate and
     *  the smoother the animation will look.
     *  However, the smaller the interval, the more CPU time will be used.
     *  The default interval is 16 ms which corresponds to almost 60 fps.
     *  <br>
     *  This property is used as default value by the {@link swingtree.animation.LifeTime}
     *  object which is used to define the duration of an {@link swingtree.animation.Animation}.
     *  The value returned by this is used by animations
     *  if no other interval is specified through {@link swingtree.animation.LifeTime#withInterval(long, TimeUnit)}.
     * @return The default animation interval in milliseconds.
     */
    public long getDefaultAnimationInterval() {
        return _config.defaultAnimationInterval();
    }

    /**
     *  Sets the default animation interval in milliseconds
     *  which is a property that
     *  determines the delay between two consecutive animation steps.
     *  You can think of it as the time between the heartbeats of the animation.
     *  The smaller the interval, the higher the refresh rate and
     *  the smoother the animation will look.
     *  However, the smaller the interval, the more CPU time will be used.
     *  The default interval is 16 ms which corresponds to almost 60 fps.
     *  <br>
     *  This property is used as default value by the {@link swingtree.animation.LifeTime}
     *  object which is used to define the duration of an {@link swingtree.animation.Animation}.
     *  The value returned by this is used by animations
     *  if no other interval is specified through {@link swingtree.animation.LifeTime#withInterval(long, TimeUnit)}.
     * @param defaultAnimationInterval The default animation interval in milliseconds.
     */
    public void setDefaultAnimationInterval( long defaultAnimationInterval ) {
        _config = _config.defaultAnimationInterval(defaultAnimationInterval);
    }

    /**
     *  Exposes a set of system properties in the form of a nicely formatted string.
     *  These are used by the SwingTree library to determine the system configuration
     *  and to adjust the UI accordingly.
     *
     * @return A string containing system information.
     */
    public String getSystemInfo() {
        return SystemInfo.getAsPrettyString();
    }

    /**
     * This class handles scaling in SwingTree UIs.
     * It computes user scaling factor based on font size and
     * provides methods to scale integer, float, {@link Dimension} and {@link Insets}.
     * This class is look and feel independent.
     * <p>
     * Two scaling modes are supported by SwingTree for HiDPI displays:
     * <p>
     * <h2>1) system scaling mode</h2>
     *
     * 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.
     * The scale factor may change for a window when moving the window from one display to another one.
     * <p>
     * <h2>2) user scaling mode</h2>
     *
     * This mode is mainly for Java 8 compatibility, but is also used on Linux
     * or if the default font is changed.
     * The user scale factor is computed based on the used font.
     * The JRE does not scale anything.
     * So we have to invoke {@link UI#scale(float)} where necessary.
     * There is only one user scale factor for all displays.
     * The user scale factor may change if the active LaF, "defaultFont" or "Label.font" has changed.
     * If system scaling mode is available the user scale factor is usually 1,
     * but may be larger on Linux or if the default font is changed.
     *
     * @author Daniel Nepp, but a derivative work originally from Karl Tauber (com.formdev.flatlaf.util.UIScale)
     */
    static final class UiScale
    {
        private final SwingTreeInitConfig config;
        private @Nullable PropertyChangeSupport changeSupport;

        private static @Nullable Boolean jreHiDPI;

        private float scaleFactor = 1;
        private boolean initialized;


        private UiScale( SwingTreeInitConfig config ) // private to prevent instantiation from outside
        {
            this.config = config;
            try {
                // add user scale factor to allow layout managers (e.g. MigLayout) to use it
                UIManager.put( "laf.scaleFactor", (UIDefaults.ActiveValue) t -> {
                    return this.scaleFactor;
                });

                if ( config.scalingStrategy() == SwingTreeInitConfig.Scaling.NONE ) {
                    this.scaleFactor = 1;
                    this.initialized = true;
                    return;
                }

                if ( config.scalingStrategy() == SwingTreeInitConfig.Scaling.FROM_DEFAULT_FONT ) {
                    Font uiScaleReferenceFont = config.defaultFont().orElse(null);
                    if ( uiScaleReferenceFont != null ) {
                        UIManager.getDefaults().put(_DEFAULT_FONT, uiScaleReferenceFont);
                        log.debug("Setting default font ('{}') to in UIManager to {}", _DEFAULT_FONT, uiScaleReferenceFont);
                    }
                }

                if ( config.scalingStrategy() == SwingTreeInitConfig.Scaling.FROM_SYSTEM_FONT ) {
                    float defaultScale = this.scaleFactor;
                    Font highDPIFont = _calculateDPIAwarePlatformFont();
                    boolean updated = _initialize( highDPIFont );
                    if ( this.scaleFactor != defaultScale ) {
                        UIManager.getDefaults().put(_DEFAULT_FONT, highDPIFont);
                        log.debug("Setting default font ('{}') to in UIManager to {}", _DEFAULT_FONT, highDPIFont);
                    }
                    if ( updated )
                        _setScalePropertyListeners();
                }
                else
                    _initialize();

            } catch (Exception ex) {
                log.error("Error initializing "+ UiScale.class.getSimpleName(), ex);
                // Usually there should be no exception, if there is one, the library will still work, but
                // the UI may not be scaled correctly. Please report this exception to the library author.
            }
        }

        private Font _calculateDPIAwarePlatformFont()
        {
            FontUIResource dpiAwareFont = null;

            // determine UI font based on operating system
            if( SystemInfo.isWindows ) {
                Font winFont = (Font) Toolkit.getDefaultToolkit().getDesktopProperty( "win.messagebox.font" );
                if( winFont != null ) {
                    if( SystemInfo.isWinPE ) {
                        // on WinPE use "win.defaultGUI.font", which is usually Tahoma,
                        // because Segoe UI font is not available on WinPE
                        Font winPEFont = (Font) Toolkit.getDefaultToolkit().getDesktopProperty( "win.defaultGUI.font" );
                        if ( winPEFont != null )
                            dpiAwareFont = _createCompositeFont( winPEFont.getFamily(), winPEFont.getStyle(), winFont.getSize() );
                    }
                    else
                        dpiAwareFont = _createCompositeFont( winFont.getFamily(), winFont.getStyle(), winFont.getSize() );
                }

            } else if ( SystemInfo.isMacOS ) {
                String fontName;
                if( SystemInfo.isMacOS_10_15_Catalina_orLater ) {
                    if ( SystemInfo.isJetBrainsJVM_11_orLater ) {
                        // See https://youtrack.jetbrains.com/issue/JBR-1915
                        fontName = ".AppleSystemUIFont";
                    } else {
                        // use Helvetica Neue font
                        fontName = "Helvetica Neue";
                    }
                } else if( SystemInfo.isMacOS_10_11_ElCapitan_orLater ) {
                    // use San Francisco Text font
                    fontName = ".SF NS Text";
                } else {
                    // default font on older systems (see com.apple.laf.AquaFonts)
                    fontName = "Lucida Grande";
                }

                dpiAwareFont = _createCompositeFont( fontName, Font.PLAIN, 13 );

            } else if( SystemInfo.isLinux ) {
                try {
                    Font font = LinuxFontPolicy.getFont();
                    dpiAwareFont = (font instanceof FontUIResource) ? (FontUIResource) font : new FontUIResource( font );
                } catch (Exception e) {
                    log.error("Failed to find linux font for scaling!", e);
                }
            }

            // fallback
            if( dpiAwareFont == null )
                dpiAwareFont = _createCompositeFont( Font.SANS_SERIF, Font.PLAIN, 12 );

            // Todo: Look at ActiveFont in FlatLaf to see if we need to do anything with it here.
            // Comment from FlatLaf code base below:
            /*
                // get/remove "defaultFont" from defaults if set in properties files
                // (use remove() to avoid that ActiveFont.createValue() gets invoked)
                Object defaultFont = defaults.remove( _DEFAULT_FONT );
                // use font from OS as base font and derive the UI font from it
                if( defaultFont instanceof ActiveFont ) {
                    Font baseFont = uiFont;
                    uiFont = ((ActiveFont)defaultFont).derive( baseFont, fontSize -> {
                        return Math.round( fontSize * computeFontScaleFactor( baseFont ) );
                    } );
                }
            */

            // increase font size if system property "swingtree.uiScale" is set
            dpiAwareFont = _applyCustomScaleFactor( dpiAwareFont );

            return dpiAwareFont;
        }

        private static FontUIResource _createCompositeFont( String family, int style, int size ) {
            // using StyleContext.getFont() here because it uses
            // sun.font.FontUtilities.getCompositeFontUIResource()
            // and creates a composite font that is able to display all Unicode characters
            Font font = StyleContext.getDefaultStyleContext().getFont( family, style, size );
            return (font instanceof FontUIResource) ? (FontUIResource) font : new FontUIResource( font );
        }

        public void addPropertyChangeListener( PropertyChangeListener listener ) {
            if( changeSupport == null )
                changeSupport = new PropertyChangeSupport( UiScale.class );
            changeSupport.addPropertyChangeListener( listener );
        }

        public void removePropertyChangeListener( PropertyChangeListener listener ) {
            if( changeSupport == null )
                return;
            changeSupport.removePropertyChangeListener( listener );
        }

        //---- system scaling (Java 9) --------------------------------------------

        /**
         * Returns whether system scaling is enabled.
         * System scaling means that the JRE scales everything
         * through the {@link java.awt.geom.AffineTransform} of the {@link Graphics2D}.
         * If this is the case, then we do not have to do scaled painting
         * and can use the original size of icons, gaps, etc.
         */
        static boolean _isSystemScalingEnabled() {
            if ( jreHiDPI != null )
                return jreHiDPI;

            jreHiDPI = false;

            if ( SystemInfo.isJava_9_orLater ) {
                // Java 9 and later supports per-monitor scaling
                jreHiDPI = true;
            } else if ( SystemInfo.isJetBrainsJVM ) {
                // IntelliJ IDEA ships its own JetBrains Java 8 JRE that may support 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;
        }

        /**
         * Returns the system scale factor for the given graphics context.
         * The system scale factor is the scale factor that the JRE uses
         * to scale everything (text, icons, gaps, etc).
         *
         * @param g The graphics context to get the system scale factor for.
         * @return The system scale factor for the given graphics context.
         */
        private static double _getSystemScaleFactorOf( Graphics2D g ) {
            return _isSystemScalingEnabled() ? _getSystemScaleFactorOf( g.getDeviceConfiguration() ) : 1;
        }

        /**
         * @param gc The graphics configuration to get the system scale factor for.
         * @return The system scale factor for the given graphics configuration.
         */
        private static double _getSystemScaleFactorOf( GraphicsConfiguration gc ) {
            return (_isSystemScalingEnabled() && gc != null) ? gc.getDefaultTransform().getScaleX() : 1;
        }

        //---- user scaling (Java 8) ----------------------------------------------

        private void _initialize()
        {
            boolean updated = _initialize( _getDefaultFont() );
            if ( updated )
                _setScalePropertyListeners();
        }

        private boolean _initialize( Font uiScaleReferenceFont )
        {
            if ( initialized )
                return false;

            initialized = true;

            if ( !config.isUiScaleFactorEnabled() )
                return false;

            _setUserScaleFactor( _calculateScaleFactor( uiScaleReferenceFont ) );

            return true;
        }

        private void _setScalePropertyListeners() {
            // listener to update scale factor if LaF changed, "defaultFont" or "Label.font" changed
            PropertyChangeListener listener = new PropertyChangeListener() {
                @Override
                public void propertyChange( PropertyChangeEvent e ) {
                    switch( e.getPropertyName() ) {
                        case "lookAndFeel":
                            // it is not necessary (and possible) to remove listener of old LaF defaults
                            if( e.getNewValue() instanceof LookAndFeel)
                                UIManager.getLookAndFeelDefaults().addPropertyChangeListener( this );

                            _setUserScaleFactor( _calculateScaleFactor( _getDefaultFont() ) );
                            break;

                        case _DEFAULT_FONT:
                        case "Label.font":
                            _setUserScaleFactor( _calculateScaleFactor( _getDefaultFont() ) );
                            break;
                    }
                }
            };
            UIManager.addPropertyChangeListener( listener );
            UIManager.getDefaults().addPropertyChangeListener( listener );
            UIManager.getLookAndFeelDefaults().addPropertyChangeListener( listener );
        }

        private Font _getDefaultFont() {
            // 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( _DEFAULT_FONT );
            if ( font == null )
                font = UIManager.getFont( "Label.font" );

            return font;
        }

        /**
         * Computes the scale factor based on the given font.
         * @param font font to compute scale factor from
         * @return scale factor, normalized
         */
        private float _calculateScaleFactor( Font font ) {
            // apply custom scale factor specified in system property "swingtree.uiScale"
            float customScaleFactor = config.uiScaleFactor();
            if ( customScaleFactor > 0 ) {
                return customScaleFactor;
            }

            return _normalize(_internalComputeScaleFactorFrom( font ) );
        }

        /**
         * Computes the scale factor based on the given font.
         * @param font font to compute scale factor from
         * @return scale factor
         */
        private float _internalComputeScaleFactorFrom( Font font ) {
            if ( SystemInfo.isWindows ) {
                // Special handling for Windows to be compatible with OS scaling,
                // which distinguish between "screen scaling" and "text scaling".
                //  - Windows "screen scaling" scales everything (text, icon, gaps, etc)
                //    and may have different scaling factors for each screen.
                //  - Windows "text scaling" increases only the font size, but on all screens.
                //
                // Both can be changed by the user in the Windows 10 Settings:
                //  - Settings > Display > Scale and layout
                //  - Settings > Ease of Access > Display > Make text bigger (100% - 225%)
                if( font instanceof UIResource) {
                    Font uiFont = (Font) Toolkit.getDefaultToolkit().getDesktopProperty( "win.messagebox.font" );
                    if( uiFont == null || uiFont.getSize() == font.getSize() ) {
                        if( _isSystemScalingEnabled() ) {
                            // Do not apply own scaling if the JRE scales using Windows screen scale factor.
                            // If user increases font size in Windows 10 settings, desktop property
                            // "win.messagebox.font" is changed and SwingTree uses the larger font.
                            return 1;
                        } else {
                            // If the JRE does not scale (Java 8), the size of the UI font
                            // (usually from desktop property "win.messagebox.font")
                            // combines the Windows screen and text scale factors.
                            // But the font in desktop property "win.defaultGUI.font" is only
                            // scaled with the Windows screen scale factor. So use it to compute
                            // our scale factor that is equal to Windows screen scale factor.
                            Font winFont = (Font) Toolkit.getDefaultToolkit().getDesktopProperty( "win.defaultGUI.font" );
                            return _computeScaleFactorFromFontSize( (winFont != null) ? winFont : font );
                        }
                    }
                }

                // If font was explicitly set from outside (is not a UIResource),
                // or was set in SwingTree properties files (is a UIResource),
                // use it to compute scale factor. This allows applications to
                // use custom fonts (e.g. that the user can change in UI) and
                // get scaling if a larger font size is used.
                // E.g. SwingTree Demo supports increasing font size in "Font" menu and UI scales.
            }

            return _computeScaleFactorFromFontSize( font );
        }

        private static float _computeScaleFactorFromFontSize(Font font ) {
            // default font size
            float fontSizeDivider = 12f;

            if( SystemInfo.isWindows ) {
                // 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.isMacOS ) {
                // default font size on macOS is 13
                fontSizeDivider = 13f;
            } else if( SystemInfo.isLinux ) {
                // default font size for Unity and Gnome is 15 and for KDE it is 13
                fontSizeDivider = SystemInfo.isKDE ? 13f : 15f;
            }

            return font.getSize() / fontSizeDivider;
        }

        /**
         * Applies a custom scale factor given in system property "swingtree.uiScale"
         * to the given font.
         */
        private FontUIResource _applyCustomScaleFactor( FontUIResource font )
        {
            if ( !config.isUiScaleFactorEnabled() )
                return font;

            float scaleFactor = config.uiScaleFactor();
            if ( scaleFactor <= 0 )
                return font;

            float fontScaleFactor = _computeScaleFactorFromFontSize( font );
            if ( scaleFactor == fontScaleFactor )
                return font;

            int newFontSize = Math.max( Math.round( (font.getSize() / fontScaleFactor) * scaleFactor ), 1 );
            return new FontUIResource( font.deriveFont( (float) newFontSize ) );
        }

        /**
         * Returns the user scale factor is a scaling factor is used by SwingTree's
         * style engine to scale the UI during painting.
         * Note that this is different from the system scale factor, which is
         * the scale factor that the JRE uses to scale everything through the
         * {@link java.awt.geom.AffineTransform} of the {@link Graphics2D}.
         * <p>
         * Use this scaling factor for painting operations that are not performed
         * by SwingTree's style engine, e.g. custom painting
         * (see {@link swingtree.style.ComponentStyleDelegate#painter(UI.Layer, Painter)}).
         * <p>
         * You can configure this scaling factor through the library initialization
         * method {@link SwingTree#initialiseUsing(SwingTreeConfigurator)},
         * or through the system property "swingtree.uiScale".
         *
         * @return The user scale factor.
         */
        public float getUserScaleFactor() {
            _initialize();
            return scaleFactor;
        }

        public void setUserScaleFactor( float scaleFactor ) {
            _initialize();
            _setUserScaleFactor( _normalize(scaleFactor) );
        }

        private float _normalize( float scaleFactor ) {
            if ( scaleFactor < 1f ) {
                scaleFactor = config.isUiScaleDownAllowed()
                        ? Math.round( scaleFactor * 10f ) / 10f // round small scale factor to 1/10
                        : 1f;
            }
            else if ( scaleFactor > 1f ) // round scale factor to 1/4
                scaleFactor = Math.round( scaleFactor * 4f ) / 4f;

            return scaleFactor;
        }

        /**
         * Sets the user scale factor.
         */
        private void _setUserScaleFactor(float scaleFactor) {
            // minimum scale factor
            scaleFactor = Math.max( scaleFactor, 0.1f );

            float oldScaleFactor = this.scaleFactor;
            this.scaleFactor = scaleFactor;

            if ( changeSupport != null )
                changeSupport.firePropertyChange( "userScaleFactor", oldScaleFactor, scaleFactor );
        }

        /**
         * Returns true if the JRE scales, which is the case if:
         *   - environment variable GDK_SCALE is set and running on Java 9 or later
         *   - running on JetBrains Runtime 11 or later and scaling is enabled in system Settings
         */
        static boolean _isSystemScaling() {
            if( GraphicsEnvironment.isHeadless() )
                return true;

            GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment()
                                                          .getDefaultScreenDevice()
                                                          .getDefaultConfiguration();

            return UiScale._getSystemScaleFactorOf( gc ) > 1;
        }

        static double _getSystemScaleFactor() {
            if ( GraphicsEnvironment.isHeadless() )
                return 1;

            return UiScale._getSystemScaleFactorOf(
                                GraphicsEnvironment.getLocalGraphicsEnvironment()
                                                   .getDefaultScreenDevice()
                                                   .getDefaultConfiguration()
                            );
        }

    }

    /**
     * Provides information about the current system.
     *
     * @author Daniel Nepp, but originally a derivative work of
     *         Karl Tauber (com.formdev.flatlaf.util.SystemInfo)
     */
    private static final class SystemInfo
    {
        // platforms
        public static final boolean isWindows;
        public static final boolean isMacOS;
        public static final boolean isLinux;

        // OS versions
        public static final long osVersion;
        public static final boolean isWindows_10_orLater;
        /** <strong>Note</strong>: This requires Java 8u321, 11.0.14, 17.0.2 or 18 (or later).
         * (see https://bugs.openjdk.java.net/browse/JDK-8274840)
         **/
        public static final boolean isWindows_11_orLater;
        public static final boolean isMacOS_10_11_ElCapitan_orLater;
        public static final boolean isMacOS_10_14_Mojave_orLater;
        public static final boolean isMacOS_10_15_Catalina_orLater;

        // OS architecture
        public static final boolean isX86;
        public static final boolean isX86_64;
        public static final boolean isAARCH64;

        // Java versions
        public static final long javaVersion;
        public static final boolean isJava_9_orLater;
        public static final boolean isJava_11_orLater;
        public static final boolean isJava_15_orLater;
        public static final boolean isJava_17_orLater;
        public static final boolean isJava_18_orLater;

        // Java VMs
        public static final boolean isJetBrainsJVM;
        public static final boolean isJetBrainsJVM_11_orLater;

        // UI toolkits
        public static final boolean isKDE;

        // other
        public static final boolean isProjector;
        public static final boolean isWebswing;
        public static final boolean isWinPE;

        static {
            // platforms
            String osName = System.getProperty( "os.name" ).toLowerCase( Locale.ENGLISH );
            isWindows = osName.startsWith( "windows" );
            isMacOS = osName.startsWith( "mac" );
            isLinux = osName.startsWith( "linux" );

            // OS versions
            osVersion = scanVersion( System.getProperty( "os.version" ) );
            isWindows_10_orLater = (isWindows && osVersion >= toVersion( 10, 0, 0, 0 ));
            isWindows_11_orLater = (isWindows_10_orLater && osName.length() > "windows ".length() &&
                    scanVersion( osName.substring( "windows ".length() ) ) >= toVersion( 11, 0, 0, 0 ));
            isMacOS_10_11_ElCapitan_orLater = (isMacOS && osVersion >= toVersion( 10, 11, 0, 0 ));
            isMacOS_10_14_Mojave_orLater = (isMacOS && osVersion >= toVersion( 10, 14, 0, 0 ));
            isMacOS_10_15_Catalina_orLater = (isMacOS && osVersion >= toVersion( 10, 15, 0, 0 ));

            // OS architecture
            String osArch = System.getProperty( "os.arch" );
            isX86 = osArch.equals( "x86" );
            isX86_64 = osArch.equals( "amd64" ) || osArch.equals( "x86_64" );
            isAARCH64 = osArch.equals( "aarch64" );

            // Java versions
            javaVersion = scanVersion( System.getProperty( "java.version" ) );
            isJava_9_orLater = (javaVersion >= toVersion( 9, 0, 0, 0 ));
            isJava_11_orLater = (javaVersion >= toVersion( 11, 0, 0, 0 ));
            isJava_15_orLater = (javaVersion >= toVersion( 15, 0, 0, 0 ));
            isJava_17_orLater = (javaVersion >= toVersion( 17, 0, 0, 0 ));
            isJava_18_orLater = (javaVersion >= toVersion( 18, 0, 0, 0 ));

            // Java VMs
            isJetBrainsJVM = System.getProperty( "java.vm.vendor", "Unknown" )
                    .toLowerCase( Locale.ENGLISH ).contains( "jetbrains" );
            isJetBrainsJVM_11_orLater = isJetBrainsJVM && isJava_11_orLater;

            // UI toolkits
            isKDE = (isLinux && System.getenv( "KDE_FULL_SESSION" ) != null);

            // other
            isProjector = Boolean.getBoolean( "org.jetbrains.projector.server.enable" );
            isWebswing = (System.getProperty( "webswing.rootDir" ) != null);
            isWinPE = isWindows && "X:\\Windows\\System32".equalsIgnoreCase( System.getProperty( "user.dir" ) );
        }

        public static long 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 );
        }

        public static long toVersion( int major, int minor, int micro, int patch ) {
            return ((long) major << 48) + ((long) minor << 32) + ((long) micro << 16) + patch;
        }

        static String getAsPrettyString() {
            return SystemInfo.class.getSimpleName() + "[\n" +
                    "    isWindows=" + isWindows + ",\n" +
                    "    isMacOS=" + isMacOS + ",\n" +
                    "    isLinux=" + isLinux + ",\n" +
                    "    osVersion=" + osVersion + ",\n" +
                    "    isWindows_10_orLater=" + isWindows_10_orLater + ",\n" +
                    "    isWindows_11_orLater=" + isWindows_11_orLater + ",\n" +
                    "    isMacOS_10_11_ElCapitan_orLater=" + isMacOS_10_11_ElCapitan_orLater + ",\n" +
                    "    isMacOS_10_14_Mojave_orLater=" + isMacOS_10_14_Mojave_orLater + ",\n" +
                    "    isMacOS_10_15_Catalina_orLater=" + isMacOS_10_15_Catalina_orLater + ",\n" +
                    "    isX86=" + isX86 + ",\n" +
                    "    isX86_64=" + isX86_64 + ",\n" +
                    "    isAARCH64=" + isAARCH64 + ",\n" +
                    "    javaVersion=" + javaVersion + ",\n" +
                    "    isJava_9_orLater=" + isJava_9_orLater + ",\n" +
                    "    isJava_11_orLater=" + isJava_11_orLater + ",\n" +
                    "    isJava_15_orLater=" + isJava_15_orLater + ",\n" +
                    "    isJava_17_orLater=" + isJava_17_orLater + ",\n" +
                    "    isJava_18_orLater=" + isJava_18_orLater + ",\n" +
                    "    isJetBrainsJVM=" + isJetBrainsJVM + ",\n" +
                    "    isJetBrainsJVM_11_orLater=" + isJetBrainsJVM_11_orLater + ",\n" +
                    "    isKDE=" + isKDE + ",\n" +
                    "    isProjector=" + isProjector + ",\n" +
                    "    isWebswing=" + isWebswing + ",\n" +
                    "    isWinPE=" + isWinPE + "\n" +
                    "]\n";
        }
    }

    private static class LinuxFontPolicy
    {
        static Font getFont() {
            return SystemInfo.isKDE ? getKDEFont() : getGnomeFont();
        }

        /**
         * Gets the default font for Gnome.
         */
        private static Font getGnomeFont() {
            // see class com.sun.java.swing.plaf.gtk.PangoFonts background information

            Object fontName = Toolkit.getDefaultToolkit().getDesktopProperty( "gnome.Gtk/FontName" );
            if( !(fontName instanceof String) )
                fontName = "sans 10";

            String family = "";
            int style = Font.PLAIN;
            double dsize = 10;

            // parse pango font description
            // see https://developer.gnome.org/pango/1.46/pango-Fonts.html#pango-font-description-from-string
            StringTokenizer st = new StringTokenizer( (String) fontName );
            while( st.hasMoreTokens() ) {
                String word = st.nextToken();

                // remove trailing ',' (e.g. in "Ubuntu Condensed, 11" or "Ubuntu Condensed, Bold 11")
                if( word.endsWith( "," ) )
                    word = word.substring( 0, word.length() - 1 ).trim();

                String lword = word.toLowerCase(Locale.ENGLISH);
                if( lword.equals( "italic" ) || lword.equals( "oblique" ) )
                    style |= Font.ITALIC;
                else if( lword.equals( "bold" ) )
                    style |= Font.BOLD;
                else if( Character.isDigit( word.charAt( 0 ) ) ) {
                    try {
                        dsize = Double.parseDouble( word );
                    } catch( NumberFormatException ex ) {
                        // ignore
                    }
                } else {
                    // remove '-' from "Semi-Bold", "Extra-Light", etc
                    if( lword.startsWith( "semi-" ) || lword.startsWith( "demi-" ) )
                        word = word.substring( 0, 4 ) + word.substring( 5 );
                    else if( lword.startsWith( "extra-" ) || lword.startsWith( "ultra-" ) )
                        word = word.substring( 0, 5 ) + word.substring( 6 );

                    family = family.isEmpty() ? word : (family + ' ' + word);
                }
            }

            // Ubuntu font is rendered poorly (except if running in JetBrains VM)
            // --> use Liberation Sans font
            String useUbuntu = "swingtree.USE_UBUNTU_FONT";

            if( family.startsWith( "Ubuntu" ) && !SystemInfo.isJetBrainsJVM && !getBoolean( useUbuntu, false ) )
                family = "Liberation Sans";

            // scale font size
            dsize *= getGnomeFontScale();
            int size = (int) (dsize + 0.5);
            if( size < 1 )
                size = 1;

            // handle logical font names
            String logicalFamily = mapFcName( family.toLowerCase(Locale.ENGLISH) );
            if( logicalFamily != null )
                family = logicalFamily;

            return createFontEx( family, style, size, dsize );
        }

        /**
         * Checks whether a system property is set and returns {@code true} if its value
         * is {@code "true"} (case-insensitive), otherwise it returns {@code false}.
         * If the system property is not set, {@code defaultValue} is returned.
         */
        static boolean getBoolean( String key, boolean defaultValue ) {
            String value = System.getProperty( key );
            return (value != null) ? Boolean.parseBoolean( value ) : defaultValue;
        }

        /**
         * Create a font for the given family, style and size.
         * If the font family does not match any font on the system,
         * then the last word (usually a font weight) from the family name is removed and tried again.
         * E.g. family 'URW Bookman Light' is not found, but 'URW Bookman' is found.
         * If still not found, then font of family 'Dialog' is returned.
         */
        private static Font createFontEx( String family, int style, int size, double dsize ) {
            for(;;) {
                Font font = createFont( family, style, size, dsize );

                if( Font.DIALOG.equals( family ) )
                    return font;

                // if the font family does not match any font on the system, "Dialog" family is returned
                if( !Font.DIALOG.equals( font.getFamily() ) ) {
                    // check for font problems
                    // - font height much larger than expected (e.g. font Inter; Oracle Java 8)
                    // - character width is zero (e.g. font Cantarell; Fedora; Oracle Java 8)
                    FontMetrics fm = StyleContext.getDefaultStyleContext().getFontMetrics( font );
                    if( fm.getHeight() > size * 2 || fm.stringWidth( "a" ) == 0 )
                        return createFont( Font.DIALOG, style, size, dsize );

                    return font;
                }

                // find last word in family
                int index = family.lastIndexOf( ' ' );
                if( index < 0 )
                    return createFont( Font.DIALOG, style, size, dsize );

                // check whether last work contains some font weight (e.g. Ultra-Bold or Heavy)
                String lastWord = family.substring( index + 1 ).toLowerCase(Locale.ENGLISH);
                if( lastWord.contains( "bold" ) || lastWord.contains( "heavy" ) || lastWord.contains( "black" ) )
                    style |= Font.BOLD;

                // remove last word from family and try again
                family = family.substring( 0, index );
            }
        }

        private static Font createFont( String family, int style, int size, double dsize ) {
            Font font = UiScale._createCompositeFont( family, style, size );

            // set font size in floating points
            font = font.deriveFont( style, (float) dsize );

            return font;
        }

        private static double getGnomeFontScale() {
            // do not scale font here if JRE scales
            if( UiScale._isSystemScaling() )
                return 96. / 72.;

            // see class com.sun.java.swing.plaf.gtk.PangoFonts background information

            Object value = Toolkit.getDefaultToolkit().getDesktopProperty( "gnome.Xft/DPI" );
            if( value instanceof Integer ) {
                int dpi = (Integer) value / 1024;
                if( dpi == -1 )
                    dpi = 96;
                if( dpi < 50 )
                    dpi = 50;
                return dpi / 72.0;
            } else {
                return GraphicsEnvironment.getLocalGraphicsEnvironment()
                                          .getDefaultScreenDevice()
                                          .getDefaultConfiguration()
                                          .getNormalizingTransform()
                                          .getScaleY();
            }
        }

        /**
         * map GTK/fontconfig names to equivalent JDK logical font name
         */
        private static @Nullable String mapFcName( String name ) {
            switch( name ) {
                case "sans":		return "sansserif";
                case "sans-serif":	return "sansserif";
                case "serif":		return "serif";
                case "monospace":	return "monospaced";
            }
            return null;
        }

        /**
         * Gets the default font for KDE from KDE configuration files.
         * <p>
         * The Swing fonts are not updated when the user changes system font size
         * (System Settings > Fonts > Force Font DPI). A application restart is necessary.
         * This is the same behavior as in native KDE applications.
         * <p>
         * The "display scale factor" (kdeglobals: [KScreen] > ScaleFactor) is not used
         * KDE also does not use it to calculate font size. Only forceFontDPI is used by KDE.
         * If user changes "display scale factor" (System Settings > Display and Monitors >
         * Displays > Scale Display), the forceFontDPI is also changed to reflect the scale factor.
         */
        private static Font getKDEFont() {
            List<String> kdeglobals = readConfig( "kdeglobals" );
            List<String> kcmfonts   = readConfig( "kcmfonts" );

            String generalFont  = getConfigEntry( kdeglobals, "General", "font" );
            String forceFontDPI = getConfigEntry( kcmfonts, "General", "forceFontDPI" );

            String family = "sansserif";
            int style = Font.PLAIN;
            int size = 10;

            if ( generalFont != null ) {
                List<String> strs = StringUtils.split( generalFont, ',' );
                try {
                    family = strs.get( 0 );
                    size = Integer.parseInt( strs.get( 1 ) );
                    if( "75".equals( strs.get( 4 ) ) )
                        style |= Font.BOLD;
                    if( "1".equals( strs.get( 5 ) ) )
                        style |= Font.ITALIC;
                } catch ( RuntimeException ex ) {
                    log.error("Failed to parse KDE system font from String '"+generalFont+"'.", ex);
                }
            }

            // font dpi
            int dpi = 96;
            if( forceFontDPI != null && !UiScale._isSystemScaling() ) {
                try {
                    dpi = Integer.parseInt( forceFontDPI );
                    if( dpi <= 0 )
                        dpi = 96;
                    if( dpi < 50 )
                        dpi = 50;
                } catch( NumberFormatException ex ) {
                    log.error("Failed to parse DPI scale from font size String '"+forceFontDPI+"'", ex);
                }
            }

            // scale font size
            double fontScale = dpi / 72.0;
            double dsize = size * fontScale;
            size = (int) (dsize + 0.5);
            if( size < 1 )
                size = 1;

            return createFont( family, style, size, dsize );
        }

        private static List<String> readConfig( String filename ) {
            File userHome = new File( System.getProperty( "user.home" ) );

            // search for config file
            String[] configDirs = {
                    ".config", // KDE 5
                    ".kde4/share/config", // KDE 4
                    ".kde/share/config"// KDE 3
            };
            File file = null;
            for( String configDir : configDirs ) {
                file = new File( userHome, configDir + "/" + filename );
                if( file.isFile() )
                    break;
            }
            if( file == null || !file.isFile() )
                return Collections.emptyList();

            // read config file
            ArrayList<String> lines = new ArrayList<>( 200 );
            try( BufferedReader reader = new BufferedReader( new FileReader( file ) ) ) {
                String line;
                while( (line = reader.readLine()) != null )
                    lines.add( line );
            } catch( IOException ex ) {
                log.error("Failed to read KDE font config files for determining DPI scale factor.", ex);
            }
            return Collections.unmodifiableList(lines);
        }

        private static @Nullable String getConfigEntry( List<String> config, String group, String key ) {
            int groupLength = group.length();
            int keyLength = key.length();
            boolean inGroup = false;
            for( String line : config ) {
                if( !inGroup ) {
                    if( line.length() >= groupLength + 2 &&
                            line.charAt( 0 ) == '[' &&
                            line.charAt( groupLength + 1 ) == ']' &&
                            line.indexOf( group ) == 1 )
                    {
                        inGroup = true;
                    }
                } else {
                    if( line.startsWith( "[" ) )
                        return null;

                    if( line.length() >= keyLength + 2 &&
                            line.charAt( keyLength ) == '=' &&
                            line.startsWith( key ) )
                    {
                        return line.substring( keyLength + 1 );
                    }
                }
            }
            return null;
        }

    }

    private static class LazyRef<T> {
        private final Supplier<T> supplier;
        private volatile @Nullable T value;

        LazyRef( Supplier<T> supplier ) {
            this.supplier = Objects.requireNonNull( supplier );
        }

        T get() {
            @Nullable T value = this.value;
            if( value == null ) {
                synchronized( this ) {
                    value = this.value;
                    if ( value == null )
                        this.value = value = supplier.get();
                }
            }
            return value;
        }
    }
}