diff --git a/README.md b/README.md index 3cf0049..7f37934 100644 --- a/README.md +++ b/README.md @@ -268,11 +268,20 @@ the reasoning behind it. Sign in, unlock, browse hosts, open a shell, and read t host-key decisions and the counted delete confirmations are there too, and none of them were softened to fit 360dp. -Its interface is the **v2 design**: four destinations in a bottom bar — Hosts, Terminal, Keychain and More -— with snippets, SFTP, S3 buckets, logs and preferences one tap deeper behind the last. Both heads are on -that design now; the desktop's own v2 is a 190-pixel labelled sidebar in place of the icon rail, a centred -search box in the titlebar, and session tabs as pills. Its light theme is not built — see -[`docs/design-import-gaps.md`](docs/design-import-gaps.md) — so the application is dark on both. +Its interface is the **v2 design**: destinations in a bottom bar, with the rest one tap deeper behind the +last. The bar is three — **Hosts**, **Connections** and **Settings** — with the keychain, snippets, SFTP, +S3 buckets, logs and preferences behind Settings. A bottom bar is for the places a session moves between, +and managing keys is not one of those. Both heads are on that design now; the desktop's own v2 is a +190-pixel labelled sidebar in place of the icon rail, a centred search box in the titlebar, and session tabs +as pills, and it keeps its Keychain entry — a rail with nine slots has room. Its light theme is not built — +see [`docs/design-import-gaps.md`](docs/design-import-gaps.md) — so the application is dark on both. + +**Connections is where a connection is made, not only where one is shown.** With nothing open it offers a +box taking `user@host` or `user@host:port` and a password, and lists the machines most recently connected +to underneath. That box is the one path in the product to a machine the keychain has never heard of — the +case somebody has just been handed an address — and nothing typed into it is saved: a machine worth keeping +belongs on Hosts, where it can carry a key, a group's defaults and a name. Tapping a recent machine goes to +its host if it has one and back into the box if it does not. **A connected phone shows one bar and then the terminal.** The header, the session strip and the bottom bar are collapsed while a shell is up, and a single 35-pixel row replaces them: back on the left, the diff --git a/docs/android-port.md b/docs/android-port.md index d9242fe..2adb52d 100644 --- a/docs/android-port.md +++ b/docs/android-port.md @@ -493,11 +493,18 @@ go at 360dp: **Superseded by v2.** A second design — *DodoSSH Android v2* — is what this head now draws, and it took the "additive rather than structural" claim at its word: snippets, logs, SFTP and S3 are built, over the - view models that were already shared, behind a MORE hub that holds the five destinations the bottom bar - has no room for. `ShellScreen` gained `More` and `Buckets`. The palette went blue and the two heads moved - together, because it is shared. What v2 asked for and did not get is in + view models that were already shared, behind a hub that holds the destinations the bottom bar has no room + for. `ShellScreen` gained `More` and `Buckets`. The palette went blue and the two heads moved together, + because it is shared. What v2 asked for and did not get is in [`docs/design-import-gaps.md`](design-import-gaps.md) — the short version being that port forwarding is named as absent rather than drawn, because nothing in the SSH layer forwards anything. + + **The bar has since gone from four entries to three.** The keychain joined the hub, which is now called + SETTINGS rather than MORE and wears a gear: a bottom bar is for the places a session moves between, and + managing keys is not one of them. `IsMoreSurface` gained `ShellScreen.Vault`, which is the whole of it — + the membership test, the tab that lights and the back gesture's first case all read that one property. + The desktop keeps its Keychain rail entry, so this is the second thing the two heads arrange + deliberately differently, after the hub itself. 8. ~~**The terminal.**~~ **Done** — one `NativeWebView` for every session, as on the desktop and for the same reason, with the tab strip scrolling horizontally rather than wrapping so that opening a tab never reflows a terminal while output is arriving. @@ -516,6 +523,24 @@ go at 360dp: WebView actually composites above Avalonia content is still the unverified question recorded below, and collapsing is correct under either answer. + **The + also puts the software keyboard away, and that needed the platform.** `TopLevel.InputPane` + reports the keyboard and cannot close one — Avalonia's model is that it belongs to whatever has focus, so + the supported dismissal is to move focus — and the keyboard over a terminal was raised by the `WebView`'s + own text input, by a native view Avalonia's focus manager never owned. Clearing Avalonia's focus does + nothing to it. `Platform/SoftKeyboard.cs` asks `InputMethodManager` directly, off the decor view's window + token, and every step of it is allowed to be absent. Without it the sheet arrives under a keyboard + covering the half of the screen the sheet is on — and worse, laid out into what is left, since the + keyboard's inset shortens everything this head draws. + + **With nothing open the surface is a connect screen rather than an empty state**, which is why the bar + calls it Connections. It offers a `user@host[:port]` box with a password and the machines most recently + connected to, out of the vault's connection log. The connect path in `VaultViewModel` was shaped like + `HostRowViewModel` all the way down; it now hangs off a four-field `ConnectionTarget`, so the ladder of + refusals, the host-key question, the log entry and the tab's lifecycle are shared rather than copied. The + host-key retry replays the attempt that raised the question instead of re-running whichever host was + selected — which was correct while a selected host was the only way to connect, and would now dial the + wrong machine. + All eight are done for the decided scope, and v2 has since gone past it — see the note under step 7. What is left, in the order it matters: diff --git a/docs/design-import-gaps.md b/docs/design-import-gaps.md index 9492bba..25f42fb 100644 --- a/docs/design-import-gaps.md +++ b/docs/design-import-gaps.md @@ -6,10 +6,18 @@ the chrome, hosts and terminals, file transfer, the vault, teams, and preference > ## The second import — v2, phone > > A later design in the same project, **DodoSSH Android v2**, is what the phone head now draws: eight -> screens, a bottom bar of four with the rest behind MORE, and a new palette. It has been imported, and +> screens, a bottom bar with the rest behind a hub, and a new palette. It has been imported, and > this section is the same accounting for it. The desktop's own v2 has **not** been imported — only its > palette, because the palette is shared and the two heads may not disagree about a colour. > +> **The bar is three rather than the design's four, and the hub is SETTINGS rather than MORE.** The design +> puts Hosts, Terminal, Keychain and More along the bottom. The keychain moved behind the hub: a bottom bar +> is for the places a session moves between, and keys, credentials and tags are managed occasionally and +> then left alone, which is the shape of everything already behind it. With the keychain on it the hub is no +> longer "more" of anything — it is where everything that is not connecting to a machine lives — so it takes +> the name and the glyph that has meant exactly that on a phone since before this application existed. +> Terminal became **Connections** in the same pass, for a reason below. +> > **The palette went from green to blue, and one colour changed meaning rather than value.** Green used to > be the accent, so `Ellipse.dot.live` filled with `Accent` and "the thing to press" and "a shell is open > here" were the same colour by construction. v2 makes the accent `#5B8CFF` and keeps a green, `#3DDC97`, @@ -37,6 +45,7 @@ the chrome, hosts and terminals, file transfer, the vault, teams, and preference > | A 14px rounded inset around the terminal | Not drawn. The renderer is a native child view composited above everything Avalonia draws, so a rounded frame behind it clips nothing. What the design was after comes from the page's own background, which is the design's `#171A26`. | > | Instrument Sans / Fira Code | Inter and the system monospace stack, as before and for the same reason — see the note on `MonoFont` in `Palette.axaml`. | > | A theme toggle (`☀`) in the header, and the desktop v2's light theme | Omitted. There is one theme by decision, and a switch with one position is a control that does nothing. | +> | TERMINAL's empty state: a sentence saying nothing is open | ◆ **More than the design asked for, which is rare enough on this list to say out loud.** The tab is called **Connections** and with nothing running the screen is where one is made: a box taking `user@host` or `user@host:port` with a password, and the machines most recently connected to underneath it, read from the vault's own connection log. The box is the only path in the product to a machine that is not in the keychain — the design assumes every connection starts from a saved host, and the case it has no answer for is an address somebody was handed five minutes ago. Nothing typed there is saved, and the screen says so. | > > ## The desktop's v2 > diff --git a/docs/manual-checks.md b/docs/manual-checks.md index f6e0c67..f2d1f5e 100644 --- a/docs/manual-checks.md +++ b/docs/manual-checks.md @@ -952,3 +952,54 @@ visibly disabled rather than silently inert. **Failure means:** they have been folded into the scrolling row — which is the arrangement the connection line existed to avoid, and the reason it could be removed at all. + +### 11.6 The keyboard goes away with the connect menu + +With a shell open, tap into the terminal so the software keyboard comes up, then press `+`. + +**Pass:** the keyboard drops and the sheet sits on the bottom of the full screen, all four of its rows +reachable. Repeat on Android 14 or earlier and on 15 or later if you have both — the keyboard shortens this +head's interface by a different mechanism on each, and a sheet laid out into the strip left above a +keyboard is the failure this exists to catch. + +**Failure means:** `SoftKeyboard.Hide` found no activity, no input-method manager, or the wrong window +token. Note that Avalonia cannot do this for us — `TopLevel.InputPane` reports the keyboard and cannot close +one, and the keyboard here belongs to the `WebView`, so clearing Avalonia's focus is not a fallback. + +### 11.7 Connections, with nothing open + +Close every tab, then press Connections in the bottom bar. + +**Pass:** a box reading `user@host or user@host:port`, a password box under it, CONNECT, and — if this +keychain has ever connected to anything — a RECENT list underneath. The whole thing scrolls with the +keyboard up, and the password box shows dots with **no suggestion strip** above the keyboard. + +Type a machine you can reach and press CONNECT: a tab appears in the bar and the shell opens. Type +something malformed — no `user@`, or `:70000` — and the refusal appears under the boxes, in the warning +colour, with no tab and nothing dialled. + +**Failure means:** for the suggestion strip, the box lost `Classes="secret"` — see 10.1, and it is the same +seriousness here, since this password is typed rather than stored and so is typed often. + +### 11.8 A recent machine leads to the right place + +With at least one keychain host and one machine connected to by hand in the log, open Connections with +nothing running. + +**Pass:** tapping the keychain one lands on HOSTS with that host selected and its connect bar up. Tapping +the hand-typed one puts its address back in the box on this screen and leaves the password box **empty**. +Neither one dials anything on its own. + +**Failure means:** if the hand-typed one navigates to HOSTS, the log entry is carrying a host id it should +not have. If the password came back, something is storing it — it never was. + +### 11.9 Settings holds the keychain, and back walks out of it + +From HOSTS: Settings, then Keychain. + +**Pass:** the bottom bar has three entries and the Settings one stays lit while the keychain is showing. +The keychain draws its own header with a back arrow — the vault name and sync light are not above it. Back, +by gesture or by the arrow, returns to Settings and not to HOSTS; a second back returns to HOSTS. + +**Failure means:** `ShellScreen.Vault` is missing from `IsMoreSurface` or from the back gesture's first +case, and those two have to move together — the switch mirrors that property by construction. diff --git a/src/DodoSSH.Client.Android/Platform/SoftKeyboard.cs b/src/DodoSSH.Client.Android/Platform/SoftKeyboard.cs new file mode 100644 index 0000000..c79782b --- /dev/null +++ b/src/DodoSSH.Client.Android/Platform/SoftKeyboard.cs @@ -0,0 +1,50 @@ +using global::Android.Content; +using global::Android.Views.InputMethods; + +namespace DodoSSH.Client.Android.Platform; + +/// +/// Puts the software keyboard away. +/// +/// +/// +/// There is no Avalonia way to do this, and it is worth knowing why rather than assuming. +/// TopLevel.InputPane reports the keyboard — its state and the rectangle it occludes, which is what +/// PhoneShell keeps the interface clear of — and offers nothing that closes it. The framework's model +/// is that the keyboard belongs to whatever has focus, so the supported way to dismiss one is to move focus +/// off the text box that raised it. +/// +/// +/// That model does not reach the case this exists for. The keyboard over a terminal was raised by the +/// WebView's own text input, inside the page, by a native view Avalonia's focus manager does not own. +/// Clearing Avalonia's focus leaves it exactly where it is, because Avalonia never had it. So the request +/// goes to the platform that does own it. +/// +/// +/// HideSoftInputFromWindow needs a window token, and any attached view's will do — they all belong to +/// the same window. The decor view is the one guaranteed to exist for as long as the activity does. +/// +/// +/// Every step is allowed to be absent and none of them is an error: there is no activity while the app is +/// backgrounded, no input-method manager on a system image without one, and no keyboard up most of the time. +/// A method whose whole contract is "if a keyboard is showing, stop showing it" has nothing to report when +/// one is not. +/// +/// +internal static class SoftKeyboard +{ + public static void Hide() + { + if (PhoneEnvironment.CurrentActivity?.Window?.DecorView is not { } view) + { + return; + } + + if (view.Context?.GetSystemService(Context.InputMethodService) is not InputMethodManager manager) + { + return; + } + + manager.HideSoftInputFromWindow(view.WindowToken, HideSoftInputFlags.None); + } +} diff --git a/src/DodoSSH.Client.Android/Views/KeychainScreen.axaml b/src/DodoSSH.Client.Android/Views/KeychainScreen.axaml index b0bf8ba..5342a69 100644 --- a/src/DodoSSH.Client.Android/Views/KeychainScreen.axaml +++ b/src/DodoSSH.Client.Android/Views/KeychainScreen.axaml @@ -1,6 +1,7 @@ @@ -26,12 +27,30 @@ Mode=OneWay on the chips is load-bearing: a two-way selection binding moves the chip before the command beside it has had the chance to refuse, which would leave the row lit for a section the list is not on. + + ── v3 ──────────────────────────────────────────────────────────────────────────────────────────────── + This screen moved behind SETTINGS, so it draws its own header — the one every hub screen has. It is not + a decoration: the shell's header stands down behind the hub, and without a row of its own there would be + no back arrow and nothing on screen saying what this list is. --> - + + + + + + + + + + + + + + + + + +