mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 15:57:56 +01:00
Merge branch 'v2_3841-ConfigManager' of tig:tig/Terminal.Gui into v2_3841-ConfigManager
This commit is contained in:
@@ -6,6 +6,12 @@
|
||||
using System;
|
||||
using Terminal.Gui;
|
||||
|
||||
ConfigurationManager.Memory = """
|
||||
{
|
||||
"Application.QuitKey" : "Ctrl+Q"
|
||||
}
|
||||
""";
|
||||
|
||||
Application.Run<ExampleWindow> ().Dispose ();
|
||||
|
||||
// Before the application exits, reset Terminal.Gui for clean shutdown
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
#nullable enable
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace Terminal.Gui;
|
||||
@@ -16,7 +17,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
|
||||
get => _quitKey;
|
||||
set
|
||||
{
|
||||
//if (_quitKey != value)
|
||||
if (_quitKey != value)
|
||||
{
|
||||
ReplaceKey (_quitKey, value);
|
||||
_quitKey = value;
|
||||
|
||||
@@ -35,40 +35,38 @@ public class ConfigProperty
|
||||
/// <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;
|
||||
@@ -95,8 +93,7 @@ public class ConfigProperty
|
||||
public object? RetrieveValue () { return PropertyValue = PropertyInfo!.GetValue (null); }
|
||||
|
||||
/// <summary>
|
||||
/// Updates (using reflection) <see cref="PropertyValue"/> with
|
||||
/// the value in <paramref name="source"/>.
|
||||
/// 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>
|
||||
|
||||
@@ -291,7 +291,7 @@ public static class ConfigurationManager
|
||||
Settings?.Update ($"~/.tui/{AppName}.{_configFilename}");
|
||||
}
|
||||
|
||||
if (Locations.HasFlag (ConfigLocations.Memory) && !string.IsNullOrEmpty(Memory))
|
||||
if (Locations.HasFlag (ConfigLocations.Memory) && !string.IsNullOrEmpty (Memory))
|
||||
{
|
||||
Settings?.Update (Memory, "ConfigurationManager.Memory");
|
||||
}
|
||||
@@ -415,7 +415,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;
|
||||
}
|
||||
@@ -426,9 +432,6 @@ public static class ConfigurationManager
|
||||
{
|
||||
foreach (object? srcKey in ((IDictionary)source).Keys)
|
||||
{
|
||||
if (srcKey is string)
|
||||
{ }
|
||||
|
||||
if (((IDictionary)destination).Contains (srcKey))
|
||||
{
|
||||
((IDictionary)destination) [srcKey] =
|
||||
@@ -478,9 +481,10 @@ public static class ConfigurationManager
|
||||
}
|
||||
}
|
||||
|
||||
return destination!;
|
||||
return destination;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the hard coded default settings (static properites) from the Terminal.Gui library implementation. Used in
|
||||
/// development of
|
||||
@@ -553,9 +557,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 =>
|
||||
|
||||
@@ -405,13 +405,13 @@ public class Key : EventArgs, IEquatable<Key>
|
||||
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!.Equals(b); }
|
||||
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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
35
Terminal.Gui/Input/KeyEqualityComparer.cs
Normal file
35
Terminal.Gui/Input/KeyEqualityComparer.cs
Normal file
@@ -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 ();
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ public class ApplicationTests
|
||||
{
|
||||
_output = output;
|
||||
ConsoleDriver.RunningUnitTests = true;
|
||||
ConfigurationManager.Locations = ConfigLocations.Default;
|
||||
Locations = ConfigLocations.Default;
|
||||
|
||||
#if DEBUG_IDISPOSABLE
|
||||
View.Instances.Clear ();
|
||||
@@ -273,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 ();
|
||||
@@ -371,7 +372,7 @@ public class ApplicationTests
|
||||
Application.ResetState ();
|
||||
CheckReset ();
|
||||
|
||||
ConfigurationManager.ThrowOnJsonErrors = false;
|
||||
ThrowOnJsonErrors = false;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -399,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))]
|
||||
@@ -545,11 +543,10 @@ public class ApplicationTests
|
||||
ThrowOnJsonErrors = true;
|
||||
|
||||
Memory = """
|
||||
|
||||
{
|
||||
"Application.QuitKey": "Ctrl-Q"
|
||||
}
|
||||
""";
|
||||
{
|
||||
"Application.QuitKey": "Ctrl-Q"
|
||||
}
|
||||
""";
|
||||
|
||||
Assert.Equal (Key.Esc, Application.QuitKey);
|
||||
|
||||
@@ -558,6 +555,8 @@ public class ApplicationTests
|
||||
|
||||
Assert.Equal (Key.Q.WithCtrl, Application.QuitKey);
|
||||
|
||||
Assert.Contains (Key.Q.WithCtrl, Application.KeyBindings.Bindings);
|
||||
|
||||
Application.Shutdown ();
|
||||
Locations = ConfigLocations.Default;
|
||||
}
|
||||
|
||||
174
UnitTests/Configuration/ConfigPropertyTests.cs
Normal file
174
UnitTests/Configuration/ConfigPropertyTests.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -146,6 +147,50 @@ public class ConfigurationManagerTests
|
||||
Assert.Equal (dictDest ["Normal"], dictCopy ["Normal"]);
|
||||
}
|
||||
|
||||
public class DeepCopyTest ()
|
||||
{
|
||||
public static Key key = Key.Esc;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Illustrate_DeepMemberWiseCopy_Breaks_Dictionary ()
|
||||
{
|
||||
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);
|
||||
|
||||
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 Illustrate_DeepMemberWiseCopy_ ()
|
||||
//{
|
||||
// Assert.Equal (Key.Esc, Application.QuitKey);
|
||||
|
||||
// var o = UpdateValueFrom (Application.QuitKey);
|
||||
// DeepMemberWiseCopy (Key.Q.WithCtrl, Application.QuitKey);
|
||||
|
||||
// Assert.Equal (Key.Q.WithCtrl, Application.QuitKey);
|
||||
|
||||
// Application.ResetState (true);
|
||||
//}
|
||||
|
||||
[Fact]
|
||||
public void Load_Raises_Updated ()
|
||||
{
|
||||
|
||||
@@ -59,7 +59,7 @@ public class KeyJsonConverterTests
|
||||
Key key = Key.Q.WithCtrl;
|
||||
|
||||
// Act
|
||||
string json = """Ctrl+Q""";
|
||||
string json = "\"Ctrl+Q\"";
|
||||
Key deserializedKey = JsonSerializer.Deserialize<Key> (json, ConfigurationManager._serializerOptions);
|
||||
|
||||
// Assert
|
||||
|
||||
Reference in New Issue
Block a user