| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327 |
- using System.Collections.Concurrent;
- using System.Collections.Immutable;
- using System.Diagnostics.CodeAnalysis;
- using System.Text.Json.Serialization;
- namespace Terminal.Gui.Configuration;
- /// <summary>Manages Themes.</summary>
- /// <remarks>
- /// <para>A Theme is a collection of settings that are named. The default theme is named "Default".</para>
- /// <para>The <see cref="Theme"/> property is used to determine the currently active theme.</para>
- /// <para>The <see cref="Themes"/> property is a dictionary of themes.</para>
- /// </remarks>
- public static class ThemeManager
- {
- /// <summary>
- /// Convenience method to get the current theme. The current theme is the item in the <see cref="Themes"/> dictionary,
- /// with the key of <see cref="Theme"/>.
- /// </summary>
- /// <returns></returns>
- public static ThemeScope GetCurrentTheme () { return Themes! [Theme]; }
- /// <summary>
- /// INTERNAL: Getter for <see cref="Themes"/>.
- /// Convenience method to get the themes dictionary. The themes dictionary is a dictionary of <see cref="ThemeScope"/>
- /// objects, with the key being the name of the theme.
- /// </summary>
- /// <returns></returns>
- /// <exception cref="InvalidOperationException"></exception>
- private static ConcurrentDictionary<string, ThemeScope> GetThemes ()
- {
- if (!ConfigurationManager.IsInitialized ())
- {
- // We're being called from the module initializer.
- // We need to provide a dictionary of themes containing the hard-coded theme.
- return GetHardCodedThemes ()!;
- }
- if (ConfigurationManager.Settings is null)
- {
- throw new InvalidOperationException ("Settings is null.");
- }
- if (ConfigurationManager.Settings.TryGetValue ("Themes", out ConfigProperty? themes))
- {
- if (themes.HasValue)
- {
- return (themes.PropertyValue as ConcurrentDictionary<string, ThemeScope>)!;
- }
- return GetHardCodedThemes ()!;
- }
- throw new InvalidOperationException ("Settings has no Themes property.");
- }
- /// <summary>
- /// INTERNAL: Convenience method to get a list of theme names.
- /// </summary>
- /// <returns></returns>
- /// <exception cref="InvalidOperationException"></exception>
- public static ImmutableList<string> GetThemeNames ()
- {
- if (!ConfigurationManager.IsInitialized ())
- {
- // We're being called from the module initializer.
- // We need to provide a dictionary of themes containing the hard-coded theme.
- return GetHardCodedThemes ()!.Keys.ToImmutableList ();
- }
- if (ConfigurationManager.Settings is null)
- {
- throw new InvalidOperationException ("Settings is null.");
- }
- if (!ConfigurationManager.Settings.TryGetValue ("Themes", out ConfigProperty? themes))
- {
- throw new InvalidOperationException ("Settings has no Themes property.");
- }
- ConcurrentDictionary<string, ThemeScope>? returnConcurrentDictionary;
- if (themes.HasValue)
- {
- returnConcurrentDictionary = themes.PropertyValue as ConcurrentDictionary<string, ThemeScope>;
- }
- else
- {
- returnConcurrentDictionary = GetHardCodedThemes ();
- }
- return returnConcurrentDictionary!.Keys
- .OrderBy (key => key == DEFAULT_THEME_NAME ? string.Empty : key) // Ensure DEFAULT_THEME_NAME is first
- .ToImmutableList ();
- }
- /// <summary>
- /// Convenience method to get the current theme name. The current theme name is the value of <see cref="Theme"/>.
- /// </summary>
- /// <returns></returns>
- public static string GetCurrentThemeName () { return Theme!; }
- // TODO: Add a lock around Theme and Themes
- // TODO: For now, this test can't run in parallel with other tests that access Theme or Themes.
- // TODO: ThemeScopeList_WithThemes_ClonesSuccessfully
- /// <summary>
- /// Gets the Themes dictionary. <see cref="GetThemes"/> is preferred.
- /// The backing store is <c><see cref="ConfigurationManager.Settings"/> ["Themes"]</c>.
- /// However, if <see cref="ConfigurationManager.IsInitialized"/> is <c>false</c>, this property will return the
- /// hard-coded themes.
- /// </summary>
- /// <exception cref="InvalidOperationException"></exception>
- [JsonConverter (typeof (ConcurrentDictionaryJsonConverter<ThemeScope>))]
- [ConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
- public static ConcurrentDictionary<string, ThemeScope>? Themes
- {
- // Note: This property getter must be public; DeepClone depends on it.
- get => GetThemes ();
- internal set => SetThemes (value);
- }
- /// <summary>
- /// INTERNAL: Setter for <see cref="Themes"/>.
- /// </summary>
- /// <param name="dictionary"></param>
- /// <exception cref="InvalidOperationException"></exception>
- private static void SetThemes (ConcurrentDictionary<string, ThemeScope>? dictionary)
- {
- if (dictionary is { } && !dictionary.ContainsKey (DEFAULT_THEME_NAME))
- {
- throw new InvalidOperationException ($"Themes must include an item named {DEFAULT_THEME_NAME}");
- }
- if (ConfigurationManager.Settings is { } && ConfigurationManager.Settings.TryGetValue ("Themes", out ConfigProperty? themes))
- {
- ConfigurationManager.Settings ["Themes"].PropertyValue = dictionary;
- return;
- }
- throw new InvalidOperationException ("Settings is null.");
- }
- /// <summary>
- /// INTERNAL: Returns the hard-coded Themes dictionary.
- /// </summary>
- /// <returns></returns>
- /// <exception cref="InvalidOperationException"></exception>
- private static ConcurrentDictionary<string, ThemeScope>? GetHardCodedThemes ()
- {
- ThemeScope? hardCodedThemeScope = GetHardCodedThemeScope ();
- if (hardCodedThemeScope is null)
- {
- throw new InvalidOperationException ("Hard coded theme scope is null.");
- }
- return new (new Dictionary<string, ThemeScope> { { DEFAULT_THEME_NAME, hardCodedThemeScope } }, StringComparer.InvariantCultureIgnoreCase);
- }
- /// <summary>
- /// INTERNAL: Returns the ThemeScope containing the hard-coded Themes.
- /// </summary>
- /// <returns></returns>
- private static ThemeScope GetHardCodedThemeScope ()
- {
- IEnumerable<KeyValuePair<string, ConfigProperty>>? hardCodedThemeProperties = ConfigurationManager.GetHardCodedConfigPropertiesByScope ("ThemeScope");
- if (hardCodedThemeProperties is null)
- {
- throw new InvalidOperationException ("Hard coded theme properties are null.");
- }
- var hardCodedThemeScope = new ThemeScope ();
- foreach (KeyValuePair<string, ConfigProperty> p in hardCodedThemeProperties)
- {
- hardCodedThemeScope.AddValue (p.Key, p.Value.PropertyValue);
- }
- return hardCodedThemeScope;
- }
- /// <summary>
- /// The name of the default theme ("Default").
- /// </summary>
- public const string DEFAULT_THEME_NAME = "Default";
- /// <summary>
- /// The currently selected theme. The backing store is <c><see cref="ConfigurationManager.Settings"/> ["Theme"]</c>.
- /// </summary>
- [JsonInclude]
- [ConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
- [JsonPropertyName ("Theme")]
- public static string Theme
- {
- get
- {
- if (!ConfigurationManager.IsInitialized ())
- {
- // We're being called from the module initializer.
- // Hard coded default value
- return DEFAULT_THEME_NAME;
- }
- if (ConfigurationManager.Settings is { } && ConfigurationManager.Settings.TryGetValue ("Theme", out ConfigProperty? themeCp))
- {
- if (themeCp.HasValue)
- {
- return (themeCp.PropertyValue as string)!;
- }
- return DEFAULT_THEME_NAME;
- }
- throw new InvalidOperationException ("Settings is null.");
- }
- [RequiresUnreferencedCode ("Calls Terminal.Gui.ConfigurationManager.Settings")]
- [RequiresDynamicCode ("Calls Terminal.Gui.ConfigurationManager.Settings")]
- set
- {
- if (!ConfigurationManager.IsInitialized ())
- {
- throw new InvalidOperationException ("Theme cannot be set before ConfigurationManager is initialized.");
- }
- if (ConfigurationManager.Settings is null || !ConfigurationManager.Settings.TryGetValue ("Theme", out ConfigProperty? themeCp))
- {
- throw new InvalidOperationException ("Settings is null.");
- }
- if (themeCp is null || !themeCp.HasValue)
- {
- throw new InvalidOperationException ("Theme has no value.");
- }
- if (!ConfigurationManager.Settings.TryGetValue ("Themes", out ConfigProperty? themesCp))
- {
- throw new InvalidOperationException ("Settings has no Themes property.");
- }
- string previousThemeValue = GetCurrentThemeName ();
- if (value == previousThemeValue)
- {
- return;
- }
- if (!Themes!.ContainsKey (value))
- {
- Logging.Warning ($"{value} is not a valid theme name.");
- }
- // Update the backing store
- ConfigurationManager.Settings! ["Theme"].PropertyValue = value;
- OnThemeChanged (previousThemeValue, value);
- }
- }
- /// <summary>
- /// INTERNAL: Updates <see cref="Themes"/> to the current values of the static
- /// <see cref="ConfigurationPropertyAttribute"/> properties.
- /// </summary>
- [RequiresUnreferencedCode ("Calls Terminal.Gui.ThemeManager.Themes")]
- [RequiresDynamicCode ("Calls Terminal.Gui.ThemeManager.Themes")]
- internal static void UpdateToCurrentValues ()
- {
- // BUGBUG: This corrupts _hardCodedDefaults. See #4288
- Themes! [Theme].UpdateToCurrentValues ();
- }
- /// <summary>
- /// INTERNAL: Loads all Themes to their hard-coded default values.
- /// </summary>
- [RequiresUnreferencedCode ("Calls SchemeManager.LoadToHardCodedDefaults")]
- [RequiresDynamicCode ("Calls SchemeManager.LoadToHardCodedDefaults")]
- internal static void LoadHardCodedDefaults ()
- {
- if (!ConfigurationManager.IsInitialized ())
- {
- throw new InvalidOperationException ("ThemeManager is not initialized.");
- }
- if (ConfigurationManager.Settings is null)
- {
- return;
- }
- ThemeScope? hardCodedThemeScope = GetHardCodedThemeScope ();
- if (hardCodedThemeScope is null)
- {
- throw new InvalidOperationException ("Hard coded theme scope is null.");
- }
- ConcurrentDictionary<string, ThemeScope> hardCodedThemes = new (
- new Dictionary<string, ThemeScope>
- {
- { Theme, hardCodedThemeScope }
- },
- StringComparer.InvariantCultureIgnoreCase);
- // BUGBUG: SchemeManager is broken and needs to be fixed to not have the hard coded schemes get overwritten.
- // BUGBUG: This is a partial workaround
- // BUGBUG: See https://github.com/gui-cs/Terminal.Gui/issues/4288
- SchemeManager.LoadToHardCodedDefaults ();
- ConfigurationManager.Settings ["Themes"].PropertyValue = hardCodedThemes;
- ConfigurationManager.Settings ["Theme"].PropertyValue = DEFAULT_THEME_NAME;
- }
- /// <summary>Called when the selected theme has changed. Fires the <see cref="ThemeChanged"/> event.</summary>
- internal static void OnThemeChanged (string previousThemeName, string newThemeName)
- {
- Logging.Debug ($"Themes.OnThemeChanged({previousThemeName}) -> {Theme}");
- EventArgs<string> args = new (newThemeName);
- ThemeChanged?.Invoke (null, args);
- }
- /// <summary>Raised when the selected theme has changed.</summary>
- public static event EventHandler<EventArgs<string>>? ThemeChanged;
- }
|