Open a group by double-clicking it, and say where you are

ONE PRESS WAS DOING TWO JOBS. A group card was the only place a group could be
selected — it is what EDIT and DELETE aim at — and it was also the control that
narrowed the grid to that group. So there was no way to name a group in order to
rename it without every host outside it leaving the screen at the same moment,
and no way back except a SHOW ALL button that appeared beside the heading.

Two gestures instead. A click selects and does nothing else; a double-click
opens, which is what the host cards below already do to get a shell and what the
transfers screen's directories already do to go inside one. The grid now has one
vocabulary rather than one per list. The gesture is wired in the code-behind
beside the host one, and guarded the same way: a double-click on the space
around the cards must not open whichever group happened to be selected.

THE CARDS ARE ONE LEVEL NOW, not every group in the keychain. Groups nest, and
drawing all of them flat was the only honest thing to do while a card was a
filter — a filter nobody can see is a filter nobody can turn off. Once opening
one became navigation the cards became its contents, and VisibleGroups is that
level beside Groups the way VisibleHosts sits beside Hosts. Groups itself is
untouched: it is what every lookup reads and what the phone's headings are built
from, and the phone binds none of the new members.

Which is what the trail is for. A level with no name and no way out is a grid
that has quietly hidden things, so a breadcrumb sits above the cards — drawn
exactly as the transfers screen draws a directory path, same flat crumbs and
same separator, because it is the same control answering the same question and a
window with two breadcrumbs that look different has two ideas of what a path is.
The first crumb is always there and always goes back to every host, which is
what SHOW ALL was; that button went with it, because a control that only says
"stop" beside a trail that says where you are is a second control for one job,
and this one also gets you back one level rather than all the way.

EDIT AND DELETE AIM AT GroupTarget: the selected card, or the open group when no
card is selected. Without the fallback a group with nothing inside it could be
opened and then never edited, because opening a group is exactly what takes its
own card off the screen. It is also what a file manager does — act on the
selection, and on the current folder when there is none — and the pair is now
hidden with nothing to act on rather than shown doing nothing.

A DANGLING PARENT AND A CYCLE BOTH HAD TO END UP SOMEWHERE REACHABLE. Neither is
prevented: a parent id may point at a group deleted on another machine, and two
clients can each re-parent A under B and B under A while offline, which no merge
can see because the pointer is inside the payload. EffectiveParents promotes
both to the outermost level, which is the same degradation the resolver's
visited set produces for inheritance. The repair for either is the group's own
editor and the editor is opened from its card, so a group drawn nowhere would be
a broken state with the fix locked inside it.

Three tests in HostGridTests: the split rule through the properties the cards
bind, the pointer gesture itself in two windows so that "one press still only
selects" is asserted separately from the pair, and a nested group opened,
emptied of cards and walked back out of one level. The last presses the trail as
it is actually rendered rather than calling the command, because a crumb reaches
the vault through a $parent binding — a string that compiles whether or not it
resolves, and would otherwise leave a row of buttons that do nothing. 85 layout
tests and 234 shell-flow tests pass.

Manual-checks 3.2, 7.6 and 7.7 follow the new gestures, and 3.2a and 3.4a are
new: nesting, and the two states above, both of which need two machines and
neither of which headless Avalonia can reach.
This commit is contained in:
2026-08-03 16:16:18 +02:00
parent fd8497bb76
commit 0c0ac94312
6 changed files with 549 additions and 80 deletions
@@ -83,6 +83,13 @@ internal sealed partial class HostsScreen : UserControl
// password box above it, and a host that asks for a password still needs it typed first.
HostGrid.DoubleTapped += OnHostActivated;
// And a group card opens the group, on the same gesture, for the same reason: going inside
// something by double-clicking it is what the transfers screen's directories do and what every file
// manager does. One click used to open a group, which made the card that names a group and the
// control that narrows the grid to it the same press — so there was no way to select a group in
// order to rename it without also losing sight of every host outside it.
GroupGrid.DoubleTapped += OnGroupActivated;
// Tunnelled, so the card under the pointer is read before the ListBox has answered the press itself.
// Bubbling would work for the drag but not for the menu: by then the control has already decided
// what is selected, and the menu is about to open against it.
@@ -154,6 +161,22 @@ internal sealed partial class HostsScreen : UserControl
}
}
/// <summary>
/// Opens the group card that was double-clicked.
/// </summary>
/// <remarks>
/// Guarded over the space around the cards exactly as the host grid's is, and it is the same mistake
/// being guarded against: an unguarded handler would open whichever group happened to be selected when
/// somebody double-clicked the gap beside it, throwing every host outside that group off the screen.
/// </remarks>
private void OnGroupActivated(object? sender, TappedEventArgs e)
{
if (Vault is { } vault && RowUnder(e.Source) is HostGroupRowViewModel group)
{
vault.OpenGroupCommand.Execute(group);
}
}
/// <summary>
/// Points the menu at whatever was right-clicked.
/// </summary>