Forge
csharp4405de34
1#nullable enable
2
3using System.ComponentModel;
4using Avalonia.Controls;
5using Avalonia.Input;
6using Avalonia.Interactivity;
7using Avalonia.Threading;
8using CascadeIDE.Features.Chat;
9using CascadeIDE.Features.Cockpit.Application;
10using CascadeIDE.Models.Shell;
11using CascadeIDE.ViewModels;
12
13namespace CascadeIDE.Views;
14
15/// <summary>
16/// Оверлей Cockpit Command Line для <c>primary=editor</c> (как <see cref="CommandPaletteView"/> / Ctrl+Q).
17/// </summary>
18public partial class CockpitCommandLineOverlayView : UserControl
19{
20 private CockpitCommandLineOverlayViewModel? _overlayVm;
21 private Window? _hostWindow;
22 private IInputElement? _focusBeforeOpen;
23 private bool _wasVisible;
24 private bool _commandLineHooksAttached;
25
26 public CockpitCommandLineOverlayView()
27 {
28 InitializeComponent();
29 AttachedToVisualTree += OnAttachedToVisualTree;
30 DetachedFromVisualTree += OnDetachedFromVisualTree;
31 DataContextChanged += OnDataContextChanged;
32 AddHandler(InputElement.KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel, handledEventsToo: true);
33 }
34
35 private CockpitCommandLineOverlayViewModel? OverlayVm => DataContext as CockpitCommandLineOverlayViewModel;
36
37 private void OnAttachedToVisualTree(object? sender, Avalonia.VisualTreeAttachmentEventArgs e)
38 {
39 AttachHostWindow();
40 BindOverlayVm();
41 EnsureCommandLineHooks();
42 UpdateOpenState();
43 }
44
45 private void OnDetachedFromVisualTree(object? sender, Avalonia.VisualTreeAttachmentEventArgs e)
46 {
47 DetachOverlayVm();
48 DetachHostWindow();
49 _focusBeforeOpen = null;
50 _wasVisible = false;
51 }
52
53 private void OnDataContextChanged(object? sender, EventArgs e)
54 {
55 BindOverlayVm();
56 UpdateOpenState();
57 }
58
59 private void BindOverlayVm()
60 {
61 if (ReferenceEquals(_overlayVm, OverlayVm))
62 return;
63
64 DetachOverlayVm();
65 _overlayVm = OverlayVm;
66 if (_overlayVm is not null)
67 _overlayVm.PropertyChanged += OnOverlayVmPropertyChanged;
68 }
69
70 private void DetachOverlayVm()
71 {
72 if (_overlayVm is not null)
73 _overlayVm.PropertyChanged -= OnOverlayVmPropertyChanged;
74 _overlayVm = null;
75 }
76
77 private void OnOverlayVmPropertyChanged(object? sender, PropertyChangedEventArgs e) =>
78 UpdateOpenState();
79
80 private void EnsureCommandLineHooks()
81 {
82 if (_commandLineHooksAttached || CommandLineTextBox is null)
83 return;
84
85 CommandLineTextBox.PropertyChanged += OnCommandLineTextBoxPropertyChanged;
86 _commandLineHooksAttached = true;
87 }
88
89 private void OnCommandLineTextBoxPropertyChanged(object? sender, Avalonia.AvaloniaPropertyChangedEventArgs e)
90 {
91 if (e.Property != TextBox.TextProperty && e.Property.Name != "CaretIndex")
92 return;
93
94 SyncCommandLineDraftToViewModel();
95 }
96
97 private void SyncCommandLineDraftToViewModel()
98 {
99 var overlay = OverlayVm;
100 if (overlay is null || CommandLineTextBox is null || !IsVisible)
101 return;
102
103 CockpitCommandLineDraftBridge.ApplyDraftFromView(
104 overlay.ChatPanel,
105 CommandLineTextBox.Text,
106 CommandLineTextBox.CaretIndex);
107 }
108
109 private void SyncCommandLineFromViewModel()
110 {
111 var overlay = OverlayVm;
112 if (overlay is null || CommandLineTextBox is null)
113 return;
114
115 CockpitCommandLineDraftBridge.ApplyDraftFromViewModel(
116 overlay.ChatPanel,
117 text => CommandLineTextBox.Text = text,
118 caret => CommandLineTextBox.CaretIndex = caret);
119 }
120
121 private void AttachHostWindow()
122 {
123 var window = TopLevel.GetTopLevel(this) as Window;
124 if (ReferenceEquals(_hostWindow, window))
125 return;
126
127 DetachHostWindow();
128 _hostWindow = window;
129
130 if (_hostWindow is null)
131 return;
132
133 _hostWindow.Activated += OnHostWindowActivationChanged;
134 _hostWindow.Deactivated += OnHostWindowActivationChanged;
135 }
136
137 private void DetachHostWindow()
138 {
139 if (_hostWindow is null)
140 return;
141
142 _hostWindow.Activated -= OnHostWindowActivationChanged;
143 _hostWindow.Deactivated -= OnHostWindowActivationChanged;
144 _hostWindow = null;
145 }
146
147 private void OnHostWindowActivationChanged(object? sender, EventArgs e) => UpdateOpenState();
148
149 private void UpdateOpenState()
150 {
151 var overlay = OverlayVm;
152 var visible = overlay?.IsOverlayVisible(ResolveHost()) == true;
153 IsVisible = visible;
154
155 if (visible == _wasVisible)
156 return;
157
158 _wasVisible = visible;
159 if (visible)
160 {
161 _focusBeforeOpen = _hostWindow?.FocusManager?.GetFocusedElement();
162 Dispatcher.UIThread.Post(FocusCommandLineBox, DispatcherPriority.Input);
163 return;
164 }
165
166 var prev = _focusBeforeOpen;
167 _focusBeforeOpen = null;
168 Dispatcher.UIThread.Post(() =>
169 {
170 if (prev is Control control)
171 control.Focus();
172 }, DispatcherPriority.Background);
173 }
174
175 private void FocusCommandLineBox()
176 {
177 if (!IsVisible)
178 return;
179
180 if (CommandLineTextBox is not { } box)
181 return;
182
183 InputMethod.SetIsInputMethodEnabled(box, false);
184 SyncCommandLineFromViewModel();
185 box.Focus();
186 SyncCommandLineDraftToViewModel();
187 }
188
189 private CommandPaletteHost ResolveHost() =>
190 _hostWindow switch
191 {
192 MfdHostWindow => CommandPaletteHost.MfdHost,
193 PfdHostWindow => CommandPaletteHost.PfdHost,
194 PmSplitHostWindow => CommandPaletteHost.PmSplitHost,
195 _ => CommandPaletteHost.MainWindow,
196 };
197
198 private void OnPreviewKeyDown(object? sender, KeyEventArgs e)
199 {
200 var overlay = OverlayVm;
201 if (overlay is null || !IsVisible)
202 return;
203
204 var kind = e.Key switch
205 {
206 Key.Escape => IntercomComposerKeyKind.Escape,
207 Key.Enter => IntercomComposerKeyKind.Enter,
208 Key.Up => IntercomComposerKeyKind.SlashUp,
209 Key.Down => IntercomComposerKeyKind.SlashDown,
210 Key.Tab => IntercomComposerKeyKind.Tab,
211 _ => (IntercomComposerKeyKind?)null,
212 };
213
214 if (kind is null)
215 return;
216
217 var result = overlay.ChatPanel.TryHandleSlashComposerKey(kind.Value, e);
218 if (!result.Handled)
219 return;
220
221 e.Handled = true;
222
223 if (result.SyncCommandLineFromViewModel)
224 SyncCommandLineFromViewModel();
225
226 if (result.RunCockpitCommit)
227 _ = overlay.ChatPanel.TryCommitCockpitCommandLineAsync();
228 }
229
230 private void OnDimmerPressed(object? sender, PointerPressedEventArgs e)
231 {
232 OverlayVm?.ChatPanel.CommandLineSession.Close();
233 e.Handled = true;
234 }
235
236 private void OnPanelPressed(object? sender, PointerPressedEventArgs e)
237 {
238 FocusCommandLineBox();
239 e.Handled = true;
240 }
241}
242
View only · write via MCP/CIDE