GP-1981 Refactored Gui to use ThemeManager

This commit is contained in:
ghidragon 2022-11-07 16:52:28 -05:00
parent a92a27e9f1
commit edfb5a0877
78 changed files with 1902 additions and 1230 deletions

View File

@ -212,10 +212,10 @@ public interface AutoOptions {
else {
options.registerOption(key.getName(), type, defaultValue, help, description,
editor);
// TODO: Wish Ghidra would do this upon any option registration
options.putObject(key.getName(), defaultValue, type);
}
// TODO: Wish Ghidra would do this upon any option registration
options.putObject(key.getName(), defaultValue, type);
}
}

View File

@ -68,6 +68,8 @@ color.fg.plugin.comments.history.text = blue
color.fg.plugin.comments.history.user = color.fg
color.fg.plugin.comments.history.date = rgb(124, 37, 18)
color.bg.plugin.programtree = color.bg
color.bg.plugin.datamgr.edge.default = blue
color.bg.plugin.datamgr.edge.composite = magenta
color.bg.plugin.datamgr.edge.reference = blue

View File

@ -37,6 +37,6 @@
icon.fsbrowser.file.substring.release. = images/famfamfam_silk_icons_v013/bullet_purple.png
icon.fsbrowser.file.overlay.imported = images/checkmark_green.gif
icon.fsbrowser.file.overlay.filesystem = EMPTY_ICON{images/nuvola/16x16/ledgreen.png[size(8,8)][move(8,8)]} // lower right quadrant
icon.fsbrowser.file.overlay.imported = EMPTY_ICON{images/checkmark_green.gif[size(8,8)][move(8,8)]} // lower right quadrant
icon.fsbrowser.file.overlay.filesystem = EMPTY_ICON{images/nuvola/16x16/ledgreen.png[size(8,8)][move(0,8)]} // lower left quadrant
icon.fsbrowser.file.overlay.missing.password = EMPTY_ICON{images/lock.png[size(8,8)][move(8,0)]} // upper right quadrant

View File

@ -38,7 +38,6 @@ import ghidra.program.database.ProgramDB;
import ghidra.util.*;
import ghidra.util.exception.UsrException;
import ghidra.util.task.TaskLauncher;
import ghidra.util.task.TaskMonitorAdapter;
/**
* Main Ghidra application class. Creates
@ -73,7 +72,6 @@ public class GhidraRun implements GhidraLaunchable {
Runnable mainTask = () -> {
GhidraApplicationConfiguration configuration = new GhidraApplicationConfiguration();
configuration.setTaskMonitor(new StatusReportingTaskMonitor());
Application.initializeApplication(layout, configuration);
log = LogManager.getLogger(GhidraRun.class);
@ -243,15 +241,3 @@ public class GhidraRun implements GhidraLaunchable {
// this exists just to allow access to the constructor
}
}
class StatusReportingTaskMonitor extends TaskMonitorAdapter {
@Override
public synchronized void setCancelEnabled(boolean enable) {
// Not permitted
}
@Override
public void setMessage(String message) {
SplashScreen.updateSplashScreenStatus(message);
}
}

View File

@ -33,8 +33,7 @@ import docking.actions.KeyBindingUtils;
import docking.options.editor.FontEditor;
import docking.widgets.OptionDialog;
import docking.widgets.filechooser.GhidraFileChooser;
import generic.theme.GIcon;
import generic.theme.Gui;
import generic.theme.*;
import ghidra.framework.plugintool.ComponentProviderAdapter;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
@ -274,7 +273,7 @@ public class TextEditorComponentProvider extends ComponentProviderAdapter {
FontEditor editor = new FontEditor();
editor.setValue(Gui.getFont(FONT_ID));
editor.showDialog();
Gui.setFont(FONT_ID, (Font) editor.getValue());
ThemeManager.getInstance().setFont(FONT_ID, (Font) editor.getValue());
}
private void save() {

View File

@ -34,6 +34,8 @@ import resources.ResourceManager;
* Cell renderer for the drag and drop tree.
*/
class DnDTreeCellRenderer extends DefaultTreeCellRenderer {
private static final Color BACKGROUND_UNSELECTED = new GColor("color.bg.tree");
private static final Color BACKGROUND_SELECTED = new GColor("color.bg.tree.selected");
private static final String DISABLED_DOCS = "DisabledDocument.gif";
private static final String DISABLED_FRAGMENT = "DisabledFragment";
@ -73,8 +75,8 @@ class DnDTreeCellRenderer extends DefaultTreeCellRenderer {
*/
DnDTreeCellRenderer() {
super();
defaultNonSelectionColor = new GColor("Tree.textBackground");
defaultSelectionColor = new GColor("Tree.selectionBackground");
defaultNonSelectionColor = BACKGROUND_UNSELECTED;
defaultSelectionColor = BACKGROUND_SELECTED;
rowForFeedback = -1;
// disable HTML rendering

View File

@ -70,6 +70,7 @@ public abstract class DragNDropTree extends JTree implements Draggable, Droppabl
public DragNDropTree(DefaultTreeModel model) {
super(model);
setBackground(new GColor("color.bg.tree"));
treeModel = model;
this.root = (ProgramNode) model.getRoot();
//setEditable(true); // edit interferes with drag gesture listener

View File

@ -315,8 +315,9 @@ public class ProgramDnDTree extends DragNDropTree {
return false;
}
try {
Object data = e.getTransferable().getTransferData(
SelectionTransferable.localProgramSelectionFlavor);
Object data = e.getTransferable()
.getTransferData(
SelectionTransferable.localProgramSelectionFlavor);
SelectionTransferData transferData = (SelectionTransferData) data;
return program.getDomainFile().getPathname().equals(transferData.getProgramPath());
}

View File

@ -31,9 +31,8 @@ import docking.actions.KeyBindingUtils;
import docking.options.editor.FontEditor;
import docking.widgets.OptionDialog;
import generic.jar.ResourceFile;
import generic.theme.GIcon;
import generic.theme.*;
import generic.theme.GThemeDefaults.Colors;
import generic.theme.Gui;
import ghidra.app.script.GhidraScriptUtil;
import ghidra.util.*;
import ghidra.util.datastruct.FixedSizeStack;
@ -508,7 +507,7 @@ public class GhidraScriptEditorComponentProvider extends ComponentProvider {
FontEditor editor = new FontEditor();
editor.setValue(Gui.getFont(FONT_ID));
editor.showDialog();
Gui.setFont(FONT_ID, (Font) editor.getValue());
ThemeManager.getInstance().setFont(FONT_ID, (Font) editor.getValue());
}
private void save() {

View File

@ -20,7 +20,7 @@ import docking.DockingWindowManager;
import docking.framework.ApplicationInformationDisplayFactory;
import docking.framework.SplashScreen;
import docking.widgets.PopupKeyStorePasswordProvider;
import generic.theme.Gui;
import generic.theme.ApplicationThemeManager;
import ghidra.docking.util.LookAndFeelUtils;
import ghidra.formats.gfilesystem.crypto.CryptoProviders;
import ghidra.formats.gfilesystem.crypto.PopupGUIPasswordProvider;
@ -30,6 +30,7 @@ import ghidra.framework.preferences.Preferences;
import ghidra.net.ApplicationKeyManagerFactory;
import ghidra.util.ErrorDisplay;
import ghidra.util.SystemUtilities;
import ghidra.util.task.TaskMonitorAdapter;
public class GhidraApplicationConfiguration extends HeadlessGhidraApplicationConfiguration {
@ -43,12 +44,13 @@ public class GhidraApplicationConfiguration extends HeadlessGhidraApplicationCon
@Override
protected void initializeApplication() {
Gui.initialize();
ApplicationThemeManager.initialize();
LookAndFeelUtils.performPlatformSpecificFixups();
if (showSplashScreen) {
showUserAgreement();
SplashScreen.showSplashScreen();
this.monitor = new StatusReportingTaskMonitor();
}
super.initializeApplication();
@ -89,4 +91,17 @@ public class GhidraApplicationConfiguration extends HeadlessGhidraApplicationCon
public ErrorDisplay getErrorDisplay() {
return new DockingErrorDisplay();
}
private static class StatusReportingTaskMonitor extends TaskMonitorAdapter {
@Override
public synchronized void setCancelEnabled(boolean enable) {
// Not permitted
}
@Override
public void setMessage(String message) {
SplashScreen.updateSplashScreenStatus(message);
}
}
}

View File

@ -66,7 +66,7 @@ public class FileIconService {
}
private void createSubstringMap() {
GThemeValueMap values = Gui.getAllValues();
GThemeValueMap values = ThemeManager.getInstance().getCurrentValues();
List<IconValue> icons = values.getIcons();
for (IconValue iconValue : icons) {
String id = iconValue.getId();

View File

@ -32,7 +32,7 @@ import org.junit.*;
import docking.test.AbstractDockingTest;
import generic.theme.GColor;
import generic.theme.GThemeDefaults.Colors.Palette;
import generic.theme.Gui;
import generic.theme.ThemeManager;
import ghidra.framework.options.*;
import ghidra.framework.options.OptionsTest.FRUIT;
import ghidra.program.database.ProgramBuilder;
@ -61,7 +61,7 @@ public class OptionsDBTest extends AbstractDockingTest {
ProgramDB program = builder.getProgram();
txID = program.startTransaction("Test");
options = new OptionsDB(program);
Gui.setColor("color.test", Palette.MAGENTA);
ThemeManager.getInstance().setColor("color.test", Palette.MAGENTA);
testColor = new GColor("color.test");
}

View File

@ -33,7 +33,7 @@ public class TokenHighlightColors {
float h = (float) Math.random(); // 0-360
float s = .25f; // saturation; gray to full color; full color is too harsh for highlights
float b = 1f; // brightness; black to full color
if (Gui.getActiveTheme().useDarkDefaults()) {
if (Gui.isDarkTheme()) {
s = .5f; // a bit more color against a dark background
b = .5f; // less brightness, as the background is not as bright
}

View File

@ -92,7 +92,7 @@ src/main/resources/images/page_go.png||FAMFAMFAM Icons - CC 2.5||||END|
src/main/resources/images/page_green.png||FAMFAMFAM Icons - CC 2.5||||END|
src/main/resources/images/play.png||GHIDRA||||END|
src/main/resources/images/preferences-system-windows.png||Tango Icons - Public Domain||||END|
src/main/resources/images/redo.png||GHIDRA||||END|
src/main/resources/images/redo.png||Crystal Clear Icons - LGPL 2.1||||END|
src/main/resources/images/software-update-available.png||Tango Icons - Public Domain|||tango icon set|END|
src/main/resources/images/table.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
src/main/resources/images/tag.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
@ -100,7 +100,7 @@ src/main/resources/images/text_lowercase.png||FAMFAMFAM Icons - CC 2.5|||famfamf
src/main/resources/images/textfield_rename.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
src/main/resources/images/tip.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END|
src/main/resources/images/trash-empty.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END|
src/main/resources/images/undo.png||GHIDRA||||END|
src/main/resources/images/undo.png||Crystal Clear Icons - LGPL 2.1||||END|
src/main/resources/images/user-home.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END|
src/main/resources/images/view-filter.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END|
src/main/resources/images/warning.help.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END|

View File

@ -82,12 +82,16 @@ color.border.provider.disconnected = orange
color.bg.filechooser = color.bg
color.fg.filechooser = color.fg
color.bg.filechooser.shortcut = lightGray
color.bg.fieldpanel = color.bg
color.fg.fieldpanel = color.fg
color.bg.fieldpanel.selection = color.bg.selection
color.bg.fieldpanel.highlight = color.bg.highlight
color.bg.fieldpanel.selection.and.highlight = green
color.bg.tree = [color]Tree.textBackground
color.bg.tree.selected = [color]Tree.selectionBackground
// docking buttons
color.fg.button = black
@ -239,3 +243,10 @@ color.bg.tableheader.gradient.end.primary = darkBlue
// docking buttons
color.fg.button = darkGray
color.bg.filechooser.shortcut = system.color.bg.widget
color.bg.tree = color.bg
color.bg.tree.selected = [color]Tree.selectionBackground
icon.undo = eatbits1.png
icon.redo = eatbits2.png

View File

@ -23,6 +23,7 @@ import javax.swing.*;
import docking.action.*;
import generic.theme.Gui;
import generic.theme.ThemeManager;
import ghidra.util.*;
import ghidra.util.exception.AssertException;
import help.HelpDescriptor;
@ -801,7 +802,7 @@ public abstract class ComponentProvider implements HelpDescriptor, ActionContext
else {
size = Math.max(size - 1, 3);
}
Gui.setFont(registeredFontId, font.deriveFont((float) size));
ThemeManager.getInstance().setFont(registeredFontId, font.deriveFont((float) size));
}
/**

View File

@ -38,7 +38,7 @@ import docking.widgets.list.GListCellRenderer;
import docking.widgets.table.GTableCellRenderer;
import docking.widgets.tree.support.GTreeRenderer;
import generic.theme.GThemeDefaults.Colors.Palette;
import generic.theme.Gui;
import ghidra.docking.util.LookAndFeelUtils;
import ghidra.util.HTMLUtilities;
import resources.ResourceManager;
@ -126,7 +126,7 @@ public class DockingUtils {
public static JSeparator createToolbarSeparator() {
Dimension sepDim = new Dimension(2, ICON_SIZE + 2);
JSeparator separator = new JSeparator(SwingConstants.VERTICAL);
if (Gui.isUsingAquaUI(separator.getUI())) {
if (LookAndFeelUtils.isUsingAquaUI(separator.getUI())) {
separator.setUI(new BasicSeparatorUI());
}
separator.setPreferredSize(sepDim); // ugly work around to force height of separator

View File

@ -312,7 +312,7 @@ public class StatusBar extends JPanel {
int value = 0;
int delta = 16;
if (Gui.getActiveTheme().useDarkDefaults()) {
if (Gui.isDarkTheme()) {
value = 128;
delta = -16;
}

View File

@ -32,9 +32,9 @@ import ghidra.util.Msg;
public class ApplicationInformationDisplayFactory {
private static final GIcon ICON_HOME = new GIcon("icon.docking.application.home");
private static final GIcon ICON_16 = new GIcon("icon.docking.application.16");
private static final GIcon ICON_128 = new GIcon("icon.base.application.128");
private static final String ICON_HOME = "icon.docking.application.home";
private static final String ICON_16 = "icon.docking.application.16";
private static final String ICON_128 = "icon.docking.application.128";
static {
PluggableServiceRegistry.registerPluggableService(
@ -144,13 +144,13 @@ public class ApplicationInformationDisplayFactory {
}
protected Icon getSplashScreenIcon128() {
return ICON_128;
return new GIcon(ICON_128);
}
protected List<Image> doGetWindowIcons() {
List<Image> list = new ArrayList<>();
list.add(ICON_128.getImageIcon().getImage());
list.add(ICON_16.getImageIcon().getImage());
list.add(new GIcon(ICON_128).getImageIcon().getImage());
list.add(new GIcon(ICON_16).getImageIcon().getImage());
return list;
}
@ -163,7 +163,7 @@ public class ApplicationInformationDisplayFactory {
}
protected Icon doGetHomeIcon() {
return ICON_HOME;
return new GIcon(ICON_HOME);
}
protected Runnable doGetHomeCallback() {

View File

@ -17,7 +17,7 @@ package docking.framework;
import docking.DockingErrorDisplay;
import docking.widgets.PopupKeyStorePasswordProvider;
import generic.theme.Gui;
import generic.theme.ApplicationThemeManager;
import ghidra.docking.util.LookAndFeelUtils;
import ghidra.framework.ApplicationConfiguration;
import ghidra.net.ApplicationKeyManagerFactory;
@ -49,7 +49,7 @@ public class DockingApplicationConfiguration extends ApplicationConfiguration {
protected void initializeApplication() {
super.initializeApplication();
Gui.initialize();
ApplicationThemeManager.initialize();
LookAndFeelUtils.performPlatformSpecificFixups();
if (showSplashScreen) {

View File

@ -24,7 +24,7 @@ import javax.swing.KeyStroke;
import org.apache.commons.lang3.StringUtils;
import docking.action.DockingActionIf;
import generic.theme.Gui;
import ghidra.docking.util.LookAndFeelUtils;
import ghidra.util.StringUtilities;
class DockingToolBarUtils {
@ -96,7 +96,7 @@ class DockingToolBarUtils {
builder.append(InputEvent.getModifiersExText(modifiers));
// The Aqua LaF does not use the '+' symbol between modifiers
if (!Gui.isUsingAquaUI(button.getUI())) {
if (!LookAndFeelUtils.isUsingAquaUI(button.getUI())) {
builder.append('+');
}
}

View File

@ -97,6 +97,16 @@ public class MultipleActionDockingToolbarButton extends EmptyBorderButton {
popupContext = createPopupContext();
}
@Override
public void updateUI() {
removeMouseListener(popupListener);
super.updateUI();
installMouseListeners();
}
private void installMouseListeners() {
MouseListener[] mouseListeners = getMouseListeners();
for (MouseListener mouseListener : mouseListeners) {

View File

@ -43,9 +43,11 @@ public class ExportThemeDialog extends DialogComponentProvider {
private JTextField fileTextField;
private GCheckBox includeDefaultsCheckbox;
private boolean exportAsZip;
private ThemeManager themeManager;
public ExportThemeDialog(boolean exportAsZip) {
public ExportThemeDialog(ThemeManager themeManager, boolean exportAsZip) {
super("Export Theme");
this.themeManager = themeManager;
this.exportAsZip = exportAsZip;
addWorkPanel(buildMainPanel());
addOKButton();
@ -62,7 +64,7 @@ public class ExportThemeDialog extends DialogComponentProvider {
private boolean exportTheme() {
String themeName = nameField.getText();
GTheme activeTheme = Gui.getActiveTheme();
GTheme activeTheme = themeManager.getActiveTheme();
LafType laf = activeTheme.getLookAndFeelType();
boolean useDarkDefaults = activeTheme.useDarkDefaults();
File file = new File(fileTextField.getText());
@ -87,10 +89,10 @@ public class ExportThemeDialog extends DialogComponentProvider {
private void loadValues(GTheme exportTheme) {
if (includeDefaultsCheckbox.isSelected()) {
exportTheme.load(Gui.getAllValues());
exportTheme.load(themeManager.getCurrentValues());
}
else {
exportTheme.load(Gui.getNonDefaultValues());
exportTheme.load(themeManager.getNonDefaultValues());
}
}
@ -114,7 +116,7 @@ public class ExportThemeDialog extends DialogComponentProvider {
private Component buildNameField() {
nameField = new JTextField(25);
nameField.setText(Gui.getActiveTheme().getName());
nameField.setText(themeManager.getActiveTheme().getName());
return nameField;
}
@ -127,7 +129,7 @@ public class ExportThemeDialog extends DialogComponentProvider {
private Component buildFilePanel() {
File homeDir = new File(System.getProperty("user.home")); // prefer the home directory
String name = Gui.getActiveTheme().getName();
String name = themeManager.getActiveTheme().getName();
String filename = name.replaceAll(" ", "_") + ".";
filename += exportAsZip ? GTheme.ZIP_FILE_EXTENSION : GTheme.FILE_EXTENSION;
File file = new File(homeDir, filename);

View File

@ -37,7 +37,7 @@ public class IconValueEditor extends ThemeValueEditor<Icon> {
@Override
protected Icon getRawValue(String id) {
return Gui.getIcon(id, true);
return Gui.getIcon(id);
}
@Override

View File

@ -29,7 +29,7 @@ import docking.action.ActionContextProvider;
import docking.widgets.table.GFilterTable;
import docking.widgets.table.GTable;
import generic.theme.ColorValue;
import generic.theme.Gui;
import generic.theme.ThemeManager;
import ghidra.util.Swing;
/**
@ -41,10 +41,12 @@ public class ThemeColorTable extends JPanel implements ActionContextProvider {
private ColorValueEditor colorEditor = new ColorValueEditor(this::colorValueChanged);
private GTable table;
private GFilterTable<ColorValue> filterTable;
private ThemeManager themeManager;
public ThemeColorTable() {
public ThemeColorTable(ThemeManager themeManager) {
super(new BorderLayout());
colorTableModel = new ThemeColorTableModel();
this.themeManager = themeManager;
colorTableModel = new ThemeColorTableModel(themeManager);
filterTable = new GFilterTable<>(colorTableModel);
table = filterTable.getTable();
@ -87,7 +89,7 @@ public class ThemeColorTable extends JPanel implements ActionContextProvider {
// run later - don't rock the boat in the middle of a listener callback
Swing.runLater(() -> {
ColorValue newValue = (ColorValue) event.getNewValue();
Gui.setColor(newValue);
themeManager.setColor(newValue);
});
}

View File

@ -43,9 +43,11 @@ public class ThemeColorTableModel extends GDynamicColumnTableModel<ColorValue, O
private GThemeValueMap defaultValues;
private GThemeValueMap lightDefaultValues;
private GThemeValueMap darkDefaultValues;
private ThemeManager themeManager;
public ThemeColorTableModel() {
public ThemeColorTableModel(ThemeManager themeManager) {
super(new ServiceProviderStub());
this.themeManager = themeManager;
load();
}
@ -53,7 +55,7 @@ public class ThemeColorTableModel extends GDynamicColumnTableModel<ColorValue, O
* Reloads the just the current values shown in the table. Called whenever a color changes.
*/
public void reloadCurrent() {
currentValues = Gui.getAllValues();
currentValues = themeManager.getCurrentValues();
colors = currentValues.getColors();
fireTableDataChanged();
}
@ -67,20 +69,17 @@ public class ThemeColorTableModel extends GDynamicColumnTableModel<ColorValue, O
fireTableDataChanged();
}
/**
* Returns the original value for the current theme
*/
public ColorValue getThemeValue(String id) {
ColorValue getThemeValue(String id) {
return themeValues.getColor(id);
}
private void load() {
currentValues = Gui.getAllValues();
currentValues = themeManager.getCurrentValues();
colors = currentValues.getColors();
themeValues = Gui.getThemeValues();
defaultValues = Gui.getDefaults();
lightDefaultValues = Gui.getApplicationLightDefaults();
darkDefaultValues = Gui.getApplicationDarkDefaults();
themeValues = themeManager.getThemeValues();
defaultValues = themeManager.getDefaults();
lightDefaultValues = themeManager.getApplicationLightDefaults();
darkDefaultValues = themeManager.getApplicationDarkDefaults();
}
@ -155,7 +154,10 @@ public class ThemeColorTableModel extends GDynamicColumnTableModel<ColorValue, O
if (colorValue == null) {
return null;
}
Color color = colorValue.get(valueMap);
Color color = colorValue.hasResolvableValue(valueMap) ? colorValue.get(valueMap) : null;
if (color == null) {
return null;
}
return new ResolvedColor(id, colorValue.getReferenceId(), color);
}

View File

@ -49,8 +49,11 @@ public class ThemeDialog extends DialogComponentProvider {
private ThemeFontTable fontTable;
private ThemeIconTable iconTable;
public ThemeDialog() {
private ThemeManager themeManager;
public ThemeDialog(ThemeManager themeManager) {
super("Theme Dialog", false);
this.themeManager = themeManager;
addWorkPanel(createMainPanel());
addDismissButton();
@ -79,7 +82,7 @@ public class ThemeDialog extends DialogComponentProvider {
.enabledWhen(c -> c.isChanged())
.popupWhen(c -> true)
.helpLocation(new HelpLocation("Theming", "Restore_Value"))
.onAction(c -> c.getThemeValue().installValue())
.onAction(c -> c.getThemeValue().installValue(themeManager))
.build();
addAction(resetValueAction);
}
@ -93,44 +96,44 @@ public class ThemeDialog extends DialogComponentProvider {
}
private boolean handleChanges() {
if (Gui.hasThemeChanges()) {
if (themeManager.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 ThemeUtils.saveThemeChanges();
return ThemeUtils.saveThemeChanges(themeManager);
}
Gui.restoreThemeValues();
themeManager.restoreThemeValues();
}
return true;
}
protected void saveCallback() {
ThemeUtils.saveThemeChanges();
ThemeUtils.saveThemeChanges(themeManager);
}
private void restoreCallback() {
if (Gui.hasThemeChanges()) {
if (themeManager.hasThemeChanges()) {
int result = OptionDialog.showYesNoDialog(null, "Restore Theme Values",
"Are you sure you want to discard all your changes?");
if (result == OptionDialog.NO_OPTION) {
return;
}
}
Gui.restoreThemeValues();
themeManager.restoreThemeValues();
}
private void reloadDefaultsCallback() {
if (Gui.hasThemeChanges()) {
if (themeManager.hasThemeChanges()) {
int result = OptionDialog.showYesNoDialog(null, "Reload Ghidra Default Values",
"This will discard all your theme changes. Continue?");
if (result == OptionDialog.NO_OPTION) {
return;
}
}
Gui.reloadApplicationDefaults();
themeManager.reloadApplicationDefaults();
}
private void reset() {
@ -147,15 +150,15 @@ public class ThemeDialog extends DialogComponentProvider {
return;
}
if (!ThemeUtils.askToSaveThemeChanges()) {
if (!ThemeUtils.askToSaveThemeChanges(themeManager)) {
Swing.runLater(() -> updateCombo());
return;
}
String themeName = (String) e.getItem();
Swing.runLater(() -> {
GTheme theme = Gui.getTheme(themeName);
Gui.setTheme(theme);
GTheme theme = themeManager.getTheme(themeName);
themeManager.setTheme(theme);
if (theme.getLookAndFeelType() == LafType.GTK) {
setStatusText(
"Warning - Themes using the GTK LookAndFeel do not support changing java component colors, fonts or icons.",
@ -171,7 +174,7 @@ public class ThemeDialog extends DialogComponentProvider {
}
private void updateButtons() {
boolean hasChanges = Gui.hasThemeChanges();
boolean hasChanges = themeManager.hasThemeChanges();
saveButton.setEnabled(hasChanges);
restoreButton.setEnabled(hasChanges);
}
@ -194,25 +197,25 @@ public class ThemeDialog extends DialogComponentProvider {
}
private void updateCombo() {
Set<GTheme> supportedThemes = Gui.getSupportedThemes();
Set<GTheme> supportedThemes = themeManager.getSupportedThemes();
List<String> themeNames =
supportedThemes.stream().map(t -> t.getName()).collect(Collectors.toList());
Collections.sort(themeNames);
combo.removeItemListener(comboListener);
combo.setModel(new DefaultComboBoxModel<String>(new Vector<String>(themeNames)));
combo.setSelectedItem(Gui.getActiveTheme().getName());
combo.setSelectedItem(themeManager.getActiveTheme().getName());
combo.addItemListener(comboListener);
}
private Component buildThemeCombo() {
JPanel panel = new JPanel();
Set<GTheme> supportedThemes = Gui.getSupportedThemes();
Set<GTheme> supportedThemes = themeManager.getSupportedThemes();
List<String> themeNames =
supportedThemes.stream().map(t -> t.getName()).collect(Collectors.toList());
Collections.sort(themeNames);
combo = new GhidraComboBox<>(themeNames);
combo.setSelectedItem(Gui.getActiveTheme().getName());
combo.setSelectedItem(themeManager.getActiveTheme().getName());
combo.addItemListener(comboListener);
panel.add(new JLabel("Theme: "), BorderLayout.WEST);
@ -223,9 +226,9 @@ public class ThemeDialog extends DialogComponentProvider {
private Component buildTabedTables() {
tabbedPane = new JTabbedPane();
colorTable = new ThemeColorTable();
fontTable = new ThemeFontTable();
iconTable = new ThemeIconTable();
colorTable = new ThemeColorTable(themeManager);
fontTable = new ThemeFontTable(themeManager);
iconTable = new ThemeIconTable(themeManager);
tabbedPane.add("Colors", colorTable);
tabbedPane.add("Fonts", fontTable);
tabbedPane.add("Icons", iconTable);
@ -250,12 +253,16 @@ public class ThemeDialog extends DialogComponentProvider {
return saveButton;
}
public static void editTheme() {
/**
* Edits the current theme
* @param themeManager the application ThemeManager
*/
public static void editTheme(ThemeManager themeManager) {
if (INSTANCE != null) {
INSTANCE.toFront();
return;
}
INSTANCE = new ThemeDialog();
INSTANCE = new ThemeDialog(themeManager);
DockingWindowManager.showDialog(INSTANCE);
}

View File

@ -29,7 +29,7 @@ import docking.action.ActionContextProvider;
import docking.widgets.table.GFilterTable;
import docking.widgets.table.GTable;
import generic.theme.FontValue;
import generic.theme.Gui;
import generic.theme.ThemeManager;
import ghidra.util.Swing;
/**
@ -41,11 +41,13 @@ public class ThemeFontTable extends JPanel implements ActionContextProvider {
private FontValueEditor fontEditor = new FontValueEditor(this::fontValueChanged);
private GTable table;
private GFilterTable<FontValue> filterTable;
private ThemeManager themeManager;
public ThemeFontTable() {
public ThemeFontTable(ThemeManager themeManager) {
super(new BorderLayout());
this.themeManager = themeManager;
fontTableModel = new ThemeFontTableModel();
fontTableModel = new ThemeFontTableModel(themeManager);
filterTable = new GFilterTable<>(fontTableModel);
table = filterTable.getTable();
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
@ -86,7 +88,7 @@ public class ThemeFontTable extends JPanel implements ActionContextProvider {
// run later - don't rock the boat in the middle of a listener callback
Swing.runLater(() -> {
FontValue newValue = (FontValue) event.getNewValue();
Gui.setFont(newValue);
themeManager.setFont(newValue);
});
}

View File

@ -39,9 +39,11 @@ public class ThemeFontTableModel extends GDynamicColumnTableModel<FontValue, Obj
private GThemeValueMap currentValues;
private GThemeValueMap themeValues;
private GThemeValueMap defaultValues;
private ThemeManager themeManager;
public ThemeFontTableModel() {
public ThemeFontTableModel(ThemeManager themeManager) {
super(new ServiceProviderStub());
this.themeManager = themeManager;
load();
}
@ -49,7 +51,7 @@ public class ThemeFontTableModel extends GDynamicColumnTableModel<FontValue, Obj
* Reloads the just the current values shown in the table. Called whenever a font changes.
*/
public void reloadCurrent() {
currentValues = Gui.getAllValues();
currentValues = themeManager.getCurrentValues();
fonts = currentValues.getFonts();
fireTableDataChanged();
}
@ -64,10 +66,10 @@ public class ThemeFontTableModel extends GDynamicColumnTableModel<FontValue, Obj
}
private void load() {
currentValues = Gui.getAllValues();
currentValues = themeManager.getCurrentValues();
fonts = currentValues.getFonts();
themeValues = Gui.getThemeValues();
defaultValues = Gui.getDefaults();
themeValues = themeManager.getThemeValues();
defaultValues = themeManager.getDefaults();
}
@Override

View File

@ -26,8 +26,8 @@ import docking.ActionContext;
import docking.action.ActionContextProvider;
import docking.widgets.table.GFilterTable;
import docking.widgets.table.GTable;
import generic.theme.Gui;
import generic.theme.IconValue;
import generic.theme.ThemeManager;
import ghidra.util.Swing;
/**
@ -39,10 +39,12 @@ public class ThemeIconTable extends JPanel implements ActionContextProvider {
private IconValueEditor iconEditor = new IconValueEditor(this::iconValueChanged);
private GTable table;
private GFilterTable<IconValue> filterTable;
private ThemeManager themeManager;
public ThemeIconTable() {
public ThemeIconTable(ThemeManager themeManager) {
super(new BorderLayout());
iconTableModel = new ThemeIconTableModel();
this.themeManager = themeManager;
iconTableModel = new ThemeIconTableModel(themeManager);
filterTable = new GFilterTable<>(iconTableModel);
table = filterTable.getTable();
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
@ -82,7 +84,7 @@ public class ThemeIconTable extends JPanel implements ActionContextProvider {
// run later - don't rock the boat in the middle of a listener callback
Swing.runLater(() -> {
IconValue newValue = (IconValue) event.getNewValue();
Gui.setIcon(newValue);
themeManager.setIcon(newValue);
});
}

View File

@ -39,9 +39,11 @@ public class ThemeIconTableModel extends GDynamicColumnTableModel<IconValue, Obj
private GThemeValueMap currentValues;
private GThemeValueMap themeValues;
private GThemeValueMap defaultValues;
private ThemeManager themeManager;
public ThemeIconTableModel() {
public ThemeIconTableModel(ThemeManager themeManager) {
super(new ServiceProviderStub());
this.themeManager = themeManager;
load();
}
@ -49,7 +51,7 @@ public class ThemeIconTableModel extends GDynamicColumnTableModel<IconValue, Obj
* Reloads the just the current values shown in the table. Called whenever an icon changes.
*/
public void reloadCurrent() {
currentValues = Gui.getAllValues();
currentValues = themeManager.getCurrentValues();
icons = currentValues.getIcons();
fireTableDataChanged();
}
@ -64,10 +66,10 @@ public class ThemeIconTableModel extends GDynamicColumnTableModel<IconValue, Obj
}
private void load() {
currentValues = Gui.getAllValues();
currentValues = themeManager.getCurrentValues();
icons = currentValues.getIcons();
themeValues = Gui.getThemeValues();
defaultValues = Gui.getDefaults();
themeValues = themeManager.getThemeValues();
defaultValues = themeManager.getDefaults();
}
@Override

View File

@ -43,17 +43,17 @@ public class ThemeUtils {
* overwrites an existing file.
* @return true if the operation was not cancelled
*/
public static boolean askToSaveThemeChanges() {
if (Gui.hasThemeChanges()) {
public static boolean askToSaveThemeChanges(ThemeManager themeManager) {
if (themeManager.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();
return ThemeUtils.saveThemeChanges(themeManager);
}
Gui.reloadApplicationDefaults();
themeManager.reloadApplicationDefaults();
}
return true;
}
@ -63,32 +63,33 @@ public class ThemeUtils {
* name and asking to overwrite an existing file.
* @return true if the operation was not cancelled
*/
public static boolean saveThemeChanges() {
GTheme activeTheme = Gui.getActiveTheme();
public static boolean saveThemeChanges(ThemeManager themeManager) {
GTheme activeTheme = themeManager.getActiveTheme();
String name = activeTheme.getName();
while (!canSaveToName(name)) {
while (!canSaveToName(themeManager, name)) {
name = getNameFromUser(name);
if (name == null) {
return false;
}
}
return saveCurrentValues(name);
return saveCurrentValues(themeManager, name);
}
/**
* Resets the theme to the default, handling the case where the current theme has changes.
*/
public static void resetThemeToDefault() {
if (askToSaveThemeChanges()) {
Gui.setTheme(Gui.getDefaultTheme());
public static void resetThemeToDefault(ThemeManager themeManager) {
if (askToSaveThemeChanges(themeManager)) {
themeManager.setTheme(themeManager.getDefaultTheme());
}
}
/**
* Imports a theme. Handles the case where there are existing changes to the current theme.
* @param themeManager the application ThemeManager
*/
public static void importTheme() {
public static void importTheme(ThemeManager themeManager) {
GhidraFileChooser chooser = new GhidraFileChooser(null);
chooser.setTitle("Choose Theme File");
chooser.setApproveButtonToolTipText("Select File");
@ -100,14 +101,14 @@ public class ThemeUtils {
if (file == null) {
return;
}
importTheme(file);
importTheme(themeManager, file);
}
static void importTheme(File themeFile) {
if (!ThemeUtils.askToSaveThemeChanges()) {
static void importTheme(ThemeManager themeManager, File themeFile) {
if (!ThemeUtils.askToSaveThemeChanges(themeManager)) {
return;
}
GTheme startingTheme = Gui.getActiveTheme();
GTheme startingTheme = themeManager.getActiveTheme();
try {
GTheme imported = GTheme.loadTheme(themeFile);
// by setting the theme, we can let the normal save handle all the edge cases
@ -115,9 +116,9 @@ public class ThemeUtils {
// Also, the imported theme may contain default values which we don't want to save. So
// by going through the usual save mechanism, only values that differ from defaults
// be saved.
Gui.setTheme(imported);
if (!ThemeUtils.saveThemeChanges()) {
Gui.setTheme(startingTheme);
themeManager.setTheme(imported);
if (!ThemeUtils.saveThemeChanges(themeManager)) {
themeManager.setTheme(startingTheme);
}
}
catch (IOException e) {
@ -130,12 +131,13 @@ public class ThemeUtils {
/**
* Exports a theme, prompting the user to pick an file. Also handles dealing with any
* existing changes to the current theme.
* @param themeManager the ThemeManager that actually does the export
*/
public static void exportTheme() {
if (!ThemeUtils.askToSaveThemeChanges()) {
public static void exportTheme(ThemeManager themeManager) {
if (!ThemeUtils.askToSaveThemeChanges(themeManager)) {
return;
}
boolean hasExternalIcons = !Gui.getActiveTheme().getExternalIconFiles().isEmpty();
boolean hasExternalIcons = !themeManager.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.)";
@ -151,16 +153,16 @@ public class ThemeUtils {
}
boolean exportAsZip = result == OptionDialog.OPTION_ONE;
ExportThemeDialog dialog = new ExportThemeDialog(exportAsZip);
ExportThemeDialog dialog = new ExportThemeDialog(themeManager, exportAsZip);
DockingWindowManager.showDialog(dialog);
}
/**
* Prompts for and deletes a selected theme.
*/
public static void deleteTheme() {
public static void deleteTheme(ThemeManager themeManager) {
List<GTheme> savedThemes = new ArrayList<>(
Gui.getAllThemes().stream().filter(t -> t.getFile() != null).toList());
themeManager.getAllThemes().stream().filter(t -> t.getFile() != null).toList());
if (savedThemes.isEmpty()) {
Msg.showInfo(ThemeUtils.class, null, "Delete Theme", "There are no deletable themes");
return;
@ -171,7 +173,7 @@ public class ThemeUtils {
if (selectedTheme == null) {
return;
}
if (Gui.getActiveTheme().equals(selectedTheme)) {
if (themeManager.getActiveTheme().equals(selectedTheme)) {
Msg.showWarn(ThemeUtils.class, null, "Delete Failed",
"Can't delete the current theme.");
return;
@ -180,7 +182,7 @@ public class ThemeUtils {
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);
themeManager.deleteTheme(fileTheme);
}
}
@ -190,8 +192,8 @@ public class ThemeUtils {
return inputDialog.getValue();
}
private static boolean canSaveToName(String name) {
GTheme existing = Gui.getTheme(name);
private static boolean canSaveToName(ThemeManager themeManager, String name) {
GTheme existing = themeManager.getTheme(name);
// if no theme exists with that name, then we are save to save it
if (existing == null) {
return true;
@ -210,17 +212,17 @@ public class ThemeUtils {
return result == OptionDialog.YES_OPTION;
}
private static boolean saveCurrentValues(String themeName) {
GTheme activeTheme = Gui.getActiveTheme();
private static boolean saveCurrentValues(ThemeManager themeManager, String themeName) {
GTheme activeTheme = themeManager.getActiveTheme();
File file = getSaveFile(themeName);
GTheme newTheme = new GTheme(file, themeName, activeTheme.getLookAndFeelType(),
activeTheme.useDarkDefaults());
newTheme.load(Gui.getNonDefaultValues());
newTheme.load(themeManager.getNonDefaultValues());
try {
newTheme.save();
Gui.addTheme(newTheme);
Gui.setTheme(newTheme);
themeManager.addTheme(newTheme);
themeManager.setTheme(newTheme);
}
catch (IOException e) {
Msg.showError(ThemeUtils.class, null, "I/O Error",

View File

@ -24,7 +24,7 @@ import javax.swing.border.EmptyBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import generic.theme.Gui;
import ghidra.docking.util.LookAndFeelUtils;
import resources.ResourceManager;
/**
@ -123,7 +123,7 @@ public class EmptyBorderButton extends JButton {
// Mac OSX LNF doesn't give us rollover callbacks, so we have to add a mouse listener to
// do the work
if (Gui.isUsingAquaUI(getUI())) {
if (LookAndFeelUtils.isUsingAquaUI(getUI())) {
addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {

View File

@ -38,7 +38,6 @@ import docking.widgets.label.GLabel;
import docking.widgets.list.GListCellRenderer;
import generic.theme.GColor;
import generic.theme.GIcon;
import generic.theme.GThemeDefaults.Colors.Palette;
import ghidra.framework.preferences.Preferences;
import ghidra.util.*;
import ghidra.util.exception.AssertException;
@ -74,6 +73,8 @@ public class GhidraFileChooser extends DialogComponentProvider implements FileFi
static final String UP_BUTTON_NAME = "UP_BUTTON";
private static final Color FOREROUND_COLOR = new GColor("color.fg.filechooser");
private static final Color BACKGROUND_COLOR = new GColor("color.bg.filechooser");
private static final Color SHORTCUT_BACKGROUND_COLOR =
new GColor("color.bg.filechooser.shortcut");
static final String PREFERENCES_PREFIX = "G_FILE_CHOOSER";
private static final String WIDTH_PREFERENCE_PREFIX = PREFERENCES_PREFIX + ".WIDTH.";
private static final String HEIGHT_PREFERENCE_PREFIX = PREFERENCES_PREFIX + ".HEIGHT.";
@ -343,7 +344,7 @@ public class GhidraFileChooser extends DialogComponentProvider implements FileFi
JPanel panel = new JPanel(new BorderLayout());
panel.setBorder(BorderFactory.createLoweredBevelBorder());
panel.setBackground(Palette.DARK_GRAY);
panel.setBackground(SHORTCUT_BACKGROUND_COLOR);
panel.add(shortCutPanel, BorderLayout.NORTH);
return panel;
}

View File

@ -22,12 +22,14 @@ import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Map;
import javax.swing.*;
import javax.swing.JTable;
import javax.swing.SwingConstants;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableModel;
import docking.widgets.AbstractGCellRenderer;
import generic.theme.GColor;
import generic.theme.Gui;
import ghidra.docking.settings.*;
import ghidra.util.*;
import ghidra.util.exception.AssertException;
@ -56,7 +58,13 @@ public class GTableCellRenderer extends AbstractGCellRenderer implements TableCe
* Constructs a new GTableCellRenderer.
*/
public GTableCellRenderer() {
// When the Look And Feel changes, renderers are not auto updated because they
// are not part of the component tree. So listen for a change to the Look And Feel.
Gui.addThemeListener(e -> {
if (e.isLookAndFeelChanged()) {
updateUI();
}
});
}
/**
@ -100,10 +108,7 @@ public class GTableCellRenderer extends AbstractGCellRenderer implements TableCe
"Using a GTableCellRenderer in a non-GTable table. (Model class: " +
table.getModel().getClass().getName() + ")");
}
// check if LookAndFeel has changed
if (UIManager.getUI(this) != getUI()) {
updateUI();
}
GTable gTable = (GTable) table;
GTableCellRenderingData data = gTable.getRenderingData(column);
Object rowObject = null;

View File

@ -45,6 +45,7 @@ import docking.widgets.tree.internal.*;
import docking.widgets.tree.support.*;
import docking.widgets.tree.support.GTreeSelectionEvent.EventOrigin;
import docking.widgets.tree.tasks.*;
import generic.theme.*;
import generic.timer.ExpiringSwingTimer;
import ghidra.util.*;
import ghidra.util.exception.CancelledException;
@ -55,8 +56,8 @@ import ghidra.util.worker.PriorityWorker;
* Class for creating a JTree that supports filtering, threading, and a progress bar.
*/
public class GTree extends JPanel implements BusyListener {
public class GTree extends JPanel implements BusyListener, ThemeListener {
private static final Color BACKGROUND = new GColor("color.bg.tree");
private AutoScrollTree tree;
private GTreeModel model;
@ -134,6 +135,7 @@ public class GTree extends JPanel implements BusyListener {
uniquePreferenceKey));
filterUpdateManager = new SwingUpdateManager(1000, 30000, () -> updateModelFilter());
Gui.addThemeListener(this);
}
/**
@ -146,6 +148,13 @@ public class GTree extends JPanel implements BusyListener {
threadLocalMonitor.set(monitor);
}
@Override
public void themeChanged(ThemeEvent event) {
if (event.isLookAndFeelChanged()) {
model.fireNodeStructureChanged(getModelRoot());
}
}
/**
* Returns the monitor in associated with the GTree for the calling thread. This method is
* designed to be used by slow loading nodes that are loading <b>off the Swing thread</b>. Some
@ -1391,6 +1400,7 @@ public class GTree extends JPanel implements BusyListener {
public AutoScrollTree(TreeModel model) {
super(model);
setBackground(BACKGROUND);
scroller = new AutoscrollAdapter(this, 5);
setRowHeight(-1);// variable size rows

View File

@ -31,6 +31,8 @@ public class GTreeRenderer extends DefaultTreeCellRenderer implements GComponent
private static final Color VALID_DROP_TARGET_COLOR = new GColor("color.bg.tree.drag");
private static final int DEFAULT_MIN_ICON_WIDTH = 22;
private static final Color BACKGROUND_UNSELECTED = new GColor("color.bg.tree");
private static final Color BACKGROUND_SELECTED = new GColor("color.bg.tree.selected");
private Object dropTarget;
private boolean paintDropTarget;
@ -41,6 +43,8 @@ public class GTreeRenderer extends DefaultTreeCellRenderer implements GComponent
public GTreeRenderer() {
setHTMLRenderingEnabled(false);
setBackgroundNonSelectionColor(BACKGROUND_UNSELECTED);
setBackgroundSelectionColor(BACKGROUND_SELECTED);
}
@Override

View File

@ -21,10 +21,12 @@ import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Set;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import javax.swing.*;
import javax.swing.plaf.ComponentUI;
import docking.framework.ApplicationInformationDisplayFactory;
import generic.theme.LafType;
import generic.theme.ThemeManager;
import ghidra.framework.preferences.Preferences;
import ghidra.util.SystemUtilities;
@ -81,4 +83,30 @@ public class LookAndFeelUtils {
}
}
}
/**
* Returns the {@link LafType} for the currently active {@link LookAndFeel}
* @return the {@link LafType} for the currently active {@link LookAndFeel}
*/
public static LafType getLookAndFeelType() {
return ThemeManager.getInstance().getLookAndFeelType();
}
/**
* Returns true if the given UI object is using the Aqua Look and Feel.
* @param UI the UI to examine.
* @return true if the UI is using Aqua
*/
public static boolean isUsingAquaUI(ComponentUI UI) {
return ThemeManager.getInstance().isUsingAquaUI(UI);
}
/**
* Returns true if 'Nimbus' is the current Look and Feel
* @return true if 'Nimbus' is the current Look and Feel
*/
public static boolean isUsingNimbusUI() {
return ThemeManager.getInstance().isUsingNimbusUI();
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 B

After

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 B

After

Width:  |  Height:  |  Size: 692 B

View File

@ -41,67 +41,69 @@ import generic.theme.builtin.NimbusTheme;
public class ThemeUtilsTest extends AbstractDockingTest {
private Color testColor = Palette.RED;
private ThemeManager themeManager;
@Before
public void setup() {
themeManager = ThemeManager.getInstance();
GTheme nimbusTheme = new NimbusTheme();
GTheme metalTheme = new MetalTheme();
Gui.addTheme(nimbusTheme);
Gui.addTheme(metalTheme);
Gui.setTheme(nimbusTheme);
themeManager.addTheme(nimbusTheme);
themeManager.addTheme(metalTheme);
themeManager.setTheme(nimbusTheme);
// get rid of any leftover imported themes from previous tests
Set<GTheme> allThemes = Gui.getAllThemes();
Set<GTheme> allThemes = themeManager.getAllThemes();
for (GTheme theme : allThemes) {
if (!(theme instanceof DiscoverableGTheme)) {
Gui.deleteTheme(theme);
themeManager.deleteTheme(theme);
}
}
}
@Test
public void testImportThemeNonZip() throws IOException {
assertEquals("Nimbus Theme", Gui.getActiveTheme().getName());
assertEquals("Nimbus Theme", themeManager.getActiveTheme().getName());
File themeFile = createThemeFile("Bob");
ThemeUtils.importTheme(themeFile);
assertEquals("Bob", Gui.getActiveTheme().getName());
ThemeUtils.importTheme(themeManager, themeFile);
assertEquals("Bob", themeManager.getActiveTheme().getName());
}
@Test
public void testImportThemeFromZip() throws IOException {
assertEquals("Nimbus Theme", Gui.getActiveTheme().getName());
assertEquals("Nimbus Theme", themeManager.getActiveTheme().getName());
File themeFile = createZipThemeFile("zippy");
ThemeUtils.importTheme(themeFile);
assertEquals("zippy", Gui.getActiveTheme().getName());
ThemeUtils.importTheme(themeManager, themeFile);
assertEquals("zippy", themeManager.getActiveTheme().getName());
}
@Test
public void testImportThemeWithCurrentChangesCancelled() throws IOException {
assertEquals("Nimbus Theme", Gui.getActiveTheme().getName());
Gui.setColor("Panel.background", testColor);
assertTrue(Gui.hasThemeChanges());
assertEquals("Nimbus Theme", themeManager.getActiveTheme().getName());
themeManager.setColor("Panel.background", testColor);
assertTrue(themeManager.hasThemeChanges());
File themeFile = createThemeFile("Bob");
runSwingLater(() -> ThemeUtils.importTheme(themeFile));
runSwingLater(() -> ThemeUtils.importTheme(themeManager, themeFile));
OptionDialog dialog = waitForDialogComponent(OptionDialog.class);
assertNotNull(dialog);
assertEquals("Save Theme Changes?", dialog.getTitle());
pressButtonByText(dialog, "Cancel");
waitForSwing();
assertEquals("Nimbus Theme", Gui.getActiveTheme().getName());
assertEquals("Nimbus Theme", themeManager.getActiveTheme().getName());
}
@Test
public void testImportThemeWithCurrentChangesSaved() throws IOException {
assertEquals("Nimbus Theme", Gui.getActiveTheme().getName());
assertEquals("Nimbus Theme", themeManager.getActiveTheme().getName());
// make a change in the current theme, so you get asked to save
Gui.setColor("Panel.background", testColor);
assertTrue(Gui.hasThemeChanges());
themeManager.setColor("Panel.background", testColor);
assertTrue(themeManager.hasThemeChanges());
File themeFile = createThemeFile("Bob");
runSwingLater(() -> ThemeUtils.importTheme(themeFile));
runSwingLater(() -> ThemeUtils.importTheme(themeManager, themeFile));
OptionDialog dialog = waitForDialogComponent(OptionDialog.class);
assertNotNull(dialog);
assertEquals("Save Theme Changes?", dialog.getTitle());
@ -111,32 +113,32 @@ public class ThemeUtilsTest extends AbstractDockingTest {
runSwing(() -> inputDialog.setValue("Joe"));
pressButtonByText(inputDialog, "OK");
waitForSwing();
assertEquals("Bob", Gui.getActiveTheme().getName());
assertNotNull(Gui.getTheme("Joe"));
assertEquals("Bob", themeManager.getActiveTheme().getName());
assertNotNull(themeManager.getTheme("Joe"));
}
@Test
public void testImportThemeWithCurrentChangesThrownAway() throws IOException {
assertEquals("Nimbus Theme", Gui.getActiveTheme().getName());
assertEquals("Nimbus Theme", themeManager.getActiveTheme().getName());
// make a change in the current theme, so you get asked to save
Gui.setColor("Panel.background", testColor);
assertTrue(Gui.hasThemeChanges());
themeManager.setColor("Panel.background", testColor);
assertTrue(themeManager.hasThemeChanges());
File bobThemeFile = createThemeFile("Bob");
runSwingLater(() -> ThemeUtils.importTheme(bobThemeFile));
runSwingLater(() -> ThemeUtils.importTheme(themeManager, bobThemeFile));
OptionDialog dialog = waitForDialogComponent(OptionDialog.class);
assertNotNull(dialog);
assertEquals("Save Theme Changes?", dialog.getTitle());
pressButtonByText(dialog, "No");
waitForSwing();
assertEquals("Bob", Gui.getActiveTheme().getName());
assertEquals("Bob", themeManager.getActiveTheme().getName());
}
@Test
public void testExportThemeAsZip() throws IOException {
runSwingLater(() -> ThemeUtils.exportTheme());
runSwingLater(() -> ThemeUtils.exportTheme(themeManager));
OptionDialog dialog = waitForDialogComponent(OptionDialog.class);
pressButtonByText(dialog, "Export Zip");
ExportThemeDialog exportDialog = waitForDialogComponent(ExportThemeDialog.class);
@ -151,7 +153,7 @@ public class ThemeUtilsTest extends AbstractDockingTest {
@Test
public void testExportThemeAsFile() throws IOException {
runSwingLater(() -> ThemeUtils.exportTheme());
runSwingLater(() -> ThemeUtils.exportTheme(themeManager));
OptionDialog dialog = waitForDialogComponent(OptionDialog.class);
pressButtonByText(dialog, "Export File");
ExportThemeDialog exportDialog = waitForDialogComponent(ExportThemeDialog.class);
@ -167,29 +169,29 @@ public class ThemeUtilsTest extends AbstractDockingTest {
@Test
public void testDeleteTheme() throws IOException {
File themeFile = createThemeFile("Bob");
ThemeUtils.importTheme(themeFile);
ThemeUtils.importTheme(themeManager, themeFile);
themeFile = createThemeFile("Joe");
ThemeUtils.importTheme(themeFile);
ThemeUtils.importTheme(themeManager, themeFile);
themeFile = createThemeFile("Lisa");
ThemeUtils.importTheme(themeFile);
ThemeUtils.importTheme(themeManager, themeFile);
assertNotNull(Gui.getTheme("Bob"));
assertNotNull(Gui.getTheme("Joe"));
assertNotNull(Gui.getTheme("Lisa"));
assertNotNull(themeManager.getTheme("Bob"));
assertNotNull(themeManager.getTheme("Joe"));
assertNotNull(themeManager.getTheme("Lisa"));
runSwingLater(() -> ThemeUtils.deleteTheme());
runSwingLater(() -> ThemeUtils.deleteTheme(themeManager));
@SuppressWarnings("unchecked")
SelectFromListDialog<GTheme> dialog = waitForDialogComponent(SelectFromListDialog.class);
runSwing(() -> dialog.setSelectedObject(Gui.getTheme("Bob")));
runSwing(() -> dialog.setSelectedObject(themeManager.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"));
assertNotNull(themeManager.getTheme("Bob"));
assertNull(themeManager.getTheme("Joe"));
assertNotNull(themeManager.getTheme("Lisa"));
}
@ -231,7 +233,7 @@ public class ThemeUtilsTest extends AbstractDockingTest {
File file = createTempFile("Test_Theme", ".theme.zip");
GTheme outputTheme = new GTheme(file, themeName, LafType.METAL, false);
outputTheme.addColor(new ColorValue("Panel.Background", testColor));
outputTheme.saveToZip(file, false);
new ThemeWriter(outputTheme).writeThemeToZipFile(file);
return file;
}

View File

@ -28,7 +28,7 @@ icon.expand.all = expand_all.png
icon.configure.filter = exec.png
icon.clear = erase16.png
icon.delete = icon.error
icon.delete = edit-delete.png
icon.error = emblem-important.png
icon.home = go-home.png

View File

@ -0,0 +1,484 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package generic.theme;
import java.awt.Component;
import java.io.File;
import java.util.*;
import javax.swing.*;
import javax.swing.plaf.ComponentUI;
import com.formdev.flatlaf.FlatDarkLaf;
import com.formdev.flatlaf.FlatLightLaf;
import generic.theme.laf.LookAndFeelManager;
import ghidra.util.Msg;
import ghidra.util.classfinder.ClassSearcher;
/**
* This is the fully functional {@link ThemeManager} that manages themes in a application. To
* activate the theme functionality, Applications (or tests) must call
* {@link ApplicationThemeManager#initialize()}
*/
public class ApplicationThemeManager extends ThemeManager {
private GTheme activeTheme = getDefaultTheme();
private Set<GTheme> allThemes = null;
private GThemeValueMap applicationDefaults = new GThemeValueMap();
private GThemeValueMap applicationDarkDefaults = new GThemeValueMap();
private GThemeValueMap javaDefaults = new GThemeValueMap();
private GThemeValueMap systemValues = new GThemeValueMap();
protected ThemeFileLoader themeFileLoader = new ThemeFileLoader();
protected ThemePreferences themePreferences = new ThemePreferences();
private Map<String, GColorUIResource> gColorMap = new HashMap<>();
private Map<String, GIconUIResource> gIconMap = new HashMap<>();
// stores the original value for ids whose value has changed from the current theme
private GThemeValueMap changedValuesMap = new GThemeValueMap();
protected LookAndFeelManager lookAndFeelManager;
/**
* Initialized the Theme and its values for the application.
*/
public static void initialize() {
if (INSTANCE instanceof ApplicationThemeManager) {
Msg.error(ThemeManager.class, "Attempted to initialize theming more than once!");
return;
}
ApplicationThemeManager themeManager = new ApplicationThemeManager();
themeManager.doInitialize();
}
protected ApplicationThemeManager() {
// AppliationThemeManagers always replace any other instances
INSTANCE = this;
installInGui();
}
protected void doInitialize() {
installFlatLookAndFeels();
loadThemeDefaults();
setTheme(themePreferences.load());
}
@Override
public void reloadApplicationDefaults() {
loadThemeDefaults();
buildCurrentValues();
lookAndFeelManager.resetAll(javaDefaults);
notifyThemeChanged(new AllValuesChangedThemeEvent(false));
}
@Override
public void restoreThemeValues() {
buildCurrentValues();
lookAndFeelManager.resetAll(javaDefaults);
notifyThemeChanged(new AllValuesChangedThemeEvent(false));
}
@Override
public void restoreColor(String id) {
if (changedValuesMap.containsColor(id)) {
setColor(changedValuesMap.getColor(id));
}
}
@Override
public void restoreFont(String id) {
if (changedValuesMap.containsFont(id)) {
setFont(changedValuesMap.getFont(id));
}
}
@Override
public void restoreIcon(String id) {
if (changedValuesMap.containsIcon(id)) {
setIcon(changedValuesMap.getIcon(id));
}
}
@Override
public boolean isChangedColor(String id) {
return changedValuesMap.containsColor(id);
}
@Override
public boolean isChangedFont(String id) {
return changedValuesMap.containsFont(id);
}
@Override
public boolean isChangedIcon(String id) {
return changedValuesMap.containsIcon(id);
}
@Override
public void setTheme(GTheme theme) {
if (theme.hasSupportedLookAndFeel()) {
activeTheme = theme;
LafType lafType = theme.getLookAndFeelType();
lookAndFeelManager = lafType.getLookAndFeelManager(this);
try {
lookAndFeelManager.installLookAndFeel();
themePreferences.save(theme);
notifyThemeChanged(new AllValuesChangedThemeEvent(true));
}
catch (Exception e) {
Msg.error(this, "Error setting LookAndFeel: " + lafType.getName(), e);
}
}
currentValues.checkForUnresolvedReferences();
}
@Override
public void addTheme(GTheme newTheme) {
loadThemes();
allThemes.remove(newTheme);
allThemes.add(newTheme);
}
@Override
public void deleteTheme(GTheme theme) {
File file = theme.getFile();
if (file != null) {
file.delete();
}
if (allThemes != null) {
allThemes.remove(theme);
}
}
@Override
public Set<GTheme> getAllThemes() {
loadThemes();
return new HashSet<>(allThemes);
}
@Override
public Set<GTheme> getSupportedThemes() {
loadThemes();
Set<GTheme> supported = new HashSet<>();
for (GTheme theme : allThemes) {
if (theme.hasSupportedLookAndFeel()) {
supported.add(theme);
}
}
return supported;
}
@Override
public GTheme getActiveTheme() {
return activeTheme;
}
@Override
public LafType getLookAndFeelType() {
return activeTheme.getLookAndFeelType();
}
@Override
public GTheme getTheme(String themeName) {
Optional<GTheme> first =
getAllThemes().stream().filter(t -> t.getName().equals(themeName)).findFirst();
return first.orElse(null);
}
@Override
public GThemeValueMap getThemeValues() {
GThemeValueMap map = new GThemeValueMap();
map.load(javaDefaults);
map.load(systemValues);
map.load(applicationDefaults);
if (activeTheme.useDarkDefaults()) {
map.load(applicationDarkDefaults);
}
map.load(activeTheme);
return map;
}
@Override
public void setFont(FontValue newValue) {
FontValue currentValue = currentValues.getFont(newValue.getId());
if (newValue.equals(currentValue)) {
return;
}
updateChangedValuesMap(currentValue, newValue);
currentValues.addFont(newValue);
notifyThemeChanged(new FontChangedThemeEvent(currentValues, newValue));
// update all java LookAndFeel fonts affected by this changed
String id = newValue.getId();
Set<String> changedFontIds = findChangedJavaFontIds(id);
lookAndFeelManager.fontsChanged(changedFontIds);
}
@Override
public void setColor(ColorValue newValue) {
ColorValue currentValue = currentValues.getColor(newValue.getId());
if (newValue.equals(currentValue)) {
return;
}
updateChangedValuesMap(currentValue, newValue);
currentValues.addColor(newValue);
notifyThemeChanged(new ColorChangedThemeEvent(currentValues, newValue));
// now update the ui
if (lookAndFeelManager != null) {
lookAndFeelManager.colorsChanged();
}
}
@Override
public void setIcon(IconValue newValue) {
IconValue currentValue = currentValues.getIcon(newValue.getId());
if (newValue.equals(currentValue)) {
return;
}
updateChangedValuesMap(currentValue, newValue);
currentValues.addIcon(newValue);
notifyThemeChanged(new IconChangedThemeEvent(currentValues, newValue));
// now update the ui
// update all java LookAndFeel icons affected by this changed
String id = newValue.getId();
Set<String> changedIconIds = findChangedJavaIconIds(id);
Icon newIcon = newValue.get(currentValues);
lookAndFeelManager.iconsChanged(changedIconIds, newIcon);
}
@Override
public GColorUIResource getGColorUiResource(String id) {
GColorUIResource gColor = gColorMap.get(id);
if (gColor == null) {
gColor = new GColorUIResource(id);
gColorMap.put(id, gColor);
}
return gColor;
}
@Override
public GIconUIResource getGIconUiResource(String id) {
GIconUIResource gIcon = gIconMap.get(id);
if (gIcon == null) {
gIcon = new GIconUIResource(id);
gIconMap.put(id, gIcon);
}
return gIcon;
}
@Override
public GThemeValueMap getJavaDefaults() {
GThemeValueMap map = new GThemeValueMap();
map.load(javaDefaults);
return map;
}
@Override
public GThemeValueMap getApplicationDarkDefaults() {
GThemeValueMap map = new GThemeValueMap(applicationDefaults);
map.load(applicationDarkDefaults);
return map;
}
@Override
public GThemeValueMap getApplicationLightDefaults() {
GThemeValueMap map = new GThemeValueMap(applicationDefaults);
return map;
}
/**
* Returns a {@link GThemeValueMap} containing all default values for the current theme. It
* is a combination of application defined defaults and java {@link LookAndFeel} defaults.
* @return the current set of defaults.
*/
public GThemeValueMap getDefaults() {
GThemeValueMap currentDefaults = new GThemeValueMap(javaDefaults);
currentDefaults.load(systemValues);
currentDefaults.load(applicationDefaults);
if (activeTheme.useDarkDefaults()) {
currentDefaults.load(applicationDarkDefaults);
}
return currentDefaults;
}
/**
* Sets specially defined system UI values. These values are created by the application as a
* convenience for mapping generic concepts to values that differ by Look and Feel. This allows
* clients to use 'system' properties without knowing the actual Look and Feel terms.
*
* <p>For example, 'system.color.border' defaults to 'controlShadow', but maps to 'nimbusBorder'
* in the Nimbus Look and Feel.
*
* @param map the map
*/
public void setSystemDefaults(GThemeValueMap map) {
systemValues = map;
}
/**
* Sets the map of Java default UI values. These are the UI values defined by the current Java
* Look and Feel.
* @param map the default theme values defined by the {@link LookAndFeel}
*/
public void setJavaDefaults(GThemeValueMap map) {
javaDefaults = map;
buildCurrentValues();
GColor.refreshAll(currentValues);
GIcon.refreshAll(currentValues);
}
@Override
public boolean isUsingAquaUI(ComponentUI UI) {
return activeTheme.getLookAndFeelType() == LafType.MAC;
}
@Override
public boolean isUsingNimbusUI() {
return activeTheme.getLookAndFeelType() == LafType.NIMBUS;
}
@Override
public boolean hasThemeChanges() {
return !changedValuesMap.isEmpty();
}
@Override
public void registerFont(Component component, String fontId) {
lookAndFeelManager.registerFont(component, fontId);
}
public boolean isDarkTheme() {
return activeTheme.useDarkDefaults();
}
private void installFlatLookAndFeels() {
UIManager.installLookAndFeel(LafType.FLAT_LIGHT.getName(), FlatLightLaf.class.getName());
UIManager.installLookAndFeel(LafType.FLAT_DARK.getName(), FlatDarkLaf.class.getName());
}
private void loadThemeDefaults() {
themeFileLoader.loadThemeDefaultFiles();
applicationDefaults = themeFileLoader.getDefaults();
applicationDarkDefaults = themeFileLoader.getDarkDefaults();
}
private void buildCurrentValues() {
GThemeValueMap map = new GThemeValueMap();
map.load(javaDefaults);
map.load(systemValues);
map.load(applicationDefaults);
if (activeTheme.useDarkDefaults()) {
map.load(applicationDarkDefaults);
}
map.load(activeTheme);
currentValues = map;
changedValuesMap.clear();
}
private void loadThemes() {
if (allThemes == null) {
Set<GTheme> set = new HashSet<>();
set.addAll(findDiscoverableThemes());
set.addAll(themeFileLoader.loadThemeFiles());
allThemes = set;
}
}
private Collection<DiscoverableGTheme> findDiscoverableThemes() {
return ClassSearcher.getInstances(DiscoverableGTheme.class);
}
private 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 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 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);
}
}
private Set<String> findChangedJavaFontIds(String id) {
Set<String> affectedIds = new HashSet<>();
List<FontValue> fonts = javaDefaults.getFonts();
for (FontValue fontValue : fonts) {
String fontId = fontValue.getId();
FontValue currentFontValue = currentValues.getFont(fontId);
if (fontId.equals(id) || currentFontValue.inheritsFrom(id, currentValues)) {
affectedIds.add(fontId);
}
}
return affectedIds;
}
private Set<String> findChangedJavaIconIds(String id) {
Set<String> affectedIds = new HashSet<>();
List<IconValue> icons = javaDefaults.getIcons();
for (IconValue iconValue : icons) {
String iconId = iconValue.getId();
if (iconId.equals(id) || iconValue.inheritsFrom(id, currentValues)) {
affectedIds.add(iconId);
}
}
return affectedIds;
}
public void refreshGThemeValues() {
GColor.refreshAll(currentValues);
GIcon.refreshAll(currentValues);
}
}

View File

@ -150,8 +150,8 @@ public class ColorValue extends ThemeValue<Color> {
}
@Override
public void installValue() {
Gui.setColor(this);
public void installValue(ThemeManager themeManager) {
themeManager.setColor(this);
}
}

View File

@ -232,8 +232,8 @@ public class FontValue extends ThemeValue<Font> {
}
@Override
public void installValue() {
Gui.setFont(this);
public void installValue(ThemeManager themeManager) {
themeManager.setFont(this);
}
}

View File

@ -50,20 +50,9 @@ public class GColor extends Color {
* @param id the id used to lookup the current value for this color
*/
public GColor(String id) {
this(id, true);
}
/**
* Construct a GColor with an id that will be used to look up the current color associated with
* that id, which can be changed at runtime.
* @param id the id used to lookup the current value for this color
* @param validate if true, an error will be generated if the id can't be resolved to a color
* at this time
*/
public GColor(String id, boolean validate) {
super(0x808080);
this.id = id;
delegate = Gui.getColor(id, validate);
delegate = Gui.getColor(id);
inUseColors.add(this);
}
@ -230,9 +219,11 @@ public class GColor extends Color {
/**
* Reloads the delegate.
* @param currentValues the map of current theme values
*/
public void refresh() {
Color color = Gui.getColor(id, false);
public void refresh(GThemeValueMap currentValues) {
ColorValue value = currentValues.getColor(id);
Color color = value == null ? null : value.get(currentValues);
if (color != null) {
if (alpha != null) {
delegate = new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha);
@ -246,10 +237,11 @@ public class GColor extends Color {
/**
* Static method for notifying all the existing GColors that colors have changed and they
* should reload their cached indirect color.
* @param currentValues the map of current theme values
*/
public static void refreshAll() {
public static void refreshAll(GThemeValueMap currentValues) {
for (GColor gcolor : inUseColors.getValues()) {
gcolor.refresh();
gcolor.refresh(currentValues);
}
}

View File

@ -46,10 +46,11 @@ public class GIcon implements Icon {
/**
* Static method for notifying all the existing GIcon that icons have changed and they
* should reload their cached indirect icon.
* @param currentValues the map of all current theme values
*/
public static void refreshAll() {
public static void refreshAll(GThemeValueMap currentValues) {
for (GIcon gIcon : inUseIcons.getValues()) {
gIcon.refresh();
gIcon.refresh(currentValues);
}
}
@ -59,19 +60,8 @@ public class GIcon implements Icon {
* @param id the id used to lookup the current value for this color
*/
public GIcon(String id) {
this(id, true);
}
/**
* Construct a GIcon with an id that will be used to look up the current icon associated with
* that id, which can be changed at runtime.
* @param id the id used to lookup the current value for this icon
* @param validate if true, an error will be generated if the id can't be resolved to a icon
* at this time
*/
public GIcon(String id, boolean validate) {
this.id = id;
delegate = Gui.getIcon(id, validate);
delegate = Gui.getIcon(id);
inUseIcons.add(this);
}
@ -134,14 +124,25 @@ public class GIcon implements Icon {
/**
* Reloads the delegate.
* @param currentValues the map of current theme values
*/
public void refresh() {
Icon icon = Gui.getIcon(id, false);
public void refresh(GThemeValueMap currentValues) {
IconValue value = currentValues.getIcon(id);
Icon icon = value == null ? null : value.get(currentValues);
if (icon != null) {
delegate = icon;
}
}
/**
* Returns the current delegate for this GIcon. Note that this delegate can change when the
* theme changes or is edited.
* @return the current delegate icon for this GIcon.
*/
public Icon getDelegate() {
return delegate;
}
@Override
public int hashCode() {
return id.hashCode();

View File

@ -217,43 +217,6 @@ public class GTheme extends GThemeValueMap {
writer.writeThemeToFile(file);
}
/**
* Saves this theme to a new theme file.
* @param outputFile the file to save to
* @param includeDefaults if true, write all values to the theme file including default values.
* Otherwise, just values that are not the default values are written to the file.
* @return a new FileGTheme that represents the new file/theme
* @throws IOException if an I/O error occurs writing the theme file
*/
public GTheme saveToFile(File outputFile, boolean includeDefaults) throws IOException {
GTheme fileTheme = new GTheme(outputFile, name, lookAndFeel, useDarkDefaults);
if (includeDefaults) {
fileTheme.load(Gui.getDefaults());
}
fileTheme.load(this);
fileTheme.save();
return fileTheme;
}
/**
* Saves this theme to a new theme file.
* @param outputFile the file to save to
* @param includeDefaults if true, write all values to the theme file including default values.
* Otherwise, just values that are not the default values are written to the file.
* @throws IOException if an I/O error occurs writing the theme file
*/
public void saveToZip(File outputFile, boolean includeDefaults) throws IOException {
GTheme theme = new GTheme(name, lookAndFeel, useDarkDefaults);
if (includeDefaults) {
theme.load(Gui.getDefaults());
}
theme.load(this);
ThemeWriter writer = new ThemeWriter(theme);
writer.writeThemeToZipFile(outputFile);
}
/**
* Reads a theme from a file. The file can be either a theme file or a zip file containing
* a theme file and optionally a set of icon files.

View File

@ -16,26 +16,9 @@
package generic.theme;
import java.awt.*;
import java.io.File;
import java.util.*;
import java.util.List;
import javax.swing.*;
import javax.swing.plaf.ComponentUI;
import com.formdev.flatlaf.FlatDarkLaf;
import com.formdev.flatlaf.FlatLightLaf;
import generic.theme.builtin.*;
import generic.theme.laf.LookAndFeelManager;
import ghidra.framework.OperatingSystem;
import ghidra.framework.Platform;
import ghidra.util.Msg;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.datastruct.WeakDataStructureFactory;
import ghidra.util.datastruct.WeakSet;
import resources.ResourceManager;
import utilities.util.reflection.ReflectionUtilities;
import javax.swing.Icon;
import javax.swing.LookAndFeel;
/**
* Provides a static set of methods for globally managing application themes and their values.
@ -53,267 +36,15 @@ import utilities.util.reflection.ReflectionUtilities;
*
*/
public class Gui {
public static final String BACKGROUND_KEY = "color.bg.text";
private static GTheme activeTheme = getDefaultTheme();
private static Set<GTheme> allThemes = null;
private static GThemeValueMap applicationDefaults = new GThemeValueMap();
private static GThemeValueMap applicationDarkDefaults = new GThemeValueMap();
private static GThemeValueMap javaDefaults = new GThemeValueMap();
private static GThemeValueMap currentValues = new GThemeValueMap();
private static GThemeValueMap systemValues = new GThemeValueMap();
private static ThemeFileLoader themeFileLoader = new ThemeFileLoader();
private static ThemePreferenceManager themePreferenceManager = new ThemePreferenceManager();
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.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;
static Font DEFAULT_FONT = new Font("Dialog", Font.PLAIN, 12);
// Start with an StubThemeManager so that simple tests can operate without having
// to initialize the theme system. Applications and integration tests will
// called ThemeManager.initialize() which will replace this with a fully initialized version.
private static ThemeManager themeManager = new StubThemeManager();
private Gui() {
// static utils class, can't construct
}
/**
* Initialized the Theme and its values for the application.
*/
public static void initialize() {
isInitialized = true;
installFlatLookAndFeels();
loadThemeDefaults();
setTheme(themePreferenceManager.getTheme());
}
/**
* Reloads the defaults from all the discoverable theme.property files.
*/
public static void reloadApplicationDefaults() {
loadThemeDefaults();
buildCurrentValues();
lookAndFeelManager.resetAll(javaDefaults);
notifyThemeChanged(new AllValuesChangedThemeEvent(false));
}
/**
* Restores all the current application back to the values as specified by the active theme.
* In other words, reverts any changes to the active theme that haven't been saved.
*/
public static void restoreThemeValues() {
buildCurrentValues();
lookAndFeelManager.resetAll(javaDefaults);
notifyThemeChanged(new AllValuesChangedThemeEvent(false));
}
/**
* Restores the current color value for the given color id to the value established by the
* current theme.
* @param id the color id to restore back to the original theme value
*/
public static void restoreColor(String id) {
if (changedValuesMap.containsColor(id)) {
Gui.setColor(changedValuesMap.getColor(id));
}
}
/**
* Restores the current font value for the given font id to the value established by the
* current theme.
* @param id the font id to restore back to the original theme value
*/
public static void restoreFont(String id) {
if (changedValuesMap.containsFont(id)) {
Gui.setFont(changedValuesMap.getFont(id));
}
}
/**
* Restores the current icon value for the given icon id to the value established by the
* current theme.
* @param id the icon id to restore back to the original theme value
*/
public static void restoreIcon(String id) {
if (changedValuesMap.containsIcon(id)) {
Gui.setIcon(changedValuesMap.getIcon(id));
}
}
/**
* Returns true if the color associated with the given id has been changed from the current
* theme value for that id.
* @param id the color id to check if it has been changed
* @return true if the color associated with the given id has been changed from the current
* theme value for that id.
*/
public static boolean isChangedColor(String id) {
return changedValuesMap.containsColor(id);
}
/**
* Returns true if the font associated with the given id has been changed from the current
* theme value for that id.
* @param id the font id to check if it has been changed
* @return true if the font associated with the given id has been changed from the current
* theme value for that id.
*/
public static boolean isChangedFont(String id) {
return changedValuesMap.containsFont(id);
}
/**
* Returns true if the Icon associated with the given id has been changed from the current
* theme value for that id.
* @param id the Icon id to check if it has been changed
* @return true if the Icon associated with the given id has been changed from the current
* theme value for that id.
*/
public static boolean isChangedIcon(String id) {
return changedValuesMap.containsIcon(id);
}
/**
* Sets the application's active theme to the given theme.
* @param theme the theme to make active
*/
public static void setTheme(GTheme theme) {
if (theme.hasSupportedLookAndFeel()) {
activeTheme = theme;
LafType lookAndFeel = theme.getLookAndFeelType();
lookAndFeelManager = lookAndFeel.getLookAndFeelManager();
try {
lookAndFeelManager.installLookAndFeel();
themePreferenceManager.saveThemeToPreferences(theme);
notifyThemeChanged(new AllValuesChangedThemeEvent(true));
}
catch (Exception e) {
Msg.error(Gui.class,
"Error setting LookAndFeel: " + lookAndFeel.getName(), e);
}
}
currentValues.checkForUnresolvedReferences();
}
/**
* Adds the given theme to set of all themes.
* @param newTheme the theme to add
*/
public static void addTheme(GTheme newTheme) {
loadThemes();
allThemes.remove(newTheme);
allThemes.add(newTheme);
}
/**
* Removes the theme from the set of all themes. Also, if the theme has an associated
* file, the file will be deleted.
* @param theme the theme to delete
*/
public static void deleteTheme(GTheme theme) {
File file = theme.getFile();
if (file != null) {
file.delete();
}
if (allThemes != null) {
allThemes.remove(theme);
}
}
/**
* Returns a set of all known themes.
* @return a set of all known themes.
*/
public static Set<GTheme> getAllThemes() {
loadThemes();
return new HashSet<>(allThemes);
}
/**
* Returns a set of all known themes that are supported on the current platform.
* @return a set of all known themes that are supported on the current platform.
*/
public static Set<GTheme> getSupportedThemes() {
loadThemes();
Set<GTheme> supported = new HashSet<>();
for (GTheme theme : allThemes) {
if (theme.hasSupportedLookAndFeel()) {
supported.add(theme);
}
}
return supported;
}
/**
* Returns the active theme.
* @return the active theme.
*/
public static GTheme getActiveTheme() {
return activeTheme;
}
/**
* Returns the {@link LafType} for the currently active {@link LookAndFeel}
* @return the {@link LafType} for the currently active {@link LookAndFeel}
*/
public static LafType getLookAndFeelType() {
return activeTheme.getLookAndFeelType();
}
/**
* Returns the known theme that has the given name.
* @param themeName the name of the theme to retrieve
* @return the known theme that has the given name
*/
public static GTheme getTheme(String themeName) {
Optional<GTheme> first =
getAllThemes().stream().filter(t -> t.getName().equals(themeName)).findFirst();
return first.orElse(null);
}
/**
* Returns a {@link GThemeValueMap} of all current theme values.
* @return a {@link GThemeValueMap} of all current theme values.
*/
public static GThemeValueMap getAllValues() {
return new GThemeValueMap(currentValues);
}
/**
* Returns the theme values as defined by the current theme, ignoring any unsaved changes that
* are currently applied to the application.
* @return the theme values as defined by the current theme, ignoring any unsaved changes that
* are currently applied to the application.
*/
public static GThemeValueMap getThemeValues() {
GThemeValueMap map = new GThemeValueMap();
map.load(javaDefaults);
map.load(systemValues);
map.load(applicationDefaults);
if (activeTheme.useDarkDefaults()) {
map.load(applicationDarkDefaults);
}
map.load(activeTheme);
return map;
}
/**
* Returns a {@link GThemeValueMap} contains all values that differ from the default
* values (values defined by the {@link LookAndFeel} or in the theme.properties files.
* @return a {@link GThemeValueMap} contains all values that differ from the defaults.
*/
public static GThemeValueMap getNonDefaultValues() {
return currentValues.getChangedValues(getDefaults());
}
/**
* Returns the current {@link Font} associated with the given id. A default font will be
* returned if the font can't be resolved and an error message will be printed to the console.
@ -321,32 +52,7 @@ public class Gui {
* @return the current {@link Font} associated with the given id.
*/
public static Font getFont(String id) {
Font font = getFont(id, true);
if (font == FontValue.LAST_RESORT_DEFAULT) {
return null;
}
return font;
}
/**
* Returns the current {@link Font} associated with the given id.
* @param id the id for the desired font
* @param validate if true, will print an error message to the console if the id can't be
* resolved
* @return the current {@link Font} associated with the given id.
*/
public static Font getFont(String id, boolean validate) {
FontValue font = currentValues.getFont(id);
if (font == null) {
if (validate && isInitialized) {
Throwable t = getFilteredTrace();
Msg.error(Gui.class,
"No color value registered for: '" + id + "'", t);
}
return DEFAULT_FONT;
}
return font.get(currentValues);
return themeManager.getFont(id);
}
/**
@ -356,221 +62,7 @@ public class Gui {
* @return the {@link Color} registered for the given id.
*/
public static Color getColor(String id) {
return getColor(id, true);
}
/**
* Updates the current font for the given id.
* @param id the font id to update to the new color
* @param font the new font for the id
*/
public static void setFont(String id, Font font) {
setFont(new FontValue(id, font));
}
/**
* Updates the current value for the font id in the newValue
* @param newValue the new {@link FontValue} to install in the current values.
*/
public static void setFont(FontValue newValue) {
FontValue currentValue = currentValues.getFont(newValue.getId());
if (newValue.equals(currentValue)) {
return;
}
updateChangedValuesMap(currentValue, newValue);
currentValues.addFont(newValue);
notifyThemeChanged(new FontChangedThemeEvent(currentValues, newValue));
// update all java LookAndFeel fonts affected by this changed
String id = newValue.getId();
Set<String> changedFontIds = findChangedJavaFontIds(id);
lookAndFeelManager.fontsChanged(changedFontIds);
}
/**
* Updates the current color for the given id.
* @param id the color id to update to the new color
* @param color the new color for the id
*/
public static void setColor(String id, Color color) {
if (color == null) {
throw new IllegalArgumentException("Can't set theme value to null!");
}
if (color instanceof GColor gColor) {
if (id.equals(gColor.getId())) {
Msg.warn(Gui.class, "Attempted to set a color to a reference to itself!");
return; // this would create a circular reference to itself, don't do it
}
}
setColor(new ColorValue(id, color));
}
/**
* Updates the current value for the color id in the newValue
* @param newValue the new {@link ColorValue} to install in the current values.
*/
public static void setColor(ColorValue newValue) {
ColorValue currentValue = currentValues.getColor(newValue.getId());
if (newValue.equals(currentValue)) {
return;
}
updateChangedValuesMap(currentValue, newValue);
currentValues.addColor(newValue);
notifyThemeChanged(new ColorChangedThemeEvent(currentValues, newValue));
// now update the ui
if (lookAndFeelManager != null) {
lookAndFeelManager.colorsChanged();
}
}
/**
* Updates the current {@link Icon} for the given id.
* @param id the icon id to update to the new icon
* @param icon the new {@link Icon} for the id
*/
public static void setIcon(String id, Icon icon) {
setIcon(new IconValue(id, icon));
}
/**
* Updates the current value for the {@link Icon} id in the newValue
* @param newValue the new {@link IconValue} to install in the current values.
*/
public static void setIcon(IconValue newValue) {
IconValue currentValue = currentValues.getIcon(newValue.getId());
if (newValue.equals(currentValue)) {
return;
}
updateChangedValuesMap(currentValue, newValue);
currentValues.addIcon(newValue);
notifyThemeChanged(new IconChangedThemeEvent(currentValues, newValue));
// now update the ui
// update all java LookAndFeel icons affected by this changed
String id = newValue.getId();
Set<String> changedIconIds = findChangedJavaIconIds(id);
Icon newIcon = newValue.get(currentValues);
lookAndFeelManager.iconsChanged(changedIconIds, newIcon);
}
/**
* gets a UIResource version of the GColor for the given id. Using this method ensures that
* the same instance is used for a given id. This combats some poor code in some of the
* {@link LookAndFeel}s where the use == in some places to test for equals.
* @param id the id to get a GColorUIResource for
* @return a GColorUIResource for the given id
*/
public static GColorUIResource getGColorUiResource(String id) {
GColorUIResource gColor = gColorMap.get(id);
if (gColor == null) {
gColor = new GColorUIResource(id);
gColorMap.put(id, gColor);
}
return gColor;
}
/**
* gets a UIResource version of the GIcon for the given id. Using this method ensures that
* the same instance is used for a given id. This combats some poor code in some of the
* {@link LookAndFeel}s where the use == in some places to test for equals.
* @param id the id to get a {@link GIconUIResource} for
* @return a GIconUIResource for the given id
*/
public static GIconUIResource getGIconUiResource(String id) {
GIconUIResource gIcon = gIconMap.get(id);
if (gIcon == null) {
gIcon = new GIconUIResource(id);
gIconMap.put(id, gIcon);
}
return gIcon;
}
// used by
public static void setSystemDefaults(GThemeValueMap map) {
systemValues = map;
}
/**
* Sets the map of JavaDefaults defined by the current {@link LookAndFeel}.
* @param map the default theme values defined by the {@link LookAndFeel}
*/
public static void setJavaDefaults(GThemeValueMap map) {
javaDefaults = map;
buildCurrentValues();
GColor.refreshAll();
GIcon.refreshAll();
}
/**
* Returns the {@link GThemeValueMap} containing all the default theme values defined by the
* current {@link LookAndFeel}.
* @return the {@link GThemeValueMap} containing all the default theme values defined by the
* current {@link LookAndFeel}
*/
public static GThemeValueMap getJavaDefaults() {
GThemeValueMap map = new GThemeValueMap();
map.load(javaDefaults);
return map;
}
/**
* Returns the {@link GThemeValueMap} containing all the dark default values defined
* in theme.properties files. Note that dark defaults includes light defaults that haven't
* been overridden by a dark default with the same id.
* @return the {@link GThemeValueMap} containing all the dark values defined in
* theme.properties files
*/
public static GThemeValueMap getApplicationDarkDefaults() {
GThemeValueMap map = new GThemeValueMap(applicationDefaults);
map.load(applicationDarkDefaults);
return map;
}
/**
* Returns the {@link GThemeValueMap} containing all the standard default values defined
* in theme.properties files.
* @return the {@link GThemeValueMap} containing all the standard values defined in
* theme.properties files
*/
public static GThemeValueMap getApplicationLightDefaults() {
GThemeValueMap map = new GThemeValueMap(applicationDefaults);
return map;
}
/**
* Returns a {@link GThemeValueMap} containing all default values for the current theme. It
* is a combination of application defined defaults and java {@link LookAndFeel} defaults.
* @return the current set of defaults.
*/
public static GThemeValueMap getDefaults() {
GThemeValueMap currentDefaults = new GThemeValueMap(javaDefaults);
currentDefaults.load(systemValues);
currentDefaults.load(applicationDefaults);
if (activeTheme.useDarkDefaults()) {
currentDefaults.load(applicationDarkDefaults);
}
return currentDefaults;
}
/**
* Returns true if the given UI object is using the Aqua Look and Feel.
* @param UI the UI to examine.
* @return true if the UI is using Aqua
*/
public static boolean isUsingAquaUI(ComponentUI UI) {
return activeTheme.getLookAndFeelType() == LafType.MAC;
}
/**
* Returns true if 'Nimbus' is the current Look and Feel
* @return true if 'Nimbus' is the current Look and Feel
*/
public static boolean isUsingNimbusUI() {
return activeTheme.getLookAndFeelType() == LafType.NIMBUS;
return themeManager.getColor(id);
}
/**
@ -578,7 +70,7 @@ public class Gui {
* @param listener the listener to be notified
*/
public static void addThemeListener(ThemeListener listener) {
themeListeners.add(listener);
themeManager.addThemeListener(listener);
}
/**
@ -587,54 +79,7 @@ public class Gui {
* @param listener the listener to be removed
*/
public static void removeThemeListener(ThemeListener listener) {
themeListeners.add(listener);
}
/**
* Returns the default theme for the current platform.
* @return the default theme for the current platform.
*/
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();
}
}
/**
* Returns true if there are any unsaved changes to the current theme.
* @return true if there are any unsaved changes to the current theme.
*/
public static boolean hasThemeChanges() {
return !changedValuesMap.isEmpty();
}
/**
* Returns the color for the id. If there is no color registered for this id, then Color.CYAN
* is returned as the default color.
* @param id the id to get the direct color for
* @param validate if true, will output an error if the id can't be resolved at this time
* @return the actual direct color for the id, not a GColor
*/
public static Color getColor(String id, boolean validate) {
ColorValue color = currentValues.getColor(id);
if (color == null) {
if (validate && isInitialized) {
Throwable t = getFilteredTrace();
Msg.error(Gui.class,
"No color value registered for: '" + id + "'", t);
}
return Color.CYAN;
}
return color.get(currentValues);
themeManager.removeThemeListener(listener);
}
/**
@ -644,7 +89,7 @@ public class Gui {
* @return the actual icon registered for the given id
*/
public static Icon getIcon(String id) {
return getIcon(id, true);
return themeManager.getIcon(id);
}
/**
@ -653,7 +98,7 @@ public class Gui {
* @return true if an color for the given Id has been defined
*/
public static boolean hasColor(String id) {
return currentValues.containsColor(id);
return themeManager.hasColor(id);
}
/**
@ -662,7 +107,7 @@ public class Gui {
* @return true if an font for the given Id has been defined
*/
public static boolean hasFont(String id) {
return currentValues.containsFont(id);
return themeManager.hasFont(id);
}
/**
@ -671,27 +116,7 @@ public class Gui {
* @return true if an icon for the given Id has been defined
*/
public static boolean hasIcon(String id) {
return currentValues.containsIcon(id);
}
/**
* Returns the {@link Icon} registered for the given id. If no icon is registered, returns
* the default icon (bomb).
* @param id the id to get the register icon for
* @param validate if true, will output an error if the id can't be resolved at this time
* @return the Icon registered for the given id
*/
public static Icon getIcon(String id, boolean validate) {
IconValue icon = currentValues.getIcon(id);
if (icon == null) {
if (validate && isInitialized) {
Throwable t = getFilteredTrace();
Msg.error(Gui.class,
"No icon value registered for: '" + id + "'", t);
}
return ResourceManager.getDefaultIcon();
}
return icon.get(currentValues);
return themeManager.hasIcon(id);
}
/**
@ -700,7 +125,7 @@ public class Gui {
* @return a darker version of the given color or brighter if the current theme is dark
*/
public static Color darker(Color color) {
if (activeTheme.useDarkDefaults()) {
if (isDarkTheme()) {
return color.brighter();
}
return color.darker();
@ -712,7 +137,7 @@ public class Gui {
* @return a brighter version of the given color or darker if the current theme is dark
*/
public static Color brighter(Color color) {
if (activeTheme.useDarkDefaults()) {
if (isDarkTheme()) {
return color.darker();
}
return color.brighter();
@ -725,137 +150,19 @@ public class Gui {
* @param fontId the id of the font to register with the given component
*/
public static void registerFont(Component component, String fontId) {
lookAndFeelManager.registerFont(component, fontId);
themeManager.registerFont(component, fontId);
}
private static void installFlatLookAndFeels() {
UIManager.installLookAndFeel(LafType.FLAT_LIGHT.getName(), FlatLightLaf.class.getName());
UIManager.installLookAndFeel(LafType.FLAT_DARK.getName(), FlatDarkLaf.class.getName());
/**
* Returns true if the active theme is using dark defaults
* @return true if the active theme is using dark defaults
*/
public static boolean isDarkTheme() {
return themeManager.isDarkTheme();
}
private static void loadThemeDefaults() {
themeFileLoader.loadThemeDefaultFiles();
applicationDefaults = themeFileLoader.getDefaults();
applicationDarkDefaults = themeFileLoader.getDarkDefaults();
}
private static void notifyThemeChanged(ThemeEvent event) {
for (ThemeListener listener : themeListeners) {
listener.themeChanged(event);
}
}
private static Throwable getFilteredTrace() {
Throwable t = ReflectionUtilities.createThrowableWithStackOlderThan();
StackTraceElement[] trace = t.getStackTrace();
StackTraceElement[] filtered =
ReflectionUtilities.filterStackTrace(trace, "java.", "theme.Gui", "theme.GColor");
t.setStackTrace(filtered);
return t;
}
private static void buildCurrentValues() {
GThemeValueMap map = new GThemeValueMap();
map.load(javaDefaults);
map.load(systemValues);
map.load(applicationDefaults);
if (activeTheme.useDarkDefaults()) {
map.load(applicationDarkDefaults);
}
map.load(activeTheme);
currentValues = map;
changedValuesMap.clear();
}
private static void loadThemes() {
if (allThemes == null) {
Set<GTheme> set = new HashSet<>();
set.addAll(findDiscoverableThemes());
set.addAll(themeFileLoader.loadThemeFiles());
allThemes = set;
}
}
private static Collection<DiscoverableGTheme> findDiscoverableThemes() {
return ClassSearcher.getInstances(DiscoverableGTheme.class);
}
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);
}
}
private static Set<String> findChangedJavaFontIds(String id) {
Set<String> affectedIds = new HashSet<>();
List<FontValue> fonts = javaDefaults.getFonts();
for (FontValue fontValue : fonts) {
String fontId = fontValue.getId();
FontValue currentFontValue = currentValues.getFont(fontId);
if (fontId.equals(id) || currentFontValue.inheritsFrom(id, currentValues)) {
affectedIds.add(fontId);
}
}
return affectedIds;
}
private static Set<String> findChangedJavaIconIds(String id) {
Set<String> affectedIds = new HashSet<>();
List<IconValue> icons = javaDefaults.getIcons();
for (IconValue iconValue : icons) {
String iconId = iconValue.getId();
if (iconId.equals(id) || iconValue.inheritsFrom(id, currentValues)) {
affectedIds.add(iconId);
}
}
return affectedIds;
}
// for testing
public static void setPropertiesLoader(ThemeFileLoader loader) {
allThemes = null;
themeFileLoader = loader;
}
public static void setThemePreferenceManager(ThemePreferenceManager manager) {
themePreferenceManager = manager;
static void setThemeManager(ThemeManager manager) {
themeManager = manager;
}
}

View File

@ -247,8 +247,8 @@ public class IconValue extends ThemeValue<Icon> {
}
@Override
public void installValue() {
Gui.setIcon(this);
public void installValue(ThemeManager themeManager) {
themeManager.setIcon(this);
}
}

View File

@ -85,34 +85,36 @@ public enum LafType {
/**
* Returns a LookAndFeelManager that can install and update the {@link LookAndFeel} associated
* with this LafType.
* @param themeManager The application ThemeManager
* @return a LookAndFeelManager that can install and update the {@link LookAndFeel} associated
* with this LafType.
*/
public LookAndFeelManager getLookAndFeelManager() {
return getManager(this);
public LookAndFeelManager getLookAndFeelManager(ApplicationThemeManager themeManager) {
return createManager(this, themeManager);
}
private static LookAndFeelManager getManager(LafType lookAndFeel) {
switch (lookAndFeel) {
private static LookAndFeelManager createManager(LafType type,
ApplicationThemeManager themeManager) {
switch (type) {
case MAC:
return new MacLookAndFeelManager();
return new MacLookAndFeelManager(themeManager);
case METAL:
return new MetalLookAndFeelManager();
return new MetalLookAndFeelManager(themeManager);
case WINDOWS:
return new WindowsLookAndFeelManager();
return new WindowsLookAndFeelManager(themeManager);
case WINDOWS_CLASSIC:
return new WindowsClassicLookAndFeelManager();
return new WindowsClassicLookAndFeelManager(themeManager);
case GTK:
return new GtkLookAndFeelManager();
return new GtkLookAndFeelManager(themeManager);
case MOTIF:
return new MotifLookAndFeelManager();
return new MotifLookAndFeelManager(themeManager);
case NIMBUS:
return new NimbusLookAndFeelManager();
return new NimbusLookAndFeelManager(themeManager);
case FLAT_DARK:
case FLAT_LIGHT:
return new FlatLookAndFeelManager(lookAndFeel);
return new FlatLookAndFeelManager(type, themeManager);
default:
throw new AssertException("No lookAndFeelManager defined for " + lookAndFeel);
throw new AssertException("No lookAndFeelManager defined for " + type);
}
}

View File

@ -0,0 +1,227 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package generic.theme;
import static ghidra.util.WebColors.*;
import java.awt.Color;
import java.awt.Component;
import java.util.Set;
import javax.swing.plaf.ComponentUI;
/**
* Version of ThemeManager that is used before an application or test installs a full
* ApplicationThemeManager. Provides enough basic functionality used by the Gui class to
* allow simple unit tests to run.
*/
public class StubThemeManager extends ThemeManager {
public StubThemeManager() {
installPaletteColors();
}
// palette colors are used statically throughout the application, so having them have values
// in the stub will allow unit tests to run withouth initializing theming
protected void installPaletteColors() {
addPalette("nocolor", BLACK);
addPalette("black", BLACK);
addPalette("blue", BLUE);
addPalette("cyan", CYAN);
addPalette("darkgray", DARK_GRAY);
addPalette("gold", GOLD);
addPalette("gray", GRAY);
addPalette("green", GREEN);
addPalette("lavender", LAVENDER);
addPalette("lightgray", LIGHT_GRAY);
addPalette("lime", LIME);
addPalette("magenta", MAGENTA);
addPalette("maroon", MAROON);
addPalette("orange", ORANGE);
addPalette("pink", PINK);
addPalette("purple", PURPLE);
addPalette("red", RED);
addPalette("silver", SILVER);
addPalette("white", WHITE);
addPalette("yellow", YELLOW);
}
@Override
public void reloadApplicationDefaults() {
throw new UnsupportedOperationException();
}
@Override
public void restoreThemeValues() {
throw new UnsupportedOperationException();
}
@Override
public void restoreColor(String id) {
throw new UnsupportedOperationException();
}
@Override
public void restoreFont(String id) {
throw new UnsupportedOperationException();
}
@Override
public void restoreIcon(String id) {
throw new UnsupportedOperationException();
}
@Override
public boolean isChangedColor(String id) {
return false;
}
@Override
public boolean isChangedFont(String id) {
return false;
}
@Override
public boolean isChangedIcon(String id) {
return false;
}
@Override
public void setTheme(GTheme theme) {
throw new UnsupportedOperationException();
}
@Override
public void addTheme(GTheme newTheme) {
throw new UnsupportedOperationException();
}
@Override
public void deleteTheme(GTheme theme) {
throw new UnsupportedOperationException();
}
@Override
public Set<GTheme> getAllThemes() {
throw new UnsupportedOperationException();
}
@Override
public Set<GTheme> getSupportedThemes() {
throw new UnsupportedOperationException();
}
@Override
public GTheme getActiveTheme() {
throw new UnsupportedOperationException();
}
@Override
public LafType getLookAndFeelType() {
throw new UnsupportedOperationException();
}
@Override
public GTheme getTheme(String themeName) {
throw new UnsupportedOperationException();
}
@Override
public GThemeValueMap getThemeValues() {
throw new UnsupportedOperationException();
}
@Override
public void setFont(FontValue newValue) {
currentValues.addFont(newValue);
}
@Override
public void setColor(ColorValue newValue) {
currentValues.addColor(newValue);
}
@Override
public void setIcon(IconValue newValue) {
currentValues.addIcon(newValue);
}
@Override
public GColorUIResource getGColorUiResource(String id) {
throw new UnsupportedOperationException();
}
@Override
public GIconUIResource getGIconUiResource(String id) {
throw new UnsupportedOperationException();
}
@Override
public GThemeValueMap getJavaDefaults() {
throw new UnsupportedOperationException();
}
@Override
public GThemeValueMap getApplicationDarkDefaults() {
throw new UnsupportedOperationException();
}
@Override
public GThemeValueMap getApplicationLightDefaults() {
throw new UnsupportedOperationException();
}
@Override
public GThemeValueMap getDefaults() {
throw new UnsupportedOperationException();
}
@Override
public boolean isUsingAquaUI(ComponentUI UI) {
return false;
}
@Override
public boolean isUsingNimbusUI() {
return false;
}
@Override
public boolean hasThemeChanges() {
return false;
}
@Override
public void registerFont(Component component, String fontId) {
// do nothing
}
@Override
public boolean isDarkTheme() {
return false;
}
@Override
protected void error(String message) {
// don't report errors in stub for test purposes
}
private void addPalette(String paletteId, Color color) {
setColor(new ColorValue("color.palette." + paletteId, color));
}
}

View File

@ -0,0 +1,483 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package generic.theme;
import java.awt.*;
import java.util.Set;
import javax.swing.Icon;
import javax.swing.LookAndFeel;
import javax.swing.plaf.ComponentUI;
import generic.theme.builtin.*;
import ghidra.framework.OperatingSystem;
import ghidra.framework.Platform;
import ghidra.util.Msg;
import ghidra.util.datastruct.WeakDataStructureFactory;
import ghidra.util.datastruct.WeakSet;
import resources.ResourceManager;
import utilities.util.reflection.ReflectionUtilities;
/**
* This class manages application themes and their values. The ThemeManager is an abstract
* base class that has two concrete subclasses (and others for testing purposes) -
* StubThemeManager and ApplicationThememManager. The StubThemeManager exists as a placeholder
* until the ApplicationThemeManager is installed via {@link ApplicationThemeManager#initialize()}.
* <P>
* The basic idea is that all the colors, fonts, and icons used in an application should be
* accessed indirectly via an "id" string. Then the actual color, font, or icon can be changed
* without changing the source code. The default mapping of the id strings to a value is defined
* in <name>.theme.properties files which are dynamically discovered by searching the module's
* data directory. Also, these files can optionally define a dark default value for an id which
* would replace the standard default value in the event that the current theme specifies that it
* is a dark theme. Themes are used to specify the application's {@link LookAndFeel}, whether or
* not it is dark, and any customized values for colors, fonts, or icons. There are several
* "built-in" themes, one for each supported {@link LookAndFeel}, but additional themes can
* be defined and stored in the users application home directory as a <name>.theme file.
* <P>
* Clients that just need to access the colors, fonts, and icons from the theme can use the
* convenience methods in the {@link Gui} class. Clients that need to directly manipulate the
* themes and values will need to directly use the ThemeManager which and be retrieved using the
* static {@link #getInstance()} method.
*/
public abstract class ThemeManager {
static final Font DEFAULT_FONT = new Font("Dialog", Font.PLAIN, 12);
static final Color DEFAULT_COLOR = Color.CYAN;
protected static ThemeManager INSTANCE;
protected GThemeValueMap currentValues = new GThemeValueMap();
// these notifications are only when the user is manipulating theme values, so rare and at
// user speed, so using copy on read
private WeakSet<ThemeListener> themeListeners =
WeakDataStructureFactory.createCopyOnReadWeakSet();
public static ThemeManager getInstance() {
return INSTANCE;
}
public ThemeManager() {
if (INSTANCE == null) {
// default behavior is only install to INSTANCE if first time
INSTANCE = this;
}
}
protected void installInGui() {
Gui.setThemeManager(this);
}
/**
* Reloads the defaults from all the discoverable theme.property files.
*/
public abstract void reloadApplicationDefaults();
/**
* Restores all the current application back to the values as specified by the active theme.
* In other words, reverts any changes to the active theme that haven't been saved.
*/
public abstract void restoreThemeValues();
/**
* Restores the current color value for the given color id to the value established by the
* current theme.
* @param id the color id to restore back to the original theme value
*/
public abstract void restoreColor(String id);
/**
* Restores the current font value for the given font id to the value established by the
* current theme.
* @param id the font id to restore back to the original theme value
*/
public abstract void restoreFont(String id);
/**
* Restores the current icon value for the given icon id to the value established by the
* current theme.
* @param id the icon id to restore back to the original theme value
*/
public abstract void restoreIcon(String id);
/**
* Returns true if the color associated with the given id has been changed from the current
* theme value for that id.
* @param id the color id to check if it has been changed
* @return true if the color associated with the given id has been changed from the current
* theme value for that id.
*/
public abstract boolean isChangedColor(String id);
/**
* Returns true if the font associated with the given id has been changed from the current
* theme value for that id.
* @param id the font id to check if it has been changed
* @return true if the font associated with the given id has been changed from the current
* theme value for that id.
*/
public abstract boolean isChangedFont(String id);
/**
* Returns true if the Icon associated with the given id has been changed from the current
* theme value for that id.
* @param id the Icon id to check if it has been changed
* @return true if the Icon associated with the given id has been changed from the current
* theme value for that id.
*/
public abstract boolean isChangedIcon(String id);
/**
* Sets the application's active theme to the given theme.
* @param theme the theme to make active
*/
public abstract void setTheme(GTheme theme);
/**
* Adds the given theme to set of all themes.
* @param newTheme the theme to add
*/
public abstract void addTheme(GTheme newTheme);
/**
* Removes the theme from the set of all themes. Also, if the theme has an associated
* file, the file will be deleted.
* @param theme the theme to delete
*/
public abstract void deleteTheme(GTheme theme);
/**
* Returns a set of all known themes.
* @return a set of all known themes.
*/
public abstract Set<GTheme> getAllThemes();
/**
* Returns a set of all known themes that are supported on the current platform.
* @return a set of all known themes that are supported on the current platform.
*/
public abstract Set<GTheme> getSupportedThemes();
/**
* Returns the active theme.
* @return the active theme.
*/
public abstract GTheme getActiveTheme();
/**
* Returns the {@link LafType} for the currently active {@link LookAndFeel}
* @return the {@link LafType} for the currently active {@link LookAndFeel}
*/
public abstract LafType getLookAndFeelType();
/**
* Returns the known theme that has the given name.
* @param themeName the name of the theme to retrieve
* @return the known theme that has the given name
*/
public abstract GTheme getTheme(String themeName);
/**
* Returns a {@link GThemeValueMap} of all current theme values including unsaved changes to the
* theme.
* @return a {@link GThemeValueMap} of all current theme values
*/
public GThemeValueMap getCurrentValues() {
return new GThemeValueMap(currentValues);
}
/**
* Returns the theme values as defined by the current theme, ignoring any unsaved changes that
* are currently applied to the application.
* @return the theme values as defined by the current theme, ignoring any unsaved changes that
* are currently applied to the application
*/
public abstract GThemeValueMap getThemeValues();
/**
* Returns a {@link GThemeValueMap} contains all values that differ from the default
* values (values defined by the {@link LookAndFeel} or in the theme.properties files.
* @return a {@link GThemeValueMap} contains all values that differ from the defaults.
*/
public GThemeValueMap getNonDefaultValues() {
return currentValues.getChangedValues(getDefaults());
}
/**
* Returns the {@link Color} registered for the given id. Will output an error message if
* the id can't be resolved.
* @param id the id to get the direct color for
* @return the {@link Color} registered for the given id.
*/
public Color getColor(String id) {
ColorValue color = currentValues.getColor(id);
if (color == null) {
error("No color value registered for: '" + id + "'");
return DEFAULT_COLOR;
}
return color.get(currentValues);
}
/**
* Returns the current {@link Font} associated with the given id. A default font will be
* returned if the font can't be resolved and an error message will be printed to the console.
* @param id the id for the desired font
* @return the current {@link Font} associated with the given id.
*/
public Font getFont(String id) {
FontValue font = currentValues.getFont(id);
if (font == null) {
error("No color value registered for: '" + id + "'");
return DEFAULT_FONT;
}
return font.get(currentValues);
}
/**
* Returns the Icon registered for the given id. If no icon is registered for the id,
* the default icon will be returned and an error message will be dumped to the console
* @param id the id to get the registered icon for
* @return the actual icon registered for the given id
*/
public Icon getIcon(String id) {
IconValue icon = currentValues.getIcon(id);
if (icon == null) {
error("No icon value registered for: '" + id + "'");
return ResourceManager.getDefaultIcon();
}
return icon.get(currentValues);
}
/**
* Updates the current font for the given id.
* @param id the font id to update to the new color
* @param font the new font for the id
*/
public void setFont(String id, Font font) {
setFont(new FontValue(id, font));
}
/**
* Updates the current value for the font id in the newValue
* @param newValue the new {@link FontValue} to install in the current values.
*/
public abstract void setFont(FontValue newValue);
/**
* Updates the current color for the given id.
* @param id the color id to update to the new color
* @param color the new color for the id
*/
public void setColor(String id, Color color) {
if (color == null) {
throw new IllegalArgumentException("Can't set theme value to null!");
}
if (color instanceof GColor gColor) {
if (id.equals(gColor.getId())) {
Throwable t = new Throwable();
Msg.error(this, "Attempted to set a color for id \"" + id + "\" using a GColor" +
" defined using that same id! This would create a self reference!", t);
return; // this would create a circular reference to itself, don't do it
}
}
setColor(new ColorValue(id, color));
}
/**
* Updates the current value for the color id in the newValue
* @param newValue the new {@link ColorValue} to install in the current values.
*/
public abstract void setColor(ColorValue newValue);
/**
* Updates the current {@link Icon} for the given id.
* @param id the icon id to update to the new icon
* @param icon the new {@link Icon} for the id
*/
public void setIcon(String id, Icon icon) {
setIcon(new IconValue(id, icon));
}
/**
* Updates the current value for the {@link Icon} id in the newValue
* @param newValue the new {@link IconValue} to install in the current values.
*/
public abstract void setIcon(IconValue newValue);
/**
* gets a UIResource version of the GColor for the given id. Using this method ensures that
* the same instance is used for a given id. This combats some poor code in some of the
* {@link LookAndFeel}s where the use == in some places to test for equals.
* @param id the id to get a GColorUIResource for
* @return a GColorUIResource for the given id
*/
public abstract GColorUIResource getGColorUiResource(String id);
/**
* gets a UIResource version of the GIcon for the given id. Using this method ensures that
* the same instance is used for a given id. This combats some poor code in some of the
* {@link LookAndFeel}s where the use == in some places to test for equals.
* @param id the id to get a {@link GIconUIResource} for
* @return a GIconUIResource for the given id
*/
public abstract GIconUIResource getGIconUiResource(String id);
/**
* Returns the {@link GThemeValueMap} containing all the default theme values defined by the
* current {@link LookAndFeel}.
* @return the {@link GThemeValueMap} containing all the default theme values defined by the
* current {@link LookAndFeel}
*/
public abstract GThemeValueMap getJavaDefaults();
/**
* Returns the {@link GThemeValueMap} containing all the dark default values defined
* in theme.properties files. Note that dark defaults includes light defaults that haven't
* been overridden by a dark default with the same id.
* @return the {@link GThemeValueMap} containing all the dark values defined in
* theme.properties files
*/
public abstract GThemeValueMap getApplicationDarkDefaults();
/**
* Returns the {@link GThemeValueMap} containing all the standard default values defined
* in theme.properties files.
* @return the {@link GThemeValueMap} containing all the standard values defined in
* theme.properties files
*/
public abstract GThemeValueMap getApplicationLightDefaults();
/**
* Returns a {@link GThemeValueMap} containing all default values for the current theme. It
* is a combination of application defined defaults and java {@link LookAndFeel} defaults.
* @return the current set of defaults.
*/
public abstract GThemeValueMap getDefaults();
/**
* Returns true if the given UI object is using the Aqua Look and Feel.
* @param UI the UI to examine.
* @return true if the UI is using Aqua
*/
public abstract boolean isUsingAquaUI(ComponentUI UI);
/**
* Returns true if 'Nimbus' is the current Look and Feel
* @return true if 'Nimbus' is the current Look and Feel
*/
public abstract boolean isUsingNimbusUI();
/**
* Adds a {@link ThemeListener} to be notified of theme changes.
* @param listener the listener to be notified
*/
public void addThemeListener(ThemeListener listener) {
themeListeners.add(listener);
}
/**
* Removes the given {@link ThemeListener} from the list of listeners to be notified of
* theme changes.
* @param listener the listener to be removed
*/
public void removeThemeListener(ThemeListener listener) {
themeListeners.remove(listener);
}
/**
* Returns the default theme for the current platform.
* @return the default theme for the current platform.
*/
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();
}
}
/**
* Returns true if there are any unsaved changes to the current theme.
* @return true if there are any unsaved changes to the current theme.
*/
public abstract boolean hasThemeChanges();
/**
* Returns true if an color for the given Id has been defined
* @param id the id to check for an existing color.
* @return true if an color for the given Id has been defined
*/
public boolean hasColor(String id) {
return currentValues.containsColor(id);
}
/**
* Returns true if an font for the given Id has been defined
* @param id the id to check for an existing font.
* @return true if an font for the given Id has been defined
*/
public boolean hasFont(String id) {
return currentValues.containsFont(id);
}
/**
* Returns true if an icon for the given Id has been defined
* @param id the id to check for an existing icon.
* @return true if an icon for the given Id has been defined
*/
public boolean hasIcon(String id) {
return currentValues.containsIcon(id);
}
/**
* Binds the component to the font identified by the given font id. Whenever the font for
* the font id changes, the component will updated with the new font.
* @param component the component to set/update the font
* @param fontId the id of the font to register with the given component
*/
public abstract void registerFont(Component component, String fontId);
/**
* Returns true if the current theme use dark default values.
* @return true if the current theme use dark default values.
*/
public abstract boolean isDarkTheme();
protected void notifyThemeChanged(ThemeEvent event) {
for (ThemeListener listener : themeListeners) {
listener.themeChanged(event);
}
}
protected void error(String message) {
Throwable t = ReflectionUtilities.createThrowableWithStackOlderThan();
StackTraceElement[] trace = t.getStackTrace();
StackTraceElement[] filtered =
ReflectionUtilities.filterStackTrace(trace, "java.", "theme.Gui", "theme.ThemeManager",
"theme.GColor");
t.setStackTrace(filtered);
Msg.error(this, message, t);
}
}

View File

@ -24,7 +24,7 @@ import ghidra.util.Msg;
/**
* Reads and writes current theme info to preferences
*/
public class ThemePreferenceManager {
public class ThemePreferences {
private static final String THEME_PREFFERENCE_KEY = "Theme";
/**
@ -32,7 +32,7 @@ public class ThemePreferenceManager {
* @return the last theme used (stored in preferences) or the default theme if not stored
* in preferences
*/
public GTheme getTheme() {
public GTheme load() {
String themeId = Preferences.getProperty(THEME_PREFFERENCE_KEY, "Default", true);
if (themeId.startsWith(GTheme.FILE_PREFIX)) {
String filename = themeId.substring(GTheme.FILE_PREFIX.length());
@ -55,14 +55,14 @@ public class ThemePreferenceManager {
"Can't find or instantiate class: " + className, e);
}
}
return Gui.getDefaultTheme();
return ThemeManager.getDefaultTheme();
}
/**
* Saves the current theme choice to {@link Preferences}.
* @param theme the theme to remember in {@link Preferences}
*/
public void saveThemeToPreferences(GTheme theme) {
public void save(GTheme theme) {
Preferences.setProperty(THEME_PREFFERENCE_KEY, theme.getThemeLocater());
Preferences.store();
}

View File

@ -73,7 +73,10 @@ public abstract class ThemeValue<T> implements Comparable<ThemeValue<T>> {
/**
* Returns the T value for this instance, following references as needed. Uses the given
* preferredValues map to resolve references.
* preferredValues map to resolve references. If the value can't be resolved by following
* reference chains, an error stack trace will be generated and the default T value will
* be returned. In rare situations where it is acceptable for the value to not be resolvable,
* use the {@link #hasResolvableValue(GThemeValueMap)} method first.
* @param values the {@link GThemeValueMap} used to resolve references if this
* instance doesn't have an actual value.
* @return the T value for this instance, following references as needed.
@ -85,23 +88,55 @@ public abstract class ThemeValue<T> implements Comparable<ThemeValue<T>> {
Set<String> visitedKeys = new HashSet<>();
visitedKeys.add(id);
ThemeValue<T> parent = getReferredValue(values, referenceId);
ThemeValue<T> referred = getReferredValue(values, referenceId);
// loop resolving indirect references
while (parent != null) {
if (parent.value != null) {
return parent.value;
while (referred != null) {
if (referred.value != null) {
return referred.value;
}
visitedKeys.add(parent.id);
if (visitedKeys.contains(parent.referenceId)) {
visitedKeys.add(referred.id);
if (visitedKeys.contains(referred.referenceId)) {
Msg.warn(this, "Theme value reference loop detected for key: " + id);
return getUnresolvedReferenceValue(id, parent.referenceId);
return getUnresolvedReferenceValue(id, referred.referenceId);
}
parent = getReferredValue(values, parent.referenceId);
referred = getReferredValue(values, referred.referenceId);
}
return getUnresolvedReferenceValue(id, referenceId);
}
/**
* Returns true if the ThemeValue can resolve to the concrete T value (color, font, or icon)
* from the given set of theme values.
* @param values the set of values to use to try and follow reference chains to ultimately
* resolve the ThemeValue to a an actual T value
* @return true if the ThemeValue can resolve to the concrete T value (color, font, or icon)
* from the given set of theme values.
*/
public boolean hasResolvableValue(GThemeValueMap values) {
if (value != null) {
return true;
}
Set<String> visitedKeys = new HashSet<>();
visitedKeys.add(id);
ThemeValue<T> referred = getReferredValue(values, referenceId);
// loop resolving indirect references
while (referred != null) {
if (referred.value != null) {
return true;
}
visitedKeys.add(referred.id);
if (visitedKeys.contains(referred.referenceId)) {
Msg.warn(this, "Theme value reference loop detected for key: " + id);
return false;
}
referred = getReferredValue(values, referred.referenceId);
}
return false;
}
/**
* Returns true if this ThemeValue derives its value from the given ancestorId.
* @param ancestorId the id to test if this Theme value inherits from
@ -208,7 +243,8 @@ public abstract class ThemeValue<T> implements Comparable<ThemeValue<T>> {
/**
* Install this value as the current value for the application
* @param themeManager the application ThemeManager
*/
public abstract void installValue();
public abstract void installValue(ThemeManager themeManager);
}

View File

@ -17,13 +17,12 @@ package generic.theme.laf;
import javax.swing.UIManager;
import generic.theme.ColorValue;
import generic.theme.LafType;
import generic.theme.*;
public class FlatLookAndFeelManager extends LookAndFeelManager {
public FlatLookAndFeelManager(LafType laf) {
super(laf);
public FlatLookAndFeelManager(LafType laf, ApplicationThemeManager themeManager) {
super(laf, themeManager);
// establish system color to LookAndFeel colors
systemToLafMap.addColor(new ColorValue(SYSTEM_WIDGET_BACKGROUND_COLOR_ID, "text"));

View File

@ -19,18 +19,32 @@ import java.awt.Color;
import java.awt.Font;
import java.util.List;
import javax.swing.Icon;
import javax.swing.UIDefaults;
import javax.swing.*;
import javax.swing.plaf.FontUIResource;
import javax.swing.plaf.nimbus.NimbusLookAndFeel;
import generic.theme.*;
/**
* Extends the NimbusLookAndFeel to intercept the {@link #getDefaults()}. To get Nimbus
* to use our indirect values, we have to get in early.
* Extends the {@link NimbusLookAndFeel} to intercept the {@link #getDefaults()}. Nimbus does
* not honor changes to the UIDefaults after it is installed as the active
* {@link LookAndFeel}, so we have to make the changes at the time the UIDefaults are installed.
*
* To get around this issue, we extend the NimbusLookAndFeel
* so that we can install our GColors and overridden properties as Nimbus is being installed,
* specifically during the call to the getDefaults() method. For all other Look And Feels, the
* GColors and overridden properties are changed in the UIDefaults after the Look And Feel is
* installed, so they don't need to extends the Look and Feel class.
*
* Also, note that Nimbus needs to be reinstalled every time we need to make a change to any of the
* UIDefaults values, since it does not respond to changes other than when first installed.
*/
public class GNimbusLookAndFeel extends NimbusLookAndFeel {
private ApplicationThemeManager themeManager;
GNimbusLookAndFeel(ApplicationThemeManager themeManager) {
this.themeManager = themeManager;
}
@Override
public UIDefaults getDefaults() {
@ -40,13 +54,13 @@ public class GNimbusLookAndFeel extends NimbusLookAndFeel {
// replace all colors with GColors
for (ColorValue colorValue : javaDefaults.getColors()) {
String id = colorValue.getId();
defaults.put(id, Gui.getGColorUiResource(id));
defaults.put(id, themeManager.getGColorUiResource(id));
}
// put fonts back into defaults in case they have been changed by the current theme
for (FontValue fontValue : javaDefaults.getFonts()) {
String id = fontValue.getId();
Font font = Gui.getFont(id);
Font font = themeManager.getFont(id);
defaults.put(id, new FontUIResource(font));
}
@ -55,13 +69,12 @@ public class GNimbusLookAndFeel extends NimbusLookAndFeel {
String id = iconValue.getId();
// because some icons are weird, put raw icons into defaults, only use GIcons for
// setting Icons explicitly on components
Icon icon = Gui.getIcon(id, true);
Icon icon = themeManager.getIcon(id);
defaults.put(id, icon);
}
defaults.put("Label.textForeground", Gui.getGColorUiResource("Label.foreground"));
GColor.refreshAll();
GIcon.refreshAll();
defaults.put("Label.textForeground", themeManager.getGColorUiResource("Label.foreground"));
themeManager.refreshGThemeValues();
return defaults;
}
@ -90,7 +103,7 @@ public class GNimbusLookAndFeel extends NimbusLookAndFeel {
}
// need to set javaDefalts now to trigger building currentValues so the when
// we create GColors below, they can be resolved.
Gui.setJavaDefaults(javaDefaults);
themeManager.setJavaDefaults(javaDefaults);
return javaDefaults;
}
}

View File

@ -15,6 +15,7 @@
*/
package generic.theme.laf;
import generic.theme.ApplicationThemeManager;
import generic.theme.LafType;
/**
@ -22,7 +23,7 @@ import generic.theme.LafType;
*/
public class GtkLookAndFeelManager extends LookAndFeelManager {
public GtkLookAndFeelManager() {
super(LafType.GTK);
public GtkLookAndFeelManager(ApplicationThemeManager themeManager) {
super(LafType.GTK, themeManager);
}
}

View File

@ -51,9 +51,11 @@ public abstract class LookAndFeelManager {
private LafType laf;
private Map<String, ComponentFontRegistry> fontRegistryMap = new HashMap<>();
protected GThemeValueMap systemToLafMap = new GThemeValueMap();
protected ApplicationThemeManager themeManager;
protected LookAndFeelManager(LafType laf) {
protected LookAndFeelManager(LafType laf, ApplicationThemeManager themeManager) {
this.laf = laf;
this.themeManager = themeManager;
// establish system color to LookAndFeel colors
systemToLafMap.addColor(new ColorValue(SYSTEM_APP_BACKGROUND_COLOR_ID, "control"));
@ -84,7 +86,7 @@ public abstract class LookAndFeelManager {
IllegalAccessException, UnsupportedLookAndFeelException {
cleanUiDefaults();
Gui.setSystemDefaults(systemToLafMap);
themeManager.setSystemDefaults(systemToLafMap);
doInstallLookAndFeel();
installJavaDefaults();
fixupLookAndFeelIssues();
@ -98,8 +100,7 @@ public abstract class LookAndFeelManager {
* special as needed by the current {@link LookAndFeel}
*/
public void resetAll(GThemeValueMap javaDefaults) {
GColor.refreshAll();
GIcon.refreshAll();
themeManager.refreshGThemeValues();
resetIcons(javaDefaults);
resetFonts(javaDefaults);
updateAllRegisteredComponentFonts();
@ -130,7 +131,7 @@ public abstract class LookAndFeelManager {
UIDefaults defaults = UIManager.getDefaults();
for (IconValue iconValue : icons) {
String id = iconValue.getId();
Icon correctIcon = Gui.getIcon(id, false);
Icon correctIcon = Gui.getIcon(id);
Icon storedIcon = defaults.getIcon(id);
if (correctIcon != null && !correctIcon.equals(storedIcon)) {
defaults.put(id, correctIcon);
@ -142,7 +143,7 @@ public abstract class LookAndFeelManager {
* Called when one or more colors have changed.
*/
public void colorsChanged() {
GColor.refreshAll();
themeManager.refreshGThemeValues();
repaintAll();
}
@ -162,7 +163,7 @@ public abstract class LookAndFeelManager {
}
updateComponentUis();
}
GIcon.refreshAll();
themeManager.refreshGThemeValues();
repaintAll();
}
@ -265,7 +266,7 @@ public abstract class LookAndFeelManager {
GThemeValueMap javaDefaults = extractJavaDefaults();
ThemeGrouper grouper = getThemeGrouper();
grouper.group(javaDefaults);
Gui.setJavaDefaults(javaDefaults);
themeManager.setJavaDefaults(javaDefaults);
installPropertiesBackIntoUiDefaults(javaDefaults);
}
@ -276,7 +277,7 @@ public abstract class LookAndFeelManager {
private void installPropertiesBackIntoUiDefaults(GThemeValueMap javaDefaults) {
UIDefaults defaults = UIManager.getDefaults();
GTheme theme = Gui.getActiveTheme();
GTheme theme = themeManager.getActiveTheme();
// we replace java default colors with GColor equivalents so that we
// can change colors without having to reinstall ui on each component
@ -284,7 +285,7 @@ public abstract class LookAndFeelManager {
// allow being wrapped like colors do.
for (ColorValue colorValue : javaDefaults.getColors()) {
String id = colorValue.getId();
defaults.put(id, Gui.getGColorUiResource(id));
defaults.put(id, themeManager.getGColorUiResource(id));
}
// put fonts back into defaults in case they have been changed by the current theme
@ -304,7 +305,7 @@ public abstract class LookAndFeelManager {
if (themeValue != null) {
// because some icons are weird, put raw icons into defaults, only use GIcons for
// setting Icons explicitly on components
Icon icon = Gui.getIcon(id, true);
Icon icon = Gui.getIcon(id);
defaults.put(id, icon);
}
}
@ -463,7 +464,7 @@ public abstract class LookAndFeelManager {
}
private void cleanUiDefaults() {
GThemeValueMap javaDefaults = Gui.getJavaDefaults();
GThemeValueMap javaDefaults = themeManager.getJavaDefaults();
if (javaDefaults == null) {
return;
}

View File

@ -15,12 +15,13 @@
*/
package generic.theme.laf;
import generic.theme.ApplicationThemeManager;
import generic.theme.LafType;
public class MacLookAndFeelManager extends LookAndFeelManager {
public MacLookAndFeelManager() {
super(LafType.MAC);
public MacLookAndFeelManager(ApplicationThemeManager themeManager) {
super(LafType.MAC, themeManager);
}
@Override

View File

@ -15,11 +15,12 @@
*/
package generic.theme.laf;
import generic.theme.ApplicationThemeManager;
import generic.theme.LafType;
public class MetalLookAndFeelManager extends LookAndFeelManager {
public MetalLookAndFeelManager() {
super(LafType.METAL);
public MetalLookAndFeelManager(ApplicationThemeManager themeManager) {
super(LafType.METAL, themeManager);
}
}

View File

@ -15,16 +15,15 @@
*/
package generic.theme.laf;
import generic.theme.ColorValue;
import generic.theme.LafType;
import generic.theme.*;
/**
* Motif {@link LookAndFeelManager}. Specialized so that it can return the Motif installer
*/
public class MotifLookAndFeelManager extends LookAndFeelManager {
public MotifLookAndFeelManager() {
super(LafType.MOTIF);
public MotifLookAndFeelManager(ApplicationThemeManager themeManager) {
super(LafType.MOTIF, themeManager);
// establish system color to LookAndFeel colors
systemToLafMap.addColor(new ColorValue(SYSTEM_APP_BACKGROUND_COLOR_ID, "control"));
systemToLafMap.addColor(new ColorValue(SYSTEM_WIDGET_BACKGROUND_COLOR_ID, "window"));

View File

@ -30,8 +30,8 @@ import ghidra.util.exception.AssertException;
*/
public class NimbusLookAndFeelManager extends LookAndFeelManager {
public NimbusLookAndFeelManager() {
super(LafType.NIMBUS);
public NimbusLookAndFeelManager(ApplicationThemeManager themeManager) {
super(LafType.NIMBUS, themeManager);
// establish system color specific to Nimbus
systemToLafMap.addColor(new ColorValue(SYSTEM_BORDER_COLOR_ID, "nimbusBorder"));
@ -39,8 +39,7 @@ public class NimbusLookAndFeelManager extends LookAndFeelManager {
@Override
public void resetAll(GThemeValueMap javaDefaults) {
GColor.refreshAll();
GIcon.refreshAll();
themeManager.refreshGThemeValues();
reinstallNimubus();
}
@ -58,16 +57,16 @@ public class NimbusLookAndFeelManager extends LookAndFeelManager {
if (!affectedJavaIds.isEmpty()) {
reinstallNimubus();
}
GIcon.refreshAll();
themeManager.refreshGThemeValues();
repaintAll();
}
private void reinstallNimubus() {
try {
UIManager.setLookAndFeel(new GNimbusLookAndFeel() {
UIManager.setLookAndFeel(new GNimbusLookAndFeel(themeManager) {
@Override
protected GThemeValueMap extractJavaDefaults(UIDefaults defaults) {
return Gui.getJavaDefaults();
return themeManager.getJavaDefaults();
}
});
}
@ -79,13 +78,13 @@ public class NimbusLookAndFeelManager extends LookAndFeelManager {
@Override
protected void doInstallLookAndFeel() throws UnsupportedLookAndFeelException {
UIManager.setLookAndFeel(new GNimbusLookAndFeel());
UIManager.setLookAndFeel(new GNimbusLookAndFeel(themeManager));
}
@Override
protected GThemeValueMap extractJavaDefaults() {
// The GNimbusLookAndFeel already extracted the java defaults and installed them in the Gui
return Gui.getJavaDefaults();
return themeManager.getJavaDefaults();
}
@Override

View File

@ -15,11 +15,12 @@
*/
package generic.theme.laf;
import generic.theme.ApplicationThemeManager;
import generic.theme.LafType;
public class WindowsClassicLookAndFeelManager extends LookAndFeelManager {
public WindowsClassicLookAndFeelManager() {
super(LafType.WINDOWS_CLASSIC);
public WindowsClassicLookAndFeelManager(ApplicationThemeManager themeManager) {
super(LafType.WINDOWS_CLASSIC, themeManager);
}
}

View File

@ -15,12 +15,13 @@
*/
package generic.theme.laf;
import generic.theme.ApplicationThemeManager;
import generic.theme.LafType;
public class WindowsLookAndFeelManager extends LookAndFeelManager {
public WindowsLookAndFeelManager() {
super(LafType.WINDOWS);
public WindowsLookAndFeelManager(ApplicationThemeManager themeManager) {
super(LafType.WINDOWS, themeManager);
}
}

View File

@ -108,7 +108,14 @@ public interface Options {
/**
* Registers an option with a description, help location, and a default value without specifying
* the option type. This form requires that the default value not be null so that the option
* type can be inferred from the default value.
* type can be inferred from the default value.
* <P>
* Note, this method should not be used for
* colors and font as doing so will result in those colors and fonts becoming disconnected
* to the current theme. Instead use
*
* {@link #registerThemeColorBinding(String, String, HelpLocation, String)} or
* {@link #registerThemeFontBinding(String, String, HelpLocation, String)}.
* @param optionName the name of the option being registered.
* @param defaultValue the defaultValue for the option. The default value must not be
* null so that the OptionType can be determined. If the default value should be null, use
@ -123,6 +130,13 @@ public interface Options {
/**
* Registers an option with a description, help location, and a optional default value. With an optional
* default value, an OptionType must be passed as it is otherwise derived from the default value.
* <P>
* Note, this method should not be used for
* colors and font as doing so will result in those colors and fonts becoming disconnected
* to the current theme. Instead use
* {@link #registerThemeColorBinding(String, String, HelpLocation, String)} or
* {@link #registerThemeFontBinding(String, String, HelpLocation, String)}.
*
* @param optionName the name of the option being registered.
* @param type the OptionType for this options.
* @param defaultValue the defaultValue for the option. In this version of the method, the default
@ -136,6 +150,13 @@ public interface Options {
/**
* Registers an option with a description, help location, and a optional default value. With an optional
* default value, an OptionType must be passed as it is otherwise derived from the default value.
* <P>
* Note, this method should not be used for
* colors and font as doing so will result in those colors and fonts becoming disconnected
* to the current theme. Instead use
* {@link #registerThemeColorBinding(String, String, HelpLocation, String)} or
* {@link #registerThemeFontBinding(String, String, HelpLocation, String)}.
*
* @param optionName the name of the option being registered.
* @param type the OptionType for this options.
* @param defaultValue the defaultValue for the option. In this version of the method, the default

View File

@ -17,8 +17,7 @@ package ghidra.framework.options;
import java.awt.Color;
import generic.theme.GColor;
import generic.theme.Gui;
import generic.theme.*;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
@ -55,17 +54,17 @@ public class ThemeColorOption extends Option {
@Override
public void doSetCurrentValue(Object value) {
Gui.setColor(colorId, (Color) value);
ThemeManager.getInstance().setColor(colorId, (Color) value);
}
@Override
public boolean isDefault() {
return !Gui.isChangedColor(colorId);
return !ThemeManager.getInstance().isChangedColor(colorId);
}
@Override
public void restoreDefault() {
Gui.restoreColor(colorId);
ThemeManager.getInstance().restoreColor(colorId);
}
}

View File

@ -18,6 +18,7 @@ package ghidra.framework.options;
import java.awt.Font;
import generic.theme.Gui;
import generic.theme.ThemeManager;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
@ -54,17 +55,17 @@ public class ThemeFontOption extends Option {
@Override
public void doSetCurrentValue(Object value) {
Gui.setFont(fontId, (Font) value);
ThemeManager.getInstance().setFont(fontId, (Font) value);
}
@Override
public boolean isDefault() {
return !Gui.isChangedFont(fontId);
return !ThemeManager.getInstance().isChangedFont(fontId);
}
@Override
public void restoreDefault() {
Gui.restoreFont(fontId);
ThemeManager.getInstance().restoreFont(fontId);
}
}

View File

@ -23,6 +23,7 @@ import java.util.Objects;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import generic.theme.GIcon;
import generic.util.image.ImageUtils;
import resources.ResourceManager;
@ -32,6 +33,7 @@ import resources.ResourceManager;
public class DerivedImageIcon extends LazyImageIcon {
private Icon sourceIcon;
private Image sourceImage;
private Icon cachedDelegate;
/**
* Constructor for deriving from an icon
@ -56,6 +58,16 @@ public class DerivedImageIcon extends LazyImageIcon {
return sourceIcon;
}
protected boolean sourceIconChanged() {
if (sourceIcon instanceof GIcon gIcon) {
if (cachedDelegate != gIcon.getDelegate()) {
cachedDelegate = gIcon.getDelegate();
return true;
}
}
return false;
}
protected ImageIcon createImageIcon() {
Image image = createImage();
String imageName = getFilename();

View File

@ -39,7 +39,7 @@ public abstract class LazyImageIcon extends ImageIcon implements FileBasedIcon {
}
private synchronized void init() {
if (!loaded) {
if (!loaded || sourceIconChanged()) {
loaded = true;
ImageIcon imageIcon = createImageIcon();
if (imageIcon == null) {
@ -52,6 +52,10 @@ public abstract class LazyImageIcon extends ImageIcon implements FileBasedIcon {
protected abstract ImageIcon createImageIcon();
protected boolean sourceIconChanged() {
return false;
}
@Override
public String getFilename() {
return getDescription();

View File

@ -44,13 +44,13 @@ public class GThemeTest extends AbstractGenericTest {
@Before
public void setUp() {
theme = Gui.getDefaultTheme();
theme = new GTheme("TestTheme");
new Font("Courier", Font.BOLD, 12);
}
@Test
public void testGetName() {
assertEquals(Gui.getDefaultTheme().getName(), theme.getName());
assertEquals("TestTheme", theme.getName());
}
@Test
@ -97,7 +97,7 @@ public class GThemeTest extends AbstractGenericTest {
File file = createTempFile("themeTest", ".theme");
theme.saveToFile(file, false); // saveToFile returns new theme instance
new ThemeWriter(theme).writeThemeToFile(file);
theme = new ThemeReader(file).readTheme();
assertEquals("abc", theme.getName());

View File

@ -34,7 +34,7 @@ import generic.theme.builtin.*;
import resources.ResourceManager;
import resources.icons.UrlImageIcon;
public class GuiTest {
public class ThemeManagerTest {
private Font FONT = new Font("Dialog", Font.PLAIN, 13);
private Font SMALL_FONT = new Font("Dialog", Font.PLAIN, 4);
@ -49,9 +49,11 @@ public class GuiTest {
private GTheme NIMBUS_THEME = new NimbusTheme();
private GTheme WINDOWS_THEME = new WindowsTheme();
private GTheme MAC_THEME = new MacTheme();
private ThemeManager themeManager;
@Before
public void setUp() {
themes = new HashSet<>();
themes.add(METAL_THEME);
themes.add(NIMBUS_THEME);
@ -66,43 +68,8 @@ public class GuiTest {
darkDefaultValues.addColor(new ColorValue("color.test.bg", BLACK));
darkDefaultValues.addColor(new ColorValue("color.test.fg", BLUE));
themeManager = new DummyApplicationThemeManager();
Gui.setThemePreferenceManager(new ThemePreferenceManager() {
@Override
public GTheme getTheme() {
return new MetalTheme();
}
@Override
public void saveThemeToPreferences(GTheme theme) {
// do nothing
}
});
Gui.setPropertiesLoader(new ThemeFileLoader() {
@Override
public void loadThemeDefaultFiles() {
// do nothing
}
@Override
public Collection<GTheme> loadThemeFiles() {
return new HashSet<>(themes);
}
@Override
public GThemeValueMap getDefaults() {
return defaultValues;
}
@Override
public GThemeValueMap getDarkDefaults() {
return darkDefaultValues;
}
});
Gui.initialize();
}
@Test
@ -110,12 +77,11 @@ public class GuiTest {
GColor gColor = new GColor("color.test.bg");
assertColor(WHITE, gColor);
Gui.setTheme(new GTheme("Test", LafType.FLAT_DARK, true));
themeManager.setTheme(new GTheme("Test", LafType.FLAT_DARK, true));
assertEquals(BLACK, gColor);
Gui.setTheme(new GTheme("Test2"));
themeManager.setTheme(new GTheme("Test2"));
assertEquals(WHITE, gColor);
}
@Test
@ -126,25 +92,25 @@ public class GuiTest {
theme.setColor("color.test.bg", GREEN);
assertColor(WHITE, gColor);
Gui.setTheme(theme);
themeManager.setTheme(theme);
assertEquals(GREEN, gColor);
Gui.setTheme(new GTheme("Test2"));
themeManager.setTheme(new GTheme("Test2"));
assertEquals(WHITE, gColor);
}
@Test
public void testThemeFontOverride() {
assertEquals(FONT, Gui.getFont("font.test.foo"));
assertEquals(FONT, themeManager.getFont("font.test.foo"));
GTheme theme = new GTheme("Test");
theme.setFont("font.test.foo", SMALL_FONT);
Gui.setTheme(theme);
themeManager.setTheme(theme);
assertEquals(SMALL_FONT, Gui.getFont("font.test.foo"));
assertEquals(SMALL_FONT, themeManager.getFont("font.test.foo"));
Gui.setTheme(new GTheme("Test2"));
assertEquals(FONT, Gui.getFont("font.test.foo"));
themeManager.setTheme(new GTheme("Test2"));
assertEquals(FONT, themeManager.getFont("font.test.foo"));
}
@Test
@ -155,10 +121,10 @@ public class GuiTest {
theme.setIcon("icon.test.foo", ICON2);
assertIcon(ICON1, gIcon);
Gui.setTheme(theme);
themeManager.setTheme(theme);
assertIcon(ICON2, gIcon);
Gui.setTheme(new GTheme("Test2"));
themeManager.setTheme(new GTheme("Test2"));
assertIcon(ICON1, gIcon);
}
@ -168,7 +134,7 @@ public class GuiTest {
assertColor(WHITE, gColor);
defaultValues.addColor(new ColorValue("color.test.bg", YELLOW));
Gui.reloadApplicationDefaults();
themeManager.reloadApplicationDefaults();
assertEquals(YELLOW, gColor);
}
@ -177,50 +143,50 @@ public class GuiTest {
GColor gColor = new GColor("color.test.bg");
assertColor(WHITE, gColor);
Gui.setColor("color.test.bg", PURPLE);
themeManager.setColor("color.test.bg", PURPLE);
assertColor(PURPLE, gColor);
Gui.restoreThemeValues();
themeManager.restoreThemeValues();
assertEquals(WHITE, gColor);
}
@Test
public void testGetAllThemes() {
assertEquals(themes, Gui.getAllThemes());
assertEquals(themes, themeManager.getAllThemes());
}
@Test
public void testAddTheme() {
GTheme newTheme = new GTheme("Test");
Set<GTheme> allThemes = Gui.getAllThemes();
Set<GTheme> allThemes = themeManager.getAllThemes();
assertEquals(themes.size(), allThemes.size());
assertFalse(allThemes.contains(newTheme));
Gui.addTheme(newTheme);
allThemes = Gui.getAllThemes();
themeManager.addTheme(newTheme);
allThemes = themeManager.getAllThemes();
assertTrue(allThemes.contains(newTheme));
}
@Test
public void testDeleteTheme() {
GTheme newTheme = new GTheme("Test");
Set<GTheme> allThemes = Gui.getAllThemes();
Set<GTheme> allThemes = themeManager.getAllThemes();
assertFalse(allThemes.contains(newTheme));
Gui.addTheme(newTheme);
allThemes = Gui.getAllThemes();
themeManager.addTheme(newTheme);
allThemes = themeManager.getAllThemes();
assertTrue(allThemes.contains(newTheme));
Gui.deleteTheme(newTheme);
allThemes = Gui.getAllThemes();
themeManager.deleteTheme(newTheme);
allThemes = themeManager.getAllThemes();
assertFalse(allThemes.contains(newTheme));
}
@Test
public void testGetSupportedThemes() {
Set<GTheme> supportedThemes = Gui.getSupportedThemes();
Set<GTheme> supportedThemes = themeManager.getSupportedThemes();
// since we put mac specific and windows specific themes, they can't all be here
// regardless of the current platform
assertTrue(supportedThemes.size() < themes.size());
@ -231,31 +197,31 @@ public class GuiTest {
@Test
public void testGetLookAndFeelType() {
LafType lookAndFeelType = Gui.getLookAndFeelType();
LafType lookAndFeelType = themeManager.getLookAndFeelType();
// in the test setup, we defaulted to the MetalLookAndFeel
assertEquals(LafType.METAL, lookAndFeelType);
}
@Test
public void testGetActiveTheme() {
GTheme activeTheme = Gui.getActiveTheme();
GTheme activeTheme = themeManager.getActiveTheme();
assertEquals(METAL_THEME, activeTheme);
}
@Test
public void testGetThemeByName() {
GTheme theme = Gui.getTheme("Nimbus Theme");
GTheme theme = themeManager.getTheme("Nimbus Theme");
assertEquals(NIMBUS_THEME, theme);
}
@Test
public void testGetAllValues() {
GThemeValueMap allValues = Gui.getAllValues();
GThemeValueMap allValues = themeManager.getCurrentValues();
assertEquals(WHITE, allValues.getColor("color.test.bg").getRawValue());
Gui.setColor("color.test.bg", PURPLE);
themeManager.setColor("color.test.bg", PURPLE);
allValues = Gui.getAllValues();
allValues = themeManager.getCurrentValues();
assertEquals(PURPLE, allValues.getColor("color.test.bg").getRawValue());
}
@ -263,17 +229,17 @@ public class GuiTest {
@Test
public void testGetNonDefaultValues() {
// should be empty if we haven't changed any themeValues
GThemeValueMap nonDefaultValues = Gui.getNonDefaultValues();
GThemeValueMap nonDefaultValues = themeManager.getNonDefaultValues();
assertTrue(nonDefaultValues.isEmpty());
// change some values and see that they show up in the nonDefaultValues
Gui.setColor("color.test.bg", RED);
Gui.setFont("font.test.foo", SMALL_FONT);
Gui.setIcon("icon.test.foo", ICON2);
themeManager.setColor("color.test.bg", RED);
themeManager.setFont("font.test.foo", SMALL_FONT);
themeManager.setIcon("icon.test.foo", ICON2);
// also add in a totally new value
Gui.setColor("color.test.xxx", GREEN);
themeManager.setColor("color.test.xxx", GREEN);
nonDefaultValues = Gui.getNonDefaultValues();
nonDefaultValues = themeManager.getNonDefaultValues();
assertEquals(4, nonDefaultValues.size());
assertEquals(RED, nonDefaultValues.getColor("color.test.bg").getRawValue());
assertEquals(GREEN, nonDefaultValues.getColor("color.test.xxx").getRawValue());
@ -283,57 +249,57 @@ public class GuiTest {
@Test
public void testGetColor() {
assertEquals(WHITE, Gui.getColor("color.test.bg"));
assertEquals(WHITE, themeManager.getColor("color.test.bg"));
}
@Test
public void testGetFont() {
assertEquals(FONT, Gui.getFont("font.test.foo"));
assertEquals(FONT, themeManager.getFont("font.test.foo"));
}
@Test
public void testGetIcon() {
assertEquals(ICON1, Gui.getIcon("icon.test.foo"));
assertEquals(ICON1, themeManager.getIcon("icon.test.foo"));
}
@Test
public void testGetColorWithUnresolvedId() {
assertEquals(CYAN, Gui.getColor("color.badid", false));
assertEquals(CYAN, themeManager.getColor("color.badid"));
}
@Test
public void testGetIconWithUnresolvedId() {
assertEquals(ResourceManager.getDefaultIcon(), Gui.getIcon("icon.badid", false));
assertEquals(ResourceManager.getDefaultIcon(), themeManager.getIcon("icon.badid"));
}
@Test
public void testGetFontWithUnresolvedId() {
assertEquals(Gui.DEFAULT_FONT, Gui.getFont("font.badid", false));
assertEquals(ThemeManager.DEFAULT_FONT, themeManager.getFont("font.badid"));
}
@Test
public void testGetGColorUiResource() {
Color color = Gui.getGColorUiResource("color.test.bg");
Color color = themeManager.getGColorUiResource("color.test.bg");
assertTrue(color instanceof UIResource);
// make sure there is only one instance for an id;
Color color2 = Gui.getGColorUiResource("color.test.bg");
Color color2 = themeManager.getGColorUiResource("color.test.bg");
assertTrue(color == color2);
}
@Test
public void testGetGIconUiResource() {
Icon icon = Gui.getGIconUiResource("icon.test.foo");
Icon icon = themeManager.getGIconUiResource("icon.test.foo");
assertTrue(icon instanceof UIResource);
// make sure there is only one instance for an id;
Icon gIcon2 = Gui.getGIconUiResource("icon.test.foo");
Icon gIcon2 = themeManager.getGIconUiResource("icon.test.foo");
assertTrue(icon == gIcon2);
}
@Test
public void testGetApplicationLightDefaults() {
assertEquals(defaultValues, Gui.getApplicationLightDefaults());
assertEquals(defaultValues, themeManager.getApplicationLightDefaults());
}
@Test
@ -342,17 +308,17 @@ public class GuiTest {
GThemeValueMap expected = new GThemeValueMap();
expected.load(defaultValues);
expected.load(darkDefaultValues);
assertEquals(expected, Gui.getApplicationDarkDefaults());
assertEquals(expected, themeManager.getApplicationDarkDefaults());
}
@Test
public void testRegisterFont() {
Gui.setFont(new FontValue("font.test", SMALL_FONT));
themeManager.setFont(new FontValue("font.test", SMALL_FONT));
JLabel label = new JLabel("Test");
assertNotEquals(SMALL_FONT, label.getFont());
Gui.registerFont(label, "font.test");
themeManager.registerFont(label, "font.test");
assertEquals(SMALL_FONT, label.getFont());
Gui.setFont(new FontValue("font.test", FONT));
themeManager.setFont(new FontValue("font.test", FONT));
assertEquals(FONT, label.getFont());
}
@ -369,4 +335,43 @@ public class GuiTest {
fail("Icons don't match. Expected " + url + ", but got " + gUrl);
}
}
// ApplicationThemeManager that doesn't read in theme.properties files or preferences
class DummyApplicationThemeManager extends ApplicationThemeManager {
DummyApplicationThemeManager() {
themePreferences = new ThemePreferences() {
@Override
public GTheme load() {
return new MetalTheme();
}
@Override
public void save(GTheme theme) {
// do nothing
}
};
themeFileLoader = new ThemeFileLoader() {
@Override
public void loadThemeDefaultFiles() {
// do nothing
}
@Override
public Collection<GTheme> loadThemeFiles() {
return new HashSet<>(themes);
}
@Override
public GThemeValueMap getDefaults() {
return defaultValues;
}
@Override
public GThemeValueMap getDarkDefaults() {
return darkDefaultValues;
}
};
doInitialize();
}
}
}

View File

@ -21,9 +21,11 @@ import java.util.List;
import org.junit.Test;
public class WeakStoreTest {
import generic.test.AbstractGenericTest;
public class WeakStoreTest extends AbstractGenericTest {
@Test
public void testStore() throws InterruptedException {
public void testStore() {
WeakStore<Foo> store = new WeakStore<>();
store.add(new Foo("AAA"));
store.add(new Foo("BBB"));
@ -38,13 +40,10 @@ public class WeakStoreTest {
assertEquals("CCC", values.get(2).getName());
values = null;
for (int i = 0; i < 20; i++) {
waitFor(() -> {
System.gc();
if (store.size() == 0) {
break;
}
}
assertEquals(0, store.size());
return store.size() == 0;
}, "Weak store values were never garbage collected");
}
static class Foo {

View File

@ -17,6 +17,7 @@ package ghidra.service.graph;
import static org.junit.Assert.*;
import java.awt.Color;
import java.awt.Font;
import java.util.Arrays;
import java.util.List;
@ -24,32 +25,23 @@ import java.util.List;
import org.junit.Before;
import org.junit.Test;
import docking.FakeDockingTool;
import docking.test.AbstractDockingTest;
import generic.theme.*;
import generic.theme.GThemeDefaults.Colors.Palette;
import generic.theme.Gui;
import ghidra.framework.options.Options;
import ghidra.framework.options.ToolOptions;
import ghidra.util.HelpLocation;
public class GraphDisplayOptionsTest extends AbstractDockingTest {
public class GraphDisplayOptionsTest {
private GraphType graphType;
private GraphDisplayOptions options;
@Before
public void setUp() {
Gui.setColor("color.V1", Palette.BLACK);
Gui.setColor("color.V2", Palette.BLACK);
Gui.setColor("color.V3", Palette.BLACK);
Gui.setColor("color.E1", Palette.BLACK);
Gui.setColor("color.E2", Palette.BLACK);
Gui.setColor("color.E3", Palette.BLACK);
Gui.setColor("color.edge.default", Palette.BLACK);
Gui.setColor("color.vertex.default", Palette.BLACK);
Gui.setColor("color.edge.selected", Palette.BLACK);
Gui.setColor("color.vertex.selected", Palette.BLACK);
Gui.setFont("font.graph", new Font("monospaced", Font.PLAIN, 12));
// create a dummy theme manager that defines values for use in this test
DummyThemeManager themeManager = new DummyThemeManager();
// create a new graph definition and options using theme properties
List<String> vertexTypes = Arrays.asList("V1", "V2", "V3");
List<String> edgeTypes = Arrays.asList("E1", "E2", "E3");
graphType = new GraphType("Test", "Test Description", vertexTypes, edgeTypes);
@ -286,22 +278,44 @@ public class GraphDisplayOptionsTest extends AbstractDockingTest {
}
@Test
public void testChangingToolOptionsAffectsGraph() {
FakeDockingTool tool = new FakeDockingTool();
ToolOptions toolOptions = tool.getOptions("Graph");
options.registerOptions(toolOptions, null);
options.initializeFromOptions(tool);
// Create a ThemeManager that it not fully initialized for speed. This class provides
// fake property theme values.
class DummyThemeManager extends StubThemeManager {
DummyThemeManager() {
installTestValues();
installExpectedValues();
installInGui();
}
AttributedVertex vertex = new AttributedVertex("Foo");
vertex.setVertexType("V1");
assertEquals(Palette.BLACK.getRGB(), options.getVertexColor(vertex).getRGB());
private void installExpectedValues() {
setColor(new ColorValue("color.vertex.selected", Color.BLACK));
setColor(new ColorValue("color.edge.selected", Color.BLACK));
setColor(new ColorValue("color.graphdisplay.vertex.default", Color.BLACK));
setColor(new ColorValue("color.graphdisplay.edge.default", Color.BLACK));
setFont(new FontValue("font.graph", new Font("monospaced", Font.PLAIN, 12)));
Options graphDisplayOptions = toolOptions.getOptions(options.getRootOptionsName());
Options vertexColorOptions = graphDisplayOptions.getOptions("Vertex Colors");
vertexColorOptions.setColor("V1", Palette.GOLD);
}
protected void installTestValues() {
setColor(new ColorValue("color.V1", Color.BLACK));
setColor(new ColorValue("color.V2", Color.BLACK));
setColor(new ColorValue("color.V3", Color.BLACK));
setColor(new ColorValue("color.E1", Color.BLACK));
setColor(new ColorValue("color.E2", Color.BLACK));
setColor(new ColorValue("color.E3", Color.BLACK));
setColor(new ColorValue("color.edge.default", Color.BLACK));
setColor(new ColorValue("color.vertex.default", Color.BLACK));
setColor(new ColorValue("color.edge.selected", Color.BLACK));
setColor(new ColorValue("color.vertex.selected", Color.BLACK));
setColor(new ColorValue("color.graphdisplay.vertex.selected", Color.BLACK));
setColor(new ColorValue("color.graphdisplay.edge.selected", Color.BLACK));
setColor(new ColorValue("color.vertex.selected", Color.BLACK));
setColor(new ColorValue("color.vertex.selected", Color.BLACK));
setFont(new FontValue("font.graph", new Font("monospaced", Font.PLAIN, 12)));
setFont(
new FontValue("font.graphdisplay.default", new Font("monospaced", Font.PLAIN, 12)));
}
assertEquals(Palette.GOLD.getRGB(), options.getVertexColor(vertex).getRGB());
}
}

View File

@ -21,7 +21,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import generic.theme.Gui;
import generic.theme.ApplicationThemeManager;
import ghidra.framework.Application;
import ghidra.framework.ApplicationConfiguration;
import help.validator.*;
@ -69,7 +69,7 @@ public class GHelpBuilder {
ApplicationConfiguration config = new ApplicationConfiguration() {
@Override
protected void initializeApplication() {
Gui.initialize();
ApplicationThemeManager.initialize();
}
@Override

View File

@ -25,7 +25,7 @@ import java.nio.file.Paths;
import org.junit.Test;
import generic.theme.Gui;
import generic.theme.ApplicationThemeManager;
import ghidra.GhidraTestApplicationLayout;
import ghidra.framework.ApplicationConfiguration;
import utility.application.ApplicationLayout;
@ -101,7 +101,7 @@ public class HelpBuildUtilsTest extends AbstractHelpTest {
@Test
public void testLocateReferences_Icons() throws URISyntaxException {
Gui.initialize();
ApplicationThemeManager.initialize();
Path sourceFile = Paths.get(HTML_FILE_PATH);
String reference = "Icons.REFRESH_ICON"; // see Icons class
ImageLocation location = HelpBuildUtils.locateImageReference(sourceFile, reference);

View File

@ -18,7 +18,7 @@ package ghidra.app.plugin.gui;
import docking.action.builder.ActionBuilder;
import docking.theme.gui.ThemeDialog;
import docking.theme.gui.ThemeUtils;
import generic.theme.Gui;
import generic.theme.ThemeManager;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.framework.main.ApplicationLevelOnlyPlugin;
import ghidra.framework.main.UtilityPluginPackage;
@ -38,8 +38,11 @@ import ghidra.util.HelpLocation;
//@formatter:on
public class ThemeManagerPlugin extends Plugin implements ApplicationLevelOnlyPlugin {
private ThemeManager themeManager;
public ThemeManagerPlugin(PluginTool tool) {
super(tool);
themeManager = ThemeManager.getInstance();
}
@Override
@ -52,35 +55,35 @@ public class ThemeManagerPlugin extends Plugin implements ApplicationLevelOnlyPl
.menuPath("Edit", "Theme")
.menuGroup(group, "1")
.helpLocation(new HelpLocation("Theming", "Edit_Theme"))
.onAction(e -> ThemeDialog.editTheme())
.onAction(e -> ThemeDialog.editTheme(themeManager))
.buildAndInstall(tool);
new ActionBuilder("Reset", owner)
.menuPath("Edit", themeSubMenu, "Reset Theme Values")
.menuGroup(group, "2")
.helpLocation(new HelpLocation("Theming", "Reset_Theme_Values"))
.onAction(e -> ThemeUtils.resetThemeToDefault())
.onAction(e -> ThemeUtils.resetThemeToDefault(themeManager))
.buildAndInstall(tool);
new ActionBuilder("Import Theme", owner)
.menuPath("Edit", themeSubMenu, "Import...")
.menuGroup(group, "3")
.helpLocation(new HelpLocation("Theming", "Import_Theme"))
.onAction(e -> ThemeUtils.importTheme())
.onAction(e -> ThemeUtils.importTheme(themeManager))
.buildAndInstall(tool);
new ActionBuilder("Export Theme", owner)
.menuPath("Edit", themeSubMenu, "Export...")
.menuGroup(group, "4")
.helpLocation(new HelpLocation("Theming", "Export_Theme"))
.onAction(e -> ThemeUtils.exportTheme())
.onAction(e -> ThemeUtils.exportTheme(themeManager))
.buildAndInstall(tool);
new ActionBuilder("Delete Theme", owner)
.menuPath("Edit", themeSubMenu, "Delete...")
.menuGroup(group, "5")
.helpLocation(new HelpLocation("Theming", "Delete_Theme"))
.onAction(e -> ThemeUtils.deleteTheme())
.onAction(e -> ThemeUtils.deleteTheme(themeManager))
.buildAndInstall(tool);
tool.setMenuGroup(new String[] { "Edit", themeSubMenu }, group, "2");
@ -89,8 +92,8 @@ public class ThemeManagerPlugin extends Plugin implements ApplicationLevelOnlyPl
@Override
protected boolean canClose() {
if (Gui.hasThemeChanges()) {
return ThemeUtils.askToSaveThemeChanges();
if (themeManager.hasThemeChanges()) {
return ThemeUtils.askToSaveThemeChanges(themeManager);
}
return true;
}

View File

@ -26,13 +26,16 @@ import resources.ResourceManager;
public class ThemingScreenShots extends GhidraScreenShotGenerator {
private ThemeManager themeManager;
public ThemingScreenShots() {
super();
themeManager = ThemeManager.getInstance();
}
@Test
public void testThemeDialog() {
showDialogWithoutBlocking(tool, new ThemeDialog());
showDialogWithoutBlocking(tool, new ThemeDialog(themeManager));
captureDialog(1000, 500);
}
@ -41,7 +44,7 @@ public class ThemingScreenShots extends GhidraScreenShotGenerator {
ColorValueEditor editor = new ColorValueEditor(e -> {
/**/});
ColorValue value = new ColorValue("color.bg.test", Palette.BLUE);
Gui.setColor(value);
themeManager.setColor(value);
editor.editValue(value);
captureDialog();
}
@ -51,7 +54,7 @@ public class ThemingScreenShots extends GhidraScreenShotGenerator {
FontValueEditor editor = new FontValueEditor(e -> {
/**/});
FontValue value = new FontValue("font.xyz", new Font("Monospaced", Font.BOLD, 14));
Gui.setFont(value);
themeManager.setFont(value);
editor.editValue(value);
captureDialog();
}
@ -61,7 +64,7 @@ public class ThemingScreenShots extends GhidraScreenShotGenerator {
IconValueEditor editor = new IconValueEditor(e -> {
/**/});
IconValue value = new IconValue("icon.bomb", ResourceManager.getDefaultIcon());
Gui.setIcon(value);
themeManager.setIcon(value);
editor.editValue(value);
captureDialog();
}