diff --git a/Ghidra/Features/Decompiler/src/decompile/cpp/blockaction.cc b/Ghidra/Features/Decompiler/src/decompile/cpp/blockaction.cc
index ece85ac09b..799fb002c8 100644
--- a/Ghidra/Features/Decompiler/src/decompile/cpp/blockaction.cc
+++ b/Ghidra/Features/Decompiler/src/decompile/cpp/blockaction.cc
@@ -1188,13 +1188,11 @@ void CollapseStructure::orderLoopBodies(void)
bool CollapseStructure::updateLoopBody(void)
{
+ if (finaltrace) { // If we've already performed trace on DAG with no likely goto edges
+ return false; // don't repeat the trace
+ }
FlowBlock *loopbottom = (FlowBlock *)0;
FlowBlock *looptop = (FlowBlock *)0;
- if (finaltrace) { // If we've already performed the final trace
- if (likelyiter == likelygoto.end())
- return false; // We have nothing more to give
- return true;
- }
while (loopbodyiter != loopbody.end()) { // Last innermost loop
loopbottom = (*loopbodyiter).getCurrentBounds(&looptop,&graph);
if (loopbottom != (FlowBlock *)0) {
@@ -1206,8 +1204,10 @@ bool CollapseStructure::updateLoopBody(void)
likelylistfull = false; // Need to generate likely list for new loopbody (or no loopbody)
loopbottom = (FlowBlock *)0;
}
- if (likelylistfull) return true;
- // If we reach here, need to generate likely gotos for a new inner loop
+ if (likelylistfull && likelyiter != likelygoto.end())
+ return true;
+
+ // If we reach here, need to generate likely gotos for a new inner loop or DAG
likelygoto.clear(); // Clear out any old likely gotos from last inner loop
TraceDAG tracer(likelygoto);
if (loopbottom != (FlowBlock *)0) {
@@ -1216,7 +1216,6 @@ bool CollapseStructure::updateLoopBody(void)
(*loopbodyiter).setExitMarks(&graph); // Set the bounds of the TraceDAG
}
else {
- finaltrace = true;
for(uint4 i=0;isizeIn() == 0)
@@ -1225,11 +1224,15 @@ bool CollapseStructure::updateLoopBody(void)
}
tracer.initialize();
tracer.pushBranches();
+ likelylistfull = true; // Mark likelygoto generation complete for current loop or DAG
if (loopbottom != (FlowBlock *)0) {
(*loopbodyiter).emitLikelyEdges(likelygoto,&graph);
(*loopbodyiter).clearExitMarks(&graph);
}
- likelylistfull = true;
+ else if (likelygoto.empty()) {
+ finaltrace = true; // No loops left and trace didn't find gotos
+ return false;
+ }
likelyiter = likelygoto.begin();
return true;
}
diff --git a/Ghidra/Framework/Docking/src/main/help/help/topics/Theming/ThemingDeveloperDocs.html b/Ghidra/Framework/Docking/src/main/help/help/topics/Theming/ThemingDeveloperDocs.html
index af3700ef0f..0d967d4ccb 100644
--- a/Ghidra/Framework/Docking/src/main/help/help/topics/Theming/ThemingDeveloperDocs.html
+++ b/Ghidra/Framework/Docking/src/main/help/help/topics/Theming/ThemingDeveloperDocs.html
@@ -333,10 +333,10 @@ color.fg.listing.bytes = orange
Properties defined by the theming system do not follow this pattern. To reference a
- property that does not have a standard prefix, an ID can be prefixed with [color]
+ property that does not have a standard prefix, an ID can be prefixed with [color]
, [font], or [icon] as appropriate to allow the theme
property parser to recognize the values as IDs to other properties. For example, to refer to a
- system property namedsystem.color.bg.view,
+ system property named system.color.bg.view,
you would use the following definition:
- A list of palette colors has been defined in gui.palette.theme.properties..
+ A list of palette colors has been defined in gui.palette.theme.properties.
These palette colors values are meant to be used by developers to reduce the total number
of colors used in the application. These color ids and values are viewable in the
Theme Editor Dialog.
diff --git a/Ghidra/Framework/Docking/src/main/help/help/topics/Theming/ThemingInternals.html b/Ghidra/Framework/Docking/src/main/help/help/topics/Theming/ThemingInternals.html
index b7629ee79d..0cae3798a4 100644
--- a/Ghidra/Framework/Docking/src/main/help/help/topics/Theming/ThemingInternals.html
+++ b/Ghidra/Framework/Docking/src/main/help/help/topics/Theming/ThemingInternals.html
@@ -38,8 +38,8 @@
ColorValue, FontValue, and
IconValue. Resource values are stored in these ThemeValue sub-classes
because the value can be
- either a concrete value or be a reference to some other resource of the same type. So, for
- example, "color.bg.foo" could map directly to an actual color or its value could be reference
+ either a concrete value or a reference to some other resource of the same type. So, for
+ example, "color.bg.foo" could map directly to an actual color or its value could be a reference
to some other indirect color like "color.bg.bar". In any ThemeValue object, either
the referenced ID or the direct value must be null. To get the ultimate concrete value, there
is a convenience method called get() on ThemeValues that takes a
diff --git a/Ghidra/Framework/Docking/src/main/help/help/topics/Theming/ThemingOverview.html b/Ghidra/Framework/Docking/src/main/help/help/topics/Theming/ThemingOverview.html
index 66ba9d24c2..fa27ddaa11 100644
--- a/Ghidra/Framework/Docking/src/main/help/help/topics/Theming/ThemingOverview.html
+++ b/Ghidra/Framework/Docking/src/main/help/help/topics/Theming/ThemingOverview.html
@@ -2,7 +2,7 @@
- General Overivew
+ Theming Overivew
@@ -86,7 +86,7 @@
themes to choose from and by simply switching the theme, the system will then update the Look
and Feel along with the colors, fonts, and icons, all with one action. The set of themes to
choose from is a mix of built-in themes and saved custom themes. There is one built-in theme
- for each supported Look and Feel. The chosen theme will use the UI values defied by the Look
+ for each supported Look and Feel. The chosen theme will use the UI values defined by the Look
and Feel, as well as all the values for the defined property IDs. Users are able to
create custom themes to change any color, font, or icon defined by the application, along with
UI values supplied by the associated Look and Feel.
Theme Property Names (also referred to as IDs or keys) that are defined by the application
- use a common format to help make sorting and viewing properties more intuitive as to their use. See
- the Developer Documentation for more details on the property ID
- format and naming conventions.
+ use a common format to help make sorting and viewing properties more intuitive as to their use.
+ See the Developer Documentation for more
+ details on the property ID format and naming conventions.
Theme Files
@@ -243,10 +243,10 @@
Theme Files are used to store saved custom themes. They are simple text files and are
stored in the user's home application directory under
- <home>/.ghidra/.ghidra-<version>/themes. The first three properties are always the
- theme name, the Look and Feel name, and whether the theme uses standard defaults or dark
- defaults. Finally, there is a list of overridden property "name = value" lines. The format
- is:
+ <home>/.ghidra/.ghidra-<version>/themes. The first three properties
+ are always the theme name, the Look and Feel name, and whether the theme uses standard
+ defaults or dark defaults. Finally, there is a list of overridden property "name = value"
+ lines. The format is:
Each property line is expected to begin with either "color.", "font.", or "icon." Since
- java defined properties don't start with these prefixes, they will have "[color]", "[font]",
- or "[icon]" prepended to their property name. These brackets are only used to aid in
- parsing this file. When the properties are used in Ghidra, the bracketed prefixes are
+ java defined properties don't start with these prefixes, they will have "[laf.color]",
+ "[laf.font]", or "[laf.icon]" prepended to their property name. These brackets are only used
+ to aid in parsing this file. When the properties are used in Ghidra, the bracketed prefixes are
removed.
Also, note that the values of these properties can reference other property names. If the
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/ReusableDialogComponentProvider.java b/Ghidra/Framework/Docking/src/main/java/docking/ReusableDialogComponentProvider.java
index a8db9a4509..22ee4c84ea 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/ReusableDialogComponentProvider.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/ReusableDialogComponentProvider.java
@@ -24,7 +24,7 @@ import ghidra.util.task.Task;
/**
* A version of {@link DialogComponentProvider} for clients to extend when they intend for their
- * dialog to be reused. Typically, dialogs are used once and then no longer referenced.
+ * dialog to be reused. Typically, dialogs are used once and then no longer referenced.
* Alternatively, some clients create a dialog and use it for the lifetime of their code. This
* is typical of non-modal plugins.
*
@@ -32,7 +32,7 @@ import ghidra.util.task.Task;
* with the dialog, such as in your plugin's {@code dispose()} method.
*
* The primary benefit of using this dialog is that any updates to the current theme will update
- * this dialog, even when the dialog is not visible. For dialogs that extend
+ * this dialog, even when the dialog is not visible. For dialogs that extend
* {@link DialogComponentProvider} directly, they only receive theme updates if they are visible.
*
* @see DialogComponentProvider
@@ -62,12 +62,7 @@ public class ReusableDialogComponentProvider extends DialogComponentProvider {
}
private void themeChanged(ThemeEvent ev) {
- if (!ev.isLookAndFeelChanged()) {
- return; // we only care if the Look and Feel changes
- }
-
- // if we are visible, then we don't need to update as the system updates all
- // visible components
+ // if we are visible, then we don't need to update as the system updates all visible components
if (isVisible()) {
return;
}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/action/ComponentThemeInspectorAction.java b/Ghidra/Framework/Docking/src/main/java/docking/action/ComponentThemeInspectorAction.java
index 798feb4a35..41c0b19997 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/action/ComponentThemeInspectorAction.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/action/ComponentThemeInspectorAction.java
@@ -127,6 +127,7 @@ public class ComponentThemeInspectorAction extends DockingAction {
Color bg = component.getBackground();
Color fg = component.getForeground();
+ Font font = component.getFont();
String id;
String clazz = component.getClass().getSimpleName();
if (clazz.isEmpty()) {
@@ -175,6 +176,10 @@ public class ComponentThemeInspectorAction extends DockingAction {
.append(spacer)
.append("fg: ")
.append(fgText)
+ .append(tabs)
+ .append(spacer)
+ .append("font: ")
+ .append(font)
.append('\n');
}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeEditorDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeEditorDialog.java
index 7468128461..9dcf7da99c 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeEditorDialog.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeEditorDialog.java
@@ -174,6 +174,18 @@ public class ThemeEditorDialog extends DialogComponentProvider {
updateButtons();
}
+ private void resetSelectedLookAndFeel() {
+ Swing.runLater(() -> {
+ try {
+ combo.removeItemListener(comboListener);
+ combo.setSelectedItem(themeManager.getActiveTheme().getLookAndFeelType());
+ }
+ finally {
+ combo.addItemListener(comboListener);
+ }
+ });
+ }
+
private void themeComboChanged(ItemEvent e) {
if (e.getStateChange() != ItemEvent.SELECTED) {
@@ -181,24 +193,43 @@ public class ThemeEditorDialog extends DialogComponentProvider {
}
LafType lafType = (LafType) e.getItem();
- Swing.runLater(() -> {
+ if (!themeManager.hasThemeValueChanges()) {
+ setLookAndFeel(lafType);
+ return;
+ }
- themeManager.setLookAndFeel(lafType, lafType.usesDarkDefaults());
- if (lafType == LafType.GTK) {
- setStatusText(
- "Warning - Themes using the GTK LookAndFeel do not support changing java " +
- "component colors, fonts or icons.",
- MessageType.ERROR);
- }
- else {
- setStatusText("");
- }
- colorTree.rebuild();
- colorTable.reloadAll();
- paletteTable.reloadAll();
- fontTable.reloadAll();
- iconTable.reloadAll();
- });
+ //@formatter:off
+ int result = OptionDialog.showOptionDialog(null, "Discard Changes?",
+ "Changing the Look and Feel type will cause you to lose your changes.\n" +
+ "If you would like to keep your changes, cancel this dialog and then save the theme\n" +
+ "Would you like to continue?",
+ "Lose Changes");
+ //@formatter:on
+ if (result == OptionDialog.CANCEL_OPTION) {
+ resetSelectedLookAndFeel();
+ return;
+ }
+
+ setLookAndFeel(lafType);
+ }
+
+ private void setLookAndFeel(LafType lafType) {
+
+ themeManager.setLookAndFeel(lafType, lafType.usesDarkDefaults());
+ if (lafType == LafType.GTK) {
+ setStatusText(
+ "Warning - Themes using the GTK LookAndFeel do not support changing java " +
+ "component colors, fonts or icons.",
+ MessageType.WARNING);
+ }
+ else {
+ setStatusText("");
+ }
+ colorTree.rebuild();
+ colorTable.reloadAll();
+ paletteTable.reloadAll();
+ fontTable.reloadAll();
+ iconTable.reloadAll();
}
private void updateButtons() {
diff --git a/Ghidra/Framework/Gui/data/gui.theme.properties b/Ghidra/Framework/Gui/data/gui.theme.properties
index 8576fa0a70..4b98c4ec43 100644
--- a/Ghidra/Framework/Gui/data/gui.theme.properties
+++ b/Ghidra/Framework/Gui/data/gui.theme.properties
@@ -4,9 +4,6 @@
color.bg = [color]system.color.bg.view
color.fg = [color]system.color.fg.view
-// On some LaFs the tables and trees use the bg color we define. Make that consistent for all LaFs.
-[color]Viewport.background = color.bg
-
color.fg.error = color.palette.red
color.fg.disabled = color.palette.lightgray
color.bg.uneditable = [color]system.color.bg.control
@@ -40,6 +37,27 @@ font.standard = [font]system.font.control
font.monospaced = monospaced-PLAIN-12
+//
+// Java LaF Fixups
+//
+// Prefer buttons that change on hover
+[laf.boolean]Button.rollover = true
+[laf.boolean]Toolbar.isRollover = true
+
+// Java 1.6 UI consumes MousePressed event when dismissing popup menu
+// which prevents application components from getting this event.
+[laf.boolean]PopupMenu.consumeEventOnClose = false
+
+// On some LaFs the tables and trees use the bg color we define. Make that consistent for all LaFs.
+[laf.color]Viewport.background = color.bg
+
+// Fix up the default fonts that Java 1.5.0 changed to Courier
+[laf.font]TextArea.font = font.monospaced
+[laf.font]PasswordField.font = font.monospaced
+
+
+
+
// Icons files
icon.flag = flag.png
icon.lock = kgpg.png
@@ -108,3 +126,15 @@ color.cursor.unfocused = color.palette.darkgray
icon.make.selection = stack.png
+
+
+
+[Flat Dark]
+
+[laf.boolean]ToolBar.focusableButtons = true
+
+[Flat Light]
+
+[laf.boolean]ToolBar.focusableButtons = true
+
+
diff --git a/Ghidra/Framework/Gui/src/main/java/generic/theme/AbstractThemeReader.java b/Ghidra/Framework/Gui/src/main/java/generic/theme/AbstractThemeReader.java
index 62f2eda453..583f46dbea 100644
--- a/Ghidra/Framework/Gui/src/main/java/generic/theme/AbstractThemeReader.java
+++ b/Ghidra/Framework/Gui/src/main/java/generic/theme/AbstractThemeReader.java
@@ -94,6 +94,18 @@ public abstract class AbstractThemeReader {
reportDuplicateKey(oldValue, lineNumber);
}
}
+
+ // 'external' look and feel property used by the UIManager
+ else if (BooleanPropertyValue.isBooleanKey(key)) {
+ JavaPropertyValue oldValue =
+ valueMap.addProperty(parseBooleanProperty(key, value, lineNumber));
+ reportDuplicateKey(oldValue, lineNumber);
+ }
+ else if (StringPropertyValue.isStringKey(key)) {
+ JavaPropertyValue oldValue =
+ valueMap.addProperty(parseStringProperty(key, value, lineNumber));
+ reportDuplicateKey(oldValue, lineNumber);
+ }
else {
error(lineNumber, "Can't process property: " + key + " = " + value);
}
@@ -143,6 +155,22 @@ public abstract class AbstractThemeReader {
return parsedValue;
}
+ private BooleanPropertyValue parseBooleanProperty(String key, String value, int lineNumber) {
+ BooleanPropertyValue parsedValue = BooleanPropertyValue.parse(key, value);
+ if (parsedValue == null) {
+ error(lineNumber, "Could not parse boolean property value: " + value);
+ }
+ return parsedValue;
+ }
+
+ private StringPropertyValue parseStringProperty(String key, String value, int lineNumber) {
+ StringPropertyValue parsedValue = StringPropertyValue.parse(key, value);
+ if (parsedValue == null) {
+ error(lineNumber, "Could not parse String property value: " + value);
+ }
+ return parsedValue;
+ }
+
private List readSections(LineNumberReader reader) throws IOException {
List sections = new ArrayList<>();
@@ -208,7 +236,7 @@ public abstract class AbstractThemeReader {
}
/**
- * Represents all the value found in a section of the theme properties file. Sections are
+ * Represents all the value found in a section of the theme properties file. Sections are
* defined by a line containing just "[section name]"
*/
protected class Section {
@@ -287,7 +315,7 @@ public abstract class AbstractThemeReader {
}
/**
- * Adds a raw line from the file to this section. The line will be parsed into a a
+ * Adds a raw line from the file to this section. The line will be parsed into a a
* key-value pair.
* @param line the line to be added/parsed
* @param lineNumber the line number in the file for this line
diff --git a/Ghidra/Framework/Gui/src/main/java/generic/theme/ApplicationThemeManager.java b/Ghidra/Framework/Gui/src/main/java/generic/theme/ApplicationThemeManager.java
index e5be537a4a..c7d8eb4682 100644
--- a/Ghidra/Framework/Gui/src/main/java/generic/theme/ApplicationThemeManager.java
+++ b/Ghidra/Framework/Gui/src/main/java/generic/theme/ApplicationThemeManager.java
@@ -73,7 +73,7 @@ public class ApplicationThemeManager extends ThemeManager {
@Override
public void restoreThemeValues() {
- applicationDefaults = getApplicationDefaults();
+ applicationDefaults = loadApplicationDefaults();
buildCurrentValues();
lookAndFeelManager.resetAll(javaDefaults);
notifyThemeChanged(new AllValuesChangedThemeEvent(false));
@@ -288,6 +288,11 @@ public class ApplicationThemeManager extends ThemeManager {
return false;
}
+ @Override
+ public boolean hasThemeValueChanges() {
+ return !changedValuesMap.isEmpty();
+ }
+
@Override
public void registerFont(Component component, String fontId) {
lookAndFeelManager.registerFont(component, fontId);
diff --git a/Ghidra/Framework/Gui/src/main/java/generic/theme/BooleanPropertyValue.java b/Ghidra/Framework/Gui/src/main/java/generic/theme/BooleanPropertyValue.java
new file mode 100644
index 0000000000..1836e9a616
--- /dev/null
+++ b/Ghidra/Framework/Gui/src/main/java/generic/theme/BooleanPropertyValue.java
@@ -0,0 +1,79 @@
+/* ###
+ * 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 ghidra.util.Msg;
+
+/**
+ * A Java property value for keys that use boolean values.
+ */
+public class BooleanPropertyValue extends JavaPropertyValue {
+
+ private static final String EXTERNAL_LAF_ID_PREFIX = "[laf.boolean]";
+
+ public BooleanPropertyValue(String id, boolean value) {
+ this(id, null, value);
+ }
+
+ public BooleanPropertyValue(String id, String refId, Boolean value) {
+ super(id, refId, value);
+ }
+
+ public static boolean isBooleanKey(String key) {
+ return key.toLowerCase().startsWith(EXTERNAL_LAF_ID_PREFIX);
+ }
+
+ public static BooleanPropertyValue parse(String key, String value) {
+ String id = fromExternalId(key);
+
+ if (isBooleanKey(value)) {
+ String refId = fromExternalId(value);
+ return new BooleanPropertyValue(key, refId, null);
+ }
+
+ boolean b = Boolean.parseBoolean(value);
+ return new BooleanPropertyValue(id, b);
+ }
+
+ private static String fromExternalId(String externalId) {
+ if (!externalId.toLowerCase().startsWith(EXTERNAL_LAF_ID_PREFIX)) {
+ return externalId;
+ }
+
+ // We return the raw property name (e.g., TextArea.background), not the normalized name
+ // (e.g., laf.color.TextArea.background), since the system currently does not provide the
+ // end-user a way to change these values from the UI.
+ return externalId.substring(EXTERNAL_LAF_ID_PREFIX.length());
+ }
+
+ @Override
+ protected Object getUnresolvedReferenceValue(String primaryId, String unresolvedId) {
+ Msg.warn(this,
+ "Could not resolve indirect property for \"" + unresolvedId +
+ "\" for primary id \"" + primaryId + "\", using last resort default");
+ return false;
+ }
+
+ @Override
+ protected String toExternalId(String internalId) {
+ return EXTERNAL_LAF_ID_PREFIX + internalId;
+ }
+
+ @Override
+ protected String getSerializedValue() {
+ return Boolean.toString((Boolean) value);
+ }
+}
diff --git a/Ghidra/Framework/Gui/src/main/java/generic/theme/ColorValue.java b/Ghidra/Framework/Gui/src/main/java/generic/theme/ColorValue.java
index 55c28ed3b1..a4e95c4b15 100644
--- a/Ghidra/Framework/Gui/src/main/java/generic/theme/ColorValue.java
+++ b/Ghidra/Framework/Gui/src/main/java/generic/theme/ColorValue.java
@@ -17,6 +17,8 @@ package generic.theme;
import java.awt.Color;
+import org.apache.commons.lang3.StringUtils;
+
import ghidra.util.Msg;
import ghidra.util.WebColors;
import utilities.util.reflection.ReflectionUtilities;
@@ -28,6 +30,10 @@ 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 {
+
+ public static final String LAF_ID_PREFIX = "laf.color.";
+ public static final String EXTERNAL_LAF_ID_PREFIX = "[laf.color]";
+
private static final String COLOR_ID_PREFIX = "color.";
private static final String EXTERNAL_PREFIX = "[color]";
@@ -65,13 +71,14 @@ public class ColorValue extends ThemeValue {
return !id.startsWith(COLOR_ID_PREFIX);
}
- /**
+ /**
* 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);
+ return StringUtils.startsWithAny(key, COLOR_ID_PREFIX, EXTERNAL_PREFIX,
+ EXTERNAL_LAF_ID_PREFIX);
}
/**
@@ -115,6 +122,12 @@ public class ColorValue extends ThemeValue {
if (internalId.startsWith(COLOR_ID_PREFIX)) {
return internalId;
}
+
+ if (internalId.startsWith(LAF_ID_PREFIX)) {
+ String baseId = internalId.substring(LAF_ID_PREFIX.length());
+ return EXTERNAL_LAF_ID_PREFIX + baseId;
+ }
+
return EXTERNAL_PREFIX + internalId;
}
@@ -122,6 +135,9 @@ public class ColorValue extends ThemeValue {
if (externalId.startsWith(EXTERNAL_PREFIX)) {
return externalId.substring(EXTERNAL_PREFIX.length());
}
+ if (externalId.startsWith(EXTERNAL_LAF_ID_PREFIX)) {
+ return LAF_ID_PREFIX + externalId.substring(EXTERNAL_LAF_ID_PREFIX.length());
+ }
return externalId;
}
diff --git a/Ghidra/Framework/Gui/src/main/java/generic/theme/FontValue.java b/Ghidra/Framework/Gui/src/main/java/generic/theme/FontValue.java
index af304469d8..dac9fbf36f 100644
--- a/Ghidra/Framework/Gui/src/main/java/generic/theme/FontValue.java
+++ b/Ghidra/Framework/Gui/src/main/java/generic/theme/FontValue.java
@@ -18,6 +18,8 @@ package generic.theme;
import java.awt.Font;
import java.text.ParseException;
+import org.apache.commons.lang3.StringUtils;
+
import ghidra.util.Msg;
/**
@@ -27,9 +29,14 @@ import ghidra.util.Msg;
* and if the class's refId is non-null, then the font value will be null.
*/
public class FontValue extends ThemeValue {
+
+ public static final String LAF_ID_PREFIX = "laf.font.";
+ public static final String EXTERNAL_LAF_ID_PREFIX = "[laf.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]";
+
+ public static final Font LAST_RESORT_DEFAULT = new Font("monospaced", Font.PLAIN, 12);
private FontModifier modifier;
/**
@@ -97,13 +104,14 @@ public class FontValue extends ThemeValue {
return String.format("%s-%s-%s", font.getName(), getStyleString(font), font.getSize());
}
- /**
+ /**
* Returns true if the given key string is a valid external key for a font value
* @param key the key string to test
* @return true if the given key string is a valid external key for a font value
*/
public static boolean isFontKey(String key) {
- return key.startsWith(FONT_ID_PREFIX) || key.startsWith(EXTERNAL_PREFIX);
+ return StringUtils.startsWithAny(key, FONT_ID_PREFIX, EXTERNAL_PREFIX,
+ EXTERNAL_LAF_ID_PREFIX);
}
/**
@@ -112,7 +120,7 @@ public class FontValue extends ThemeValue {
* @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 if there is an exception parsing
+ * @throws ParseException if there is an exception parsing
*/
public static FontValue parse(String key, String value) throws ParseException {
String id = fromExternalId(key);
@@ -164,6 +172,12 @@ public class FontValue extends ThemeValue {
if (internalId.startsWith(FONT_ID_PREFIX)) {
return internalId;
}
+
+ if (internalId.startsWith(LAF_ID_PREFIX)) {
+ String baseId = internalId.substring(LAF_ID_PREFIX.length());
+ return EXTERNAL_LAF_ID_PREFIX + baseId;
+ }
+
return EXTERNAL_PREFIX + internalId;
}
@@ -171,6 +185,9 @@ public class FontValue extends ThemeValue {
if (externalId.startsWith(EXTERNAL_PREFIX)) {
return externalId.substring(EXTERNAL_PREFIX.length());
}
+ if (externalId.startsWith(EXTERNAL_LAF_ID_PREFIX)) {
+ return LAF_ID_PREFIX + externalId.substring(EXTERNAL_LAF_ID_PREFIX.length());
+ }
return externalId;
}
@@ -198,9 +215,7 @@ public class FontValue extends ThemeValue {
}
private static FontValue getRefFontValue(String id, String value) throws ParseException {
- if (value.startsWith(EXTERNAL_PREFIX)) {
- value = value.substring(EXTERNAL_PREFIX.length());
- }
+ value = fromExternalId(value);
int modIndex = value.indexOf("[");
if (modIndex < 0) {
return new FontValue(id, fromExternalId(value));
diff --git a/Ghidra/Framework/Gui/src/main/java/generic/theme/GThemeValueMap.java b/Ghidra/Framework/Gui/src/main/java/generic/theme/GThemeValueMap.java
index 43d601b784..18c7b5b2f5 100644
--- a/Ghidra/Framework/Gui/src/main/java/generic/theme/GThemeValueMap.java
+++ b/Ghidra/Framework/Gui/src/main/java/generic/theme/GThemeValueMap.java
@@ -33,6 +33,7 @@ public class GThemeValueMap {
protected Map colorMap = new HashMap<>();
protected Map fontMap = new HashMap<>();
protected Map iconMap = new HashMap<>();
+ protected Map propertyMap = new HashMap<>();
/**
* Constructs a new empty map.
@@ -88,6 +89,19 @@ public class GThemeValueMap {
return null;
}
+ /**
+ * Adds the given property value to this map. If a property value already exists in the map with
+ * the same id, it will be replaced.
+ * @param value the {@link JavaPropertyValue} to store in the map.
+ * @return the previous value for the icon key or null if no previous value existed.
+ */
+ public JavaPropertyValue addProperty(JavaPropertyValue value) {
+ if (value != null) {
+ return propertyMap.put(value.getId(), value);
+ }
+ return null;
+ }
+
/**
* Returns the current {@link ColorValue} for the given id or null if none exists.
* @param id the id to look up a color for
@@ -116,7 +130,16 @@ public class GThemeValueMap {
}
/**
- * Loads all the values from the given map into this map, replacing values with the
+ * Returns the current {@link JavaPropertyValue} for the given id or null if none exists.
+ * @param id the id to look up a icon for
+ * @return the current {@link JavaPropertyValue} for the given id or null if none exists.
+ */
+ public JavaPropertyValue getProperty(String id) {
+ return propertyMap.get(id);
+ }
+
+ /**
+ * Loads all the values from the given map into this map, replacing values with the
* same ids.
* @param valueMap the map whose values are to be loaded into this map
*/
@@ -127,6 +150,7 @@ public class GThemeValueMap {
valueMap.colorMap.values().forEach(v -> addColor(v));
valueMap.fontMap.values().forEach(v -> addFont(v));
valueMap.iconMap.values().forEach(v -> addIcon(v));
+ valueMap.propertyMap.values().forEach(v -> addProperty(v));
}
/**
@@ -153,6 +177,14 @@ public class GThemeValueMap {
return new ArrayList<>(iconMap.values());
}
+ /**
+ * Returns a list of all the {@link JavaPropertyValue}s stored in this map.
+ * @return a list of all the {@link JavaPropertyValue}s stored in this map.
+ */
+ public List getProperties() {
+ return new ArrayList<>(propertyMap.values());
+ }
+
/**
* Returns true if a {@link ColorValue} exists in this map for the given id.
* @param id the id to check
@@ -181,11 +213,20 @@ public class GThemeValueMap {
}
/**
- * Returns the total number of color, font, and icon values stored in this map
- * @return the total number of color, font, and icon values stored in this map
+ * Returns true if an {@link JavaPropertyValue} exists in this map for the given id.
+ * @param id the id to check
+ * @return true if an {@link JavaPropertyValue} exists in this map for the given id
+ */
+ public boolean containsProperty(String id) {
+ return propertyMap.containsKey(id);
+ }
+
+ /**
+ * Returns the total number of color, font, icon and property values stored in this map
+ * @return the total number of color, font, icon and property values stored in this map
*/
public Object size() {
- return colorMap.size() + fontMap.size() + iconMap.size();
+ return colorMap.size() + fontMap.size() + iconMap.size() + propertyMap.size();
}
/**
@@ -195,14 +236,16 @@ public class GThemeValueMap {
colorMap.clear();
fontMap.clear();
iconMap.clear();
+ propertyMap.clear();
}
/**
- * Returns true if there are not color, font, or icon values in this map
- * @return true if there are not color, font, or icon values in this map
+ * Returns true if there are not color, font, icon or property values in this map
+ * @return true if there are not color, font, icon or property values in this map
*/
public boolean isEmpty() {
- return colorMap.isEmpty() && fontMap.isEmpty() && iconMap.isEmpty();
+ return colorMap.isEmpty() && fontMap.isEmpty() && iconMap.isEmpty() &&
+ propertyMap.isEmpty();
}
/**
@@ -229,10 +272,18 @@ public class GThemeValueMap {
iconMap.remove(id);
}
+ /**
+ * removes any {@link JavaPropertyValue} with the given id from this map.
+ * @param id the id to remove
+ */
+ public void removeProperty(String id) {
+ propertyMap.remove(id);
+ }
+
/**
* Returns a new {@link GThemeValueMap} that is only populated by values that don't exist
* in the give map.
- * @param base the set of values (usually the default set) to compare against to determine
+ * @param base the set of values (usually the default set) to compare against to determine
* what values are changed.
* @return a new {@link GThemeValueMap} that is only populated by values that don't exist
* in the give map
@@ -254,13 +305,18 @@ public class GThemeValueMap {
map.addIcon(icon);
}
}
+ for (JavaPropertyValue property : propertyMap.values()) {
+ if (!property.equals(base.getProperty(property.getId()))) {
+ map.addProperty(property);
+ }
+ }
return map;
}
/**
* Gets the set of icon (.png, .gif) files that are used by IconValues that came from files
- * versus resources in the classpath. These are the icon files that need to be included
- * when exporting this set of values to a zip file.
+ * versus resources in the classpath. These are the icon files that need to be included when
+ * exporting this set of values to a zip file.
* @return the set of icon (.png, .gif) files that are used by IconValues that came from files
* versus resources in the classpath
*/
@@ -268,18 +324,24 @@ public class GThemeValueMap {
Set files = new HashSet<>();
for (IconValue iconValue : iconMap.values()) {
Icon icon = iconValue.getRawValue();
- if (icon instanceof UrlImageIcon urlIcon) {
- String originalPath = urlIcon.getOriginalPath();
- if (originalPath.startsWith(ResourceManager.EXTERNAL_ICON_PREFIX)) {
- URL url = urlIcon.getUrl();
- String filePath = url.getFile();
- if (filePath != null) {
- File iconFile = new File(filePath);
- if (iconFile.exists()) {
- files.add(iconFile);
- }
- }
- }
+ if (!(icon instanceof UrlImageIcon urlIcon)) {
+ continue;
+ }
+
+ String originalPath = urlIcon.getOriginalPath();
+ if (!originalPath.startsWith(ResourceManager.EXTERNAL_ICON_PREFIX)) {
+ continue;
+ }
+
+ URL url = urlIcon.getUrl();
+ String filePath = url.getFile();
+ if (filePath == null) {
+ continue;
+ }
+
+ File iconFile = new File(filePath);
+ if (iconFile.exists()) {
+ files.add(iconFile);
}
}
return files;
@@ -287,7 +349,7 @@ public class GThemeValueMap {
@Override
public int hashCode() {
- return Objects.hash(colorMap, fontMap, iconMap);
+ return Objects.hash(colorMap, fontMap, iconMap, propertyMap);
}
@Override
@@ -302,8 +364,10 @@ public class GThemeValueMap {
return false;
}
GThemeValueMap other = (GThemeValueMap) obj;
- return Objects.equals(colorMap, other.colorMap) && Objects.equals(fontMap, other.fontMap) &&
- Objects.equals(iconMap, other.iconMap);
+ return Objects.equals(colorMap, other.colorMap) &&
+ Objects.equals(fontMap, other.fontMap) &&
+ Objects.equals(iconMap, other.iconMap) &&
+ Objects.equals(propertyMap, other.propertyMap);
}
public void checkForUnresolvedReferences() {
@@ -317,6 +381,9 @@ public class GThemeValueMap {
for (IconValue iconValue : iconMap.values()) {
iconValue.get(this);
}
+ for (JavaPropertyValue propertyValue : propertyMap.values()) {
+ propertyValue.get(this);
+ }
}
/**
@@ -344,10 +411,18 @@ public class GThemeValueMap {
}
/**
- * Returns the resolved color, following indirections as need to get the color ultimately
+ * Returns the set of all Java property ids in this map
+ * @return the set of all Java property ids in this map
+ */
+ public Set getPropertyIds() {
+ return propertyMap.keySet();
+ }
+
+ /**
+ * Returns the resolved color, following indirections as needed to get the color ultimately
* assigned to the given id.
* @param id the id for which to get a color
- * @return the resolved color, following indirections as need to get the color ultimately
+ * @return the resolved color, following indirections as needed to get the color ultimately
* assigned to the given id.
*/
public Color getResolvedColor(String id) {
@@ -359,10 +434,10 @@ public class GThemeValueMap {
}
/**
- * Returns the resolved font, following indirections as need to get the font ultimately
+ * Returns the resolved font, following indirections as needed to get the font ultimately
* assigned to the given id.
* @param id the id for which to get a font
- * @return the resolved font, following indirections as need to get the font ultimately
+ * @return the resolved font, following indirections as needed to get the font ultimately
* assigned to the given id
*/
public Font getResolvedFont(String id) {
@@ -374,10 +449,10 @@ public class GThemeValueMap {
}
/**
- * Returns the resolved icon, following indirections as need to get the icon ultimately
+ * Returns the resolved icon, following indirections as needed to get the icon ultimately
* assigned to the given id.
* @param id the id for which to get an icon
- * @return the resolved icon, following indirections as need to get the icon ultimately
+ * @return the resolved icon, following indirections as needed to get the icon ultimately
* assigned to the given id
*/
public Icon getResolvedIcon(String id) {
@@ -388,4 +463,18 @@ public class GThemeValueMap {
return null;
}
+ /**
+ * Returns the resolved property, following indirections as needed to get the property
+ * ultimately assigned to the given id.
+ * @param id the id for which to get an property
+ * @return the resolved property, following indirections as needed to get the property
+ * ultimately assigned to the given id
+ */
+ public Object getResolvedProperty(String id) {
+ JavaPropertyValue propertyValue = propertyMap.get(id);
+ if (propertyValue != null) {
+ return propertyValue.get(this);
+ }
+ return null;
+ }
}
diff --git a/Ghidra/Framework/Gui/src/main/java/generic/theme/IconValue.java b/Ghidra/Framework/Gui/src/main/java/generic/theme/IconValue.java
index b2767a0b4b..45af8edf42 100644
--- a/Ghidra/Framework/Gui/src/main/java/generic/theme/IconValue.java
+++ b/Ghidra/Framework/Gui/src/main/java/generic/theme/IconValue.java
@@ -19,6 +19,8 @@ import java.text.ParseException;
import javax.swing.Icon;
+import org.apache.commons.lang3.StringUtils;
+
import ghidra.util.Msg;
import resources.ResourceManager;
import resources.icons.EmptyIcon;
@@ -33,14 +35,14 @@ import resources.icons.UrlImageIcon;
public class IconValue extends ThemeValue {
private static final String EMPTY_ICON_STRING = "EMPTY_ICON";
+ public static final String LAF_ID_PREFIX = "laf.icon.";
+ public static final String EXTERNAL_LAF_ID_PREFIX = "[laf.icon]";
+
static final String ICON_ID_PREFIX = "icon.";
-
- public static final Icon LAST_RESORT_DEFAULT = ResourceManager.getDefaultIcon();
-
private static final String EXTERNAL_PREFIX = "[icon]";
+ public static final Icon LAST_RESORT_DEFAULT = ResourceManager.getDefaultIcon();
private static final int STANDARD_EMPTY_ICON_SIZE = 16;
-
private IconModifier modifier;
/**
@@ -94,13 +96,14 @@ public class IconValue extends ThemeValue {
return icon;
}
- /**
+ /**
* 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);
+ return StringUtils.startsWithAny(key, ICON_ID_PREFIX, EXTERNAL_PREFIX,
+ EXTERNAL_LAF_ID_PREFIX);
}
/**
@@ -172,9 +175,7 @@ public class IconValue extends ThemeValue {
}
private static IconValue parseRefIcon(String id, String value) throws ParseException {
- if (value.startsWith(EXTERNAL_PREFIX)) {
- value = value.substring(EXTERNAL_PREFIX.length());
- }
+ value = fromExternalId(value);
int modifierIndex = getModifierIndex(value);
if (modifierIndex < 0) {
return new IconValue(id, value);
@@ -213,6 +214,12 @@ public class IconValue extends ThemeValue {
if (internalId.startsWith(ICON_ID_PREFIX)) {
return internalId;
}
+
+ if (internalId.startsWith(LAF_ID_PREFIX)) {
+ String baseId = internalId.substring(LAF_ID_PREFIX.length());
+ return EXTERNAL_LAF_ID_PREFIX + baseId;
+ }
+
return EXTERNAL_PREFIX + internalId;
}
@@ -220,6 +227,9 @@ public class IconValue extends ThemeValue {
if (externalId.startsWith(EXTERNAL_PREFIX)) {
return externalId.substring(EXTERNAL_PREFIX.length());
}
+ if (externalId.startsWith(EXTERNAL_LAF_ID_PREFIX)) {
+ return LAF_ID_PREFIX + externalId.substring(EXTERNAL_LAF_ID_PREFIX.length());
+ }
return externalId;
}
diff --git a/Ghidra/Framework/Gui/src/main/java/generic/theme/JavaPropertyValue.java b/Ghidra/Framework/Gui/src/main/java/generic/theme/JavaPropertyValue.java
new file mode 100644
index 0000000000..7fce2d518a
--- /dev/null
+++ b/Ghidra/Framework/Gui/src/main/java/generic/theme/JavaPropertyValue.java
@@ -0,0 +1,55 @@
+/* ###
+ * 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;
+
+/**
+ * A base class that represents a Java UIManager property. This value is used to allow for
+ * overriding Java UI values using the theme properties files.
+ */
+public abstract class JavaPropertyValue extends ThemeValue