diff --git a/Ghidra/Features/Base/data/base.listing.theme.properties b/Ghidra/Features/Base/data/base.listing.theme.properties index ac529527d8..81510ac0a9 100644 --- a/Ghidra/Features/Base/data/base.listing.theme.properties +++ b/Ghidra/Features/Base/data/base.listing.theme.properties @@ -6,7 +6,7 @@ color.bg.selection.listing = color.bg.selection color.bg.highlight.listing = color.bg.highlight color.bg.listing.tabs.selected = #788CBD -color.bg.listing.tabs.unselected = [color]control +color.bg.listing.tabs.unselected = system.color.bg.application color.bg.listing.tabs.highlighted = #ABC8FF color.bg.listing.tabs.list = rgb(255, 255, 230) color.bg.listing.tabs.more.tabs.hover = rgb(255, 226, 213) @@ -75,7 +75,7 @@ color.fg.listing.pcode.userop = blue [Dark Defaults] color.bg.listing.tabs.selected = #788CBD -color.bg.listing.tabs.unselected = [color]control +color.bg.listing.tabs.unselected = system.color.bg.application color.bg.listing.tabs.highlighted = #ABC8FF color.bg.listing.tabs.list = rgb(255, 255, 230) color.fg.listing.tabs.text.selected = black diff --git a/Ghidra/Features/Base/data/base.theme.properties b/Ghidra/Features/Base/data/base.theme.properties index 2aa066b8a8..2db3831876 100644 --- a/Ghidra/Features/Base/data/base.theme.properties +++ b/Ghidra/Features/Base/data/base.theme.properties @@ -69,7 +69,7 @@ color.bg.plugin.datamgr.edge.reference = blue color.bg.plugin.datamgr.icon.highlight = rgb(204, 204, 255) color.bg.plugin.editors.compositeeditor.text = color.fg -color.bg.plugin.editors.compositeeditor.line = [color]Component.borderColor +color.bg.plugin.editors.compositeeditor.line = system.color.border color.bg.plugin.editors.compositeeditor.line.interior = #D4D4D4 color.bg.plugin.editors.compositeeditor.byte.header = #DFDFDF color.bg.plugin.editors.compositeeditor.bit.undefined = #F8F8F8 diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ColorValueEditor.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ColorValueEditor.java index c2fc6432f7..cadc6022b3 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ColorValueEditor.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ColorValueEditor.java @@ -37,7 +37,7 @@ public class ColorValueEditor extends ThemeValueEditor { @Override protected Color getRawValue(String id) { - return Gui.getRawColor(id); + return Gui.getColor(id); } @Override diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ExportThemeDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ExportThemeDialog.java index baf2254e9b..a2e55e9a34 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ExportThemeDialog.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ExportThemeDialog.java @@ -34,6 +34,9 @@ import ghidra.util.Msg; import ghidra.util.filechooser.GhidraFileFilter; import ghidra.util.layout.PairLayout; +/** + * Dialog for exporting themes to external files or zip files. + */ public class ExportThemeDialog extends DialogComponentProvider { private JTextField nameField; diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/IconValueEditor.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/IconValueEditor.java index 4787baa9f1..c0150a5cce 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/IconValueEditor.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/IconValueEditor.java @@ -20,7 +20,6 @@ import java.beans.PropertyChangeListener; import javax.swing.Icon; import docking.options.editor.IconPropertyEditor; -import docking.theme.*; import generic.theme.*; /** @@ -38,7 +37,7 @@ public class IconValueEditor extends ThemeValueEditor { @Override protected Icon getRawValue(String id) { - return Gui.getRawIcon(id, true); + return Gui.getIcon(id, true); } @Override diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ProtectedIcon.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ProtectedIcon.java index 110c424416..6c9ab3d276 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ProtectedIcon.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ProtectedIcon.java @@ -19,9 +19,16 @@ import java.awt.Component; import java.awt.Graphics; import javax.swing.Icon; +import javax.swing.LookAndFeel; import resources.ResourceManager; +/** + * A wrapper for an icon that suppresses errors. Some Icons that are mined from a + * {@link LookAndFeel} have specialized uses and will throw exceptions if used outside + * their intended component. This class is used when trying to show them in the the theme + * editor table. + */ public class ProtectedIcon implements Icon { Icon bomb = ResourceManager.getDefaultIcon(); Icon delegate; @@ -31,10 +38,6 @@ public class ProtectedIcon implements Icon { this.delegate = delegate; } - public boolean hasError() { - return isError; - } - @Override public void paintIcon(Component c, Graphics g, int x, int y) { try { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeColorTableModel.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeColorTableModel.java index 3caf8da13f..0b95e002e8 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeColorTableModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeColorTableModel.java @@ -33,6 +33,9 @@ import ghidra.util.WebColors; import ghidra.util.table.column.AbstractGColumnRenderer; import ghidra.util.table.column.GColumnRenderer; +/** + * Table model for theme colors + */ public class ThemeColorTableModel extends GDynamicColumnTableModel { private List colors; private GThemeValueMap currentValues; @@ -46,12 +49,19 @@ public class ThemeColorTableModel extends GDynamicColumnTableModel"; } - if (resolvedColor.refId() != null) { - return "[" + resolvedColor.refId() + "]"; - } Color color = resolvedColor.color(); String text = WebColors.toString(color, false); String name = WebColors.toWebColorName(color); + if (resolvedColor.refId() != null) { + return "[" + resolvedColor.refId() + "] " + text; + } if (name != null) { text += " (" + name + ")"; } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeDialog.java index c83b3c5bf6..68da119797 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeDialog.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeDialog.java @@ -37,6 +37,9 @@ import generic.theme.*; import ghidra.util.MessageType; import ghidra.util.Swing; +/** + * Primary dialog for editing Themes. + */ public class ThemeDialog extends DialogComponentProvider { private static ThemeDialog INSTANCE; private ThemeColorTableModel colorTableModel; @@ -122,7 +125,7 @@ public class ThemeDialog extends DialogComponentProvider { return; } } - Gui.reloadGhidraDefaults(); + Gui.reloadApplicationDefaults(); } private void reset() { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeFontTableModel.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeFontTableModel.java index e2e8db7b08..c01b8ebff7 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeFontTableModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeFontTableModel.java @@ -31,6 +31,9 @@ import ghidra.framework.plugintool.ServiceProviderStub; import ghidra.util.table.column.AbstractGColumnRenderer; import ghidra.util.table.column.GColumnRenderer; +/** + * Table model for theme fonts + */ public class ThemeFontTableModel extends GDynamicColumnTableModel { private List fonts; private GThemeValueMap currentValues; @@ -42,6 +45,24 @@ public class ThemeFontTableModel extends GDynamicColumnTableModel"; } - Font font = resolvedFont.font(); - String fontString = ThemeWriter.fontToString(font); - - if (resolvedFont.refId() != null) { - return "[" + resolvedFont.refId() + "]";//+ " [" + fontString + "]"; + if (fontValue.getReferenceId() != null) { + return fontValue.getReferenceId(); } - return fontString; + + Font font = fontValue.getRawValue(); + return FontValue.fontToString(font); } class IdColumn extends AbstractDynamicTableColumn { @@ -106,7 +126,7 @@ public class ThemeFontTableModel extends GDynamicColumnTableModel { + class FontValueColumn extends AbstractDynamicTableColumn { private ThemeFontRenderer renderer; private String name; private Supplier valueSupplier; @@ -123,24 +143,19 @@ public class ThemeFontTableModel extends GDynamicColumnTableModel getColumnRenderer() { + public GColumnRenderer getColumnRenderer() { return renderer; } - public Comparator getComparator() { + public Comparator getComparator() { return (v1, v2) -> { if (v1 == null && v2 == null) { return 0; @@ -162,43 +177,32 @@ public class ThemeFontTableModel extends GDynamicColumnTableModel { - - public ThemeFontRenderer() { - setFont(new Font("Monospaced", Font.PLAIN, 12)); - } + private class ThemeFontRenderer extends AbstractGColumnRenderer { + private Font regularFont = new Font("Monospaced", Font.BOLD, 12); + private Font indirectFont = new Font("Monospaced", Font.ITALIC, 12); @Override public Component getTableCellRendererComponent(GTableCellRenderingData data) { JLabel label = (JLabel) super.getTableCellRendererComponent(data); - ResolvedFont resolved = (ResolvedFont) data.getValue(); + FontValue fontValue = (FontValue) data.getValue(); - String text = getValueText(resolved); + String text = getValueText(fontValue); + if (fontValue.getReferenceId() != null) { + label.setFont(indirectFont); + } + else { + label.setFont(regularFont); + } label.setText(text); label.setOpaque(true); return label; } @Override - public String getFilterString(ResolvedFont fontValue, Settings settings) { + public String getFilterString(FontValue fontValue, Settings settings) { return getValueText(fontValue); } } - record ResolvedFont(String id, String refId, Font font) {/**/} - - public void reloadCurrent() { - - currentValues = Gui.getAllValues(); - fonts = currentValues.getFonts(); - fireTableDataChanged(); - - } - - public void reloadAll() { - load(); - fireTableDataChanged(); - } - } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeIconTableModel.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeIconTableModel.java index 42c7ea826d..5deb69a4b6 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeIconTableModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeIconTableModel.java @@ -32,6 +32,9 @@ import ghidra.util.table.column.AbstractGColumnRenderer; import ghidra.util.table.column.GColumnRenderer; import resources.icons.*; +/** + * Table model for theme icons + */ public class ThemeIconTableModel extends GDynamicColumnTableModel { private List icons; private GThemeValueMap currentValues; @@ -43,6 +46,24 @@ public class ThemeIconTableModel extends GDynamicColumnTableModel savedThemes = new ArrayList<>( Gui.getAllThemes().stream().filter(t -> t.getFile() != null).toList()); @@ -210,7 +234,7 @@ public class ThemeUtils { private static File getSaveFile(String themeName) { File dir = Application.getUserSettingsDirectory(); - File themeDir = new File(dir, Gui.THEME_DIR); + File themeDir = new File(dir, ThemeFileLoader.THEME_DIR); if (!themeDir.exists()) { themeDir.mkdir(); } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/AbstractThemeReader.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/AbstractThemeReader.java index bac3292a23..a7abd2195e 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/AbstractThemeReader.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/AbstractThemeReader.java @@ -15,17 +15,10 @@ */ 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 @@ -101,33 +94,29 @@ public abstract class AbstractThemeReader { } 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); + return IconValue.parse(key, value); } private FontValue parseFontProperty(String key, String value, int lineNumber) { - if (FontValue.isFontKey(value)) { - return new FontValue(key, value); + try { + FontValue parsedValue = FontValue.parse(key, value); + if (parsedValue == null) { + error(lineNumber, "Could not parse Font value: " + value); + } + return parsedValue; } - Font font = Font.decode(value); - if (font == null) { - error(lineNumber, "Could not parse Color: " + value); + catch (Exception e) { + error(lineNumber, "Could not parse Font value: " + value + "because " + e.getMessage()); } - return font == null ? null : new FontValue(key, new FontUIResource(font)); + return null; } private ColorValue parseColorProperty(String key, String value, int lineNumber) { - if (ColorValue.isColorKey(value)) { - return new ColorValue(key, value); + ColorValue parsedValue = ColorValue.parse(key, value); + if (parsedValue == null) { + error(lineNumber, "Could not parse Color value: " + value); } - Color color = WebColors.getColor(value); - if (color == null) { - error(lineNumber, "Could not parse Color: " + value); - } - return color == null ? null : new ColorValue(key, color); + return parsedValue; } private List
readSections(LineNumberReader reader) throws IOException { diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ColorValue.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ColorValue.java index 4068bdec98..764b740305 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/ColorValue.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ColorValue.java @@ -18,6 +18,7 @@ package generic.theme; import java.awt.Color; import ghidra.util.Msg; +import ghidra.util.WebColors; import utilities.util.reflection.ReflectionUtilities; /** @@ -27,8 +28,9 @@ import utilities.util.reflection.ReflectionUtilities; * and if the class's refId is non-null, then the color value will be null. */ public class ColorValue extends ThemeValue { - static final String COLOR_ID_PREFIX = "color."; - static final String EXTERNAL_PREFIX = "[color]"; + private static final String COLOR_ID_PREFIX = "color."; + private static final String EXTERNAL_PREFIX = "[color]"; + private static final String SYSTEM_COLOR_PREFIX = "system.color"; public static final Color LAST_RESORT_DEFAULT = Color.GRAY; @@ -53,13 +55,45 @@ public class ColorValue extends ThemeValue { super(id, refId, null); } + @Override + public String getSerializationString() { + String outputId = toExternalId(id); + return outputId + " = " + getSerializedValue(); + } + + /** + * 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) || + key.startsWith(SYSTEM_COLOR_PREFIX); + } + + /** + * Parses the value string into a color or reference and creates a new ColorValue using + * the given key and the parse results. + * @param key the key to associate the parsed value with + * @param value the color value to parse + * @return a ColorValue with the given key and the parsed value + */ + public static ColorValue parse(String key, String value) { + String id = fromExternalId(key); + if (isColorKey(value)) { + return new ColorValue(id, fromExternalId(value)); + } + Color color = WebColors.getColor(value); + return color == null ? null : new ColorValue(id, color); + } + @Override protected ColorValue getReferredValue(GThemeValueMap values, String refId) { return values.getColor(refId); } @Override - protected Color getUnresolvedReferenceValue(String id) { + protected Color getUnresolvedReferenceValue(String unresolvedId) { Throwable t = ReflectionUtilities.createThrowableWithStackOlderThan(); StackTraceElement[] trace = t.getStackTrace(); @@ -69,35 +103,26 @@ public class ColorValue extends ThemeValue { t.setStackTrace(filtered); Msg.error(this, - "Could not resolve indirect color for \"" + id + "\", using last resort default!", t); + "Could not resolve indirect color for \"" + unresolvedId + + "\", using last resort default!", + t); return LAST_RESORT_DEFAULT; } - @Override - public String toExternalId(String internalId) { - if (internalId.startsWith(COLOR_ID_PREFIX)) { + private static String toExternalId(String internalId) { + if (internalId.startsWith(COLOR_ID_PREFIX) || internalId.startsWith(SYSTEM_COLOR_PREFIX)) { return internalId; } return EXTERNAL_PREFIX + internalId; } - @Override - public String fromExternalId(String externalId) { + private static String fromExternalId(String externalId) { if (externalId.startsWith(EXTERNAL_PREFIX)) { return externalId.substring(EXTERNAL_PREFIX.length()); } 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); - } - private static Color getRawColor(Color value) { if (value instanceof GColor) { return null; @@ -112,4 +137,16 @@ public class ColorValue extends ThemeValue { return null; } + private String getSerializedValue() { + if (referenceId != null) { + return toExternalId(referenceId); + } + String outputString = WebColors.toString(value, false); + String colorName = WebColors.toWebColorName(value); + if (colorName != null) { + outputString += " // " + colorName; + } + return outputString; + } + } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/FontModifier.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/FontModifier.java new file mode 100644 index 0000000000..48d7824b16 --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/FontModifier.java @@ -0,0 +1,166 @@ +/* ### + * 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.Font; +import java.util.*; +import java.util.regex.Pattern; + +public class FontModifier { + + private static final Pattern MODIFIER_PATTERN = Pattern.compile("(\\[([a-zA-Z]+|[0-9]+)\\])*"); + private String family; + private Integer style; + private Integer size; + + public FontModifier() { + + } + + public FontModifier(String family, Integer style, Integer size) { + this.family = family; + this.style = style; + this.size = size; + } + + public void addFamilyModifier(String newFamily) { + if (family != null) { + throw new IllegalStateException("Multiple font family names specified"); + } + this.family = newFamily; + } + + public void addSizeModfier(int newSize) { + if (size != null) { + throw new IllegalStateException("Multiple font sizes specified"); + } + this.size = newSize; + } + + public void addStyleModifier(int newStyle) { + if (style == null) { + style = newStyle; + return; + } + if (style == Font.PLAIN || newStyle == Font.PLAIN) { + throw new IllegalStateException("Attempted to set incompable styles"); + } + style = style | newStyle; + } + + public Font modify(Font font) { + if (family == null) { + if (style != null && size != null) { + return font.deriveFont(style, size); + } + else if (style != null) { + return font.deriveFont(style); + } + return font.deriveFont((float) size); + } + int newStyle = style != null ? style : font.getStyle(); + int newSize = size != null ? size : font.getSize(); + return new Font(family, newStyle, newSize); + } + + public static FontModifier parse(String value) { + List modifierValues = getModifierPieces(value); + if (modifierValues.isEmpty()) { + return null; + } + FontModifier modifier = new FontModifier(); + for (String modifierString : modifierValues) { + if (setSize(modifier, modifierString)) { + continue; + } + if (setStyle(modifier, modifierString)) { + continue; + } + modifier.addFamilyModifier(modifierString); + } + if (modifier.hadModifications()) { + return modifier; + } + return null; + } + + public String getSerializationString() { + StringBuilder builder = new StringBuilder(); + if (family != null) { + builder.append("[" + family + "]"); + } + if (size != null) { + builder.append("[" + size + "]"); + } + if (style != null) { + switch (style.intValue()) { + case Font.PLAIN: + builder.append("[plain]"); + break; + case Font.BOLD: + builder.append("[bold]"); + break; + case Font.ITALIC: + builder.append("[italic]"); + break; + case Font.BOLD | Font.ITALIC: + builder.append("[bold][italic]"); + break; + } + } + + return builder.toString(); + } + + private boolean hadModifications() { + return family != null || size != null || style != null; + } + + private static boolean setStyle(FontModifier modifier, String modifierString) { + int style = FontValue.getStyle(modifierString); + if (style >= 0) { + modifier.addStyleModifier(style); + return true; + } + return false; + } + + private static boolean setSize(FontModifier modifier, String modifierString) { + try { + int size = Integer.parseInt(modifierString); + modifier.addSizeModfier(size); + return true; + } + catch (NumberFormatException e) { + return false; + } + } + + private static List getModifierPieces(String value) { + if (!MODIFIER_PATTERN.matcher(value).matches()) { + throw new IllegalArgumentException("Invalid font modifier string"); + } + StringTokenizer tokenizer = new StringTokenizer(value, "[]"); + List list = new ArrayList<>(); + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken().trim(); + if (!token.isBlank()) { + list.add(token); + } + } + return list; + } +} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/FontValue.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/FontValue.java index 2b98be01a3..51ce630368 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/FontValue.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/FontValue.java @@ -29,6 +29,7 @@ public class FontValue extends ThemeValue { 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]"; + private FontModifier modifier; /** * Constructor used when the FontValue will have a direct {@link Font} value. The refId @@ -50,31 +51,44 @@ public class FontValue extends ThemeValue { super(id, refId, null); } - @Override - protected FontValue getReferredValue(GThemeValueMap values, String refId) { - return values.getFont(refId); + private FontValue(String id, String refId, FontModifier modifier) { + super(id, refId, null); + this.modifier = modifier; } @Override - protected Font getUnresolvedReferenceValue(String id) { - Msg.warn(this, "Could not resolve indirect font for" + id + ", using last resort default"); - return LAST_RESORT_DEFAULT; - } - - @Override - public String toExternalId(String internalId) { - if (internalId.startsWith(FONT_ID_PREFIX)) { - return internalId; + public Font get(GThemeValueMap values) { + Font font = super.get(values); + if (modifier != null) { + return modifier.modify(font); } - return EXTERNAL_PREFIX + internalId; + return font; } @Override - public String fromExternalId(String externalId) { - if (externalId.startsWith(EXTERNAL_PREFIX)) { - return externalId.substring(EXTERNAL_PREFIX.length()); + public String getSerializationString() { + String outputId = toExternalId(id); + return outputId + " = " + getValueOutput(); + } + + private String getValueOutput() { + if (referenceId != null) { + String refId = toExternalId(referenceId); + if (modifier != null) { + return "(" + refId + modifier.getSerializationString() + ")"; + } + return refId; } - return externalId; + return fontToString(value); + } + + /** + * 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()); } /** @@ -86,4 +100,127 @@ public class FontValue extends ThemeValue { return key.startsWith(FONT_ID_PREFIX) || key.startsWith(EXTERNAL_PREFIX); } + /** + * Parses the value string into a font or reference and creates a new FontValue using + * the given key and the parse results. + * @param key the key to associate the parsed value with + * @param value the font value to parse + * @return a FontValue with the given key and the parsed value + */ + public static FontValue parse(String key, String value) { + String id = fromExternalId(key); + + value = clean(value); + + if (isFontKey(value)) { + return getRefFontValue(id, value); + } + Font font = parseFont(value); + return font == null ? null : new FontValue(id, font); + } + + public static int getStyle(String styleString) { + if ("plain".equalsIgnoreCase(styleString)) { + return Font.PLAIN; + } + if ("bold".equalsIgnoreCase(styleString)) { + return Font.BOLD; + } + if ("italic".equalsIgnoreCase(styleString)) { + return Font.ITALIC; + } + if ("bolditalic".equalsIgnoreCase(styleString)) { + return Font.BOLD | Font.ITALIC; + } + return -1; + } + + @Override + protected FontValue getReferredValue(GThemeValueMap values, String refId) { + return values.getFont(refId); + } + + @Override + protected Font getUnresolvedReferenceValue(String unresolvedId) { + Msg.warn(this, "Could not resolve indirect font for" + unresolvedId + + ", using last resort default"); + return LAST_RESORT_DEFAULT; + } + + private static String toExternalId(String internalId) { + if (internalId.startsWith(FONT_ID_PREFIX)) { + return internalId; + } + return EXTERNAL_PREFIX + internalId; + } + + private static String fromExternalId(String externalId) { + if (externalId.startsWith(EXTERNAL_PREFIX)) { + return externalId.substring(EXTERNAL_PREFIX.length()); + } + return externalId; + } + + private static Font parseFont(String value) { + int sizeIndex = value.lastIndexOf("-"); + int styleIndex = value.lastIndexOf("-", sizeIndex - 1); + if (sizeIndex <= 0 || styleIndex <= 0) { + return null; + } + String sizeString = value.substring(sizeIndex + 1); + String styleString = value.substring(styleIndex + 1, sizeIndex); + String familyName = value.substring(0, styleIndex); + + try { + int size = Integer.parseInt(sizeString); + int style = getStyle(styleString); + if (style >= 0) { + return new Font(familyName, style, size); + } + } + catch (NumberFormatException e) { + // parse failed, return null + } + return null; + } + + private static FontValue getRefFontValue(String id, String value) { + if (value.startsWith(EXTERNAL_PREFIX)) { + value = value.substring(EXTERNAL_PREFIX.length()); + } + int modIndex = value.indexOf("["); + if (modIndex < 0) { + return new FontValue(id, fromExternalId(value)); + } + String refId = value.substring(0, modIndex).trim(); + FontModifier modifier = FontModifier.parse(value.substring(modIndex)); + return new FontValue(id, refId, modifier); + } + + private static String clean(String value) { + value = value.trim(); + if (value.startsWith("(")) { + value = value.substring(1); + } + if (value.endsWith(")")) { + value = value.substring(0, value.length() - 1); + } + return value; + } + + private static String getStyleString(Font font) { + boolean bold = font.isBold(); + boolean italic = font.isItalic(); + if (bold && italic) { + return "BOLDITALIC"; + } + if (bold) { + return "BOLD"; + } + if (italic) { + return "ITALIC"; + } + return "PLAIN"; + } + } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/GColor.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/GColor.java index fbea5b49e0..ea7d703caa 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/GColor.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/GColor.java @@ -63,7 +63,7 @@ public class GColor extends Color { public GColor(String id, boolean validate) { super(0x808080); this.id = id; - delegate = Gui.getRawColor(id, validate); + delegate = Gui.getColor(id, validate); inUseColors.add(this); } @@ -214,7 +214,7 @@ public class GColor extends Color { * Reloads the delegate. */ public void refresh() { - Color color = Gui.getRawColor(id, false); + Color color = Gui.getColor(id, false); if (color != null) { if (alpha != null) { delegate = new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha); diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/GIcon.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/GIcon.java index 6460683f46..e5d9e9ebb0 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/GIcon.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/GIcon.java @@ -71,7 +71,7 @@ public class GIcon implements Icon { */ public GIcon(String id, boolean validate) { this.id = id; - delegate = Gui.getRawIcon(id, validate); + delegate = Gui.getIcon(id, validate); inUseIcons.add(this); } @@ -122,7 +122,7 @@ public class GIcon implements Icon { * Reloads the delegate. */ public void refresh() { - Icon icon = Gui.getRawIcon(id, false); + Icon icon = Gui.getIcon(id, false); if (icon != null) { delegate = icon; } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/GThemeDefaults.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/GThemeDefaults.java index e61fe9071c..e6965076e1 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/GThemeDefaults.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/GThemeDefaults.java @@ -32,7 +32,7 @@ public class GThemeDefaults { public static final String COLOR_BG = "color.bg"; // TODO do we need this?; rename to use 'background'? public static class Java { - public static final String BORDER = "Component.borderColor"; // TODO + public static final String BORDER = "system.color.border"; // TODO } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/GThemeValueMap.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/GThemeValueMap.java index 8cbe0fa28e..6da376d243 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/GThemeValueMap.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/GThemeValueMap.java @@ -275,4 +275,25 @@ public class GThemeValueMap { return files; } + @Override + public int hashCode() { + return Objects.hash(colorMap, fontMap, iconMap); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + GThemeValueMap other = (GThemeValueMap) obj; + return Objects.equals(colorMap, other.colorMap) && Objects.equals(fontMap, other.fontMap) && + Objects.equals(iconMap, other.iconMap); + } + } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/Gui.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/Gui.java index 2025e07238..8ee0d48dee 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/Gui.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/Gui.java @@ -16,20 +16,19 @@ package generic.theme; import java.awt.*; -import java.io.*; +import java.io.File; import java.util.*; import java.util.List; import javax.swing.*; import javax.swing.plaf.ComponentUI; -import javax.swing.plaf.basic.BasicLookAndFeel; import com.formdev.flatlaf.*; import generic.theme.builtin.*; import generic.theme.laf.LookAndFeelManager; -import ghidra.framework.*; -import ghidra.framework.preferences.Preferences; +import ghidra.framework.OperatingSystem; +import ghidra.framework.Platform; import ghidra.util.Msg; import ghidra.util.classfinder.ClassSearcher; import ghidra.util.datastruct.WeakDataStructureFactory; @@ -53,20 +52,18 @@ import utilities.util.reflection.ReflectionUtilities; * */ public class Gui { - public static final String THEME_DIR = "themes"; public static final String BACKGROUND_KEY = "color.bg.text"; - private static final String THEME_PREFFERENCE_KEY = "Theme"; - private static GTheme activeTheme = getDefaultTheme(); private static Set allThemes = null; - private static GThemeValueMap ghidraLightDefaults = new GThemeValueMap(); - private static GThemeValueMap ghidraDarkDefaults = new GThemeValueMap(); + private static GThemeValueMap applicationDefaults = new GThemeValueMap(); + private static GThemeValueMap applicationDarkDefaults = new GThemeValueMap(); private static GThemeValueMap javaDefaults = new GThemeValueMap(); private static GThemeValueMap currentValues = new GThemeValueMap(); - private static ThemePropertiesLoader themePropertiesLoader = new ThemePropertiesLoader(); + private static ThemeFileLoader themeFileLoader = new ThemeFileLoader(); + private static ThemePreferenceManager themePreferenceManager = new ThemePreferenceManager(); private static Map gColorMap = new HashMap<>(); private static boolean isInitialized; @@ -80,6 +77,7 @@ public class Gui { // stores the original value for ids whose value has changed from the current theme private static GThemeValueMap changedValuesMap = new GThemeValueMap(); private static LookAndFeelManager lookAndFeelManager; + static Font DEFAULT_FONT = new Font("Dialog", Font.PLAIN, 12); private Gui() { // static utils class, can't construct @@ -92,14 +90,14 @@ public class Gui { isInitialized = true; installFlatLookAndFeels(); loadThemeDefaults(); - setTheme(getThemeFromPreferences()); + setTheme(themePreferenceManager.getTheme()); // LookAndFeelUtils.installGlobalOverrides(); } /** * Reloads the defaults from all the discoverable theme.property files. */ - public static void reloadGhidraDefaults() { + public static void reloadApplicationDefaults() { loadThemeDefaults(); buildCurrentValues(); lookAndFeelManager.resetAll(javaDefaults); @@ -128,10 +126,11 @@ public class Gui { try { lookAndFeelManager.installLookAndFeel(); notifyThemeChanged(new AllValuesChangedThemeEvent(true)); - saveThemeToPreferences(theme); + themePreferenceManager.saveThemeToPreferences(theme); } catch (Exception e) { - Msg.error(Gui.class, "Error setting LookAndFeel: " + lookAndFeel.getName(), e); + Msg.error(Gui.class, + "Error setting LookAndFeel: " + lookAndFeel.getName(), e); } } } @@ -230,38 +229,53 @@ public class Gui { } /** - * Saves the current theme choice to {@link Preferences}. - * @param theme the theme to remember in {@link Preferences} + * Returns the current {@link Font} associated with the given id. A default font will be + * returned if the font can't be resolved and an error message will be printed to the console. + * @param id the id for the desired font + * @return the current {@link Font} associated with the given id. */ - public static void saveThemeToPreferences(GTheme theme) { - Preferences.setProperty(THEME_PREFFERENCE_KEY, theme.getThemeLocater()); - Preferences.store(); + public static Font getFont(String id) { + return getFont(id, true); } /** * Returns the current {@link Font} associated with the given id. * @param id the id for the desired font + * @param validate if true, will print an error message to the console if the id can't be + * resolved * @return the current {@link Font} associated with the given id. */ - public static Font getFont(String id) { + public static Font getFont(String id, boolean validate) { FontValue font = currentValues.getFont(id); - if (font == null) { - Throwable t = getFilteredTrace(); - Msg.error(Gui.class, "No font value registered for: " + id, t); - return null; + if (font == null) { + if (validate && isInitialized) { + Throwable t = getFilteredTrace(); + Msg.error(Gui.class, + "No color value registered for: '" + id + "'", t); + } + return DEFAULT_FONT; } return font.get(currentValues); } /** - * Returns the actual direct color for the id, not a GColor. Will output an error message if + * Returns the {@link Color} registered for the given id. Will output an error message if * the id can't be resolved. * @param id the id to get the direct color for - * @return the actual direct color for the id, not a GColor + * @return the {@link Color} registered for the given id. */ - public static Color getRawColor(String id) { - return getRawColor(id, true); + public static Color getColor(String id) { + return getColor(id, true); + } + + /** + * Updates the current font for the given id. + * @param id the font id to update to the new color + * @param font the new font for the id + */ + public static void setFont(String id, Font font) { + setFont(new FontValue(id, font)); } /** @@ -281,8 +295,7 @@ public class Gui { // update all java LookAndFeel fonts affected by this changed String id = newValue.getId(); Set changedFontIds = findChangedJavaFontIds(id); - Font newFont = newValue.get(currentValues); - lookAndFeelManager.fontsChanged(changedFontIds, newFont); + lookAndFeelManager.fontsChanged(changedFontIds); } /** @@ -380,24 +393,12 @@ public class Gui { * @param map the default theme values defined by the {@link LookAndFeel} */ public static void setJavaDefaults(GThemeValueMap map) { - javaDefaults = fixupJavaDefaultsInheritence(map); + javaDefaults = map; buildCurrentValues(); GColor.refreshAll(); GIcon.refreshAll(); } - /** - * Attempts to restore the relationships between various theme values that derive from - * other theme values as defined in {@link BasicLookAndFeel} - * @param map the map of value ids to its inherited id - * @return a fixed up version of the given map with relationships restored where possible - */ - public static GThemeValueMap fixupJavaDefaultsInheritence(GThemeValueMap map) { - JavaColorMapping.fixupJavaDefaultsInheritence(map); - JavaFontMapping.fixupJavaDefaultsInheritence(map); - return map; - } - /** * Returns the {@link GThemeValueMap} containing all the default theme values defined by the * current {@link LookAndFeel}. @@ -417,9 +418,9 @@ public class Gui { * @return the {@link GThemeValueMap} containing all the dark values defined in * theme.properties files */ - public static GThemeValueMap getGhidraDarkDefaults() { - GThemeValueMap map = new GThemeValueMap(ghidraLightDefaults); - map.load(ghidraDarkDefaults); + public static GThemeValueMap getApplicationDarkDefaults() { + GThemeValueMap map = new GThemeValueMap(applicationDefaults); + map.load(applicationDarkDefaults); return map; } @@ -429,8 +430,8 @@ public class Gui { * @return the {@link GThemeValueMap} containing all the standard values defined in * theme.properties files */ - public static GThemeValueMap getGhidraLightDefaults() { - GThemeValueMap map = new GThemeValueMap(ghidraLightDefaults); + public static GThemeValueMap getApplicationLightDefaults() { + GThemeValueMap map = new GThemeValueMap(applicationDefaults); return map; } @@ -441,9 +442,9 @@ public class Gui { */ public static GThemeValueMap getDefaults() { GThemeValueMap currentDefaults = new GThemeValueMap(javaDefaults); - currentDefaults.load(ghidraLightDefaults); + currentDefaults.load(applicationDefaults); if (activeTheme.useDarkDefaults()) { - currentDefaults.load(ghidraDarkDefaults); + currentDefaults.load(applicationDarkDefaults); } return currentDefaults; } @@ -509,18 +510,20 @@ public class Gui { } /** - * Returns the actual direct color for the id, not a GColor. + * Returns the color for the id. If there is no color registered for this id, then Color.CYAN + * is returned as the default color. * @param id the id to get the direct color for * @param validate if true, will output an error if the id can't be resolved at this time * @return the actual direct color for the id, not a GColor */ - public static Color getRawColor(String id, boolean validate) { + public static Color getColor(String id, boolean validate) { ColorValue color = currentValues.getColor(id); if (color == null) { if (validate && isInitialized) { Throwable t = getFilteredTrace(); - Msg.error(Gui.class, "No color value registered for: '" + id + "'", t); + Msg.error(Gui.class, + "No color value registered for: '" + id + "'", t); } return Color.CYAN; } @@ -528,17 +531,29 @@ public class Gui { } /** - * Returns the actual direct icon for the id, not a GIcon. - * @param id the id to get the direct icon for - * @param validate if true, will output an error if the id can't be resolved at this time - * @return the actual direct icon for the id, not a GIcon + * Returns the Icon registered for the given id. If no icon is registered for the id, + * the default icon will be returned and an error message will be dumped to the console + * @param id the id to get the registered icon for + * @return the actual icon registered for the given id */ - public static Icon getRawIcon(String id, boolean validate) { + public static Icon getIcon(String id) { + return getIcon(id, true); + } + + /** + * Returns the {@link Icon} registered for the given id. If no icon is registered, returns + * the default icon (bomb). + * @param id the id to get the register icon for + * @param validate if true, will output an error if the id can't be resolved at this time + * @return the Icon registered for the given id + */ + public static Icon getIcon(String id, boolean validate) { IconValue icon = currentValues.getIcon(id); if (icon == null) { if (validate && isInitialized) { Throwable t = getFilteredTrace(); - Msg.error(Gui.class, "No icon value registered for: '" + id + "'", t); + Msg.error(Gui.class, + "No icon value registered for: '" + id + "'", t); } return ResourceManager.getDefaultIcon(); } @@ -569,11 +584,6 @@ public class Gui { return color.brighter(); } - // for testing - public static void setPropertiesLoader(ThemePropertiesLoader loader) { - themePropertiesLoader = loader; - } - /** * Binds the component to the font identified by the given font id. Whenever the font for * the font id changes, the component will updated with the new font. @@ -592,9 +602,9 @@ public class Gui { } private static void loadThemeDefaults() { - themePropertiesLoader.load(); - ghidraLightDefaults = themePropertiesLoader.getDefaults(); - ghidraDarkDefaults = themePropertiesLoader.getDarkDefaults(); + themeFileLoader.loadThemeDefaultFiles(); + applicationDefaults = themeFileLoader.getDefaults(); + applicationDarkDefaults = themeFileLoader.getDarkDefaults(); } private static void notifyThemeChanged(ThemeEvent event) { @@ -616,9 +626,9 @@ public class Gui { GThemeValueMap map = new GThemeValueMap(); map.load(javaDefaults); - map.load(ghidraLightDefaults); + map.load(applicationDefaults); if (activeTheme.useDarkDefaults()) { - map.load(ghidraDarkDefaults); + map.load(applicationDarkDefaults); } map.load(activeTheme); currentValues = map; @@ -629,72 +639,15 @@ public class Gui { if (allThemes == null) { Set set = new HashSet<>(); set.addAll(findDiscoverableThemes()); - set.addAll(loadThemesFromFiles()); + set.addAll(themeFileLoader.loadThemeFiles()); allThemes = set; } } - private static Collection loadThemesFromFiles() { - List fileList = new ArrayList<>(); - FileFilter themeFileFilter = file -> file.getName().endsWith("." + GTheme.FILE_EXTENSION); - - File dir = Application.getUserSettingsDirectory(); - File themeDir = new File(dir, THEME_DIR); - File[] files = themeDir.listFiles(themeFileFilter); - if (files != null) { - fileList.addAll(Arrays.asList(files)); - } - - List list = new ArrayList<>(); - for (File file : fileList) { - GTheme theme = loadTheme(file); - if (theme != null) { - list.add(theme); - } - } - return list; - } - - private static GTheme loadTheme(File file) { - try { - return new ThemeReader(file).readTheme(); - } - catch (IOException e) { - Msg.error(Gui.class, "Could not load theme from file: " + file.getAbsolutePath(), e); - } - return null; - } - private static Collection findDiscoverableThemes() { return ClassSearcher.getInstances(DiscoverableGTheme.class); } - private static GTheme getThemeFromPreferences() { - String themeId = Preferences.getProperty(THEME_PREFFERENCE_KEY, "Default", true); - if (themeId.startsWith(GTheme.FILE_PREFIX)) { - String filename = themeId.substring(GTheme.FILE_PREFIX.length()); - try { - return new ThemeReader(new File(filename)).readTheme(); - } - catch (IOException e) { - Msg.showError(GTheme.class, null, "Can't Load Previous Theme", - "Error loading theme file: " + filename, e); - } - } - else if (themeId.startsWith(DiscoverableGTheme.CLASS_PREFIX)) { - String className = themeId.substring(DiscoverableGTheme.CLASS_PREFIX.length()); - try { - Class forName = Class.forName(className); - return (GTheme) forName.getDeclaredConstructor().newInstance(); - } - catch (Exception e) { - Msg.showError(GTheme.class, null, "Can't Load Previous Theme", - "Can't find or instantiate class: " + className); - } - } - return getDefaultTheme(); - } - private static void updateChangedValuesMap(ColorValue currentValue, ColorValue newValue) { String id = newValue.getId(); ColorValue originalValue = changedValuesMap.getColor(id); @@ -762,4 +715,14 @@ public class Gui { return affectedIds; } + // for testing + public static void setPropertiesLoader(ThemeFileLoader loader) { + allThemes = null; + themeFileLoader = loader; + } + + public static void setThemePreferenceManager(ThemePreferenceManager manager) { + themePreferenceManager = manager; + } + } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/IconValue.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/IconValue.java index 4495c1da25..ffdf7220a6 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/IconValue.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/IconValue.java @@ -19,6 +19,7 @@ import javax.swing.Icon; import ghidra.util.Msg; import resources.ResourceManager; +import resources.icons.UrlImageIcon; /** * A class for storing {@link Icon} values that have a String id (e.g. icon.bg.foo) and either @@ -54,43 +55,76 @@ public class IconValue extends ThemeValue { super(id, refId, null); } + @Override + public String getSerializationString() { + String outputId = toExternalId(id); + return outputId + " = " + getValueOutput(); + } + + /** + * 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); + } + + /** + * 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; + } + + /** + * Parses the value string into an icon or reference and creates a new IconValue using + * the given key and the parse results. + * @param key the key to associate the parsed value with + * @param value the color value to parse + * @return an IconValue with the given key and the parsed value + */ + public static IconValue parse(String key, String value) { + String id = fromExternalId(key); + if (isIconKey(value)) { + return new IconValue(id, fromExternalId(value)); + } + Icon icon = ResourceManager.loadImage(value); + return new IconValue(id, icon); + } + @Override protected IconValue getReferredValue(GThemeValueMap values, String refId) { return values.getIcon(refId); } @Override - protected Icon getUnresolvedReferenceValue(String id) { + protected Icon getUnresolvedReferenceValue(String unresolvedId) { Msg.warn(this, - "Could not resolve indirect icon path for" + id + ", using last resort default"); + "Could not resolve indirect icon path for" + unresolvedId + + ", using last resort default"); return LAST_RESORT_DEFAULT; } - @Override - public String toExternalId(String internalId) { + private static String toExternalId(String internalId) { if (internalId.startsWith(ICON_ID_PREFIX)) { return internalId; } return EXTERNAL_PREFIX + internalId; } - @Override - public String fromExternalId(String externalId) { + private static String fromExternalId(String externalId) { if (externalId.startsWith(EXTERNAL_PREFIX)) { return externalId.substring(EXTERNAL_PREFIX.length()); } 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); - } - private static Icon getRawIcon(Icon value) { if (value instanceof GIcon) { return null; @@ -104,4 +138,12 @@ public class IconValue extends ThemeValue { } return null; } + + private String getValueOutput() { + if (referenceId != null) { + return toExternalId(referenceId); + } + return iconToString(value); + } + } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/LafType.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/LafType.java index d04702fe1c..f921b418e6 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/LafType.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/LafType.java @@ -96,20 +96,23 @@ public enum LafType { private static LookAndFeelManager getManager(LafType lookAndFeel) { switch (lookAndFeel) { case MAC: + return new MacLookAndFeelManager(); case METAL: + return new MetalLookAndFeelManager(); case WINDOWS: + return new WindowsLookAndFeelManager(); case WINDOWS_CLASSIC: - return new GenericLookAndFeelManager(lookAndFeel); - case FLAT_DARCULA: - case FLAT_DARK: - case FLAT_LIGHT: - return new GenericFlatLookAndFeelManager(lookAndFeel); + return new WindowsClassicLookAndFeelManager(); case GTK: return new GtkLookAndFeelManager(); case MOTIF: return new MotifLookAndFeelManager(); case NIMBUS: return new NimbusLookAndFeelManager(); + case FLAT_DARCULA: + case FLAT_DARK: + case FLAT_LIGHT: + return new FlatLookAndFeelManager(lookAndFeel); default: throw new AssertException("No lookAndFeelManager defined for " + lookAndFeel); } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePropertiesLoader.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeFileLoader.java similarity index 64% rename from Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePropertiesLoader.java rename to Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeFileLoader.java index 1ade6faad3..4cf43b6bcf 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePropertiesLoader.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeFileLoader.java @@ -15,8 +15,8 @@ */ package generic.theme; -import java.io.IOException; -import java.util.List; +import java.io.*; +import java.util.*; import generic.jar.ResourceFile; import ghidra.framework.Application; @@ -26,23 +26,23 @@ 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(); +public class ThemeFileLoader { + public static final String THEME_DIR = "themes"; - public ThemePropertiesLoader() { - } + private GThemeValueMap defaults = new GThemeValueMap(); + private GThemeValueMap darkDefaults = new GThemeValueMap(); /** * 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 themeDefaultFiles = - Application.findFilesByExtensionInApplication(".theme.properties"); + public void loadThemeDefaultFiles() { defaults.clear(); darkDefaults.clear(); + List themeDefaultFiles = + Application.findFilesByExtensionInApplication(".theme.properties"); + for (ResourceFile resourceFile : themeDefaultFiles) { Msg.debug(this, "found theme file: " + resourceFile.getAbsolutePath()); try { @@ -57,6 +57,28 @@ public class ThemePropertiesLoader { } } + public Collection loadThemeFiles() { + List fileList = new ArrayList<>(); + FileFilter themeFileFilter = file -> file.getName().endsWith("." + GTheme.FILE_EXTENSION); + + File dir = Application.getUserSettingsDirectory(); + File themeDir = new File(dir, THEME_DIR); + File[] files = themeDir.listFiles(themeFileFilter); + if (files != null) { + fileList.addAll(Arrays.asList(files)); + } + + List list = new ArrayList<>(); + for (File file : fileList) { + GTheme theme = loadTheme(file); + if (theme != null) { + list.add(theme); + } + } + return list; + + } + /** * Returns the standard defaults {@link GThemeValueMap} * @return the standard defaults {@link GThemeValueMap} @@ -73,4 +95,13 @@ public class ThemePropertiesLoader { return darkDefaults; } + private static GTheme loadTheme(File file) { + try { + return new ThemeReader(file).readTheme(); + } + catch (IOException e) { + Msg.error(Gui.class, "Could not load theme from file: " + file.getAbsolutePath(), e); + } + return null; + } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePreferenceManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePreferenceManager.java new file mode 100644 index 0000000000..5e8cdfad2a --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePreferenceManager.java @@ -0,0 +1,69 @@ +/* ### + * 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.File; +import java.io.IOException; + +import ghidra.framework.preferences.Preferences; +import ghidra.util.Msg; + +/** + * Reads and writes current theme info to preferences + */ +public class ThemePreferenceManager { + private static final String THEME_PREFFERENCE_KEY = "Theme"; + + /** + * Returns the theme that was stored in preferences or the default theme if none stored. + * @return the last theme used (stored in preferences) or the default theme if not stored + * in preferences + */ + public GTheme getTheme() { + String themeId = Preferences.getProperty(THEME_PREFFERENCE_KEY, "Default", true); + if (themeId.startsWith(GTheme.FILE_PREFIX)) { + String filename = themeId.substring(GTheme.FILE_PREFIX.length()); + try { + return new ThemeReader(new File(filename)).readTheme(); + } + catch (IOException e) { + Msg.showError(GTheme.class, null, "Can't Load Previous Theme", + "Error loading theme file: " + filename, e); + } + } + else if (themeId.startsWith(DiscoverableGTheme.CLASS_PREFIX)) { + String className = themeId.substring(DiscoverableGTheme.CLASS_PREFIX.length()); + try { + Class forName = Class.forName(className); + return (GTheme) forName.getDeclaredConstructor().newInstance(); + } + catch (Exception e) { + Msg.showError(GTheme.class, null, "Can't Load Previous Theme", + "Can't find or instantiate class: " + className); + } + } + return Gui.getDefaultTheme(); + } + + /** + * Saves the current theme choice to {@link Preferences}. + * @param theme the theme to remember in {@link Preferences} + */ + public void saveThemeToPreferences(GTheme theme) { + Preferences.setProperty(THEME_PREFFERENCE_KEY, theme.getThemeLocater()); + Preferences.store(); + } +} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeValue.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeValue.java index 70f22aa995..c1fa9c9ed8 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeValue.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeValue.java @@ -28,13 +28,13 @@ import ghidra.util.Msg; * @param the base type this ThemeValue works on (i.e., Colors, Fonts, Icons) */ public abstract class ThemeValue implements Comparable> { - private final String id; - private final T value; - private final String refId; + protected final String id; + protected final T value; + protected final String referenceId; - protected ThemeValue(String id, String refId, T value) { - this.id = fromExternalId(id); - this.refId = (refId == null) ? null : fromExternalId(refId); + protected ThemeValue(String id, String referenceId, T value) { + this.id = id; + this.referenceId = referenceId; this.value = value; } @@ -54,7 +54,7 @@ public abstract class ThemeValue implements Comparable> { * a value */ public String getReferenceId() { - return refId; + return referenceId; } /** @@ -82,7 +82,7 @@ public abstract class ThemeValue implements Comparable> { Set visitedKeys = new HashSet<>(); visitedKeys.add(id); - ThemeValue parent = getReferredValue(values, refId); + ThemeValue parent = getReferredValue(values, referenceId); // loop resolving indirect references while (parent != null) { @@ -90,11 +90,11 @@ public abstract class ThemeValue implements Comparable> { return parent.value; } visitedKeys.add(parent.id); - if (visitedKeys.contains(parent.refId)) { + if (visitedKeys.contains(parent.referenceId)) { Msg.warn(this, "Theme value reference loop detected for key: " + id); return getUnresolvedReferenceValue(id); } - parent = getReferredValue(values, parent.refId); + parent = getReferredValue(values, parent.referenceId); } return getUnresolvedReferenceValue(id); } @@ -107,66 +107,64 @@ public abstract class ThemeValue implements Comparable> { * @return true if this ThemeValue derives its value from the given ancestorId. */ public boolean inheritsFrom(String ancestorId, GThemeValueMap values) { - if (refId == null) { + if (referenceId == null) { return false; } - if (refId.equals(ancestorId)) { + if (referenceId.equals(ancestorId)) { return true; } Set visitedKeys = new HashSet<>(); visitedKeys.add(id); - ThemeValue parent = getReferredValue(values, refId); + ThemeValue parent = getReferredValue(values, referenceId); // loop resolving indirect references while (parent != null) { - if (parent.refId == null) { + if (parent.referenceId == null) { return false; } - if (parent.refId.equals(ancestorId)) { + if (parent.referenceId.equals(ancestorId)) { return true; } visitedKeys.add(parent.id); - if (visitedKeys.contains(parent.refId)) { + if (visitedKeys.contains(parent.referenceId)) { return false; } - parent = getReferredValue(values, parent.refId); + parent = getReferredValue(values, parent.referenceId); } return false; } + /** + * Returns true if this ColorValue gets its value from some other ColorValue + * @return true if this ColorValue gets its value from some other ColorValue + */ + public boolean isIndirect() { + return referenceId != null; + } + + /** + * Returns the "key = value" String for writing this ThemeValue to a file + * @return the "key = value" String for writing this ThemeValue to a file + */ + public abstract String getSerializationString(); + /** * 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); + protected abstract T getUnresolvedReferenceValue(String unresolvedId); /** * 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 + * @param refId the id of the reference ThemeValue * @return the ThemeValue referred to by this ThemeValue. */ - abstract protected ThemeValue getReferredValue(GThemeValueMap preferredValues, - String referenceId); + protected abstract ThemeValue getReferredValue(GThemeValueMap preferredValues, + String refId); @Override public int compareTo(ThemeValue o) { @@ -175,7 +173,7 @@ public abstract class ThemeValue implements Comparable> { @Override public int hashCode() { - return Objects.hash(id, refId, value); + return Objects.hash(id, referenceId, value); } @Override @@ -191,17 +189,17 @@ public abstract class ThemeValue implements Comparable> { } ThemeValue other = (ThemeValue) obj; - return Objects.equals(id, other.id) && Objects.equals(refId, other.refId) && + return Objects.equals(id, other.id) && Objects.equals(referenceId, other.referenceId) && Objects.equals(value, other.value); } @Override public String toString() { String name = getClass().getSimpleName(); - if (refId == null) { + if (referenceId == null) { return name + " (" + id + ", " + value + ")"; } - return name + " (" + id + ", " + refId + ")"; + return name + " (" + id + ", " + referenceId + ")"; } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeWriter.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeWriter.java index 16016ba1a7..1d778e867c 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeWriter.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeWriter.java @@ -15,20 +15,13 @@ */ package generic.theme; -import java.awt.Color; -import java.awt.Font; import java.io.*; 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; - /** * 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. @@ -113,87 +106,20 @@ public class ThemeWriter { writer.newLine(); for (ColorValue colorValue : colors) { - String outputId = colorValue.toExternalId(colorValue.getId()); - writer.write(outputId + " = " + getValueOutput(colorValue)); + writer.write(colorValue.getSerializationString()); writer.newLine(); } for (FontValue fontValue : fonts) { - String outputId = fontValue.toExternalId(fontValue.getId()); - writer.write(outputId + " = " + getValueOutput(fontValue)); + writer.write(fontValue.getSerializationString()); writer.newLine(); } for (IconValue iconValue : icons) { - String outputId = iconValue.toExternalId(iconValue.getId()); - writer.write(outputId + " = " + getValueOutput(iconValue)); + writer.write(iconValue.getSerializationString()); writer.newLine(); - } - } - private String getValueOutput(ColorValue colorValue) { - if (colorValue.getReferenceId() != null) { - return colorValue.toExternalId(colorValue.getReferenceId()); } - Color color = colorValue.getRawValue(); - String outputString = WebColors.toString(color, false); - String colorName = WebColors.toWebColorName(color); - if (colorName != null) { - outputString += " // " + colorName; - } - return outputString; - } - - private String getValueOutput(IconValue iconValue) { - if (iconValue.getReferenceId() != null) { - return iconValue.toExternalId(iconValue.getReferenceId()); - } - Icon icon = iconValue.getRawValue(); - return iconToString(icon); - } - - private String getValueOutput(FontValue fontValue) { - if (fontValue.getReferenceId() != null) { - return fontValue.toExternalId(fontValue.getReferenceId()); - } - Font font = fontValue.getRawValue(); - return fontToString(font); - } - - private static String getStyleString(Font font) { - boolean bold = font.isBold(); - boolean italic = font.isItalic(); - if (bold && italic) { - return "BOLDITALIC"; - } - if (bold) { - return "BOLD"; - } - if (italic) { - return "ITALIC"; - } - 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 { diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/JavaColorMapping.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/JavaColorMapping.java deleted file mode 100644 index 474e783d3d..0000000000 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/JavaColorMapping.java +++ /dev/null @@ -1,253 +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.builtin; - -import static java.util.Map.*; - -import java.awt.Color; -import java.util.List; -import java.util.Map; - -import generic.theme.ColorValue; -import generic.theme.GThemeValueMap; - -/** - * Maps Java UIDefaults color ids relationships to an id it inherits from - */ -public class JavaColorMapping { - private static Map map = Map.ofEntries( - entry("Button.background", "control"), - entry("Button.foreground", "controlText"), - entry("Button.shadow", "controlShadow"), - entry("Button.darkShadow", "controlDkShadow"), - entry("Button.light", "controlHighlight"), - entry("Button.highlight", "controlLtHighlight"), - entry("ToggleButton.background", "control"), - entry("ToggleButton.foreground", "controlText"), - entry("ToggleButton.shadow", "controlShadow"), - entry("ToggleButton.darkShadow", "controlDkShadow"), - entry("ToggleButton.light", "controlHighlight"), - entry("ToggleButton.highlight", "controlLtHighlight"), - entry("RadioButton.background", "control"), - entry("RadioButton.foreground", "controlText"), - entry("RadioButton.shadow", "controlShadow"), - entry("RadioButton.darkShadow", "controlDkShadow"), - entry("RadioButton.light", "controlHighlight"), - entry("RadioButton.highlight", "controlLtHighlight"), - entry("CheckBox.background", "control"), - entry("CheckBox.foreground", "controlText"), - entry("ColorChooser.background", "control"), - entry("ColorChooser.foreground", "controlText"), - entry("ColorChooser.swatchesDefaultRecentColor", "control"), - entry("ComboBox.background", "window"), - entry("ComboBox.foreground", "textText"), - entry("ComboBox.buttonBackground", "control"), - entry("ComboBox.buttonShadow", "controlShadow"), - entry("ComboBox.buttonDarkShadow", "controlDkShadow"), - entry("ComboBox.buttonHighlight", "controlLtHighlight"), - entry("ComboBox.selectionBackground", "textHighlight"), - entry("ComboBox.selectionForeground", "textHighlightText"), - entry("ComboBox.disabledBackground", "control"), - entry("ComboBox.disabledForeground", "textHInactiveText"), - entry("InternalFrame.borderColor", "control"), - entry("InternalFrame.borderShadow", "controlShadow"), - entry("InternalFrame.borderDarkShadow", "controlDkShadow"), - entry("InternalFrame.borderHighlight", "controlLtHighlight"), - entry("InternalFrame.borderLight", "controlHighlight"), - entry("InternalFrame.activeTitleBackground", "activeCaption"), - entry("InternalFrame.activeTitleForeground", "activeCaptionText"), - entry("InternalFrame.inactiveTitleBackground", "inactiveCaption"), - entry("InternalFrame.inactiveTitleForeground", "inactiveCaptionText"), - entry("Label.background", "control"), - entry("Label.foreground", "controlText"), - entry("Label.disabledShadow", "controlShadow"), - entry("List.background", "window"), - entry("List.foreground", "textText"), - entry("List.selectionBackground", "textHighlight"), - entry("List.selectionForeground", "textHighlightText"), - entry("List.dropLineColor", "controlShadow"), - entry("MenuBar.background", "menu"), - entry("MenuBar.foreground", "menuText"), - entry("MenuBar.shadow", "controlShadow"), - entry("MenuBar.highlight", "controlLtHighlight"), - entry("MenuItem.background", "menu"), - entry("MenuItem.foreground", "menuText"), - entry("MenuItem.selectionForeground", "textHighlightText"), - entry("MenuItem.selectionBackground", "textHighlight"), - entry("MenuItem.acceleratorForeground", "menuText"), - entry("MenuItem.acceleratorSelectionForeground", "textHighlightText"), - entry("RadioButtonMenuItem.background", "menu"), - entry("RadioButtonMenuItem.foreground", "menuText"), - entry("RadioButtonMenuItem.selectionForeground", "textHighlightText"), - entry("RadioButtonMenuItem.selectionBackground", "textHighlight"), - entry("RadioButtonMenuItem.acceleratorForeground", "menuText"), - entry("RadioButtonMenuItem.acceleratorSelectionForeground", "textHighlightText"), - entry("CheckBoxMenuItem.background", "menu"), - entry("CheckBoxMenuItem.foreground", "menuText"), - entry("CheckBoxMenuItem.selectionForeground", "textHighlightText"), - entry("CheckBoxMenuItem.selectionBackground", "textHighlight"), - entry("CheckBoxMenuItem.acceleratorForeground", "menuText"), - entry("CheckBoxMenuItem.acceleratorSelectionForeground", "textHighlightText"), - entry("Menu.background", "menu"), - entry("Menu.foreground", "menuText"), - entry("Menu.selectionForeground", "textHighlightText"), - entry("Menu.selectionBackground", "textHighlight"), - entry("Menu.acceleratorForeground", "menuText"), - entry("Menu.acceleratorSelectionForeground", "textHighlightText"), - entry("PopupMenu.background", "menu"), - entry("PopupMenu.foreground", "menuText"), - entry("OptionPane.background", "control"), - entry("OptionPane.foreground", "controlText"), - entry("OptionPane.messageForeground", "controlText"), - entry("Panel.background", "control"), - entry("Panel.foreground", "textText"), - entry("ProgressBar.foreground", "textHighlight"), - entry("ProgressBar.background", "control"), - entry("ProgressBar.selectionForeground", "control"), - entry("ProgressBar.selectionBackground", "textHighlight"), - entry("Separator.background", "controlLtHighlight"), - entry("Separator.foreground", "controlShadow"), - entry("ScrollBar.foreground", "control"), - entry("ScrollBar.track", "scrollbar"), - entry("ScrollBar.trackHighlight", "controlDkShadow"), - entry("ScrollBar.thumb", "control"), - entry("ScrollBar.thumbHighlight", "controlLtHighlight"), - entry("ScrollBar.thumbDarkShadow", "controlDkShadow"), - entry("ScrollBar.thumbShadow", "controlShadow"), - entry("ScrollPane.background", "control"), - entry("ScrollPane.foreground", "controlText"), - entry("Viewport.background", "control"), - entry("Viewport.foreground", "textText"), - entry("Slider.foreground", "control"), - entry("Slider.background", "control"), - entry("Slider.highlight", "controlLtHighlight"), - entry("Slider.shadow", "controlShadow"), - entry("Slider.focus", "controlDkShadow"), - entry("Spinner.background", "control"), - entry("Spinner.foreground", "control"), - entry("SplitPane.background", "control"), - entry("SplitPane.highlight", "controlLtHighlight"), - entry("SplitPane.shadow", "controlShadow"), - entry("SplitPane.darkShadow", "controlDkShadow"), - entry("TabbedPane.background", "control"), - entry("TabbedPane.foreground", "controlText"), - entry("TabbedPane.highlight", "controlLtHighlight"), - entry("TabbedPane.light", "controlHighlight"), - entry("TabbedPane.shadow", "controlShadow"), - entry("TabbedPane.darkShadow", "controlDkShadow"), - entry("TabbedPane.focus", "controlText"), - entry("Table.foreground", "controlText"), - entry("Table.background", "window"), - entry("Table.selectionForeground", "textHighlightText"), - entry("Table.selectionBackground", "textHighlight"), - entry("Table.dropLineColor", "controlShadow"), - entry("Table.focusCellBackground", "window"), - entry("Table.focusCellForeground", "controlText"), - entry("TableHeader.foreground", "controlText"), - entry("TableHeader.background", "control"), - entry("TableHeader.focusCellBackground", "text"), - entry("TextField.background", "window"), - entry("TextField.foreground", "textText"), - entry("TextField.shadow", "controlShadow"), - entry("TextField.darkShadow", "controlDkShadow"), - entry("TextField.light", "controlHighlight"), - entry("TextField.highlight", "controlLtHighlight"), - entry("TextField.inactiveForeground", "textHInactiveText"), - entry("TextField.inactiveBackground", "control"), - entry("TextField.selectionBackground", "textHighlight"), - entry("TextField.selectionForeground", "textHighlightText"), - entry("TextField.caretForeground", "textText"), - entry("FormattedTextField.background", "window"), - entry("FormattedTextField.foreground", "textText"), - entry("FormattedTextField.inactiveForeground", "textHInactiveText"), - entry("FormattedTextField.inactiveBackground", "control"), - entry("FormattedTextField.selectionBackground", "textHighlight"), - entry("FormattedTextField.selectionForeground", "textHighlightText"), - entry("FormattedTextField.caretForeground", "textText"), - entry("PasswordField.background", "window"), - entry("PasswordField.foreground", "textText"), - entry("PasswordField.inactiveForeground", "textHInactiveText"), - entry("PasswordField.inactiveBackground", "control"), - entry("PasswordField.selectionBackground", "textHighlight"), - entry("PasswordField.selectionForeground", "textHighlightText"), - entry("PasswordField.caretForeground", "textText"), - entry("TextArea.background", "window"), - entry("TextArea.foreground", "textText"), - entry("TextArea.inactiveForeground", "textHInactiveText"), - entry("TextArea.selectionBackground", "textHighlight"), - entry("TextArea.selectionForeground", "textHighlightText"), - entry("TextArea.caretForeground", "textText"), - entry("TextPane.foreground", "textText"), - entry("TextPane.selectionBackground", "textHighlight"), - entry("TextPane.selectionForeground", "textHighlightText"), - entry("TextPane.caretForeground", "textText"), - entry("TextPane.inactiveForeground", "textHInactiveText"), - entry("EditorPane.foreground", "textText"), - entry("EditorPane.selectionBackground", "textHighlight"), - entry("EditorPane.selectionForeground", "textHighlightText"), - entry("EditorPane.caretForeground", "textText"), - entry("EditorPane.inactiveForeground", "textHInactiveText"), - entry("TitledBorder.titleColor", "controlText"), - entry("ToolBar.background", "control"), - entry("ToolBar.foreground", "controlText"), - entry("ToolBar.shadow", "controlShadow"), - entry("ToolBar.darkShadow", "controlDkShadow"), - entry("ToolBar.light", "controlHighlight"), - entry("ToolBar.highlight", "controlLtHighlight"), - entry("ToolBar.dockingBackground", "control"), - entry("ToolBar.floatingBackground", "control"), - entry("ToolTip.background", "info"), - entry("ToolTip.foreground", "infoText"), - entry("Tree.background", "window"), - entry("Tree.foreground", "textText"), - entry("Tree.textForeground", "textText"), - entry("Tree.textBackground", "text"), - entry("Tree.selectionForeground", "textHighlightText"), - entry("Tree.selectionBackground", "textHighlight"), - entry("Tree.dropLineColor", "controlShadow")); - - public static void fixupJavaDefaultsInheritence(GThemeValueMap values) { - List colors = values.getColors(); - for (ColorValue value : colors) { - ColorValue mapped = map(values, value); - if (mapped != null) { - values.addColor(mapped); - } - } - } - - private static ColorValue map(GThemeValueMap values, ColorValue value) { - String id = value.getId(); - String refId = map.get(id); - if (refId == null) { - return null; - } - ColorValue refValue = values.getColor(refId); - if (refValue == null) { - return null; - } - Color originalColor = value.get(values); - Color refColor = refValue.get(values); - if (originalColor == null || refColor == null) { - return null; - } - if (originalColor.getRGB() == refColor.getRGB()) { - return new ColorValue(id, refId); - } - return null; - } - -} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/JavaFontMapping.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/JavaFontMapping.java deleted file mode 100644 index b6650385f3..0000000000 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/JavaFontMapping.java +++ /dev/null @@ -1,143 +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.builtin; - -import static java.util.Map.*; - -import java.awt.Font; -import java.util.List; -import java.util.Map; - -import generic.theme.FontValue; -import generic.theme.GThemeValueMap; - -/** - * Maps Java UIDefaults font ids relationships to an id it inherits from - */ -public class JavaFontMapping { - private final static String BUTTON_GROUP = "ButtonComponents.font"; - private final static String TEXT_GROUP = "TextComponents.font"; - private final static String MENU_GROUP = "MenuComponents.font"; - private final static String MENU_ACCELERATOR_GROUP = "MenuComponents.acceleratorFont"; - private final static String DIALOG_GROUP = "Dialogs.font"; - private final static String WIDGET_GROUP = "Components.font"; - - private static Map map = Map.ofEntries( - entry("ArrowButton.font", BUTTON_GROUP), // nimbus - entry("Button.font", BUTTON_GROUP), - entry("CheckBox.font", BUTTON_GROUP), - entry("RadioButton.font", BUTTON_GROUP), - entry("ToggleButton.font", BUTTON_GROUP), - - entry("CheckBoxMenuItem.font", MENU_GROUP), - entry("Menu.font", MENU_GROUP), - entry("MenuBar.font", MENU_GROUP), - entry("MenuItem.font", MENU_GROUP), - entry("PopupMenu.font", MENU_GROUP), - entry("RadioButtonMenuItem.font", MENU_GROUP), - - entry("CheckBoxMenuItem.acceleratorFont", MENU_ACCELERATOR_GROUP), // metal, motif - entry("Menu.acceleratorFont", MENU_ACCELERATOR_GROUP), // metal, motif - entry("MenuItem.acceleratorFont", MENU_ACCELERATOR_GROUP), // metal, motfi - entry("RadioButtonMenuItem.acceleratorFont", MENU_ACCELERATOR_GROUP), // metal - - entry("EditorPane.font", TEXT_GROUP), - entry("FormattedTextField.font", TEXT_GROUP), - entry("PasswordField.font", TEXT_GROUP), - entry("TextArea.font", TEXT_GROUP), - entry("TextField.font", TEXT_GROUP), - entry("TextPane.font", TEXT_GROUP), - - entry("ColorChooser.font", DIALOG_GROUP), - entry("FileChooser.font", DIALOG_GROUP), // nimbus - - entry("ComboBox.font", WIDGET_GROUP), - entry("InternalFrame.titleFont", WIDGET_GROUP), // metal, motif, flat - entry("Label.font", WIDGET_GROUP), - entry("List.font", WIDGET_GROUP), - entry("OptionPane.font", DIALOG_GROUP), - entry("Panel.font", WIDGET_GROUP), - entry("ProgressBar.font", WIDGET_GROUP), - entry("RootPane.font", WIDGET_GROUP), - entry("Scrollbar.font", WIDGET_GROUP), - entry("ScrollBarThumb.font", WIDGET_GROUP), // nimbus - entry("ScrollBarTrack.font", WIDGET_GROUP), // nimbus - entry("ScrollPane.font", WIDGET_GROUP), - entry("Separator.font", WIDGET_GROUP), // nimbus - entry("Slider.font", WIDGET_GROUP), - entry("SliderThumb.font", WIDGET_GROUP), // nimbus - entry("SliderTrack.font", WIDGET_GROUP), // nimbus - entry("Spinner.font", WIDGET_GROUP), - entry("SplitPane.font", WIDGET_GROUP), // nimbus - entry("TabbedPane.font", WIDGET_GROUP), - entry("TitledBorder.font", WIDGET_GROUP), - entry("ToolBar.font", WIDGET_GROUP), - entry("ToolTip.font", TEXT_GROUP), - entry("Viewport.font", WIDGET_GROUP), - - entry("Tree.font", WIDGET_GROUP), - entry("Table.font", WIDGET_GROUP), - entry("TableHeader.font", "Table.font")); - - public static void fixupJavaDefaultsInheritence(GThemeValueMap values) { - createGroupDefaults(values); - List fonts = values.getFonts(); - for (FontValue value : fonts) { - FontValue mapped = map(values, value); - if (mapped != null) { - values.addFont(mapped); - } - } - } - - private static FontValue map(GThemeValueMap values, FontValue value) { - String id = value.getId(); - String refId = map.get(id); - if (refId == null) { - return null; - } - FontValue refValue = values.getFont(refId); - if (refValue == null) { - return null; - } - Font originalFont = value.get(values); - Font refFont = refValue.get(values); - if (originalFont == null || refFont == null) { - return null; - } - if (originalFont.equals(refFont)) { - return new FontValue(id, refId); - } - return null; - } - - public static void createGroupDefaults(GThemeValueMap valuesMap) { - addFontValue(valuesMap, BUTTON_GROUP, "Button.font"); - addFontValue(valuesMap, TEXT_GROUP, "TextField.font"); - addFontValue(valuesMap, MENU_GROUP, "Menu.font"); - addFontValue(valuesMap, MENU_ACCELERATOR_GROUP, "Menu.acceleratorFont"); - addFontValue(valuesMap, DIALOG_GROUP, "ColorChooser.font"); - addFontValue(valuesMap, WIDGET_GROUP, "Label.font"); - } - - private static void addFontValue(GThemeValueMap valuesMap, String groupId, String exemplarId) { - FontValue font = valuesMap.getFont(exemplarId); - if (font != null) { - valuesMap.addFont(new FontValue(groupId, font.get(valuesMap))); - } - } - -} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/FlatLookAndFeelInstaller.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/FlatLookAndFeelManager.java similarity index 61% rename from Ghidra/Framework/Generic/src/main/java/generic/theme/laf/FlatLookAndFeelInstaller.java rename to Ghidra/Framework/Generic/src/main/java/generic/theme/laf/FlatLookAndFeelManager.java index 93248db7c6..778099ddfd 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/FlatLookAndFeelInstaller.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/FlatLookAndFeelManager.java @@ -17,24 +17,30 @@ package generic.theme.laf; import javax.swing.UIManager; +import generic.theme.ColorValue; import generic.theme.LafType; -/** - * Common {@link LookAndFeelInstaller} for any of the "Flat" lookAndFeels - */ -public class FlatLookAndFeelInstaller extends LookAndFeelInstaller { +public class FlatLookAndFeelManager extends LookAndFeelManager { - public FlatLookAndFeelInstaller(LafType lookAndFeelType) { - super(lookAndFeelType); + public FlatLookAndFeelManager(LafType laf) { + super(laf); + + // establish system color to LookAndFeel colors + systemToLafMap.addColor(new ColorValue(SYSTEM_WIDGET_BACKGROUND_COLOR_ID, "text")); + systemToLafMap.addColor(new ColorValue(SYSTEM_TOOLTIP_BACKGROUND_COLOR_ID, "info")); } @Override protected void fixupLookAndFeelIssues() { super.fixupLookAndFeelIssues(); - // We have historically managed button focusability ourselves. Allow this by default so + // We have historically managed button focus-ability ourselves. Allow this by default so // features continue to work as expected, such as right-clicking on ToolButtons. UIManager.put("ToolBar.focusableButtons", Boolean.TRUE); } + @Override + protected ThemeGrouper getThemeGrouper() { + return new FlatThemeGrouper(); + } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/FlatThemeGrouper.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/FlatThemeGrouper.java new file mode 100644 index 0000000000..695cf94e6a --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/FlatThemeGrouper.java @@ -0,0 +1,64 @@ +/* ### + * 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.laf; + +import generic.theme.GThemeValueMap; + +/** + * Adds specialized groupings unique to the Flat LookAndFeels + */ +public class FlatThemeGrouper extends ThemeGrouper { + + @Override + public void group(GThemeValueMap values) { + // we made up a source property for a common Flat color. Picked one of them as + // an exemplar (menu.foreground) + // @formatter:off + defineCustomColorGroup(values, "color.flat.menu.hover.bg", "MenuBar.hoverBackground"); + defineCustomColorGroup(values, "color.flat.button.hover.bg", "Button.hoverBackground"); + defineCustomColorGroup(values, "color.flat.button.selected.bg","Button.selectedBackground"); + defineCustomColorGroup(values, "color.flat.button.toolbar.hover.bg","Button.toolbar.hoverBackground"); + defineCustomColorGroup(values, "color.flat.button.toolbar.pressed.bg","Button.toolbar.pressedBackground"); + defineCustomColorGroup(values, "color.flat.checkbox.icon.focus.border","CheckBox.icon.focusedBorderColor"); + defineCustomColorGroup(values, "color.flat.menu.accelerator.fg","Menu.acceleratorForeground"); + + + defineCustomColorGroup(values, "color.flat.focus.border", "Button.focusedBorderColor"); + defineCustomColorGroup(values, "color.flat.focus", "Component.focusColor"); + defineCustomColorGroup(values, "color.flat.focus.bg", "Button.focusedBackground"); + defineCustomColorGroup(values, "color.flat.checkmark", "CheckBox.icon.checkmarkColor"); + defineCustomColorGroup(values, "color.flat.disabled", "Button.disabledBorderColor"); + defineCustomColorGroup(values, "color.flat.disabled.selected","Button.disabledSelectedBackground"); + defineCustomColorGroup(values, "color.flat.arrow","Spinner.buttonArrowColor"); + defineCustomColorGroup(values, "color.flat.arrow.disabled","Spinner.buttonDisabledArrowColor"); + defineCustomColorGroup(values, "color.flat.arrow.hover","Spinner.buttonHoverArrowColor"); + defineCustomColorGroup(values, "color.flat.arrow.pressed","Spinner.buttonPressedArrowColor"); + + defineCustomColorGroup(values, "color.flat.dropcell.bg","List.dropCellBackground"); + defineCustomColorGroup(values, "color.flat.dropline","List.dropLineColor"); + defineCustomColorGroup(values, "color.flat.underline","MenuItem.underlineSelectionColor"); + defineCustomColorGroup(values, "color.flat.docking.bg","ToolBar.dockingBackground"); + defineCustomColorGroup(values, "color.flat.progressbar.bg","ProgressBar.background"); + defineCustomColorGroup(values, "color.flat.progressbar.fg","ProgressBar.foreground"); + defineCustomColorGroup(values, "color.flat.icon.bg","Tree.icon.openColor"); + defineCustomColorGroup(values, "color.flat.selection.inactive","Tree.selectionInactiveBackground"); + + + // @formatter:on + super.group(values); + } + +} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GNimbusLookAndFeel.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GNimbusLookAndFeel.java index ddb7714af4..575840e6ef 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GNimbusLookAndFeel.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GNimbusLookAndFeel.java @@ -55,7 +55,7 @@ public class GNimbusLookAndFeel extends NimbusLookAndFeel { String id = iconValue.getId(); // because some icons are weird, put raw icons into defaults, only use GIcons for // setting Icons explicitly on components - Icon icon = Gui.getRawIcon(id, true); + Icon icon = Gui.getIcon(id, true); defaults.put(id, icon); } @@ -69,21 +69,21 @@ public class GNimbusLookAndFeel extends NimbusLookAndFeel { GThemeValueMap javaDefaults = new GThemeValueMap(); List colorIds = - LookAndFeelInstaller.getLookAndFeelIdsForType(defaults, Color.class); + LookAndFeelManager.getLookAndFeelIdsForType(defaults, Color.class); for (String id : colorIds) { Color color = defaults.getColor(id); ColorValue value = new ColorValue(id, color); javaDefaults.addColor(value); } List fontIds = - LookAndFeelInstaller.getLookAndFeelIdsForType(defaults, Font.class); + LookAndFeelManager.getLookAndFeelIdsForType(defaults, Font.class); for (String id : fontIds) { Font font = defaults.getFont(id); - FontValue value = new FontValue(id, LookAndFeelInstaller.fromUiResource(font)); + FontValue value = new FontValue(id, LookAndFeelManager.fromUiResource(font)); javaDefaults.addFont(value); } List iconIds = - LookAndFeelInstaller.getLookAndFeelIdsForType(defaults, Icon.class); + LookAndFeelManager.getLookAndFeelIdsForType(defaults, Icon.class); for (String id : iconIds) { Icon icon = defaults.getIcon(id); javaDefaults.addIcon(new IconValue(id, icon)); @@ -91,7 +91,6 @@ public class GNimbusLookAndFeel extends NimbusLookAndFeel { // need to set javaDefalts now to trigger building currentValues so the when // we create GColors below, they can be resolved. Gui.setJavaDefaults(javaDefaults); - return javaDefaults; } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelInstaller.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelInstaller.java deleted file mode 100644 index e15b0194cf..0000000000 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelInstaller.java +++ /dev/null @@ -1,45 +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.laf; - -import javax.swing.LookAndFeel; -import javax.swing.UnsupportedLookAndFeelException; - -import generic.theme.LafType; - -/** - * LookAndFeelInstaller for the GTK {@link LookAndFeel} - */ -public class GtkLookAndFeelInstaller extends LookAndFeelInstaller { - - public GtkLookAndFeelInstaller() { - super(LafType.GTK); - } - - @Override - protected void installLookAndFeel() throws ClassNotFoundException, InstantiationException, - IllegalAccessException, UnsupportedLookAndFeelException { - - super.installLookAndFeel(); - } - -// @Override -// protected void installJavaDefaults() { -// // GTK does not support changing its values, so set the javaDefaults to an empty map -// Gui.setJavaDefaults(new GThemeValueMap()); -// } -// -} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelManager.java index 7e4cf24476..f0853d0840 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelManager.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelManager.java @@ -25,10 +25,4 @@ public class GtkLookAndFeelManager extends LookAndFeelManager { public GtkLookAndFeelManager() { super(LafType.GTK); } - - @Override - protected LookAndFeelInstaller getLookAndFeelInstaller() { - return new GtkLookAndFeelInstaller(); - } - } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelInstaller.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelInstaller.java deleted file mode 100644 index de502dca09..0000000000 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelInstaller.java +++ /dev/null @@ -1,341 +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.laf; - -import java.awt.Color; -import java.awt.Font; -import java.util.*; -import java.util.Map.Entry; - -import javax.swing.*; -import javax.swing.UIManager.LookAndFeelInfo; -import javax.swing.plaf.FontUIResource; -import javax.swing.plaf.UIResource; - -import org.apache.commons.collections4.IteratorUtils; - -import generic.theme.*; -import generic.util.action.*; -import ghidra.util.Msg; -import ghidra.util.SystemUtilities; - -/** - * Installs a specific {@link LookAndFeel} into the {@link UIManager}. The idea is that there - * is a specific installer for each supported {@link LookAndFeel} to handle unique needs for - * that LookAndFeel. Subclasses can also override {@link #fixupLookAndFeelIssues()} to make - * UI tweaks to specific LookAndFeels. - */ -public class LookAndFeelInstaller { - - private LafType lookAndFeel; - - public LookAndFeelInstaller(LafType lookAndFeel) { - this.lookAndFeel = lookAndFeel; - } - - /** - * Installs the {@link LookAndFeel} associated with this installer - * @throws ClassNotFoundException if the LookAndFeel - * class could not be found - * @throws InstantiationException if a new instance of the class - * couldn't be created - * @throws IllegalAccessException if the class or initializer isn't accessible - * @throws UnsupportedLookAndFeelException if - * lnf.isSupportedLookAndFeel() is false - */ - public final void install() throws ClassNotFoundException, InstantiationException, - IllegalAccessException, UnsupportedLookAndFeelException { - cleanUiDefaults(); - installLookAndFeel(); - installJavaDefaults(); - fixupLookAndFeelIssues(); - installGlobalProperties(); - } - - /** - * Subclass provide this method to install the specific loo - * @throws ClassNotFoundException if the LookAndFeel - * class could not be found - * @throws InstantiationException if a new instance of the class - * couldn't be created - * @throws IllegalAccessException if the class or initializer isn't accessible - * @throws UnsupportedLookAndFeelException if - * lnf.isSupportedLookAndFeel() is false - */ - protected void installLookAndFeel() throws ClassNotFoundException, InstantiationException, - IllegalAccessException, UnsupportedLookAndFeelException { - String name = lookAndFeel.getName(); - UIManager.setLookAndFeel(findLookAndFeelClassName(name)); - - } - - /** - * Subclass can override this method to do specific LookAndFeel fix ups - */ - protected void fixupLookAndFeelIssues() { - // no generic fix-ups at this time. - } - - /** - * Extracts java default colors, fonts, and icons and stores them in {@link Gui}. - */ - protected void installJavaDefaults() { - GThemeValueMap javaDefaults = extractJavaDefaults(); - Gui.setJavaDefaults(javaDefaults); - installPropertiesBackIntoUiDefaults(javaDefaults); - } - - private void installPropertiesBackIntoUiDefaults(GThemeValueMap javaDefaults) { - UIDefaults defaults = UIManager.getDefaults(); - - GTheme theme = Gui.getActiveTheme(); - - // we replace java default colors with GColor equivalents so that we - // can change colors without having to reinstall ui on each component - // This trick only works for colors. Fonts and icons don't universally - // allow being wrapped like colors do. - for (ColorValue colorValue : javaDefaults.getColors()) { - String id = colorValue.getId(); - defaults.put(id, Gui.getGColorUiResource(id)); - } - - // put fonts back into defaults in case they have been changed by the current theme - for (FontValue fontValue : javaDefaults.getFonts()) { - String id = fontValue.getId(); - FontValue themeValue = theme.getFont(id); - if (themeValue != null) { - Font font = Gui.getFont(id); - defaults.put(id, new FontUIResource(font)); - } - } - - // put icons back into defaults in case they have been changed by the current theme - for (IconValue iconValue : javaDefaults.getIcons()) { - String id = iconValue.getId(); - IconValue themeValue = theme.getIcon(id); - if (themeValue != null) { - // because some icons are weird, put raw icons into defaults, only use GIcons for - // setting Icons explicitly on components - Icon icon = Gui.getRawIcon(id, true); - defaults.put(id, icon); - } - } - } - - protected GThemeValueMap extractJavaDefaults() { - return extractJavaDefaults(UIManager.getDefaults()); - } - - protected GThemeValueMap extractJavaDefaults(UIDefaults defaults) { - GThemeValueMap values = new GThemeValueMap(); - // for now, just doing color properties. - List ids = getLookAndFeelIdsForType(defaults, Color.class); - for (String id : ids) { - // convert UIResource color to regular colors so if used, they don't get wiped - // out when we update the UIs - values.addColor(new ColorValue(id, fromUiResource(UIManager.getColor(id)))); - } - ids = getLookAndFeelIdsForType(defaults, Font.class); - for (String id : ids) { - // convert UIResource fonts to regular fonts so if used, they don't get wiped - // out when we update UIs - values.addFont(new FontValue(id, fromUiResource(UIManager.getFont(id)))); - } - ids = getLookAndFeelIdsForType(defaults, Icon.class); - for (String id : ids) { - Icon icon = UIManager.getIcon(id); - values.addIcon(new IconValue(id, icon)); - } - - return values; - } - - protected String findLookAndFeelClassName(String lookAndFeelName) { - LookAndFeelInfo[] installedLookAndFeels = UIManager.getInstalledLookAndFeels(); - for (LookAndFeelInfo info : installedLookAndFeels) { - String className = info.getClassName(); - if (lookAndFeelName.equals(className) || lookAndFeelName.equals(info.getName())) { - return className; - } - } - - Msg.debug(this, "Unable to find requested Look and Feel: " + lookAndFeelName); - return UIManager.getSystemLookAndFeelClassName(); - } - - protected boolean isSupported(String lookAndFeelName) { - LookAndFeelInfo[] installedLookAndFeels = UIManager.getInstalledLookAndFeels(); - for (LookAndFeelInfo info : installedLookAndFeels) { - if (lookAndFeelName.equals(info.getName())) { - return true; - } - } - return false; - } - - protected void setKeyBinding(String existingKsText, String newKsText, String[] prefixValues) { - - KeyStroke existingKs = KeyStroke.getKeyStroke(existingKsText); - KeyStroke newKs = KeyStroke.getKeyStroke(newKsText); - - for (String properyPrefix : prefixValues) { - - UIDefaults defaults = UIManager.getDefaults(); - Object object = defaults.get(properyPrefix + ".focusInputMap"); - InputMap inputMap = (InputMap) object; - Object action = inputMap.get(existingKs); - inputMap.put(newKs, action); - } - } - - private void installGlobalLookAndFeelAttributes() { - // Fix up the default fonts that Java 1.5.0 changed to Courier, which looked terrible. - Font f = new Font("Monospaced", Font.PLAIN, 12); - UIManager.put("PasswordField.font", f); - UIManager.put("TextArea.font", f); - - // We like buttons that change on hover, so force that to happen (see Tracker SCR 3966) - UIManager.put("Button.rollover", Boolean.TRUE); - UIManager.put("ToolBar.isRollover", Boolean.TRUE); - } - - private void installPopupMenuSettingsOverride() { - // Java 1.6 UI consumes MousePressed event when dismissing popup menu - // which prevents application components from getting this event. - UIManager.put("PopupMenu.consumeEventOnClose", Boolean.FALSE); - } - - private void installGlobalFontSizeOverride() { - - // only set a global size if the property is set - Integer overrideFontInteger = SystemUtilities.getFontSizeOverrideValue(); - if (overrideFontInteger == null) { - return; - } - - setGlobalFontSizeOverride(overrideFontInteger); - } - - private void installCustomLookAndFeelActions() { - // these prefixes are for text components - String[] UIPrefixValues = - { "TextField", "FormattedTextField", "TextArea", "TextPane", "EditorPane" }; - - DeleteToStartOfWordAction deleteToStartOfWordAction = new DeleteToStartOfWordAction(); - registerAction(deleteToStartOfWordAction, DeleteToStartOfWordAction.KEY_STROKE, - UIPrefixValues); - - DeleteToEndOfWordAction deleteToEndOfWordAction = new DeleteToEndOfWordAction(); - registerAction(deleteToEndOfWordAction, DeleteToEndOfWordAction.KEY_STROKE, UIPrefixValues); - - BeginningOfLineAction beginningOfLineAction = new BeginningOfLineAction(); - registerAction(beginningOfLineAction, BeginningOfLineAction.KEY_STROKE, UIPrefixValues); - - EndOfLineAction endOfLineAction = new EndOfLineAction(); - registerAction(endOfLineAction, EndOfLineAction.KEY_STROKE, UIPrefixValues); - - SelectBeginningOfLineAction selectBeginningOfLineAction = new SelectBeginningOfLineAction(); - registerAction(selectBeginningOfLineAction, SelectBeginningOfLineAction.KEY_STROKE, - UIPrefixValues); - - SelectEndOfLineAction selectEndOfLineAction = new SelectEndOfLineAction(); - registerAction(selectEndOfLineAction, SelectEndOfLineAction.KEY_STROKE, UIPrefixValues); - } - - /** Allows you to globally set the font size (don't use this method!) */ - private void setGlobalFontSizeOverride(int fontSize) { - UIDefaults defaults = UIManager.getDefaults(); - - Set> set = defaults.entrySet(); - Iterator> iterator = set.iterator(); - while (iterator.hasNext()) { - Entry entry = iterator.next(); - Object key = entry.getKey(); - - if (key.toString().toLowerCase().indexOf("font") != -1) { - Font currentFont = defaults.getFont(key); - if (currentFont != null) { - Font newFont = currentFont.deriveFont((float) fontSize); - UIManager.put(key, newFont); - } - } - } - } - - private void registerAction(Action action, KeyStroke keyStroke, String[] prefixValues) { - for (String properyPrefix : prefixValues) { - UIDefaults defaults = UIManager.getDefaults(); - Object object = defaults.get(properyPrefix + ".focusInputMap"); - InputMap inputMap = (InputMap) object; - inputMap.put(keyStroke, action); - } - } - - private void installGlobalProperties() { - installGlobalLookAndFeelAttributes(); - installGlobalFontSizeOverride(); - installCustomLookAndFeelActions(); - installPopupMenuSettingsOverride(); - } - - public static Color fromUiResource(Color color) { - if (color.getClass() == Color.class) { - return color; - } - return new Color(color.getRGB(), true); - } - - public static Font fromUiResource(Font font) { - if (font instanceof UIResource) { - return new FontNonUiResource(font); - } - return font; - } - - private void cleanUiDefaults() { - GThemeValueMap javaDefaults = Gui.getJavaDefaults(); - if (javaDefaults == null) { - return; - } - UIDefaults defaults = UIManager.getDefaults(); - for (ColorValue colorValue : javaDefaults.getColors()) { - String id = colorValue.getId(); - defaults.put(id, null); - } - for (FontValue fontValue : javaDefaults.getFonts()) { - String id = fontValue.getId(); - defaults.put(id, null); - } - for (IconValue iconValue : javaDefaults.getIcons()) { - String id = iconValue.getId(); - defaults.put(id, null); - } - } - - public static List getLookAndFeelIdsForType(UIDefaults defaults, Class clazz) { - List colorKeys = new ArrayList<>(); - List keyList = IteratorUtils.toList(defaults.keys().asIterator()); - for (Object key : keyList) { - if (key instanceof String) { - Object value = defaults.get(key); - if (clazz.isInstance(value)) { - colorKeys.add((String) key); - } - } - } - return colorKeys; - } -} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelManager.java index 934e4553db..12ece6a4e4 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelManager.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelManager.java @@ -18,25 +18,49 @@ package generic.theme.laf; import java.awt.*; import java.util.*; import java.util.List; +import java.util.Map.Entry; import javax.swing.*; -import javax.swing.plaf.FontUIResource; +import javax.swing.UIManager.LookAndFeelInfo; +import javax.swing.plaf.*; + +import org.apache.commons.collections4.IteratorUtils; import generic.theme.*; +import generic.util.action.*; +import ghidra.util.Msg; +import ghidra.util.SystemUtilities; /** * Manages installing and updating a {@link LookAndFeel} */ public abstract class LookAndFeelManager { + /** + * These are color ids (see {@link GColor} used to represent general concepts that + * application developers can use to get the color for that concept as defined by + * a specific {@link LookAndFeel}. This class will define some standard defaults + * mappings in the constructor, but it is expected that each specific LookAndFeelManager + * will override these mappings with values appropriate for that LookAndFeel. + */ + protected static final String SYSTEM_APP_BACKGROUND_COLOR_ID = "system.color.bg.application"; + protected static final String SYSTEM_WIDGET_BACKGROUND_COLOR_ID = "system.color.bg.widget"; + protected static final String SYSTEM_TOOLTIP_BACKGROUND_COLOR_ID = "system.color.bg.tooltip"; + protected static final String SYSTEM_BORDER_COLOR_ID = "system.color.border"; + private LafType laf; private Map fontRegistryMap = new HashMap<>(); + protected GThemeValueMap systemToLafMap = new GThemeValueMap(); protected LookAndFeelManager(LafType laf) { this.laf = laf; - } - protected abstract LookAndFeelInstaller getLookAndFeelInstaller(); + // establish system color to LookAndFeel colors + systemToLafMap.addColor(new ColorValue(SYSTEM_APP_BACKGROUND_COLOR_ID, "control")); + systemToLafMap.addColor(new ColorValue(SYSTEM_WIDGET_BACKGROUND_COLOR_ID, "control")); + systemToLafMap.addColor(new ColorValue(SYSTEM_TOOLTIP_BACKGROUND_COLOR_ID, "control")); + systemToLafMap.addColor(new ColorValue(SYSTEM_BORDER_COLOR_ID, "controlShadow")); + } /** * Returns the {@link LafType} managed by this manager. @@ -59,8 +83,11 @@ public abstract class LookAndFeelManager { public void installLookAndFeel() throws ClassNotFoundException, InstantiationException, IllegalAccessException, UnsupportedLookAndFeelException { - LookAndFeelInstaller installer = getLookAndFeelInstaller(); - installer.install(); + cleanUiDefaults(); + doInstallLookAndFeel(); + installJavaDefaults(); + fixupLookAndFeelIssues(); + installGlobalProperties(); updateComponentUis(); } @@ -102,7 +129,7 @@ public abstract class LookAndFeelManager { UIDefaults defaults = UIManager.getDefaults(); for (IconValue iconValue : icons) { String id = iconValue.getId(); - Icon correctIcon = Gui.getRawIcon(id, false); + Icon correctIcon = Gui.getIcon(id, false); Icon storedIcon = defaults.getIcon(id); if (correctIcon != null && !correctIcon.equals(storedIcon)) { defaults.put(id, correctIcon); @@ -120,11 +147,13 @@ public abstract class LookAndFeelManager { /** * Called when one or more icons have changed. - * @param id the id of primary icon that changed - * @param changedIconIds - * @param newIcon + * @param changedIconIds set of icon ids affected by this icon change + * @param newIcon the new icon to use for the given set of icon ids */ public void iconsChanged(Set changedIconIds, Icon newIcon) { + if (!(newIcon instanceof UIResource)) { + newIcon = new IconUIResource(newIcon); + } if (!changedIconIds.isEmpty()) { UIDefaults defaults = UIManager.getDefaults(); for (String javaIconId : changedIconIds) { @@ -139,14 +168,15 @@ public abstract class LookAndFeelManager { /** * Called when one or more fonts have changed. * @param changedJavaFontIds the set of Java Font ids that are affected by this change - * @param newFont the new font for the given ids */ - public void fontsChanged(Set changedJavaFontIds, Font newFont) { + public void fontsChanged(Set changedJavaFontIds) { if (!changedJavaFontIds.isEmpty()) { UIDefaults defaults = UIManager.getDefaults(); - newFont = new FontUIResource(newFont); for (String javaFontId : changedJavaFontIds) { - defaults.put(javaFontId, newFont); + // even though all these derive from the new font, they might be different + // because of FontModifiers. + Font font = Gui.getFont(javaFontId); + defaults.put(javaFontId, new FontUIResource(font)); } updateComponentFonts(changedJavaFontIds); updateComponentUis(); @@ -175,11 +205,311 @@ public abstract class LookAndFeelManager { } } - public void registerFont(Component c, String fontId) { + /** + * Binds the component to the font identified by the given font id. Whenever the font for + * the font id changes, the component will updated with the new font. + * @param component the component to set/update the font + * @param fontId the id of the font to register with the given component + */ + public void registerFont(Component component, String fontId) { ComponentFontRegistry register = fontRegistryMap.computeIfAbsent(fontId, id -> new ComponentFontRegistry(id)); - register.addComponent(c); + register.addComponent(component); + } + + /** + * Returns a color that is not a {@link UIResource}. + * @param color the color to return an non UIResource color for + * @return a color that is not a {@link UIResource}. + */ + public static Color fromUiResource(Color color) { + if (color.getClass() == Color.class) { + return color; + } + return new Color(color.getRGB(), true); + } + + /** + * Returns a font that is not a {@link UIResource}. + * @param font the font to return an non UIResource font for + * @return a font that is not a {@link UIResource}. + */ + public static Font fromUiResource(Font font) { + if (font instanceof UIResource) { + return new FontNonUiResource(font); + } + return font; + } + + /** + * Subclass provide this method to install the specific loo + * @throws ClassNotFoundException if the LookAndFeel + * class could not be found + * @throws InstantiationException if a new instance of the class + * couldn't be created + * @throws IllegalAccessException if the class or initializer isn't accessible + * @throws UnsupportedLookAndFeelException if + * lnf.isSupportedLookAndFeel() is false + */ + protected void doInstallLookAndFeel() throws ClassNotFoundException, InstantiationException, + IllegalAccessException, UnsupportedLookAndFeelException { + String name = laf.getName(); + UIManager.setLookAndFeel(findLookAndFeelClassName(name)); + + } + + /** + * Subclass can override this method to do specific LookAndFeel fix ups + */ + protected void fixupLookAndFeelIssues() { + // no generic fix-ups at this time. + } + + /** + * Extracts java default colors, fonts, and icons and stores them in {@link Gui}. + */ + private void installJavaDefaults() { + GThemeValueMap javaDefaults = extractJavaDefaults(); + javaDefaults.load(systemToLafMap); // add in our system color mappings + ThemeGrouper grouper = getThemeGrouper(); + grouper.group(javaDefaults); + Gui.setJavaDefaults(javaDefaults); + installPropertiesBackIntoUiDefaults(javaDefaults); + } + + protected ThemeGrouper getThemeGrouper() { + return new ThemeGrouper(); + } + + private void installPropertiesBackIntoUiDefaults(GThemeValueMap javaDefaults) { + UIDefaults defaults = UIManager.getDefaults(); + + GTheme theme = Gui.getActiveTheme(); + + // we replace java default colors with GColor equivalents so that we + // can change colors without having to reinstall ui on each component + // This trick only works for colors. Fonts and icons don't universally + // allow being wrapped like colors do. + for (ColorValue colorValue : javaDefaults.getColors()) { + String id = colorValue.getId(); + defaults.put(id, Gui.getGColorUiResource(id)); + } + + // put fonts back into defaults in case they have been changed by the current theme + for (FontValue fontValue : javaDefaults.getFonts()) { + String id = fontValue.getId(); + FontValue themeValue = theme.getFont(id); + if (themeValue != null) { + Font font = Gui.getFont(id); + defaults.put(id, new FontUIResource(font)); + } + } + + // put icons back into defaults in case they have been changed by the current theme + for (IconValue iconValue : javaDefaults.getIcons()) { + String id = iconValue.getId(); + IconValue themeValue = theme.getIcon(id); + if (themeValue != null) { + // because some icons are weird, put raw icons into defaults, only use GIcons for + // setting Icons explicitly on components + Icon icon = Gui.getIcon(id, true); + defaults.put(id, icon); + } + } + } + + protected GThemeValueMap extractJavaDefaults() { + UIDefaults defaults = UIManager.getDefaults(); + GThemeValueMap values = new GThemeValueMap(); + // for now, just doing color properties. + List ids = getLookAndFeelIdsForType(defaults, Color.class); + for (String id : ids) { + // convert UIResource color to regular colors so if used, they don't get wiped + // out when we update the UIs + values.addColor(new ColorValue(id, fromUiResource(UIManager.getColor(id)))); + } + ids = getLookAndFeelIdsForType(defaults, Font.class); + for (String id : ids) { + // convert UIResource fonts to regular fonts so if used, they don't get wiped + // out when we update UIs + values.addFont(new FontValue(id, fromUiResource(UIManager.getFont(id)))); + } + ids = getLookAndFeelIdsForType(defaults, Icon.class); + for (String id : ids) { + Icon icon = UIManager.getIcon(id); + values.addIcon(new IconValue(id, icon)); + } + + return values; + } + + protected String findLookAndFeelClassName(String lookAndFeelName) { + LookAndFeelInfo[] installedLookAndFeels = UIManager.getInstalledLookAndFeels(); + for (LookAndFeelInfo info : installedLookAndFeels) { + String className = info.getClassName(); + if (lookAndFeelName.equals(className) || lookAndFeelName.equals(info.getName())) { + return className; + } + } + + Msg.debug(this, "Unable to find requested Look and Feel: " + lookAndFeelName); + return UIManager.getSystemLookAndFeelClassName(); + } + + protected boolean isSupported(String lookAndFeelName) { + LookAndFeelInfo[] installedLookAndFeels = UIManager.getInstalledLookAndFeels(); + for (LookAndFeelInfo info : installedLookAndFeels) { + if (lookAndFeelName.equals(info.getName())) { + return true; + } + } + return false; + } + + protected void setKeyBinding(String existingKsText, String newKsText, String[] prefixValues) { + + KeyStroke existingKs = KeyStroke.getKeyStroke(existingKsText); + KeyStroke newKs = KeyStroke.getKeyStroke(newKsText); + + for (String properyPrefix : prefixValues) { + + UIDefaults defaults = UIManager.getDefaults(); + Object object = defaults.get(properyPrefix + ".focusInputMap"); + InputMap inputMap = (InputMap) object; + Object action = inputMap.get(existingKs); + inputMap.put(newKs, action); + } + } + + private void installGlobalLookAndFeelAttributes() { + // Fix up the default fonts that Java 1.5.0 changed to Courier, which looked terrible. + Font f = new Font("Monospaced", Font.PLAIN, 12); + UIManager.put("PasswordField.font", f); + UIManager.put("TextArea.font", f); + + // We like buttons that change on hover, so force that to happen (see Tracker SCR 3966) + UIManager.put("Button.rollover", Boolean.TRUE); + UIManager.put("ToolBar.isRollover", Boolean.TRUE); + } + + private void installPopupMenuSettingsOverride() { + // Java 1.6 UI consumes MousePressed event when dismissing popup menu + // which prevents application components from getting this event. + UIManager.put("PopupMenu.consumeEventOnClose", Boolean.FALSE); + } + + private void installGlobalFontSizeOverride() { + + // only set a global size if the property is set + Integer overrideFontInteger = SystemUtilities.getFontSizeOverrideValue(); + if (overrideFontInteger == null) { + return; + } + + setGlobalFontSizeOverride(overrideFontInteger); + } + + private void installCustomLookAndFeelActions() { + // these prefixes are for text components + String[] UIPrefixValues = + { "TextField", "FormattedTextField", "TextArea", "TextPane", "EditorPane" }; + + DeleteToStartOfWordAction deleteToStartOfWordAction = new DeleteToStartOfWordAction(); + registerAction(deleteToStartOfWordAction, DeleteToStartOfWordAction.KEY_STROKE, + UIPrefixValues); + + DeleteToEndOfWordAction deleteToEndOfWordAction = new DeleteToEndOfWordAction(); + registerAction(deleteToEndOfWordAction, DeleteToEndOfWordAction.KEY_STROKE, UIPrefixValues); + + BeginningOfLineAction beginningOfLineAction = new BeginningOfLineAction(); + registerAction(beginningOfLineAction, BeginningOfLineAction.KEY_STROKE, UIPrefixValues); + + EndOfLineAction endOfLineAction = new EndOfLineAction(); + registerAction(endOfLineAction, EndOfLineAction.KEY_STROKE, UIPrefixValues); + + SelectBeginningOfLineAction selectBeginningOfLineAction = new SelectBeginningOfLineAction(); + registerAction(selectBeginningOfLineAction, SelectBeginningOfLineAction.KEY_STROKE, + UIPrefixValues); + + SelectEndOfLineAction selectEndOfLineAction = new SelectEndOfLineAction(); + registerAction(selectEndOfLineAction, SelectEndOfLineAction.KEY_STROKE, UIPrefixValues); + } + + /** Allows you to globally set the font size (don't use this method!) */ + private void setGlobalFontSizeOverride(int fontSize) { + UIDefaults defaults = UIManager.getDefaults(); + + Set> set = defaults.entrySet(); + Iterator> iterator = set.iterator(); + while (iterator.hasNext()) { + Entry entry = iterator.next(); + Object key = entry.getKey(); + + if (key.toString().toLowerCase().indexOf("font") != -1) { + Font currentFont = defaults.getFont(key); + if (currentFont != null) { + Font newFont = currentFont.deriveFont((float) fontSize); + UIManager.put(key, newFont); + } + } + } + } + + private void registerAction(Action action, KeyStroke keyStroke, String[] prefixValues) { + for (String properyPrefix : prefixValues) { + UIDefaults defaults = UIManager.getDefaults(); + Object object = defaults.get(properyPrefix + ".focusInputMap"); + InputMap inputMap = (InputMap) object; + inputMap.put(keyStroke, action); + } + } + + private void installGlobalProperties() { + installGlobalLookAndFeelAttributes(); + installGlobalFontSizeOverride(); + installCustomLookAndFeelActions(); + installPopupMenuSettingsOverride(); + } + + private void cleanUiDefaults() { + GThemeValueMap javaDefaults = Gui.getJavaDefaults(); + if (javaDefaults == null) { + return; + } + UIDefaults defaults = UIManager.getDefaults(); + for (ColorValue colorValue : javaDefaults.getColors()) { + String id = colorValue.getId(); + defaults.put(id, null); + } + for (FontValue fontValue : javaDefaults.getFonts()) { + String id = fontValue.getId(); + defaults.put(id, null); + } + for (IconValue iconValue : javaDefaults.getIcons()) { + String id = iconValue.getId(); + defaults.put(id, null); + } + } + + /** + * Searches the given UIDefaults for ids whose value matches the given class + * @param defaults the UIDefaults to search + * @param clazz the value class to look for (i.e., Color, Font, or Icon) + * @return the list of ids whose value is of the given class type. + */ + public static List getLookAndFeelIdsForType(UIDefaults defaults, Class clazz) { + List colorKeys = new ArrayList<>(); + List keyList = IteratorUtils.toList(defaults.keys().asIterator()); + for (Object key : keyList) { + if (key instanceof String) { + Object value = defaults.get(key); + if (clazz.isInstance(value)) { + colorKeys.add((String) key); + } + } + } + return colorKeys; } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GenericFlatLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MacLookAndFeelManager.java similarity index 66% rename from Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GenericFlatLookAndFeelManager.java rename to Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MacLookAndFeelManager.java index 0adb54705f..7262d8914e 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GenericFlatLookAndFeelManager.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MacLookAndFeelManager.java @@ -17,18 +17,14 @@ package generic.theme.laf; import generic.theme.LafType; -/** - * Common {@link LookAndFeelManager} for any of the "Flat" lookAndFeels - */ -public class GenericFlatLookAndFeelManager extends LookAndFeelManager { +public class MacLookAndFeelManager extends LookAndFeelManager { - public GenericFlatLookAndFeelManager(LafType laf) { - super(laf); + public MacLookAndFeelManager() { + super(LafType.MAC); } @Override - protected LookAndFeelInstaller getLookAndFeelInstaller() { - return new FlatLookAndFeelInstaller(getLookAndFeelType()); + protected ThemeGrouper getThemeGrouper() { + return new MacThemeGrouper(); } - } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MacThemeGrouper.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MacThemeGrouper.java new file mode 100644 index 0000000000..19eae9bd69 --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MacThemeGrouper.java @@ -0,0 +1,38 @@ +/* ### + * 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.laf; + +import generic.theme.GThemeValueMap; + +/** + * Adds specialized groupings unique to the Mac LookAndFeel + */ +public class MacThemeGrouper extends ThemeGrouper { + + @Override + public void group(GThemeValueMap values) { + // @formatter:off + defineCustomColorGroup(values, "color.mac.disabled.fg", "Menu.disabledForeground"); + defineCustomColorGroup(values, "color.mac.button.select", "Button.select"); + defineCustomColorGroup(values, "color.mac.menu.select","Menu.selectionBackground"); + defineCustomColorGroup(values, "color.mac.seletion.inactive.bg","List.selectionInactiveBackground");//d4d4d4 + + defineCustomFontGroup(values, "font.mac.smallFont", "IconButton.font"); + // @formatter:on + super.group(values); + } + +} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MetalLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MetalLookAndFeelManager.java new file mode 100644 index 0000000000..3b7e172517 --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MetalLookAndFeelManager.java @@ -0,0 +1,25 @@ +/* ### + * 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.laf; + +import generic.theme.LafType; + +public class MetalLookAndFeelManager extends LookAndFeelManager { + + public MetalLookAndFeelManager() { + super(LafType.METAL); + } +} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MotifLookAndFeelInstaller.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MotifLookAndFeelInstaller.java deleted file mode 100644 index ed5dd610ca..0000000000 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MotifLookAndFeelInstaller.java +++ /dev/null @@ -1,41 +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.laf; - -import generic.theme.LafType; - -public class MotifLookAndFeelInstaller extends LookAndFeelInstaller { - - public MotifLookAndFeelInstaller() { - super(LafType.MOTIF); - } - - @Override - protected void fixupLookAndFeelIssues() { - // - // The Motif LaF does not bind copy/paste/cut to Control-C/V/X by default. Rather, they - // only use the COPY/PASTE/CUT keys. The other LaFs bind both shortcuts. - // - - // these prefixes are for text components - String[] UIPrefixValues = - { "TextField", "FormattedTextField", "TextArea", "TextPane", "EditorPane" }; - - setKeyBinding("COPY", "ctrl C", UIPrefixValues); - setKeyBinding("PASTE", "ctrl V", UIPrefixValues); - setKeyBinding("CUT", "ctrl X", UIPrefixValues); - } -} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MotifLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MotifLookAndFeelManager.java index ea92e1bb09..ff5afdca9c 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MotifLookAndFeelManager.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MotifLookAndFeelManager.java @@ -15,17 +15,42 @@ */ package generic.theme.laf; +import generic.theme.ColorValue; import generic.theme.LafType; +/** + * Motif {@link LookAndFeelManager}. Specialized so that it can return the Motif installer + */ public class MotifLookAndFeelManager extends LookAndFeelManager { public MotifLookAndFeelManager() { super(LafType.MOTIF); + // establish system color to LookAndFeel colors + systemToLafMap.addColor(new ColorValue(SYSTEM_APP_BACKGROUND_COLOR_ID, "control")); + systemToLafMap.addColor(new ColorValue(SYSTEM_WIDGET_BACKGROUND_COLOR_ID, "window")); + systemToLafMap.addColor(new ColorValue(SYSTEM_TOOLTIP_BACKGROUND_COLOR_ID, "info")); + systemToLafMap.addColor(new ColorValue(SYSTEM_BORDER_COLOR_ID, "activeCaptionBorder")); } @Override - protected LookAndFeelInstaller getLookAndFeelInstaller() { - return new MotifLookAndFeelInstaller(); + protected void fixupLookAndFeelIssues() { + // + // The Motif LaF does not bind copy/paste/cut to Control-C/V/X by default. Rather, they + // only use the COPY/PASTE/CUT keys. The other LaFs bind both shortcuts. + // + + // these prefixes are for text components + String[] UIPrefixValues = + { "TextField", "FormattedTextField", "TextArea", "TextPane", "EditorPane" }; + + setKeyBinding("COPY", "ctrl C", UIPrefixValues); + setKeyBinding("PASTE", "ctrl V", UIPrefixValues); + setKeyBinding("CUT", "ctrl X", UIPrefixValues); + } + + @Override + protected ThemeGrouper getThemeGrouper() { + return new MotifThemeGrouper(); } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MotifThemeGrouper.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MotifThemeGrouper.java new file mode 100644 index 0000000000..cbe2ead176 --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MotifThemeGrouper.java @@ -0,0 +1,35 @@ +/* ### + * 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.laf; + +import generic.theme.GThemeValueMap; + +/** + * Adds specialized groupings unique to the Motif LookAndFeel + */ +public class MotifThemeGrouper extends ThemeGrouper { + public MotifThemeGrouper() { + + } + + @Override + public void group(GThemeValueMap values) { + defineCustomFontGroup(values, "font.monospaced", "Spinner.font"); + + super.group(values); + } + +} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelInstaller.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelInstaller.java deleted file mode 100644 index 78c5b36ad6..0000000000 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelInstaller.java +++ /dev/null @@ -1,54 +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.laf; - -import java.awt.Dimension; - -import javax.swing.*; - -import generic.theme.*; - -public class NimbusLookAndFeelInstaller extends LookAndFeelInstaller { - - public NimbusLookAndFeelInstaller() { - super(LafType.NIMBUS); - } - - @Override - protected void installLookAndFeel() throws UnsupportedLookAndFeelException { - UIManager.setLookAndFeel(new GNimbusLookAndFeel()); - } - - @Override - protected void installJavaDefaults() { - // 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 - protected void fixupLookAndFeelIssues() { - super.fixupLookAndFeelIssues(); - - // fix scroll bar grabber disappearing. See https://bugs.openjdk.java.net/browse/JDK-8134828 - // This fix looks like it should not cause harm even if the bug is fixed on the jdk side. - UIDefaults defaults = UIManager.getDefaults(); - defaults.put("ScrollBar.minimumThumbSize", new Dimension(30, 30)); - - // (see NimbusDefaults for key values that can be changed here) - } -} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelManager.java index a9dc9545d2..7f17b9bdeb 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelManager.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelManager.java @@ -15,7 +15,7 @@ */ package generic.theme.laf; -import java.awt.Font; +import java.awt.Dimension; import java.util.Set; import javax.swing.*; @@ -23,15 +23,18 @@ import javax.swing.*; import generic.theme.*; import ghidra.util.exception.AssertException; +/** + * Nimbus {@link LookAndFeelManager}. Specialized so that it can return the Nimbus installer and + * do specialized updating when icons or fonts change. Basically, needs to re-install a new + * instance of the Nimbus LookAndFeel each time a font or icon changes + */ public class NimbusLookAndFeelManager extends LookAndFeelManager { public NimbusLookAndFeelManager() { super(LafType.NIMBUS); - } - @Override - protected LookAndFeelInstaller getLookAndFeelInstaller() { - return new NimbusLookAndFeelInstaller(); + // establish system color specific to Nimbus + systemToLafMap.addColor(new ColorValue(SYSTEM_BORDER_COLOR_ID, "nimbusBorder")); } @Override @@ -42,7 +45,7 @@ public class NimbusLookAndFeelManager extends LookAndFeelManager { } @Override - public void fontsChanged(Set affectedJavaIds, Font newFont) { + public void fontsChanged(Set affectedJavaIds) { if (!affectedJavaIds.isEmpty()) { reinstallNimubus(); updateComponentFonts(affectedJavaIds); @@ -62,6 +65,7 @@ public class NimbusLookAndFeelManager extends LookAndFeelManager { private void reinstallNimubus() { try { UIManager.setLookAndFeel(new GNimbusLookAndFeel() { + @Override protected GThemeValueMap extractJavaDefaults(UIDefaults defaults) { return Gui.getJavaDefaults(); } @@ -73,4 +77,32 @@ public class NimbusLookAndFeelManager extends LookAndFeelManager { updateComponentUis(); } + @Override + protected void doInstallLookAndFeel() throws UnsupportedLookAndFeelException { + UIManager.setLookAndFeel(new GNimbusLookAndFeel()); + } + + @Override + protected GThemeValueMap extractJavaDefaults() { + // The GNimbusLookAndFeel already extracted the java defaults and installed them in the Gui + return Gui.getJavaDefaults(); + } + + @Override + protected ThemeGrouper getThemeGrouper() { + return new NimbusThemeGrouper(); + } + + @Override + protected void fixupLookAndFeelIssues() { + super.fixupLookAndFeelIssues(); + + // fix scroll bar grabber disappearing. See https://bugs.openjdk.java.net/browse/JDK-8134828 + // This fix looks like it should not cause harm even if the bug is fixed on the jdk side. + UIDefaults defaults = UIManager.getDefaults(); + defaults.put("ScrollBar.minimumThumbSize", new Dimension(30, 30)); + + // (see NimbusDefaults for key values that can be changed here) + } + } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusThemeGrouper.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusThemeGrouper.java new file mode 100644 index 0000000000..e6c62ccdc7 --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusThemeGrouper.java @@ -0,0 +1,43 @@ +/* ### + * 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.laf; + +import generic.theme.GThemeValueMap; + +/** + * Adds specialized groupings unique to the Nimbus LookAndFeel + */ +public class NimbusThemeGrouper extends ThemeGrouper { + public NimbusThemeGrouper() { + // Nimbus defines a new type of button + buttonGroup.addComponents("ArrowButton"); + + // Nimbus defines some other color sources + colorSourceProperties.add("nimbusFocus"); + colorSourceProperties.add("nimbusOrange"); + colorSourceProperties.add("nimbusBorder"); + + } + + @Override + public void group(GThemeValueMap values) { + defineCustomColorGroup(values, "color.nimbus.text.alt", "Menu.foreground"); + defineCustomFontGroup(values, "font.titledborder", "TitledBorder.font"); + + super.group(values); + } + +} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/ThemeGrouper.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/ThemeGrouper.java new file mode 100644 index 0000000000..0c3259e648 --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/ThemeGrouper.java @@ -0,0 +1,433 @@ +/* ### + * 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.laf; + +import java.awt.Color; +import java.awt.Font; +import java.util.*; + +import javax.swing.LookAndFeel; +import javax.swing.plaf.basic.BasicLookAndFeel; + +import generic.theme.*; + +/** + * Organizes UIDefaults color and font properties into groups so that every property doesn't + * have its own direct value. The idea is that users can affect many properties that have the + * same value by just changing the value for the group. For colors, the {@link LookAndFeel}s + * organize the properties internally and this class attempts to restore that organization + * as much as possible by using the values defined in the {@link BasicLookAndFeel} such as + * "control", "window", "controlShadlow", etc. The fonts don't appear to have any such internal + * organization, so we created our own groups and used an exemplar property to initialize each + * group source value. Then whenever the font matched a group source value, the color is replace + * with an indirect reference to the group source property value. + *

+ * This class is sometimes sub-classed for a particular {@link LookAndFeel}. The subclass can + * create new groups and mappings that are unique to that LookAndFeel. + */ +public class ThemeGrouper { + private static String DEFAULT_FONT_GROUP_ID = "font.default"; + private static String BUTTON_FONT_GROUP_ID = "font.button"; + private static String TEXT_FONT_GROUP_ID = "font.text"; + private static String WIDGET_FONT_GROUP_ID = "font.widget"; + private static String COMPONENT_FONT_GROUP_ID = "font.component"; + private static String MENU_FONT_GROUP_ID = "font.menu"; + private static String MENU_ACCELERATOR_FONT_GROUP_ID = "font.menu.accelerator"; + + static List DEFAULT_FONT_SOURCE_PROPERTIES = List.of( + DEFAULT_FONT_GROUP_ID, + COMPONENT_FONT_GROUP_ID, + WIDGET_FONT_GROUP_ID, + TEXT_FONT_GROUP_ID, + BUTTON_FONT_GROUP_ID, + MENU_FONT_GROUP_ID, + MENU_ACCELERATOR_FONT_GROUP_ID); + + // The list of color properties (defined in BasicLookAndFeel) that are used to populate + // other component specific colors. + // The order is important. If any have the same color value, the one higher in the list is used. + // Individual groups (buttons, menus, etc.) may define a different order that is more specific + // to that group + public static List DEFAULT_COLOR_SOURCE_PROPERTIES = List.of( + "control", + "window", + "activeCaption", + "activeCaptionBorder", + "activeCaptionText", + "controlDkShadow", + "controlHighlight", + "controlLtHighlight", + "controlShadow", + "controlText", + "desktp", + "inactiveCaption", + "inactiveCaptionBorder", + "inactiveCaptionText", + "info", + "infoText", + "menu", + "menuText", + "scrollbar", + "scrollBarTrack", + "text", + "textHighlight", + "textHighlightText", + "textInactiveText", + "textText", + "windowBorder", + "windowText"); + + private static final String[] BUTTON_GROUP = { + "Button", + "ToggleButton", + "RadioButton", + "CheckBox" + }; + private static final String[] MENU_GROUP = { + "Menu", + "MenuBar", + "MenuItem", + "PopupMenu", + "RadioButtonMenuItem", + "CheckBoxMenuItem" + }; + private static final String[] TEXT_GROUP = { + "TextField", + "FormattedTextField", + "PasswordField", + "TextArea", + "TextPane", + "EditorPane" + }; + private static final String[] WIDGET_GROUP = { + "FileChooser", + "ColorChooser", + "ComboBox", + "List", + "Table", + "Tree" + }; + private static final String[] COMPONENT_GROUP = { + "Desktop", + "Panel", + "InternalFrame", + "Label", + "OptionPane", + "ProgressBar", + "Separator", + "ScrollBar", + "ScrollPane", + "Viewport", + "Slider", + "Spinner", + "SplitPane", + "TabbedPane", + "TableHeader", + "TitledBorder", + "ToolBar", + "ToolTip" + }; + + // often the many of the various group source ids have the same color value. To try and group + // properties as defined in BasicLookAndFeel, the preferred source ids are + // defined for each group. These will be tried first, but if a match isn't found among the + // preferred sources, then all the sources will be searched for a match + private static final String[] BUTTON_PREFERRED_SOURCES = { + "control", + "controlText", + "controlShadow", + "controlDkShadow", + "controlHighlight", + "controlLtHighlight" + }; + private static final String[] MENU_PREFERRED_SOURCES = { + "menu", + "menuText", + "textHighlightText", + "textHighlight", + "controlShadow", + "controlDkShadow", + "controlHighlight", + "controlLtHighlight" + }; + private static final String[] TEXT_PREFERRED_SOURCES = { + "window", + "text", + "textText", + "textInactiveText", + "textHighlight", + "textHighlightText", + "controlShadow", + "controlDkShadow", + "controlHighlight", + "controlLtHighlight" + }; + private static final String[] WIDGET_PREFERRED_SOURCES = { + "window", + "textText", + "textHighlight", + "textHighlightText", + "control", + "controlShadow", + "controlDkShadow", + "controlHighlight", + "controlLtHighlight" + }; + private static final String[] COMPONENT_PREFERRED_SOURCES = { + "control", + "controlText", + "controlShadow", + "controlDkShadow", + "controlHighlight", + "controlLtHighlight", + "textText", + "textHighlight" + }; + + protected List colorSourceProperties; + protected List fontSourceProperties; + protected Set groups; + protected PropertyGroup buttonGroup = new PropertyGroup(BUTTON_GROUP, BUTTON_PREFERRED_SOURCES); + protected PropertyGroup menuGroup = new PropertyGroup(MENU_GROUP, MENU_PREFERRED_SOURCES); + protected PropertyGroup widgetGroup = new PropertyGroup(WIDGET_GROUP, WIDGET_PREFERRED_SOURCES); + protected PropertyGroup textGroup = new PropertyGroup(TEXT_GROUP, TEXT_PREFERRED_SOURCES); + protected PropertyGroup componentGroup = + new PropertyGroup(COMPONENT_GROUP, COMPONENT_PREFERRED_SOURCES); + + public ThemeGrouper() { + colorSourceProperties = new ArrayList<>(DEFAULT_COLOR_SOURCE_PROPERTIES); + fontSourceProperties = new ArrayList<>(DEFAULT_FONT_SOURCE_PROPERTIES); + groups = getPropertyGroups(); + } + + /** + * Replaces direct property values in the given GThemeValueMap with indirect references + * using the values from match source ids. + * @param values the values to search and replace source matches + */ + public void group(GThemeValueMap values) { + initialize(values); + Map groupMap = buildGroupMap(values); + groupColors(values, groupMap); + groupFonts(values, groupMap); + } + + protected void defineCustomColorGroup(GThemeValueMap values, String customGroupName, + String exemplarComponentId) { + + colorSourceProperties.add(customGroupName); + ColorValue colorValue = values.getColor(exemplarComponentId); + if (colorValue != null) { + Color color = colorValue.get(values); + values.addColor(new ColorValue(customGroupName, color)); + } + } + + protected void defineCustomFontGroup(GThemeValueMap values, String customGroupName, + String exemplarComponentId) { + fontSourceProperties.add(customGroupName); + FontValue fontValue = values.getFont(exemplarComponentId); + if (fontValue != null) { + Font font = fontValue.get(values); + values.addFont(new FontValue(customGroupName, font)); + } + } + + private void groupColors(GThemeValueMap values, Map groupMap) { + Set skip = new HashSet<>(colorSourceProperties); // we don't want to map sources + Map defaultColorMapping = buildColorToSourceMap(values); + // try to map each color property to a source property (e.g., Button.background -> control) + for (ColorValue colorValue : values.getColors()) { + String id = colorValue.getId(); + if (colorValue.isIndirect() || skip.contains(id)) { + continue; + } + PropertyGroup group = groupMap.get(getComponentName(id)); + int rgb = colorValue.getRawValue().getRGB(); + String sourceProperty = group == null ? null : group.getSourceProperty(rgb); + if (sourceProperty == null) { + sourceProperty = defaultColorMapping.get(rgb); + } + + if (sourceProperty != null) { + values.addColor(new ColorValue(id, sourceProperty)); + } + } + } + + private void groupFonts(GThemeValueMap values, Map groupMap) { + Set skip = new HashSet<>(fontSourceProperties); // we don't want to map sources + Map defaultFontMapping = buildFontToSourceMap(values); + + // try to map each color property to a source property (e.g., Button.background -> control) + for (FontValue fontValue : values.getFonts()) { + String id = fontValue.getId(); + if (fontValue.isIndirect() || skip.contains(id)) { + continue; + } + Font font = fontValue.getRawValue(); + PropertyGroup group = groupMap.get(getComponentName(id)); + String sourceProperty = group == null ? null : group.getSourceProperty(font); + if (sourceProperty == null) { + sourceProperty = defaultFontMapping.get(font); + } + if (sourceProperty != null) { + values.addFont(new FontValue(id, sourceProperty)); + } + } + } + + private void initialize(GThemeValueMap values) { + // initialized default font to the Panel's font + FontValue defaultFontValue = values.getFont("Panel.font"); + if (defaultFontValue != null) { + values.addFont(new FontValue(DEFAULT_FONT_GROUP_ID, defaultFontValue.get(values))); + } + + // initialize the default group fonts to a font from an exemplar property in that group + initializeFontGroup(buttonGroup, BUTTON_FONT_GROUP_ID, "Button.font", values); + initializeFontGroup(textGroup, TEXT_FONT_GROUP_ID, "TextField.font", values); + initializeFontGroup(widgetGroup, WIDGET_FONT_GROUP_ID, "Table.font", values); + initializeFontGroup(componentGroup, COMPONENT_FONT_GROUP_ID, "Panel.font", values); + initializeFontGroup(menuGroup, MENU_FONT_GROUP_ID, "Menu.font", values); + initializeFontGroup(menuGroup, MENU_ACCELERATOR_FONT_GROUP_ID, "Menu.acceleratorFont", + values); + } + + private void initializeFontGroup(PropertyGroup group, String fontGroupId, String exemplarId, + GThemeValueMap values) { + FontValue fontValue = values.getFont(exemplarId); + if (fontValue != null) { + Font font = fontValue.getRawValue(); + values.addFont(new FontValue(fontGroupId, font)); + group.addFontMapping(font, fontGroupId); + } + } + + private Set getPropertyGroups() { + Set set = new HashSet<>(); + set.add(buttonGroup); + set.add(menuGroup); + set.add(textGroup); + set.add(widgetGroup); + set.add(componentGroup); + return set; + } + + private Map buildGroupMap(GThemeValueMap values) { + Map map = new HashMap<>(); + for (PropertyGroup group : groups) { + group.initialize(values); + group.populateGroupMap(map); + } + return map; + } + + private String getComponentName(String id) { + int dotIndex = id.indexOf("."); + if (dotIndex < 0) { + return id; + } + return id.substring(0, dotIndex); + } + + private Map buildColorToSourceMap(GThemeValueMap values) { + Map colorMapping = new HashMap<>(); + ArrayList reversed = new ArrayList<>(colorSourceProperties); + Collections.reverse(reversed); + // go through in reverse order so that values at the top of the list have precedence + // if multiple propertyBases have the save value. + for (String propertyBase : reversed) { + ColorValue colorValue = values.getColor(propertyBase); + if (colorValue != null) { + Color color = colorValue.get(values); + colorMapping.put(color.getRGB(), propertyBase); + } + } + return colorMapping; + } + + private Map buildFontToSourceMap(GThemeValueMap values) { + Map fontMapping = new HashMap<>(); + ArrayList reversed = new ArrayList<>(fontSourceProperties); + Collections.reverse(reversed); + // go through in reverse order so that values at the top of the list have precedence + // if multiple propertyBases have the save value. + for (String propertyBase : reversed) { + FontValue fontValue = values.getFont(propertyBase); + if (fontValue != null) { + Font font = fontValue.get(values); + fontMapping.put(font, propertyBase); + } + } + return fontMapping; + } + + static class PropertyGroup { + private Set groupComponents = new HashSet<>(); + private List preferredPropertyColorSources = new ArrayList<>(); + private Map colorMapping; + private Map fontMapping = new HashMap<>(); + + PropertyGroup(String[] components, String[] perferredSources) { + addComponents(components); + addPreferredColorSources(perferredSources); + } + + String getSourceProperty(int rgb) { + return colorMapping.get(rgb); + } + + String getSourceProperty(Font font) { + return fontMapping.get(font); + } + + void populateGroupMap(Map groupMap) { + for (String component : groupComponents) { + groupMap.put(component, this); + } + } + + void addPreferredColorSources(String... preferedColorSources) { + this.preferredPropertyColorSources.addAll(Arrays.asList(preferedColorSources)); + } + + void addComponents(String... properties) { + groupComponents.addAll(Arrays.asList(properties)); + } + + void addFontMapping(Font font, String sourceId) { + fontMapping.put(font, sourceId); + } + + private Map initialize(GThemeValueMap values) { + colorMapping = new HashMap<>(); + ArrayList reversed = new ArrayList<>(preferredPropertyColorSources); + Collections.reverse(reversed); + // go through in reverse order so that values at the top of the list have precedence + // if multiple propertyBases have the save value. + for (String propertyBase : reversed) { + ColorValue colorValue = values.getColor(propertyBase); + if (colorValue != null) { + Color color = colorValue.get(values); + colorMapping.put(color.getRGB(), propertyBase); + } + } + return colorMapping; + } + } + +} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/WindowsClassicLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/WindowsClassicLookAndFeelManager.java new file mode 100644 index 0000000000..7fcc199657 --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/WindowsClassicLookAndFeelManager.java @@ -0,0 +1,25 @@ +/* ### + * 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.laf; + +import generic.theme.LafType; + +public class WindowsClassicLookAndFeelManager extends LookAndFeelManager { + + public WindowsClassicLookAndFeelManager() { + super(LafType.WINDOWS_CLASSIC); + } +} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GenericLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/WindowsLookAndFeelManager.java similarity index 62% rename from Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GenericLookAndFeelManager.java rename to Ghidra/Framework/Generic/src/main/java/generic/theme/laf/WindowsLookAndFeelManager.java index 411a5fc835..43d9423a0c 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GenericLookAndFeelManager.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/WindowsLookAndFeelManager.java @@ -17,19 +17,10 @@ package generic.theme.laf; import generic.theme.LafType; -/** - * Generic {@link LookAndFeelManager} for lookAndFeels that do not require any special handling - * to install or update - */ -public class GenericLookAndFeelManager extends LookAndFeelManager { +public class WindowsLookAndFeelManager extends LookAndFeelManager { - public GenericLookAndFeelManager(LafType laf) { - super(laf); - } - - @Override - protected LookAndFeelInstaller getLookAndFeelInstaller() { - return new LookAndFeelInstaller(getLookAndFeelType()); + public WindowsLookAndFeelManager() { + super(LafType.WINDOWS); } } diff --git a/Ghidra/Framework/Generic/src/main/java/resources/icons/UrlImageIcon.java b/Ghidra/Framework/Generic/src/main/java/resources/icons/UrlImageIcon.java index 0bc49b0812..6904c7c547 100644 --- a/Ghidra/Framework/Generic/src/main/java/resources/icons/UrlImageIcon.java +++ b/Ghidra/Framework/Generic/src/main/java/resources/icons/UrlImageIcon.java @@ -96,4 +96,25 @@ public class UrlImageIcon extends LazyImageIcon { } return null; } + + @Override + public int hashCode() { + return imageUrl.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + UrlImageIcon other = (UrlImageIcon) obj; + return Objects.equals(imageUrl, other.imageUrl); + } + } diff --git a/Ghidra/Framework/Generic/src/test/java/generic/theme/ColorValueTest.java b/Ghidra/Framework/Generic/src/test/java/generic/theme/ColorValueTest.java index b0fe8eb847..ce0ef48ba0 100644 --- a/Ghidra/Framework/Generic/src/test/java/generic/theme/ColorValueTest.java +++ b/Ghidra/Framework/Generic/src/test/java/generic/theme/ColorValueTest.java @@ -22,6 +22,8 @@ import java.awt.Color; import org.junit.Before; import org.junit.Test; +import ghidra.util.WebColors; + public class ColorValueTest { private GThemeValueMap values; @@ -85,17 +87,33 @@ public class ColorValueTest { } @Test - public void testToExernalId() { + public void testGetSerializationString() { ColorValue value = new ColorValue("color.test", Color.BLUE); - assertEquals("color.test", value.toExternalId("color.test")); - assertEquals("[color]foo.bar", value.toExternalId("foo.bar")); + assertEquals("color.test = #0000ff // Blue", value.getSerializationString()); + + value = new ColorValue("foo.bar", Color.BLUE); + assertEquals("[color]foo.bar = #0000ff // Blue", value.getSerializationString()); + + value = new ColorValue("color.test", "xyz.abc"); + assertEquals("color.test = [color]xyz.abc", value.getSerializationString()); } @Test - public void testFromExternalId() { - ColorValue value = new ColorValue("color.test", Color.BLUE); - assertEquals("color.test", value.fromExternalId("color.test")); - assertEquals("foo.bar", value.fromExternalId("[color]foo.bar")); + public void testParse() { + ColorValue value = ColorValue.parse("color.test", "#0000ff"); + assertEquals("color.test", value.getId()); + assertEquals(WebColors.BLUE, value.getRawValue()); + assertEquals(null, value.getReferenceId()); + + value = ColorValue.parse("[color]foo.bar", "#0000ff"); + assertEquals("foo.bar", value.getId()); + assertEquals(WebColors.BLUE, value.getRawValue()); + assertEquals(null, value.getReferenceId()); + + value = ColorValue.parse("color.test", "[color]xyz.abc"); + assertEquals("color.test", value.getId()); + assertEquals(null, value.getRawValue()); + assertEquals("xyz.abc", value.getReferenceId()); } @Test diff --git a/Ghidra/Framework/Generic/src/test/java/generic/theme/FontModifierTest.java b/Ghidra/Framework/Generic/src/test/java/generic/theme/FontModifierTest.java new file mode 100644 index 0000000000..3b6528b003 --- /dev/null +++ b/Ghidra/Framework/Generic/src/test/java/generic/theme/FontModifierTest.java @@ -0,0 +1,191 @@ +/* ### + * 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 static org.junit.Assert.*; + +import java.awt.Font; + +import org.junit.Test; + +public class FontModifierTest { + private Font baseFont = new Font("Dialog", Font.PLAIN, 12); + + @Test + public void testNoModifiers() { + assertNull(FontModifier.parse("")); + } + + @Test + public void testSizeModifier() { + FontModifier modifier = FontModifier.parse("[6]"); + assertNotNull(modifier); + Font newFont = modifier.modify(baseFont); + assertEquals(6, newFont.getSize()); + assertEquals(baseFont.getName(), newFont.getName()); + assertEquals(baseFont.getStyle(), newFont.getStyle()); + } + + @Test + public void testStyleModifierPlain() { + FontModifier modifier = FontModifier.parse("[plain]"); + assertNotNull(modifier); + Font newFont = modifier.modify(baseFont); + assertEquals(Font.PLAIN, newFont.getStyle()); + assertEquals(baseFont.getName(), newFont.getName()); + assertEquals(baseFont.getSize(), newFont.getSize()); + } + + @Test + public void testStyleModifierBold() { + FontModifier modifier = FontModifier.parse("[bold]"); + assertNotNull(modifier); + Font newFont = modifier.modify(baseFont); + assertEquals(Font.BOLD, newFont.getStyle()); + assertEquals(baseFont.getName(), newFont.getName()); + assertEquals(baseFont.getSize(), newFont.getSize()); + } + + @Test + public void testStyleModifierItalic() { + FontModifier modifier = FontModifier.parse("[ITALIC]"); + assertNotNull(modifier); + Font newFont = modifier.modify(baseFont); + assertEquals(Font.ITALIC, newFont.getStyle()); + assertEquals(baseFont.getName(), newFont.getName()); + assertEquals(baseFont.getSize(), newFont.getSize()); + } + + @Test + public void testStyleModifierBoldItalic() { + FontModifier modifier = FontModifier.parse("[BOLDitalic]"); + assertNotNull(modifier); + Font newFont = modifier.modify(baseFont); + assertEquals(Font.ITALIC | Font.BOLD, newFont.getStyle()); + assertEquals(baseFont.getName(), newFont.getName()); + assertEquals(baseFont.getSize(), newFont.getSize()); + } + + @Test + public void testStyleModifierBoldItalic2() { + FontModifier modifier = FontModifier.parse("[BOLD][italic]"); + assertNotNull(modifier); + Font newFont = modifier.modify(baseFont); + assertEquals(Font.ITALIC | Font.BOLD, newFont.getStyle()); + assertEquals(baseFont.getName(), newFont.getName()); + assertEquals(baseFont.getSize(), newFont.getSize()); + } + + @Test + public void testFamilyModification() { + FontModifier modifier = FontModifier.parse("[monospaced]"); + assertNotNull(modifier); + Font newFont = modifier.modify(baseFont); + assertEquals("Monospaced", newFont.getFamily()); + assertEquals(baseFont.getStyle(), newFont.getStyle()); + assertEquals(baseFont.getSize(), newFont.getSize()); + } + + @Test + public void testSizeAndStyleModification() { + FontModifier modifier = FontModifier.parse("[16][bold]"); + assertNotNull(modifier); + Font newFont = modifier.modify(baseFont); + assertEquals(baseFont.getName(), newFont.getFamily()); + assertEquals(Font.BOLD, newFont.getStyle()); + assertEquals(16, newFont.getSize()); + + } + + @Test + public void testFamilyModificationMultiple() { + try { + FontModifier.parse("[monospaced][courier]"); + fail("Expecected Exception"); + } + catch (IllegalStateException e) { + // expected + } + } + + @Test + public void testStyleModifierIncompatableStyles() { + try { + FontModifier.parse("[plain][italic]"); + fail("Expected IllegalStateException"); + } + catch (IllegalStateException e) { + // expected + } + } + + @Test + public void testInvalidModifierString() { + try { + FontModifier.parse("asdfasf"); + fail("Expected IllegalArgumentExcption"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testInvalidModifierString2() { + try { + FontModifier.parse("[12]aa[13]"); + fail("Expected IllegalArgumentExcption"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testInvalidModifierString3() { + try { + FontModifier.parse("[12]aa13]"); + fail("Expected IllegalArgumentExcption"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testInvalidModifierString4() { + try { + FontModifier.parse("[12][plain]sz"); + fail("Expected IllegalArgumentExcption"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testGetSerializationString() { + //@formatter:off + assertEquals("[12]", new FontModifier(null, null, 12).getSerializationString()); + assertEquals("[plain]", new FontModifier(null, Font.PLAIN, null).getSerializationString()); + assertEquals("[bold]", new FontModifier(null, Font.BOLD, null).getSerializationString()); + assertEquals("[italic]", new FontModifier(null, Font.ITALIC, null).getSerializationString()); + assertEquals("[bold][italic]", new FontModifier(null, Font.BOLD | Font.ITALIC, null).getSerializationString()); + assertEquals("[Monospaced]",new FontModifier("Monospaced", null, null).getSerializationString()); + assertEquals("[Monospaced][12][plain]",new FontModifier("Monospaced", Font.PLAIN, 12).getSerializationString()); + //@formatter:on + } +} diff --git a/Ghidra/Framework/Generic/src/test/java/generic/theme/FontValueTest.java b/Ghidra/Framework/Generic/src/test/java/generic/theme/FontValueTest.java index c353544e39..d601e11826 100644 --- a/Ghidra/Framework/Generic/src/test/java/generic/theme/FontValueTest.java +++ b/Ghidra/Framework/Generic/src/test/java/generic/theme/FontValueTest.java @@ -22,11 +22,8 @@ import java.awt.Font; import org.junit.Before; import org.junit.Test; -import generic.theme.FontValue; -import generic.theme.GThemeValueMap; - public class FontValueTest { - private static Font FONT = new Font("Dialog", 12, Font.PLAIN); + private static Font FONT = new Font("Dialog", Font.PLAIN, 12); private GThemeValueMap values; @Before @@ -88,17 +85,33 @@ public class FontValueTest { } @Test - public void testToExernalId() { + public void testGetSerializationString() { FontValue value = new FontValue("font.test", FONT); - assertEquals("font.test", value.toExternalId("font.test")); - assertEquals("[font]foo.bar", value.toExternalId("foo.bar")); + assertEquals("font.test = Dialog-PLAIN-12", value.getSerializationString()); + + value = new FontValue("foo.bar", FONT); + assertEquals("[font]foo.bar = Dialog-PLAIN-12", value.getSerializationString()); + + value = new FontValue("font.test", "xyz.abc"); + assertEquals("font.test = [font]xyz.abc", value.getSerializationString()); } @Test - public void testFromExternalId() { - FontValue value = new FontValue("font.test", FONT); - assertEquals("font.test", value.fromExternalId("font.test")); - assertEquals("foo.bar", value.fromExternalId("[font]foo.bar")); + public void testParse() { + FontValue value = FontValue.parse("font.test", "Dialog-PLAIN-12"); + assertEquals("font.test", value.getId()); + assertEquals(FONT, value.getRawValue()); + assertEquals(null, value.getReferenceId()); + + value = FontValue.parse("[font]foo.bar", "Dialog-PLAIN-12"); + assertEquals("foo.bar", value.getId()); + assertEquals(FONT, value.getRawValue()); + assertEquals(null, value.getReferenceId()); + + value = FontValue.parse("font.test", "[font]xyz.abc"); + assertEquals("font.test", value.getId()); + assertEquals(null, value.getRawValue()); + assertEquals("xyz.abc", value.getReferenceId()); } @Test diff --git a/Ghidra/Framework/Generic/src/test/java/generic/theme/GuiTest.java b/Ghidra/Framework/Generic/src/test/java/generic/theme/GuiTest.java index 298d52daf9..535860d49b 100644 --- a/Ghidra/Framework/Generic/src/test/java/generic/theme/GuiTest.java +++ b/Ghidra/Framework/Generic/src/test/java/generic/theme/GuiTest.java @@ -15,90 +15,345 @@ */ package generic.theme; +import static ghidra.util.WebColors.*; +import static org.junit.Assert.*; + +import java.awt.Color; +import java.awt.Font; +import java.net.URL; +import java.util.*; + +import javax.swing.Icon; +import javax.swing.plaf.UIResource; + import org.junit.Before; +import org.junit.Test; + +import generic.theme.builtin.*; +import resources.ResourceManager; +import resources.icons.UrlImageIcon; public class GuiTest { - private GThemeValueMap darkValues = new GThemeValueMap(); + private Font FONT = new Font("Dialog", Font.PLAIN, 13); + private Font SMALL_FONT = new Font("Dialog", Font.PLAIN, 4); + + private Icon ICON1 = ResourceManager.loadImage("images/exec.png"); + private Icon ICON2 = ResourceManager.loadImage("images/flag.png"); + + private GThemeValueMap defaultValues = new GThemeValueMap(); + private GThemeValueMap darkDefaultValues = new GThemeValueMap(); + private Set themes; + private GTheme METAL_THEME = new MetalTheme(); + private GTheme NIMBUS_THEME = new NimbusTheme(); + private GTheme WINDOWS_THEME = new WindowsTheme(); + private GTheme MAC_THEME = new MacTheme(); @Before public void setUp() { - Gui.setPropertiesLoader(new ThemePropertiesLoader() { + themes = new HashSet<>(); + themes.add(METAL_THEME); + themes.add(NIMBUS_THEME); + themes.add(WINDOWS_THEME); + themes.add(MAC_THEME); + + defaultValues.addColor(new ColorValue("color.test.bg", WHITE)); + defaultValues.addColor(new ColorValue("color.test.fg", RED)); + + defaultValues.addFont(new FontValue("font.test.foo", FONT)); + defaultValues.addIcon(new IconValue("icon.test.foo", ICON1)); + + darkDefaultValues.addColor(new ColorValue("color.test.bg", BLACK)); + darkDefaultValues.addColor(new ColorValue("color.test.fg", BLUE)); + + Gui.setThemePreferenceManager(new ThemePreferenceManager() { + + public GTheme getTheme() { + return new MetalTheme(); + } + @Override - public void load() { + public void saveThemeToPreferences(GTheme theme) { + // do nothing + } + }); + + Gui.setPropertiesLoader(new ThemeFileLoader() { + + @Override + public void loadThemeDefaultFiles() { // do nothing } + @Override + public Collection loadThemeFiles() { + return new HashSet<>(themes); + } + + @Override + public GThemeValueMap getDefaults() { + return defaultValues; + } + @Override public GThemeValueMap getDarkDefaults() { - return darkValues; + return darkDefaultValues; } }); + Gui.initialize(); } -// @Test -// public void testRegisteredColorBeforeAndAfterGuiInit() { -// Gui.registerColor("test.before", Color.RED); -// Gui.initialize(); -// Gui.registerColor("test.after", Color.BLUE); -// -// assertEquals(Color.RED, Gui.getColor("test.before")); -// assertEquals(Color.BLUE, Gui.getColor("test.after")); -// } + @Test + public void testDarkThemeColorOverride() { + GColor gColor = new GColor("color.test.bg"); -// @Test -// public void testThemeColorOverride() { -// Gui.initialize(); -// String id = "color.test.bg"; -// Gui.registerColor(id, Color.RED); -// assertEquals(Color.RED, Gui.getColor(id)); -// -// GTheme theme = new GTheme("Test"); -// theme.setColor(id, Color.BLUE); -// Gui.setTheme(theme); -// -// assertEquals(Color.BLUE, Gui.getColor(id)); -// -// Gui.setTheme(new GTheme("Test2")); -// assertEquals(Color.RED, Gui.getColor(id)); -// -// } + assertColor(WHITE, gColor); + Gui.setTheme(new GTheme("Test", LafType.FLAT_DARK, true)); + assertEquals(BLACK, gColor); -// @Test -// public void testDarkOverride() { -// String id = "color.test.bg"; -// // simulate registered dark color from theme property file -// darkValues.addColor(new ColorValue(id, Color.BLACK)); -// -// Gui.registerColor(id, Color.RED); -// Gui.initialize(); -// -// assertEquals(Color.RED, Gui.getColor(id)); -// -// GTheme theme = new GTheme("Dark Test", "System", true); -// Gui.setTheme(theme); -// -// assertEquals(Color.BLACK, Gui.getColor(id)); -// } + Gui.setTheme(new GTheme("Test2")); + assertEquals(WHITE, gColor); -// @Test -// public void testAliasOverride() { -// String id = "color.test.bg"; -// //simulate alias defined -// List aliases = Arrays.asList("Menu.background"); -// aliasMap.put(id, aliases); -// -// Gui.registerColor(id, Color.RED); -// Gui.initialize(); -// Color menuColor = UIManager.getColor("Menu.background"); -// assertNotEquals(menuColor, Color.RED); -// assertEquals(menuColor, Gui.getColor(id)); -// } + } -// private void assertEqual(Color a, GColor b) { -// if (a.getRGB() != b.getRGB()) { -// fail("Expected: " + a.toString() + " but was: " + b.toString()); -// } -// } + @Test + public void testThemeColorOverride() { + GColor gColor = new GColor("color.test.bg"); + GTheme theme = new GTheme("Test"); + theme.setColor("color.test.bg", GREEN); + + assertColor(WHITE, gColor); + Gui.setTheme(theme); + assertEquals(GREEN, gColor); + + Gui.setTheme(new GTheme("Test2")); + assertEquals(WHITE, gColor); + } + + @Test + public void testThemeFontOverride() { + assertEquals(FONT, Gui.getFont("font.test.foo")); + + GTheme theme = new GTheme("Test"); + theme.setFont("font.test.foo", SMALL_FONT); + Gui.setTheme(theme); + + assertEquals(SMALL_FONT, Gui.getFont("font.test.foo")); + + Gui.setTheme(new GTheme("Test2")); + assertEquals(FONT, Gui.getFont("font.test.foo")); + } + + @Test + public void testThemeIconOverride() { + GIcon gIcon = new GIcon("icon.test.foo"); + + GTheme theme = new GTheme("Test"); + theme.setIcon("icon.test.foo", ICON2); + + assertIcon(ICON1, gIcon); + Gui.setTheme(theme); + assertIcon(ICON2, gIcon); + + Gui.setTheme(new GTheme("Test2")); + assertIcon(ICON1, gIcon); + } + + @Test + public void testReloadGhidraDefaults() { + GColor gColor = new GColor("color.test.bg"); + assertColor(WHITE, gColor); + + defaultValues.addColor(new ColorValue("color.test.bg", YELLOW)); + Gui.reloadApplicationDefaults(); + assertEquals(YELLOW, gColor); + } + + @Test + public void testRestoreThemeValues() { + GColor gColor = new GColor("color.test.bg"); + assertColor(WHITE, gColor); + + Gui.setColor("color.test.bg", PURPLE); + assertColor(PURPLE, gColor); + + Gui.restoreThemeValues(); + assertEquals(WHITE, gColor); + + } + + @Test + public void testGetAllThemes() { + assertEquals(themes, Gui.getAllThemes()); + } + + @Test + public void testAddTheme() { + GTheme newTheme = new GTheme("Test"); + + Set allThemes = Gui.getAllThemes(); + assertEquals(themes.size(), allThemes.size()); + assertFalse(allThemes.contains(newTheme)); + + Gui.addTheme(newTheme); + allThemes = Gui.getAllThemes(); + assertTrue(allThemes.contains(newTheme)); + } + + @Test + public void testDeleteTheme() { + GTheme newTheme = new GTheme("Test"); + Set allThemes = Gui.getAllThemes(); + assertFalse(allThemes.contains(newTheme)); + + Gui.addTheme(newTheme); + allThemes = Gui.getAllThemes(); + assertTrue(allThemes.contains(newTheme)); + + Gui.deleteTheme(newTheme); + allThemes = Gui.getAllThemes(); + assertFalse(allThemes.contains(newTheme)); + } + + @Test + public void testGetSupportedThemes() { + Set supportedThemes = Gui.getSupportedThemes(); + // since we put mac specific and windows specific themes, they can't all be here + // regardless of the current platform + assertTrue(supportedThemes.size() < themes.size()); + for (GTheme gTheme : supportedThemes) { + assertTrue(gTheme.hasSupportedLookAndFeel()); + } + } + + @Test + public void testGetLookAndFeelType() { + LafType lookAndFeelType = Gui.getLookAndFeelType(); + // in the test setup, we defaulted to the MetalLookAndFeel + assertEquals(LafType.METAL, lookAndFeelType); + } + + @Test + public void testGetActiveTheme() { + GTheme activeTheme = Gui.getActiveTheme(); + assertEquals(METAL_THEME, activeTheme); + } + + @Test + public void testGetThemeByName() { + GTheme theme = Gui.getTheme("Nimbus Theme"); + assertEquals(NIMBUS_THEME, theme); + } + + @Test + public void testGetAllValues() { + GThemeValueMap allValues = Gui.getAllValues(); + assertEquals(WHITE, allValues.getColor("color.test.bg").getRawValue()); + + Gui.setColor("color.test.bg", PURPLE); + + allValues = Gui.getAllValues(); + assertEquals(PURPLE, allValues.getColor("color.test.bg").getRawValue()); + + } + + @Test + public void testGetNonDefaultValues() { + // should be empty if we haven't changed any themeValues + GThemeValueMap nonDefaultValues = Gui.getNonDefaultValues(); + assertTrue(nonDefaultValues.isEmpty()); + + // change some values and see that they show up in the nonDefaultValues + Gui.setColor("color.test.bg", RED); + Gui.setFont("font.test.foo", SMALL_FONT); + Gui.setIcon("icon.test.foo", ICON2); + // also add in a totally new value + Gui.setColor("color.test.xxx", GREEN); + + nonDefaultValues = Gui.getNonDefaultValues(); + assertEquals(4, nonDefaultValues.size()); + assertEquals(RED, nonDefaultValues.getColor("color.test.bg").getRawValue()); + assertEquals(GREEN, nonDefaultValues.getColor("color.test.xxx").getRawValue()); + assertEquals(SMALL_FONT, nonDefaultValues.getFont("font.test.foo").getRawValue()); + assertEquals(ICON2, nonDefaultValues.getIcon("icon.test.foo").getRawValue()); + } + + @Test + public void testGetColor() { + assertEquals(WHITE, Gui.getColor("color.test.bg")); + } + + @Test + public void testGetFont() { + assertEquals(FONT, Gui.getFont("font.test.foo")); + } + + @Test + public void testGetIcon() { + assertEquals(ICON1, Gui.getIcon("icon.test.foo")); + } + + @Test + public void testGetColorWithUnresolvedId() { + assertEquals(CYAN, Gui.getColor("color.badid", false)); + } + + @Test + public void testGetIconWithUnresolvedId() { + assertEquals(ResourceManager.getDefaultIcon(), Gui.getIcon("icon.badid", false)); + } + + @Test + public void testGetFontWithUnresolvedId() { + assertEquals(Gui.DEFAULT_FONT, Gui.getFont("font.badid", false)); + } + + @Test + public void testGetGColorUiResource() { + Color color = Gui.getGColorUiResource("color.test.bg"); + assertTrue(color instanceof UIResource); + + // make sure there is only one instance for an id; + Color color2 = Gui.getGColorUiResource("color.test.bg"); + assertTrue(color == color2); + } + + @Test + public void testGetGIconUiResource() { + Icon icon = Gui.getGIconUiResource("icon.test.foo"); + assertTrue(icon instanceof UIResource); + + // make sure there is only one instance for an id; + Icon gIcon2 = Gui.getGIconUiResource("icon.test.foo"); + assertTrue(icon == gIcon2); + } + + @Test + public void testGetApplicationLightDefaults() { + assertEquals(defaultValues, Gui.getApplicationLightDefaults()); + } + + @Test + public void testGetApplicationDarkDefaults() { + // dark defaults are a combination of standard defalts overlayed with dark defaults + GThemeValueMap expected = new GThemeValueMap(); + expected.load(defaultValues); + expected.load(darkDefaultValues); + assertEquals(expected, Gui.getApplicationDarkDefaults()); + } + + private void assertColor(Color color, GColor gColor) { + if (color.getRGB() != gColor.getRGB()) { + fail("RGB values don't match! Expected " + color + " but got " + gColor); + } + } + + private void assertIcon(Icon icon, GIcon gIcon) { + URL url = ((UrlImageIcon) icon).getUrl(); + URL gUrl = gIcon.getUrl(); + if (!url.equals(gUrl)) { + fail("Icons don't match. Expected " + url + ", but got " + gUrl); + } + } } diff --git a/Ghidra/Framework/Generic/src/test/java/generic/theme/IconValueTest.java b/Ghidra/Framework/Generic/src/test/java/generic/theme/IconValueTest.java index b5fb3d4a2a..220db3a084 100644 --- a/Ghidra/Framework/Generic/src/test/java/generic/theme/IconValueTest.java +++ b/Ghidra/Framework/Generic/src/test/java/generic/theme/IconValueTest.java @@ -87,17 +87,33 @@ public class IconValueTest { } @Test - public void testToExernalId() { + public void testGetSerializationString() { IconValue value = new IconValue("icon.test", ICON1); - assertEquals("icon.test", value.toExternalId("icon.test")); - assertEquals("[icon]foo.bar", value.toExternalId("foo.bar")); + assertEquals("icon.test = images/core.png", value.getSerializationString()); + + value = new IconValue("foo.bar", ICON1); + assertEquals("[icon]foo.bar = images/core.png", value.getSerializationString()); + + value = new IconValue("icon.test", "xyz.abc"); + assertEquals("icon.test = [icon]xyz.abc", value.getSerializationString()); } @Test - public void testFromExternalId() { - IconValue value = new IconValue("icon.test", ICON1); - assertEquals("icon.test", value.fromExternalId("icon.test")); - assertEquals("foo.bar", value.fromExternalId("[icon]foo.bar")); + public void testParse() { + IconValue value = IconValue.parse("icon.test", "images/core.png"); + assertEquals("icon.test", value.getId()); + assertEquals(ICON1, value.getRawValue()); + assertEquals(null, value.getReferenceId()); + + value = IconValue.parse("[icon]foo.bar", "images/core.png"); + assertEquals("foo.bar", value.getId()); + assertEquals(ICON1, value.getRawValue()); + assertEquals(null, value.getReferenceId()); + + value = IconValue.parse("icon.test", "[icon]xyz.abc"); + assertEquals("icon.test", value.getId()); + assertEquals(null, value.getRawValue()); + assertEquals("xyz.abc", value.getReferenceId()); } @Test diff --git a/Ghidra/Framework/Generic/src/test/java/generic/theme/ThemePropertyFileReaderTest.java b/Ghidra/Framework/Generic/src/test/java/generic/theme/ThemePropertyFileReaderTest.java index 42a332d453..5e06c62d06 100644 --- a/Ghidra/Framework/Generic/src/test/java/generic/theme/ThemePropertyFileReaderTest.java +++ b/Ghidra/Framework/Generic/src/test/java/generic/theme/ThemePropertyFileReaderTest.java @@ -24,6 +24,8 @@ import java.io.IOException; import java.io.StringReader; import java.util.List; +import javax.swing.Icon; + import org.junit.Test; import resources.ResourceManager; @@ -44,6 +46,7 @@ public class ThemePropertyFileReaderTest { " color.b.7 = color.b.1", // ref " font.a.8 = dialog-PLAIN-14", " font.a.9 = font.a.8", + " font.a.b = (font.a.8[20][BOLD])", " icon.a.10 = core.png", " icon.a.11 = icon.a.10", ""))); @@ -51,21 +54,22 @@ public class ThemePropertyFileReaderTest { Color halfAlphaRed = new Color(0x80ff0000, true); GThemeValueMap values = reader.getDefaultValues(); - assertEquals(11, values.size()); + assertEquals(12, values.size()); - assertEquals(WHITE, getColorOrRef(values, "color.b.1")); - assertEquals(RED, getColorOrRef(values, "color.b.2")); - assertEquals(GREEN, getColorOrRef(values, "color.b.3")); - assertEquals(halfAlphaRed, getColorOrRef(values, "color.b.4")); - assertEquals(BLUE, getColorOrRef(values, "color.b.5")); - assertEquals(halfAlphaRed, getColorOrRef(values, "color.b.6")); - assertEquals("color.b.1", getColorOrRef(values, "color.b.7")); + assertEquals(WHITE, getColor(values, "color.b.1")); + assertEquals(RED, getColor(values, "color.b.2")); + assertEquals(GREEN, getColor(values, "color.b.3")); + assertEquals(halfAlphaRed, getColor(values, "color.b.4")); + assertEquals(BLUE, getColor(values, "color.b.5")); + assertEquals(halfAlphaRed, getColor(values, "color.b.6")); + assertEquals(WHITE, getColor(values, "color.b.7")); - assertEquals(new Font("dialog", Font.PLAIN, 14), getFontOrRef(values, "font.a.8")); - assertEquals("font.a.8", getFontOrRef(values, "font.a.9")); + assertEquals(new Font("dialog", Font.PLAIN, 14), getFont(values, "font.a.8")); + assertEquals(new Font("dialog", Font.PLAIN, 14), getFont(values, "font.a.9")); + assertEquals(new Font("dialog", Font.BOLD, 20), getFont(values, "font.a.b")); - assertEquals(ResourceManager.loadImage("core.png"), getIconOrRef(values, "icon.a.10")); - assertEquals("icon.a.10", getIconOrRef(values, "icon.a.11")); + assertEquals(ResourceManager.loadImage("core.png"), getIcon(values, "icon.a.10")); + assertEquals(ResourceManager.loadImage("core.png"), getIcon(values, "icon.a.11")); } @@ -88,13 +92,13 @@ public class ThemePropertyFileReaderTest { GThemeValueMap values = reader.getDarkDefaultValues(); assertEquals(7, values.size()); - assertEquals(WHITE, getColorOrRef(values, "color.b.1")); - assertEquals(RED, getColorOrRef(values, "color.b.2")); - assertEquals(GREEN, getColorOrRef(values, "color.b.3")); - assertEquals(halfAlphaRed, getColorOrRef(values, "color.b.4")); - assertEquals(BLUE, getColorOrRef(values, "color.b.5")); - assertEquals(halfAlphaRed, getColorOrRef(values, "color.b.6")); - assertEquals("color.b.1", getColorOrRef(values, "color.b.7")); + assertEquals(WHITE, getColor(values, "color.b.1")); + assertEquals(RED, getColor(values, "color.b.2")); + assertEquals(GREEN, getColor(values, "color.b.3")); + assertEquals(halfAlphaRed, getColor(values, "color.b.4")); + assertEquals(BLUE, getColor(values, "color.b.5")); + assertEquals(halfAlphaRed, getColor(values, "color.b.6")); + assertEquals(WHITE, getColor(values, "color.b.7")); } @Test @@ -116,10 +120,10 @@ public class ThemePropertyFileReaderTest { GThemeValueMap darkValues = reader.getDarkDefaultValues(); assertEquals(2, values.size()); - assertEquals(WHITE, getColorOrRef(values, "color.b.1")); - assertEquals(RED, getColorOrRef(values, "color.b.2")); - assertEquals(BLACK, getColorOrRef(darkValues, "color.b.1")); - assertEquals(BLUE, getColorOrRef(darkValues, "color.b.2")); + assertEquals(WHITE, getColor(values, "color.b.1")); + assertEquals(RED, getColor(values, "color.b.2")); + assertEquals(BLACK, getColor(darkValues, "color.b.1")); + assertEquals(BLUE, getColor(darkValues, "color.b.2")); } @Test @@ -133,32 +137,51 @@ public class ThemePropertyFileReaderTest { //@formatter:on List errors = reader.getErrors(); assertEquals(1, errors.size()); - assertEquals("Error parsing file \"test\" at line: 3, Could not parse Color: sdfsdf", + assertEquals("Error parsing file \"test\" at line: 3, Could not parse Color value: sdfsdf", errors.get(0)); + } + + @Test + public void testParseFontError() throws IOException { + //@formatter:off + ThemePropertyFileReader reader = new ThemePropertyFileReader("test", new StringReader(String.join("\n", + "[Defaults]", + " font.b.1 = Dialog-PLAIN-14", + " font.b.2 = Dialog-PLANE-13", + " font.b.3 = Dialog-BOLD-ITALIC", + ""))); + //@formatter:on + List errors = reader.getErrors(); + assertEquals(2, errors.size()); } - private Object getColorOrRef(GThemeValueMap values, String id) { + @Test + public void testParseFontModiferError() throws IOException { + //@formatter:off + ThemePropertyFileReader reader = new ThemePropertyFileReader("test", new StringReader(String.join("\n", + "[Defaults]", + " font.b.1 = Dialog-PLAIN-14", + " font.b.2 = (font.b.1[)", + ""))); + //@formatter:on + List errors = reader.getErrors(); + assertEquals(1, errors.size()); + + } + + private Color getColor(GThemeValueMap values, String id) { ColorValue color = values.getColor(id); - if (color.getReferenceId() != null) { - return color.getReferenceId(); - } - return color.getRawValue(); + return color.get(values); } - private Object getFontOrRef(GThemeValueMap values, String id) { + private Font getFont(GThemeValueMap values, String id) { FontValue font = values.getFont(id); - if (font.getReferenceId() != null) { - return font.getReferenceId(); - } - return font.getRawValue(); + return font.get(values); } - private Object getIconOrRef(GThemeValueMap values, String id) { + private Icon getIcon(GThemeValueMap values, String id) { IconValue icon = values.getIcon(id); - if (icon.getReferenceId() != null) { - return icon.getReferenceId(); - } - return icon.getRawValue(); + return icon.get(values); } } diff --git a/Ghidra/Framework/Generic/src/test/java/generic/theme/laf/ThemeGrouperTest.java b/Ghidra/Framework/Generic/src/test/java/generic/theme/laf/ThemeGrouperTest.java new file mode 100644 index 0000000000..6dfe6f4e23 --- /dev/null +++ b/Ghidra/Framework/Generic/src/test/java/generic/theme/laf/ThemeGrouperTest.java @@ -0,0 +1,104 @@ +/* ### + * 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.laf; + +import static ghidra.util.WebColors.*; +import static org.junit.Assert.*; + +import java.awt.Color; +import java.awt.Font; + +import org.junit.Before; +import org.junit.Test; + +import generic.theme.*; + +public class ThemeGrouperTest { + private static Font FONT1 = new Font("Dialog", Font.PLAIN, 12); + private static Font FONT2 = new Font("Dialog", Font.BOLD, 16); + + private ThemeGrouper grouper; + private GThemeValueMap values; + + @Before + public void setUp() { + grouper = new ThemeGrouper(); + values = new GThemeValueMap(); + } + + @Test + public void testGroupColorUsingPreferredSources() { + initColor("control", RED); + initColor("menu", RED); + initColor("Menu.background", RED); + grouper.group(values); + + ColorValue colorValue = values.getColor("Menu.background"); + assertEquals("menu", colorValue.getReferenceId()); + } + + @Test + public void testGroupColorUsingNonPreferredSourceWhenPreferredDoesntMatch() { + initColor("control", RED); + initColor("menu", BLUE); + initColor("Menu.background", RED); + grouper.group(values); + + ColorValue colorValue = values.getColor("Menu.background"); + assertEquals("control", colorValue.getReferenceId()); + } + + @Test + public void testGroupFontUsingPreferredSources() { + initFont("Button.font", FONT1); + initFont("RadioButton.font", FONT1); + initFont("Menu.font", FONT1); + initFont("MenuItem.font", FONT1); + grouper.group(values); + + assertEquals(FONT1, values.getFont("font.button").getRawValue()); + assertEquals(FONT1, values.getFont("font.menu").getRawValue()); + assertEquals("font.button", values.getFont("Button.font").getReferenceId()); + assertEquals("font.button", values.getFont("RadioButton.font").getReferenceId()); + assertEquals("font.menu", values.getFont("Menu.font").getReferenceId()); + assertEquals("font.menu", values.getFont("MenuItem.font").getReferenceId()); + } + + @Test + public void testGroupFontUsingNonPreferredSourceWhenPreferredDoesntMatch() { + initFont("Button.font", FONT1); + initFont("RadioButton.font", FONT1); + initFont("Menu.font", FONT2); + initFont("MenuItem.font", FONT1); + grouper.group(values); + + assertEquals(FONT1, values.getFont("font.button").getRawValue()); + assertEquals(FONT2, values.getFont("font.menu").getRawValue()); + assertEquals("font.button", values.getFont("Button.font").getReferenceId()); + assertEquals("font.button", values.getFont("RadioButton.font").getReferenceId()); + assertEquals("font.menu", values.getFont("Menu.font").getReferenceId()); + assertEquals("font.button", values.getFont("MenuItem.font").getReferenceId()); + } + + private void initColor(String id, Color color) { + values.addColor(new ColorValue(id, color)); + } + + private void initFont(String id, Font font) { + values.addFont(new FontValue(id, font)); + } + +}