Public Access
The terminal surface with no session used to be an empty frame, with the tab row's "+" the only thing on it that did anything. It now holds a prompt, in the order ssh asks: "connect to:" takes a saved host's name, matched exactly, with the hosts that match so far listed under it for Tab or a click, or user@host[:port] for a machine that is not saved. A password line follows only when one is needed, worded as ssh words it. Escape steps back a line; recent connections are listed below. It is drawn by Avalonia in the WebView's rectangle, which the occlusion rule allows because the two are exclusive: the prompt shows only while there are no tabs, the WebView only for a tab with a session. Saved hosts connect through a new VaultViewModel.ConnectToHostAsync, which the drawer's own connect now goes through too, so every refusal is still made once. Each attempt now carries whether it may act on the drawer's remember-password tick, and the prompt's may not: otherwise a tick left on in a closed drawer would save a password typed at a prompt that never offered to keep anything. Covered by nine ShellFlowTests cases and four ConnectPromptTests; manual check 7.10b.
126 lines
4.3 KiB
C#
126 lines
4.3 KiB
C#
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Threading;
|
|
using DodoSSH.Client.Shell.ViewModels;
|
|
|
|
namespace DodoSSH.Client.App.Views;
|
|
|
|
/// <summary>The empty terminal's prompt: what to connect to, and its password when one is needed.</summary>
|
|
/// <remarks>
|
|
/// The keys and the caret live here, as the palette's do in <see cref="QuickConnect"/> and for its reason: a
|
|
/// <see cref="UserControl"/> hosts in a bare window under the headless harness, and the window itself cannot
|
|
/// be shown there at all.
|
|
/// </remarks>
|
|
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;
|
|
}
|
|
|
|
/// <summary>The first line's box, for the caret and for the tests.</summary>
|
|
internal TextBox TargetBox => Target;
|
|
|
|
/// <summary>The password line's box, for the caret and for the tests.</summary>
|
|
internal TextBox PasswordBox => Password;
|
|
|
|
private MainWindowViewModel? Shell => DataContext as MainWindowViewModel;
|
|
|
|
/// <inheritdoc />
|
|
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<bool>())
|
|
{
|
|
Dispatcher.UIThread.Post(TakeKeyboard, DispatcherPriority.Loaded);
|
|
}
|
|
}
|
|
|
|
private void OnLineChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
|
|
{
|
|
if (e.Property == IsVisibleProperty && IsVisible)
|
|
{
|
|
Dispatcher.UIThread.Post(TakeKeyboard, DispatcherPriority.Loaded);
|
|
}
|
|
}
|
|
|
|
/// <summary>Puts the caret on whichever line is asking.</summary>
|
|
/// <remarks>
|
|
/// Both halves, for the reason <see cref="NativeKeyboardFocus"/> 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.
|
|
/// </remarks>
|
|
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;
|
|
}
|
|
}
|
|
}
|