changed settings saving (#3)
Reviewed-on: #3 Co-authored-by: Aleksandr Neychev <alexnejchev73@gmail.com>
This commit was merged in pull request #3.
This commit is contained in:
@@ -16,18 +16,115 @@ public sealed class AppSettingsTests
|
||||
Assert.Equal("en", settings.Language);
|
||||
Assert.Equal(AppTheme.System, settings.Theme);
|
||||
Assert.Equal(PopupPlacementMode.AtCursor, settings.PlacementMode);
|
||||
Assert.Equal(AnchorSide.BottomRight, settings.CursorSide);
|
||||
Assert.Equal(16, settings.CursorOffset);
|
||||
Assert.Equal(AnchorSide.BottomRight, settings.CaretSide);
|
||||
Assert.Equal(16, settings.CaretOffset);
|
||||
Assert.Equal(ScreenPosition.BottomRight, settings.ScreenPosition);
|
||||
Assert.Equal(24, settings.ScreenMargin);
|
||||
Assert.Equal(20, settings.FontSize);
|
||||
Assert.Equal(0.9, settings.Opacity);
|
||||
Assert.Equal(500, settings.DurationMilliseconds);
|
||||
Assert.Equal(300, settings.CapsLockHoldMilliseconds);
|
||||
Assert.Equal(Color.FromArgb(0x20, 0x20, 0x20), settings.BackgroundColor);
|
||||
Assert.Equal(Color.FromArgb(0xFF, 0xFF, 0xFF), settings.ForegroundColor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Every_mode_starts_out_looking_the_same()
|
||||
{
|
||||
var settings = new AppSettings();
|
||||
|
||||
Assert.Equal(AnchorSide.BottomRight, settings.AtCursor.Side);
|
||||
Assert.Equal(16, settings.AtCursor.Offset);
|
||||
Assert.Equal(CaretSide.Right, settings.AtCaret.Side);
|
||||
Assert.Equal(16, settings.AtCaret.Offset);
|
||||
|
||||
// The middle of the monitor, where there is no edge to stand off from
|
||||
Assert.Equal(ScreenPosition.Center, settings.FixedPoint.Position);
|
||||
Assert.Equal(0, settings.FixedPoint.Offset);
|
||||
|
||||
PopupModeSettings[] modes = [settings.AtCursor, settings.AtCaret, settings.FixedPoint];
|
||||
|
||||
Assert.All(modes, mode =>
|
||||
{
|
||||
Assert.Equal(20, mode.FontSize);
|
||||
Assert.Equal(0.9, mode.Opacity);
|
||||
Assert.Equal(Color.FromArgb(0x20, 0x20, 0x20), mode.BackgroundColor);
|
||||
Assert.Equal(Color.FromArgb(0xFF, 0xFF, 0xFF), mode.ForegroundColor);
|
||||
});
|
||||
|
||||
// Three modes and three sets of settings: none of them is shared
|
||||
Assert.Equal(3, modes.Distinct().Count());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(PopupPlacementMode.AtCursor)]
|
||||
[InlineData(PopupPlacementMode.AtCaret)]
|
||||
[InlineData(PopupPlacementMode.FixedPoint)]
|
||||
public void The_chosen_mode_is_the_one_handed_out(PopupPlacementMode mode)
|
||||
{
|
||||
var settings = new AppSettings { PlacementMode = mode };
|
||||
|
||||
PopupModeSettings expected = mode switch
|
||||
{
|
||||
PopupPlacementMode.AtCaret => settings.AtCaret,
|
||||
PopupPlacementMode.FixedPoint => settings.FixedPoint,
|
||||
_ => settings.AtCursor,
|
||||
};
|
||||
|
||||
Assert.Same(expected, settings.Current);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The settings of a mode are its own: setting one up leaves the others alone.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the whole point of keeping them per mode. A look shared by the modes
|
||||
/// meant setting it up again after every switch, and switching modes to see what
|
||||
/// they do undid what had just been set up.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public void Setting_one_mode_up_leaves_the_others_where_they_were()
|
||||
{
|
||||
var settings = new AppSettings();
|
||||
|
||||
settings.AtCaret.FontSize = 12;
|
||||
settings.AtCaret.Side = CaretSide.Left;
|
||||
settings.AtCaret.BackgroundColor = Color.FromArgb(0x11, 0x22, 0x33);
|
||||
|
||||
Assert.Equal(20, settings.AtCursor.FontSize);
|
||||
Assert.Equal(AnchorSide.BottomRight, settings.AtCursor.Side);
|
||||
Assert.Equal(20, settings.FixedPoint.FontSize);
|
||||
Assert.Equal(Color.FromArgb(0x20, 0x20, 0x20), settings.FixedPoint.BackgroundColor);
|
||||
}
|
||||
|
||||
// The mode in force decides what the look means, so a switch of it is a change of
|
||||
// everything the window shows through the current mode
|
||||
[Fact]
|
||||
public void A_switch_of_the_mode_is_announced_as_a_change_of_the_current_one()
|
||||
{
|
||||
var settings = new AppSettings();
|
||||
List<string?> changed = [];
|
||||
settings.PropertyChanged += (_, e) => changed.Add(e.PropertyName);
|
||||
|
||||
settings.PlacementMode = PopupPlacementMode.FixedPoint;
|
||||
|
||||
Assert.Contains(nameof(AppSettings.PlacementMode), changed);
|
||||
Assert.Contains(nameof(AppSettings.Current), changed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A change inside a mode is passed on as a change of the settings.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The service that writes the file listens to the settings alone. Without this, a
|
||||
/// font size dragged in the window would never reach the disk.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public void A_change_inside_a_mode_is_announced_by_the_settings()
|
||||
{
|
||||
var settings = new AppSettings();
|
||||
List<string?> changed = [];
|
||||
settings.PropertyChanged += (_, e) => changed.Add(e.PropertyName);
|
||||
|
||||
settings.AtCaret.FontSize = 42;
|
||||
settings.FixedPoint.Position = ScreenPosition.Bottom;
|
||||
settings.AtCursor.Offset = 8;
|
||||
|
||||
Assert.Contains("AtCaret.FontSize", changed);
|
||||
Assert.Contains("FixedPoint.Position", changed);
|
||||
Assert.Contains("AtCursor.Offset", changed);
|
||||
}
|
||||
|
||||
// The app must not change how the system behaves until it is asked to
|
||||
@@ -82,8 +179,60 @@ public sealed class AppSettingsTests
|
||||
Assert.Empty(changed);
|
||||
}
|
||||
|
||||
// Duration and CapsLockHoldDelay are derived from other settings and have
|
||||
// no business being in the file
|
||||
/// <summary>
|
||||
/// Pouring another instance in fills the modes in place rather than replacing them.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the agent's side of the connection: the fresh values arrive as a freshly
|
||||
/// parsed instance. Replacing the mode objects would leave the popup bound to the
|
||||
/// old ones.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public void Taking_other_settings_over_fills_the_modes_that_are_already_there()
|
||||
{
|
||||
var settings = new AppSettings();
|
||||
CursorModeSettings cursor = settings.AtCursor;
|
||||
|
||||
var other = new AppSettings
|
||||
{
|
||||
Language = "ru",
|
||||
Theme = AppTheme.Dark,
|
||||
PlacementMode = PopupPlacementMode.FixedPoint,
|
||||
DurationMilliseconds = 900,
|
||||
UseCapsLockHotkey = true,
|
||||
CapsLockHoldMilliseconds = 450,
|
||||
};
|
||||
|
||||
other.AtCursor.Side = AnchorSide.TopLeft;
|
||||
other.AtCursor.Offset = 5;
|
||||
other.AtCursor.FontSize = 11;
|
||||
other.AtCaret.Side = CaretSide.Left;
|
||||
other.AtCaret.ForegroundColor = Color.FromArgb(0x0A, 0x0B, 0x0C);
|
||||
other.FixedPoint.Position = ScreenPosition.Top;
|
||||
other.FixedPoint.Offset = 64;
|
||||
|
||||
settings.CopyFrom(other);
|
||||
|
||||
Assert.Same(cursor, settings.AtCursor);
|
||||
|
||||
Assert.Equal("ru", settings.Language);
|
||||
Assert.Equal(AppTheme.Dark, settings.Theme);
|
||||
Assert.Equal(PopupPlacementMode.FixedPoint, settings.PlacementMode);
|
||||
Assert.Equal(900, settings.DurationMilliseconds);
|
||||
Assert.True(settings.UseCapsLockHotkey);
|
||||
Assert.Equal(450, settings.CapsLockHoldMilliseconds);
|
||||
|
||||
Assert.Equal(AnchorSide.TopLeft, settings.AtCursor.Side);
|
||||
Assert.Equal(5, settings.AtCursor.Offset);
|
||||
Assert.Equal(11, settings.AtCursor.FontSize);
|
||||
Assert.Equal(CaretSide.Left, settings.AtCaret.Side);
|
||||
Assert.Equal(Color.FromArgb(0x0A, 0x0B, 0x0C), settings.AtCaret.ForegroundColor);
|
||||
Assert.Equal(ScreenPosition.Top, settings.FixedPoint.Position);
|
||||
Assert.Equal(64, settings.FixedPoint.Offset);
|
||||
}
|
||||
|
||||
// Derived values and the modes handed out for convenience are computed from what
|
||||
// is stored and have no business being in the file themselves
|
||||
[Fact]
|
||||
public void Derived_values_stay_out_of_the_file()
|
||||
{
|
||||
@@ -93,10 +242,14 @@ public sealed class AppSettingsTests
|
||||
|
||||
Assert.DoesNotContain(nameof(AppSettings.Duration), names);
|
||||
Assert.DoesNotContain(nameof(AppSettings.CapsLockHoldDelay), names);
|
||||
Assert.DoesNotContain(nameof(AppSettings.Current), names);
|
||||
|
||||
// What they are derived from, on the other hand, has to be stored
|
||||
Assert.Contains(nameof(AppSettings.DurationMilliseconds), names);
|
||||
Assert.Contains(nameof(AppSettings.CapsLockHoldMilliseconds), names);
|
||||
Assert.Contains(nameof(AppSettings.AtCursor), names);
|
||||
Assert.Contains(nameof(AppSettings.AtCaret), names);
|
||||
Assert.Contains(nameof(AppSettings.FixedPoint), names);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -118,6 +271,11 @@ public sealed class AppSettingsTests
|
||||
}
|
||||
|
||||
/// <summary>Names of the settings the user is able to change.</summary>
|
||||
/// <remarks>
|
||||
/// The modes are handed out rather than assigned — they are filled in place — so
|
||||
/// they are not among these. What is inside them is checked by
|
||||
/// <see cref="PopupModeSettingsTests"/>.
|
||||
/// </remarks>
|
||||
internal static IEnumerable<string> WritablePropertyNames() =>
|
||||
typeof(AppSettings).GetProperties()
|
||||
.Where(property => property.CanWrite)
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using CursorLang.Core.Models;
|
||||
|
||||
namespace CursorLang.Core.Tests.Models;
|
||||
|
||||
/// <summary>
|
||||
/// The settings of a single placement mode: what the popup looks like there and how
|
||||
/// far from its anchor it sits.
|
||||
/// </summary>
|
||||
public sealed class PopupModeSettingsTests
|
||||
{
|
||||
[Fact]
|
||||
public void The_cursor_mode_starts_out_below_and_right_of_the_pointer()
|
||||
{
|
||||
var mode = new CursorModeSettings();
|
||||
|
||||
Assert.Equal(AnchorSide.BottomRight, mode.Side);
|
||||
Assert.Equal(16, mode.Offset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Next to the caret the popup goes beside it, and to the right by default.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Above or below the caret is where the next line of the text is, so those sides
|
||||
/// are not on offer at all — the type has the two of them and no more.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public void The_caret_mode_starts_out_to_the_right_of_the_caret()
|
||||
{
|
||||
var mode = new CaretModeSettings();
|
||||
|
||||
Assert.Equal(CaretSide.Right, mode.Side);
|
||||
Assert.Equal(16, mode.Offset);
|
||||
Assert.Equal([CaretSide.Left, CaretSide.Right], Enum.GetValues<CaretSide>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(CaretSide.Left, AnchorSide.Left)]
|
||||
[InlineData(CaretSide.Right, AnchorSide.Right)]
|
||||
public void The_side_of_the_caret_lines_the_popup_up_with_it(CaretSide side, AnchorSide expected)
|
||||
{
|
||||
var mode = new CaretModeSettings { Side = side };
|
||||
|
||||
Assert.Equal(expected, mode.Anchor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_changed_side_of_the_caret_is_a_change_of_what_it_lines_up_with()
|
||||
{
|
||||
var mode = new CaretModeSettings();
|
||||
List<string?> changed = [];
|
||||
mode.PropertyChanged += (_, e) => changed.Add(e.PropertyName);
|
||||
|
||||
mode.Side = CaretSide.Left;
|
||||
|
||||
Assert.Contains(nameof(CaretModeSettings.Anchor), changed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The fixed point starts out in the middle of the monitor, where there is no edge
|
||||
/// to stand off from.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void The_fixed_point_starts_out_in_the_middle_with_no_offset()
|
||||
{
|
||||
var mode = new FixedPointModeSettings();
|
||||
|
||||
Assert.Equal(ScreenPosition.Center, mode.Position);
|
||||
Assert.Equal(0, mode.Offset);
|
||||
Assert.False(mode.IsAtAnEdge);
|
||||
}
|
||||
|
||||
// A setting that is not shown must not be one that still applies
|
||||
[Fact]
|
||||
public void Moving_the_fixed_point_to_the_middle_drops_the_offset()
|
||||
{
|
||||
var mode = new FixedPointModeSettings { Position = ScreenPosition.TopLeft, Offset = 40 };
|
||||
|
||||
Assert.True(mode.IsAtAnEdge);
|
||||
|
||||
mode.Position = ScreenPosition.Center;
|
||||
|
||||
Assert.Equal(0, mode.Offset);
|
||||
Assert.False(mode.IsAtAnEdge);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(ScreenPosition.TopLeft)]
|
||||
[InlineData(ScreenPosition.Top)]
|
||||
[InlineData(ScreenPosition.TopRight)]
|
||||
[InlineData(ScreenPosition.BottomLeft)]
|
||||
[InlineData(ScreenPosition.Bottom)]
|
||||
[InlineData(ScreenPosition.BottomRight)]
|
||||
public void Away_from_the_middle_the_offset_is_kept(ScreenPosition position)
|
||||
{
|
||||
var mode = new FixedPointModeSettings { Position = position, Offset = 40 };
|
||||
|
||||
Assert.Equal(40, mode.Offset);
|
||||
Assert.True(mode.IsAtAnEdge);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_changed_place_of_the_fixed_point_is_a_change_of_having_an_edge()
|
||||
{
|
||||
var mode = new FixedPointModeSettings();
|
||||
List<string?> changed = [];
|
||||
mode.PropertyChanged += (_, e) => changed.Add(e.PropertyName);
|
||||
|
||||
mode.Position = ScreenPosition.Bottom;
|
||||
|
||||
Assert.Contains(nameof(FixedPointModeSettings.IsAtAnEdge), changed);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(WritableProperties), typeof(CursorModeSettings))]
|
||||
[MemberData(nameof(WritableProperties), typeof(CaretModeSettings))]
|
||||
[MemberData(nameof(WritableProperties), typeof(FixedPointModeSettings))]
|
||||
public void A_changed_setting_is_announced_to_subscribers(Type type, string propertyName)
|
||||
{
|
||||
PopupModeSettings mode = Create(type);
|
||||
List<string?> changed = [];
|
||||
mode.PropertyChanged += (_, e) => changed.Add(e.PropertyName);
|
||||
|
||||
SetDifferentValue(mode, propertyName);
|
||||
|
||||
Assert.Contains(propertyName, changed);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(WritableProperties), typeof(CursorModeSettings))]
|
||||
[MemberData(nameof(WritableProperties), typeof(CaretModeSettings))]
|
||||
[MemberData(nameof(WritableProperties), typeof(FixedPointModeSettings))]
|
||||
public void Writing_the_same_value_leaves_subscribers_alone(Type type, string propertyName)
|
||||
{
|
||||
PopupModeSettings mode = Create(type);
|
||||
PropertyInfo property = type.GetProperty(propertyName)!;
|
||||
|
||||
List<string?> changed = [];
|
||||
mode.PropertyChanged += (_, e) => changed.Add(e.PropertyName);
|
||||
|
||||
property.SetValue(mode, property.GetValue(mode));
|
||||
|
||||
Assert.Empty(changed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_cursor_mode_takes_another_one_over_whole()
|
||||
{
|
||||
var mode = new CursorModeSettings();
|
||||
var other = new CursorModeSettings
|
||||
{
|
||||
Side = AnchorSide.Left,
|
||||
Offset = 3,
|
||||
FontSize = 41,
|
||||
Opacity = 0.25,
|
||||
BackgroundColor = Color.FromArgb(0x01, 0x02, 0x03),
|
||||
ForegroundColor = Color.FromArgb(0x04, 0x05, 0x06),
|
||||
};
|
||||
|
||||
mode.CopyFrom(other);
|
||||
|
||||
Assert.Equal(AnchorSide.Left, mode.Side);
|
||||
Assert.Equal(3, mode.Offset);
|
||||
Assert.Equal(41, mode.FontSize);
|
||||
Assert.Equal(0.25, mode.Opacity);
|
||||
Assert.Equal(Color.FromArgb(0x01, 0x02, 0x03), mode.BackgroundColor);
|
||||
Assert.Equal(Color.FromArgb(0x04, 0x05, 0x06), mode.ForegroundColor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_caret_mode_takes_another_one_over_whole()
|
||||
{
|
||||
var mode = new CaretModeSettings();
|
||||
var other = new CaretModeSettings { Side = CaretSide.Left, Offset = 2, FontSize = 15 };
|
||||
|
||||
mode.CopyFrom(other);
|
||||
|
||||
Assert.Equal(CaretSide.Left, mode.Side);
|
||||
Assert.Equal(2, mode.Offset);
|
||||
Assert.Equal(15, mode.FontSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_fixed_point_takes_another_one_over_whole()
|
||||
{
|
||||
var mode = new FixedPointModeSettings();
|
||||
var other = new FixedPointModeSettings
|
||||
{
|
||||
Position = ScreenPosition.Bottom,
|
||||
Offset = 120,
|
||||
FontSize = 60,
|
||||
Opacity = 0.5,
|
||||
BackgroundColor = Color.FromArgb(0x07, 0x08, 0x09),
|
||||
ForegroundColor = Color.FromArgb(0x0A, 0x0B, 0x0C),
|
||||
};
|
||||
|
||||
mode.CopyFrom(other);
|
||||
|
||||
Assert.Equal(ScreenPosition.Bottom, mode.Position);
|
||||
Assert.Equal(120, mode.Offset);
|
||||
Assert.Equal(60, mode.FontSize);
|
||||
Assert.Equal(0.5, mode.Opacity);
|
||||
Assert.Equal(Color.FromArgb(0x07, 0x08, 0x09), mode.BackgroundColor);
|
||||
Assert.Equal(Color.FromArgb(0x0A, 0x0B, 0x0C), mode.ForegroundColor);
|
||||
}
|
||||
|
||||
// Taking over a mode that sits in the middle takes its lack of an offset over too
|
||||
[Fact]
|
||||
public void Taking_over_a_fixed_point_in_the_middle_leaves_no_offset()
|
||||
{
|
||||
var mode = new FixedPointModeSettings { Position = ScreenPosition.Top, Offset = 32 };
|
||||
|
||||
mode.CopyFrom(new FixedPointModeSettings());
|
||||
|
||||
Assert.Equal(ScreenPosition.Center, mode.Position);
|
||||
Assert.Equal(0, mode.Offset);
|
||||
}
|
||||
|
||||
// What a mode works out from what it stores is not stored itself
|
||||
[Fact]
|
||||
public void Derived_values_of_a_mode_stay_out_of_the_file()
|
||||
{
|
||||
List<string> caret = Stored(new CaretModeSettings());
|
||||
List<string> fixedPoint = Stored(new FixedPointModeSettings());
|
||||
|
||||
Assert.DoesNotContain(nameof(CaretModeSettings.Anchor), caret);
|
||||
Assert.DoesNotContain(nameof(FixedPointModeSettings.IsAtAnEdge), fixedPoint);
|
||||
|
||||
Assert.Contains(nameof(CaretModeSettings.Side), caret);
|
||||
Assert.Contains(nameof(FixedPointModeSettings.Position), fixedPoint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_mode_reports_changes_as_INotifyPropertyChanged()
|
||||
{
|
||||
Assert.IsAssignableFrom<INotifyPropertyChanged>(new CursorModeSettings());
|
||||
Assert.IsAssignableFrom<INotifyPropertyChanged>(new CaretModeSettings());
|
||||
Assert.IsAssignableFrom<INotifyPropertyChanged>(new FixedPointModeSettings());
|
||||
}
|
||||
|
||||
public static TheoryData<Type, string> WritableProperties(Type type)
|
||||
{
|
||||
var data = new TheoryData<Type, string>();
|
||||
|
||||
foreach (string name in WritablePropertyNames(type))
|
||||
{
|
||||
data.Add(type, name);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>Names of the settings of a mode the user is able to change.</summary>
|
||||
internal static IEnumerable<string> WritablePropertyNames(Type type) =>
|
||||
type.GetProperties().Where(property => property.CanWrite).Select(property => property.Name);
|
||||
|
||||
private static PopupModeSettings Create(Type type) => (PopupModeSettings)Activator.CreateInstance(type)!;
|
||||
|
||||
private static List<string> Stored<TMode>(TMode mode) where TMode : PopupModeSettings
|
||||
{
|
||||
using JsonDocument document = JsonSerializer.SerializeToDocument(mode);
|
||||
|
||||
return [.. document.RootElement.EnumerateObject().Select(property => property.Name)];
|
||||
}
|
||||
|
||||
// A value guaranteed to differ from the current one: each kind of setting has its
|
||||
// own way of differing
|
||||
private static void SetDifferentValue(PopupModeSettings mode, string propertyName)
|
||||
{
|
||||
PropertyInfo property = mode.GetType().GetProperty(propertyName)!;
|
||||
|
||||
object next = property.GetValue(mode) switch
|
||||
{
|
||||
double number => number + 1,
|
||||
Color color => Color.FromArgb((byte)(color.R + 1), color.G, color.B),
|
||||
Enum value => Enum.GetValues(value.GetType())
|
||||
.Cast<object>()
|
||||
.First(other => !Equals(other, value)),
|
||||
|
||||
var other => throw new NotSupportedException($"Unknown kind of setting: {other?.GetType()}"),
|
||||
};
|
||||
|
||||
property.SetValue(mode, next);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user