added multilang and settings
This commit is contained in:
+11
-5
@@ -1,4 +1,5 @@
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using CursorLang.Models;
|
||||||
using CursorLang.Services;
|
using CursorLang.Services;
|
||||||
using CursorLang.ViewModels;
|
using CursorLang.ViewModels;
|
||||||
using CursorLang.Views;
|
using CursorLang.Views;
|
||||||
@@ -7,7 +8,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
namespace CursorLang;
|
namespace CursorLang;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Композиционный корень: собирает контейнер и запускает главное окно.
|
/// Композиционный корень: собирает контейнер и запускает окно настроек.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
@@ -24,12 +25,13 @@ public partial class App : Application
|
|||||||
MainWindow = _services.GetRequiredService<MainWindow>();
|
MainWindow = _services.GetRequiredService<MainWindow>();
|
||||||
MainWindow.Show();
|
MainWindow.Show();
|
||||||
|
|
||||||
_services.GetRequiredService<IKeyboardLayoutService>().Start();
|
_services.GetRequiredService<LayoutNotificationCoordinator>().Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnExit(ExitEventArgs e)
|
protected override void OnExit(ExitEventArgs e)
|
||||||
{
|
{
|
||||||
// Контейнер сам остановит таймеры и закроет окно подсказки
|
// Контейнер сам остановит таймеры, сохранит настройки
|
||||||
|
// и закроет окно подсказки
|
||||||
_services?.Dispose();
|
_services?.Dispose();
|
||||||
base.OnExit(e);
|
base.OnExit(e);
|
||||||
}
|
}
|
||||||
@@ -37,13 +39,17 @@ public partial class App : Application
|
|||||||
private static void ConfigureServices(IServiceCollection services)
|
private static void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddSingleton(new KeyboardLayoutOptions());
|
services.AddSingleton(new KeyboardLayoutOptions());
|
||||||
services.AddSingleton(new PopupOptions());
|
|
||||||
|
|
||||||
|
services.AddSingleton<SettingsService>();
|
||||||
|
services.AddSingleton(provider => provider.GetRequiredService<SettingsService>().Load());
|
||||||
|
|
||||||
|
services.AddSingleton<ILocalizationService, LocalizationService>();
|
||||||
services.AddSingleton<IKeyboardLayoutService, KeyboardLayoutService>();
|
services.AddSingleton<IKeyboardLayoutService, KeyboardLayoutService>();
|
||||||
services.AddSingleton<ILayoutPopupService, LayoutPopupService>();
|
services.AddSingleton<ILayoutPopupService, LayoutPopupService>();
|
||||||
|
services.AddSingleton<LayoutNotificationCoordinator>();
|
||||||
|
|
||||||
services.AddSingleton<LayoutPopupViewModel>();
|
services.AddSingleton<LayoutPopupViewModel>();
|
||||||
services.AddSingleton<MainViewModel>();
|
services.AddSingleton<SettingsViewModel>();
|
||||||
|
|
||||||
services.AddSingleton<LayoutPopupWindow>();
|
services.AddSingleton<LayoutPopupWindow>();
|
||||||
services.AddSingleton<MainWindow>();
|
services.AddSingleton<MainWindow>();
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ internal static class PopupWindowNative
|
|||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
private static extern bool GetCursorPos(out Point lpPoint);
|
private static extern bool GetCursorPos(out Point lpPoint);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern IntPtr GetForegroundWindow();
|
||||||
|
|
||||||
[DllImport("user32.dll", SetLastError = true)]
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
|
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
|
||||||
|
|
||||||
@@ -34,6 +37,30 @@ internal static class PopupWindowNative
|
|||||||
[DllImport("shcore.dll")]
|
[DllImport("shcore.dll")]
|
||||||
private static extern int GetDpiForMonitor(IntPtr hMonitor, int dpiType, out uint dpiX, out uint dpiY);
|
private static extern int GetDpiForMonitor(IntPtr hMonitor, int dpiType, out uint dpiX, out uint dpiY);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern IntPtr MonitorFromWindow(IntPtr hWnd, uint dwFlags);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MonitorInfo lpmi);
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
internal struct Rect
|
||||||
|
{
|
||||||
|
public int Left;
|
||||||
|
public int Top;
|
||||||
|
public int Right;
|
||||||
|
public int Bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct MonitorInfo
|
||||||
|
{
|
||||||
|
public int cbSize;
|
||||||
|
public Rect rcMonitor;
|
||||||
|
public Rect rcWork;
|
||||||
|
public uint dwFlags;
|
||||||
|
}
|
||||||
|
|
||||||
private const int GWL_EXSTYLE = -20;
|
private const int GWL_EXSTYLE = -20;
|
||||||
// Окно не забирает фокус у активного приложения
|
// Окно не забирает фокус у активного приложения
|
||||||
private const int WS_EX_NOACTIVATE = 0x08000000;
|
private const int WS_EX_NOACTIVATE = 0x08000000;
|
||||||
@@ -75,9 +102,28 @@ internal static class PopupWindowNative
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Масштаб монитора, на котором находится точка (1.0 при 96 DPI).</summary>
|
/// <summary>Масштаб монитора, на котором находится точка (1.0 при 96 DPI).</summary>
|
||||||
internal static double GetScaleAt(Point point)
|
internal static double GetScaleAt(Point point) =>
|
||||||
|
GetScaleOf(MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Рабочая область монитора с активным окном — без панели задач — и его масштаб.
|
||||||
|
/// Именно на этом мониторе пользователь сейчас работает.
|
||||||
|
/// </summary>
|
||||||
|
internal static (Rect WorkArea, double Scale) GetActiveMonitorWorkArea()
|
||||||
|
{
|
||||||
|
IntPtr monitor = MonitorFromWindow(GetForegroundWindow(), MONITOR_DEFAULTTONEAREST);
|
||||||
|
|
||||||
|
var info = new MonitorInfo { cbSize = Marshal.SizeOf<MonitorInfo>() };
|
||||||
|
if (!GetMonitorInfo(monitor, ref info))
|
||||||
|
{
|
||||||
|
return (new Rect(), 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (info.rcWork, GetScaleOf(monitor));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double GetScaleOf(IntPtr monitor)
|
||||||
{
|
{
|
||||||
IntPtr monitor = MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST);
|
|
||||||
if (GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, out uint dpiX, out _) < 0)
|
if (GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, out uint dpiX, out _) < 0)
|
||||||
{
|
{
|
||||||
return 1.0;
|
return 1.0;
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace CursorLang.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настройки приложения. Все изменения применяются на лету: подсказка и окно
|
||||||
|
/// настроек привязаны к этим свойствам, а <c>SettingsService</c> сохраняет их на диск.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class AppSettings : ObservableObject
|
||||||
|
{
|
||||||
|
/// <summary>Язык интерфейса в виде кода культуры: «ru», «en».</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _language = "en";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private PopupPlacementMode _placementMode = PopupPlacementMode.AtCursor;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private CursorCorner _cursorCorner = CursorCorner.BottomRight;
|
||||||
|
|
||||||
|
/// <summary>Отступ от курсора в единицах WPF.</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private double _cursorOffset = 16;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private ScreenPosition _screenPosition = ScreenPosition.BottomRight;
|
||||||
|
|
||||||
|
/// <summary>Отступ от края монитора в единицах WPF.</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private double _screenMargin = 24;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private double _fontSize = 20;
|
||||||
|
|
||||||
|
/// <summary>Непрозрачность подсказки: 1.0 — полностью непрозрачная.</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private double _opacity = 0.9;
|
||||||
|
|
||||||
|
/// <summary>Сколько подсказка держится на экране, в миллисекундах.</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
private double _durationMilliseconds = 500;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private Color _backgroundColor = Color.FromRgb(0x20, 0x20, 0x20);
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public TimeSpan Duration => TimeSpan.FromMilliseconds(DurationMilliseconds);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
namespace CursorLang.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Способ выбора места для подсказки.
|
||||||
|
/// </summary>
|
||||||
|
public enum PopupPlacementMode
|
||||||
|
{
|
||||||
|
/// <summary>Рядом с курсором мыши.</summary>
|
||||||
|
AtCursor,
|
||||||
|
|
||||||
|
/// <summary>В заданной точке монитора с активным окном.</summary>
|
||||||
|
FixedPoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// С какой стороны от курсора показывать подсказку.
|
||||||
|
/// </summary>
|
||||||
|
public enum CursorCorner
|
||||||
|
{
|
||||||
|
BottomRight,
|
||||||
|
BottomLeft,
|
||||||
|
TopRight,
|
||||||
|
TopLeft,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Место на мониторе для режима <see cref="PopupPlacementMode.FixedPoint"/>.
|
||||||
|
/// </summary>
|
||||||
|
public enum ScreenPosition
|
||||||
|
{
|
||||||
|
TopLeft,
|
||||||
|
TopRight,
|
||||||
|
BottomLeft,
|
||||||
|
BottomRight,
|
||||||
|
Center,
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="SettingsTitle" xml:space="preserve">
|
||||||
|
<value>CursorLang — Settings</value>
|
||||||
|
</data>
|
||||||
|
<data name="SectionInterface" xml:space="preserve">
|
||||||
|
<value>Interface</value>
|
||||||
|
</data>
|
||||||
|
<data name="LanguageLabel" xml:space="preserve">
|
||||||
|
<value>Interface language</value>
|
||||||
|
</data>
|
||||||
|
<data name="SectionPlacement" xml:space="preserve">
|
||||||
|
<value>Placement</value>
|
||||||
|
</data>
|
||||||
|
<data name="PlacementModeLabel" xml:space="preserve">
|
||||||
|
<value>Mode</value>
|
||||||
|
</data>
|
||||||
|
<data name="PopupPlacementMode_AtCursor" xml:space="preserve">
|
||||||
|
<value>Near the cursor</value>
|
||||||
|
</data>
|
||||||
|
<data name="PopupPlacementMode_FixedPoint" xml:space="preserve">
|
||||||
|
<value>Fixed point on screen</value>
|
||||||
|
</data>
|
||||||
|
<data name="CursorCornerLabel" xml:space="preserve">
|
||||||
|
<value>Side of the cursor</value>
|
||||||
|
</data>
|
||||||
|
<data name="CursorCorner_BottomRight" xml:space="preserve">
|
||||||
|
<value>Bottom right</value>
|
||||||
|
</data>
|
||||||
|
<data name="CursorCorner_BottomLeft" xml:space="preserve">
|
||||||
|
<value>Bottom left</value>
|
||||||
|
</data>
|
||||||
|
<data name="CursorCorner_TopRight" xml:space="preserve">
|
||||||
|
<value>Top right</value>
|
||||||
|
</data>
|
||||||
|
<data name="CursorCorner_TopLeft" xml:space="preserve">
|
||||||
|
<value>Top left</value>
|
||||||
|
</data>
|
||||||
|
<data name="CursorOffsetLabel" xml:space="preserve">
|
||||||
|
<value>Offset from cursor</value>
|
||||||
|
</data>
|
||||||
|
<data name="ScreenPositionLabel" xml:space="preserve">
|
||||||
|
<value>Position on screen</value>
|
||||||
|
</data>
|
||||||
|
<data name="ScreenPosition_TopLeft" xml:space="preserve">
|
||||||
|
<value>Top left</value>
|
||||||
|
</data>
|
||||||
|
<data name="ScreenPosition_TopRight" xml:space="preserve">
|
||||||
|
<value>Top right</value>
|
||||||
|
</data>
|
||||||
|
<data name="ScreenPosition_BottomLeft" xml:space="preserve">
|
||||||
|
<value>Bottom left</value>
|
||||||
|
</data>
|
||||||
|
<data name="ScreenPosition_BottomRight" xml:space="preserve">
|
||||||
|
<value>Bottom right</value>
|
||||||
|
</data>
|
||||||
|
<data name="ScreenPosition_Center" xml:space="preserve">
|
||||||
|
<value>Center</value>
|
||||||
|
</data>
|
||||||
|
<data name="ScreenMarginLabel" xml:space="preserve">
|
||||||
|
<value>Margin from screen edge</value>
|
||||||
|
</data>
|
||||||
|
<data name="SectionAppearance" xml:space="preserve">
|
||||||
|
<value>Appearance</value>
|
||||||
|
</data>
|
||||||
|
<data name="FontSizeLabel" xml:space="preserve">
|
||||||
|
<value>Font size</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpacityLabel" xml:space="preserve">
|
||||||
|
<value>Opacity</value>
|
||||||
|
</data>
|
||||||
|
<data name="BackgroundColorLabel" xml:space="preserve">
|
||||||
|
<value>Background color</value>
|
||||||
|
</data>
|
||||||
|
<data name="SectionBehavior" xml:space="preserve">
|
||||||
|
<value>Behavior</value>
|
||||||
|
</data>
|
||||||
|
<data name="DurationLabel" xml:space="preserve">
|
||||||
|
<value>Display time</value>
|
||||||
|
</data>
|
||||||
|
<data name="PreviewLabel" xml:space="preserve">
|
||||||
|
<value>Preview</value>
|
||||||
|
</data>
|
||||||
|
<data name="MillisecondsSuffix" xml:space="preserve">
|
||||||
|
<value>ms</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="SettingsTitle" xml:space="preserve">
|
||||||
|
<value>CursorLang — Настройки</value>
|
||||||
|
</data>
|
||||||
|
<data name="SectionInterface" xml:space="preserve">
|
||||||
|
<value>Интерфейс</value>
|
||||||
|
</data>
|
||||||
|
<data name="LanguageLabel" xml:space="preserve">
|
||||||
|
<value>Язык интерфейса</value>
|
||||||
|
</data>
|
||||||
|
<data name="SectionPlacement" xml:space="preserve">
|
||||||
|
<value>Расположение</value>
|
||||||
|
</data>
|
||||||
|
<data name="PlacementModeLabel" xml:space="preserve">
|
||||||
|
<value>Режим</value>
|
||||||
|
</data>
|
||||||
|
<data name="PopupPlacementMode_AtCursor" xml:space="preserve">
|
||||||
|
<value>Рядом с курсором</value>
|
||||||
|
</data>
|
||||||
|
<data name="PopupPlacementMode_FixedPoint" xml:space="preserve">
|
||||||
|
<value>В заданной точке экрана</value>
|
||||||
|
</data>
|
||||||
|
<data name="CursorCornerLabel" xml:space="preserve">
|
||||||
|
<value>Сторона от курсора</value>
|
||||||
|
</data>
|
||||||
|
<data name="CursorCorner_BottomRight" xml:space="preserve">
|
||||||
|
<value>Справа снизу</value>
|
||||||
|
</data>
|
||||||
|
<data name="CursorCorner_BottomLeft" xml:space="preserve">
|
||||||
|
<value>Слева снизу</value>
|
||||||
|
</data>
|
||||||
|
<data name="CursorCorner_TopRight" xml:space="preserve">
|
||||||
|
<value>Справа сверху</value>
|
||||||
|
</data>
|
||||||
|
<data name="CursorCorner_TopLeft" xml:space="preserve">
|
||||||
|
<value>Слева сверху</value>
|
||||||
|
</data>
|
||||||
|
<data name="CursorOffsetLabel" xml:space="preserve">
|
||||||
|
<value>Отступ от курсора</value>
|
||||||
|
</data>
|
||||||
|
<data name="ScreenPositionLabel" xml:space="preserve">
|
||||||
|
<value>Позиция на экране</value>
|
||||||
|
</data>
|
||||||
|
<data name="ScreenPosition_TopLeft" xml:space="preserve">
|
||||||
|
<value>Слева сверху</value>
|
||||||
|
</data>
|
||||||
|
<data name="ScreenPosition_TopRight" xml:space="preserve">
|
||||||
|
<value>Справа сверху</value>
|
||||||
|
</data>
|
||||||
|
<data name="ScreenPosition_BottomLeft" xml:space="preserve">
|
||||||
|
<value>Слева снизу</value>
|
||||||
|
</data>
|
||||||
|
<data name="ScreenPosition_BottomRight" xml:space="preserve">
|
||||||
|
<value>Справа снизу</value>
|
||||||
|
</data>
|
||||||
|
<data name="ScreenPosition_Center" xml:space="preserve">
|
||||||
|
<value>По центру</value>
|
||||||
|
</data>
|
||||||
|
<data name="ScreenMarginLabel" xml:space="preserve">
|
||||||
|
<value>Отступ от края экрана</value>
|
||||||
|
</data>
|
||||||
|
<data name="SectionAppearance" xml:space="preserve">
|
||||||
|
<value>Внешний вид</value>
|
||||||
|
</data>
|
||||||
|
<data name="FontSizeLabel" xml:space="preserve">
|
||||||
|
<value>Размер шрифта</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpacityLabel" xml:space="preserve">
|
||||||
|
<value>Прозрачность</value>
|
||||||
|
</data>
|
||||||
|
<data name="BackgroundColorLabel" xml:space="preserve">
|
||||||
|
<value>Цвет фона</value>
|
||||||
|
</data>
|
||||||
|
<data name="SectionBehavior" xml:space="preserve">
|
||||||
|
<value>Поведение</value>
|
||||||
|
</data>
|
||||||
|
<data name="DurationLabel" xml:space="preserve">
|
||||||
|
<value>Время отображения</value>
|
||||||
|
</data>
|
||||||
|
<data name="PreviewLabel" xml:space="preserve">
|
||||||
|
<value>Предпросмотр</value>
|
||||||
|
</data>
|
||||||
|
<data name="MillisecondsSuffix" xml:space="preserve">
|
||||||
|
<value>мс</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
+16
-16
@@ -1,36 +1,36 @@
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CursorLang.Models;
|
using CursorLang.Models;
|
||||||
using CursorLang.Services;
|
|
||||||
|
|
||||||
namespace CursorLang.ViewModels;
|
namespace CursorLang.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Показывает текущую раскладку в главном окне и просит показать подсказку
|
/// Связывает слежение за раскладкой с показом подсказки.
|
||||||
/// у курсора, когда пользователь переключил раскладку сам.
|
/// Живёт всё время работы приложения независимо от открытых окон.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class MainViewModel : ObservableObject, IDisposable
|
public sealed class LayoutNotificationCoordinator : IDisposable
|
||||||
{
|
{
|
||||||
private readonly IKeyboardLayoutService _layoutService;
|
private readonly IKeyboardLayoutService _layoutService;
|
||||||
private readonly ILayoutPopupService _popupService;
|
private readonly ILayoutPopupService _popupService;
|
||||||
|
|
||||||
[ObservableProperty]
|
public LayoutNotificationCoordinator(IKeyboardLayoutService layoutService, ILayoutPopupService popupService)
|
||||||
private string _currentLayout;
|
|
||||||
|
|
||||||
public MainViewModel(IKeyboardLayoutService layoutService, ILayoutPopupService popupService)
|
|
||||||
{
|
{
|
||||||
_layoutService = layoutService;
|
_layoutService = layoutService;
|
||||||
_popupService = popupService;
|
_popupService = popupService;
|
||||||
|
|
||||||
_currentLayout = layoutService.Current.DisplayName;
|
|
||||||
_layoutService.LayoutChanged += OnLayoutChanged;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() => _layoutService.LayoutChanged -= OnLayoutChanged;
|
public void Start()
|
||||||
|
{
|
||||||
|
_layoutService.LayoutChanged += OnLayoutChanged;
|
||||||
|
_layoutService.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_layoutService.LayoutChanged -= OnLayoutChanged;
|
||||||
|
_layoutService.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
private void OnLayoutChanged(object? sender, LayoutChangedEventArgs e)
|
private void OnLayoutChanged(object? sender, LayoutChangedEventArgs e)
|
||||||
{
|
{
|
||||||
CurrentLayout = e.Layout.DisplayName;
|
|
||||||
|
|
||||||
// При переходе в другое приложение раскладка меняется без участия
|
// При переходе в другое приложение раскладка меняется без участия
|
||||||
// пользователя, и всплывающая подсказка была бы навязчивой
|
// пользователя, и всплывающая подсказка была бы навязчивой
|
||||||
if (e.Reason == LayoutChangeReason.UserSwitched)
|
if (e.Reason == LayoutChangeReason.UserSwitched)
|
||||||
@@ -5,18 +5,6 @@ using CursorLang.Views;
|
|||||||
|
|
||||||
namespace CursorLang.Services;
|
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>
|
/// <summary>
|
||||||
/// Управляет временем жизни подсказки: окно отвечает только за показ,
|
/// Управляет временем жизни подсказки: окно отвечает только за показ,
|
||||||
/// а решение «когда показать и когда убрать» принимается здесь.
|
/// а решение «когда показать и когда убрать» принимается здесь.
|
||||||
@@ -25,24 +13,27 @@ public sealed class LayoutPopupService : ILayoutPopupService, IDisposable
|
|||||||
{
|
{
|
||||||
private readonly LayoutPopupWindow _window;
|
private readonly LayoutPopupWindow _window;
|
||||||
private readonly LayoutPopupViewModel _viewModel;
|
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;
|
_window = window;
|
||||||
_viewModel = viewModel;
|
_viewModel = viewModel;
|
||||||
|
_settings = settings;
|
||||||
|
|
||||||
_hideTimer = new DispatcherTimer { Interval = options.Duration };
|
|
||||||
_hideTimer.Tick += OnHideTimerTick;
|
_hideTimer.Tick += OnHideTimerTick;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Show(KeyboardLayout layout)
|
public void Show(KeyboardLayout layout)
|
||||||
{
|
{
|
||||||
_viewModel.ShortName = layout.ShortName;
|
_viewModel.ShortName = layout.ShortName;
|
||||||
_window.ShowAtCursor();
|
_window.ShowPopup();
|
||||||
|
|
||||||
// Перезапускаем таймер, чтобы быстрые переключения продлевали показ
|
// Длительность читаем при каждом показе: её меняют в настройках на лету.
|
||||||
|
// Перезапуск таймера заодно продлевает показ при быстрых переключениях
|
||||||
_hideTimer.Stop();
|
_hideTimer.Stop();
|
||||||
|
_hideTimer.Interval = _settings.Duration;
|
||||||
_hideTimer.Start();
|
_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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CursorLang.Models;
|
||||||
|
|
||||||
namespace CursorLang.ViewModels;
|
namespace CursorLang.ViewModels;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Содержимое подсказки у курсора.
|
/// Содержимое подсказки у курсора. Внешний вид берётся прямо из настроек,
|
||||||
|
/// поэтому их правка применяется без перезапуска.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class LayoutPopupViewModel : ObservableObject
|
public sealed partial class LayoutPopupViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _shortName = "—";
|
private string _shortName = "—";
|
||||||
|
|
||||||
|
public LayoutPopupViewModel(AppSettings settings) => Settings = settings;
|
||||||
|
|
||||||
|
public AppSettings Settings { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CursorLang.Models;
|
||||||
|
using CursorLang.Services;
|
||||||
|
|
||||||
|
namespace CursorLang.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>Вариант выбора в списке: значение и его подпись на текущем языке.</summary>
|
||||||
|
public sealed record EnumOption<T>(T Value, string Display);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Окно настроек. Значения правятся прямо в <see cref="AppSettings"/>,
|
||||||
|
/// поэтому подсказка подхватывает их сразу, без кнопки «Применить».
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class SettingsViewModel : ObservableObject, IDisposable
|
||||||
|
{
|
||||||
|
private static readonly Color[] Palette =
|
||||||
|
[
|
||||||
|
Color.FromRgb(0x20, 0x20, 0x20),
|
||||||
|
Color.FromRgb(0x00, 0x00, 0x00),
|
||||||
|
Color.FromRgb(0x1E, 0x3A, 0x8A),
|
||||||
|
Color.FromRgb(0x0F, 0x76, 0x6E),
|
||||||
|
Color.FromRgb(0x7C, 0x2D, 0x12),
|
||||||
|
Color.FromRgb(0x86, 0x19, 0x8F),
|
||||||
|
Color.FromRgb(0xB9, 0x1C, 0x1C),
|
||||||
|
Color.FromRgb(0xF5, 0xF5, 0xF5),
|
||||||
|
];
|
||||||
|
|
||||||
|
public SettingsViewModel(AppSettings settings, ILocalizationService localization)
|
||||||
|
{
|
||||||
|
Settings = settings;
|
||||||
|
Localization = localization;
|
||||||
|
|
||||||
|
// Язык интерфейса — такая же настройка, как остальные, и хранится там же
|
||||||
|
Localization.CurrentLanguage = settings.Language;
|
||||||
|
Settings.PropertyChanged += OnSettingsChanged;
|
||||||
|
Localization.PropertyChanged += OnLocalizationChanged;
|
||||||
|
|
||||||
|
BuildLocalizedOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppSettings Settings { get; }
|
||||||
|
|
||||||
|
public ILocalizationService Localization { get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<Color> BackgroundPalette { get; } = Palette;
|
||||||
|
|
||||||
|
public IReadOnlyList<EnumOption<PopupPlacementMode>> PlacementModes { get; private set; } = [];
|
||||||
|
|
||||||
|
public IReadOnlyList<EnumOption<CursorCorner>> CursorCorners { get; private set; } = [];
|
||||||
|
|
||||||
|
public IReadOnlyList<EnumOption<ScreenPosition>> ScreenPositions { get; private set; } = [];
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Settings.PropertyChanged -= OnSettingsChanged;
|
||||||
|
Localization.PropertyChanged -= OnLocalizationChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSettingsChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(AppSettings.Language))
|
||||||
|
{
|
||||||
|
Localization.CurrentLanguage = Settings.Language;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подписи вариантов приходят из ресурсов, поэтому при смене языка
|
||||||
|
// списки нужно собрать заново — привязки сами перечитают их
|
||||||
|
private void OnLocalizationChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == Binding.IndexerName)
|
||||||
|
{
|
||||||
|
BuildLocalizedOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildLocalizedOptions()
|
||||||
|
{
|
||||||
|
PlacementModes = BuildOptions<PopupPlacementMode>(nameof(PopupPlacementMode));
|
||||||
|
CursorCorners = BuildOptions<CursorCorner>(nameof(CursorCorner));
|
||||||
|
ScreenPositions = BuildOptions<ScreenPosition>(nameof(ScreenPosition));
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(PlacementModes));
|
||||||
|
OnPropertyChanged(nameof(CursorCorners));
|
||||||
|
OnPropertyChanged(nameof(ScreenPositions));
|
||||||
|
}
|
||||||
|
|
||||||
|
private EnumOption<T>[] BuildOptions<T>(string resourcePrefix) where T : struct, Enum =>
|
||||||
|
[.. Enum.GetValues<T>().Select(value => new EnumOption<T>(value, Localization[$"{resourcePrefix}_{value}"]))];
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace CursorLang.Views;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Показывает элемент, только если значение совпадает с параметром.
|
||||||
|
/// Нужен, чтобы настройки курсора и экрана не показывались одновременно.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class EnumToVisibilityConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>
|
||||||
|
value?.ToString() == parameter?.ToString() ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
|
||||||
|
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
|
||||||
|
Binding.DoNothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Цвет в запись вида «#RRGGBB»: альфа не показывается, потому что
|
||||||
|
/// за прозрачность отвечает отдельная настройка.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ColorToHexConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>
|
||||||
|
value is Color color ? $"#{color.R:X2}{color.G:X2}{color.B:X2}" : string.Empty;
|
||||||
|
|
||||||
|
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
|
||||||
|
Binding.DoNothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Цвет в кисть — для образцов палитры в списке.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ColorToBrushConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>
|
||||||
|
value is Color color ? new SolidColorBrush(color) : Brushes.Transparent;
|
||||||
|
|
||||||
|
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
|
||||||
|
value is SolidColorBrush brush ? brush.Color : Binding.DoNothing;
|
||||||
|
}
|
||||||
@@ -14,13 +14,16 @@
|
|||||||
ShowActivated="False"
|
ShowActivated="False"
|
||||||
Topmost="True"
|
Topmost="True"
|
||||||
Focusable="False"
|
Focusable="False"
|
||||||
IsHitTestVisible="False">
|
IsHitTestVisible="False"
|
||||||
<Border Background="#E6202020"
|
Opacity="{Binding Settings.Opacity}">
|
||||||
CornerRadius="4"
|
<Border CornerRadius="4"
|
||||||
Padding="10,4">
|
Padding="10,4">
|
||||||
|
<Border.Background>
|
||||||
|
<SolidColorBrush Color="{Binding Settings.BackgroundColor}" />
|
||||||
|
</Border.Background>
|
||||||
<TextBlock Foreground="White"
|
<TextBlock Foreground="White"
|
||||||
FontSize="20"
|
|
||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
|
FontSize="{Binding Settings.FontSize}"
|
||||||
Text="{Binding ShortName}" />
|
Text="{Binding ShortName}" />
|
||||||
</Border>
|
</Border>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -1,39 +1,38 @@
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Interop;
|
using System.Windows.Interop;
|
||||||
using CursorLang.Interop;
|
using CursorLang.Interop;
|
||||||
using CursorLang.Services;
|
using CursorLang.Models;
|
||||||
using CursorLang.ViewModels;
|
using CursorLang.ViewModels;
|
||||||
|
|
||||||
namespace CursorLang.Views;
|
namespace CursorLang.Views;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Всплывающая подсказка у курсора с коротким именем раскладки.
|
/// Всплывающая подсказка с коротким именем раскладки.
|
||||||
/// Отвечает только за показ: когда её убрать, решает <see cref="LayoutPopupService"/>.
|
/// Отвечает только за показ и место на экране: когда её убрать,
|
||||||
|
/// решает <see cref="Services.LayoutPopupService"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class LayoutPopupWindow : Window
|
public partial class LayoutPopupWindow : Window
|
||||||
{
|
{
|
||||||
private readonly double _cursorOffset;
|
private readonly AppSettings _settings;
|
||||||
private PopupWindowNative.Point _cursor;
|
|
||||||
|
|
||||||
public LayoutPopupWindow(LayoutPopupViewModel viewModel, PopupOptions options)
|
public LayoutPopupWindow(LayoutPopupViewModel viewModel, AppSettings settings)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
DataContext = viewModel;
|
DataContext = viewModel;
|
||||||
_cursorOffset = options.CursorOffset;
|
_settings = settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Показывает подсказку у текущего положения курсора.
|
/// Показывает подсказку в месте, заданном настройками.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ShowAtCursor()
|
public void ShowPopup()
|
||||||
{
|
{
|
||||||
ResizeToContent();
|
ResizeToContent();
|
||||||
_cursor = PopupWindowNative.GetCursorPosition();
|
|
||||||
|
|
||||||
// Для уже созданного окна двигаем до показа; при самом первом вызове
|
// Для уже созданного окна двигаем до показа; при самом первом вызове
|
||||||
// хэндла ещё нет, и позиционирование выполнит OnSourceInitialized
|
// хэндла ещё нет, и позиционирование выполнит OnSourceInitialized
|
||||||
MoveToCursor();
|
MoveToTargetPosition();
|
||||||
Show();
|
Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +43,7 @@ public partial class LayoutPopupWindow : Window
|
|||||||
base.OnSourceInitialized(e);
|
base.OnSourceInitialized(e);
|
||||||
|
|
||||||
PopupWindowNative.MakePassive(new WindowInteropHelper(this).Handle);
|
PopupWindowNative.MakePassive(new WindowInteropHelper(this).Handle);
|
||||||
MoveToCursor();
|
MoveToTargetPosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Размер считаем сами, а не через SizeToContent: тот вычисляет его при создании
|
// Размер считаем сами, а не через SizeToContent: тот вычисляет его при создании
|
||||||
@@ -59,7 +58,7 @@ public partial class LayoutPopupWindow : Window
|
|||||||
Height = content.DesiredSize.Height;
|
Height = content.DesiredSize.Height;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MoveToCursor()
|
private void MoveToTargetPosition()
|
||||||
{
|
{
|
||||||
IntPtr handle = new WindowInteropHelper(this).Handle;
|
IntPtr handle = new WindowInteropHelper(this).Handle;
|
||||||
if (handle == IntPtr.Zero)
|
if (handle == IntPtr.Zero)
|
||||||
@@ -67,9 +66,46 @@ public partial class LayoutPopupWindow : Window
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отступ задан в единицах WPF, а двигаем окно в пикселях: пересчитываем
|
(int x, int y) = _settings.PlacementMode == PopupPlacementMode.AtCursor
|
||||||
// его по масштабу монитора, на котором сейчас курсор
|
? GetCursorPosition()
|
||||||
int offset = (int)Math.Round(_cursorOffset * PopupWindowNative.GetScaleAt(_cursor));
|
: GetScreenPosition();
|
||||||
PopupWindowNative.MoveTo(handle, _cursor.X + offset, _cursor.Y + offset);
|
|
||||||
|
PopupWindowNative.MoveTo(handle, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Отступы и размеры заданы в единицах WPF, а окно двигаем в пикселях,
|
||||||
|
// поэтому всё пересчитывается по масштабу нужного монитора
|
||||||
|
private (int X, int Y) GetCursorPosition()
|
||||||
|
{
|
||||||
|
PopupWindowNative.Point cursor = PopupWindowNative.GetCursorPosition();
|
||||||
|
double scale = PopupWindowNative.GetScaleAt(cursor);
|
||||||
|
int offset = (int)Math.Round(_settings.CursorOffset * scale);
|
||||||
|
|
||||||
|
bool toRight = _settings.CursorCorner is CursorCorner.BottomRight or CursorCorner.TopRight;
|
||||||
|
bool toBottom = _settings.CursorCorner is CursorCorner.BottomRight or CursorCorner.BottomLeft;
|
||||||
|
|
||||||
|
int x = toRight ? cursor.X + offset : cursor.X - offset - ToPixels(Width, scale);
|
||||||
|
int y = toBottom ? cursor.Y + offset : cursor.Y - offset - ToPixels(Height, scale);
|
||||||
|
return (x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private (int X, int Y) GetScreenPosition()
|
||||||
|
{
|
||||||
|
(PopupWindowNative.Rect work, double scale) = PopupWindowNative.GetActiveMonitorWorkArea();
|
||||||
|
int margin = (int)Math.Round(_settings.ScreenMargin * scale);
|
||||||
|
int width = ToPixels(Width, scale);
|
||||||
|
int height = ToPixels(Height, scale);
|
||||||
|
|
||||||
|
return _settings.ScreenPosition switch
|
||||||
|
{
|
||||||
|
ScreenPosition.TopLeft => (work.Left + margin, work.Top + margin),
|
||||||
|
ScreenPosition.TopRight => (work.Right - margin - width, work.Top + margin),
|
||||||
|
ScreenPosition.BottomLeft => (work.Left + margin, work.Bottom - margin - height),
|
||||||
|
ScreenPosition.BottomRight => (work.Right - margin - width, work.Bottom - margin - height),
|
||||||
|
_ => (work.Left + ((work.Right - work.Left - width) / 2),
|
||||||
|
work.Top + ((work.Bottom - work.Top - height) / 2)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ToPixels(double wpfUnits, double scale) => (int)Math.Round(wpfUnits * scale);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,246 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:local="clr-namespace:CursorLang.Views"
|
||||||
|
xmlns:models="clr-namespace:CursorLang.Models"
|
||||||
xmlns:vm="clr-namespace:CursorLang.ViewModels"
|
xmlns:vm="clr-namespace:CursorLang.ViewModels"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
d:DataContext="{d:DesignInstance Type=vm:MainViewModel}"
|
d:DataContext="{d:DesignInstance Type=vm:SettingsViewModel}"
|
||||||
Title="MainWindow" Height="450" Width="800">
|
Title="{Binding Localization[SettingsTitle]}"
|
||||||
|
Width="560" SizeToContent="Height" MaxHeight="900"
|
||||||
|
ResizeMode="CanMinimize">
|
||||||
|
<Window.Resources>
|
||||||
|
<local:EnumToVisibilityConverter x:Key="EnumToVisibility" />
|
||||||
|
<local:ColorToBrushConverter x:Key="ColorToBrush" />
|
||||||
|
<local:ColorToHexConverter x:Key="ColorToHex" />
|
||||||
|
|
||||||
|
<Style TargetType="TextBlock" x:Key="FieldLabel">
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
<Setter Property="Margin" Value="0,0,12,0" />
|
||||||
|
</Style>
|
||||||
|
<Style TargetType="TextBlock" x:Key="FieldValue">
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
<Setter Property="Margin" Value="12,0,0,0" />
|
||||||
|
<Setter Property="MinWidth" Value="48" />
|
||||||
|
<Setter Property="TextAlignment" Value="Right" />
|
||||||
|
<Setter Property="Foreground" Value="#666" />
|
||||||
|
</Style>
|
||||||
|
<Style TargetType="GroupBox">
|
||||||
|
<Setter Property="Padding" Value="12" />
|
||||||
|
<Setter Property="Margin" Value="0,0,0,12" />
|
||||||
|
</Style>
|
||||||
|
<Style TargetType="ComboBox">
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
<Style TargetType="Slider">
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
<Setter Property="IsSnapToTickEnabled" Value="True" />
|
||||||
|
</Style>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="16">
|
||||||
|
<StackPanel>
|
||||||
|
|
||||||
|
<GroupBox Header="{Binding Localization[SectionInterface]}">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Label HorizontalAlignment="Center"
|
<Grid.ColumnDefinitions>
|
||||||
VerticalAlignment="Center"
|
<ColumnDefinition Width="180" />
|
||||||
FontSize="48"
|
<ColumnDefinition Width="*" />
|
||||||
Content="{Binding CurrentLayout}" />
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Style="{StaticResource FieldLabel}"
|
||||||
|
Text="{Binding Localization[LanguageLabel]}" />
|
||||||
|
<ComboBox Grid.Column="1"
|
||||||
|
ItemsSource="{Binding Localization.AvailableLanguages}"
|
||||||
|
DisplayMemberPath="DisplayName"
|
||||||
|
SelectedValuePath="Code"
|
||||||
|
SelectedValue="{Binding Settings.Language}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<GroupBox Header="{Binding Localization[SectionPlacement]}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="180" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Style="{StaticResource FieldLabel}"
|
||||||
|
Text="{Binding Localization[PlacementModeLabel]}" />
|
||||||
|
<ComboBox Grid.Column="1" Grid.ColumnSpan="2"
|
||||||
|
ItemsSource="{Binding PlacementModes}"
|
||||||
|
DisplayMemberPath="Display"
|
||||||
|
SelectedValuePath="Value"
|
||||||
|
SelectedValue="{Binding Settings.PlacementMode}" />
|
||||||
|
|
||||||
|
<!-- Режим «у курсора» -->
|
||||||
|
<TextBlock Grid.Row="1" Margin="0,12,12,0"
|
||||||
|
Style="{StaticResource FieldLabel}"
|
||||||
|
Text="{Binding Localization[CursorCornerLabel]}"
|
||||||
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
|
Converter={StaticResource EnumToVisibility},
|
||||||
|
ConverterParameter={x:Static models:PopupPlacementMode.AtCursor}}" />
|
||||||
|
<ComboBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||||
|
ItemsSource="{Binding CursorCorners}"
|
||||||
|
DisplayMemberPath="Display"
|
||||||
|
SelectedValuePath="Value"
|
||||||
|
SelectedValue="{Binding Settings.CursorCorner}"
|
||||||
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
|
Converter={StaticResource EnumToVisibility},
|
||||||
|
ConverterParameter={x:Static models:PopupPlacementMode.AtCursor}}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Margin="0,12,12,0"
|
||||||
|
Style="{StaticResource FieldLabel}"
|
||||||
|
Text="{Binding Localization[CursorOffsetLabel]}"
|
||||||
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
|
Converter={StaticResource EnumToVisibility},
|
||||||
|
ConverterParameter={x:Static models:PopupPlacementMode.AtCursor}}" />
|
||||||
|
<Slider Grid.Row="2" Grid.Column="1" Margin="0,12,0,0"
|
||||||
|
Minimum="0" Maximum="80" TickFrequency="1"
|
||||||
|
Value="{Binding Settings.CursorOffset}"
|
||||||
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
|
Converter={StaticResource EnumToVisibility},
|
||||||
|
ConverterParameter={x:Static models:PopupPlacementMode.AtCursor}}" />
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="2" Margin="12,12,0,0"
|
||||||
|
Style="{StaticResource FieldValue}"
|
||||||
|
Text="{Binding Settings.CursorOffset, StringFormat={}{0:F0}}"
|
||||||
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
|
Converter={StaticResource EnumToVisibility},
|
||||||
|
ConverterParameter={x:Static models:PopupPlacementMode.AtCursor}}" />
|
||||||
|
|
||||||
|
<!-- Режим «фиксированная точка» -->
|
||||||
|
<TextBlock Grid.Row="3" Margin="0,12,12,0"
|
||||||
|
Style="{StaticResource FieldLabel}"
|
||||||
|
Text="{Binding Localization[ScreenPositionLabel]}"
|
||||||
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
|
Converter={StaticResource EnumToVisibility},
|
||||||
|
ConverterParameter={x:Static models:PopupPlacementMode.FixedPoint}}" />
|
||||||
|
<ComboBox Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||||
|
ItemsSource="{Binding ScreenPositions}"
|
||||||
|
DisplayMemberPath="Display"
|
||||||
|
SelectedValuePath="Value"
|
||||||
|
SelectedValue="{Binding Settings.ScreenPosition}"
|
||||||
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
|
Converter={StaticResource EnumToVisibility},
|
||||||
|
ConverterParameter={x:Static models:PopupPlacementMode.FixedPoint}}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="4" Margin="0,12,12,0"
|
||||||
|
Style="{StaticResource FieldLabel}"
|
||||||
|
Text="{Binding Localization[ScreenMarginLabel]}"
|
||||||
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
|
Converter={StaticResource EnumToVisibility},
|
||||||
|
ConverterParameter={x:Static models:PopupPlacementMode.FixedPoint}}" />
|
||||||
|
<Slider Grid.Row="4" Grid.Column="1" Margin="0,12,0,0"
|
||||||
|
Minimum="0" Maximum="200" TickFrequency="1"
|
||||||
|
Value="{Binding Settings.ScreenMargin}"
|
||||||
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
|
Converter={StaticResource EnumToVisibility},
|
||||||
|
ConverterParameter={x:Static models:PopupPlacementMode.FixedPoint}}" />
|
||||||
|
<TextBlock Grid.Row="4" Grid.Column="2" Margin="12,12,0,0"
|
||||||
|
Style="{StaticResource FieldValue}"
|
||||||
|
Text="{Binding Settings.ScreenMargin, StringFormat={}{0:F0}}"
|
||||||
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
|
Converter={StaticResource EnumToVisibility},
|
||||||
|
ConverterParameter={x:Static models:PopupPlacementMode.FixedPoint}}" />
|
||||||
|
</Grid>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<GroupBox Header="{Binding Localization[SectionAppearance]}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="180" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Style="{StaticResource FieldLabel}"
|
||||||
|
Text="{Binding Localization[FontSizeLabel]}" />
|
||||||
|
<Slider Grid.Column="1" Minimum="10" Maximum="72" TickFrequency="1"
|
||||||
|
Value="{Binding Settings.FontSize}" />
|
||||||
|
<TextBlock Grid.Column="2" Style="{StaticResource FieldValue}"
|
||||||
|
Text="{Binding Settings.FontSize, StringFormat={}{0:F0}}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Margin="0,12,12,0"
|
||||||
|
Style="{StaticResource FieldLabel}"
|
||||||
|
Text="{Binding Localization[OpacityLabel]}" />
|
||||||
|
<Slider Grid.Row="1" Grid.Column="1" Margin="0,12,0,0"
|
||||||
|
Minimum="0.1" Maximum="1" TickFrequency="0.05"
|
||||||
|
Value="{Binding Settings.Opacity}" />
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="2" Margin="12,12,0,0"
|
||||||
|
Style="{StaticResource FieldValue}"
|
||||||
|
Text="{Binding Settings.Opacity, StringFormat={}{0:P0}}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Margin="0,12,12,0"
|
||||||
|
Style="{StaticResource FieldLabel}"
|
||||||
|
Text="{Binding Localization[BackgroundColorLabel]}" />
|
||||||
|
<ComboBox Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||||
|
ItemsSource="{Binding BackgroundPalette}"
|
||||||
|
SelectedItem="{Binding Settings.BackgroundColor}">
|
||||||
|
<ComboBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Border Width="32" Height="16" CornerRadius="2"
|
||||||
|
BorderBrush="#888" BorderThickness="1"
|
||||||
|
Background="{Binding Converter={StaticResource ColorToBrush}}" />
|
||||||
|
<TextBlock Margin="8,0,0,0"
|
||||||
|
Text="{Binding Converter={StaticResource ColorToHex}}" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ComboBox.ItemTemplate>
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Margin="0,12,12,0"
|
||||||
|
Style="{StaticResource FieldLabel}"
|
||||||
|
Text="{Binding Localization[PreviewLabel]}" />
|
||||||
|
<Border Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2"
|
||||||
|
Margin="0,12,0,0" Padding="16"
|
||||||
|
Background="#F0F0F0" CornerRadius="4"
|
||||||
|
HorizontalAlignment="Stretch">
|
||||||
|
<Border CornerRadius="4" Padding="10,4"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Opacity="{Binding Settings.Opacity}"
|
||||||
|
Background="{Binding Settings.BackgroundColor,
|
||||||
|
Converter={StaticResource ColorToBrush}}">
|
||||||
|
<TextBlock Text="RU" Foreground="White" FontWeight="SemiBold"
|
||||||
|
FontSize="{Binding Settings.FontSize}" />
|
||||||
|
</Border>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<GroupBox Header="{Binding Localization[SectionBehavior]}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="180" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Style="{StaticResource FieldLabel}"
|
||||||
|
Text="{Binding Localization[DurationLabel]}" />
|
||||||
|
<Slider Grid.Column="1" Minimum="200" Maximum="5000" TickFrequency="100"
|
||||||
|
Value="{Binding Settings.DurationMilliseconds}" />
|
||||||
|
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<TextBlock Style="{StaticResource FieldValue}"
|
||||||
|
Text="{Binding Settings.DurationMilliseconds, StringFormat={}{0:F0}}" />
|
||||||
|
<TextBlock Margin="4,0,0,0" Foreground="#666"
|
||||||
|
Text="{Binding Localization[MillisecondsSuffix]}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ using CursorLang.ViewModels;
|
|||||||
namespace CursorLang.Views;
|
namespace CursorLang.Views;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Главное окно: показывает текущую раскладку.
|
/// Окно настроек приложения.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
public MainWindow(MainViewModel viewModel)
|
public MainWindow(SettingsViewModel viewModel)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
DataContext = viewModel;
|
DataContext = viewModel;
|
||||||
|
|||||||
Reference in New Issue
Block a user