BASE Temp 1.2.2

This commit is contained in:
AlastorFox committed 2026-10-11 23:49:55 +03:00
1 parent 195360dc47
commit 63be149caf
9 files changed
+430 -10

No files matched your search

+1
View File
@@ -8,6 +8,7 @@ public sealed class AppSettings
{ {
public string AppDisplayName { get; set; } = "FoxBrowser"; public string AppDisplayName { get; set; } = "FoxBrowser";
public string? AppIconPath { get; set; } public string? AppIconPath { get; set; }
public Dictionary<string, string> Shortcuts { get; set; } = new();
} }
public static class AppSettingsStore public static class AppSettingsStore
+130
View File
@@ -0,0 +1,130 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;
namespace FoxBrowser.Core;
public sealed class ShortcutManager
{
public const string CloseTab = "CloseTab";
public const string NewTab = "NewTab";
public const string ToggleTopPanel = "ToggleTopPanel";
private static readonly IReadOnlyDictionary<string, KeyGesture> Defaults =
new Dictionary<string, KeyGesture>
{
[CloseTab] = new(Key.W, ModifierKeys.Control),
[NewTab] = new(Key.N, ModifierKeys.Control),
[ToggleTopPanel] = new(Key.F, ModifierKeys.Control | ModifierKeys.Shift),
};
private static readonly Lazy<ShortcutManager> LazyInstance = new(() => new ShortcutManager());
private readonly Dictionary<string, KeyGesture> _shortcuts = new();
private ShortcutManager()
{
ResetToDefaults(save: false);
Load(AppSettingsStore.Load());
}
public static ShortcutManager Instance => LazyInstance.Value;
public IReadOnlyDictionary<string, KeyGesture> Shortcuts => _shortcuts;
public event EventHandler? Changed;
public KeyGesture Get(string action) => _shortcuts[action];
public string? FindAction(Key key, ModifierKeys modifiers)
{
return _shortcuts.FirstOrDefault(pair => pair.Value.Key == key && pair.Value.Modifiers == modifiers).Key;
}
public bool TrySet(string action, KeyGesture gesture, out string? conflictAction)
{
conflictAction = _shortcuts.FirstOrDefault(pair => pair.Key != action && GesturesEqual(pair.Value, gesture)).Key;
if (!string.IsNullOrEmpty(conflictAction)) return false;
_shortcuts[action] = gesture;
Save();
Changed?.Invoke(this, EventArgs.Empty);
return true;
}
public void ResetToDefaults()
{
ResetToDefaults(save: true);
}
public static string GetActionLabel(string action) => action switch
{
CloseTab => "Закрити вкладку",
NewTab => "Нова вкладка",
ToggleTopPanel => "Показати/сховати верхню панель",
_ => action,
};
public static string Format(KeyGesture gesture)
{
var parts = new List<string>();
if (gesture.Modifiers.HasFlag(ModifierKeys.Control)) parts.Add("Ctrl");
if (gesture.Modifiers.HasFlag(ModifierKeys.Shift)) parts.Add("Shift");
if (gesture.Modifiers.HasFlag(ModifierKeys.Alt)) parts.Add("Alt");
if (gesture.Modifiers.HasFlag(ModifierKeys.Windows)) parts.Add("Win");
parts.Add(gesture.Key.ToString());
return string.Join("+", parts);
}
public static bool TryCreateGesture(Key key, ModifierKeys modifiers, out KeyGesture? gesture)
{
gesture = null;
if (key is Key.LeftCtrl or Key.RightCtrl or Key.LeftShift or Key.RightShift or Key.LeftAlt or Key.RightAlt or Key.LWin or Key.RWin)
return false;
if (key == Key.None || modifiers == ModifierKeys.None)
return false;
gesture = new KeyGesture(key, modifiers);
return true;
}
private void Load(AppSettings settings)
{
if (settings.Shortcuts is null) return;
foreach (var pair in settings.Shortcuts)
{
if (!Defaults.ContainsKey(pair.Key)) continue;
if (TryParse(pair.Value, out var gesture)) _shortcuts[pair.Key] = gesture!;
}
}
private void Save()
{
var settings = AppSettingsStore.Load();
settings.Shortcuts = _shortcuts.ToDictionary(pair => pair.Key, pair => Format(pair.Value));
AppSettingsStore.Save(settings);
}
private void ResetToDefaults(bool save)
{
_shortcuts.Clear();
foreach (var pair in Defaults) _shortcuts[pair.Key] = pair.Value;
if (save) Save();
}
private static bool GesturesEqual(KeyGesture left, KeyGesture right) => left.Key == right.Key && left.Modifiers == right.Modifiers;
private static bool TryParse(string? value, out KeyGesture? gesture)
{
gesture = null;
if (string.IsNullOrWhiteSpace(value)) return false;
var parts = value.Split('+', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length < 2 || !Enum.TryParse(parts[^1], true, out Key key)) return false;
var modifiers = ModifierKeys.None;
foreach (var part in parts[..^1])
{
if (part.Equals("Ctrl", StringComparison.OrdinalIgnoreCase) || part.Equals("Control", StringComparison.OrdinalIgnoreCase)) modifiers |= ModifierKeys.Control;
else if (part.Equals("Shift", StringComparison.OrdinalIgnoreCase)) modifiers |= ModifierKeys.Shift;
else if (part.Equals("Alt", StringComparison.OrdinalIgnoreCase)) modifiers |= ModifierKeys.Alt;
else if (part.Equals("Win", StringComparison.OrdinalIgnoreCase) || part.Equals("Windows", StringComparison.OrdinalIgnoreCase)) modifiers |= ModifierKeys.Windows;
else return false;
}
if (!TryCreateGesture(key, modifiers, out gesture)) return false;
return true;
}
}
+16 -2
View File
@@ -27,6 +27,7 @@ public sealed class TabManager : IAsyncDisposable
private bool _navigationInProgress; private bool _navigationInProgress;
private bool _switchInProgress; private bool _switchInProgress;
private bool _restoringSession; private bool _restoringSession;
private readonly List<BrowserTab> _sessionOrder = new();
private readonly string _blockedElementsPath = Path.Combine(AppContext.BaseDirectory, "blocked-elements.json"); private readonly string _blockedElementsPath = Path.Combine(AppContext.BaseDirectory, "blocked-elements.json");
private readonly Dictionary<string, List<string>> _blockedElements; private readonly Dictionary<string, List<string>> _blockedElements;
@@ -81,13 +82,16 @@ public sealed class TabManager : IAsyncDisposable
_restoringSession = false; _restoringSession = false;
SessionReady = true; SessionReady = true;
} }
_sessionOrder.AddRange(Tabs);
await SaveSessionAsync(); await SaveSessionAsync();
} }
public async Task SaveSessionAsync() public async Task SaveSessionAsync()
{ {
var activeIndex = ActiveTab is not null ? Tabs.IndexOf(ActiveTab) : 0; var orderedTabs = _sessionOrder.Where(Tabs.Contains).Concat(Tabs.Where(tab => !_sessionOrder.Contains(tab)));
await SessionStore.SaveAsync(Tabs, activeIndex); var snapshotOrder = orderedTabs.ToList();
var activeIndex = ActiveTab is not null ? snapshotOrder.IndexOf(ActiveTab) : 0;
await SessionStore.SaveAsync(snapshotOrder, activeIndex);
} }
private async Task RestoreSessionOrDefaultAsync() private async Task RestoreSessionOrDefaultAsync()
@@ -125,6 +129,7 @@ public sealed class TabManager : IAsyncDisposable
{ {
var tab = new BrowserTab(NormalizeAddress(address)); var tab = new BrowserTab(NormalizeAddress(address));
Tabs.Add(tab); Tabs.Add(tab);
if (SessionReady) _sessionOrder.Add(tab);
await SwitchToTabAsync(tab); await SwitchToTabAsync(tab);
return tab; return tab;
} }
@@ -318,6 +323,15 @@ public sealed class TabManager : IAsyncDisposable
await SaveSessionAsync(); await SaveSessionAsync();
} }
public void MoveTab(BrowserTab tab, int targetIndex)
{
var sourceIndex = Tabs.IndexOf(tab);
if (sourceIndex < 0 || Tabs.Count < 2) return;
targetIndex = Math.Clamp(targetIndex, 0, Tabs.Count - 1);
if (sourceIndex == targetIndex) return;
Tabs.Move(sourceIndex, targetIndex);
}
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
if (ActiveTab is not null) await CaptureActiveStateAsync(); if (ActiveTab is not null) await CaptureActiveStateAsync();
+10 -2
View File
@@ -96,8 +96,9 @@
</Window.Resources> </Window.Resources>
<DockPanel> <DockPanel>
<StackPanel x:Name="TopPanel" DockPanel.Dock="Top">
<!-- Tab strip --> <!-- Tab strip -->
<Border DockPanel.Dock="Top" Background="{StaticResource HeaderBg}" Padding="12,10,12,8" BorderBrush="{StaticResource BorderDark}" BorderThickness="0,0,0,1"> <Border Background="{StaticResource HeaderBg}" Padding="12,10,12,8" BorderBrush="{StaticResource BorderDark}" BorderThickness="0,0,0,1">
<DockPanel> <DockPanel>
<StackPanel DockPanel.Dock="Left" Orientation="Horizontal" VerticalAlignment="Center" Margin="2,0,18,0"> <StackPanel DockPanel.Dock="Left" Orientation="Horizontal" VerticalAlignment="Center" Margin="2,0,18,0">
<Border Width="32" Height="32" CornerRadius="9" Background="{StaticResource AurBlueSoft}"> <Border Width="32" Height="32" CornerRadius="9" Background="{StaticResource AurBlueSoft}">
@@ -109,6 +110,11 @@
ItemsSource="{Binding Tabs}" ItemsSource="{Binding Tabs}"
ItemTemplate="{StaticResource TabTemplate}" ItemTemplate="{StaticResource TabTemplate}"
SelectionChanged="TabStrip_SelectionChanged" SelectionChanged="TabStrip_SelectionChanged"
AllowDrop="True"
PreviewMouseLeftButtonDown="TabStrip_PreviewMouseLeftButtonDown"
PreviewMouseMove="TabStrip_PreviewMouseMove"
DragOver="TabStrip_DragOver"
Drop="TabStrip_Drop"
Background="Transparent" Background="Transparent"
BorderThickness="0" BorderThickness="0"
Padding="0" Padding="0"
@@ -121,7 +127,7 @@
</Border> </Border>
<!-- Browser controls --> <!-- Browser controls -->
<Border DockPanel.Dock="Top" Background="{StaticResource HeaderBgElevated}" Padding="12,9" BorderBrush="{StaticResource BorderDark}" BorderThickness="0,0,0,1"> <Border Background="{StaticResource HeaderBgElevated}" Padding="12,9" BorderBrush="{StaticResource BorderDark}" BorderThickness="0,0,0,1">
<DockPanel LastChildFill="True"> <DockPanel LastChildFill="True">
<StackPanel DockPanel.Dock="Left" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,10,0"> <StackPanel DockPanel.Dock="Left" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,10,0">
<Button Content="←" ToolTip="Назад" Click="Back_Click" Style="{StaticResource IconButton}" /> <Button Content="←" ToolTip="Назад" Click="Back_Click" Style="{StaticResource IconButton}" />
@@ -187,6 +193,7 @@
</Popup> </Popup>
<Button Content="♫" ToolTip="Режим лише аудіо" Click="AudioOnly_Click" Height="32" Width="36" Padding="0" Margin="0,0,4,0" /> <Button Content="♫" ToolTip="Режим лише аудіо" Click="AudioOnly_Click" Height="32" Width="36" Padding="0" Margin="0,0,4,0" />
<Button Content="Розширення" ToolTip="Керування unpacked extensions" Click="Extensions_Click" Height="32" Padding="11,0" /> <Button Content="Розширення" ToolTip="Керування unpacked extensions" Click="Extensions_Click" Height="32" Padding="11,0" />
<Button Content="⌨" ToolTip="Клавіатурні скорочення" Click="Shortcuts_Click" Height="32" Width="36" Padding="0" Margin="4,0,0,0" />
</StackPanel> </StackPanel>
<Grid VerticalAlignment="Center"> <Grid VerticalAlignment="Center">
@@ -199,6 +206,7 @@
</Grid> </Grid>
</DockPanel> </DockPanel>
</Border> </Border>
</StackPanel>
<!-- Web content --> <!-- Web content -->
<Grid x:Name="WebViewHost" Background="#0B0E12"> <Grid x:Name="WebViewHost" Background="#0B0E12">
+96 -1
View File
@@ -11,6 +11,11 @@ using Microsoft.Web.WebView2.Core;
using FoxBrowser.Core; using FoxBrowser.Core;
using FoxBrowser.Native; using FoxBrowser.Native;
using FoxBrowser.UI; using FoxBrowser.UI;
using WpfDragDropEffects = System.Windows.DragDropEffects;
using WpfDragEventArgs = System.Windows.DragEventArgs;
using WpfKeyEventArgs = System.Windows.Input.KeyEventArgs;
using WpfMouseEventArgs = System.Windows.Input.MouseEventArgs;
using WpfPoint = System.Windows.Point;
namespace FoxBrowser; namespace FoxBrowser;
@@ -24,6 +29,9 @@ public partial class MainWindow : Window
private AudioSessionLabeler? _audioLabeler; private AudioSessionLabeler? _audioLabeler;
private bool _updatingDarkModeMenu; private bool _updatingDarkModeMenu;
private AppSettings _appSettings; private AppSettings _appSettings;
private WpfPoint _tabDragStartPoint;
private BrowserTab? _tabDragItem;
private bool _topPanelVisible = true;
private const int WmNcRButtonDown = 0x00A4; private const int WmNcRButtonDown = 0x00A4;
private const int WmNcRButtonUp = 0x00A5; private const int WmNcRButtonUp = 0x00A5;
@@ -34,6 +42,7 @@ public partial class MainWindow : Window
InitializeComponent(); InitializeComponent();
_appSettings = AppSettingsStore.Load(); _appSettings = AppSettingsStore.Load();
ApplyAppIdentity(_appSettings); ApplyAppIdentity(_appSettings);
PreviewKeyDown += MainWindow_PreviewKeyDown;
SourceInitialized += MainWindow_SourceInitialized; SourceInitialized += MainWindow_SourceInitialized;
Loaded += MainWindow_Loaded; Loaded += MainWindow_Loaded;
} }
@@ -75,6 +84,16 @@ public partial class MainWindow : Window
popup.IsOpen = false; popup.IsOpen = false;
OpenAppSettings(); OpenAppSettings();
}; };
var shortcutsItem = new System.Windows.Controls.Button
{
Content = "Клавіатурні скорочення…",
Style = (Style)FindResource("FoxMenuButton")
};
shortcutsItem.Click += (_, _) =>
{
popup.IsOpen = false;
Shortcuts_Click(this, new RoutedEventArgs());
};
popup.Child = new Border popup.Child = new Border
{ {
Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(23, 27, 34)), Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(23, 27, 34)),
@@ -82,7 +101,10 @@ public partial class MainWindow : Window
BorderThickness = new Thickness(1), BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(8), CornerRadius = new CornerRadius(8),
Padding = new Thickness(8), Padding = new Thickness(8),
Child = item Child = new StackPanel
{
Children = { item, shortcutsItem }
}
}; };
popup.IsOpen = true; popup.IsOpen = true;
} }
@@ -302,6 +324,79 @@ public partial class MainWindow : Window
window.ShowDialog(); window.ShowDialog();
} }
private void Shortcuts_Click(object sender, RoutedEventArgs e)
{
var window = new ShortcutSettingsWindow { Owner = this };
window.ShowDialog();
}
private async void MainWindow_PreviewKeyDown(object sender, WpfKeyEventArgs e)
{
var key = e.Key == Key.System ? e.SystemKey : e.Key;
var action = ShortcutManager.Instance.FindAction(key, Keyboard.Modifiers);
if (action is null) return;
e.Handled = true;
switch (action)
{
case ShortcutManager.CloseTab when _tabManager?.ActiveTab is not null:
await _tabManager.CloseTabAsync(_tabManager.ActiveTab);
RefreshAddressBar();
break;
case ShortcutManager.NewTab when _tabManager is not null:
await _tabManager.AddTabAsync();
RefreshAddressBar();
break;
case ShortcutManager.ToggleTopPanel:
_topPanelVisible = !_topPanelVisible;
TopPanel.Visibility = _topPanelVisible ? Visibility.Visible : Visibility.Collapsed;
break;
}
}
private void TabStrip_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_tabDragStartPoint = e.GetPosition(TabStrip);
_tabDragItem = FindTabItem(e.OriginalSource as DependencyObject)?.DataContext as BrowserTab;
}
private void TabStrip_PreviewMouseMove(object sender, WpfMouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed || _tabDragItem is null) return;
var position = e.GetPosition(TabStrip);
if (Math.Abs(position.X - _tabDragStartPoint.X) < SystemParameters.MinimumHorizontalDragDistance &&
Math.Abs(position.Y - _tabDragStartPoint.Y) < SystemParameters.MinimumVerticalDragDistance) return;
var item = _tabDragItem;
_tabDragItem = null;
System.Windows.DragDrop.DoDragDrop(TabStrip, item, WpfDragDropEffects.Move);
}
private void TabStrip_DragOver(object sender, WpfDragEventArgs e)
{
e.Effects = e.Data.GetDataPresent(typeof(BrowserTab)) ? WpfDragDropEffects.Move : WpfDragDropEffects.None;
e.Handled = true;
}
private void TabStrip_Drop(object sender, WpfDragEventArgs e)
{
if (_tabManager is null || !e.Data.GetDataPresent(typeof(BrowserTab))) return;
var source = e.Data.GetData(typeof(BrowserTab)) as BrowserTab;
var target = FindTabItem(e.OriginalSource as DependencyObject)?.DataContext as BrowserTab;
if (source is null || target is null || ReferenceEquals(source, target)) return;
var targetIndex = _tabManager.Tabs.IndexOf(target);
if (targetIndex >= 0) _tabManager.MoveTab(source, targetIndex);
e.Handled = true;
}
private static TabItem? FindTabItem(DependencyObject? element)
{
while (element is not null)
{
if (element is TabItem tabItem) return tabItem;
element = System.Windows.Media.VisualTreeHelper.GetParent(element);
}
return null;
}
private void PinnedExtension_Click(object sender, RoutedEventArgs e) private void PinnedExtension_Click(object sender, RoutedEventArgs e)
{ {
if (_tabManager?.ExtensionManager is null || sender is not System.Windows.Controls.Button button || button.DataContext is not PinnedExtensionItem item) if (_tabManager?.ExtensionManager is null || sender is not System.Windows.Controls.Button button || button.DataContext is not PinnedExtensionItem item)
+6 -1
View File
@@ -47,7 +47,12 @@ public partial class AppSettingsWindow : Window
if (name.Length == 0) name = "FoxBrowser"; if (name.Length == 0) name = "FoxBrowser";
try try
{ {
var settings = new AppSettings { AppDisplayName = name, AppIconPath = _iconPath }; var settings = new AppSettings
{
AppDisplayName = name,
AppIconPath = _iconPath,
Shortcuts = ShortcutManager.Instance.Shortcuts.ToDictionary(pair => pair.Key, pair => ShortcutManager.Format(pair.Value))
};
AppSettingsStore.Save(settings); AppSettingsStore.Save(settings);
_apply(settings); _apply(settings);
DialogResult = true; DialogResult = true;
+12 -4
View File
@@ -11,6 +11,7 @@ public partial class ExtensionPopupWindow : Window
private readonly CoreWebView2Environment _environment; private readonly CoreWebView2Environment _environment;
private readonly string _extensionId; private readonly string _extensionId;
private readonly string _popupPage; private readonly string _popupPage;
private WebView2? _view;
public ExtensionPopupWindow( public ExtensionPopupWindow(
CoreWebView2Environment environment, CoreWebView2Environment environment,
@@ -47,14 +48,21 @@ public partial class ExtensionPopupWindow : Window
{ {
try try
{ {
var view = new WebView2(); _view = new WebView2();
PopupHost.Children.Add(view); PopupHost.Children.Add(_view);
await view.EnsureCoreWebView2Async(_environment); await _view.EnsureCoreWebView2Async(_environment);
view.CoreWebView2.Navigate($"chrome-extension://{_extensionId}/{_popupPage}"); _view.CoreWebView2.Navigate($"chrome-extension://{_extensionId}/{_popupPage}");
} }
catch catch
{ {
Close(); Close();
} }
} }
protected override void OnClosed(EventArgs e)
{
_view?.Dispose();
_view = null;
base.OnClosed(e);
}
} }
+21
View File
@@ -0,0 +1,21 @@
<Window x:Class="FoxBrowser.UI.ShortcutSettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Клавіатурні скорочення" Width="580" Height="330"
MinWidth="520" Background="#15171A" Foreground="#F2F4F7"
WindowStartupLocation="CenterOwner" ResizeMode="NoResize">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="Клавіатурні скорочення" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,8" />
<TextBlock Grid.Row="0" Text="Натисніть «Змінити», потім потрібну комбінацію Ctrl/Shift/Alt + клавіша." Foreground="#AEB4BE" Margin="0,30,0,12" />
<StackPanel x:Name="ShortcutList" Grid.Row="1" />
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,14,0,0">
<Button Content="Скинути до стандартних" Click="Reset_Click" Padding="10,6" Margin="0,0,8,0" />
<Button Content="Закрити" Click="Close_Click" Padding="14,6" />
</StackPanel>
</Grid>
</Window>
+138
View File
@@ -0,0 +1,138 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using FoxBrowser.Core;
using WpfButton = System.Windows.Controls.Button;
using WpfColor = System.Windows.Media.Color;
using WpfKeyEventArgs = System.Windows.Input.KeyEventArgs;
using WpfTextBox = System.Windows.Controls.TextBox;
namespace FoxBrowser.UI;
public partial class ShortcutSettingsWindow : Window
{
private readonly ShortcutManager _shortcuts = ShortcutManager.Instance;
private readonly Dictionary<string, WpfTextBox> _boxes = new();
private string? _capturingAction;
private KeyGesture? _pendingGesture;
public ShortcutSettingsWindow()
{
InitializeComponent();
_shortcuts.Changed += Shortcuts_Changed;
BuildRows();
}
private void BuildRows()
{
ShortcutList.Children.Clear();
_boxes.Clear();
foreach (var action in new[] { ShortcutManager.CloseTab, ShortcutManager.NewTab, ShortcutManager.ToggleTopPanel })
{
var box = new WpfTextBox
{
Text = ShortcutManager.Format(_shortcuts.Get(action)),
IsReadOnly = true,
IsTabStop = false,
Width = 150,
Padding = new Thickness(7, 4, 7, 4),
Background = new SolidColorBrush(WpfColor.FromRgb(32, 35, 40)),
Foreground = new SolidColorBrush(WpfColor.FromRgb(242, 244, 247)),
BorderBrush = new SolidColorBrush(WpfColor.FromRgb(69, 76, 87)),
HorizontalContentAlignment = System.Windows.HorizontalAlignment.Center,
Tag = action,
};
var change = new WpfButton { Content = "Змінити", Padding = new Thickness(10, 5, 10, 5), Tag = action };
change.Click += Change_Click;
var row = new Grid { Margin = new Thickness(0, 0, 0, 8) };
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
row.Children.Add(new TextBlock { Text = ShortcutManager.GetActionLabel(action), VerticalAlignment = VerticalAlignment.Center });
Grid.SetColumn(box, 1);
Grid.SetColumn(change, 2);
change.Margin = new Thickness(8, 0, 0, 0);
row.Children.Add(box);
row.Children.Add(change);
ShortcutList.Children.Add(row);
_boxes[action] = box;
}
}
private void Change_Click(object sender, RoutedEventArgs e)
{
if (sender is not WpfButton button || button.Tag is not string action || !_boxes.TryGetValue(action, out var box)) return;
_capturingAction = action;
_pendingGesture = null;
box.Text = "Натисніть комбінацію…";
box.Focus();
box.PreviewKeyDown -= CaptureBox_PreviewKeyDown;
box.PreviewKeyDown += CaptureBox_PreviewKeyDown;
box.PreviewKeyUp -= CaptureBox_PreviewKeyUp;
box.PreviewKeyUp += CaptureBox_PreviewKeyUp;
}
private void CaptureBox_PreviewKeyDown(object sender, WpfKeyEventArgs e)
{
if (_capturingAction is null || sender is not WpfTextBox box) return;
var key = e.Key == Key.System ? e.SystemKey : e.Key;
if (key == Key.Escape)
{
CancelCapture(box);
e.Handled = true;
return;
}
var modifiers = Keyboard.Modifiers & (ModifierKeys.Control | ModifierKeys.Shift | ModifierKeys.Alt);
if (!ShortcutManager.TryCreateGesture(key, modifiers, out var gesture)) return;
_pendingGesture = gesture;
box.Text = ShortcutManager.Format(gesture);
e.Handled = true;
}
private void CaptureBox_PreviewKeyUp(object sender, WpfKeyEventArgs e)
{
if (_capturingAction is null || _pendingGesture is null || sender is not WpfTextBox box) return;
var action = _capturingAction;
if (!_shortcuts.TrySet(action, _pendingGesture, out var conflictAction))
{
System.Windows.MessageBox.Show(this, $"Комбінація вже призначена для «{ShortcutManager.GetActionLabel(conflictAction!)}». Оберіть іншу.", "Конфлікт шорткатів", MessageBoxButton.OK, MessageBoxImage.Warning);
box.Text = ShortcutManager.Format(_shortcuts.Get(action));
}
StopCapture(box);
}
private void CancelCapture(WpfTextBox box)
{
box.Text = ShortcutManager.Format(_shortcuts.Get(_capturingAction!));
StopCapture(box);
}
private void StopCapture(WpfTextBox box)
{
box.PreviewKeyDown -= CaptureBox_PreviewKeyDown;
box.PreviewKeyUp -= CaptureBox_PreviewKeyUp;
_capturingAction = null;
_pendingGesture = null;
Keyboard.ClearFocus();
}
private void Shortcuts_Changed(object? sender, EventArgs e)
{
if (!Dispatcher.CheckAccess())
{
Dispatcher.BeginInvoke(BuildRows);
return;
}
BuildRows();
}
private void Reset_Click(object sender, RoutedEventArgs e) => _shortcuts.ResetToDefaults();
private void Close_Click(object sender, RoutedEventArgs e) => Close();
protected override void OnClosed(EventArgs e)
{
_shortcuts.Changed -= Shortcuts_Changed;
base.OnClosed(e);
}
}