GP-1981 Added IconModifiers for sizing,translating, disabling, and

creating overlayed icons in the theme files. Also some VT icon
externalization
This commit is contained in:
ghidragon 2022-09-21 16:28:28 -04:00
parent b2d16ab982
commit dd31ff47a2
80 changed files with 1555 additions and 525 deletions

View file

@ -16,6 +16,7 @@
package generic.theme;
import java.io.*;
import java.text.ParseException;
import java.util.*;
import ghidra.util.Msg;
@ -84,7 +85,7 @@ public abstract class AbstractThemeReader {
}
else if (IconValue.isIconKey(key)) {
if (!GTheme.JAVA_ICON.equals(value)) {
valueMap.addIcon(parseIconProperty(key, value));
valueMap.addIcon(parseIconProperty(key, value, lineNumber));
}
}
else {
@ -93,8 +94,14 @@ public abstract class AbstractThemeReader {
}
}
private IconValue parseIconProperty(String key, String value) {
return IconValue.parse(key, value);
private IconValue parseIconProperty(String key, String value, int lineNumber) {
try {
return IconValue.parse(key, value);
}
catch (ParseException e) {
error(lineNumber, "Could not parse Icon value: " + value + "because " + e.getMessage());
}
return null;
}
private FontValue parseFontProperty(String key, String value, int lineNumber) {

View file

@ -16,9 +16,15 @@
package generic.theme;
import java.awt.Font;
import java.util.*;
import java.text.ParseException;
import java.util.List;
import java.util.regex.Pattern;
/**
* Class that can transform one font into another. For example if want a font that is the same
* basic font as some other font, but is just a different size,style, or family, you use a
* FontModifier
*/
public class FontModifier {
private static final Pattern MODIFIER_PATTERN = Pattern.compile("(\\[([a-zA-Z]+|[0-9]+)\\])*");
@ -26,16 +32,26 @@ public class FontModifier {
private Integer style;
private Integer size;
public FontModifier() {
private FontModifier() {
}
/**
* Creates a new FontModifier that can change a given font by one or more font properties.
* @param family if non-null, modifies a font to use this family
* @param style if non-null, modifies a font to use this style
* @param size if non-null, modifies a font to be this size
*/
public FontModifier(String family, Integer style, Integer size) {
this.family = family;
this.style = style;
this.size = size;
}
/**
* Sets the family for modifying a font
* @param newFamily the font family to use when modifying fonts
*/
public void addFamilyModifier(String newFamily) {
if (family != null) {
throw new IllegalStateException("Multiple font family names specified");
@ -43,6 +59,10 @@ public class FontModifier {
this.family = newFamily;
}
/**
* Sets the font size modifier
* @param newSize the size to use when modifying fonts
*/
public void addSizeModfier(int newSize) {
if (size != null) {
throw new IllegalStateException("Multiple font sizes specified");
@ -50,6 +70,10 @@ public class FontModifier {
this.size = newSize;
}
/**
* Sets the font stle modifier. This can be called multiple times to bold and italicize.
* @param newStyle the style to use for the font.
*/
public void addStyleModifier(int newStyle) {
if (style == null) {
style = newStyle;
@ -61,6 +85,11 @@ public class FontModifier {
style = style | newStyle;
}
/**
* Returns a modified font for the given font.
* @param font the font to be modified
* @return a new modified font
*/
public Font modify(Font font) {
if (family == null) {
if (style != null && size != null) {
@ -76,27 +105,10 @@ public class FontModifier {
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;
}
/**
* Returns a string that can be parsed by the {@link #parse(String)} method of this class
* @return a string that can be parsed by the {@link #parse(String)} method of this class
*/
public String getSerializationString() {
StringBuilder builder = new StringBuilder();
if (family != null) {
@ -125,14 +137,58 @@ public class FontModifier {
return builder.toString();
}
/**
* Parses the given string as one or more font modifiers
* @param value the string to parse as modifiers
* @return a FontModifier as specified by the given string
* @throws ParseException if The value can't be parsed
*/
public static FontModifier parse(String value) throws ParseException {
List<String> modifierValues = ThemeValueUtils.parseGroupings(value, '[', ']');
if (modifierValues.isEmpty()) {
return null;
}
FontModifier modifier = new FontModifier();
for (String modifierString : modifierValues) {
if (setSize(modifier, modifierString)) {
continue;
}
if (setStyle(modifier, modifierString)) {
continue;
}
setFamily(modifier, modifierString);
}
if (modifier.hadModifications()) {
return modifier;
}
return null;
}
private static void setFamily(FontModifier modifier, String modifierString)
throws ParseException {
try {
modifier.addFamilyModifier(modifierString);
}
catch (IllegalStateException e) {
throw new ParseException("Multiple Font Families specfied", 0);
}
}
private boolean hadModifications() {
return family != null || size != null || style != null;
}
private static boolean setStyle(FontModifier modifier, String modifierString) {
private static boolean setStyle(FontModifier modifier, String modifierString)
throws ParseException {
int style = FontValue.getStyle(modifierString);
if (style >= 0) {
modifier.addStyleModifier(style);
try {
modifier.addStyleModifier(style);
}
catch (IllegalStateException e) {
throw new ParseException("Illegal style combination", 0);
}
return true;
}
return false;
@ -149,18 +205,4 @@ public class FontModifier {
}
}
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;
}
}

View file

@ -16,6 +16,7 @@
package generic.theme;
import java.awt.Font;
import java.text.ParseException;
import ghidra.util.Msg;
@ -75,7 +76,7 @@ public class FontValue extends ThemeValue<Font> {
if (referenceId != null) {
String refId = toExternalId(referenceId);
if (modifier != null) {
return "(" + refId + modifier.getSerializationString() + ")";
return refId + modifier.getSerializationString();
}
return refId;
}
@ -106,8 +107,9 @@ public class FontValue extends ThemeValue<Font> {
* @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
* @throws ParseException
*/
public static FontValue parse(String key, String value) {
public static FontValue parse(String key, String value) throws ParseException {
String id = fromExternalId(key);
value = clean(value);
@ -184,7 +186,7 @@ public class FontValue extends ThemeValue<Font> {
return null;
}
private static FontValue getRefFontValue(String id, String value) {
private static FontValue getRefFontValue(String id, String value) throws ParseException {
if (value.startsWith(EXTERNAL_PREFIX)) {
value = value.substring(EXTERNAL_PREFIX.length());
}

View file

@ -24,7 +24,7 @@ import javax.swing.ImageIcon;
import ghidra.util.datastruct.WeakStore;
import resources.ResourceManager;
import resources.icons.UrlImageIcon;
import resources.icons.*;
/**
* An {@link Icon} whose value is dynamically determined by looking up its id into a global
@ -89,10 +89,24 @@ public class GIcon implements Icon {
* @return the icon or null
*/
public URL getUrl() {
if (delegate instanceof UrlImageIcon) {
return ((UrlImageIcon) delegate).getUrl();
return getUrl(delegate);
}
private URL getUrl(Icon icon) {
if (icon instanceof UrlImageIcon urlIcon) {
return urlIcon.getUrl();
}
else if (icon instanceof TranslateIcon translateIcon) {
return getUrl(translateIcon.getBaseIcon());
}
else if (icon instanceof DerivedImageIcon derivedIcon) {
return getUrl(derivedIcon.getSourceIcon());
}
else if (icon instanceof RotateIcon rotateIcon) {
return getUrl(rotateIcon.getSourceIcon());
}
return null;
}
/**

View file

@ -125,4 +125,12 @@ public class GThemeDefaults {
}
}
}
public static class Fonts {
public static final String STANDARD = "font.standard";
public static final String BOLD = "font.bold";
public static final String ITALIC = "font.italic";
public static final String BOLD_ITALIC = "font.bold.italic";
public static final String MONOSPACED = "font.monospaced";
}
}

View file

@ -0,0 +1,295 @@
/* ###
* 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.Dimension;
import java.awt.Point;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import javax.swing.Icon;
//font.foo = images/flag.png[size(12,16)][move(3,4)][disable]
import resources.MultiIcon;
import resources.ResourceManager;
import resources.icons.RotateIcon;
import resources.icons.TranslateIcon;
/**
* Class that can transform one icon into another. Useful for scaling, translating, disabling,
* or overlaying an icon.
*/
public class IconModifier {
Dimension size;
Point translation;
boolean disabled;
Integer rotation;
List<IconValue> overlayIconValues = null;
/**
* Creates an IconModifier that can scale, translate, or disable an icon.
* @param size if non-null, scales an icon to this size.
* @param translation if non-null, translates an icon by this amount
* @param rotation if non-null, the amount in degrees to rotate the icon
* @param disabled if true, creates a disabled version of the icon
*/
public IconModifier(Dimension size, Point translation, Integer rotation, boolean disabled) {
this.size = size;
this.translation = translation;
this.rotation = rotation;
this.disabled = disabled;
}
private IconModifier() {
}
/**
* Sets size modifier. Icons that are modified by this IconModifier will be scaled to this size.
* @param size the size to scale modified icons.
*/
public void setSizeModifier(Dimension size) {
this.size = size;
}
/**
* Sets the translation for this modifier. Icons that are modified by this IconModifier will
* be translated by the amount of the given point.
* @param point the x,y amount to translate an image
*/
public void setMoveModifier(Point point) {
this.translation = point;
}
/**
* Sets the rotation for this modifier. Icons that are modified by this IconModifier will
* be rotated by the given amount (in degrees)
* @param degrees the rotation amount;
*/
public void setRotationModifer(int degrees) {
this.rotation = degrees;
}
/**
* Sets this modifier to disable an icon
*/
public void setDisabled() {
disabled = true;
}
/**
* Modifies the given icon by the any of the modifiers set.
* @param icon the icon to be modified
* @param values the ThemeValueMap needed if the modify action is to overlay other icons. The
* values are used to resolve indirect overlay icon references
* @return A new Icon that is a modified version of the given icon
*/
public Icon modify(Icon icon, GThemeValueMap values) {
Icon modified = icon;
if (size != null) {
modified = ResourceManager.getScaledIcon(modified, size.width, size.height);
}
if (disabled) {
modified = ResourceManager.getDisabledIcon(modified);
}
if (rotation != null) {
modified = new RotateIcon(icon, rotation);
}
if (translation != null) {
modified = new TranslateIcon(modified, translation.x, translation.y);
}
if (overlayIconValues != null) {
MultiIcon multiIcon = new MultiIcon(modified);
for (IconValue iconValue : overlayIconValues) {
multiIcon.addIcon(iconValue.get(values));
}
modified = multiIcon;
}
return modified;
}
/**
* Returns a string that can be parsed by the {@link #parse(String)} method of this class
* @return a string that can be parsed by the {@link #parse(String)} method of this class
*/
public String getSerializationString() {
StringBuilder builder = new StringBuilder();
if (size != null) {
builder.append("[" + "size(" + size.width + "," + size.height + ")]");
}
if (rotation != null) {
builder.append("[rotate(" + rotation + ")]");
}
if (translation != null) {
builder.append("[" + "move(" + translation.x + "," + translation.y + ")]");
}
if (disabled) {
builder.append("[disabled]");
}
return builder.toString();
}
/**
* Parses the given string as one or more icon modifiers
* @param iconModifierString the string to parse as modifiers
* @return an IconModifier as specified by the given string
* @throws ParseException if the iconModifierString in not properly formatted icon modifier
*/
public static IconModifier parse(String iconModifierString) throws ParseException {
if (iconModifierString.isBlank()) {
return null;
}
IconModifier modifier = new IconModifier();
String baseModifierString = getBaseModifierString(iconModifierString);
parseBaseModifiers(modifier, baseModifierString);
String overlayValuesString = getIconOverlaysString(iconModifierString);
parseOverlayModifiers(modifier, overlayValuesString);
if (modifier.hadModifications()) {
return modifier;
}
return null;
}
private static void parseOverlayModifiers(IconModifier modifier, String overlayValuesString)
throws ParseException {
List<String> overlayModifierStrings =
ThemeValueUtils.parseGroupings(overlayValuesString, '{', '}');
for (String overlayIconString : overlayModifierStrings) {
IconValue overlayIconValue = IconValue.parse("", overlayIconString);
modifier.addOverlayIcon(overlayIconValue);
}
}
private void addOverlayIcon(IconValue overlayIconValue) {
if (overlayIconValues == null) {
overlayIconValues = new ArrayList<>();
}
overlayIconValues.add(overlayIconValue);
}
private static void parseBaseModifiers(IconModifier modifier, String baseModifierString)
throws ParseException {
List<String> modifierValues = ThemeValueUtils.parseGroupings(baseModifierString, '[', ']');
for (String modifierString : modifierValues) {
modifierString = modifierString.replaceAll("\\s", "").toLowerCase();
if (modifierString.startsWith("size")) {
parseSizeModifier(modifier, modifierString);
}
else if (modifierString.startsWith("move")) {
parseMoveModifier(modifier, modifierString);
}
else if (modifierString.startsWith("rotate")) {
parseRotateModifier(modifier, modifierString);
}
else if (modifierString.startsWith("disabled")) {
parseDisabledModifier(modifier, modifierString);
}
else {
throw new ParseException("Invalid icon modifier: " + modifierString, 0);
}
}
}
private static String getBaseModifierString(String value) {
int overlayStart = value.indexOf("{");
if (overlayStart < 0) {
return value;
}
if (overlayStart == 0) {
return "";
}
return value.substring(0, overlayStart);
}
private static String getIconOverlaysString(String value) {
int overlayStart = value.indexOf("{");
if (overlayStart >= 0) {
return value.substring(overlayStart);
}
return "";
}
private boolean hadModifications() {
return size != null || translation != null || overlayIconValues != null ||
rotation != null || disabled;
}
private static void parseDisabledModifier(IconModifier modifier, String modifierString)
throws ParseException {
if (!modifierString.equals("disabled")) {
throw new ParseException("Illegal Icon modifier: " + modifier, 0);
}
modifier.setDisabled();
}
private static void parseRotateModifier(IconModifier modifier, String modifierString)
throws ParseException {
String argsString = modifierString.substring("rotate".length());
int rotation = parseIntArg(argsString);
modifier.setRotationModifer(rotation);
}
private static void parseMoveModifier(IconModifier modifier, String modifierString)
throws ParseException {
String argsString = modifierString.substring("move".length());
Point argValue = parsePointArgs(argsString);
modifier.setMoveModifier(argValue);
}
private static void parseSizeModifier(IconModifier modifier, String modifierString)
throws ParseException {
String argsString = modifierString.substring("size".length());
Point argValue = parsePointArgs(argsString);
modifier.setSizeModifier(new Dimension(argValue.x, argValue.y));
}
private static Point parsePointArgs(String argsString) throws ParseException {
if (!(argsString.startsWith("(") && argsString.endsWith(")"))) {
throw new ParseException("Invalid arguments: " + argsString, 0);
}
argsString = argsString.substring(1, argsString.length() - 1);
String[] split = argsString.split(",");
if (split.length != 2) {
throw new ParseException("Invalid arguments: " + argsString, 0);
}
try {
int arg1 = Integer.parseInt(split[0]);
int arg2 = Integer.parseInt(split[1]);
return new Point(arg1, arg2);
}
catch (NumberFormatException e) {
throw new ParseException("Invalid arguments: " + argsString, 0);
}
}
private static int parseIntArg(String argString) throws ParseException {
if (!(argString.startsWith("(") && argString.endsWith(")"))) {
throw new ParseException("Invalid arguments: " + argString, 0);
}
argString = argString.substring(1, argString.length() - 1);
try {
return Integer.parseInt(argString);
}
catch (NumberFormatException e) {
throw new ParseException("Invalid arguments: " + argString, 0);
}
}
}

View file

@ -15,10 +15,13 @@
*/
package generic.theme;
import java.text.ParseException;
import javax.swing.Icon;
import ghidra.util.Msg;
import resources.ResourceManager;
import resources.icons.EmptyIcon;
import resources.icons.UrlImageIcon;
/**
@ -28,12 +31,18 @@ import resources.icons.UrlImageIcon;
* and if the class's refId is non-null, then the icon value will be null.
*/
public class IconValue extends ThemeValue<Icon> {
private static final String EMPTY_ICON_STRING = "EMPTY_ICON";
static final String ICON_ID_PREFIX = "icon.";
public static final Icon LAST_RESORT_DEFAULT = ResourceManager.getDefaultIcon();
private static final String EXTERNAL_PREFIX = "[icon]";
private static final int STANDARD_EMPTY_ICON_SIZE = 16;
private IconModifier modifier;
/**
* Constructor used when the ColorValue will have a direct {@link Icon} value. The refId will
* be null. Note: if a {@link GIcon} is passed in as the value, then this will be an indirect
@ -55,6 +64,25 @@ public class IconValue extends ThemeValue<Icon> {
super(id, refId, null);
}
private IconValue(String id, String refId, IconModifier modifier) {
super(id, refId, null);
this.modifier = modifier;
}
private IconValue(String id, Icon icon, IconModifier modifier) {
super(id, null, icon);
this.modifier = modifier;
}
@Override
public Icon get(GThemeValueMap values) {
Icon icon = super.get(values);
if (modifier != null) {
return modifier.modify(icon, values);
}
return icon;
}
@Override
public String getSerializationString() {
String outputId = toExternalId(id);
@ -76,6 +104,15 @@ public class IconValue extends ThemeValue<Icon> {
* @return a String that represents the icon
*/
public static String iconToString(Icon icon) {
if (icon instanceof EmptyIcon) {
int iconWidth = icon.getIconWidth();
int iconHeight = icon.getIconHeight();
if (iconWidth == STANDARD_EMPTY_ICON_SIZE && iconHeight == STANDARD_EMPTY_ICON_SIZE) {
return EMPTY_ICON_STRING;
}
return EMPTY_ICON_STRING + "[size(" + iconWidth + "," + iconHeight + ")]";
}
if (icon instanceof UrlImageIcon urlIcon) {
return urlIcon.getOriginalPath();
}
@ -88,14 +125,60 @@ public class IconValue extends ThemeValue<Icon> {
* @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
* @throws ParseException
*/
public static IconValue parse(String key, String value) {
public static IconValue parse(String key, String value) throws ParseException {
String id = fromExternalId(key);
if (isIconKey(value)) {
return new IconValue(id, fromExternalId(value));
return parseRefIcon(id, value);
}
Icon icon = ResourceManager.loadImage(value);
return new IconValue(id, icon);
return parseIcon(id, value);
}
private static IconValue parseIcon(String id, String value) throws ParseException {
int modifierIndex = getModifierIndex(value);
if (modifierIndex < 0) {
return new IconValue(id, getIcon(value));
}
String baseIconString = value.substring(0, modifierIndex).trim();
Icon icon = getIcon(baseIconString);
String iconModifierString = value.substring(modifierIndex);
IconModifier modifier = IconModifier.parse(iconModifierString);
return new IconValue(id, icon, modifier);
}
private static Icon getIcon(String baseIconString) {
if (EMPTY_ICON_STRING.equals(baseIconString)) {
return new EmptyIcon(STANDARD_EMPTY_ICON_SIZE, STANDARD_EMPTY_ICON_SIZE);
}
return ResourceManager.loadImage(baseIconString);
}
private static IconValue parseRefIcon(String id, String value) throws ParseException {
if (value.startsWith(EXTERNAL_PREFIX)) {
value = value.substring(EXTERNAL_PREFIX.length());
}
int modifierIndex = getModifierIndex(value);
if (modifierIndex < 0) {
return new IconValue(id, value);
}
String refId = value.substring(0, modifierIndex).trim();
IconModifier modifier = IconModifier.parse(value.substring(modifierIndex));
return new IconValue(id, refId, modifier);
}
private static int getModifierIndex(String value) {
int baseModifierIndex = value.indexOf("[", 1); // start past first char as it coud be valid "[EXTERNAL]" prefix
int overlayModifierIndex = value.indexOf("{");
if (baseModifierIndex < 0) {
return overlayModifierIndex;
}
if (overlayModifierIndex < 0) {
return baseModifierIndex;
}
return Math.min(overlayModifierIndex, baseModifierIndex);
}
@Override
@ -140,10 +223,17 @@ public class IconValue extends ThemeValue<Icon> {
}
private String getValueOutput() {
String outputString = null;
if (referenceId != null) {
return toExternalId(referenceId);
outputString = toExternalId(referenceId);
}
return iconToString(value);
else {
outputString = iconToString(value);
}
if (modifier != null) {
outputString += modifier.getSerializationString();
}
return outputString;
}
@Override

View file

@ -0,0 +1,84 @@
/* ###
* 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.text.ParseException;
import java.util.ArrayList;
import java.util.List;
public class ThemeValueUtils {
/**
* Parses the given source string into a list of strings, one for each group. The startChar
* and endChar defined the group characters. So, for example, "(ab (cd))(ef)((gh))" would
* result in a list with the following values: "ab (cd)", "ef", and "(gh)"
* @param source the source string to parse into groups
* @param startChar the character that defines the start of a group
* @param endChar the character that defines then end of a group
* @return a List of strings, one for each consecutive group contained in the string
* @throws ParseException if the groupings are not balanced or missing altogether
*/
public static List<String> parseGroupings(String source, char startChar, char endChar)
throws ParseException {
List<String> results = new ArrayList<>();
int index = 0;
while (index < source.length()) {
int groupStart = findNextNonWhiteSpaceChar(source, index);
if (groupStart < 0) {
break;
}
if (source.charAt(groupStart) != startChar) {
throw new ParseException("Error parsing groupings for " + source, index);
}
int groupEnd = findMatchingEnd(source, groupStart + 1, startChar, endChar);
if (groupEnd < 0) {
throw new ParseException("Error parsing groupings for " + source, index);
}
results.add(source.substring(groupStart + 1, groupEnd));
index = groupEnd + 1;
}
return results;
}
private static int findMatchingEnd(String source, int index, char startChar, char endChar) {
int level = 0;
while (index < source.length()) {
char c = source.charAt(index);
if (c == startChar) {
level++;
}
else if (c == endChar) {
if (level == 0) {
return index;
}
level--;
}
index++;
}
return -1;
}
private static int findNextNonWhiteSpaceChar(String source, int index) {
while (index < source.length()) {
if (!Character.isWhitespace(source.charAt(index))) {
return index;
}
index++;
}
return -1;
}
}

View file

@ -26,8 +26,6 @@ import javax.swing.ImageIcon;
import generic.theme.GIcon;
import generic.theme.GThemeDefaults.Colors;
import ghidra.util.Msg;
import resources.icons.RotateIcon;
import resources.icons.TranslateIcon;
/**
* A class to get generic icons for standard actions. All methods in this class return an
@ -51,7 +49,7 @@ public class Icons {
public static final Icon NAVIGATE_ON_INCOMING_EVENT_ICON = new GIcon("icon.navigate.in");
public static final Icon NAVIGATE_ON_OUTGOING_EVENT_ICON = new GIcon("icon.navigate.out");
public static final Icon NOT_ALLOWED_ICON = new GIcon("icon.notallowed");
public static final Icon NOT_ALLOWED_ICON = new GIcon("icon.not.allowed");
public static final Icon OPEN_FOLDER_ICON = new GIcon("icon.folder.open");
public static final Icon REFRESH_ICON = new GIcon("icon.refresh");
@ -78,16 +76,10 @@ public class Icons {
// Not necessarily re-usable, but this is needed for the help system; these should
// probably be moved to the client that uses them, while updating the
// help system to use them there.
public static final Icon ARROW_DOWN_RIGHT_ICON =
ResourceManager.getImageIcon(new RotateIcon(new GIcon("icon.arrow.up.right"), 90));
public static final Icon ARROW_UP_LEFT_ICON =
ResourceManager.getImageIcon(new RotateIcon(new GIcon("icon.arrow.up.right"), 275));
public static final Icon FILTER_NOT_ACCEPTED_ICON =
ResourceManager.getImageIcon(new MultiIcon(new GIcon("icon.flag"),
new TranslateIcon(ResourceManager.loadImage("icon.notallowed", 10, 10), 6, 6)));
public static final Icon APPLY_BLOCKED_MATCH_ICON =
ResourceManager.getImageIcon(new MultiIcon(new GIcon("icon.lock"),
new TranslateIcon(ResourceManager.loadImage("icon.checkmark.green", 12, 12), 4, 0)));
public static final Icon ARROW_DOWN_RIGHT_ICON = new GIcon("icon.arrow.down.right");
public static final Icon ARROW_UP_LEFT_ICON = new GIcon("icon.arrow.up.left");
public static final Icon FILTER_NOT_ACCEPTED_ICON = new GIcon("icon.filter.not.accepted");
public static final Icon APPLY_BLOCKED_MATCH_ICON = new GIcon("icon.blocked.match");
/**
* Returns true if the given string is a Java code snippet that references this class

View file

@ -52,7 +52,7 @@ public class MultiIconBuilder {
* @return this builder (for chaining)
*/
public MultiIconBuilder addIcon(Icon icon, int w, int h, QUADRANT quandrant) {
ImageIcon scaled = ResourceManager.getScaledIcon(icon, w, h);
Icon scaled = ResourceManager.getScaledIcon(icon, w, h);
int x = (multiIcon.getIconWidth() - scaled.getIconWidth()) * quandrant.x;
int y = (multiIcon.getIconHeight() - scaled.getIconHeight()) * quandrant.y;
@ -75,7 +75,7 @@ public class MultiIconBuilder {
* @return this builder (for chaining)
*/
public MultiIconBuilder addIcon(Icon icon, int w, int h, int x, int y) {
ImageIcon scaled = ResourceManager.getScaledIcon(icon, w, h);
Icon scaled = ResourceManager.getScaledIcon(icon, w, h);
TranslateIcon txIcon = new TranslateIcon(scaled, x, y);
multiIcon.addIcon(txIcon);
return this;

View file

@ -338,7 +338,25 @@ public class ResourceManager {
* @param height the height of the new icon
* @return A new, scaled ImageIcon
*/
public static ImageIcon getScaledIcon(Icon icon, int width, int height) {
public static ImageIcon getScaledIcon(ImageIcon icon, int width, int height) {
return new ScaledImageIcon(icon, width, height);
}
/**
* Creates a scaled Icon from the given icon with scaling of
* {@link Image#SCALE_AREA_AVERAGING}. If an EmptyIcon is passed, a new EmptyIcon is returned
* with the new dimensions.
*
* @param icon the icon to scale
* @param width the width of the new icon
* @param height the height of the new icon
* @return A new, scaled ImageIcon
*/
public static Icon getScaledIcon(Icon icon, int width, int height) {
if (icon instanceof EmptyIcon) {
return new EmptyIcon(width, height);
}
return new ScaledImageIcon(icon, width, height);
}
@ -478,7 +496,7 @@ public class ResourceManager {
if (loadImage == null) {
return null;
}
return getScaledIcon(loadImage, width, height);
return (ImageIcon) getScaledIcon(loadImage, width, height);
}
/**
@ -514,6 +532,7 @@ public class ResourceManager {
if (icon == null) {
icon = doLoadIcon(filename);
if (icon == null) {
Msg.warn(ResourceManager.class, "Can't resolve icon: " + filename);
icon = new UnresolvedIcon(filename, getDefaultIcon());
}
iconMap.put(filename, icon);

View file

@ -52,6 +52,10 @@ public class DerivedImageIcon extends LazyImageIcon {
this.sourceImage = Objects.requireNonNull(image);
}
public Icon getSourceIcon() {
return sourceIcon;
}
protected ImageIcon createImageIcon() {
Image image = createImage();
String imageName = getFilename();

View file

@ -1,6 +1,5 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -18,29 +17,55 @@ package resources.icons;
import java.awt.Component;
import java.awt.Graphics;
import java.util.Objects;
import javax.swing.Icon;
public class EmptyIcon implements Icon {
public class EmptyIcon implements Icon {
private int width;
private int height;
public EmptyIcon( int width, int height ) {
this.width = width;
this.height = height;
}
public int getIconHeight() {
return height;
}
private int width;
private int height;
public int getIconWidth() {
return width;
}
public EmptyIcon(int width, int height) {
this.width = width;
this.height = height;
}
public void paintIcon( Component c, Graphics g, int x, int y ) {
// no-op
}
public int getIconHeight() {
return height;
}
public int getIconWidth() {
return width;
}
public void paintIcon(Component c, Graphics g, int x, int y) {
// no-op
}
@Override
public int hashCode() {
return Objects.hash(height, width);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
EmptyIcon other = (EmptyIcon) obj;
return height == other.height && width == other.width;
}
@Override
public String toString() {
return "EmptyIcon(" + width + "," + height + ")";
}
}

View file

@ -60,4 +60,20 @@ public class RotateIcon implements Icon {
}
return description;
}
/**
* The source icon being rotated.
* @return the source icon being rotate
*/
public Icon getSourceIcon() {
return icon;
}
/**
* Returns the rotation amount.
* @return the rotation amount
*/
public int getRotation() {
return degrees;
}
}

View file

@ -58,4 +58,29 @@ public class TranslateIcon implements Icon {
public String toString() {
return getClass().getSimpleName() + "[" + ResourceManager.getIconName(icon) + "]";
}
// for testing
/**
* Returns the icon that is being translated
* @return the icon that is being translated
*/
public Icon getBaseIcon() {
return icon;
}
/**
* Returns the amount the icon is being translated on the x axis;
* @return the amount the icon is being translated on the x axis;
*/
public int getX() {
return translateX;
}
/**
* Returns the amount the icon is being translated on the y axis;
* @return the amount the icon is being translated on the y axis;
*/
public int getY() {
return translateY;
}
}

View file

@ -18,6 +18,7 @@ package generic.theme;
import static org.junit.Assert.*;
import java.awt.Font;
import java.text.ParseException;
import org.junit.Test;
@ -25,12 +26,12 @@ public class FontModifierTest {
private Font baseFont = new Font("Dialog", Font.PLAIN, 12);
@Test
public void testNoModifiers() {
public void testNoModifiers() throws ParseException {
assertNull(FontModifier.parse(""));
}
@Test
public void testSizeModifier() {
public void testSizeModifier() throws ParseException {
FontModifier modifier = FontModifier.parse("[6]");
assertNotNull(modifier);
Font newFont = modifier.modify(baseFont);
@ -40,7 +41,7 @@ public class FontModifierTest {
}
@Test
public void testStyleModifierPlain() {
public void testStyleModifierPlain() throws ParseException {
FontModifier modifier = FontModifier.parse("[plain]");
assertNotNull(modifier);
Font newFont = modifier.modify(baseFont);
@ -50,7 +51,7 @@ public class FontModifierTest {
}
@Test
public void testStyleModifierBold() {
public void testStyleModifierBold() throws ParseException {
FontModifier modifier = FontModifier.parse("[bold]");
assertNotNull(modifier);
Font newFont = modifier.modify(baseFont);
@ -60,7 +61,7 @@ public class FontModifierTest {
}
@Test
public void testStyleModifierItalic() {
public void testStyleModifierItalic() throws ParseException {
FontModifier modifier = FontModifier.parse("[ITALIC]");
assertNotNull(modifier);
Font newFont = modifier.modify(baseFont);
@ -70,7 +71,7 @@ public class FontModifierTest {
}
@Test
public void testStyleModifierBoldItalic() {
public void testStyleModifierBoldItalic() throws ParseException {
FontModifier modifier = FontModifier.parse("[BOLDitalic]");
assertNotNull(modifier);
Font newFont = modifier.modify(baseFont);
@ -80,7 +81,7 @@ public class FontModifierTest {
}
@Test
public void testStyleModifierBoldItalic2() {
public void testStyleModifierBoldItalic2() throws ParseException {
FontModifier modifier = FontModifier.parse("[BOLD][italic]");
assertNotNull(modifier);
Font newFont = modifier.modify(baseFont);
@ -90,7 +91,7 @@ public class FontModifierTest {
}
@Test
public void testFamilyModification() {
public void testFamilyModification() throws ParseException {
FontModifier modifier = FontModifier.parse("[monospaced]");
assertNotNull(modifier);
Font newFont = modifier.modify(baseFont);
@ -100,7 +101,7 @@ public class FontModifierTest {
}
@Test
public void testSizeAndStyleModification() {
public void testSizeAndStyleModification() throws ParseException {
FontModifier modifier = FontModifier.parse("[16][bold]");
assertNotNull(modifier);
Font newFont = modifier.modify(baseFont);
@ -116,7 +117,7 @@ public class FontModifierTest {
FontModifier.parse("[monospaced][courier]");
fail("Expecected Exception");
}
catch (IllegalStateException e) {
catch (ParseException e) {
// expected
}
}
@ -127,7 +128,7 @@ public class FontModifierTest {
FontModifier.parse("[plain][italic]");
fail("Expected IllegalStateException");
}
catch (IllegalStateException e) {
catch (ParseException e) {
// expected
}
}
@ -138,7 +139,7 @@ public class FontModifierTest {
FontModifier.parse("asdfasf");
fail("Expected IllegalArgumentExcption");
}
catch (IllegalArgumentException e) {
catch (ParseException e) {
// expected
}
}
@ -149,7 +150,7 @@ public class FontModifierTest {
FontModifier.parse("[12]aa[13]");
fail("Expected IllegalArgumentExcption");
}
catch (IllegalArgumentException e) {
catch (ParseException e) {
// expected
}
}
@ -160,7 +161,7 @@ public class FontModifierTest {
FontModifier.parse("[12]aa13]");
fail("Expected IllegalArgumentExcption");
}
catch (IllegalArgumentException e) {
catch (ParseException e) {
// expected
}
}
@ -171,7 +172,7 @@ public class FontModifierTest {
FontModifier.parse("[12][plain]sz");
fail("Expected IllegalArgumentExcption");
}
catch (IllegalArgumentException e) {
catch (ParseException e) {
// expected
}
}

View file

@ -18,6 +18,7 @@ package generic.theme;
import static org.junit.Assert.*;
import java.awt.Font;
import java.text.ParseException;
import org.junit.Before;
import org.junit.Test;
@ -97,7 +98,7 @@ public class FontValueTest {
}
@Test
public void testParse() {
public void testParse() throws ParseException {
FontValue value = FontValue.parse("font.test", "Dialog-PLAIN-12");
assertEquals("font.test", value.getId());
assertEquals(FONT, value.getRawValue());

View file

@ -0,0 +1,189 @@
/* ###
* 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.Dimension;
import java.awt.Point;
import java.text.ParseException;
import javax.swing.Icon;
import org.junit.Test;
import resources.MultiIcon;
import resources.ResourceManager;
import resources.icons.RotateIcon;
import resources.icons.TranslateIcon;
public class IconModifierTest {
private Icon baseIcon = ResourceManager.getDefaultIcon();
private GThemeValueMap values = new GThemeValueMap();
@Test
public void testNoModifiers() throws Exception {
assertNull(IconModifier.parse(""));
}
@Test
public void testSizeModifier() throws Exception {
IconModifier modifier = IconModifier.parse("[size(7,13)]");
Icon modifiedIcon = modifier.modify(baseIcon, values);
assertEquals(7, modifiedIcon.getIconWidth());
assertEquals(13, modifiedIcon.getIconHeight());
}
@Test
public void testSizeModifier2() throws Exception {
IconModifier modifier = IconModifier.parse("[SIZE(7,13)]");
Icon modifiedIcon = modifier.modify(baseIcon, values);
assertEquals(7, modifiedIcon.getIconWidth());
assertEquals(13, modifiedIcon.getIconHeight());
}
@Test
public void testMoveModifier() throws Exception {
IconModifier modifier = IconModifier.parse("[move(4, 3)]");
Icon modifiedIcon = modifier.modify(baseIcon, values);
assertTrue(modifiedIcon instanceof TranslateIcon);
TranslateIcon translateIcon = (TranslateIcon) modifiedIcon;
assertEquals(4, translateIcon.getX());
assertEquals(3, translateIcon.getY());
}
@Test
public void testRotateModifier() throws Exception {
IconModifier modifier = IconModifier.parse("[rotate(90)]");
Icon modifiedIcon = modifier.modify(baseIcon, values);
assertTrue(modifiedIcon instanceof RotateIcon);
RotateIcon rotateIcon = (RotateIcon) modifiedIcon;
assertEquals(90, rotateIcon.getRotation());
}
@Test
public void testDisabledModifier() throws Exception {
IconModifier modifier = IconModifier.parse("[disabled]");
Icon modifiedIcon = modifier.modify(baseIcon, values);
assertNotEquals(baseIcon, modifiedIcon);
}
@Test
public void testOverlayIcon() throws Exception {
IconModifier modifier = IconModifier.parse("{images/flag.png}");
Icon modifiedIcon = modifier.modify(baseIcon, values);
assertTrue(modifiedIcon instanceof MultiIcon);
MultiIcon multiIcon = (MultiIcon) modifiedIcon;
Icon[] icons = multiIcon.getIcons();
assertEquals(2, icons.length);
assertEquals(baseIcon, icons[0]);
assertEquals(ResourceManager.loadImage("images/flag.png"), icons[1]);
}
@Test
public void testOverlayIcon2() throws Exception {
IconModifier modifier =
IconModifier.parse("[size(20,25)]{images/flag.png[size(8,9)][move(4,4)]}");
Icon modifiedIcon = modifier.modify(baseIcon, values);
assertTrue(modifiedIcon instanceof MultiIcon);
MultiIcon multiIcon = (MultiIcon) modifiedIcon;
Icon[] icons = multiIcon.getIcons();
assertEquals(2, icons.length);
assertEquals(20, icons[0].getIconWidth());
assertEquals(25, icons[0].getIconHeight());
assertEquals(8, icons[1].getIconWidth());
assertEquals(9, icons[1].getIconHeight());
}
@Test
public void testInvalidModifierString() {
try {
IconModifier.parse("dasdf");
fail("Expected IllegalArgumentExcption");
}
catch (ParseException e) {
// expected
}
}
@Test
public void testInvalidModifierString2() {
try {
IconModifier.parse("disabledx");
fail("Expected IllegalArgumentExcption");
}
catch (ParseException e) {
// expected
}
}
@Test
public void testInvalidModifierString3() {
try {
IconModifier.parse("[size(13,14,13)]");
fail("Expected IllegalArgumentExcption");
}
catch (ParseException e) {
// expected
}
}
@Test
public void testInvalidModifierString4() {
try {
IconModifier.parse("[size(14,12]");
fail("Expected IllegalArgumentExcption");
}
catch (ParseException e) {
// expected
}
}
@Test
public void testInvalidModifierString5() {
try {
IconModifier.parse("[size(14)]");
fail("Expected IllegalArgumentExcption");
}
catch (ParseException e) {
// expected
}
}
@Test
public void testInvalidModifierString6() {
try {
IconModifier.parse("[size(10,10)]move(3,4)]");
fail("Expected IllegalArgumentExcption");
}
catch (ParseException e) {
// expected
}
}
@Test
public void testGetSerializationString() {
//@formatter:off
assertEquals("[size(5,9)]", new IconModifier(new Dimension(5,9), null, null, false).getSerializationString());
assertEquals("[move(8,7)]", new IconModifier(null, new Point(8,7), null,false).getSerializationString());
assertEquals("[disabled]", new IconModifier(null, null, null, true).getSerializationString());
assertEquals("[size(5,0)][move(8,7)][disabled]", new IconModifier(new Dimension(5,0), new Point(8,7), null, true).getSerializationString());
assertEquals("[rotate(90)]", new IconModifier(null, null, 90, false).getSerializationString());
//@formatter:on
}
}

View file

@ -17,12 +17,17 @@ package generic.theme;
import static org.junit.Assert.*;
import java.text.ParseException;
import javax.swing.Icon;
import org.junit.Before;
import org.junit.Test;
import resources.MultiIcon;
import resources.ResourceManager;
import resources.icons.EmptyIcon;
import resources.icons.TranslateIcon;
public class IconValueTest {
private static Icon ICON1 = ResourceManager.getDefaultIcon();
@ -99,7 +104,7 @@ public class IconValueTest {
}
@Test
public void testParse() {
public void testParse() throws ParseException {
IconValue value = IconValue.parse("icon.test", "images/core.png");
assertEquals("icon.test", value.getId());
assertEquals(ICON1, value.getRawValue());
@ -116,6 +121,23 @@ public class IconValueTest {
assertEquals("xyz.abc", value.getReferenceId());
}
@Test
public void testParseWithOverlays() throws ParseException {
IconValue value = IconValue.parse("icon.test",
"images/core.png[size(25,25)]{images/flag.png[size(8,8)][move(4,4)]}");
assertEquals("icon.test", value.getId());
Icon icon = value.get(values);
assertTrue(icon instanceof MultiIcon);
MultiIcon multiIcon = (MultiIcon) icon;
Icon[] icons = multiIcon.getIcons();
assertEquals(2, icons.length);
assertEquals(25, icons[0].getIconWidth());
assertEquals(25, icons[0].getIconWidth());
assertEquals(8, icons[1].getIconWidth());
assertEquals(8, icons[1].getIconWidth());
assertTrue(icons[1] instanceof TranslateIcon);
}
@Test
public void testIsIconKey() {
assertTrue(IconValue.isIconKey("icon.a.b.c"));
@ -150,4 +172,33 @@ public class IconValueTest {
assertEquals("icon.parent", value.getReferenceId());
assertNull(value.getRawValue());
}
@Test
public void testParseEmptyIcon() throws ParseException {
IconValue value = IconValue.parse("icon.test", "EMPTY_ICON");
assertEquals("icon.test", value.getId());
Icon icon = value.get(values);
assertEquals(new EmptyIcon(16, 16), icon);
}
@Test
public void testParseEmptyIconWithSize() throws ParseException {
IconValue value = IconValue.parse("icon.test", "EMPTY_ICON[size(12,15)]");
assertEquals("icon.test", value.getId());
Icon icon = value.get(values);
assertEquals(new EmptyIcon(12, 15), icon);
}
@Test
public void testGetSerializationStringWithEmptyIcon() {
IconValue value = new IconValue("icon.test", new EmptyIcon(16, 16));
assertEquals("icon.test = EMPTY_ICON", value.getSerializationString());
}
@Test
public void testGetSerializationStringWithEmptyCustomSizeIcon() {
IconValue value = new IconValue("icon.test", new EmptyIcon(22, 13));
assertEquals("icon.test = EMPTY_ICON[size(22,13)]", value.getSerializationString());
}
}

View file

@ -28,6 +28,7 @@ import javax.swing.Icon;
import org.junit.Test;
import resources.MultiIcon;
import resources.ResourceManager;
public class ThemePropertyFileReaderTest {
@ -49,12 +50,15 @@ public class ThemePropertyFileReaderTest {
" font.a.b = (font.a.8[20][BOLD])",
" icon.a.10 = core.png",
" icon.a.11 = icon.a.10",
" icon.a.12 = icon.a.10[size(17,21)]",
" icon.a.13 = core.png[size(17,21)]",
" icon.a.14 = icon.a.10{core.png[size(4,4)][move(8, 8)]",
"")));
//@formatter:on
Color halfAlphaRed = new Color(0x80ff0000, true);
GThemeValueMap values = reader.getDefaultValues();
assertEquals(12, values.size());
assertEquals(15, values.size());
assertEquals(WHITE, getColor(values, "color.b.1"));
assertEquals(RED, getColor(values, "color.b.2"));
@ -70,6 +74,16 @@ public class ThemePropertyFileReaderTest {
assertEquals(ResourceManager.loadImage("core.png"), getIcon(values, "icon.a.10"));
assertEquals(ResourceManager.loadImage("core.png"), getIcon(values, "icon.a.11"));
Icon icon = getIcon(values, "icon.a.12");
assertEquals(17, icon.getIconWidth());
assertEquals(21, icon.getIconHeight());
icon = getIcon(values, "icon.a.13");
assertEquals(17, icon.getIconWidth());
assertEquals(21, icon.getIconHeight());
icon = getIcon(values, "icon.a.14");
assertTrue(icon instanceof MultiIcon);
}

View file

@ -0,0 +1,60 @@
/* ###
* 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.text.ParseException;
import java.util.List;
import org.junit.Test;
public class ThemeValueUtilsTest {
@Test
public void testParseGroupings() throws ParseException {
String source = "(ab (cd))(ef)(( gh))";
List<String> results = ThemeValueUtils.parseGroupings(source, '(', ')');
assertEquals(3, results.size());
assertEquals("ab (cd)", results.get(0));
assertEquals("ef", results.get(1));
assertEquals("( gh)", results.get(2));
}
@Test
public void testParseGroupingsParseError() {
String source = "(ab (cd))(ef)( gh))";
try {
ThemeValueUtils.parseGroupings(source, '(', ')');
fail("Expected parse Exception");
}
catch (ParseException e) {
//expected
}
}
@Test
public void testParseGroupingsParseError2() {
String source = " xx";
try {
ThemeValueUtils.parseGroupings(source, '(', ')');
fail("Expected parse Exception");
}
catch (ParseException e) {
// expected
}
}
}