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 { else {
options.registerOption(key.getName(), type, defaultValue, help, description, options.registerOption(key.getName(), type, defaultValue, help, description,
editor); 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.user = color.fg
color.fg.plugin.comments.history.date = rgb(124, 37, 18) 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.default = blue
color.bg.plugin.datamgr.edge.composite = magenta color.bg.plugin.datamgr.edge.composite = magenta
color.bg.plugin.datamgr.edge.reference = blue 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.substring.release. = images/famfamfam_silk_icons_v013/bullet_purple.png
icon.fsbrowser.file.overlay.imported = images/checkmark_green.gif 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(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 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.*;
import ghidra.util.exception.UsrException; import ghidra.util.exception.UsrException;
import ghidra.util.task.TaskLauncher; import ghidra.util.task.TaskLauncher;
import ghidra.util.task.TaskMonitorAdapter;
/** /**
* Main Ghidra application class. Creates * Main Ghidra application class. Creates
@ -73,7 +72,6 @@ public class GhidraRun implements GhidraLaunchable {
Runnable mainTask = () -> { Runnable mainTask = () -> {
GhidraApplicationConfiguration configuration = new GhidraApplicationConfiguration(); GhidraApplicationConfiguration configuration = new GhidraApplicationConfiguration();
configuration.setTaskMonitor(new StatusReportingTaskMonitor());
Application.initializeApplication(layout, configuration); Application.initializeApplication(layout, configuration);
log = LogManager.getLogger(GhidraRun.class); log = LogManager.getLogger(GhidraRun.class);
@ -243,15 +241,3 @@ public class GhidraRun implements GhidraLaunchable {
// this exists just to allow access to the constructor // 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.options.editor.FontEditor;
import docking.widgets.OptionDialog; import docking.widgets.OptionDialog;
import docking.widgets.filechooser.GhidraFileChooser; import docking.widgets.filechooser.GhidraFileChooser;
import generic.theme.GIcon; import generic.theme.*;
import generic.theme.Gui;
import ghidra.framework.plugintool.ComponentProviderAdapter; import ghidra.framework.plugintool.ComponentProviderAdapter;
import ghidra.util.HelpLocation; import ghidra.util.HelpLocation;
import ghidra.util.Msg; import ghidra.util.Msg;
@ -274,7 +273,7 @@ public class TextEditorComponentProvider extends ComponentProviderAdapter {
FontEditor editor = new FontEditor(); FontEditor editor = new FontEditor();
editor.setValue(Gui.getFont(FONT_ID)); editor.setValue(Gui.getFont(FONT_ID));
editor.showDialog(); editor.showDialog();
Gui.setFont(FONT_ID, (Font) editor.getValue()); ThemeManager.getInstance().setFont(FONT_ID, (Font) editor.getValue());
} }
private void save() { private void save() {

View file

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

View file

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

View file

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

View file

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

View file

@ -20,7 +20,7 @@ import docking.DockingWindowManager;
import docking.framework.ApplicationInformationDisplayFactory; import docking.framework.ApplicationInformationDisplayFactory;
import docking.framework.SplashScreen; import docking.framework.SplashScreen;
import docking.widgets.PopupKeyStorePasswordProvider; import docking.widgets.PopupKeyStorePasswordProvider;
import generic.theme.Gui; import generic.theme.ApplicationThemeManager;
import ghidra.docking.util.LookAndFeelUtils; import ghidra.docking.util.LookAndFeelUtils;
import ghidra.formats.gfilesystem.crypto.CryptoProviders; import ghidra.formats.gfilesystem.crypto.CryptoProviders;
import ghidra.formats.gfilesystem.crypto.PopupGUIPasswordProvider; import ghidra.formats.gfilesystem.crypto.PopupGUIPasswordProvider;
@ -30,6 +30,7 @@ import ghidra.framework.preferences.Preferences;
import ghidra.net.ApplicationKeyManagerFactory; import ghidra.net.ApplicationKeyManagerFactory;
import ghidra.util.ErrorDisplay; import ghidra.util.ErrorDisplay;
import ghidra.util.SystemUtilities; import ghidra.util.SystemUtilities;
import ghidra.util.task.TaskMonitorAdapter;
public class GhidraApplicationConfiguration extends HeadlessGhidraApplicationConfiguration { public class GhidraApplicationConfiguration extends HeadlessGhidraApplicationConfiguration {
@ -43,12 +44,13 @@ public class GhidraApplicationConfiguration extends HeadlessGhidraApplicationCon
@Override @Override
protected void initializeApplication() { protected void initializeApplication() {
Gui.initialize(); ApplicationThemeManager.initialize();
LookAndFeelUtils.performPlatformSpecificFixups(); LookAndFeelUtils.performPlatformSpecificFixups();
if (showSplashScreen) { if (showSplashScreen) {
showUserAgreement(); showUserAgreement();
SplashScreen.showSplashScreen(); SplashScreen.showSplashScreen();
this.monitor = new StatusReportingTaskMonitor();
} }
super.initializeApplication(); super.initializeApplication();
@ -89,4 +91,17 @@ public class GhidraApplicationConfiguration extends HeadlessGhidraApplicationCon
public ErrorDisplay getErrorDisplay() { public ErrorDisplay getErrorDisplay() {
return new DockingErrorDisplay(); 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() { private void createSubstringMap() {
GThemeValueMap values = Gui.getAllValues(); GThemeValueMap values = ThemeManager.getInstance().getCurrentValues();
List<IconValue> icons = values.getIcons(); List<IconValue> icons = values.getIcons();
for (IconValue iconValue : icons) { for (IconValue iconValue : icons) {
String id = iconValue.getId(); String id = iconValue.getId();

View file

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

View file

@ -33,7 +33,7 @@ public class TokenHighlightColors {
float h = (float) Math.random(); // 0-360 float h = (float) Math.random(); // 0-360
float s = .25f; // saturation; gray to full color; full color is too harsh for highlights float s = .25f; // saturation; gray to full color; full color is too harsh for highlights
float b = 1f; // brightness; black to full color 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 s = .5f; // a bit more color against a dark background
b = .5f; // less brightness, as the background is not as bright 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/page_green.png||FAMFAMFAM Icons - CC 2.5||||END|
src/main/resources/images/play.png||GHIDRA||||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/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/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/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| 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/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/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/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/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/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| 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.bg.filechooser = color.bg
color.fg.filechooser = color.fg color.fg.filechooser = color.fg
color.bg.filechooser.shortcut = lightGray
color.bg.fieldpanel = color.bg color.bg.fieldpanel = color.bg
color.fg.fieldpanel = color.fg color.fg.fieldpanel = color.fg
color.bg.fieldpanel.selection = color.bg.selection color.bg.fieldpanel.selection = color.bg.selection
color.bg.fieldpanel.highlight = color.bg.highlight color.bg.fieldpanel.highlight = color.bg.highlight
color.bg.fieldpanel.selection.and.highlight = green color.bg.fieldpanel.selection.and.highlight = green
color.bg.tree = [color]Tree.textBackground
color.bg.tree.selected = [color]Tree.selectionBackground
// docking buttons // docking buttons
color.fg.button = black color.fg.button = black
@ -239,3 +243,10 @@ color.bg.tableheader.gradient.end.primary = darkBlue
// docking buttons // docking buttons
color.fg.button = darkGray 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 docking.action.*;
import generic.theme.Gui; import generic.theme.Gui;
import generic.theme.ThemeManager;
import ghidra.util.*; import ghidra.util.*;
import ghidra.util.exception.AssertException; import ghidra.util.exception.AssertException;
import help.HelpDescriptor; import help.HelpDescriptor;
@ -801,7 +802,7 @@ public abstract class ComponentProvider implements HelpDescriptor, ActionContext
else { else {
size = Math.max(size - 1, 3); 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.table.GTableCellRenderer;
import docking.widgets.tree.support.GTreeRenderer; import docking.widgets.tree.support.GTreeRenderer;
import generic.theme.GThemeDefaults.Colors.Palette; import generic.theme.GThemeDefaults.Colors.Palette;
import generic.theme.Gui; import ghidra.docking.util.LookAndFeelUtils;
import ghidra.util.HTMLUtilities; import ghidra.util.HTMLUtilities;
import resources.ResourceManager; import resources.ResourceManager;
@ -126,7 +126,7 @@ public class DockingUtils {
public static JSeparator createToolbarSeparator() { public static JSeparator createToolbarSeparator() {
Dimension sepDim = new Dimension(2, ICON_SIZE + 2); Dimension sepDim = new Dimension(2, ICON_SIZE + 2);
JSeparator separator = new JSeparator(SwingConstants.VERTICAL); JSeparator separator = new JSeparator(SwingConstants.VERTICAL);
if (Gui.isUsingAquaUI(separator.getUI())) { if (LookAndFeelUtils.isUsingAquaUI(separator.getUI())) {
separator.setUI(new BasicSeparatorUI()); separator.setUI(new BasicSeparatorUI());
} }
separator.setPreferredSize(sepDim); // ugly work around to force height of separator 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 value = 0;
int delta = 16; int delta = 16;
if (Gui.getActiveTheme().useDarkDefaults()) { if (Gui.isDarkTheme()) {
value = 128; value = 128;
delta = -16; delta = -16;
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -29,7 +29,7 @@ import docking.action.ActionContextProvider;
import docking.widgets.table.GFilterTable; import docking.widgets.table.GFilterTable;
import docking.widgets.table.GTable; import docking.widgets.table.GTable;
import generic.theme.ColorValue; import generic.theme.ColorValue;
import generic.theme.Gui; import generic.theme.ThemeManager;
import ghidra.util.Swing; import ghidra.util.Swing;
/** /**
@ -41,10 +41,12 @@ public class ThemeColorTable extends JPanel implements ActionContextProvider {
private ColorValueEditor colorEditor = new ColorValueEditor(this::colorValueChanged); private ColorValueEditor colorEditor = new ColorValueEditor(this::colorValueChanged);
private GTable table; private GTable table;
private GFilterTable<ColorValue> filterTable; private GFilterTable<ColorValue> filterTable;
private ThemeManager themeManager;
public ThemeColorTable() { public ThemeColorTable(ThemeManager themeManager) {
super(new BorderLayout()); super(new BorderLayout());
colorTableModel = new ThemeColorTableModel(); this.themeManager = themeManager;
colorTableModel = new ThemeColorTableModel(themeManager);
filterTable = new GFilterTable<>(colorTableModel); filterTable = new GFilterTable<>(colorTableModel);
table = filterTable.getTable(); 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 // run later - don't rock the boat in the middle of a listener callback
Swing.runLater(() -> { Swing.runLater(() -> {
ColorValue newValue = (ColorValue) event.getNewValue(); 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 defaultValues;
private GThemeValueMap lightDefaultValues; private GThemeValueMap lightDefaultValues;
private GThemeValueMap darkDefaultValues; private GThemeValueMap darkDefaultValues;
private ThemeManager themeManager;
public ThemeColorTableModel() { public ThemeColorTableModel(ThemeManager themeManager) {
super(new ServiceProviderStub()); super(new ServiceProviderStub());
this.themeManager = themeManager;
load(); 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. * Reloads the just the current values shown in the table. Called whenever a color changes.
*/ */
public void reloadCurrent() { public void reloadCurrent() {
currentValues = Gui.getAllValues(); currentValues = themeManager.getCurrentValues();
colors = currentValues.getColors(); colors = currentValues.getColors();
fireTableDataChanged(); fireTableDataChanged();
} }
@ -67,20 +69,17 @@ public class ThemeColorTableModel extends GDynamicColumnTableModel<ColorValue, O
fireTableDataChanged(); fireTableDataChanged();
} }
/** ColorValue getThemeValue(String id) {
* Returns the original value for the current theme
*/
public ColorValue getThemeValue(String id) {
return themeValues.getColor(id); return themeValues.getColor(id);
} }
private void load() { private void load() {
currentValues = Gui.getAllValues(); currentValues = themeManager.getCurrentValues();
colors = currentValues.getColors(); colors = currentValues.getColors();
themeValues = Gui.getThemeValues(); themeValues = themeManager.getThemeValues();
defaultValues = Gui.getDefaults(); defaultValues = themeManager.getDefaults();
lightDefaultValues = Gui.getApplicationLightDefaults(); lightDefaultValues = themeManager.getApplicationLightDefaults();
darkDefaultValues = Gui.getApplicationDarkDefaults(); darkDefaultValues = themeManager.getApplicationDarkDefaults();
} }
@ -155,7 +154,10 @@ public class ThemeColorTableModel extends GDynamicColumnTableModel<ColorValue, O
if (colorValue == null) { if (colorValue == null) {
return 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); return new ResolvedColor(id, colorValue.getReferenceId(), color);
} }

View file

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

View file

@ -29,7 +29,7 @@ import docking.action.ActionContextProvider;
import docking.widgets.table.GFilterTable; import docking.widgets.table.GFilterTable;
import docking.widgets.table.GTable; import docking.widgets.table.GTable;
import generic.theme.FontValue; import generic.theme.FontValue;
import generic.theme.Gui; import generic.theme.ThemeManager;
import ghidra.util.Swing; import ghidra.util.Swing;
/** /**
@ -41,11 +41,13 @@ public class ThemeFontTable extends JPanel implements ActionContextProvider {
private FontValueEditor fontEditor = new FontValueEditor(this::fontValueChanged); private FontValueEditor fontEditor = new FontValueEditor(this::fontValueChanged);
private GTable table; private GTable table;
private GFilterTable<FontValue> filterTable; private GFilterTable<FontValue> filterTable;
private ThemeManager themeManager;
public ThemeFontTable() { public ThemeFontTable(ThemeManager themeManager) {
super(new BorderLayout()); super(new BorderLayout());
this.themeManager = themeManager;
fontTableModel = new ThemeFontTableModel(); fontTableModel = new ThemeFontTableModel(themeManager);
filterTable = new GFilterTable<>(fontTableModel); filterTable = new GFilterTable<>(fontTableModel);
table = filterTable.getTable(); table = filterTable.getTable();
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 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 // run later - don't rock the boat in the middle of a listener callback
Swing.runLater(() -> { Swing.runLater(() -> {
FontValue newValue = (FontValue) event.getNewValue(); 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 currentValues;
private GThemeValueMap themeValues; private GThemeValueMap themeValues;
private GThemeValueMap defaultValues; private GThemeValueMap defaultValues;
private ThemeManager themeManager;
public ThemeFontTableModel() { public ThemeFontTableModel(ThemeManager themeManager) {
super(new ServiceProviderStub()); super(new ServiceProviderStub());
this.themeManager = themeManager;
load(); 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. * Reloads the just the current values shown in the table. Called whenever a font changes.
*/ */
public void reloadCurrent() { public void reloadCurrent() {
currentValues = Gui.getAllValues(); currentValues = themeManager.getCurrentValues();
fonts = currentValues.getFonts(); fonts = currentValues.getFonts();
fireTableDataChanged(); fireTableDataChanged();
} }
@ -64,10 +66,10 @@ public class ThemeFontTableModel extends GDynamicColumnTableModel<FontValue, Obj
} }
private void load() { private void load() {
currentValues = Gui.getAllValues(); currentValues = themeManager.getCurrentValues();
fonts = currentValues.getFonts(); fonts = currentValues.getFonts();
themeValues = Gui.getThemeValues(); themeValues = themeManager.getThemeValues();
defaultValues = Gui.getDefaults(); defaultValues = themeManager.getDefaults();
} }
@Override @Override

View file

@ -26,8 +26,8 @@ import docking.ActionContext;
import docking.action.ActionContextProvider; import docking.action.ActionContextProvider;
import docking.widgets.table.GFilterTable; import docking.widgets.table.GFilterTable;
import docking.widgets.table.GTable; import docking.widgets.table.GTable;
import generic.theme.Gui;
import generic.theme.IconValue; import generic.theme.IconValue;
import generic.theme.ThemeManager;
import ghidra.util.Swing; import ghidra.util.Swing;
/** /**
@ -39,10 +39,12 @@ public class ThemeIconTable extends JPanel implements ActionContextProvider {
private IconValueEditor iconEditor = new IconValueEditor(this::iconValueChanged); private IconValueEditor iconEditor = new IconValueEditor(this::iconValueChanged);
private GTable table; private GTable table;
private GFilterTable<IconValue> filterTable; private GFilterTable<IconValue> filterTable;
private ThemeManager themeManager;
public ThemeIconTable() { public ThemeIconTable(ThemeManager themeManager) {
super(new BorderLayout()); super(new BorderLayout());
iconTableModel = new ThemeIconTableModel(); this.themeManager = themeManager;
iconTableModel = new ThemeIconTableModel(themeManager);
filterTable = new GFilterTable<>(iconTableModel); filterTable = new GFilterTable<>(iconTableModel);
table = filterTable.getTable(); table = filterTable.getTable();
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 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 // run later - don't rock the boat in the middle of a listener callback
Swing.runLater(() -> { Swing.runLater(() -> {
IconValue newValue = (IconValue) event.getNewValue(); 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 currentValues;
private GThemeValueMap themeValues; private GThemeValueMap themeValues;
private GThemeValueMap defaultValues; private GThemeValueMap defaultValues;
private ThemeManager themeManager;
public ThemeIconTableModel() { public ThemeIconTableModel(ThemeManager themeManager) {
super(new ServiceProviderStub()); super(new ServiceProviderStub());
this.themeManager = themeManager;
load(); 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. * Reloads the just the current values shown in the table. Called whenever an icon changes.
*/ */
public void reloadCurrent() { public void reloadCurrent() {
currentValues = Gui.getAllValues(); currentValues = themeManager.getCurrentValues();
icons = currentValues.getIcons(); icons = currentValues.getIcons();
fireTableDataChanged(); fireTableDataChanged();
} }
@ -64,10 +66,10 @@ public class ThemeIconTableModel extends GDynamicColumnTableModel<IconValue, Obj
} }
private void load() { private void load() {
currentValues = Gui.getAllValues(); currentValues = themeManager.getCurrentValues();
icons = currentValues.getIcons(); icons = currentValues.getIcons();
themeValues = Gui.getThemeValues(); themeValues = themeManager.getThemeValues();
defaultValues = Gui.getDefaults(); defaultValues = themeManager.getDefaults();
} }
@Override @Override

View file

@ -43,17 +43,17 @@ public class ThemeUtils {
* overwrites an existing file. * overwrites an existing file.
* @return true if the operation was not cancelled * @return true if the operation was not cancelled
*/ */
public static boolean askToSaveThemeChanges() { public static boolean askToSaveThemeChanges(ThemeManager themeManager) {
if (Gui.hasThemeChanges()) { if (themeManager.hasThemeChanges()) {
int result = OptionDialog.showYesNoCancelDialog(null, "Save Theme Changes?", int result = OptionDialog.showYesNoCancelDialog(null, "Save Theme Changes?",
"You have made changes to the theme.\n Do you want save your changes?"); "You have made changes to the theme.\n Do you want save your changes?");
if (result == OptionDialog.CANCEL_OPTION) { if (result == OptionDialog.CANCEL_OPTION) {
return false; return false;
} }
if (result == OptionDialog.YES_OPTION) { if (result == OptionDialog.YES_OPTION) {
return ThemeUtils.saveThemeChanges(); return ThemeUtils.saveThemeChanges(themeManager);
} }
Gui.reloadApplicationDefaults(); themeManager.reloadApplicationDefaults();
} }
return true; return true;
} }
@ -63,32 +63,33 @@ public class ThemeUtils {
* name and asking to overwrite an existing file. * name and asking to overwrite an existing file.
* @return true if the operation was not cancelled * @return true if the operation was not cancelled
*/ */
public static boolean saveThemeChanges() { public static boolean saveThemeChanges(ThemeManager themeManager) {
GTheme activeTheme = Gui.getActiveTheme(); GTheme activeTheme = themeManager.getActiveTheme();
String name = activeTheme.getName(); String name = activeTheme.getName();
while (!canSaveToName(name)) { while (!canSaveToName(themeManager, name)) {
name = getNameFromUser(name); name = getNameFromUser(name);
if (name == null) { if (name == null) {
return false; return false;
} }
} }
return saveCurrentValues(name); return saveCurrentValues(themeManager, name);
} }
/** /**
* Resets the theme to the default, handling the case where the current theme has changes. * Resets the theme to the default, handling the case where the current theme has changes.
*/ */
public static void resetThemeToDefault() { public static void resetThemeToDefault(ThemeManager themeManager) {
if (askToSaveThemeChanges()) { if (askToSaveThemeChanges(themeManager)) {
Gui.setTheme(Gui.getDefaultTheme()); themeManager.setTheme(themeManager.getDefaultTheme());
} }
} }
/** /**
* Imports a theme. Handles the case where there are existing changes to the current theme. * 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); GhidraFileChooser chooser = new GhidraFileChooser(null);
chooser.setTitle("Choose Theme File"); chooser.setTitle("Choose Theme File");
chooser.setApproveButtonToolTipText("Select File"); chooser.setApproveButtonToolTipText("Select File");
@ -100,14 +101,14 @@ public class ThemeUtils {
if (file == null) { if (file == null) {
return; return;
} }
importTheme(file); importTheme(themeManager, file);
} }
static void importTheme(File themeFile) { static void importTheme(ThemeManager themeManager, File themeFile) {
if (!ThemeUtils.askToSaveThemeChanges()) { if (!ThemeUtils.askToSaveThemeChanges(themeManager)) {
return; return;
} }
GTheme startingTheme = Gui.getActiveTheme(); GTheme startingTheme = themeManager.getActiveTheme();
try { try {
GTheme imported = GTheme.loadTheme(themeFile); GTheme imported = GTheme.loadTheme(themeFile);
// by setting the theme, we can let the normal save handle all the edge cases // 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 // 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 // by going through the usual save mechanism, only values that differ from defaults
// be saved. // be saved.
Gui.setTheme(imported); themeManager.setTheme(imported);
if (!ThemeUtils.saveThemeChanges()) { if (!ThemeUtils.saveThemeChanges(themeManager)) {
Gui.setTheme(startingTheme); themeManager.setTheme(startingTheme);
} }
} }
catch (IOException e) { 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 * Exports a theme, prompting the user to pick an file. Also handles dealing with any
* existing changes to the current theme. * existing changes to the current theme.
* @param themeManager the ThemeManager that actually does the export
*/ */
public static void exportTheme() { public static void exportTheme(ThemeManager themeManager) {
if (!ThemeUtils.askToSaveThemeChanges()) { if (!ThemeUtils.askToSaveThemeChanges(themeManager)) {
return; return;
} }
boolean hasExternalIcons = !Gui.getActiveTheme().getExternalIconFiles().isEmpty(); boolean hasExternalIcons = !themeManager.getActiveTheme().getExternalIconFiles().isEmpty();
String message = String message =
"Export as zip file? (You are not using any external icons so the zip\n" + "Export as zip file? (You are not using any external icons so the zip\n" +
"file would only contain a single theme file.)"; "file would only contain a single theme file.)";
@ -151,16 +153,16 @@ public class ThemeUtils {
} }
boolean exportAsZip = result == OptionDialog.OPTION_ONE; boolean exportAsZip = result == OptionDialog.OPTION_ONE;
ExportThemeDialog dialog = new ExportThemeDialog(exportAsZip); ExportThemeDialog dialog = new ExportThemeDialog(themeManager, exportAsZip);
DockingWindowManager.showDialog(dialog); DockingWindowManager.showDialog(dialog);
} }
/** /**
* Prompts for and deletes a selected theme. * Prompts for and deletes a selected theme.
*/ */
public static void deleteTheme() { public static void deleteTheme(ThemeManager themeManager) {
List<GTheme> savedThemes = new ArrayList<>( 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()) { if (savedThemes.isEmpty()) {
Msg.showInfo(ThemeUtils.class, null, "Delete Theme", "There are no deletable themes"); Msg.showInfo(ThemeUtils.class, null, "Delete Theme", "There are no deletable themes");
return; return;
@ -171,7 +173,7 @@ public class ThemeUtils {
if (selectedTheme == null) { if (selectedTheme == null) {
return; return;
} }
if (Gui.getActiveTheme().equals(selectedTheme)) { if (themeManager.getActiveTheme().equals(selectedTheme)) {
Msg.showWarn(ThemeUtils.class, null, "Delete Failed", Msg.showWarn(ThemeUtils.class, null, "Delete Failed",
"Can't delete the current theme."); "Can't delete the current theme.");
return; return;
@ -180,7 +182,7 @@ public class ThemeUtils {
int result = OptionDialog.showYesNoDialog(null, "Delete Theme: " + fileTheme.getName(), int result = OptionDialog.showYesNoDialog(null, "Delete Theme: " + fileTheme.getName(),
"Are you sure you want to delete theme " + fileTheme.getName()); "Are you sure you want to delete theme " + fileTheme.getName());
if (result == OptionDialog.YES_OPTION) { if (result == OptionDialog.YES_OPTION) {
Gui.deleteTheme(fileTheme); themeManager.deleteTheme(fileTheme);
} }
} }
@ -190,8 +192,8 @@ public class ThemeUtils {
return inputDialog.getValue(); return inputDialog.getValue();
} }
private static boolean canSaveToName(String name) { private static boolean canSaveToName(ThemeManager themeManager, String name) {
GTheme existing = Gui.getTheme(name); GTheme existing = themeManager.getTheme(name);
// if no theme exists with that name, then we are save to save it // if no theme exists with that name, then we are save to save it
if (existing == null) { if (existing == null) {
return true; return true;
@ -210,17 +212,17 @@ public class ThemeUtils {
return result == OptionDialog.YES_OPTION; return result == OptionDialog.YES_OPTION;
} }
private static boolean saveCurrentValues(String themeName) { private static boolean saveCurrentValues(ThemeManager themeManager, String themeName) {
GTheme activeTheme = Gui.getActiveTheme(); GTheme activeTheme = themeManager.getActiveTheme();
File file = getSaveFile(themeName); File file = getSaveFile(themeName);
GTheme newTheme = new GTheme(file, themeName, activeTheme.getLookAndFeelType(), GTheme newTheme = new GTheme(file, themeName, activeTheme.getLookAndFeelType(),
activeTheme.useDarkDefaults()); activeTheme.useDarkDefaults());
newTheme.load(Gui.getNonDefaultValues()); newTheme.load(themeManager.getNonDefaultValues());
try { try {
newTheme.save(); newTheme.save();
Gui.addTheme(newTheme); themeManager.addTheme(newTheme);
Gui.setTheme(newTheme); themeManager.setTheme(newTheme);
} }
catch (IOException e) { catch (IOException e) {
Msg.showError(ThemeUtils.class, null, "I/O Error", 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.ChangeEvent;
import javax.swing.event.ChangeListener; import javax.swing.event.ChangeListener;
import generic.theme.Gui; import ghidra.docking.util.LookAndFeelUtils;
import resources.ResourceManager; 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 // Mac OSX LNF doesn't give us rollover callbacks, so we have to add a mouse listener to
// do the work // do the work
if (Gui.isUsingAquaUI(getUI())) { if (LookAndFeelUtils.isUsingAquaUI(getUI())) {
addMouseListener(new MouseAdapter() { addMouseListener(new MouseAdapter() {
@Override @Override
public void mouseEntered(MouseEvent e) { public void mouseEntered(MouseEvent e) {

View file

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

View file

@ -22,12 +22,14 @@ import java.text.DecimalFormat;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import javax.swing.*; import javax.swing.JTable;
import javax.swing.SwingConstants;
import javax.swing.table.TableCellRenderer; import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableModel; import javax.swing.table.TableModel;
import docking.widgets.AbstractGCellRenderer; import docking.widgets.AbstractGCellRenderer;
import generic.theme.GColor; import generic.theme.GColor;
import generic.theme.Gui;
import ghidra.docking.settings.*; import ghidra.docking.settings.*;
import ghidra.util.*; import ghidra.util.*;
import ghidra.util.exception.AssertException; import ghidra.util.exception.AssertException;
@ -56,7 +58,13 @@ public class GTableCellRenderer extends AbstractGCellRenderer implements TableCe
* Constructs a new GTableCellRenderer. * Constructs a new GTableCellRenderer.
*/ */
public 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: " + "Using a GTableCellRenderer in a non-GTable table. (Model class: " +
table.getModel().getClass().getName() + ")"); table.getModel().getClass().getName() + ")");
} }
// check if LookAndFeel has changed
if (UIManager.getUI(this) != getUI()) {
updateUI();
}
GTable gTable = (GTable) table; GTable gTable = (GTable) table;
GTableCellRenderingData data = gTable.getRenderingData(column); GTableCellRenderingData data = gTable.getRenderingData(column);
Object rowObject = null; Object rowObject = null;

View file

@ -45,6 +45,7 @@ import docking.widgets.tree.internal.*;
import docking.widgets.tree.support.*; import docking.widgets.tree.support.*;
import docking.widgets.tree.support.GTreeSelectionEvent.EventOrigin; import docking.widgets.tree.support.GTreeSelectionEvent.EventOrigin;
import docking.widgets.tree.tasks.*; import docking.widgets.tree.tasks.*;
import generic.theme.*;
import generic.timer.ExpiringSwingTimer; import generic.timer.ExpiringSwingTimer;
import ghidra.util.*; import ghidra.util.*;
import ghidra.util.exception.CancelledException; 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. * 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 AutoScrollTree tree;
private GTreeModel model; private GTreeModel model;
@ -134,6 +135,7 @@ public class GTree extends JPanel implements BusyListener {
uniquePreferenceKey)); uniquePreferenceKey));
filterUpdateManager = new SwingUpdateManager(1000, 30000, () -> updateModelFilter()); filterUpdateManager = new SwingUpdateManager(1000, 30000, () -> updateModelFilter());
Gui.addThemeListener(this);
} }
/** /**
@ -146,6 +148,13 @@ public class GTree extends JPanel implements BusyListener {
threadLocalMonitor.set(monitor); 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 * 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 * 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) { public AutoScrollTree(TreeModel model) {
super(model); super(model);
setBackground(BACKGROUND);
scroller = new AutoscrollAdapter(this, 5); scroller = new AutoscrollAdapter(this, 5);
setRowHeight(-1);// variable size rows 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 Color VALID_DROP_TARGET_COLOR = new GColor("color.bg.tree.drag");
private static final int DEFAULT_MIN_ICON_WIDTH = 22; 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 Object dropTarget;
private boolean paintDropTarget; private boolean paintDropTarget;
@ -41,6 +43,8 @@ public class GTreeRenderer extends DefaultTreeCellRenderer implements GComponent
public GTreeRenderer() { public GTreeRenderer() {
setHTMLRenderingEnabled(false); setHTMLRenderingEnabled(false);
setBackgroundNonSelectionColor(BACKGROUND_UNSELECTED);
setBackgroundSelectionColor(BACKGROUND_SELECTED);
} }
@Override @Override

View file

@ -21,10 +21,12 @@ import java.util.Iterator;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
import javax.swing.UIDefaults; import javax.swing.*;
import javax.swing.UIManager; import javax.swing.plaf.ComponentUI;
import docking.framework.ApplicationInformationDisplayFactory; import docking.framework.ApplicationInformationDisplayFactory;
import generic.theme.LafType;
import generic.theme.ThemeManager;
import ghidra.framework.preferences.Preferences; import ghidra.framework.preferences.Preferences;
import ghidra.util.SystemUtilities; 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

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 B

After

Width:  |  Height:  |  Size: 692 B

Before After
Before After

View file

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

View file

@ -28,7 +28,7 @@ icon.expand.all = expand_all.png
icon.configure.filter = exec.png icon.configure.filter = exec.png
icon.clear = erase16.png icon.clear = erase16.png
icon.delete = icon.error icon.delete = edit-delete.png
icon.error = emblem-important.png icon.error = emblem-important.png
icon.home = go-home.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 @Override
public void installValue() { public void installValue(ThemeManager themeManager) {
Gui.setColor(this); themeManager.setColor(this);
} }
} }

View file

@ -232,8 +232,8 @@ public class FontValue extends ThemeValue<Font> {
} }
@Override @Override
public void installValue() { public void installValue(ThemeManager themeManager) {
Gui.setFont(this); 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 * @param id the id used to lookup the current value for this color
*/ */
public GColor(String id) { 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); super(0x808080);
this.id = id; this.id = id;
delegate = Gui.getColor(id, validate); delegate = Gui.getColor(id);
inUseColors.add(this); inUseColors.add(this);
} }
@ -230,9 +219,11 @@ public class GColor extends Color {
/** /**
* Reloads the delegate. * Reloads the delegate.
* @param currentValues the map of current theme values
*/ */
public void refresh() { public void refresh(GThemeValueMap currentValues) {
Color color = Gui.getColor(id, false); ColorValue value = currentValues.getColor(id);
Color color = value == null ? null : value.get(currentValues);
if (color != null) { if (color != null) {
if (alpha != null) { if (alpha != null) {
delegate = new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha); 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 * Static method for notifying all the existing GColors that colors have changed and they
* should reload their cached indirect color. * 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()) { 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 * Static method for notifying all the existing GIcon that icons have changed and they
* should reload their cached indirect icon. * 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()) { 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 * @param id the id used to lookup the current value for this color
*/ */
public GIcon(String id) { 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; this.id = id;
delegate = Gui.getIcon(id, validate); delegate = Gui.getIcon(id);
inUseIcons.add(this); inUseIcons.add(this);
} }
@ -134,14 +124,25 @@ public class GIcon implements Icon {
/** /**
* Reloads the delegate. * Reloads the delegate.
* @param currentValues the map of current theme values
*/ */
public void refresh() { public void refresh(GThemeValueMap currentValues) {
Icon icon = Gui.getIcon(id, false); IconValue value = currentValues.getIcon(id);
Icon icon = value == null ? null : value.get(currentValues);
if (icon != null) { if (icon != null) {
delegate = icon; 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 @Override
public int hashCode() { public int hashCode() {
return id.hashCode(); return id.hashCode();

View file

@ -217,43 +217,6 @@ public class GTheme extends GThemeValueMap {
writer.writeThemeToFile(file); 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 * 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. * a theme file and optionally a set of icon files.

View file

@ -16,26 +16,9 @@
package generic.theme; package generic.theme;
import java.awt.*; import java.awt.*;
import java.io.File;
import java.util.*;
import java.util.List;
import javax.swing.*; import javax.swing.Icon;
import javax.swing.plaf.ComponentUI; import javax.swing.LookAndFeel;
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;
/** /**
* Provides a static set of methods for globally managing application themes and their values. * 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 class Gui {
public static final String BACKGROUND_KEY = "color.bg.text"; // Start with an StubThemeManager so that simple tests can operate without having
// to initialize the theme system. Applications and integration tests will
private static GTheme activeTheme = getDefaultTheme(); // called ThemeManager.initialize() which will replace this with a fully initialized version.
private static Set<GTheme> allThemes = null; private static ThemeManager themeManager = new StubThemeManager();
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);
private Gui() { private Gui() {
// static utils class, can't construct // 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 * 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. * 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. * @return the current {@link Font} associated with the given id.
*/ */
public static Font getFont(String id) { public static Font getFont(String id) {
Font font = getFont(id, true); return themeManager.getFont(id);
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);
} }
/** /**
@ -356,221 +62,7 @@ public class Gui {
* @return the {@link Color} registered for the given id. * @return the {@link Color} registered for the given id.
*/ */
public static Color getColor(String id) { public static Color getColor(String id) {
return getColor(id, true); return themeManager.getColor(id);
}
/**
* 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;
} }
/** /**
@ -578,7 +70,7 @@ public class Gui {
* @param listener the listener to be notified * @param listener the listener to be notified
*/ */
public static void addThemeListener(ThemeListener listener) { 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 * @param listener the listener to be removed
*/ */
public static void removeThemeListener(ThemeListener listener) { public static void removeThemeListener(ThemeListener listener) {
themeListeners.add(listener); themeManager.removeThemeListener(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);
} }
/** /**
@ -644,7 +89,7 @@ public class Gui {
* @return the actual icon registered for the given id * @return the actual icon registered for the given id
*/ */
public static Icon getIcon(String 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 * @return true if an color for the given Id has been defined
*/ */
public static boolean hasColor(String id) { 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 * @return true if an font for the given Id has been defined
*/ */
public static boolean hasFont(String id) { 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 * @return true if an icon for the given Id has been defined
*/ */
public static boolean hasIcon(String id) { public static boolean hasIcon(String id) {
return currentValues.containsIcon(id); return themeManager.hasIcon(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);
} }
/** /**
@ -700,7 +125,7 @@ public class Gui {
* @return a darker version of the given color or brighter if the current theme is dark * @return a darker version of the given color or brighter if the current theme is dark
*/ */
public static Color darker(Color color) { public static Color darker(Color color) {
if (activeTheme.useDarkDefaults()) { if (isDarkTheme()) {
return color.brighter(); return color.brighter();
} }
return color.darker(); 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 * @return a brighter version of the given color or darker if the current theme is dark
*/ */
public static Color brighter(Color color) { public static Color brighter(Color color) {
if (activeTheme.useDarkDefaults()) { if (isDarkTheme()) {
return color.darker(); return color.darker();
} }
return color.brighter(); return color.brighter();
@ -725,137 +150,19 @@ public class Gui {
* @param fontId the id of the font to register with the given component * @param fontId the id of the font to register with the given component
*/ */
public static void registerFont(Component component, String fontId) { 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()); * Returns true if the active theme is using dark defaults
UIManager.installLookAndFeel(LafType.FLAT_DARK.getName(), FlatDarkLaf.class.getName()); * @return true if the active theme is using dark defaults
*/
public static boolean isDarkTheme() {
return themeManager.isDarkTheme();
} }
private static void loadThemeDefaults() { static void setThemeManager(ThemeManager manager) {
themeFileLoader.loadThemeDefaultFiles(); themeManager = manager;
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;
} }
} }

View file

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

View file

@ -85,34 +85,36 @@ public enum LafType {
/** /**
* Returns a LookAndFeelManager that can install and update the {@link LookAndFeel} associated * Returns a LookAndFeelManager that can install and update the {@link LookAndFeel} associated
* with this LafType. * with this LafType.
* @param themeManager The application ThemeManager
* @return a LookAndFeelManager that can install and update the {@link LookAndFeel} associated * @return a LookAndFeelManager that can install and update the {@link LookAndFeel} associated
* with this LafType. * with this LafType.
*/ */
public LookAndFeelManager getLookAndFeelManager() { public LookAndFeelManager getLookAndFeelManager(ApplicationThemeManager themeManager) {
return getManager(this); return createManager(this, themeManager);
} }
private static LookAndFeelManager getManager(LafType lookAndFeel) { private static LookAndFeelManager createManager(LafType type,
switch (lookAndFeel) { ApplicationThemeManager themeManager) {
switch (type) {
case MAC: case MAC:
return new MacLookAndFeelManager(); return new MacLookAndFeelManager(themeManager);
case METAL: case METAL:
return new MetalLookAndFeelManager(); return new MetalLookAndFeelManager(themeManager);
case WINDOWS: case WINDOWS:
return new WindowsLookAndFeelManager(); return new WindowsLookAndFeelManager(themeManager);
case WINDOWS_CLASSIC: case WINDOWS_CLASSIC:
return new WindowsClassicLookAndFeelManager(); return new WindowsClassicLookAndFeelManager(themeManager);
case GTK: case GTK:
return new GtkLookAndFeelManager(); return new GtkLookAndFeelManager(themeManager);
case MOTIF: case MOTIF:
return new MotifLookAndFeelManager(); return new MotifLookAndFeelManager(themeManager);
case NIMBUS: case NIMBUS:
return new NimbusLookAndFeelManager(); return new NimbusLookAndFeelManager(themeManager);
case FLAT_DARK: case FLAT_DARK:
case FLAT_LIGHT: case FLAT_LIGHT:
return new FlatLookAndFeelManager(lookAndFeel); return new FlatLookAndFeelManager(type, themeManager);
default: 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 * Reads and writes current theme info to preferences
*/ */
public class ThemePreferenceManager { public class ThemePreferences {
private static final String THEME_PREFFERENCE_KEY = "Theme"; 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 * @return the last theme used (stored in preferences) or the default theme if not stored
* in preferences * in preferences
*/ */
public GTheme getTheme() { public GTheme load() {
String themeId = Preferences.getProperty(THEME_PREFFERENCE_KEY, "Default", true); String themeId = Preferences.getProperty(THEME_PREFFERENCE_KEY, "Default", true);
if (themeId.startsWith(GTheme.FILE_PREFIX)) { if (themeId.startsWith(GTheme.FILE_PREFIX)) {
String filename = themeId.substring(GTheme.FILE_PREFIX.length()); String filename = themeId.substring(GTheme.FILE_PREFIX.length());
@ -55,14 +55,14 @@ public class ThemePreferenceManager {
"Can't find or instantiate class: " + className, e); "Can't find or instantiate class: " + className, e);
} }
} }
return Gui.getDefaultTheme(); return ThemeManager.getDefaultTheme();
} }
/** /**
* Saves the current theme choice to {@link Preferences}. * Saves the current theme choice to {@link Preferences}.
* @param theme the theme to remember in {@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.setProperty(THEME_PREFFERENCE_KEY, theme.getThemeLocater());
Preferences.store(); 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 * 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 * @param values the {@link GThemeValueMap} used to resolve references if this
* instance doesn't have an actual value. * instance doesn't have an actual value.
* @return the T value for this instance, following references as needed. * @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<>(); Set<String> visitedKeys = new HashSet<>();
visitedKeys.add(id); visitedKeys.add(id);
ThemeValue<T> parent = getReferredValue(values, referenceId); ThemeValue<T> referred = getReferredValue(values, referenceId);
// loop resolving indirect references // loop resolving indirect references
while (parent != null) { while (referred != null) {
if (parent.value != null) { if (referred.value != null) {
return parent.value; return referred.value;
} }
visitedKeys.add(parent.id); visitedKeys.add(referred.id);
if (visitedKeys.contains(parent.referenceId)) { if (visitedKeys.contains(referred.referenceId)) {
Msg.warn(this, "Theme value reference loop detected for key: " + id); 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); 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. * Returns true if this ThemeValue derives its value from the given ancestorId.
* @param ancestorId the id to test if this Theme value inherits from * @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 * 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 javax.swing.UIManager;
import generic.theme.ColorValue; import generic.theme.*;
import generic.theme.LafType;
public class FlatLookAndFeelManager extends LookAndFeelManager { public class FlatLookAndFeelManager extends LookAndFeelManager {
public FlatLookAndFeelManager(LafType laf) { public FlatLookAndFeelManager(LafType laf, ApplicationThemeManager themeManager) {
super(laf); super(laf, themeManager);
// establish system color to LookAndFeel colors // establish system color to LookAndFeel colors
systemToLafMap.addColor(new ColorValue(SYSTEM_WIDGET_BACKGROUND_COLOR_ID, "text")); 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.awt.Font;
import java.util.List; import java.util.List;
import javax.swing.Icon; import javax.swing.*;
import javax.swing.UIDefaults;
import javax.swing.plaf.FontUIResource; import javax.swing.plaf.FontUIResource;
import javax.swing.plaf.nimbus.NimbusLookAndFeel; import javax.swing.plaf.nimbus.NimbusLookAndFeel;
import generic.theme.*; import generic.theme.*;
/** /**
* Extends the NimbusLookAndFeel to intercept the {@link #getDefaults()}. To get Nimbus * Extends the {@link NimbusLookAndFeel} to intercept the {@link #getDefaults()}. Nimbus does
* to use our indirect values, we have to get in early. * 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 { public class GNimbusLookAndFeel extends NimbusLookAndFeel {
private ApplicationThemeManager themeManager;
GNimbusLookAndFeel(ApplicationThemeManager themeManager) {
this.themeManager = themeManager;
}
@Override @Override
public UIDefaults getDefaults() { public UIDefaults getDefaults() {
@ -40,13 +54,13 @@ public class GNimbusLookAndFeel extends NimbusLookAndFeel {
// replace all colors with GColors // replace all colors with GColors
for (ColorValue colorValue : javaDefaults.getColors()) { for (ColorValue colorValue : javaDefaults.getColors()) {
String id = colorValue.getId(); 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 // put fonts back into defaults in case they have been changed by the current theme
for (FontValue fontValue : javaDefaults.getFonts()) { for (FontValue fontValue : javaDefaults.getFonts()) {
String id = fontValue.getId(); String id = fontValue.getId();
Font font = Gui.getFont(id); Font font = themeManager.getFont(id);
defaults.put(id, new FontUIResource(font)); defaults.put(id, new FontUIResource(font));
} }
@ -55,13 +69,12 @@ public class GNimbusLookAndFeel extends NimbusLookAndFeel {
String id = iconValue.getId(); String id = iconValue.getId();
// because some icons are weird, put raw icons into defaults, only use GIcons for // because some icons are weird, put raw icons into defaults, only use GIcons for
// setting Icons explicitly on components // setting Icons explicitly on components
Icon icon = Gui.getIcon(id, true); Icon icon = themeManager.getIcon(id);
defaults.put(id, icon); defaults.put(id, icon);
} }
defaults.put("Label.textForeground", Gui.getGColorUiResource("Label.foreground")); defaults.put("Label.textForeground", themeManager.getGColorUiResource("Label.foreground"));
GColor.refreshAll(); themeManager.refreshGThemeValues();
GIcon.refreshAll();
return defaults; return defaults;
} }
@ -90,7 +103,7 @@ public class GNimbusLookAndFeel extends NimbusLookAndFeel {
} }
// need to set javaDefalts now to trigger building currentValues so the when // need to set javaDefalts now to trigger building currentValues so the when
// we create GColors below, they can be resolved. // we create GColors below, they can be resolved.
Gui.setJavaDefaults(javaDefaults); themeManager.setJavaDefaults(javaDefaults);
return javaDefaults; return javaDefaults;
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -15,16 +15,15 @@
*/ */
package generic.theme.laf; package generic.theme.laf;
import generic.theme.ColorValue; import generic.theme.*;
import generic.theme.LafType;
/** /**
* Motif {@link LookAndFeelManager}. Specialized so that it can return the Motif installer * Motif {@link LookAndFeelManager}. Specialized so that it can return the Motif installer
*/ */
public class MotifLookAndFeelManager extends LookAndFeelManager { public class MotifLookAndFeelManager extends LookAndFeelManager {
public MotifLookAndFeelManager() { public MotifLookAndFeelManager(ApplicationThemeManager themeManager) {
super(LafType.MOTIF); super(LafType.MOTIF, themeManager);
// establish system color to LookAndFeel colors // establish system color to LookAndFeel colors
systemToLafMap.addColor(new ColorValue(SYSTEM_APP_BACKGROUND_COLOR_ID, "control")); systemToLafMap.addColor(new ColorValue(SYSTEM_APP_BACKGROUND_COLOR_ID, "control"));
systemToLafMap.addColor(new ColorValue(SYSTEM_WIDGET_BACKGROUND_COLOR_ID, "window")); 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 class NimbusLookAndFeelManager extends LookAndFeelManager {
public NimbusLookAndFeelManager() { public NimbusLookAndFeelManager(ApplicationThemeManager themeManager) {
super(LafType.NIMBUS); super(LafType.NIMBUS, themeManager);
// establish system color specific to Nimbus // establish system color specific to Nimbus
systemToLafMap.addColor(new ColorValue(SYSTEM_BORDER_COLOR_ID, "nimbusBorder")); systemToLafMap.addColor(new ColorValue(SYSTEM_BORDER_COLOR_ID, "nimbusBorder"));
@ -39,8 +39,7 @@ public class NimbusLookAndFeelManager extends LookAndFeelManager {
@Override @Override
public void resetAll(GThemeValueMap javaDefaults) { public void resetAll(GThemeValueMap javaDefaults) {
GColor.refreshAll(); themeManager.refreshGThemeValues();
GIcon.refreshAll();
reinstallNimubus(); reinstallNimubus();
} }
@ -58,16 +57,16 @@ public class NimbusLookAndFeelManager extends LookAndFeelManager {
if (!affectedJavaIds.isEmpty()) { if (!affectedJavaIds.isEmpty()) {
reinstallNimubus(); reinstallNimubus();
} }
GIcon.refreshAll(); themeManager.refreshGThemeValues();
repaintAll(); repaintAll();
} }
private void reinstallNimubus() { private void reinstallNimubus() {
try { try {
UIManager.setLookAndFeel(new GNimbusLookAndFeel() { UIManager.setLookAndFeel(new GNimbusLookAndFeel(themeManager) {
@Override @Override
protected GThemeValueMap extractJavaDefaults(UIDefaults defaults) { protected GThemeValueMap extractJavaDefaults(UIDefaults defaults) {
return Gui.getJavaDefaults(); return themeManager.getJavaDefaults();
} }
}); });
} }
@ -79,13 +78,13 @@ public class NimbusLookAndFeelManager extends LookAndFeelManager {
@Override @Override
protected void doInstallLookAndFeel() throws UnsupportedLookAndFeelException { protected void doInstallLookAndFeel() throws UnsupportedLookAndFeelException {
UIManager.setLookAndFeel(new GNimbusLookAndFeel()); UIManager.setLookAndFeel(new GNimbusLookAndFeel(themeManager));
} }
@Override @Override
protected GThemeValueMap extractJavaDefaults() { protected GThemeValueMap extractJavaDefaults() {
// The GNimbusLookAndFeel already extracted the java defaults and installed them in the Gui // The GNimbusLookAndFeel already extracted the java defaults and installed them in the Gui
return Gui.getJavaDefaults(); return themeManager.getJavaDefaults();
} }
@Override @Override

View file

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

View file

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

View file

@ -109,6 +109,13 @@ public interface Options {
* Registers an option with a description, help location, and a default value without specifying * 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 * 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 optionName the name of the option being registered.
* @param defaultValue the defaultValue for the option. The default value must not be * @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 * 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 * 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. * 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 optionName the name of the option being registered.
* @param type the OptionType for this options. * @param type the OptionType for this options.
* @param defaultValue the defaultValue for the option. In this version of the method, the default * @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 * 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. * 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 optionName the name of the option being registered.
* @param type the OptionType for this options. * @param type the OptionType for this options.
* @param defaultValue the defaultValue for the option. In this version of the method, the default * @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 java.awt.Color;
import generic.theme.GColor; import generic.theme.*;
import generic.theme.Gui;
import ghidra.util.HelpLocation; import ghidra.util.HelpLocation;
import ghidra.util.Msg; import ghidra.util.Msg;
@ -55,17 +54,17 @@ public class ThemeColorOption extends Option {
@Override @Override
public void doSetCurrentValue(Object value) { public void doSetCurrentValue(Object value) {
Gui.setColor(colorId, (Color) value); ThemeManager.getInstance().setColor(colorId, (Color) value);
} }
@Override @Override
public boolean isDefault() { public boolean isDefault() {
return !Gui.isChangedColor(colorId); return !ThemeManager.getInstance().isChangedColor(colorId);
} }
@Override @Override
public void restoreDefault() { 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 java.awt.Font;
import generic.theme.Gui; import generic.theme.Gui;
import generic.theme.ThemeManager;
import ghidra.util.HelpLocation; import ghidra.util.HelpLocation;
import ghidra.util.Msg; import ghidra.util.Msg;
@ -54,17 +55,17 @@ public class ThemeFontOption extends Option {
@Override @Override
public void doSetCurrentValue(Object value) { public void doSetCurrentValue(Object value) {
Gui.setFont(fontId, (Font) value); ThemeManager.getInstance().setFont(fontId, (Font) value);
} }
@Override @Override
public boolean isDefault() { public boolean isDefault() {
return !Gui.isChangedFont(fontId); return !ThemeManager.getInstance().isChangedFont(fontId);
} }
@Override @Override
public void restoreDefault() { 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.Icon;
import javax.swing.ImageIcon; import javax.swing.ImageIcon;
import generic.theme.GIcon;
import generic.util.image.ImageUtils; import generic.util.image.ImageUtils;
import resources.ResourceManager; import resources.ResourceManager;
@ -32,6 +33,7 @@ import resources.ResourceManager;
public class DerivedImageIcon extends LazyImageIcon { public class DerivedImageIcon extends LazyImageIcon {
private Icon sourceIcon; private Icon sourceIcon;
private Image sourceImage; private Image sourceImage;
private Icon cachedDelegate;
/** /**
* Constructor for deriving from an icon * Constructor for deriving from an icon
@ -56,6 +58,16 @@ public class DerivedImageIcon extends LazyImageIcon {
return sourceIcon; return sourceIcon;
} }
protected boolean sourceIconChanged() {
if (sourceIcon instanceof GIcon gIcon) {
if (cachedDelegate != gIcon.getDelegate()) {
cachedDelegate = gIcon.getDelegate();
return true;
}
}
return false;
}
protected ImageIcon createImageIcon() { protected ImageIcon createImageIcon() {
Image image = createImage(); Image image = createImage();
String imageName = getFilename(); String imageName = getFilename();

View file

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

View file

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

View file

@ -34,7 +34,7 @@ import generic.theme.builtin.*;
import resources.ResourceManager; import resources.ResourceManager;
import resources.icons.UrlImageIcon; import resources.icons.UrlImageIcon;
public class GuiTest { public class ThemeManagerTest {
private Font FONT = new Font("Dialog", Font.PLAIN, 13); private Font FONT = new Font("Dialog", Font.PLAIN, 13);
private Font SMALL_FONT = new Font("Dialog", Font.PLAIN, 4); 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 NIMBUS_THEME = new NimbusTheme();
private GTheme WINDOWS_THEME = new WindowsTheme(); private GTheme WINDOWS_THEME = new WindowsTheme();
private GTheme MAC_THEME = new MacTheme(); private GTheme MAC_THEME = new MacTheme();
private ThemeManager themeManager;
@Before @Before
public void setUp() { public void setUp() {
themes = new HashSet<>(); themes = new HashSet<>();
themes.add(METAL_THEME); themes.add(METAL_THEME);
themes.add(NIMBUS_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.bg", BLACK));
darkDefaultValues.addColor(new ColorValue("color.test.fg", BLUE)); 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 @Test
@ -110,12 +77,11 @@ public class GuiTest {
GColor gColor = new GColor("color.test.bg"); GColor gColor = new GColor("color.test.bg");
assertColor(WHITE, gColor); assertColor(WHITE, gColor);
Gui.setTheme(new GTheme("Test", LafType.FLAT_DARK, true)); themeManager.setTheme(new GTheme("Test", LafType.FLAT_DARK, true));
assertEquals(BLACK, gColor); assertEquals(BLACK, gColor);
Gui.setTheme(new GTheme("Test2")); themeManager.setTheme(new GTheme("Test2"));
assertEquals(WHITE, gColor); assertEquals(WHITE, gColor);
} }
@Test @Test
@ -126,25 +92,25 @@ public class GuiTest {
theme.setColor("color.test.bg", GREEN); theme.setColor("color.test.bg", GREEN);
assertColor(WHITE, gColor); assertColor(WHITE, gColor);
Gui.setTheme(theme); themeManager.setTheme(theme);
assertEquals(GREEN, gColor); assertEquals(GREEN, gColor);
Gui.setTheme(new GTheme("Test2")); themeManager.setTheme(new GTheme("Test2"));
assertEquals(WHITE, gColor); assertEquals(WHITE, gColor);
} }
@Test @Test
public void testThemeFontOverride() { public void testThemeFontOverride() {
assertEquals(FONT, Gui.getFont("font.test.foo")); assertEquals(FONT, themeManager.getFont("font.test.foo"));
GTheme theme = new GTheme("Test"); GTheme theme = new GTheme("Test");
theme.setFont("font.test.foo", SMALL_FONT); 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")); themeManager.setTheme(new GTheme("Test2"));
assertEquals(FONT, Gui.getFont("font.test.foo")); assertEquals(FONT, themeManager.getFont("font.test.foo"));
} }
@Test @Test
@ -155,10 +121,10 @@ public class GuiTest {
theme.setIcon("icon.test.foo", ICON2); theme.setIcon("icon.test.foo", ICON2);
assertIcon(ICON1, gIcon); assertIcon(ICON1, gIcon);
Gui.setTheme(theme); themeManager.setTheme(theme);
assertIcon(ICON2, gIcon); assertIcon(ICON2, gIcon);
Gui.setTheme(new GTheme("Test2")); themeManager.setTheme(new GTheme("Test2"));
assertIcon(ICON1, gIcon); assertIcon(ICON1, gIcon);
} }
@ -168,7 +134,7 @@ public class GuiTest {
assertColor(WHITE, gColor); assertColor(WHITE, gColor);
defaultValues.addColor(new ColorValue("color.test.bg", YELLOW)); defaultValues.addColor(new ColorValue("color.test.bg", YELLOW));
Gui.reloadApplicationDefaults(); themeManager.reloadApplicationDefaults();
assertEquals(YELLOW, gColor); assertEquals(YELLOW, gColor);
} }
@ -177,50 +143,50 @@ public class GuiTest {
GColor gColor = new GColor("color.test.bg"); GColor gColor = new GColor("color.test.bg");
assertColor(WHITE, gColor); assertColor(WHITE, gColor);
Gui.setColor("color.test.bg", PURPLE); themeManager.setColor("color.test.bg", PURPLE);
assertColor(PURPLE, gColor); assertColor(PURPLE, gColor);
Gui.restoreThemeValues(); themeManager.restoreThemeValues();
assertEquals(WHITE, gColor); assertEquals(WHITE, gColor);
} }
@Test @Test
public void testGetAllThemes() { public void testGetAllThemes() {
assertEquals(themes, Gui.getAllThemes()); assertEquals(themes, themeManager.getAllThemes());
} }
@Test @Test
public void testAddTheme() { public void testAddTheme() {
GTheme newTheme = new GTheme("Test"); GTheme newTheme = new GTheme("Test");
Set<GTheme> allThemes = Gui.getAllThemes(); Set<GTheme> allThemes = themeManager.getAllThemes();
assertEquals(themes.size(), allThemes.size()); assertEquals(themes.size(), allThemes.size());
assertFalse(allThemes.contains(newTheme)); assertFalse(allThemes.contains(newTheme));
Gui.addTheme(newTheme); themeManager.addTheme(newTheme);
allThemes = Gui.getAllThemes(); allThemes = themeManager.getAllThemes();
assertTrue(allThemes.contains(newTheme)); assertTrue(allThemes.contains(newTheme));
} }
@Test @Test
public void testDeleteTheme() { public void testDeleteTheme() {
GTheme newTheme = new GTheme("Test"); GTheme newTheme = new GTheme("Test");
Set<GTheme> allThemes = Gui.getAllThemes(); Set<GTheme> allThemes = themeManager.getAllThemes();
assertFalse(allThemes.contains(newTheme)); assertFalse(allThemes.contains(newTheme));
Gui.addTheme(newTheme); themeManager.addTheme(newTheme);
allThemes = Gui.getAllThemes(); allThemes = themeManager.getAllThemes();
assertTrue(allThemes.contains(newTheme)); assertTrue(allThemes.contains(newTheme));
Gui.deleteTheme(newTheme); themeManager.deleteTheme(newTheme);
allThemes = Gui.getAllThemes(); allThemes = themeManager.getAllThemes();
assertFalse(allThemes.contains(newTheme)); assertFalse(allThemes.contains(newTheme));
} }
@Test @Test
public void testGetSupportedThemes() { 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 // since we put mac specific and windows specific themes, they can't all be here
// regardless of the current platform // regardless of the current platform
assertTrue(supportedThemes.size() < themes.size()); assertTrue(supportedThemes.size() < themes.size());
@ -231,31 +197,31 @@ public class GuiTest {
@Test @Test
public void testGetLookAndFeelType() { public void testGetLookAndFeelType() {
LafType lookAndFeelType = Gui.getLookAndFeelType(); LafType lookAndFeelType = themeManager.getLookAndFeelType();
// in the test setup, we defaulted to the MetalLookAndFeel // in the test setup, we defaulted to the MetalLookAndFeel
assertEquals(LafType.METAL, lookAndFeelType); assertEquals(LafType.METAL, lookAndFeelType);
} }
@Test @Test
public void testGetActiveTheme() { public void testGetActiveTheme() {
GTheme activeTheme = Gui.getActiveTheme(); GTheme activeTheme = themeManager.getActiveTheme();
assertEquals(METAL_THEME, activeTheme); assertEquals(METAL_THEME, activeTheme);
} }
@Test @Test
public void testGetThemeByName() { public void testGetThemeByName() {
GTheme theme = Gui.getTheme("Nimbus Theme"); GTheme theme = themeManager.getTheme("Nimbus Theme");
assertEquals(NIMBUS_THEME, theme); assertEquals(NIMBUS_THEME, theme);
} }
@Test @Test
public void testGetAllValues() { public void testGetAllValues() {
GThemeValueMap allValues = Gui.getAllValues(); GThemeValueMap allValues = themeManager.getCurrentValues();
assertEquals(WHITE, allValues.getColor("color.test.bg").getRawValue()); 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()); assertEquals(PURPLE, allValues.getColor("color.test.bg").getRawValue());
} }
@ -263,17 +229,17 @@ public class GuiTest {
@Test @Test
public void testGetNonDefaultValues() { public void testGetNonDefaultValues() {
// should be empty if we haven't changed any themeValues // should be empty if we haven't changed any themeValues
GThemeValueMap nonDefaultValues = Gui.getNonDefaultValues(); GThemeValueMap nonDefaultValues = themeManager.getNonDefaultValues();
assertTrue(nonDefaultValues.isEmpty()); assertTrue(nonDefaultValues.isEmpty());
// change some values and see that they show up in the nonDefaultValues // change some values and see that they show up in the nonDefaultValues
Gui.setColor("color.test.bg", RED); themeManager.setColor("color.test.bg", RED);
Gui.setFont("font.test.foo", SMALL_FONT); themeManager.setFont("font.test.foo", SMALL_FONT);
Gui.setIcon("icon.test.foo", ICON2); themeManager.setIcon("icon.test.foo", ICON2);
// also add in a totally new value // 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(4, nonDefaultValues.size());
assertEquals(RED, nonDefaultValues.getColor("color.test.bg").getRawValue()); assertEquals(RED, nonDefaultValues.getColor("color.test.bg").getRawValue());
assertEquals(GREEN, nonDefaultValues.getColor("color.test.xxx").getRawValue()); assertEquals(GREEN, nonDefaultValues.getColor("color.test.xxx").getRawValue());
@ -283,57 +249,57 @@ public class GuiTest {
@Test @Test
public void testGetColor() { public void testGetColor() {
assertEquals(WHITE, Gui.getColor("color.test.bg")); assertEquals(WHITE, themeManager.getColor("color.test.bg"));
} }
@Test @Test
public void testGetFont() { public void testGetFont() {
assertEquals(FONT, Gui.getFont("font.test.foo")); assertEquals(FONT, themeManager.getFont("font.test.foo"));
} }
@Test @Test
public void testGetIcon() { public void testGetIcon() {
assertEquals(ICON1, Gui.getIcon("icon.test.foo")); assertEquals(ICON1, themeManager.getIcon("icon.test.foo"));
} }
@Test @Test
public void testGetColorWithUnresolvedId() { public void testGetColorWithUnresolvedId() {
assertEquals(CYAN, Gui.getColor("color.badid", false)); assertEquals(CYAN, themeManager.getColor("color.badid"));
} }
@Test @Test
public void testGetIconWithUnresolvedId() { public void testGetIconWithUnresolvedId() {
assertEquals(ResourceManager.getDefaultIcon(), Gui.getIcon("icon.badid", false)); assertEquals(ResourceManager.getDefaultIcon(), themeManager.getIcon("icon.badid"));
} }
@Test @Test
public void testGetFontWithUnresolvedId() { public void testGetFontWithUnresolvedId() {
assertEquals(Gui.DEFAULT_FONT, Gui.getFont("font.badid", false)); assertEquals(ThemeManager.DEFAULT_FONT, themeManager.getFont("font.badid"));
} }
@Test @Test
public void testGetGColorUiResource() { public void testGetGColorUiResource() {
Color color = Gui.getGColorUiResource("color.test.bg"); Color color = themeManager.getGColorUiResource("color.test.bg");
assertTrue(color instanceof UIResource); assertTrue(color instanceof UIResource);
// make sure there is only one instance for an id; // 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); assertTrue(color == color2);
} }
@Test @Test
public void testGetGIconUiResource() { public void testGetGIconUiResource() {
Icon icon = Gui.getGIconUiResource("icon.test.foo"); Icon icon = themeManager.getGIconUiResource("icon.test.foo");
assertTrue(icon instanceof UIResource); assertTrue(icon instanceof UIResource);
// make sure there is only one instance for an id; // 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); assertTrue(icon == gIcon2);
} }
@Test @Test
public void testGetApplicationLightDefaults() { public void testGetApplicationLightDefaults() {
assertEquals(defaultValues, Gui.getApplicationLightDefaults()); assertEquals(defaultValues, themeManager.getApplicationLightDefaults());
} }
@Test @Test
@ -342,17 +308,17 @@ public class GuiTest {
GThemeValueMap expected = new GThemeValueMap(); GThemeValueMap expected = new GThemeValueMap();
expected.load(defaultValues); expected.load(defaultValues);
expected.load(darkDefaultValues); expected.load(darkDefaultValues);
assertEquals(expected, Gui.getApplicationDarkDefaults()); assertEquals(expected, themeManager.getApplicationDarkDefaults());
} }
@Test @Test
public void testRegisterFont() { public void testRegisterFont() {
Gui.setFont(new FontValue("font.test", SMALL_FONT)); themeManager.setFont(new FontValue("font.test", SMALL_FONT));
JLabel label = new JLabel("Test"); JLabel label = new JLabel("Test");
assertNotEquals(SMALL_FONT, label.getFont()); assertNotEquals(SMALL_FONT, label.getFont());
Gui.registerFont(label, "font.test"); themeManager.registerFont(label, "font.test");
assertEquals(SMALL_FONT, label.getFont()); assertEquals(SMALL_FONT, label.getFont());
Gui.setFont(new FontValue("font.test", FONT)); themeManager.setFont(new FontValue("font.test", FONT));
assertEquals(FONT, label.getFont()); assertEquals(FONT, label.getFont());
} }
@ -369,4 +335,43 @@ public class GuiTest {
fail("Icons don't match. Expected " + url + ", but got " + gUrl); 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; import org.junit.Test;
public class WeakStoreTest { import generic.test.AbstractGenericTest;
public class WeakStoreTest extends AbstractGenericTest {
@Test @Test
public void testStore() throws InterruptedException { public void testStore() {
WeakStore<Foo> store = new WeakStore<>(); WeakStore<Foo> store = new WeakStore<>();
store.add(new Foo("AAA")); store.add(new Foo("AAA"));
store.add(new Foo("BBB")); store.add(new Foo("BBB"));
@ -38,13 +40,10 @@ public class WeakStoreTest {
assertEquals("CCC", values.get(2).getName()); assertEquals("CCC", values.get(2).getName());
values = null; values = null;
for (int i = 0; i < 20; i++) { waitFor(() -> {
System.gc(); System.gc();
if (store.size() == 0) { return store.size() == 0;
break; }, "Weak store values were never garbage collected");
}
}
assertEquals(0, store.size());
} }
static class Foo { static class Foo {

View file

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

View file

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

View file

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

View file

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