added multilang and settings

This commit is contained in:
2026-08-08 21:48:16 +05:00
parent 3d614255d8
commit d146fd442a
18 changed files with 1115 additions and 72 deletions
+44
View File
@@ -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;
}
+7 -4
View File
@@ -14,13 +14,16 @@
ShowActivated="False"
Topmost="True"
Focusable="False"
IsHitTestVisible="False">
<Border Background="#E6202020"
CornerRadius="4"
IsHitTestVisible="False"
Opacity="{Binding Settings.Opacity}">
<Border CornerRadius="4"
Padding="10,4">
<Border.Background>
<SolidColorBrush Color="{Binding Settings.BackgroundColor}" />
</Border.Background>
<TextBlock Foreground="White"
FontSize="20"
FontWeight="SemiBold"
FontSize="{Binding Settings.FontSize}"
Text="{Binding ShortName}" />
</Border>
</Window>
+53 -17
View File
@@ -1,39 +1,38 @@
using System.Windows;
using System.Windows.Interop;
using CursorLang.Interop;
using CursorLang.Services;
using CursorLang.Models;
using CursorLang.ViewModels;
namespace CursorLang.Views;
/// <summary>
/// Всплывающая подсказка у курсора с коротким именем раскладки.
/// Отвечает только за показ: когда её убрать, решает <see cref="LayoutPopupService"/>.
/// Всплывающая подсказка с коротким именем раскладки.
/// Отвечает только за показ и место на экране: когда её убрать,
/// решает <see cref="Services.LayoutPopupService"/>.
/// </summary>
public partial class LayoutPopupWindow : Window
{
private readonly double _cursorOffset;
private PopupWindowNative.Point _cursor;
private readonly AppSettings _settings;
public LayoutPopupWindow(LayoutPopupViewModel viewModel, PopupOptions options)
public LayoutPopupWindow(LayoutPopupViewModel viewModel, AppSettings settings)
{
InitializeComponent();
DataContext = viewModel;
_cursorOffset = options.CursorOffset;
_settings = settings;
}
/// <summary>
/// Показывает подсказку у текущего положения курсора.
/// Показывает подсказку в месте, заданном настройками.
/// </summary>
public void ShowAtCursor()
public void ShowPopup()
{
ResizeToContent();
_cursor = PopupWindowNative.GetCursorPosition();
// Для уже созданного окна двигаем до показа; при самом первом вызове
// хэндла ещё нет, и позиционирование выполнит OnSourceInitialized
MoveToCursor();
MoveToTargetPosition();
Show();
}
@@ -44,7 +43,7 @@ public partial class LayoutPopupWindow : Window
base.OnSourceInitialized(e);
PopupWindowNative.MakePassive(new WindowInteropHelper(this).Handle);
MoveToCursor();
MoveToTargetPosition();
}
// Размер считаем сами, а не через SizeToContent: тот вычисляет его при создании
@@ -59,7 +58,7 @@ public partial class LayoutPopupWindow : Window
Height = content.DesiredSize.Height;
}
private void MoveToCursor()
private void MoveToTargetPosition()
{
IntPtr handle = new WindowInteropHelper(this).Handle;
if (handle == IntPtr.Zero)
@@ -67,9 +66,46 @@ public partial class LayoutPopupWindow : Window
return;
}
// Отступ задан в единицах WPF, а двигаем окно в пикселях: пересчитываем
// его по масштабу монитора, на котором сейчас курсор
int offset = (int)Math.Round(_cursorOffset * PopupWindowNative.GetScaleAt(_cursor));
PopupWindowNative.MoveTo(handle, _cursor.X + offset, _cursor.Y + offset);
(int x, int y) = _settings.PlacementMode == PopupPlacementMode.AtCursor
? GetCursorPosition()
: GetScreenPosition();
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);
}
+240 -8
View File
@@ -3,14 +3,246 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
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"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=vm:MainViewModel}"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Label HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="48"
Content="{Binding CurrentLayout}" />
</Grid>
d:DataContext="{d:DesignInstance Type=vm:SettingsViewModel}"
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.ColumnDefinitions>
<ColumnDefinition Width="180" />
<ColumnDefinition Width="*" />
</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>
</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>
+2 -2
View File
@@ -4,11 +4,11 @@ using CursorLang.ViewModels;
namespace CursorLang.Views;
/// <summary>
/// Главное окно: показывает текущую раскладку.
/// Окно настроек приложения.
/// </summary>
public partial class MainWindow : Window
{
public MainWindow(MainViewModel viewModel)
public MainWindow(SettingsViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;