ThemeManager.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. #nullable enable
  2. using System.Collections.Concurrent;
  3. using System.Collections.Immutable;
  4. using System.Diagnostics.CodeAnalysis;
  5. using System.Text.Json.Serialization;
  6. namespace Terminal.Gui.Configuration;
  7. /// <summary>Manages Themes.</summary>
  8. /// <remarks>
  9. /// <para>A Theme is a collection of settings that are named. The default theme is named "Default".</para>
  10. /// <para>The <see cref="Theme"/> property is used to determine the currently active theme.</para>
  11. /// <para>The <see cref="Themes"/> property is a dictionary of themes.</para>
  12. /// </remarks>
  13. public static class ThemeManager
  14. {
  15. /// <summary>
  16. /// Convenience method to get the current theme. The current theme is the item in the <see cref="Themes"/> dictionary,
  17. /// with the key of <see cref="Theme"/>.
  18. /// </summary>
  19. /// <returns></returns>
  20. public static ThemeScope GetCurrentTheme () { return Themes! [Theme]; }
  21. /// <summary>
  22. /// Convenience method to get the themes dictionary. The themes dictionary is a dictionary of <see cref="ThemeScope"/>
  23. /// objects, with the key being the name of the theme.
  24. /// </summary>
  25. /// <returns></returns>
  26. /// <exception cref="InvalidOperationException"></exception>
  27. public static ConcurrentDictionary<string, ThemeScope> GetThemes ()
  28. {
  29. if (!ConfigurationManager.IsInitialized ())
  30. {
  31. // We're being called from the module initializer.
  32. // We need to provide a dictionary of themes containing the hard-coded theme.
  33. return HardCodedThemes ()!;
  34. }
  35. if (ConfigurationManager.Settings is null)
  36. {
  37. throw new InvalidOperationException ("Settings is null.");
  38. }
  39. if (ConfigurationManager.Settings.TryGetValue ("Themes", out ConfigProperty? themes))
  40. {
  41. if (themes.HasValue)
  42. {
  43. return (themes.PropertyValue as ConcurrentDictionary<string, ThemeScope>)!;
  44. }
  45. return HardCodedThemes ()!;
  46. }
  47. throw new InvalidOperationException ("Settings has no Themes property.");
  48. }
  49. /// <summary>
  50. /// Convenience method to get a list of theme names.
  51. /// </summary>
  52. /// <returns></returns>
  53. /// <exception cref="InvalidOperationException"></exception>
  54. public static ImmutableList<string> GetThemeNames ()
  55. {
  56. if (!ConfigurationManager.IsInitialized ())
  57. {
  58. // We're being called from the module initializer.
  59. // We need to provide a dictionary of themes containing the hard-coded theme.
  60. return HardCodedThemes ()!.Keys.ToImmutableList ();
  61. }
  62. if (ConfigurationManager.Settings is null)
  63. {
  64. throw new InvalidOperationException ("Settings is null.");
  65. }
  66. if (!ConfigurationManager.Settings.TryGetValue ("Themes", out ConfigProperty? themes))
  67. {
  68. throw new InvalidOperationException ("Settings has no Themes property.");
  69. }
  70. ConcurrentDictionary<string, ThemeScope>? returnConcurrentDictionary;
  71. if (themes.HasValue)
  72. {
  73. returnConcurrentDictionary = themes.PropertyValue as ConcurrentDictionary<string, ThemeScope>;
  74. }
  75. else
  76. {
  77. returnConcurrentDictionary = HardCodedThemes ();
  78. }
  79. return returnConcurrentDictionary!.Keys
  80. .OrderBy (key => key == DEFAULT_THEME_NAME ? string.Empty : key) // Ensure DEFAULT_THEME_NAME is first
  81. .ToImmutableList ();
  82. }
  83. /// <summary>
  84. /// Convenience method to get the current theme name. The current theme name is the value of <see cref="Theme"/>.
  85. /// </summary>
  86. /// <returns></returns>
  87. public static string GetCurrentThemeName () { return Theme!; }
  88. // TODO: Add a lock around Theme and Themes
  89. // TODO: For now, this test can't run in parallel with other tests that access Theme or Themes.
  90. // TODO: ThemeScopeList_WithThemes_ClonesSuccessfully
  91. /// <summary>
  92. /// Gets the Themes dictionary. <see cref="GetThemes"/> is preferred.
  93. /// The backing store is <c><see cref="ConfigurationManager.Settings"/> ["Themes"]</c>.
  94. /// However, if <see cref="ConfigurationManager.IsInitialized"/> is <c>false</c>, this property will return the
  95. /// hard-coded themes.
  96. /// </summary>
  97. /// <exception cref="InvalidOperationException"></exception>
  98. [JsonConverter (typeof (ConcurrentDictionaryJsonConverter<ThemeScope>))]
  99. [ConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
  100. public static ConcurrentDictionary<string, ThemeScope>? Themes
  101. {
  102. // Note: This property getter must be public; DeepClone depends on it.
  103. get => GetThemes ();
  104. internal set => SetThemes (value);
  105. }
  106. private static void SetThemes (ConcurrentDictionary<string, ThemeScope>? dictionary)
  107. {
  108. if (dictionary is { } && !dictionary.ContainsKey (DEFAULT_THEME_NAME))
  109. {
  110. throw new InvalidOperationException ($"Themes must include an item named {DEFAULT_THEME_NAME}");
  111. }
  112. if (ConfigurationManager.Settings is { } && ConfigurationManager.Settings.TryGetValue ("Themes", out ConfigProperty? themes))
  113. {
  114. ConfigurationManager.Settings ["Themes"].PropertyValue = dictionary;
  115. return;
  116. }
  117. throw new InvalidOperationException ("Settings is null.");
  118. }
  119. private static ConcurrentDictionary<string, ThemeScope>? HardCodedThemes ()
  120. {
  121. ThemeScope? hardCodedThemeScope = GetHardCodedThemeScope ();
  122. if (hardCodedThemeScope is null)
  123. {
  124. throw new InvalidOperationException ("Hard coded theme scope is null.");
  125. }
  126. return new (new Dictionary<string, ThemeScope> { { DEFAULT_THEME_NAME, hardCodedThemeScope } }, StringComparer.InvariantCultureIgnoreCase);
  127. }
  128. /// <summary>
  129. /// Returns a dictionary of hard-coded ThemeScope properties.
  130. /// </summary>
  131. /// <returns></returns>
  132. private static ThemeScope? GetHardCodedThemeScope ()
  133. {
  134. IEnumerable<KeyValuePair<string, ConfigProperty>>? hardCodedThemeProperties = ConfigurationManager.GetHardCodedConfigPropertiesByScope ("ThemeScope");
  135. if (hardCodedThemeProperties is null)
  136. {
  137. throw new InvalidOperationException ("Hard coded theme properties are null.");
  138. }
  139. var hardCodedThemeScope = new ThemeScope ();
  140. foreach (KeyValuePair<string, ConfigProperty> p in hardCodedThemeProperties)
  141. {
  142. hardCodedThemeScope.AddValue (p.Key, p.Value.PropertyValue);
  143. }
  144. return hardCodedThemeScope;
  145. }
  146. /// <summary>
  147. /// Since Theme is a dynamic property, we need to cache the value of the selected theme for when CM is not enabled.
  148. /// </summary>
  149. internal const string DEFAULT_THEME_NAME = "Default";
  150. /// <summary>
  151. /// The currently selected theme. The backing store is <c><see cref="ConfigurationManager.Settings"/> ["Theme"]</c>.
  152. /// </summary>
  153. [JsonInclude]
  154. [ConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
  155. [JsonPropertyName ("Theme")]
  156. public static string Theme
  157. {
  158. get
  159. {
  160. if (!ConfigurationManager.IsInitialized ())
  161. {
  162. // We're being called from the module initializer.
  163. // Hard coded default value
  164. return DEFAULT_THEME_NAME;
  165. }
  166. if (ConfigurationManager.Settings is { } && ConfigurationManager.Settings.TryGetValue ("Theme", out ConfigProperty? themeCp))
  167. {
  168. if (themeCp.HasValue)
  169. {
  170. return (themeCp.PropertyValue as string)!;
  171. }
  172. return DEFAULT_THEME_NAME;
  173. }
  174. throw new InvalidOperationException ("Settings is null.");
  175. }
  176. [RequiresUnreferencedCode ("Calls Terminal.Gui.ConfigurationManager.Settings")]
  177. [RequiresDynamicCode ("Calls Terminal.Gui.ConfigurationManager.Settings")]
  178. set
  179. {
  180. if (!ConfigurationManager.IsInitialized ())
  181. {
  182. throw new InvalidOperationException ("Theme cannot be set before ConfigurationManager is initialized.");
  183. }
  184. if (ConfigurationManager.Settings is null || !ConfigurationManager.Settings.TryGetValue ("Theme", out ConfigProperty? themeCp))
  185. {
  186. throw new InvalidOperationException ("Settings is null.");
  187. }
  188. if (themeCp is null || !themeCp.HasValue)
  189. {
  190. throw new InvalidOperationException ("Theme has no value.");
  191. }
  192. if (!ConfigurationManager.Settings.TryGetValue ("Themes", out ConfigProperty? themesCp))
  193. {
  194. throw new InvalidOperationException ("Settings has no Themes property.");
  195. }
  196. string previousThemeValue = GetCurrentThemeName ();
  197. if (value == previousThemeValue)
  198. {
  199. return;
  200. }
  201. if (!Themes!.ContainsKey (value))
  202. {
  203. Logging.Warning ($"{value} is not a valid theme name.");
  204. }
  205. // Update the backing store
  206. ConfigurationManager.Settings! ["Theme"].PropertyValue = value;
  207. OnThemeChanged (previousThemeValue, value);
  208. }
  209. }
  210. /// <summary>
  211. /// INTERNAL: Updates <see cref="Themes"/> to the current values of the static
  212. /// <see cref="ConfigurationPropertyAttribute"/> properties.
  213. /// </summary>
  214. [RequiresUnreferencedCode ("Calls Terminal.Gui.ThemeManager.Themes")]
  215. [RequiresDynamicCode ("Calls Terminal.Gui.ThemeManager.Themes")]
  216. internal static void UpdateToCurrentValues () { Themes! [Theme].LoadCurrentValues (); }
  217. /// <summary>
  218. /// INTERNAL: Resets all themes to the values the <see cref="ConfigurationPropertyAttribute"/> properties contained
  219. /// when the module was initialized.
  220. /// </summary>
  221. internal static void ResetToHardCodedDefaults ()
  222. {
  223. if (!ConfigurationManager.IsInitialized ())
  224. {
  225. throw new InvalidOperationException ("ThemeManager is not initialized.");
  226. }
  227. if (ConfigurationManager.Settings is null)
  228. {
  229. return;
  230. }
  231. ThemeScope? hardCodedThemeScope = GetHardCodedThemeScope ();
  232. if (hardCodedThemeScope is null)
  233. {
  234. throw new InvalidOperationException ("Hard coded theme scope is null.");
  235. }
  236. ConcurrentDictionary<string, ThemeScope> hardCodedThemes = new (
  237. new Dictionary<string, ThemeScope>
  238. {
  239. { Theme, hardCodedThemeScope }
  240. },
  241. StringComparer.InvariantCultureIgnoreCase);
  242. ConfigurationManager.Settings ["Themes"].PropertyValue = hardCodedThemes;
  243. ConfigurationManager.Settings ["Theme"].PropertyValue = DEFAULT_THEME_NAME;
  244. }
  245. /// <summary>Called when the selected theme has changed. Fires the <see cref="ThemeChanged"/> event.</summary>
  246. internal static void OnThemeChanged (string previousThemeName, string newThemeName)
  247. {
  248. Logging.Debug ($"Themes.OnThemeChanged({previousThemeName}) -> {Theme}");
  249. EventArgs<string> args = new (newThemeName);
  250. ThemeChanged?.Invoke (null, args);
  251. }
  252. /// <summary>Raised when the selected theme has changed.</summary>
  253. public static event EventHandler<EventArgs<string>>? ThemeChanged;
  254. /// <summary>
  255. /// Validates all themes in the <see cref="Themes"/> dictionary.
  256. /// </summary>
  257. public static void Validate ()
  258. {
  259. foreach (ThemeScope theme in Themes!.Values)
  260. {
  261. theme.Validate ();
  262. }
  263. }
  264. }