diff --git a/Ghidra/Features/Base/src/main/java/ghidra/base/help/GhidraHelpService.java b/Ghidra/Features/Base/src/main/java/ghidra/base/help/GhidraHelpService.java
index cb1595c10f..ffe1d46e84 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/base/help/GhidraHelpService.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/base/help/GhidraHelpService.java
@@ -24,8 +24,7 @@ import javax.help.HelpSetException;
import docking.help.*;
import generic.jar.ResourceFile;
-import generic.theme.Gui;
-import generic.theme.ThemeListener;
+import generic.theme.*;
import ghidra.framework.Application;
import ghidra.util.Msg;
import help.HelpService;
@@ -38,7 +37,7 @@ import resources.ResourceManager;
public class GhidraHelpService extends HelpManager {
private static final String MASTER_HELP_SET_HS = "Base_HelpSet.hs";
- private ThemeListener listener = t -> reload();
+ private ThemeListener listener = new HelpThemeListener();
public static void install() {
try {
@@ -120,4 +119,11 @@ public class GhidraHelpService extends HelpManager {
return results;
}
+
+ class HelpThemeListener implements ThemeListener {
+ @Override
+ public void themeChanged(GTheme newTheme) {
+ reload();
+ }
+ }
}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemProbeConflictResolver.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemProbeConflictResolver.java
index dbc3e1ab60..73b4ff3422 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemProbeConflictResolver.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemProbeConflictResolver.java
@@ -17,6 +17,7 @@ package ghidra.formats.gfilesystem;
import java.util.List;
+import docking.widgets.SelectFromListDialog;
import ghidra.formats.gfilesystem.factory.FileSystemInfoRec;
/**
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionManager.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionManager.java
index 26ecb0bca1..eec3c39acf 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionManager.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionManager.java
@@ -16,13 +16,12 @@
package ghidra.plugins.fsbrowser;
import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*;
-import static java.util.Map.entry;
-
-import java.util.*;
-import java.util.function.Function;
+import static java.util.Map.*;
import java.awt.Component;
import java.io.*;
+import java.util.*;
+import java.util.function.Function;
import javax.swing.*;
@@ -31,6 +30,7 @@ import org.apache.commons.io.FilenameUtils;
import docking.action.DockingAction;
import docking.action.builder.ActionBuilder;
import docking.widgets.OptionDialog;
+import docking.widgets.SelectFromListDialog;
import docking.widgets.dialogs.MultiLineMessageDialog;
import docking.widgets.filechooser.GhidraFileChooser;
import docking.widgets.filechooser.GhidraFileChooserMode;
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBUtils.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBUtils.java
index 7da421e420..0089879bb2 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBUtils.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBUtils.java
@@ -18,8 +18,8 @@ package ghidra.plugins.fsbrowser;
import java.util.ArrayList;
import java.util.List;
+import docking.widgets.SelectFromListDialog;
import ghidra.app.services.ProgramManager;
-import ghidra.formats.gfilesystem.SelectFromListDialog;
import ghidra.framework.plugintool.PluginTool;
import ghidra.util.Msg;
@@ -28,7 +28,6 @@ import ghidra.util.Msg;
*/
public class FSBUtils {
-
/**
* Returns the {@link ProgramManager} associated with this fs browser plugin.
*
diff --git a/Ghidra/Framework/Docking/certification.manifest b/Ghidra/Framework/Docking/certification.manifest
index 3e51efa9e1..5141d01af6 100644
--- a/Ghidra/Framework/Docking/certification.manifest
+++ b/Ghidra/Framework/Docking/certification.manifest
@@ -88,6 +88,8 @@ src/main/resources/images/left.png||GHIDRA||reviewed||END|
src/main/resources/images/locationIn.gif||GHIDRA||||END|
src/main/resources/images/locationOut.gif||GHIDRA||||END|
src/main/resources/images/magnifier.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
+src/main/resources/images/mail-folder-outbox.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END|
+src/main/resources/images/mail-receive.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END|
src/main/resources/images/media-playback-start.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END|
src/main/resources/images/menu16.gif||GHIDRA||reviewed||END|
src/main/resources/images/note-red.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END|
diff --git a/Ghidra/Framework/Docking/data/docking.theme.properties b/Ghidra/Framework/Docking/data/docking.theme.properties
index 6754ebfba0..11ca64cd94 100644
--- a/Ghidra/Framework/Docking/data/docking.theme.properties
+++ b/Ghidra/Framework/Docking/data/docking.theme.properties
@@ -80,6 +80,8 @@ icon.flag = images/flag.png
icon.lock = images/kgpg.png
icon.checkmark.green = images/checkmark_green.gif
+icon.theme.import = images/mail-receive.png
+icon.theme.export = images/mail-folder-outbox.png
[Dark Defaults]
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/options/editor/IconPropertyEditor.java b/Ghidra/Framework/Docking/src/main/java/docking/options/editor/IconPropertyEditor.java
index 6d3b42342e..368eb43b66 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/options/editor/IconPropertyEditor.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/options/editor/IconPropertyEditor.java
@@ -33,6 +33,7 @@ import docking.widgets.filechooser.GhidraFileChooserMode;
import docking.widgets.label.GDLabel;
import docking.widgets.list.GListCellRenderer;
import ghidra.framework.Application;
+import ghidra.framework.preferences.Preferences;
import ghidra.util.Msg;
import ghidra.util.filechooser.ExtensionFileFilter;
import resources.ResourceManager;
@@ -71,11 +72,13 @@ public class IconPropertyEditor extends PropertyEditorSupport {
if (icon instanceof UrlImageIcon urlIcon) {
return urlIcon.getOriginalPath();
}
- return "";
+ return "";
}
class IconChooserPanel extends JPanel {
+ private static final String IMAGE_DIR = "images/";
+ private static final String LAST_ICON_DIR_PREFERENCE_KEY = "IconEditor.lastDir";
private GDLabel previewLabel;
private DropDownSelectionTextField dropDown;
private IconDropDownDataModel dataModel;
@@ -151,8 +154,14 @@ public class IconPropertyEditor extends PropertyEditorSupport {
chooser.setFileSelectionMode(GhidraFileChooserMode.FILES_ONLY);
chooser.setSelectedFileFilter(
ExtensionFileFilter.forExtensions("Icon Files", ".png", "gif"));
+ String lastDir = Preferences.getProperty(LAST_ICON_DIR_PREFERENCE_KEY);
+ if (lastDir != null) {
+ chooser.setCurrentDirectory(new File(lastDir));
+ }
File file = chooser.getSelectedFile();
if (file != null) {
+ File dir = chooser.getCurrentDirectory();
+ Preferences.setProperty(LAST_ICON_DIR_PREFERENCE_KEY, dir.getAbsolutePath());
importIconFile(file);
}
}
@@ -164,8 +173,9 @@ public class IconPropertyEditor extends PropertyEditorSupport {
return;
}
File dir = Application.getUserSettingsDirectory();
- File destinationDir = new File(dir, "themes/images");
- File destinationFile = new File(destinationDir, file.getName());
+ String relativePath = IMAGE_DIR + file.getName();
+
+ File destinationFile = new File(dir, relativePath);
if (destinationFile.exists()) {
int result = OptionDialog.showYesNoDialog(dropDown, "Overwrite?",
"An icon with that name already exists.\n Do you want to overwrite it?");
@@ -175,7 +185,8 @@ public class IconPropertyEditor extends PropertyEditorSupport {
}
try {
FileUtils.copyFile(file, destinationFile);
- ImageIcon icon = ResourceManager.loadImage("themes/images/" + file.getName());
+ String path = ResourceManager.EXTERNAL_ICON_PREFIX + relativePath;
+ ImageIcon icon = ResourceManager.loadImage(path);
setValue(icon);
}
catch (IOException e) {
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ExportThemeDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ExportThemeDialog.java
index ee91a618a4..d4ea21b15e 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ExportThemeDialog.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ExportThemeDialog.java
@@ -24,7 +24,6 @@ import javax.swing.*;
import docking.DialogComponentProvider;
import docking.options.editor.ButtonPanelFactory;
-import docking.theme.*;
import docking.widgets.checkbox.GCheckBox;
import docking.widgets.filechooser.GhidraFileChooser;
import docking.widgets.filechooser.GhidraFileChooserMode;
@@ -40,10 +39,11 @@ public class ExportThemeDialog extends DialogComponentProvider {
private JTextField nameField;
private JTextField fileTextField;
private GCheckBox includeDefaultsCheckbox;
+ private boolean exportAsZip;
- protected ExportThemeDialog() {
+ public ExportThemeDialog(boolean exportAsZip) {
super("Export Theme");
-
+ this.exportAsZip = exportAsZip;
addWorkPanel(buildMainPanel());
addOKButton();
addCancelButton();
@@ -58,33 +58,45 @@ public class ExportThemeDialog extends DialogComponentProvider {
}
private boolean exportTheme() {
- File file = new File(fileTextField.getText());
String themeName = nameField.getText();
if (themeName.isBlank()) {
setStatusText("Missing Theme Name", MessageType.ERROR, true);
return false;
}
- boolean includeDefaults = includeDefaultsCheckbox.isSelected();
-
- GTheme activeTheme = Gui.getActiveTheme();
- FileGTheme fileTheme = new FileGTheme(file, themeName, activeTheme.getLookAndFeelType(),
- activeTheme.useDarkDefaults());
-
- if (includeDefaults) {
- fileTheme.load(Gui.getAllValues());
- }
- else {
- fileTheme.load(Gui.getNonDefaultValues());
- }
try {
- fileTheme.save();
+ FileGTheme exportTheme = createExternalTheme(themeName);
+ loadValues(exportTheme);
+ exportTheme.save();
return true;
}
catch (IOException e) {
Msg.error("Error Exporting Theme", "I/O Error encountered trying to export theme!", e);
- return false;
}
+ return false;
+ }
+
+ private void loadValues(FileGTheme exportTheme) {
+ if (includeDefaultsCheckbox.isSelected()) {
+ exportTheme.load(Gui.getAllValues());
+ }
+ else {
+ exportTheme.load(Gui.getNonDefaultValues());
+ }
+ }
+
+ private FileGTheme createExternalTheme(String themeName) {
+ File file = new File(fileTextField.getText());
+
+ GTheme activeTheme = Gui.getActiveTheme();
+ LafType laf = activeTheme.getLookAndFeelType();
+ boolean useDarkDefaults = activeTheme.useDarkDefaults();
+
+ if (exportAsZip) {
+ return new ZipGTheme(file, themeName, laf, useDarkDefaults);
+ }
+ return new FileGTheme(file, themeName, laf, useDarkDefaults);
+
}
@Override
@@ -118,10 +130,12 @@ public class ExportThemeDialog extends DialogComponentProvider {
}
private Component buildFilePanel() {
- String name = Gui.getActiveTheme().getName();
- String fileName = name.replaceAll(" ", "_") + GTheme.FILE_EXTENSION;
File homeDir = new File(System.getProperty("user.home")); // prefer the home directory
- File file = new File(homeDir, fileName);
+
+ String name = Gui.getActiveTheme().getName();
+ String filename = name.replaceAll(" ", "_") + ".";
+ filename += exportAsZip ? GTheme.ZIP_FILE_EXTENSION : GTheme.FILE_EXTENSION;
+ File file = new File(homeDir, filename);
fileTextField = new JTextField();
fileTextField.setText(file.getAbsolutePath());
@@ -149,4 +163,9 @@ public class ExportThemeDialog extends DialogComponentProvider {
}
}
+ // used for testing
+ public void setOutputFile(File outputFile) {
+ fileTextField.setText(outputFile.getAbsolutePath());
+ }
+
}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ProtectedIcon.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ProtectedIcon.java
index f036d70f1e..110c424416 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ProtectedIcon.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ProtectedIcon.java
@@ -47,11 +47,11 @@ public class ProtectedIcon implements Icon {
@Override
public int getIconWidth() {
- return delegate.getIconWidth();
+ return Math.max(1, delegate.getIconWidth());
}
@Override
public int getIconHeight() {
- return delegate.getIconHeight();
+ return Math.max(1, delegate.getIconHeight());
}
}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeDialog.java
index 1632db5b76..55d0fa3531 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeDialog.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeDialog.java
@@ -19,8 +19,6 @@ import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.event.*;
import java.beans.PropertyChangeEvent;
-import java.io.File;
-import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
@@ -31,18 +29,13 @@ import docking.DialogComponentProvider;
import docking.DockingWindowManager;
import docking.action.DockingAction;
import docking.action.builder.ActionBuilder;
-import docking.theme.*;
import docking.widgets.OptionDialog;
import docking.widgets.combobox.GhidraComboBox;
-import docking.widgets.dialogs.InputDialog;
-import docking.widgets.filechooser.GhidraFileChooser;
-import docking.widgets.filechooser.GhidraFileChooserMode;
import docking.widgets.table.GFilterTable;
import docking.widgets.table.GTable;
import generic.theme.*;
-import ghidra.framework.Application;
-import ghidra.util.*;
-import ghidra.util.filechooser.ExtensionFileFilter;
+import ghidra.util.MessageType;
+import ghidra.util.Swing;
public class ThemeDialog extends DialogComponentProvider {
private static ThemeDialog INSTANCE;
@@ -54,12 +47,11 @@ public class ThemeDialog extends DialogComponentProvider {
private FontValueEditor fontEditor = new FontValueEditor(this::fontValueChanged);
private IconValueEditor iconEditor = new IconValueEditor(this::iconValueChanged);
- // stores the original value for ids whose value has changed
- private GThemeValueMap changedValuesMap = new GThemeValueMap();
private JButton saveButton;
private JButton restoreButton;
private GhidraComboBox combo;
private ItemListener comboListener = this::themeComboChanged;
+ private ThemeListener listener = new DialogThemeListener();
public ThemeDialog() {
super("Theme Dialog", false);
@@ -73,21 +65,10 @@ public class ThemeDialog extends DialogComponentProvider {
setRememberSize(false);
updateButtons();
createActions();
+ Gui.addThemeListener(listener);
}
private void createActions() {
- DockingAction importAction =
- new ActionBuilder("Import Theme", getTitle()).toolBarIcon(new GIcon("icon.navigate.in"))
- .onAction(e -> importTheme())
- .build();
- addAction(importAction);
-
- DockingAction exportAction = new ActionBuilder("Export Theme", getTitle())
- .toolBarIcon(new GIcon("icon.navigate.out"))
- .onAction(e -> exportTheme())
- .build();
- addAction(exportAction);
-
DockingAction reloadDefaultsAction = new ActionBuilder("Reload Ghidra Defaults", getTitle())
.toolBarIcon(new GIcon("icon.refresh"))
.onAction(e -> reloadDefaultsCallback())
@@ -104,14 +85,14 @@ public class ThemeDialog extends DialogComponentProvider {
}
private boolean handleChanges() {
- if (hasChanges()) {
+ if (Gui.hasThemeChanges()) {
int result = OptionDialog.showYesNoCancelDialog(null, "Close Theme Dialog",
"You have changed the theme.\n Do you want save your changes?");
if (result == OptionDialog.CANCEL_OPTION) {
return false;
}
if (result == OptionDialog.YES_OPTION) {
- return save();
+ return ThemeUtils.saveThemeChanges();
}
Gui.reloadGhidraDefaults();
}
@@ -119,12 +100,11 @@ public class ThemeDialog extends DialogComponentProvider {
}
protected void saveCallback() {
- save();
- reset();
+ ThemeUtils.saveThemeChanges();
}
private void restoreCallback() {
- if (hasChanges()) {
+ if (Gui.hasThemeChanges()) {
int result = OptionDialog.showYesNoDialog(null, "Restore Theme Values",
"Are you sure you want to discard all your changes?");
if (result == OptionDialog.NO_OPTION) {
@@ -132,11 +112,10 @@ public class ThemeDialog extends DialogComponentProvider {
}
}
Gui.restoreThemeValues();
- reset();
}
private void reloadDefaultsCallback() {
- if (hasChanges()) {
+ if (Gui.hasThemeChanges()) {
int result = OptionDialog.showYesNoDialog(null, "Reload Ghidra Default Values",
"This will discard all your theme changes. Continue?");
if (result == OptionDialog.NO_OPTION) {
@@ -144,11 +123,9 @@ public class ThemeDialog extends DialogComponentProvider {
}
}
Gui.reloadGhidraDefaults();
- reset();
}
private void reset() {
- changedValuesMap.clear();
colorTableModel.reloadAll();
fontTableModel.reloadAll();
iconTableModel.reloadAll();
@@ -156,141 +133,33 @@ public class ThemeDialog extends DialogComponentProvider {
updateCombo();
}
- /**
- * Saves all current theme changes
- * @return true if the operation was not cancelled.
- */
- private boolean save() {
- GTheme activeTheme = Gui.getActiveTheme();
-
- String name = activeTheme.getName();
-
- while (!canSaveToName(name)) {
- name = getNameFromUser(name);
- if (name == null) {
- return false;
- }
- }
- return saveCurrentValues(name);
- }
-
- private String getNameFromUser(String name) {
- InputDialog inputDialog = new InputDialog("Create Theme", "New Theme Name", name);
- DockingWindowManager.showDialog(inputDialog);
- return inputDialog.getValue();
- }
-
- private boolean canSaveToName(String name) {
- GTheme existing = Gui.getTheme(name);
- if (existing == null) {
- return true;
- }
- if (existing instanceof FileGTheme fileTheme) {
- int result = OptionDialog.showYesNoDialog(null, "Overwrite Existing Theme?",
- "Do you want to overwrite the existing theme file for \"" + name + "\"?");
- if (result == OptionDialog.YES_OPTION) {
- return true;
- }
- }
- return false;
- }
-
- private boolean saveCurrentValues(String themeName) {
- GTheme activeTheme = Gui.getActiveTheme();
- File file = getSaveFile(themeName);
-
- FileGTheme newTheme = new FileGTheme(file, themeName, activeTheme.getLookAndFeelType(),
- activeTheme.useDarkDefaults());
- newTheme.load(Gui.getNonDefaultValues());
- try {
- newTheme.save();
- Gui.addTheme(newTheme);
- Gui.setTheme(newTheme);
- }
- catch (IOException e) {
- Msg.showError(this, null, "I/O Error",
- "Error writing theme file: " + newTheme.getFile().getAbsolutePath(), e);
- return false;
- }
-
- return true;
-
- }
-
- private File getSaveFile(String themeName) {
- File dir = Application.getUserSettingsDirectory();
- File themeDir = new File(dir, Gui.THEME_DIR);
- if (!themeDir.exists()) {
- themeDir.mkdir();
- }
- String cleanedName = themeName.replaceAll(" ", "_") + GTheme.FILE_EXTENSION;
- return new File(themeDir, cleanedName);
- }
-
- private void importTheme() {
- if (!handleChanges()) {
- return;
- }
- GTheme startingTheme = Gui.getActiveTheme();
- GhidraFileChooser chooser = new GhidraFileChooser(getComponent());
- chooser.setTitle("Choose Theme File");
- chooser.setApproveButtonToolTipText("Select File");
- chooser.setFileSelectionMode(GhidraFileChooserMode.FILES_ONLY);
- chooser.setSelectedFileFilter(
- new ExtensionFileFilter("Ghidra Theme Files", GTheme.FILE_EXTENSION));
- File file = chooser.getSelectedFile();
- if (file == null) {
- return;
- }
- try {
- FileGTheme imported = new FileGTheme(file);
- Gui.setTheme(imported);
- if (!save()) {
- Gui.setTheme(startingTheme);
- }
- }
- catch (IOException e) {
- Msg.showError(this, null, "Error Importing Theme File",
- "Error encountered importing file: " + file.getAbsolutePath(), e);
- }
- reset();
- }
-
- private void exportTheme() {
- ExportThemeDialog dialog = new ExportThemeDialog();
- DockingWindowManager.showDialog(dialog);
- }
-
private void themeComboChanged(ItemEvent e) {
- if (e.getStateChange() == ItemEvent.SELECTED) {
- String themeName = (String) e.getItem();
- Swing.runLater(() -> {
- GTheme theme = Gui.getTheme(themeName);
- Gui.setTheme(theme);
- if (theme.getLookAndFeelType() == LafType.GTK) {
- setStatusText(
- "Warning - Themes using the GTK LookAndFeel do not support changing java component colors, fonts or icons. You can still change Ghidra values.",
- MessageType.ERROR, true);
- }
- else if (theme.getLookAndFeelType() == LafType.NIMBUS) {
- setStatusText(
- "Warning - Themes using the Nimbus LookAndFeel do not support changing java component fonts or icons. You can still change Ghidra values.",
- MessageType.ERROR, true);
- }
- else {
- setStatusText("");
- }
- changedValuesMap.clear();
- colorTableModel.reloadAll();
- fontTableModel.reloadAll();
- iconTableModel.reloadAll();
- });
+ if (e.getStateChange() != ItemEvent.SELECTED) {
+ return;
}
- }
- private boolean hasChanges() {
- return !changedValuesMap.isEmpty();
+ if (!ThemeUtils.askToSaveThemeChanges()) {
+ Swing.runLater(() -> updateCombo());
+ return;
+ }
+ String themeName = (String) e.getItem();
+
+ Swing.runLater(() -> {
+ GTheme theme = Gui.getTheme(themeName);
+ Gui.setTheme(theme);
+ if (theme.getLookAndFeelType() == LafType.GTK) {
+ setStatusText(
+ "Warning - Themes using the GTK LookAndFeel do not support changing java component colors, fonts or icons.",
+ MessageType.ERROR);
+ }
+ else {
+ setStatusText("");
+ }
+ colorTableModel.reloadAll();
+ fontTableModel.reloadAll();
+ iconTableModel.reloadAll();
+ });
}
protected void editColor(ColorValue value) {
@@ -306,76 +175,31 @@ public class ThemeDialog extends DialogComponentProvider {
}
void colorValueChanged(PropertyChangeEvent event) {
- ColorValue oldValue = (ColorValue) event.getOldValue();
- ColorValue newValue = (ColorValue) event.getNewValue();
- updateChangedValueMap(oldValue, newValue);
// run later - don't rock the boat in the middle of a listener callback
Swing.runLater(() -> {
+ ColorValue newValue = (ColorValue) event.getNewValue();
Gui.setColor(newValue);
- colorTableModel.reloadCurrent();
});
}
void fontValueChanged(PropertyChangeEvent event) {
- FontValue oldValue = (FontValue) event.getOldValue();
- FontValue newValue = (FontValue) event.getNewValue();
- updateChangedValueMap(oldValue, newValue);
// run later - don't rock the boat in the middle of a listener callback
Swing.runLater(() -> {
+ FontValue newValue = (FontValue) event.getNewValue();
Gui.setFont(newValue);
- fontTableModel.reloadCurrent();
});
}
void iconValueChanged(PropertyChangeEvent event) {
- IconValue oldValue = (IconValue) event.getOldValue();
- IconValue newValue = (IconValue) event.getNewValue();
- updateChangedValueMap(oldValue, newValue);
// run later - don't rock the boat in the middle of a listener callback
Swing.runLater(() -> {
+ IconValue newValue = (IconValue) event.getNewValue();
Gui.setIcon(newValue);
- iconTableModel.reloadCurrent();
});
}
- private void updateChangedValueMap(ColorValue oldValue, ColorValue newValue) {
- ColorValue originalValue = changedValuesMap.getColor(oldValue.getId());
- if (originalValue == null) {
- changedValuesMap.addColor(oldValue);
- }
- else if (originalValue.equals(newValue)) {
- // if restoring the original color, remove it from the map of changes
- changedValuesMap.removeColor(oldValue.getId());
- }
- updateButtons();
- }
-
- private void updateChangedValueMap(FontValue oldValue, FontValue newValue) {
- FontValue originalValue = changedValuesMap.getFont(oldValue.getId());
- if (originalValue == null) {
- changedValuesMap.addFont(oldValue);
- }
- else if (originalValue.equals(newValue)) {
- // if restoring the original color, remove it from the map of changes
- changedValuesMap.removeFont(oldValue.getId());
- }
- updateButtons();
- }
-
- private void updateChangedValueMap(IconValue oldValue, IconValue newValue) {
- IconValue originalValue = changedValuesMap.getIcon(oldValue.getId());
- if (originalValue == null) {
- changedValuesMap.addIcon(oldValue);
- }
- else if (originalValue.equals(newValue)) {
- // if restoring the original color, remove it from the map of changes
- changedValuesMap.removeFont(oldValue.getId());
- }
- updateButtons();
- }
-
private void updateButtons() {
- boolean hasChanges = hasChanges();
+ boolean hasChanges = Gui.hasThemeChanges();
saveButton.setEnabled(hasChanges);
restoreButton.setEnabled(hasChanges);
}
@@ -577,4 +401,40 @@ public class ThemeDialog extends DialogComponentProvider {
DockingWindowManager.showDialog(INSTANCE);
}
+
+ @Override
+ public void close() {
+ Gui.removeThemeListener(listener);
+ super.close();
+ }
+
+ class DialogThemeListener implements ThemeListener {
+ @Override
+ public void themeChanged(GTheme newTheme) {
+ reset();
+ }
+
+ @Override
+ public void themeValuesRestored() {
+ reset();
+ }
+
+ @Override
+ public void fontChanged(String id) {
+ fontTableModel.reloadCurrent();
+ updateButtons();
+ }
+
+ @Override
+ public void colorChanged(String id) {
+ colorTableModel.reloadCurrent();
+ updateButtons();
+ }
+
+ @Override
+ public void iconChanged(String id) {
+ iconTableModel.reloadCurrent();
+ updateButtons();
+ }
+ }
}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeIconTableModel.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeIconTableModel.java
index 22d0562245..31cfc47f76 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeIconTableModel.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeIconTableModel.java
@@ -23,13 +23,11 @@ import java.util.function.Supplier;
import javax.swing.*;
-import docking.theme.*;
import docking.widgets.table.*;
import generic.theme.*;
import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.framework.plugintool.ServiceProviderStub;
-import ghidra.util.Msg;
import ghidra.util.table.column.AbstractGColumnRenderer;
import ghidra.util.table.column.GColumnRenderer;
import resources.icons.*;
@@ -46,7 +44,6 @@ public class ThemeIconTableModel extends GDynamicColumnTableModel savedThemes = new ArrayList<>(
+ Gui.getAllThemes().stream().filter(t -> t instanceof FileGTheme).toList());
+ if (savedThemes.isEmpty()) {
+ Msg.showInfo(ThemeUtils.class, null, "Delete Theme", "There are no deletable themes");
+ return;
+ }
+
+ GTheme selectedTheme = SelectFromListDialog.selectFromList(savedThemes, "Delete Theme",
+ "Select theme to delete", t -> t.getName());
+ if (selectedTheme == null) {
+ return;
+ }
+ if (Gui.getActiveTheme().equals(selectedTheme)) {
+ Msg.showWarn(ThemeUtils.class, null, "Delete Failed",
+ "Can't delete the current theme.");
+ return;
+ }
+ FileGTheme fileTheme = (FileGTheme) selectedTheme;
+ int result = OptionDialog.showYesNoDialog(null, "Delete Theme: " + fileTheme.getName(),
+ "Are you sure you want to delete theme " + fileTheme.getName());
+ if (result == OptionDialog.YES_OPTION) {
+ Gui.deleteTheme(fileTheme);
+ }
+ }
+
+ private static String getNameFromUser(String name) {
+ InputDialog inputDialog = new InputDialog("Create Theme", "New Theme Name", name);
+ DockingWindowManager.showDialog(inputDialog);
+ return inputDialog.getValue();
+ }
+
+ private static boolean canSaveToName(String name) {
+ GTheme existing = Gui.getTheme(name);
+ if (existing == null) {
+ return true;
+ }
+ if (existing instanceof FileGTheme fileTheme) {
+ int result = OptionDialog.showYesNoDialog(null, "Overwrite Existing Theme?",
+ "Do you want to overwrite the existing theme file for \"" + name + "\"?");
+ if (result == OptionDialog.YES_OPTION) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean saveCurrentValues(String themeName) {
+ GTheme activeTheme = Gui.getActiveTheme();
+ File file = getSaveFile(themeName);
+
+ FileGTheme newTheme = new FileGTheme(file, themeName, activeTheme.getLookAndFeelType(),
+ activeTheme.useDarkDefaults());
+ newTheme.load(Gui.getNonDefaultValues());
+ try {
+ newTheme.save();
+ Gui.addTheme(newTheme);
+ Gui.setTheme(newTheme);
+ }
+ catch (IOException e) {
+ Msg.showError(ThemeUtils.class, null, "I/O Error",
+ "Error writing theme file: " + newTheme.getFile().getAbsolutePath(), e);
+ return false;
+ }
+
+ return true;
+
+ }
+
+ private static File getSaveFile(String themeName) {
+ File dir = Application.getUserSettingsDirectory();
+ File themeDir = new File(dir, Gui.THEME_DIR);
+ if (!themeDir.exists()) {
+ themeDir.mkdir();
+ }
+ String cleanedName = themeName.replaceAll(" ", "_") + "." + GTheme.FILE_EXTENSION;
+ return new File(themeDir, cleanedName);
+ }
+}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeValueEditor.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeValueEditor.java
index 25dca22dd5..439465113f 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeValueEditor.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeValueEditor.java
@@ -56,13 +56,12 @@ public abstract class ThemeValueEditor {
*/
public void editValue(ThemeValue themeValue) {
this.currentThemeValue = themeValue;
- T value = getRawValue(themeValue.getId());
if (dialog == null) {
- dialog = new EditorDialog(value);
+ dialog = new EditorDialog(themeValue);
DockingWindowManager.showDialog(dialog);
}
else {
- dialog.setValue(value);
+ dialog.setValue(themeValue);
dialog.toFront();
}
@@ -83,22 +82,30 @@ public abstract class ThemeValueEditor {
*/
protected abstract ThemeValue createNewThemeValue(String id, T newValue);
- private void valueChanged(T newValue) {
+ private void valueChanged(T value) {
ThemeValue oldValue = currentThemeValue;
String id = oldValue.getId();
+ ThemeValue newValue = createNewThemeValue(id, value);
+ firePropertyChangeEvent(oldValue, newValue);
PropertyChangeEvent event =
- new PropertyChangeEvent(this, id, oldValue, createNewThemeValue(id, newValue));
+ new PropertyChangeEvent(this, id, oldValue, newValue);
+ clientListener.propertyChange(event);
+ }
+
+ private void firePropertyChangeEvent(ThemeValue oldValue, ThemeValue newValue) {
+ PropertyChangeEvent event =
+ new PropertyChangeEvent(this, oldValue.getId(), oldValue, newValue);
clientListener.propertyChange(event);
}
class EditorDialog extends DialogComponentProvider {
private PropertyChangeListener internalListener = ev -> editorChanged();
- private T originalValue;
+ private ThemeValue originalValue;
- protected EditorDialog(T initialValue) {
+ protected EditorDialog(ThemeValue initialValue) {
super("Edit " + typeName + ": " + currentThemeValue.getId(), false, false, true, false);
this.originalValue = initialValue;
- addWorkPanel(buildWorkPanel(initialValue));
+ addWorkPanel(buildWorkPanel(getRawValue(initialValue.getId())));
addOKButton();
addCancelButton();
setRememberSize(false);
@@ -119,10 +126,10 @@ public abstract class ThemeValueEditor {
return panel;
}
- void setValue(T value) {
+ void setValue(ThemeValue value) {
originalValue = value;
editor.removePropertyChangeListener(internalListener);
- editor.setValue(value);
+ editor.setValue(getRawValue(value.getId()));
editor.addPropertyChangeListener(internalListener);
}
@@ -134,7 +141,7 @@ public abstract class ThemeValueEditor {
@Override
protected void cancelCallback() {
- valueChanged(originalValue);
+ firePropertyChangeEvent(currentThemeValue, originalValue);
close();
dialog = null;
}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/SelectFromListDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/SelectFromListDialog.java
similarity index 97%
rename from Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/SelectFromListDialog.java
rename to Ghidra/Framework/Docking/src/main/java/docking/widgets/SelectFromListDialog.java
index b1236c7a6f..a1c1723da5 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/SelectFromListDialog.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/SelectFromListDialog.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package ghidra.formats.gfilesystem;
+package docking.widgets;
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
@@ -25,7 +25,6 @@ import javax.swing.*;
import docking.DialogComponentProvider;
import docking.DockingWindowManager;
-import docking.widgets.MultiLineLabel;
import docking.widgets.list.ListPanel;
import ghidra.util.SystemUtilities;
@@ -92,6 +91,10 @@ public class SelectFromListDialog extends DialogComponentProvider {
return selectedObject;
}
+ public void setSelectedObject(T obj) {
+ listPanel.setSelectedValue(obj);
+ }
+
private void doSelect() {
selectedObject = null;
actionComplete = false;
diff --git a/Ghidra/Framework/Docking/src/main/resources/images/mail-folder-outbox.png b/Ghidra/Framework/Docking/src/main/resources/images/mail-folder-outbox.png
new file mode 100644
index 0000000000..63bc71b87c
Binary files /dev/null and b/Ghidra/Framework/Docking/src/main/resources/images/mail-folder-outbox.png differ
diff --git a/Ghidra/Framework/Docking/src/main/resources/images/mail-receive.png b/Ghidra/Framework/Docking/src/main/resources/images/mail-receive.png
new file mode 100644
index 0000000000..ea9eac679f
Binary files /dev/null and b/Ghidra/Framework/Docking/src/main/resources/images/mail-receive.png differ
diff --git a/Ghidra/Framework/Docking/src/test/java/docking/theme/gui/ThemeUtilsTest.java b/Ghidra/Framework/Docking/src/test/java/docking/theme/gui/ThemeUtilsTest.java
new file mode 100644
index 0000000000..6bf0c4a5fb
--- /dev/null
+++ b/Ghidra/Framework/Docking/src/test/java/docking/theme/gui/ThemeUtilsTest.java
@@ -0,0 +1,217 @@
+/* ###
+ * 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 docking.theme.gui;
+
+import static org.junit.Assert.*;
+
+import java.awt.Color;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.Set;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.Before;
+import org.junit.Test;
+
+import docking.test.AbstractDockingTest;
+import docking.widgets.OptionDialog;
+import docking.widgets.SelectFromListDialog;
+import docking.widgets.dialogs.InputDialog;
+import generic.theme.*;
+import generic.theme.builtin.MetalTheme;
+import generic.theme.builtin.NimbusTheme;
+
+public class ThemeUtilsTest extends AbstractDockingTest {
+
+ @Before
+ public void setup() {
+ GTheme nimbusTheme = new NimbusTheme();
+ GTheme metalTheme = new MetalTheme();
+ Gui.addTheme(nimbusTheme);
+ Gui.addTheme(metalTheme);
+ Gui.setTheme(nimbusTheme);
+
+ // get rid of any leftover imported themes from previous tests
+ Set allThemes = Gui.getAllThemes();
+ for (GTheme gTheme : allThemes) {
+ if (gTheme instanceof FileGTheme fileTheme) {
+ Gui.deleteTheme(fileTheme);
+ }
+ }
+ }
+
+ @Test
+ public void testImportThemeNonZip() throws IOException {
+ assertEquals("Nimbus Theme", Gui.getActiveTheme().getName());
+ File themeFile = createThemeFile("Bob");
+ ThemeUtils.importTheme(themeFile);
+ assertEquals("Bob", Gui.getActiveTheme().getName());
+
+ }
+
+ @Test
+ public void testImportThemeFromZip() throws IOException {
+ assertEquals("Nimbus Theme", Gui.getActiveTheme().getName());
+ File themeFile = createZipThemeFile("zippy");
+ ThemeUtils.importTheme(themeFile);
+ assertEquals("zippy", Gui.getActiveTheme().getName());
+ }
+
+ @Test
+ public void testImportThemeWithCurrentChangesCancelled() throws IOException {
+ assertEquals("Nimbus Theme", Gui.getActiveTheme().getName());
+ Gui.setColor("Panel.background", Color.RED);
+ assertTrue(Gui.hasThemeChanges());
+
+ File themeFile = createThemeFile("Bob");
+ runSwingLater(() -> ThemeUtils.importTheme(themeFile));
+ OptionDialog dialog = waitForDialogComponent(OptionDialog.class);
+ assertNotNull(dialog);
+ assertEquals("Save Theme Changes?", dialog.getTitle());
+ pressButtonByText(dialog, "Cancel");
+ waitForSwing();
+ assertEquals("Nimbus Theme", Gui.getActiveTheme().getName());
+ }
+
+ @Test
+ public void testImportThemeWithCurrentChangesSaved() throws IOException {
+ assertEquals("Nimbus Theme", Gui.getActiveTheme().getName());
+
+ // make a change in the current theme, so you get asked to save
+ Gui.setColor("Panel.background", Color.RED);
+ assertTrue(Gui.hasThemeChanges());
+
+ File themeFile = createThemeFile("Bob");
+ runSwingLater(() -> ThemeUtils.importTheme(themeFile));
+ OptionDialog dialog = waitForDialogComponent(OptionDialog.class);
+ assertNotNull(dialog);
+ assertEquals("Save Theme Changes?", dialog.getTitle());
+ pressButtonByText(dialog, "Yes");
+ InputDialog inputDialog = waitForDialogComponent(InputDialog.class);
+ assertNotNull(inputDialog);
+ runSwing(() -> inputDialog.setValue("Joe"));
+ pressButtonByText(inputDialog, "OK");
+ waitForSwing();
+ assertEquals("Bob", Gui.getActiveTheme().getName());
+ assertNotNull(Gui.getTheme("Joe"));
+ }
+
+ @Test
+ public void testImportThemeWithCurrentChangesThrownAway() throws IOException {
+ assertEquals("Nimbus Theme", Gui.getActiveTheme().getName());
+
+ // make a change in the current theme, so you get asked to save
+ Gui.setColor("Panel.background", Color.RED);
+ assertTrue(Gui.hasThemeChanges());
+
+ File bobThemeFile = createThemeFile("Bob");
+ runSwingLater(() -> ThemeUtils.importTheme(bobThemeFile));
+
+ OptionDialog dialog = waitForDialogComponent(OptionDialog.class);
+ assertNotNull(dialog);
+ assertEquals("Save Theme Changes?", dialog.getTitle());
+ pressButtonByText(dialog, "No");
+ waitForSwing();
+ assertEquals("Bob", Gui.getActiveTheme().getName());
+ }
+
+ @Test
+ public void testExportThemeAsZip() throws IOException {
+ runSwingLater(() -> ThemeUtils.exportTheme());
+ OptionDialog dialog = waitForDialogComponent(OptionDialog.class);
+ pressButtonByText(dialog, "Export Zip");
+ ExportThemeDialog exportDialog = waitForDialogComponent(ExportThemeDialog.class);
+ File exportFile = createTempFile("whatever", ".theme.zip");
+ runSwing(() -> exportDialog.setOutputFile(exportFile));
+ pressButtonByText(exportDialog, "OK");
+ waitForSwing();
+ assertTrue(exportFile.exists());
+ ZipGTheme zipTheme = new ZipGTheme(exportFile);
+ assertEquals("Nimbus Theme", zipTheme.getName());
+ }
+
+ @Test
+ public void testExportThemeAsFile() throws IOException {
+ runSwingLater(() -> ThemeUtils.exportTheme());
+ OptionDialog dialog = waitForDialogComponent(OptionDialog.class);
+ pressButtonByText(dialog, "Export File");
+ ExportThemeDialog exportDialog = waitForDialogComponent(ExportThemeDialog.class);
+ File exportFile = createTempFile("whatever", ".theme");
+ runSwing(() -> exportDialog.setOutputFile(exportFile));
+ pressButtonByText(exportDialog, "OK");
+ waitForSwing();
+ assertTrue(exportFile.exists());
+ FileGTheme fileTheme = new FileGTheme(exportFile);
+ assertEquals("Nimbus Theme", fileTheme.getName());
+ }
+
+ @Test
+ public void testDeleteTheme() throws IOException {
+ File themeFile = createThemeFile("Bob");
+ ThemeUtils.importTheme(themeFile);
+ themeFile = createThemeFile("Joe");
+ ThemeUtils.importTheme(themeFile);
+ themeFile = createThemeFile("Lisa");
+ ThemeUtils.importTheme(themeFile);
+
+ assertNotNull(Gui.getTheme("Bob"));
+ assertNotNull(Gui.getTheme("Joe"));
+ assertNotNull(Gui.getTheme("Lisa"));
+
+ runSwingLater(() -> ThemeUtils.deleteTheme());
+ @SuppressWarnings("unchecked")
+ SelectFromListDialog dialog = waitForDialogComponent(SelectFromListDialog.class);
+ runSwing(() -> dialog.setSelectedObject(Gui.getTheme("Bob")));
+ pressButtonByText(dialog, "OK");
+
+ OptionDialog optionDialog = waitForDialogComponent(OptionDialog.class);
+ pressButtonByText(optionDialog, "Yes");
+ waitForSwing();
+
+ assertNotNull(Gui.getTheme("Bob"));
+ assertNull(Gui.getTheme("Joe"));
+ assertNotNull(Gui.getTheme("Lisa"));
+
+ }
+
+ private File createZipThemeFile(String themeName) throws IOException {
+ File file = createTempFile("Test_Theme", ".theme.zip");
+ ZipGTheme zipGTheme = new ZipGTheme(file, themeName, LafType.METAL, false);
+ zipGTheme.addColor(new ColorValue("Panel.Background", Color.RED));
+ zipGTheme.save();
+ return file;
+ }
+
+ private File createThemeFile(String themeName) throws IOException {
+ String themeData = createThemeDataString(themeName);
+ File file = createTempFile("Test_Theme", ".theme");
+ FileUtils.writeStringToFile(file, themeData, Charset.defaultCharset());
+ return file;
+ }
+
+ private String createThemeDataString(String themeName) {
+ String themeData = """
+ name = THEMENAME
+ lookAndFeel = Metal
+ useDarkDefaults = false
+ [color]Panel.background = #ffcccc
+ """;
+
+ return themeData.replace("THEMENAME", themeName);
+ }
+
+}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ExternalThemeReader.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ExternalThemeReader.java
new file mode 100644
index 0000000000..43b3023b51
--- /dev/null
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ExternalThemeReader.java
@@ -0,0 +1,63 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package generic.theme;
+
+import java.io.*;
+import java.util.Enumeration;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import org.apache.commons.io.FileUtils;
+
+import ghidra.framework.Application;
+import ghidra.util.Msg;
+
+public class ExternalThemeReader extends ThemeReader {
+
+ public ExternalThemeReader(File file) throws IOException {
+ try (ZipFile zipFile = new ZipFile(file)) {
+ Enumeration extends ZipEntry> entries = zipFile.entries();
+ while (entries.hasMoreElements()) {
+ ZipEntry entry = entries.nextElement();
+ String name = entry.getName();
+ try (InputStream is = zipFile.getInputStream(entry)) {
+ if (name.endsWith(".theme")) {
+ processThemeData(name, is);
+ }
+ else {
+ processIconFile(name, is);
+ }
+ }
+ }
+ }
+ }
+
+ private void processIconFile(String path, InputStream is) throws IOException {
+ int indexOf = path.indexOf("images/");
+ if (indexOf < 0) {
+ Msg.error(this, "Unknown file: " + path);
+ }
+ String relativePath = path.substring(indexOf, path.length());
+ File dir = Application.getUserSettingsDirectory();
+ File iconFile = new File(dir, relativePath);
+ FileUtils.copyInputStreamToFile(is, iconFile);
+ }
+
+ private void processThemeData(String name, InputStream is) throws IOException {
+ InputStreamReader reader = new InputStreamReader(is);
+ read(reader);
+ }
+}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/FileGTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/FileGTheme.java
index 00b5fd9a4a..8de78f5948 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/FileGTheme.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/FileGTheme.java
@@ -27,8 +27,9 @@ import ghidra.util.WebColors;
import resources.icons.UrlImageIcon;
public class FileGTheme extends GTheme {
+ public static final String JAVA_ICON = "";
public static final String FILE_PREFIX = "File:";
- private final File file;
+ protected final File file;
public FileGTheme(File file) throws IOException {
this(file, new ThemeReader(file));
@@ -40,7 +41,7 @@ public class FileGTheme extends GTheme {
}
FileGTheme(File file, ThemeReader reader) {
- super(reader.getThemeName(), reader.getLookAndFeelType(), false);
+ super(reader.getThemeName(), reader.getLookAndFeelType(), reader.useDarkDefaults());
this.file = file;
reader.loadValues(this);
}
@@ -63,46 +64,50 @@ public class FileGTheme extends GTheme {
public void save() throws IOException {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
- List colors = getColors();
- Collections.sort(colors);
-
- List fonts = getFonts();
- Collections.sort(fonts);
-
- List icons = getIcons();
- Collections.sort(icons);
-
- writer.write(THEME_NAME_KEY + " = " + getName());
- writer.newLine();
-
- writer.write(THEME_LOOK_AND_FEEL_KEY + " = " + getLookAndFeelType().getName());
- writer.newLine();
-
- writer.write(THEME_USE_DARK_DEFAULTS + " = " + useDarkDefaults());
- writer.newLine();
-
- for (ColorValue colorValue : colors) {
- String outputId = colorValue.toExternalId(colorValue.getId());
- writer.write(outputId + " = " + getValueOutput(colorValue));
- writer.newLine();
- }
-
- for (FontValue fontValue : fonts) {
- String outputId = fontValue.toExternalId(fontValue.getId());
- writer.write(outputId + " = " + getValueOutput(fontValue));
- writer.newLine();
- }
-
- for (IconValue iconValue : icons) {
- String outputId = iconValue.toExternalId(iconValue.getId());
- writer.write(outputId + " = " + getValueOutput(iconValue));
- writer.newLine();
- }
+ writeThemeValues(writer);
}
}
+ protected void writeThemeValues(BufferedWriter writer) throws IOException {
+ List colors = getColors();
+ Collections.sort(colors);
+
+ List fonts = getFonts();
+ Collections.sort(fonts);
+
+ List icons = getIcons();
+ Collections.sort(icons);
+
+ writer.write(THEME_NAME_KEY + " = " + getName());
+ writer.newLine();
+
+ writer.write(THEME_LOOK_AND_FEEL_KEY + " = " + getLookAndFeelType().getName());
+ writer.newLine();
+
+ writer.write(THEME_USE_DARK_DEFAULTS + " = " + useDarkDefaults());
+ writer.newLine();
+
+ for (ColorValue colorValue : colors) {
+ String outputId = colorValue.toExternalId(colorValue.getId());
+ writer.write(outputId + " = " + getValueOutput(colorValue));
+ writer.newLine();
+ }
+
+ for (FontValue fontValue : fonts) {
+ String outputId = fontValue.toExternalId(fontValue.getId());
+ writer.write(outputId + " = " + getValueOutput(fontValue));
+ writer.newLine();
+ }
+
+ for (IconValue iconValue : icons) {
+ String outputId = iconValue.toExternalId(iconValue.getId());
+ writer.write(outputId + " = " + getValueOutput(iconValue));
+ writer.newLine();
+ }
+ }
+
private String getValueOutput(ColorValue colorValue) {
if (colorValue.getReferenceId() != null) {
return colorValue.toExternalId(colorValue.getReferenceId());
@@ -128,7 +133,7 @@ public class FileGTheme extends GTheme {
if (icon instanceof UrlImageIcon urlIcon) {
return urlIcon.getOriginalPath();
}
- return "";
+ return JAVA_ICON;
}
private String getValueOutput(FontValue fontValue) {
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/GTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/GTheme.java
index 59046860e5..594148b371 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/GTheme.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/GTheme.java
@@ -28,7 +28,8 @@ import javax.swing.Icon;
* in an application.
*/
public class GTheme extends GThemeValueMap {
- public static String FILE_EXTENSION = ".theme";
+ public static String FILE_EXTENSION = "theme";
+ public static String ZIP_FILE_EXTENSION = "theme.zip";
static final String THEME_NAME_KEY = "name";
static final String THEME_LOOK_AND_FEEL_KEY = "lookAndFeel";
@@ -39,7 +40,7 @@ public class GTheme extends GThemeValueMap {
private final boolean useDarkDefaults;
public GTheme(String name) {
- this(name, LafType.SYSTEM, false);
+ this(name, LafType.getDefaultLookAndFeel(), false);
}
@@ -49,7 +50,7 @@ public class GTheme extends GThemeValueMap {
* @param lookAndFeel the look and feel type used by this theme
* @param useDarkDefaults determines whether or
*/
- protected GTheme(String name, LafType lookAndFeel, boolean useDarkDefaults) {
+ public GTheme(String name, LafType lookAndFeel, boolean useDarkDefaults) {
this.name = name;
this.lookAndFeel = lookAndFeel;
this.useDarkDefaults = useDarkDefaults;
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/GThemeValueMap.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/GThemeValueMap.java
index ccdd38ede8..43fde6cc9f 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/GThemeValueMap.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/GThemeValueMap.java
@@ -15,12 +15,19 @@
*/
package generic.theme;
+import java.io.File;
+import java.net.URL;
import java.util.*;
+import javax.swing.Icon;
+
+import resources.ResourceManager;
+import resources.icons.UrlImageIcon;
+
public class GThemeValueMap {
- Map colorMap = new HashMap<>();
- Map fontMap = new HashMap<>();
- Map iconMap = new HashMap<>();
+ protected Map colorMap = new HashMap<>();
+ protected Map fontMap = new HashMap<>();
+ protected Map iconMap = new HashMap<>();
public GThemeValueMap() {
}
@@ -132,4 +139,29 @@ public class GThemeValueMap {
fontMap.remove(id);
}
+ public void removeIcon(String id) {
+ iconMap.remove(id);
+ }
+
+ public Set getExternalIconFiles() {
+ 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);
+ }
+ }
+ }
+ }
+ }
+ return files;
+ }
+
}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/Gui.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/Gui.java
index 47899a5496..e629b9526a 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/Gui.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/Gui.java
@@ -15,18 +15,20 @@
*/
package generic.theme;
-import java.awt.*;
+import java.awt.Color;
+import java.awt.Font;
import java.io.*;
import java.util.*;
-import java.util.List;
-import javax.swing.*;
+import javax.swing.Icon;
+import javax.swing.UIManager;
import javax.swing.plaf.ComponentUI;
import com.formdev.flatlaf.*;
-import generic.theme.builtin.JavaColorMapping;
-import ghidra.framework.Application;
+import generic.theme.builtin.*;
+import generic.theme.laf.LookAndFeelManager;
+import ghidra.framework.*;
import ghidra.framework.preferences.Preferences;
import ghidra.util.Msg;
import ghidra.util.classfinder.ClassSearcher;
@@ -42,8 +44,8 @@ public class Gui {
private static final String THEME_PREFFERENCE_KEY = "Theme";
- private static GTheme activeTheme = new DefaultTheme();
- private static Set allThemes;
+ private static GTheme activeTheme = getDefaultTheme();
+ private static Set allThemes = null;
private static GThemeValueMap ghidraLightDefaults = new GThemeValueMap();
private static GThemeValueMap ghidraDarkDefaults = new GThemeValueMap();
@@ -55,8 +57,15 @@ public class Gui {
private static Map gColorMap = new HashMap<>();
private static boolean isInitialized;
private static Map gIconMap = new HashMap<>();
+
+ // these notifications are only when the user is manipulating theme values, so rare and at
+ // user speed, so using copy on read
private static WeakSet themeListeners =
- WeakDataStructureFactory.createCopyOnWriteWeakSet();
+ WeakDataStructureFactory.createCopyOnReadWeakSet();
+
+ // stores the original value for ids whose value has changed from the current theme
+ private static GThemeValueMap changedValuesMap = new GThemeValueMap();
+ private static LookAndFeelManager lookAndFeelManager;
private Gui() {
// static utils class, can't construct
@@ -86,23 +95,25 @@ public class Gui {
public static void reloadGhidraDefaults() {
loadThemeDefaults();
buildCurrentValues();
+ lookAndFeelManager.update();
+ notifyThemeValuesRestored();
}
public static void restoreThemeValues() {
buildCurrentValues();
+ lookAndFeelManager.update();
+ notifyThemeValuesRestored();
}
public static void setTheme(GTheme theme) {
if (theme.hasSupportedLookAndFeel()) {
activeTheme = theme;
LafType lookAndFeel = theme.getLookAndFeelType();
+ lookAndFeelManager = lookAndFeel.getLookAndFeelManager();
try {
- lookAndFeel.install();
+ lookAndFeelManager.installLookAndFeel();
+ notifyThemeChanged();
saveThemeToPreferences(theme);
- fixupJavaDefaults();
- buildCurrentValues();
- updateUIs();
- notifyThemeListeners();
}
catch (Exception e) {
Msg.error(Gui.class, "Error setting LookAndFeel: " + lookAndFeel.getName(), e);
@@ -110,20 +121,46 @@ public class Gui {
}
}
- private static void notifyThemeListeners() {
+ private static void notifyThemeChanged() {
for (ThemeListener listener : themeListeners) {
listener.themeChanged(activeTheme);
}
}
+ private static void notifyThemeValuesRestored() {
+ for (ThemeListener listener : themeListeners) {
+ listener.themeValuesRestored();
+ }
+ }
+
+ private static void notifyColorChanged(String id) {
+ for (ThemeListener listener : themeListeners) {
+ listener.colorChanged(id);
+ }
+ }
+
+ private static void notifyFontChanged(String id) {
+ for (ThemeListener listener : themeListeners) {
+ listener.fontChanged(id);
+ }
+ }
+
+ private static void notifyIconChanged(String id) {
+ for (ThemeListener listener : themeListeners) {
+ listener.iconChanged(id);
+ }
+ }
+
public static void addTheme(GTheme newTheme) {
+ loadThemes();
allThemes.remove(newTheme);
allThemes.add(newTheme);
}
- private static void updateUIs() {
- for (Window window : Window.getWindows()) {
- SwingUtilities.updateComponentTreeUI(window);
+ public static void deleteTheme(FileGTheme theme) {
+ theme.file.delete();
+ if (allThemes != null) {
+ allThemes.remove(theme);
}
}
@@ -140,16 +177,12 @@ public class Gui {
}
public static Set getAllThemes() {
- if (allThemes == null) {
- allThemes = findThemes();
- }
- return Collections.unmodifiableSet(allThemes);
+ loadThemes();
+ return new HashSet<>(allThemes);
}
public static Set getSupportedThemes() {
- if (allThemes == null) {
- allThemes = findThemes();
- }
+ loadThemes();
Set supported = new HashSet<>();
for (GTheme theme : allThemes) {
if (theme.hasSupportedLookAndFeel()) {
@@ -251,26 +284,21 @@ public class Gui {
}
map.load(activeTheme);
currentValues = map;
- GColor.refreshAll();
- GIcon.refreshAll();
- repaintAll();
+ changedValuesMap.clear();
}
- private static Set findThemes() {
- Set set = new HashSet<>();
- set.addAll(findDiscoverableThemes());
- set.addAll(loadThemesFromFiles());
-
- // The set should contains a duplicate of the active theme. Make sure the active theme
- // instance is the one in the set
- set.remove(activeTheme);
- set.add(activeTheme);
- return set;
+ private static void loadThemes() {
+ if (allThemes == null) {
+ Set set = new HashSet<>();
+ set.addAll(findDiscoverableThemes());
+ set.addAll(loadThemesFromFiles());
+ allThemes = set;
+ }
}
private static Collection loadThemesFromFiles() {
List fileList = new ArrayList<>();
- FileFilter themeFileFilter = file -> file.getName().endsWith(GTheme.FILE_EXTENSION);
+ FileFilter themeFileFilter = file -> file.getName().endsWith("." + GTheme.FILE_EXTENSION);
File dir = Application.getUserSettingsDirectory();
File themeDir = new File(dir, THEME_DIR);
@@ -326,34 +354,84 @@ public class Gui {
"Can't find or instantiate class: " + className);
}
}
- return new DefaultTheme();
+ return getDefaultTheme();
}
public static void setFont(FontValue newValue) {
+ FontValue currentValue = currentValues.getFont(newValue.getId());
+ if (newValue.equals(currentValue)) {
+ return;
+ }
+ updateChangedValuesMap(currentValue, newValue);
+
currentValues.addFont(newValue);
// all fonts are direct (there is no GFont), so to we need to update the
// UiDefaults for java fonts. Ghidra fonts are expected to be "on the fly" (they
// call Gui.getFont(id) for every use.
String id = newValue.getId();
- if (javaDefaults.containsFont(id)) {
- UIManager.getDefaults().put(id, newValue.get(currentValues));
- updateUIs();
- }
- else {
- repaintAll();
- }
+ boolean isJavaFont = javaDefaults.containsFont(id);
+ lookAndFeelManager.updateFont(id, newValue.get(currentValues), isJavaFont);
+ notifyFontChanged(id);
}
public static void setColor(String id, Color color) {
setColor(new ColorValue(id, color));
}
- public static void setColor(ColorValue colorValue) {
- currentValues.addColor(colorValue);
- // all colors use indirection via GColor, so to update all we need to do is refresh GColors
- // and repaint
- GColor.refreshAll();
- repaintAll();
+ public static void setColor(ColorValue newValue) {
+ ColorValue currentValue = currentValues.getColor(newValue.getId());
+ if (newValue.equals(currentValue)) {
+ return;
+ }
+ updateChangedValuesMap(currentValue, newValue);
+
+ currentValues.addColor(newValue);
+ String id = newValue.getId();
+ boolean isJavaColor = javaDefaults.containsColor(id);
+ lookAndFeelManager.updateColor(id, newValue.get(currentValues), isJavaColor);
+ notifyColorChanged(newValue.getId());
+ }
+
+ private static void updateChangedValuesMap(ColorValue currentValue, ColorValue newValue) {
+ String id = newValue.getId();
+ ColorValue originalValue = changedValuesMap.getColor(id);
+
+ // if new value is original value, it is no longer changed, remove it from changed map
+ if (newValue.equals(originalValue)) {
+ changedValuesMap.removeColor(id);
+ }
+ else if (originalValue == null) {
+ // first time changed, so current value is original value
+ changedValuesMap.addColor(currentValue);
+ }
+ }
+
+ private static void updateChangedValuesMap(FontValue currentValue, FontValue newValue) {
+ String id = newValue.getId();
+ FontValue originalValue = changedValuesMap.getFont(id);
+
+ // if new value is original value, it is no longer changed, remove it from changed map
+ if (newValue.equals(originalValue)) {
+ changedValuesMap.removeFont(id);
+ }
+ else if (originalValue == null) {
+ // first time changed, so current value is original value
+ changedValuesMap.addFont(currentValue);
+ }
+ }
+
+ private static void updateChangedValuesMap(IconValue currentValue, IconValue newValue) {
+ String id = newValue.getId();
+ IconValue originalValue = changedValuesMap.getIcon(id);
+
+ // if new value is original value, it is no longer changed, remove it from changed map
+ if (newValue.equals(originalValue)) {
+ changedValuesMap.removeIcon(id);
+ }
+ else if (originalValue == null) {
+ // first time changed, so current value is original value
+ changedValuesMap.addIcon(currentValue);
+ }
}
public static void setIcon(String id, Icon icon) {
@@ -361,31 +439,20 @@ public class Gui {
}
public static void setIcon(IconValue newValue) {
+ IconValue currentValue = currentValues.getIcon(newValue.getId());
+ if (newValue.equals(currentValue)) {
+ return;
+ }
+ updateChangedValuesMap(currentValue, newValue);
+
currentValues.addIcon(newValue);
-
- // Icons are a mixed bag. Java Icons are direct and Ghidra Icons are indirect (to support static use)
- // Mainly because Nimbus is buggy and can't handle non-nimbus Icons, so we can't wrap them
- // So need to update UiDefaults for java icons. For Ghidra Icons, it is sufficient to refrech
- // GIcons and repaint
String id = newValue.getId();
- if (javaDefaults.containsIcon(id)) {
- UIManager.getDefaults().put(id, newValue.get(currentValues));
- updateUIs();
- }
- else {
- GIcon.refreshAll();
- repaintAll();
- }
- }
-
- private static void repaintAll() {
- for (Window window : Window.getWindows()) {
- window.repaint();
- }
+ boolean isJavaIcon = javaDefaults.containsIcon(id);
+ lookAndFeelManager.updateIcon(id, newValue.get(currentValues), isJavaIcon);
+ notifyIconChanged(id);
}
public static GColorUIResource getGColorUiResource(String id) {
-
GColorUIResource gColor = gColorMap.get(id);
if (gColor == null) {
gColor = new GColorUIResource(id);
@@ -405,11 +472,13 @@ public class Gui {
}
public static void setJavaDefaults(GThemeValueMap map) {
- javaDefaults = map;
+ javaDefaults = fixupJavaDefaultsInheritence(map);
buildCurrentValues();
+ GColor.refreshAll();
+ GIcon.refreshAll();
}
- public static void fixupJavaDefaults() {
+ public static GThemeValueMap fixupJavaDefaultsInheritence(GThemeValueMap map) {
List colors = javaDefaults.getColors();
JavaColorMapping mapping = new JavaColorMapping();
for (ColorValue value : colors) {
@@ -418,6 +487,7 @@ public class Gui {
javaDefaults.addColor(mapped);
}
}
+ return map;
}
public static GThemeValueMap getJavaDefaults() {
@@ -476,4 +546,22 @@ public class Gui {
themePropertiesLoader = loader;
}
+ public static GTheme getDefaultTheme() {
+ OperatingSystem OS = Platform.CURRENT_PLATFORM.getOperatingSystem();
+ switch (OS) {
+ case MAC_OS_X:
+ return new MacTheme();
+ case WINDOWS:
+ return new WindowsTheme();
+ case LINUX:
+ case UNSUPPORTED:
+ default:
+ return new NimbusTheme();
+ }
+ }
+
+ public static boolean hasThemeChanges() {
+ return !changedValuesMap.isEmpty();
+ }
+
}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/LafType.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/LafType.java
index 4384c8d9c7..e6fada20d1 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/LafType.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/LafType.java
@@ -33,8 +33,7 @@ public enum LafType {
FLAT_DARCULA("Flat Darcula"),
WINDOWS("Windows"),
WINDOWS_CLASSIC("Windows Classic"),
- MAC("Mac OS X"),
- SYSTEM("System");
+ MAC("Mac OS X");
private String name;
@@ -55,24 +54,7 @@ public enum LafType {
return null;
}
- private static LookAndFeelInstaller getSystemLookAndFeelInstaller() {
- OperatingSystem OS = Platform.CURRENT_PLATFORM.getOperatingSystem();
- if (OS == OperatingSystem.LINUX) {
- return getInstaller(NIMBUS);
- }
- else if (OS == OperatingSystem.MAC_OS_X) {
- return getInstaller(MAC);
- }
- else if (OS == OperatingSystem.WINDOWS) {
- return getInstaller(WINDOWS);
- }
- return getInstaller(NIMBUS);
- }
-
public boolean isSupported() {
- if (this == SYSTEM) {
- return true;
- }
LookAndFeelInfo[] installedLookAndFeels = UIManager.getInstalledLookAndFeels();
for (LookAndFeelInfo info : installedLookAndFeels) {
if (name.equals(info.getName())) {
@@ -82,36 +64,43 @@ public enum LafType {
return false;
}
- public void install() throws Exception {
- getInstaller(this).install();
+ public LookAndFeelManager getLookAndFeelManager() {
+ return getManager(this);
}
- private static LookAndFeelInstaller getInstaller(LafType lookAndFeel) {
+ private static LookAndFeelManager getManager(LafType lookAndFeel) {
switch (lookAndFeel) {
- case FLAT_DARCULA:
- return new FlatLookAndFeelInstaller(FLAT_DARCULA);
- case FLAT_DARK:
- return new FlatLookAndFeelInstaller(FLAT_DARK);
- case FLAT_LIGHT:
- return new FlatLookAndFeelInstaller(FLAT_LIGHT);
- case GTK:
- return new GTKLookAndFeelInstaller();
case MAC:
- return new LookAndFeelInstaller(MAC);
case METAL:
- return new LookAndFeelInstaller(METAL);
- case MOTIF:
- return new MotifLookAndFeelInstaller(); // Motif has some specific ui fix ups
- case NIMBUS:
- return new NimbusLookAndFeelInstaller(); // Nimbus installs a special way
- case SYSTEM:
- return getSystemLookAndFeelInstaller();
case WINDOWS:
- return new LookAndFeelInstaller(WINDOWS);
case WINDOWS_CLASSIC:
- return new LookAndFeelInstaller(WINDOWS_CLASSIC);
+ return new GenericLookAndFeelManager(lookAndFeel);
+ case FLAT_DARCULA:
+ case FLAT_DARK:
+ case FLAT_LIGHT:
+ return new GenericFlatLookAndFeelManager(lookAndFeel);
+ case GTK:
+ return new GtkLookAndFeelManager();
+ case MOTIF:
+ return new MotifLookAndFeelManager();
+ case NIMBUS:
+ return new NimbusLookAndFeelManager();
default:
- throw new AssertException("No lookAndFeelInstaller defined for " + lookAndFeel);
+ throw new AssertException("No lookAndFeelManager defined for " + lookAndFeel);
+ }
+ }
+
+ public static LafType getDefaultLookAndFeel() {
+ OperatingSystem OS = Platform.CURRENT_PLATFORM.getOperatingSystem();
+ switch (OS) {
+ case MAC_OS_X:
+ return MAC;
+ case WINDOWS:
+ return WINDOWS;
+ case LINUX:
+ case UNSUPPORTED:
+ default:
+ return NIMBUS;
}
}
}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeListener.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeListener.java
index 55d58820fc..b273adc3a1 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeListener.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeListener.java
@@ -16,5 +16,23 @@
package generic.theme;
public interface ThemeListener {
- public void themeChanged(GTheme newTheme);
+ public default void themeChanged(GTheme newTheme) {
+ // default do nothing
+ }
+
+ public default void colorChanged(String id) {
+ // default do nothing
+ }
+
+ public default void fontChanged(String id) {
+ // default do nothing
+ }
+
+ public default void iconChanged(String id) {
+ // default do nothing
+ }
+
+ public default void themeValuesRestored() {
+ // default do nothing
+ }
}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePropertyFileReader.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePropertyFileReader.java
index 7893ca2e25..db7ca47808 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePropertyFileReader.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePropertyFileReader.java
@@ -56,6 +56,10 @@ public class ThemePropertyFileReader {
}
}
+ protected ThemePropertyFileReader() {
+
+ }
+
ThemePropertyFileReader(String source, Reader reader) throws IOException {
filePath = source;
read(reader);
@@ -77,7 +81,7 @@ public class ThemePropertyFileReader {
return errors;
}
- private void read(Reader reader) throws IOException {
+ protected void read(Reader reader) throws IOException {
List sections = readSections(new LineNumberReader(reader));
for (Section section : sections) {
switch (section.getName()) {
@@ -116,7 +120,9 @@ public class ThemePropertyFileReader {
valueMap.addFont(parseFontProperty(key, value, lineNumber));
}
else if (IconValue.isIconKey(key)) {
- valueMap.addIcon(parseIconProperty(key, value));
+ if (!FileGTheme.JAVA_ICON.equals(value)) {
+ valueMap.addIcon(parseIconProperty(key, value));
+ }
}
else {
error(lineNumber, "Can't process property: " + key + " = " + value);
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeReader.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeReader.java
index 1eaa2c3813..d56fcf3ab7 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeReader.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeReader.java
@@ -29,6 +29,10 @@ public class ThemeReader extends ThemePropertyFileReader {
super(file);
}
+ protected ThemeReader() {
+
+ }
+
@Override
protected void processNoSection(Section section) throws IOException {
themeSection = section;
@@ -63,4 +67,7 @@ public class ThemeReader extends ThemePropertyFileReader {
return lookAndFeel;
}
+ public boolean useDarkDefaults() {
+ return useDarkDefaults;
+ }
}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ZipGTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ZipGTheme.java
new file mode 100644
index 0000000000..9d1d496486
--- /dev/null
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ZipGTheme.java
@@ -0,0 +1,68 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package generic.theme;
+
+import java.io.*;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import com.google.common.io.Files;
+
+public class ZipGTheme extends FileGTheme {
+
+ public ZipGTheme(File file, String name, LafType laf, boolean useDarkDefaults) {
+ super(file, name, laf, useDarkDefaults);
+ }
+
+ public ZipGTheme(File file) throws IOException {
+ this(file, new ExternalThemeReader(file));
+ }
+
+ public ZipGTheme(File file, ThemeReader reader) {
+ super(file, reader.getThemeName(), reader.getLookAndFeelType(), reader.useDarkDefaults());
+ reader.loadValues(this);
+ }
+
+ @Override
+ public void save() throws IOException {
+ String dir = getName() + ".theme/";
+ try (FileOutputStream fos = new FileOutputStream(file)) {
+ ZipOutputStream zos = new ZipOutputStream(fos);
+ saveThemeFileToZip(dir, zos);
+ Set iconFiles = getExternalIconFiles();
+ for (File iconFile : iconFiles) {
+ copyToZipFile(dir, iconFile, zos);
+ }
+ zos.finish();
+ }
+ }
+
+ private void copyToZipFile(String dir, File iconFile, ZipOutputStream zos) throws IOException {
+ ZipEntry entry = new ZipEntry(dir + "images/" + iconFile.getName());
+ zos.putNextEntry(entry);
+ Files.copy(iconFile, zos);
+ }
+
+ private void saveThemeFileToZip(String dir, ZipOutputStream zos) throws IOException {
+ ZipEntry entry = new ZipEntry(dir + getName() + ".theme");
+ zos.putNextEntry(entry);
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(zos));
+ writeThemeValues(writer);
+ writer.flush();
+ }
+
+}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/CDEMotifTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/CDEMotifTheme.java
index 9b20383c0a..9c95a19d0c 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/CDEMotifTheme.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/CDEMotifTheme.java
@@ -21,7 +21,7 @@ import generic.theme.LafType;
public class CDEMotifTheme extends DiscoverableGTheme {
public CDEMotifTheme() {
- super("Motif", LafType.MOTIF, false);
+ super("Motif Theme", LafType.MOTIF, false);
}
}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatDarculaTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatDarculaTheme.java
index d3889efa9d..dd0b021a2d 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatDarculaTheme.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatDarculaTheme.java
@@ -20,6 +20,6 @@ import generic.theme.LafType;
public class FlatDarculaTheme extends DiscoverableGTheme {
public FlatDarculaTheme() {
- super("Flat Darcula", LafType.FLAT_DARCULA, true);
+ super("Flat Darcula Theme", LafType.FLAT_DARCULA, true);
}
}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatDarkTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatDarkTheme.java
index 989beceff1..66e43dc376 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatDarkTheme.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatDarkTheme.java
@@ -20,6 +20,6 @@ import generic.theme.LafType;
public class FlatDarkTheme extends DiscoverableGTheme {
public FlatDarkTheme() {
- super("Flat Dark", LafType.FLAT_DARK, true);
+ super("Flat Dark Theme", LafType.FLAT_DARK, true);
}
}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatLightTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatLightTheme.java
index 1a6a249812..45e5de4ae2 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatLightTheme.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatLightTheme.java
@@ -21,7 +21,7 @@ import generic.theme.LafType;
public class FlatLightTheme extends DiscoverableGTheme {
public FlatLightTheme() {
- super("Flat Light", LafType.FLAT_LIGHT, false);
+ super("Flat Light Theme", LafType.FLAT_LIGHT, false);
}
}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/GTKTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/GTKTheme.java
index 7644cbd668..6d3e964c46 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/GTKTheme.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/GTKTheme.java
@@ -21,7 +21,7 @@ import generic.theme.LafType;
public class GTKTheme extends DiscoverableGTheme {
public GTKTheme() {
- super("GTK+", LafType.GTK, false);
+ super("GTK+ Theme", LafType.GTK, false);
}
}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/MacTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/MacTheme.java
index 98167df786..903edcf114 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/MacTheme.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/MacTheme.java
@@ -21,6 +21,6 @@ import generic.theme.LafType;
public class MacTheme extends DiscoverableGTheme {
public MacTheme() {
- super("Mac OS X", LafType.MAC, false);
+ super("Mac OS X Theme", LafType.MAC, false);
}
}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/MetalTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/MetalTheme.java
index fbb2fa0d9a..8a9ffecfa8 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/MetalTheme.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/MetalTheme.java
@@ -21,7 +21,7 @@ import generic.theme.LafType;
public class MetalTheme extends DiscoverableGTheme {
public MetalTheme() {
- super("Metal", LafType.METAL, false);
+ super("Metal Theme", LafType.METAL, false);
}
}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/NimbusTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/NimbusTheme.java
index 117fbb1b7c..8b244dc213 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/NimbusTheme.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/NimbusTheme.java
@@ -21,7 +21,7 @@ import generic.theme.LafType;
public class NimbusTheme extends DiscoverableGTheme {
public NimbusTheme() {
- super("Nimbus", LafType.NIMBUS, false);
+ super("Nimbus Theme", LafType.NIMBUS, false);
}
}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/WindowsClassicTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/WindowsClassicTheme.java
index 8286d13d42..9790379cad 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/WindowsClassicTheme.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/WindowsClassicTheme.java
@@ -21,6 +21,6 @@ import generic.theme.LafType;
public class WindowsClassicTheme extends DiscoverableGTheme {
public WindowsClassicTheme() {
- super("Windows Classic", LafType.WINDOWS_CLASSIC, false);
+ super("Windows Classic Theme", LafType.WINDOWS_CLASSIC, false);
}
}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/WindowsTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/WindowsTheme.java
index 1ee1dc4992..f7ed0f2f5a 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/WindowsTheme.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/WindowsTheme.java
@@ -21,6 +21,6 @@ import generic.theme.LafType;
public class WindowsTheme extends DiscoverableGTheme {
public WindowsTheme() {
- super("Windows", LafType.WINDOWS, false);
+ super("Windows Theme", LafType.WINDOWS, false);
}
}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GenericFlatLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GenericFlatLookAndFeelManager.java
new file mode 100644
index 0000000000..0877b57a0c
--- /dev/null
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GenericFlatLookAndFeelManager.java
@@ -0,0 +1,34 @@
+/* ###
+ * 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;
+
+/**
+ * Common {@link LookAndFeelInstaller} for any of the "Flat" lookAndFeels
+ */
+public class GenericFlatLookAndFeelManager extends LookAndFeelManager {
+
+ public GenericFlatLookAndFeelManager(LafType laf) {
+ super(laf);
+ }
+
+ @Override
+ protected LookAndFeelInstaller getLookAndFeelInstaller() {
+ return new FlatLookAndFeelInstaller(getLookAndFeelType());
+ }
+
+}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GenericLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GenericLookAndFeelManager.java
new file mode 100644
index 0000000000..411a5fc835
--- /dev/null
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GenericLookAndFeelManager.java
@@ -0,0 +1,35 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package generic.theme.laf;
+
+import generic.theme.LafType;
+
+/**
+ * Generic {@link LookAndFeelManager} for lookAndFeels that do not require any special handling
+ * to install or update
+ */
+public class GenericLookAndFeelManager extends LookAndFeelManager {
+
+ public GenericLookAndFeelManager(LafType laf) {
+ super(laf);
+ }
+
+ @Override
+ protected LookAndFeelInstaller getLookAndFeelInstaller() {
+ return new LookAndFeelInstaller(getLookAndFeelType());
+ }
+
+}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GTKLookAndFeelInstaller.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelInstaller.java
similarity index 68%
rename from Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GTKLookAndFeelInstaller.java
rename to Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelInstaller.java
index fecaa8a017..bcc955381b 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GTKLookAndFeelInstaller.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelInstaller.java
@@ -15,13 +15,13 @@
*/
package generic.theme.laf;
-import javax.swing.*;
+import javax.swing.UnsupportedLookAndFeelException;
import generic.theme.LafType;
-public class GTKLookAndFeelInstaller extends LookAndFeelInstaller {
+public class GtkLookAndFeelInstaller extends LookAndFeelInstaller {
- public GTKLookAndFeelInstaller() {
+ public GtkLookAndFeelInstaller() {
super(LafType.GTK);
}
@@ -30,14 +30,12 @@ public class GTKLookAndFeelInstaller extends LookAndFeelInstaller {
IllegalAccessException, UnsupportedLookAndFeelException {
super.installLookAndFeel();
- LookAndFeel lookAndFeel = UIManager.getLookAndFeel();
- WrappingLookAndFeel wrappingLookAndFeel = new WrappingLookAndFeel(lookAndFeel);
- UIManager.setLookAndFeel(wrappingLookAndFeel);
- }
-
- @Override
- protected void installJavaDefaults() {
- // handled by WrappingLookAndFeel
}
+// @Override
+// protected void installJavaDefaults() {
+// // GTK does not support changing its values, so set the javaDefaults to an empty map
+// Gui.setJavaDefaults(new GThemeValueMap());
+// }
+//
}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/DefaultTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelManager.java
similarity index 66%
rename from Ghidra/Framework/Generic/src/main/java/generic/theme/DefaultTheme.java
rename to Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelManager.java
index 8750fb1015..93e3016005 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/DefaultTheme.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelManager.java
@@ -13,11 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package generic.theme;
+package generic.theme.laf;
-public class DefaultTheme extends DiscoverableGTheme {
+import generic.theme.LafType;
- public DefaultTheme() {
- super("Default", LafType.SYSTEM, false);
+public class GtkLookAndFeelManager extends LookAndFeelManager {
+
+ public GtkLookAndFeelManager() {
+ super(LafType.GTK);
}
+
+ @Override
+ protected LookAndFeelInstaller getLookAndFeelInstaller() {
+ return new GtkLookAndFeelInstaller();
+ }
+
}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelInstaller.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelInstaller.java
index b601cd4097..9d07a74aa1 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelInstaller.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelInstaller.java
@@ -54,7 +54,7 @@ public class LookAndFeelInstaller {
* @throws UnsupportedLookAndFeelException if
* lnf.isSupportedLookAndFeel()
is false
*/
- public void install() throws ClassNotFoundException, InstantiationException,
+ public final void install() throws ClassNotFoundException, InstantiationException,
IllegalAccessException, UnsupportedLookAndFeelException {
cleanUiDefaults();
installLookAndFeel();
@@ -88,8 +88,7 @@ public class LookAndFeelInstaller {
}
/**
- * Installs Colors, Fonts, and Icons into the UIDefaults. Subclasses my override this if they need to install
- * UI properties in a different way.
+ * Extracts java default colors, fonts, and icons and stores them in {@link Gui}.
*/
protected void installJavaDefaults() {
GThemeValueMap javaDefaults = extractJavaDefaults();
@@ -99,22 +98,37 @@ public class LookAndFeelInstaller {
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();
GColorUIResource gColor = Gui.getGColorUiResource(id);
defaults.put(id, gColor);
}
+
+ // For fonts and icons we only want to install values that have been changed by
+ // the theme
for (FontValue fontValue : javaDefaults.getFonts()) {
String id = fontValue.getId();
- //Note: fonts don't support indirect values, so there is no GFont object
- Font font = Gui.getFont(id);
- defaults.put(id, font);
+ FontValue themeValue = theme.getFont(id);
+ if (themeValue != null) {
+ Font font = Gui.getFont(id);
+ defaults.put(id, font);
+ }
+ }
+ for (IconValue iconValue : javaDefaults.getIcons()) {
+ String id = iconValue.getId();
+ IconValue themeValue = theme.getIcon(id);
+ if (themeValue != null) {
+ Icon icon = Gui.getRawIcon(id, true);
+ defaults.put(id, icon);
+ }
}
-// for (IconValue iconValue : javaDefaults.getIcons()) {
-// String id = iconValue.getId();
-// GIconUIResource gIcon = Gui.getGIconUiResource(id);
-// defaults.put(id, gIcon);
-// }
}
protected GThemeValueMap extractJavaDefaults() {
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelManager.java
new file mode 100644
index 0000000000..eb92e37dd2
--- /dev/null
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelManager.java
@@ -0,0 +1,97 @@
+/* ###
+ * 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.*;
+
+import javax.swing.*;
+
+import generic.theme.*;
+
+/**
+ * Manages installing and updating a {@link LookAndFeel}
+ */
+public abstract class LookAndFeelManager {
+
+ private LafType laf;
+
+ protected LookAndFeelManager(LafType laf) {
+ this.laf = laf;
+ }
+
+ protected abstract LookAndFeelInstaller getLookAndFeelInstaller();
+
+ public LafType getLookAndFeelType() {
+ return laf;
+ }
+
+ public void installLookAndFeel() throws ClassNotFoundException, InstantiationException,
+ IllegalAccessException, UnsupportedLookAndFeelException {
+
+ LookAndFeelInstaller installer = getLookAndFeelInstaller();
+ installer.install();
+ updateComponentUis();
+ }
+
+ public void update() {
+ GColor.refreshAll();
+ GIcon.refreshAll();
+ updateComponentUis();
+// repaintAll();
+ }
+
+ public void updateColor(String id, Color color, boolean isJavaColor) {
+ GColor.refreshAll();
+ repaintAll();
+ }
+
+ public void updateIcon(String id, Icon icon, boolean isJavaIcon) {
+ // Icons are a mixed bag. Java Icons are direct and Ghidra Icons are indirect (to support static use)
+ // Mainly because Nimbus is buggy and can't handle non-nimbus Icons, so we can't wrap them
+ // So need to update UiDefaults for java icons. For Ghidra Icons, it is sufficient to refrech
+ // GIcons and repaint
+ if (isJavaIcon) {
+ UIManager.getDefaults().put(id, icon);
+ updateComponentUis();
+ }
+ GIcon.refreshAll();
+ repaintAll();
+ }
+
+ public void updateFont(String id, Font font, boolean isJavaFont) {
+ if (isJavaFont) {
+ UIManager.getDefaults().put(id, font);
+ updateComponentUis();
+ }
+ else {
+ repaintAll();
+ }
+
+ }
+
+ private void updateComponentUis() {
+ for (Window window : Window.getWindows()) {
+ SwingUtilities.updateComponentTreeUI(window);
+ }
+ }
+
+ protected void repaintAll() {
+ for (Window window : Window.getWindows()) {
+ window.repaint();
+ }
+ }
+
+}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MotifLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MotifLookAndFeelManager.java
new file mode 100644
index 0000000000..ea92e1bb09
--- /dev/null
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MotifLookAndFeelManager.java
@@ -0,0 +1,31 @@
+/* ###
+ * 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 MotifLookAndFeelManager extends LookAndFeelManager {
+
+ public MotifLookAndFeelManager() {
+ super(LafType.MOTIF);
+ }
+
+ @Override
+ protected LookAndFeelInstaller getLookAndFeelInstaller() {
+ return new MotifLookAndFeelInstaller();
+ }
+
+}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelInstaller.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelInstaller.java
index 72436a9ae0..414dc79d9b 100644
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelInstaller.java
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelInstaller.java
@@ -22,7 +22,6 @@ import javax.swing.*;
import javax.swing.plaf.nimbus.NimbusLookAndFeel;
import generic.theme.*;
-import ghidra.util.Msg;
public class NimbusLookAndFeelInstaller extends LookAndFeelInstaller {
@@ -37,7 +36,7 @@ public class NimbusLookAndFeelInstaller extends LookAndFeelInstaller {
@Override
protected void installJavaDefaults() {
- // do nothing - already handled by extended NimbusLookAndFeel
+ // do nothing - already handled by installing extended NimbusLookAndFeel
}
@Override
@@ -60,9 +59,44 @@ public class NimbusLookAndFeelInstaller extends LookAndFeelInstaller {
@Override
public UIDefaults getDefaults() {
+ UIDefaults defaults = super.getDefaults();
+ GThemeValueMap javaDefaults = extractJavaDefaults(defaults);
+
+ // need to set javaDefalts now to trigger building currentValues so the when
+ // we create GColors below, they can be resolved.
+ Gui.setJavaDefaults(javaDefaults);
+
+ // replace all colors with GColors
+ for (ColorValue colorValue : javaDefaults.getColors()) {
+ String id = colorValue.getId();
+ defaults.put(id, Gui.getGColorUiResource(id));
+ }
+
+ GTheme theme = Gui.getActiveTheme();
+
+ // only replace fonts that have been changed by the theme
+ for (FontValue fontValue : theme.getFonts()) {
+ String id = fontValue.getId();
+ Font font = Gui.getFont(id);
+ defaults.put(id, font);
+ }
+
+ // only replace icons that have been changed by the theme
+ for (IconValue iconValue : theme.getIcons()) {
+ String id = iconValue.getId();
+ Icon icon = Gui.getRawIcon(id, true);
+ defaults.put(id, icon);
+ }
+
+ defaults.put("Label.textForeground", Gui.getGColorUiResource("Label.foreground"));
+ GColor.refreshAll();
+ GIcon.refreshAll();
+ return defaults;
+ }
+
+ private GThemeValueMap extractJavaDefaults(UIDefaults defaults) {
GThemeValueMap javaDefaults = new GThemeValueMap();
- UIDefaults defaults = super.getDefaults();
List colorIds =
LookAndFeelInstaller.getLookAndFeelIdsForType(defaults, Color.class);
for (String id : colorIds) {
@@ -79,31 +113,12 @@ public class NimbusLookAndFeelInstaller extends LookAndFeelInstaller {
}
List iconIds =
LookAndFeelInstaller.getLookAndFeelIdsForType(defaults, Icon.class);
- Msg.debug(LookAndFeelInstaller.class, "Icons found: " + iconIds.size());
for (String id : iconIds) {
Icon icon = defaults.getIcon(id);
javaDefaults.addIcon(new IconValue(id, icon));
}
- Gui.setJavaDefaults(javaDefaults);
- for (String id : colorIds) {
- defaults.put(id, Gui.getGColorUiResource(id));
- }
-// for (String id : iconIds) {
-// GIconUIResource icon = Gui.getGIconUiResource(id);
-// if (icon.getId().equals("Menu.arrowIcon")) {
-// defaults.put(id, new IconWrappedImageIcon(Gui.getRawIcon(id, false)));
-// }
-// else {
-// defaults.put(id, Gui.getGIconUiResource(id));
-// }
-// }
-
-// javaDefaults.addColor(new ColorValue("Label.textForground", "Label.foreground"));
- defaults.put("Label.textForeground", Gui.getGColorUiResource("Label.foreground"));
- GColor.refreshAll();
- GIcon.refreshAll();
- return defaults;
+ return javaDefaults;
}
}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelManager.java
new file mode 100644
index 0000000000..79a7df090d
--- /dev/null
+++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelManager.java
@@ -0,0 +1,102 @@
+/* ###
+ * 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.*;
+
+import javax.swing.*;
+
+import generic.theme.LafType;
+
+public class NimbusLookAndFeelManager extends LookAndFeelManager {
+ private UIDefaults overrides = new UIDefaults();
+
+ public NimbusLookAndFeelManager() {
+ super(LafType.NIMBUS);
+ }
+
+ @Override
+ protected LookAndFeelInstaller getLookAndFeelInstaller() {
+ return new NimbusLookAndFeelInstaller();
+ }
+
+ @Override
+ public void updateColor(String id, Color color, boolean isJavaColor) {
+ super.updateColor(id, color, isJavaColor);
+ }
+
+ @Override
+ public void updateFont(String id, Font font, boolean isJavaFont) {
+ if (isJavaFont) {
+ overrides.put(id, font);
+ updateNimbusOverrides();
+ }
+ repaintAll();
+ }
+
+ @Override
+ public void updateIcon(String id, Icon icon, boolean isJavaIcon) {
+ if (isJavaIcon) {
+ overrides.put(id, icon);
+ updateNimbusOverrides();
+ }
+ repaintAll();
+ }
+
+ private void updateNimbusOverrides() {
+ UIDefaults defaults = getNimbusOverrides();
+ for (Window window : Window.getWindows()) {
+ updateNimbusUI(window, defaults);
+ }
+ }
+
+ private void updateNimbusUI(Component c, UIDefaults defaults) {
+ updateNimbusUIComp(c, defaults);
+ c.invalidate();
+ c.validate();
+ c.repaint();
+ }
+
+ private UIDefaults getNimbusOverrides() {
+ UIDefaults defaults = new UIDefaults();
+ defaults.putAll(overrides);
+ return defaults;
+ }
+
+ private void updateNimbusUIComp(Component c, UIDefaults defaults) {
+ if (c instanceof JComponent) {
+ JComponent jc = (JComponent) c;
+ jc.putClientProperty("Nimbus.Overrides", defaults);
+ JPopupMenu jpm = jc.getComponentPopupMenu();
+ if (jpm != null) {
+ updateNimbusUI(jpm, defaults);
+ }
+ }
+ Component[] children = null;
+ if (c instanceof JMenu) {
+ children = ((JMenu) c).getMenuComponents();
+ }
+ else if (c instanceof Container) {
+ children = ((Container) c).getComponents();
+ }
+ if (children != null) {
+ for (Component child : children) {
+ updateNimbusUIComp(child, defaults);
+ }
+ }
+ }
+
+}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/WrappingLookAndFeel.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/WrappingLookAndFeel.java
deleted file mode 100644
index 0f969d925e..0000000000
--- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/WrappingLookAndFeel.java
+++ /dev/null
@@ -1,120 +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.Component;
-import java.util.List;
-
-import javax.swing.*;
-
-import generic.theme.*;
-
-public class WrappingLookAndFeel extends LookAndFeel {
- private LookAndFeel delegate;
-
- WrappingLookAndFeel(LookAndFeel delegate) {
- this.delegate = delegate;
- }
-
- @Override
- public UIDefaults getDefaults() {
- GThemeValueMap javaDefaults = new GThemeValueMap();
-
- UIDefaults defaults = delegate.getDefaults();
- List colorIds =
- LookAndFeelInstaller.getLookAndFeelIdsForType(defaults, Color.class);
- for (String id : colorIds) {
- Color color = defaults.getColor(id);
- ColorValue value = new ColorValue(id, color);
- javaDefaults.addColor(value);
- }
- Gui.setJavaDefaults(javaDefaults);
- for (String id : colorIds) {
- defaults.put(id, Gui.getGColorUiResource(id));
-// defaults.put(id, new GColor(id));
- }
- defaults.put("Label.textForeground", Gui.getGColorUiResource("Label.foreground"));
- GColor.refreshAll();
- GIcon.refreshAll();
- return defaults;
- }
-
- @Override
- public String getName() {
- return delegate.getName();
- }
-
- @Override
- public String getID() {
- return delegate.getID();
- }
-
- @Override
- public String getDescription() {
- return delegate.getDescription();
- }
-
- @Override
- public boolean isNativeLookAndFeel() {
- return delegate.isNativeLookAndFeel();
- }
-
- @Override
- public boolean isSupportedLookAndFeel() {
- return delegate.isSupportedLookAndFeel();
- }
-
- @Override
- public LayoutStyle getLayoutStyle() {
- return delegate.getLayoutStyle();
- }
-
- @Override
- public void provideErrorFeedback(Component component) {
- delegate.provideErrorFeedback(component);
- }
-
- @Override
- public Icon getDisabledIcon(JComponent component, Icon icon) {
- return delegate.getDisabledIcon(component, icon);
- }
-
- @Override
- public Icon getDisabledSelectedIcon(JComponent component, Icon icon) {
- return delegate.getDisabledSelectedIcon(component, icon);
- }
-
- @Override
- public boolean getSupportsWindowDecorations() {
- return delegate.getSupportsWindowDecorations();
- }
-
- @Override
- public void initialize() {
- delegate.initialize();
- }
-
- @Override
- public void uninitialize() {
- delegate.uninitialize();
- }
-
- @Override
- public String toString() {
- return "Wrapped: " + delegate.toString();
- }
-}
diff --git a/Ghidra/Framework/Generic/src/main/java/resources/ResourceManager.java b/Ghidra/Framework/Generic/src/main/java/resources/ResourceManager.java
index 6b92305ea8..cec8a034fe 100644
--- a/Ghidra/Framework/Generic/src/main/java/resources/ResourceManager.java
+++ b/Ghidra/Framework/Generic/src/main/java/resources/ResourceManager.java
@@ -47,7 +47,7 @@ import utility.module.ModuleUtilities;
* as opposed to using the flawed constructor {@link ImageIcon#ImageIcon(Image)}.
*/
public class ResourceManager {
-
+ public final static String EXTERNAL_ICON_PREFIX = "[EXTERNAL]";
private final static String DEFAULT_ICON_FILENAME = Images.BOMB;
private static ImageIcon DEFAULT_ICON;
private static Map iconMap = new HashMap<>();
@@ -525,46 +525,50 @@ public class ResourceManager {
return icons;
}
- private static ImageIcon doLoadIcon(String filename) {
+ private static ImageIcon doLoadIcon(String path) {
+
+ // if the has the "external prefix", it is an icon in the user's application directory
+ if (path.startsWith(EXTERNAL_ICON_PREFIX)) {
+ String relativePath = path.substring(EXTERNAL_ICON_PREFIX.length());
+ File dir = Application.getUserSettingsDirectory();
+ File iconFile = new File(dir, relativePath);
+ if (iconFile.exists()) {
+ try {
+ return new UrlImageIcon(path, iconFile.toURI().toURL());
+ }
+ catch (MalformedURLException e) {
+ // handled below
+ }
+ }
+ return null;
+ }
+
// if only the name of an icon is given, but not a path, check to see if it is
// a resource that lives under our "images/" folder
- if (!filename.contains("/")) {
- URL url = getResource("images/" + filename);
+ if (!path.contains("/")) {
+ URL url = getResource("images/" + path);
if (url != null) {
- return new UrlImageIcon(filename, url);
+ return new UrlImageIcon(path, url);
}
}
// look for it directly with the given path
- URL url = getResource(filename);
+ URL url = getResource(path);
if (url != null) {
- return new UrlImageIcon(filename, url);
+ return new UrlImageIcon(path, url);
}
// try using the filename as a file path
- File imageFile = new File(filename);
+ File imageFile = new File(path);
if (imageFile.exists()) {
try {
- return new UrlImageIcon(filename, imageFile.toURI().toURL());
+ return new UrlImageIcon(path, imageFile.toURI().toURL());
}
catch (MalformedURLException e) {
// handled below
}
}
- // try to see if is an icon in the users application directory
- File dir = Application.getUserSettingsDirectory();
- File iconFile = new File(dir, filename);
- if (iconFile.exists()) {
- try {
- return new UrlImageIcon(filename, iconFile.toURI().toURL());
- }
- catch (MalformedURLException e) {
- // handled below
- }
-
- }
-
return null;
}
diff --git a/Ghidra/Framework/Generic/src/test/java/generic/theme/GThemeTest.java b/Ghidra/Framework/Generic/src/test/java/generic/theme/GThemeTest.java
index 3896f9ba2e..542bac96be 100644
--- a/Ghidra/Framework/Generic/src/test/java/generic/theme/GThemeTest.java
+++ b/Ghidra/Framework/Generic/src/test/java/generic/theme/GThemeTest.java
@@ -44,7 +44,7 @@ public class GThemeTest extends AbstractGenericTest {
@Before
public void setUp() {
- theme = new DefaultTheme();
+ theme = Gui.getDefaultTheme();
new Font("Courier", Font.BOLD, 12);
}
@@ -101,7 +101,7 @@ public class GThemeTest extends AbstractGenericTest {
theme = new FileGTheme(file);
assertEquals("abc", theme.getName());
- assertEquals(LafType.SYSTEM, theme.getLookAndFeelType());
+ assertEquals(LafType.getDefaultLookAndFeel(), theme.getLookAndFeelType());
assertEquals(Color.RED, theme.getColor("color.a.1").get(theme));
assertEquals(Color.BLUE, theme.getColor("color.a.2").get(theme));
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/app/plugin/gui/ThemeManagerPlugin.java b/Ghidra/Framework/Project/src/main/java/ghidra/app/plugin/gui/ThemeManagerPlugin.java
index 2282d4a45a..c71d9ed133 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/app/plugin/gui/ThemeManagerPlugin.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/app/plugin/gui/ThemeManagerPlugin.java
@@ -17,6 +17,7 @@ package ghidra.app.plugin.gui;
import docking.action.builder.ActionBuilder;
import docking.theme.gui.ThemeDialog;
+import docking.theme.gui.ThemeUtils;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.framework.main.ApplicationLevelOnlyPlugin;
import ghidra.framework.main.UtilityPluginPackage;
@@ -41,15 +42,42 @@ public class ThemeManagerPlugin extends Plugin implements ApplicationLevelOnlyPl
@Override
protected void init() {
+ String owner = getName();
+ String themeSubMenu = "Theme Actions";
- new ActionBuilder("", getName()).menuPath("Edit", "Theme")
- .onAction(e -> showThemeProperties())
+ String group = "theme";
+ new ActionBuilder("Edit Theme", owner)
+ .menuPath("Edit", "Theme")
+ .menuGroup(group, "1")
+ .onAction(e -> ThemeDialog.editTheme())
.buildAndInstall(tool);
- }
+ new ActionBuilder("Reset To Default", owner)
+ .menuPath("Edit", themeSubMenu, "Reset To Default")
+ .menuGroup(group, "2")
+ .onAction(e -> ThemeUtils.resetThemeToDefault())
+ .buildAndInstall(tool);
- private void showThemeProperties() {
- ThemeDialog.editTheme();
- }
+ new ActionBuilder("Import Theme", owner)
+ .menuPath("Edit", themeSubMenu, "Import...")
+ .menuGroup(group, "3")
+ .onAction(e -> ThemeUtils.importTheme())
+ .buildAndInstall(tool);
+ new ActionBuilder("Export Theme", owner)
+ .menuPath("Edit", themeSubMenu, "Export...")
+ .menuGroup(group, "4")
+ .onAction(e -> ThemeUtils.exportTheme())
+ .buildAndInstall(tool);
+
+ new ActionBuilder("Delete Theme", owner)
+ .menuPath("Edit", themeSubMenu, "Delete...")
+ .menuGroup(group, "5")
+// .enabledWhen(e -> Gui.getActiveTheme() instanceof FileGTheme)
+ .onAction(e -> ThemeUtils.deleteTheme())
+ .buildAndInstall(tool);
+
+ tool.setMenuGroup(new String[] { "Edit", themeSubMenu }, group, "2");
+
+ }
}