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}"];
}