Public Access
Post the terminal's focus return past the dispatch that steals it
The first fix handed Android's focus back from inside the keys' Click handlers — which fire inside the UP event's dispatch, and Avalonia's own view requests focus for itself after every handled touch dispatch returns (AvaloniaView.DispatchTouchEvent, decompiled from 12.1.1). So the platform's request ran after ours and undid it microseconds later, which is exactly what the phone showed: the terminal still lost focus. The return is now posted onto the main looper, landing one message after the dispatch that stole, and it is wired at the row for both halves of a press — DOWN steals too, and Click only exists for UP, so a keyboard detached at DOWN would otherwise stay detached for the whole length of the press. Check 11.10a now also says what a tolerable blink looks like against a failure that stays.
This commit is contained in:
@@ -1750,15 +1750,18 @@ tap of an arrow key.
|
||||
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
|
||||
**Pass:** the keyboard settles back unchanged — same layout, same suggestion strip, same height — and
|
||||
everything typed after the tap still reaches the terminal. The accessory row stays visible above the
|
||||
keyboard throughout.
|
||||
keyboard throughout. A blink during the press itself is tolerable: the platform takes the focus on both
|
||||
halves of every touch and the return is posted right behind each theft, so the connection can visibly flap
|
||||
for the press's own duration — what it must never do is *stay* swapped after the finger lifts.
|
||||
|
||||
**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.
|
||||
handed back. This is the half `Focusable = false` cannot reach — the platform requests focus for its own
|
||||
view after dispatching every handled touch — 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. The first fix for this failed by
|
||||
timing alone: it handed focus back from inside the very dispatch the platform re-steals it after. See
|
||||
`TerminalFocus` in the Android head's Platform folder for both the mechanism and the fix's shape.
|
||||
|
||||
### 11.11 Closing a connection and opening a new one both take you somewhere real
|
||||
|
||||
|
||||
@@ -10,17 +10,24 @@ namespace DodoSSH.Client.Android.Platform;
|
||||
/// <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.
|
||||
/// idea of focus never leaves the terminal when one is tapped. What still moves is <em>Android's</em>:
|
||||
/// <c>AvaloniaView.DispatchTouchEvent</c> (decompiled from Avalonia.Android 12.1.1) ends every handled
|
||||
/// touch — DOWN and UP alike — with a <c>RequestFocus()</c> for Avalonia's own view. 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.
|
||||
/// <b>Posted, not called — the posting is the fix's second attempt, and the first one's failure is why.</b>
|
||||
/// The first version called <c>RequestFocus()</c> from the keys' own Click handlers, which fire
|
||||
/// <em>inside</em> the UP event's dispatch — and the platform's own request runs <em>after</em> dispatch
|
||||
/// returns, so it undid ours a few microseconds later and the terminal stayed unfocused. A posted runnable
|
||||
/// runs on the next main-looper message, after the platform has taken its turn, so ours is the request that
|
||||
/// sticks. The focus check lives inside the posted runnable for the same reason: the answer at call time is
|
||||
/// about to be made stale by the very mechanism this exists to counter.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The page inside the WebView never noticed any of this — 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.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Found by walking the decor view rather than asked of the <c>NativeWebView</c> control, because the
|
||||
@@ -39,10 +46,18 @@ internal static class TerminalFocus
|
||||
return;
|
||||
}
|
||||
|
||||
if (FindWebView(decor) is { IsFocused: false } webView)
|
||||
if (FindWebView(decor) is not { } webView)
|
||||
{
|
||||
webView.RequestFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
webView.Post(() =>
|
||||
{
|
||||
if (!webView.IsFocused)
|
||||
{
|
||||
webView.RequestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static View? FindWebView(ViewGroup parent)
|
||||
|
||||
@@ -139,29 +139,27 @@ internal sealed partial class TerminalScreen : UserControl
|
||||
{
|
||||
var row = this.FindControl<StackPanel>("AccessoryKeys")!;
|
||||
|
||||
// Both halves of a press steal Android's own focus — the platform requests it for Avalonia's view
|
||||
// after dispatching every handled touch, DOWN and UP alike; TerminalFocus carries the decompiled
|
||||
// citation. Countered at the row rather than inside each key's Click, and for two reasons: Click
|
||||
// only exists for the UP half, so a keyboard detached at DOWN would stay detached for the whole
|
||||
// length of the press; and the Return is posted past the current dispatch, so its ordering against
|
||||
// the key's own handler does not matter — which is what lets one pair of handlers cover ten keys.
|
||||
row.AddHandler(PointerPressedEvent, (_, _) => TerminalFocus.Return(), RoutingStrategies.Tunnel);
|
||||
row.AddHandler(PointerReleasedEvent, (_, _) => TerminalFocus.Return(), RoutingStrategies.Tunnel);
|
||||
|
||||
foreach (var (label, bytes, latches) in Keys)
|
||||
{
|
||||
var key = CreateKey(label);
|
||||
|
||||
// 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)
|
||||
{
|
||||
controlKey = key;
|
||||
key.Click += (_, _) =>
|
||||
{
|
||||
ToggleControl();
|
||||
TerminalFocus.Return();
|
||||
};
|
||||
key.Click += (_, _) => ToggleControl();
|
||||
}
|
||||
else
|
||||
{
|
||||
key.Click += (_, _) =>
|
||||
{
|
||||
SendAsync(bytes);
|
||||
TerminalFocus.Return();
|
||||
};
|
||||
key.Click += (_, _) => SendAsync(bytes);
|
||||
}
|
||||
|
||||
row.Children.Add(key);
|
||||
@@ -213,8 +211,9 @@ internal sealed partial class TerminalScreen : UserControl
|
||||
// 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.
|
||||
// swapping its layout mid-typing. The row's own pointer handlers in BuildAccessoryRow hand
|
||||
// that half back; see TerminalFocus for the whole story, including why the handing back has
|
||||
// to be posted rather than done inline.
|
||||
Focusable = false,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user