diff --git a/docs/manual-checks.md b/docs/manual-checks.md index 911d665..ea206ea 100644 --- a/docs/manual-checks.md +++ b/docs/manual-checks.md @@ -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 diff --git a/src/DodoSSH.Client.Android/Platform/TerminalFocus.cs b/src/DodoSSH.Client.Android/Platform/TerminalFocus.cs index a2d4617..97b929e 100644 --- a/src/DodoSSH.Client.Android/Platform/TerminalFocus.cs +++ b/src/DodoSSH.Client.Android/Platform/TerminalFocus.cs @@ -10,17 +10,24 @@ namespace DodoSSH.Client.Android.Platform; /// The sibling of , and it exists for the same reason that one does: 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 Focusable=false — see TerminalScreen — so Avalonia's -/// idea of focus never leaves the terminal when one is tapped. What still moves is Android's: 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 Android's: +/// AvaloniaView.DispatchTouchEvent (decompiled from Avalonia.Android 12.1.1) ends every handled +/// touch — DOWN and UP alike — with a RequestFocus() 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. /// /// -/// 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. +/// Posted, not called — the posting is the fix's second attempt, and the first one's failure is why. +/// The first version called RequestFocus() from the keys' own Click handlers, which fire +/// inside the UP event's dispatch — and the platform's own request runs after 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. +/// +/// +/// 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. /// /// /// Found by walking the decor view rather than asked of the NativeWebView 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) diff --git a/src/DodoSSH.Client.Android/Theme/Phone.axaml b/src/DodoSSH.Client.Android/Theme/Phone.axaml index 5ee8c3c..725e1dd 100644 --- a/src/DodoSSH.Client.Android/Theme/Phone.axaml +++ b/src/DodoSSH.Client.Android/Theme/Phone.axaml @@ -116,11 +116,27 @@ - +