diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/framework/options/AutoOptions.java b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/framework/options/AutoOptions.java index c364441fed..7a5ccf36cf 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/framework/options/AutoOptions.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/ghidra/framework/options/AutoOptions.java @@ -212,10 +212,10 @@ public interface AutoOptions { else { options.registerOption(key.getName(), type, defaultValue, help, description, editor); + // TODO: Wish Ghidra would do this upon any option registration + options.putObject(key.getName(), defaultValue, type); } - // TODO: Wish Ghidra would do this upon any option registration - options.putObject(key.getName(), defaultValue, type); } } diff --git a/Ghidra/Features/Base/data/base.theme.properties b/Ghidra/Features/Base/data/base.theme.properties index 8f02f867d8..1cd006513a 100644 --- a/Ghidra/Features/Base/data/base.theme.properties +++ b/Ghidra/Features/Base/data/base.theme.properties @@ -68,6 +68,8 @@ color.fg.plugin.comments.history.text = blue color.fg.plugin.comments.history.user = color.fg color.fg.plugin.comments.history.date = rgb(124, 37, 18) +color.bg.plugin.programtree = color.bg + color.bg.plugin.datamgr.edge.default = blue color.bg.plugin.datamgr.edge.composite = magenta color.bg.plugin.datamgr.edge.reference = blue diff --git a/Ghidra/Features/Base/data/typeinfo/base.file.extensions.icons.theme.properties b/Ghidra/Features/Base/data/typeinfo/base.file.extensions.icons.theme.properties index 9554e479de..457230c488 100644 --- a/Ghidra/Features/Base/data/typeinfo/base.file.extensions.icons.theme.properties +++ b/Ghidra/Features/Base/data/typeinfo/base.file.extensions.icons.theme.properties @@ -37,6 +37,6 @@ icon.fsbrowser.file.substring.release. = images/famfamfam_silk_icons_v013/bullet_purple.png - icon.fsbrowser.file.overlay.imported = images/checkmark_green.gif - icon.fsbrowser.file.overlay.filesystem = EMPTY_ICON{images/nuvola/16x16/ledgreen.png[size(8,8)][move(8,8)]} // lower right quadrant + icon.fsbrowser.file.overlay.imported = EMPTY_ICON{images/checkmark_green.gif[size(8,8)][move(8,8)]} // lower right quadrant + icon.fsbrowser.file.overlay.filesystem = EMPTY_ICON{images/nuvola/16x16/ledgreen.png[size(8,8)][move(0,8)]} // lower left quadrant icon.fsbrowser.file.overlay.missing.password = EMPTY_ICON{images/lock.png[size(8,8)][move(8,0)]} // upper right quadrant \ No newline at end of file diff --git a/Ghidra/Features/Base/src/main/java/ghidra/GhidraRun.java b/Ghidra/Features/Base/src/main/java/ghidra/GhidraRun.java index b7e5951772..22ad3f23f7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/GhidraRun.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/GhidraRun.java @@ -38,7 +38,6 @@ import ghidra.program.database.ProgramDB; import ghidra.util.*; import ghidra.util.exception.UsrException; import ghidra.util.task.TaskLauncher; -import ghidra.util.task.TaskMonitorAdapter; /** * Main Ghidra application class. Creates @@ -73,7 +72,6 @@ public class GhidraRun implements GhidraLaunchable { Runnable mainTask = () -> { GhidraApplicationConfiguration configuration = new GhidraApplicationConfiguration(); - configuration.setTaskMonitor(new StatusReportingTaskMonitor()); Application.initializeApplication(layout, configuration); log = LogManager.getLogger(GhidraRun.class); @@ -243,15 +241,3 @@ public class GhidraRun implements GhidraLaunchable { // this exists just to allow access to the constructor } } - -class StatusReportingTaskMonitor extends TaskMonitorAdapter { - @Override - public synchronized void setCancelEnabled(boolean enable) { - // Not permitted - } - - @Override - public void setMessage(String message) { - SplashScreen.updateSplashScreenStatus(message); - } -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/editor/TextEditorComponentProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/editor/TextEditorComponentProvider.java index 0fd822fcb4..28597cca52 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/editor/TextEditorComponentProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/editor/TextEditorComponentProvider.java @@ -33,8 +33,7 @@ import docking.actions.KeyBindingUtils; import docking.options.editor.FontEditor; import docking.widgets.OptionDialog; import docking.widgets.filechooser.GhidraFileChooser; -import generic.theme.GIcon; -import generic.theme.Gui; +import generic.theme.*; import ghidra.framework.plugintool.ComponentProviderAdapter; import ghidra.util.HelpLocation; import ghidra.util.Msg; @@ -274,7 +273,7 @@ public class TextEditorComponentProvider extends ComponentProviderAdapter { FontEditor editor = new FontEditor(); editor.setValue(Gui.getFont(FONT_ID)); editor.showDialog(); - Gui.setFont(FONT_ID, (Font) editor.getValue()); + ThemeManager.getInstance().setFont(FONT_ID, (Font) editor.getValue()); } private void save() { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/programtree/DnDTreeCellRenderer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/programtree/DnDTreeCellRenderer.java index 6b9010fd1c..43c2e48d82 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/programtree/DnDTreeCellRenderer.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/programtree/DnDTreeCellRenderer.java @@ -34,6 +34,8 @@ import resources.ResourceManager; * Cell renderer for the drag and drop tree. */ class DnDTreeCellRenderer extends DefaultTreeCellRenderer { + private static final Color BACKGROUND_UNSELECTED = new GColor("color.bg.tree"); + private static final Color BACKGROUND_SELECTED = new GColor("color.bg.tree.selected"); private static final String DISABLED_DOCS = "DisabledDocument.gif"; private static final String DISABLED_FRAGMENT = "DisabledFragment"; @@ -73,8 +75,8 @@ class DnDTreeCellRenderer extends DefaultTreeCellRenderer { */ DnDTreeCellRenderer() { super(); - defaultNonSelectionColor = new GColor("Tree.textBackground"); - defaultSelectionColor = new GColor("Tree.selectionBackground"); + defaultNonSelectionColor = BACKGROUND_UNSELECTED; + defaultSelectionColor = BACKGROUND_SELECTED; rowForFeedback = -1; // disable HTML rendering diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/programtree/DragNDropTree.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/programtree/DragNDropTree.java index c2076f4b4f..5d7e59c13a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/programtree/DragNDropTree.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/programtree/DragNDropTree.java @@ -70,6 +70,7 @@ public abstract class DragNDropTree extends JTree implements Draggable, Droppabl public DragNDropTree(DefaultTreeModel model) { super(model); + setBackground(new GColor("color.bg.tree")); treeModel = model; this.root = (ProgramNode) model.getRoot(); //setEditable(true); // edit interferes with drag gesture listener diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/programtree/ProgramDnDTree.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/programtree/ProgramDnDTree.java index 2da9e4d094..81878f9acf 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/programtree/ProgramDnDTree.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/programtree/ProgramDnDTree.java @@ -315,8 +315,9 @@ public class ProgramDnDTree extends DragNDropTree { return false; } try { - Object data = e.getTransferable().getTransferData( - SelectionTransferable.localProgramSelectionFlavor); + Object data = e.getTransferable() + .getTransferData( + SelectionTransferable.localProgramSelectionFlavor); SelectionTransferData transferData = (SelectionTransferData) data; return program.getDomainFile().getPathname().equals(transferData.getProgramPath()); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptEditorComponentProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptEditorComponentProvider.java index 878c7c00cb..62bbdf4135 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptEditorComponentProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptEditorComponentProvider.java @@ -31,9 +31,8 @@ import docking.actions.KeyBindingUtils; import docking.options.editor.FontEditor; import docking.widgets.OptionDialog; import generic.jar.ResourceFile; -import generic.theme.GIcon; +import generic.theme.*; import generic.theme.GThemeDefaults.Colors; -import generic.theme.Gui; import ghidra.app.script.GhidraScriptUtil; import ghidra.util.*; import ghidra.util.datastruct.FixedSizeStack; @@ -508,7 +507,7 @@ public class GhidraScriptEditorComponentProvider extends ComponentProvider { FontEditor editor = new FontEditor(); editor.setValue(Gui.getFont(FONT_ID)); editor.showDialog(); - Gui.setFont(FONT_ID, (Font) editor.getValue()); + ThemeManager.getInstance().setFont(FONT_ID, (Font) editor.getValue()); } private void save() { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/framework/GhidraApplicationConfiguration.java b/Ghidra/Features/Base/src/main/java/ghidra/framework/GhidraApplicationConfiguration.java index c6ec824e3d..c39a1351b2 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/framework/GhidraApplicationConfiguration.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/framework/GhidraApplicationConfiguration.java @@ -20,7 +20,7 @@ import docking.DockingWindowManager; import docking.framework.ApplicationInformationDisplayFactory; import docking.framework.SplashScreen; import docking.widgets.PopupKeyStorePasswordProvider; -import generic.theme.Gui; +import generic.theme.ApplicationThemeManager; import ghidra.docking.util.LookAndFeelUtils; import ghidra.formats.gfilesystem.crypto.CryptoProviders; import ghidra.formats.gfilesystem.crypto.PopupGUIPasswordProvider; @@ -30,6 +30,7 @@ import ghidra.framework.preferences.Preferences; import ghidra.net.ApplicationKeyManagerFactory; import ghidra.util.ErrorDisplay; import ghidra.util.SystemUtilities; +import ghidra.util.task.TaskMonitorAdapter; public class GhidraApplicationConfiguration extends HeadlessGhidraApplicationConfiguration { @@ -43,12 +44,13 @@ public class GhidraApplicationConfiguration extends HeadlessGhidraApplicationCon @Override protected void initializeApplication() { - Gui.initialize(); + ApplicationThemeManager.initialize(); LookAndFeelUtils.performPlatformSpecificFixups(); if (showSplashScreen) { showUserAgreement(); SplashScreen.showSplashScreen(); + this.monitor = new StatusReportingTaskMonitor(); } super.initializeApplication(); @@ -89,4 +91,17 @@ public class GhidraApplicationConfiguration extends HeadlessGhidraApplicationCon public ErrorDisplay getErrorDisplay() { return new DockingErrorDisplay(); } + + private static class StatusReportingTaskMonitor extends TaskMonitorAdapter { + @Override + public synchronized void setCancelEnabled(boolean enable) { + // Not permitted + } + + @Override + public void setMessage(String message) { + SplashScreen.updateSplashScreenStatus(message); + } + } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileIconService.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileIconService.java index 1b8c738a64..122a1335d7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileIconService.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FileIconService.java @@ -66,7 +66,7 @@ public class FileIconService { } private void createSubstringMap() { - GThemeValueMap values = Gui.getAllValues(); + GThemeValueMap values = ThemeManager.getInstance().getCurrentValues(); List icons = values.getIcons(); for (IconValue iconValue : icons) { String id = iconValue.getId(); diff --git a/Ghidra/Features/Base/src/test/java/ghidra/framework/data/OptionsDBTest.java b/Ghidra/Features/Base/src/test/java/ghidra/framework/data/OptionsDBTest.java index 650b63b499..e48a7012e2 100644 --- a/Ghidra/Features/Base/src/test/java/ghidra/framework/data/OptionsDBTest.java +++ b/Ghidra/Features/Base/src/test/java/ghidra/framework/data/OptionsDBTest.java @@ -32,7 +32,7 @@ import org.junit.*; import docking.test.AbstractDockingTest; import generic.theme.GColor; import generic.theme.GThemeDefaults.Colors.Palette; -import generic.theme.Gui; +import generic.theme.ThemeManager; import ghidra.framework.options.*; import ghidra.framework.options.OptionsTest.FRUIT; import ghidra.program.database.ProgramBuilder; @@ -61,7 +61,7 @@ public class OptionsDBTest extends AbstractDockingTest { ProgramDB program = builder.getProgram(); txID = program.startTransaction("Test"); options = new OptionsDB(program); - Gui.setColor("color.test", Palette.MAGENTA); + ThemeManager.getInstance().setColor("color.test", Palette.MAGENTA); testColor = new GColor("color.test"); } diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/TokenHighlightColors.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/TokenHighlightColors.java index 8be5cc1d89..50624937ed 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/TokenHighlightColors.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/TokenHighlightColors.java @@ -33,7 +33,7 @@ public class TokenHighlightColors { float h = (float) Math.random(); // 0-360 float s = .25f; // saturation; gray to full color; full color is too harsh for highlights float b = 1f; // brightness; black to full color - if (Gui.getActiveTheme().useDarkDefaults()) { + if (Gui.isDarkTheme()) { s = .5f; // a bit more color against a dark background b = .5f; // less brightness, as the background is not as bright } diff --git a/Ghidra/Framework/Docking/certification.manifest b/Ghidra/Framework/Docking/certification.manifest index abe644a7bf..bec96bb78a 100644 --- a/Ghidra/Framework/Docking/certification.manifest +++ b/Ghidra/Framework/Docking/certification.manifest @@ -92,7 +92,7 @@ src/main/resources/images/page_go.png||FAMFAMFAM Icons - CC 2.5||||END| src/main/resources/images/page_green.png||FAMFAMFAM Icons - CC 2.5||||END| src/main/resources/images/play.png||GHIDRA||||END| src/main/resources/images/preferences-system-windows.png||Tango Icons - Public Domain||||END| -src/main/resources/images/redo.png||GHIDRA||||END| +src/main/resources/images/redo.png||Crystal Clear Icons - LGPL 2.1||||END| src/main/resources/images/software-update-available.png||Tango Icons - Public Domain|||tango icon set|END| src/main/resources/images/table.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END| src/main/resources/images/tag.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END| @@ -100,7 +100,7 @@ src/main/resources/images/text_lowercase.png||FAMFAMFAM Icons - CC 2.5|||famfamf src/main/resources/images/textfield_rename.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END| src/main/resources/images/tip.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| src/main/resources/images/trash-empty.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| -src/main/resources/images/undo.png||GHIDRA||||END| +src/main/resources/images/undo.png||Crystal Clear Icons - LGPL 2.1||||END| src/main/resources/images/user-home.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| src/main/resources/images/view-filter.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| src/main/resources/images/warning.help.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| diff --git a/Ghidra/Framework/Docking/data/docking.theme.properties b/Ghidra/Framework/Docking/data/docking.theme.properties index 4b562165aa..27d2eca28f 100644 --- a/Ghidra/Framework/Docking/data/docking.theme.properties +++ b/Ghidra/Framework/Docking/data/docking.theme.properties @@ -82,12 +82,16 @@ color.border.provider.disconnected = orange color.bg.filechooser = color.bg color.fg.filechooser = color.fg +color.bg.filechooser.shortcut = lightGray color.bg.fieldpanel = color.bg color.fg.fieldpanel = color.fg color.bg.fieldpanel.selection = color.bg.selection color.bg.fieldpanel.highlight = color.bg.highlight color.bg.fieldpanel.selection.and.highlight = green + +color.bg.tree = [color]Tree.textBackground +color.bg.tree.selected = [color]Tree.selectionBackground // docking buttons color.fg.button = black @@ -239,3 +243,10 @@ color.bg.tableheader.gradient.end.primary = darkBlue // docking buttons color.fg.button = darkGray + +color.bg.filechooser.shortcut = system.color.bg.widget + +color.bg.tree = color.bg +color.bg.tree.selected = [color]Tree.selectionBackground +icon.undo = eatbits1.png +icon.redo = eatbits2.png diff --git a/Ghidra/Framework/Docking/src/main/java/docking/ComponentProvider.java b/Ghidra/Framework/Docking/src/main/java/docking/ComponentProvider.java index cb8cd57e74..20bfef2bc5 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/ComponentProvider.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/ComponentProvider.java @@ -23,6 +23,7 @@ import javax.swing.*; import docking.action.*; import generic.theme.Gui; +import generic.theme.ThemeManager; import ghidra.util.*; import ghidra.util.exception.AssertException; import help.HelpDescriptor; @@ -801,7 +802,7 @@ public abstract class ComponentProvider implements HelpDescriptor, ActionContext else { size = Math.max(size - 1, 3); } - Gui.setFont(registeredFontId, font.deriveFont((float) size)); + ThemeManager.getInstance().setFont(registeredFontId, font.deriveFont((float) size)); } /** diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DockingUtils.java b/Ghidra/Framework/Docking/src/main/java/docking/DockingUtils.java index 9bdcf706eb..6da2c86f27 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/DockingUtils.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/DockingUtils.java @@ -38,7 +38,7 @@ import docking.widgets.list.GListCellRenderer; import docking.widgets.table.GTableCellRenderer; import docking.widgets.tree.support.GTreeRenderer; import generic.theme.GThemeDefaults.Colors.Palette; -import generic.theme.Gui; +import ghidra.docking.util.LookAndFeelUtils; import ghidra.util.HTMLUtilities; import resources.ResourceManager; @@ -126,7 +126,7 @@ public class DockingUtils { public static JSeparator createToolbarSeparator() { Dimension sepDim = new Dimension(2, ICON_SIZE + 2); JSeparator separator = new JSeparator(SwingConstants.VERTICAL); - if (Gui.isUsingAquaUI(separator.getUI())) { + if (LookAndFeelUtils.isUsingAquaUI(separator.getUI())) { separator.setUI(new BasicSeparatorUI()); } separator.setPreferredSize(sepDim); // ugly work around to force height of separator diff --git a/Ghidra/Framework/Docking/src/main/java/docking/StatusBar.java b/Ghidra/Framework/Docking/src/main/java/docking/StatusBar.java index 03a111f0f5..9ed42d82cf 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/StatusBar.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/StatusBar.java @@ -312,7 +312,7 @@ public class StatusBar extends JPanel { int value = 0; int delta = 16; - if (Gui.getActiveTheme().useDarkDefaults()) { + if (Gui.isDarkTheme()) { value = 128; delta = -16; } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/framework/ApplicationInformationDisplayFactory.java b/Ghidra/Framework/Docking/src/main/java/docking/framework/ApplicationInformationDisplayFactory.java index d9c063901c..94d5a40d59 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/framework/ApplicationInformationDisplayFactory.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/framework/ApplicationInformationDisplayFactory.java @@ -32,9 +32,9 @@ import ghidra.util.Msg; public class ApplicationInformationDisplayFactory { - private static final GIcon ICON_HOME = new GIcon("icon.docking.application.home"); - private static final GIcon ICON_16 = new GIcon("icon.docking.application.16"); - private static final GIcon ICON_128 = new GIcon("icon.base.application.128"); + private static final String ICON_HOME = "icon.docking.application.home"; + private static final String ICON_16 = "icon.docking.application.16"; + private static final String ICON_128 = "icon.docking.application.128"; static { PluggableServiceRegistry.registerPluggableService( @@ -144,13 +144,13 @@ public class ApplicationInformationDisplayFactory { } protected Icon getSplashScreenIcon128() { - return ICON_128; + return new GIcon(ICON_128); } protected List doGetWindowIcons() { List list = new ArrayList<>(); - list.add(ICON_128.getImageIcon().getImage()); - list.add(ICON_16.getImageIcon().getImage()); + list.add(new GIcon(ICON_128).getImageIcon().getImage()); + list.add(new GIcon(ICON_16).getImageIcon().getImage()); return list; } @@ -163,7 +163,7 @@ public class ApplicationInformationDisplayFactory { } protected Icon doGetHomeIcon() { - return ICON_HOME; + return new GIcon(ICON_HOME); } protected Runnable doGetHomeCallback() { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/framework/DockingApplicationConfiguration.java b/Ghidra/Framework/Docking/src/main/java/docking/framework/DockingApplicationConfiguration.java index 730d0087ac..fef37b3cf4 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/framework/DockingApplicationConfiguration.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/framework/DockingApplicationConfiguration.java @@ -17,7 +17,7 @@ package docking.framework; import docking.DockingErrorDisplay; import docking.widgets.PopupKeyStorePasswordProvider; -import generic.theme.Gui; +import generic.theme.ApplicationThemeManager; import ghidra.docking.util.LookAndFeelUtils; import ghidra.framework.ApplicationConfiguration; import ghidra.net.ApplicationKeyManagerFactory; @@ -49,7 +49,7 @@ public class DockingApplicationConfiguration extends ApplicationConfiguration { protected void initializeApplication() { super.initializeApplication(); - Gui.initialize(); + ApplicationThemeManager.initialize(); LookAndFeelUtils.performPlatformSpecificFixups(); if (showSplashScreen) { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/menu/DockingToolBarUtils.java b/Ghidra/Framework/Docking/src/main/java/docking/menu/DockingToolBarUtils.java index eb7df7e4a2..f5601a1ae2 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/menu/DockingToolBarUtils.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/menu/DockingToolBarUtils.java @@ -24,7 +24,7 @@ import javax.swing.KeyStroke; import org.apache.commons.lang3.StringUtils; import docking.action.DockingActionIf; -import generic.theme.Gui; +import ghidra.docking.util.LookAndFeelUtils; import ghidra.util.StringUtilities; class DockingToolBarUtils { @@ -96,7 +96,7 @@ class DockingToolBarUtils { builder.append(InputEvent.getModifiersExText(modifiers)); // The Aqua LaF does not use the '+' symbol between modifiers - if (!Gui.isUsingAquaUI(button.getUI())) { + if (!LookAndFeelUtils.isUsingAquaUI(button.getUI())) { builder.append('+'); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/menu/MultipleActionDockingToolbarButton.java b/Ghidra/Framework/Docking/src/main/java/docking/menu/MultipleActionDockingToolbarButton.java index 1acf733b12..6492259535 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/menu/MultipleActionDockingToolbarButton.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/menu/MultipleActionDockingToolbarButton.java @@ -97,6 +97,16 @@ public class MultipleActionDockingToolbarButton extends EmptyBorderButton { popupContext = createPopupContext(); } + @Override + public void updateUI() { + + removeMouseListener(popupListener); + + super.updateUI(); + + installMouseListeners(); + } + private void installMouseListeners() { MouseListener[] mouseListeners = getMouseListeners(); for (MouseListener mouseListener : mouseListeners) { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ExportThemeDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ExportThemeDialog.java index a2e55e9a34..482be31be4 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ExportThemeDialog.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ExportThemeDialog.java @@ -43,9 +43,11 @@ public class ExportThemeDialog extends DialogComponentProvider { private JTextField fileTextField; private GCheckBox includeDefaultsCheckbox; private boolean exportAsZip; + private ThemeManager themeManager; - public ExportThemeDialog(boolean exportAsZip) { + public ExportThemeDialog(ThemeManager themeManager, boolean exportAsZip) { super("Export Theme"); + this.themeManager = themeManager; this.exportAsZip = exportAsZip; addWorkPanel(buildMainPanel()); addOKButton(); @@ -62,7 +64,7 @@ public class ExportThemeDialog extends DialogComponentProvider { private boolean exportTheme() { String themeName = nameField.getText(); - GTheme activeTheme = Gui.getActiveTheme(); + GTheme activeTheme = themeManager.getActiveTheme(); LafType laf = activeTheme.getLookAndFeelType(); boolean useDarkDefaults = activeTheme.useDarkDefaults(); File file = new File(fileTextField.getText()); @@ -87,10 +89,10 @@ public class ExportThemeDialog extends DialogComponentProvider { private void loadValues(GTheme exportTheme) { if (includeDefaultsCheckbox.isSelected()) { - exportTheme.load(Gui.getAllValues()); + exportTheme.load(themeManager.getCurrentValues()); } else { - exportTheme.load(Gui.getNonDefaultValues()); + exportTheme.load(themeManager.getNonDefaultValues()); } } @@ -114,7 +116,7 @@ public class ExportThemeDialog extends DialogComponentProvider { private Component buildNameField() { nameField = new JTextField(25); - nameField.setText(Gui.getActiveTheme().getName()); + nameField.setText(themeManager.getActiveTheme().getName()); return nameField; } @@ -127,7 +129,7 @@ public class ExportThemeDialog extends DialogComponentProvider { private Component buildFilePanel() { File homeDir = new File(System.getProperty("user.home")); // prefer the home directory - String name = Gui.getActiveTheme().getName(); + String name = themeManager.getActiveTheme().getName(); String filename = name.replaceAll(" ", "_") + "."; filename += exportAsZip ? GTheme.ZIP_FILE_EXTENSION : GTheme.FILE_EXTENSION; File file = new File(homeDir, filename); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/IconValueEditor.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/IconValueEditor.java index c0150a5cce..541641eb88 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/IconValueEditor.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/IconValueEditor.java @@ -37,7 +37,7 @@ public class IconValueEditor extends ThemeValueEditor { @Override protected Icon getRawValue(String id) { - return Gui.getIcon(id, true); + return Gui.getIcon(id); } @Override diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeColorTable.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeColorTable.java index c06f729bef..12a328bdbc 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeColorTable.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeColorTable.java @@ -29,7 +29,7 @@ import docking.action.ActionContextProvider; import docking.widgets.table.GFilterTable; import docking.widgets.table.GTable; import generic.theme.ColorValue; -import generic.theme.Gui; +import generic.theme.ThemeManager; import ghidra.util.Swing; /** @@ -41,10 +41,12 @@ public class ThemeColorTable extends JPanel implements ActionContextProvider { private ColorValueEditor colorEditor = new ColorValueEditor(this::colorValueChanged); private GTable table; private GFilterTable filterTable; + private ThemeManager themeManager; - public ThemeColorTable() { + public ThemeColorTable(ThemeManager themeManager) { super(new BorderLayout()); - colorTableModel = new ThemeColorTableModel(); + this.themeManager = themeManager; + colorTableModel = new ThemeColorTableModel(themeManager); filterTable = new GFilterTable<>(colorTableModel); table = filterTable.getTable(); @@ -87,7 +89,7 @@ public class ThemeColorTable extends JPanel implements ActionContextProvider { // run later - don't rock the boat in the middle of a listener callback Swing.runLater(() -> { ColorValue newValue = (ColorValue) event.getNewValue(); - Gui.setColor(newValue); + themeManager.setColor(newValue); }); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeColorTableModel.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeColorTableModel.java index 89e27eaba4..cba8f3dc41 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeColorTableModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeColorTableModel.java @@ -43,9 +43,11 @@ public class ThemeColorTableModel extends GDynamicColumnTableModel c.isChanged()) .popupWhen(c -> true) .helpLocation(new HelpLocation("Theming", "Restore_Value")) - .onAction(c -> c.getThemeValue().installValue()) + .onAction(c -> c.getThemeValue().installValue(themeManager)) .build(); addAction(resetValueAction); } @@ -93,44 +96,44 @@ public class ThemeDialog extends DialogComponentProvider { } private boolean handleChanges() { - if (Gui.hasThemeChanges()) { + if (themeManager.hasThemeChanges()) { int result = OptionDialog.showYesNoCancelDialog(null, "Close Theme Dialog", "You have changed the theme.\n Do you want save your changes?"); if (result == OptionDialog.CANCEL_OPTION) { return false; } if (result == OptionDialog.YES_OPTION) { - return ThemeUtils.saveThemeChanges(); + return ThemeUtils.saveThemeChanges(themeManager); } - Gui.restoreThemeValues(); + themeManager.restoreThemeValues(); } return true; } protected void saveCallback() { - ThemeUtils.saveThemeChanges(); + ThemeUtils.saveThemeChanges(themeManager); } private void restoreCallback() { - if (Gui.hasThemeChanges()) { + if (themeManager.hasThemeChanges()) { int result = OptionDialog.showYesNoDialog(null, "Restore Theme Values", "Are you sure you want to discard all your changes?"); if (result == OptionDialog.NO_OPTION) { return; } } - Gui.restoreThemeValues(); + themeManager.restoreThemeValues(); } private void reloadDefaultsCallback() { - if (Gui.hasThemeChanges()) { + if (themeManager.hasThemeChanges()) { int result = OptionDialog.showYesNoDialog(null, "Reload Ghidra Default Values", "This will discard all your theme changes. Continue?"); if (result == OptionDialog.NO_OPTION) { return; } } - Gui.reloadApplicationDefaults(); + themeManager.reloadApplicationDefaults(); } private void reset() { @@ -147,15 +150,15 @@ public class ThemeDialog extends DialogComponentProvider { return; } - if (!ThemeUtils.askToSaveThemeChanges()) { + if (!ThemeUtils.askToSaveThemeChanges(themeManager)) { Swing.runLater(() -> updateCombo()); return; } String themeName = (String) e.getItem(); Swing.runLater(() -> { - GTheme theme = Gui.getTheme(themeName); - Gui.setTheme(theme); + GTheme theme = themeManager.getTheme(themeName); + themeManager.setTheme(theme); if (theme.getLookAndFeelType() == LafType.GTK) { setStatusText( "Warning - Themes using the GTK LookAndFeel do not support changing java component colors, fonts or icons.", @@ -171,7 +174,7 @@ public class ThemeDialog extends DialogComponentProvider { } private void updateButtons() { - boolean hasChanges = Gui.hasThemeChanges(); + boolean hasChanges = themeManager.hasThemeChanges(); saveButton.setEnabled(hasChanges); restoreButton.setEnabled(hasChanges); } @@ -194,25 +197,25 @@ public class ThemeDialog extends DialogComponentProvider { } private void updateCombo() { - Set supportedThemes = Gui.getSupportedThemes(); + Set supportedThemes = themeManager.getSupportedThemes(); List themeNames = supportedThemes.stream().map(t -> t.getName()).collect(Collectors.toList()); Collections.sort(themeNames); combo.removeItemListener(comboListener); combo.setModel(new DefaultComboBoxModel(new Vector(themeNames))); - combo.setSelectedItem(Gui.getActiveTheme().getName()); + combo.setSelectedItem(themeManager.getActiveTheme().getName()); combo.addItemListener(comboListener); } private Component buildThemeCombo() { JPanel panel = new JPanel(); - Set supportedThemes = Gui.getSupportedThemes(); + Set supportedThemes = themeManager.getSupportedThemes(); List themeNames = supportedThemes.stream().map(t -> t.getName()).collect(Collectors.toList()); Collections.sort(themeNames); combo = new GhidraComboBox<>(themeNames); - combo.setSelectedItem(Gui.getActiveTheme().getName()); + combo.setSelectedItem(themeManager.getActiveTheme().getName()); combo.addItemListener(comboListener); panel.add(new JLabel("Theme: "), BorderLayout.WEST); @@ -223,9 +226,9 @@ public class ThemeDialog extends DialogComponentProvider { private Component buildTabedTables() { tabbedPane = new JTabbedPane(); - colorTable = new ThemeColorTable(); - fontTable = new ThemeFontTable(); - iconTable = new ThemeIconTable(); + colorTable = new ThemeColorTable(themeManager); + fontTable = new ThemeFontTable(themeManager); + iconTable = new ThemeIconTable(themeManager); tabbedPane.add("Colors", colorTable); tabbedPane.add("Fonts", fontTable); tabbedPane.add("Icons", iconTable); @@ -250,12 +253,16 @@ public class ThemeDialog extends DialogComponentProvider { return saveButton; } - public static void editTheme() { + /** + * Edits the current theme + * @param themeManager the application ThemeManager + */ + public static void editTheme(ThemeManager themeManager) { if (INSTANCE != null) { INSTANCE.toFront(); return; } - INSTANCE = new ThemeDialog(); + INSTANCE = new ThemeDialog(themeManager); DockingWindowManager.showDialog(INSTANCE); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeFontTable.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeFontTable.java index 698e2d1ace..0b36688696 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeFontTable.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeFontTable.java @@ -29,7 +29,7 @@ import docking.action.ActionContextProvider; import docking.widgets.table.GFilterTable; import docking.widgets.table.GTable; import generic.theme.FontValue; -import generic.theme.Gui; +import generic.theme.ThemeManager; import ghidra.util.Swing; /** @@ -41,11 +41,13 @@ public class ThemeFontTable extends JPanel implements ActionContextProvider { private FontValueEditor fontEditor = new FontValueEditor(this::fontValueChanged); private GTable table; private GFilterTable filterTable; + private ThemeManager themeManager; - public ThemeFontTable() { + public ThemeFontTable(ThemeManager themeManager) { super(new BorderLayout()); + this.themeManager = themeManager; - fontTableModel = new ThemeFontTableModel(); + fontTableModel = new ThemeFontTableModel(themeManager); filterTable = new GFilterTable<>(fontTableModel); table = filterTable.getTable(); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); @@ -86,7 +88,7 @@ public class ThemeFontTable extends JPanel implements ActionContextProvider { // run later - don't rock the boat in the middle of a listener callback Swing.runLater(() -> { FontValue newValue = (FontValue) event.getNewValue(); - Gui.setFont(newValue); + themeManager.setFont(newValue); }); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeFontTableModel.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeFontTableModel.java index 24e1628707..35740f365b 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeFontTableModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeFontTableModel.java @@ -39,9 +39,11 @@ public class ThemeFontTableModel extends GDynamicColumnTableModel filterTable; + private ThemeManager themeManager; - public ThemeIconTable() { + public ThemeIconTable(ThemeManager themeManager) { super(new BorderLayout()); - iconTableModel = new ThemeIconTableModel(); + this.themeManager = themeManager; + iconTableModel = new ThemeIconTableModel(themeManager); filterTable = new GFilterTable<>(iconTableModel); table = filterTable.getTable(); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); @@ -82,7 +84,7 @@ public class ThemeIconTable extends JPanel implements ActionContextProvider { // run later - don't rock the boat in the middle of a listener callback Swing.runLater(() -> { IconValue newValue = (IconValue) event.getNewValue(); - Gui.setIcon(newValue); + themeManager.setIcon(newValue); }); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeIconTableModel.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeIconTableModel.java index c7f95abe59..13b60bc181 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeIconTableModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeIconTableModel.java @@ -39,9 +39,11 @@ public class ThemeIconTableModel extends GDynamicColumnTableModel savedThemes = new ArrayList<>( - Gui.getAllThemes().stream().filter(t -> t.getFile() != null).toList()); + themeManager.getAllThemes().stream().filter(t -> t.getFile() != null).toList()); if (savedThemes.isEmpty()) { Msg.showInfo(ThemeUtils.class, null, "Delete Theme", "There are no deletable themes"); return; @@ -171,7 +173,7 @@ public class ThemeUtils { if (selectedTheme == null) { return; } - if (Gui.getActiveTheme().equals(selectedTheme)) { + if (themeManager.getActiveTheme().equals(selectedTheme)) { Msg.showWarn(ThemeUtils.class, null, "Delete Failed", "Can't delete the current theme."); return; @@ -180,7 +182,7 @@ public class ThemeUtils { int result = OptionDialog.showYesNoDialog(null, "Delete Theme: " + fileTheme.getName(), "Are you sure you want to delete theme " + fileTheme.getName()); if (result == OptionDialog.YES_OPTION) { - Gui.deleteTheme(fileTheme); + themeManager.deleteTheme(fileTheme); } } @@ -190,8 +192,8 @@ public class ThemeUtils { return inputDialog.getValue(); } - private static boolean canSaveToName(String name) { - GTheme existing = Gui.getTheme(name); + private static boolean canSaveToName(ThemeManager themeManager, String name) { + GTheme existing = themeManager.getTheme(name); // if no theme exists with that name, then we are save to save it if (existing == null) { return true; @@ -210,17 +212,17 @@ public class ThemeUtils { return result == OptionDialog.YES_OPTION; } - private static boolean saveCurrentValues(String themeName) { - GTheme activeTheme = Gui.getActiveTheme(); + private static boolean saveCurrentValues(ThemeManager themeManager, String themeName) { + GTheme activeTheme = themeManager.getActiveTheme(); File file = getSaveFile(themeName); GTheme newTheme = new GTheme(file, themeName, activeTheme.getLookAndFeelType(), activeTheme.useDarkDefaults()); - newTheme.load(Gui.getNonDefaultValues()); + newTheme.load(themeManager.getNonDefaultValues()); try { newTheme.save(); - Gui.addTheme(newTheme); - Gui.setTheme(newTheme); + themeManager.addTheme(newTheme); + themeManager.setTheme(newTheme); } catch (IOException e) { Msg.showError(ThemeUtils.class, null, "I/O Error", diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/EmptyBorderButton.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/EmptyBorderButton.java index ab20bdf12e..084fdab51e 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/EmptyBorderButton.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/EmptyBorderButton.java @@ -24,7 +24,7 @@ import javax.swing.border.EmptyBorder; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; -import generic.theme.Gui; +import ghidra.docking.util.LookAndFeelUtils; import resources.ResourceManager; /** @@ -123,7 +123,7 @@ public class EmptyBorderButton extends JButton { // Mac OSX LNF doesn't give us rollover callbacks, so we have to add a mouse listener to // do the work - if (Gui.isUsingAquaUI(getUI())) { + if (LookAndFeelUtils.isUsingAquaUI(getUI())) { addMouseListener(new MouseAdapter() { @Override public void mouseEntered(MouseEvent e) { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/GhidraFileChooser.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/GhidraFileChooser.java index 542bf21c17..9a30edb1de 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/GhidraFileChooser.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/GhidraFileChooser.java @@ -38,7 +38,6 @@ import docking.widgets.label.GLabel; import docking.widgets.list.GListCellRenderer; import generic.theme.GColor; import generic.theme.GIcon; -import generic.theme.GThemeDefaults.Colors.Palette; import ghidra.framework.preferences.Preferences; import ghidra.util.*; import ghidra.util.exception.AssertException; @@ -74,6 +73,8 @@ public class GhidraFileChooser extends DialogComponentProvider implements FileFi static final String UP_BUTTON_NAME = "UP_BUTTON"; private static final Color FOREROUND_COLOR = new GColor("color.fg.filechooser"); private static final Color BACKGROUND_COLOR = new GColor("color.bg.filechooser"); + private static final Color SHORTCUT_BACKGROUND_COLOR = + new GColor("color.bg.filechooser.shortcut"); static final String PREFERENCES_PREFIX = "G_FILE_CHOOSER"; private static final String WIDTH_PREFERENCE_PREFIX = PREFERENCES_PREFIX + ".WIDTH."; private static final String HEIGHT_PREFERENCE_PREFIX = PREFERENCES_PREFIX + ".HEIGHT."; @@ -343,7 +344,7 @@ public class GhidraFileChooser extends DialogComponentProvider implements FileFi JPanel panel = new JPanel(new BorderLayout()); panel.setBorder(BorderFactory.createLoweredBevelBorder()); - panel.setBackground(Palette.DARK_GRAY); + panel.setBackground(SHORTCUT_BACKGROUND_COLOR); panel.add(shortCutPanel, BorderLayout.NORTH); return panel; } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableCellRenderer.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableCellRenderer.java index ea37f6b6da..19f3339b70 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableCellRenderer.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableCellRenderer.java @@ -22,12 +22,14 @@ import java.text.DecimalFormat; import java.util.HashMap; import java.util.Map; -import javax.swing.*; +import javax.swing.JTable; +import javax.swing.SwingConstants; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableModel; import docking.widgets.AbstractGCellRenderer; import generic.theme.GColor; +import generic.theme.Gui; import ghidra.docking.settings.*; import ghidra.util.*; import ghidra.util.exception.AssertException; @@ -56,7 +58,13 @@ public class GTableCellRenderer extends AbstractGCellRenderer implements TableCe * Constructs a new GTableCellRenderer. */ public GTableCellRenderer() { - + // When the Look And Feel changes, renderers are not auto updated because they + // are not part of the component tree. So listen for a change to the Look And Feel. + Gui.addThemeListener(e -> { + if (e.isLookAndFeelChanged()) { + updateUI(); + } + }); } /** @@ -100,10 +108,7 @@ public class GTableCellRenderer extends AbstractGCellRenderer implements TableCe "Using a GTableCellRenderer in a non-GTable table. (Model class: " + table.getModel().getClass().getName() + ")"); } - // check if LookAndFeel has changed - if (UIManager.getUI(this) != getUI()) { - updateUI(); - } + GTable gTable = (GTable) table; GTableCellRenderingData data = gTable.getRenderingData(column); Object rowObject = null; diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTree.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTree.java index eb0bc1024d..8ac9994022 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTree.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/GTree.java @@ -45,6 +45,7 @@ import docking.widgets.tree.internal.*; import docking.widgets.tree.support.*; import docking.widgets.tree.support.GTreeSelectionEvent.EventOrigin; import docking.widgets.tree.tasks.*; +import generic.theme.*; import generic.timer.ExpiringSwingTimer; import ghidra.util.*; import ghidra.util.exception.CancelledException; @@ -55,8 +56,8 @@ import ghidra.util.worker.PriorityWorker; * Class for creating a JTree that supports filtering, threading, and a progress bar. */ -public class GTree extends JPanel implements BusyListener { - +public class GTree extends JPanel implements BusyListener, ThemeListener { + private static final Color BACKGROUND = new GColor("color.bg.tree"); private AutoScrollTree tree; private GTreeModel model; @@ -134,6 +135,7 @@ public class GTree extends JPanel implements BusyListener { uniquePreferenceKey)); filterUpdateManager = new SwingUpdateManager(1000, 30000, () -> updateModelFilter()); + Gui.addThemeListener(this); } /** @@ -146,6 +148,13 @@ public class GTree extends JPanel implements BusyListener { threadLocalMonitor.set(monitor); } + @Override + public void themeChanged(ThemeEvent event) { + if (event.isLookAndFeelChanged()) { + model.fireNodeStructureChanged(getModelRoot()); + } + } + /** * Returns the monitor in associated with the GTree for the calling thread. This method is * designed to be used by slow loading nodes that are loading off the Swing thread. Some @@ -1391,6 +1400,7 @@ public class GTree extends JPanel implements BusyListener { public AutoScrollTree(TreeModel model) { super(model); + setBackground(BACKGROUND); scroller = new AutoscrollAdapter(this, 5); setRowHeight(-1);// variable size rows diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/support/GTreeRenderer.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/support/GTreeRenderer.java index 8c1f37b767..c76a67e2f7 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/support/GTreeRenderer.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tree/support/GTreeRenderer.java @@ -31,6 +31,8 @@ public class GTreeRenderer extends DefaultTreeCellRenderer implements GComponent private static final Color VALID_DROP_TARGET_COLOR = new GColor("color.bg.tree.drag"); private static final int DEFAULT_MIN_ICON_WIDTH = 22; + private static final Color BACKGROUND_UNSELECTED = new GColor("color.bg.tree"); + private static final Color BACKGROUND_SELECTED = new GColor("color.bg.tree.selected"); private Object dropTarget; private boolean paintDropTarget; @@ -41,6 +43,8 @@ public class GTreeRenderer extends DefaultTreeCellRenderer implements GComponent public GTreeRenderer() { setHTMLRenderingEnabled(false); + setBackgroundNonSelectionColor(BACKGROUND_UNSELECTED); + setBackgroundSelectionColor(BACKGROUND_SELECTED); } @Override diff --git a/Ghidra/Framework/Docking/src/main/java/ghidra/docking/util/LookAndFeelUtils.java b/Ghidra/Framework/Docking/src/main/java/ghidra/docking/util/LookAndFeelUtils.java index 3d5cf6de12..1e9d3a2e9e 100644 --- a/Ghidra/Framework/Docking/src/main/java/ghidra/docking/util/LookAndFeelUtils.java +++ b/Ghidra/Framework/Docking/src/main/java/ghidra/docking/util/LookAndFeelUtils.java @@ -21,10 +21,12 @@ import java.util.Iterator; import java.util.Map.Entry; import java.util.Set; -import javax.swing.UIDefaults; -import javax.swing.UIManager; +import javax.swing.*; +import javax.swing.plaf.ComponentUI; import docking.framework.ApplicationInformationDisplayFactory; +import generic.theme.LafType; +import generic.theme.ThemeManager; import ghidra.framework.preferences.Preferences; import ghidra.util.SystemUtilities; @@ -81,4 +83,30 @@ public class LookAndFeelUtils { } } } + + /** + * Returns the {@link LafType} for the currently active {@link LookAndFeel} + * @return the {@link LafType} for the currently active {@link LookAndFeel} + */ + public static LafType getLookAndFeelType() { + return ThemeManager.getInstance().getLookAndFeelType(); + } + + /** + * Returns true if the given UI object is using the Aqua Look and Feel. + * @param UI the UI to examine. + * @return true if the UI is using Aqua + */ + public static boolean isUsingAquaUI(ComponentUI UI) { + return ThemeManager.getInstance().isUsingAquaUI(UI); + } + + /** + * Returns true if 'Nimbus' is the current Look and Feel + * @return true if 'Nimbus' is the current Look and Feel + */ + public static boolean isUsingNimbusUI() { + return ThemeManager.getInstance().isUsingNimbusUI(); + } + } diff --git a/Ghidra/Framework/Docking/src/main/resources/images/redo.png b/Ghidra/Framework/Docking/src/main/resources/images/redo.png index 88035efeec..5d6121b821 100644 Binary files a/Ghidra/Framework/Docking/src/main/resources/images/redo.png and b/Ghidra/Framework/Docking/src/main/resources/images/redo.png differ diff --git a/Ghidra/Framework/Docking/src/main/resources/images/undo.png b/Ghidra/Framework/Docking/src/main/resources/images/undo.png index edd57f5c15..ee410a9048 100644 Binary files a/Ghidra/Framework/Docking/src/main/resources/images/undo.png and b/Ghidra/Framework/Docking/src/main/resources/images/undo.png differ diff --git a/Ghidra/Framework/Docking/src/test/java/docking/theme/gui/ThemeUtilsTest.java b/Ghidra/Framework/Docking/src/test/java/docking/theme/gui/ThemeUtilsTest.java index 557a6b3a0b..6d207d7bec 100644 --- a/Ghidra/Framework/Docking/src/test/java/docking/theme/gui/ThemeUtilsTest.java +++ b/Ghidra/Framework/Docking/src/test/java/docking/theme/gui/ThemeUtilsTest.java @@ -41,67 +41,69 @@ import generic.theme.builtin.NimbusTheme; public class ThemeUtilsTest extends AbstractDockingTest { private Color testColor = Palette.RED; + private ThemeManager themeManager; @Before public void setup() { + themeManager = ThemeManager.getInstance(); GTheme nimbusTheme = new NimbusTheme(); GTheme metalTheme = new MetalTheme(); - Gui.addTheme(nimbusTheme); - Gui.addTheme(metalTheme); - Gui.setTheme(nimbusTheme); + themeManager.addTheme(nimbusTheme); + themeManager.addTheme(metalTheme); + themeManager.setTheme(nimbusTheme); // get rid of any leftover imported themes from previous tests - Set allThemes = Gui.getAllThemes(); + Set allThemes = themeManager.getAllThemes(); for (GTheme theme : allThemes) { if (!(theme instanceof DiscoverableGTheme)) { - Gui.deleteTheme(theme); + themeManager.deleteTheme(theme); } } } @Test public void testImportThemeNonZip() throws IOException { - assertEquals("Nimbus Theme", Gui.getActiveTheme().getName()); + assertEquals("Nimbus Theme", themeManager.getActiveTheme().getName()); File themeFile = createThemeFile("Bob"); - ThemeUtils.importTheme(themeFile); - assertEquals("Bob", Gui.getActiveTheme().getName()); + ThemeUtils.importTheme(themeManager, themeFile); + assertEquals("Bob", themeManager.getActiveTheme().getName()); } @Test public void testImportThemeFromZip() throws IOException { - assertEquals("Nimbus Theme", Gui.getActiveTheme().getName()); + assertEquals("Nimbus Theme", themeManager.getActiveTheme().getName()); File themeFile = createZipThemeFile("zippy"); - ThemeUtils.importTheme(themeFile); - assertEquals("zippy", Gui.getActiveTheme().getName()); + ThemeUtils.importTheme(themeManager, themeFile); + assertEquals("zippy", themeManager.getActiveTheme().getName()); } @Test public void testImportThemeWithCurrentChangesCancelled() throws IOException { - assertEquals("Nimbus Theme", Gui.getActiveTheme().getName()); - Gui.setColor("Panel.background", testColor); - assertTrue(Gui.hasThemeChanges()); + assertEquals("Nimbus Theme", themeManager.getActiveTheme().getName()); + themeManager.setColor("Panel.background", testColor); + assertTrue(themeManager.hasThemeChanges()); File themeFile = createThemeFile("Bob"); - runSwingLater(() -> ThemeUtils.importTheme(themeFile)); + runSwingLater(() -> ThemeUtils.importTheme(themeManager, themeFile)); OptionDialog dialog = waitForDialogComponent(OptionDialog.class); assertNotNull(dialog); assertEquals("Save Theme Changes?", dialog.getTitle()); pressButtonByText(dialog, "Cancel"); waitForSwing(); - assertEquals("Nimbus Theme", Gui.getActiveTheme().getName()); + assertEquals("Nimbus Theme", themeManager.getActiveTheme().getName()); } @Test public void testImportThemeWithCurrentChangesSaved() throws IOException { - assertEquals("Nimbus Theme", Gui.getActiveTheme().getName()); + assertEquals("Nimbus Theme", themeManager.getActiveTheme().getName()); // make a change in the current theme, so you get asked to save - Gui.setColor("Panel.background", testColor); - assertTrue(Gui.hasThemeChanges()); + themeManager.setColor("Panel.background", testColor); + assertTrue(themeManager.hasThemeChanges()); File themeFile = createThemeFile("Bob"); - runSwingLater(() -> ThemeUtils.importTheme(themeFile)); + runSwingLater(() -> ThemeUtils.importTheme(themeManager, themeFile)); OptionDialog dialog = waitForDialogComponent(OptionDialog.class); assertNotNull(dialog); assertEquals("Save Theme Changes?", dialog.getTitle()); @@ -111,32 +113,32 @@ public class ThemeUtilsTest extends AbstractDockingTest { runSwing(() -> inputDialog.setValue("Joe")); pressButtonByText(inputDialog, "OK"); waitForSwing(); - assertEquals("Bob", Gui.getActiveTheme().getName()); - assertNotNull(Gui.getTheme("Joe")); + assertEquals("Bob", themeManager.getActiveTheme().getName()); + assertNotNull(themeManager.getTheme("Joe")); } @Test public void testImportThemeWithCurrentChangesThrownAway() throws IOException { - assertEquals("Nimbus Theme", Gui.getActiveTheme().getName()); + assertEquals("Nimbus Theme", themeManager.getActiveTheme().getName()); // make a change in the current theme, so you get asked to save - Gui.setColor("Panel.background", testColor); - assertTrue(Gui.hasThemeChanges()); + themeManager.setColor("Panel.background", testColor); + assertTrue(themeManager.hasThemeChanges()); File bobThemeFile = createThemeFile("Bob"); - runSwingLater(() -> ThemeUtils.importTheme(bobThemeFile)); + runSwingLater(() -> ThemeUtils.importTheme(themeManager, bobThemeFile)); OptionDialog dialog = waitForDialogComponent(OptionDialog.class); assertNotNull(dialog); assertEquals("Save Theme Changes?", dialog.getTitle()); pressButtonByText(dialog, "No"); waitForSwing(); - assertEquals("Bob", Gui.getActiveTheme().getName()); + assertEquals("Bob", themeManager.getActiveTheme().getName()); } @Test public void testExportThemeAsZip() throws IOException { - runSwingLater(() -> ThemeUtils.exportTheme()); + runSwingLater(() -> ThemeUtils.exportTheme(themeManager)); OptionDialog dialog = waitForDialogComponent(OptionDialog.class); pressButtonByText(dialog, "Export Zip"); ExportThemeDialog exportDialog = waitForDialogComponent(ExportThemeDialog.class); @@ -151,7 +153,7 @@ public class ThemeUtilsTest extends AbstractDockingTest { @Test public void testExportThemeAsFile() throws IOException { - runSwingLater(() -> ThemeUtils.exportTheme()); + runSwingLater(() -> ThemeUtils.exportTheme(themeManager)); OptionDialog dialog = waitForDialogComponent(OptionDialog.class); pressButtonByText(dialog, "Export File"); ExportThemeDialog exportDialog = waitForDialogComponent(ExportThemeDialog.class); @@ -167,29 +169,29 @@ public class ThemeUtilsTest extends AbstractDockingTest { @Test public void testDeleteTheme() throws IOException { File themeFile = createThemeFile("Bob"); - ThemeUtils.importTheme(themeFile); + ThemeUtils.importTheme(themeManager, themeFile); themeFile = createThemeFile("Joe"); - ThemeUtils.importTheme(themeFile); + ThemeUtils.importTheme(themeManager, themeFile); themeFile = createThemeFile("Lisa"); - ThemeUtils.importTheme(themeFile); + ThemeUtils.importTheme(themeManager, themeFile); - assertNotNull(Gui.getTheme("Bob")); - assertNotNull(Gui.getTheme("Joe")); - assertNotNull(Gui.getTheme("Lisa")); + assertNotNull(themeManager.getTheme("Bob")); + assertNotNull(themeManager.getTheme("Joe")); + assertNotNull(themeManager.getTheme("Lisa")); - runSwingLater(() -> ThemeUtils.deleteTheme()); + runSwingLater(() -> ThemeUtils.deleteTheme(themeManager)); @SuppressWarnings("unchecked") SelectFromListDialog dialog = waitForDialogComponent(SelectFromListDialog.class); - runSwing(() -> dialog.setSelectedObject(Gui.getTheme("Bob"))); + runSwing(() -> dialog.setSelectedObject(themeManager.getTheme("Bob"))); pressButtonByText(dialog, "OK"); OptionDialog optionDialog = waitForDialogComponent(OptionDialog.class); pressButtonByText(optionDialog, "Yes"); waitForSwing(); - assertNotNull(Gui.getTheme("Bob")); - assertNull(Gui.getTheme("Joe")); - assertNotNull(Gui.getTheme("Lisa")); + assertNotNull(themeManager.getTheme("Bob")); + assertNull(themeManager.getTheme("Joe")); + assertNotNull(themeManager.getTheme("Lisa")); } @@ -231,7 +233,7 @@ public class ThemeUtilsTest extends AbstractDockingTest { File file = createTempFile("Test_Theme", ".theme.zip"); GTheme outputTheme = new GTheme(file, themeName, LafType.METAL, false); outputTheme.addColor(new ColorValue("Panel.Background", testColor)); - outputTheme.saveToZip(file, false); + new ThemeWriter(outputTheme).writeThemeToZipFile(file); return file; } diff --git a/Ghidra/Framework/Generic/data/generic.theme.properties b/Ghidra/Framework/Generic/data/generic.theme.properties index d1c9d63cfc..8db3c102fb 100644 --- a/Ghidra/Framework/Generic/data/generic.theme.properties +++ b/Ghidra/Framework/Generic/data/generic.theme.properties @@ -28,7 +28,7 @@ icon.expand.all = expand_all.png icon.configure.filter = exec.png icon.clear = erase16.png -icon.delete = icon.error +icon.delete = edit-delete.png icon.error = emblem-important.png icon.home = go-home.png diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ApplicationThemeManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ApplicationThemeManager.java new file mode 100644 index 0000000000..6e07ea273c --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ApplicationThemeManager.java @@ -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 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 gColorMap = new HashMap<>(); + private Map 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 getAllThemes() { + loadThemes(); + return new HashSet<>(allThemes); + } + + @Override + public Set getSupportedThemes() { + loadThemes(); + Set 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 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 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 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. + * + *

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 set = new HashSet<>(); + set.addAll(findDiscoverableThemes()); + set.addAll(themeFileLoader.loadThemeFiles()); + allThemes = set; + } + } + + private Collection 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 findChangedJavaFontIds(String id) { + Set affectedIds = new HashSet<>(); + List 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 findChangedJavaIconIds(String id) { + Set affectedIds = new HashSet<>(); + List 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); + } + +} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ColorValue.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ColorValue.java index 0190d7aa5f..9c2fdf0318 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/ColorValue.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ColorValue.java @@ -150,8 +150,8 @@ public class ColorValue extends ThemeValue { } @Override - public void installValue() { - Gui.setColor(this); + public void installValue(ThemeManager themeManager) { + themeManager.setColor(this); } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/FontValue.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/FontValue.java index 105e7b914f..50e95a91b5 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/FontValue.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/FontValue.java @@ -232,8 +232,8 @@ public class FontValue extends ThemeValue { } @Override - public void installValue() { - Gui.setFont(this); + public void installValue(ThemeManager themeManager) { + themeManager.setFont(this); } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/GColor.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/GColor.java index 3f82c4316d..653a85c9c0 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/GColor.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/GColor.java @@ -50,20 +50,9 @@ public class GColor extends Color { * @param id the id used to lookup the current value for this color */ public GColor(String id) { - this(id, true); - } - - /** - * Construct a GColor with an id that will be used to look up the current color associated with - * that id, which can be changed at runtime. - * @param id the id used to lookup the current value for this color - * @param validate if true, an error will be generated if the id can't be resolved to a color - * at this time - */ - public GColor(String id, boolean validate) { super(0x808080); this.id = id; - delegate = Gui.getColor(id, validate); + delegate = Gui.getColor(id); inUseColors.add(this); } @@ -230,9 +219,11 @@ public class GColor extends Color { /** * Reloads the delegate. + * @param currentValues the map of current theme values */ - public void refresh() { - Color color = Gui.getColor(id, false); + public void refresh(GThemeValueMap currentValues) { + ColorValue value = currentValues.getColor(id); + Color color = value == null ? null : value.get(currentValues); if (color != null) { if (alpha != null) { delegate = new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha); @@ -246,10 +237,11 @@ public class GColor extends Color { /** * Static method for notifying all the existing GColors that colors have changed and they * should reload their cached indirect color. + * @param currentValues the map of current theme values */ - public static void refreshAll() { + public static void refreshAll(GThemeValueMap currentValues) { for (GColor gcolor : inUseColors.getValues()) { - gcolor.refresh(); + gcolor.refresh(currentValues); } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/GIcon.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/GIcon.java index 72e98b2a46..17aed43333 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/GIcon.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/GIcon.java @@ -46,10 +46,11 @@ public class GIcon implements Icon { /** * Static method for notifying all the existing GIcon that icons have changed and they * should reload their cached indirect icon. + * @param currentValues the map of all current theme values */ - public static void refreshAll() { + public static void refreshAll(GThemeValueMap currentValues) { for (GIcon gIcon : inUseIcons.getValues()) { - gIcon.refresh(); + gIcon.refresh(currentValues); } } @@ -59,19 +60,8 @@ public class GIcon implements Icon { * @param id the id used to lookup the current value for this color */ public GIcon(String id) { - this(id, true); - } - - /** - * Construct a GIcon with an id that will be used to look up the current icon associated with - * that id, which can be changed at runtime. - * @param id the id used to lookup the current value for this icon - * @param validate if true, an error will be generated if the id can't be resolved to a icon - * at this time - */ - public GIcon(String id, boolean validate) { this.id = id; - delegate = Gui.getIcon(id, validate); + delegate = Gui.getIcon(id); inUseIcons.add(this); } @@ -134,14 +124,25 @@ public class GIcon implements Icon { /** * Reloads the delegate. + * @param currentValues the map of current theme values */ - public void refresh() { - Icon icon = Gui.getIcon(id, false); + public void refresh(GThemeValueMap currentValues) { + IconValue value = currentValues.getIcon(id); + Icon icon = value == null ? null : value.get(currentValues); if (icon != null) { delegate = icon; } } + /** + * Returns the current delegate for this GIcon. Note that this delegate can change when the + * theme changes or is edited. + * @return the current delegate icon for this GIcon. + */ + public Icon getDelegate() { + return delegate; + } + @Override public int hashCode() { return id.hashCode(); diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/GTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/GTheme.java index fda6ea33f7..277a67b463 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/GTheme.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/GTheme.java @@ -217,43 +217,6 @@ public class GTheme extends GThemeValueMap { writer.writeThemeToFile(file); } - /** - * Saves this theme to a new theme file. - * @param outputFile the file to save to - * @param includeDefaults if true, write all values to the theme file including default values. - * Otherwise, just values that are not the default values are written to the file. - * @return a new FileGTheme that represents the new file/theme - * @throws IOException if an I/O error occurs writing the theme file - */ - public GTheme saveToFile(File outputFile, boolean includeDefaults) throws IOException { - - GTheme fileTheme = new GTheme(outputFile, name, lookAndFeel, useDarkDefaults); - if (includeDefaults) { - fileTheme.load(Gui.getDefaults()); - } - fileTheme.load(this); - fileTheme.save(); - return fileTheme; - } - - /** - * Saves this theme to a new theme file. - * @param outputFile the file to save to - * @param includeDefaults if true, write all values to the theme file including default values. - * Otherwise, just values that are not the default values are written to the file. - * @throws IOException if an I/O error occurs writing the theme file - */ - public void saveToZip(File outputFile, boolean includeDefaults) throws IOException { - - GTheme theme = new GTheme(name, lookAndFeel, useDarkDefaults); - if (includeDefaults) { - theme.load(Gui.getDefaults()); - } - theme.load(this); - ThemeWriter writer = new ThemeWriter(theme); - writer.writeThemeToZipFile(outputFile); - } - /** * Reads a theme from a file. The file can be either a theme file or a zip file containing * a theme file and optionally a set of icon files. diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/Gui.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/Gui.java index ce14d3ae63..a9c3d86428 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/Gui.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/Gui.java @@ -16,26 +16,9 @@ package generic.theme; import java.awt.*; -import java.io.File; -import java.util.*; -import java.util.List; -import javax.swing.*; -import javax.swing.plaf.ComponentUI; - -import com.formdev.flatlaf.FlatDarkLaf; -import com.formdev.flatlaf.FlatLightLaf; - -import generic.theme.builtin.*; -import generic.theme.laf.LookAndFeelManager; -import ghidra.framework.OperatingSystem; -import ghidra.framework.Platform; -import ghidra.util.Msg; -import ghidra.util.classfinder.ClassSearcher; -import ghidra.util.datastruct.WeakDataStructureFactory; -import ghidra.util.datastruct.WeakSet; -import resources.ResourceManager; -import utilities.util.reflection.ReflectionUtilities; +import javax.swing.Icon; +import javax.swing.LookAndFeel; /** * Provides a static set of methods for globally managing application themes and their values. @@ -53,267 +36,15 @@ import utilities.util.reflection.ReflectionUtilities; * */ public class Gui { - public static final String BACKGROUND_KEY = "color.bg.text"; - - private static GTheme activeTheme = getDefaultTheme(); - private static Set allThemes = null; - - private static GThemeValueMap applicationDefaults = new GThemeValueMap(); - private static GThemeValueMap applicationDarkDefaults = new GThemeValueMap(); - private static GThemeValueMap javaDefaults = new GThemeValueMap(); - private static GThemeValueMap currentValues = new GThemeValueMap(); - private static GThemeValueMap systemValues = new GThemeValueMap(); - - private static ThemeFileLoader themeFileLoader = new ThemeFileLoader(); - private static ThemePreferenceManager themePreferenceManager = new ThemePreferenceManager(); - - private static Map gColorMap = new HashMap<>(); - private static boolean isInitialized; - private static Map gIconMap = new HashMap<>(); - - // these notifications are only when the user is manipulating theme values, so rare and at - // user speed, so using copy on read - private static WeakSet themeListeners = - WeakDataStructureFactory.createCopyOnReadWeakSet(); - - // stores the original value for ids whose value has changed from the current theme - private static GThemeValueMap changedValuesMap = new GThemeValueMap(); - private static LookAndFeelManager lookAndFeelManager; - static Font DEFAULT_FONT = new Font("Dialog", Font.PLAIN, 12); + // Start with an StubThemeManager so that simple tests can operate without having + // to initialize the theme system. Applications and integration tests will + // called ThemeManager.initialize() which will replace this with a fully initialized version. + private static ThemeManager themeManager = new StubThemeManager(); private Gui() { // static utils class, can't construct } - /** - * Initialized the Theme and its values for the application. - */ - public static void initialize() { - isInitialized = true; - installFlatLookAndFeels(); - loadThemeDefaults(); - setTheme(themePreferenceManager.getTheme()); - } - - /** - * Reloads the defaults from all the discoverable theme.property files. - */ - public static void reloadApplicationDefaults() { - loadThemeDefaults(); - buildCurrentValues(); - lookAndFeelManager.resetAll(javaDefaults); - notifyThemeChanged(new AllValuesChangedThemeEvent(false)); - } - - /** - * Restores all the current application back to the values as specified by the active theme. - * In other words, reverts any changes to the active theme that haven't been saved. - */ - public static void restoreThemeValues() { - buildCurrentValues(); - lookAndFeelManager.resetAll(javaDefaults); - notifyThemeChanged(new AllValuesChangedThemeEvent(false)); - } - - /** - * Restores the current color value for the given color id to the value established by the - * current theme. - * @param id the color id to restore back to the original theme value - */ - public static void restoreColor(String id) { - if (changedValuesMap.containsColor(id)) { - Gui.setColor(changedValuesMap.getColor(id)); - } - } - - /** - * Restores the current font value for the given font id to the value established by the - * current theme. - * @param id the font id to restore back to the original theme value - */ - public static void restoreFont(String id) { - if (changedValuesMap.containsFont(id)) { - Gui.setFont(changedValuesMap.getFont(id)); - } - } - - /** - * Restores the current icon value for the given icon id to the value established by the - * current theme. - * @param id the icon id to restore back to the original theme value - */ - public static void restoreIcon(String id) { - if (changedValuesMap.containsIcon(id)) { - Gui.setIcon(changedValuesMap.getIcon(id)); - } - } - - /** - * Returns true if the color associated with the given id has been changed from the current - * theme value for that id. - * @param id the color id to check if it has been changed - * @return true if the color associated with the given id has been changed from the current - * theme value for that id. - */ - public static boolean isChangedColor(String id) { - return changedValuesMap.containsColor(id); - } - - /** - * Returns true if the font associated with the given id has been changed from the current - * theme value for that id. - * @param id the font id to check if it has been changed - * @return true if the font associated with the given id has been changed from the current - * theme value for that id. - */ - public static boolean isChangedFont(String id) { - return changedValuesMap.containsFont(id); - } - - /** - * Returns true if the Icon associated with the given id has been changed from the current - * theme value for that id. - * @param id the Icon id to check if it has been changed - * @return true if the Icon associated with the given id has been changed from the current - * theme value for that id. - */ - public static boolean isChangedIcon(String id) { - return changedValuesMap.containsIcon(id); - } - - /** - * Sets the application's active theme to the given theme. - * @param theme the theme to make active - */ - public static void setTheme(GTheme theme) { - if (theme.hasSupportedLookAndFeel()) { - activeTheme = theme; - LafType lookAndFeel = theme.getLookAndFeelType(); - lookAndFeelManager = lookAndFeel.getLookAndFeelManager(); - try { - lookAndFeelManager.installLookAndFeel(); - themePreferenceManager.saveThemeToPreferences(theme); - notifyThemeChanged(new AllValuesChangedThemeEvent(true)); - } - catch (Exception e) { - Msg.error(Gui.class, - "Error setting LookAndFeel: " + lookAndFeel.getName(), e); - } - } - currentValues.checkForUnresolvedReferences(); - } - - /** - * Adds the given theme to set of all themes. - * @param newTheme the theme to add - */ - public static void addTheme(GTheme newTheme) { - loadThemes(); - allThemes.remove(newTheme); - allThemes.add(newTheme); - } - - /** - * Removes the theme from the set of all themes. Also, if the theme has an associated - * file, the file will be deleted. - * @param theme the theme to delete - */ - public static void deleteTheme(GTheme theme) { - File file = theme.getFile(); - if (file != null) { - file.delete(); - } - if (allThemes != null) { - allThemes.remove(theme); - } - } - - /** - * Returns a set of all known themes. - * @return a set of all known themes. - */ - public static Set 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 getSupportedThemes() { - loadThemes(); - Set 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 first = - getAllThemes().stream().filter(t -> t.getName().equals(themeName)).findFirst(); - return first.orElse(null); - } - - /** - * Returns a {@link GThemeValueMap} of all current theme values. - * @return a {@link GThemeValueMap} of all current theme values. - */ - public static GThemeValueMap getAllValues() { - return new GThemeValueMap(currentValues); - } - - /** - * Returns the theme values as defined by the current theme, ignoring any unsaved changes that - * are currently applied to the application. - * @return the theme values as defined by the current theme, ignoring any unsaved changes that - * are currently applied to the application. - */ - public static GThemeValueMap getThemeValues() { - GThemeValueMap map = new GThemeValueMap(); - map.load(javaDefaults); - map.load(systemValues); - map.load(applicationDefaults); - if (activeTheme.useDarkDefaults()) { - map.load(applicationDarkDefaults); - } - map.load(activeTheme); - return map; - } - - /** - * Returns a {@link GThemeValueMap} contains all values that differ from the default - * values (values defined by the {@link LookAndFeel} or in the theme.properties files. - * @return a {@link GThemeValueMap} contains all values that differ from the defaults. - */ - public static GThemeValueMap getNonDefaultValues() { - return currentValues.getChangedValues(getDefaults()); - } - /** * Returns the current {@link Font} associated with the given id. A default font will be * returned if the font can't be resolved and an error message will be printed to the console. @@ -321,32 +52,7 @@ public class Gui { * @return the current {@link Font} associated with the given id. */ public static Font getFont(String id) { - Font font = getFont(id, true); - if (font == FontValue.LAST_RESORT_DEFAULT) { - return null; - } - return font; - } - - /** - * Returns the current {@link Font} associated with the given id. - * @param id the id for the desired font - * @param validate if true, will print an error message to the console if the id can't be - * resolved - * @return the current {@link Font} associated with the given id. - */ - public static Font getFont(String id, boolean validate) { - FontValue font = currentValues.getFont(id); - - if (font == null) { - if (validate && isInitialized) { - Throwable t = getFilteredTrace(); - Msg.error(Gui.class, - "No color value registered for: '" + id + "'", t); - } - return DEFAULT_FONT; - } - return font.get(currentValues); + return themeManager.getFont(id); } /** @@ -356,221 +62,7 @@ public class Gui { * @return the {@link Color} registered for the given id. */ public static Color getColor(String id) { - return getColor(id, true); - } - - /** - * Updates the current font for the given id. - * @param id the font id to update to the new color - * @param font the new font for the id - */ - public static void setFont(String id, Font font) { - setFont(new FontValue(id, font)); - } - - /** - * Updates the current value for the font id in the newValue - * @param newValue the new {@link FontValue} to install in the current values. - */ - public static void setFont(FontValue newValue) { - FontValue currentValue = currentValues.getFont(newValue.getId()); - if (newValue.equals(currentValue)) { - return; - } - updateChangedValuesMap(currentValue, newValue); - - currentValues.addFont(newValue); - notifyThemeChanged(new FontChangedThemeEvent(currentValues, newValue)); - - // update all java LookAndFeel fonts affected by this changed - String id = newValue.getId(); - Set 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 changedIconIds = findChangedJavaIconIds(id); - Icon newIcon = newValue.get(currentValues); - lookAndFeelManager.iconsChanged(changedIconIds, newIcon); - } - - /** - * gets a UIResource version of the GColor for the given id. Using this method ensures that - * the same instance is used for a given id. This combats some poor code in some of the - * {@link LookAndFeel}s where the use == in some places to test for equals. - * @param id the id to get a GColorUIResource for - * @return a GColorUIResource for the given id - */ - public static GColorUIResource getGColorUiResource(String id) { - GColorUIResource gColor = gColorMap.get(id); - if (gColor == null) { - gColor = new GColorUIResource(id); - gColorMap.put(id, gColor); - } - return gColor; - } - - /** - * gets a UIResource version of the GIcon for the given id. Using this method ensures that - * the same instance is used for a given id. This combats some poor code in some of the - * {@link LookAndFeel}s where the use == in some places to test for equals. - * @param id the id to get a {@link GIconUIResource} for - * @return a GIconUIResource for the given id - */ - public static GIconUIResource getGIconUiResource(String id) { - - GIconUIResource gIcon = gIconMap.get(id); - if (gIcon == null) { - gIcon = new GIconUIResource(id); - gIconMap.put(id, gIcon); - } - return gIcon; - } - - // used by - public static void setSystemDefaults(GThemeValueMap map) { - systemValues = map; - } - - /** - * Sets the map of JavaDefaults defined by the current {@link LookAndFeel}. - * @param map the default theme values defined by the {@link LookAndFeel} - */ - public static void setJavaDefaults(GThemeValueMap map) { - javaDefaults = map; - buildCurrentValues(); - GColor.refreshAll(); - GIcon.refreshAll(); - } - - /** - * Returns the {@link GThemeValueMap} containing all the default theme values defined by the - * current {@link LookAndFeel}. - * @return the {@link GThemeValueMap} containing all the default theme values defined by the - * current {@link LookAndFeel} - */ - public static GThemeValueMap getJavaDefaults() { - GThemeValueMap map = new GThemeValueMap(); - map.load(javaDefaults); - return map; - } - - /** - * Returns the {@link GThemeValueMap} containing all the dark default values defined - * in theme.properties files. Note that dark defaults includes light defaults that haven't - * been overridden by a dark default with the same id. - * @return the {@link GThemeValueMap} containing all the dark values defined in - * theme.properties files - */ - public static GThemeValueMap getApplicationDarkDefaults() { - GThemeValueMap map = new GThemeValueMap(applicationDefaults); - map.load(applicationDarkDefaults); - return map; - } - - /** - * Returns the {@link GThemeValueMap} containing all the standard default values defined - * in theme.properties files. - * @return the {@link GThemeValueMap} containing all the standard values defined in - * theme.properties files - */ - public static GThemeValueMap getApplicationLightDefaults() { - GThemeValueMap map = new GThemeValueMap(applicationDefaults); - return map; - } - - /** - * Returns a {@link GThemeValueMap} containing all default values for the current theme. It - * is a combination of application defined defaults and java {@link LookAndFeel} defaults. - * @return the current set of defaults. - */ - public static GThemeValueMap getDefaults() { - GThemeValueMap currentDefaults = new GThemeValueMap(javaDefaults); - currentDefaults.load(systemValues); - currentDefaults.load(applicationDefaults); - if (activeTheme.useDarkDefaults()) { - currentDefaults.load(applicationDarkDefaults); - } - return currentDefaults; - } - - /** - * Returns true if the given UI object is using the Aqua Look and Feel. - * @param UI the UI to examine. - * @return true if the UI is using Aqua - */ - public static boolean isUsingAquaUI(ComponentUI UI) { - return activeTheme.getLookAndFeelType() == LafType.MAC; - } - - /** - * Returns true if 'Nimbus' is the current Look and Feel - * @return true if 'Nimbus' is the current Look and Feel - */ - public static boolean isUsingNimbusUI() { - return activeTheme.getLookAndFeelType() == LafType.NIMBUS; + return themeManager.getColor(id); } /** @@ -578,7 +70,7 @@ public class Gui { * @param listener the listener to be notified */ public static void addThemeListener(ThemeListener listener) { - themeListeners.add(listener); + themeManager.addThemeListener(listener); } /** @@ -587,54 +79,7 @@ public class Gui { * @param listener the listener to be removed */ public static void removeThemeListener(ThemeListener listener) { - themeListeners.add(listener); - } - - /** - * Returns the default theme for the current platform. - * @return the default theme for the current platform. - */ - public static GTheme getDefaultTheme() { - OperatingSystem OS = Platform.CURRENT_PLATFORM.getOperatingSystem(); - switch (OS) { - case MAC_OS_X: - return new MacTheme(); - case WINDOWS: - return new WindowsTheme(); - case LINUX: - case UNSUPPORTED: - default: - return new NimbusTheme(); - } - } - - /** - * Returns true if there are any unsaved changes to the current theme. - * @return true if there are any unsaved changes to the current theme. - */ - public static boolean hasThemeChanges() { - return !changedValuesMap.isEmpty(); - } - - /** - * Returns the color for the id. If there is no color registered for this id, then Color.CYAN - * is returned as the default color. - * @param id the id to get the direct color for - * @param validate if true, will output an error if the id can't be resolved at this time - * @return the actual direct color for the id, not a GColor - */ - public static Color getColor(String id, boolean validate) { - ColorValue color = currentValues.getColor(id); - - if (color == null) { - if (validate && isInitialized) { - Throwable t = getFilteredTrace(); - Msg.error(Gui.class, - "No color value registered for: '" + id + "'", t); - } - return Color.CYAN; - } - return color.get(currentValues); + themeManager.removeThemeListener(listener); } /** @@ -644,7 +89,7 @@ public class Gui { * @return the actual icon registered for the given id */ public static Icon getIcon(String id) { - return getIcon(id, true); + return themeManager.getIcon(id); } /** @@ -653,7 +98,7 @@ public class Gui { * @return true if an color for the given Id has been defined */ public static boolean hasColor(String id) { - return currentValues.containsColor(id); + return themeManager.hasColor(id); } /** @@ -662,7 +107,7 @@ public class Gui { * @return true if an font for the given Id has been defined */ public static boolean hasFont(String id) { - return currentValues.containsFont(id); + return themeManager.hasFont(id); } /** @@ -671,27 +116,7 @@ public class Gui { * @return true if an icon for the given Id has been defined */ public static boolean hasIcon(String id) { - return currentValues.containsIcon(id); - } - - /** - * Returns the {@link Icon} registered for the given id. If no icon is registered, returns - * the default icon (bomb). - * @param id the id to get the register icon for - * @param validate if true, will output an error if the id can't be resolved at this time - * @return the Icon registered for the given id - */ - public static Icon getIcon(String id, boolean validate) { - IconValue icon = currentValues.getIcon(id); - if (icon == null) { - if (validate && isInitialized) { - Throwable t = getFilteredTrace(); - Msg.error(Gui.class, - "No icon value registered for: '" + id + "'", t); - } - return ResourceManager.getDefaultIcon(); - } - return icon.get(currentValues); + return themeManager.hasIcon(id); } /** @@ -700,7 +125,7 @@ public class Gui { * @return a darker version of the given color or brighter if the current theme is dark */ public static Color darker(Color color) { - if (activeTheme.useDarkDefaults()) { + if (isDarkTheme()) { return color.brighter(); } return color.darker(); @@ -712,7 +137,7 @@ public class Gui { * @return a brighter version of the given color or darker if the current theme is dark */ public static Color brighter(Color color) { - if (activeTheme.useDarkDefaults()) { + if (isDarkTheme()) { return color.darker(); } return color.brighter(); @@ -725,137 +150,19 @@ public class Gui { * @param fontId the id of the font to register with the given component */ public static void registerFont(Component component, String fontId) { - lookAndFeelManager.registerFont(component, fontId); + themeManager.registerFont(component, fontId); } - private static void installFlatLookAndFeels() { - UIManager.installLookAndFeel(LafType.FLAT_LIGHT.getName(), FlatLightLaf.class.getName()); - UIManager.installLookAndFeel(LafType.FLAT_DARK.getName(), FlatDarkLaf.class.getName()); + /** + * Returns true if the active theme is using dark defaults + * @return true if the active theme is using dark defaults + */ + public static boolean isDarkTheme() { + return themeManager.isDarkTheme(); } - private static void loadThemeDefaults() { - themeFileLoader.loadThemeDefaultFiles(); - applicationDefaults = themeFileLoader.getDefaults(); - applicationDarkDefaults = themeFileLoader.getDarkDefaults(); - } - - private static void notifyThemeChanged(ThemeEvent event) { - for (ThemeListener listener : themeListeners) { - listener.themeChanged(event); - } - } - - private static Throwable getFilteredTrace() { - Throwable t = ReflectionUtilities.createThrowableWithStackOlderThan(); - StackTraceElement[] trace = t.getStackTrace(); - StackTraceElement[] filtered = - ReflectionUtilities.filterStackTrace(trace, "java.", "theme.Gui", "theme.GColor"); - t.setStackTrace(filtered); - return t; - } - - private static void buildCurrentValues() { - GThemeValueMap map = new GThemeValueMap(); - - map.load(javaDefaults); - map.load(systemValues); - map.load(applicationDefaults); - if (activeTheme.useDarkDefaults()) { - map.load(applicationDarkDefaults); - } - map.load(activeTheme); - currentValues = map; - changedValuesMap.clear(); - } - - private static void loadThemes() { - if (allThemes == null) { - Set set = new HashSet<>(); - set.addAll(findDiscoverableThemes()); - set.addAll(themeFileLoader.loadThemeFiles()); - allThemes = set; - } - } - - private static Collection 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 findChangedJavaFontIds(String id) { - Set affectedIds = new HashSet<>(); - List 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 findChangedJavaIconIds(String id) { - Set affectedIds = new HashSet<>(); - List icons = javaDefaults.getIcons(); - for (IconValue iconValue : icons) { - String iconId = iconValue.getId(); - if (iconId.equals(id) || iconValue.inheritsFrom(id, currentValues)) { - affectedIds.add(iconId); - } - } - return affectedIds; - } - - // for testing - public static void setPropertiesLoader(ThemeFileLoader loader) { - allThemes = null; - themeFileLoader = loader; - } - - public static void setThemePreferenceManager(ThemePreferenceManager manager) { - themePreferenceManager = manager; + static void setThemeManager(ThemeManager manager) { + themeManager = manager; } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/IconValue.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/IconValue.java index 5018daacc1..ae5479e5e8 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/IconValue.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/IconValue.java @@ -247,8 +247,8 @@ public class IconValue extends ThemeValue { } @Override - public void installValue() { - Gui.setIcon(this); + public void installValue(ThemeManager themeManager) { + themeManager.setIcon(this); } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/LafType.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/LafType.java index 0d80c38610..c8fc06d5c2 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/LafType.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/LafType.java @@ -85,34 +85,36 @@ public enum LafType { /** * Returns a LookAndFeelManager that can install and update the {@link LookAndFeel} associated * with this LafType. + * @param themeManager The application ThemeManager * @return a LookAndFeelManager that can install and update the {@link LookAndFeel} associated * with this LafType. */ - public LookAndFeelManager getLookAndFeelManager() { - return getManager(this); + public LookAndFeelManager getLookAndFeelManager(ApplicationThemeManager themeManager) { + return createManager(this, themeManager); } - private static LookAndFeelManager getManager(LafType lookAndFeel) { - switch (lookAndFeel) { + private static LookAndFeelManager createManager(LafType type, + ApplicationThemeManager themeManager) { + switch (type) { case MAC: - return new MacLookAndFeelManager(); + return new MacLookAndFeelManager(themeManager); case METAL: - return new MetalLookAndFeelManager(); + return new MetalLookAndFeelManager(themeManager); case WINDOWS: - return new WindowsLookAndFeelManager(); + return new WindowsLookAndFeelManager(themeManager); case WINDOWS_CLASSIC: - return new WindowsClassicLookAndFeelManager(); + return new WindowsClassicLookAndFeelManager(themeManager); case GTK: - return new GtkLookAndFeelManager(); + return new GtkLookAndFeelManager(themeManager); case MOTIF: - return new MotifLookAndFeelManager(); + return new MotifLookAndFeelManager(themeManager); case NIMBUS: - return new NimbusLookAndFeelManager(); + return new NimbusLookAndFeelManager(themeManager); case FLAT_DARK: case FLAT_LIGHT: - return new FlatLookAndFeelManager(lookAndFeel); + return new FlatLookAndFeelManager(type, themeManager); default: - throw new AssertException("No lookAndFeelManager defined for " + lookAndFeel); + throw new AssertException("No lookAndFeelManager defined for " + type); } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/StubThemeManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/StubThemeManager.java new file mode 100644 index 0000000000..82c56dfa90 --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/StubThemeManager.java @@ -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 getAllThemes() { + throw new UnsupportedOperationException(); + } + + @Override + public Set 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)); + } + +} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeManager.java new file mode 100644 index 0000000000..ad5720773f --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeManager.java @@ -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()}. + *

+ * 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 .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 .theme file. + *

+ * 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 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 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 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); + } +} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePreferenceManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePreferences.java similarity index 93% rename from Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePreferenceManager.java rename to Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePreferences.java index ab2051c202..6c8f84d556 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePreferenceManager.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePreferences.java @@ -24,7 +24,7 @@ import ghidra.util.Msg; /** * Reads and writes current theme info to preferences */ -public class ThemePreferenceManager { +public class ThemePreferences { private static final String THEME_PREFFERENCE_KEY = "Theme"; /** @@ -32,7 +32,7 @@ public class ThemePreferenceManager { * @return the last theme used (stored in preferences) or the default theme if not stored * in preferences */ - public GTheme getTheme() { + public GTheme load() { String themeId = Preferences.getProperty(THEME_PREFFERENCE_KEY, "Default", true); if (themeId.startsWith(GTheme.FILE_PREFIX)) { String filename = themeId.substring(GTheme.FILE_PREFIX.length()); @@ -55,14 +55,14 @@ public class ThemePreferenceManager { "Can't find or instantiate class: " + className, e); } } - return Gui.getDefaultTheme(); + return ThemeManager.getDefaultTheme(); } /** * Saves the current theme choice to {@link Preferences}. * @param theme the theme to remember in {@link Preferences} */ - public void saveThemeToPreferences(GTheme theme) { + public void save(GTheme theme) { Preferences.setProperty(THEME_PREFFERENCE_KEY, theme.getThemeLocater()); Preferences.store(); } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeValue.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeValue.java index 3ccc6c1a92..5d45bcd1dc 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeValue.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeValue.java @@ -73,7 +73,10 @@ public abstract class ThemeValue implements Comparable> { /** * Returns the T value for this instance, following references as needed. Uses the given - * preferredValues map to resolve references. + * preferredValues map to resolve references. If the value can't be resolved by following + * reference chains, an error stack trace will be generated and the default T value will + * be returned. In rare situations where it is acceptable for the value to not be resolvable, + * use the {@link #hasResolvableValue(GThemeValueMap)} method first. * @param values the {@link GThemeValueMap} used to resolve references if this * instance doesn't have an actual value. * @return the T value for this instance, following references as needed. @@ -85,23 +88,55 @@ public abstract class ThemeValue implements Comparable> { Set visitedKeys = new HashSet<>(); visitedKeys.add(id); - ThemeValue parent = getReferredValue(values, referenceId); + ThemeValue referred = getReferredValue(values, referenceId); // loop resolving indirect references - while (parent != null) { - if (parent.value != null) { - return parent.value; + while (referred != null) { + if (referred.value != null) { + return referred.value; } - visitedKeys.add(parent.id); - if (visitedKeys.contains(parent.referenceId)) { + visitedKeys.add(referred.id); + if (visitedKeys.contains(referred.referenceId)) { Msg.warn(this, "Theme value reference loop detected for key: " + id); - return getUnresolvedReferenceValue(id, parent.referenceId); + return getUnresolvedReferenceValue(id, referred.referenceId); } - parent = getReferredValue(values, parent.referenceId); + referred = getReferredValue(values, referred.referenceId); } return getUnresolvedReferenceValue(id, referenceId); } + /** + * Returns true if the ThemeValue can resolve to the concrete T value (color, font, or icon) + * from the given set of theme values. + * @param values the set of values to use to try and follow reference chains to ultimately + * resolve the ThemeValue to a an actual T value + * @return true if the ThemeValue can resolve to the concrete T value (color, font, or icon) + * from the given set of theme values. + */ + public boolean hasResolvableValue(GThemeValueMap values) { + if (value != null) { + return true; + } + + Set visitedKeys = new HashSet<>(); + visitedKeys.add(id); + ThemeValue referred = getReferredValue(values, referenceId); + + // loop resolving indirect references + while (referred != null) { + if (referred.value != null) { + return true; + } + visitedKeys.add(referred.id); + if (visitedKeys.contains(referred.referenceId)) { + Msg.warn(this, "Theme value reference loop detected for key: " + id); + return false; + } + referred = getReferredValue(values, referred.referenceId); + } + return false; + } + /** * Returns true if this ThemeValue derives its value from the given ancestorId. * @param ancestorId the id to test if this Theme value inherits from @@ -208,7 +243,8 @@ public abstract class ThemeValue implements Comparable> { /** * 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); } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/FlatLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/FlatLookAndFeelManager.java index 778099ddfd..39c61836d4 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/FlatLookAndFeelManager.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/FlatLookAndFeelManager.java @@ -17,13 +17,12 @@ package generic.theme.laf; import javax.swing.UIManager; -import generic.theme.ColorValue; -import generic.theme.LafType; +import generic.theme.*; public class FlatLookAndFeelManager extends LookAndFeelManager { - public FlatLookAndFeelManager(LafType laf) { - super(laf); + public FlatLookAndFeelManager(LafType laf, ApplicationThemeManager themeManager) { + super(laf, themeManager); // establish system color to LookAndFeel colors systemToLafMap.addColor(new ColorValue(SYSTEM_WIDGET_BACKGROUND_COLOR_ID, "text")); diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GNimbusLookAndFeel.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GNimbusLookAndFeel.java index 575840e6ef..e3f4528c02 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GNimbusLookAndFeel.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GNimbusLookAndFeel.java @@ -19,18 +19,32 @@ import java.awt.Color; import java.awt.Font; import java.util.List; -import javax.swing.Icon; -import javax.swing.UIDefaults; +import javax.swing.*; import javax.swing.plaf.FontUIResource; import javax.swing.plaf.nimbus.NimbusLookAndFeel; import generic.theme.*; /** - * Extends the NimbusLookAndFeel to intercept the {@link #getDefaults()}. To get Nimbus - * to use our indirect values, we have to get in early. + * Extends the {@link NimbusLookAndFeel} to intercept the {@link #getDefaults()}. Nimbus does + * not honor changes to the UIDefaults after it is installed as the active + * {@link LookAndFeel}, so we have to make the changes at the time the UIDefaults are installed. + * + * To get around this issue, we extend the NimbusLookAndFeel + * so that we can install our GColors and overridden properties as Nimbus is being installed, + * specifically during the call to the getDefaults() method. For all other Look And Feels, the + * GColors and overridden properties are changed in the UIDefaults after the Look And Feel is + * installed, so they don't need to extends the Look and Feel class. + * + * Also, note that Nimbus needs to be reinstalled every time we need to make a change to any of the + * UIDefaults values, since it does not respond to changes other than when first installed. */ public class GNimbusLookAndFeel extends NimbusLookAndFeel { + private ApplicationThemeManager themeManager; + + GNimbusLookAndFeel(ApplicationThemeManager themeManager) { + this.themeManager = themeManager; + } @Override public UIDefaults getDefaults() { @@ -40,13 +54,13 @@ public class GNimbusLookAndFeel extends NimbusLookAndFeel { // replace all colors with GColors for (ColorValue colorValue : javaDefaults.getColors()) { String id = colorValue.getId(); - defaults.put(id, Gui.getGColorUiResource(id)); + defaults.put(id, themeManager.getGColorUiResource(id)); } // put fonts back into defaults in case they have been changed by the current theme for (FontValue fontValue : javaDefaults.getFonts()) { String id = fontValue.getId(); - Font font = Gui.getFont(id); + Font font = themeManager.getFont(id); defaults.put(id, new FontUIResource(font)); } @@ -55,13 +69,12 @@ public class GNimbusLookAndFeel extends NimbusLookAndFeel { String id = iconValue.getId(); // because some icons are weird, put raw icons into defaults, only use GIcons for // setting Icons explicitly on components - Icon icon = Gui.getIcon(id, true); + Icon icon = themeManager.getIcon(id); defaults.put(id, icon); } - defaults.put("Label.textForeground", Gui.getGColorUiResource("Label.foreground")); - GColor.refreshAll(); - GIcon.refreshAll(); + defaults.put("Label.textForeground", themeManager.getGColorUiResource("Label.foreground")); + themeManager.refreshGThemeValues(); return defaults; } @@ -90,7 +103,7 @@ public class GNimbusLookAndFeel extends NimbusLookAndFeel { } // need to set javaDefalts now to trigger building currentValues so the when // we create GColors below, they can be resolved. - Gui.setJavaDefaults(javaDefaults); + themeManager.setJavaDefaults(javaDefaults); return javaDefaults; } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelManager.java index f0853d0840..edf22b089a 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelManager.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelManager.java @@ -15,6 +15,7 @@ */ package generic.theme.laf; +import generic.theme.ApplicationThemeManager; import generic.theme.LafType; /** @@ -22,7 +23,7 @@ import generic.theme.LafType; */ public class GtkLookAndFeelManager extends LookAndFeelManager { - public GtkLookAndFeelManager() { - super(LafType.GTK); + public GtkLookAndFeelManager(ApplicationThemeManager themeManager) { + super(LafType.GTK, themeManager); } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelManager.java index 1129c1e2c0..3ba62a1971 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelManager.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelManager.java @@ -51,9 +51,11 @@ public abstract class LookAndFeelManager { private LafType laf; private Map fontRegistryMap = new HashMap<>(); protected GThemeValueMap systemToLafMap = new GThemeValueMap(); + protected ApplicationThemeManager themeManager; - protected LookAndFeelManager(LafType laf) { + protected LookAndFeelManager(LafType laf, ApplicationThemeManager themeManager) { this.laf = laf; + this.themeManager = themeManager; // establish system color to LookAndFeel colors systemToLafMap.addColor(new ColorValue(SYSTEM_APP_BACKGROUND_COLOR_ID, "control")); @@ -84,7 +86,7 @@ public abstract class LookAndFeelManager { IllegalAccessException, UnsupportedLookAndFeelException { cleanUiDefaults(); - Gui.setSystemDefaults(systemToLafMap); + themeManager.setSystemDefaults(systemToLafMap); doInstallLookAndFeel(); installJavaDefaults(); fixupLookAndFeelIssues(); @@ -98,8 +100,7 @@ public abstract class LookAndFeelManager { * special as needed by the current {@link LookAndFeel} */ public void resetAll(GThemeValueMap javaDefaults) { - GColor.refreshAll(); - GIcon.refreshAll(); + themeManager.refreshGThemeValues(); resetIcons(javaDefaults); resetFonts(javaDefaults); updateAllRegisteredComponentFonts(); @@ -130,7 +131,7 @@ public abstract class LookAndFeelManager { UIDefaults defaults = UIManager.getDefaults(); for (IconValue iconValue : icons) { String id = iconValue.getId(); - Icon correctIcon = Gui.getIcon(id, false); + Icon correctIcon = Gui.getIcon(id); Icon storedIcon = defaults.getIcon(id); if (correctIcon != null && !correctIcon.equals(storedIcon)) { defaults.put(id, correctIcon); @@ -142,7 +143,7 @@ public abstract class LookAndFeelManager { * Called when one or more colors have changed. */ public void colorsChanged() { - GColor.refreshAll(); + themeManager.refreshGThemeValues(); repaintAll(); } @@ -162,7 +163,7 @@ public abstract class LookAndFeelManager { } updateComponentUis(); } - GIcon.refreshAll(); + themeManager.refreshGThemeValues(); repaintAll(); } @@ -265,7 +266,7 @@ public abstract class LookAndFeelManager { GThemeValueMap javaDefaults = extractJavaDefaults(); ThemeGrouper grouper = getThemeGrouper(); grouper.group(javaDefaults); - Gui.setJavaDefaults(javaDefaults); + themeManager.setJavaDefaults(javaDefaults); installPropertiesBackIntoUiDefaults(javaDefaults); } @@ -276,7 +277,7 @@ public abstract class LookAndFeelManager { private void installPropertiesBackIntoUiDefaults(GThemeValueMap javaDefaults) { UIDefaults defaults = UIManager.getDefaults(); - GTheme theme = Gui.getActiveTheme(); + GTheme theme = themeManager.getActiveTheme(); // we replace java default colors with GColor equivalents so that we // can change colors without having to reinstall ui on each component @@ -284,7 +285,7 @@ public abstract class LookAndFeelManager { // allow being wrapped like colors do. for (ColorValue colorValue : javaDefaults.getColors()) { String id = colorValue.getId(); - defaults.put(id, Gui.getGColorUiResource(id)); + defaults.put(id, themeManager.getGColorUiResource(id)); } // put fonts back into defaults in case they have been changed by the current theme @@ -304,7 +305,7 @@ public abstract class LookAndFeelManager { if (themeValue != null) { // because some icons are weird, put raw icons into defaults, only use GIcons for // setting Icons explicitly on components - Icon icon = Gui.getIcon(id, true); + Icon icon = Gui.getIcon(id); defaults.put(id, icon); } } @@ -463,7 +464,7 @@ public abstract class LookAndFeelManager { } private void cleanUiDefaults() { - GThemeValueMap javaDefaults = Gui.getJavaDefaults(); + GThemeValueMap javaDefaults = themeManager.getJavaDefaults(); if (javaDefaults == null) { return; } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MacLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MacLookAndFeelManager.java index 7262d8914e..7c6caf885f 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MacLookAndFeelManager.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MacLookAndFeelManager.java @@ -15,12 +15,13 @@ */ package generic.theme.laf; +import generic.theme.ApplicationThemeManager; import generic.theme.LafType; public class MacLookAndFeelManager extends LookAndFeelManager { - public MacLookAndFeelManager() { - super(LafType.MAC); + public MacLookAndFeelManager(ApplicationThemeManager themeManager) { + super(LafType.MAC, themeManager); } @Override diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MetalLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MetalLookAndFeelManager.java index 3b7e172517..0eb565025e 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MetalLookAndFeelManager.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MetalLookAndFeelManager.java @@ -15,11 +15,12 @@ */ package generic.theme.laf; +import generic.theme.ApplicationThemeManager; import generic.theme.LafType; public class MetalLookAndFeelManager extends LookAndFeelManager { - public MetalLookAndFeelManager() { - super(LafType.METAL); + public MetalLookAndFeelManager(ApplicationThemeManager themeManager) { + super(LafType.METAL, themeManager); } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MotifLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MotifLookAndFeelManager.java index ff5afdca9c..ba8421359e 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MotifLookAndFeelManager.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MotifLookAndFeelManager.java @@ -15,16 +15,15 @@ */ package generic.theme.laf; -import generic.theme.ColorValue; -import generic.theme.LafType; +import generic.theme.*; /** * Motif {@link LookAndFeelManager}. Specialized so that it can return the Motif installer */ public class MotifLookAndFeelManager extends LookAndFeelManager { - public MotifLookAndFeelManager() { - super(LafType.MOTIF); + public MotifLookAndFeelManager(ApplicationThemeManager themeManager) { + super(LafType.MOTIF, themeManager); // establish system color to LookAndFeel colors systemToLafMap.addColor(new ColorValue(SYSTEM_APP_BACKGROUND_COLOR_ID, "control")); systemToLafMap.addColor(new ColorValue(SYSTEM_WIDGET_BACKGROUND_COLOR_ID, "window")); diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelManager.java index da432acb63..e2d9414c31 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelManager.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelManager.java @@ -30,8 +30,8 @@ import ghidra.util.exception.AssertException; */ public class NimbusLookAndFeelManager extends LookAndFeelManager { - public NimbusLookAndFeelManager() { - super(LafType.NIMBUS); + public NimbusLookAndFeelManager(ApplicationThemeManager themeManager) { + super(LafType.NIMBUS, themeManager); // establish system color specific to Nimbus systemToLafMap.addColor(new ColorValue(SYSTEM_BORDER_COLOR_ID, "nimbusBorder")); @@ -39,8 +39,7 @@ public class NimbusLookAndFeelManager extends LookAndFeelManager { @Override public void resetAll(GThemeValueMap javaDefaults) { - GColor.refreshAll(); - GIcon.refreshAll(); + themeManager.refreshGThemeValues(); reinstallNimubus(); } @@ -58,16 +57,16 @@ public class NimbusLookAndFeelManager extends LookAndFeelManager { if (!affectedJavaIds.isEmpty()) { reinstallNimubus(); } - GIcon.refreshAll(); + themeManager.refreshGThemeValues(); repaintAll(); } private void reinstallNimubus() { try { - UIManager.setLookAndFeel(new GNimbusLookAndFeel() { + UIManager.setLookAndFeel(new GNimbusLookAndFeel(themeManager) { @Override protected GThemeValueMap extractJavaDefaults(UIDefaults defaults) { - return Gui.getJavaDefaults(); + return themeManager.getJavaDefaults(); } }); } @@ -79,13 +78,13 @@ public class NimbusLookAndFeelManager extends LookAndFeelManager { @Override protected void doInstallLookAndFeel() throws UnsupportedLookAndFeelException { - UIManager.setLookAndFeel(new GNimbusLookAndFeel()); + UIManager.setLookAndFeel(new GNimbusLookAndFeel(themeManager)); } @Override protected GThemeValueMap extractJavaDefaults() { // The GNimbusLookAndFeel already extracted the java defaults and installed them in the Gui - return Gui.getJavaDefaults(); + return themeManager.getJavaDefaults(); } @Override diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/WindowsClassicLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/WindowsClassicLookAndFeelManager.java index 7fcc199657..c6ba737d0e 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/WindowsClassicLookAndFeelManager.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/WindowsClassicLookAndFeelManager.java @@ -15,11 +15,12 @@ */ package generic.theme.laf; +import generic.theme.ApplicationThemeManager; import generic.theme.LafType; public class WindowsClassicLookAndFeelManager extends LookAndFeelManager { - public WindowsClassicLookAndFeelManager() { - super(LafType.WINDOWS_CLASSIC); + public WindowsClassicLookAndFeelManager(ApplicationThemeManager themeManager) { + super(LafType.WINDOWS_CLASSIC, themeManager); } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/WindowsLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/WindowsLookAndFeelManager.java index 43d9423a0c..ab86b2c231 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/WindowsLookAndFeelManager.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/WindowsLookAndFeelManager.java @@ -15,12 +15,13 @@ */ package generic.theme.laf; +import generic.theme.ApplicationThemeManager; import generic.theme.LafType; public class WindowsLookAndFeelManager extends LookAndFeelManager { - public WindowsLookAndFeelManager() { - super(LafType.WINDOWS); + public WindowsLookAndFeelManager(ApplicationThemeManager themeManager) { + super(LafType.WINDOWS, themeManager); } } diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/Options.java b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/Options.java index e9e666698e..821431476d 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/Options.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/Options.java @@ -108,7 +108,14 @@ public interface Options { /** * Registers an option with a description, help location, and a default value without specifying * the option type. This form requires that the default value not be null so that the option - * type can be inferred from the default value. + * type can be inferred from the default value. + *

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

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

+ * Note, this method should not be used for + * colors and font as doing so will result in those colors and fonts becoming disconnected + * to the current theme. Instead use + * {@link #registerThemeColorBinding(String, String, HelpLocation, String)} or + * {@link #registerThemeFontBinding(String, String, HelpLocation, String)}. + * * @param optionName the name of the option being registered. * @param type the OptionType for this options. * @param defaultValue the defaultValue for the option. In this version of the method, the default diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/ThemeColorOption.java b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/ThemeColorOption.java index 0b8e698279..91fec39a35 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/ThemeColorOption.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/ThemeColorOption.java @@ -17,8 +17,7 @@ package ghidra.framework.options; import java.awt.Color; -import generic.theme.GColor; -import generic.theme.Gui; +import generic.theme.*; import ghidra.util.HelpLocation; import ghidra.util.Msg; @@ -55,17 +54,17 @@ public class ThemeColorOption extends Option { @Override public void doSetCurrentValue(Object value) { - Gui.setColor(colorId, (Color) value); + ThemeManager.getInstance().setColor(colorId, (Color) value); } @Override public boolean isDefault() { - return !Gui.isChangedColor(colorId); + return !ThemeManager.getInstance().isChangedColor(colorId); } @Override public void restoreDefault() { - Gui.restoreColor(colorId); + ThemeManager.getInstance().restoreColor(colorId); } } diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/ThemeFontOption.java b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/ThemeFontOption.java index 428662bc66..f073273478 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/ThemeFontOption.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/ThemeFontOption.java @@ -18,6 +18,7 @@ package ghidra.framework.options; import java.awt.Font; import generic.theme.Gui; +import generic.theme.ThemeManager; import ghidra.util.HelpLocation; import ghidra.util.Msg; @@ -54,17 +55,17 @@ public class ThemeFontOption extends Option { @Override public void doSetCurrentValue(Object value) { - Gui.setFont(fontId, (Font) value); + ThemeManager.getInstance().setFont(fontId, (Font) value); } @Override public boolean isDefault() { - return !Gui.isChangedFont(fontId); + return !ThemeManager.getInstance().isChangedFont(fontId); } @Override public void restoreDefault() { - Gui.restoreFont(fontId); + ThemeManager.getInstance().restoreFont(fontId); } } diff --git a/Ghidra/Framework/Generic/src/main/java/resources/icons/DerivedImageIcon.java b/Ghidra/Framework/Generic/src/main/java/resources/icons/DerivedImageIcon.java index 4f4c2fbf7e..5605b28b69 100644 --- a/Ghidra/Framework/Generic/src/main/java/resources/icons/DerivedImageIcon.java +++ b/Ghidra/Framework/Generic/src/main/java/resources/icons/DerivedImageIcon.java @@ -23,6 +23,7 @@ import java.util.Objects; import javax.swing.Icon; import javax.swing.ImageIcon; +import generic.theme.GIcon; import generic.util.image.ImageUtils; import resources.ResourceManager; @@ -32,6 +33,7 @@ import resources.ResourceManager; public class DerivedImageIcon extends LazyImageIcon { private Icon sourceIcon; private Image sourceImage; + private Icon cachedDelegate; /** * Constructor for deriving from an icon @@ -56,6 +58,16 @@ public class DerivedImageIcon extends LazyImageIcon { return sourceIcon; } + protected boolean sourceIconChanged() { + if (sourceIcon instanceof GIcon gIcon) { + if (cachedDelegate != gIcon.getDelegate()) { + cachedDelegate = gIcon.getDelegate(); + return true; + } + } + return false; + } + protected ImageIcon createImageIcon() { Image image = createImage(); String imageName = getFilename(); diff --git a/Ghidra/Framework/Generic/src/main/java/resources/icons/LazyImageIcon.java b/Ghidra/Framework/Generic/src/main/java/resources/icons/LazyImageIcon.java index 8c02405404..0004f3446f 100644 --- a/Ghidra/Framework/Generic/src/main/java/resources/icons/LazyImageIcon.java +++ b/Ghidra/Framework/Generic/src/main/java/resources/icons/LazyImageIcon.java @@ -39,7 +39,7 @@ public abstract class LazyImageIcon extends ImageIcon implements FileBasedIcon { } private synchronized void init() { - if (!loaded) { + if (!loaded || sourceIconChanged()) { loaded = true; ImageIcon imageIcon = createImageIcon(); if (imageIcon == null) { @@ -52,6 +52,10 @@ public abstract class LazyImageIcon extends ImageIcon implements FileBasedIcon { protected abstract ImageIcon createImageIcon(); + protected boolean sourceIconChanged() { + return false; + } + @Override public String getFilename() { return getDescription(); diff --git a/Ghidra/Framework/Generic/src/test/java/generic/theme/GThemeTest.java b/Ghidra/Framework/Generic/src/test/java/generic/theme/GThemeTest.java index c8d60e963c..4c0d3801b5 100644 --- a/Ghidra/Framework/Generic/src/test/java/generic/theme/GThemeTest.java +++ b/Ghidra/Framework/Generic/src/test/java/generic/theme/GThemeTest.java @@ -44,13 +44,13 @@ public class GThemeTest extends AbstractGenericTest { @Before public void setUp() { - theme = Gui.getDefaultTheme(); + theme = new GTheme("TestTheme"); new Font("Courier", Font.BOLD, 12); } @Test public void testGetName() { - assertEquals(Gui.getDefaultTheme().getName(), theme.getName()); + assertEquals("TestTheme", theme.getName()); } @Test @@ -97,7 +97,7 @@ public class GThemeTest extends AbstractGenericTest { File file = createTempFile("themeTest", ".theme"); - theme.saveToFile(file, false); // saveToFile returns new theme instance + new ThemeWriter(theme).writeThemeToFile(file); theme = new ThemeReader(file).readTheme(); assertEquals("abc", theme.getName()); diff --git a/Ghidra/Framework/Generic/src/test/java/generic/theme/GuiTest.java b/Ghidra/Framework/Generic/src/test/java/generic/theme/ThemeManagerTest.java similarity index 65% rename from Ghidra/Framework/Generic/src/test/java/generic/theme/GuiTest.java rename to Ghidra/Framework/Generic/src/test/java/generic/theme/ThemeManagerTest.java index c65c8106b1..ec6ba59828 100644 --- a/Ghidra/Framework/Generic/src/test/java/generic/theme/GuiTest.java +++ b/Ghidra/Framework/Generic/src/test/java/generic/theme/ThemeManagerTest.java @@ -34,7 +34,7 @@ import generic.theme.builtin.*; import resources.ResourceManager; import resources.icons.UrlImageIcon; -public class GuiTest { +public class ThemeManagerTest { private Font FONT = new Font("Dialog", Font.PLAIN, 13); private Font SMALL_FONT = new Font("Dialog", Font.PLAIN, 4); @@ -49,9 +49,11 @@ public class GuiTest { private GTheme NIMBUS_THEME = new NimbusTheme(); private GTheme WINDOWS_THEME = new WindowsTheme(); private GTheme MAC_THEME = new MacTheme(); + private ThemeManager themeManager; @Before public void setUp() { + themes = new HashSet<>(); themes.add(METAL_THEME); themes.add(NIMBUS_THEME); @@ -66,43 +68,8 @@ public class GuiTest { darkDefaultValues.addColor(new ColorValue("color.test.bg", BLACK)); darkDefaultValues.addColor(new ColorValue("color.test.fg", BLUE)); + themeManager = new DummyApplicationThemeManager(); - Gui.setThemePreferenceManager(new ThemePreferenceManager() { - - @Override - public GTheme getTheme() { - return new MetalTheme(); - } - - @Override - public void saveThemeToPreferences(GTheme theme) { - // do nothing - } - }); - - Gui.setPropertiesLoader(new ThemeFileLoader() { - - @Override - public void loadThemeDefaultFiles() { - // do nothing - } - - @Override - public Collection loadThemeFiles() { - return new HashSet<>(themes); - } - - @Override - public GThemeValueMap getDefaults() { - return defaultValues; - } - - @Override - public GThemeValueMap getDarkDefaults() { - return darkDefaultValues; - } - }); - Gui.initialize(); } @Test @@ -110,12 +77,11 @@ public class GuiTest { GColor gColor = new GColor("color.test.bg"); assertColor(WHITE, gColor); - Gui.setTheme(new GTheme("Test", LafType.FLAT_DARK, true)); + themeManager.setTheme(new GTheme("Test", LafType.FLAT_DARK, true)); assertEquals(BLACK, gColor); - Gui.setTheme(new GTheme("Test2")); + themeManager.setTheme(new GTheme("Test2")); assertEquals(WHITE, gColor); - } @Test @@ -126,25 +92,25 @@ public class GuiTest { theme.setColor("color.test.bg", GREEN); assertColor(WHITE, gColor); - Gui.setTheme(theme); + themeManager.setTheme(theme); assertEquals(GREEN, gColor); - Gui.setTheme(new GTheme("Test2")); + themeManager.setTheme(new GTheme("Test2")); assertEquals(WHITE, gColor); } @Test public void testThemeFontOverride() { - assertEquals(FONT, Gui.getFont("font.test.foo")); + assertEquals(FONT, themeManager.getFont("font.test.foo")); GTheme theme = new GTheme("Test"); theme.setFont("font.test.foo", SMALL_FONT); - Gui.setTheme(theme); + themeManager.setTheme(theme); - assertEquals(SMALL_FONT, Gui.getFont("font.test.foo")); + assertEquals(SMALL_FONT, themeManager.getFont("font.test.foo")); - Gui.setTheme(new GTheme("Test2")); - assertEquals(FONT, Gui.getFont("font.test.foo")); + themeManager.setTheme(new GTheme("Test2")); + assertEquals(FONT, themeManager.getFont("font.test.foo")); } @Test @@ -155,10 +121,10 @@ public class GuiTest { theme.setIcon("icon.test.foo", ICON2); assertIcon(ICON1, gIcon); - Gui.setTheme(theme); + themeManager.setTheme(theme); assertIcon(ICON2, gIcon); - Gui.setTheme(new GTheme("Test2")); + themeManager.setTheme(new GTheme("Test2")); assertIcon(ICON1, gIcon); } @@ -168,7 +134,7 @@ public class GuiTest { assertColor(WHITE, gColor); defaultValues.addColor(new ColorValue("color.test.bg", YELLOW)); - Gui.reloadApplicationDefaults(); + themeManager.reloadApplicationDefaults(); assertEquals(YELLOW, gColor); } @@ -177,50 +143,50 @@ public class GuiTest { GColor gColor = new GColor("color.test.bg"); assertColor(WHITE, gColor); - Gui.setColor("color.test.bg", PURPLE); + themeManager.setColor("color.test.bg", PURPLE); assertColor(PURPLE, gColor); - Gui.restoreThemeValues(); + themeManager.restoreThemeValues(); assertEquals(WHITE, gColor); } @Test public void testGetAllThemes() { - assertEquals(themes, Gui.getAllThemes()); + assertEquals(themes, themeManager.getAllThemes()); } @Test public void testAddTheme() { GTheme newTheme = new GTheme("Test"); - Set allThemes = Gui.getAllThemes(); + Set allThemes = themeManager.getAllThemes(); assertEquals(themes.size(), allThemes.size()); assertFalse(allThemes.contains(newTheme)); - Gui.addTheme(newTheme); - allThemes = Gui.getAllThemes(); + themeManager.addTheme(newTheme); + allThemes = themeManager.getAllThemes(); assertTrue(allThemes.contains(newTheme)); } @Test public void testDeleteTheme() { GTheme newTheme = new GTheme("Test"); - Set allThemes = Gui.getAllThemes(); + Set allThemes = themeManager.getAllThemes(); assertFalse(allThemes.contains(newTheme)); - Gui.addTheme(newTheme); - allThemes = Gui.getAllThemes(); + themeManager.addTheme(newTheme); + allThemes = themeManager.getAllThemes(); assertTrue(allThemes.contains(newTheme)); - Gui.deleteTheme(newTheme); - allThemes = Gui.getAllThemes(); + themeManager.deleteTheme(newTheme); + allThemes = themeManager.getAllThemes(); assertFalse(allThemes.contains(newTheme)); } @Test public void testGetSupportedThemes() { - Set supportedThemes = Gui.getSupportedThemes(); + Set supportedThemes = themeManager.getSupportedThemes(); // since we put mac specific and windows specific themes, they can't all be here // regardless of the current platform assertTrue(supportedThemes.size() < themes.size()); @@ -231,31 +197,31 @@ public class GuiTest { @Test public void testGetLookAndFeelType() { - LafType lookAndFeelType = Gui.getLookAndFeelType(); + LafType lookAndFeelType = themeManager.getLookAndFeelType(); // in the test setup, we defaulted to the MetalLookAndFeel assertEquals(LafType.METAL, lookAndFeelType); } @Test public void testGetActiveTheme() { - GTheme activeTheme = Gui.getActiveTheme(); + GTheme activeTheme = themeManager.getActiveTheme(); assertEquals(METAL_THEME, activeTheme); } @Test public void testGetThemeByName() { - GTheme theme = Gui.getTheme("Nimbus Theme"); + GTheme theme = themeManager.getTheme("Nimbus Theme"); assertEquals(NIMBUS_THEME, theme); } @Test public void testGetAllValues() { - GThemeValueMap allValues = Gui.getAllValues(); + GThemeValueMap allValues = themeManager.getCurrentValues(); assertEquals(WHITE, allValues.getColor("color.test.bg").getRawValue()); - Gui.setColor("color.test.bg", PURPLE); + themeManager.setColor("color.test.bg", PURPLE); - allValues = Gui.getAllValues(); + allValues = themeManager.getCurrentValues(); assertEquals(PURPLE, allValues.getColor("color.test.bg").getRawValue()); } @@ -263,17 +229,17 @@ public class GuiTest { @Test public void testGetNonDefaultValues() { // should be empty if we haven't changed any themeValues - GThemeValueMap nonDefaultValues = Gui.getNonDefaultValues(); + GThemeValueMap nonDefaultValues = themeManager.getNonDefaultValues(); assertTrue(nonDefaultValues.isEmpty()); // change some values and see that they show up in the nonDefaultValues - Gui.setColor("color.test.bg", RED); - Gui.setFont("font.test.foo", SMALL_FONT); - Gui.setIcon("icon.test.foo", ICON2); + themeManager.setColor("color.test.bg", RED); + themeManager.setFont("font.test.foo", SMALL_FONT); + themeManager.setIcon("icon.test.foo", ICON2); // also add in a totally new value - Gui.setColor("color.test.xxx", GREEN); + themeManager.setColor("color.test.xxx", GREEN); - nonDefaultValues = Gui.getNonDefaultValues(); + nonDefaultValues = themeManager.getNonDefaultValues(); assertEquals(4, nonDefaultValues.size()); assertEquals(RED, nonDefaultValues.getColor("color.test.bg").getRawValue()); assertEquals(GREEN, nonDefaultValues.getColor("color.test.xxx").getRawValue()); @@ -283,57 +249,57 @@ public class GuiTest { @Test public void testGetColor() { - assertEquals(WHITE, Gui.getColor("color.test.bg")); + assertEquals(WHITE, themeManager.getColor("color.test.bg")); } @Test public void testGetFont() { - assertEquals(FONT, Gui.getFont("font.test.foo")); + assertEquals(FONT, themeManager.getFont("font.test.foo")); } @Test public void testGetIcon() { - assertEquals(ICON1, Gui.getIcon("icon.test.foo")); + assertEquals(ICON1, themeManager.getIcon("icon.test.foo")); } @Test public void testGetColorWithUnresolvedId() { - assertEquals(CYAN, Gui.getColor("color.badid", false)); + assertEquals(CYAN, themeManager.getColor("color.badid")); } @Test public void testGetIconWithUnresolvedId() { - assertEquals(ResourceManager.getDefaultIcon(), Gui.getIcon("icon.badid", false)); + assertEquals(ResourceManager.getDefaultIcon(), themeManager.getIcon("icon.badid")); } @Test public void testGetFontWithUnresolvedId() { - assertEquals(Gui.DEFAULT_FONT, Gui.getFont("font.badid", false)); + assertEquals(ThemeManager.DEFAULT_FONT, themeManager.getFont("font.badid")); } @Test public void testGetGColorUiResource() { - Color color = Gui.getGColorUiResource("color.test.bg"); + Color color = themeManager.getGColorUiResource("color.test.bg"); assertTrue(color instanceof UIResource); // make sure there is only one instance for an id; - Color color2 = Gui.getGColorUiResource("color.test.bg"); + Color color2 = themeManager.getGColorUiResource("color.test.bg"); assertTrue(color == color2); } @Test public void testGetGIconUiResource() { - Icon icon = Gui.getGIconUiResource("icon.test.foo"); + Icon icon = themeManager.getGIconUiResource("icon.test.foo"); assertTrue(icon instanceof UIResource); // make sure there is only one instance for an id; - Icon gIcon2 = Gui.getGIconUiResource("icon.test.foo"); + Icon gIcon2 = themeManager.getGIconUiResource("icon.test.foo"); assertTrue(icon == gIcon2); } @Test public void testGetApplicationLightDefaults() { - assertEquals(defaultValues, Gui.getApplicationLightDefaults()); + assertEquals(defaultValues, themeManager.getApplicationLightDefaults()); } @Test @@ -342,17 +308,17 @@ public class GuiTest { GThemeValueMap expected = new GThemeValueMap(); expected.load(defaultValues); expected.load(darkDefaultValues); - assertEquals(expected, Gui.getApplicationDarkDefaults()); + assertEquals(expected, themeManager.getApplicationDarkDefaults()); } @Test public void testRegisterFont() { - Gui.setFont(new FontValue("font.test", SMALL_FONT)); + themeManager.setFont(new FontValue("font.test", SMALL_FONT)); JLabel label = new JLabel("Test"); assertNotEquals(SMALL_FONT, label.getFont()); - Gui.registerFont(label, "font.test"); + themeManager.registerFont(label, "font.test"); assertEquals(SMALL_FONT, label.getFont()); - Gui.setFont(new FontValue("font.test", FONT)); + themeManager.setFont(new FontValue("font.test", FONT)); assertEquals(FONT, label.getFont()); } @@ -369,4 +335,43 @@ public class GuiTest { fail("Icons don't match. Expected " + url + ", but got " + gUrl); } } + + // ApplicationThemeManager that doesn't read in theme.properties files or preferences + class DummyApplicationThemeManager extends ApplicationThemeManager { + DummyApplicationThemeManager() { + themePreferences = new ThemePreferences() { + @Override + public GTheme load() { + return new MetalTheme(); + } + + @Override + public void save(GTheme theme) { + // do nothing + } + }; + themeFileLoader = new ThemeFileLoader() { + @Override + public void loadThemeDefaultFiles() { + // do nothing + } + + @Override + public Collection loadThemeFiles() { + return new HashSet<>(themes); + } + + @Override + public GThemeValueMap getDefaults() { + return defaultValues; + } + + @Override + public GThemeValueMap getDarkDefaults() { + return darkDefaultValues; + } + }; + doInitialize(); + } + } } diff --git a/Ghidra/Framework/Generic/src/test/java/ghidra/util/datastruct/WeakStoreTest.java b/Ghidra/Framework/Generic/src/test/java/ghidra/util/datastruct/WeakStoreTest.java index 9963cb7d3a..aed0ca4694 100644 --- a/Ghidra/Framework/Generic/src/test/java/ghidra/util/datastruct/WeakStoreTest.java +++ b/Ghidra/Framework/Generic/src/test/java/ghidra/util/datastruct/WeakStoreTest.java @@ -21,9 +21,11 @@ import java.util.List; import org.junit.Test; -public class WeakStoreTest { +import generic.test.AbstractGenericTest; + +public class WeakStoreTest extends AbstractGenericTest { @Test - public void testStore() throws InterruptedException { + public void testStore() { WeakStore store = new WeakStore<>(); store.add(new Foo("AAA")); store.add(new Foo("BBB")); @@ -38,13 +40,10 @@ public class WeakStoreTest { assertEquals("CCC", values.get(2).getName()); values = null; - for (int i = 0; i < 20; i++) { + waitFor(() -> { System.gc(); - if (store.size() == 0) { - break; - } - } - assertEquals(0, store.size()); + return store.size() == 0; + }, "Weak store values were never garbage collected"); } static class Foo { diff --git a/Ghidra/Framework/Graph/src/test/java/ghidra/service/graph/GraphDisplayOptionsTest.java b/Ghidra/Framework/Graph/src/test/java/ghidra/service/graph/GraphDisplayOptionsTest.java index bee4f05b9f..ae019bbed5 100644 --- a/Ghidra/Framework/Graph/src/test/java/ghidra/service/graph/GraphDisplayOptionsTest.java +++ b/Ghidra/Framework/Graph/src/test/java/ghidra/service/graph/GraphDisplayOptionsTest.java @@ -17,6 +17,7 @@ package ghidra.service.graph; import static org.junit.Assert.*; +import java.awt.Color; import java.awt.Font; import java.util.Arrays; import java.util.List; @@ -24,32 +25,23 @@ import java.util.List; import org.junit.Before; import org.junit.Test; -import docking.FakeDockingTool; -import docking.test.AbstractDockingTest; +import generic.theme.*; import generic.theme.GThemeDefaults.Colors.Palette; -import generic.theme.Gui; import ghidra.framework.options.Options; import ghidra.framework.options.ToolOptions; import ghidra.util.HelpLocation; -public class GraphDisplayOptionsTest extends AbstractDockingTest { +public class GraphDisplayOptionsTest { private GraphType graphType; private GraphDisplayOptions options; @Before public void setUp() { - Gui.setColor("color.V1", Palette.BLACK); - Gui.setColor("color.V2", Palette.BLACK); - Gui.setColor("color.V3", Palette.BLACK); - Gui.setColor("color.E1", Palette.BLACK); - Gui.setColor("color.E2", Palette.BLACK); - Gui.setColor("color.E3", Palette.BLACK); - Gui.setColor("color.edge.default", Palette.BLACK); - Gui.setColor("color.vertex.default", Palette.BLACK); - Gui.setColor("color.edge.selected", Palette.BLACK); - Gui.setColor("color.vertex.selected", Palette.BLACK); - Gui.setFont("font.graph", new Font("monospaced", Font.PLAIN, 12)); + // create a dummy theme manager that defines values for use in this test + DummyThemeManager themeManager = new DummyThemeManager(); + + // create a new graph definition and options using theme properties List vertexTypes = Arrays.asList("V1", "V2", "V3"); List edgeTypes = Arrays.asList("E1", "E2", "E3"); graphType = new GraphType("Test", "Test Description", vertexTypes, edgeTypes); @@ -286,22 +278,44 @@ public class GraphDisplayOptionsTest extends AbstractDockingTest { } - @Test - public void testChangingToolOptionsAffectsGraph() { - FakeDockingTool tool = new FakeDockingTool(); - ToolOptions toolOptions = tool.getOptions("Graph"); - options.registerOptions(toolOptions, null); - options.initializeFromOptions(tool); + // Create a ThemeManager that it not fully initialized for speed. This class provides + // fake property theme values. + class DummyThemeManager extends StubThemeManager { + DummyThemeManager() { + installTestValues(); + installExpectedValues(); + installInGui(); + } - AttributedVertex vertex = new AttributedVertex("Foo"); - vertex.setVertexType("V1"); - assertEquals(Palette.BLACK.getRGB(), options.getVertexColor(vertex).getRGB()); + private void installExpectedValues() { + setColor(new ColorValue("color.vertex.selected", Color.BLACK)); + setColor(new ColorValue("color.edge.selected", Color.BLACK)); + setColor(new ColorValue("color.graphdisplay.vertex.default", Color.BLACK)); + setColor(new ColorValue("color.graphdisplay.edge.default", Color.BLACK)); + setFont(new FontValue("font.graph", new Font("monospaced", Font.PLAIN, 12))); - Options graphDisplayOptions = toolOptions.getOptions(options.getRootOptionsName()); - Options vertexColorOptions = graphDisplayOptions.getOptions("Vertex Colors"); - vertexColorOptions.setColor("V1", Palette.GOLD); + } + + protected void installTestValues() { + setColor(new ColorValue("color.V1", Color.BLACK)); + setColor(new ColorValue("color.V2", Color.BLACK)); + setColor(new ColorValue("color.V3", Color.BLACK)); + setColor(new ColorValue("color.E1", Color.BLACK)); + setColor(new ColorValue("color.E2", Color.BLACK)); + setColor(new ColorValue("color.E3", Color.BLACK)); + setColor(new ColorValue("color.edge.default", Color.BLACK)); + setColor(new ColorValue("color.vertex.default", Color.BLACK)); + setColor(new ColorValue("color.edge.selected", Color.BLACK)); + setColor(new ColorValue("color.vertex.selected", Color.BLACK)); + setColor(new ColorValue("color.graphdisplay.vertex.selected", Color.BLACK)); + setColor(new ColorValue("color.graphdisplay.edge.selected", Color.BLACK)); + setColor(new ColorValue("color.vertex.selected", Color.BLACK)); + setColor(new ColorValue("color.vertex.selected", Color.BLACK)); + + setFont(new FontValue("font.graph", new Font("monospaced", Font.PLAIN, 12))); + setFont( + new FontValue("font.graphdisplay.default", new Font("monospaced", Font.PLAIN, 12))); + } - assertEquals(Palette.GOLD.getRGB(), options.getVertexColor(vertex).getRGB()); } - } diff --git a/Ghidra/Framework/Help/src/main/java/help/GHelpBuilder.java b/Ghidra/Framework/Help/src/main/java/help/GHelpBuilder.java index 2a51ad64c3..bac98cf874 100644 --- a/Ghidra/Framework/Help/src/main/java/help/GHelpBuilder.java +++ b/Ghidra/Framework/Help/src/main/java/help/GHelpBuilder.java @@ -21,7 +21,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; -import generic.theme.Gui; +import generic.theme.ApplicationThemeManager; import ghidra.framework.Application; import ghidra.framework.ApplicationConfiguration; import help.validator.*; @@ -69,7 +69,7 @@ public class GHelpBuilder { ApplicationConfiguration config = new ApplicationConfiguration() { @Override protected void initializeApplication() { - Gui.initialize(); + ApplicationThemeManager.initialize(); } @Override diff --git a/Ghidra/Framework/Help/src/test/java/help/HelpBuildUtilsTest.java b/Ghidra/Framework/Help/src/test/java/help/HelpBuildUtilsTest.java index 357bf1a880..b6a3a53683 100644 --- a/Ghidra/Framework/Help/src/test/java/help/HelpBuildUtilsTest.java +++ b/Ghidra/Framework/Help/src/test/java/help/HelpBuildUtilsTest.java @@ -25,7 +25,7 @@ import java.nio.file.Paths; import org.junit.Test; -import generic.theme.Gui; +import generic.theme.ApplicationThemeManager; import ghidra.GhidraTestApplicationLayout; import ghidra.framework.ApplicationConfiguration; import utility.application.ApplicationLayout; @@ -101,7 +101,7 @@ public class HelpBuildUtilsTest extends AbstractHelpTest { @Test public void testLocateReferences_Icons() throws URISyntaxException { - Gui.initialize(); + ApplicationThemeManager.initialize(); Path sourceFile = Paths.get(HTML_FILE_PATH); String reference = "Icons.REFRESH_ICON"; // see Icons class ImageLocation location = HelpBuildUtils.locateImageReference(sourceFile, reference); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/app/plugin/gui/ThemeManagerPlugin.java b/Ghidra/Framework/Project/src/main/java/ghidra/app/plugin/gui/ThemeManagerPlugin.java index 33da983020..956d93c334 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/app/plugin/gui/ThemeManagerPlugin.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/app/plugin/gui/ThemeManagerPlugin.java @@ -18,7 +18,7 @@ package ghidra.app.plugin.gui; import docking.action.builder.ActionBuilder; import docking.theme.gui.ThemeDialog; import docking.theme.gui.ThemeUtils; -import generic.theme.Gui; +import generic.theme.ThemeManager; import ghidra.app.plugin.PluginCategoryNames; import ghidra.framework.main.ApplicationLevelOnlyPlugin; import ghidra.framework.main.UtilityPluginPackage; @@ -38,8 +38,11 @@ import ghidra.util.HelpLocation; //@formatter:on public class ThemeManagerPlugin extends Plugin implements ApplicationLevelOnlyPlugin { + private ThemeManager themeManager; + public ThemeManagerPlugin(PluginTool tool) { super(tool); + themeManager = ThemeManager.getInstance(); } @Override @@ -52,35 +55,35 @@ public class ThemeManagerPlugin extends Plugin implements ApplicationLevelOnlyPl .menuPath("Edit", "Theme") .menuGroup(group, "1") .helpLocation(new HelpLocation("Theming", "Edit_Theme")) - .onAction(e -> ThemeDialog.editTheme()) + .onAction(e -> ThemeDialog.editTheme(themeManager)) .buildAndInstall(tool); new ActionBuilder("Reset", owner) .menuPath("Edit", themeSubMenu, "Reset Theme Values") .menuGroup(group, "2") .helpLocation(new HelpLocation("Theming", "Reset_Theme_Values")) - .onAction(e -> ThemeUtils.resetThemeToDefault()) + .onAction(e -> ThemeUtils.resetThemeToDefault(themeManager)) .buildAndInstall(tool); new ActionBuilder("Import Theme", owner) .menuPath("Edit", themeSubMenu, "Import...") .menuGroup(group, "3") .helpLocation(new HelpLocation("Theming", "Import_Theme")) - .onAction(e -> ThemeUtils.importTheme()) + .onAction(e -> ThemeUtils.importTheme(themeManager)) .buildAndInstall(tool); new ActionBuilder("Export Theme", owner) .menuPath("Edit", themeSubMenu, "Export...") .menuGroup(group, "4") .helpLocation(new HelpLocation("Theming", "Export_Theme")) - .onAction(e -> ThemeUtils.exportTheme()) + .onAction(e -> ThemeUtils.exportTheme(themeManager)) .buildAndInstall(tool); new ActionBuilder("Delete Theme", owner) .menuPath("Edit", themeSubMenu, "Delete...") .menuGroup(group, "5") .helpLocation(new HelpLocation("Theming", "Delete_Theme")) - .onAction(e -> ThemeUtils.deleteTheme()) + .onAction(e -> ThemeUtils.deleteTheme(themeManager)) .buildAndInstall(tool); tool.setMenuGroup(new String[] { "Edit", themeSubMenu }, group, "2"); @@ -89,8 +92,8 @@ public class ThemeManagerPlugin extends Plugin implements ApplicationLevelOnlyPl @Override protected boolean canClose() { - if (Gui.hasThemeChanges()) { - return ThemeUtils.askToSaveThemeChanges(); + if (themeManager.hasThemeChanges()) { + return ThemeUtils.askToSaveThemeChanges(themeManager); } return true; } diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ThemingScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ThemingScreenShots.java index 8900ad526e..2277863784 100644 --- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ThemingScreenShots.java +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/ThemingScreenShots.java @@ -26,13 +26,16 @@ import resources.ResourceManager; public class ThemingScreenShots extends GhidraScreenShotGenerator { + private ThemeManager themeManager; + public ThemingScreenShots() { super(); + themeManager = ThemeManager.getInstance(); } @Test public void testThemeDialog() { - showDialogWithoutBlocking(tool, new ThemeDialog()); + showDialogWithoutBlocking(tool, new ThemeDialog(themeManager)); captureDialog(1000, 500); } @@ -41,7 +44,7 @@ public class ThemingScreenShots extends GhidraScreenShotGenerator { ColorValueEditor editor = new ColorValueEditor(e -> { /**/}); ColorValue value = new ColorValue("color.bg.test", Palette.BLUE); - Gui.setColor(value); + themeManager.setColor(value); editor.editValue(value); captureDialog(); } @@ -51,7 +54,7 @@ public class ThemingScreenShots extends GhidraScreenShotGenerator { FontValueEditor editor = new FontValueEditor(e -> { /**/}); FontValue value = new FontValue("font.xyz", new Font("Monospaced", Font.BOLD, 14)); - Gui.setFont(value); + themeManager.setFont(value); editor.editValue(value); captureDialog(); } @@ -61,7 +64,7 @@ public class ThemingScreenShots extends GhidraScreenShotGenerator { IconValueEditor editor = new IconValueEditor(e -> { /**/}); IconValue value = new IconValue("icon.bomb", ResourceManager.getDefaultIcon()); - Gui.setIcon(value); + themeManager.setIcon(value); editor.editValue(value); captureDialog(); }