Public Access
Terminal: ask what to connect to when Terminal is pressed with nothing open
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.
This commit is contained in:
1 parent
5b2e3f3651
commit
3c8c0980c2
9 files changed
+1154
-33
No files matched your search
@@ -0,0 +1,125 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user