ConfigurationManager.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Diagnostics;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Reflection;
  8. using System.Text;
  9. using System.Text.Json;
  10. using System.Text.Json.Serialization;
  11. using System.Threading.Tasks;
  12. using static Terminal.Gui.Configuration.ConfigurationManager;
  13. #nullable enable
  14. namespace Terminal.Gui.Configuration {
  15. /// <summary>
  16. /// Provides settings and configuration management for Terminal.Gui applications.
  17. /// <para>
  18. /// Users can set Terminal.Gui settings on a global or per-application basis by providing JSON formatted configuration files.
  19. /// The configuration files can be placed in at <c>.tui</c> folder in the user's home directory (e.g. <c>C:/Users/username/.tui</c>,
  20. /// or <c>/usr/username/.tui</c>),
  21. /// the folder where the Terminal.Gui application was launched from (e.g. <c>./.tui</c>), or as a resource
  22. /// within the Terminal.Gui application's main assembly.
  23. /// </para>
  24. /// <para>
  25. /// Settings are defined in JSON format, according to this schema:
  26. /// https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json
  27. /// </para>
  28. /// <para>
  29. /// Settings that will apply to all applications (global settings) reside in files named <c>config.json</c>. Settings
  30. /// that will apply to a specific Terminal.Gui application reside in files named <c>appname.config.json</c>,
  31. /// where <c>appname</c> is the assembly name of the application (e.g. <c>UICatalog.config.json</c>).
  32. /// </para>
  33. /// Settings are applied using the following precedence (higher precedence settings
  34. /// overwrite lower precedence settings):
  35. /// <para>
  36. /// 1. Application configuration found in the users's home directory (<c>~/.tui/appname.config.json</c>) -- Highest precedence
  37. /// </para>
  38. /// <para>
  39. /// 2. Application configuration found in the directory the app was launched from (<c>./.tui/appname.config.json</c>).
  40. /// </para>
  41. /// <para>
  42. /// 3. Application configuration found in the applications's resources (<c>Resources/config.json</c>).
  43. /// </para>
  44. /// <para>
  45. /// 4. Global configuration found in the user's home directory (<c>~/.tui/config.json</c>).
  46. /// </para>
  47. /// <para>
  48. /// 5. Global configuration found in the directory the app was launched from (<c>./.tui/config.json</c>).
  49. /// </para>
  50. /// <para>
  51. /// 6. Global configuration in <c>Terminal.Gui.dll</c>'s resources (<c>Terminal.Gui.Resources.config.json</c>) -- Lowest Precidence.
  52. /// </para>
  53. /// </summary>
  54. public static partial class ConfigurationManager {
  55. private static readonly string _configFilename = "config.json";
  56. private static readonly JsonSerializerOptions serializerOptions = new JsonSerializerOptions {
  57. ReadCommentHandling = JsonCommentHandling.Skip,
  58. PropertyNameCaseInsensitive = true,
  59. DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
  60. WriteIndented = true,
  61. Converters = {
  62. // No need to set converters - the ConfigRootConverter uses property attributes apply the correct
  63. // Converter.
  64. },
  65. };
  66. /// <summary>
  67. /// An attribute that can be applied to a property to indicate that it should included in the configuration file.
  68. /// </summary>
  69. /// <example>
  70. /// [SerializableConfigurationProperty(Scope = typeof(Configuration.ThemeManager.ThemeScope)), JsonConverter (typeof (JsonStringEnumConverter))]
  71. /// public static BorderStyle DefaultBorderStyle {
  72. /// ...
  73. /// </example>
  74. [AttributeUsage (AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
  75. public class SerializableConfigurationProperty : System.Attribute {
  76. /// <summary>
  77. /// Specifies the scope of the property.
  78. /// </summary>
  79. public Type? Scope { get; set; }
  80. /// <summary>
  81. /// If <see langword="true"/>, the property will be serialized to the configuration file using only the property name
  82. /// as the key. If <see langword="false"/>, the property will be serialized to the configuration file using the
  83. /// property name pre-pended with the classname (e.g. <c>Application.UseSystemConsole</c>).
  84. /// </summary>
  85. public bool OmitClassName { get; set; }
  86. }
  87. /// <summary>
  88. /// Holds a property's value and the <see cref="PropertyInfo"/> that allows <see cref="ConfigurationManager"/>
  89. /// to get and set the property's value.
  90. /// </summary>
  91. /// <remarks>
  92. /// Configuration properties must be <see langword="public"/> and <see langword="static"/>
  93. /// and have the <see cref="SerializableConfigurationProperty"/>
  94. /// attribute. If the type of the property requires specialized JSON serialization,
  95. /// a <see cref="JsonConverter"/> must be provided using
  96. /// the <see cref="JsonConverterAttribute"/> attribute.
  97. /// </remarks>
  98. public class ConfigProperty {
  99. private object? propertyValue;
  100. /// <summary>
  101. /// Describes the property.
  102. /// </summary>
  103. public PropertyInfo? PropertyInfo { get; set; }
  104. /// <summary>
  105. /// Helper to get either the Json property named (specified by [JsonPropertyName(name)]
  106. /// or the actual property name.
  107. /// </summary>
  108. /// <param name="pi"></param>
  109. /// <returns></returns>
  110. public static string GetJsonPropertyName (PropertyInfo pi)
  111. {
  112. var jpna = pi.GetCustomAttribute (typeof (JsonPropertyNameAttribute)) as JsonPropertyNameAttribute;
  113. return jpna?.Name ?? pi.Name;
  114. }
  115. /// <summary>
  116. /// Holds the property's value as it was either read from the class's implementation or from a config file.
  117. /// If the property has not been set (e.g. because no configuration file specified a value),
  118. /// this will be <see langword="null"/>.
  119. /// </summary>
  120. /// <remarks>
  121. /// On <see langword="set"/>, performs a sparse-copy of the new value to the existing value (only copies elements of
  122. /// the object that are non-null).
  123. /// </remarks>
  124. public object? PropertyValue {
  125. get => propertyValue;
  126. set {
  127. propertyValue = value;
  128. }
  129. }
  130. internal object? UpdateValueFrom (object source)
  131. {
  132. if (source == null) {
  133. return PropertyValue;
  134. }
  135. var ut = Nullable.GetUnderlyingType (PropertyInfo!.PropertyType);
  136. if (source.GetType () != PropertyInfo!.PropertyType && (ut != null && source.GetType () != ut)) {
  137. throw new ArgumentException ($"The source object ({PropertyInfo!.DeclaringType}.{PropertyInfo!.Name}) is not of type {PropertyInfo!.PropertyType}.");
  138. }
  139. if (PropertyValue != null && source != null) {
  140. PropertyValue = DeepMemberwiseCopy (source, PropertyValue);
  141. } else {
  142. PropertyValue = source;
  143. }
  144. return PropertyValue;
  145. }
  146. /// <summary>
  147. /// Retrieves (using reflection) the value of the static property described in <see cref="PropertyInfo"/>
  148. /// into <see cref="PropertyValue"/>.
  149. /// </summary>
  150. /// <returns></returns>
  151. public object? RetrieveValue ()
  152. {
  153. return PropertyValue = PropertyInfo!.GetValue (null);
  154. }
  155. /// <summary>
  156. /// Applies the <see cref="PropertyValue"/> to the property described by <see cref="PropertyInfo"/>.
  157. /// </summary>
  158. /// <returns></returns>
  159. public bool Apply ()
  160. {
  161. if (PropertyValue != null) {
  162. PropertyInfo?.SetValue (null, DeepMemberwiseCopy (PropertyValue, PropertyInfo?.GetValue (null)));
  163. }
  164. return PropertyValue != null;
  165. }
  166. }
  167. /// <summary>
  168. /// A dictionary of all properties in the Terminal.Gui project that are decorated with the <see cref="SerializableConfigurationProperty"/> attribute.
  169. /// The keys are the property names pre-pended with the class that implements the property (e.g. <c>Application.UseSystemConsole</c>).
  170. /// The values are instances of <see cref="ConfigProperty"/> which hold the property's value and the
  171. /// <see cref="PropertyInfo"/> that allows <see cref="ConfigurationManager"/> to get and set the property's value.
  172. /// </summary>
  173. /// <remarks>
  174. /// Is <see langword="null"/> until <see cref="Initialize"/> is called.
  175. /// </remarks>
  176. private static Dictionary<string, ConfigProperty>? _allConfigProperties;
  177. /// <summary>
  178. /// The backing property for <see cref="Settings"/>.
  179. /// </summary>
  180. /// <remarks>
  181. /// Is <see langword="null"/> until <see cref="Reset"/> is called. Gets set to a new instance by
  182. /// deserialization (see <see cref="Load"/>).
  183. /// </remarks>
  184. private static SettingsScope? _settings;
  185. /// <summary>
  186. /// The root object of Terminal.Gui configuration settings / JSON schema. Contains only properties with the <see cref="SettingsScope"/>
  187. /// attribute value.
  188. /// </summary>
  189. public static SettingsScope? Settings {
  190. get {
  191. if (_settings == null) {
  192. throw new InvalidOperationException ("ConfigurationManager has not been initialized. Call ConfigurationManager.Reset() before accessing the Settings property.");
  193. }
  194. return _settings;
  195. }
  196. set {
  197. _settings = value!;
  198. }
  199. }
  200. /// <summary>
  201. /// The root object of Terminal.Gui themes manager. Contains only properties with the <see cref="ThemeScope"/>
  202. /// attribute value.
  203. /// </summary>
  204. public static ThemeManager? Themes => ThemeManager.Instance;
  205. /// <summary>
  206. /// Application-specific configuration settings scope.
  207. /// </summary>
  208. [SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true), JsonPropertyName ("AppSettings")]
  209. public static AppScope? AppSettings { get; set; }
  210. /// <summary>
  211. /// Initializes the internal state of ConfigurationManager. Nominally called once as part of application
  212. /// startup to initialize global state. Also called from some Unit Tests to ensure correctness (e.g. Reset()).
  213. /// </summary>
  214. internal static void Initialize ()
  215. {
  216. _allConfigProperties = new Dictionary<string, ConfigProperty> ();
  217. _settings = null;
  218. Dictionary<string, Type> classesWithConfigProps = new Dictionary<string, Type> (StringComparer.InvariantCultureIgnoreCase);
  219. // Get Terminal.Gui.dll classes
  220. var types = from assembly in AppDomain.CurrentDomain.GetAssemblies ()
  221. from type in assembly.GetTypes ()
  222. where type.GetProperties ().Any (prop => prop.GetCustomAttribute (typeof (SerializableConfigurationProperty)) != null)
  223. select type;
  224. foreach (var classWithConfig in types) {
  225. classesWithConfigProps.Add (classWithConfig.Name, classWithConfig);
  226. }
  227. Debug.WriteLine ($"ConfigManager.getConfigProperties found {classesWithConfigProps.Count} clases:");
  228. classesWithConfigProps.ToList ().ForEach (x => Debug.WriteLine ($" Class: {x.Key}"));
  229. foreach (var p in from c in classesWithConfigProps
  230. let props = c.Value.GetProperties (BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public).Where (prop =>
  231. prop.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty)
  232. let enumerable = props
  233. from p in enumerable
  234. select p) {
  235. if (p.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty scp) {
  236. if (p.GetGetMethod (true)!.IsStatic) {
  237. // If the class name is omitted, JsonPropertyName is allowed.
  238. _allConfigProperties!.Add (scp.OmitClassName ? ConfigProperty.GetJsonPropertyName (p) : $"{p.DeclaringType?.Name}.{p.Name}", new ConfigProperty {
  239. PropertyInfo = p,
  240. PropertyValue = null
  241. });
  242. } else {
  243. throw new Exception ($"Property {p.Name} in class {p.DeclaringType?.Name} is not static. All SerializableConfigurationProperty properties must be static.");
  244. }
  245. }
  246. }
  247. _allConfigProperties = _allConfigProperties!.OrderBy (x => x.Key).ToDictionary (x => x.Key, x => x.Value, StringComparer.InvariantCultureIgnoreCase);
  248. Debug.WriteLine ($"ConfigManager.Initialize found {_allConfigProperties.Count} properties:");
  249. _allConfigProperties.ToList ().ForEach (x => Debug.WriteLine ($" Property: {x.Key}"));
  250. AppSettings = new AppScope ();
  251. }
  252. /// <summary>
  253. /// Creates a JSON document with the configuration specified.
  254. /// </summary>
  255. /// <returns></returns>
  256. internal static string ToJson ()
  257. {
  258. Debug.WriteLine ($"ConfigurationManager.ToJson()");
  259. return JsonSerializer.Serialize<SettingsScope> (Settings!, serializerOptions);
  260. }
  261. internal static Stream ToStream ()
  262. {
  263. var json = JsonSerializer.Serialize<SettingsScope> (Settings!, serializerOptions);
  264. // turn it into a stream
  265. var stream = new MemoryStream ();
  266. var writer = new StreamWriter (stream);
  267. writer.Write (json);
  268. writer.Flush ();
  269. stream.Position = 0;
  270. return stream;
  271. }
  272. /// <summary>
  273. /// Event arguments for the <see cref="ConfigurationManager"/> events.
  274. /// </summary>
  275. public class ConfigurationManagerEventArgs : EventArgs {
  276. /// <summary>
  277. /// Initializes a new instance of <see cref="ConfigurationManagerEventArgs"/>
  278. /// </summary>
  279. public ConfigurationManagerEventArgs ()
  280. {
  281. }
  282. }
  283. /// <summary>
  284. /// Gets or sets whether the <see cref="ConfigurationManager"/> should throw an exception if it encounters
  285. /// an error on deserialization. If <see langword="false"/> (the default), the error is logged and printed to the
  286. /// console when <see cref="Application.Shutdown"/> is called.
  287. /// </summary>
  288. [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
  289. public static bool? ThrowOnJsonErrors { get; set; } = false;
  290. internal static StringBuilder jsonErrors = new StringBuilder ();
  291. private static void AddJsonError (string error)
  292. {
  293. Debug.WriteLine ($"ConfigurationManager: {error}");
  294. jsonErrors.AppendLine (error);
  295. }
  296. /// <summary>
  297. /// Prints any Json deserialization errors that occurred during deserialization to the console.
  298. /// </summary>
  299. public static void PrintJsonErrors ()
  300. {
  301. if (jsonErrors.Length > 0) {
  302. Console.WriteLine ($"Terminal.Gui ConfigurationManager encountered the following errors while deserializing configuration files:");
  303. Console.WriteLine (jsonErrors.ToString ());
  304. }
  305. }
  306. private static void ClearJsonErrors ()
  307. {
  308. jsonErrors.Clear ();
  309. }
  310. /// <summary>
  311. /// Called when the configuration has been updated from a configuration file. Invokes the <see cref="Updated"/>
  312. /// event.
  313. /// </summary>
  314. public static void OnUpdated ()
  315. {
  316. Debug.WriteLine ($"ConfigurationManager.OnApplied()");
  317. Updated?.Invoke (new ConfigurationManagerEventArgs ());
  318. }
  319. /// <summary>
  320. /// Event fired when the configuration has been updated from a configuration source.
  321. /// application.
  322. /// </summary>
  323. public static event Action<ConfigurationManagerEventArgs>? Updated;
  324. /// <summary>
  325. /// Resets the state of <see cref="ConfigurationManager"/>. Should be called whenever a new app session
  326. /// (e.g. in <see cref="Application.Init(ConsoleDriver, IMainLoopDriver)"/> starts. Called by <see cref="Load"/>
  327. /// if the <c>reset</c> parameter is <see langword="true"/>.
  328. /// </summary>
  329. /// <remarks>
  330. ///
  331. /// </remarks>
  332. public static void Reset ()
  333. {
  334. Debug.WriteLine ($"ConfigurationManager.Reset()");
  335. if (_allConfigProperties == null) {
  336. ConfigurationManager.Initialize ();
  337. }
  338. ClearJsonErrors ();
  339. Settings = new SettingsScope ();
  340. ThemeManager.Reset ();
  341. AppSettings = new AppScope ();
  342. // To enable some unit tests, we only load from resources if the flag is set
  343. if (Locations.HasFlag (ConfigLocations.DefaultOnly)) Settings.UpdateFromResource (typeof (ConfigurationManager).Assembly, $"Terminal.Gui.Resources.{_configFilename}");
  344. Apply ();
  345. ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply ();
  346. AppSettings?.Apply ();
  347. }
  348. /// <summary>
  349. /// Retrieves the hard coded default settings from the Terminal.Gui library implementation. Used in development of
  350. /// the library to generate the default configuration file. Before calling Application.Init, make sure
  351. /// <see cref="Locations"/> is set to <see cref="ConfigLocations.None"/>.
  352. /// </summary>
  353. /// <remarks>
  354. /// <para>
  355. /// This method is only really useful when using ConfigurationManagerTests
  356. /// to generate the JSON doc that is embedded into Terminal.Gui (during development).
  357. /// </para>
  358. /// <para>
  359. /// WARNING: The <c>Terminal.Gui.Resources.config.json</c> resource has setting definitions (Themes)
  360. /// that are NOT generated by this function. If you use this function to regenerate <c>Terminal.Gui.Resources.config.json</c>,
  361. /// make sure you copy the Theme definitions from the existing <c>Terminal.Gui.Resources.config.json</c> file.
  362. /// </para>
  363. /// </remarks>
  364. internal static void GetHardCodedDefaults ()
  365. {
  366. if (_allConfigProperties == null) {
  367. throw new InvalidOperationException ("Initialize must be called first.");
  368. }
  369. Settings = new SettingsScope ();
  370. ThemeManager.GetHardCodedDefaults ();
  371. AppSettings?.RetrieveValues ();
  372. foreach (var p in Settings!.Where (cp => cp.Value.PropertyInfo != null)) {
  373. Settings! [p.Key].PropertyValue = p.Value.PropertyInfo?.GetValue (null);
  374. }
  375. }
  376. /// <summary>
  377. /// Applies the configuration settings to the running <see cref="Application"/> instance.
  378. /// </summary>
  379. public static void Apply ()
  380. {
  381. bool settings = Settings?.Apply () ?? false;
  382. bool themes = ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply () ?? false;
  383. bool appsettings = AppSettings?.Apply () ?? false;
  384. if (settings || themes || appsettings) {
  385. OnApplied ();
  386. }
  387. }
  388. /// <summary>
  389. /// Called when an updated configuration has been applied to the
  390. /// application. Fires the <see cref="Applied"/> event.
  391. /// </summary>
  392. public static void OnApplied ()
  393. {
  394. Debug.WriteLine ($"ConfigurationManager.OnApplied()");
  395. Applied?.Invoke (new ConfigurationManagerEventArgs ());
  396. }
  397. /// <summary>
  398. /// Event fired when an updated configuration has been applied to the
  399. /// application.
  400. /// </summary>
  401. public static event Action<ConfigurationManagerEventArgs>? Applied;
  402. /// <summary>
  403. /// Name of the running application. By default this property is set to the application's assembly name.
  404. /// </summary>
  405. public static string AppName { get; set; } = Assembly.GetEntryAssembly ()?.FullName?.Split (',') [0]?.Trim ()!;
  406. /// <summary>
  407. /// Describes the location of the configuration files. The constants can be
  408. /// combined (bitwise) to specify multiple locations.
  409. /// </summary>
  410. [Flags]
  411. public enum ConfigLocations {
  412. /// <summary>
  413. /// No configuration will be loaded.
  414. /// </summary>
  415. /// <remarks>
  416. /// Used for development and testing only. For Terminal,Gui to function properly, at least
  417. /// <see cref="DefaultOnly"/> should be set.
  418. /// </remarks>
  419. None = 0,
  420. /// <summary>
  421. /// Global configuration in <c>Terminal.Gui.dll</c>'s resources (<c>Terminal.Gui.Resources.config.json</c>) -- Lowest Precidence.
  422. /// </summary>
  423. DefaultOnly,
  424. /// <summary>
  425. /// This constant is a combination of all locations
  426. /// </summary>
  427. All = -1
  428. }
  429. /// <summary>
  430. /// Gets and sets the locations where <see cref="ConfigurationManager"/> will look for config files.
  431. /// The value is <see cref="ConfigLocations.All"/>.
  432. /// </summary>
  433. public static ConfigLocations Locations { get; set; } = ConfigLocations.All;
  434. /// <summary>
  435. /// Loads all settings found in the various configuration storage locations to
  436. /// the <see cref="ConfigurationManager"/>. Optionally,
  437. /// resets all settings attributed with <see cref="SerializableConfigurationProperty"/> to the defaults.
  438. /// </summary>
  439. /// <remarks>
  440. /// Use <see cref="Apply"/> to cause the loaded settings to be applied to the running application.
  441. /// </remarks>
  442. /// <param name="reset">If <see langword="true"/> the state of <see cref="ConfigurationManager"/> will
  443. /// be reset to the defaults.</param>
  444. public static void Load (bool reset = false)
  445. {
  446. Debug.WriteLine ($"ConfigurationManager.Load()");
  447. if (reset) Reset ();
  448. // LibraryResources is always loaded by Reset
  449. if (Locations == ConfigLocations.All) {
  450. var embeddedStylesResourceName = Assembly.GetEntryAssembly ()?
  451. .GetManifestResourceNames ().FirstOrDefault (x => x.EndsWith (_configFilename));
  452. if (string.IsNullOrEmpty (embeddedStylesResourceName)) {
  453. embeddedStylesResourceName = _configFilename;
  454. }
  455. Settings = Settings?
  456. // Global current directory
  457. .Update ($"./.tui/{_configFilename}")?
  458. // Global home directory
  459. .Update ($"~/.tui/{_configFilename}")?
  460. // App resources
  461. .UpdateFromResource (Assembly.GetEntryAssembly ()!, embeddedStylesResourceName!)?
  462. // App current directory
  463. .Update ($"./.tui/{AppName}.{_configFilename}")?
  464. // App home directory
  465. .Update ($"~/.tui/{AppName}.{_configFilename}");
  466. }
  467. }
  468. /// <summary>
  469. /// Returns an empty Json document with just the $schema tag.
  470. /// </summary>
  471. /// <returns></returns>
  472. public static string GetEmptyJson ()
  473. {
  474. var emptyScope = new SettingsScope ();
  475. emptyScope.Clear ();
  476. return JsonSerializer.Serialize<SettingsScope> (emptyScope, serializerOptions);
  477. }
  478. /// <summary>
  479. /// System.Text.Json does not support copying a deserialized object to an existing instance.
  480. /// To work around this, we implement a 'deep, memberwise copy' method.
  481. /// </summary>
  482. /// <remarks>
  483. /// TOOD: When System.Text.Json implements `PopulateObject` revisit
  484. /// https://github.com/dotnet/corefx/issues/37627
  485. /// </remarks>
  486. /// <param name="source"></param>
  487. /// <param name="destination"></param>
  488. /// <returns><paramref name="destination"/> updated from <paramref name="source"/></returns>
  489. internal static object? DeepMemberwiseCopy (object? source, object? destination)
  490. {
  491. if (destination == null) {
  492. throw new ArgumentNullException (nameof (destination));
  493. }
  494. if (source == null) {
  495. return null!;
  496. }
  497. if (source.GetType () == typeof (SettingsScope)) {
  498. return ((SettingsScope)destination).Update ((SettingsScope)source);
  499. }
  500. if (source.GetType () == typeof (ThemeScope)) {
  501. return ((ThemeScope)destination).Update ((ThemeScope)source);
  502. }
  503. if (source.GetType () == typeof (AppScope)) {
  504. return ((AppScope)destination).Update ((AppScope)source);
  505. }
  506. // If value type, just use copy constructor.
  507. if (source.GetType ().IsValueType || source.GetType () == typeof (string)) {
  508. return source;
  509. }
  510. // Dictionary
  511. if (source.GetType ().IsGenericType && source.GetType ().GetGenericTypeDefinition ().IsAssignableFrom (typeof (Dictionary<,>))) {
  512. foreach (var srcKey in ((IDictionary)source).Keys) {
  513. if (((IDictionary)destination).Contains (srcKey))
  514. ((IDictionary)destination) [srcKey] = DeepMemberwiseCopy (((IDictionary)source) [srcKey], ((IDictionary)destination) [srcKey]);
  515. else {
  516. ((IDictionary)destination).Add (srcKey, ((IDictionary)source) [srcKey]);
  517. }
  518. }
  519. return destination;
  520. }
  521. // ALl other object types
  522. var sourceProps = source?.GetType ().GetProperties ().Where (x => x.CanRead).ToList ();
  523. var destProps = destination?.GetType ().GetProperties ().Where (x => x.CanWrite).ToList ()!;
  524. foreach (var (sourceProp, destProp) in
  525. from sourceProp in sourceProps
  526. where destProps.Any (x => x.Name == sourceProp.Name)
  527. let destProp = destProps.First (x => x.Name == sourceProp.Name)
  528. where destProp.CanWrite
  529. select (sourceProp, destProp)) {
  530. var sourceVal = sourceProp.GetValue (source);
  531. var destVal = destProp.GetValue (destination);
  532. if (sourceVal != null) {
  533. if (destVal != null) {
  534. // Recurse
  535. destProp.SetValue (destination, DeepMemberwiseCopy (sourceVal, destVal));
  536. } else {
  537. destProp.SetValue (destination, sourceVal);
  538. }
  539. }
  540. }
  541. return destination!;
  542. }
  543. //public class ConfiguraitonLocation
  544. //{
  545. // public string Name { get; set; } = string.Empty;
  546. // public string? Path { get; set; }
  547. // public async Task<SettingsScope> UpdateAsync (Stream stream)
  548. // {
  549. // var scope = await JsonSerializer.DeserializeAsync<SettingsScope> (stream, serializerOptions);
  550. // if (scope != null) {
  551. // ConfigurationManager.Settings?.UpdateFrom (scope);
  552. // return scope;
  553. // }
  554. // return new SettingsScope ();
  555. // }
  556. //}
  557. //public class StreamConfiguration {
  558. // private bool _reset;
  559. // public StreamConfiguration (bool reset)
  560. // {
  561. // _reset = reset;
  562. // }
  563. // public StreamConfiguration UpdateAppResources ()
  564. // {
  565. // if (Locations.HasFlag (ConfigLocations.AppResources)) LoadAppResources ();
  566. // return this;
  567. // }
  568. // public StreamConfiguration UpdateAppDirectory ()
  569. // {
  570. // if (Locations.HasFlag (ConfigLocations.AppDirectory)) LoadAppDirectory ();
  571. // return this;
  572. // }
  573. // // Additional update methods for each location here
  574. // private void LoadAppResources ()
  575. // {
  576. // // Load AppResources logic here
  577. // }
  578. // private void LoadAppDirectory ()
  579. // {
  580. // // Load AppDirectory logic here
  581. // }
  582. //}
  583. }
  584. }