SwingTree.java

  1. package swingtree;

  2. import org.jspecify.annotations.Nullable;
  3. import org.slf4j.Logger;
  4. import swingtree.api.IconDeclaration;
  5. import swingtree.api.Painter;
  6. import swingtree.style.StyleSheet;
  7. import swingtree.threading.EventProcessor;

  8. import javax.swing.ImageIcon;
  9. import javax.swing.LookAndFeel;
  10. import javax.swing.UIDefaults;
  11. import javax.swing.UIManager;
  12. import javax.swing.plaf.FontUIResource;
  13. import javax.swing.plaf.UIResource;
  14. import javax.swing.text.StyleContext;
  15. import java.awt.*;
  16. import java.beans.PropertyChangeEvent;
  17. import java.beans.PropertyChangeListener;
  18. import java.beans.PropertyChangeSupport;
  19. import java.io.BufferedReader;
  20. import java.io.File;
  21. import java.io.IOException;
  22. import java.lang.reflect.Method;
  23. import java.nio.file.Files;
  24. import java.util.List;
  25. import java.util.*;
  26. import java.util.concurrent.TimeUnit;
  27. import java.util.function.Supplier;

  28. import static java.nio.charset.StandardCharsets.UTF_8;

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

  41.     private static final String _DEFAULT_FONT = "defaultFont";

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

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

  53.         return _INSTANCE.get();
  54.     }

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

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

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

  87.     private SwingTreeInitConfig _config;

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


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

  91.     private SwingTree( SwingTreeConfigurator configurator ) {
  92.         _config = _resolveConfiguration(configurator);
  93.         _uiScale = new LazyRef<>( () -> new UiScale(_config) );
  94.         _establishMainFont(_config);
  95.     }

  96.     private SwingTreeInitConfig _resolveConfiguration( SwingTreeConfigurator configurator ) {
  97.         try {
  98.             Objects.requireNonNull(configurator);
  99.             SwingTreeInitConfig config = configurator.configure(SwingTreeInitConfig.standard());
  100.             Objects.requireNonNull(config);
  101.             return config;
  102.         } catch (Exception ex) {
  103.             log.error("Error resolving SwingTree configuration", ex);
  104.             ex.printStackTrace();
  105.             return SwingTreeInitConfig.standard();
  106.         }
  107.     }

  108.     private static void _establishMainFont( SwingTreeInitConfig config ) {
  109.         try {
  110.             if (config.fontInstallation() == SwingTreeInitConfig.FontInstallation.HARD)
  111.                 config.defaultFont().ifPresent(font -> {
  112.                     if (font instanceof FontUIResource)
  113.                         _installFontInUIManager((FontUIResource) font);
  114.                     else
  115.                         _installFontInUIManager(new FontUIResource(font));
  116.                 });
  117.         } catch (Exception ex) {
  118.             log.error("Error installing font in UIManager", ex);
  119.             ex.printStackTrace();
  120.         }
  121.     }

  122.     private static void _installFontInUIManager(javax.swing.plaf.FontUIResource f){
  123.         Enumeration<Object> keys = UIManager.getDefaults().keys();
  124.         while ( keys.hasMoreElements() ) {
  125.             Object key = keys.nextElement();
  126.             Object value = UIManager.get (key);
  127.             if ( value instanceof javax.swing.plaf.FontUIResource )
  128.                 UIManager.put(key, f);
  129.         }
  130.     }

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

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

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

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

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

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

  238.     /**
  239.      * Returns the system scale factor for the given graphics context.
  240.      * The system scale factor is the scale factor that the JRE uses
  241.      * to scale everything (text, icons, gaps, etc).
  242.      *
  243.      * @param g The graphics context to get the system scale factor for.
  244.      * @return The system scale factor for the given graphics context.
  245.      */
  246.     public double getSystemScaleFactorOf( Graphics2D g ) {
  247.         return UiScale._getSystemScaleFactorOf(g);
  248.     }

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

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

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

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

  300.     /**
  301.      * Sets the {@link StyleSheet} that is used to style components.
  302.      * Use {@link StyleSheet#none()} instead of null to switch off global styling.
  303.      * @param styleSheet The {@link StyleSheet} that is used to style components.
  304.      * @throws NullPointerException if styleSheet is null!
  305.      */
  306.     public void setStyleSheet( StyleSheet styleSheet ) {
  307.         try {
  308.             _config = _config.styleSheet(Objects.requireNonNull(styleSheet));
  309.         } catch ( Exception ex ) {
  310.             log.error("Error setting style sheet", ex);
  311.             ex.printStackTrace();
  312.         }
  313.     }

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

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

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

  362.     /**
  363.      * This class handles scaling in SwingTree UIs.
  364.      * It computes user scaling factor based on font size and
  365.      * provides methods to scale integer, float, {@link Dimension} and {@link Insets}.
  366.      * This class is look and feel independent.
  367.      * <p>
  368.      * Two scaling modes are supported by SwingTree for HiDPI displays:
  369.      * <p>
  370.      * <h2>1) system scaling mode</h2>
  371.      *
  372.      * This mode is supported since Java 9 on all platforms and in some Java 8 VMs
  373.      * (e.g. Apple and JetBrains). The JRE determines the scale factor per-display and
  374.      * adds a scaling transformation to the graphics object.
  375.      * E.g. invokes {@code java.awt.Graphics2D.scale( 1.5, 1.5 )} for 150%.
  376.      * So the JRE does the scaling itself.
  377.      * E.g. when you draw a 10px line, a 15px line is drawn on screen.
  378.      * The scale factor may be different for each connected display.
  379.      * The scale factor may change for a window when moving the window from one display to another one.
  380.      * <p>
  381.      * <h2>2) user scaling mode</h2>
  382.      *
  383.      * This mode is mainly for Java 8 compatibility, but is also used on Linux
  384.      * or if the default font is changed.
  385.      * The user scale factor is computed based on the used font.
  386.      * The JRE does not scale anything.
  387.      * So we have to invoke {@link UI#scale(float)} where necessary.
  388.      * There is only one user scale factor for all displays.
  389.      * The user scale factor may change if the active LaF, "defaultFont" or "Label.font" has changed.
  390.      * If system scaling mode is available the user scale factor is usually 1,
  391.      * but may be larger on Linux or if the default font is changed.
  392.      *
  393.      * @author Daniel Nepp, but a derivative work originally from Karl Tauber (com.formdev.flatlaf.util.UIScale)
  394.      */
  395.     static final class UiScale
  396.     {
  397.         private final SwingTreeInitConfig config;
  398.         private @Nullable PropertyChangeSupport changeSupport;

  399.         private static @Nullable Boolean jreHiDPI;

  400.         private float scaleFactor = 1;
  401.         private boolean initialized;


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

  410.                 if ( config.scalingStrategy() == SwingTreeInitConfig.Scaling.NONE ) {
  411.                     this.scaleFactor = 1;
  412.                     this.initialized = true;
  413.                     return;
  414.                 }

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

  422.                 if ( config.scalingStrategy() == SwingTreeInitConfig.Scaling.FROM_SYSTEM_FONT ) {
  423.                     float defaultScale = this.scaleFactor;
  424.                     Font highDPIFont = _calculateDPIAwarePlatformFont();
  425.                     boolean updated = _initialize( highDPIFont );
  426.                     if ( this.scaleFactor != defaultScale ) {
  427.                         UIManager.getDefaults().put(_DEFAULT_FONT, highDPIFont);
  428.                         log.debug("Setting default font ('{}') to in UIManager to {}", _DEFAULT_FONT, highDPIFont);
  429.                     }
  430.                     if ( updated )
  431.                         _setScalePropertyListeners();
  432.                 }
  433.                 else
  434.                     _initialize();

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

  441.         private Font _calculateDPIAwarePlatformFont()
  442.         {
  443.             FontUIResource dpiAwareFont = null;

  444.             // determine UI font based on operating system
  445.             if( SystemInfo.isWindows ) {
  446.                 Font winFont = (Font) Toolkit.getDefaultToolkit().getDesktopProperty( "win.messagebox.font" );
  447.                 if( winFont != null ) {
  448.                     if( SystemInfo.isWinPE ) {
  449.                         // on WinPE use "win.defaultGUI.font", which is usually Tahoma,
  450.                         // because Segoe UI font is not available on WinPE
  451.                         Font winPEFont = (Font) Toolkit.getDefaultToolkit().getDesktopProperty( "win.defaultGUI.font" );
  452.                         if ( winPEFont != null )
  453.                             dpiAwareFont = _createCompositeFont( winPEFont.getFamily(), winPEFont.getStyle(), winFont.getSize() );
  454.                     }
  455.                     else
  456.                         dpiAwareFont = _createCompositeFont( winFont.getFamily(), winFont.getStyle(), winFont.getSize() );
  457.                 }

  458.             } else if ( SystemInfo.isMacOS ) {
  459.                 String fontName;
  460.                 if( SystemInfo.isMacOS_10_15_Catalina_orLater ) {
  461.                     if ( SystemInfo.isJetBrainsJVM_11_orLater ) {
  462.                         // See https://youtrack.jetbrains.com/issue/JBR-1915
  463.                         fontName = ".AppleSystemUIFont";
  464.                     } else {
  465.                         // use Helvetica Neue font
  466.                         fontName = "Helvetica Neue";
  467.                     }
  468.                 } else if( SystemInfo.isMacOS_10_11_ElCapitan_orLater ) {
  469.                     // use San Francisco Text font
  470.                     fontName = ".SF NS Text";
  471.                 } else {
  472.                     // default font on older systems (see com.apple.laf.AquaFonts)
  473.                     fontName = "Lucida Grande";
  474.                 }

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

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

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

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

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

  503.             return dpiAwareFont;
  504.         }

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

  512.         public void addPropertyChangeListener( PropertyChangeListener listener ) {
  513.             if( changeSupport == null )
  514.                 changeSupport = new PropertyChangeSupport( UiScale.class );
  515.             changeSupport.addPropertyChangeListener( listener );
  516.         }

  517.         public void removePropertyChangeListener( PropertyChangeListener listener ) {
  518.             if( changeSupport == null )
  519.                 return;
  520.             changeSupport.removePropertyChangeListener( listener );
  521.         }

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

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

  533.             jreHiDPI = false;

  534.             if ( SystemInfo.isJava_9_orLater ) {
  535.                 // Java 9 and later supports per-monitor scaling
  536.                 jreHiDPI = true;
  537.             } else if ( SystemInfo.isJetBrainsJVM ) {
  538.                 // IntelliJ IDEA ships its own JetBrains Java 8 JRE that may support per-monitor scaling
  539.                 // see com.intellij.ui.JreHiDpiUtil.isJreHiDPIEnabled()
  540.                 try {
  541.                     GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
  542.                     Class<?> sunGeClass = Class.forName( "sun.java2d.SunGraphicsEnvironment" );
  543.                     if( sunGeClass.isInstance( ge ) ) {
  544.                         Method m = sunGeClass.getDeclaredMethod( "isUIScaleOn" );
  545.                         jreHiDPI = (Boolean) m.invoke( ge );
  546.                     }
  547.                 } catch( Throwable ex ) {
  548.                     // ignore
  549.                 }
  550.             }

  551.             return jreHiDPI;
  552.         }

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

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

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

  572.         private void _initialize()
  573.         {
  574.             boolean updated = _initialize( _getDefaultFont() );
  575.             if ( updated )
  576.                 _setScalePropertyListeners();
  577.         }

  578.         private boolean _initialize( Font uiScaleReferenceFont )
  579.         {
  580.             if ( initialized )
  581.                 return false;

  582.             initialized = true;

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

  585.             _setUserScaleFactor( _calculateScaleFactor( uiScaleReferenceFont ) );

  586.             return true;
  587.         }

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

  598.                             _setUserScaleFactor( _calculateScaleFactor( _getDefaultFont() ) );
  599.                             break;

  600.                         case _DEFAULT_FONT:
  601.                         case "Label.font":
  602.                             _setUserScaleFactor( _calculateScaleFactor( _getDefaultFont() ) );
  603.                             break;
  604.                     }
  605.                 }
  606.             };
  607.             UIManager.addPropertyChangeListener( listener );
  608.             UIManager.getDefaults().addPropertyChangeListener( listener );
  609.             UIManager.getLookAndFeelDefaults().addPropertyChangeListener( listener );
  610.         }

  611.         private Font _getDefaultFont() {
  612.             // use font size to calculate scale factor (instead of DPI)
  613.             // because even if we are on a HiDPI display it is not sure
  614.             // that a larger font size is set by the current LaF
  615.             // (e.g. can avoid large icons with small text)
  616.             Font font = UIManager.getFont( _DEFAULT_FONT );
  617.             if ( font == null )
  618.                 font = UIManager.getFont( "Label.font" );

  619.             return font;
  620.         }

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

  632.             return _normalize(_internalComputeScaleFactorFrom( font ) );
  633.         }

  634.         /**
  635.          * Computes the scale factor based on the given font.
  636.          * @param font font to compute scale factor from
  637.          * @return scale factor
  638.          */
  639.         private float _internalComputeScaleFactorFrom( Font font ) {
  640.             if ( SystemInfo.isWindows ) {
  641.                 // Special handling for Windows to be compatible with OS scaling,
  642.                 // which distinguish between "screen scaling" and "text scaling".
  643.                 //  - Windows "screen scaling" scales everything (text, icon, gaps, etc)
  644.                 //    and may have different scaling factors for each screen.
  645.                 //  - Windows "text scaling" increases only the font size, but on all screens.
  646.                 //
  647.                 // Both can be changed by the user in the Windows 10 Settings:
  648.                 //  - Settings > Display > Scale and layout
  649.                 //  - Settings > Ease of Access > Display > Make text bigger (100% - 225%)
  650.                 if( font instanceof UIResource) {
  651.                     Font uiFont = (Font) Toolkit.getDefaultToolkit().getDesktopProperty( "win.messagebox.font" );
  652.                     if( uiFont == null || uiFont.getSize() == font.getSize() ) {
  653.                         if( _isSystemScalingEnabled() ) {
  654.                             // Do not apply own scaling if the JRE scales using Windows screen scale factor.
  655.                             // If user increases font size in Windows 10 settings, desktop property
  656.                             // "win.messagebox.font" is changed and SwingTree uses the larger font.
  657.                             return 1;
  658.                         } else {
  659.                             // If the JRE does not scale (Java 8), the size of the UI font
  660.                             // (usually from desktop property "win.messagebox.font")
  661.                             // combines the Windows screen and text scale factors.
  662.                             // But the font in desktop property "win.defaultGUI.font" is only
  663.                             // scaled with the Windows screen scale factor. So use it to compute
  664.                             // our scale factor that is equal to Windows screen scale factor.
  665.                             Font winFont = (Font) Toolkit.getDefaultToolkit().getDesktopProperty( "win.defaultGUI.font" );
  666.                             return _computeScaleFactorFromFontSize( (winFont != null) ? winFont : font );
  667.                         }
  668.                     }
  669.                 }

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

  677.             return _computeScaleFactorFromFontSize( font );
  678.         }

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

  682.             if( SystemInfo.isWindows ) {
  683.                 // Windows LaF uses Tahoma font rather than the actual Windows system font (Segoe UI),
  684.                 // and its size is always ca. 10% smaller than the actual system font size.
  685.                 // Tahoma 11 is used at 100%
  686.                 if( "Tahoma".equals( font.getFamily() ) )
  687.                     fontSizeDivider = 11f;
  688.             } else if( SystemInfo.isMacOS ) {
  689.                 // default font size on macOS is 13
  690.                 fontSizeDivider = 13f;
  691.             } else if( SystemInfo.isLinux ) {
  692.                 // default font size for Unity and Gnome is 15 and for KDE it is 13
  693.                 fontSizeDivider = SystemInfo.isKDE ? 13f : 15f;
  694.             }

  695.             return font.getSize() / fontSizeDivider;
  696.         }

  697.         /**
  698.          * Applies a custom scale factor given in system property "swingtree.uiScale"
  699.          * to the given font.
  700.          */
  701.         private FontUIResource _applyCustomScaleFactor( FontUIResource font )
  702.         {
  703.             if ( !config.isUiScaleFactorEnabled() )
  704.                 return font;

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

  708.             float fontScaleFactor = _computeScaleFactorFromFontSize( font );
  709.             if ( scaleFactor == fontScaleFactor )
  710.                 return font;

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

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

  735.         public void setUserScaleFactor( float scaleFactor ) {
  736.             _initialize();
  737.             _setUserScaleFactor( _normalize(scaleFactor) );
  738.         }

  739.         private float _normalize( float scaleFactor ) {
  740.             if ( scaleFactor < 1f ) {
  741.                 scaleFactor = config.isUiScaleDownAllowed()
  742.                         ? Math.round( scaleFactor * 10f ) / 10f // round small scale factor to 1/10
  743.                         : 1f;
  744.             }
  745.             else if ( scaleFactor > 1f ) // round scale factor to 1/4
  746.                 scaleFactor = Math.round( scaleFactor * 4f ) / 4f;

  747.             return scaleFactor;
  748.         }

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

  755.             float oldScaleFactor = this.scaleFactor;
  756.             this.scaleFactor = scaleFactor;

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

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

  768.             GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment()
  769.                                                           .getDefaultScreenDevice()
  770.                                                           .getDefaultConfiguration();

  771.             return UiScale._getSystemScaleFactorOf( gc ) > 1;
  772.         }

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

  776.             return UiScale._getSystemScaleFactorOf(
  777.                                 GraphicsEnvironment.getLocalGraphicsEnvironment()
  778.                                                    .getDefaultScreenDevice()
  779.                                                    .getDefaultConfiguration()
  780.                             );
  781.         }

  782.     }

  783.     /**
  784.      * Provides information about the current system.
  785.      *
  786.      * @author Daniel Nepp, but originally a derivative work of
  787.      *         Karl Tauber (com.formdev.flatlaf.util.SystemInfo)
  788.      */
  789.     private static final class SystemInfo
  790.     {
  791.         // platforms
  792.         public static final boolean isWindows;
  793.         public static final boolean isMacOS;
  794.         public static final boolean isLinux;

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

  805.         // OS architecture
  806.         public static final boolean isX86;
  807.         public static final boolean isX86_64;
  808.         public static final boolean isAARCH64;

  809.         // Java versions
  810.         public static final long javaVersion;
  811.         public static final boolean isJava_9_orLater;
  812.         public static final boolean isJava_11_orLater;
  813.         public static final boolean isJava_15_orLater;
  814.         public static final boolean isJava_17_orLater;
  815.         public static final boolean isJava_18_orLater;

  816.         // Java VMs
  817.         public static final boolean isJetBrainsJVM;
  818.         public static final boolean isJetBrainsJVM_11_orLater;

  819.         // UI toolkits
  820.         public static final boolean isKDE;

  821.         // other
  822.         public static final boolean isProjector;
  823.         public static final boolean isWebswing;
  824.         public static final boolean isWinPE;

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

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

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

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

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

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

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

  862.         public static long scanVersion( String version ) {
  863.             int major = 1;
  864.             int minor = 0;
  865.             int micro = 0;
  866.             int patch = 0;
  867.             try {
  868.                 StringTokenizer st = new StringTokenizer( version, "._-+" );
  869.                 major = Integer.parseInt( st.nextToken() );
  870.                 minor = Integer.parseInt( st.nextToken() );
  871.                 micro = Integer.parseInt( st.nextToken() );
  872.                 patch = Integer.parseInt( st.nextToken() );
  873.             } catch( Exception ex ) {
  874.                 // ignore
  875.             }

  876.             return toVersion( major, minor, micro, patch );
  877.         }

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

  881.         static String getAsPrettyString() {
  882.             return SystemInfo.class.getSimpleName() + "[\n" +
  883.                     "    isWindows=" + isWindows + ",\n" +
  884.                     "    isMacOS=" + isMacOS + ",\n" +
  885.                     "    isLinux=" + isLinux + ",\n" +
  886.                     "    osVersion=" + osVersion + ",\n" +
  887.                     "    isWindows_10_orLater=" + isWindows_10_orLater + ",\n" +
  888.                     "    isWindows_11_orLater=" + isWindows_11_orLater + ",\n" +
  889.                     "    isMacOS_10_11_ElCapitan_orLater=" + isMacOS_10_11_ElCapitan_orLater + ",\n" +
  890.                     "    isMacOS_10_14_Mojave_orLater=" + isMacOS_10_14_Mojave_orLater + ",\n" +
  891.                     "    isMacOS_10_15_Catalina_orLater=" + isMacOS_10_15_Catalina_orLater + ",\n" +
  892.                     "    isX86=" + isX86 + ",\n" +
  893.                     "    isX86_64=" + isX86_64 + ",\n" +
  894.                     "    isAARCH64=" + isAARCH64 + ",\n" +
  895.                     "    javaVersion=" + javaVersion + ",\n" +
  896.                     "    isJava_9_orLater=" + isJava_9_orLater + ",\n" +
  897.                     "    isJava_11_orLater=" + isJava_11_orLater + ",\n" +
  898.                     "    isJava_15_orLater=" + isJava_15_orLater + ",\n" +
  899.                     "    isJava_17_orLater=" + isJava_17_orLater + ",\n" +
  900.                     "    isJava_18_orLater=" + isJava_18_orLater + ",\n" +
  901.                     "    isJetBrainsJVM=" + isJetBrainsJVM + ",\n" +
  902.                     "    isJetBrainsJVM_11_orLater=" + isJetBrainsJVM_11_orLater + ",\n" +
  903.                     "    isKDE=" + isKDE + ",\n" +
  904.                     "    isProjector=" + isProjector + ",\n" +
  905.                     "    isWebswing=" + isWebswing + ",\n" +
  906.                     "    isWinPE=" + isWinPE + "\n" +
  907.                     "]\n";
  908.         }
  909.     }

  910.     private static class LinuxFontPolicy
  911.     {
  912.         static Font getFont() {
  913.             return SystemInfo.isKDE ? getKDEFont() : getGnomeFont();
  914.         }

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

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

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

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

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

  934.                 String lword = word.toLowerCase(Locale.ENGLISH);
  935.                 if( lword.equals( "italic" ) || lword.equals( "oblique" ) )
  936.                     style |= Font.ITALIC;
  937.                 else if( lword.equals( "bold" ) )
  938.                     style |= Font.BOLD;
  939.                 else if( Character.isDigit( word.charAt( 0 ) ) ) {
  940.                     try {
  941.                         dsize = Double.parseDouble( word );
  942.                     } catch( NumberFormatException ex ) {
  943.                         // ignore
  944.                     }
  945.                 } else {
  946.                     // remove '-' from "Semi-Bold", "Extra-Light", etc
  947.                     if( lword.startsWith( "semi-" ) || lword.startsWith( "demi-" ) )
  948.                         word = word.substring( 0, 4 ) + word.substring( 5 );
  949.                     else if( lword.startsWith( "extra-" ) || lword.startsWith( "ultra-" ) )
  950.                         word = word.substring( 0, 5 ) + word.substring( 6 );

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

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

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

  959.             // scale font size
  960.             dsize *= getGnomeFontScale();
  961.             int size = (int) (dsize + 0.5);
  962.             if( size < 1 )
  963.                 size = 1;

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

  968.             return createFontEx( family, style, size, dsize );
  969.         }

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

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

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

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

  999.                     return font;
  1000.                 }

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

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

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

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

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

  1017.             return font;
  1018.         }

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

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

  1024.             Object value = Toolkit.getDefaultToolkit().getDesktopProperty( "gnome.Xft/DPI" );
  1025.             if( value instanceof Integer ) {
  1026.                 int dpi = (Integer) value / 1024;
  1027.                 if( dpi == -1 )
  1028.                     dpi = 96;
  1029.                 if( dpi < 50 )
  1030.                     dpi = 50;
  1031.                 return dpi / 72.0;
  1032.             } else {
  1033.                 return GraphicsEnvironment.getLocalGraphicsEnvironment()
  1034.                                           .getDefaultScreenDevice()
  1035.                                           .getDefaultConfiguration()
  1036.                                           .getNormalizingTransform()
  1037.                                           .getScaleY();
  1038.             }
  1039.         }

  1040.         /**
  1041.          * map GTK/fontconfig names to equivalent JDK logical font name
  1042.          */
  1043.         private static @Nullable String mapFcName( String name ) {
  1044.             switch( name ) {
  1045.                 case "sans":        return "sansserif";
  1046.                 case "sans-serif":  return "sansserif";
  1047.                 case "serif":       return "serif";
  1048.                 case "monospace":   return "monospaced";
  1049.             }
  1050.             return null;
  1051.         }

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

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

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

  1072.             if ( generalFont != null ) {
  1073.                 List<String> strs = StringUtils.split( generalFont, ',' );
  1074.                 try {
  1075.                     family = strs.get( 0 );
  1076.                     size = Integer.parseInt( strs.get( 1 ) );
  1077.                     if( "75".equals( strs.get( 4 ) ) )
  1078.                         style |= Font.BOLD;
  1079.                     if( "1".equals( strs.get( 5 ) ) )
  1080.                         style |= Font.ITALIC;
  1081.                 } catch ( RuntimeException ex ) {
  1082.                     log.error("Failed to parse KDE system font from String '"+generalFont+"'.", ex);
  1083.                 }
  1084.             }

  1085.             // font dpi
  1086.             int dpi = 96;
  1087.             if( forceFontDPI != null && !UiScale._isSystemScaling() ) {
  1088.                 try {
  1089.                     dpi = Integer.parseInt( forceFontDPI );
  1090.                     if( dpi <= 0 )
  1091.                         dpi = 96;
  1092.                     if( dpi < 50 )
  1093.                         dpi = 50;
  1094.                 } catch( NumberFormatException ex ) {
  1095.                     log.error("Failed to parse DPI scale from font size String '"+forceFontDPI+"'", ex);
  1096.                 }
  1097.             }

  1098.             // scale font size
  1099.             double fontScale = dpi / 72.0;
  1100.             double dsize = size * fontScale;
  1101.             size = (int) (dsize + 0.5);
  1102.             if( size < 1 )
  1103.                 size = 1;

  1104.             return createFont( family, style, size, dsize );
  1105.         }

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

  1108.             // search for config file
  1109.             String[] configDirs = {
  1110.                     ".config", // KDE 5
  1111.                     ".kde4/share/config", // KDE 4
  1112.                     ".kde/share/config"// KDE 3
  1113.             };
  1114.             File file = null;
  1115.             for( String configDir : configDirs ) {
  1116.                 file = new File( userHome, configDir + "/" + filename );
  1117.                 if( file.isFile() )
  1118.                     break;
  1119.             }
  1120.             if( file == null || !file.isFile() )
  1121.                 return Collections.emptyList();

  1122.             // read config file
  1123.             ArrayList<String> lines = new ArrayList<>( 200 );
  1124.             try( BufferedReader reader = Files.newBufferedReader(file.toPath(), UTF_8) ) {
  1125.                 String line;
  1126.                 while( (line = reader.readLine()) != null )
  1127.                     lines.add( line );
  1128.             } catch( IOException ex ) {
  1129.                 log.error("Failed to read KDE font config files for determining DPI scale factor.", ex);
  1130.             }
  1131.             return Collections.unmodifiableList(lines);
  1132.         }

  1133.         private static @Nullable String getConfigEntry( List<String> config, String group, String key ) {
  1134.             int groupLength = group.length();
  1135.             int keyLength = key.length();
  1136.             boolean inGroup = false;
  1137.             for( String line : config ) {
  1138.                 if( !inGroup ) {
  1139.                     if( line.length() >= groupLength + 2 &&
  1140.                             line.charAt( 0 ) == '[' &&
  1141.                             line.charAt( groupLength + 1 ) == ']' &&
  1142.                             line.indexOf( group ) == 1 )
  1143.                     {
  1144.                         inGroup = true;
  1145.                     }
  1146.                 } else {
  1147.                     if( line.startsWith( "[" ) )
  1148.                         return null;

  1149.                     if( line.length() >= keyLength + 2 &&
  1150.                             line.charAt( keyLength ) == '=' &&
  1151.                             line.startsWith( key ) )
  1152.                     {
  1153.                         return line.substring( keyLength + 1 );
  1154.                     }
  1155.                 }
  1156.             }
  1157.             return null;
  1158.         }

  1159.     }

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

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

  1166.         T get() {
  1167.             @Nullable T value = this.value;
  1168.             if( value == null ) {
  1169.                 synchronized( this ) {
  1170.                     value = this.value;
  1171.                     if ( value == null )
  1172.                         this.value = value = supplier.get();
  1173.                 }
  1174.             }
  1175.             return value;
  1176.         }
  1177.     }
  1178. }