using System.ComponentModel; using System.Drawing; using System.Windows.Data; using CommunityToolkit.Mvvm.ComponentModel; using CursorLang.Core.Models; using CursorLang.Core.Services; namespace CursorLang.Settings.ViewModels; /// /// An option in a list. The caption changes together with the language, while the /// object itself lives for as long as the window does: recreating the list items /// makes the ComboBox drop the selected value. /// public sealed class EnumOption : ObservableObject where T : struct, Enum { private string _display; public EnumOption(T value, string display) { Value = value; _display = display; } public T Value { get; } public string Display { get => _display; set => SetProperty(ref _display, value); } // Accessibility tools take the name of the list item from here public override string ToString() => Display; } /// /// The settings window. The values are edited right in , /// so the popup picks them up at once, without an "Apply" button. /// public sealed partial class SettingsViewModel : ObservableObject, IDisposable { private static readonly Color[] Palette = [ Color.FromArgb(0x20, 0x20, 0x20), Color.FromArgb(0x00, 0x00, 0x00), Color.FromArgb(0x1E, 0x3A, 0x8A), Color.FromArgb(0x0F, 0x76, 0x6E), Color.FromArgb(0x7C, 0x2D, 0x12), Color.FromArgb(0x86, 0x19, 0x8F), Color.FromArgb(0xB9, 0x1C, 0x1C), Color.FromArgb(0xF5, 0xF5, 0xF5), ]; private static readonly Color[] ForegroundPalette = [ Color.FromArgb(0xFF, 0xFF, 0xFF), Color.FromArgb(0xD4, 0xD4, 0xD4), Color.FromArgb(0x00, 0x00, 0x00), Color.FromArgb(0xFA, 0xCC, 0x15), Color.FromArgb(0x4A, 0xDE, 0x80), Color.FromArgb(0x60, 0xA5, 0xFA), Color.FromArgb(0xF9, 0x73, 0x16), Color.FromArgb(0xF8, 0x71, 0x71), ]; private readonly IStartupService _startup; private readonly Version _version; private StartupState _startupState = StartupState.Unavailable; public SettingsViewModel( AppSettings settings, ILocalizationService localization, IStartupService startup, Version version) { Settings = settings; Localization = localization; _startup = startup; _version = version; // The interface language is a setting like any other and is stored in the same place Localization.CurrentLanguage = settings.Language; Settings.PropertyChanged += OnSettingsChanged; Localization.PropertyChanged += OnLocalizationChanged; Themes = CreateOptions(); PlacementModes = CreateOptions(); AnchorSides = CreateOptions(); CaretSides = CreateOptions(); ScreenPositions = CreateOptions(); } public AppSettings Settings { get; } public ILocalizationService Localization { get; } /// /// The title of the window, with the version in it. /// /// /// The version is nowhere else in the interface: a package installed from the /// Store is updated by the Store, so the window has nothing to say about /// updates — but which version is running is still worth knowing, if only to /// name it in a bug report. /// /// Three numbers, not four: the fourth one is the revision the Store keeps /// for itself, and it is a zero in every package we build. The numbers are /// put together by hand rather than by ToString(3), which throws on a /// version that has fewer of them than asked for. /// public string Title => string.Format( Localization["SettingsTitle"], $"{_version.Major}.{_version.Minor}.{Math.Max(_version.Build, 0)}"); public IReadOnlyList BackgroundPalette { get; } = Palette; public IReadOnlyList TextPalette { get; } = ForegroundPalette; public IReadOnlyList> Themes { get; } public IReadOnlyList> PlacementModes { get; } public IReadOnlyList> AnchorSides { get; } /// The sides on offer next to the caret: there the popup only goes beside it. public IReadOnlyList> CaretSides { get; } public IReadOnlyList> ScreenPositions { get; } /// Whether to show the startup setting. public bool IsStartupAvailable => _startupState != StartupState.Unavailable; /// Startup is up to the application rather than to Windows. public bool CanChangeStartup => _startupState is StartupState.Enabled or StartupState.Disabled; /// It has to be explained why the setting does not give in. public bool IsStartupLocked => IsStartupAvailable && !CanChangeStartup; /// /// Start the application together with Windows. /// /// /// The value is not stored in the application settings: Windows knows it, and it /// may be changed behind our back — in the "Startup apps" section. So the property /// answers from the last known state of the task every time. /// public bool RunAtStartup { get => _startupState is StartupState.Enabled or StartupState.EnabledByPolicy; set { if (value == RunAtStartup) { return; } _ = ApplyStartupAsync(value); } } /// /// Asks Windows about the state of startup. Called after the window is shown: the /// answer has to be waited for, while the settings must open right away. /// public async Task InitializeAsync() => UpdateStartupState(await _startup.GetStateAsync()); public void Dispose() { Settings.PropertyChanged -= OnSettingsChanged; Localization.PropertyChanged -= OnLocalizationChanged; } private async Task ApplyStartupAsync(bool enabled) => UpdateStartupState(await _startup.SetEnabledAsync(enabled)); private void UpdateStartupState(StartupState state) { _startupState = state; // The change is reported even when the state is the same: the checkbox has // already been toggled, and only a re-read value can put it back OnPropertyChanged(nameof(RunAtStartup)); OnPropertyChanged(nameof(IsStartupAvailable)); OnPropertyChanged(nameof(CanChangeStartup)); OnPropertyChanged(nameof(IsStartupLocked)); } private void OnSettingsChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(AppSettings.Language)) { Localization.CurrentLanguage = Settings.Language; } } // The option captions come from the resources, so on a language change we update // only the text — the list items themselves stay the same private void OnLocalizationChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName != Binding.IndexerName) { return; } OnPropertyChanged(nameof(Title)); Translate(Themes); Translate(PlacementModes); Translate(AnchorSides); Translate(CaretSides); Translate(ScreenPositions); } private EnumOption[] CreateOptions() where T : struct, Enum => [.. Enum.GetValues().Select(value => new EnumOption(value, GetDisplayName(value)))]; private void Translate(IReadOnlyList> options) where T : struct, Enum { foreach (EnumOption option in options) { option.Display = GetDisplayName(option.Value); } } // The resource key is built from the type name and the value: PopupPlacementMode_AtCursor private string GetDisplayName(T value) where T : struct, Enum => Localization[$"{typeof(T).Name}_{value}"]; }