diff --git a/Ghidra/Features/Base/src/main/java/ghidra/base/help/GhidraHelpService.java b/Ghidra/Features/Base/src/main/java/ghidra/base/help/GhidraHelpService.java index cb1595c10f..ffe1d46e84 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/base/help/GhidraHelpService.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/base/help/GhidraHelpService.java @@ -24,8 +24,7 @@ import javax.help.HelpSetException; import docking.help.*; import generic.jar.ResourceFile; -import generic.theme.Gui; -import generic.theme.ThemeListener; +import generic.theme.*; import ghidra.framework.Application; import ghidra.util.Msg; import help.HelpService; @@ -38,7 +37,7 @@ import resources.ResourceManager; public class GhidraHelpService extends HelpManager { private static final String MASTER_HELP_SET_HS = "Base_HelpSet.hs"; - private ThemeListener listener = t -> reload(); + private ThemeListener listener = new HelpThemeListener(); public static void install() { try { @@ -120,4 +119,11 @@ public class GhidraHelpService extends HelpManager { return results; } + + class HelpThemeListener implements ThemeListener { + @Override + public void themeChanged(GTheme newTheme) { + reload(); + } + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemProbeConflictResolver.java b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemProbeConflictResolver.java index dbc3e1ab60..73b4ff3422 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemProbeConflictResolver.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/FileSystemProbeConflictResolver.java @@ -17,6 +17,7 @@ package ghidra.formats.gfilesystem; import java.util.List; +import docking.widgets.SelectFromListDialog; import ghidra.formats.gfilesystem.factory.FileSystemInfoRec; /** diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionManager.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionManager.java index 26ecb0bca1..eec3c39acf 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionManager.java @@ -16,13 +16,12 @@ package ghidra.plugins.fsbrowser; import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*; -import static java.util.Map.entry; - -import java.util.*; -import java.util.function.Function; +import static java.util.Map.*; import java.awt.Component; import java.io.*; +import java.util.*; +import java.util.function.Function; import javax.swing.*; @@ -31,6 +30,7 @@ import org.apache.commons.io.FilenameUtils; import docking.action.DockingAction; import docking.action.builder.ActionBuilder; import docking.widgets.OptionDialog; +import docking.widgets.SelectFromListDialog; import docking.widgets.dialogs.MultiLineMessageDialog; import docking.widgets.filechooser.GhidraFileChooser; import docking.widgets.filechooser.GhidraFileChooserMode; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBUtils.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBUtils.java index 7da421e420..0089879bb2 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBUtils.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBUtils.java @@ -18,8 +18,8 @@ package ghidra.plugins.fsbrowser; import java.util.ArrayList; import java.util.List; +import docking.widgets.SelectFromListDialog; import ghidra.app.services.ProgramManager; -import ghidra.formats.gfilesystem.SelectFromListDialog; import ghidra.framework.plugintool.PluginTool; import ghidra.util.Msg; @@ -28,7 +28,6 @@ import ghidra.util.Msg; */ public class FSBUtils { - /** * Returns the {@link ProgramManager} associated with this fs browser plugin. *

diff --git a/Ghidra/Framework/Docking/certification.manifest b/Ghidra/Framework/Docking/certification.manifest index 3e51efa9e1..5141d01af6 100644 --- a/Ghidra/Framework/Docking/certification.manifest +++ b/Ghidra/Framework/Docking/certification.manifest @@ -88,6 +88,8 @@ src/main/resources/images/left.png||GHIDRA||reviewed||END| src/main/resources/images/locationIn.gif||GHIDRA||||END| src/main/resources/images/locationOut.gif||GHIDRA||||END| src/main/resources/images/magnifier.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END| +src/main/resources/images/mail-folder-outbox.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| +src/main/resources/images/mail-receive.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| src/main/resources/images/media-playback-start.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| src/main/resources/images/menu16.gif||GHIDRA||reviewed||END| src/main/resources/images/note-red.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| diff --git a/Ghidra/Framework/Docking/data/docking.theme.properties b/Ghidra/Framework/Docking/data/docking.theme.properties index 6754ebfba0..11ca64cd94 100644 --- a/Ghidra/Framework/Docking/data/docking.theme.properties +++ b/Ghidra/Framework/Docking/data/docking.theme.properties @@ -80,6 +80,8 @@ icon.flag = images/flag.png icon.lock = images/kgpg.png icon.checkmark.green = images/checkmark_green.gif +icon.theme.import = images/mail-receive.png +icon.theme.export = images/mail-folder-outbox.png [Dark Defaults] diff --git a/Ghidra/Framework/Docking/src/main/java/docking/options/editor/IconPropertyEditor.java b/Ghidra/Framework/Docking/src/main/java/docking/options/editor/IconPropertyEditor.java index 6d3b42342e..368eb43b66 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/options/editor/IconPropertyEditor.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/options/editor/IconPropertyEditor.java @@ -33,6 +33,7 @@ import docking.widgets.filechooser.GhidraFileChooserMode; import docking.widgets.label.GDLabel; import docking.widgets.list.GListCellRenderer; import ghidra.framework.Application; +import ghidra.framework.preferences.Preferences; import ghidra.util.Msg; import ghidra.util.filechooser.ExtensionFileFilter; import resources.ResourceManager; @@ -71,11 +72,13 @@ public class IconPropertyEditor extends PropertyEditorSupport { if (icon instanceof UrlImageIcon urlIcon) { return urlIcon.getOriginalPath(); } - return ""; + return ""; } class IconChooserPanel extends JPanel { + private static final String IMAGE_DIR = "images/"; + private static final String LAST_ICON_DIR_PREFERENCE_KEY = "IconEditor.lastDir"; private GDLabel previewLabel; private DropDownSelectionTextField dropDown; private IconDropDownDataModel dataModel; @@ -151,8 +154,14 @@ public class IconPropertyEditor extends PropertyEditorSupport { chooser.setFileSelectionMode(GhidraFileChooserMode.FILES_ONLY); chooser.setSelectedFileFilter( ExtensionFileFilter.forExtensions("Icon Files", ".png", "gif")); + String lastDir = Preferences.getProperty(LAST_ICON_DIR_PREFERENCE_KEY); + if (lastDir != null) { + chooser.setCurrentDirectory(new File(lastDir)); + } File file = chooser.getSelectedFile(); if (file != null) { + File dir = chooser.getCurrentDirectory(); + Preferences.setProperty(LAST_ICON_DIR_PREFERENCE_KEY, dir.getAbsolutePath()); importIconFile(file); } } @@ -164,8 +173,9 @@ public class IconPropertyEditor extends PropertyEditorSupport { return; } File dir = Application.getUserSettingsDirectory(); - File destinationDir = new File(dir, "themes/images"); - File destinationFile = new File(destinationDir, file.getName()); + String relativePath = IMAGE_DIR + file.getName(); + + File destinationFile = new File(dir, relativePath); if (destinationFile.exists()) { int result = OptionDialog.showYesNoDialog(dropDown, "Overwrite?", "An icon with that name already exists.\n Do you want to overwrite it?"); @@ -175,7 +185,8 @@ public class IconPropertyEditor extends PropertyEditorSupport { } try { FileUtils.copyFile(file, destinationFile); - ImageIcon icon = ResourceManager.loadImage("themes/images/" + file.getName()); + String path = ResourceManager.EXTERNAL_ICON_PREFIX + relativePath; + ImageIcon icon = ResourceManager.loadImage(path); setValue(icon); } catch (IOException e) { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ExportThemeDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ExportThemeDialog.java index ee91a618a4..d4ea21b15e 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ExportThemeDialog.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ExportThemeDialog.java @@ -24,7 +24,6 @@ import javax.swing.*; import docking.DialogComponentProvider; import docking.options.editor.ButtonPanelFactory; -import docking.theme.*; import docking.widgets.checkbox.GCheckBox; import docking.widgets.filechooser.GhidraFileChooser; import docking.widgets.filechooser.GhidraFileChooserMode; @@ -40,10 +39,11 @@ public class ExportThemeDialog extends DialogComponentProvider { private JTextField nameField; private JTextField fileTextField; private GCheckBox includeDefaultsCheckbox; + private boolean exportAsZip; - protected ExportThemeDialog() { + public ExportThemeDialog(boolean exportAsZip) { super("Export Theme"); - + this.exportAsZip = exportAsZip; addWorkPanel(buildMainPanel()); addOKButton(); addCancelButton(); @@ -58,33 +58,45 @@ public class ExportThemeDialog extends DialogComponentProvider { } private boolean exportTheme() { - File file = new File(fileTextField.getText()); String themeName = nameField.getText(); if (themeName.isBlank()) { setStatusText("Missing Theme Name", MessageType.ERROR, true); return false; } - boolean includeDefaults = includeDefaultsCheckbox.isSelected(); - - GTheme activeTheme = Gui.getActiveTheme(); - FileGTheme fileTheme = new FileGTheme(file, themeName, activeTheme.getLookAndFeelType(), - activeTheme.useDarkDefaults()); - - if (includeDefaults) { - fileTheme.load(Gui.getAllValues()); - } - else { - fileTheme.load(Gui.getNonDefaultValues()); - } try { - fileTheme.save(); + FileGTheme exportTheme = createExternalTheme(themeName); + loadValues(exportTheme); + exportTheme.save(); return true; } catch (IOException e) { Msg.error("Error Exporting Theme", "I/O Error encountered trying to export theme!", e); - return false; } + return false; + } + + private void loadValues(FileGTheme exportTheme) { + if (includeDefaultsCheckbox.isSelected()) { + exportTheme.load(Gui.getAllValues()); + } + else { + exportTheme.load(Gui.getNonDefaultValues()); + } + } + + private FileGTheme createExternalTheme(String themeName) { + File file = new File(fileTextField.getText()); + + GTheme activeTheme = Gui.getActiveTheme(); + LafType laf = activeTheme.getLookAndFeelType(); + boolean useDarkDefaults = activeTheme.useDarkDefaults(); + + if (exportAsZip) { + return new ZipGTheme(file, themeName, laf, useDarkDefaults); + } + return new FileGTheme(file, themeName, laf, useDarkDefaults); + } @Override @@ -118,10 +130,12 @@ public class ExportThemeDialog extends DialogComponentProvider { } private Component buildFilePanel() { - String name = Gui.getActiveTheme().getName(); - String fileName = name.replaceAll(" ", "_") + GTheme.FILE_EXTENSION; File homeDir = new File(System.getProperty("user.home")); // prefer the home directory - File file = new File(homeDir, fileName); + + String name = Gui.getActiveTheme().getName(); + String filename = name.replaceAll(" ", "_") + "."; + filename += exportAsZip ? GTheme.ZIP_FILE_EXTENSION : GTheme.FILE_EXTENSION; + File file = new File(homeDir, filename); fileTextField = new JTextField(); fileTextField.setText(file.getAbsolutePath()); @@ -149,4 +163,9 @@ public class ExportThemeDialog extends DialogComponentProvider { } } + // used for testing + public void setOutputFile(File outputFile) { + fileTextField.setText(outputFile.getAbsolutePath()); + } + } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ProtectedIcon.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ProtectedIcon.java index f036d70f1e..110c424416 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ProtectedIcon.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ProtectedIcon.java @@ -47,11 +47,11 @@ public class ProtectedIcon implements Icon { @Override public int getIconWidth() { - return delegate.getIconWidth(); + return Math.max(1, delegate.getIconWidth()); } @Override public int getIconHeight() { - return delegate.getIconHeight(); + return Math.max(1, delegate.getIconHeight()); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeDialog.java index 1632db5b76..55d0fa3531 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeDialog.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeDialog.java @@ -19,8 +19,6 @@ import java.awt.BorderLayout; import java.awt.Component; import java.awt.event.*; import java.beans.PropertyChangeEvent; -import java.io.File; -import java.io.IOException; import java.util.*; import java.util.stream.Collectors; @@ -31,18 +29,13 @@ import docking.DialogComponentProvider; import docking.DockingWindowManager; import docking.action.DockingAction; import docking.action.builder.ActionBuilder; -import docking.theme.*; import docking.widgets.OptionDialog; import docking.widgets.combobox.GhidraComboBox; -import docking.widgets.dialogs.InputDialog; -import docking.widgets.filechooser.GhidraFileChooser; -import docking.widgets.filechooser.GhidraFileChooserMode; import docking.widgets.table.GFilterTable; import docking.widgets.table.GTable; import generic.theme.*; -import ghidra.framework.Application; -import ghidra.util.*; -import ghidra.util.filechooser.ExtensionFileFilter; +import ghidra.util.MessageType; +import ghidra.util.Swing; public class ThemeDialog extends DialogComponentProvider { private static ThemeDialog INSTANCE; @@ -54,12 +47,11 @@ public class ThemeDialog extends DialogComponentProvider { private FontValueEditor fontEditor = new FontValueEditor(this::fontValueChanged); private IconValueEditor iconEditor = new IconValueEditor(this::iconValueChanged); - // stores the original value for ids whose value has changed - private GThemeValueMap changedValuesMap = new GThemeValueMap(); private JButton saveButton; private JButton restoreButton; private GhidraComboBox combo; private ItemListener comboListener = this::themeComboChanged; + private ThemeListener listener = new DialogThemeListener(); public ThemeDialog() { super("Theme Dialog", false); @@ -73,21 +65,10 @@ public class ThemeDialog extends DialogComponentProvider { setRememberSize(false); updateButtons(); createActions(); + Gui.addThemeListener(listener); } private void createActions() { - DockingAction importAction = - new ActionBuilder("Import Theme", getTitle()).toolBarIcon(new GIcon("icon.navigate.in")) - .onAction(e -> importTheme()) - .build(); - addAction(importAction); - - DockingAction exportAction = new ActionBuilder("Export Theme", getTitle()) - .toolBarIcon(new GIcon("icon.navigate.out")) - .onAction(e -> exportTheme()) - .build(); - addAction(exportAction); - DockingAction reloadDefaultsAction = new ActionBuilder("Reload Ghidra Defaults", getTitle()) .toolBarIcon(new GIcon("icon.refresh")) .onAction(e -> reloadDefaultsCallback()) @@ -104,14 +85,14 @@ public class ThemeDialog extends DialogComponentProvider { } private boolean handleChanges() { - if (hasChanges()) { + if (Gui.hasThemeChanges()) { int result = OptionDialog.showYesNoCancelDialog(null, "Close Theme Dialog", "You have changed the theme.\n Do you want save your changes?"); if (result == OptionDialog.CANCEL_OPTION) { return false; } if (result == OptionDialog.YES_OPTION) { - return save(); + return ThemeUtils.saveThemeChanges(); } Gui.reloadGhidraDefaults(); } @@ -119,12 +100,11 @@ public class ThemeDialog extends DialogComponentProvider { } protected void saveCallback() { - save(); - reset(); + ThemeUtils.saveThemeChanges(); } private void restoreCallback() { - if (hasChanges()) { + if (Gui.hasThemeChanges()) { int result = OptionDialog.showYesNoDialog(null, "Restore Theme Values", "Are you sure you want to discard all your changes?"); if (result == OptionDialog.NO_OPTION) { @@ -132,11 +112,10 @@ public class ThemeDialog extends DialogComponentProvider { } } Gui.restoreThemeValues(); - reset(); } private void reloadDefaultsCallback() { - if (hasChanges()) { + if (Gui.hasThemeChanges()) { int result = OptionDialog.showYesNoDialog(null, "Reload Ghidra Default Values", "This will discard all your theme changes. Continue?"); if (result == OptionDialog.NO_OPTION) { @@ -144,11 +123,9 @@ public class ThemeDialog extends DialogComponentProvider { } } Gui.reloadGhidraDefaults(); - reset(); } private void reset() { - changedValuesMap.clear(); colorTableModel.reloadAll(); fontTableModel.reloadAll(); iconTableModel.reloadAll(); @@ -156,141 +133,33 @@ public class ThemeDialog extends DialogComponentProvider { updateCombo(); } - /** - * Saves all current theme changes - * @return true if the operation was not cancelled. - */ - private boolean save() { - GTheme activeTheme = Gui.getActiveTheme(); - - String name = activeTheme.getName(); - - while (!canSaveToName(name)) { - name = getNameFromUser(name); - if (name == null) { - return false; - } - } - return saveCurrentValues(name); - } - - private String getNameFromUser(String name) { - InputDialog inputDialog = new InputDialog("Create Theme", "New Theme Name", name); - DockingWindowManager.showDialog(inputDialog); - return inputDialog.getValue(); - } - - private boolean canSaveToName(String name) { - GTheme existing = Gui.getTheme(name); - if (existing == null) { - return true; - } - if (existing instanceof FileGTheme fileTheme) { - int result = OptionDialog.showYesNoDialog(null, "Overwrite Existing Theme?", - "Do you want to overwrite the existing theme file for \"" + name + "\"?"); - if (result == OptionDialog.YES_OPTION) { - return true; - } - } - return false; - } - - private boolean saveCurrentValues(String themeName) { - GTheme activeTheme = Gui.getActiveTheme(); - File file = getSaveFile(themeName); - - FileGTheme newTheme = new FileGTheme(file, themeName, activeTheme.getLookAndFeelType(), - activeTheme.useDarkDefaults()); - newTheme.load(Gui.getNonDefaultValues()); - try { - newTheme.save(); - Gui.addTheme(newTheme); - Gui.setTheme(newTheme); - } - catch (IOException e) { - Msg.showError(this, null, "I/O Error", - "Error writing theme file: " + newTheme.getFile().getAbsolutePath(), e); - return false; - } - - return true; - - } - - private File getSaveFile(String themeName) { - File dir = Application.getUserSettingsDirectory(); - File themeDir = new File(dir, Gui.THEME_DIR); - if (!themeDir.exists()) { - themeDir.mkdir(); - } - String cleanedName = themeName.replaceAll(" ", "_") + GTheme.FILE_EXTENSION; - return new File(themeDir, cleanedName); - } - - private void importTheme() { - if (!handleChanges()) { - return; - } - GTheme startingTheme = Gui.getActiveTheme(); - GhidraFileChooser chooser = new GhidraFileChooser(getComponent()); - chooser.setTitle("Choose Theme File"); - chooser.setApproveButtonToolTipText("Select File"); - chooser.setFileSelectionMode(GhidraFileChooserMode.FILES_ONLY); - chooser.setSelectedFileFilter( - new ExtensionFileFilter("Ghidra Theme Files", GTheme.FILE_EXTENSION)); - File file = chooser.getSelectedFile(); - if (file == null) { - return; - } - try { - FileGTheme imported = new FileGTheme(file); - Gui.setTheme(imported); - if (!save()) { - Gui.setTheme(startingTheme); - } - } - catch (IOException e) { - Msg.showError(this, null, "Error Importing Theme File", - "Error encountered importing file: " + file.getAbsolutePath(), e); - } - reset(); - } - - private void exportTheme() { - ExportThemeDialog dialog = new ExportThemeDialog(); - DockingWindowManager.showDialog(dialog); - } - private void themeComboChanged(ItemEvent e) { - if (e.getStateChange() == ItemEvent.SELECTED) { - String themeName = (String) e.getItem(); - Swing.runLater(() -> { - GTheme theme = Gui.getTheme(themeName); - Gui.setTheme(theme); - if (theme.getLookAndFeelType() == LafType.GTK) { - setStatusText( - "Warning - Themes using the GTK LookAndFeel do not support changing java component colors, fonts or icons. You can still change Ghidra values.", - MessageType.ERROR, true); - } - else if (theme.getLookAndFeelType() == LafType.NIMBUS) { - setStatusText( - "Warning - Themes using the Nimbus LookAndFeel do not support changing java component fonts or icons. You can still change Ghidra values.", - MessageType.ERROR, true); - } - else { - setStatusText(""); - } - changedValuesMap.clear(); - colorTableModel.reloadAll(); - fontTableModel.reloadAll(); - iconTableModel.reloadAll(); - }); + if (e.getStateChange() != ItemEvent.SELECTED) { + return; } - } - private boolean hasChanges() { - return !changedValuesMap.isEmpty(); + if (!ThemeUtils.askToSaveThemeChanges()) { + Swing.runLater(() -> updateCombo()); + return; + } + String themeName = (String) e.getItem(); + + Swing.runLater(() -> { + GTheme theme = Gui.getTheme(themeName); + Gui.setTheme(theme); + if (theme.getLookAndFeelType() == LafType.GTK) { + setStatusText( + "Warning - Themes using the GTK LookAndFeel do not support changing java component colors, fonts or icons.", + MessageType.ERROR); + } + else { + setStatusText(""); + } + colorTableModel.reloadAll(); + fontTableModel.reloadAll(); + iconTableModel.reloadAll(); + }); } protected void editColor(ColorValue value) { @@ -306,76 +175,31 @@ public class ThemeDialog extends DialogComponentProvider { } void colorValueChanged(PropertyChangeEvent event) { - ColorValue oldValue = (ColorValue) event.getOldValue(); - ColorValue newValue = (ColorValue) event.getNewValue(); - updateChangedValueMap(oldValue, newValue); // run later - don't rock the boat in the middle of a listener callback Swing.runLater(() -> { + ColorValue newValue = (ColorValue) event.getNewValue(); Gui.setColor(newValue); - colorTableModel.reloadCurrent(); }); } void fontValueChanged(PropertyChangeEvent event) { - FontValue oldValue = (FontValue) event.getOldValue(); - FontValue newValue = (FontValue) event.getNewValue(); - updateChangedValueMap(oldValue, newValue); // run later - don't rock the boat in the middle of a listener callback Swing.runLater(() -> { + FontValue newValue = (FontValue) event.getNewValue(); Gui.setFont(newValue); - fontTableModel.reloadCurrent(); }); } void iconValueChanged(PropertyChangeEvent event) { - IconValue oldValue = (IconValue) event.getOldValue(); - IconValue newValue = (IconValue) event.getNewValue(); - updateChangedValueMap(oldValue, newValue); // run later - don't rock the boat in the middle of a listener callback Swing.runLater(() -> { + IconValue newValue = (IconValue) event.getNewValue(); Gui.setIcon(newValue); - iconTableModel.reloadCurrent(); }); } - private void updateChangedValueMap(ColorValue oldValue, ColorValue newValue) { - ColorValue originalValue = changedValuesMap.getColor(oldValue.getId()); - if (originalValue == null) { - changedValuesMap.addColor(oldValue); - } - else if (originalValue.equals(newValue)) { - // if restoring the original color, remove it from the map of changes - changedValuesMap.removeColor(oldValue.getId()); - } - updateButtons(); - } - - private void updateChangedValueMap(FontValue oldValue, FontValue newValue) { - FontValue originalValue = changedValuesMap.getFont(oldValue.getId()); - if (originalValue == null) { - changedValuesMap.addFont(oldValue); - } - else if (originalValue.equals(newValue)) { - // if restoring the original color, remove it from the map of changes - changedValuesMap.removeFont(oldValue.getId()); - } - updateButtons(); - } - - private void updateChangedValueMap(IconValue oldValue, IconValue newValue) { - IconValue originalValue = changedValuesMap.getIcon(oldValue.getId()); - if (originalValue == null) { - changedValuesMap.addIcon(oldValue); - } - else if (originalValue.equals(newValue)) { - // if restoring the original color, remove it from the map of changes - changedValuesMap.removeFont(oldValue.getId()); - } - updateButtons(); - } - private void updateButtons() { - boolean hasChanges = hasChanges(); + boolean hasChanges = Gui.hasThemeChanges(); saveButton.setEnabled(hasChanges); restoreButton.setEnabled(hasChanges); } @@ -577,4 +401,40 @@ public class ThemeDialog extends DialogComponentProvider { DockingWindowManager.showDialog(INSTANCE); } + + @Override + public void close() { + Gui.removeThemeListener(listener); + super.close(); + } + + class DialogThemeListener implements ThemeListener { + @Override + public void themeChanged(GTheme newTheme) { + reset(); + } + + @Override + public void themeValuesRestored() { + reset(); + } + + @Override + public void fontChanged(String id) { + fontTableModel.reloadCurrent(); + updateButtons(); + } + + @Override + public void colorChanged(String id) { + colorTableModel.reloadCurrent(); + updateButtons(); + } + + @Override + public void iconChanged(String id) { + iconTableModel.reloadCurrent(); + updateButtons(); + } + } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeIconTableModel.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeIconTableModel.java index 22d0562245..31cfc47f76 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeIconTableModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeIconTableModel.java @@ -23,13 +23,11 @@ import java.util.function.Supplier; import javax.swing.*; -import docking.theme.*; import docking.widgets.table.*; import generic.theme.*; import ghidra.docking.settings.Settings; import ghidra.framework.plugintool.ServiceProvider; import ghidra.framework.plugintool.ServiceProviderStub; -import ghidra.util.Msg; import ghidra.util.table.column.AbstractGColumnRenderer; import ghidra.util.table.column.GColumnRenderer; import resources.icons.*; @@ -46,7 +44,6 @@ public class ThemeIconTableModel extends GDynamicColumnTableModel savedThemes = new ArrayList<>( + Gui.getAllThemes().stream().filter(t -> t instanceof FileGTheme).toList()); + if (savedThemes.isEmpty()) { + Msg.showInfo(ThemeUtils.class, null, "Delete Theme", "There are no deletable themes"); + return; + } + + GTheme selectedTheme = SelectFromListDialog.selectFromList(savedThemes, "Delete Theme", + "Select theme to delete", t -> t.getName()); + if (selectedTheme == null) { + return; + } + if (Gui.getActiveTheme().equals(selectedTheme)) { + Msg.showWarn(ThemeUtils.class, null, "Delete Failed", + "Can't delete the current theme."); + return; + } + FileGTheme fileTheme = (FileGTheme) selectedTheme; + int result = OptionDialog.showYesNoDialog(null, "Delete Theme: " + fileTheme.getName(), + "Are you sure you want to delete theme " + fileTheme.getName()); + if (result == OptionDialog.YES_OPTION) { + Gui.deleteTheme(fileTheme); + } + } + + private static String getNameFromUser(String name) { + InputDialog inputDialog = new InputDialog("Create Theme", "New Theme Name", name); + DockingWindowManager.showDialog(inputDialog); + return inputDialog.getValue(); + } + + private static boolean canSaveToName(String name) { + GTheme existing = Gui.getTheme(name); + if (existing == null) { + return true; + } + if (existing instanceof FileGTheme fileTheme) { + int result = OptionDialog.showYesNoDialog(null, "Overwrite Existing Theme?", + "Do you want to overwrite the existing theme file for \"" + name + "\"?"); + if (result == OptionDialog.YES_OPTION) { + return true; + } + } + return false; + } + + private static boolean saveCurrentValues(String themeName) { + GTheme activeTheme = Gui.getActiveTheme(); + File file = getSaveFile(themeName); + + FileGTheme newTheme = new FileGTheme(file, themeName, activeTheme.getLookAndFeelType(), + activeTheme.useDarkDefaults()); + newTheme.load(Gui.getNonDefaultValues()); + try { + newTheme.save(); + Gui.addTheme(newTheme); + Gui.setTheme(newTheme); + } + catch (IOException e) { + Msg.showError(ThemeUtils.class, null, "I/O Error", + "Error writing theme file: " + newTheme.getFile().getAbsolutePath(), e); + return false; + } + + return true; + + } + + private static File getSaveFile(String themeName) { + File dir = Application.getUserSettingsDirectory(); + File themeDir = new File(dir, Gui.THEME_DIR); + if (!themeDir.exists()) { + themeDir.mkdir(); + } + String cleanedName = themeName.replaceAll(" ", "_") + "." + GTheme.FILE_EXTENSION; + return new File(themeDir, cleanedName); + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeValueEditor.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeValueEditor.java index 25dca22dd5..439465113f 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeValueEditor.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeValueEditor.java @@ -56,13 +56,12 @@ public abstract class ThemeValueEditor { */ public void editValue(ThemeValue themeValue) { this.currentThemeValue = themeValue; - T value = getRawValue(themeValue.getId()); if (dialog == null) { - dialog = new EditorDialog(value); + dialog = new EditorDialog(themeValue); DockingWindowManager.showDialog(dialog); } else { - dialog.setValue(value); + dialog.setValue(themeValue); dialog.toFront(); } @@ -83,22 +82,30 @@ public abstract class ThemeValueEditor { */ protected abstract ThemeValue createNewThemeValue(String id, T newValue); - private void valueChanged(T newValue) { + private void valueChanged(T value) { ThemeValue oldValue = currentThemeValue; String id = oldValue.getId(); + ThemeValue newValue = createNewThemeValue(id, value); + firePropertyChangeEvent(oldValue, newValue); PropertyChangeEvent event = - new PropertyChangeEvent(this, id, oldValue, createNewThemeValue(id, newValue)); + new PropertyChangeEvent(this, id, oldValue, newValue); + clientListener.propertyChange(event); + } + + private void firePropertyChangeEvent(ThemeValue oldValue, ThemeValue newValue) { + PropertyChangeEvent event = + new PropertyChangeEvent(this, oldValue.getId(), oldValue, newValue); clientListener.propertyChange(event); } class EditorDialog extends DialogComponentProvider { private PropertyChangeListener internalListener = ev -> editorChanged(); - private T originalValue; + private ThemeValue originalValue; - protected EditorDialog(T initialValue) { + protected EditorDialog(ThemeValue initialValue) { super("Edit " + typeName + ": " + currentThemeValue.getId(), false, false, true, false); this.originalValue = initialValue; - addWorkPanel(buildWorkPanel(initialValue)); + addWorkPanel(buildWorkPanel(getRawValue(initialValue.getId()))); addOKButton(); addCancelButton(); setRememberSize(false); @@ -119,10 +126,10 @@ public abstract class ThemeValueEditor { return panel; } - void setValue(T value) { + void setValue(ThemeValue value) { originalValue = value; editor.removePropertyChangeListener(internalListener); - editor.setValue(value); + editor.setValue(getRawValue(value.getId())); editor.addPropertyChangeListener(internalListener); } @@ -134,7 +141,7 @@ public abstract class ThemeValueEditor { @Override protected void cancelCallback() { - valueChanged(originalValue); + firePropertyChangeEvent(currentThemeValue, originalValue); close(); dialog = null; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/SelectFromListDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/SelectFromListDialog.java similarity index 97% rename from Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/SelectFromListDialog.java rename to Ghidra/Framework/Docking/src/main/java/docking/widgets/SelectFromListDialog.java index b1236c7a6f..a1c1723da5 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/formats/gfilesystem/SelectFromListDialog.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/SelectFromListDialog.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.formats.gfilesystem; +package docking.widgets; import java.awt.BorderLayout; import java.awt.event.ActionEvent; @@ -25,7 +25,6 @@ import javax.swing.*; import docking.DialogComponentProvider; import docking.DockingWindowManager; -import docking.widgets.MultiLineLabel; import docking.widgets.list.ListPanel; import ghidra.util.SystemUtilities; @@ -92,6 +91,10 @@ public class SelectFromListDialog extends DialogComponentProvider { return selectedObject; } + public void setSelectedObject(T obj) { + listPanel.setSelectedValue(obj); + } + private void doSelect() { selectedObject = null; actionComplete = false; diff --git a/Ghidra/Framework/Docking/src/main/resources/images/mail-folder-outbox.png b/Ghidra/Framework/Docking/src/main/resources/images/mail-folder-outbox.png new file mode 100644 index 0000000000..63bc71b87c Binary files /dev/null and b/Ghidra/Framework/Docking/src/main/resources/images/mail-folder-outbox.png differ diff --git a/Ghidra/Framework/Docking/src/main/resources/images/mail-receive.png b/Ghidra/Framework/Docking/src/main/resources/images/mail-receive.png new file mode 100644 index 0000000000..ea9eac679f Binary files /dev/null and b/Ghidra/Framework/Docking/src/main/resources/images/mail-receive.png differ diff --git a/Ghidra/Framework/Docking/src/test/java/docking/theme/gui/ThemeUtilsTest.java b/Ghidra/Framework/Docking/src/test/java/docking/theme/gui/ThemeUtilsTest.java new file mode 100644 index 0000000000..6bf0c4a5fb --- /dev/null +++ b/Ghidra/Framework/Docking/src/test/java/docking/theme/gui/ThemeUtilsTest.java @@ -0,0 +1,217 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package docking.theme.gui; + +import static org.junit.Assert.*; + +import java.awt.Color; +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Set; + +import org.apache.commons.io.FileUtils; +import org.junit.Before; +import org.junit.Test; + +import docking.test.AbstractDockingTest; +import docking.widgets.OptionDialog; +import docking.widgets.SelectFromListDialog; +import docking.widgets.dialogs.InputDialog; +import generic.theme.*; +import generic.theme.builtin.MetalTheme; +import generic.theme.builtin.NimbusTheme; + +public class ThemeUtilsTest extends AbstractDockingTest { + + @Before + public void setup() { + GTheme nimbusTheme = new NimbusTheme(); + GTheme metalTheme = new MetalTheme(); + Gui.addTheme(nimbusTheme); + Gui.addTheme(metalTheme); + Gui.setTheme(nimbusTheme); + + // get rid of any leftover imported themes from previous tests + Set allThemes = Gui.getAllThemes(); + for (GTheme gTheme : allThemes) { + if (gTheme instanceof FileGTheme fileTheme) { + Gui.deleteTheme(fileTheme); + } + } + } + + @Test + public void testImportThemeNonZip() throws IOException { + assertEquals("Nimbus Theme", Gui.getActiveTheme().getName()); + File themeFile = createThemeFile("Bob"); + ThemeUtils.importTheme(themeFile); + assertEquals("Bob", Gui.getActiveTheme().getName()); + + } + + @Test + public void testImportThemeFromZip() throws IOException { + assertEquals("Nimbus Theme", Gui.getActiveTheme().getName()); + File themeFile = createZipThemeFile("zippy"); + ThemeUtils.importTheme(themeFile); + assertEquals("zippy", Gui.getActiveTheme().getName()); + } + + @Test + public void testImportThemeWithCurrentChangesCancelled() throws IOException { + assertEquals("Nimbus Theme", Gui.getActiveTheme().getName()); + Gui.setColor("Panel.background", Color.RED); + assertTrue(Gui.hasThemeChanges()); + + File themeFile = createThemeFile("Bob"); + runSwingLater(() -> ThemeUtils.importTheme(themeFile)); + OptionDialog dialog = waitForDialogComponent(OptionDialog.class); + assertNotNull(dialog); + assertEquals("Save Theme Changes?", dialog.getTitle()); + pressButtonByText(dialog, "Cancel"); + waitForSwing(); + assertEquals("Nimbus Theme", Gui.getActiveTheme().getName()); + } + + @Test + public void testImportThemeWithCurrentChangesSaved() throws IOException { + assertEquals("Nimbus Theme", Gui.getActiveTheme().getName()); + + // make a change in the current theme, so you get asked to save + Gui.setColor("Panel.background", Color.RED); + assertTrue(Gui.hasThemeChanges()); + + File themeFile = createThemeFile("Bob"); + runSwingLater(() -> ThemeUtils.importTheme(themeFile)); + OptionDialog dialog = waitForDialogComponent(OptionDialog.class); + assertNotNull(dialog); + assertEquals("Save Theme Changes?", dialog.getTitle()); + pressButtonByText(dialog, "Yes"); + InputDialog inputDialog = waitForDialogComponent(InputDialog.class); + assertNotNull(inputDialog); + runSwing(() -> inputDialog.setValue("Joe")); + pressButtonByText(inputDialog, "OK"); + waitForSwing(); + assertEquals("Bob", Gui.getActiveTheme().getName()); + assertNotNull(Gui.getTheme("Joe")); + } + + @Test + public void testImportThemeWithCurrentChangesThrownAway() throws IOException { + assertEquals("Nimbus Theme", Gui.getActiveTheme().getName()); + + // make a change in the current theme, so you get asked to save + Gui.setColor("Panel.background", Color.RED); + assertTrue(Gui.hasThemeChanges()); + + File bobThemeFile = createThemeFile("Bob"); + runSwingLater(() -> ThemeUtils.importTheme(bobThemeFile)); + + OptionDialog dialog = waitForDialogComponent(OptionDialog.class); + assertNotNull(dialog); + assertEquals("Save Theme Changes?", dialog.getTitle()); + pressButtonByText(dialog, "No"); + waitForSwing(); + assertEquals("Bob", Gui.getActiveTheme().getName()); + } + + @Test + public void testExportThemeAsZip() throws IOException { + runSwingLater(() -> ThemeUtils.exportTheme()); + OptionDialog dialog = waitForDialogComponent(OptionDialog.class); + pressButtonByText(dialog, "Export Zip"); + ExportThemeDialog exportDialog = waitForDialogComponent(ExportThemeDialog.class); + File exportFile = createTempFile("whatever", ".theme.zip"); + runSwing(() -> exportDialog.setOutputFile(exportFile)); + pressButtonByText(exportDialog, "OK"); + waitForSwing(); + assertTrue(exportFile.exists()); + ZipGTheme zipTheme = new ZipGTheme(exportFile); + assertEquals("Nimbus Theme", zipTheme.getName()); + } + + @Test + public void testExportThemeAsFile() throws IOException { + runSwingLater(() -> ThemeUtils.exportTheme()); + OptionDialog dialog = waitForDialogComponent(OptionDialog.class); + pressButtonByText(dialog, "Export File"); + ExportThemeDialog exportDialog = waitForDialogComponent(ExportThemeDialog.class); + File exportFile = createTempFile("whatever", ".theme"); + runSwing(() -> exportDialog.setOutputFile(exportFile)); + pressButtonByText(exportDialog, "OK"); + waitForSwing(); + assertTrue(exportFile.exists()); + FileGTheme fileTheme = new FileGTheme(exportFile); + assertEquals("Nimbus Theme", fileTheme.getName()); + } + + @Test + public void testDeleteTheme() throws IOException { + File themeFile = createThemeFile("Bob"); + ThemeUtils.importTheme(themeFile); + themeFile = createThemeFile("Joe"); + ThemeUtils.importTheme(themeFile); + themeFile = createThemeFile("Lisa"); + ThemeUtils.importTheme(themeFile); + + assertNotNull(Gui.getTheme("Bob")); + assertNotNull(Gui.getTheme("Joe")); + assertNotNull(Gui.getTheme("Lisa")); + + runSwingLater(() -> ThemeUtils.deleteTheme()); + @SuppressWarnings("unchecked") + SelectFromListDialog dialog = waitForDialogComponent(SelectFromListDialog.class); + runSwing(() -> dialog.setSelectedObject(Gui.getTheme("Bob"))); + pressButtonByText(dialog, "OK"); + + OptionDialog optionDialog = waitForDialogComponent(OptionDialog.class); + pressButtonByText(optionDialog, "Yes"); + waitForSwing(); + + assertNotNull(Gui.getTheme("Bob")); + assertNull(Gui.getTheme("Joe")); + assertNotNull(Gui.getTheme("Lisa")); + + } + + private File createZipThemeFile(String themeName) throws IOException { + File file = createTempFile("Test_Theme", ".theme.zip"); + ZipGTheme zipGTheme = new ZipGTheme(file, themeName, LafType.METAL, false); + zipGTheme.addColor(new ColorValue("Panel.Background", Color.RED)); + zipGTheme.save(); + return file; + } + + private File createThemeFile(String themeName) throws IOException { + String themeData = createThemeDataString(themeName); + File file = createTempFile("Test_Theme", ".theme"); + FileUtils.writeStringToFile(file, themeData, Charset.defaultCharset()); + return file; + } + + private String createThemeDataString(String themeName) { + String themeData = """ + name = THEMENAME + lookAndFeel = Metal + useDarkDefaults = false + [color]Panel.background = #ffcccc + """; + + return themeData.replace("THEMENAME", themeName); + } + +} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ExternalThemeReader.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ExternalThemeReader.java new file mode 100644 index 0000000000..43b3023b51 --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ExternalThemeReader.java @@ -0,0 +1,63 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package generic.theme; + +import java.io.*; +import java.util.Enumeration; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.apache.commons.io.FileUtils; + +import ghidra.framework.Application; +import ghidra.util.Msg; + +public class ExternalThemeReader extends ThemeReader { + + public ExternalThemeReader(File file) throws IOException { + try (ZipFile zipFile = new ZipFile(file)) { + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + String name = entry.getName(); + try (InputStream is = zipFile.getInputStream(entry)) { + if (name.endsWith(".theme")) { + processThemeData(name, is); + } + else { + processIconFile(name, is); + } + } + } + } + } + + private void processIconFile(String path, InputStream is) throws IOException { + int indexOf = path.indexOf("images/"); + if (indexOf < 0) { + Msg.error(this, "Unknown file: " + path); + } + String relativePath = path.substring(indexOf, path.length()); + File dir = Application.getUserSettingsDirectory(); + File iconFile = new File(dir, relativePath); + FileUtils.copyInputStreamToFile(is, iconFile); + } + + private void processThemeData(String name, InputStream is) throws IOException { + InputStreamReader reader = new InputStreamReader(is); + read(reader); + } +} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/FileGTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/FileGTheme.java index 00b5fd9a4a..8de78f5948 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/FileGTheme.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/FileGTheme.java @@ -27,8 +27,9 @@ import ghidra.util.WebColors; import resources.icons.UrlImageIcon; public class FileGTheme extends GTheme { + public static final String JAVA_ICON = ""; public static final String FILE_PREFIX = "File:"; - private final File file; + protected final File file; public FileGTheme(File file) throws IOException { this(file, new ThemeReader(file)); @@ -40,7 +41,7 @@ public class FileGTheme extends GTheme { } FileGTheme(File file, ThemeReader reader) { - super(reader.getThemeName(), reader.getLookAndFeelType(), false); + super(reader.getThemeName(), reader.getLookAndFeelType(), reader.useDarkDefaults()); this.file = file; reader.loadValues(this); } @@ -63,46 +64,50 @@ public class FileGTheme extends GTheme { public void save() throws IOException { try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { - List colors = getColors(); - Collections.sort(colors); - - List fonts = getFonts(); - Collections.sort(fonts); - - List icons = getIcons(); - Collections.sort(icons); - - writer.write(THEME_NAME_KEY + " = " + getName()); - writer.newLine(); - - writer.write(THEME_LOOK_AND_FEEL_KEY + " = " + getLookAndFeelType().getName()); - writer.newLine(); - - writer.write(THEME_USE_DARK_DEFAULTS + " = " + useDarkDefaults()); - writer.newLine(); - - for (ColorValue colorValue : colors) { - String outputId = colorValue.toExternalId(colorValue.getId()); - writer.write(outputId + " = " + getValueOutput(colorValue)); - writer.newLine(); - } - - for (FontValue fontValue : fonts) { - String outputId = fontValue.toExternalId(fontValue.getId()); - writer.write(outputId + " = " + getValueOutput(fontValue)); - writer.newLine(); - } - - for (IconValue iconValue : icons) { - String outputId = iconValue.toExternalId(iconValue.getId()); - writer.write(outputId + " = " + getValueOutput(iconValue)); - writer.newLine(); - } + writeThemeValues(writer); } } + protected void writeThemeValues(BufferedWriter writer) throws IOException { + List colors = getColors(); + Collections.sort(colors); + + List fonts = getFonts(); + Collections.sort(fonts); + + List icons = getIcons(); + Collections.sort(icons); + + writer.write(THEME_NAME_KEY + " = " + getName()); + writer.newLine(); + + writer.write(THEME_LOOK_AND_FEEL_KEY + " = " + getLookAndFeelType().getName()); + writer.newLine(); + + writer.write(THEME_USE_DARK_DEFAULTS + " = " + useDarkDefaults()); + writer.newLine(); + + for (ColorValue colorValue : colors) { + String outputId = colorValue.toExternalId(colorValue.getId()); + writer.write(outputId + " = " + getValueOutput(colorValue)); + writer.newLine(); + } + + for (FontValue fontValue : fonts) { + String outputId = fontValue.toExternalId(fontValue.getId()); + writer.write(outputId + " = " + getValueOutput(fontValue)); + writer.newLine(); + } + + for (IconValue iconValue : icons) { + String outputId = iconValue.toExternalId(iconValue.getId()); + writer.write(outputId + " = " + getValueOutput(iconValue)); + writer.newLine(); + } + } + private String getValueOutput(ColorValue colorValue) { if (colorValue.getReferenceId() != null) { return colorValue.toExternalId(colorValue.getReferenceId()); @@ -128,7 +133,7 @@ public class FileGTheme extends GTheme { if (icon instanceof UrlImageIcon urlIcon) { return urlIcon.getOriginalPath(); } - return ""; + return JAVA_ICON; } private String getValueOutput(FontValue fontValue) { diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/GTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/GTheme.java index 59046860e5..594148b371 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/GTheme.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/GTheme.java @@ -28,7 +28,8 @@ import javax.swing.Icon; * in an application. */ public class GTheme extends GThemeValueMap { - public static String FILE_EXTENSION = ".theme"; + public static String FILE_EXTENSION = "theme"; + public static String ZIP_FILE_EXTENSION = "theme.zip"; static final String THEME_NAME_KEY = "name"; static final String THEME_LOOK_AND_FEEL_KEY = "lookAndFeel"; @@ -39,7 +40,7 @@ public class GTheme extends GThemeValueMap { private final boolean useDarkDefaults; public GTheme(String name) { - this(name, LafType.SYSTEM, false); + this(name, LafType.getDefaultLookAndFeel(), false); } @@ -49,7 +50,7 @@ public class GTheme extends GThemeValueMap { * @param lookAndFeel the look and feel type used by this theme * @param useDarkDefaults determines whether or */ - protected GTheme(String name, LafType lookAndFeel, boolean useDarkDefaults) { + public GTheme(String name, LafType lookAndFeel, boolean useDarkDefaults) { this.name = name; this.lookAndFeel = lookAndFeel; this.useDarkDefaults = useDarkDefaults; diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/GThemeValueMap.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/GThemeValueMap.java index ccdd38ede8..43fde6cc9f 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/GThemeValueMap.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/GThemeValueMap.java @@ -15,12 +15,19 @@ */ package generic.theme; +import java.io.File; +import java.net.URL; import java.util.*; +import javax.swing.Icon; + +import resources.ResourceManager; +import resources.icons.UrlImageIcon; + public class GThemeValueMap { - Map colorMap = new HashMap<>(); - Map fontMap = new HashMap<>(); - Map iconMap = new HashMap<>(); + protected Map colorMap = new HashMap<>(); + protected Map fontMap = new HashMap<>(); + protected Map iconMap = new HashMap<>(); public GThemeValueMap() { } @@ -132,4 +139,29 @@ public class GThemeValueMap { fontMap.remove(id); } + public void removeIcon(String id) { + iconMap.remove(id); + } + + public Set getExternalIconFiles() { + Set files = new HashSet<>(); + for (IconValue iconValue : iconMap.values()) { + Icon icon = iconValue.getRawValue(); + if (icon instanceof UrlImageIcon urlIcon) { + String originalPath = urlIcon.getOriginalPath(); + if (originalPath.startsWith(ResourceManager.EXTERNAL_ICON_PREFIX)) { + URL url = urlIcon.getUrl(); + String filePath = url.getFile(); + if (filePath != null) { + File iconFile = new File(filePath); + if (iconFile.exists()) { + files.add(iconFile); + } + } + } + } + } + return files; + } + } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/Gui.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/Gui.java index 47899a5496..e629b9526a 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/Gui.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/Gui.java @@ -15,18 +15,20 @@ */ package generic.theme; -import java.awt.*; +import java.awt.Color; +import java.awt.Font; import java.io.*; import java.util.*; -import java.util.List; -import javax.swing.*; +import javax.swing.Icon; +import javax.swing.UIManager; import javax.swing.plaf.ComponentUI; import com.formdev.flatlaf.*; -import generic.theme.builtin.JavaColorMapping; -import ghidra.framework.Application; +import generic.theme.builtin.*; +import generic.theme.laf.LookAndFeelManager; +import ghidra.framework.*; import ghidra.framework.preferences.Preferences; import ghidra.util.Msg; import ghidra.util.classfinder.ClassSearcher; @@ -42,8 +44,8 @@ public class Gui { private static final String THEME_PREFFERENCE_KEY = "Theme"; - private static GTheme activeTheme = new DefaultTheme(); - private static Set allThemes; + private static GTheme activeTheme = getDefaultTheme(); + private static Set allThemes = null; private static GThemeValueMap ghidraLightDefaults = new GThemeValueMap(); private static GThemeValueMap ghidraDarkDefaults = new GThemeValueMap(); @@ -55,8 +57,15 @@ public class Gui { private static Map gColorMap = new HashMap<>(); private static boolean isInitialized; private static Map gIconMap = new HashMap<>(); + + // these notifications are only when the user is manipulating theme values, so rare and at + // user speed, so using copy on read private static WeakSet themeListeners = - WeakDataStructureFactory.createCopyOnWriteWeakSet(); + WeakDataStructureFactory.createCopyOnReadWeakSet(); + + // stores the original value for ids whose value has changed from the current theme + private static GThemeValueMap changedValuesMap = new GThemeValueMap(); + private static LookAndFeelManager lookAndFeelManager; private Gui() { // static utils class, can't construct @@ -86,23 +95,25 @@ public class Gui { public static void reloadGhidraDefaults() { loadThemeDefaults(); buildCurrentValues(); + lookAndFeelManager.update(); + notifyThemeValuesRestored(); } public static void restoreThemeValues() { buildCurrentValues(); + lookAndFeelManager.update(); + notifyThemeValuesRestored(); } public static void setTheme(GTheme theme) { if (theme.hasSupportedLookAndFeel()) { activeTheme = theme; LafType lookAndFeel = theme.getLookAndFeelType(); + lookAndFeelManager = lookAndFeel.getLookAndFeelManager(); try { - lookAndFeel.install(); + lookAndFeelManager.installLookAndFeel(); + notifyThemeChanged(); saveThemeToPreferences(theme); - fixupJavaDefaults(); - buildCurrentValues(); - updateUIs(); - notifyThemeListeners(); } catch (Exception e) { Msg.error(Gui.class, "Error setting LookAndFeel: " + lookAndFeel.getName(), e); @@ -110,20 +121,46 @@ public class Gui { } } - private static void notifyThemeListeners() { + private static void notifyThemeChanged() { for (ThemeListener listener : themeListeners) { listener.themeChanged(activeTheme); } } + private static void notifyThemeValuesRestored() { + for (ThemeListener listener : themeListeners) { + listener.themeValuesRestored(); + } + } + + private static void notifyColorChanged(String id) { + for (ThemeListener listener : themeListeners) { + listener.colorChanged(id); + } + } + + private static void notifyFontChanged(String id) { + for (ThemeListener listener : themeListeners) { + listener.fontChanged(id); + } + } + + private static void notifyIconChanged(String id) { + for (ThemeListener listener : themeListeners) { + listener.iconChanged(id); + } + } + public static void addTheme(GTheme newTheme) { + loadThemes(); allThemes.remove(newTheme); allThemes.add(newTheme); } - private static void updateUIs() { - for (Window window : Window.getWindows()) { - SwingUtilities.updateComponentTreeUI(window); + public static void deleteTheme(FileGTheme theme) { + theme.file.delete(); + if (allThemes != null) { + allThemes.remove(theme); } } @@ -140,16 +177,12 @@ public class Gui { } public static Set getAllThemes() { - if (allThemes == null) { - allThemes = findThemes(); - } - return Collections.unmodifiableSet(allThemes); + loadThemes(); + return new HashSet<>(allThemes); } public static Set getSupportedThemes() { - if (allThemes == null) { - allThemes = findThemes(); - } + loadThemes(); Set supported = new HashSet<>(); for (GTheme theme : allThemes) { if (theme.hasSupportedLookAndFeel()) { @@ -251,26 +284,21 @@ public class Gui { } map.load(activeTheme); currentValues = map; - GColor.refreshAll(); - GIcon.refreshAll(); - repaintAll(); + changedValuesMap.clear(); } - private static Set findThemes() { - Set set = new HashSet<>(); - set.addAll(findDiscoverableThemes()); - set.addAll(loadThemesFromFiles()); - - // The set should contains a duplicate of the active theme. Make sure the active theme - // instance is the one in the set - set.remove(activeTheme); - set.add(activeTheme); - return set; + private static void loadThemes() { + if (allThemes == null) { + Set set = new HashSet<>(); + set.addAll(findDiscoverableThemes()); + set.addAll(loadThemesFromFiles()); + allThemes = set; + } } private static Collection loadThemesFromFiles() { List fileList = new ArrayList<>(); - FileFilter themeFileFilter = file -> file.getName().endsWith(GTheme.FILE_EXTENSION); + FileFilter themeFileFilter = file -> file.getName().endsWith("." + GTheme.FILE_EXTENSION); File dir = Application.getUserSettingsDirectory(); File themeDir = new File(dir, THEME_DIR); @@ -326,34 +354,84 @@ public class Gui { "Can't find or instantiate class: " + className); } } - return new DefaultTheme(); + return getDefaultTheme(); } public static void setFont(FontValue newValue) { + FontValue currentValue = currentValues.getFont(newValue.getId()); + if (newValue.equals(currentValue)) { + return; + } + updateChangedValuesMap(currentValue, newValue); + currentValues.addFont(newValue); // all fonts are direct (there is no GFont), so to we need to update the // UiDefaults for java fonts. Ghidra fonts are expected to be "on the fly" (they // call Gui.getFont(id) for every use. String id = newValue.getId(); - if (javaDefaults.containsFont(id)) { - UIManager.getDefaults().put(id, newValue.get(currentValues)); - updateUIs(); - } - else { - repaintAll(); - } + boolean isJavaFont = javaDefaults.containsFont(id); + lookAndFeelManager.updateFont(id, newValue.get(currentValues), isJavaFont); + notifyFontChanged(id); } public static void setColor(String id, Color color) { setColor(new ColorValue(id, color)); } - public static void setColor(ColorValue colorValue) { - currentValues.addColor(colorValue); - // all colors use indirection via GColor, so to update all we need to do is refresh GColors - // and repaint - GColor.refreshAll(); - repaintAll(); + public static void setColor(ColorValue newValue) { + ColorValue currentValue = currentValues.getColor(newValue.getId()); + if (newValue.equals(currentValue)) { + return; + } + updateChangedValuesMap(currentValue, newValue); + + currentValues.addColor(newValue); + String id = newValue.getId(); + boolean isJavaColor = javaDefaults.containsColor(id); + lookAndFeelManager.updateColor(id, newValue.get(currentValues), isJavaColor); + notifyColorChanged(newValue.getId()); + } + + private static void updateChangedValuesMap(ColorValue currentValue, ColorValue newValue) { + String id = newValue.getId(); + ColorValue originalValue = changedValuesMap.getColor(id); + + // if new value is original value, it is no longer changed, remove it from changed map + if (newValue.equals(originalValue)) { + changedValuesMap.removeColor(id); + } + else if (originalValue == null) { + // first time changed, so current value is original value + changedValuesMap.addColor(currentValue); + } + } + + private static void updateChangedValuesMap(FontValue currentValue, FontValue newValue) { + String id = newValue.getId(); + FontValue originalValue = changedValuesMap.getFont(id); + + // if new value is original value, it is no longer changed, remove it from changed map + if (newValue.equals(originalValue)) { + changedValuesMap.removeFont(id); + } + else if (originalValue == null) { + // first time changed, so current value is original value + changedValuesMap.addFont(currentValue); + } + } + + private static void updateChangedValuesMap(IconValue currentValue, IconValue newValue) { + String id = newValue.getId(); + IconValue originalValue = changedValuesMap.getIcon(id); + + // if new value is original value, it is no longer changed, remove it from changed map + if (newValue.equals(originalValue)) { + changedValuesMap.removeIcon(id); + } + else if (originalValue == null) { + // first time changed, so current value is original value + changedValuesMap.addIcon(currentValue); + } } public static void setIcon(String id, Icon icon) { @@ -361,31 +439,20 @@ public class Gui { } public static void setIcon(IconValue newValue) { + IconValue currentValue = currentValues.getIcon(newValue.getId()); + if (newValue.equals(currentValue)) { + return; + } + updateChangedValuesMap(currentValue, newValue); + currentValues.addIcon(newValue); - - // Icons are a mixed bag. Java Icons are direct and Ghidra Icons are indirect (to support static use) - // Mainly because Nimbus is buggy and can't handle non-nimbus Icons, so we can't wrap them - // So need to update UiDefaults for java icons. For Ghidra Icons, it is sufficient to refrech - // GIcons and repaint String id = newValue.getId(); - if (javaDefaults.containsIcon(id)) { - UIManager.getDefaults().put(id, newValue.get(currentValues)); - updateUIs(); - } - else { - GIcon.refreshAll(); - repaintAll(); - } - } - - private static void repaintAll() { - for (Window window : Window.getWindows()) { - window.repaint(); - } + boolean isJavaIcon = javaDefaults.containsIcon(id); + lookAndFeelManager.updateIcon(id, newValue.get(currentValues), isJavaIcon); + notifyIconChanged(id); } public static GColorUIResource getGColorUiResource(String id) { - GColorUIResource gColor = gColorMap.get(id); if (gColor == null) { gColor = new GColorUIResource(id); @@ -405,11 +472,13 @@ public class Gui { } public static void setJavaDefaults(GThemeValueMap map) { - javaDefaults = map; + javaDefaults = fixupJavaDefaultsInheritence(map); buildCurrentValues(); + GColor.refreshAll(); + GIcon.refreshAll(); } - public static void fixupJavaDefaults() { + public static GThemeValueMap fixupJavaDefaultsInheritence(GThemeValueMap map) { List colors = javaDefaults.getColors(); JavaColorMapping mapping = new JavaColorMapping(); for (ColorValue value : colors) { @@ -418,6 +487,7 @@ public class Gui { javaDefaults.addColor(mapped); } } + return map; } public static GThemeValueMap getJavaDefaults() { @@ -476,4 +546,22 @@ public class Gui { themePropertiesLoader = loader; } + public static GTheme getDefaultTheme() { + OperatingSystem OS = Platform.CURRENT_PLATFORM.getOperatingSystem(); + switch (OS) { + case MAC_OS_X: + return new MacTheme(); + case WINDOWS: + return new WindowsTheme(); + case LINUX: + case UNSUPPORTED: + default: + return new NimbusTheme(); + } + } + + public static boolean hasThemeChanges() { + return !changedValuesMap.isEmpty(); + } + } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/LafType.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/LafType.java index 4384c8d9c7..e6fada20d1 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/LafType.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/LafType.java @@ -33,8 +33,7 @@ public enum LafType { FLAT_DARCULA("Flat Darcula"), WINDOWS("Windows"), WINDOWS_CLASSIC("Windows Classic"), - MAC("Mac OS X"), - SYSTEM("System"); + MAC("Mac OS X"); private String name; @@ -55,24 +54,7 @@ public enum LafType { return null; } - private static LookAndFeelInstaller getSystemLookAndFeelInstaller() { - OperatingSystem OS = Platform.CURRENT_PLATFORM.getOperatingSystem(); - if (OS == OperatingSystem.LINUX) { - return getInstaller(NIMBUS); - } - else if (OS == OperatingSystem.MAC_OS_X) { - return getInstaller(MAC); - } - else if (OS == OperatingSystem.WINDOWS) { - return getInstaller(WINDOWS); - } - return getInstaller(NIMBUS); - } - public boolean isSupported() { - if (this == SYSTEM) { - return true; - } LookAndFeelInfo[] installedLookAndFeels = UIManager.getInstalledLookAndFeels(); for (LookAndFeelInfo info : installedLookAndFeels) { if (name.equals(info.getName())) { @@ -82,36 +64,43 @@ public enum LafType { return false; } - public void install() throws Exception { - getInstaller(this).install(); + public LookAndFeelManager getLookAndFeelManager() { + return getManager(this); } - private static LookAndFeelInstaller getInstaller(LafType lookAndFeel) { + private static LookAndFeelManager getManager(LafType lookAndFeel) { switch (lookAndFeel) { - case FLAT_DARCULA: - return new FlatLookAndFeelInstaller(FLAT_DARCULA); - case FLAT_DARK: - return new FlatLookAndFeelInstaller(FLAT_DARK); - case FLAT_LIGHT: - return new FlatLookAndFeelInstaller(FLAT_LIGHT); - case GTK: - return new GTKLookAndFeelInstaller(); case MAC: - return new LookAndFeelInstaller(MAC); case METAL: - return new LookAndFeelInstaller(METAL); - case MOTIF: - return new MotifLookAndFeelInstaller(); // Motif has some specific ui fix ups - case NIMBUS: - return new NimbusLookAndFeelInstaller(); // Nimbus installs a special way - case SYSTEM: - return getSystemLookAndFeelInstaller(); case WINDOWS: - return new LookAndFeelInstaller(WINDOWS); case WINDOWS_CLASSIC: - return new LookAndFeelInstaller(WINDOWS_CLASSIC); + return new GenericLookAndFeelManager(lookAndFeel); + case FLAT_DARCULA: + case FLAT_DARK: + case FLAT_LIGHT: + return new GenericFlatLookAndFeelManager(lookAndFeel); + case GTK: + return new GtkLookAndFeelManager(); + case MOTIF: + return new MotifLookAndFeelManager(); + case NIMBUS: + return new NimbusLookAndFeelManager(); default: - throw new AssertException("No lookAndFeelInstaller defined for " + lookAndFeel); + throw new AssertException("No lookAndFeelManager defined for " + lookAndFeel); + } + } + + public static LafType getDefaultLookAndFeel() { + OperatingSystem OS = Platform.CURRENT_PLATFORM.getOperatingSystem(); + switch (OS) { + case MAC_OS_X: + return MAC; + case WINDOWS: + return WINDOWS; + case LINUX: + case UNSUPPORTED: + default: + return NIMBUS; } } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeListener.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeListener.java index 55d58820fc..b273adc3a1 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeListener.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeListener.java @@ -16,5 +16,23 @@ package generic.theme; public interface ThemeListener { - public void themeChanged(GTheme newTheme); + public default void themeChanged(GTheme newTheme) { + // default do nothing + } + + public default void colorChanged(String id) { + // default do nothing + } + + public default void fontChanged(String id) { + // default do nothing + } + + public default void iconChanged(String id) { + // default do nothing + } + + public default void themeValuesRestored() { + // default do nothing + } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePropertyFileReader.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePropertyFileReader.java index 7893ca2e25..db7ca47808 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePropertyFileReader.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemePropertyFileReader.java @@ -56,6 +56,10 @@ public class ThemePropertyFileReader { } } + protected ThemePropertyFileReader() { + + } + ThemePropertyFileReader(String source, Reader reader) throws IOException { filePath = source; read(reader); @@ -77,7 +81,7 @@ public class ThemePropertyFileReader { return errors; } - private void read(Reader reader) throws IOException { + protected void read(Reader reader) throws IOException { List

sections = readSections(new LineNumberReader(reader)); for (Section section : sections) { switch (section.getName()) { @@ -116,7 +120,9 @@ public class ThemePropertyFileReader { valueMap.addFont(parseFontProperty(key, value, lineNumber)); } else if (IconValue.isIconKey(key)) { - valueMap.addIcon(parseIconProperty(key, value)); + if (!FileGTheme.JAVA_ICON.equals(value)) { + valueMap.addIcon(parseIconProperty(key, value)); + } } else { error(lineNumber, "Can't process property: " + key + " = " + value); diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeReader.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeReader.java index 1eaa2c3813..d56fcf3ab7 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeReader.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ThemeReader.java @@ -29,6 +29,10 @@ public class ThemeReader extends ThemePropertyFileReader { super(file); } + protected ThemeReader() { + + } + @Override protected void processNoSection(Section section) throws IOException { themeSection = section; @@ -63,4 +67,7 @@ public class ThemeReader extends ThemePropertyFileReader { return lookAndFeel; } + public boolean useDarkDefaults() { + return useDarkDefaults; + } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/ZipGTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/ZipGTheme.java new file mode 100644 index 0000000000..9d1d496486 --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/ZipGTheme.java @@ -0,0 +1,68 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package generic.theme; + +import java.io.*; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import com.google.common.io.Files; + +public class ZipGTheme extends FileGTheme { + + public ZipGTheme(File file, String name, LafType laf, boolean useDarkDefaults) { + super(file, name, laf, useDarkDefaults); + } + + public ZipGTheme(File file) throws IOException { + this(file, new ExternalThemeReader(file)); + } + + public ZipGTheme(File file, ThemeReader reader) { + super(file, reader.getThemeName(), reader.getLookAndFeelType(), reader.useDarkDefaults()); + reader.loadValues(this); + } + + @Override + public void save() throws IOException { + String dir = getName() + ".theme/"; + try (FileOutputStream fos = new FileOutputStream(file)) { + ZipOutputStream zos = new ZipOutputStream(fos); + saveThemeFileToZip(dir, zos); + Set iconFiles = getExternalIconFiles(); + for (File iconFile : iconFiles) { + copyToZipFile(dir, iconFile, zos); + } + zos.finish(); + } + } + + private void copyToZipFile(String dir, File iconFile, ZipOutputStream zos) throws IOException { + ZipEntry entry = new ZipEntry(dir + "images/" + iconFile.getName()); + zos.putNextEntry(entry); + Files.copy(iconFile, zos); + } + + private void saveThemeFileToZip(String dir, ZipOutputStream zos) throws IOException { + ZipEntry entry = new ZipEntry(dir + getName() + ".theme"); + zos.putNextEntry(entry); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(zos)); + writeThemeValues(writer); + writer.flush(); + } + +} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/CDEMotifTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/CDEMotifTheme.java index 9b20383c0a..9c95a19d0c 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/CDEMotifTheme.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/CDEMotifTheme.java @@ -21,7 +21,7 @@ import generic.theme.LafType; public class CDEMotifTheme extends DiscoverableGTheme { public CDEMotifTheme() { - super("Motif", LafType.MOTIF, false); + super("Motif Theme", LafType.MOTIF, false); } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatDarculaTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatDarculaTheme.java index d3889efa9d..dd0b021a2d 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatDarculaTheme.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatDarculaTheme.java @@ -20,6 +20,6 @@ import generic.theme.LafType; public class FlatDarculaTheme extends DiscoverableGTheme { public FlatDarculaTheme() { - super("Flat Darcula", LafType.FLAT_DARCULA, true); + super("Flat Darcula Theme", LafType.FLAT_DARCULA, true); } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatDarkTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatDarkTheme.java index 989beceff1..66e43dc376 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatDarkTheme.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatDarkTheme.java @@ -20,6 +20,6 @@ import generic.theme.LafType; public class FlatDarkTheme extends DiscoverableGTheme { public FlatDarkTheme() { - super("Flat Dark", LafType.FLAT_DARK, true); + super("Flat Dark Theme", LafType.FLAT_DARK, true); } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatLightTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatLightTheme.java index 1a6a249812..45e5de4ae2 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatLightTheme.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/FlatLightTheme.java @@ -21,7 +21,7 @@ import generic.theme.LafType; public class FlatLightTheme extends DiscoverableGTheme { public FlatLightTheme() { - super("Flat Light", LafType.FLAT_LIGHT, false); + super("Flat Light Theme", LafType.FLAT_LIGHT, false); } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/GTKTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/GTKTheme.java index 7644cbd668..6d3e964c46 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/GTKTheme.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/GTKTheme.java @@ -21,7 +21,7 @@ import generic.theme.LafType; public class GTKTheme extends DiscoverableGTheme { public GTKTheme() { - super("GTK+", LafType.GTK, false); + super("GTK+ Theme", LafType.GTK, false); } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/MacTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/MacTheme.java index 98167df786..903edcf114 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/MacTheme.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/MacTheme.java @@ -21,6 +21,6 @@ import generic.theme.LafType; public class MacTheme extends DiscoverableGTheme { public MacTheme() { - super("Mac OS X", LafType.MAC, false); + super("Mac OS X Theme", LafType.MAC, false); } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/MetalTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/MetalTheme.java index fbb2fa0d9a..8a9ffecfa8 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/MetalTheme.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/MetalTheme.java @@ -21,7 +21,7 @@ import generic.theme.LafType; public class MetalTheme extends DiscoverableGTheme { public MetalTheme() { - super("Metal", LafType.METAL, false); + super("Metal Theme", LafType.METAL, false); } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/NimbusTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/NimbusTheme.java index 117fbb1b7c..8b244dc213 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/NimbusTheme.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/NimbusTheme.java @@ -21,7 +21,7 @@ import generic.theme.LafType; public class NimbusTheme extends DiscoverableGTheme { public NimbusTheme() { - super("Nimbus", LafType.NIMBUS, false); + super("Nimbus Theme", LafType.NIMBUS, false); } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/WindowsClassicTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/WindowsClassicTheme.java index 8286d13d42..9790379cad 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/WindowsClassicTheme.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/WindowsClassicTheme.java @@ -21,6 +21,6 @@ import generic.theme.LafType; public class WindowsClassicTheme extends DiscoverableGTheme { public WindowsClassicTheme() { - super("Windows Classic", LafType.WINDOWS_CLASSIC, false); + super("Windows Classic Theme", LafType.WINDOWS_CLASSIC, false); } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/WindowsTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/WindowsTheme.java index 1ee1dc4992..f7ed0f2f5a 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/WindowsTheme.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/builtin/WindowsTheme.java @@ -21,6 +21,6 @@ import generic.theme.LafType; public class WindowsTheme extends DiscoverableGTheme { public WindowsTheme() { - super("Windows", LafType.WINDOWS, false); + super("Windows Theme", LafType.WINDOWS, false); } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GenericFlatLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GenericFlatLookAndFeelManager.java new file mode 100644 index 0000000000..0877b57a0c --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GenericFlatLookAndFeelManager.java @@ -0,0 +1,34 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package generic.theme.laf; + +import generic.theme.LafType; + +/** + * Common {@link LookAndFeelInstaller} for any of the "Flat" lookAndFeels + */ +public class GenericFlatLookAndFeelManager extends LookAndFeelManager { + + public GenericFlatLookAndFeelManager(LafType laf) { + super(laf); + } + + @Override + protected LookAndFeelInstaller getLookAndFeelInstaller() { + return new FlatLookAndFeelInstaller(getLookAndFeelType()); + } + +} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GenericLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GenericLookAndFeelManager.java new file mode 100644 index 0000000000..411a5fc835 --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GenericLookAndFeelManager.java @@ -0,0 +1,35 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package generic.theme.laf; + +import generic.theme.LafType; + +/** + * Generic {@link LookAndFeelManager} for lookAndFeels that do not require any special handling + * to install or update + */ +public class GenericLookAndFeelManager extends LookAndFeelManager { + + public GenericLookAndFeelManager(LafType laf) { + super(laf); + } + + @Override + protected LookAndFeelInstaller getLookAndFeelInstaller() { + return new LookAndFeelInstaller(getLookAndFeelType()); + } + +} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GTKLookAndFeelInstaller.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelInstaller.java similarity index 68% rename from Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GTKLookAndFeelInstaller.java rename to Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelInstaller.java index fecaa8a017..bcc955381b 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GTKLookAndFeelInstaller.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelInstaller.java @@ -15,13 +15,13 @@ */ package generic.theme.laf; -import javax.swing.*; +import javax.swing.UnsupportedLookAndFeelException; import generic.theme.LafType; -public class GTKLookAndFeelInstaller extends LookAndFeelInstaller { +public class GtkLookAndFeelInstaller extends LookAndFeelInstaller { - public GTKLookAndFeelInstaller() { + public GtkLookAndFeelInstaller() { super(LafType.GTK); } @@ -30,14 +30,12 @@ public class GTKLookAndFeelInstaller extends LookAndFeelInstaller { IllegalAccessException, UnsupportedLookAndFeelException { super.installLookAndFeel(); - LookAndFeel lookAndFeel = UIManager.getLookAndFeel(); - WrappingLookAndFeel wrappingLookAndFeel = new WrappingLookAndFeel(lookAndFeel); - UIManager.setLookAndFeel(wrappingLookAndFeel); - } - - @Override - protected void installJavaDefaults() { - // handled by WrappingLookAndFeel } +// @Override +// protected void installJavaDefaults() { +// // GTK does not support changing its values, so set the javaDefaults to an empty map +// Gui.setJavaDefaults(new GThemeValueMap()); +// } +// } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/DefaultTheme.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelManager.java similarity index 66% rename from Ghidra/Framework/Generic/src/main/java/generic/theme/DefaultTheme.java rename to Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelManager.java index 8750fb1015..93e3016005 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/DefaultTheme.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/GtkLookAndFeelManager.java @@ -13,11 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package generic.theme; +package generic.theme.laf; -public class DefaultTheme extends DiscoverableGTheme { +import generic.theme.LafType; - public DefaultTheme() { - super("Default", LafType.SYSTEM, false); +public class GtkLookAndFeelManager extends LookAndFeelManager { + + public GtkLookAndFeelManager() { + super(LafType.GTK); } + + @Override + protected LookAndFeelInstaller getLookAndFeelInstaller() { + return new GtkLookAndFeelInstaller(); + } + } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelInstaller.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelInstaller.java index b601cd4097..9d07a74aa1 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelInstaller.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelInstaller.java @@ -54,7 +54,7 @@ public class LookAndFeelInstaller { * @throws UnsupportedLookAndFeelException if * lnf.isSupportedLookAndFeel() is false */ - public void install() throws ClassNotFoundException, InstantiationException, + public final void install() throws ClassNotFoundException, InstantiationException, IllegalAccessException, UnsupportedLookAndFeelException { cleanUiDefaults(); installLookAndFeel(); @@ -88,8 +88,7 @@ public class LookAndFeelInstaller { } /** - * Installs Colors, Fonts, and Icons into the UIDefaults. Subclasses my override this if they need to install - * UI properties in a different way. + * Extracts java default colors, fonts, and icons and stores them in {@link Gui}. */ protected void installJavaDefaults() { GThemeValueMap javaDefaults = extractJavaDefaults(); @@ -99,22 +98,37 @@ public class LookAndFeelInstaller { private void installPropertiesBackIntoUiDefaults(GThemeValueMap javaDefaults) { UIDefaults defaults = UIManager.getDefaults(); + + GTheme theme = Gui.getActiveTheme(); + + // we replace java default colors with GColor equivalents so that we + // can change colors without having to reinstall ui on each component + // This trick only works for colors. Fonts and icons don't universally + // allow being wrapped like colors do. for (ColorValue colorValue : javaDefaults.getColors()) { String id = colorValue.getId(); GColorUIResource gColor = Gui.getGColorUiResource(id); defaults.put(id, gColor); } + + // For fonts and icons we only want to install values that have been changed by + // the theme for (FontValue fontValue : javaDefaults.getFonts()) { String id = fontValue.getId(); - //Note: fonts don't support indirect values, so there is no GFont object - Font font = Gui.getFont(id); - defaults.put(id, font); + FontValue themeValue = theme.getFont(id); + if (themeValue != null) { + Font font = Gui.getFont(id); + defaults.put(id, font); + } + } + for (IconValue iconValue : javaDefaults.getIcons()) { + String id = iconValue.getId(); + IconValue themeValue = theme.getIcon(id); + if (themeValue != null) { + Icon icon = Gui.getRawIcon(id, true); + defaults.put(id, icon); + } } -// for (IconValue iconValue : javaDefaults.getIcons()) { -// String id = iconValue.getId(); -// GIconUIResource gIcon = Gui.getGIconUiResource(id); -// defaults.put(id, gIcon); -// } } protected GThemeValueMap extractJavaDefaults() { diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelManager.java new file mode 100644 index 0000000000..eb92e37dd2 --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/LookAndFeelManager.java @@ -0,0 +1,97 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package generic.theme.laf; + +import java.awt.*; + +import javax.swing.*; + +import generic.theme.*; + +/** + * Manages installing and updating a {@link LookAndFeel} + */ +public abstract class LookAndFeelManager { + + private LafType laf; + + protected LookAndFeelManager(LafType laf) { + this.laf = laf; + } + + protected abstract LookAndFeelInstaller getLookAndFeelInstaller(); + + public LafType getLookAndFeelType() { + return laf; + } + + public void installLookAndFeel() throws ClassNotFoundException, InstantiationException, + IllegalAccessException, UnsupportedLookAndFeelException { + + LookAndFeelInstaller installer = getLookAndFeelInstaller(); + installer.install(); + updateComponentUis(); + } + + public void update() { + GColor.refreshAll(); + GIcon.refreshAll(); + updateComponentUis(); +// repaintAll(); + } + + public void updateColor(String id, Color color, boolean isJavaColor) { + GColor.refreshAll(); + repaintAll(); + } + + public void updateIcon(String id, Icon icon, boolean isJavaIcon) { + // Icons are a mixed bag. Java Icons are direct and Ghidra Icons are indirect (to support static use) + // Mainly because Nimbus is buggy and can't handle non-nimbus Icons, so we can't wrap them + // So need to update UiDefaults for java icons. For Ghidra Icons, it is sufficient to refrech + // GIcons and repaint + if (isJavaIcon) { + UIManager.getDefaults().put(id, icon); + updateComponentUis(); + } + GIcon.refreshAll(); + repaintAll(); + } + + public void updateFont(String id, Font font, boolean isJavaFont) { + if (isJavaFont) { + UIManager.getDefaults().put(id, font); + updateComponentUis(); + } + else { + repaintAll(); + } + + } + + private void updateComponentUis() { + for (Window window : Window.getWindows()) { + SwingUtilities.updateComponentTreeUI(window); + } + } + + protected void repaintAll() { + for (Window window : Window.getWindows()) { + window.repaint(); + } + } + +} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MotifLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MotifLookAndFeelManager.java new file mode 100644 index 0000000000..ea92e1bb09 --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/MotifLookAndFeelManager.java @@ -0,0 +1,31 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package generic.theme.laf; + +import generic.theme.LafType; + +public class MotifLookAndFeelManager extends LookAndFeelManager { + + public MotifLookAndFeelManager() { + super(LafType.MOTIF); + } + + @Override + protected LookAndFeelInstaller getLookAndFeelInstaller() { + return new MotifLookAndFeelInstaller(); + } + +} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelInstaller.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelInstaller.java index 72436a9ae0..414dc79d9b 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelInstaller.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelInstaller.java @@ -22,7 +22,6 @@ import javax.swing.*; import javax.swing.plaf.nimbus.NimbusLookAndFeel; import generic.theme.*; -import ghidra.util.Msg; public class NimbusLookAndFeelInstaller extends LookAndFeelInstaller { @@ -37,7 +36,7 @@ public class NimbusLookAndFeelInstaller extends LookAndFeelInstaller { @Override protected void installJavaDefaults() { - // do nothing - already handled by extended NimbusLookAndFeel + // do nothing - already handled by installing extended NimbusLookAndFeel } @Override @@ -60,9 +59,44 @@ public class NimbusLookAndFeelInstaller extends LookAndFeelInstaller { @Override public UIDefaults getDefaults() { + UIDefaults defaults = super.getDefaults(); + GThemeValueMap javaDefaults = extractJavaDefaults(defaults); + + // need to set javaDefalts now to trigger building currentValues so the when + // we create GColors below, they can be resolved. + Gui.setJavaDefaults(javaDefaults); + + // replace all colors with GColors + for (ColorValue colorValue : javaDefaults.getColors()) { + String id = colorValue.getId(); + defaults.put(id, Gui.getGColorUiResource(id)); + } + + GTheme theme = Gui.getActiveTheme(); + + // only replace fonts that have been changed by the theme + for (FontValue fontValue : theme.getFonts()) { + String id = fontValue.getId(); + Font font = Gui.getFont(id); + defaults.put(id, font); + } + + // only replace icons that have been changed by the theme + for (IconValue iconValue : theme.getIcons()) { + String id = iconValue.getId(); + Icon icon = Gui.getRawIcon(id, true); + defaults.put(id, icon); + } + + defaults.put("Label.textForeground", Gui.getGColorUiResource("Label.foreground")); + GColor.refreshAll(); + GIcon.refreshAll(); + return defaults; + } + + private GThemeValueMap extractJavaDefaults(UIDefaults defaults) { GThemeValueMap javaDefaults = new GThemeValueMap(); - UIDefaults defaults = super.getDefaults(); List colorIds = LookAndFeelInstaller.getLookAndFeelIdsForType(defaults, Color.class); for (String id : colorIds) { @@ -79,31 +113,12 @@ public class NimbusLookAndFeelInstaller extends LookAndFeelInstaller { } List iconIds = LookAndFeelInstaller.getLookAndFeelIdsForType(defaults, Icon.class); - Msg.debug(LookAndFeelInstaller.class, "Icons found: " + iconIds.size()); for (String id : iconIds) { Icon icon = defaults.getIcon(id); javaDefaults.addIcon(new IconValue(id, icon)); } - Gui.setJavaDefaults(javaDefaults); - for (String id : colorIds) { - defaults.put(id, Gui.getGColorUiResource(id)); - } -// for (String id : iconIds) { -// GIconUIResource icon = Gui.getGIconUiResource(id); -// if (icon.getId().equals("Menu.arrowIcon")) { -// defaults.put(id, new IconWrappedImageIcon(Gui.getRawIcon(id, false))); -// } -// else { -// defaults.put(id, Gui.getGIconUiResource(id)); -// } -// } - -// javaDefaults.addColor(new ColorValue("Label.textForground", "Label.foreground")); - defaults.put("Label.textForeground", Gui.getGColorUiResource("Label.foreground")); - GColor.refreshAll(); - GIcon.refreshAll(); - return defaults; + return javaDefaults; } } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelManager.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelManager.java new file mode 100644 index 0000000000..79a7df090d --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/NimbusLookAndFeelManager.java @@ -0,0 +1,102 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package generic.theme.laf; + +import java.awt.*; + +import javax.swing.*; + +import generic.theme.LafType; + +public class NimbusLookAndFeelManager extends LookAndFeelManager { + private UIDefaults overrides = new UIDefaults(); + + public NimbusLookAndFeelManager() { + super(LafType.NIMBUS); + } + + @Override + protected LookAndFeelInstaller getLookAndFeelInstaller() { + return new NimbusLookAndFeelInstaller(); + } + + @Override + public void updateColor(String id, Color color, boolean isJavaColor) { + super.updateColor(id, color, isJavaColor); + } + + @Override + public void updateFont(String id, Font font, boolean isJavaFont) { + if (isJavaFont) { + overrides.put(id, font); + updateNimbusOverrides(); + } + repaintAll(); + } + + @Override + public void updateIcon(String id, Icon icon, boolean isJavaIcon) { + if (isJavaIcon) { + overrides.put(id, icon); + updateNimbusOverrides(); + } + repaintAll(); + } + + private void updateNimbusOverrides() { + UIDefaults defaults = getNimbusOverrides(); + for (Window window : Window.getWindows()) { + updateNimbusUI(window, defaults); + } + } + + private void updateNimbusUI(Component c, UIDefaults defaults) { + updateNimbusUIComp(c, defaults); + c.invalidate(); + c.validate(); + c.repaint(); + } + + private UIDefaults getNimbusOverrides() { + UIDefaults defaults = new UIDefaults(); + defaults.putAll(overrides); + return defaults; + } + + private void updateNimbusUIComp(Component c, UIDefaults defaults) { + if (c instanceof JComponent) { + JComponent jc = (JComponent) c; + jc.putClientProperty("Nimbus.Overrides", defaults); + JPopupMenu jpm = jc.getComponentPopupMenu(); + if (jpm != null) { + updateNimbusUI(jpm, defaults); + } + } + Component[] children = null; + if (c instanceof JMenu) { + children = ((JMenu) c).getMenuComponents(); + } + else if (c instanceof Container) { + children = ((Container) c).getComponents(); + } + if (children != null) { + for (Component child : children) { + updateNimbusUIComp(child, defaults); + } + } + } + +} diff --git a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/WrappingLookAndFeel.java b/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/WrappingLookAndFeel.java deleted file mode 100644 index 0f969d925e..0000000000 --- a/Ghidra/Framework/Generic/src/main/java/generic/theme/laf/WrappingLookAndFeel.java +++ /dev/null @@ -1,120 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package generic.theme.laf; - -import java.awt.Color; -import java.awt.Component; -import java.util.List; - -import javax.swing.*; - -import generic.theme.*; - -public class WrappingLookAndFeel extends LookAndFeel { - private LookAndFeel delegate; - - WrappingLookAndFeel(LookAndFeel delegate) { - this.delegate = delegate; - } - - @Override - public UIDefaults getDefaults() { - GThemeValueMap javaDefaults = new GThemeValueMap(); - - UIDefaults defaults = delegate.getDefaults(); - List colorIds = - LookAndFeelInstaller.getLookAndFeelIdsForType(defaults, Color.class); - for (String id : colorIds) { - Color color = defaults.getColor(id); - ColorValue value = new ColorValue(id, color); - javaDefaults.addColor(value); - } - Gui.setJavaDefaults(javaDefaults); - for (String id : colorIds) { - defaults.put(id, Gui.getGColorUiResource(id)); -// defaults.put(id, new GColor(id)); - } - defaults.put("Label.textForeground", Gui.getGColorUiResource("Label.foreground")); - GColor.refreshAll(); - GIcon.refreshAll(); - return defaults; - } - - @Override - public String getName() { - return delegate.getName(); - } - - @Override - public String getID() { - return delegate.getID(); - } - - @Override - public String getDescription() { - return delegate.getDescription(); - } - - @Override - public boolean isNativeLookAndFeel() { - return delegate.isNativeLookAndFeel(); - } - - @Override - public boolean isSupportedLookAndFeel() { - return delegate.isSupportedLookAndFeel(); - } - - @Override - public LayoutStyle getLayoutStyle() { - return delegate.getLayoutStyle(); - } - - @Override - public void provideErrorFeedback(Component component) { - delegate.provideErrorFeedback(component); - } - - @Override - public Icon getDisabledIcon(JComponent component, Icon icon) { - return delegate.getDisabledIcon(component, icon); - } - - @Override - public Icon getDisabledSelectedIcon(JComponent component, Icon icon) { - return delegate.getDisabledSelectedIcon(component, icon); - } - - @Override - public boolean getSupportsWindowDecorations() { - return delegate.getSupportsWindowDecorations(); - } - - @Override - public void initialize() { - delegate.initialize(); - } - - @Override - public void uninitialize() { - delegate.uninitialize(); - } - - @Override - public String toString() { - return "Wrapped: " + delegate.toString(); - } -} diff --git a/Ghidra/Framework/Generic/src/main/java/resources/ResourceManager.java b/Ghidra/Framework/Generic/src/main/java/resources/ResourceManager.java index 6b92305ea8..cec8a034fe 100644 --- a/Ghidra/Framework/Generic/src/main/java/resources/ResourceManager.java +++ b/Ghidra/Framework/Generic/src/main/java/resources/ResourceManager.java @@ -47,7 +47,7 @@ import utility.module.ModuleUtilities; * as opposed to using the flawed constructor {@link ImageIcon#ImageIcon(Image)}. */ public class ResourceManager { - + public final static String EXTERNAL_ICON_PREFIX = "[EXTERNAL]"; private final static String DEFAULT_ICON_FILENAME = Images.BOMB; private static ImageIcon DEFAULT_ICON; private static Map iconMap = new HashMap<>(); @@ -525,46 +525,50 @@ public class ResourceManager { return icons; } - private static ImageIcon doLoadIcon(String filename) { + private static ImageIcon doLoadIcon(String path) { + + // if the has the "external prefix", it is an icon in the user's application directory + if (path.startsWith(EXTERNAL_ICON_PREFIX)) { + String relativePath = path.substring(EXTERNAL_ICON_PREFIX.length()); + File dir = Application.getUserSettingsDirectory(); + File iconFile = new File(dir, relativePath); + if (iconFile.exists()) { + try { + return new UrlImageIcon(path, iconFile.toURI().toURL()); + } + catch (MalformedURLException e) { + // handled below + } + } + return null; + } + // if only the name of an icon is given, but not a path, check to see if it is // a resource that lives under our "images/" folder - if (!filename.contains("/")) { - URL url = getResource("images/" + filename); + if (!path.contains("/")) { + URL url = getResource("images/" + path); if (url != null) { - return new UrlImageIcon(filename, url); + return new UrlImageIcon(path, url); } } // look for it directly with the given path - URL url = getResource(filename); + URL url = getResource(path); if (url != null) { - return new UrlImageIcon(filename, url); + return new UrlImageIcon(path, url); } // try using the filename as a file path - File imageFile = new File(filename); + File imageFile = new File(path); if (imageFile.exists()) { try { - return new UrlImageIcon(filename, imageFile.toURI().toURL()); + return new UrlImageIcon(path, imageFile.toURI().toURL()); } catch (MalformedURLException e) { // handled below } } - // try to see if is an icon in the users application directory - File dir = Application.getUserSettingsDirectory(); - File iconFile = new File(dir, filename); - if (iconFile.exists()) { - try { - return new UrlImageIcon(filename, iconFile.toURI().toURL()); - } - catch (MalformedURLException e) { - // handled below - } - - } - return null; } diff --git a/Ghidra/Framework/Generic/src/test/java/generic/theme/GThemeTest.java b/Ghidra/Framework/Generic/src/test/java/generic/theme/GThemeTest.java index 3896f9ba2e..542bac96be 100644 --- a/Ghidra/Framework/Generic/src/test/java/generic/theme/GThemeTest.java +++ b/Ghidra/Framework/Generic/src/test/java/generic/theme/GThemeTest.java @@ -44,7 +44,7 @@ public class GThemeTest extends AbstractGenericTest { @Before public void setUp() { - theme = new DefaultTheme(); + theme = Gui.getDefaultTheme(); new Font("Courier", Font.BOLD, 12); } @@ -101,7 +101,7 @@ public class GThemeTest extends AbstractGenericTest { theme = new FileGTheme(file); assertEquals("abc", theme.getName()); - assertEquals(LafType.SYSTEM, theme.getLookAndFeelType()); + assertEquals(LafType.getDefaultLookAndFeel(), theme.getLookAndFeelType()); assertEquals(Color.RED, theme.getColor("color.a.1").get(theme)); assertEquals(Color.BLUE, theme.getColor("color.a.2").get(theme)); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/app/plugin/gui/ThemeManagerPlugin.java b/Ghidra/Framework/Project/src/main/java/ghidra/app/plugin/gui/ThemeManagerPlugin.java index 2282d4a45a..c71d9ed133 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/app/plugin/gui/ThemeManagerPlugin.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/app/plugin/gui/ThemeManagerPlugin.java @@ -17,6 +17,7 @@ package ghidra.app.plugin.gui; import docking.action.builder.ActionBuilder; import docking.theme.gui.ThemeDialog; +import docking.theme.gui.ThemeUtils; import ghidra.app.plugin.PluginCategoryNames; import ghidra.framework.main.ApplicationLevelOnlyPlugin; import ghidra.framework.main.UtilityPluginPackage; @@ -41,15 +42,42 @@ public class ThemeManagerPlugin extends Plugin implements ApplicationLevelOnlyPl @Override protected void init() { + String owner = getName(); + String themeSubMenu = "Theme Actions"; - new ActionBuilder("", getName()).menuPath("Edit", "Theme") - .onAction(e -> showThemeProperties()) + String group = "theme"; + new ActionBuilder("Edit Theme", owner) + .menuPath("Edit", "Theme") + .menuGroup(group, "1") + .onAction(e -> ThemeDialog.editTheme()) .buildAndInstall(tool); - } + new ActionBuilder("Reset To Default", owner) + .menuPath("Edit", themeSubMenu, "Reset To Default") + .menuGroup(group, "2") + .onAction(e -> ThemeUtils.resetThemeToDefault()) + .buildAndInstall(tool); - private void showThemeProperties() { - ThemeDialog.editTheme(); - } + new ActionBuilder("Import Theme", owner) + .menuPath("Edit", themeSubMenu, "Import...") + .menuGroup(group, "3") + .onAction(e -> ThemeUtils.importTheme()) + .buildAndInstall(tool); + new ActionBuilder("Export Theme", owner) + .menuPath("Edit", themeSubMenu, "Export...") + .menuGroup(group, "4") + .onAction(e -> ThemeUtils.exportTheme()) + .buildAndInstall(tool); + + new ActionBuilder("Delete Theme", owner) + .menuPath("Edit", themeSubMenu, "Delete...") + .menuGroup(group, "5") +// .enabledWhen(e -> Gui.getActiveTheme() instanceof FileGTheme) + .onAction(e -> ThemeUtils.deleteTheme()) + .buildAndInstall(tool); + + tool.setMenuGroup(new String[] { "Edit", themeSubMenu }, group, "2"); + + } }