using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Threading; using DodoSSH.Client.Shell.ViewModels; namespace DodoSSH.Client.App.Views; /// The empty terminal's prompt: what to connect to, and its password when one is needed. /// /// The keys and the caret live here, as the palette's do in and for its reason: a /// hosts in a bare window under the headless harness, and the window itself cannot /// be shown there at all. /// internal sealed partial class ConnectPrompt : UserControl { public ConnectPrompt() { InitializeComponent(); // Tunnelled, for the reason QuickConnect gives: the prompt owns Enter, Escape and Tab while the caret is // in it, and a text box on the route must not get to them first. AddHandler(KeyDownEvent, OnPromptKey, RoutingStrategies.Tunnel); // The password line appearing, or going, moves the caret with it: Enter on a name that needs a password // should leave somebody typing the password, and Escape off it should leave them editing the name. PasswordLine.PropertyChanged += OnLineChanged; } /// The first line's box, for the caret and for the tests. internal TextBox TargetBox => Target; /// The password line's box, for the caret and for the tests. internal TextBox PasswordBox => Password; private MainWindowViewModel? Shell => DataContext as MainWindowViewModel; /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); // Posted, for the reason QuickConnect.TakeKeyboard gives at length: at the instant this becomes // visible its boxes are not in the visual tree yet, and focus is refused to anything that is not. if (change.Property == IsVisibleProperty && change.GetNewValue()) { Dispatcher.UIThread.Post(TakeKeyboard, DispatcherPriority.Loaded); } } private void OnLineChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { if (e.Property == IsVisibleProperty && IsVisible) { Dispatcher.UIThread.Post(TakeKeyboard, DispatcherPriority.Loaded); } } /// Puts the caret on whichever line is asking. /// /// Both halves, for the reason gives: the terminal's WebView can still /// hold the native window's focus after it has been collapsed, and an Avalonia focus without the native /// call is a caret that receives nothing. /// private void TakeKeyboard() { if (!IsEffectivelyVisible) { return; } if (TopLevel.GetTopLevel(this) is Window window) { NativeKeyboardFocus.ReturnTo(window); } if (PasswordLine.IsVisible) { Password.Focus(); } else { Target.Focus(); Target.CaretIndex = Target.Text?.Length ?? 0; } } private void OnPromptKey(object? sender, KeyEventArgs e) { if (Shell is not { IsConnectPromptShowing: true } shell || e.KeyModifiers != KeyModifiers.None) { return; } switch (e.Key) { case Key.Enter: // Fire-and-forget, as the palette's is: the command reports its own refusals on the prompt, // and a connection's progress and failure in the tab it opens. _ = shell.SubmitPromptCommand.ExecuteAsync(null); e.Handled = true; break; case Key.Escape: shell.CancelPromptCommand.Execute(null); e.Handled = true; break; case Key.Tab: // Only when there is something to complete to; otherwise Tab moves the focus, as it always does. e.Handled = shell.CompletePrompt(); if (e.Handled) { Target.CaretIndex = Target.Text?.Length ?? 0; } break; default: break; } } }