Public Access
Merge branch 'main' into the Android head
Main grew the screens the host-management plan called for — hosts, pins, snippets, logs, import, teams — plus the ObjectStore and Import projects behind two of them, and moved WindowsDeviceKeyStore into the desktop head's Platform folder. Five of those view models landed in a directory this branch had already moved, so they join the rest in DodoSSH.Client.Shell: git spotted the rename and put them there, and the namespaces followed. Shell picks up ObjectStore and Import as a result, which the Android head then gets transitively and will use neither of at first — scoped storage means there is no ~/.ssh/config to import, and file transfer is out of its first scope. Desktop suites green at 155 and 64.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System.ComponentModel;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Threading;
|
||||
using DodoSSH.Client.Shell.ViewModels;
|
||||
|
||||
namespace DodoSSH.Client.App.Views;
|
||||
@@ -66,14 +67,56 @@ internal sealed partial class MainWindow : Window
|
||||
/// keyboard nowhere: focus does not stay where it was, because collapsing the control it was on clears
|
||||
/// it outright, and the fallback's own <c>Focus()</c> call was failing silently.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The terminal answers first, and it has to, because <see cref="MainWindowViewModel.Screen"/> still
|
||||
/// names a page while a terminal is showing — that is the point of it. Asking the screen would hand the
|
||||
/// keyboard to a host list nobody can see.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private IInputElement KeyboardHome => shell?.Screen switch
|
||||
private IInputElement KeyboardHome => shell switch
|
||||
{
|
||||
ShellScreen.Vault => VaultPane.KeyboardTarget,
|
||||
ShellScreen.Hosts => Hosts.KeyboardTarget,
|
||||
{ IsTerminalShowing: true } => Terminal,
|
||||
{ Screen: ShellScreen.Vault } => VaultPane.KeyboardTarget,
|
||||
{ Screen: ShellScreen.Hosts } => HostsPane.KeyboardTarget,
|
||||
{ Screen: ShellScreen.KnownHosts } => PinsPane.KeyboardTarget,
|
||||
{ Screen: ShellScreen.Import } => ImportPane.KeyboardTarget,
|
||||
{ Screen: ShellScreen.Snippets } => SnippetsPane.KeyboardTarget,
|
||||
{ Screen: ShellScreen.Logs } => LogsPane.KeyboardTarget,
|
||||
_ => this,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Asks for the terminal to take the keyboard, once layout has run.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Posted, not called.</b> Every path that reaches here has revealed the WebView in this same turn —
|
||||
/// a session opened from another screen, a tab clicked while a page was showing, the palette closing
|
||||
/// back onto a terminal. <c>NativeControlHost</c> re-pushes its bounds on the next layout pass, so
|
||||
/// focusing microseconds ahead of that pass races exactly the thing the focus depends on, and the
|
||||
/// symptom is silent: a terminal that looks selected and receives nothing until it is clicked.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <c>DispatcherPriority.Loaded</c> runs after layout. It is the same fix and the same reasoning as
|
||||
/// <see cref="QuickConnect"/>'s, which posts its own focus for the same race in the other direction.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Re-checked inside the post rather than trusted from outside it, because a turn is long enough for the
|
||||
/// user to have navigated away — closing the last tab, or clicking the rail — and stealing the keyboard
|
||||
/// into a collapsed WebView would leave the window with nothing focused at all.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private void FocusTerminalWhenLaidOut() =>
|
||||
Dispatcher.UIThread.Post(
|
||||
() =>
|
||||
{
|
||||
if (shell is { IsTerminalShowing: true })
|
||||
{
|
||||
Terminal.Focus();
|
||||
}
|
||||
},
|
||||
DispatcherPriority.Loaded);
|
||||
|
||||
/// <summary>
|
||||
/// Where the keyboard belongs once the vault is no longer open.
|
||||
/// </summary>
|
||||
@@ -160,14 +203,19 @@ internal sealed partial class MainWindow : Window
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// A bare <c>Focus()</c> is the whole fix in this direction: <c>NativeWebView.OnGotFocus</c> pushes
|
||||
/// Win32 focus into WebView2 for us. It has to happen while the control is visible, which it is —
|
||||
/// a session can only be opened from the hosts screen of an unlocked vault, and that is exactly the
|
||||
/// state in which the terminal is showing. Focus() on a collapsed control is measurably a no-op and is
|
||||
/// not replayed when it is revealed.
|
||||
/// <c>NativeWebView.OnGotFocus</c> pushes Win32 focus into WebView2 for us, so a <c>Focus()</c> call is
|
||||
/// the whole fix in this direction — but it has to happen while the control is visible, and it no longer
|
||||
/// reliably is at this instant. A session can now be opened from any screen, so this event routinely
|
||||
/// arrives in the same turn that revealed the WebView. Hence the post; see
|
||||
/// <see cref="FocusTerminalWhenLaidOut"/>.
|
||||
/// </remarks>
|
||||
private void OnTerminalSessionOpened(object? sender, EventArgs e) => Terminal.Focus();
|
||||
private void OnTerminalSessionOpened(object? sender, EventArgs e) => FocusTerminalWhenLaidOut();
|
||||
|
||||
/// <remarks>
|
||||
/// A dispatch and nothing else. Every arm below is a separate decision about where the keyboard goes,
|
||||
/// and they were one method until the four of them stopped fitting in a screenful — which is roughly the
|
||||
/// point at which "does this one return early" stops being obvious to a reader.
|
||||
/// </remarks>
|
||||
private void OnShellPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (shell is not { } viewModel)
|
||||
@@ -175,54 +223,117 @@ internal sealed partial class MainWindow : Window
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(e.PropertyName, nameof(MainWindowViewModel.IsUnlocked), StringComparison.Ordinal))
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
var unlocked = viewModel.IsUnlocked;
|
||||
case nameof(MainWindowViewModel.IsUnlocked):
|
||||
OnVaultOpenedOrClosed(viewModel);
|
||||
break;
|
||||
|
||||
// Only the transition out of unlocked matters. IsUnlocked is re-raised for every shell state
|
||||
// change, and reacting to all of them would move focus during setup and sign-in.
|
||||
if (wasUnlocked && !unlocked)
|
||||
{
|
||||
ReleaseKeyboardTo(ClosedVaultKeyboardHome);
|
||||
}
|
||||
case nameof(MainWindowViewModel.IsSearching):
|
||||
OnPaletteToggled(viewModel);
|
||||
break;
|
||||
|
||||
wasUnlocked = unlocked;
|
||||
// One arm for both, deliberately. They mean the same thing to this handler — what the window is
|
||||
// showing may have changed — and answering them separately would make the order of two
|
||||
// PropertyChanged raises decide the outcome. Connecting from the palette moves both.
|
||||
case nameof(MainWindowViewModel.Surface):
|
||||
case nameof(MainWindowViewModel.Screen):
|
||||
OnShowingSomethingElse(viewModel);
|
||||
break;
|
||||
|
||||
case nameof(MainWindowViewModel.SelectedTab):
|
||||
OnSelectedTabChanged(viewModel);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnVaultOpenedOrClosed(MainWindowViewModel viewModel)
|
||||
{
|
||||
var unlocked = viewModel.IsUnlocked;
|
||||
|
||||
// Only the transition out of unlocked matters. IsUnlocked is re-raised for every shell state
|
||||
// change, and reacting to all of them would move focus during setup and sign-in.
|
||||
if (wasUnlocked && !unlocked)
|
||||
{
|
||||
ReleaseKeyboardTo(ClosedVaultKeyboardHome);
|
||||
}
|
||||
|
||||
wasUnlocked = unlocked;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Closing only. Opening also has to move the keyboard — the palette is a text box somebody is expected
|
||||
/// to start typing into immediately — but the palette does that for itself when it becomes visible,
|
||||
/// which is a moment this handler is measurably ahead of: it runs from the view model's
|
||||
/// <c>PropertyChanged</c>, before the binding that reveals the control, and <c>Focus()</c> on a control
|
||||
/// that is still collapsed is a no-op that is not replayed when it is revealed.
|
||||
/// </remarks>
|
||||
private void OnPaletteToggled(MainWindowViewModel viewModel)
|
||||
{
|
||||
if (viewModel.IsSearching)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Closing only. Opening also has to move the keyboard — the palette is a text box somebody is
|
||||
// expected to start typing into immediately — but the palette does that for itself when it becomes
|
||||
// visible, which is a moment this handler is measurably ahead of: it runs from the view model's
|
||||
// PropertyChanged, before the binding that reveals the control, and Focus() on a control that is
|
||||
// still collapsed is a no-op that is not replayed when it is revealed.
|
||||
if (string.Equals(e.PropertyName, nameof(MainWindowViewModel.IsSearching), StringComparison.Ordinal))
|
||||
// Closing the palette over a terminal reveals the WebView in this same turn, so it needs the posted
|
||||
// focus rather than the immediate one.
|
||||
if (viewModel.IsTerminalShowing)
|
||||
{
|
||||
if (!viewModel.IsSearching)
|
||||
{
|
||||
ReleaseKeyboardTo(KeyboardHome);
|
||||
}
|
||||
FocusTerminalWhenLaidOut();
|
||||
}
|
||||
else
|
||||
{
|
||||
ReleaseKeyboardTo(KeyboardHome);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the keyboard when the window swaps a page for a terminal, or one page for another.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The most common gesture in the window now that the strip spans every screen: a tab and a rail entry
|
||||
/// are both one click away at all times.
|
||||
/// <para>
|
||||
/// <c>ReleaseKeyboardTo</c>, not <c>Focus()</c>, in the page direction — and that is the whole of why
|
||||
/// this method is worth reading. <b>Collapsing the WebView does not release the keyboard.</b> The native
|
||||
/// child window goes on holding Win32 focus, Avalonia then sees no key events at all, and the screen
|
||||
/// that just appeared silently swallows every keystroke. It was a latent defect while leaving a terminal
|
||||
/// was rare; it is the hot path now. See <c>docs/platform-flags.md</c>, and
|
||||
/// <see cref="NativeKeyboardFocus"/> for why only one direction needs the Win32 call.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private void OnShowingSomethingElse(MainWindowViewModel viewModel)
|
||||
{
|
||||
if (!viewModel.IsUnlocked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Switching screens moves the keyboard to whatever the new screen offers, for the same reason:
|
||||
// leaving it on a control that has just been collapsed leaves the window with nothing focused.
|
||||
if (string.Equals(e.PropertyName, nameof(MainWindowViewModel.Screen), StringComparison.Ordinal)
|
||||
&& viewModel.IsUnlocked)
|
||||
if (viewModel.IsTerminalShowing)
|
||||
{
|
||||
KeyboardHome.Focus();
|
||||
return;
|
||||
FocusTerminalWhenLaidOut();
|
||||
}
|
||||
|
||||
// Clicking a tab moves both Win32 and Avalonia focus onto the button that was clicked — the click
|
||||
// is what took the WebView's Win32 focus away in the first place. term.focus() in the page only
|
||||
// ever reaches document.activeElement, which does nothing for a page that no longer holds the
|
||||
// native focus, so without this the pane looks selected and every keystroke goes to the button
|
||||
// instead of the shell until the user clicks inside the terminal by hand.
|
||||
if (string.Equals(e.PropertyName, nameof(MainWindowViewModel.SelectedTab), StringComparison.Ordinal)
|
||||
&& viewModel.SelectedTab is not null && viewModel.IsTerminalShowing)
|
||||
else
|
||||
{
|
||||
Terminal.Focus();
|
||||
ReleaseKeyboardTo(KeyboardHome);
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Clicking a tab moves both Win32 and Avalonia focus onto the button that was clicked — the click is
|
||||
/// what took the WebView's Win32 focus away in the first place. <c>term.focus()</c> in the page only
|
||||
/// ever reaches <c>document.activeElement</c>, which does nothing for a page that no longer holds the
|
||||
/// native focus, so without this the pane looks selected and every keystroke goes to the button instead
|
||||
/// of the shell until the user clicks inside the terminal by hand.
|
||||
/// </remarks>
|
||||
private void OnSelectedTabChanged(MainWindowViewModel viewModel)
|
||||
{
|
||||
if (viewModel.SelectedTab is not null && viewModel.IsTerminalShowing)
|
||||
{
|
||||
FocusTerminalWhenLaidOut();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user