using global::Android.Views; namespace DodoSSH.Client.Android.Platform; /// /// Hands native focus back to the terminal's WebView after Avalonia chrome took it. /// /// /// /// 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: /// 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. /// /// /// 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 /// 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 /// 's are: no activity while backgrounded, no WebView while the terminal /// surface has never been shown, and nothing to do in either case. /// /// internal static class TerminalFocus { public static void Return() { if (PhoneEnvironment.CurrentActivity?.Window?.DecorView is not ViewGroup decor) { return; } if (FindWebView(decor) is not { } webView) { return; } webView.Post(() => { if (!webView.IsFocused) { 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; } }