#nullable enable using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; namespace Terminal.Gui.Configuration; /// /// Defines a configuration settings scope. Classes that inherit from this abstract class can be used to define /// scopes for configuration settings. Each scope is a JSON object that contains a set of configuration settings. /// /// When constructed, the dictionary will be populated with uninitialized configuration properties for the /// scope ( will be ). /// /// public class Scope : ConcurrentDictionary { /// /// Creates a new instance. The dictionary will be populated with uninitialized ( /// will be ). /// [RequiresUnreferencedCode ( "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).")] public Scope () : base (StringComparer.InvariantCultureIgnoreCase) { } /// /// INTERNAL: Adds a new ConfigProperty given a . Determines the correct PropertyInfo etc... by /// retrieving the /// hard coded value for . /// /// /// internal void AddValue (string name, object? value) { ConfigProperty? configProperty = GetHardCodedProperty (name); if (configProperty is null) { throw new InvalidOperationException ($@"{name} is not a hard coded property."); } TryAdd (name, ConfigProperty.CreateCopy (configProperty)); this [name].PropertyValue = configProperty.PropertyValue; } internal ConfigProperty? GetHardCodedProperty (string name) { ConfigProperty? configProperty = ConfigurationManager.GetHardCodedConfigPropertiesByScope (typeof (T).Name)! .FirstOrDefault (hardCodedKeyValuePair => hardCodedKeyValuePair.Key == name) .Value; if (configProperty is null) { return null; } var copy = ConfigProperty.CreateCopy (configProperty); copy.PropertyValue = configProperty.PropertyValue; return copy; } internal ConfigProperty GetUninitializedProperty (string name) { ConfigProperty? configProperty = ConfigurationManager.GetUninitializedConfigPropertiesByScope (typeof (T).Name)! .FirstOrDefault (hardCodedKeyValuePair => hardCodedKeyValuePair.Key == name) .Value; if (configProperty is null) { throw new InvalidOperationException ($@"{name} is not a ConfigProperty."); } var copy = ConfigProperty.CreateCopy (configProperty); copy.PropertyValue = configProperty.PropertyValue; return copy; } /// /// INTERNAL: Updates the values of the properties of this scope to their corresponding static /// properties. /// [RequiresDynamicCode ("Uses reflection to retrieve property values")] internal void UpdateToCurrentValues () { foreach (KeyValuePair validProperties in this.Where (cp => cp.Value.PropertyInfo is { })) { validProperties.Value.UpdateToCurrentValue (); } } /// /// INTERNAL: Updates the values of all properties of this scope to their corresponding hard-coded original values. /// internal void LoadHardCodedDefaults () { foreach (KeyValuePair hardCodedKeyValuePair in ConfigurationManager.GetHardCodedConfigPropertiesByScope (typeof (T).Name)!) { var copy = ConfigProperty.CreateCopy (hardCodedKeyValuePair.Value); TryAdd (hardCodedKeyValuePair.Key, copy); this [hardCodedKeyValuePair.Key].PropertyValue = hardCodedKeyValuePair.Value.PropertyValue; } } /// /// INTERNAL: Updates this scope with the values in using a deep clone. /// /// /// The updated scope (this). [RequiresUnreferencedCode ("Calls Terminal.Gui.ConfigProperty.UpdateFrom(Object)")] [RequiresDynamicCode ("Calls Terminal.Gui.ConfigProperty.UpdateFrom(Object)")] internal Scope? UpdateFrom (Scope scope) { foreach (KeyValuePair prop in scope) { if (!prop.Value.HasValue) { continue; } if (!ContainsKey (prop.Key)) { if (!prop.Value.HasValue) { continue; } // Add an empty (HasValue = false) property to this scope var copy = ConfigProperty.CreateCopy (prop.Value); copy.PropertyValue = prop.Value.PropertyValue; TryAdd (prop.Key, copy); } // Update the property value this [prop.Key].UpdateFrom (prop.Value.PropertyValue); } return this; } /// /// INTERNAL: Applies the values of the properties of this scope to their corresponding /// properties. /// /// if one or more property value was applied; otherwise. [RequiresDynamicCode ("Uses reflection to get and set property values")] [RequiresUnreferencedCode ("Calls Terminal.Gui.DeepCloner.DeepClone(T)")] internal bool Apply () { if (!ConfigurationManager.IsEnabled) { Logging.Warning ("Apply called when CM is not Enabled. This should only be done from unit tests where side-effects are managed."); } var set = false; foreach (KeyValuePair propWithValue in this.Where (t => t.Value.HasValue)) { if (propWithValue.Value.PropertyInfo != null) { object? currentValue = propWithValue.Value.PropertyInfo.GetValue (null); object? newValue = null; // QUESTION: Should we avoid setting if currentValue == newValue? if (propWithValue.Value.PropertyValue is Scope scopeSource && currentValue is Scope scopeDest) { newValue = scopeDest.UpdateFrom (scopeSource); } else { // Use DeepCloner to create a deep copy of the property value newValue = DeepCloner.DeepClone (propWithValue.Value.PropertyValue); } // Logging.Debug($"{propWithValue.Key}: {currentValue} -> {newValue}"); Debug.Assert (!propWithValue.Value.Immutable); propWithValue.Value.PropertyInfo.SetValue (null, newValue); set = true; } } return set; } }