GP-1981 Created font modifiers for referred fonts

and added property grouping for UIDefault colors and fonts. Also added a
lot of javadocs and unit tests. Also introduced concept of "system"
values so we can map to java LookAndFeel values

GP-1981 added "system" properties
This commit is contained in:
ghidragon
2022-08-22 17:39:58 -04:00
parent 15e633c239
commit 780d4b7671
55 changed files with 2719 additions and 1485 deletions
@@ -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
@@ -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
@@ -37,7 +37,7 @@ public class ColorValueEditor extends ThemeValueEditor<Color> {
@Override
protected Color getRawValue(String id) {
return Gui.getRawColor(id);
return Gui.getColor(id);
}
@Override
@@ -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;
@@ -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<Icon> {
@Override
protected Icon getRawValue(String id) {
return Gui.getRawIcon(id, true);
return Gui.getIcon(id, true);
}
@Override
@@ -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 {
@@ -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<ColorValue, Object> {
private List<ColorValue> colors;
private GThemeValueMap currentValues;
@@ -46,12 +49,19 @@ public class ThemeColorTableModel extends GDynamicColumnTableModel<ColorValue, O
load();
}
/**
* Reloads the just the current values shown in the table. Called whenever a color changes.
*/
public void reloadCurrent() {
currentValues = Gui.getAllValues();
colors = currentValues.getColors();
fireTableDataChanged();
}
/**
* Reloads all the current values and all the default values in the table. Called when the
* theme changes or the application defaults have been forced to reload.
*/
public void reloadAll() {
load();
fireTableDataChanged();
@@ -62,8 +72,8 @@ public class ThemeColorTableModel extends GDynamicColumnTableModel<ColorValue, O
colors = currentValues.getColors();
themeValues = new GThemeValueMap(currentValues);
defaultValues = Gui.getDefaults();
lightDefaultValues = Gui.getGhidraLightDefaults();
darkDefaultValues = Gui.getGhidraDarkDefaults();
lightDefaultValues = Gui.getApplicationLightDefaults();
darkDefaultValues = Gui.getApplicationDarkDefaults();
}
@@ -192,12 +202,12 @@ public class ThemeColorTableModel extends GDynamicColumnTableModel<ColorValue, O
if (resolvedColor == null) {
return "<No Value>";
}
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 + ")";
}
@@ -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() {
@@ -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<FontValue, Object> {
private List<FontValue> fonts;
private GThemeValueMap currentValues;
@@ -42,6 +45,24 @@ public class ThemeFontTableModel extends GDynamicColumnTableModel<FontValue, Obj
load();
}
/**
* Reloads the just the current values shown in the table. Called whenever a font changes.
*/
public void reloadCurrent() {
currentValues = Gui.getAllValues();
fonts = currentValues.getFonts();
fireTableDataChanged();
}
/**
* Reloads all the current values and all the default values in the table. Called when the
* theme changes or the application defaults have been forced to reload.
*/
public void reloadAll() {
load();
fireTableDataChanged();
}
private void load() {
currentValues = Gui.getAllValues();
fonts = currentValues.getFonts();
@@ -74,17 +95,16 @@ public class ThemeFontTableModel extends GDynamicColumnTableModel<FontValue, Obj
return null;
}
private String getValueText(ResolvedFont resolvedFont) {
if (resolvedFont == null) {
private String getValueText(FontValue fontValue) {
if (fontValue == null) {
return "<No Value>";
}
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<FontValue, String, Object> {
@@ -106,7 +126,7 @@ public class ThemeFontTableModel extends GDynamicColumnTableModel<FontValue, Obj
}
}
class FontValueColumn extends AbstractDynamicTableColumn<FontValue, ResolvedFont, Object> {
class FontValueColumn extends AbstractDynamicTableColumn<FontValue, FontValue, Object> {
private ThemeFontRenderer renderer;
private String name;
private Supplier<GThemeValueMap> valueSupplier;
@@ -123,24 +143,19 @@ public class ThemeFontTableModel extends GDynamicColumnTableModel<FontValue, Obj
}
@Override
public ResolvedFont getValue(FontValue fontValue, Settings settings, Object data,
public FontValue getValue(FontValue fontValue, Settings settings, Object data,
ServiceProvider provider) throws IllegalArgumentException {
GThemeValueMap valueMap = valueSupplier.get();
String id = fontValue.getId();
FontValue value = valueMap.getFont(id);
if (value == null) {
return null;
}
Font font = value.get(valueMap);
return new ResolvedFont(id, value.getReferenceId(), font);
return valueMap.getFont(id);
}
@Override
public GColumnRenderer<ResolvedFont> getColumnRenderer() {
public GColumnRenderer<FontValue> getColumnRenderer() {
return renderer;
}
public Comparator<ResolvedFont> getComparator() {
public Comparator<FontValue> getComparator() {
return (v1, v2) -> {
if (v1 == null && v2 == null) {
return 0;
@@ -162,43 +177,32 @@ public class ThemeFontTableModel extends GDynamicColumnTableModel<FontValue, Obj
}
private class ThemeFontRenderer extends AbstractGColumnRenderer<ResolvedFont> {
public ThemeFontRenderer() {
setFont(new Font("Monospaced", Font.PLAIN, 12));
}
private class ThemeFontRenderer extends AbstractGColumnRenderer<FontValue> {
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();
}
}
@@ -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<IconValue, Object> {
private List<IconValue> icons;
private GThemeValueMap currentValues;
@@ -43,6 +46,24 @@ public class ThemeIconTableModel extends GDynamicColumnTableModel<IconValue, Obj
load();
}
/**
* Reloads the just the current values shown in the table. Called whenever an icon changes.
*/
public void reloadCurrent() {
currentValues = Gui.getAllValues();
icons = currentValues.getIcons();
fireTableDataChanged();
}
/**
* Reloads all the current values and all the default values in the table. Called when the
* theme changes or the application defaults have been forced to reload.
*/
public void reloadAll() {
load();
fireTableDataChanged();
}
private void load() {
currentValues = Gui.getAllValues();
icons = currentValues.getIcons();
@@ -211,20 +232,5 @@ public class ThemeIconTableModel extends GDynamicColumnTableModel<IconValue, Obj
}
record ResolvedIcon(String id, String refId, Icon icon) {
/**/}
public void reloadCurrent() {
currentValues = Gui.getAllValues();
icons = currentValues.getIcons();
fireTableDataChanged();
}
public void reloadAll() {
load();
fireTableDataChanged();
}
record ResolvedIcon(String id, String refId, Icon icon) {/**/}
}
@@ -31,8 +31,18 @@ import ghidra.framework.Application;
import ghidra.util.Msg;
import ghidra.util.filechooser.ExtensionFileFilter;
/**
* Some common methods related to saving themes. These are invoked from various places to handle
* what to do if a change is made that would result in loosing theme changes.
*/
public class ThemeUtils {
/**
* Asks the user if they want to save the current theme changes. If they answer yes, it
* will handle several use cases such as whether it gets saved to a new file or
* overwrites an existing file.
* @return true if the operation was not cancelled
*/
public static boolean askToSaveThemeChanges() {
if (Gui.hasThemeChanges()) {
int result = OptionDialog.showYesNoCancelDialog(null, "Save Theme Changes?",
@@ -43,14 +53,15 @@ public class ThemeUtils {
if (result == OptionDialog.YES_OPTION) {
return ThemeUtils.saveThemeChanges();
}
Gui.reloadGhidraDefaults();
Gui.reloadApplicationDefaults();
}
return true;
}
/**
* Saves all current theme changes
* @return true if the operation was not cancelled.
* Saves all current theme changes. Handles several use cases such as requesting a new theme
* name and asking to overwrite an existing file.
* @return true if the operation was not cancelled
*/
public static boolean saveThemeChanges() {
GTheme activeTheme = Gui.getActiveTheme();
@@ -65,12 +76,18 @@ public class ThemeUtils {
return saveCurrentValues(name);
}
/**
* Resets the theme to the default, handling the case where the current theme has changes.
*/
public static void resetThemeToDefault() {
if (askToSaveThemeChanges()) {
Gui.setTheme(Gui.getDefaultTheme());
}
}
/**
* Imports a theme. Handles the case where there are existing changes to the current theme.
*/
public static void importTheme() {
GhidraFileChooser chooser = new GhidraFileChooser(null);
chooser.setTitle("Choose Theme File");
@@ -110,6 +127,10 @@ public class ThemeUtils {
}
/**
* Exports a theme, prompting the user to pick an file. Also handles dealing with any
* existing changes to the current theme.
*/
public static void exportTheme() {
if (!ThemeUtils.askToSaveThemeChanges()) {
return;
@@ -134,6 +155,9 @@ public class ThemeUtils {
DockingWindowManager.showDialog(dialog);
}
/**
* Prompts for and deletes a selected theme.
*/
public static void deleteTheme() {
List<GTheme> 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();
}
@@ -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<Section> readSections(LineNumberReader reader) throws IOException {
@@ -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<Color> {
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<Color> {
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<Color> {
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<Color> {
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;
}
}
@@ -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<String> 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<String> getModifierPieces(String value) {
if (!MODIFIER_PATTERN.matcher(value).matches()) {
throw new IllegalArgumentException("Invalid font modifier string");
}
StringTokenizer tokenizer = new StringTokenizer(value, "[]");
List<String> list = new ArrayList<>();
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken().trim();
if (!token.isBlank()) {
list.add(token);
}
}
return list;
}
}
@@ -29,6 +29,7 @@ public class FontValue extends ThemeValue<Font> {
static final String FONT_ID_PREFIX = "font.";
public static final Font LAST_RESORT_DEFAULT = new Font("monospaced", Font.PLAIN, 12);
private static final String EXTERNAL_PREFIX = "[font]";
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<Font> {
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<Font> {
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";
}
}
@@ -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);
@@ -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;
}
@@ -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
}
}
@@ -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);
}
}
@@ -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<GTheme> 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<String, GColorUIResource> 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<String> 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<GTheme> set = new HashSet<>();
set.addAll(findDiscoverableThemes());
set.addAll(loadThemesFromFiles());
set.addAll(themeFileLoader.loadThemeFiles());
allThemes = set;
}
}
private static Collection<GTheme> loadThemesFromFiles() {
List<File> 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<GTheme> 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<DiscoverableGTheme> 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;
}
}
@@ -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<Icon> {
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<Icon> {
}
return null;
}
private String getValueOutput() {
if (referenceId != null) {
return toExternalId(referenceId);
}
return iconToString(value);
}
}
@@ -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);
}
@@ -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<ResourceFile> themeDefaultFiles =
Application.findFilesByExtensionInApplication(".theme.properties");
public void loadThemeDefaultFiles() {
defaults.clear();
darkDefaults.clear();
List<ResourceFile> 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<GTheme> loadThemeFiles() {
List<File> 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<GTheme> 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;
}
}
@@ -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();
}
}
@@ -28,13 +28,13 @@ import ghidra.util.Msg;
* @param <T> the base type this ThemeValue works on (i.e., Colors, Fonts, Icons)
*/
public abstract class ThemeValue<T> implements Comparable<ThemeValue<T>> {
private final String id;
private final T value;
private final String refId;
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<T> implements Comparable<ThemeValue<T>> {
* a value
*/
public String getReferenceId() {
return refId;
return referenceId;
}
/**
@@ -82,7 +82,7 @@ public abstract class ThemeValue<T> implements Comparable<ThemeValue<T>> {
Set<String> visitedKeys = new HashSet<>();
visitedKeys.add(id);
ThemeValue<T> parent = getReferredValue(values, refId);
ThemeValue<T> parent = getReferredValue(values, referenceId);
// loop resolving indirect references
while (parent != null) {
@@ -90,11 +90,11 @@ public abstract class ThemeValue<T> implements Comparable<ThemeValue<T>> {
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<T> implements Comparable<ThemeValue<T>> {
* @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<String> visitedKeys = new HashSet<>();
visitedKeys.add(id);
ThemeValue<T> parent = getReferredValue(values, refId);
ThemeValue<T> 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<T> getReferredValue(GThemeValueMap preferredValues,
String referenceId);
protected abstract ThemeValue<T> getReferredValue(GThemeValueMap preferredValues,
String refId);
@Override
public int compareTo(ThemeValue<T> o) {
@@ -175,7 +173,7 @@ public abstract class ThemeValue<T> implements Comparable<ThemeValue<T>> {
@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<T> implements Comparable<ThemeValue<T>> {
}
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 + ")";
}
}
@@ -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 {
@@ -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<String, String> 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<ColorValue> 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;
}
}
@@ -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<String, String> 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<FontValue> 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)));
}
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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<String> 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<String> 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<String> 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;
}
}
@@ -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());
// }
//
}
@@ -25,10 +25,4 @@ public class GtkLookAndFeelManager extends LookAndFeelManager {
public GtkLookAndFeelManager() {
super(LafType.GTK);
}
@Override
protected LookAndFeelInstaller getLookAndFeelInstaller() {
return new GtkLookAndFeelInstaller();
}
}
@@ -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 <code>LookAndFeel</code>
* 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
* <code>lnf.isSupportedLookAndFeel()</code> 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 <code>LookAndFeel</code>
* 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
* <code>lnf.isSupportedLookAndFeel()</code> 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<String> 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<Entry<Object, Object>> set = defaults.entrySet();
Iterator<Entry<Object, Object>> iterator = set.iterator();
while (iterator.hasNext()) {
Entry<Object, Object> 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<String> getLookAndFeelIdsForType(UIDefaults defaults, Class<?> clazz) {
List<String> colorKeys = new ArrayList<>();
List<Object> 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;
}
}
@@ -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<String, ComponentFontRegistry> 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<String> 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<String> changedJavaFontIds, Font newFont) {
public void fontsChanged(Set<String> 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 <code>LookAndFeel</code>
* 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
* <code>lnf.isSupportedLookAndFeel()</code> 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<String> 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<Entry<Object, Object>> set = defaults.entrySet();
Iterator<Entry<Object, Object>> iterator = set.iterator();
while (iterator.hasNext()) {
Entry<Object, Object> 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<String> getLookAndFeelIdsForType(UIDefaults defaults, Class<?> clazz) {
List<String> colorKeys = new ArrayList<>();
List<Object> 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;
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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)
}
}
@@ -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<String> affectedJavaIds, Font newFont) {
public void fontsChanged(Set<String> 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)
}
}
@@ -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);
}
}
@@ -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.
* <p>
* 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<String> 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<String> 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<String> colorSourceProperties;
protected List<String> fontSourceProperties;
protected Set<PropertyGroup> 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<String, PropertyGroup> 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<String, PropertyGroup> groupMap) {
Set<String> skip = new HashSet<>(colorSourceProperties); // we don't want to map sources
Map<Integer, String> 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<String, PropertyGroup> groupMap) {
Set<String> skip = new HashSet<>(fontSourceProperties); // we don't want to map sources
Map<Font, String> 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<PropertyGroup> getPropertyGroups() {
Set<PropertyGroup> set = new HashSet<>();
set.add(buttonGroup);
set.add(menuGroup);
set.add(textGroup);
set.add(widgetGroup);
set.add(componentGroup);
return set;
}
private Map<String, PropertyGroup> buildGroupMap(GThemeValueMap values) {
Map<String, PropertyGroup> 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<Integer, String> buildColorToSourceMap(GThemeValueMap values) {
Map<Integer, String> colorMapping = new HashMap<>();
ArrayList<String> 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<Font, String> buildFontToSourceMap(GThemeValueMap values) {
Map<Font, String> fontMapping = new HashMap<>();
ArrayList<String> 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<String> groupComponents = new HashSet<>();
private List<String> preferredPropertyColorSources = new ArrayList<>();
private Map<Integer, String> colorMapping;
private Map<Font, String> 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<String, PropertyGroup> 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<Integer, String> initialize(GThemeValueMap values) {
colorMapping = new HashMap<>();
ArrayList<String> 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;
}
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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
@@ -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
}
}

Some files were not shown because too many files have changed in this diff Show More