Преглед на файлове

Merge pull request #3843 from tig/v2_3841-ConfigManager

Fixes #3841 - `ConfigurationManager` not loading correctly
Tig преди 8 месеца
родител
ревизия
2a30e9e21a
променени са 35 файла, в които са добавени 700 реда и са изтрити 247 реда
  1. 3 0
      Example/Example.cs
  2. 3 3
      Terminal.Gui/Application/Application.Initialization.cs
  3. 9 1
      Terminal.Gui/Application/Application.Keyboard.cs
  4. 1 0
      Terminal.Gui/Application/Application.Run.cs
  5. 57 0
      Terminal.Gui/Configuration/ConfigLocations.cs
  6. 31 27
      Terminal.Gui/Configuration/ConfigProperty.cs
  7. 69 69
      Terminal.Gui/Configuration/ConfigurationManager.cs
  8. 6 1
      Terminal.Gui/Configuration/SettingsScope.cs
  9. 3 1
      Terminal.Gui/Configuration/ThemeManager.cs
  10. 11 4
      Terminal.Gui/Input/Key.cs
  11. 16 4
      Terminal.Gui/Input/KeyBindings.cs
  12. 35 0
      Terminal.Gui/Input/KeyEqualityComparer.cs
  13. 49 8
      UnitTests/Application/ApplicationTests.cs
  14. 1 1
      UnitTests/Configuration/AppScopeTests.cs
  15. 174 0
      UnitTests/Configuration/ConfigPropertyTests.cs
  16. 109 28
      UnitTests/Configuration/ConfigurationMangerTests.cs
  17. 14 0
      UnitTests/Configuration/KeyJsonConverterTests.cs
  18. 37 3
      UnitTests/Configuration/SettingsScopeTests.cs
  19. 4 4
      UnitTests/Configuration/ThemeScopeTests.cs
  20. 2 2
      UnitTests/Configuration/ThemeTests.cs
  21. 13 0
      UnitTests/Input/KeyBindingTests.cs
  22. 4 0
      UnitTests/Input/KeyTests.cs
  23. 2 2
      UnitTests/TestHelpers.cs
  24. 8 8
      UnitTests/UICatalog/ScenarioTests.cs
  25. 2 0
      UnitTests/View/Draw/AllViewsDrawTests.cs
  26. 1 1
      UnitTests/View/ViewTests.cs
  27. 1 1
      UnitTests/Views/ComboBoxTests.cs
  28. 2 2
      UnitTests/Views/MenuBarTests.cs
  29. 1 1
      UnitTests/Views/TabViewTests.cs
  30. 9 9
      UnitTests/Views/TableViewTests.cs
  31. 2 2
      UnitTests/Views/TextViewTests.cs
  32. 1 1
      UnitTests/Views/TreeTableSourceTests.cs
  33. 20 64
      docfx/docs/config.md
  34. BIN
      local_packages/Terminal.Gui.2.0.0.nupkg
  35. BIN
      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 configuraiton 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

+ 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>
+    ///     Global settings in the current directory (e.g. <c>./.tui/config.json</c>).
+    /// </summary>
+    GlobalCurrent = 0b_0000_0010,
+
+    /// <summary>
+    ///    Global settings in the home directory (e.g. <c>~/.tui/config.json</c>).
+    /// </summary>
+    GlobalHome = 0b_0000_0100,
+
+    /// <summary>
+    ///     App resources (e.g. <c>MyApp.Resources.config.json</c>).
+    /// </summary>
+    AppResources = 0b_0000_1000,
+
+    /// <summary>
+    ///     App settings in the current directory (e.g. <c>./.tui/MyApp.config.json</c>).
+    /// </summary>
+    AppCurrent = 0b_0001_0000,
+
+    /// <summary>
+    ///     App settings in the home directory (e.g. <c>~/.tui/MyApp.config.json</c>).
+    /// </summary>
+    AppHome = 0b_0010_0000,
+
+    /// <summary>
+    ///     Settings in the <see cref="ConfigurationManager.RuntimeConfig"/> static property.
+    /// </summary>
+    Runtime = 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)

+ 69 - 69
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,17 @@ public static class ConfigurationManager
             Reset ();
         }
 
-        // LibraryResources is always loaded by Reset
-        if (Locations == ConfigLocations.All)
+        if (Locations.HasFlag (ConfigLocations.GlobalCurrent))
+        {
+            Settings?.Update ($"./.tui/{_configFilename}");
+        }
+
+        if (Locations.HasFlag (ConfigLocations.GlobalHome))
+        {
+            Settings?.Update ($"~/.tui/{_configFilename}");
+        }
+
+        if (Locations.HasFlag (ConfigLocations.AppResources))
         {
             string? embeddedStylesResourceName = Assembly.GetEntryAssembly ()
                                                          ?
@@ -276,27 +272,25 @@ public static class ConfigurationManager
                 embeddedStylesResourceName = _configFilename;
             }
 
-            Settings = Settings?
-
-                       // Global current directory
-                       .Update ($"./.tui/{_configFilename}")
-                       ?
-
-                       // Global home directory
-                       .Update ($"~/.tui/{_configFilename}")
-                       ?
+            Settings?.UpdateFromResource (Assembly.GetEntryAssembly ()!, embeddedStylesResourceName!);
+        }
 
-                       // App resources
-                       .UpdateFromResource (Assembly.GetEntryAssembly ()!, embeddedStylesResourceName!)
-                       ?
+        if (Locations.HasFlag (ConfigLocations.AppCurrent))
+        {
+            Settings?.Update ($"./.tui/{AppName}.{_configFilename}");
+        }
 
-                       // App current directory
-                       .Update ($"./.tui/{AppName}.{_configFilename}")
-                       ?
+        if (Locations.HasFlag (ConfigLocations.AppHome))
+        {
+            Settings?.Update ($"~/.tui/{AppName}.{_configFilename}");
+        }
 
-                       // App home directory
-                       .Update ($"~/.tui/{AppName}.{_configFilename}");
+        if (Locations.HasFlag (ConfigLocations.Runtime) && !string.IsNullOrEmpty (RuntimeConfig))
+        {
+            Settings?.Update (RuntimeConfig, "ConfigurationManager.Memory");
         }
+
+        ThemeManager.SelectedTheme = Settings!["Theme"].PropertyValue as string ?? "Default";
     }
 
     /// <summary>
@@ -314,12 +308,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,7 +354,7 @@ 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,
@@ -367,12 +362,14 @@ public static class ConfigurationManager
                                         );
         }
 
+        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 +411,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 +428,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 +477,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 +553,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 +581,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."
                               );
                 }
             }

+ 6 - 1
Terminal.Gui/Configuration/SettingsScope.cs

@@ -117,8 +117,13 @@ public class SettingsScope : Scope<SettingsScope>
     /// <param name="source">The source (filename/resource name) the Json document was read from.</param>
     [RequiresUnreferencedCode ("AOT")]
     [RequiresDynamicCode ("AOT")]
-    public SettingsScope? Update (string json, string source)
+    public SettingsScope? Update (string? json, string source)
     {
+        //if (string.IsNullOrEmpty (json))
+        //{
+        //    Debug.WriteLine ($"ConfigurationManager: Configuration file \"{source}\" is empty.");
+        //    return this;
+        //}
         var stream = new MemoryStream ();
         var writer = new StreamWriter (stream);
         writer.Write (json);

+ 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 ();
+    }
+}

+ 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;
+    }
+}

+ 109 - 28
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,46 +147,93 @@ 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;
 
-        void ConfigurationManager_Updated (object sender, ConfigurationManagerEventArgs obj)
+        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);
@@ -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 ()

+ 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");
+
+        // 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.Runtime - Settings stored in the @Terminal.Gui.ConfigurationManager.RuntimeConfig static property --- Hightest precedence.
 
-2. App-specific settings in the directory the app was launched from (`./.tui/appname.config.json`).
+2. @Terminal.Gui.ConfigLocations.AppHome - App-specific settings in the users's home directory (`~/.tui/appname.config.json`). 
 
-3. App settings in app resources (`Resources/config.json`).
+3. @Terminal.Gui.ConfigLocations.AppCurrent - App-specific settings in the directory the app was launched from (`./.tui/appname.config.json`).
 
-4. Global settings in the the user's home directory (`~/.tui/config.json`).
+4. @Terminal.Gui.ConfigLocations.AppResources - App settings in app resources (`Resources/config.json`).
 
-5. Global settings in the directory the app was launched from (`./.tui/config.json`).
+5. @Terminal.Gui.ConfigLocations.GlobalHome - Global settings in the the user's home directory (`~/.tui/config.json`).
 
-6. Default settings in the Terminal.Gui assembly -- Lowest precedence.
+6. @Terminal.Gui.ConfigLocations.GlobalCurrent - Global settings in the directory the app was launched from (`./.tui/config.json`).
+
+7. @Terminal.Gui.ConfigLocations.Default - Default settings in the Terminal.Gui assembly -- Lowest 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)]

BIN
local_packages/Terminal.Gui.2.0.0.nupkg


BIN
local_packages/Terminal.Gui.2.0.0.snupkg