using System.Collections.Concurrent; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace Terminal.Gui.Configuration; /// Manages Themes. /// /// A Theme is a collection of settings that are named. The default theme is named "Default". /// The property is used to determine the currently active theme. /// The property is a dictionary of themes. /// public static class ThemeManager { /// /// Convenience method to get the current theme. The current theme is the item in the dictionary, /// with the key of . /// /// public static ThemeScope GetCurrentTheme () { return Themes! [Theme]; } /// /// INTERNAL: Getter for . /// Convenience method to get the themes dictionary. The themes dictionary is a dictionary of /// objects, with the key being the name of the theme. /// /// /// private static ConcurrentDictionary 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)!; } return GetHardCodedThemes ()!; } throw new InvalidOperationException ("Settings has no Themes property."); } /// /// INTERNAL: Convenience method to get a list of theme names. /// /// /// public static ImmutableList 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? returnConcurrentDictionary; if (themes.HasValue) { returnConcurrentDictionary = themes.PropertyValue as ConcurrentDictionary; } else { returnConcurrentDictionary = GetHardCodedThemes (); } return returnConcurrentDictionary!.Keys .OrderBy (key => key == DEFAULT_THEME_NAME ? string.Empty : key) // Ensure DEFAULT_THEME_NAME is first .ToImmutableList (); } /// /// Convenience method to get the current theme name. The current theme name is the value of . /// /// 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 /// /// Gets the Themes dictionary. is preferred. /// The backing store is ["Themes"]. /// However, if is false, this property will return the /// hard-coded themes. /// /// [JsonConverter (typeof (ConcurrentDictionaryJsonConverter))] [ConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)] public static ConcurrentDictionary? Themes { // Note: This property getter must be public; DeepClone depends on it. get => GetThemes (); internal set => SetThemes (value); } /// /// INTERNAL: Setter for . /// /// /// private static void SetThemes (ConcurrentDictionary? 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."); } /// /// INTERNAL: Returns the hard-coded Themes dictionary. /// /// /// private static ConcurrentDictionary? GetHardCodedThemes () { ThemeScope? hardCodedThemeScope = GetHardCodedThemeScope (); if (hardCodedThemeScope is null) { throw new InvalidOperationException ("Hard coded theme scope is null."); } return new (new Dictionary { { DEFAULT_THEME_NAME, hardCodedThemeScope } }, StringComparer.InvariantCultureIgnoreCase); } /// /// INTERNAL: Returns the ThemeScope containing the hard-coded Themes. /// /// private static ThemeScope GetHardCodedThemeScope () { IEnumerable>? hardCodedThemeProperties = ConfigurationManager.GetHardCodedConfigPropertiesByScope ("ThemeScope"); if (hardCodedThemeProperties is null) { throw new InvalidOperationException ("Hard coded theme properties are null."); } var hardCodedThemeScope = new ThemeScope (); foreach (KeyValuePair p in hardCodedThemeProperties) { hardCodedThemeScope.AddValue (p.Key, p.Value.PropertyValue); } return hardCodedThemeScope; } /// /// The name of the default theme ("Default"). /// public const string DEFAULT_THEME_NAME = "Default"; /// /// The currently selected theme. The backing store is ["Theme"]. /// [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); } } /// /// INTERNAL: Updates to the current values of the static /// properties. /// [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 (); } /// /// INTERNAL: Loads all Themes to their hard-coded default values. /// [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 hardCodedThemes = new ( new Dictionary { { 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; } /// Called when the selected theme has changed. Fires the event. internal static void OnThemeChanged (string previousThemeName, string newThemeName) { Logging.Debug ($"Themes.OnThemeChanged({previousThemeName}) -> {Theme}"); EventArgs args = new (newThemeName); ThemeChanged?.Invoke (null, args); } /// Raised when the selected theme has changed. public static event EventHandler>? ThemeChanged; }