GP-2874 added support for Look and Feel specific theme properties

This commit is contained in:
ghidragon
2022-11-23 16:02:38 -05:00
parent d93eb7603f
commit 2c8f82e26b
12 changed files with 260 additions and 87 deletions
@@ -27,9 +27,9 @@ import ghidra.util.Msg;
*/
public abstract class AbstractThemeReader {
private static final String NO_SECTION = "[No Section]";
private static final String DEFAULTS = "[Defaults]";
private static final String DARK_DEFAULTS = "[Dark Defaults]";
private static final String NO_SECTION = "No Section";
private static final String DEFAULTS = "Defaults";
private static final String DARK_DEFAULTS = "Dark Defaults";
private List<String> errors = new ArrayList<>();
protected String source;
@@ -60,8 +60,7 @@ public abstract class AbstractThemeReader {
processDarkDefaultSection(section);
break;
default:
error(section.getLineNumber(),
"Encounded unknown theme file section: " + section.getName());
processCustomSection(section);
}
}
@@ -73,6 +72,8 @@ public abstract class AbstractThemeReader {
protected abstract void processDarkDefaultSection(Section section) throws IOException;
protected abstract void processCustomSection(Section section) throws IOException;
protected void processValues(GThemeValueMap valueMap, Section section) {
for (String key : section.getKeys()) {
String value = section.getValue(key);
@@ -157,7 +158,8 @@ public abstract class AbstractThemeReader {
}
if (isSectionHeader(line)) {
currentSection = new Section(line, reader.getLineNumber());
String name = line.substring(1, line.length() - 1);
currentSection = new Section(name, reader.getLineNumber());
sections.add(currentSection);
}
else {
@@ -191,6 +193,10 @@ public abstract class AbstractThemeReader {
String msg =
"Error parsing theme file \"" + source + "\" at line: " + lineNumber + ", " + message;
errors.add(msg);
outputError(msg);
}
protected void outputError(String msg) {
Msg.error(this, msg);
}
@@ -0,0 +1,86 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package generic.theme;
import java.io.IOException;
import java.util.*;
import generic.jar.ResourceFile;
import ghidra.framework.Application;
import ghidra.util.Msg;
/**
* Loads all the system theme.property files that contain all the default color, font, and
* icon values.
*/
public class ApplicationThemeDefaultsProvider implements ThemeDefaultsProvider {
private GThemeValueMap defaults = new GThemeValueMap();
private GThemeValueMap darkDefaults = new GThemeValueMap();
private Map<LafType, GThemeValueMap> lafDefaultsMap = new HashMap<>();
ApplicationThemeDefaultsProvider() {
loadThemeDefaultFiles();
}
/**
* Searches for all the theme.property files and loads them into either the standard
* defaults (light) map or the dark defaults map.
*/
private void loadThemeDefaultFiles() {
defaults.clear();
darkDefaults.clear();
List<ResourceFile> themeDefaultFiles =
Application.findFilesByExtensionInApplication(".theme.properties");
for (ResourceFile resourceFile : themeDefaultFiles) {
try {
ThemePropertyFileReader reader = new ThemePropertyFileReader(resourceFile);
defaults.load(reader.getDefaultValues());
darkDefaults.load(reader.getDarkDefaultValues());
processLookAndFeelSpecificValues(reader.getLookAndFeelSections());
}
catch (IOException e) {
Msg.error(this,
"Error reading theme properties file: " + resourceFile.getAbsolutePath(), e);
}
}
}
private void processLookAndFeelSpecificValues(Map<LafType, GThemeValueMap> customSections) {
for (LafType lafType : customSections.keySet()) {
GThemeValueMap map = lafDefaultsMap.computeIfAbsent(lafType, t -> new GThemeValueMap());
map.load(customSections.get(lafType));
}
}
@Override
public GThemeValueMap getDefaults() {
return defaults;
}
@Override
public GThemeValueMap getDarkDefaults() {
return darkDefaults;
}
@Override
public GThemeValueMap getLookAndFeelDefaults(LafType lafType) {
return lafDefaultsMap.get(lafType);
}
}
@@ -69,13 +69,12 @@ public class ApplicationThemeManager extends ThemeManager {
protected void doInitialize() {
installFlatLookAndFeels();
loadDefaultThemeValues();
setTheme(themePreferences.load());
}
@Override
public void reloadApplicationDefaults() {
loadDefaultThemeValues();
themeDefaultsProvider = getThemeDefaultsProvider();
buildCurrentValues();
lookAndFeelManager.resetAll(javaDefaults);
notifyThemeChanged(new AllValuesChangedThemeEvent(false));
@@ -420,5 +419,4 @@ public class ApplicationThemeManager extends ThemeManager {
GColor.refreshAll(currentValues);
GIcon.refreshAll(currentValues);
}
}
@@ -119,10 +119,12 @@ public class GThemeValueMap {
* @param valueMap the map whose values are to be loaded into this map
*/
public void load(GThemeValueMap valueMap) {
if (valueMap == null) {
return;
}
valueMap.colorMap.values().forEach(v -> addColor(v));
valueMap.fontMap.values().forEach(v -> addFont(v));
valueMap.iconMap.values().forEach(v -> addIcon(v));
}
/**
@@ -41,8 +41,6 @@ public class HeadlessThemeManager extends ThemeManager {
}
private void doInitialize() {
loadDefaultThemeValues();
buildCurrentValues();
GColor.refreshAll(currentValues);
GIcon.refreshAll(currentValues);
@@ -224,4 +224,26 @@ public class StubThemeManager extends ThemeManager {
setColor(new ColorValue("color.palette." + paletteId, color));
}
@Override
protected ThemeDefaultsProvider getThemeDefaultsProvider() {
return new ThemeDefaultsProvider() {
@Override
public GThemeValueMap getDefaults() {
return null;
}
@Override
public GThemeValueMap getDarkDefaults() {
return null;
}
@Override
public GThemeValueMap getLookAndFeelDefaults(LafType lafType) {
return null;
}
};
}
}
@@ -15,63 +15,29 @@
*/
package generic.theme;
import java.io.IOException;
import java.util.List;
import generic.jar.ResourceFile;
import ghidra.framework.Application;
import ghidra.util.Msg;
/**
* Loads all the system theme.property files that contain all the default color, font, and
* icon values.
*/
public class ThemeDefaultsProvider {
private GThemeValueMap defaults = new GThemeValueMap();
private GThemeValueMap darkDefaults = new GThemeValueMap();
ThemeDefaultsProvider() {
loadThemeDefaultFiles();
}
/**
* Searches for all the theme.property files and loads them into either the standard
* defaults (light) map or the dark defaults map.
*/
private void loadThemeDefaultFiles() {
defaults.clear();
darkDefaults.clear();
List<ResourceFile> themeDefaultFiles =
Application.findFilesByExtensionInApplication(".theme.properties");
for (ResourceFile resourceFile : themeDefaultFiles) {
try {
ThemePropertyFileReader reader = new ThemePropertyFileReader(resourceFile);
defaults.load(reader.getDefaultValues());
darkDefaults.load(reader.getDarkDefaultValues());
}
catch (IOException e) {
Msg.error(this,
"Error reading theme properties file: " + resourceFile.getAbsolutePath(), e);
}
}
}
public interface ThemeDefaultsProvider {
/**
* Returns the standard defaults {@link GThemeValueMap}
* @return the standard defaults {@link GThemeValueMap}
*/
public GThemeValueMap getDefaults() {
return defaults;
}
public GThemeValueMap getDefaults();
/**
* Returns the dark defaults {@link GThemeValueMap}
* @return the dark defaults {@link GThemeValueMap}
*/
public GThemeValueMap getDarkDefaults() {
return darkDefaults;
}
public GThemeValueMap getDarkDefaults();
/**
* Returns the defaults specific to a given Look and Feel
* @param lafType the Look and Feel type
* @return the defaults specific to a given Look and Feel
*/
public GThemeValueMap getLookAndFeelDefaults(LafType lafType);
}
@@ -69,8 +69,7 @@ public abstract class ThemeManager {
protected GThemeValueMap systemValues = new GThemeValueMap();
protected GThemeValueMap currentValues = new GThemeValueMap();
protected GThemeValueMap applicationDefaults = new GThemeValueMap();
protected GThemeValueMap applicationDarkDefaults = new GThemeValueMap();
protected ThemeDefaultsProvider themeDefaultsProvider;
// these notifications are only when the user is manipulating theme values, so rare and at
// user speed, so using copy on read
@@ -86,27 +85,27 @@ public abstract class ThemeManager {
// default behavior is only install to INSTANCE if first time
INSTANCE = this;
}
themeDefaultsProvider = getThemeDefaultsProvider();
}
protected ThemeDefaultsProvider getThemeDefaultsProvider() {
return new ApplicationThemeDefaultsProvider();
}
protected void installInGui() {
Gui.setThemeManager(this);
}
protected void loadDefaultThemeValues() {
ThemeDefaultsProvider provider = new ThemeDefaultsProvider();
applicationDefaults = provider.getDefaults();
applicationDarkDefaults = provider.getDarkDefaults();
}
protected void buildCurrentValues() {
GThemeValueMap map = new GThemeValueMap();
map.load(javaDefaults);
map.load(systemValues);
map.load(applicationDefaults);
map.load(themeDefaultsProvider.getDefaults());
if (activeTheme.useDarkDefaults()) {
map.load(applicationDarkDefaults);
map.load(themeDefaultsProvider.getDarkDefaults());
}
map.load(themeDefaultsProvider.getLookAndFeelDefaults(getLookAndFeelType()));
map.load(activeTheme);
currentValues = map;
}
@@ -271,10 +270,11 @@ public abstract class ThemeManager {
GThemeValueMap map = new GThemeValueMap();
map.load(javaDefaults);
map.load(systemValues);
map.load(applicationDefaults);
map.load(themeDefaultsProvider.getDefaults());
if (activeTheme.useDarkDefaults()) {
map.load(applicationDarkDefaults);
map.load(themeDefaultsProvider.getDarkDefaults());
}
map.load(themeDefaultsProvider.getLookAndFeelDefaults(getLookAndFeelType()));
map.load(activeTheme);
return map;
}
@@ -439,8 +439,9 @@ public abstract class ThemeManager {
* theme.properties files
*/
public GThemeValueMap getApplicationDarkDefaults() {
GThemeValueMap map = new GThemeValueMap(applicationDefaults);
map.load(applicationDarkDefaults);
GThemeValueMap map = new GThemeValueMap(themeDefaultsProvider.getDefaults());
map.load(themeDefaultsProvider.getDarkDefaults());
map.load(themeDefaultsProvider.getLookAndFeelDefaults(getLookAndFeelType()));
return map;
}
@@ -451,7 +452,7 @@ public abstract class ThemeManager {
* theme.properties files
*/
public GThemeValueMap getApplicationLightDefaults() {
GThemeValueMap map = new GThemeValueMap(applicationDefaults);
GThemeValueMap map = new GThemeValueMap(themeDefaultsProvider.getDefaults());
return map;
}
@@ -463,10 +464,11 @@ public abstract class ThemeManager {
public GThemeValueMap getDefaults() {
GThemeValueMap currentDefaults = new GThemeValueMap(javaDefaults);
currentDefaults.load(systemValues);
currentDefaults.load(applicationDefaults);
currentDefaults.load(themeDefaultsProvider.getDefaults());
if (activeTheme.useDarkDefaults()) {
currentDefaults.load(applicationDarkDefaults);
currentDefaults.load(themeDefaultsProvider.getDarkDefaults());
}
currentDefaults.load(themeDefaultsProvider.getLookAndFeelDefaults(getLookAndFeelType()));
return currentDefaults;
}
@@ -476,7 +478,7 @@ public abstract class ThemeManager {
* @return true if the UI is using Aqua
*/
public boolean isUsingAquaUI(ComponentUI UI) {
return activeTheme.getLookAndFeelType() == LafType.MAC;
return getLookAndFeelType() == LafType.MAC;
}
/**
@@ -484,7 +486,7 @@ public abstract class ThemeManager {
* @return true if 'Nimbus' is the current Look and Feel
*/
public boolean isUsingNimbusUI() {
return activeTheme.getLookAndFeelType() == LafType.NIMBUS;
return getLookAndFeelType() == LafType.NIMBUS;
}
/**
@@ -16,6 +16,8 @@
package generic.theme;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import generic.jar.ResourceFile;
@@ -26,6 +28,7 @@ public class ThemePropertyFileReader extends AbstractThemeReader {
private GThemeValueMap defaults;
private GThemeValueMap darkDefaults;
private Map<LafType, GThemeValueMap> customSectionsMap = new HashMap<>();
/**
* Constructor for when the the theme.properties file is a {@link ResourceFile}
@@ -68,6 +71,14 @@ public class ThemePropertyFileReader extends AbstractThemeReader {
return darkDefaults == null ? new GThemeValueMap() : darkDefaults;
}
/**
* Returns a map of all the custom (look and feel specific) value maps
* @return a map of all the custom (look and feel specific) value maps
*/
public Map<LafType, GThemeValueMap> getLookAndFeelSections() {
return customSectionsMap;
}
protected void processNoSection(Section section) throws IOException {
if (!section.isEmpty()) {
error(0, "Theme properties file has values defined outside of a defined section");
@@ -86,4 +97,16 @@ public class ThemePropertyFileReader extends AbstractThemeReader {
processValues(darkDefaults, section);
}
@Override
protected void processCustomSection(Section section) throws IOException {
String name = section.getName();
LafType lafType = LafType.fromName(name);
if (lafType == null) {
error(0, "Unknown Look and Feel section found: " + name);
return;
}
GThemeValueMap customValues = new GThemeValueMap();
processValues(customValues, section);
customSectionsMap.put(lafType, customValues);
}
}
@@ -57,8 +57,6 @@ class ThemeReader extends AbstractThemeReader {
/**
* Assumes the file is a theme file and reads it.
* @return
* @throws IOException
*/
private GTheme readFileTheme() throws IOException {
try (Reader reader = new FileReader(file)) {
@@ -126,6 +124,12 @@ class ThemeReader extends AbstractThemeReader {
error(section.getLineNumber(), "[Dark Defaults] section not allowed in theme files!");
}
@Override
protected void processCustomSection(Section section) throws IOException {
error(section.getLineNumber(),
"Custom sections not allowed in theme files! " + section.getName());
}
private void processIconFile(String path, InputStream is) throws IOException {
int indexOf = path.indexOf("images/");
if (indexOf < 0) {
@@ -358,9 +358,25 @@ public class ApplicationThemeManagerTest {
}
@Override
protected void loadDefaultThemeValues() {
this.applicationDefaults = defaultValues;
this.applicationDarkDefaults = darkDefaultValues;
protected ThemeDefaultsProvider getThemeDefaultsProvider() {
return new ThemeDefaultsProvider() {
@Override
public GThemeValueMap getDefaults() {
return defaultValues;
}
@Override
public GThemeValueMap getDarkDefaults() {
return darkDefaultValues;
}
@Override
public GThemeValueMap getLookAndFeelDefaults(LafType lafType) {
return null;
}
};
}
@Override
@@ -20,9 +20,9 @@ import static org.junit.Assert.*;
import java.awt.Color;
import java.awt.Font;
import java.io.IOException;
import java.io.StringReader;
import java.io.*;
import java.util.List;
import java.util.Map;
import javax.swing.Icon;
@@ -141,9 +141,46 @@ public class ThemePropertyFileReaderTest {
}
@Test
public void testParseColorError() throws IOException {
public void testLookAndFeelValues() throws IOException {
//@formatter:off
ThemePropertyFileReader reader = new ThemePropertyFileReader("test", new StringReader(String.join("\n",
"[Defaults]",
" color.b.1 = white",
"[Dark Defaults]",
" color.b.1 = black",
"[Metal]",
" color.b.1 = red",
"[Nimbus]",
" color.b.1 = green",
"")));
//@formatter:on
GThemeValueMap values = reader.getDefaultValues();
assertEquals(1, values.size());
GThemeValueMap darkValues = reader.getDarkDefaultValues();
assertEquals(1, values.size());
assertEquals(WHITE, getColor(values, "color.b.1"));
assertEquals(BLACK, getColor(darkValues, "color.b.1"));
Map<LafType, GThemeValueMap> customSections = reader.getLookAndFeelSections();
assertEquals(2, customSections.size());
GThemeValueMap customValues = customSections.get(LafType.NIMBUS);
assertNotNull(customValues);
assertEquals(GREEN, getColor(customValues, "color.b.1"));
customValues = customSections.get(LafType.METAL);
assertNotNull(customValues);
assertEquals(RED, getColor(customValues, "color.b.1"));
}
@Test
public void testParseColorError() throws IOException {
//@formatter:off
ThemePropertyFileReader reader = new SilentThemePropertyFileReader("test", new StringReader(String.join("\n",
"[Defaults]",
" color.b.1 = white", // WHITE
" color.b.2 = sdfsdf", // RED
@@ -159,7 +196,7 @@ public class ThemePropertyFileReaderTest {
@Test
public void testParseFontError() throws IOException {
//@formatter:off
ThemePropertyFileReader reader = new ThemePropertyFileReader("test", new StringReader(String.join("\n",
ThemePropertyFileReader reader = new SilentThemePropertyFileReader("test", new StringReader(String.join("\n",
"[Defaults]",
" font.b.1 = Dialog-PLAIN-14",
" font.b.2 = Dialog-PLANE-13",
@@ -174,7 +211,7 @@ public class ThemePropertyFileReaderTest {
@Test
public void testParseFontModiferError() throws IOException {
//@formatter:off
ThemePropertyFileReader reader = new ThemePropertyFileReader("test", new StringReader(String.join("\n",
ThemePropertyFileReader reader = new SilentThemePropertyFileReader("test", new StringReader(String.join("\n",
"[Defaults]",
" font.b.1 = Dialog-PLAIN-14",
" font.b.2 = (font.b.1[)",
@@ -188,7 +225,7 @@ public class ThemePropertyFileReaderTest {
@Test
public void testIconNoRightHandValueError() throws IOException {
//@formatter:off
ThemePropertyFileReader reader = new ThemePropertyFileReader("test", new StringReader(String.join("\n",
ThemePropertyFileReader reader = new SilentThemePropertyFileReader("test", new StringReader(String.join("\n",
"[Defaults]",
" icon.b.1 = core.png",
" icon.b.2 = ",
@@ -213,4 +250,17 @@ public class ThemePropertyFileReaderTest {
IconValue icon = values.getIcon(id);
return icon.get(values);
}
private class SilentThemePropertyFileReader extends ThemePropertyFileReader {
protected SilentThemePropertyFileReader(String source, Reader reader) throws IOException {
super(source, reader);
}
@Override
protected void outputError(String msg) {
// be silent
}
}
}