diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/AbstractThemeReader.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/AbstractThemeReader.java index a6b7bbfc63..b587e94058 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/AbstractThemeReader.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/AbstractThemeReader.java @@ -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 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); } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ApplicationThemeDefaultsProvider.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ApplicationThemeDefaultsProvider.java new file mode 100644 index 0000000000..09b746fc63 --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ApplicationThemeDefaultsProvider.java @@ -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 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 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 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); + } + +} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ApplicationThemeManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ApplicationThemeManager.java index 0c682d1462..21068e7c5f 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/ApplicationThemeManager.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ApplicationThemeManager.java @@ -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); } - } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/GThemeValueMap.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/GThemeValueMap.java index 17736605f6..865a2a0478 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/GThemeValueMap.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/GThemeValueMap.java @@ -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)); - } /** diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/HeadlessThemeManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/HeadlessThemeManager.java index 4c2bcb73b0..192fcc5f1f 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/HeadlessThemeManager.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/HeadlessThemeManager.java @@ -41,8 +41,6 @@ public class HeadlessThemeManager extends ThemeManager { } private void doInitialize() { - loadDefaultThemeValues(); - buildCurrentValues(); GColor.refreshAll(currentValues); GIcon.refreshAll(currentValues); diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/StubThemeManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/StubThemeManager.java index 36799bc022..2a5941e60b 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/StubThemeManager.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/StubThemeManager.java @@ -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; + } + + }; + } + } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeDefaultsProvider.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeDefaultsProvider.java index 13572d8b72..d1c7ef7cc3 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeDefaultsProvider.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeDefaultsProvider.java @@ -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 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); + } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeManager.java index be3e98dcfc..024b7a0997 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeManager.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeManager.java @@ -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; } /** diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePropertyFileReader.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePropertyFileReader.java index 9d66a17c4d..9a503366e8 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePropertyFileReader.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePropertyFileReader.java @@ -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 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 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); + } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeReader.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeReader.java index 88a64ee26a..a9db5d5a8c 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeReader.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeReader.java @@ -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) { diff --git a/Ghidra/Framework/Generic/src/test/java/generic/theme/ApplicationThemeManagerTest.java b/Ghidra/Framework/Generic/src/test/java/generic/theme/ApplicationThemeManagerTest.java index 1d5bf0b725..6eb358a915 100644 --- a/Ghidra/Framework/Generic/src/test/java/generic/theme/ApplicationThemeManagerTest.java +++ b/Ghidra/Framework/Generic/src/test/java/generic/theme/ApplicationThemeManagerTest.java @@ -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 diff --git a/Ghidra/Framework/Generic/src/test/java/generic/theme/ThemePropertyFileReaderTest.java b/Ghidra/Framework/Generic/src/test/java/generic/theme/ThemePropertyFileReaderTest.java index c30ea39c52..21f14e6c9e 100644 --- a/Ghidra/Framework/Generic/src/test/java/generic/theme/ThemePropertyFileReaderTest.java +++ b/Ghidra/Framework/Generic/src/test/java/generic/theme/ThemePropertyFileReaderTest.java @@ -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 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 + } + + } }