GHelpBuilder fix to allow theme usage in headless

This commit is contained in:
dragonmacher 2022-11-18 12:17:52 -05:00
parent 3586062eb4
commit 65542e9937
10 changed files with 332 additions and 258 deletions

View file

@ -33,14 +33,15 @@ import ghidra.util.filechooser.ExtensionFileFilter;
/** /**
* Some common methods related to saving themes. These are invoked from various places to handle * Some common methods related to saving themes. These are invoked from various places to handle
* what to do if a change is made that would result in loosing theme changes. * what to do if a change is made that would result in loosing theme changes.
*/ */
public class ThemeUtils { public class ThemeUtils {
/** /**
* Asks the user if they want to save the current theme changes. If they answer yes, it * Asks the user if they want to save the current theme changes. If they answer yes, it
* will handle several use cases such as whether it gets saved to a new file or * will handle several use cases such as whether it gets saved to a new file or
* overwrites an existing file. * overwrites an existing file.
* @param themeManager the theme manager
* @return true if the operation was not cancelled * @return true if the operation was not cancelled
*/ */
public static boolean askToSaveThemeChanges(ThemeManager themeManager) { public static boolean askToSaveThemeChanges(ThemeManager themeManager) {
@ -61,6 +62,7 @@ public class ThemeUtils {
/** /**
* Saves all current theme changes. Handles several use cases such as requesting a new theme * Saves all current theme changes. Handles several use cases such as requesting a new theme
* name and asking to overwrite an existing file. * name and asking to overwrite an existing file.
* @param themeManager the theme manager
* @return true if the operation was not cancelled * @return true if the operation was not cancelled
*/ */
public static boolean saveThemeChanges(ThemeManager themeManager) { public static boolean saveThemeChanges(ThemeManager themeManager) {
@ -78,10 +80,11 @@ public class ThemeUtils {
/** /**
* Resets the theme to the default, handling the case where the current theme has changes. * Resets the theme to the default, handling the case where the current theme has changes.
* @param themeManager the theme manager
*/ */
public static void resetThemeToDefault(ThemeManager themeManager) { public static void resetThemeToDefault(ThemeManager themeManager) {
if (askToSaveThemeChanges(themeManager)) { if (askToSaveThemeChanges(themeManager)) {
themeManager.setTheme(themeManager.getDefaultTheme()); themeManager.setTheme(ThemeManager.getDefaultTheme());
} }
} }
@ -129,7 +132,7 @@ public class ThemeUtils {
} }
/** /**
* Exports a theme, prompting the user to pick an file. Also handles dealing with any * Exports a theme, prompting the user to pick an file. Also handles dealing with any
* existing changes to the current theme. * existing changes to the current theme.
* @param themeManager the ThemeManager that actually does the export * @param themeManager the ThemeManager that actually does the export
*/ */
@ -138,9 +141,8 @@ public class ThemeUtils {
return; return;
} }
boolean hasExternalIcons = !themeManager.getActiveTheme().getExternalIconFiles().isEmpty(); boolean hasExternalIcons = !themeManager.getActiveTheme().getExternalIconFiles().isEmpty();
String message = String message = "Export as zip file? (You are not using any external icons so the zip\n" +
"Export as zip file? (You are not using any external icons so the zip\n" + "file would only contain a single theme file.)";
"file would only contain a single theme file.)";
if (hasExternalIcons) { if (hasExternalIcons) {
message = message =
"Export as zip file? (You have external icons so a zip file is required if you\n" + "Export as zip file? (You have external icons so a zip file is required if you\n" +
@ -159,6 +161,7 @@ public class ThemeUtils {
/** /**
* Prompts for and deletes a selected theme. * Prompts for and deletes a selected theme.
* @param themeManager the theme manager
*/ */
public static void deleteTheme(ThemeManager themeManager) { public static void deleteTheme(ThemeManager themeManager) {
List<GTheme> savedThemes = new ArrayList<>( List<GTheme> savedThemes = new ArrayList<>(
@ -198,7 +201,7 @@ public class ThemeUtils {
if (existing == null) { if (existing == null) {
return true; return true;
} }
// if the existing theme is a built-in theme, then we definitely can't save to that name // if the existing theme is a built-in theme, then we definitely can't save to that name
if (existing instanceof DiscoverableGTheme) { if (existing instanceof DiscoverableGTheme) {
return false; return false;
} }
@ -239,7 +242,7 @@ public class ThemeUtils {
private static File getSaveFile(String themeName) { private static File getSaveFile(String themeName) {
File dir = Application.getUserSettingsDirectory(); File dir = Application.getUserSettingsDirectory();
File themeDir = new File(dir, ThemeFileLoader.THEME_DIR); File themeDir = new File(dir, ThemeManager.THEME_DIR);
if (!themeDir.exists()) { if (!themeDir.exists()) {
themeDir.mkdir(); themeDir.mkdir();
} }

View file

@ -16,34 +16,28 @@
package generic.theme; package generic.theme;
import java.awt.Component; import java.awt.Component;
import java.io.File; import java.io.*;
import java.util.*; import java.util.*;
import javax.swing.*; import javax.swing.*;
import javax.swing.plaf.ComponentUI;
import com.formdev.flatlaf.FlatDarkLaf; import com.formdev.flatlaf.FlatDarkLaf;
import com.formdev.flatlaf.FlatLightLaf; import com.formdev.flatlaf.FlatLightLaf;
import generic.theme.laf.LookAndFeelManager; import generic.theme.laf.LookAndFeelManager;
import ghidra.framework.Application;
import ghidra.util.Msg; import ghidra.util.Msg;
import ghidra.util.classfinder.ClassSearcher; import ghidra.util.classfinder.ClassSearcher;
/** /**
* This is the fully functional {@link ThemeManager} that manages themes in a application. To * This is the fully functional {@link ThemeManager} that manages themes in a application. To
* activate the theme functionality, Applications (or tests) must call * activate the theme functionality, Applications (or tests) must call
* {@link ApplicationThemeManager#initialize()} * {@link ApplicationThemeManager#initialize()}
*/ */
public class ApplicationThemeManager extends ThemeManager { public class ApplicationThemeManager extends ThemeManager {
private GTheme activeTheme = getDefaultTheme();
private Set<GTheme> allThemes = null; private Set<GTheme> allThemes = null;
private GThemeValueMap applicationDefaults = new GThemeValueMap();
private GThemeValueMap applicationDarkDefaults = new GThemeValueMap();
private GThemeValueMap javaDefaults = new GThemeValueMap();
private GThemeValueMap systemValues = new GThemeValueMap();
protected ThemeFileLoader themeFileLoader = new ThemeFileLoader();
protected ThemePreferences themePreferences = new ThemePreferences(); protected ThemePreferences themePreferences = new ThemePreferences();
private Map<String, GColorUIResource> gColorMap = new HashMap<>(); private Map<String, GColorUIResource> gColorMap = new HashMap<>();
@ -58,7 +52,8 @@ public class ApplicationThemeManager extends ThemeManager {
*/ */
public static void initialize() { public static void initialize() {
if (INSTANCE instanceof ApplicationThemeManager) { if (INSTANCE instanceof ApplicationThemeManager) {
Msg.error(ThemeManager.class, "Attempted to initialize theming more than once!"); Msg.error(ApplicationThemeManager.class,
"Attempted to initialize theming more than once!");
return; return;
} }
@ -74,13 +69,13 @@ public class ApplicationThemeManager extends ThemeManager {
protected void doInitialize() { protected void doInitialize() {
installFlatLookAndFeels(); installFlatLookAndFeels();
loadThemeDefaults(); loadDefaultThemeValues();
setTheme(themePreferences.load()); setTheme(themePreferences.load());
} }
@Override @Override
public void reloadApplicationDefaults() { public void reloadApplicationDefaults() {
loadThemeDefaults(); loadDefaultThemeValues();
buildCurrentValues(); buildCurrentValues();
lookAndFeelManager.resetAll(javaDefaults); lookAndFeelManager.resetAll(javaDefaults);
notifyThemeChanged(new AllValuesChangedThemeEvent(false)); notifyThemeChanged(new AllValuesChangedThemeEvent(false));
@ -183,16 +178,6 @@ public class ApplicationThemeManager extends ThemeManager {
return supported; return supported;
} }
@Override
public GTheme getActiveTheme() {
return activeTheme;
}
@Override
public LafType getLookAndFeelType() {
return activeTheme.getLookAndFeelType();
}
@Override @Override
public GTheme getTheme(String themeName) { public GTheme getTheme(String themeName) {
Optional<GTheme> first = Optional<GTheme> first =
@ -200,19 +185,6 @@ public class ApplicationThemeManager extends ThemeManager {
return first.orElse(null); return first.orElse(null);
} }
@Override
public GThemeValueMap getThemeValues() {
GThemeValueMap map = new GThemeValueMap();
map.load(javaDefaults);
map.load(systemValues);
map.load(applicationDefaults);
if (activeTheme.useDarkDefaults()) {
map.load(applicationDarkDefaults);
}
map.load(activeTheme);
return map;
}
@Override @Override
public void setFont(FontValue newValue) { public void setFont(FontValue newValue) {
FontValue currentValue = currentValues.getFont(newValue.getId()); FontValue currentValue = currentValues.getFont(newValue.getId());
@ -286,49 +258,14 @@ public class ApplicationThemeManager extends ThemeManager {
return gIcon; return gIcon;
} }
@Override
public GThemeValueMap getJavaDefaults() {
GThemeValueMap map = new GThemeValueMap();
map.load(javaDefaults);
return map;
}
@Override
public GThemeValueMap getApplicationDarkDefaults() {
GThemeValueMap map = new GThemeValueMap(applicationDefaults);
map.load(applicationDarkDefaults);
return map;
}
@Override
public GThemeValueMap getApplicationLightDefaults() {
GThemeValueMap map = new GThemeValueMap(applicationDefaults);
return map;
}
/**
* Returns a {@link GThemeValueMap} containing all default values for the current theme. It
* is a combination of application defined defaults and java {@link LookAndFeel} defaults.
* @return the current set of defaults.
*/
public GThemeValueMap getDefaults() {
GThemeValueMap currentDefaults = new GThemeValueMap(javaDefaults);
currentDefaults.load(systemValues);
currentDefaults.load(applicationDefaults);
if (activeTheme.useDarkDefaults()) {
currentDefaults.load(applicationDarkDefaults);
}
return currentDefaults;
}
/** /**
* Sets specially defined system UI values. These values are created by the application as a * Sets specially defined system UI values. These values are created by the application as a
* convenience for mapping generic concepts to values that differ by Look and Feel. This allows * convenience for mapping generic concepts to values that differ by Look and Feel. This allows
* clients to use 'system' properties without knowing the actual Look and Feel terms. * clients to use 'system' properties without knowing the actual Look and Feel terms.
* *
* <p>For example, 'system.color.border' defaults to 'controlShadow', but maps to 'nimbusBorder' * <p>For example, 'system.color.border' defaults to 'controlShadow', but maps to 'nimbusBorder'
* in the Nimbus Look and Feel. * in the Nimbus Look and Feel.
* *
* @param map the map * @param map the map
*/ */
public void setSystemDefaults(GThemeValueMap map) { public void setSystemDefaults(GThemeValueMap map) {
@ -336,7 +273,7 @@ public class ApplicationThemeManager extends ThemeManager {
} }
/** /**
* Sets the map of Java default UI values. These are the UI values defined by the current Java * Sets the map of Java default UI values. These are the UI values defined by the current Java
* Look and Feel. * Look and Feel.
* @param map the default theme values defined by the {@link LookAndFeel} * @param map the default theme values defined by the {@link LookAndFeel}
*/ */
@ -347,16 +284,6 @@ public class ApplicationThemeManager extends ThemeManager {
GIcon.refreshAll(currentValues); GIcon.refreshAll(currentValues);
} }
@Override
public boolean isUsingAquaUI(ComponentUI UI) {
return activeTheme.getLookAndFeelType() == LafType.MAC;
}
@Override
public boolean isUsingNimbusUI() {
return activeTheme.getLookAndFeelType() == LafType.NIMBUS;
}
@Override @Override
public boolean hasThemeChanges() { public boolean hasThemeChanges() {
return !changedValuesMap.isEmpty(); return !changedValuesMap.isEmpty();
@ -367,32 +294,14 @@ public class ApplicationThemeManager extends ThemeManager {
lookAndFeelManager.registerFont(component, fontId); lookAndFeelManager.registerFont(component, fontId);
} }
public boolean isDarkTheme() {
return activeTheme.useDarkDefaults();
}
private void installFlatLookAndFeels() { private void installFlatLookAndFeels() {
UIManager.installLookAndFeel(LafType.FLAT_LIGHT.getName(), FlatLightLaf.class.getName()); UIManager.installLookAndFeel(LafType.FLAT_LIGHT.getName(), FlatLightLaf.class.getName());
UIManager.installLookAndFeel(LafType.FLAT_DARK.getName(), FlatDarkLaf.class.getName()); UIManager.installLookAndFeel(LafType.FLAT_DARK.getName(), FlatDarkLaf.class.getName());
} }
private void loadThemeDefaults() { @Override
themeFileLoader.loadThemeDefaultFiles(); protected void buildCurrentValues() {
applicationDefaults = themeFileLoader.getDefaults(); super.buildCurrentValues();
applicationDarkDefaults = themeFileLoader.getDarkDefaults();
}
private void buildCurrentValues() {
GThemeValueMap map = new GThemeValueMap();
map.load(javaDefaults);
map.load(systemValues);
map.load(applicationDefaults);
if (activeTheme.useDarkDefaults()) {
map.load(applicationDarkDefaults);
}
map.load(activeTheme);
currentValues = map;
changedValuesMap.clear(); changedValuesMap.clear();
} }
@ -400,11 +309,42 @@ public class ApplicationThemeManager extends ThemeManager {
if (allThemes == null) { if (allThemes == null) {
Set<GTheme> set = new HashSet<>(); Set<GTheme> set = new HashSet<>();
set.addAll(findDiscoverableThemes()); set.addAll(findDiscoverableThemes());
set.addAll(themeFileLoader.loadThemeFiles()); set.addAll(loadThemeFiles());
allThemes = set; allThemes = set;
} }
} }
protected Collection<GTheme> loadThemeFiles() {
List<File> fileList = new ArrayList<>();
FileFilter themeFileFilter = file -> file.getName().endsWith("." + GTheme.FILE_EXTENSION);
File dir = Application.getUserSettingsDirectory();
File themeDir = new File(dir, THEME_DIR);
File[] files = themeDir.listFiles(themeFileFilter);
if (files != null) {
fileList.addAll(Arrays.asList(files));
}
List<GTheme> list = new ArrayList<>();
for (File file : fileList) {
GTheme theme = loadTheme(file);
if (theme != null) {
list.add(theme);
}
}
return list;
}
private static GTheme loadTheme(File file) {
try {
return new ThemeReader(file).readTheme();
}
catch (IOException e) {
Msg.error(Gui.class, "Could not load theme from file: " + file.getAbsolutePath(), e);
}
return null;
}
private Collection<DiscoverableGTheme> findDiscoverableThemes() { private Collection<DiscoverableGTheme> findDiscoverableThemes() {
return ClassSearcher.getInstances(DiscoverableGTheme.class); return ClassSearcher.getInstances(DiscoverableGTheme.class);
} }

View file

@ -24,19 +24,19 @@ import javax.swing.LookAndFeel;
* Provides a static set of methods for globally managing application themes and their values. * Provides a static set of methods for globally managing application themes and their values.
* <P> * <P>
* The basic idea is that all the colors, fonts, and icons used in an application should be * The basic idea is that all the colors, fonts, and icons used in an application should be
* accessed indirectly via an "id" string. Then the actual color, font, or icon can be changed * accessed indirectly via an "id" string. Then the actual color, font, or icon can be changed
* without changing the source code. The default mapping of the id strings to a value is defined * without changing the source code. The default mapping of the id strings to a value is defined
* in <name>.theme.properties files which are dynamically discovered by searching the module's * in {name}.theme.properties files which are dynamically discovered by searching the module's
* data directory. Also, these files can optionally define a dark default value for an id which * data directory. Also, these files can optionally define a dark default value for an id which
* would replace the standard default value in the event that the current theme specifies that it * would replace the standard default value in the event that the current theme specifies that it
* is a dark theme. Themes are used to specify the application's {@link LookAndFeel}, whether or * is a dark theme. Themes are used to specify the application's {@link LookAndFeel}, whether or
* not it is dark, and any customized values for colors, fonts, or icons. There are several * not it is dark, and any customized values for colors, fonts, or icons. There are several
* "built-in" themes, one for each supported {@link LookAndFeel}, but additional themes can * "built-in" themes, one for each supported {@link LookAndFeel}, but additional themes can
* be defined and stored in the users application home directory as a <name>.theme file. * be defined and stored in the users application home directory as a {name}.theme file.
* *
*/ */
public class Gui { public class Gui {
// Start with an StubThemeManager so that simple tests can operate without having // Start with an StubThemeManager so that simple tests can operate without having
// to initialize the theme system. Applications and integration tests will // to initialize the theme system. Applications and integration tests will
// called ThemeManager.initialize() which will replace this with a fully initialized version. // called ThemeManager.initialize() which will replace this with a fully initialized version.
private static ThemeManager themeManager = new StubThemeManager(); private static ThemeManager themeManager = new StubThemeManager();

View file

@ -0,0 +1,50 @@
/* ###
* 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 ghidra.util.Msg;
/**
* This is a strange implementation of {@link ThemeManager} that is meant to be used in a headless
* environment, but also needs theme properties to have been loaded. This is needed by any
* application that needs to do theme property validation.
*/
public class HeadlessThemeManager extends ThemeManager {
public static void initialize() {
if (INSTANCE instanceof HeadlessThemeManager) {
Msg.error(HeadlessThemeManager.class,
"Attempted to initialize theming more than once!");
return;
}
HeadlessThemeManager themeManager = new HeadlessThemeManager();
themeManager.doInitialize();
}
public HeadlessThemeManager() {
INSTANCE = this;
installInGui();
}
private void doInitialize() {
loadDefaultThemeValues();
buildCurrentValues();
GColor.refreshAll(currentValues);
GIcon.refreshAll(currentValues);
}
}

View file

@ -24,7 +24,7 @@ import java.util.Set;
import javax.swing.plaf.ComponentUI; import javax.swing.plaf.ComponentUI;
/** /**
* Version of ThemeManager that is used before an application or test installs a full * Version of ThemeManager that is used before an application or test installs a full
* ApplicationThemeManager. Provides enough basic functionality used by the Gui class to * ApplicationThemeManager. Provides enough basic functionality used by the Gui class to
* allow simple unit tests to run. * allow simple unit tests to run.
*/ */
@ -35,7 +35,7 @@ public class StubThemeManager extends ThemeManager {
} }
// palette colors are used statically throughout the application, so having them have values // palette colors are used statically throughout the application, so having them have values
// in the stub will allow unit tests to run withouth initializing theming // in the stub will allow unit tests to run without initializing theming
protected void installPaletteColors() { protected void installPaletteColors() {
addPalette("nocolor", BLACK); addPalette("nocolor", BLACK);
addPalette("black", BLACK); addPalette("black", BLACK);

View file

@ -15,8 +15,8 @@
*/ */
package generic.theme; package generic.theme;
import java.io.*; import java.io.IOException;
import java.util.*; import java.util.List;
import generic.jar.ResourceFile; import generic.jar.ResourceFile;
import ghidra.framework.Application; import ghidra.framework.Application;
@ -26,17 +26,20 @@ import ghidra.util.Msg;
* Loads all the system theme.property files that contain all the default color, font, and * Loads all the system theme.property files that contain all the default color, font, and
* icon values. * icon values.
*/ */
public class ThemeFileLoader { public class ThemeDefaultsProvider {
public static final String THEME_DIR = "themes";
private GThemeValueMap defaults = new GThemeValueMap(); private GThemeValueMap defaults = new GThemeValueMap();
private GThemeValueMap darkDefaults = new GThemeValueMap(); private GThemeValueMap darkDefaults = new GThemeValueMap();
ThemeDefaultsProvider() {
loadThemeDefaultFiles();
}
/** /**
* Searches for all the theme.property files and loads them into either the standard * Searches for all the theme.property files and loads them into either the standard
* defaults (light) map or the dark defaults map. * defaults (light) map or the dark defaults map.
*/ */
public void loadThemeDefaultFiles() { private void loadThemeDefaultFiles() {
defaults.clear(); defaults.clear();
darkDefaults.clear(); darkDefaults.clear();
@ -56,28 +59,6 @@ public class ThemeFileLoader {
} }
} }
public Collection<GTheme> loadThemeFiles() {
List<File> fileList = new ArrayList<>();
FileFilter themeFileFilter = file -> file.getName().endsWith("." + GTheme.FILE_EXTENSION);
File dir = Application.getUserSettingsDirectory();
File themeDir = new File(dir, THEME_DIR);
File[] files = themeDir.listFiles(themeFileFilter);
if (files != null) {
fileList.addAll(Arrays.asList(files));
}
List<GTheme> list = new ArrayList<>();
for (File file : fileList) {
GTheme theme = loadTheme(file);
if (theme != null) {
list.add(theme);
}
}
return list;
}
/** /**
* Returns the standard defaults {@link GThemeValueMap} * Returns the standard defaults {@link GThemeValueMap}
* @return the standard defaults {@link GThemeValueMap} * @return the standard defaults {@link GThemeValueMap}
@ -93,14 +74,4 @@ public class ThemeFileLoader {
public GThemeValueMap getDarkDefaults() { public GThemeValueMap getDarkDefaults() {
return darkDefaults; return darkDefaults;
} }
private static GTheme loadTheme(File file) {
try {
return new ThemeReader(file).readTheme();
}
catch (IOException e) {
Msg.error(Gui.class, "Could not load theme from file: " + file.getAbsolutePath(), e);
}
return null;
}
} }

View file

@ -33,35 +33,45 @@ import utilities.util.reflection.ReflectionUtilities;
/** /**
* This class manages application themes and their values. The ThemeManager is an abstract * This class manages application themes and their values. The ThemeManager is an abstract
* base class that has two concrete subclasses (and others for testing purposes) - * base class that has two concrete subclasses (and others for testing purposes) -
* StubThemeManager and ApplicationThememManager. The StubThemeManager exists as a placeholder * StubThemeManager and ApplicationThememManager. The StubThemeManager exists as a placeholder
* until the ApplicationThemeManager is installed via {@link ApplicationThemeManager#initialize()}. * until the ApplicationThemeManager is installed via {@link ApplicationThemeManager#initialize()}.
* <P> * <P>
* The basic idea is that all the colors, fonts, and icons used in an application should be * The basic idea is that all the colors, fonts, and icons used in an application should be
* accessed indirectly via an "id" string. Then the actual color, font, or icon can be changed * accessed indirectly via an "id" string. Then the actual color, font, or icon can be changed
* without changing the source code. The default mapping of the id strings to a value is defined * without changing the source code. The default mapping of the id strings to a value is defined
* in <name>.theme.properties files which are dynamically discovered by searching the module's * in <name>.theme.properties files which are dynamically discovered by searching the module's
* data directory. Also, these files can optionally define a dark default value for an id which * data directory. Also, these files can optionally define a dark default value for an id which
* would replace the standard default value in the event that the current theme specifies that it * would replace the standard default value in the event that the current theme specifies that it
* is a dark theme. Themes are used to specify the application's {@link LookAndFeel}, whether or * is a dark theme. Themes are used to specify the application's {@link LookAndFeel}, whether or
* not it is dark, and any customized values for colors, fonts, or icons. There are several * not it is dark, and any customized values for colors, fonts, or icons. There are several
* "built-in" themes, one for each supported {@link LookAndFeel}, but additional themes can * "built-in" themes, one for each supported {@link LookAndFeel}, but additional themes can
* be defined and stored in the users application home directory as a <name>.theme file. * be defined and stored in the users application home directory as a <name>.theme file.
* <P> * <P>
* Clients that just need to access the colors, fonts, and icons from the theme can use the * Clients that just need to access the colors, fonts, and icons from the theme can use the
* convenience methods in the {@link Gui} class. Clients that need to directly manipulate the * convenience methods in the {@link Gui} class. Clients that need to directly manipulate the
* themes and values will need to directly use the ThemeManager which and be retrieved using the * themes and values will need to directly use the ThemeManager which and be retrieved using the
* static {@link #getInstance()} method. * static {@link #getInstance()} method.
*/ */
public abstract class ThemeManager { public abstract class ThemeManager {
public static final String THEME_DIR = "themes";
static final Font DEFAULT_FONT = new Font("Dialog", Font.PLAIN, 12); static final Font DEFAULT_FONT = new Font("Dialog", Font.PLAIN, 12);
static final Color DEFAULT_COLOR = Color.CYAN; static final Color DEFAULT_COLOR = Color.CYAN;
protected static ThemeManager INSTANCE; protected static ThemeManager INSTANCE;
protected GTheme activeTheme = getDefaultTheme();
protected GThemeValueMap javaDefaults = new GThemeValueMap();
protected GThemeValueMap systemValues = new GThemeValueMap();
protected GThemeValueMap currentValues = new GThemeValueMap(); protected GThemeValueMap currentValues = new GThemeValueMap();
protected GThemeValueMap applicationDefaults = new GThemeValueMap();
protected GThemeValueMap applicationDarkDefaults = new GThemeValueMap();
// these notifications are only when the user is manipulating theme values, so rare and at // these notifications are only when the user is manipulating theme values, so rare and at
// user speed, so using copy on read // user speed, so using copy on read
private WeakSet<ThemeListener> themeListeners = private WeakSet<ThemeListener> themeListeners =
@ -82,37 +92,66 @@ public abstract class ThemeManager {
Gui.setThemeManager(this); Gui.setThemeManager(this);
} }
protected void loadDefaultThemeValues() {
ThemeDefaultsProvider provider = new ThemeDefaultsProvider();
applicationDefaults = provider.getDefaults();
applicationDarkDefaults = provider.getDarkDefaults();
}
protected void buildCurrentValues() {
GThemeValueMap map = new GThemeValueMap();
map.load(javaDefaults);
map.load(systemValues);
map.load(applicationDefaults);
if (activeTheme.useDarkDefaults()) {
map.load(applicationDarkDefaults);
}
map.load(activeTheme);
currentValues = map;
}
/** /**
* Reloads the defaults from all the discoverable theme.property files. * Reloads the defaults from all the discoverable theme.property files.
*/ */
public abstract void reloadApplicationDefaults(); public void reloadApplicationDefaults() {
throw new UnsupportedOperationException();
}
/** /**
* Restores all the current application back to the values as specified by the active theme. * Restores all the current application back to the values as specified by the active theme.
* In other words, reverts any changes to the active theme that haven't been saved. * In other words, reverts any changes to the active theme that haven't been saved.
*/ */
public abstract void restoreThemeValues(); public void restoreThemeValues() {
throw new UnsupportedOperationException();
}
/** /**
* Restores the current color value for the given color id to the value established by the * Restores the current color value for the given color id to the value established by the
* current theme. * current theme.
* @param id the color id to restore back to the original theme value * @param id the color id to restore back to the original theme value
*/ */
public abstract void restoreColor(String id); public void restoreColor(String id) {
throw new UnsupportedOperationException();
}
/** /**
* Restores the current font value for the given font id to the value established by the * Restores the current font value for the given font id to the value established by the
* current theme. * current theme.
* @param id the font id to restore back to the original theme value * @param id the font id to restore back to the original theme value
*/ */
public abstract void restoreFont(String id); public void restoreFont(String id) {
throw new UnsupportedOperationException();
}
/** /**
* Restores the current icon value for the given icon id to the value established by the * Restores the current icon value for the given icon id to the value established by the
* current theme. * current theme.
* @param id the icon id to restore back to the original theme value * @param id the icon id to restore back to the original theme value
*/ */
public abstract void restoreIcon(String id); public void restoreIcon(String id) {
throw new UnsupportedOperationException();
}
/** /**
* Returns true if the color associated with the given id has been changed from the current * Returns true if the color associated with the given id has been changed from the current
@ -121,7 +160,9 @@ public abstract class ThemeManager {
* @return true if the color associated with the given id has been changed from the current * @return true if the color associated with the given id has been changed from the current
* theme value for that id. * theme value for that id.
*/ */
public abstract boolean isChangedColor(String id); public boolean isChangedColor(String id) {
return false;
}
/** /**
* Returns true if the font associated with the given id has been changed from the current * Returns true if the font associated with the given id has been changed from the current
@ -130,7 +171,9 @@ public abstract class ThemeManager {
* @return true if the font associated with the given id has been changed from the current * @return true if the font associated with the given id has been changed from the current
* theme value for that id. * theme value for that id.
*/ */
public abstract boolean isChangedFont(String id); public boolean isChangedFont(String id) {
return false;
}
/** /**
* Returns true if the Icon associated with the given id has been changed from the current * Returns true if the Icon associated with the given id has been changed from the current
@ -139,57 +182,75 @@ public abstract class ThemeManager {
* @return true if the Icon associated with the given id has been changed from the current * @return true if the Icon associated with the given id has been changed from the current
* theme value for that id. * theme value for that id.
*/ */
public abstract boolean isChangedIcon(String id); public boolean isChangedIcon(String id) {
return false;
}
/** /**
* Sets the application's active theme to the given theme. * Sets the application's active theme to the given theme.
* @param theme the theme to make active * @param theme the theme to make active
*/ */
public abstract void setTheme(GTheme theme); public void setTheme(GTheme theme) {
throw new UnsupportedOperationException();
}
/** /**
* Adds the given theme to set of all themes. * Adds the given theme to set of all themes.
* @param newTheme the theme to add * @param newTheme the theme to add
*/ */
public abstract void addTheme(GTheme newTheme); public void addTheme(GTheme newTheme) {
throw new UnsupportedOperationException();
}
/** /**
* Removes the theme from the set of all themes. Also, if the theme has an associated * Removes the theme from the set of all themes. Also, if the theme has an associated
* file, the file will be deleted. * file, the file will be deleted.
* @param theme the theme to delete * @param theme the theme to delete
*/ */
public abstract void deleteTheme(GTheme theme); public void deleteTheme(GTheme theme) {
throw new UnsupportedOperationException();
}
/** /**
* Returns a set of all known themes. * Returns a set of all known themes.
* @return a set of all known themes. * @return a set of all known themes.
*/ */
public abstract Set<GTheme> getAllThemes(); public Set<GTheme> getAllThemes() {
throw new UnsupportedOperationException();
}
/** /**
* Returns a set of all known themes that are supported on the current platform. * Returns a set of all known themes that are supported on the current platform.
* @return a set of all known themes that are supported on the current platform. * @return a set of all known themes that are supported on the current platform.
*/ */
public abstract Set<GTheme> getSupportedThemes(); public Set<GTheme> getSupportedThemes() {
throw new UnsupportedOperationException();
}
/** /**
* Returns the active theme. * Returns the active theme.
* @return the active theme. * @return the active theme.
*/ */
public abstract GTheme getActiveTheme(); public GTheme getActiveTheme() {
return activeTheme;
}
/** /**
* Returns the {@link LafType} for the currently active {@link LookAndFeel} * Returns the {@link LafType} for the currently active {@link LookAndFeel}
* @return the {@link LafType} for the currently active {@link LookAndFeel} * @return the {@link LafType} for the currently active {@link LookAndFeel}
*/ */
public abstract LafType getLookAndFeelType(); public LafType getLookAndFeelType() {
return activeTheme.getLookAndFeelType();
}
/** /**
* Returns the known theme that has the given name. * Returns the known theme that has the given name.
* @param themeName the name of the theme to retrieve * @param themeName the name of the theme to retrieve
* @return the known theme that has the given name * @return the known theme that has the given name
*/ */
public abstract GTheme getTheme(String themeName); public GTheme getTheme(String themeName) {
throw new UnsupportedOperationException();
}
/** /**
* Returns a {@link GThemeValueMap} of all current theme values including unsaved changes to the * Returns a {@link GThemeValueMap} of all current theme values including unsaved changes to the
@ -206,7 +267,17 @@ public abstract class ThemeManager {
* @return the theme values as defined by the current theme, ignoring any unsaved changes that * @return the theme values as defined by the current theme, ignoring any unsaved changes that
* are currently applied to the application * are currently applied to the application
*/ */
public abstract GThemeValueMap getThemeValues(); public GThemeValueMap getThemeValues() {
GThemeValueMap map = new GThemeValueMap();
map.load(javaDefaults);
map.load(systemValues);
map.load(applicationDefaults);
if (activeTheme.useDarkDefaults()) {
map.load(applicationDarkDefaults);
}
map.load(activeTheme);
return map;
}
/** /**
* Returns a {@link GThemeValueMap} contains all values that differ from the default * Returns a {@link GThemeValueMap} contains all values that differ from the default
@ -277,7 +348,9 @@ public abstract class ThemeManager {
* Updates the current value for the font id in the newValue * Updates the current value for the font id in the newValue
* @param newValue the new {@link FontValue} to install in the current values. * @param newValue the new {@link FontValue} to install in the current values.
*/ */
public abstract void setFont(FontValue newValue); public void setFont(FontValue newValue) {
throw new UnsupportedOperationException();
}
/** /**
* Updates the current color for the given id. * Updates the current color for the given id.
@ -303,7 +376,9 @@ public abstract class ThemeManager {
* Updates the current value for the color id in the newValue * Updates the current value for the color id in the newValue
* @param newValue the new {@link ColorValue} to install in the current values. * @param newValue the new {@link ColorValue} to install in the current values.
*/ */
public abstract void setColor(ColorValue newValue); public void setColor(ColorValue newValue) {
throw new UnsupportedOperationException();
}
/** /**
* Updates the current {@link Icon} for the given id. * Updates the current {@link Icon} for the given id.
@ -318,25 +393,31 @@ public abstract class ThemeManager {
* Updates the current value for the {@link Icon} id in the newValue * Updates the current value for the {@link Icon} id in the newValue
* @param newValue the new {@link IconValue} to install in the current values. * @param newValue the new {@link IconValue} to install in the current values.
*/ */
public abstract void setIcon(IconValue newValue); public void setIcon(IconValue newValue) {
throw new UnsupportedOperationException();
}
/** /**
* gets a UIResource version of the GColor for the given id. Using this method ensures that * gets a UIResource version of the GColor for the given id. Using this method ensures that
* the same instance is used for a given id. This combats some poor code in some of the * the same instance is used for a given id. This combats some poor code in some of the
* {@link LookAndFeel}s where the use == in some places to test for equals. * {@link LookAndFeel}s where the use == in some places to test for equals.
* @param id the id to get a GColorUIResource for * @param id the id to get a GColorUIResource for
* @return a GColorUIResource for the given id * @return a GColorUIResource for the given id
*/ */
public abstract GColorUIResource getGColorUiResource(String id); public GColorUIResource getGColorUiResource(String id) {
throw new UnsupportedOperationException();
}
/** /**
* gets a UIResource version of the GIcon for the given id. Using this method ensures that * gets a UIResource version of the GIcon for the given id. Using this method ensures that
* the same instance is used for a given id. This combats some poor code in some of the * the same instance is used for a given id. This combats some poor code in some of the
* {@link LookAndFeel}s where the use == in some places to test for equals. * {@link LookAndFeel}s where the use == in some places to test for equals.
* @param id the id to get a {@link GIconUIResource} for * @param id the id to get a {@link GIconUIResource} for
* @return a GIconUIResource for the given id * @return a GIconUIResource for the given id
*/ */
public abstract GIconUIResource getGIconUiResource(String id); public GIconUIResource getGIconUiResource(String id) {
throw new UnsupportedOperationException();
}
/** /**
* Returns the {@link GThemeValueMap} containing all the default theme values defined by the * Returns the {@link GThemeValueMap} containing all the default theme values defined by the
@ -344,44 +425,67 @@ public abstract class ThemeManager {
* @return the {@link GThemeValueMap} containing all the default theme values defined by the * @return the {@link GThemeValueMap} containing all the default theme values defined by the
* current {@link LookAndFeel} * current {@link LookAndFeel}
*/ */
public abstract GThemeValueMap getJavaDefaults(); public GThemeValueMap getJavaDefaults() {
GThemeValueMap map = new GThemeValueMap();
map.load(javaDefaults);
return map;
}
/** /**
* Returns the {@link GThemeValueMap} containing all the dark default values defined * Returns the {@link GThemeValueMap} containing all the dark default values defined
* in theme.properties files. Note that dark defaults includes light defaults that haven't * in theme.properties files. Note that dark defaults includes light defaults that haven't
* been overridden by a dark default with the same id. * been overridden by a dark default with the same id.
* @return the {@link GThemeValueMap} containing all the dark values defined in * @return the {@link GThemeValueMap} containing all the dark values defined in
* theme.properties files * theme.properties files
*/ */
public abstract GThemeValueMap getApplicationDarkDefaults(); public GThemeValueMap getApplicationDarkDefaults() {
GThemeValueMap map = new GThemeValueMap(applicationDefaults);
map.load(applicationDarkDefaults);
return map;
}
/** /**
* Returns the {@link GThemeValueMap} containing all the standard default values defined * Returns the {@link GThemeValueMap} containing all the standard default values defined
* in theme.properties files. * in theme.properties files.
* @return the {@link GThemeValueMap} containing all the standard values defined in * @return the {@link GThemeValueMap} containing all the standard values defined in
* theme.properties files * theme.properties files
*/ */
public abstract GThemeValueMap getApplicationLightDefaults(); public GThemeValueMap getApplicationLightDefaults() {
GThemeValueMap map = new GThemeValueMap(applicationDefaults);
return map;
}
/** /**
* Returns a {@link GThemeValueMap} containing all default values for the current theme. It * Returns a {@link GThemeValueMap} containing all default values for the current theme. It
* is a combination of application defined defaults and java {@link LookAndFeel} defaults. * is a combination of application defined defaults and java {@link LookAndFeel} defaults.
* @return the current set of defaults. * @return the current set of defaults.
*/ */
public abstract GThemeValueMap getDefaults(); public GThemeValueMap getDefaults() {
GThemeValueMap currentDefaults = new GThemeValueMap(javaDefaults);
currentDefaults.load(systemValues);
currentDefaults.load(applicationDefaults);
if (activeTheme.useDarkDefaults()) {
currentDefaults.load(applicationDarkDefaults);
}
return currentDefaults;
}
/** /**
* Returns true if the given UI object is using the Aqua Look and Feel. * Returns true if the given UI object is using the Aqua Look and Feel.
* @param UI the UI to examine. * @param UI the UI to examine.
* @return true if the UI is using Aqua * @return true if the UI is using Aqua
*/ */
public abstract boolean isUsingAquaUI(ComponentUI UI); public boolean isUsingAquaUI(ComponentUI UI) {
return activeTheme.getLookAndFeelType() == LafType.MAC;
}
/** /**
* Returns true if 'Nimbus' is the current Look and Feel * Returns true if 'Nimbus' is the current Look and Feel
* @return true if 'Nimbus' is the current Look and Feel * @return true if 'Nimbus' is the current Look and Feel
*/ */
public abstract boolean isUsingNimbusUI(); public boolean isUsingNimbusUI() {
return activeTheme.getLookAndFeelType() == LafType.NIMBUS;
}
/** /**
* Adds a {@link ThemeListener} to be notified of theme changes. * Adds a {@link ThemeListener} to be notified of theme changes.
@ -400,29 +504,13 @@ public abstract class ThemeManager {
themeListeners.remove(listener); themeListeners.remove(listener);
} }
/**
* Returns the default theme for the current platform.
* @return the default theme for the current platform.
*/
public static GTheme getDefaultTheme() {
OperatingSystem OS = Platform.CURRENT_PLATFORM.getOperatingSystem();
switch (OS) {
case MAC_OS_X:
return new MacTheme();
case WINDOWS:
return new WindowsTheme();
case LINUX:
case UNSUPPORTED:
default:
return new NimbusTheme();
}
}
/** /**
* Returns true if there are any unsaved changes to the current theme. * Returns true if there are any unsaved changes to the current theme.
* @return true if there are any unsaved changes to the current theme. * @return true if there are any unsaved changes to the current theme.
*/ */
public abstract boolean hasThemeChanges(); public boolean hasThemeChanges() {
return false;
}
/** /**
* Returns true if an color for the given Id has been defined * Returns true if an color for the given Id has been defined
@ -457,13 +545,35 @@ public abstract class ThemeManager {
* @param component the component to set/update the font * @param component the component to set/update the font
* @param fontId the id of the font to register with the given component * @param fontId the id of the font to register with the given component
*/ */
public abstract void registerFont(Component component, String fontId); public void registerFont(Component component, String fontId) {
// do nothing
}
/** /**
* Returns true if the current theme use dark default values. * Returns true if the current theme use dark default values.
* @return true if the current theme use dark default values. * @return true if the current theme use dark default values.
*/ */
public abstract boolean isDarkTheme(); public boolean isDarkTheme() {
return activeTheme.useDarkDefaults();
}
/**
* Returns the default theme for the current platform.
* @return the default theme for the current platform.
*/
public static GTheme getDefaultTheme() {
OperatingSystem OS = Platform.CURRENT_PLATFORM.getOperatingSystem();
switch (OS) {
case MAC_OS_X:
return new MacTheme();
case WINDOWS:
return new WindowsTheme();
case LINUX:
case UNSUPPORTED:
default:
return new NimbusTheme();
}
}
protected void notifyThemeChanged(ThemeEvent event) { protected void notifyThemeChanged(ThemeEvent event) {
for (ThemeListener listener : themeListeners) { for (ThemeListener listener : themeListeners) {
@ -474,10 +584,10 @@ public abstract class ThemeManager {
protected void error(String message) { protected void error(String message) {
Throwable t = ReflectionUtilities.createThrowableWithStackOlderThan(); Throwable t = ReflectionUtilities.createThrowableWithStackOlderThan();
StackTraceElement[] trace = t.getStackTrace(); StackTraceElement[] trace = t.getStackTrace();
StackTraceElement[] filtered = StackTraceElement[] filtered = ReflectionUtilities.filterStackTrace(trace, "java.",
ReflectionUtilities.filterStackTrace(trace, "java.", "theme.Gui", "theme.ThemeManager", "theme.Gui", "theme.ThemeManager", "theme.GColor");
"theme.GColor");
t.setStackTrace(filtered); t.setStackTrace(filtered);
Msg.error(this, message, t); Msg.error(this, message, t);
} }
} }

View file

@ -36,11 +36,11 @@ import ghidra.util.SystemUtilities;
*/ */
public abstract class LookAndFeelManager { public abstract class LookAndFeelManager {
/** /**
* These are color ids (see {@link GColor} used to represent general concepts that * These are color ids (see {@link GColor} used to represent general concepts that
* application developers can use to get the color for that concept as defined by * application developers can use to get the color for that concept as defined by
* a specific {@link LookAndFeel}. This class will define some standard default * a specific {@link LookAndFeel}. This class will define some standard default
* mappings in the constructor, but it is expected that each specific LookAndFeelManager * mappings in the constructor, but it is expected that each specific LookAndFeelManager
* will override these mappings with values appropriate for that LookAndFeel. * will override these mappings with values appropriate for that LookAndFeel.
*/ */
protected static final String SYSTEM_APP_BACKGROUND_COLOR_ID = "system.color.bg.application"; protected static final String SYSTEM_APP_BACKGROUND_COLOR_ID = "system.color.bg.application";
@ -91,6 +91,7 @@ public abstract class LookAndFeelManager {
installJavaDefaults(); installJavaDefaults();
fixupLookAndFeelIssues(); fixupLookAndFeelIssues();
installGlobalProperties(); installGlobalProperties();
installCustomLookAndFeelActions();
updateComponentUis(); updateComponentUis();
} }
@ -459,7 +460,6 @@ public abstract class LookAndFeelManager {
private void installGlobalProperties() { private void installGlobalProperties() {
installGlobalLookAndFeelAttributes(); installGlobalLookAndFeelAttributes();
installGlobalFontSizeOverride(); installGlobalFontSizeOverride();
installCustomLookAndFeelActions();
installPopupMenuSettingsOverride(); installPopupMenuSettingsOverride();
} }

View file

@ -34,7 +34,7 @@ import generic.theme.builtin.*;
import resources.ResourceManager; import resources.ResourceManager;
import resources.icons.UrlImageIcon; import resources.icons.UrlImageIcon;
public class ThemeManagerTest { public class ApplicationThemeManagerTest {
private Font FONT = new Font("Dialog", Font.PLAIN, 13); private Font FONT = new Font("Dialog", Font.PLAIN, 13);
private Font SMALL_FONT = new Font("Dialog", Font.PLAIN, 4); private Font SMALL_FONT = new Font("Dialog", Font.PLAIN, 4);
@ -51,6 +51,8 @@ public class ThemeManagerTest {
private GTheme MAC_THEME = new MacTheme(); private GTheme MAC_THEME = new MacTheme();
private ThemeManager themeManager; private ThemeManager themeManager;
private boolean errorsExpected;
@Before @Before
public void setUp() { public void setUp() {
@ -69,7 +71,6 @@ public class ThemeManagerTest {
darkDefaultValues.addColor(new ColorValue("color.test.bg", BLACK)); darkDefaultValues.addColor(new ColorValue("color.test.bg", BLACK));
darkDefaultValues.addColor(new ColorValue("color.test.fg", BLUE)); darkDefaultValues.addColor(new ColorValue("color.test.fg", BLUE));
themeManager = new DummyApplicationThemeManager(); themeManager = new DummyApplicationThemeManager();
} }
@Test @Test
@ -264,16 +265,19 @@ public class ThemeManagerTest {
@Test @Test
public void testGetColorWithUnresolvedId() { public void testGetColorWithUnresolvedId() {
errorsExpected = true;
assertEquals(CYAN, themeManager.getColor("color.badid")); assertEquals(CYAN, themeManager.getColor("color.badid"));
} }
@Test @Test
public void testGetIconWithUnresolvedId() { public void testGetIconWithUnresolvedId() {
errorsExpected = true;
assertEquals(ResourceManager.getDefaultIcon(), themeManager.getIcon("icon.badid")); assertEquals(ResourceManager.getDefaultIcon(), themeManager.getIcon("icon.badid"));
} }
@Test @Test
public void testGetFontWithUnresolvedId() { public void testGetFontWithUnresolvedId() {
errorsExpected = true;
assertEquals(ThemeManager.DEFAULT_FONT, themeManager.getFont("font.badid")); assertEquals(ThemeManager.DEFAULT_FONT, themeManager.getFont("font.badid"));
} }
@ -337,7 +341,7 @@ public class ThemeManagerTest {
} }
// ApplicationThemeManager that doesn't read in theme.properties files or preferences // ApplicationThemeManager that doesn't read in theme.properties files or preferences
class DummyApplicationThemeManager extends ApplicationThemeManager { private class DummyApplicationThemeManager extends ApplicationThemeManager {
DummyApplicationThemeManager() { DummyApplicationThemeManager() {
themePreferences = new ThemePreferences() { themePreferences = new ThemePreferences() {
@Override @Override
@ -350,28 +354,25 @@ public class ThemeManagerTest {
// do nothing // do nothing
} }
}; };
themeFileLoader = new ThemeFileLoader() {
@Override
public void loadThemeDefaultFiles() {
// do nothing
}
@Override
public Collection<GTheme> loadThemeFiles() {
return new HashSet<>(themes);
}
@Override
public GThemeValueMap getDefaults() {
return defaultValues;
}
@Override
public GThemeValueMap getDarkDefaults() {
return darkDefaultValues;
}
};
doInitialize(); doInitialize();
} }
@Override
protected void loadDefaultThemeValues() {
this.applicationDefaults = defaultValues;
this.applicationDarkDefaults = darkDefaultValues;
}
@Override
protected Collection<GTheme> loadThemeFiles() {
return new HashSet<>(themes);
}
@Override
protected void error(String message) {
if (!errorsExpected) {
super.error(message);
}
}
} }
} }

View file

@ -21,7 +21,7 @@ import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.*; import java.util.*;
import generic.theme.ApplicationThemeManager; import generic.theme.HeadlessThemeManager;
import ghidra.framework.Application; import ghidra.framework.Application;
import ghidra.framework.ApplicationConfiguration; import ghidra.framework.ApplicationConfiguration;
import help.validator.*; import help.validator.*;
@ -69,12 +69,11 @@ public class GHelpBuilder {
ApplicationConfiguration config = new ApplicationConfiguration() { ApplicationConfiguration config = new ApplicationConfiguration() {
@Override @Override
protected void initializeApplication() { protected void initializeApplication() {
ApplicationThemeManager.initialize(); //
} // We must be headless, as we are utility class. But, we also need theme properties
// to be loaded and correct for the help system to function properly.
@Override //
public boolean isHeadless() { HeadlessThemeManager.initialize();
return false;
} }
}; };
Application.initializeApplication(new HelpApplicationLayout("Help Builder", "0.1"), config); Application.initializeApplication(new HelpApplicationLayout("Help Builder", "0.1"), config);