Forge
csharpdeeb25a2
1using System.ComponentModel;
2using Avalonia.Controls;
3using Avalonia.Input;
4using Avalonia.Interactivity;
5using Avalonia.Threading;
6using CascadeIDE.Models.Shell;
7using CascadeIDE.ViewModels;
8
9namespace CascadeIDE.Views;
10
11public partial class CommandPaletteView : UserControl
12{
13 private INotifyPropertyChanged? _boundVm;
14 private Window? _hostWindow;
15 private IInputElement? _focusBeforeOpen;
16 private bool _wasVisible;
17
18 public CommandPaletteView()
19 {
20 InitializeComponent();
21 AttachedToVisualTree += OnAttachedToVisualTree;
22 DetachedFromVisualTree += OnDetachedFromVisualTree;
23 DataContextChanged += OnDataContextChanged;
24 AddHandler(InputElement.KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel, handledEventsToo: true);
25 }
26
27 private MainWindowViewModel? ViewModel => DataContext as MainWindowViewModel;
28
29 private TextBox? SearchBox => this.FindControl<TextBox>("SearchTextBox");
30
31 private void OnAttachedToVisualTree(object? sender, Avalonia.VisualTreeAttachmentEventArgs e)
32 {
33 AttachHostWindow();
34 BindVm();
35 UpdateOpenState();
36 }
37
38 private void OnDetachedFromVisualTree(object? sender, Avalonia.VisualTreeAttachmentEventArgs e)
39 {
40 DetachVm();
41 DetachHostWindow();
42 _focusBeforeOpen = null;
43 _wasVisible = false;
44 }
45
46 private void OnDataContextChanged(object? sender, EventArgs e)
47 {
48 BindVm();
49 UpdateOpenState();
50 }
51
52 private void BindVm()
53 {
54 if (ReferenceEquals(_boundVm, DataContext))
55 return;
56
57 DetachVm();
58
59 _boundVm = DataContext as INotifyPropertyChanged;
60 if (_boundVm is not null)
61 _boundVm.PropertyChanged += OnVmPropertyChanged;
62 }
63
64 private void DetachVm()
65 {
66 if (_boundVm is not null)
67 _boundVm.PropertyChanged -= OnVmPropertyChanged;
68 _boundVm = null;
69 }
70
71 private void AttachHostWindow()
72 {
73 var window = TopLevel.GetTopLevel(this) as Window;
74 if (ReferenceEquals(_hostWindow, window))
75 return;
76
77 DetachHostWindow();
78 _hostWindow = window;
79
80 if (_hostWindow is null)
81 return;
82
83 _hostWindow.Activated += OnHostWindowActivationChanged;
84 _hostWindow.Deactivated += OnHostWindowActivationChanged;
85 }
86
87 private void DetachHostWindow()
88 {
89 if (_hostWindow is null)
90 return;
91
92 _hostWindow.Activated -= OnHostWindowActivationChanged;
93 _hostWindow.Deactivated -= OnHostWindowActivationChanged;
94 _hostWindow = null;
95 }
96
97 private void OnHostWindowActivationChanged(object? sender, EventArgs e) => UpdateOpenState();
98
99 private void OnVmPropertyChanged(object? sender, PropertyChangedEventArgs e)
100 {
101 if (e.PropertyName is nameof(MainWindowViewModel.IsCommandPaletteOpen)
102 or nameof(MainWindowViewModel.CommandPaletteHost))
103 UpdateOpenState();
104 }
105
106 private void UpdateOpenState()
107 {
108 var vm = ViewModel;
109 var visible = vm?.IsCommandPaletteOpen == true
110 && vm.CommandPaletteHost == ResolveHost();
111 IsVisible = visible;
112
113 if (visible == _wasVisible)
114 return;
115
116 _wasVisible = visible;
117 if (visible)
118 {
119 _focusBeforeOpen = _hostWindow?.FocusManager?.GetFocusedElement();
120 Dispatcher.UIThread.Post(FocusSearchBox, DispatcherPriority.Input);
121 return;
122 }
123
124 var prev = _focusBeforeOpen;
125 _focusBeforeOpen = null;
126 Dispatcher.UIThread.Post(() =>
127 {
128 if (prev is Control control)
129 control.Focus();
130 }, DispatcherPriority.Background);
131 }
132
133 private void FocusSearchBox()
134 {
135 if (!IsVisible)
136 return;
137
138 if (SearchBox is not { } searchBox)
139 return;
140
141 // Иначе IME/ввод может «съедать» Ctrl+Shift+буква до routed KeyDown (см. hotkey-log: только модификаторы).
142 InputMethod.SetIsInputMethodEnabled(searchBox, false);
143 searchBox.Focus();
144 searchBox.SelectAll();
145 }
146
147 private CommandPaletteHost ResolveHost() =>
148 _hostWindow switch
149 {
150 MfdHostWindow => CommandPaletteHost.MfdHost,
151 PfdHostWindow => CommandPaletteHost.PfdHost,
152 PmSplitHostWindow => CommandPaletteHost.PmSplitHost,
153 _ => CommandPaletteHost.MainWindow,
154 };
155
156 private void OnPreviewKeyDown(object? sender, KeyEventArgs e)
157 {
158 var vm = ViewModel;
159 if (vm is null || !vm.IsCommandPaletteOpen)
160 return;
161
162 switch (e.Key)
163 {
164 case Key.Escape:
165 if (vm.CloseCommandPaletteCommand.CanExecute(null))
166 vm.CloseCommandPaletteCommand.Execute(null);
167 e.Handled = true;
168 break;
169 case Key.Enter:
170 if (vm.ExecuteCommandPaletteSelectionCommand.CanExecute(null))
171 vm.ExecuteCommandPaletteSelectionCommand.Execute(null);
172 e.Handled = true;
173 break;
174 case Key.Down:
175 if (vm.CommandPaletteMoveSelectionCommand.CanExecute(1))
176 vm.CommandPaletteMoveSelectionCommand.Execute(1);
177 e.Handled = true;
178 break;
179 case Key.Up:
180 if (vm.CommandPaletteMoveSelectionCommand.CanExecute(-1))
181 vm.CommandPaletteMoveSelectionCommand.Execute(-1);
182 e.Handled = true;
183 break;
184 case Key.PageDown:
185 if (vm.CommandPalettePageMoveCommand.CanExecute(1))
186 vm.CommandPalettePageMoveCommand.Execute(1);
187 e.Handled = true;
188 break;
189 case Key.PageUp:
190 if (vm.CommandPalettePageMoveCommand.CanExecute(-1))
191 vm.CommandPalettePageMoveCommand.Execute(-1);
192 e.Handled = true;
193 break;
194 }
195 }
196
197 private void OnDimmerPressed(object? sender, PointerPressedEventArgs e)
198 {
199 if (ViewModel?.CloseCommandPaletteCommand.CanExecute(null) == true)
200 ViewModel.CloseCommandPaletteCommand.Execute(null);
201 e.Handled = true;
202 }
203
204 private void OnPanelPressed(object? sender, PointerPressedEventArgs e)
205 {
206 FocusSearchBox();
207 e.Handled = true;
208 }
209}
210
View only · write via MCP/CIDE