GP-1981 added import/export, theme listener, ThemePlugin tests, renamed

themes, deleteTheme action, zip support and got Nimbus working
This commit is contained in:
ghidragon 2022-08-08 18:18:51 -04:00
parent b4d2271474
commit 25f7df2aa7
50 changed files with 1507 additions and 637 deletions

View file

@ -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();
}
}
}

View file

@ -17,6 +17,7 @@ package ghidra.formats.gfilesystem;
import java.util.List;
import docking.widgets.SelectFromListDialog;
import ghidra.formats.gfilesystem.factory.FileSystemInfoRec;
/**

View file

@ -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;

View file

@ -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.
* <p>

View file

@ -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|

View file

@ -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]

View file

@ -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 "<Default>";
return "<Original>";
}
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<Icon> 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) {

View file

@ -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());
}
}

View file

@ -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());
}
}

View file

@ -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<String> 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();
}
}
}

View file

@ -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<IconValue, Obj
}
private void load() {
Msg.debug(this, "loading");
currentValues = Gui.getAllValues();
icons = currentValues.getIcons();
themeValues = new GThemeValueMap(currentValues);
@ -189,7 +186,8 @@ public class ThemeIconTableModel extends GDynamicColumnTableModel<IconValue, Obj
}
Icon icon = resolvedIcon.icon();
String sizeString = "[" + icon.getIconWidth() + "x" + icon.getIconHeight() + "] ";
String iconString = "<Internal>";
String iconString = FileGTheme.JAVA_ICON;
if (icon instanceof UrlImageIcon urlIcon) {
iconString = urlIcon.getOriginalPath();
}
@ -202,7 +200,7 @@ public class ThemeIconTableModel extends GDynamicColumnTableModel<IconValue, Obj
if (resolvedIcon.refId() != null) {
iconString = resolvedIcon.refId() + " [" + iconString + "]";
}
return sizeString + iconString;
return String.format("%-8s%s", sizeString, iconString);
}
@Override

View file

@ -0,0 +1,222 @@
/* ###
* 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 java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import docking.DockingWindowManager;
import docking.widgets.OptionDialog;
import docking.widgets.SelectFromListDialog;
import docking.widgets.dialogs.InputDialog;
import docking.widgets.filechooser.GhidraFileChooser;
import docking.widgets.filechooser.GhidraFileChooserMode;
import generic.theme.*;
import ghidra.framework.Application;
import ghidra.util.Msg;
import ghidra.util.filechooser.ExtensionFileFilter;
public class ThemeUtils {
public static boolean askToSaveThemeChanges() {
if (Gui.hasThemeChanges()) {
int result = OptionDialog.showYesNoCancelDialog(null, "Save Theme Changes?",
"You have made changes to the theme.\n Do you want save your changes?");
if (result == OptionDialog.CANCEL_OPTION) {
return false;
}
if (result == OptionDialog.YES_OPTION) {
return ThemeUtils.saveThemeChanges();
}
Gui.reloadGhidraDefaults();
}
return true;
}
/**
* Saves all current theme changes
* @return true if the operation was not cancelled.
*/
public static boolean saveThemeChanges() {
GTheme activeTheme = Gui.getActiveTheme();
String name = activeTheme.getName();
while (!canSaveToName(name)) {
name = getNameFromUser(name);
if (name == null) {
return false;
}
}
return saveCurrentValues(name);
}
public static void resetThemeToDefault() {
if (askToSaveThemeChanges()) {
Gui.setTheme(Gui.getDefaultTheme());
}
}
public static void importTheme() {
GhidraFileChooser chooser = new GhidraFileChooser(null);
chooser.setTitle("Choose Theme File");
chooser.setApproveButtonToolTipText("Select File");
chooser.setFileSelectionMode(GhidraFileChooserMode.FILES_ONLY);
chooser.setFileFilter(ExtensionFileFilter.forExtensions("Ghidra Theme Files",
GTheme.FILE_EXTENSION, GTheme.ZIP_FILE_EXTENSION));
File file = chooser.getSelectedFile();
if (file == null) {
return;
}
importTheme(file);
}
static void importTheme(File themeFile) {
if (!ThemeUtils.askToSaveThemeChanges()) {
return;
}
GTheme startingTheme = Gui.getActiveTheme();
try {
FileGTheme imported;
if (themeFile.getName().endsWith(GTheme.ZIP_FILE_EXTENSION)) {
imported = new ZipGTheme(themeFile);
}
else if (themeFile.getName().endsWith(GTheme.FILE_EXTENSION)) {
imported = new FileGTheme(themeFile);
}
else {
Msg.showError(ThemeUtils.class, null, "Error Importing Theme",
"Imported File must end in either " + GTheme.FILE_EXTENSION + " or " +
GTheme.ZIP_FILE_EXTENSION);
return;
}
Gui.setTheme(imported);
if (!ThemeUtils.saveThemeChanges()) {
Gui.setTheme(startingTheme);
}
}
catch (IOException e) {
Msg.showError(ThemeUtils.class, null, "Error Importing Theme File",
"Error encountered importing file: " + themeFile.getAbsolutePath(), e);
}
}
public static void exportTheme() {
if (!ThemeUtils.askToSaveThemeChanges()) {
return;
}
boolean hasExternalIcons = !Gui.getActiveTheme().getExternalIconFiles().isEmpty();
String message =
"Export as zip file? (You are not using any external icons so the zip\n" +
"file would only contain a single theme file.)";
if (hasExternalIcons) {
message =
"Export as zip file? (You have external icons so a zip file is required if you\n" +
"want to include the icons in the export.)";
}
int result = OptionDialog.showOptionDialog(null, "Export as Zip?", message, "Export Zip",
"Export File", OptionDialog.QUESTION_MESSAGE);
if (result == OptionDialog.CANCEL_OPTION) {
return;
}
boolean exportAsZip = result == OptionDialog.OPTION_ONE;
ExportThemeDialog dialog = new ExportThemeDialog(exportAsZip);
DockingWindowManager.showDialog(dialog);
}
public static void deleteTheme() {
List<GTheme> 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);
}
}

View file

@ -56,13 +56,12 @@ public abstract class ThemeValueEditor<T> {
*/
public void editValue(ThemeValue<T> 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<T> {
*/
protected abstract ThemeValue<T> createNewThemeValue(String id, T newValue);
private void valueChanged(T newValue) {
private void valueChanged(T value) {
ThemeValue<T> oldValue = currentThemeValue;
String id = oldValue.getId();
ThemeValue<T> 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<T> oldValue, ThemeValue<T> 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<T> originalValue;
protected EditorDialog(T initialValue) {
protected EditorDialog(ThemeValue<T> 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<T> {
return panel;
}
void setValue(T value) {
void setValue(ThemeValue<T> 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<T> {
@Override
protected void cancelCallback() {
valueChanged(originalValue);
firePropertyChangeEvent(currentThemeValue, originalValue);
close();
dialog = null;
}

View file

@ -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<T> extends DialogComponentProvider {
return selectedObject;
}
public void setSelectedObject(T obj) {
listPanel.setSelectedValue(obj);
}
private void doSelect() {
selectedObject = null;
actionComplete = false;

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 B

View file

@ -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<GTheme> 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<GTheme> 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);
}
}

View file

@ -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);
}
}

View file

@ -27,8 +27,9 @@ import ghidra.util.WebColors;
import resources.icons.UrlImageIcon;
public class FileGTheme extends GTheme {
public static final String JAVA_ICON = "<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<ColorValue> colors = getColors();
Collections.sort(colors);
List<FontValue> fonts = getFonts();
Collections.sort(fonts);
List<IconValue> 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<ColorValue> colors = getColors();
Collections.sort(colors);
List<FontValue> fonts = getFonts();
Collections.sort(fonts);
List<IconValue> 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 "<UNKNOWN>";
return JAVA_ICON;
}
private String getValueOutput(FontValue fontValue) {

View file

@ -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;

View file

@ -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<String, ColorValue> colorMap = new HashMap<>();
Map<String, FontValue> fontMap = new HashMap<>();
Map<String, IconValue> iconMap = new HashMap<>();
protected Map<String, ColorValue> colorMap = new HashMap<>();
protected Map<String, FontValue> fontMap = new HashMap<>();
protected Map<String, IconValue> 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<File> getExternalIconFiles() {
Set<File> 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;
}
}

View file

@ -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<GTheme> allThemes;
private static GTheme activeTheme = getDefaultTheme();
private static Set<GTheme> allThemes = null;
private static GThemeValueMap ghidraLightDefaults = new GThemeValueMap();
private static GThemeValueMap ghidraDarkDefaults = new GThemeValueMap();
@ -55,8 +57,15 @@ public class Gui {
private static Map<String, GColorUIResource> gColorMap = new HashMap<>();
private static boolean isInitialized;
private static Map<String, GIconUIResource> 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<ThemeListener> 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<GTheme> getAllThemes() {
if (allThemes == null) {
allThemes = findThemes();
}
return Collections.unmodifiableSet(allThemes);
loadThemes();
return new HashSet<>(allThemes);
}
public static Set<GTheme> getSupportedThemes() {
if (allThemes == null) {
allThemes = findThemes();
}
loadThemes();
Set<GTheme> 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<GTheme> findThemes() {
Set<GTheme> 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<GTheme> set = new HashSet<>();
set.addAll(findDiscoverableThemes());
set.addAll(loadThemesFromFiles());
allThemes = set;
}
}
private static Collection<GTheme> loadThemesFromFiles() {
List<File> 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<ColorValue> 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();
}
}

View file

@ -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;
}
}
}

View file

@ -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
}
}

View file

@ -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<Section> 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);

View file

@ -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;
}
}

View file

@ -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<File> 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();
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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());
}
}

View file

@ -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());
}
}

View file

@ -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());
// }
//
}

View file

@ -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();
}
}

View file

@ -54,7 +54,7 @@ public class LookAndFeelInstaller {
* @throws UnsupportedLookAndFeelException if
* <code>lnf.isSupportedLookAndFeel()</code> 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() {

View file

@ -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();
}
}
}

View file

@ -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();
}
}

View file

@ -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<String> colorIds =
LookAndFeelInstaller.getLookAndFeelIdsForType(defaults, Color.class);
for (String id : colorIds) {
@ -79,31 +113,12 @@ public class NimbusLookAndFeelInstaller extends LookAndFeelInstaller {
}
List<String> 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;
}
}

View file

@ -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);
}
}
}
}

View file

@ -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<String> 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();
}
}

View file

@ -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<String, ImageIcon> 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;
}

View file

@ -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));

View file

@ -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");
}
}