added multilang and settings
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>Язык интерфейса для выбора в настройках.</summary>
|
||||
/// <param name="Code">Код культуры: «ru», «en».</param>
|
||||
/// <param name="DisplayName">Название на самом этом языке.</param>
|
||||
public sealed record LanguageOption(string Code, string DisplayName);
|
||||
|
||||
/// <summary>
|
||||
/// Даёт строки интерфейса и умеет менять язык без перезапуска.
|
||||
/// </summary>
|
||||
public interface ILocalizationService : INotifyPropertyChanged
|
||||
{
|
||||
/// <summary>Строка по ключу ресурса. Привязки обновляются при смене языка.</summary>
|
||||
string this[string key] { get; }
|
||||
|
||||
IReadOnlyList<LanguageOption> AvailableLanguages { get; }
|
||||
|
||||
string CurrentLanguage { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using CursorLang.Models;
|
||||
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Связывает слежение за раскладкой с показом подсказки.
|
||||
/// Живёт всё время работы приложения независимо от открытых окон.
|
||||
/// </summary>
|
||||
public sealed class LayoutNotificationCoordinator : IDisposable
|
||||
{
|
||||
private readonly IKeyboardLayoutService _layoutService;
|
||||
private readonly ILayoutPopupService _popupService;
|
||||
|
||||
public LayoutNotificationCoordinator(IKeyboardLayoutService layoutService, ILayoutPopupService popupService)
|
||||
{
|
||||
_layoutService = layoutService;
|
||||
_popupService = popupService;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_layoutService.LayoutChanged += OnLayoutChanged;
|
||||
_layoutService.Start();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_layoutService.LayoutChanged -= OnLayoutChanged;
|
||||
_layoutService.Stop();
|
||||
}
|
||||
|
||||
private void OnLayoutChanged(object? sender, LayoutChangedEventArgs e)
|
||||
{
|
||||
// При переходе в другое приложение раскладка меняется без участия
|
||||
// пользователя, и всплывающая подсказка была бы навязчивой
|
||||
if (e.Reason == LayoutChangeReason.UserSwitched)
|
||||
{
|
||||
_popupService.Show(e.Layout);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,18 +5,6 @@ using CursorLang.Views;
|
||||
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Настройки подсказки у курсора.
|
||||
/// </summary>
|
||||
public sealed class PopupOptions
|
||||
{
|
||||
/// <summary>Сколько подсказка держится на экране после последнего переключения.</summary>
|
||||
public TimeSpan Duration { get; init; } = TimeSpan.FromMilliseconds(500);
|
||||
|
||||
/// <summary>Отступ от курсора в единицах WPF, чтобы подсказка не оказалась под ним.</summary>
|
||||
public double CursorOffset { get; init; } = 16;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Управляет временем жизни подсказки: окно отвечает только за показ,
|
||||
/// а решение «когда показать и когда убрать» принимается здесь.
|
||||
@@ -25,24 +13,27 @@ public sealed class LayoutPopupService : ILayoutPopupService, IDisposable
|
||||
{
|
||||
private readonly LayoutPopupWindow _window;
|
||||
private readonly LayoutPopupViewModel _viewModel;
|
||||
private readonly DispatcherTimer _hideTimer;
|
||||
private readonly AppSettings _settings;
|
||||
private readonly DispatcherTimer _hideTimer = new();
|
||||
|
||||
public LayoutPopupService(LayoutPopupWindow window, LayoutPopupViewModel viewModel, PopupOptions options)
|
||||
public LayoutPopupService(LayoutPopupWindow window, LayoutPopupViewModel viewModel, AppSettings settings)
|
||||
{
|
||||
_window = window;
|
||||
_viewModel = viewModel;
|
||||
_settings = settings;
|
||||
|
||||
_hideTimer = new DispatcherTimer { Interval = options.Duration };
|
||||
_hideTimer.Tick += OnHideTimerTick;
|
||||
}
|
||||
|
||||
public void Show(KeyboardLayout layout)
|
||||
{
|
||||
_viewModel.ShortName = layout.ShortName;
|
||||
_window.ShowAtCursor();
|
||||
_window.ShowPopup();
|
||||
|
||||
// Перезапускаем таймер, чтобы быстрые переключения продлевали показ
|
||||
// Длительность читаем при каждом показе: её меняют в настройках на лету.
|
||||
// Перезапуск таймера заодно продлевает показ при быстрых переключениях
|
||||
_hideTimer.Stop();
|
||||
_hideTimer.Interval = _settings.Duration;
|
||||
_hideTimer.Start();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Globalization;
|
||||
using System.Resources;
|
||||
using System.Windows.Data;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Берёт строки из ресурсов и при смене языка просит WPF перечитать все привязки.
|
||||
/// </summary>
|
||||
public sealed class LocalizationService : ObservableObject, ILocalizationService
|
||||
{
|
||||
private static readonly ResourceManager Resources =
|
||||
new("CursorLang.Resources.Strings", typeof(App).Assembly);
|
||||
|
||||
private CultureInfo _culture = CultureInfo.GetCultureInfo("en");
|
||||
|
||||
public string this[string key] => Resources.GetString(key, _culture) ?? key;
|
||||
|
||||
public IReadOnlyList<LanguageOption> AvailableLanguages { get; } =
|
||||
[
|
||||
new LanguageOption("en", "English"),
|
||||
new LanguageOption("ru", "Русский"),
|
||||
];
|
||||
|
||||
public string CurrentLanguage
|
||||
{
|
||||
get => _culture.TwoLetterISOLanguageName;
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value) || value == CurrentLanguage)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_culture = CultureInfo.GetCultureInfo(value);
|
||||
CultureInfo.CurrentUICulture = _culture;
|
||||
|
||||
OnPropertyChanged(nameof(CurrentLanguage));
|
||||
|
||||
// Сообщаем об изменении индексатора: так обновляются все привязки
|
||||
// вида {Binding Localization[Key]}, то есть весь текст интерфейса
|
||||
OnPropertyChanged(Binding.IndexerName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using CursorLang.Models;
|
||||
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Хранит настройки в %APPDATA%\CursorLang\settings.json.
|
||||
/// </summary>
|
||||
public sealed class SettingsService : IDisposable
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
Converters = { new ColorJsonConverter(), new JsonStringEnumConverter() },
|
||||
};
|
||||
|
||||
private readonly string _filePath;
|
||||
private readonly DispatcherTimer _saveTimer;
|
||||
private AppSettings? _settings;
|
||||
|
||||
public SettingsService()
|
||||
{
|
||||
string folder = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"CursorLang");
|
||||
_filePath = Path.Combine(folder, "settings.json");
|
||||
|
||||
// Ползунки меняют значения непрерывно, поэтому запись на диск
|
||||
// откладывается до паузы в изменениях
|
||||
_saveTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
|
||||
_saveTimer.Tick += OnSaveTimerTick;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Читает настройки с диска либо отдаёт значения по умолчанию,
|
||||
/// и дальше сам сохраняет любые изменения.
|
||||
/// </summary>
|
||||
public AppSettings Load()
|
||||
{
|
||||
_settings = ReadFile() ?? CreateDefault();
|
||||
_settings.PropertyChanged += OnSettingsChanged;
|
||||
return _settings;
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
if (_settings is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!);
|
||||
File.WriteAllText(_filePath, JsonSerializer.Serialize(_settings, SerializerOptions));
|
||||
}
|
||||
catch (Exception e) when (e is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
// Настройки — не тот случай, ради которого стоит ронять приложение
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_saveTimer.Stop();
|
||||
_saveTimer.Tick -= OnSaveTimerTick;
|
||||
|
||||
if (_settings is not null)
|
||||
{
|
||||
_settings.PropertyChanged -= OnSettingsChanged;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
private AppSettings? ReadFile()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_filePath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<AppSettings>(File.ReadAllText(_filePath), SerializerOptions);
|
||||
}
|
||||
catch (Exception e) when (e is IOException or UnauthorizedAccessException or JsonException)
|
||||
{
|
||||
// Испорченный файл не должен мешать запуску: начнём с умолчаний
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Язык по умолчанию берём системный, если он поддерживается
|
||||
private static AppSettings CreateDefault()
|
||||
{
|
||||
string uiLanguage = System.Globalization.CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
|
||||
return new AppSettings { Language = uiLanguage == "ru" ? "ru" : "en" };
|
||||
}
|
||||
|
||||
private void OnSettingsChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
_saveTimer.Stop();
|
||||
_saveTimer.Start();
|
||||
}
|
||||
|
||||
private void OnSaveTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
_saveTimer.Stop();
|
||||
Save();
|
||||
}
|
||||
|
||||
// Color не сериализуется штатно, а хранить его читаемым в файле удобно
|
||||
private sealed class ColorJsonConverter : JsonConverter<Color>
|
||||
{
|
||||
public override Color Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
string? value = reader.GetString();
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return Colors.Black;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return (Color)ColorConverter.ConvertFromString(value)!;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return Colors.Black;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, Color value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user