lightweight variant (#1)
Reviewed-on: #1 Co-authored-by: Aleksandr Neychev <alexnejchev73@gmail.com>
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
using System.Drawing;
|
||||
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>
|
||||
/// The colours are <see cref="System.Drawing.Color"/> rather than
|
||||
/// <c>System.Windows.Media.Color</c>. The former lives in System.Drawing.Primitives,
|
||||
/// which is part of the base runtime and brings neither WPF nor GDI+ along; the latter
|
||||
/// is WindowsBase, and this type is read by the agent, which must stay clear of it.
|
||||
/// The settings window turns them into brushes in its converters.
|
||||
///
|
||||
/// 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;
|
||||
|
||||
// The side and the offset are stored per mode: the cursor and the caret call for
|
||||
// different settings, and switching the mode does not reset them
|
||||
|
||||
/// <summary>The side of the cursor the popup is put on.</summary>
|
||||
[ObservableProperty]
|
||||
private AnchorSide _cursorSide = AnchorSide.BottomRight;
|
||||
|
||||
/// <summary>The offset from the cursor in WPF units.</summary>
|
||||
[ObservableProperty]
|
||||
private double _cursorOffset = 16;
|
||||
|
||||
/// <summary>The side of the caret the popup is put on.</summary>
|
||||
[ObservableProperty]
|
||||
private AnchorSide _caretSide = AnchorSide.BottomRight;
|
||||
|
||||
/// <summary>The offset from the caret in WPF units.</summary>
|
||||
[ObservableProperty]
|
||||
private double _caretOffset = 16;
|
||||
|
||||
/// <summary>
|
||||
/// The place on the monitor for the <see cref="PopupPlacementMode.FixedPoint"/> mode.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private ScreenPosition _screenPosition = ScreenPosition.BottomRight;
|
||||
|
||||
/// <summary>The offset from the monitor edge in WPF units.</summary>
|
||||
[ObservableProperty]
|
||||
private double _screenMargin = 24;
|
||||
|
||||
/// <summary>The size of the layout name in the popup, in WPF units.</summary>
|
||||
[ObservableProperty]
|
||||
private double _fontSize = 20;
|
||||
|
||||
/// <summary>The popup opacity: 1.0 is fully opaque.</summary>
|
||||
[ObservableProperty]
|
||||
private double _opacity = 0.9;
|
||||
|
||||
/// <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;
|
||||
|
||||
/// <summary>
|
||||
/// Ask the repository about new versions on startup. A check can always be
|
||||
/// started by hand — this setting turns off only the automatic one.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private bool _checkForUpdates = true;
|
||||
|
||||
/// <summary>
|
||||
/// When the application last asked about new versions successfully.
|
||||
/// Stored so as not to go to the network on every startup.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private DateTimeOffset? _lastUpdateCheck;
|
||||
|
||||
/// <summary>The fill of the popup. The opacity is set by <see cref="Opacity"/>.</summary>
|
||||
[ObservableProperty]
|
||||
private Color _backgroundColor = Color.FromArgb(0xFF, 0x20, 0x20, 0x20);
|
||||
|
||||
/// <summary>The colour of the layout name in the popup.</summary>
|
||||
[ObservableProperty]
|
||||
private Color _foregroundColor = Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF);
|
||||
|
||||
/// <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.
|
||||
/// </remarks>
|
||||
public void CopyFrom(AppSettings other)
|
||||
{
|
||||
Language = other.Language;
|
||||
Theme = other.Theme;
|
||||
PlacementMode = other.PlacementMode;
|
||||
CursorSide = other.CursorSide;
|
||||
CursorOffset = other.CursorOffset;
|
||||
CaretSide = other.CaretSide;
|
||||
CaretOffset = other.CaretOffset;
|
||||
ScreenPosition = other.ScreenPosition;
|
||||
ScreenMargin = other.ScreenMargin;
|
||||
FontSize = other.FontSize;
|
||||
Opacity = other.Opacity;
|
||||
DurationMilliseconds = other.DurationMilliseconds;
|
||||
UseCapsLockHotkey = other.UseCapsLockHotkey;
|
||||
CapsLockHoldMilliseconds = other.CapsLockHoldMilliseconds;
|
||||
CheckForUpdates = other.CheckForUpdates;
|
||||
LastUpdateCheck = other.LastUpdateCheck;
|
||||
BackgroundColor = other.BackgroundColor;
|
||||
ForegroundColor = other.ForegroundColor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace CursorLang.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// The look of the settings window. By default the application follows the Windows
|
||||
/// theme, but the user can pin the light or the dark one.
|
||||
/// </summary>
|
||||
public enum AppTheme
|
||||
{
|
||||
System,
|
||||
Light,
|
||||
Dark
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace CursorLang.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A keyboard layout in a form convenient for display.
|
||||
/// </summary>
|
||||
/// <param name="LocaleId">The locale identifier (the low word of HKL).</param>
|
||||
/// <param name="ShortName">A short name for the popup at the cursor, "RU" for instance.</param>
|
||||
/// <param name="DisplayName">The full name, "RU — русский (Россия)" for instance.</param>
|
||||
public sealed record KeyboardLayout(int LocaleId, string ShortName, string DisplayName)
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds the model from a locale identifier. Unknown locales are not an
|
||||
/// error: for them we show the identifier itself.
|
||||
/// </summary>
|
||||
public static KeyboardLayout FromLocaleId(int localeId)
|
||||
{
|
||||
CultureInfo? culture = TryGetCulture(localeId);
|
||||
if (culture is null)
|
||||
{
|
||||
string fallback = $"0x{localeId:X4}";
|
||||
return new KeyboardLayout(localeId, fallback, fallback);
|
||||
}
|
||||
|
||||
string shortName = culture.TwoLetterISOLanguageName.ToUpperInvariant();
|
||||
return new KeyboardLayout(localeId, shortName, $"{shortName} — {culture.NativeName}");
|
||||
}
|
||||
|
||||
private static CultureInfo? TryGetCulture(int localeId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new CultureInfo(localeId);
|
||||
}
|
||||
catch (CultureNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace CursorLang.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Why the current layout has changed.
|
||||
/// </summary>
|
||||
public enum LayoutChangeReason
|
||||
{
|
||||
/// <summary>The user switched the layout in the active application.</summary>
|
||||
UserSwitched,
|
||||
|
||||
/// <summary>The user moved to another application that has a layout of its own.</summary>
|
||||
ApplicationSwitched,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The data of a layout change event.
|
||||
/// </summary>
|
||||
public sealed class LayoutChangedEventArgs(KeyboardLayout layout, LayoutChangeReason reason) : EventArgs
|
||||
{
|
||||
public KeyboardLayout Layout { get; } = layout;
|
||||
|
||||
public LayoutChangeReason Reason { get; } = reason;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
namespace CursorLang.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// How the place for the popup is chosen.
|
||||
/// </summary>
|
||||
public enum PopupPlacementMode
|
||||
{
|
||||
/// <summary>Next to the mouse cursor.</summary>
|
||||
AtCursor,
|
||||
|
||||
/// <summary>
|
||||
/// Next to the caret in the active input field. When the application does not
|
||||
/// report its position, the popup is shown at the mouse cursor.
|
||||
/// </summary>
|
||||
AtCaret,
|
||||
|
||||
/// <summary>At a fixed point of the monitor holding the active window.</summary>
|
||||
FixedPoint,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Which side of the cursor or the caret to show the popup on.
|
||||
/// </summary>
|
||||
public enum AnchorSide
|
||||
{
|
||||
TopLeft,
|
||||
TopRight,
|
||||
Left,
|
||||
Right,
|
||||
BottomLeft,
|
||||
BottomRight,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The place on the monitor for the <see cref="PopupPlacementMode.FixedPoint"/> mode.
|
||||
/// </summary>
|
||||
public enum ScreenPosition
|
||||
{
|
||||
TopLeft,
|
||||
Top,
|
||||
TopRight,
|
||||
Center,
|
||||
BottomLeft,
|
||||
Bottom,
|
||||
BottomRight,
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace CursorLang.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A file attached to a release.
|
||||
/// </summary>
|
||||
/// <param name="FileName">The name the file is saved to disk under.</param>
|
||||
/// <param name="Url">A direct link to the content.</param>
|
||||
/// <param name="Size">The size in bytes; zero when the hosting did not report it.</param>
|
||||
public sealed record ReleaseAsset(string FileName, Uri Url, long Size);
|
||||
|
||||
/// <summary>
|
||||
/// A release found in the repository.
|
||||
/// </summary>
|
||||
/// <param name="Version">The version parsed from the tag.</param>
|
||||
/// <param name="Tag">The tag as is — that is what the interface shows.</param>
|
||||
/// <param name="PageUrl">The release page: the release notes live there too.</param>
|
||||
/// <param name="Package">The MSIX package the application updates itself with.</param>
|
||||
public sealed record ReleaseInfo(Version Version, string Tag, Uri? PageUrl, ReleaseAsset Package);
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace CursorLang.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// The state of startup. Follows <c>StartupTaskState</c> of Windows: the user and
|
||||
/// the administrator each have their own way of forbidding startup, and the app has
|
||||
/// to tell them apart so as not to promise what it cannot do.
|
||||
/// </summary>
|
||||
public enum StartupState
|
||||
{
|
||||
/// <summary>Windows will not answer about startup — the setting is not shown.</summary>
|
||||
Unavailable,
|
||||
|
||||
/// <summary>Off, and the app can switch it on.</summary>
|
||||
Disabled,
|
||||
|
||||
/// <summary>On.</summary>
|
||||
Enabled,
|
||||
|
||||
/// <summary>
|
||||
/// Switched off by the user in the settings of Windows. It goes back on only
|
||||
/// there: a ban by the user is not for the app to overrule.
|
||||
/// </summary>
|
||||
DisabledByUser,
|
||||
|
||||
/// <summary>Forbidden by the policy of the organisation.</summary>
|
||||
DisabledByPolicy,
|
||||
|
||||
/// <summary>Switched on by the policy of the organisation and not to be switched off.</summary>
|
||||
EnabledByPolicy,
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace CursorLang.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Which step the update is at. One value — one state of the interface:
|
||||
/// the caption, the button and the progress bar are shown from it.
|
||||
/// </summary>
|
||||
public enum UpdateStatus
|
||||
{
|
||||
/// <summary>There have been no checks yet.</summary>
|
||||
Idle,
|
||||
|
||||
/// <summary>A request to the repository is in flight.</summary>
|
||||
Checking,
|
||||
|
||||
/// <summary>The latest version is installed.</summary>
|
||||
UpToDate,
|
||||
|
||||
/// <summary>There is a newer version — it can be downloaded.</summary>
|
||||
Available,
|
||||
|
||||
/// <summary>The package is downloading.</summary>
|
||||
Downloading,
|
||||
|
||||
/// <summary>The package is downloaded and ready to install.</summary>
|
||||
Ready,
|
||||
|
||||
/// <summary>The check or the download failed.</summary>
|
||||
Failed,
|
||||
}
|
||||
Reference in New Issue
Block a user