- support key prefixes for Linux desktop environments (issue #974)

- support custom key prefixes (issue #649)
- support multi-prefixed keys
- changed handling of prefixed keys
This commit is contained in:
Karl Tauber
2025-03-08 18:11:38 +01:00
parent babc8aa55d
commit f7495a0a5b
11 changed files with 218 additions and 79 deletions

View File

@@ -31,6 +31,12 @@ FlatLaf Change Log
- Updated to latest versions and fixed various issues.
- Support customizing through properties files. (issue #824)
- SwingX: Support `JXTipOfTheDay` component. (issue #980)
- Support key prefixes for Linux desktop environments (e.g. `[gnome]`, `[kde]`
or `[xfce]`) in properties files. (issue #974)
- Support custom key prefixes (e.g. `[win10]` or `[test]`) in properties files.
(issue #649)
- Support multi-prefixed keys (e.g. `[dark][gnome]TitlePane.buttonBackground`).
The value is only used if all prefixes match current platform/theme.
#### Fixed bugs
@@ -66,6 +72,23 @@ FlatLaf Change Log
`com.formdev.flatlaf.intellijthemes.materialthemeuilite` from `Flat<theme>`
to `FlatMT<theme>`.
- Removed `Gruvbox Dark Medium` and `Gruvbox Dark Soft` themes.
- Prefixed keys in properties files (e.g. `[dark]Button.background` or
`[win]Button.arc`) are now handled earlier than before. In previous versions,
prefixed keys always had higher priority than unprefixed keys and did always
overwrite unprefixed keys. Now prefixed keys are handled in same order as
unprefixed keys, which means that if a key is prefixed and unprefixed (e.g.
`[win]Button.arc` and `Button.arc`), the one which is last specified in
properties file is used.\
Following worked in previous versions, but now `Button.arc` is always `6`:
~~~properties
[win]Button.arc = 12
Button.arc = 6
~~~
This works in new (and old) versions:
~~~properties
Button.arc = 6
[win]Button.arc = 12
~~~
## 3.5.4

View File

@@ -37,6 +37,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -44,6 +45,7 @@ import java.util.MissingResourceException;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntUnaryOperator;
@@ -100,6 +102,8 @@ public abstract class FlatLaf
private static Map<String, String> globalExtraDefaults;
private Map<String, String> extraDefaults;
private static Function<String, Color> systemColorGetter;
private static Set<String> uiKeyPlatformPrefixes;
private static Set<String> uiKeySpecialPrefixes;
private String desktopPropertyName;
private String desktopPropertyName2;
@@ -1122,6 +1126,92 @@ public abstract class FlatLaf
FlatLaf.systemColorGetter = systemColorGetter;
}
/**
* Returns UI key prefix, used in FlatLaf properties files, for light or dark themes.
* Return value is either {@code [light]} or {@code [dark]}.
*
* @since 3.6
*/
public static String getUIKeyLightOrDarkPrefix( boolean dark ) {
return dark ? "[dark]" : "[light]";
}
/**
* Returns set of UI key prefixes, used in FlatLaf properties files, for current platform.
* If UI keys in properties files start with a prefix (e.g. {@code [someprefix]Button.background}),
* then they are only used if that prefix is contained in this set
* (or is one of {@code [light]} or {@code [dark]} depending on current theme).
* <p>
* By default, the set contains one or more of following prefixes:
* <ul>
* <li>{@code [win]} on Windows
* <li>{@code [mac]} on macOS
* <li>{@code [linux]} on Linux
* <li>{@code [unknown]} on other platforms
* <li>{@code [gnome]} on Linux with GNOME desktop environment
* <li>{@code [kde]} on Linux with KDE desktop environment
* <li>on Linux, the value of the environment variable {@code XDG_CURRENT_DESKTOP},
* split at colons and converted to lower case (e.g. if value of {@code XDG_CURRENT_DESKTOP}
* is {@code ubuntu:GNOME}, then {@code [ubuntu]} and {@code [gnome]})
* </ul>
* <p>
* You can add own prefixes to the set.
* The prefixes must start with '[' and end with ']' characters, otherwise they will be ignored.
*
* @since 3.6
*/
public static Set<String> getUIKeyPlatformPrefixes() {
if( uiKeyPlatformPrefixes == null ) {
uiKeyPlatformPrefixes = new HashSet<>();
uiKeyPlatformPrefixes.add(
SystemInfo.isWindows ? "[win]" :
SystemInfo.isMacOS ? "[mac]" :
SystemInfo.isLinux ? "[linux]" : "[unknown]" );
// Linux
if( SystemInfo.isLinux ) {
if( SystemInfo.isGNOME )
uiKeyPlatformPrefixes.add( "[gnome]" );
else if( SystemInfo.isKDE )
uiKeyPlatformPrefixes.add( "[kde]" );
// add values from XDG_CURRENT_DESKTOP for other desktops
String desktop = System.getenv( "XDG_CURRENT_DESKTOP" );
if( desktop != null ) {
// XDG_CURRENT_DESKTOP is a colon-separated list of strings
// https://specifications.freedesktop.org/desktop-entry-spec/latest/recognized-keys.html#key-onlyshowin
// e.g. "ubuntu:GNOME" on Ubuntu 24.10 or "GNOME-Classic:GNOME" on CentOS 7
for( String desk : StringUtils.split( desktop.toLowerCase( Locale.ENGLISH ), ':', true, true ) )
uiKeyPlatformPrefixes.add( '[' + desk + ']' );
}
}
}
return uiKeyPlatformPrefixes;
}
/**
* Returns set of special UI key prefixes, used in FlatLaf properties files.
* Unlike other prefixes, properties with special prefixes are preserved.
* You can access them using `UIManager`. E.g. `UIManager.get( "[someSpecialPrefix]someKey" )`.
* <p>
* By default, the set contains following special prefixes:
* <ul>
* <li>{@code [style]}
* </ul>
* <p>
* You can add own prefixes to the set.
* The prefixes must start with '[' and end with ']' characters, otherwise they will be ignored.
*
* @since 3.6
*/
public static Set<String> getUIKeySpecialPrefixes() {
if( uiKeySpecialPrefixes == null ) {
uiKeySpecialPrefixes = new HashSet<>();
uiKeySpecialPrefixes.add( "[style]" );
}
return uiKeySpecialPrefixes;
}
private static void reSetLookAndFeel() {
EventQueue.invokeLater( () -> {
LookAndFeel lookAndFeel = UIManager.getLookAndFeel();

View File

@@ -41,6 +41,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.swing.Icon;
@@ -62,7 +63,6 @@ import com.formdev.flatlaf.util.HSLColor;
import com.formdev.flatlaf.util.LoggingFacade;
import com.formdev.flatlaf.util.SoftCache;
import com.formdev.flatlaf.util.StringUtils;
import com.formdev.flatlaf.util.SystemInfo;
import com.formdev.flatlaf.util.UIScale;
/**
@@ -105,6 +105,39 @@ class UIDefaultsLoader
return lafClasses;
}
static Properties newUIProperties( boolean dark ) {
// UI key prefixes
String lightOrDarkPrefix = FlatLaf.getUIKeyLightOrDarkPrefix( dark );
Set<String> platformPrefixes = FlatLaf.getUIKeyPlatformPrefixes();
Set<String> specialPrefixes = FlatLaf.getUIKeySpecialPrefixes();
return new Properties() {
@Override
public synchronized Object put( Object k, Object value ) {
// process key prefixes (while loading properties files)
String key = (String) k;
while( key.startsWith( "[" ) ) {
int closeIndex = key.indexOf( ']' );
if( closeIndex < 0 )
return null; // ignore property with invalid prefix
String prefix = key.substring( 0, closeIndex + 1 );
if( specialPrefixes.contains( prefix ) )
break; // keep special prefix
if( !lightOrDarkPrefix.equals( prefix ) && !platformPrefixes.contains( prefix ) )
return null; // ignore property
// prefix is known and enabled --> remove prefix
key = key.substring( closeIndex + 1 );
}
return super.put( key, value );
}
};
}
static void loadDefaultsFromProperties( List<Class<?>> lafClasses, List<FlatDefaultsAddon> addons,
Consumer<Properties> intellijThemesHook, Properties additionalDefaults, boolean dark, UIDefaults defaults )
{
@@ -113,8 +146,10 @@ class UIDefaultsLoader
// which avoids that system color getter is invoked multiple times
systemColorCache = (FlatLaf.getSystemColorGetter() != null) ? new HashMap<>() : null;
// all properties files will be loaded into this map
Properties properties = newUIProperties( dark );
// load core properties files
Properties properties = new Properties();
for( Class<?> lafClass : lafClasses ) {
String propertiesName = '/' + lafClass.getName().replace( '.', '/' ) + ".properties";
try( InputStream in = lafClass.getResourceAsStream( propertiesName ) ) {
@@ -201,41 +236,6 @@ class UIDefaultsLoader
if( additionalDefaults != null )
properties.putAll( additionalDefaults );
// collect all platform specific keys (but do not modify properties)
ArrayList<String> platformSpecificKeys = new ArrayList<>();
for( Object okey : properties.keySet() ) {
String key = (String) okey;
if( key.startsWith( "[" ) &&
(key.startsWith( "[win]" ) ||
key.startsWith( "[mac]" ) ||
key.startsWith( "[linux]" ) ||
key.startsWith( "[light]" ) ||
key.startsWith( "[dark]" )) )
platformSpecificKeys.add( key );
}
// remove platform specific properties and re-add only properties
// for current platform, but with platform prefix removed
if( !platformSpecificKeys.isEmpty() ) {
// handle light/dark specific properties
String lightOrDarkPrefix = dark ? "[dark]" : "[light]";
for( String key : platformSpecificKeys ) {
if( key.startsWith( lightOrDarkPrefix ) )
properties.put( key.substring( lightOrDarkPrefix.length() ), properties.remove( key ) );
}
// handle platform specific properties
String platformPrefix =
SystemInfo.isWindows ? "[win]" :
SystemInfo.isMacOS ? "[mac]" :
SystemInfo.isLinux ? "[linux]" : "[unknown]";
for( String key : platformSpecificKeys ) {
Object value = properties.remove( key );
if( key.startsWith( platformPrefix ) )
properties.put( key.substring( platformPrefix.length() ), value );
}
}
// get (and remove) wildcard replacements, which override all other defaults that end with same suffix
HashMap<String, String> wildcards = new HashMap<>();
Iterator<Entry<Object, Object>> it = properties.entrySet().iterator();

View File

@@ -42,7 +42,6 @@ import com.formdev.flatlaf.FlatClientProperties;
import com.formdev.flatlaf.FlatLaf;
import com.formdev.flatlaf.util.HiDPIUtils;
import com.formdev.flatlaf.util.StringUtils;
import com.formdev.flatlaf.util.SystemInfo;
/**
* Support for styling components in CSS syntax.
@@ -325,22 +324,24 @@ public class FlatStylingSupport
return null;
Map<String, Object> oldValues = new HashMap<>();
outer:
for( Map.Entry<String, Object> e : style.entrySet() ) {
String key = e.getKey();
Object newValue = e.getValue();
// handle key prefix
if( key.startsWith( "[" ) ) {
if( (SystemInfo.isWindows && key.startsWith( "[win]" )) ||
(SystemInfo.isMacOS && key.startsWith( "[mac]" )) ||
(SystemInfo.isLinux && key.startsWith( "[linux]" )) ||
(key.startsWith( "[light]" ) && !FlatLaf.isLafDark()) ||
(key.startsWith( "[dark]" ) && FlatLaf.isLafDark()) )
{
// prefix is known and enabled --> remove prefix
key = key.substring( key.indexOf( ']' ) + 1 );
} else
continue;
while( key.startsWith( "[" ) ) {
int closeIndex = key.indexOf( ']' );
if( closeIndex < 0 )
continue outer;
String prefix = key.substring( 0, closeIndex + 1 );
String lightOrDarkPrefix = FlatLaf.getUIKeyLightOrDarkPrefix( FlatLaf.isLafDark() );
if( !lightOrDarkPrefix.equals( prefix ) && !FlatLaf.getUIKeyPlatformPrefixes().contains( prefix ) )
continue outer;
// prefix is known and enabled --> remove prefix
key = key.substring( closeIndex + 1 );
}
Object oldValue = applyProperty.apply( key, newValue );

View File

@@ -31,6 +31,7 @@ public class SystemInfo
public static final boolean isWindows;
public static final boolean isMacOS;
public static final boolean isLinux;
/** @since 3.6 */ public static final boolean isUnknownOS;
// OS versions
public static final long osVersion;
@@ -59,6 +60,7 @@ public class SystemInfo
public static final boolean isJetBrainsJVM_11_orLater;
// UI toolkits
/** @since 3.6 */ public static final boolean isGNOME;
public static final boolean isKDE;
// other
@@ -75,6 +77,7 @@ public class SystemInfo
isWindows = osName.startsWith( "windows" );
isMacOS = osName.startsWith( "mac" );
isLinux = osName.startsWith( "linux" );
isUnknownOS = !isWindows && !isMacOS && !isLinux;
// OS versions
osVersion = scanVersion( System.getProperty( "os.version" ) );
@@ -104,7 +107,13 @@ public class SystemInfo
isJetBrainsJVM_11_orLater = isJetBrainsJVM && isJava_11_orLater;
// UI toolkits
isKDE = (isLinux && System.getenv( "KDE_FULL_SESSION" ) != null);
String desktop = isLinux ? System.getenv( "XDG_CURRENT_DESKTOP" ) : null;
isGNOME = (isLinux &&
(System.getenv( "GNOME_DESKTOP_SESSION_ID" ) != null ||
(desktop != null && desktop.contains( "GNOME" ))));
isKDE = (isLinux &&
(System.getenv( "KDE_FULL_SESSION" ) != null ||
(desktop != null && desktop.contains( "KDE" ))));
// other
isProjector = Boolean.getBoolean( "org.jetbrains.projector.server.enable" );

View File

@@ -50,6 +50,9 @@ mini.font = -3
#defaultFont = ...
# font weights
# fallback for unknown platform
light.font = +0
semibold.font = +0
# Windows
[win]light.font = "Segoe UI Light"
[win]semibold.font = "Segoe UI Semibold"
@@ -59,15 +62,12 @@ mini.font = -3
# Linux
[linux]light.font = "Lato Light", "Ubuntu Light", "Cantarell Light"
[linux]semibold.font = "Lato Semibold", "Ubuntu Medium", "Montserrat SemiBold"
# fallback for unknown platform
light.font = +0
semibold.font = +0
# monospaced
monospaced.font = Monospaced
[win]monospaced.font = Monospaced
[mac]monospaced.font = Menlo, Monospaced
[linux]monospaced.font = "Liberation Mono", "Ubuntu Mono", Monospaced
monospaced.font = Monospaced
# styles
[style].h00 = font: $h00.font

View File

@@ -95,8 +95,8 @@ Spinner.buttonDisabledArrowColor = $ComboBox.buttonDisabledArrowColor
#---- TabbedPane ----
# colors from JBUI.CurrentTheme.DefaultTabs.inactiveUnderlineColor()
[light]TabbedPane.inactiveUnderlineColor = #9ca7b8
[dark]TabbedPane.inactiveUnderlineColor = #747a80
{*-light}TabbedPane.inactiveUnderlineColor = #9ca7b8
{*-dark}TabbedPane.inactiveUnderlineColor = #747a80
#---- ToggleButton ----

View File

@@ -17,6 +17,7 @@
package com.formdev.flatlaf;
import java.util.Collections;
import java.util.Properties;
import java.util.function.Function;
import com.formdev.flatlaf.UIDefaultsLoader.ValueType;
@@ -72,4 +73,8 @@ public class UIDefaultsLoaderAccessor
{
return UIDefaultsLoader.parseColorRGBA( value );
}
public static Properties newUIProperties( boolean dark ) {
return UIDefaultsLoader.newUIProperties( dark );
}
}

View File

@@ -41,6 +41,7 @@ import org.fife.ui.autocomplete.ParameterChoicesProvider;
import org.fife.ui.autocomplete.ParameterizedCompletion;
import org.fife.ui.autocomplete.ParameterizedCompletion.Parameter;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import com.formdev.flatlaf.FlatLaf;
/**
* @author Karl Tauber
@@ -358,12 +359,18 @@ class FlatCompletionProvider
lastKeys = keys;
completions.clear();
outer:
for( String key : keys ) {
if( key.startsWith( "[" ) ) {
while( key.startsWith( "[" ) ) {
// remove prefix
int closeIndex = key.indexOf( ']' );
if( closeIndex < 0 )
continue;
continue outer;
String prefix = key.substring( 0, closeIndex + 1 );
if( FlatLaf.getUIKeySpecialPrefixes().contains( prefix ) )
continue outer; // can not reference properties with special prefix
key = key.substring( closeIndex + 1 );
}

View File

@@ -76,14 +76,21 @@ class FlatThemePropertiesBaseManager
definedCoreKeys = new HashSet<>();
for( Properties properties : coreThemes.values() ) {
outer:
for( Object k : properties.keySet() ) {
String key = (String) k;
if( key.startsWith( "*." ) || key.startsWith( "@" ) )
continue;
if( key.startsWith( "[" ) ) {
while( key.startsWith( "[" ) ) {
int closeIndex = key.indexOf( ']' );
if( closeIndex < 0 )
continue;
continue outer;
String prefix = key.substring( 0, closeIndex + 1 );
if( FlatLaf.getUIKeySpecialPrefixes().contains( prefix ) )
break; // keep special prefix
key = key.substring( closeIndex + 1 );
}
definedCoreKeys.add( key );

View File

@@ -33,7 +33,6 @@ import javax.swing.event.DocumentListener;
import javax.swing.plaf.basic.BasicLookAndFeel;
import javax.swing.text.BadLocationException;
import com.formdev.flatlaf.UIDefaultsLoaderAccessor;
import com.formdev.flatlaf.util.SystemInfo;
/**
* Supports parsing content of text area in FlatLaf properties syntax.
@@ -54,17 +53,13 @@ class FlatThemePropertiesSupport
private final Map<String, Object> parsedValueCache2 = new HashMap<>();
private Set<String> allKeysCache;
private String baseTheme;
private boolean lastDark;
private static long globalCacheInvalidationCounter;
private long cacheInvalidationCounter;
private static Set<String> wildcardKeys;
private static final String platformPrefix =
SystemInfo.isWindows ? "[win]" :
SystemInfo.isMacOS ? "[mac]" :
SystemInfo.isLinux ? "[linux]" : "[unknown]";
FlatThemePropertiesSupport( FlatSyntaxTextArea textArea ) {
this.textArea = textArea;
@@ -115,6 +110,7 @@ class FlatThemePropertiesSupport
private KeyValue getKeyValueAtLine( int line ) {
try {
// get text at line
int startOffset = textArea.getLineStartOffset( line );
int endOffset = textArea.getLineEndOffset( line );
String text = textArea.getText( startOffset, endOffset - startOffset );
@@ -134,11 +130,13 @@ class FlatThemePropertiesSupport
text = text.substring( sepIndex + 1 );
}
// parse line
Properties properties = new Properties();
properties.load( new StringReader( text ) );
if( properties.isEmpty() )
return null;
// get key and value for line
String key = (String) properties.keys().nextElement();
String value = properties.getProperty( key );
return new KeyValue( key, value );
@@ -171,17 +169,7 @@ class FlatThemePropertiesSupport
}
private String getPropertyOrWildcard( String key ) {
// get platform specific properties
String value = getProperty( platformPrefix + key );
if( value != null )
return value;
// get light/dark specific properties
value = getProperty( (isDark( getBaseTheme() ) ? "[dark]" : "[light]") + key );
if( value != null )
return value;
value = getProperty( key );
String value = getProperty( key );
if( value != null )
return value;
@@ -213,9 +201,18 @@ class FlatThemePropertiesSupport
if( propertiesCache != null )
return propertiesCache;
propertiesCache = new Properties();
String text = textArea.getText();
try {
propertiesCache.load( new StringReader( textArea.getText() ) );
propertiesCache = UIDefaultsLoaderAccessor.newUIProperties( lastDark );
propertiesCache.load( new StringReader( text ) );
// re-load if dark has changed (getBaseTheme() invokes getProperties()!!!)
boolean dark = isDark( getBaseTheme() );
if( lastDark != dark ) {
lastDark = dark;
propertiesCache = UIDefaultsLoaderAccessor.newUIProperties( lastDark );
propertiesCache.load( new StringReader( text ) );
}
} catch( IOException ex ) {
ex.printStackTrace(); //TODO
}