using System.ComponentModel; using System.Text.Json.Serialization; using CommunityToolkit.Mvvm.ComponentModel; namespace CursorLang.Core.Models; /// /// The application settings, as the settings window writes them to settings.json. /// /// /// Everything about the popup — the side, the offset, the font size, the opacity and /// both colours — belongs to a placement mode rather than to the application, and is /// kept in a of its own for each of them. So the /// settings of a mode are still there after a trip through the other two. /// /// Both processes hold an instance of this, but only the settings window writes: the /// agent re-reads the file and pours the fresh values into the instance it already has, /// so everything subscribed to it stays subscribed. See . /// public sealed partial class AppSettings : ObservableObject { /// The interface language as a culture code: "ru", "en". [ObservableProperty] private string _language = "en"; /// The look of the settings window. [ObservableProperty] private AppTheme _theme = AppTheme.System; /// Where the popup is shown: at the cursor, at the caret or at a fixed point. [ObservableProperty] private PopupPlacementMode _placementMode = PopupPlacementMode.AtCursor; /// How long the popup stays on screen, in milliseconds. [ObservableProperty] private double _durationMilliseconds = 500; /// /// Intercept Caps Lock and switch the layout with it instead of changing the case. /// Off by default: the application must not change the behaviour of the system /// until it is asked to. /// [ObservableProperty] private bool _useCapsLockHotkey; /// /// After how long a Caps Lock hold cancels the switch, in milliseconds. /// [ObservableProperty] private double _capsLockHoldMilliseconds = 300; public AppSettings() { AtCursor.PropertyChanged += OnModeChanged; AtCaret.PropertyChanged += OnModeChanged; FixedPoint.PropertyChanged += OnModeChanged; } /// The mode. /// /// The modes are handed out rather than replaced — the instance is the same for the /// life of the settings, so a binding and a subscription to it hold. That is what /// the creation handling is for: without it the deserializer would want a setter, /// and reading the file would swap the object everything is bound to. /// [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] public CursorModeSettings AtCursor { get; } = new(); /// The mode. [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] public CaretModeSettings AtCaret { get; } = new(); /// The mode. [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] public FixedPointModeSettings FixedPoint { get; } = new(); /// The settings of the mode currently chosen. /// /// The settings window binds the look through here, and the popup asks for it here /// too: what is on screen is always the settings of the mode in force. A change of /// announces this as changed, so the bindings follow. /// [JsonIgnore] public PopupModeSettings Current => PlacementMode switch { PopupPlacementMode.AtCaret => AtCaret, PopupPlacementMode.FixedPoint => FixedPoint, _ => AtCursor, }; /// as a . [JsonIgnore] public TimeSpan Duration => TimeSpan.FromMilliseconds(DurationMilliseconds); /// as a . [JsonIgnore] public TimeSpan CapsLockHoldDelay => TimeSpan.FromMilliseconds(CapsLockHoldMilliseconds); /// /// Takes the values of another instance over, raising a change notification for /// every property that has actually moved. /// /// /// This is how the agent learns about an edit: the settings window is a separate /// process, so the fresh values arrive as a freshly parsed instance and are poured /// into the one everything is already bound to, rather than replacing it. The modes /// are filled the same way, and for the same reason. /// public void CopyFrom(AppSettings other) { Language = other.Language; Theme = other.Theme; PlacementMode = other.PlacementMode; DurationMilliseconds = other.DurationMilliseconds; UseCapsLockHotkey = other.UseCapsLockHotkey; CapsLockHoldMilliseconds = other.CapsLockHoldMilliseconds; AtCursor.CopyFrom(other.AtCursor); AtCaret.CopyFrom(other.AtCaret); FixedPoint.CopyFrom(other.FixedPoint); } // The mode in force decides what the look means, so a switch of the mode is a // change of everything bound through Current partial void OnPlacementModeChanged(PopupPlacementMode value) => OnPropertyChanged(nameof(Current)); /// /// Passes a change made inside a mode on as a change of the settings. /// /// /// A mode is an object of its own, so an edit in it is not an edit of this one as /// far as goes. The settings window binds /// through the path and hears the mode itself, but the service that writes the file /// listens to the settings alone — and without this a font size dragged in the /// window would never reach the disk. The name says where the change happened: /// "AtCaret.FontSize". /// private void OnModeChanged(object? sender, PropertyChangedEventArgs e) => OnPropertyChanged($"{NameOfMode(sender)}.{e.PropertyName}"); private string NameOfMode(object? mode) => ReferenceEquals(mode, AtCaret) ? nameof(AtCaret) : ReferenceEquals(mode, FixedPoint) ? nameof(FixedPoint) : nameof(AtCursor); }