浏览代码

Merge branch 'v2_develop' into ansi-parser

Tig 8 月之前
父节点
当前提交
b4a0167d80
共有 39 个文件被更改,包括 839 次插入372 次删除
  1. 3 0
      Example/Example.cs
  2. 12 0
      Showcase.md
  3. 3 3
      Terminal.Gui/Application/Application.Initialization.cs
  4. 9 1
      Terminal.Gui/Application/Application.Keyboard.cs
  5. 1 0
      Terminal.Gui/Application/Application.Run.cs
  6. 57 0
      Terminal.Gui/Configuration/ConfigLocations.cs
  7. 31 27
      Terminal.Gui/Configuration/ConfigProperty.cs
  8. 70 68
      Terminal.Gui/Configuration/ConfigurationManager.cs
  9. 25 24
      Terminal.Gui/Configuration/SettingsScope.cs
  10. 3 1
      Terminal.Gui/Configuration/ThemeManager.cs
  11. 11 4
      Terminal.Gui/Input/Key.cs
  12. 16 4
      Terminal.Gui/Input/KeyBindings.cs
  13. 35 0
      Terminal.Gui/Input/KeyEqualityComparer.cs
  14. 0 0
      UICatalog/Resources/config.json
  15. 95 91
      UICatalog/Scenarios/ConfigurationEditor.cs
  16. 2 2
      UICatalog/UICatalog.csproj
  17. 49 8
      UnitTests/Application/ApplicationTests.cs
  18. 1 1
      UnitTests/Configuration/AppScopeTests.cs
  19. 174 0
      UnitTests/Configuration/ConfigPropertyTests.cs
  20. 119 38
      UnitTests/Configuration/ConfigurationMangerTests.cs
  21. 14 0
      UnitTests/Configuration/KeyJsonConverterTests.cs
  22. 37 3
      UnitTests/Configuration/SettingsScopeTests.cs
  23. 4 4
      UnitTests/Configuration/ThemeScopeTests.cs
  24. 2 2
      UnitTests/Configuration/ThemeTests.cs
  25. 13 0
      UnitTests/Input/KeyBindingTests.cs
  26. 4 0
      UnitTests/Input/KeyTests.cs
  27. 2 2
      UnitTests/TestHelpers.cs
  28. 8 8
      UnitTests/UICatalog/ScenarioTests.cs
  29. 2 0
      UnitTests/View/Draw/AllViewsDrawTests.cs
  30. 1 1
      UnitTests/View/ViewTests.cs
  31. 1 1
      UnitTests/Views/ComboBoxTests.cs
  32. 2 2
      UnitTests/Views/MenuBarTests.cs
  33. 1 1
      UnitTests/Views/TabViewTests.cs
  34. 9 9
      UnitTests/Views/TableViewTests.cs
  35. 2 2
      UnitTests/Views/TextViewTests.cs
  36. 1 1
      UnitTests/Views/TreeTableSourceTests.cs
  37. 20 64
      docfx/docs/config.md
  38. 二进制
      local_packages/Terminal.Gui.2.0.0.nupkg
  39. 二进制
      local_packages/Terminal.Gui.2.0.0.snupkg

+ 3 - 0
Example/Example.cs

@@ -6,6 +6,9 @@
 using System;
 using Terminal.Gui;
 
+// Override the default configuration for the application to use the Light theme
+ConfigurationManager.RuntimeConfig = """{ "Theme": "Light" }""";
+
 Application.Run<ExampleWindow> ().Dispose ();
 
 // Before the application exits, reset Terminal.Gui for clean shutdown

+ 12 - 0
Showcase.md

@@ -21,6 +21,18 @@
 * **[Capital and Cargo](https://github.com/dhorions/Capital-and-Cargo)** - A retro console game where you buy, sell, produce and transport goods built with Terminal.Gui
  ![image](https://github.com/gui-cs/Terminal.Gui/assets/1682004/ed89f3d6-020f-4a8a-ae18-e057514f4c43)
 
+- **[Falcon](https://github.com/MaciekWin3/Falcon)** - Terminal chat application that uses SignalR and Terminal.Gui.
+  ![Falcon](https://github.com/user-attachments/assets/d505cba3-75d3-43ea-b270-924dfd257a65)
+
+- **[Muse](https://github.com/MaciekWin3/Muse)** - Muse is terminal music player built with Terminal.Gui and NAudio on .NET platform.
+  ![Muse](https://github.com/user-attachments/assets/94aeb559-a889-4b52-bb0d-453b3e19b290)
+z
+- **[Whale](https://github.com/MaciekWin3/Whale)** - Lightweight terminal user interface application that helps software engineers manage Docker containers.
+  ![Whale](https://github.com/user-attachments/assets/7ef6e348-c36b-4aee-a63c-4e5c60c3aad2)
+
+- **[TermKeyVault](https://github.com/MaciekWin3/TermKeyVault)** - Terminal based password manager built with F# and Terminal.Gui.
+  ![TermKeyVault](https://github.com/user-attachments/assets/c40e17ed-2614-4ad4-8547-e93c1b1d8937)
+
   
 # Examples #
 

+ 3 - 3
Terminal.Gui/Application/Application.Initialization.cs

@@ -86,12 +86,14 @@ public static partial class Application // Initialization (Init/Shutdown)
                 // We're running unit tests. Disable loading config files other than default
                 if (Locations == ConfigLocations.All)
                 {
-                    Locations = ConfigLocations.DefaultOnly;
+                    Locations = ConfigLocations.Default;
                     Reset ();
                 }
             }
         }
 
+        AddApplicationKeyBindings ();
+
         // Start the process of configuration management.
         // Note that we end up calling LoadConfigurationFromAllSources
         // multiple times. We need to do this because some settings are only
@@ -106,8 +108,6 @@ public static partial class Application // Initialization (Init/Shutdown)
         }
         Apply ();
 
-        AddApplicationKeyBindings ();
-
         // Ignore Configuration for ForceDriver if driverName is specified
         if (!string.IsNullOrEmpty (driverName))
         {

+ 9 - 1
Terminal.Gui/Application/Application.Keyboard.cs

@@ -290,7 +290,15 @@ public static partial class Application // Keyboard handling
         }
         else
         {
-            KeyBindings.ReplaceKey (oldKey, newKey);
+            if (KeyBindings.TryGet(oldKey, out KeyBinding binding))
+            {
+                KeyBindings.Remove (oldKey);
+                KeyBindings.Add (newKey, binding);
+            }
+            else
+            {
+                KeyBindings.Add (newKey, binding);
+            }
         }
     }
 

+ 1 - 0
Terminal.Gui/Application/Application.Run.cs

@@ -1,6 +1,7 @@
 #nullable enable
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Serialization;
 using Microsoft.CodeAnalysis.Diagnostics;
 
 namespace Terminal.Gui;

+ 57 - 0
Terminal.Gui/Configuration/ConfigLocations.cs

@@ -0,0 +1,57 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+///     Describes the location of the configuration files. The constants can be combined (bitwise) to specify multiple
+///     locations. The more significant the bit, the higher the priority meaning that the last location will override the
+///     earlier ones.
+/// </summary>
+
+[Flags]
+public enum ConfigLocations
+{
+    /// <summary>No configuration will be loaded.</summary>
+    /// <remarks>
+    ///     Used for development and testing only. For Terminal,Gui to function properly, at least
+    ///     <see cref="Default"/> should be set.
+    /// </remarks>
+    None = 0,
+
+    /// <summary>
+    ///    Deafult configuration in <c>Terminal.Gui.dll</c>'s resources (<c>Terminal.Gui.Resources.config.json</c>).
+    /// </summary>
+    Default = 0b_0000_0001,
+
+    /// <summary>
+    ///     App resources (e.g. <c>MyApp.Resources.config.json</c>).
+    /// </summary>
+    AppResources = 0b_0000_0010,
+
+    /// <summary>
+    ///     Settings in the <see cref="ConfigurationManager.RuntimeConfig"/> static property.
+    /// </summary>
+    Runtime = 0b_0000_0100,
+
+    /// <summary>
+    ///     Global settings in the current directory (e.g. <c>./.tui/config.json</c>).
+    /// </summary>
+    GlobalCurrent = 0b_0000_1000,
+
+    /// <summary>
+    ///    Global settings in the home directory (e.g. <c>~/.tui/config.json</c>).
+    /// </summary>
+    GlobalHome = 0b_0001_0000,
+
+    /// <summary>
+    ///     App settings in the current directory (e.g. <c>./.tui/MyApp.config.json</c>).
+    /// </summary>
+    AppCurrent = 0b_0010_0000,
+
+    /// <summary>
+    ///     App settings in the home directory (e.g. <c>~/.tui/MyApp.config.json</c>).
+    /// </summary>
+    AppHome = 0b_0100_0000,
+
+    /// <summary>This constant is a combination of all locations</summary>
+    All = 0b_1111_1111
+}

+ 31 - 27
Terminal.Gui/Configuration/ConfigProperty.cs

@@ -31,44 +31,42 @@ public class ConfigProperty
     /// </remarks>
     public object? PropertyValue { get; set; }
 
-    /// <summary>Applies the <see cref="PropertyValue"/> to the property described by <see cref="PropertyInfo"/>.</summary>
+    /// <summary>Applies the <see cref="PropertyValue"/> to the static property described by <see cref="PropertyInfo"/>.</summary>
     /// <returns></returns>
     public bool Apply ()
     {
-        if (PropertyValue is { })
+        try
         {
-            try
+            if (PropertyInfo?.GetValue (null) is { })
             {
-                if (PropertyInfo?.GetValue (null) is { })
-                {
-                    PropertyInfo?.SetValue (null, DeepMemberWiseCopy (PropertyValue, PropertyInfo?.GetValue (null)));
-                }
+                var val = DeepMemberWiseCopy (PropertyValue, PropertyInfo?.GetValue (null));
+                PropertyInfo?.SetValue (null, val);
             }
-            catch (TargetInvocationException tie)
+        }
+        catch (TargetInvocationException tie)
+        {
+            // Check if there is an inner exception
+            if (tie.InnerException is { })
             {
-                // Check if there is an inner exception
-                if (tie.InnerException is { })
-                {
-                    // Handle the inner exception separately without catching the outer exception
-                    Exception? innerException = tie.InnerException;
+                // Handle the inner exception separately without catching the outer exception
+                Exception? innerException = tie.InnerException;
 
-                    // Handle the inner exception here
-                    throw new JsonException (
-                                             $"Error Applying Configuration Change: {innerException.Message}",
-                                             innerException
-                                            );
-                }
-
-                // Handle the outer exception or rethrow it if needed
-                throw new JsonException ($"Error Applying Configuration Change: {tie.Message}", tie);
-            }
-            catch (ArgumentException ae)
-            {
+                // Handle the inner exception here
                 throw new JsonException (
-                                         $"Error Applying Configuration Change ({PropertyInfo?.Name}): {ae.Message}",
-                                         ae
+                                         $"Error Applying Configuration Change: {innerException.Message}",
+                                         innerException
                                         );
             }
+
+            // Handle the outer exception or rethrow it if needed
+            throw new JsonException ($"Error Applying Configuration Change: {tie.Message}", tie);
+        }
+        catch (ArgumentException ae)
+        {
+            throw new JsonException (
+                                     $"Error Applying Configuration Change ({PropertyInfo?.Name}): {ae.Message}",
+                                     ae
+                                    );
         }
 
         return PropertyValue != null;
@@ -94,6 +92,12 @@ public class ConfigProperty
     /// <returns></returns>
     public object? RetrieveValue () { return PropertyValue = PropertyInfo!.GetValue (null); }
 
+    /// <summary>
+    ///     Updates (using reflection) <see cref="PropertyValue"/> with the value in <paramref name="source"/> using a deep memberwise copy.
+    /// </summary>
+    /// <param name="source"></param>
+    /// <returns></returns>
+    /// <exception cref="ArgumentException"></exception>
     internal object? UpdateValueFrom (object source)
     {
         if (source is null)

+ 70 - 68
Terminal.Gui/Configuration/ConfigurationManager.cs

@@ -24,7 +24,7 @@ namespace Terminal.Gui;
 ///     </para>
 ///     <para>
 ///         Settings are defined in JSON format, according to this schema:
-///        https://gui-cs.github.io/Terminal.GuiV2Docs/schemas/tui-config-schema.json
+///         https://gui-cs.github.io/Terminal.GuiV2Docs/schemas/tui-config-schema.json
 ///     </para>
 ///     <para>
 ///         Settings that will apply to all applications (global settings) reside in files named <c>config.json</c>.
@@ -53,30 +53,6 @@ namespace Terminal.Gui;
 [ComponentGuarantees (ComponentGuaranteesOptions.None)]
 public static class ConfigurationManager
 {
-    /// <summary>
-    ///     Describes the location of the configuration files. The constants can be combined (bitwise) to specify multiple
-    ///     locations.
-    /// </summary>
-    [Flags]
-    public enum ConfigLocations
-    {
-        /// <summary>No configuration will be loaded.</summary>
-        /// <remarks>
-        ///     Used for development and testing only. For Terminal,Gui to function properly, at least
-        ///     <see cref="DefaultOnly"/> should be set.
-        /// </remarks>
-        None = 0,
-
-        /// <summary>
-        ///     Global configuration in <c>Terminal.Gui.dll</c>'s resources (<c>Terminal.Gui.Resources.config.json</c>) --
-        ///     Lowest Precedence.
-        /// </summary>
-        DefaultOnly,
-
-        /// <summary>This constant is a combination of all locations</summary>
-        All = -1
-    }
-
     /// <summary>
     ///     A dictionary of all properties in the Terminal.Gui project that are decorated with the
     ///     <see cref="SerializableConfigurationProperty"/> attribute. The keys are the property names pre-pended with the
@@ -201,6 +177,7 @@ public static class ConfigurationManager
             {
                 // First start. Apply settings first. This ensures if a config sets Theme to something other than "Default", it gets used
                 settings = Settings?.Apply () ?? false;
+
                 themes = !string.IsNullOrEmpty (ThemeManager.SelectedTheme)
                          && (ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply () ?? false);
             }
@@ -210,6 +187,7 @@ public static class ConfigurationManager
                 themes = ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply () ?? false;
                 settings = Settings?.Apply () ?? false;
             }
+
             appSettings = AppSettings?.Apply () ?? false;
         }
         catch (JsonException e)
@@ -243,14 +221,23 @@ public static class ConfigurationManager
     }
 
     /// <summary>
-    ///     Loads all settings found in the various configuration storage locations to the
-    ///     <see cref="ConfigurationManager"/>. Optionally, resets all settings attributed with
+    ///     Gets or sets the in-memory config.json. See <see cref="ConfigLocations.Runtime"/>.
+    /// </summary>
+    public static string? RuntimeConfig { get; set; } = """{  }""";
+
+    /// <summary>
+    ///     Loads all settings found in the configuration storage locations (<see cref="ConfigLocations"/>). Optionally, resets
+    ///     all settings attributed with
     ///     <see cref="SerializableConfigurationProperty"/> to the defaults.
     /// </summary>
-    /// <remarks>Use <see cref="Apply"/> to cause the loaded settings to be applied to the running application.</remarks>
+    /// <remarks>
+    /// <para>
+    ///     Use <see cref="Apply"/> to cause the loaded settings to be applied to the running application.
+    /// </para>
+    /// </remarks>
     /// <param name="reset">
     ///     If <see langword="true"/> the state of <see cref="ConfigurationManager"/> will be reset to the
-    ///     defaults.
+    ///     defaults (<see cref="ConfigLocations.Default"/>).
     /// </param>
     [RequiresUnreferencedCode ("AOT")]
     [RequiresDynamicCode ("AOT")]
@@ -263,8 +250,7 @@ public static class ConfigurationManager
             Reset ();
         }
 
-        // LibraryResources is always loaded by Reset
-        if (Locations == ConfigLocations.All)
+        if (Locations.HasFlag (ConfigLocations.AppResources))
         {
             string? embeddedStylesResourceName = Assembly.GetEntryAssembly ()
                                                          ?
@@ -276,27 +262,36 @@ public static class ConfigurationManager
                 embeddedStylesResourceName = _configFilename;
             }
 
-            Settings = Settings?
+            Settings?.UpdateFromResource (Assembly.GetEntryAssembly ()!, embeddedStylesResourceName!, ConfigLocations.AppResources);
+        }
 
-                       // Global current directory
-                       .Update ($"./.tui/{_configFilename}")
-                       ?
+        if (Locations.HasFlag (ConfigLocations.Runtime) && !string.IsNullOrEmpty (RuntimeConfig))
+        {
+            Settings?.Update (RuntimeConfig, "ConfigurationManager.RuntimeConfig", ConfigLocations.Runtime);
+        }
 
-                       // Global home directory
-                       .Update ($"~/.tui/{_configFilename}")
-                       ?
+        if (Locations.HasFlag (ConfigLocations.GlobalCurrent))
+        {
+            Settings?.Update ($"./.tui/{_configFilename}", ConfigLocations.GlobalCurrent);
+        }
+
+        if (Locations.HasFlag (ConfigLocations.GlobalHome))
+        {
+            Settings?.Update ($"~/.tui/{_configFilename}", ConfigLocations.GlobalHome);
+        }
 
-                       // App resources
-                       .UpdateFromResource (Assembly.GetEntryAssembly ()!, embeddedStylesResourceName!)
-                       ?
 
-                       // App current directory
-                       .Update ($"./.tui/{AppName}.{_configFilename}")
-                       ?
+        if (Locations.HasFlag (ConfigLocations.AppCurrent))
+        {
+            Settings?.Update ($"./.tui/{AppName}.{_configFilename}", ConfigLocations.AppCurrent);
+        }
 
-                       // App home directory
-                       .Update ($"~/.tui/{AppName}.{_configFilename}");
+        if (Locations.HasFlag (ConfigLocations.AppHome))
+        {
+            Settings?.Update ($"~/.tui/{AppName}.{_configFilename}", ConfigLocations.AppHome);
         }
+
+        ThemeManager.SelectedTheme = Settings!["Theme"].PropertyValue as string ?? "Default";
     }
 
     /// <summary>
@@ -314,12 +309,13 @@ public static class ConfigurationManager
     }
 
     /// <summary>
-    ///     Called when the configuration has been updated from a configuration file. Invokes the <see cref="Updated"/>
+    ///     Called when the configuration has been updated from a configuration file or reset. Invokes the
+    ///     <see cref="Updated"/>
     ///     event.
     /// </summary>
     public static void OnUpdated ()
     {
-        Debug.WriteLine (@"ConfigurationManager.OnApplied()");
+        Debug.WriteLine (@"ConfigurationManager.OnUpdated()");
         Updated?.Invoke (null, new ());
     }
 
@@ -359,20 +355,23 @@ public static class ConfigurationManager
         AppSettings = new ();
 
         // To enable some unit tests, we only load from resources if the flag is set
-        if (Locations.HasFlag (ConfigLocations.DefaultOnly))
+        if (Locations.HasFlag (ConfigLocations.Default))
         {
             Settings.UpdateFromResource (
                                          typeof (ConfigurationManager).Assembly,
-                                         $"Terminal.Gui.Resources.{_configFilename}"
+                                         $"Terminal.Gui.Resources.{_configFilename}",
+                                         ConfigLocations.Default
                                         );
         }
 
+        OnUpdated ();
+
         Apply ();
         ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply ();
         AppSettings?.Apply ();
     }
 
-    /// <summary>Event fired when the configuration has been updated from a configuration source. application.</summary>
+    /// <summary>Event fired when the configuration has been updated from a configuration source or reset.</summary>
     public static event EventHandler<ConfigurationManagerEventArgs>? Updated;
 
     internal static void AddJsonError (string error)
@@ -414,7 +413,13 @@ public static class ConfigurationManager
         }
 
         // If value type, just use copy constructor.
-        if (source.GetType ().IsValueType || source.GetType () == typeof (string))
+        if (source.GetType ().IsValueType || source is string)
+        {
+            return source;
+        }
+
+        // HACK: Key is a class, but we want to treat it as a value type so just _keyCode gets copied.
+        if (source.GetType () == typeof (Key))
         {
             return source;
         }
@@ -425,9 +430,6 @@ public static class ConfigurationManager
         {
             foreach (object? srcKey in ((IDictionary)source).Keys)
             {
-                if (srcKey is string)
-                { }
-
                 if (((IDictionary)destination).Contains (srcKey))
                 {
                     ((IDictionary)destination) [srcKey] =
@@ -477,13 +479,14 @@ public static class ConfigurationManager
             }
         }
 
-        return destination!;
+        return destination;
     }
 
+
     /// <summary>
-    ///     Retrieves the hard coded default settings from the Terminal.Gui library implementation. Used in development of
-    ///     the library to generate the default configuration file. Before calling Application.Init, make sure
-    ///     <see cref="Locations"/> is set to <see cref="ConfigLocations.None"/>.
+    ///     Retrieves the hard coded default settings (static properites) from the Terminal.Gui library implementation. Used in
+    ///     development of
+    ///     the library to generate the default configuration file.
     /// </summary>
     /// <remarks>
     ///     <para>
@@ -552,9 +555,12 @@ public static class ConfigurationManager
                                     let props = c.Value
                                                  .GetProperties (
                                                                  BindingFlags.Instance
-                                                                 | BindingFlags.Static
-                                                                 | BindingFlags.NonPublic
-                                                                 | BindingFlags.Public
+                                                                 |
+                                                                 BindingFlags.Static
+                                                                 |
+                                                                 BindingFlags.NonPublic
+                                                                 |
+                                                                 BindingFlags.Public
                                                                 )
                                                  .Where (
                                                          prop =>
@@ -577,17 +583,13 @@ public static class ConfigurationManager
                                                scp.OmitClassName
                                                    ? ConfigProperty.GetJsonPropertyName (p)
                                                    : $"{p.DeclaringType?.Name}.{p.Name}",
-                                               new() { PropertyInfo = p, PropertyValue = null }
+                                               new () { PropertyInfo = p, PropertyValue = null }
                                               );
                 }
                 else
                 {
                     throw new (
-                               $"Property {
-                                   p.Name
-                               } in class {
-                                   p.DeclaringType?.Name
-                               } is not static. All SerializableConfigurationProperty properties must be static."
+                               $"Property {p.Name} in class {p.DeclaringType?.Name} is not static. All SerializableConfigurationProperty properties must be static."
                               );
                 }
             }

+ 25 - 24
Terminal.Gui/Configuration/SettingsScope.cs

@@ -27,7 +27,7 @@ namespace Terminal.Gui;
 public class SettingsScope : Scope<SettingsScope>
 {
     /// <summary>The list of paths to the configuration files.</summary>
-    public List<string> Sources = new ();
+    public Dictionary<ConfigLocations, string> Sources { get; } = new ();
 
     /// <summary>Points to our JSON schema.</summary>
     [JsonInclude]
@@ -37,9 +37,10 @@ public class SettingsScope : Scope<SettingsScope>
     /// <summary>Updates the <see cref="SettingsScope"/> with the settings in a JSON string.</summary>
     /// <param name="stream">Json document to update the settings with.</param>
     /// <param name="source">The source (filename/resource name) the Json document was read from.</param>
+    /// <param name="location">Location</param>
     [RequiresUnreferencedCode ("AOT")]
     [RequiresDynamicCode ("AOT")]
-    public SettingsScope? Update (Stream stream, string source)
+    public SettingsScope? Update (Stream stream, string source, ConfigLocations location)
     {
         // Update the existing settings with the new settings.
         try
@@ -47,9 +48,9 @@ public class SettingsScope : Scope<SettingsScope>
             Update ((SettingsScope)JsonSerializer.Deserialize (stream, typeof (SettingsScope), _serializerOptions)!);
             OnUpdated ();
             Debug.WriteLine ($"ConfigurationManager: Read configuration from \"{source}\"");
-            if (!Sources.Contains (source))
+            if (!Sources.ContainsValue (source))
             {
-                Sources.Add (source);
+                Sources.Add (location, source);
             }
 
             return this;
@@ -68,19 +69,20 @@ public class SettingsScope : Scope<SettingsScope>
     }
 
     /// <summary>Updates the <see cref="SettingsScope"/> with the settings in a JSON file.</summary>
-    /// <param name="filePath"></param>
+    /// <param name="filePath">Path to the file.</param>
+    /// <param name="location">The location</param>
     [RequiresUnreferencedCode ("AOT")]
     [RequiresDynamicCode ("AOT")]
-    public SettingsScope? Update (string filePath)
+    public SettingsScope? Update (string filePath, ConfigLocations location)
     {
         string realPath = filePath.Replace ("~", Environment.GetFolderPath (Environment.SpecialFolder.UserProfile));
 
         if (!File.Exists (realPath))
         {
             Debug.WriteLine ($"ConfigurationManager: Configuration file \"{realPath}\" does not exist.");
-            if (!Sources.Contains (filePath))
+            if (!Sources.ContainsValue (filePath))
             {
-                Sources.Add (filePath);
+                Sources.Add (location, filePath);
             }
 
             return this;
@@ -95,7 +97,7 @@ public class SettingsScope : Scope<SettingsScope>
             try
             {
                 FileStream? stream = File.OpenRead (realPath);
-                SettingsScope? s = Update (stream, filePath);
+                SettingsScope? s = Update (stream, filePath, location);
                 stream.Close ();
                 stream.Dispose ();
 
@@ -103,7 +105,7 @@ public class SettingsScope : Scope<SettingsScope>
             }
             catch (IOException ioe)
             {
-                Debug.WriteLine($"Couldn't open {filePath}. Retrying...: {ioe}");
+                Debug.WriteLine ($"Couldn't open {filePath}. Retrying...: {ioe}");
                 Task.Delay (100);
                 retryCount++;
             }
@@ -115,27 +117,33 @@ public class SettingsScope : Scope<SettingsScope>
     /// <summary>Updates the <see cref="SettingsScope"/> with the settings in a JSON string.</summary>
     /// <param name="json">Json document to update the settings with.</param>
     /// <param name="source">The source (filename/resource name) the Json document was read from.</param>
+    /// <param name="location">The location.</param>
     [RequiresUnreferencedCode ("AOT")]
     [RequiresDynamicCode ("AOT")]
-    public SettingsScope? Update (string json, string source)
+    public SettingsScope? Update (string? json, string source, ConfigLocations location)
     {
+        if (string.IsNullOrEmpty (json))
+        {
+            return null;
+        }
         var stream = new MemoryStream ();
         var writer = new StreamWriter (stream);
         writer.Write (json);
         writer.Flush ();
         stream.Position = 0;
 
-        return Update (stream, source);
+        return Update (stream, source, location);
     }
 
     /// <summary>Updates the <see cref="SettingsScope"/> with the settings from a Json resource.</summary>
     /// <param name="assembly"></param>
     /// <param name="resourceName"></param>
+    /// <param name="location"></param>
     [RequiresUnreferencedCode ("AOT")]
     [RequiresDynamicCode ("AOT")]
-    public SettingsScope? UpdateFromResource (Assembly assembly, string resourceName)
+    public SettingsScope? UpdateFromResource (Assembly assembly, string resourceName, ConfigLocations location)
     {
-        if (resourceName is null || string.IsNullOrEmpty (resourceName))
+        if (string.IsNullOrEmpty (resourceName))
         {
             Debug.WriteLine (
                              $"ConfigurationManager: Resource \"{resourceName}\" does not exist in \"{assembly.GetName ().Name}\"."
@@ -144,20 +152,13 @@ public class SettingsScope : Scope<SettingsScope>
             return this;
         }
 
-        // BUG: Not trim-compatible
-        // Not a bug, per se, but it's easily fixable by just loading the file.
-        // Defaults can just be field initializers for involved types.
-        using Stream? stream = assembly.GetManifestResourceStream (resourceName)!;
+        using Stream? stream = assembly.GetManifestResourceStream (resourceName);
 
         if (stream is null)
         {
-            Debug.WriteLine (
-                             $"ConfigurationManager: Failed to read resource \"{resourceName}\" from \"{assembly.GetName ().Name}\"."
-                            );
-
-            return this;
+            return null;
         }
 
-        return Update (stream, $"resource://[{assembly.GetName ().Name}]/{resourceName}");
+        return Update (stream, $"resource://[{assembly.GetName ().Name}]/{resourceName}", location);
     }
 }

+ 3 - 1
Terminal.Gui/Configuration/ThemeManager.cs

@@ -110,7 +110,9 @@ public class ThemeManager : IDictionary<string, ThemeScope>
             string oldTheme = _theme;
             _theme = value;
 
-            if ((oldTheme != _theme || oldTheme != Settings! ["Theme"].PropertyValue as string) && Settings! ["Themes"]?.PropertyValue is Dictionary<string, ThemeScope> themes && themes.ContainsKey (_theme))
+            if ((oldTheme != _theme
+                 || oldTheme != Settings! ["Theme"].PropertyValue as string)
+                 && Settings! ["Themes"]?.PropertyValue is Dictionary<string, ThemeScope> themes && themes.ContainsKey (_theme))
             {
                 Settings! ["Theme"].PropertyValue = _theme;
                 Instance.OnThemeChanged (oldTheme);

+ 11 - 4
Terminal.Gui/Input/Key.cs

@@ -393,24 +393,31 @@ public class Key : EventArgs, IEquatable<Key>
     public static implicit operator string (Key key) { return key.ToString (); }
 
     /// <inheritdoc/>
-    public override bool Equals (object obj) { return obj is Key k && k.KeyCode == KeyCode && k.Handled == Handled; }
+    public override bool Equals (object obj)
+    {
+        if (obj is Key other)
+        {
+            return other._keyCode == _keyCode && other.Handled == Handled;
+        }
+        return false;
+    }
 
     bool IEquatable<Key>.Equals (Key other) { return Equals (other); }
 
     /// <inheritdoc/>
-    public override int GetHashCode () { return (int)KeyCode; }
+    public override int GetHashCode () { return _keyCode.GetHashCode (); }
 
     /// <summary>Compares two <see cref="Key"/>s for equality.</summary>
     /// <param name="a"></param>
     /// <param name="b"></param>
     /// <returns></returns>
-    public static bool operator == (Key a, Key b) { return a?.KeyCode == b?.KeyCode; }
+    public static bool operator == (Key a, Key b) { return a!.Equals (b); }
 
     /// <summary>Compares two <see cref="Key"/>s for not equality.</summary>
     /// <param name="a"></param>
     /// <param name="b"></param>
     /// <returns></returns>
-    public static bool operator != (Key a, Key b) { return a?.KeyCode != b?.KeyCode; }
+    public static bool operator != (Key a, Key b) { return !a!.Equals (b); }
 
     /// <summary>Compares two <see cref="Key"/>s for less-than.</summary>
     /// <param name="a"></param>

+ 16 - 4
Terminal.Gui/Input/KeyBindings.cs

@@ -46,7 +46,11 @@ public class KeyBindings
             binding.BoundView = boundViewForAppScope;
         }
 
-        Bindings.Add (key, binding);
+        // IMPORTANT: Add a COPY of the key. This is needed because ConfigurationManager.Apply uses DeepMemberWiseCopy 
+        // IMPORTANT: update the memory referenced by the key, and Dictionary uses caching for performance, and thus 
+        // IMPORTANT: Apply will update the Dictionary with the new key, but the old key will still be in the dictionary.
+        // IMPORTANT: See the ConfigurationManager.Illustrate_DeepMemberWiseCopy_Breaks_Dictionary test for details.
+        Bindings.Add (new (key), binding);
     }
 
     /// <summary>
@@ -213,7 +217,7 @@ public class KeyBindings
     // TODO: Add a dictionary comparer that ignores Scope
     // TODO: This should not be public!
     /// <summary>The collection of <see cref="KeyBinding"/> objects.</summary>
-    public Dictionary<Key, KeyBinding> Bindings { get; } = new ();
+    public Dictionary<Key, KeyBinding> Bindings { get; } = new (new KeyEqualityComparer ());
 
     /// <summary>
     ///     The view that the <see cref="KeyBindings"/> are bound to.
@@ -388,15 +392,23 @@ public class KeyBindings
     /// <returns><see langword="true"/> if the Key is bound; otherwise <see langword="false"/>.</returns>
     public bool TryGet (Key key, KeyBindingScope scope, out KeyBinding binding)
     {
-        binding = new (Array.Empty<Command> (), KeyBindingScope.Disabled, null);
+        if (!key.IsValid)
+        {
+            binding = new (Array.Empty<Command> (), KeyBindingScope.Disabled, null);
+            return false;
+        }
 
-        if (key.IsValid && Bindings.TryGetValue (key, out binding))
+        if (Bindings.TryGetValue (key, out binding))
         {
             if (scope.HasFlag (binding.Scope))
             {
                 return true;
             }
         }
+        else
+        {
+            binding = new (Array.Empty<Command> (), KeyBindingScope.Disabled, null);
+        }
 
         return false;
     }

+ 35 - 0
Terminal.Gui/Input/KeyEqualityComparer.cs

@@ -0,0 +1,35 @@
+#nullable enable
+using Terminal.Gui;
+
+/// <summary>
+/// 
+/// </summary>
+public class KeyEqualityComparer : IEqualityComparer<Key>
+{
+    /// <inheritdoc />
+    public bool Equals (Key? x, Key? y)
+    {
+        if (ReferenceEquals (x, y))
+        {
+            return true;
+        }
+
+        if (x is null || y is null)
+        {
+            return false;
+        }
+
+        return x.KeyCode == y.KeyCode;
+    }
+
+    /// <inheritdoc />
+    public int GetHashCode (Key? obj)
+    {
+        if (obj is null)
+        {
+            return 0;
+        }
+
+        return obj.KeyCode.GetHashCode ();
+    }
+}

+ 0 - 0
UICatalog/Scenarios/Editors/Resources/config.json → UICatalog/Resources/config.json


+ 95 - 91
UICatalog/Scenarios/ConfigurationEditor.cs

@@ -1,4 +1,5 @@
-using System;
+#nullable enable
+using System;
 using System.IO;
 using System.Linq;
 using System.Reflection;
@@ -21,9 +22,9 @@ public class ConfigurationEditor : Scenario
         HotNormal = new Attribute (Color.Magenta, Color.White)
     };
 
-    private static Action _editorColorSchemeChanged;
-    private Shortcut _lenShortcut;
-    private TileView _tileView;
+    private static Action? _editorColorSchemeChanged;
+    private TabView? _tabView;
+    private Shortcut? _lenShortcut;
 
     [SerializableConfigurationProperty (Scope = typeof (AppScope))]
     public static ColorScheme EditorColorScheme
@@ -42,26 +43,15 @@ public class ConfigurationEditor : Scenario
 
         Toplevel top = new ();
 
-        _tileView = new TileView (0)
-        {
-            Width = Dim.Fill (),
-            Height = Dim.Fill (1),
-            Orientation = Orientation.Vertical,
-            LineStyle = LineStyle.Single,
-            TabStop = TabBehavior.TabGroup
-        };
-
-        top.Add (_tileView);
-
         _lenShortcut = new Shortcut ()
         {
-            Title = "Len: ",
+            Title = "",
         };
 
         var quitShortcut = new Shortcut ()
         {
             Key = Application.QuitKey,
-            Title = $"{Application.QuitKey} Quit",
+            Title = $"Quit",
             Action = Quit
         };
 
@@ -81,35 +71,44 @@ public class ConfigurationEditor : Scenario
 
         var statusBar = new StatusBar ([quitShortcut, reloadShortcut, saveShortcut, _lenShortcut]);
 
-        top.Add (statusBar);
+        _tabView = new ()
+        {
+            Width = Dim.Fill (),
+            Height = Dim.Fill (Dim.Func (() => statusBar.Frame.Height))
+        };
+
+        top.Add (_tabView, statusBar);
 
         top.Loaded += (s, a) =>
                       {
                           Open ();
-                          //_tileView.AdvanceFocus (NavigationDirection.Forward, null);
+                          _editorColorSchemeChanged?.Invoke ();
                       };
 
-        _editorColorSchemeChanged += () =>
-                                     {
-                                         foreach (Tile t in _tileView.Tiles)
-                                         {
-                                             t.ContentView.ColorScheme = EditorColorScheme;
-                                             t.ContentView.SetNeedsDraw ();
-                                         }
+        void OnEditorColorSchemeChanged ()
+        {
+            if (Application.Top is { })
+            {
+                return;
+            }
 
-                                         ;
-                                     };
+            foreach (ConfigTextView t in _tabView.Subviews.Where (v => v is ConfigTextView).Cast<ConfigTextView> ())
+            {
+                t.ColorScheme = EditorColorScheme;
+            }
+        }
 
-        _editorColorSchemeChanged.Invoke ();
+        _editorColorSchemeChanged += OnEditorColorSchemeChanged;
 
         Application.Run (top);
+        _editorColorSchemeChanged -= OnEditorColorSchemeChanged;
         top.Dispose ();
 
         Application.Shutdown ();
     }
     public void Save ()
     {
-        if (_tileView.MostFocused is ConfigTextView editor)
+        if (Application.Navigation?.GetFocused () is ConfigTextView editor)
         {
             editor.Save ();
         }
@@ -117,56 +116,65 @@ public class ConfigurationEditor : Scenario
 
     private void Open ()
     {
-        var subMenu = new MenuBarItem { Title = "_View" };
-
-        foreach (string configFile in ConfigurationManager.Settings.Sources)
+        foreach (var config in ConfigurationManager.Settings!.Sources)
         {
             var homeDir = $"{Environment.GetFolderPath (Environment.SpecialFolder.UserProfile)}";
-            var fileInfo = new FileInfo (configFile.Replace ("~", homeDir));
-
-            Tile tile = _tileView.InsertTile (_tileView.Tiles.Count);
-            tile.Title = configFile.StartsWith ("resource://") ? fileInfo.Name : configFile;
+            var fileInfo = new FileInfo (config.Value.Replace ("~", homeDir));
 
-            var textView = new ConfigTextView
+            var editor = new ConfigTextView
             {
-                X = 0,
-                Y = 0,
+                Title = config.Value.StartsWith ("resource://") ? fileInfo.Name : config.Value,
                 Width = Dim.Fill (),
-                Height = Dim.Fill (),
+                Height = Dim.Fill(),
                 FileInfo = fileInfo,
-                Tile = tile
             };
 
-            tile.ContentView.Add (textView);
+            Tab tab = new Tab ()
+            {
+                View = editor,
+                DisplayText = config.Key.ToString ()
+            };
 
-            textView.Read ();
+            _tabView!.AddTab (tab, false);
 
-            textView.HasFocusChanged += (s, e) =>
-                                        {
-                                            if (e.NewValue)
-                                            {
-                                                _lenShortcut.Title = $"Len:{textView.Text.Length}";
-                                            }
-                                        };
-        }
+            editor.Read ();
 
-        if (_tileView.Tiles.Count > 2)
-        {
-            _tileView.Tiles.ToArray () [1].ContentView.SetFocus ();
+            editor.ContentsChanged += (sender, args) =>
+                                      {
+                                          _lenShortcut!.Title = _lenShortcut!.Title.Replace ("*", "");
+                                          if (editor.IsDirty)
+                                          {
+                                              _lenShortcut!.Title += "*";
+                                          }
+                                      };
+
+            _lenShortcut!.Title = $"{editor.Title}";
         }
+
+        _tabView!.SelectedTabChanged += (sender, args) =>
+                                       {
+                                           _lenShortcut!.Title = $"{args.NewTab.View!.Title}";
+                                       };
+
     }
 
     private void Quit ()
     {
-        foreach (Tile tile in _tileView.Tiles)
+        foreach (ConfigTextView editor in _tabView!.Tabs.Select(v =>
+                                                                {
+                                                                    if (v.View is ConfigTextView ctv)
+                                                                    {
+                                                                        return ctv;
+                                                                    }
+
+                                                                    return null;
+                                                                }).Cast<ConfigTextView> ())
         {
-            var editor = tile.ContentView.Subviews [0] as ConfigTextView;
-
             if (editor.IsDirty)
             {
                 int result = MessageBox.Query (
                                                "Save Changes",
-                                               $"Save changes to {editor.FileInfo.FullName}",
+                                               $"Save changes to {editor.FileInfo!.Name}",
                                                "_Yes",
                                                "_No",
                                                "_Cancel"
@@ -189,7 +197,7 @@ public class ConfigurationEditor : Scenario
 
     private void Reload ()
     {
-        if (_tileView.MostFocused is ConfigTextView editor)
+        if (Application.Navigation?.GetFocused () is ConfigTextView editor)
         {
             editor.Read ();
         }
@@ -199,32 +207,16 @@ public class ConfigurationEditor : Scenario
     {
         internal ConfigTextView ()
         {
-            ContentsChanged += (s, obj) =>
-                               {
-                                   if (IsDirty)
-                                   {
-                                       if (!Tile.Title.EndsWith ('*'))
-                                       {
-                                           Tile.Title += '*';
-                                       }
-                                       else
-                                       {
-                                           Tile.Title = Tile.Title.TrimEnd ('*');
-                                       }
-                                   }
-                               };
             TabStop = TabBehavior.TabGroup;
-
         }
 
-        internal FileInfo FileInfo { get; set; }
-        internal Tile Tile { get; set; }
+        internal FileInfo? FileInfo { get; set; }
 
         internal void Read ()
         {
-            Assembly assembly = null;
+            Assembly? assembly = null;
 
-            if (FileInfo.FullName.Contains ("[Terminal.Gui]"))
+            if (FileInfo!.FullName.Contains ("[Terminal.Gui]"))
             {
                 // Library resources
                 assembly = typeof (ConfigurationManager).Assembly;
@@ -236,19 +228,27 @@ public class ConfigurationEditor : Scenario
 
             if (assembly != null)
             {
-                string name = assembly
-                              .GetManifestResourceNames ()
-                              .FirstOrDefault (x => x.EndsWith ("config.json"));
-                using Stream stream = assembly.GetManifestResourceStream (name);
-                using var reader = new StreamReader (stream);
-                Text = reader.ReadToEnd ();
-                ReadOnly = true;
-                Enabled = true;
+                string? name = assembly
+                               .GetManifestResourceNames ()
+                               .FirstOrDefault (x => x.EndsWith ("config.json"));
+                if (!string.IsNullOrEmpty (name))
+                {
+
+                    using Stream? stream = assembly.GetManifestResourceStream (name);
+                    using var reader = new StreamReader (stream!);
+                    Text = reader.ReadToEnd ();
+                    ReadOnly = true;
+                    Enabled = true;
+                }
 
                 return;
             }
 
-            if (!FileInfo.Exists)
+            if (FileInfo!.FullName.Contains ("RuntimeConfig"))
+            {
+                Text = ConfigurationManager.RuntimeConfig!;
+
+            } else if (!FileInfo.Exists)
             {
                 // Create empty config file
                 Text = ConfigurationManager.GetEmptyJson ();
@@ -257,12 +257,17 @@ public class ConfigurationEditor : Scenario
             {
                 Text = File.ReadAllText (FileInfo.FullName);
             }
-
-            Tile.Title = Tile.Title.TrimEnd ('*');
         }
 
         internal void Save ()
         {
+            if (FileInfo!.FullName.Contains ("RuntimeConfig"))
+            {
+                ConfigurationManager.RuntimeConfig = Text;
+                IsDirty = false;
+                return;
+            }
+
             if (!Directory.Exists (FileInfo.DirectoryName))
             {
                 // Create dir
@@ -272,7 +277,6 @@ public class ConfigurationEditor : Scenario
             using StreamWriter writer = File.CreateText (FileInfo.FullName);
             writer.Write (Text);
             writer.Close ();
-            Tile.Title = Tile.Title.TrimEnd ('*');
             IsDirty = false;
         }
     }

+ 2 - 2
UICatalog/UICatalog.csproj

@@ -20,10 +20,10 @@
     <DefineConstants>TRACE;DEBUG_IDISPOSABLE</DefineConstants>
   </PropertyGroup>
   <ItemGroup>
-    <None Remove="Scenarios\Editors\Resources\config.json" />
+    <None Remove="Resources\config.json" />
   </ItemGroup>
   <ItemGroup>
-    <EmbeddedResource Include="Scenarios\Editors\Resources\config.json" />
+    <EmbeddedResource Include="Resources\config.json" />
   </ItemGroup>
   <ItemGroup>
   <None Update="Scenarios\AnimationScenario\Spinning_globe_dark_small.gif" CopyToOutputDirectory="PreserveNewest" />

+ 49 - 8
UnitTests/Application/ApplicationTests.cs

@@ -1,4 +1,5 @@
 using Xunit.Abstractions;
+using static Terminal.Gui.ConfigurationManager;
 
 // Alias Console to MockConsole so we don't accidentally use Console
 
@@ -10,7 +11,7 @@ public class ApplicationTests
     {
         _output = output;
         ConsoleDriver.RunningUnitTests = true;
-        ConfigurationManager.Locations = ConfigurationManager.ConfigLocations.None;
+        Locations = ConfigLocations.Default;
 
 #if DEBUG_IDISPOSABLE
         View.Instances.Clear ();
@@ -272,14 +273,15 @@ public class ApplicationTests
     [InlineData (typeof (CursesDriver))]
     public void Init_ResetState_Resets_Properties (Type driverType)
     {
-        ConfigurationManager.ThrowOnJsonErrors = true;
+        ThrowOnJsonErrors = true;
 
         // For all the fields/properties of Application, check that they are reset to their default values
 
         // Set some values
 
         Application.Init (driverName: driverType.Name);
-       // Application.IsInitialized = true;
+
+        // Application.IsInitialized = true;
 
         // Reset
         Application.ResetState ();
@@ -370,7 +372,7 @@ public class ApplicationTests
         Application.ResetState ();
         CheckReset ();
 
-        ConfigurationManager.ThrowOnJsonErrors = false;
+        ThrowOnJsonErrors = false;
     }
 
     [Fact]
@@ -398,10 +400,7 @@ public class ApplicationTests
     }
 
     [Fact]
-    public void Shutdown_Alone_Does_Nothing ()
-    {
-        Application.Shutdown ();
-    }
+    public void Shutdown_Alone_Does_Nothing () { Application.Shutdown (); }
 
     [Theory]
     [InlineData (typeof (FakeDriver))]
@@ -520,6 +519,48 @@ public class ApplicationTests
         Application.ResetState ();
     }
 
+    [Fact]
+    public void Init_KeyBindings_Set_To_Defaults ()
+    {
+        // arrange
+        Locations = ConfigLocations.All;
+        ThrowOnJsonErrors = true;
+
+        Application.QuitKey = Key.Q;
+
+        Application.Init (new FakeDriver ());
+
+        Assert.Equal (Key.Esc, Application.QuitKey);
+
+        Application.Shutdown ();
+    }
+
+    [Fact]
+    public void Init_KeyBindings_Set_To_Custom ()
+    {
+        // arrange
+        Locations = ConfigLocations.Runtime;
+        ThrowOnJsonErrors = true;
+
+        RuntimeConfig = """
+                         {
+                               "Application.QuitKey": "Ctrl-Q"
+                         }
+                 """;
+
+        Assert.Equal (Key.Esc, Application.QuitKey);
+
+        // Act
+        Application.Init (new FakeDriver ());
+
+        Assert.Equal (Key.Q.WithCtrl, Application.QuitKey);
+
+        Assert.Contains (Key.Q.WithCtrl, Application.KeyBindings.Bindings);
+
+        Application.Shutdown ();
+        Locations = ConfigLocations.Default;
+    }
+
     [Fact]
     [AutoInitShutdown (verifyShutdown: true)]
     public void Internal_Properties_Correct ()

+ 1 - 1
UnitTests/Configuration/AppScopeTests.cs

@@ -15,7 +15,7 @@ public class AppScopeTests
     };
 
     [Fact]
-    [AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)]
+    [AutoInitShutdown (configLocation: ConfigLocations.Default)]
     public void Apply_ShouldApplyUpdatedProperties ()
     {
         Reset ();

+ 174 - 0
UnitTests/Configuration/ConfigPropertyTests.cs

@@ -0,0 +1,174 @@
+using System;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Terminal.Gui;
+using Xunit;
+
+public class ConfigPropertyTests
+{
+    [Fact]
+    public void Apply_PropertyValueIsAppliedToStatic_String_Property()
+    {
+        // Arrange
+        TestConfiguration.Reset ();
+        var propertyInfo = typeof(TestConfiguration).GetProperty(nameof(TestConfiguration.TestStringProperty));
+        var configProperty = new ConfigProperty
+        {
+            PropertyInfo = propertyInfo,
+            PropertyValue = "UpdatedValue"
+        };
+
+        // Act
+        var result = configProperty.Apply();
+
+        // Assert
+        Assert.Equal (1, TestConfiguration.TestStringPropertySetCount);
+        Assert.True(result);
+        Assert.Equal("UpdatedValue", TestConfiguration.TestStringProperty);
+        TestConfiguration.Reset ();
+    }
+
+    [Fact]
+    public void Apply_PropertyValueIsAppliedToStatic_Key_Property ()
+    {
+        // Arrange
+        TestConfiguration.Reset ();
+        var propertyInfo = typeof (TestConfiguration).GetProperty (nameof (TestConfiguration.TestKeyProperty));
+        var configProperty = new ConfigProperty
+        {
+            PropertyInfo = propertyInfo,
+            PropertyValue = Key.Q.WithCtrl
+        };
+
+        // Act
+        var result = configProperty.Apply ();
+
+        // Assert
+        Assert.Equal(1, TestConfiguration.TestKeyPropertySetCount);
+        Assert.True (result);
+        Assert.Equal (Key.Q.WithCtrl, TestConfiguration.TestKeyProperty);
+        TestConfiguration.Reset ();
+    }
+
+    [Fact]
+    public void RetrieveValue_GetsCurrentValueOfStaticProperty()
+    {
+        // Arrange
+        TestConfiguration.TestStringProperty = "CurrentValue";
+        var propertyInfo = typeof(TestConfiguration).GetProperty(nameof(TestConfiguration.TestStringProperty));
+        var configProperty = new ConfigProperty
+        {
+            PropertyInfo = propertyInfo
+        };
+
+        // Act
+        var value = configProperty.RetrieveValue();
+
+        // Assert
+        Assert.Equal("CurrentValue", value);
+        Assert.Equal("CurrentValue", configProperty.PropertyValue);
+    }
+
+    [Fact]
+    public void UpdateValueFrom_Updates_String_Property_Value ()
+    {
+        // Arrange
+        TestConfiguration.Reset ();
+        var propertyInfo = typeof(TestConfiguration).GetProperty(nameof(TestConfiguration.TestStringProperty));
+        var configProperty = new ConfigProperty
+        {
+            PropertyInfo = propertyInfo,
+            PropertyValue = "InitialValue"
+        };
+
+        // Act
+        var updatedValue = configProperty.UpdateValueFrom("NewValue");
+
+        // Assert
+        Assert.Equal (0, TestConfiguration.TestStringPropertySetCount);
+        Assert.Equal("NewValue", updatedValue);
+        Assert.Equal("NewValue", configProperty.PropertyValue);
+        TestConfiguration.Reset ();
+    }
+
+    //[Fact]
+    //public void UpdateValueFrom_InvalidType_ThrowsArgumentException()
+    //{
+    //    // Arrange
+    //    var propertyInfo = typeof(TestConfiguration).GetProperty(nameof(TestConfiguration.TestStringProperty));
+    //    var configProperty = new ConfigProperty
+    //    {
+    //        PropertyInfo = propertyInfo
+    //    };
+
+    //    // Act & Assert
+    //    Assert.Throws<ArgumentException>(() => configProperty.UpdateValueFrom(123));
+    //}
+
+    [Fact]
+    public void Apply_TargetInvocationException_ThrowsJsonException()
+    {
+        // Arrange
+        var propertyInfo = typeof(TestConfiguration).GetProperty(nameof(TestConfiguration.TestStringProperty));
+        var configProperty = new ConfigProperty
+        {
+            PropertyInfo = propertyInfo,
+            PropertyValue = null // This will cause ArgumentNullException in the set accessor
+        };
+
+        // Act & Assert
+        var exception = Assert.Throws<JsonException> (() => configProperty.Apply());
+    }
+
+    [Fact]
+    public void GetJsonPropertyName_ReturnsJsonPropertyNameAttributeValue()
+    {
+        // Arrange
+        var propertyInfo = typeof(TestConfiguration).GetProperty(nameof(TestConfiguration.TestStringProperty));
+
+        // Act
+        var jsonPropertyName = ConfigProperty.GetJsonPropertyName(propertyInfo);
+
+        // Assert
+        Assert.Equal("TestStringProperty", jsonPropertyName);
+    }
+}
+
+public class TestConfiguration
+{
+    private static string _testStringProperty = "Default";
+    public static int TestStringPropertySetCount { get; set; }
+
+    [SerializableConfigurationProperty]
+    public static string TestStringProperty
+    {
+        get => _testStringProperty;
+        set
+        {
+            TestStringPropertySetCount++;
+            _testStringProperty = value ?? throw new ArgumentNullException (nameof (value));
+        }
+    }
+
+    private static Key _testKeyProperty = Key.Esc;
+
+    public static int TestKeyPropertySetCount { get; set; }
+
+    [SerializableConfigurationProperty]
+    public static Key TestKeyProperty
+    {
+        get => _testKeyProperty;
+        set
+        {
+            TestKeyPropertySetCount++;
+            _testKeyProperty = value ?? throw new ArgumentNullException (nameof (value));
+        }
+    }
+
+    public static void Reset ()
+    {
+        TestStringPropertySetCount = 0;
+        TestKeyPropertySetCount = 0;
+    }
+}

+ 119 - 38
UnitTests/Configuration/ConfigurationMangerTests.cs

@@ -1,4 +1,5 @@
-using System.Reflection;
+using System.Diagnostics;
+using System.Reflection;
 using System.Text.Json;
 using Xunit.Abstractions;
 using static Terminal.Gui.ConfigurationManager;
@@ -21,7 +22,7 @@ public class ConfigurationManagerTests
     };
 
     [Fact]
-    public void Apply_FiresApplied ()
+    public void Apply_Raises_Applied ()
     {
         Reset ();
         Applied += ConfigurationManager_Applied;
@@ -146,16 +147,44 @@ public class ConfigurationManagerTests
         Assert.Equal (dictDest ["Normal"], dictCopy ["Normal"]);
     }
 
+    public class DeepCopyTest ()
+    {
+        public static Key key = Key.Esc;
+    }
+
     [Fact]
-    public void Load_FiresUpdated ()
+    public void Illustrate_DeepMemberWiseCopy_Breaks_Dictionary ()
     {
-        ConfigLocations savedLocations = Locations;
+        Assert.Equal (Key.Esc, DeepCopyTest.key);
+
+        Dictionary<Key, string> dict = new Dictionary<Key, string> (new KeyEqualityComparer ());
+        dict.Add (new (DeepCopyTest.key), "Esc");
+        Assert.Contains (Key.Esc, dict);
+
+        DeepCopyTest.key = (Key)DeepMemberWiseCopy (Key.Q.WithCtrl, DeepCopyTest.key);
+
+        Assert.Equal (Key.Q.WithCtrl, DeepCopyTest.key);
+        Assert.Equal (Key.Esc, dict.Keys.ToArray () [0]);
+
+        var eq = new KeyEqualityComparer ();
+        Assert.True (eq.Equals (Key.Q.WithCtrl, DeepCopyTest.key));
+        Assert.Equal (Key.Q.WithCtrl.GetHashCode (), DeepCopyTest.key.GetHashCode ());
+        Assert.Equal (eq.GetHashCode (Key.Q.WithCtrl), eq.GetHashCode (DeepCopyTest.key));
+        Assert.Equal (Key.Q.WithCtrl.GetHashCode (), eq.GetHashCode (DeepCopyTest.key));
+        Assert.True (dict.ContainsKey (Key.Esc));
+
+        dict.Remove (Key.Esc);  
+        dict.Add (new (DeepCopyTest.key), "Ctrl+Q");
+        Assert.True (dict.ContainsKey (Key.Q.WithCtrl));
+    }
+
+    [Fact]
+    public void Load_Raises_Updated ()
+    {
+        ThrowOnJsonErrors = true;
         Locations = ConfigLocations.All;
         Reset ();
-
-        Settings! ["Application.QuitKey"].PropertyValue = Key.Q;
-        Settings ["Application.NextTabGroupKey"].PropertyValue = Key.F;
-        Settings ["Application.PrevTabGroupKey"].PropertyValue = Key.B;
+        Assert.Equal (Key.Esc, (((Key)Settings! ["Application.QuitKey"].PropertyValue)!).KeyCode);
 
         Updated += ConfigurationManager_Updated;
         var fired = false;
@@ -163,29 +192,48 @@ public class ConfigurationManagerTests
         void ConfigurationManager_Updated (object sender, ConfigurationManagerEventArgs obj)
         {
             fired = true;
-
-            // assert
-            Assert.Equal (Key.Esc, (((Key)Settings! ["Application.QuitKey"].PropertyValue)!).KeyCode);
-
-            Assert.Equal (
-                          KeyCode.F6,
-                          (((Key)Settings ["Application.NextTabGroupKey"].PropertyValue)!).KeyCode
-                         );
-
-            Assert.Equal (
-                          KeyCode.F6 | KeyCode.ShiftMask,
-                          (((Key)Settings ["Application.PrevTabGroupKey"].PropertyValue)!).KeyCode
-                         );
         }
 
+        // Act
+        // Reset to cause load to raise event
         Load (true);
 
         // assert
         Assert.True (fired);
 
         Updated -= ConfigurationManager_Updated;
+
+        // clean up
+        Locations = ConfigLocations.Default;
+        Reset ();
+    }
+
+
+    [Fact]
+    public void Load_Loads_Custom_Json ()
+    {
+        // arrange
+        Locations = ConfigLocations.All;
+        Reset ();
+        ThrowOnJsonErrors = true;
+
+        Assert.Equal (Key.Esc, (Key)Settings! ["Application.QuitKey"].PropertyValue);
+
+        // act
+        RuntimeConfig = """
+                   
+                           {
+                                 "Application.QuitKey": "Ctrl-Q"
+                           }
+                   """;
+        Load (false);
+
+        // assert
+        Assert.Equal (Key.Q.WithCtrl, (Key)Settings ["Application.QuitKey"].PropertyValue);
+
+        // clean up
+        Locations = ConfigLocations.Default;
         Reset ();
-        Locations = savedLocations;
     }
 
     [Fact]
@@ -224,10 +272,40 @@ public class ConfigurationManagerTests
         //Assert.Equal ("AppSpecific", ConfigurationManager.Config.Settings.TestSetting);
     }
 
+
+    [Fact]
+    public void Reset_Raises_Updated ()
+    {
+        ConfigLocations savedLocations = Locations;
+        Locations = ConfigLocations.All;
+        Reset ();
+
+        Settings! ["Application.QuitKey"].PropertyValue = Key.Q;
+
+        Updated += ConfigurationManager_Updated;
+        var fired = false;
+
+        void ConfigurationManager_Updated (object sender, ConfigurationManagerEventArgs obj)
+        {
+            fired = true;
+        }
+
+        // Act
+        Reset ();
+
+        // assert
+        Assert.True (fired);
+
+        Updated -= ConfigurationManager_Updated;
+        Reset ();
+        Locations = savedLocations;
+    }
+
+
     [Fact]
     public void Reset_and_ResetLoadWithLibraryResourcesOnly_are_same ()
     {
-        Locations = ConfigLocations.DefaultOnly;
+        Locations = ConfigLocations.Default;
 
         // arrange
         Reset ();
@@ -257,7 +335,7 @@ public class ConfigurationManagerTests
         Settings ["Application.PrevTabGroupKey"].PropertyValue = Key.B;
         Settings.Apply ();
 
-        Locations = ConfigLocations.DefaultOnly;
+        Locations = ConfigLocations.Default;
 
         // act
         Reset ();
@@ -275,7 +353,7 @@ public class ConfigurationManagerTests
     [Fact]
     public void Reset_Resets ()
     {
-        Locations = ConfigLocations.DefaultOnly;
+        Locations = ConfigLocations.Default;
         Reset ();
         Assert.NotEmpty (Themes!);
         Assert.Equal ("Default", Themes.Theme);
@@ -433,7 +511,7 @@ public class ConfigurationManagerTests
     }
 
     [Fact]
-    [AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)]
+    [AutoInitShutdown (configLocation: ConfigLocations.Default)]
     public void TestConfigurationManagerInitDriver ()
     {
         Assert.Equal ("Default", Themes!.Theme);
@@ -444,7 +522,7 @@ public class ConfigurationManagerTests
         // Change Base
         Stream json = ToStream ();
 
-        Settings!.Update (json, "TestConfigurationManagerInitDriver");
+        Settings!.Update (json, "TestConfigurationManagerInitDriver", ConfigLocations.Runtime);
 
         Dictionary<string, ColorScheme> colorSchemes =
             (Dictionary<string, ColorScheme>)Themes [Themes.Theme] ["ColorSchemes"].PropertyValue;
@@ -469,7 +547,10 @@ public class ConfigurationManagerTests
 
     [Fact]
     [AutoInitShutdown (configLocation: ConfigLocations.None)]
-    public void TestConfigurationManagerInitDriver_NoLocations () { }
+    public void TestConfigurationManagerInitDriver_NoLocations ()
+    {
+        // TODO: Write this test
+    }
 
     [Fact]
     public void TestConfigurationManagerInvalidJsonLogs ()
@@ -499,7 +580,7 @@ public class ConfigurationManagerTests
 				}
 			}";
 
-        Settings!.Update (json, "test");
+        Settings!.Update (json, "test", ConfigLocations.Runtime);
 
         // AbNormal is not a ColorScheme attribute
         json = @"
@@ -522,7 +603,7 @@ public class ConfigurationManagerTests
 				}
 			}";
 
-        Settings.Update (json, "test");
+        Settings.Update (json, "test", ConfigLocations.Runtime);
 
         // Modify hotNormal background only
         json = @"
@@ -544,9 +625,9 @@ public class ConfigurationManagerTests
 				}
 			}";
 
-        Settings.Update (json, "test");
+        Settings.Update (json, "test", ConfigLocations.Runtime);
 
-        Settings.Update ("{}}", "test");
+        Settings.Update ("{}}", "test", ConfigLocations.Runtime);
 
         Assert.NotEqual (0, _jsonErrors.Length);
 
@@ -582,7 +663,7 @@ public class ConfigurationManagerTests
 				]
 			}";
 
-        var jsonException = Assert.Throws<JsonException> (() => Settings!.Update (json, "test"));
+        var jsonException = Assert.Throws<JsonException> (() => Settings!.Update (json, "test", ConfigLocations.Runtime));
         Assert.Equal ("Unexpected color name: brownish.", jsonException.Message);
 
         // AbNormal is not a ColorScheme attribute
@@ -606,7 +687,7 @@ public class ConfigurationManagerTests
 				]
 			}";
 
-        jsonException = Assert.Throws<JsonException> (() => Settings!.Update (json, "test"));
+        jsonException = Assert.Throws<JsonException> (() => Settings!.Update (json, "test", ConfigLocations.Runtime));
         Assert.Equal ("Unrecognized ColorScheme Attribute name: AbNormal.", jsonException.Message);
 
         // Modify hotNormal background only
@@ -629,7 +710,7 @@ public class ConfigurationManagerTests
 				]
 			}";
 
-        jsonException = Assert.Throws<JsonException> (() => Settings!.Update (json, "test"));
+        jsonException = Assert.Throws<JsonException> (() => Settings!.Update (json, "test", ConfigLocations.Runtime));
         Assert.Equal ("Both Foreground and Background colors must be provided.", jsonException.Message);
 
         // Unknown property
@@ -638,7 +719,7 @@ public class ConfigurationManagerTests
 				""Unknown"" : ""Not known""
 			}";
 
-        jsonException = Assert.Throws<JsonException> (() => Settings!.Update (json, "test"));
+        jsonException = Assert.Throws<JsonException> (() => Settings!.Update (json, "test", ConfigLocations.Runtime));
         Assert.StartsWith ("Unknown property", jsonException.Message);
 
         Assert.Equal (0, _jsonErrors.Length);
@@ -654,7 +735,7 @@ public class ConfigurationManagerTests
         GetHardCodedDefaults ();
         Stream stream = ToStream ();
 
-        Settings!.Update (stream, "TestConfigurationManagerToJson");
+        Settings!.Update (stream, "TestConfigurationManagerToJson", ConfigLocations.Runtime);
     }
 
     [Fact]
@@ -803,7 +884,7 @@ public class ConfigurationManagerTests
         Reset ();
         ThrowOnJsonErrors = true;
 
-        Settings!.Update (json, "TestConfigurationManagerUpdateFromJson");
+        Settings!.Update (json, "TestConfigurationManagerUpdateFromJson", ConfigLocations.Runtime);
 
         Assert.Equal (KeyCode.Esc, Application.QuitKey.KeyCode);
         Assert.Equal (KeyCode.Z | KeyCode.AltMask, ((Key)Settings ["Application.QuitKey"].PropertyValue)!.KeyCode);

+ 14 - 0
UnitTests/Configuration/KeyJsonConverterTests.cs

@@ -52,6 +52,20 @@ public class KeyJsonConverterTests
         Assert.Equal (expectedStringTo, deserializedKey.ToString ());
     }
 
+    [Fact]
+    public void Deserialized_Key_Equals ()
+    {
+        // Arrange
+        Key key = Key.Q.WithCtrl;
+
+        // Act
+        string json = "\"Ctrl+Q\"";
+        Key deserializedKey = JsonSerializer.Deserialize<Key> (json, ConfigurationManager._serializerOptions);
+
+        // Assert
+        Assert.Equal (key, deserializedKey);
+
+    }
     [Fact]
     public void Separator_Property_Serializes_As_Glyph ()
     {

+ 37 - 3
UnitTests/Configuration/SettingsScopeTests.cs

@@ -5,9 +5,39 @@ namespace Terminal.Gui.ConfigurationTests;
 public class SettingsScopeTests
 {
     [Fact]
-    [AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)]
+    public void Update_Overrides_Defaults ()
+    {
+        // arrange
+        Locations = ConfigLocations.Default;
+        Load (true);
+
+        Assert.Equal (Key.Esc, (Key)Settings ["Application.QuitKey"].PropertyValue);
+
+        ThrowOnJsonErrors = true;
+
+        // act
+        var json = """
+                   
+                           {
+                                 "Application.QuitKey": "Ctrl-Q"
+                           }
+                   """;
+
+        Settings!.Update (json, "test", ConfigLocations.Runtime);
+
+        // assert
+        Assert.Equal (Key.Q.WithCtrl, (Key)Settings ["Application.QuitKey"].PropertyValue);
+
+        // clean up
+        Locations = ConfigLocations.All;
+    }
+
+    [Fact]
     public void Apply_ShouldApplyProperties ()
     {
+        Locations = ConfigLocations.Default;
+        Reset();
+
         // arrange
         Assert.Equal (Key.Esc, (Key)Settings ["Application.QuitKey"].PropertyValue);
 
@@ -18,7 +48,7 @@ public class SettingsScopeTests
 
         Assert.Equal (
                       Key.F6.WithShift,
-                      (Key)Settings["Application.PrevTabGroupKey"].PropertyValue
+                      (Key)Settings ["Application.PrevTabGroupKey"].PropertyValue
                      );
 
         // act
@@ -32,6 +62,10 @@ public class SettingsScopeTests
         Assert.Equal (Key.Q, Application.QuitKey);
         Assert.Equal (Key.F, Application.NextTabGroupKey);
         Assert.Equal (Key.B, Application.PrevTabGroupKey);
+
+        Locations = ConfigLocations.Default;
+        Reset ();
+
     }
 
     [Fact]
@@ -56,7 +90,7 @@ public class SettingsScopeTests
     public void GetHardCodedDefaults_ShouldSetProperties ()
     {
         ConfigLocations savedLocations = Locations;
-        Locations = ConfigLocations.DefaultOnly;
+        Locations = ConfigLocations.Default;
         Reset ();
 
         Assert.Equal (5, ((Dictionary<string, ThemeScope>)Settings ["Themes"].PropertyValue).Count);

+ 4 - 4
UnitTests/Configuration/ThemeScopeTests.cs

@@ -15,7 +15,7 @@ public class ThemeScopeTests
     };
 
     [Fact]
-    [AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)]
+    [AutoInitShutdown (configLocation: ConfigLocations.Default)]
     public void AllThemesPresent ()
     {
         Reset ();
@@ -25,7 +25,7 @@ public class ThemeScopeTests
     }
 
     [Fact]
-    [AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)]
+    [AutoInitShutdown (configLocation: ConfigLocations.Default)]
     public void Apply_ShouldApplyUpdatedProperties ()
     {
         Reset ();
@@ -54,7 +54,7 @@ public class ThemeScopeTests
     }
 
     [Fact]
-    [AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)]
+    [AutoInitShutdown (configLocation: ConfigLocations.Default)]
     public void TestSerialize_RoundTrip ()
     {
         Reset ();
@@ -71,7 +71,7 @@ public class ThemeScopeTests
     }
 
     [Fact]
-    [AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)]
+    [AutoInitShutdown (configLocation: ConfigLocations.Default)]
     public void ThemeManager_ClassMethodsWork ()
     {
         Reset ();

+ 2 - 2
UnitTests/Configuration/ThemeTests.cs

@@ -11,7 +11,7 @@ public class ThemeTests
     };
 
     [Fact]
-    [AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)]
+    [AutoInitShutdown (configLocation: ConfigLocations.Default)]
     public void TestApply ()
     {
         Reset ();
@@ -33,7 +33,7 @@ public class ThemeTests
     }
 
     [Fact]
-    [AutoInitShutdown (configLocation: ConfigLocations.DefaultOnly)]
+    [AutoInitShutdown (configLocation: ConfigLocations.Default)]
     public void TestApply_UpdatesColors ()
     {
         // Arrange

+ 13 - 0
UnitTests/Input/KeyBindingTests.cs

@@ -331,6 +331,19 @@ public class KeyBindingTests
     }
 
     // TryGet
+    [Fact]
+    public void TryGet_Succeeds ()
+    {
+        var keyBindings = new KeyBindings ();
+        keyBindings.Add (Key.Q.WithCtrl, KeyBindingScope.Application, Command.HotKey);
+        var key = new Key (Key.Q.WithCtrl);
+        bool result = keyBindings.TryGet (key, out KeyBinding _);
+        Assert.True (result);;
+
+        result = keyBindings.Bindings.TryGetValue (key, out KeyBinding _);
+        Assert.True (result);
+    }
+
     [Fact]
     public void TryGet_Unknown_ReturnsFalse ()
     {

+ 4 - 0
UnitTests/Input/KeyTests.cs

@@ -532,6 +532,10 @@ public class KeyTests
         Key a = Key.A;
         Key b = Key.A;
         Assert.True (a.Equals (b));
+
+        b.Handled = true;
+        Assert.False (a.Equals (b));
+
     }
 
     [Fact]

+ 2 - 2
UnitTests/TestHelpers.cs

@@ -49,7 +49,7 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute
         bool useFakeClipboard = true,
         bool fakeClipboardAlwaysThrowsNotSupportedException = false,
         bool fakeClipboardIsSupportedAlwaysTrue = false,
-        ConfigLocations configLocation = ConfigLocations.None,
+        ConfigLocations configLocation = ConfigLocations.Default, // DefaultOnly is the default for tests
         bool verifyShutdown = false
     )
     {
@@ -110,7 +110,7 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute
         }
 
         // Reset to defaults
-        Locations = ConfigLocations.DefaultOnly;
+        Locations = ConfigLocations.Default;
         Reset ();
 
         // Enable subsequent tests that call Init to get all config files (the default).

+ 8 - 8
UnitTests/UICatalog/ScenarioTests.cs

@@ -31,8 +31,8 @@ public class ScenarioTests : TestsAllViews
         _timeoutLock = new ();
 
         // Disable any UIConfig settings
-        ConfigurationManager.ConfigLocations savedConfigLocations = ConfigurationManager.Locations;
-        ConfigurationManager.Locations = ConfigurationManager.ConfigLocations.DefaultOnly;
+        ConfigLocations savedConfigLocations = ConfigurationManager.Locations;
+        ConfigurationManager.Locations = ConfigLocations.Default;
 
         // If a previous test failed, this will ensure that the Application is in a clean state
         Application.ResetState (true);
@@ -148,8 +148,8 @@ public class ScenarioTests : TestsAllViews
         _timeoutLock = new ();
 
         // Disable any UIConfig settings
-        ConfigurationManager.ConfigLocations savedConfigLocations = ConfigurationManager.Locations;
-        ConfigurationManager.Locations = ConfigurationManager.ConfigLocations.DefaultOnly;
+        ConfigLocations savedConfigLocations = ConfigurationManager.Locations;
+        ConfigurationManager.Locations = ConfigLocations.Default;
 
         // If a previous test failed, this will ensure that the Application is in a clean state
         Application.ResetState (true);
@@ -305,8 +305,8 @@ public class ScenarioTests : TestsAllViews
     public void Run_All_Views_Tester_Scenario ()
     {
         // Disable any UIConfig settings
-        ConfigurationManager.ConfigLocations savedConfigLocations = ConfigurationManager.Locations;
-        ConfigurationManager.Locations = ConfigurationManager.ConfigLocations.DefaultOnly;
+        ConfigLocations savedConfigLocations = ConfigurationManager.Locations;
+        ConfigurationManager.Locations = ConfigLocations.Default;
 
         Window _leftPane;
         ListView _classListView;
@@ -764,8 +764,8 @@ public class ScenarioTests : TestsAllViews
     public void Run_Generic ()
     {
         // Disable any UIConfig settings
-        ConfigurationManager.ConfigLocations savedConfigLocations = ConfigurationManager.Locations;
-        ConfigurationManager.Locations = ConfigurationManager.ConfigLocations.DefaultOnly;
+        ConfigLocations savedConfigLocations = ConfigurationManager.Locations;
+        ConfigurationManager.Locations = ConfigLocations.Default;
 
         ObservableCollection<Scenario> scenarios = Scenario.GetScenarios ();
         Assert.NotEmpty (scenarios);

+ 2 - 0
UnitTests/View/Draw/AllViewsDrawTests.cs

@@ -8,6 +8,8 @@ public class AllViewsDrawTests (ITestOutputHelper _output) : TestsAllViews
     [MemberData (nameof (AllViewTypes))]
     public void AllViews_Draw_Does_Not_Layout (Type viewType)
     {
+        Application.ResetState (true);
+
         var view = (View)CreateInstanceIfNotGeneric (viewType);
 
         if (view == null)

+ 1 - 1
UnitTests/View/ViewTests.cs

@@ -283,7 +283,7 @@ public class ViewTests (ITestOutputHelper output)
     }
 
     [Theory]
-    [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)]
+    [AutoInitShutdown (configLocation: ConfigLocations.Default)]
     [InlineData (true)]
     [InlineData (false)]
     public void Clear_Does_Not_Spillover_Its_Parent (bool label)

+ 1 - 1
UnitTests/Views/ComboBoxTests.cs

@@ -494,7 +494,7 @@ public class ComboBoxTests (ITestOutputHelper output)
     }
 
     [Fact]
-    [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)]
+    [AutoInitShutdown (configLocation: ConfigLocations.Default)]
     public void HideDropdownListOnClick_True_Highlight_Current_Item ()
     {
         var selected = "";

+ 2 - 2
UnitTests/Views/MenuBarTests.cs

@@ -315,7 +315,7 @@ public class MenuBarTests (ITestOutputHelper output)
     }
 
     [Fact]
-    [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)]
+    [AutoInitShutdown (configLocation: ConfigLocations.Default)]
     public void Disabled_MenuBar_Is_Never_Opened ()
     {
         Toplevel top = new ();
@@ -341,7 +341,7 @@ public class MenuBarTests (ITestOutputHelper output)
     }
 
     [Fact (Skip = "#3798 Broke. Will fix in #2975")]
-    [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)]
+    [AutoInitShutdown (configLocation: ConfigLocations.Default)]
     public void Disabled_MenuItem_Is_Never_Selected ()
     {
         var menu = new MenuBar

+ 1 - 1
UnitTests/Views/TabViewTests.cs

@@ -1480,7 +1480,7 @@ public class TabViewTests (ITestOutputHelper output)
 
     private void InitFakeDriver ()
     {
-        ConfigurationManager.Locations = ConfigurationManager.ConfigLocations.DefaultOnly;
+        ConfigurationManager.Locations = ConfigLocations.Default;
         ConfigurationManager.Reset ();
 
         var driver = new FakeDriver ();

+ 9 - 9
UnitTests/Views/TableViewTests.cs

@@ -54,7 +54,7 @@ public class TableViewTests (ITestOutputHelper output)
     }
 
     [Fact]
-    [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)]
+    [AutoInitShutdown (configLocation: ConfigLocations.Default)]
     public void CellEventsBackgroundFill ()
     {
         var tv = new TableView { Width = 20, Height = 4 };
@@ -412,7 +412,7 @@ public class TableViewTests (ITestOutputHelper output)
     }
 
     [Fact]
-    [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)]
+    [AutoInitShutdown (configLocation: ConfigLocations.Default)]
     public void LongColumnTest ()
     {
         var tableView = new TableView ();
@@ -593,7 +593,7 @@ public class TableViewTests (ITestOutputHelper output)
         top.Dispose ();
     }
 
-    [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)]
+    [AutoInitShutdown (configLocation: ConfigLocations.Default)]
     [Fact]
     public void PageDown_ExcludesHeaders ()
     {
@@ -993,7 +993,7 @@ public class TableViewTests (ITestOutputHelper output)
     }
 
     [Fact]
-    [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)]
+    [AutoInitShutdown (configLocation: ConfigLocations.Default)]
     public void TableView_Activate ()
     {
         string activatedValue = null;
@@ -1033,7 +1033,7 @@ public class TableViewTests (ITestOutputHelper output)
     }
 
     [Theory]
-    [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)]
+    [AutoInitShutdown (configLocation: ConfigLocations.Default)]
     [InlineData (false)]
     [InlineData (true)]
     public void TableView_ColorsTest_ColorGetter (bool focused)
@@ -1134,7 +1134,7 @@ public class TableViewTests (ITestOutputHelper output)
     }
 
     [Theory]
-    [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)]
+    [AutoInitShutdown (configLocation: ConfigLocations.Default)]
     [InlineData (false)]
     [InlineData (true)]
     public void TableView_ColorsTest_RowColorGetter (bool focused)
@@ -1228,7 +1228,7 @@ public class TableViewTests (ITestOutputHelper output)
     }
 
     [Theory]
-    [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)]
+    [AutoInitShutdown (configLocation: ConfigLocations.Default)]
     [InlineData (false)]
     [InlineData (true)]
     public void TableView_ColorTests_FocusedOrNot (bool focused)
@@ -1566,7 +1566,7 @@ public class TableViewTests (ITestOutputHelper output)
     }
 
     [Fact]
-    [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)]
+    [AutoInitShutdown (configLocation: ConfigLocations.Default)]
     public void Test_CollectionNavigator ()
     {
         var tv = new TableView ();
@@ -2572,7 +2572,7 @@ A B C
     [SetupFakeDriver]
     public void TestTableViewCheckboxes_ByObject ()
     {
-        ConfigurationManager.Locations = ConfigurationManager.ConfigLocations.DefaultOnly;
+        ConfigurationManager.Locations = ConfigLocations.Default;
         ConfigurationManager.Reset();
 
         TableView tv = GetPetTable (out EnumerableTableSource<PickablePet> source);

+ 2 - 2
UnitTests/Views/TextViewTests.cs

@@ -8525,7 +8525,7 @@ line.
     {
         public static string Txt = "TAB to jump between text fields.";
 
-        public TextViewTestsAutoInitShutdown () : base (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly) { }
+        public TextViewTestsAutoInitShutdown () : base (configLocation: ConfigLocations.Default) { }
 
         public override void After (MethodInfo methodUnderTest)
         {
@@ -8947,7 +8947,7 @@ line.  ",
     }
 
     [Fact]
-    [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)]
+    [AutoInitShutdown (configLocation: ConfigLocations.Default)]
     public void Cell_LoadCells_InheritsPreviousAttribute ()
     {
         List<Cell> cells = [];

+ 1 - 1
UnitTests/Views/TreeTableSourceTests.cs

@@ -155,7 +155,7 @@ public class TreeTableSourceTests : IDisposable
     }
 
     [Fact]
-    [AutoInitShutdown (configLocation:ConfigurationManager.ConfigLocations.DefaultOnly)]
+    [AutoInitShutdown (configLocation:ConfigLocations.Default)]
     public void TestTreeTableSource_CombinedWithCheckboxes ()
     {
         Toplevel top = new ();

+ 20 - 64
docfx/docs/config.md

@@ -12,17 +12,19 @@ Settings that will apply to all applications (global settings) reside in files n
 
 Settings are applied using the following precedence (higher precedence settings overwrite lower precedence settings):
 
-1. App-specific settings in the users's home directory (`~/.tui/appname.config.json`). -- Highest precedence.
+1. @Terminal.Gui.ConfigLocations.Default - Default settings in the Terminal.Gui assembly -- Lowest precedence.
 
-2. App-specific settings in the directory the app was launched from (`./.tui/appname.config.json`).
+2. @Terminal.Gui.ConfigLocations.Runtime - Settings stored in the @Terminal.Gui.ConfigurationManager.RuntimeConfig static property.
 
-3. App settings in app resources (`Resources/config.json`).
+3. @Terminal.Gui.ConfigLocations.AppResources - App settings in app resources (`Resources/config.json`).
 
-4. Global settings in the the user's home directory (`~/.tui/config.json`).
+4. @Terminal.Gui.ConfigLocations.AppHome - App-specific settings in the users's home directory (`~/.tui/appname.config.json`). 
 
-5. Global settings in the directory the app was launched from (`./.tui/config.json`).
+5. @Terminal.Gui.ConfigLocations.AppCurrent - App-specific settings in the directory the app was launched from (`./.tui/appname.config.json`).
 
-6. Default settings in the Terminal.Gui assembly -- Lowest precedence.
+6. @Terminal.Gui.ConfigLocations.GlobalHome - Global settings in the the user's home directory (`~/.tui/config.json`).
+
+7. @Terminal.Gui.ConfigLocations.GlobalCurrent - Global settings in the directory the app was launched from (`./.tui/config.json`) --- Hightest precedence.
 
 The `UI Catalog` application provides an example of how to use the [`ConfigurationManager`](~/api/Terminal.Gui.ConfigurationManager.yml) class to load and save configuration files. The `Configuration Editor` scenario provides an editor that allows users to edit the configuration files. UI Catalog also uses a file system watcher to detect changes to the configuration files to tell [`ConfigurationManager`](~/api/Terminal.Gui.ConfigurationManager.yml) to reload them; allowing users to change settings without having to restart the application.
 
@@ -67,71 +69,25 @@ A Theme is a named collection of settings that impact the visual style of Termin
 
 Themes support defining ColorSchemes as well as various default settings for Views. Both the default color schemes and user-defined color schemes can be configured. See [ColorSchemes](~/api/Terminal.Gui.Colors.yml) for more information.
 
-# Example Configuration File
-
-```json
-{
-  "$schema": "https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json",
-  "Application.QuitKey": {
-    "Key": "Esc"
-  },
-  "AppSettings": {
-    "UICatalog.StatusBar": false
-  },
-  "Theme": "UI Catalog Theme",
-  "Themes": [
-    {
-      "UI Catalog Theme": {
-        "ColorSchemes": [
-          {
-            "UI Catalog Scheme": {
-              "Normal": {
-                "Foreground": "White",
-                "Background": "Green"
-              },
-              "Focus": {
-                "Foreground": "Green",
-                "Background": "White"
-              },
-              "HotNormal": {
-                "Foreground": "Blue",
-                "Background": "Green"
-              },
-              "HotFocus": {
-                "Foreground": "BrightRed",
-                "Background": "White"
-              },
-              "Disabled": {
-                "Foreground": "BrightGreen",
-                "Background": "Gray"
-              }
-            }
-          },
-          {
-            "TopLevel": {
-              "Normal": {
-                "Foreground": "DarkGray",
-                "Background": "White"
-              ...
-              }
-            }
-          }
-        ],
-        "Dialog.DefaultEffect3D": false
-      }
-    }
-  ]
-}
-```
 
 # Key Bindings
 
 Key bindings are defined in the `KeyBindings` property of the configuration file. The value is an array of objects, each object defining a key binding. The key binding object has the following properties:
 
-- `Key`: The key to bind to. The format is a string describing the key (e.g. "q", "Q,  "Ctrl-Q"). Function keys are specified as "F1", "F2", etc. 
+- `Key`: The key to bind to. The format is a string describing the key (e.g. "q", "Q,  "Ctrl+Q"). Function keys are specified as "F1", "F2", etc. 
 
 # Configuration File Schema
 
-Settings are defined in JSON format, according to the schema found here: 
+Settings are defined in JSON format, according to the schema found here:
 
 https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json
+
+## Schema
+
+[!code-json[tui-config-schema.json](../schemas/tui-config-schema.json)]
+
+# The Default Config File
+
+To illustrate the syntax, the below is the `config.json` file found in `Terminal.Gui.dll`:
+
+[!code-json[config.json](../../Terminal.Gui/Resources/config.json)]

二进制
local_packages/Terminal.Gui.2.0.0.nupkg


二进制
local_packages/Terminal.Gui.2.0.0.snupkg