From 507cd9ff884ad4869efd3c20d240511053b46963 Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit | DodoTech Date: Thu, 6 Aug 2026 14:11:48 +0200 Subject: [PATCH] Choose more than one host card on the desktop, the way the phone already can MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The chosen-hosts set has been in VaultViewModel since the phone's connect card became a contextual action bar: a set of entity ids, a tick on the row, and seven things that can be done to it. Only one head could fill it. The desktop's grid answered a press with one selection — the card the drawer, CONNECT and the context menu are about — so filing eleven imported machines under a heading was eleven drags, and clearing out a vault was eleven rounds of the deletion question. So the pointer gets three ways into the same set. Ctrl-clicks a card to tick it, Shift-clicks to tick the run between the anchor and the card, and drags a band out over the space between and below the cards to tick everything it touches. Esc, CLEAR, a plain click on a card and a click on the empty space each drop it, and Ctrl+A takes every card being drawn — VisibleHosts, so with something in the find box that is the ones on screen and not the ones it is hiding, which is the version of that shortcut whose result can be checked before Delete is pressed. TWO SELECTIONS ON ONE SCREEN, AND KEEPING THEM FROM DISAGREEING IS MOST OF THE CHANGE. Ctrl and Shift are answered on the tunnel and marked handled, so the ListBox never moves its own mark onto the card: a Ctrl-click that also selected would light the card it had just unticked and open the drawer on a machine somebody is removing from a set. A plain press drops the set unless it lands on a ticked card, and that case is deferred to the release, because the press may be the start of a drag of all of it. After any ordinary click exactly one card is in play, which is what makes every command on the screen unambiguous again. The context menu is where the seven live, and it is one markup with two halves gated on IsChoosingHosts. Connect, Browse files and Edit… are drawn only for a single ticked host, as the phone's sheet collapses them and for the same reason; the other four read better for a count. A right click on a card outside the set drops the set first, so a Delete… about the card under the pointer can never be offered while six sit ticked behind the menu — the same rule OnContextRequested has always enforced for the selection, reached from the other direction. No bar of buttons: the phone raises one because it has no other way to hold seven entries, and a strip repeating a menu that already exists would be a second home for the wording that matters most. What the desktop gains instead is a count beside the HOSTS heading, CLEAR, and a sentence saying where the actions are. A drag that starts on a ticked card carries every ticked card. The payload is a list rather than a row now, and a drop of more than one goes through FileChosenHostsUnder, which makes the refusals once — an open editor, and a group belonging to one keychain — and reports a count instead of forty status lines. Moving whichever card the pointer happened to be holding and leaving the other five where they are is a gesture that quietly does a fraction of what it looks like it does, and the five left behind look filed. The three panels the set's actions raise had never been drawn in a window: the vault picker with its key question, the group picker, and the deletion question. All three sit above the grid rather than over it, which is the arrangement the GROUPS section and the phone's list already use and for the reason written there — the ticked cards are the information the question exists to give, so the grid shortens instead. A DEFECT FOUND BEHIND IT, AND IT WAS ALREADY LIVE ON THE PHONE. The deletion question names a count and the run that answers it reads the set again, and nothing kept the two the same set: the panel is deliberately above a live list, so one more tick between "Delete these 6 hosts?" and pressing DELETE deleted seven, with the seventh named in nothing the user had read. It needed a deliberate act on a phone and a second's work with a band, which is what turned it up. VaultViewModel now remembers which hosts the question was asked about and drops the question when the set stops being them — the question rather than the set, because what somebody has just chosen is what they meant. It also covers the case nobody performs: a colleague's deletion arriving mid-question and shrinking the set under it. VERIFIED. 354 tests in App.Tests and 111 in App.Layout.Tests, build clean, no new warnings. Six gesture tests drive real pointer and key input through the headless window — the modifier click and what it must not do to the selection, the run and its re-measurement from the anchor, the band and the click that drops the set, Ctrl+A under a filter, and the menu's two halves — plus a DragOver carrying two hosts. Four layout tests measure the strip and the three panels at the window's minimum; the vault panel binds a key to its host first, or it would measure the short shape and certify the tall one. Two flow tests cover the multi-drop's write and its refusal, and the deletion question dropping itself. manual-checks gains 7.6a for dragging a set, which no test can see for the reason 7.6 gives, and 7.7a for the gestures — the rectangle actually being painted and the tick and the fill being legible together are the two things the harness cannot look at. --- docs/manual-checks.md | 44 ++ src/DodoSSH.Client.App/App.axaml | 17 + .../Views/HostsScreen.axaml | 251 +++++++++- .../Views/HostsScreen.axaml.cs | 467 ++++++++++++++++-- .../ViewModels/VaultViewModel.cs | 216 +++++++- .../HostGridTests.cs | 284 ++++++++++- .../ScreenLayoutTests.cs | 109 ++++ .../ShellFlowTests.cs | 94 ++++ 8 files changed, 1418 insertions(+), 64 deletions(-) diff --git a/docs/manual-checks.md b/docs/manual-checks.md index c84a6c8..bdd01f2 100644 --- a/docs/manual-checks.md +++ b/docs/manual-checks.md @@ -882,6 +882,23 @@ the drop are covered by nothing. What *is* automated is the decision each drag e `HostGridTests.TheGroupCardsAreWhatAcceptsADroppedHost` raises a real `DragOver` over both kinds of card — and the write at the end, `ShellFlowTests.MovingAHostToAGroup_FilesItAndTakesItOffTheLevelItCameFrom`. +### 7.6a Dragging a whole set onto a group card · **also uncovered, and the same reason** + +Tick three hosts (7.7a is how), then pick one of the three up and drag it onto a group card. + +**Pass:** the card marks itself exactly as it does for one host, and the drop files **all three** — the +status line says how many, and all three leave the level. Ticking nothing and dragging a single card still +files that one card, which is what this gesture has always done. + +**And a card that is not in the set** drags alone: press one of the unticked cards and the ticks come off +before the drag starts, so what lands is the one machine that was under the pointer. + +**Failure means:** a drag that filed one of three is the payload having been built from the card rather than +from the set — the thing this gesture must never do quietly, since the other two stay behind looking filed. +The write is `ShellFlowTests.DroppingTheChosenHostsOnAGroupCard_FilesEveryOneOfThem` and the drag event's +answer is `HostGridTests.AGroupCardTakesAWholeSetOfDraggedHosts`; the platform's half of it is covered by +nothing, as 7.6 explains. + ### 7.7 A click still selects, and a double click still connects Click host cards; drag one a few pixels without releasing; double-click one. Then double-click the space @@ -897,6 +914,33 @@ double-tap handler has lost its check that the pointer was over a card. **And the same two gestures on the group cards above**, where they mean select and open rather than select and connect — the space around *those* cards must open nothing, for the same reason. See 3.2. +### 7.7a Choosing several cards with Ctrl, Shift and a band · **the desktop's half of the phone's set** + +With four or more hosts in the grid: Ctrl-click two cards, Shift-click a third, then drag a rectangle out +over the cards starting from the empty space below them. Finally press Esc, and try Ctrl+A. + +**Pass:** each ticked card takes a ✓ beside its status dot and a filled surface, and the strip beside the +HOSTS heading counts them. A Ctrl-click does **not** move the selection — whichever card was lit before +stays lit, and the drawer, if it is open, goes on showing the same machine. Shift ticks the whole run +between the anchor and the card, in the order the cards are laid out. The band draws an accent rectangle +that follows the pointer and ticks every card it *touches*, unticking them again as it is pulled back off. +Ctrl+A ticks every card being drawn — with something in the find box, that is the ones on screen and not the +ones it is hiding. Esc, CLEAR, a plain click on a card and a click on the empty space each drop the set. + +**And the menu is about the set while one is up:** right-click a ticked card and the entries are Connect, +Browse files and Edit… only while exactly one is ticked, then Change group…, Move to another vault…, Copy to +another vault…, Duplicate and Delete… — the last naming the count. Right-clicking a card **outside** the set +drops the set and gives the ordinary four back, aimed at that card. + +**Also check the band does not start** on the scrollbar, on a group card, or inside a panel above the grid, +and that dragging the scrollbar still scrolls. + +**Failure means:** a Ctrl-click that also selects is the press not being handled on the way down, and it +puts the drawer on a machine somebody is removing from a set. A menu offering both halves at once is the +`IsVisible` bindings in `HostsScreen.axaml` having come apart, which is how the wrong machine gets deleted. +`HostGridTests` drives all of these headlessly; what it cannot see is the rectangle actually being painted +and the two marks being legible together, which is what this check is for. + ### 7.8 The highlight clears after a drag that goes nowhere Drag a host over a group card and release outside the grid, or press Escape mid-drag. diff --git a/src/DodoSSH.Client.App/App.axaml b/src/DodoSSH.Client.App/App.axaml index 023eb37..b627b02 100644 --- a/src/DodoSSH.Client.App/App.axaml +++ b/src/DodoSSH.Client.App/App.axaml @@ -647,6 +647,23 @@ + + +