Public Access
Hand Android's own focus back to the terminal after an accessory key
Focusable=false was only ever half the fix, and its remark now says so: Avalonia's focus stays on the NativeWebView, but the touch that presses a key still hands Android's native focus to Avalonia's input view — the platform moves it before Avalonia decides anything. The WebView's input connection dies with it, the keyboard swaps to its no-input layout, and the inset churn parks it over the very row that was tapped. Each key now returns that focus once its byte is on the wire, through a sibling of SoftKeyboard that walks the decor view to the one WebView this application has. Free when nothing moved. Check 11.10a is the phone-in-hand proof.
This commit is contained in:
@@ -1745,6 +1745,21 @@ is a terminal that answers the buttons and ignores the keyboard: it reads as the
|
|||||||
Worth doing on the software keyboard too, where the same fault shows as the keyboard closing on the first
|
Worth doing on the software keyboard too, where the same fault shows as the keyboard closing on the first
|
||||||
tap of an arrow key.
|
tap of an arrow key.
|
||||||
|
|
||||||
|
### 11.10a The accessory keys do not cost the terminal its *software* keyboard either
|
||||||
|
|
||||||
|
With a shell open and the software keyboard up, tap **esc**, **tab** or an arrow on the accessory row, then
|
||||||
|
keep typing on the software keyboard.
|
||||||
|
|
||||||
|
**Pass:** the keyboard does not change — not its layout, not its suggestion strip, not its height — and
|
||||||
|
everything typed after the tap still reaches the terminal. The accessory row stays visible above the
|
||||||
|
keyboard throughout.
|
||||||
|
|
||||||
|
**Failure means:** Android's own view focus stayed on Avalonia's input view after the tap instead of being
|
||||||
|
handed back. This is the half `Focusable = false` cannot reach — the platform moves its focus on the touch
|
||||||
|
itself, before Avalonia decides anything — and the symptom chain is the keyboard swapping to its no-input
|
||||||
|
layout and the inset churn parking it over the very row that was tapped. See
|
||||||
|
`TerminalFocus` in the Android head's Platform folder.
|
||||||
|
|
||||||
### 11.11 Closing a connection and opening a new one both take you somewhere real
|
### 11.11 Closing a connection and opening a new one both take you somewhere real
|
||||||
|
|
||||||
Open a shell, close its tab, then open a different one from HOSTS.
|
Open a shell, close its tab, then open a different one from HOSTS.
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using global::Android.Views;
|
||||||
|
|
||||||
|
namespace DodoSSH.Client.Android.Platform;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hands native focus back to the terminal's WebView after Avalonia chrome took it.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// <b>The sibling of <see cref="SoftKeyboard"/>, and it exists for the same reason that one does:</b> the
|
||||||
|
/// keyboard over a terminal belongs to the WebView's own native view, which Avalonia's focus manager does
|
||||||
|
/// not own. The accessory row's keys are already <c>Focusable=false</c> — see TerminalScreen — so Avalonia's
|
||||||
|
/// idea of focus never leaves the terminal when one is tapped. What still moves is <em>Android's</em>: the
|
||||||
|
/// tap lands on Avalonia's own input view, which is focusable-in-touch-mode because Avalonia's text boxes
|
||||||
|
/// need it to be, and the platform hands that view focus on the way to delivering the touch. The WebView's
|
||||||
|
/// input connection dies with its focus, the keyboard swaps to the layout it shows an editor that takes no
|
||||||
|
/// text, and the inset churn that follows can leave it sitting on top of the very row that was tapped.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// So each accessory key hands focus straight back once its byte is on the wire. The page inside the
|
||||||
|
/// WebView never noticed anything — its own DOM focus never moved — so regaining native focus re-establishes
|
||||||
|
/// the same input connection and the keyboard settles back to what it was. Skipped when the WebView is still
|
||||||
|
/// focused, which makes the call free on any platform arrangement where the steal never happened.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Found by walking the decor view rather than asked of the <c>NativeWebView</c> control, because the
|
||||||
|
/// control does not expose its platform child and this application only ever has the one WebView — the
|
||||||
|
/// walk's first match is necessarily the terminal. Every step is allowed to be absent, exactly as
|
||||||
|
/// <see cref="SoftKeyboard.Hide"/>'s are: no activity while backgrounded, no WebView while the terminal
|
||||||
|
/// surface has never been shown, and nothing to do in either case.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
internal static class TerminalFocus
|
||||||
|
{
|
||||||
|
public static void Return()
|
||||||
|
{
|
||||||
|
if (PhoneEnvironment.CurrentActivity?.Window?.DecorView is not ViewGroup decor)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FindWebView(decor) is { IsFocused: false } webView)
|
||||||
|
{
|
||||||
|
webView.RequestFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static View? FindWebView(ViewGroup parent)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < parent.ChildCount; i++)
|
||||||
|
{
|
||||||
|
switch (parent.GetChildAt(i))
|
||||||
|
{
|
||||||
|
case global::Android.Webkit.WebView webView:
|
||||||
|
return webView;
|
||||||
|
|
||||||
|
case ViewGroup child when FindWebView(child) is { } found:
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -141,61 +141,83 @@ internal sealed partial class TerminalScreen : UserControl
|
|||||||
|
|
||||||
foreach (var (label, bytes, latches) in Keys)
|
foreach (var (label, bytes, latches) in Keys)
|
||||||
{
|
{
|
||||||
var key = new Button
|
var key = CreateKey(label);
|
||||||
{
|
|
||||||
Content = new TextBlock
|
|
||||||
{
|
|
||||||
Text = label,
|
|
||||||
FontFamily = (FontFamily)Application.Current!.FindResource("MonoFont")!,
|
|
||||||
FontSize = 11,
|
|
||||||
HorizontalAlignment = HorizontalAlignment.Center,
|
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
|
||||||
},
|
|
||||||
|
|
||||||
// 44 wide, and that is the number that matters: the design draws these flexed across the
|
|
||||||
// width, which at 360dp with ten keys is 32 pixels each — under every thumb-target
|
|
||||||
// guideline there is. The height came down with the row it sits in, from 38 to 30, and it
|
|
||||||
// costs nothing a width does: the keys are a single row with the terminal above and the
|
|
||||||
// system's gesture bar below, so there is no neighbour a short press can land on instead.
|
|
||||||
MinWidth = 44,
|
|
||||||
Height = 30,
|
|
||||||
Padding = new Thickness(10, 0),
|
|
||||||
CornerRadius = new CornerRadius(9),
|
|
||||||
Background = Palette("Panel"),
|
|
||||||
BorderBrush = Palette("BorderMid"),
|
|
||||||
BorderThickness = new Thickness(1),
|
|
||||||
Foreground = Palette("TextDim"),
|
|
||||||
HorizontalContentAlignment = HorizontalAlignment.Center,
|
|
||||||
|
|
||||||
// ◆ NOT FOCUSABLE, AND THAT IS THE WHOLE CONTROL RATHER THAN A DETAIL.
|
|
||||||
//
|
|
||||||
// These keys are an extension of the keyboard, not a place the keyboard should go. As
|
|
||||||
// ordinary buttons they took Avalonia's focus on tap, which takes it off the NativeWebView
|
|
||||||
// — and the package's own OnLostFocus then calls the adapter's ResignFocus(). So pressing
|
|
||||||
// Tab or an arrow handed the terminal one byte and took the keyboard away from it: the next
|
|
||||||
// thing typed on a hardware keyboard went nowhere, and the row went on working because its
|
|
||||||
// buttons are pressed rather than typed into, which is what makes it look like the terminal
|
|
||||||
// had died instead.
|
|
||||||
//
|
|
||||||
// Focusable=false is what a toolbar button is, and it means the focused element never
|
|
||||||
// changes: the WebView is still it, so nothing resigns and nothing has to be handed back.
|
|
||||||
Focusable = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// TerminalFocus.Return in both, after the key has done its work: by Click time the touch has
|
||||||
|
// already moved Android's own focus onto Avalonia's input view, and leaving it there is what
|
||||||
|
// swaps the keyboard out from over the terminal. Returning it is free when nothing moved.
|
||||||
if (latches)
|
if (latches)
|
||||||
{
|
{
|
||||||
controlKey = key;
|
controlKey = key;
|
||||||
key.Click += (_, _) => ToggleControl();
|
key.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
ToggleControl();
|
||||||
|
TerminalFocus.Return();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
key.Click += (_, _) => SendAsync(bytes);
|
key.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
SendAsync(bytes);
|
||||||
|
TerminalFocus.Return();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
row.Children.Add(key);
|
row.Children.Add(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>One key of the accessory row, before its click is wired.</summary>
|
||||||
|
/// <remarks>Split from <see cref="BuildAccessoryRow"/> for length rather than for reuse.</remarks>
|
||||||
|
private static Button CreateKey(string label) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Content = new TextBlock
|
||||||
|
{
|
||||||
|
Text = label,
|
||||||
|
FontFamily = (FontFamily)Application.Current!.FindResource("MonoFont")!,
|
||||||
|
FontSize = 11,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 44 wide, and that is the number that matters: the design draws these flexed across the
|
||||||
|
// width, which at 360dp with ten keys is 32 pixels each — under every thumb-target
|
||||||
|
// guideline there is. The height came down with the row it sits in, from 38 to 30, and it
|
||||||
|
// costs nothing a width does: the keys are a single row with the terminal above and the
|
||||||
|
// system's gesture bar below, so there is no neighbour a short press can land on instead.
|
||||||
|
MinWidth = 44,
|
||||||
|
Height = 30,
|
||||||
|
Padding = new Thickness(10, 0),
|
||||||
|
CornerRadius = new CornerRadius(9),
|
||||||
|
Background = Palette("Panel"),
|
||||||
|
BorderBrush = Palette("BorderMid"),
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
Foreground = Palette("TextDim"),
|
||||||
|
HorizontalContentAlignment = HorizontalAlignment.Center,
|
||||||
|
|
||||||
|
// ◆ NOT FOCUSABLE, AND THAT IS THE WHOLE CONTROL RATHER THAN A DETAIL.
|
||||||
|
//
|
||||||
|
// These keys are an extension of the keyboard, not a place the keyboard should go. As
|
||||||
|
// ordinary buttons they took Avalonia's focus on tap, which takes it off the NativeWebView
|
||||||
|
// — and the package's own OnLostFocus then calls the adapter's ResignFocus(). So pressing
|
||||||
|
// Tab or an arrow handed the terminal one byte and took the keyboard away from it: the next
|
||||||
|
// thing typed on a hardware keyboard went nowhere, and the row went on working because its
|
||||||
|
// buttons are pressed rather than typed into, which is what makes it look like the terminal
|
||||||
|
// had died instead.
|
||||||
|
//
|
||||||
|
// Focusable=false is what a toolbar button is, and it means the focused element never
|
||||||
|
// changes: the WebView is still it, so nothing resigns and nothing has to be handed back.
|
||||||
|
//
|
||||||
|
// At the Avalonia layer, that is. Android keeps a focus of its own, and the touch that
|
||||||
|
// presses one of these keys hands it to Avalonia's input view regardless of what Avalonia
|
||||||
|
// decides about its element — taking the keyboard's input connection off the terminal and
|
||||||
|
// swapping its layout mid-typing. The Click wiring in BuildAccessoryRow hands that half
|
||||||
|
// back; see TerminalFocus for the whole story.
|
||||||
|
Focusable = false,
|
||||||
|
};
|
||||||
|
|
||||||
private void ToggleControl()
|
private void ToggleControl()
|
||||||
{
|
{
|
||||||
controlLatched = !controlLatched;
|
controlLatched = !controlLatched;
|
||||||
|
|||||||
Reference in New Issue
Block a user