Scope.cs 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. using System.Collections.Concurrent;
  2. using System.Diagnostics;
  3. using System.Diagnostics.CodeAnalysis;
  4. namespace Terminal.Gui.Configuration;
  5. /// <summary>
  6. /// Defines a configuration settings scope. Classes that inherit from this abstract class can be used to define
  7. /// scopes for configuration settings. Each scope is a JSON object that contains a set of configuration settings.
  8. /// <para>
  9. /// When constructed, the dictionary will be populated with uninitialized configuration properties for the
  10. /// scope (<see cref="ConfigProperty.HasValue"/> will be <see langword="false"/>).
  11. /// </para>
  12. /// </summary>
  13. public class Scope<T> : ConcurrentDictionary<string, ConfigProperty>
  14. {
  15. /// <summary>
  16. /// Creates a new instance. The dictionary will be populated with uninitialized (<see cref="ConfigProperty.HasValue"/>
  17. /// will be <see langword="false"/>).
  18. /// </summary>
  19. [RequiresUnreferencedCode (
  20. "Uses cached configuration properties filtered by type T. This is AOT-safe as long as T is one of the known scope types (SettingsScope, ThemeScope, AppSettingsScope).")]
  21. public Scope () : base (StringComparer.InvariantCultureIgnoreCase) { }
  22. /// <summary>
  23. /// INTERNAL: Adds a new ConfigProperty given a <paramref name="value"/>. Determines the correct PropertyInfo etc... by
  24. /// retrieving the
  25. /// hard coded value for <paramref name="name"/>.
  26. /// </summary>
  27. /// <param name="name"></param>
  28. /// <param name="value"></param>
  29. internal void AddValue (string name, object? value)
  30. {
  31. ConfigProperty? configProperty = GetHardCodedProperty (name);
  32. if (configProperty is null)
  33. {
  34. throw new InvalidOperationException ($@"{name} is not a hard coded property.");
  35. }
  36. TryAdd (name, ConfigProperty.CreateCopy (configProperty));
  37. this [name].PropertyValue = configProperty.PropertyValue;
  38. }
  39. internal ConfigProperty? GetHardCodedProperty (string name)
  40. {
  41. ConfigProperty? configProperty = ConfigurationManager.GetHardCodedConfigPropertiesByScope (typeof (T).Name)!
  42. .FirstOrDefault (hardCodedKeyValuePair => hardCodedKeyValuePair.Key == name)
  43. .Value;
  44. if (configProperty is null)
  45. {
  46. return null;
  47. }
  48. var copy = ConfigProperty.CreateCopy (configProperty);
  49. copy.PropertyValue = configProperty.PropertyValue;
  50. return copy;
  51. }
  52. internal ConfigProperty GetUninitializedProperty (string name)
  53. {
  54. ConfigProperty? configProperty = ConfigurationManager.GetUninitializedConfigPropertiesByScope (typeof (T).Name)!
  55. .FirstOrDefault (hardCodedKeyValuePair => hardCodedKeyValuePair.Key == name)
  56. .Value;
  57. if (configProperty is null)
  58. {
  59. throw new InvalidOperationException ($@"{name} is not a ConfigProperty.");
  60. }
  61. var copy = ConfigProperty.CreateCopy (configProperty);
  62. copy.PropertyValue = configProperty.PropertyValue;
  63. return copy;
  64. }
  65. /// <summary>
  66. /// INTERNAL: Updates the values of the properties of this scope to their corresponding static
  67. /// <see cref="ConfigurationPropertyAttribute"/> properties.
  68. /// </summary>
  69. [RequiresDynamicCode ("Uses reflection to retrieve property values")]
  70. internal void UpdateToCurrentValues ()
  71. {
  72. foreach (KeyValuePair<string, ConfigProperty> validProperties in this.Where (cp => cp.Value.PropertyInfo is { }))
  73. {
  74. validProperties.Value.UpdateToCurrentValue ();
  75. }
  76. }
  77. /// <summary>
  78. /// INTERNAL: Updates the values of all properties of this scope to their corresponding hard-coded original values.
  79. /// </summary>
  80. internal void LoadHardCodedDefaults ()
  81. {
  82. foreach (KeyValuePair<string, ConfigProperty> hardCodedKeyValuePair in ConfigurationManager.GetHardCodedConfigPropertiesByScope (typeof (T).Name)!)
  83. {
  84. var copy = ConfigProperty.CreateCopy (hardCodedKeyValuePair.Value);
  85. TryAdd (hardCodedKeyValuePair.Key, copy);
  86. this [hardCodedKeyValuePair.Key].PropertyValue = hardCodedKeyValuePair.Value.PropertyValue;
  87. }
  88. }
  89. /// <summary>
  90. /// INTERNAL: Updates this scope with the values in <paramref name="scope"/> using a deep clone.
  91. /// </summary>
  92. /// <param name="scope"></param>
  93. /// <returns>The updated scope (this).</returns>
  94. [RequiresUnreferencedCode ("Calls Terminal.Gui.ConfigProperty.UpdateFrom(Object)")]
  95. [RequiresDynamicCode ("Calls Terminal.Gui.ConfigProperty.UpdateFrom(Object)")]
  96. internal Scope<T>? UpdateFrom (Scope<T> scope)
  97. {
  98. foreach (KeyValuePair<string, ConfigProperty> prop in scope)
  99. {
  100. if (!prop.Value.HasValue)
  101. {
  102. continue;
  103. }
  104. if (!ContainsKey (prop.Key))
  105. {
  106. if (!prop.Value.HasValue)
  107. {
  108. continue;
  109. }
  110. // Add an empty (HasValue = false) property to this scope
  111. var copy = ConfigProperty.CreateCopy (prop.Value);
  112. copy.PropertyValue = prop.Value.PropertyValue;
  113. TryAdd (prop.Key, copy);
  114. }
  115. // Update the property value
  116. this [prop.Key].UpdateFrom (prop.Value.PropertyValue);
  117. }
  118. return this;
  119. }
  120. /// <summary>
  121. /// INTERNAL: Applies the values of the properties of this scope to their corresponding
  122. /// <see cref="ConfigurationPropertyAttribute"/> properties.
  123. /// </summary>
  124. /// <returns><see langword="true"/> if one or more property value was applied; <see langword="false"/> otherwise.</returns>
  125. [RequiresDynamicCode ("Uses reflection to get and set property values")]
  126. [RequiresUnreferencedCode ("Calls Terminal.Gui.DeepCloner.DeepClone<T>(T)")]
  127. internal bool Apply ()
  128. {
  129. if (!ConfigurationManager.IsEnabled)
  130. {
  131. Logging.Warning ("Apply called when CM is not Enabled. This should only be done from unit tests where side-effects are managed.");
  132. }
  133. var set = false;
  134. foreach (KeyValuePair<string, ConfigProperty> propWithValue in this.Where (t => t.Value.HasValue))
  135. {
  136. if (propWithValue.Value.PropertyInfo != null)
  137. {
  138. object? currentValue = propWithValue.Value.PropertyInfo.GetValue (null);
  139. object? newValue = null;
  140. // QUESTION: Should we avoid setting if currentValue == newValue?
  141. if (propWithValue.Value.PropertyValue is Scope<T> scopeSource && currentValue is Scope<T> scopeDest)
  142. {
  143. newValue = scopeDest.UpdateFrom (scopeSource);
  144. }
  145. else
  146. {
  147. // Use DeepCloner to create a deep copy of the property value
  148. newValue = DeepCloner.DeepClone (propWithValue.Value.PropertyValue);
  149. }
  150. // Logging.Debug($"{propWithValue.Key}: {currentValue} -> {newValue}");
  151. Debug.Assert (!propWithValue.Value.Immutable);
  152. propWithValue.Value.PropertyInfo.SetValue (null, newValue);
  153. set = true;
  154. }
  155. }
  156. return set;
  157. }
  158. }