146 lines
6.3 KiB
C#
146 lines
6.3 KiB
C#
using System.ComponentModel;
|
|
using System.Text.Json.Serialization;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
|
|
namespace CursorLang.Core.Models;
|
|
|
|
/// <summary>
|
|
/// The application settings, as the settings window writes them to settings.json.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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 <see cref="PopupModeSettings"/> 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 <see cref="CopyFrom"/>.
|
|
/// </remarks>
|
|
public sealed partial class AppSettings : ObservableObject
|
|
{
|
|
/// <summary>The interface language as a culture code: "ru", "en".</summary>
|
|
[ObservableProperty]
|
|
private string _language = "en";
|
|
|
|
/// <summary>The look of the settings window.</summary>
|
|
[ObservableProperty]
|
|
private AppTheme _theme = AppTheme.System;
|
|
|
|
/// <summary>Where the popup is shown: at the cursor, at the caret or at a fixed point.</summary>
|
|
[ObservableProperty]
|
|
private PopupPlacementMode _placementMode = PopupPlacementMode.AtCursor;
|
|
|
|
/// <summary>How long the popup stays on screen, in milliseconds.</summary>
|
|
[ObservableProperty]
|
|
private double _durationMilliseconds = 500;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[ObservableProperty]
|
|
private bool _useCapsLockHotkey;
|
|
|
|
/// <summary>
|
|
/// After how long a Caps Lock hold cancels the switch, in milliseconds.
|
|
/// </summary>
|
|
[ObservableProperty]
|
|
private double _capsLockHoldMilliseconds = 300;
|
|
|
|
public AppSettings()
|
|
{
|
|
AtCursor.PropertyChanged += OnModeChanged;
|
|
AtCaret.PropertyChanged += OnModeChanged;
|
|
FixedPoint.PropertyChanged += OnModeChanged;
|
|
}
|
|
|
|
/// <summary>The <see cref="PopupPlacementMode.AtCursor"/> mode.</summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
|
|
public CursorModeSettings AtCursor { get; } = new();
|
|
|
|
/// <summary>The <see cref="PopupPlacementMode.AtCaret"/> mode.</summary>
|
|
[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
|
|
public CaretModeSettings AtCaret { get; } = new();
|
|
|
|
/// <summary>The <see cref="PopupPlacementMode.FixedPoint"/> mode.</summary>
|
|
[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
|
|
public FixedPointModeSettings FixedPoint { get; } = new();
|
|
|
|
/// <summary>The settings of the mode currently chosen.</summary>
|
|
/// <remarks>
|
|
/// 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
|
|
/// <see cref="PlacementMode"/> announces this as changed, so the bindings follow.
|
|
/// </remarks>
|
|
[JsonIgnore]
|
|
public PopupModeSettings Current => PlacementMode switch
|
|
{
|
|
PopupPlacementMode.AtCaret => AtCaret,
|
|
PopupPlacementMode.FixedPoint => FixedPoint,
|
|
_ => AtCursor,
|
|
};
|
|
|
|
/// <summary><see cref="DurationMilliseconds"/> as a <see cref="TimeSpan"/>.</summary>
|
|
[JsonIgnore]
|
|
public TimeSpan Duration => TimeSpan.FromMilliseconds(DurationMilliseconds);
|
|
|
|
/// <summary><see cref="CapsLockHoldMilliseconds"/> as a <see cref="TimeSpan"/>.</summary>
|
|
[JsonIgnore]
|
|
public TimeSpan CapsLockHoldDelay => TimeSpan.FromMilliseconds(CapsLockHoldMilliseconds);
|
|
|
|
/// <summary>
|
|
/// Takes the values of another instance over, raising a change notification for
|
|
/// every property that has actually moved.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
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));
|
|
|
|
/// <summary>
|
|
/// Passes a change made inside a mode on as a change of the settings.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A mode is an object of its own, so an edit in it is not an edit of this one as
|
|
/// far as <see cref="INotifyPropertyChanged"/> 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".
|
|
/// </remarks>
|
|
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);
|
|
}
|