mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-05-22 01:32:08 +08:00
GP-1981, javadocs, refactored ThemePropertFileReader, refactored
ThemeListener, fixed java property relationships for colors
This commit is contained in:
@@ -122,8 +122,10 @@ public class GhidraHelpService extends HelpManager {
|
||||
|
||||
class HelpThemeListener implements ThemeListener {
|
||||
@Override
|
||||
public void themeChanged(GTheme newTheme) {
|
||||
reload();
|
||||
public void themeChanged(ThemeEvent event) {
|
||||
if (event.isLookAndFeelChanged()) {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,15 +59,21 @@ public class ExportThemeDialog extends DialogComponentProvider {
|
||||
|
||||
private boolean exportTheme() {
|
||||
String themeName = nameField.getText();
|
||||
GTheme activeTheme = Gui.getActiveTheme();
|
||||
LafType laf = activeTheme.getLookAndFeelType();
|
||||
boolean useDarkDefaults = activeTheme.useDarkDefaults();
|
||||
File file = new File(fileTextField.getText());
|
||||
|
||||
if (themeName.isBlank()) {
|
||||
setStatusText("Missing Theme Name", MessageType.ERROR, true);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
FileGTheme exportTheme = createExternalTheme(themeName);
|
||||
GTheme exportTheme = new GTheme(file, themeName, laf, useDarkDefaults);
|
||||
loadValues(exportTheme);
|
||||
exportTheme.save();
|
||||
ThemeWriter themeWriter = new ThemeWriter(exportTheme);
|
||||
themeWriter.writeTheme(file, exportAsZip);
|
||||
return true;
|
||||
}
|
||||
catch (IOException e) {
|
||||
@@ -76,7 +82,7 @@ public class ExportThemeDialog extends DialogComponentProvider {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void loadValues(FileGTheme exportTheme) {
|
||||
private void loadValues(GTheme exportTheme) {
|
||||
if (includeDefaultsCheckbox.isSelected()) {
|
||||
exportTheme.load(Gui.getAllValues());
|
||||
}
|
||||
@@ -85,20 +91,6 @@ public class ExportThemeDialog extends DialogComponentProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private FileGTheme createExternalTheme(String themeName) {
|
||||
File file = new File(fileTextField.getText());
|
||||
|
||||
GTheme activeTheme = Gui.getActiveTheme();
|
||||
LafType laf = activeTheme.getLookAndFeelType();
|
||||
boolean useDarkDefaults = activeTheme.useDarkDefaults();
|
||||
|
||||
if (exportAsZip) {
|
||||
return new ZipGTheme(file, themeName, laf, useDarkDefaults);
|
||||
}
|
||||
return new FileGTheme(file, themeName, laf, useDarkDefaults);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cancelCallback() {
|
||||
close();
|
||||
|
||||
@@ -410,30 +410,20 @@ public class ThemeDialog extends DialogComponentProvider {
|
||||
|
||||
class DialogThemeListener implements ThemeListener {
|
||||
@Override
|
||||
public void themeChanged(GTheme newTheme) {
|
||||
reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void themeValuesRestored() {
|
||||
reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fontChanged(String id) {
|
||||
fontTableModel.reloadCurrent();
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void colorChanged(String id) {
|
||||
colorTableModel.reloadCurrent();
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void iconChanged(String id) {
|
||||
iconTableModel.reloadCurrent();
|
||||
public void themeChanged(ThemeEvent event) {
|
||||
if (event.haveAllValuesChanged()) {
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
if (event.hasAnyColorChanged()) {
|
||||
colorTableModel.reloadCurrent();
|
||||
}
|
||||
if (event.hasAnyFontChanged()) {
|
||||
fontTableModel.reloadCurrent();
|
||||
}
|
||||
if (event.hasAnyIconChanged()) {
|
||||
iconTableModel.reloadCurrent();
|
||||
}
|
||||
updateButtons();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import java.util.function.Supplier;
|
||||
|
||||
import javax.swing.JLabel;
|
||||
|
||||
import docking.theme.*;
|
||||
import docking.widgets.table.*;
|
||||
import generic.theme.*;
|
||||
import ghidra.docking.settings.Settings;
|
||||
@@ -172,7 +171,7 @@ public class ThemeFontTableModel extends GDynamicColumnTableModel<FontValue, Obj
|
||||
return "<No Value>";
|
||||
}
|
||||
Font font = resolvedFont.font();
|
||||
String fontString = FileGTheme.fontToString(font);
|
||||
String fontString = ThemeWriter.fontToString(font);
|
||||
|
||||
if (resolvedFont.refId() != null) {
|
||||
return resolvedFont.refId() + " [" + fontString + "]";
|
||||
|
||||
@@ -188,7 +188,7 @@ public class ThemeIconTableModel extends GDynamicColumnTableModel<IconValue, Obj
|
||||
Icon icon = resolvedIcon.icon();
|
||||
String sizeString = "[" + icon.getIconWidth() + "x" + icon.getIconHeight() + "] ";
|
||||
|
||||
String iconString = FileGTheme.JAVA_ICON;
|
||||
String iconString = GTheme.JAVA_ICON;
|
||||
if (icon instanceof UrlImageIcon urlIcon) {
|
||||
iconString = urlIcon.getOriginalPath();
|
||||
}
|
||||
|
||||
@@ -92,19 +92,12 @@ public class ThemeUtils {
|
||||
}
|
||||
GTheme startingTheme = Gui.getActiveTheme();
|
||||
try {
|
||||
FileGTheme imported;
|
||||
if (themeFile.getName().endsWith(GTheme.ZIP_FILE_EXTENSION)) {
|
||||
imported = new ZipGTheme(themeFile);
|
||||
}
|
||||
else if (themeFile.getName().endsWith(GTheme.FILE_EXTENSION)) {
|
||||
imported = new FileGTheme(themeFile);
|
||||
}
|
||||
else {
|
||||
Msg.showError(ThemeUtils.class, null, "Error Importing Theme",
|
||||
"Imported File must end in either " + GTheme.FILE_EXTENSION + " or " +
|
||||
GTheme.ZIP_FILE_EXTENSION);
|
||||
return;
|
||||
}
|
||||
GTheme imported = GTheme.loadTheme(themeFile);
|
||||
// by setting the theme, we can let the normal save handle all the edge cases
|
||||
// such as if a theme with that names exists and if so, should it be overwritten?
|
||||
// Also, the imported theme may contain default values which we don't want to save. So
|
||||
// by going through the usual save mechanism, only values that differ from defaults
|
||||
// be saved.
|
||||
Gui.setTheme(imported);
|
||||
if (!ThemeUtils.saveThemeChanges()) {
|
||||
Gui.setTheme(startingTheme);
|
||||
@@ -143,7 +136,7 @@ public class ThemeUtils {
|
||||
|
||||
public static void deleteTheme() {
|
||||
List<GTheme> savedThemes = new ArrayList<>(
|
||||
Gui.getAllThemes().stream().filter(t -> t instanceof FileGTheme).toList());
|
||||
Gui.getAllThemes().stream().filter(t -> t.getFile() != null).toList());
|
||||
if (savedThemes.isEmpty()) {
|
||||
Msg.showInfo(ThemeUtils.class, null, "Delete Theme", "There are no deletable themes");
|
||||
return;
|
||||
@@ -159,7 +152,7 @@ public class ThemeUtils {
|
||||
"Can't delete the current theme.");
|
||||
return;
|
||||
}
|
||||
FileGTheme fileTheme = (FileGTheme) selectedTheme;
|
||||
GTheme fileTheme = selectedTheme;
|
||||
int result = OptionDialog.showYesNoDialog(null, "Delete Theme: " + fileTheme.getName(),
|
||||
"Are you sure you want to delete theme " + fileTheme.getName());
|
||||
if (result == OptionDialog.YES_OPTION) {
|
||||
@@ -175,24 +168,29 @@ public class ThemeUtils {
|
||||
|
||||
private static boolean canSaveToName(String name) {
|
||||
GTheme existing = Gui.getTheme(name);
|
||||
// if no theme exists with that name, then we are save to save it
|
||||
if (existing == null) {
|
||||
return true;
|
||||
}
|
||||
if (existing instanceof FileGTheme fileTheme) {
|
||||
int result = OptionDialog.showYesNoDialog(null, "Overwrite Existing Theme?",
|
||||
"Do you want to overwrite the existing theme file for \"" + name + "\"?");
|
||||
if (result == OptionDialog.YES_OPTION) {
|
||||
return true;
|
||||
}
|
||||
// if the existing theme is a built-in theme, then we definitely can't save to that name
|
||||
if (existing instanceof DiscoverableGTheme) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
// if the existing theme with that name has no associated file, we can save it without
|
||||
// worrying about overwriting an existing file.
|
||||
if (existing.getFile() == null) {
|
||||
return true;
|
||||
}
|
||||
int result = OptionDialog.showYesNoDialog(null, "Overwrite Existing Theme?",
|
||||
"Do you want to overwrite the existing theme file for \"" + name + "\"?");
|
||||
return result == OptionDialog.YES_OPTION;
|
||||
}
|
||||
|
||||
private static boolean saveCurrentValues(String themeName) {
|
||||
GTheme activeTheme = Gui.getActiveTheme();
|
||||
File file = getSaveFile(themeName);
|
||||
|
||||
FileGTheme newTheme = new FileGTheme(file, themeName, activeTheme.getLookAndFeelType(),
|
||||
GTheme newTheme = new GTheme(file, themeName, activeTheme.getLookAndFeelType(),
|
||||
activeTheme.useDarkDefaults());
|
||||
newTheme.load(Gui.getNonDefaultValues());
|
||||
try {
|
||||
|
||||
@@ -47,9 +47,9 @@ public class ThemeUtilsTest extends AbstractDockingTest {
|
||||
|
||||
// get rid of any leftover imported themes from previous tests
|
||||
Set<GTheme> allThemes = Gui.getAllThemes();
|
||||
for (GTheme gTheme : allThemes) {
|
||||
if (gTheme instanceof FileGTheme fileTheme) {
|
||||
Gui.deleteTheme(fileTheme);
|
||||
for (GTheme theme : allThemes) {
|
||||
if (!(theme instanceof DiscoverableGTheme)) {
|
||||
Gui.deleteTheme(theme);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,7 +140,7 @@ public class ThemeUtilsTest extends AbstractDockingTest {
|
||||
pressButtonByText(exportDialog, "OK");
|
||||
waitForSwing();
|
||||
assertTrue(exportFile.exists());
|
||||
ZipGTheme zipTheme = new ZipGTheme(exportFile);
|
||||
GTheme zipTheme = GTheme.loadTheme(exportFile);
|
||||
assertEquals("Nimbus Theme", zipTheme.getName());
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ public class ThemeUtilsTest extends AbstractDockingTest {
|
||||
pressButtonByText(exportDialog, "OK");
|
||||
waitForSwing();
|
||||
assertTrue(exportFile.exists());
|
||||
FileGTheme fileTheme = new FileGTheme(exportFile);
|
||||
GTheme fileTheme = GTheme.loadTheme(exportFile);
|
||||
assertEquals("Nimbus Theme", fileTheme.getName());
|
||||
}
|
||||
|
||||
@@ -190,9 +190,9 @@ public class ThemeUtilsTest extends AbstractDockingTest {
|
||||
|
||||
private File createZipThemeFile(String themeName) throws IOException {
|
||||
File file = createTempFile("Test_Theme", ".theme.zip");
|
||||
ZipGTheme zipGTheme = new ZipGTheme(file, themeName, LafType.METAL, false);
|
||||
zipGTheme.addColor(new ColorValue("Panel.Background", Color.RED));
|
||||
zipGTheme.save();
|
||||
GTheme outputTheme = new GTheme(file, themeName, LafType.METAL, false);
|
||||
outputTheme.addColor(new ColorValue("Panel.Background", Color.RED));
|
||||
outputTheme.saveToZip(file, false);
|
||||
return file;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
/* ###
|
||||
* 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.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.plaf.FontUIResource;
|
||||
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.WebColors;
|
||||
import resources.ResourceManager;
|
||||
|
||||
/**
|
||||
* Abstract base class for reading theme values either in sections (theme property files) or no
|
||||
* sections (theme files)
|
||||
*/
|
||||
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 List<String> errors = new ArrayList<>();
|
||||
protected String source;
|
||||
|
||||
protected AbstractThemeReader(String source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of errors found while parsing
|
||||
* @return a list of errors found while parsing
|
||||
*/
|
||||
public List<String> getErrors() {
|
||||
return errors;
|
||||
}
|
||||
|
||||
protected void read(Reader reader) throws IOException {
|
||||
List<Section> sections = readSections(new LineNumberReader(reader));
|
||||
for (Section section : sections) {
|
||||
switch (section.getName()) {
|
||||
case NO_SECTION:
|
||||
processNoSection(section);
|
||||
break;
|
||||
case DEFAULTS:
|
||||
processDefaultSection(section);
|
||||
break;
|
||||
case DARK_DEFAULTS:
|
||||
processDarkDefaultSection(section);
|
||||
break;
|
||||
default:
|
||||
error(section.getLineNumber(),
|
||||
"Encounded unknown theme file section: " + section.getName());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected abstract void processNoSection(Section section) throws IOException;
|
||||
|
||||
protected abstract void processDefaultSection(Section section) throws IOException;
|
||||
|
||||
protected abstract void processDarkDefaultSection(Section section) throws IOException;
|
||||
|
||||
protected void processValues(GThemeValueMap valueMap, Section section) {
|
||||
for (String key : section.getKeys()) {
|
||||
String value = section.getValue(key);
|
||||
int lineNumber = section.getLineNumber(key);
|
||||
if (ColorValue.isColorKey(key)) {
|
||||
valueMap.addColor(parseColorProperty(key, value, lineNumber));
|
||||
}
|
||||
else if (FontValue.isFontKey(key)) {
|
||||
valueMap.addFont(parseFontProperty(key, value, lineNumber));
|
||||
}
|
||||
else if (IconValue.isIconKey(key)) {
|
||||
if (!GTheme.JAVA_ICON.equals(value)) {
|
||||
valueMap.addIcon(parseIconProperty(key, value));
|
||||
}
|
||||
}
|
||||
else {
|
||||
error(lineNumber, "Can't process property: " + key + " = " + value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IconValue parseIconProperty(String key, String value) {
|
||||
if (IconValue.isIconKey(value)) {
|
||||
return new IconValue(key, value);
|
||||
}
|
||||
Icon icon = ResourceManager.loadImage(value);
|
||||
return new IconValue(key, icon);
|
||||
}
|
||||
|
||||
private FontValue parseFontProperty(String key, String value, int lineNumber) {
|
||||
if (FontValue.isFontKey(value)) {
|
||||
return new FontValue(key, value);
|
||||
}
|
||||
Font font = Font.decode(value);
|
||||
if (font == null) {
|
||||
error(lineNumber, "Could not parse Color: " + value);
|
||||
}
|
||||
return font == null ? null : new FontValue(key, new FontUIResource(font));
|
||||
}
|
||||
|
||||
private ColorValue parseColorProperty(String key, String value, int lineNumber) {
|
||||
if (ColorValue.isColorKey(value)) {
|
||||
return new ColorValue(key, value);
|
||||
}
|
||||
Color color = WebColors.getColor(value);
|
||||
if (color == null) {
|
||||
error(lineNumber, "Could not parse Color: " + value);
|
||||
}
|
||||
return color == null ? null : new ColorValue(key, color);
|
||||
}
|
||||
|
||||
private List<Section> readSections(LineNumberReader reader) throws IOException {
|
||||
|
||||
List<Section> sections = new ArrayList<>();
|
||||
Section currentSection = new Section(NO_SECTION, 0);
|
||||
sections.add(currentSection);
|
||||
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
line = removeComments(line);
|
||||
|
||||
if (line.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isSectionHeader(line)) {
|
||||
currentSection = new Section(line, reader.getLineNumber());
|
||||
sections.add(currentSection);
|
||||
}
|
||||
else {
|
||||
currentSection.add(line, reader.getLineNumber());
|
||||
}
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
private String removeComments(String line) {
|
||||
// remove any trailing comment on line
|
||||
int commentIndex = line.indexOf("//");
|
||||
if (commentIndex >= 0) {
|
||||
line = line.substring(0, commentIndex);
|
||||
}
|
||||
line = line.trim();
|
||||
|
||||
// clear line if entire line is comment
|
||||
if (line.startsWith("#")) {
|
||||
return "";
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
private boolean isSectionHeader(String line) {
|
||||
return line.startsWith("[") && line.endsWith("]");
|
||||
}
|
||||
|
||||
protected void error(int lineNumber, String message) {
|
||||
String msg =
|
||||
"Error parsing file \"" + source + "\" at line: " + lineNumber + ", " + message;
|
||||
errors.add(msg);
|
||||
Msg.out(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents all the value found in a section of the theme properties file. Sections are
|
||||
* defined by a line containing just "[section name]"
|
||||
*/
|
||||
protected class Section {
|
||||
|
||||
private String name;
|
||||
private Map<String, String> properties = new HashMap<>();
|
||||
private Map<String, Integer> lineNumbers = new HashMap<>();
|
||||
private int startLineNumber;
|
||||
|
||||
/**
|
||||
* Constructor sectionName the section name
|
||||
* @param sectionName the name of this section
|
||||
* @param lineNumber the line number in the file where the section started
|
||||
*/
|
||||
public Section(String sectionName, int lineNumber) {
|
||||
this.name = sectionName;
|
||||
this.startLineNumber = lineNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the value with the given key
|
||||
* @param key the key to remove
|
||||
*/
|
||||
public void remove(String key) {
|
||||
properties.remove(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value for the given key.
|
||||
* @param key the key to get a value for
|
||||
* @return the value for the given key
|
||||
*/
|
||||
public String getValue(String key) {
|
||||
return properties.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of all keys in the section
|
||||
* @return a set of all keys in the section
|
||||
*/
|
||||
public Set<String> getKeys() {
|
||||
return properties.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the line number in the original file where the key was parsed
|
||||
* @param key the key to get a line number for
|
||||
* @return the line number in the original file where the key was parsed
|
||||
*/
|
||||
public int getLineNumber(String key) {
|
||||
return lineNumbers.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the section is empty.
|
||||
* @return true if the section is empty.
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return properties.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the line number in the file where this section began.
|
||||
* @return the line number in the file where this section began.
|
||||
*/
|
||||
public int getLineNumber() {
|
||||
return startLineNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of this section
|
||||
* @return the name of this section
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a raw line from the file to this section. The line will be parsed into a a
|
||||
* key-value pair.
|
||||
* @param line the line to be added/parsed
|
||||
* @param lineNumber the line number in the file for this line
|
||||
*/
|
||||
public void add(String line, int lineNumber) {
|
||||
int splitIndex = line.indexOf('=');
|
||||
if (splitIndex < 0) {
|
||||
error(lineNumber, "Missing required \"=\" for propery line: \"" + line + "\"");
|
||||
return;
|
||||
}
|
||||
String key = line.substring(0, splitIndex).trim();
|
||||
String value = line.substring(splitIndex + 1, line.length()).trim();
|
||||
if (key.isBlank()) {
|
||||
error(lineNumber, "Missing key for propery line: \"" + line + "\"");
|
||||
return;
|
||||
}
|
||||
if (key.isBlank()) {
|
||||
error(lineNumber, "Missing value for propery line: \"" + line + "\"");
|
||||
return;
|
||||
}
|
||||
properties.put(key, value);
|
||||
lineNumbers.put(key, lineNumber);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/* ###
|
||||
* 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 javax.swing.LookAndFeel;
|
||||
|
||||
/**
|
||||
* {@link ThemeEvent} for when a new theme is set or the current theme is reset to its original
|
||||
* values.
|
||||
*/
|
||||
public class AllValuesChangedThemeEvent extends ThemeEvent {
|
||||
|
||||
private boolean lookAndFeelChanged;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param lookAndFeelChanged true if the overall theme was changed which may have caused the
|
||||
* {@link LookAndFeel} to change
|
||||
*/
|
||||
public AllValuesChangedThemeEvent(boolean lookAndFeelChanged) {
|
||||
this.lookAndFeelChanged = lookAndFeelChanged;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isColorChanged(String id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFontChanged(String id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIconChanged(String id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLookAndFeelChanged() {
|
||||
return lookAndFeelChanged;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean haveAllValuesChanged() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAnyColorChanged() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAnyFontChanged() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAnyIconChanged() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/* ###
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* {@link ThemeEvent} for when a color changes for exactly one color id.
|
||||
*/
|
||||
public class ColorChangedThemeEvent extends ThemeEvent {
|
||||
private final ColorValue color;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param color the new {@link ColorValue} for the color id that changed
|
||||
*/
|
||||
public ColorChangedThemeEvent(ColorValue color) {
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isColorChanged(String id) {
|
||||
return id.equals(color.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAnyColorChanged() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -20,16 +20,38 @@ import java.awt.Color;
|
||||
import ghidra.util.Msg;
|
||||
import utilities.util.reflection.ReflectionUtilities;
|
||||
|
||||
/**
|
||||
* A class for storing {@link Color} values that have a String id (e.g. color.bg.foo) and either
|
||||
* a concrete color or a reference id which is the String id of another ColorValue that it
|
||||
* will inherit its color from. So if this class's color value is non-null, the refId will be null
|
||||
* and if the class's refId is non-null, then the color value will be null.
|
||||
*/
|
||||
public class ColorValue extends ThemeValue<Color> {
|
||||
static final String COLOR_ID_PREFIX = "color.";
|
||||
static final String EXTERNAL_PREFIX = "[color]";
|
||||
|
||||
public static final Color LAST_RESORT_DEFAULT = Color.GRAY;
|
||||
|
||||
/**
|
||||
* Constructor used when the ColorValue will have a direct {@link Color} value. The refId will
|
||||
* be null. Note: if a {@link GColor} is passed in as the value, then this will be an indirect
|
||||
* ColorValue that inherits its color from the id stored in the GColor.
|
||||
* @param id the id for this ColorValue
|
||||
* @param value the {@link Color} to associate with the given id
|
||||
*/
|
||||
public ColorValue(String id, Color value) {
|
||||
super(id, getRefId(value), getRawColor(value));
|
||||
if (value instanceof GColor) {
|
||||
throw new IllegalArgumentException("Can't use GColor as the value!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor used when the ColorValue will inherit its color from another ColorValue. The
|
||||
* color value field will be null.
|
||||
* @param id the id for this ColorValue
|
||||
* @param refId the id of another ColorValue that this ColorValue will inherit from
|
||||
*/
|
||||
public ColorValue(String id, String refId) {
|
||||
super(id, refId, null);
|
||||
}
|
||||
@@ -54,11 +76,6 @@ public class ColorValue extends ThemeValue<Color> {
|
||||
return LAST_RESORT_DEFAULT;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getIdPrefix() {
|
||||
return COLOR_ID_PREFIX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toExternalId(String internalId) {
|
||||
if (internalId.startsWith(COLOR_ID_PREFIX)) {
|
||||
@@ -75,28 +92,15 @@ public class ColorValue extends ThemeValue<Color> {
|
||||
return externalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given key string is a valid external key for a color value
|
||||
* @param key the key string to test
|
||||
* @return true if the given key string is a valid external key for a color value
|
||||
*/
|
||||
public static boolean isColorKey(String key) {
|
||||
return key.startsWith(COLOR_ID_PREFIX) || key.startsWith(EXTERNAL_PREFIX);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int compareValues(Color v1, Color v2) {
|
||||
int alpha1 = v1.getAlpha();
|
||||
int alpha2 = v2.getAlpha();
|
||||
|
||||
if (alpha1 == alpha2) {
|
||||
return getHsbCompareValue(v1) - getHsbCompareValue(v2);
|
||||
}
|
||||
return alpha1 - alpha2;
|
||||
}
|
||||
|
||||
private int getHsbCompareValue(Color v) {
|
||||
// compute a value the compares colors first by hue, then saturation, then brightness
|
||||
// reduce noise by converting float values from 0-1 to integers 0 - 7
|
||||
float[] hsb = Color.RGBtoHSB(v.getRed(), v.getGreen(), v.getBlue(), null);
|
||||
return 100 * (int) (10 * hsb[0]) + 10 * (int) (10 * hsb[1]) + (int) (10 * hsb[2]);
|
||||
}
|
||||
|
||||
private static Color getRawColor(Color value) {
|
||||
if (value instanceof GColor) {
|
||||
return null;
|
||||
|
||||
@@ -17,6 +17,9 @@ package generic.theme;
|
||||
|
||||
import ghidra.util.classfinder.ExtensionPoint;
|
||||
|
||||
/**
|
||||
* Abstract base class for built-in {@link GTheme}s.
|
||||
*/
|
||||
public abstract class DiscoverableGTheme extends GTheme implements ExtensionPoint {
|
||||
static final String CLASS_PREFIX = "Class:";
|
||||
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
/* ###
|
||||
* 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.*;
|
||||
import java.util.Enumeration;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
import ghidra.framework.Application;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
public class ExternalThemeReader extends ThemeReader {
|
||||
|
||||
public ExternalThemeReader(File file) throws IOException {
|
||||
try (ZipFile zipFile = new ZipFile(file)) {
|
||||
Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry entry = entries.nextElement();
|
||||
String name = entry.getName();
|
||||
try (InputStream is = zipFile.getInputStream(entry)) {
|
||||
if (name.endsWith(".theme")) {
|
||||
processThemeData(name, is);
|
||||
}
|
||||
else {
|
||||
processIconFile(name, is);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processIconFile(String path, InputStream is) throws IOException {
|
||||
int indexOf = path.indexOf("images/");
|
||||
if (indexOf < 0) {
|
||||
Msg.error(this, "Unknown file: " + path);
|
||||
}
|
||||
String relativePath = path.substring(indexOf, path.length());
|
||||
File dir = Application.getUserSettingsDirectory();
|
||||
File iconFile = new File(dir, relativePath);
|
||||
FileUtils.copyInputStreamToFile(is, iconFile);
|
||||
}
|
||||
|
||||
private void processThemeData(String name, InputStream is) throws IOException {
|
||||
InputStreamReader reader = new InputStreamReader(is);
|
||||
read(reader);
|
||||
}
|
||||
}
|
||||
+23
-2
@@ -15,6 +15,27 @@
|
||||
*/
|
||||
package generic.theme;
|
||||
|
||||
public interface Refreshable {
|
||||
public void refresh();
|
||||
/**
|
||||
* {@link ThemeEvent} for when a font changes for exactly one font id.
|
||||
*/
|
||||
public class FontChangedThemeEvent extends ThemeEvent {
|
||||
private final FontValue font;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param font the new {@link FontValue} for the font id that changed
|
||||
*/
|
||||
public FontChangedThemeEvent(FontValue font) {
|
||||
this.font = font;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFontChanged(String id) {
|
||||
return id.equals(font.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAnyFontChanged() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -19,15 +19,33 @@ import java.awt.Font;
|
||||
|
||||
import ghidra.util.Msg;
|
||||
|
||||
/**
|
||||
* A class for storing {@link Font} values that have a String id (e.g. font.foo.bar) and either
|
||||
* a concrete font or a reference id which is the String id of another FontValue that it
|
||||
* will inherit its font from. So if this class's font value is non-null, the refId will be null
|
||||
* and if the class's refId is non-null, then the font value will be null.
|
||||
*/
|
||||
public class FontValue extends ThemeValue<Font> {
|
||||
static final String FONT_ID_PREFIX = "font.";
|
||||
public static final Font LAST_RESORT_DEFAULT = new Font("monospaced", Font.PLAIN, 12);
|
||||
private static final String EXTERNAL_PREFIX = "[font]";
|
||||
|
||||
/**
|
||||
* Constructor used when the FontValue will have a direct {@link Font} value. The refId
|
||||
* will be null.
|
||||
* @param id the id for this FontValue
|
||||
* @param value the {@link Font} to associate with the given id
|
||||
*/
|
||||
public FontValue(String id, Font value) {
|
||||
super(id, null, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor used when the FontValue will inherit its {@link Font} from another FontValue. The
|
||||
* font value field will be null.
|
||||
* @param id the id for this FontValue
|
||||
* @param refId the id of another FontValue that this FontValue will inherit from
|
||||
*/
|
||||
public FontValue(String id, String refId) {
|
||||
super(id, refId, null);
|
||||
}
|
||||
@@ -43,11 +61,6 @@ public class FontValue extends ThemeValue<Font> {
|
||||
return LAST_RESORT_DEFAULT;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getIdPrefix() {
|
||||
return FONT_ID_PREFIX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toExternalId(String internalId) {
|
||||
if (internalId.startsWith(FONT_ID_PREFIX)) {
|
||||
@@ -64,12 +77,13 @@ public class FontValue extends ThemeValue<Font> {
|
||||
return externalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given key string is a valid external key for a font value
|
||||
* @param key the key string to test
|
||||
* @return true if the given key string is a valid external key for a font value
|
||||
*/
|
||||
public static boolean isFontKey(String key) {
|
||||
return key.startsWith(FONT_ID_PREFIX) || key.startsWith(EXTERNAL_PREFIX);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int compareValues(Font v1, Font v2) {
|
||||
return v1.toString().compareTo(v2.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,22 +25,41 @@ import java.util.Objects;
|
||||
import ghidra.util.WebColors;
|
||||
import ghidra.util.datastruct.WeakStore;
|
||||
|
||||
public class GColor extends Color implements Refreshable {
|
||||
/**
|
||||
* A {@link Color} whose value is dynamically determined by looking up its id into a global
|
||||
* color table that is determined by the active {@link GTheme}.
|
||||
* <P>The idea is for developers to
|
||||
* not use specific colors in their code, but to instead use a GColor with an id that hints at
|
||||
* its use. For example, instead of harding code a component's background color to white by coding
|
||||
* "component.setBackground(Color.white)", you would do something like
|
||||
* "component.setBackground(new GColor("color.mywidget.bg"). Then in a
|
||||
* "[module name].theme.properties" file (located in the module's data directory), you would
|
||||
* set the default value by adding this line "color.mywidget.bg = white".
|
||||
*/
|
||||
public class GColor extends Color {
|
||||
// keeps a weak reference to all uses of GColor, so their cached color value can be refreshed
|
||||
private static WeakStore<GColor> inUseColors = new WeakStore<>();
|
||||
|
||||
private String id;
|
||||
private Color delegate;
|
||||
private Short alpha;
|
||||
|
||||
public static void refreshAll() {
|
||||
for (GColor gcolor : inUseColors.getValues()) {
|
||||
gcolor.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a GColor with an id that will be used to look up the current color associated with
|
||||
* that id, which can be changed at runtime.
|
||||
* @param id the id used to lookup the current value for this color
|
||||
*/
|
||||
public GColor(String id) {
|
||||
this(id, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a GColor with an id that will be used to look up the current color associated with
|
||||
* that id, which can be changed at runtime.
|
||||
* @param id the id used to lookup the current value for this color
|
||||
* @param validate if true, an error will be generated if the id can't be resolved to a color
|
||||
* at this time
|
||||
*/
|
||||
public GColor(String id, boolean validate) {
|
||||
super(0x808080);
|
||||
this.id = id;
|
||||
@@ -55,18 +74,24 @@ public class GColor extends Color implements Refreshable {
|
||||
delegate = new Color(delegate.getRed(), delegate.getGreen(), delegate.getBlue(), alpha);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a transparent version of this GColor. If the underlying value of this GColor changes,
|
||||
* the transparent version will also change.
|
||||
* @param newAlpha the transparency level for the new color
|
||||
* @return a tranparent version of this GColor
|
||||
*/
|
||||
public GColor withAlpha(int newAlpha) {
|
||||
return new GColor(id, newAlpha);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id for this GColor.
|
||||
* @return the id for this GColor.
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public boolean isEquivalent(Color color) {
|
||||
return delegate.getRGB() == color.getRGB();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRed() {
|
||||
return delegate.getRed();
|
||||
@@ -185,7 +210,9 @@ public class GColor extends Color implements Refreshable {
|
||||
return delegate.getTransparency();
|
||||
}
|
||||
|
||||
@Override
|
||||
/**
|
||||
* Reloads the delegate.
|
||||
*/
|
||||
public void refresh() {
|
||||
Color color = Gui.getRawColor(id, false);
|
||||
if (color != null) {
|
||||
@@ -197,4 +224,15 @@ public class GColor extends Color implements Refreshable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method for notifying all the existing GColors that colors have changed and they
|
||||
* should reload their cached indirect color.
|
||||
*/
|
||||
public static void refreshAll() {
|
||||
for (GColor gcolor : inUseColors.getValues()) {
|
||||
gcolor.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,8 +15,15 @@
|
||||
*/
|
||||
package generic.theme;
|
||||
|
||||
import javax.swing.UIDefaults;
|
||||
import javax.swing.plaf.UIResource;
|
||||
|
||||
/**
|
||||
* Version of GColor that implements UIResource. It is important that when setting java defaults
|
||||
* in the {@link UIDefaults} that it implements UIResource. Otherwise, java will think the color
|
||||
* was set explicitly by client code and therefore can't update it generically when it goes to
|
||||
* update the default color in the UIs for each component.
|
||||
*/
|
||||
public class GColorUIResource extends GColor implements UIResource {
|
||||
|
||||
public GColorUIResource(String id) {
|
||||
|
||||
@@ -20,31 +20,61 @@ import java.awt.Graphics;
|
||||
|
||||
import javax.swing.Icon;
|
||||
|
||||
import generic.theme.Refreshable;
|
||||
import ghidra.util.datastruct.WeakStore;
|
||||
|
||||
public class GIcon implements Icon, Refreshable {
|
||||
/**
|
||||
* An {@link Icon} whose value is dynamically determined by looking up its id into a global
|
||||
* icon table that is determined by the active {@link GTheme}.
|
||||
* <P> The idea is for developers to
|
||||
* not use specific icons in their code, but to instead use a GIcon with an id that hints at
|
||||
* its use. For example, instead of harding code a label's icon by coding
|
||||
* "lable.setIcon(ResourceManager.loadImage("images/refresh.png", you would do something like
|
||||
* label.setIcon(new GIcon("icon.refresh"). Then in a "[module name].theme.properties" file
|
||||
* (located in the module's data directory), you would set the default value by adding this
|
||||
* line "icon.refresh = images/refresh.png".
|
||||
*/
|
||||
public class GIcon implements Icon {
|
||||
private static WeakStore<GIcon> inUseIcons = new WeakStore<>();
|
||||
|
||||
private String id;
|
||||
private Icon delegate;
|
||||
|
||||
/**
|
||||
* Static method for notifying all the existing GIcon that icons have changed and they
|
||||
* should reload their cached indirect icon.
|
||||
*/
|
||||
public static void refreshAll() {
|
||||
for (GIcon gIcon : inUseIcons.getValues()) {
|
||||
gIcon.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a GIcon with an id that will be used to look up the current icon associated with
|
||||
* that id, which can be changed at runtime.
|
||||
* @param id the id used to lookup the current value for this color
|
||||
*/
|
||||
public GIcon(String id) {
|
||||
this(id, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a GIcon with an id that will be used to look up the current icon associated with
|
||||
* that id, which can be changed at runtime.
|
||||
* @param id the id used to lookup the current value for this icon
|
||||
* @param validate if true, an error will be generated if the id can't be resolved to a icon
|
||||
* at this time
|
||||
*/
|
||||
public GIcon(String id, boolean validate) {
|
||||
this.id = id;
|
||||
delegate = Gui.getRawIcon(id, validate);
|
||||
inUseIcons.add(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id for this GIcon.
|
||||
* @return the id for this GIcon.
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -64,7 +94,9 @@ public class GIcon implements Icon, Refreshable {
|
||||
return delegate.getIconHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
/**
|
||||
* Reloads the delegate.
|
||||
*/
|
||||
public void refresh() {
|
||||
Icon icon = Gui.getRawIcon(id, false);
|
||||
if (icon != null) {
|
||||
|
||||
@@ -15,8 +15,15 @@
|
||||
*/
|
||||
package generic.theme;
|
||||
|
||||
import javax.swing.UIDefaults;
|
||||
import javax.swing.plaf.UIResource;
|
||||
|
||||
/**
|
||||
* Version of GIcon that implements UIResource. It is important that when setting java defaults
|
||||
* in the {@link UIDefaults} that it implements UIResource. Otherwise, java will think the icon
|
||||
* was set explicitly by client code and therefore can't update it generically when it goes to
|
||||
* update the default icon in the UIs for each component.
|
||||
*/
|
||||
public class GIconUIResource extends GIcon implements UIResource {
|
||||
|
||||
public GIconUIResource(String id) {
|
||||
|
||||
@@ -22,38 +22,58 @@ import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.LookAndFeel;
|
||||
|
||||
/**
|
||||
* Class to store all the configurable appearance properties (Colors, Fonts, Icons, Look and Feel)
|
||||
* in an application.
|
||||
*/
|
||||
public class GTheme extends GThemeValueMap {
|
||||
public static final String FILE_PREFIX = "File:";
|
||||
public static final String JAVA_ICON = "<JAVA ICON>";
|
||||
|
||||
public static String FILE_EXTENSION = "theme";
|
||||
public static String ZIP_FILE_EXTENSION = "theme.zip";
|
||||
|
||||
static final String THEME_NAME_KEY = "name";
|
||||
static final String THEME_LOOK_AND_FEEL_KEY = "lookAndFeel";
|
||||
static final String THEME_USE_DARK_DEFAULTS = "useDarkDefaults";
|
||||
|
||||
private final String name;
|
||||
private final LafType lookAndFeel;
|
||||
private final boolean useDarkDefaults;
|
||||
private final File file;
|
||||
|
||||
/**
|
||||
* Creates an new GTheme with the given name, the default {@link LookAndFeel} for the the
|
||||
* platform and not using dark defaults. This theme will be using all the standard defaults
|
||||
* from the theme.property files and the defaults from the default LookAndFeel.
|
||||
* @param name the name for this GTheme
|
||||
*/
|
||||
public GTheme(String name) {
|
||||
this(name, LafType.getDefaultLookAndFeel(), false);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new empty GTheme with the given name
|
||||
* Creates a new empty GTheme with the given name, {@link LookAndFeel}, and whether or not to
|
||||
* use dark defaults.
|
||||
* @param name the name for the new GTheme
|
||||
* @param lookAndFeel the look and feel type used by this theme
|
||||
* @param useDarkDefaults determines whether or
|
||||
*/
|
||||
public GTheme(String name, LafType lookAndFeel, boolean useDarkDefaults) {
|
||||
this(null, name, lookAndFeel, useDarkDefaults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for creating a GTheme with an associated File.
|
||||
* @param file the file that this theme will save to
|
||||
* @param name the name of the new theme
|
||||
* @param lookAndFeel the {@link LafType} for the new theme
|
||||
* @param useDarkDefaults true if this new theme uses dark defaults
|
||||
*/
|
||||
public GTheme(File file, String name, LafType lookAndFeel, boolean useDarkDefaults) {
|
||||
this.name = name;
|
||||
this.lookAndFeel = lookAndFeel;
|
||||
this.useDarkDefaults = useDarkDefaults;
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,9 +105,20 @@ public class GTheme extends GThemeValueMap {
|
||||
* @return a String that can be used to find and restore this theme.
|
||||
*/
|
||||
public String getThemeLocater() {
|
||||
if (file != null) {
|
||||
return FILE_PREFIX + file.getAbsolutePath();
|
||||
}
|
||||
return "Default";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file associated with this theme.
|
||||
* @return the file associated with this theme.
|
||||
*/
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Color for the given id
|
||||
* @param id the id to associate with the given Color
|
||||
@@ -167,12 +198,36 @@ public class GTheme extends GThemeValueMap {
|
||||
return Objects.equals(name, other.name) && Objects.equals(lookAndFeel, other.lookAndFeel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this theme has a {@link LookAndFeel} that is supported by the current
|
||||
* platform.
|
||||
* @return true if this theme has a {@link LookAndFeel} that is supported by the current
|
||||
* platform.
|
||||
*/
|
||||
public boolean hasSupportedLookAndFeel() {
|
||||
return lookAndFeel.isSupported();
|
||||
}
|
||||
|
||||
public FileGTheme saveToFile(File file, boolean includeDefaults) throws IOException {
|
||||
FileGTheme fileTheme = new FileGTheme(file, name, lookAndFeel, useDarkDefaults);
|
||||
/**
|
||||
* Saves this theme to its associated file.
|
||||
* @throws IOException if an I/O error occurs when writing the file
|
||||
*/
|
||||
public void save() throws IOException {
|
||||
ThemeWriter writer = new ThemeWriter(this);
|
||||
writer.writeThemeToFile(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves this theme to a new theme file.
|
||||
* @param outputFile the file to save to
|
||||
* @param includeDefaults if true, write all values to the theme file including default values.
|
||||
* Otherwise, just values that are not the default values are written to the file.
|
||||
* @return a new FileGTheme that represents the new file/theme
|
||||
* @throws IOException if an I/O error occurs writing the theme file
|
||||
*/
|
||||
public GTheme saveToFile(File outputFile, boolean includeDefaults) throws IOException {
|
||||
|
||||
GTheme fileTheme = new GTheme(outputFile, name, lookAndFeel, useDarkDefaults);
|
||||
if (includeDefaults) {
|
||||
fileTheme.load(Gui.getDefaults());
|
||||
}
|
||||
@@ -181,4 +236,35 @@ public class GTheme extends GThemeValueMap {
|
||||
return fileTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves this theme to a new theme file.
|
||||
* @param outputFile the file to save to
|
||||
* @param includeDefaults if true, write all values to the theme file including default values.
|
||||
* Otherwise, just values that are not the default values are written to the file.
|
||||
* @return a new FileGTheme that represents the new file/theme
|
||||
* @throws IOException if an I/O error occurs writing the theme file
|
||||
*/
|
||||
public void saveToZip(File outputFile, boolean includeDefaults) throws IOException {
|
||||
|
||||
GTheme theme = new GTheme(name, lookAndFeel, useDarkDefaults);
|
||||
if (includeDefaults) {
|
||||
theme.load(Gui.getDefaults());
|
||||
}
|
||||
theme.load(this);
|
||||
ThemeWriter writer = new ThemeWriter(theme);
|
||||
writer.writeThemeToZipFile(outputFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a theme from a file. The file can be either a theme file or a zip file containing
|
||||
* a theme file and optionally a set of icon files.
|
||||
* @param file the file to read.
|
||||
* @return the theme that was read from the file
|
||||
* @throws IOException if an error occcured trying to read a theme from the file.
|
||||
*/
|
||||
public static GTheme loadTheme(File file) throws IOException {
|
||||
ThemeReader reader = new ThemeReader(file);
|
||||
return reader.readTheme();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,48 +24,94 @@ import javax.swing.Icon;
|
||||
import resources.ResourceManager;
|
||||
import resources.icons.UrlImageIcon;
|
||||
|
||||
/**
|
||||
* Class for storing colors, fonts, and icons by id
|
||||
*/
|
||||
public class GThemeValueMap {
|
||||
protected Map<String, ColorValue> colorMap = new HashMap<>();
|
||||
protected Map<String, FontValue> fontMap = new HashMap<>();
|
||||
protected Map<String, IconValue> iconMap = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Constructs a new empty map.
|
||||
*/
|
||||
public GThemeValueMap() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new value map, populated by all the values in the given map. Essentailly clones
|
||||
* the given map.
|
||||
* @param initial the set of values to initialize to
|
||||
*/
|
||||
public GThemeValueMap(GThemeValueMap initial) {
|
||||
load(initial);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the {@link ColorValue} to the map. If a ColorValue already exists in the map with
|
||||
* the same id, it will be replaced
|
||||
* @param value the {@link ColorValue} to store in the map.
|
||||
*/
|
||||
public void addColor(ColorValue value) {
|
||||
if (value != null) {
|
||||
colorMap.put(value.getId(), value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the {@link FontValue} to the map. If a FontValue already exists in the map with
|
||||
* the same id, it will be replaced
|
||||
* @param value the {@link FontValue} to store in the map.
|
||||
*/
|
||||
public void addFont(FontValue value) {
|
||||
if (value != null) {
|
||||
fontMap.put(value.getId(), value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the {@link IconValue} to the map. If a IconValue already exists in the map with
|
||||
* the same id, it will be replaced
|
||||
* @param value the {@link IconValue} to store in the map.
|
||||
*/
|
||||
public void addIcon(IconValue value) {
|
||||
if (value != null) {
|
||||
iconMap.put(value.getId(), value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current {@link ColorValue} for the given id or null if none exists.
|
||||
* @param id the id to look up a color for
|
||||
* @return the current {@link ColorValue} for the given id or null if none exists.
|
||||
*/
|
||||
public ColorValue getColor(String id) {
|
||||
return colorMap.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current {@link FontValue} for the given id or null if none exists.
|
||||
* @param id the id to look up a font for
|
||||
* @return the current {@link FontValue} for the given id or null if none exists.
|
||||
*/
|
||||
public FontValue getFont(String id) {
|
||||
return fontMap.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current {@link IconValue} for the given id or null if none exists.
|
||||
* @param id the id to look up a icon for
|
||||
* @return the current {@link IconValue} for the given id or null if none exists.
|
||||
*/
|
||||
public IconValue getIcon(String id) {
|
||||
return iconMap.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all the values from the given map into this map, replacing values with the
|
||||
* same ids.
|
||||
* @param valueMap the map whose values are to be loaded into this map
|
||||
*/
|
||||
public void load(GThemeValueMap valueMap) {
|
||||
valueMap.colorMap.values().forEach(v -> addColor(v));
|
||||
valueMap.fontMap.values().forEach(v -> addFont(v));
|
||||
@@ -73,48 +119,114 @@ public class GThemeValueMap {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all the {@link ColorValue}s stored in this map.
|
||||
* @return a list of all the {@link ColorValue}s stored in this map.
|
||||
*/
|
||||
public List<ColorValue> getColors() {
|
||||
return new ArrayList<>(colorMap.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all the {@link FontValue}s stored in this map.
|
||||
* @return a list of all the {@link Fontvalue}s stored in this map.
|
||||
*/
|
||||
public List<FontValue> getFonts() {
|
||||
return new ArrayList<>(fontMap.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all the {@link IconValue}s stored in this map.
|
||||
* @return a list of all the {@link IconValue}s stored in this map.
|
||||
*/
|
||||
public List<IconValue> getIcons() {
|
||||
return new ArrayList<>(iconMap.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a {@link ColorValue} exists in this map for the given id.
|
||||
* @param id the id to check
|
||||
* @return true if a {@link ColorValue} exists in this map for the given id
|
||||
*/
|
||||
public boolean containsColor(String id) {
|
||||
return colorMap.containsKey(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a {@link FontValue} exists in this map for the given id.
|
||||
* @param id the id to check
|
||||
* @return true if a {@link FontValue} exists in this map for the given id
|
||||
*/
|
||||
public boolean containsFont(String id) {
|
||||
return fontMap.containsKey(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if an {@link IconValue} exists in this map for the given id.
|
||||
* @param id the id to check
|
||||
* @return true if an {@link IconValue} exists in this map for the given id
|
||||
*/
|
||||
public boolean containsIcon(String id) {
|
||||
return iconMap.containsKey(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of color, font, and icon values stored in this map
|
||||
* @return the total number of color, font, and icon values stored in this map
|
||||
*/
|
||||
public Object size() {
|
||||
return colorMap.size() + fontMap.size() + iconMap.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all color, font, and icon values from this map
|
||||
*/
|
||||
public void clear() {
|
||||
colorMap.clear();
|
||||
fontMap.clear();
|
||||
iconMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there are not color, font, or icon values in this map
|
||||
* @return true if there are not color, font, or icon values in this map
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return colorMap.isEmpty() && fontMap.isEmpty() && iconMap.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* removes any {@link ColorValue} with the given id from this map.
|
||||
* @param id the id to remove
|
||||
*/
|
||||
public void removeColor(String id) {
|
||||
colorMap.remove(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* removes any {@link FontValue} with the given id from this map.
|
||||
* @param id the id to remove
|
||||
*/
|
||||
public void removeFont(String id) {
|
||||
fontMap.remove(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* removes any {@link IconValue} with the given id from this map.
|
||||
* @param id the id to remove
|
||||
*/
|
||||
public void removeIcon(String id) {
|
||||
iconMap.remove(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link GThemeValueMap} that is only populated by values that don't exist
|
||||
* in the give map.
|
||||
* @param base the set of values (usually the default set) to compare against to determine
|
||||
* what values are changed.
|
||||
* @return a new {@link GThemeValueMap} that is only populated by values that don't exist
|
||||
* in the give map
|
||||
*/
|
||||
public GThemeValueMap getChangedValues(GThemeValueMap base) {
|
||||
GThemeValueMap map = new GThemeValueMap();
|
||||
for (ColorValue color : colorMap.values()) {
|
||||
@@ -135,14 +247,13 @@ public class GThemeValueMap {
|
||||
return map;
|
||||
}
|
||||
|
||||
public void removeFont(String id) {
|
||||
fontMap.remove(id);
|
||||
}
|
||||
|
||||
public void removeIcon(String id) {
|
||||
iconMap.remove(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the set of icon (.png, .gif) files that are used by IconValues that came from files
|
||||
* versus resources in the classpath. These are the icon files that need to be included
|
||||
* when exporting this set of values to a zip file.
|
||||
* @return the set of icon (.png, .gif) files that are used by IconValues that came from files
|
||||
* versus resources in the classpath
|
||||
*/
|
||||
public Set<File> getExternalIconFiles() {
|
||||
Set<File> files = new HashSet<>();
|
||||
for (IconValue iconValue : iconMap.values()) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
/* ###
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* {@link ThemeEvent} for when an icon changes for exactly one icon id.
|
||||
*/
|
||||
public class IconChangedThemeEvent extends ThemeEvent {
|
||||
private final IconValue icon;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param icon the new {@link IconValue} for the icon id that changed
|
||||
*/
|
||||
public IconChangedThemeEvent(IconValue icon) {
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIconChanged(String id) {
|
||||
return id.equals(icon.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAnyIconChanged() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,12 @@ import javax.swing.Icon;
|
||||
import ghidra.util.Msg;
|
||||
import resources.ResourceManager;
|
||||
|
||||
/**
|
||||
* A class for storing {@link Icon} values that have a String id (e.g. icon.bg.foo) and either
|
||||
* a concrete icon or a reference id which is the String id of another IconValue that it
|
||||
* will inherit its icon from. So if this class's icon value is non-null, the refId will be null
|
||||
* and if the class's refId is non-null, then the icon value will be null.
|
||||
*/
|
||||
public class IconValue extends ThemeValue<Icon> {
|
||||
static final String ICON_ID_PREFIX = "icon.";
|
||||
|
||||
@@ -27,10 +33,27 @@ public class IconValue extends ThemeValue<Icon> {
|
||||
|
||||
private static final String EXTERNAL_PREFIX = "[icon]";
|
||||
|
||||
/**
|
||||
* Constructor used when the ColorValue will have a direct {@link Icon} value. The refId will
|
||||
* be null. Note: if a {@link GIcon} is passed in as the value, then this will be an indirect
|
||||
* IconValue that inherits its icon from the id stored in the GIcon.
|
||||
* @param id the id for this IconValue
|
||||
* @param icon the {@link Icon} to associate with the given id
|
||||
*/
|
||||
public IconValue(String id, Icon icon) {
|
||||
super(id, null, icon);
|
||||
super(id, getRefId(icon), getRawIcon(icon));
|
||||
if (icon instanceof GIcon) {
|
||||
throw new IllegalArgumentException("Can't use GIcon as the value!");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor used when the IconValue will inherit its {@link Icon} from another IconValue. The
|
||||
* icon value field will be null.
|
||||
* @param id the id for this IconValue
|
||||
* @param refId the id of another IconValue that this IconValue will inherit from
|
||||
*/
|
||||
public IconValue(String id, String refId) {
|
||||
super(id, refId, null);
|
||||
}
|
||||
@@ -47,11 +70,6 @@ public class IconValue extends ThemeValue<Icon> {
|
||||
return ResourceManager.getDefaultIcon();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getIdPrefix() {
|
||||
return ICON_ID_PREFIX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toExternalId(String internalId) {
|
||||
if (internalId.startsWith(ICON_ID_PREFIX)) {
|
||||
@@ -68,12 +86,26 @@ public class IconValue extends ThemeValue<Icon> {
|
||||
return externalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given key string is a valid external key for an icon value
|
||||
* @param key the key string to test
|
||||
* @return true if the given key string is a valid external key for an icon value
|
||||
*/
|
||||
public static boolean isIconKey(String key) {
|
||||
return key.startsWith(ICON_ID_PREFIX) || key.startsWith(EXTERNAL_PREFIX);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int compareValues(Icon v1, Icon v2) {
|
||||
return v1.toString().compareTo(v2.toString());
|
||||
private static Icon getRawIcon(Icon value) {
|
||||
if (value instanceof GIcon) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String getRefId(Icon value) {
|
||||
if (value instanceof GIcon) {
|
||||
return ((GIcon) value).getId();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package generic.theme;
|
||||
|
||||
import javax.swing.LookAndFeel;
|
||||
import javax.swing.UIManager;
|
||||
import javax.swing.UIManager.LookAndFeelInfo;
|
||||
|
||||
@@ -23,6 +24,9 @@ import ghidra.framework.OperatingSystem;
|
||||
import ghidra.framework.Platform;
|
||||
import ghidra.util.exception.AssertException;
|
||||
|
||||
/**
|
||||
* An enumeration that represents the set of supported {@link LookAndFeel}s
|
||||
*/
|
||||
public enum LafType {
|
||||
METAL("Metal"),
|
||||
NIMBUS("Nimbus"),
|
||||
@@ -41,10 +45,19 @@ public enum LafType {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of this LafType.
|
||||
* @return the name of this LafType.
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the LafType for the given name or null if the given name does not match any types
|
||||
* @param name the name to search a LafType for.
|
||||
* @return the LafType for the given name or null if the given name does not match any types
|
||||
*/
|
||||
public static LafType fromName(String name) {
|
||||
for (LafType type : values()) {
|
||||
if (type.getName().equals(name)) {
|
||||
@@ -54,6 +67,12 @@ public enum LafType {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the {@link LookAndFeel} represented by this LafType is supported on the
|
||||
* current platform.
|
||||
* @return true if the {@link LookAndFeel} represented by this LafType is supported on the
|
||||
* current platform
|
||||
*/
|
||||
public boolean isSupported() {
|
||||
LookAndFeelInfo[] installedLookAndFeels = UIManager.getInstalledLookAndFeels();
|
||||
for (LookAndFeelInfo info : installedLookAndFeels) {
|
||||
@@ -64,6 +83,12 @@ public enum LafType {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a LookAndFeelManager that can install and update the {@link LookAndFeel} associated
|
||||
* with this LafType.
|
||||
* @return a LookAndFeelManager that can install and update the {@link LookAndFeel} associated
|
||||
* with this LafType.
|
||||
*/
|
||||
public LookAndFeelManager getLookAndFeelManager() {
|
||||
return getManager(this);
|
||||
}
|
||||
@@ -90,6 +115,10 @@ public enum LafType {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default LafType for the current platform.
|
||||
* @return the default LafType for the current platform.
|
||||
*/
|
||||
public static LafType getDefaultLookAndFeel() {
|
||||
OperatingSystem OS = Platform.CURRENT_PLATFORM.getOperatingSystem();
|
||||
switch (OS) {
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/* ###
|
||||
* 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 javax.swing.LookAndFeel;
|
||||
|
||||
/**
|
||||
* Event for when a theme value changes;
|
||||
*/
|
||||
public class ThemeEvent {
|
||||
|
||||
/**
|
||||
* Returns true if the color associated with the given id has changed.
|
||||
* @param id the color id to test if changed
|
||||
* @return true if the color associated with the given id has changed
|
||||
*/
|
||||
public boolean isColorChanged(String id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the font associated with the given id has changed.
|
||||
* @param id the font id to test if changed
|
||||
* @return true if the font associated with the given id has changed
|
||||
*/
|
||||
public boolean isFontChanged(String id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the icon associated with the given id has changed.
|
||||
* @param id the icon id to test if changed
|
||||
* @return true if the icon associated with the given id has changed
|
||||
*/
|
||||
public boolean isIconChanged(String id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the {@link LookAndFeel} has changed (theme changed).
|
||||
* @return true if the {@link LookAndFeel} has changed (theme changed).
|
||||
*/
|
||||
public boolean isLookAndFeelChanged() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if any color value changed.
|
||||
* @return true if any color value changed.
|
||||
*/
|
||||
public boolean hasAnyColorChanged() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if any font value changed.
|
||||
* @return true if any font value changed.
|
||||
*/
|
||||
public boolean hasAnyFontChanged() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if any icon value changed.
|
||||
* @return true if any icon value changed.
|
||||
*/
|
||||
public boolean hasAnyIconChanged() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if all colors, fonts, and icons may have changed. This doesn't guarantee that
|
||||
* all the values have actually changed, just that they might have. In other words, a mass
|
||||
* change occurred (theme change, theme reset, etc.) and any or all values may have changed.
|
||||
* @return true if all colors, fonts, and icons may have changed.
|
||||
*/
|
||||
public boolean haveAllValuesChanged() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -15,24 +15,15 @@
|
||||
*/
|
||||
package generic.theme;
|
||||
|
||||
/**
|
||||
* Listener interface for theme changes
|
||||
*/
|
||||
public interface ThemeListener {
|
||||
public default void themeChanged(GTheme newTheme) {
|
||||
// default do nothing
|
||||
}
|
||||
|
||||
public default void colorChanged(String id) {
|
||||
// default do nothing
|
||||
}
|
||||
/**
|
||||
* Called when the theme or any of its values change
|
||||
* @param event the {@link ThemeEvent} that describes what changed
|
||||
*/
|
||||
public void themeChanged(ThemeEvent event);
|
||||
|
||||
public default void fontChanged(String id) {
|
||||
// default do nothing
|
||||
}
|
||||
|
||||
public default void iconChanged(String id) {
|
||||
// default do nothing
|
||||
}
|
||||
|
||||
public default void themeValuesRestored() {
|
||||
// default do nothing
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ 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 ThemePropertiesLoader {
|
||||
GThemeValueMap defaults = new GThemeValueMap();
|
||||
GThemeValueMap darkDefaults = new GThemeValueMap();
|
||||
@@ -29,6 +33,10 @@ public class ThemePropertiesLoader {
|
||||
public ThemePropertiesLoader() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for all the theme.property files and loads them into either the standard
|
||||
* defaults (light) map or the dark defaults map.
|
||||
*/
|
||||
public void load() {
|
||||
List<ResourceFile> themeDefaultFiles =
|
||||
Application.findFilesByExtensionInApplication(".theme.properties");
|
||||
@@ -49,10 +57,18 @@ public class ThemePropertiesLoader {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the standard defaults {@link GThemeValueMap}
|
||||
* @return the standard defaults {@link GThemeValueMap}
|
||||
*/
|
||||
public GThemeValueMap getDefaults() {
|
||||
return defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the dark defaults {@link GThemeValueMap}
|
||||
* @return the dark defaults {@link GThemeValueMap}
|
||||
*/
|
||||
public GThemeValueMap getDarkDefaults() {
|
||||
return darkDefaults;
|
||||
}
|
||||
|
||||
+39
-228
@@ -15,264 +15,75 @@
|
||||
*/
|
||||
package generic.theme;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.plaf.FontUIResource;
|
||||
|
||||
import org.apache.commons.collections4.map.HashedMap;
|
||||
|
||||
import generic.jar.ResourceFile;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.WebColors;
|
||||
import resources.ResourceManager;
|
||||
|
||||
public class ThemePropertyFileReader {
|
||||
/**
|
||||
* Reads the values for a single theme.properities file
|
||||
*/
|
||||
public class ThemePropertyFileReader extends AbstractThemeReader {
|
||||
|
||||
private static final String NO_SECTION = "[No Section]";
|
||||
private static final String DEFAULTS = "[Defaults]";
|
||||
private static final String DARK_DEFAULTS = "[Dark Defaults]";
|
||||
|
||||
private GThemeValueMap defaults = new GThemeValueMap();
|
||||
private GThemeValueMap darkDefaults = new GThemeValueMap();
|
||||
private Map<String, List<String>> aliasMap = new HashedMap<>();
|
||||
private List<String> errors = new ArrayList<>();
|
||||
private String filePath;
|
||||
|
||||
public ThemePropertyFileReader(File file) throws IOException {
|
||||
filePath = file.getAbsolutePath();
|
||||
try (Reader reader = new FileReader(file)) {
|
||||
read(reader);
|
||||
}
|
||||
}
|
||||
private GThemeValueMap defaults;
|
||||
private GThemeValueMap darkDefaults;
|
||||
|
||||
/**
|
||||
* Constructor for when the the theme.properties file is a {@link ResourceFile}
|
||||
* @param file the {@link ResourceFile} esourceFileto read
|
||||
* @throws IOException if an I/O error occurs reading the file
|
||||
*/
|
||||
public ThemePropertyFileReader(ResourceFile file) throws IOException {
|
||||
filePath = file.getAbsolutePath();
|
||||
super(file.getAbsolutePath());
|
||||
|
||||
try (Reader reader = new InputStreamReader(file.getInputStream())) {
|
||||
read(reader);
|
||||
}
|
||||
}
|
||||
|
||||
protected ThemePropertyFileReader() {
|
||||
|
||||
}
|
||||
|
||||
ThemePropertyFileReader(String source, Reader reader) throws IOException {
|
||||
filePath = source;
|
||||
/**
|
||||
* Constructor using a Reader (needed for reading from zip files).
|
||||
* @param source the name or description of the Reader source
|
||||
* @param reader the {@link Reader} to parse as theme data
|
||||
* @throws IOException if an I/O error occurs while reading from the Reader
|
||||
*/
|
||||
protected ThemePropertyFileReader(String source, Reader reader) throws IOException {
|
||||
super(source);
|
||||
read(reader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the map of standard defaults values.
|
||||
* @return the map of standard defaults values.
|
||||
*/
|
||||
public GThemeValueMap getDefaultValues() {
|
||||
return defaults;
|
||||
return defaults == null ? new GThemeValueMap() : defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the map of dark defaults values.
|
||||
* @return the map of dark defaults values.
|
||||
*/
|
||||
public GThemeValueMap getDarkDefaultValues() {
|
||||
return darkDefaults;
|
||||
}
|
||||
|
||||
public Map<String, List<String>> getAliases() {
|
||||
return aliasMap;
|
||||
}
|
||||
|
||||
public List<String> getErrors() {
|
||||
return errors;
|
||||
}
|
||||
|
||||
protected void read(Reader reader) throws IOException {
|
||||
List<Section> sections = readSections(new LineNumberReader(reader));
|
||||
for (Section section : sections) {
|
||||
switch (section.getName()) {
|
||||
case NO_SECTION:
|
||||
processNoSection(section);
|
||||
break;
|
||||
case DEFAULTS:
|
||||
processValues(defaults, section);
|
||||
break;
|
||||
case DARK_DEFAULTS:
|
||||
processValues(darkDefaults, section);
|
||||
break;
|
||||
default:
|
||||
error(section.getLineNumber(),
|
||||
"Encounded unknown theme file section: " + section.getName());
|
||||
}
|
||||
}
|
||||
|
||||
return darkDefaults == null ? new GThemeValueMap() : darkDefaults;
|
||||
}
|
||||
|
||||
protected void processNoSection(Section section) throws IOException {
|
||||
if (!section.isEmpty()) {
|
||||
error(0, "Theme properties file has values defined outside of a defined section");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void processValues(GThemeValueMap valueMap, Section section) {
|
||||
for (String key : section.getKeys()) {
|
||||
String value = section.getValue(key);
|
||||
int lineNumber = section.getLineNumber(key);
|
||||
if (ColorValue.isColorKey(key)) {
|
||||
valueMap.addColor(parseColorProperty(key, value, lineNumber));
|
||||
}
|
||||
else if (FontValue.isFontKey(key)) {
|
||||
valueMap.addFont(parseFontProperty(key, value, lineNumber));
|
||||
}
|
||||
else if (IconValue.isIconKey(key)) {
|
||||
if (!FileGTheme.JAVA_ICON.equals(value)) {
|
||||
valueMap.addIcon(parseIconProperty(key, value));
|
||||
}
|
||||
}
|
||||
else {
|
||||
error(lineNumber, "Can't process property: " + key + " = " + value);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
protected void processDefaultSection(Section section) throws IOException {
|
||||
defaults = new GThemeValueMap();
|
||||
processValues(defaults, section);
|
||||
}
|
||||
|
||||
private IconValue parseIconProperty(String key, String value) {
|
||||
if (IconValue.isIconKey(value)) {
|
||||
return new IconValue(key, value);
|
||||
}
|
||||
Icon icon = ResourceManager.loadImage(value);
|
||||
return new IconValue(key, icon);
|
||||
}
|
||||
|
||||
private FontValue parseFontProperty(String key, String value, int lineNumber) {
|
||||
if (FontValue.isFontKey(value)) {
|
||||
return new FontValue(key, value);
|
||||
}
|
||||
Font font = Font.decode(value);
|
||||
if (font == null) {
|
||||
error(lineNumber, "Could not parse Color: " + value);
|
||||
}
|
||||
return font == null ? null : new FontValue(key, new FontUIResource(font));
|
||||
}
|
||||
|
||||
private ColorValue parseColorProperty(String key, String value, int lineNumber) {
|
||||
if (ColorValue.isColorKey(value)) {
|
||||
return new ColorValue(key, value);
|
||||
}
|
||||
Color color = WebColors.getColor(value);
|
||||
if (color == null) {
|
||||
error(lineNumber, "Could not parse Color: " + value);
|
||||
}
|
||||
return color == null ? null : new ColorValue(key, color);
|
||||
}
|
||||
|
||||
private List<Section> readSections(LineNumberReader reader) throws IOException {
|
||||
|
||||
List<Section> sections = new ArrayList<>();
|
||||
Section currentSection = new Section(NO_SECTION, 0);
|
||||
sections.add(currentSection);
|
||||
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
line = removeComments(line);
|
||||
|
||||
if (line.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isSectionHeader(line)) {
|
||||
currentSection = new Section(line, reader.getLineNumber());
|
||||
sections.add(currentSection);
|
||||
}
|
||||
else {
|
||||
currentSection.add(line, reader.getLineNumber());
|
||||
}
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
private String removeComments(String line) {
|
||||
// remove any trailing comment on line
|
||||
int commentIndex = line.indexOf("//");
|
||||
if (commentIndex >= 0) {
|
||||
line = line.substring(0, commentIndex);
|
||||
}
|
||||
line = line.trim();
|
||||
|
||||
// clear line if entire line is comment
|
||||
if (line.startsWith("#")) {
|
||||
return "";
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
private boolean isSectionHeader(String line) {
|
||||
return line.startsWith("[") && line.endsWith("]");
|
||||
}
|
||||
|
||||
protected void error(int lineNumber, String message) {
|
||||
String msg =
|
||||
"Error parsing file \"" + filePath + "\" at line: " + lineNumber + ", " + message;
|
||||
errors.add(msg);
|
||||
Msg.out(msg);
|
||||
}
|
||||
|
||||
protected class Section {
|
||||
|
||||
private String name;
|
||||
Map<String, String> properties = new HashMap<>();
|
||||
Map<String, Integer> lineNumbers = new HashMap<>();
|
||||
private int startLineNumber;
|
||||
|
||||
public Section(String sectionName, int lineNumber) {
|
||||
this.name = sectionName;
|
||||
this.startLineNumber = lineNumber;
|
||||
}
|
||||
|
||||
public void remove(String key) {
|
||||
properties.remove(key);
|
||||
}
|
||||
|
||||
public String getValue(String key) {
|
||||
return properties.get(key);
|
||||
}
|
||||
|
||||
public Set<String> getKeys() {
|
||||
return properties.keySet();
|
||||
}
|
||||
|
||||
public int getLineNumber(String key) {
|
||||
return lineNumbers.get(key);
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return properties.isEmpty();
|
||||
}
|
||||
|
||||
public int getLineNumber() {
|
||||
return startLineNumber;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void add(String line, int lineNumber) {
|
||||
int splitIndex = line.indexOf('=');
|
||||
if (splitIndex < 0) {
|
||||
error(lineNumber, "Missing required \"=\" for propery line: \"" + line + "\"");
|
||||
return;
|
||||
}
|
||||
String key = line.substring(0, splitIndex).trim();
|
||||
String value = line.substring(splitIndex + 1, line.length()).trim();
|
||||
if (key.isBlank()) {
|
||||
error(lineNumber, "Missing key for propery line: \"" + line + "\"");
|
||||
return;
|
||||
}
|
||||
if (key.isBlank()) {
|
||||
error(lineNumber, "Missing value for propery line: \"" + line + "\"");
|
||||
return;
|
||||
}
|
||||
properties.put(key, value);
|
||||
lineNumbers.put(key, lineNumber);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processDarkDefaultSection(Section section) throws IOException {
|
||||
darkDefaults = new GThemeValueMap();
|
||||
processValues(darkDefaults, section);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,59 +15,131 @@
|
||||
*/
|
||||
package generic.theme;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.*;
|
||||
import java.util.Enumeration;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
public class ThemeReader extends ThemePropertyFileReader {
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
private Section themeSection;
|
||||
private String themeName;
|
||||
private LafType lookAndFeel;
|
||||
private boolean useDarkDefaults;
|
||||
import ghidra.framework.Application;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
public ThemeReader(File file) throws IOException {
|
||||
super(file);
|
||||
/**
|
||||
* Reads Themes from a file or {@link Reader}
|
||||
*/
|
||||
class ThemeReader extends AbstractThemeReader {
|
||||
|
||||
private File file;
|
||||
private GTheme theme;
|
||||
|
||||
/**
|
||||
* Constructor for reading a theme from a file.
|
||||
* @param file the file to read as a theme
|
||||
* @throws IOException if an I/O error occurs reading the theme file
|
||||
*/
|
||||
ThemeReader(File file) throws IOException {
|
||||
super(file.getAbsolutePath());
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
protected ThemeReader() {
|
||||
public GTheme readTheme() throws IOException {
|
||||
if (file.getName().endsWith(GTheme.FILE_EXTENSION)) {
|
||||
return readFileTheme();
|
||||
}
|
||||
if (file.getName().endsWith(GTheme.ZIP_FILE_EXTENSION)) {
|
||||
return readZipTheme();
|
||||
}
|
||||
|
||||
throw new IOException("Imported File must end in either " + GTheme.FILE_EXTENSION + " or " +
|
||||
GTheme.ZIP_FILE_EXTENSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assumes the file is a theme file and reads it.
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
private GTheme readFileTheme() throws IOException {
|
||||
try (Reader reader = new FileReader(file)) {
|
||||
read(reader);
|
||||
}
|
||||
if (theme == null) {
|
||||
throw new IOException("Invalid Theme file: " + file);
|
||||
}
|
||||
return theme;
|
||||
}
|
||||
|
||||
private GTheme readZipTheme() throws IOException {
|
||||
try (ZipFile zipFile = new ZipFile(file)) {
|
||||
Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry entry = entries.nextElement();
|
||||
String name = entry.getName();
|
||||
try (InputStream is = zipFile.getInputStream(entry)) {
|
||||
if (name.endsWith(".theme")) {
|
||||
processThemeData(name, is);
|
||||
}
|
||||
else {
|
||||
processIconFile(name, is);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return theme;
|
||||
}
|
||||
|
||||
// for testing
|
||||
GTheme readTheme(Reader reader) throws IOException {
|
||||
read(reader);
|
||||
return theme;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processNoSection(Section section) throws IOException {
|
||||
themeSection = section;
|
||||
themeName = section.getValue(GTheme.THEME_NAME_KEY);
|
||||
String themeName = section.getValue(ThemeWriter.THEME_NAME_KEY);
|
||||
if (themeName == null) {
|
||||
throw new IOException("Missing theme name!");
|
||||
}
|
||||
String lookAndFeelName = section.getValue(GTheme.THEME_LOOK_AND_FEEL_KEY);
|
||||
lookAndFeel = LafType.fromName(lookAndFeelName);
|
||||
String lookAndFeelName = section.getValue(ThemeWriter.THEME_LOOK_AND_FEEL_KEY);
|
||||
LafType lookAndFeel = LafType.fromName(lookAndFeelName);
|
||||
if (lookAndFeel == null) {
|
||||
throw new IOException(
|
||||
"Invalid or missing lookAndFeel name: \"" + lookAndFeelName + "\"");
|
||||
}
|
||||
useDarkDefaults = Boolean.valueOf(section.getValue(GTheme.THEME_USE_DARK_DEFAULTS));
|
||||
boolean isDark = Boolean.valueOf(section.getValue(ThemeWriter.THEME_USE_DARK_DEFAULTS));
|
||||
|
||||
theme = new GTheme(file, themeName, lookAndFeel, isDark);
|
||||
section.remove(ThemeWriter.THEME_NAME_KEY);
|
||||
section.remove(ThemeWriter.THEME_LOOK_AND_FEEL_KEY);
|
||||
section.remove(ThemeWriter.THEME_USE_DARK_DEFAULTS);
|
||||
processValues(theme, section);
|
||||
}
|
||||
|
||||
void loadValues(GTheme theme) {
|
||||
|
||||
// processValues expects only colors, fonts, and icons
|
||||
themeSection.remove(GTheme.THEME_NAME_KEY);
|
||||
themeSection.remove(GTheme.THEME_LOOK_AND_FEEL_KEY);
|
||||
themeSection.remove(GTheme.THEME_USE_DARK_DEFAULTS);
|
||||
|
||||
processValues(theme, themeSection);
|
||||
@Override
|
||||
protected void processDefaultSection(Section section) throws IOException {
|
||||
error(section.getLineNumber(), "[Defaults] section not allowed in theme files!");
|
||||
}
|
||||
|
||||
public String getThemeName() {
|
||||
return themeName;
|
||||
@Override
|
||||
protected void processDarkDefaultSection(Section section) throws IOException {
|
||||
error(section.getLineNumber(), "[Dark Defaults] section not allowed in theme files!");
|
||||
}
|
||||
|
||||
public LafType getLookAndFeelType() {
|
||||
return lookAndFeel;
|
||||
private void processIconFile(String path, InputStream is) throws IOException {
|
||||
int indexOf = path.indexOf("images/");
|
||||
if (indexOf < 0) {
|
||||
Msg.error(this, "Unknown file: " + path);
|
||||
}
|
||||
String relativePath = path.substring(indexOf, path.length());
|
||||
File dir = Application.getUserSettingsDirectory();
|
||||
File iconFile = new File(dir, relativePath);
|
||||
FileUtils.copyInputStreamToFile(is, iconFile);
|
||||
}
|
||||
|
||||
public boolean useDarkDefaults() {
|
||||
return useDarkDefaults;
|
||||
private void processThemeData(String name, InputStream is) throws IOException {
|
||||
InputStreamReader reader = new InputStreamReader(is);
|
||||
read(reader);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,41 +19,64 @@ import java.util.*;
|
||||
|
||||
import ghidra.util.Msg;
|
||||
|
||||
// TODO doc why 'cachedValue' is lazy loaded
|
||||
/**
|
||||
* A generic class for storing theme values that have a String id (e.g. color.bg.foo) and either
|
||||
* a concrete value of type T or a reference id which is the String id of another ThemeValue. So
|
||||
* if this class's value is non-null, the refId will be null and if the class's refId is non-null,
|
||||
* then the value will be null.
|
||||
*
|
||||
* @param <T> the base type this ThemeValue works on (i.e., Colors, Fonts, Icons)
|
||||
*/
|
||||
public abstract class ThemeValue<T> implements Comparable<ThemeValue<T>> {
|
||||
private final String id;
|
||||
private final T value;
|
||||
private final String refId;
|
||||
// private T cachedValue;
|
||||
|
||||
protected ThemeValue(String id, String refId, T value) {
|
||||
this.id = fromExternalId(id);
|
||||
this.refId = (refId == null) ? null : fromExternalId(refId);
|
||||
this.value = value;
|
||||
if (value instanceof GColor) {
|
||||
System.out.println("Whoa");
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract String getIdPrefix();
|
||||
|
||||
/**
|
||||
* Returns the identifier for this ThemeValue.
|
||||
* @return the identifier for this ThemeValue.
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the referencId of another ThemeValue that we inherit its value pr null if we have
|
||||
* a value
|
||||
*
|
||||
* @return the referencId of another ThemeValue that we inherit its value or null if we have
|
||||
* a value
|
||||
*/
|
||||
public String getReferenceId() {
|
||||
return refId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stored value. Does not follow referenceIds. Will be null if this instance
|
||||
* has a referenceId.
|
||||
*
|
||||
* @return the stored value. Does not follow referenceIds. Will be null if this instance
|
||||
* has a referenceId.
|
||||
*/
|
||||
public T getRawValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the T value for this instance, following references as needed. Uses the given
|
||||
* preferredValues map to resolve references.
|
||||
* @param preferredValues the {@link GThemeValueMap} used to resolve references if this
|
||||
* instance doesn't have an actual value.
|
||||
* @return the T value for this instance, following references as needed.
|
||||
*/
|
||||
public T get(GThemeValueMap preferredValues) {
|
||||
// if (cachedValue == null) {
|
||||
return doGetValue(preferredValues);
|
||||
// }
|
||||
// return cachedValue;
|
||||
}
|
||||
|
||||
private T doGetValue(GThemeValueMap values) {
|
||||
@@ -75,35 +98,44 @@ public abstract class ThemeValue<T> implements Comparable<ThemeValue<T>> {
|
||||
return getUnresolvedReferenceValue(id);
|
||||
}
|
||||
|
||||
abstract protected T getUnresolvedReferenceValue(String theId);
|
||||
/**
|
||||
* Returns the T to be used if the indirect reference couldn't be resolved.
|
||||
* @param unresolvedId the id that couldn't be resolved
|
||||
* @return the default value to be used if the indirect reference couldn't be resolved.
|
||||
*/
|
||||
abstract protected T getUnresolvedReferenceValue(String unresolvedId);
|
||||
|
||||
/**
|
||||
* Returns the id to be used when writing to a theme file. For ThemeValues whose id begins
|
||||
* with the expected prefix (e.g. "color" for ColorValues), it is just the id. Otherwise, the
|
||||
* id is prepended with an appropriate string to make parsing easier.
|
||||
* @param internalId the id of this ThemeValue
|
||||
* @return the id to be used when writing to a theme file
|
||||
*/
|
||||
abstract public String toExternalId(String internalId);
|
||||
|
||||
/**
|
||||
* Converts an external id to an internal id (the id stored in this object)
|
||||
* @param externalId the external form of the id
|
||||
* @return the id for the ThemeValue being read from a file
|
||||
*/
|
||||
abstract public String fromExternalId(String externalId);
|
||||
|
||||
/**
|
||||
* Returns the ThemeValue referred to by this ThemeValue. Needs to be overridden by
|
||||
* concrete classes as they know the correct method to call on the preferredValues map.
|
||||
* @param preferredValues the {@link GThemeValueMap} to be used to resolve the reference id
|
||||
* @param referenceId the id of the reference ThemeValue
|
||||
* @return the ThemeValue referred to by this ThemeValue.
|
||||
*/
|
||||
abstract protected ThemeValue<T> getReferredValue(GThemeValueMap preferredValues,
|
||||
String theRefId);
|
||||
String referenceId);
|
||||
|
||||
@Override
|
||||
public int compareTo(ThemeValue<T> o) {
|
||||
return id.compareTo(o.id);
|
||||
}
|
||||
|
||||
public int compareValue(ThemeValue<T> o) {
|
||||
if (o == null) {
|
||||
return -1;
|
||||
}
|
||||
if (refId != null) {
|
||||
return o.refId != null ? refId.compareTo(o.refId) : -1;
|
||||
}
|
||||
if (o.refId != null) {
|
||||
return 1;
|
||||
}
|
||||
return compareValues(value, o.value);
|
||||
}
|
||||
|
||||
protected abstract int compareValues(T v1, T v2);
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, refId, value);
|
||||
|
||||
+98
-51
@@ -18,75 +18,98 @@ package generic.theme;
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.io.*;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import javax.swing.Icon;
|
||||
|
||||
import com.google.common.io.Files;
|
||||
|
||||
import ghidra.util.WebColors;
|
||||
import resources.icons.UrlImageIcon;
|
||||
|
||||
public class FileGTheme extends GTheme {
|
||||
public static final String JAVA_ICON = "<JAVA ICON>";
|
||||
public static final String FILE_PREFIX = "File:";
|
||||
protected final File file;
|
||||
/**
|
||||
* Writes a theme to a file either as a single theme file or as a zip file that contains the theme
|
||||
* file and any external (from the file system, not the classpath) icons used by the theme.
|
||||
*/
|
||||
public class ThemeWriter {
|
||||
static final String THEME_NAME_KEY = "name";
|
||||
static final String THEME_LOOK_AND_FEEL_KEY = "lookAndFeel";
|
||||
static final String THEME_USE_DARK_DEFAULTS = "useDarkDefaults";
|
||||
protected GTheme theme;
|
||||
|
||||
public FileGTheme(File file) throws IOException {
|
||||
this(file, new ThemeReader(file));
|
||||
/**
|
||||
* Constructor
|
||||
* @param theme the theme to be written to a file
|
||||
*/
|
||||
public ThemeWriter(GTheme theme) {
|
||||
this.theme = theme;
|
||||
}
|
||||
|
||||
public FileGTheme(File file, String name, LafType laf, boolean useDarkDefaults) {
|
||||
super(name, laf, useDarkDefaults);
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
FileGTheme(File file, ThemeReader reader) {
|
||||
super(reader.getThemeName(), reader.getLookAndFeelType(), reader.useDarkDefaults());
|
||||
this.file = file;
|
||||
reader.loadValues(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getThemeLocater() {
|
||||
return FILE_PREFIX + file.getAbsolutePath();
|
||||
}
|
||||
|
||||
public boolean canSave() {
|
||||
if (file.exists()) {
|
||||
return file.canWrite();
|
||||
/**
|
||||
* Writes the theme to the given file with the option to output as a zip file.
|
||||
* @param file the file to write to
|
||||
* @param asZip if true, outputs in zip format
|
||||
* @throws FileNotFoundException i
|
||||
* @throws IOException if an I/O error occurs trying to write the file
|
||||
*/
|
||||
public void writeTheme(File file, boolean asZip) throws IOException {
|
||||
if (asZip) {
|
||||
writeThemeToZipFile(file);
|
||||
}
|
||||
else {
|
||||
writeThemeToFile(file);
|
||||
}
|
||||
return file.getParentFile().canWrite();
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public void save() throws IOException {
|
||||
/**
|
||||
* Writes the theme to the given file.
|
||||
* @param file the file to write to
|
||||
* @throws FileNotFoundException i
|
||||
* @throws IOException if an I/O error occurs trying to write the file
|
||||
*/
|
||||
public void writeThemeToFile(File file) throws IOException {
|
||||
try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
|
||||
writeThemeValues(writer);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the theme to the given file in a zip format.
|
||||
* @param file the file to write to
|
||||
* @throws IOException if an I/O error occurs trying to write the file
|
||||
*/
|
||||
public void writeThemeToZipFile(File file) throws IOException {
|
||||
String dir = theme.getName() + ".theme/";
|
||||
try (FileOutputStream fos = new FileOutputStream(file)) {
|
||||
ZipOutputStream zos = new ZipOutputStream(fos);
|
||||
saveThemeFileToZip(dir, zos);
|
||||
Set<File> iconFiles = theme.getExternalIconFiles();
|
||||
for (File iconFile : iconFiles) {
|
||||
copyToZipFile(dir, iconFile, zos);
|
||||
}
|
||||
zos.finish();
|
||||
}
|
||||
}
|
||||
|
||||
protected void writeThemeValues(BufferedWriter writer) throws IOException {
|
||||
List<ColorValue> colors = getColors();
|
||||
List<ColorValue> colors = theme.getColors();
|
||||
Collections.sort(colors);
|
||||
|
||||
List<FontValue> fonts = getFonts();
|
||||
List<FontValue> fonts = theme.getFonts();
|
||||
Collections.sort(fonts);
|
||||
|
||||
List<IconValue> icons = getIcons();
|
||||
List<IconValue> icons = theme.getIcons();
|
||||
Collections.sort(icons);
|
||||
|
||||
writer.write(THEME_NAME_KEY + " = " + getName());
|
||||
writer.write(THEME_NAME_KEY + " = " + theme.getName());
|
||||
writer.newLine();
|
||||
|
||||
writer.write(THEME_LOOK_AND_FEEL_KEY + " = " + getLookAndFeelType().getName());
|
||||
writer.write(THEME_LOOK_AND_FEEL_KEY + " = " + theme.getLookAndFeelType().getName());
|
||||
writer.newLine();
|
||||
|
||||
writer.write(THEME_USE_DARK_DEFAULTS + " = " + useDarkDefaults());
|
||||
writer.write(THEME_USE_DARK_DEFAULTS + " = " + theme.useDarkDefaults());
|
||||
writer.newLine();
|
||||
|
||||
for (ColorValue colorValue : colors) {
|
||||
@@ -129,13 +152,6 @@ public class FileGTheme extends GTheme {
|
||||
return iconToString(icon);
|
||||
}
|
||||
|
||||
public static String iconToString(Icon icon) {
|
||||
if (icon instanceof UrlImageIcon urlIcon) {
|
||||
return urlIcon.getOriginalPath();
|
||||
}
|
||||
return JAVA_ICON;
|
||||
}
|
||||
|
||||
private String getValueOutput(FontValue fontValue) {
|
||||
if (fontValue.getReferenceId() != null) {
|
||||
return fontValue.toExternalId(fontValue.getReferenceId());
|
||||
@@ -144,10 +160,6 @@ public class FileGTheme extends GTheme {
|
||||
return fontToString(font);
|
||||
}
|
||||
|
||||
public static String fontToString(Font font) {
|
||||
return String.format("%s-%s-%s", font.getName(), getStyleString(font), font.getSize());
|
||||
}
|
||||
|
||||
private static String getStyleString(Font font) {
|
||||
boolean bold = font.isBold();
|
||||
boolean italic = font.isItalic();
|
||||
@@ -162,4 +174,39 @@ public class FileGTheme extends GTheme {
|
||||
}
|
||||
return "PLAIN";
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a file to a string.
|
||||
* @param font the font to convert to a String
|
||||
* @return a String that represents the font
|
||||
*/
|
||||
public static String fontToString(Font font) {
|
||||
return String.format("%s-%s-%s", font.getName(), getStyleString(font), font.getSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an icon to a string.
|
||||
* @param icon the icon to convert to a String
|
||||
* @return a String that represents the icon
|
||||
*/
|
||||
public static String iconToString(Icon icon) {
|
||||
if (icon instanceof UrlImageIcon urlIcon) {
|
||||
return urlIcon.getOriginalPath();
|
||||
}
|
||||
return GTheme.JAVA_ICON;
|
||||
}
|
||||
|
||||
private void copyToZipFile(String dir, File iconFile, ZipOutputStream zos) throws IOException {
|
||||
ZipEntry entry = new ZipEntry(dir + "images/" + iconFile.getName());
|
||||
zos.putNextEntry(entry);
|
||||
Files.copy(iconFile, zos);
|
||||
}
|
||||
|
||||
private void saveThemeFileToZip(String dir, ZipOutputStream zos) throws IOException {
|
||||
ZipEntry entry = new ZipEntry(dir + theme.getName() + ".theme");
|
||||
zos.putNextEntry(entry);
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(zos));
|
||||
writeThemeValues(writer);
|
||||
writer.flush();
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/* ###
|
||||
* 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.*;
|
||||
import java.util.Set;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import com.google.common.io.Files;
|
||||
|
||||
public class ZipGTheme extends FileGTheme {
|
||||
|
||||
public ZipGTheme(File file, String name, LafType laf, boolean useDarkDefaults) {
|
||||
super(file, name, laf, useDarkDefaults);
|
||||
}
|
||||
|
||||
public ZipGTheme(File file) throws IOException {
|
||||
this(file, new ExternalThemeReader(file));
|
||||
}
|
||||
|
||||
public ZipGTheme(File file, ThemeReader reader) {
|
||||
super(file, reader.getThemeName(), reader.getLookAndFeelType(), reader.useDarkDefaults());
|
||||
reader.loadValues(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save() throws IOException {
|
||||
String dir = getName() + ".theme/";
|
||||
try (FileOutputStream fos = new FileOutputStream(file)) {
|
||||
ZipOutputStream zos = new ZipOutputStream(fos);
|
||||
saveThemeFileToZip(dir, zos);
|
||||
Set<File> iconFiles = getExternalIconFiles();
|
||||
for (File iconFile : iconFiles) {
|
||||
copyToZipFile(dir, iconFile, zos);
|
||||
}
|
||||
zos.finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void copyToZipFile(String dir, File iconFile, ZipOutputStream zos) throws IOException {
|
||||
ZipEntry entry = new ZipEntry(dir + "images/" + iconFile.getName());
|
||||
zos.putNextEntry(entry);
|
||||
Files.copy(iconFile, zos);
|
||||
}
|
||||
|
||||
private void saveThemeFileToZip(String dir, ZipOutputStream zos) throws IOException {
|
||||
ZipEntry entry = new ZipEntry(dir + getName() + ".theme");
|
||||
zos.putNextEntry(entry);
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(zos));
|
||||
writeThemeValues(writer);
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
}
|
||||
+4
-1
@@ -36,7 +36,10 @@ public class NimbusLookAndFeelInstaller extends LookAndFeelInstaller {
|
||||
|
||||
@Override
|
||||
protected void installJavaDefaults() {
|
||||
// do nothing - already handled by installing extended NimbusLookAndFeel
|
||||
// even though java defaults have been installed, we need to fix them up now
|
||||
// that nimbus has finished initializing
|
||||
GColor.refreshAll();
|
||||
Gui.setJavaDefaults(Gui.getJavaDefaults());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -35,8 +35,8 @@ public class GThemeTest extends AbstractGenericTest {
|
||||
private static final Font COURIER = new Font("Courier", Font.BOLD, 14);
|
||||
private static final Font DIALOG = new Font("Dialog", Font.PLAIN, 16);
|
||||
private static final Color COLOR_WITH_ALPHA = new Color(10, 20, 30, 40);
|
||||
private static final String ICON_PATH_1 = "images/arrow.png";
|
||||
private static final String ICON_PATH_2 = "images/disk.png";
|
||||
private static final String ICON_PATH_1 = "images/error.png";
|
||||
private static final String ICON_PATH_2 = "images/exec.png";
|
||||
private static final Icon ICON1 = ResourceManager.loadImage(ICON_PATH_1);
|
||||
private static final Icon ICON2 = ResourceManager.loadImage(ICON_PATH_2);
|
||||
|
||||
@@ -50,7 +50,7 @@ public class GThemeTest extends AbstractGenericTest {
|
||||
|
||||
@Test
|
||||
public void testGetName() {
|
||||
assertEquals("Default", theme.getName());
|
||||
assertEquals(Gui.getDefaultTheme().getName(), theme.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -95,10 +95,10 @@ public class GThemeTest extends AbstractGenericTest {
|
||||
theme.setIcon("t.u.v", ICON1);
|
||||
theme.setIconRef("t.u.v.1", "t.u.v");
|
||||
|
||||
File file = createTempFile("themeTest.theme");
|
||||
File file = createTempFile("themeTest", ".theme");
|
||||
|
||||
theme.saveToFile(file, false); // saveToFile returns new theme instance
|
||||
theme = new FileGTheme(file);
|
||||
theme = new ThemeReader(file).readTheme();
|
||||
|
||||
assertEquals("abc", theme.getName());
|
||||
assertEquals(LafType.getDefaultLookAndFeel(), theme.getLookAndFeelType());
|
||||
@@ -116,10 +116,10 @@ public class GThemeTest extends AbstractGenericTest {
|
||||
assertEquals(COURIER, theme.getFont("x.y.z").get(theme));
|
||||
assertEquals(COURIER, theme.getFont("x.y.z.1").get(theme));
|
||||
|
||||
assertEquals(ICON_PATH_1, theme.getIcon("icon.a.1").get(theme));
|
||||
assertEquals(ICON_PATH_2, theme.getIcon("icon.a.2").get(theme));
|
||||
assertEquals(ICON_PATH_1, theme.getIcon("t.u.v").get(theme));
|
||||
assertEquals(ICON_PATH_1, theme.getIcon("t.u.v.1").get(theme));
|
||||
assertEquals(ICON1, theme.getIcon("icon.a.1").get(theme));
|
||||
assertEquals(ICON2, theme.getIcon("icon.a.2").get(theme));
|
||||
assertEquals(ICON1, theme.getIcon("t.u.v").get(theme));
|
||||
assertEquals(ICON1, theme.getIcon("t.u.v.1").get(theme));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
-7
@@ -24,14 +24,11 @@ import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class ThemePropertyFileReaderTest {
|
||||
import resources.ResourceManager;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
}
|
||||
public class ThemePropertyFileReaderTest {
|
||||
|
||||
@Test
|
||||
public void testDefaults() throws IOException {
|
||||
@@ -47,7 +44,7 @@ public class ThemePropertyFileReaderTest {
|
||||
" color.b.7 = color.b.1", // ref
|
||||
" font.a.8 = dialog-PLAIN-14",
|
||||
" font.a.9 = font.a.8",
|
||||
" icon.a.10 = foo.png",
|
||||
" icon.a.10 = core.png",
|
||||
" icon.a.11 = icon.a.10",
|
||||
"")));
|
||||
//@formatter:on
|
||||
@@ -67,7 +64,7 @@ public class ThemePropertyFileReaderTest {
|
||||
assertEquals(new Font("dialog", Font.PLAIN, 14), getFontOrRef(values, "font.a.8"));
|
||||
assertEquals("font.a.8", getFontOrRef(values, "font.a.9"));
|
||||
|
||||
assertEquals("foo.png", getIconOrRef(values, "icon.a.10"));
|
||||
assertEquals(ResourceManager.loadImage("core.png"), getIconOrRef(values, "icon.a.10"));
|
||||
assertEquals("icon.a.10", getIconOrRef(values, "icon.a.11"));
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user