Public Access
Merge branch 'claude/group-creation-hosts-dragdrop-56c6a3'
This commit is contained in:
@@ -276,9 +276,12 @@
|
||||
<Setter Property="Background" Value="{StaticResource ChromeHover}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.grouphead">
|
||||
<Setter Property="Padding" Value="10,7,10,4" />
|
||||
</Style>
|
||||
<!--
|
||||
Button.grouphead was here: the padding on the fold-away group heading that used to sit between the host
|
||||
cards. The headings went when the grid became cards and the group cards above it became the thing that
|
||||
names a group — see HostsScreen.axaml — and a style with nothing wearing it is a shape somebody will
|
||||
later reintroduce by reaching for it.
|
||||
-->
|
||||
|
||||
<!-- A category in the vault's rail: the same accent strip a selected row carries, at full row height. -->
|
||||
<Style Selector="Button.cat">
|
||||
@@ -661,19 +664,31 @@
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
The row a host is about to be dropped on. A wash rather than the accent strip a selection carries,
|
||||
because it is not a selection: it says "let go here", it lasts as long as the pointer is over the row,
|
||||
and it has to be legible on top of whatever that row already looks like — including the selected row,
|
||||
which is often the one being dragged onto.
|
||||
The group card a host is about to be dropped on. A wash rather than the accent strip a selection
|
||||
carries, because it is not a selection: it says "let go here", it lasts as long as the pointer is over
|
||||
the card, and it has to be legible on top of whatever that card already looks like — including the
|
||||
selected one, which is the group the grid is currently narrowed to and so often the one being dragged
|
||||
onto.
|
||||
|
||||
Painted on the tile rather than on the item's own presenter, which is where this used to go when the
|
||||
target was a full-width heading. A wash behind a 10-pixel-rounded tile is a square of colour with the
|
||||
card floating on it — the same four grey corners the tiles list clears the theme's own hover and
|
||||
selection brushes to avoid. Declared after that rule and after the selected-tile rule, because
|
||||
Avalonia has no specificity and settles two matching rules by declaration order.
|
||||
|
||||
◆ THE DOUBLED BORDER IS THE MARK, not the fill, and that is what the move to the tile forced. The fill
|
||||
a selected tile already carries is the strongest one this palette has, so a target that only filled
|
||||
would be invisible on the card the grid is narrowed to — which is the card a host most often has to be
|
||||
dragged off, into another group. Two pixels of accent is legible on both.
|
||||
|
||||
Set from the code-behind rather than by a binding, for the reason the whole gesture is code: the rows
|
||||
are rebuilt from scratch on every filter keystroke and every sync pass, so a flag on the view model
|
||||
would be state the list throws away halfway through the drag. See HostsScreen.axaml.cs.
|
||||
-->
|
||||
<Style Selector="ListBoxItem.droptarget /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource AccentWash}" />
|
||||
<Style Selector="ListBoxItem.droptarget Border.tile">
|
||||
<Setter Property="Background" Value="{StaticResource Active}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Accent}" />
|
||||
<Setter Property="BorderThickness" Value="0,0,0,2" />
|
||||
<Setter Property="BorderThickness" Value="2" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
|
||||
@@ -151,8 +151,17 @@
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!-- ============ THE GRID ============ -->
|
||||
<ScrollViewer Grid.Row="3" HorizontalScrollBarVisibility="Disabled">
|
||||
<!--
|
||||
============ THE GRID ============
|
||||
|
||||
A drop target that takes nothing itself. Everything the pointer can be over during a drag is inside
|
||||
this control, and it has to accept one so that the group cards at the top of it stay reachable from
|
||||
the bottom of a long grid: a host dragged off the fortieth card cannot be dropped on a group card
|
||||
that has scrolled out of sight, so a drag held at either edge scrolls this while it is in flight.
|
||||
See HostsScreen.axaml.cs.
|
||||
-->
|
||||
<ScrollViewer Grid.Row="3" x:Name="Scroll" HorizontalScrollBarVisibility="Disabled"
|
||||
DragDrop.AllowDrop="True">
|
||||
<StackPanel Margin="16,14" Spacing="16">
|
||||
|
||||
<!-- ============ GROUPS ============ -->
|
||||
@@ -198,10 +207,18 @@
|
||||
SelectedItem is GroupFilter and not SelectedGroup. The two are nearly the same thing here —
|
||||
GroupFilter assigns SelectedGroup — but only one of them may narrow the grid; see the property
|
||||
for why the phone's own use of SelectedGroup must not.
|
||||
|
||||
◆ THESE CARDS ARE THE DROP TARGET. A host card dragged onto one is filed under that group, and
|
||||
that is the whole of what a drag does on this screen. It used to be a heading inside the host
|
||||
grid, and the headings are gone — see the note on the host grid below. A group is a thing that
|
||||
is drawn once, at the top, where the eye already goes to narrow the grid; a target that is also
|
||||
the label of what you are aiming at needs no explaining. See the code-behind for the gesture.
|
||||
-->
|
||||
<ListBox Classes="tiles" x:Name="GroupGrid"
|
||||
DragDrop.AllowDrop="True"
|
||||
ItemsSource="{Binding Groups}"
|
||||
SelectedItem="{Binding GroupFilter}">
|
||||
SelectedItem="{Binding GroupFilter}"
|
||||
ToolTip.Tip="Press a group to show only its hosts. Drag a host card onto one to file it there.">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate><WrapPanel /></ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
@@ -257,15 +274,31 @@
|
||||
focus off the terminal's child window and then calls Focus() on something that refuses it,
|
||||
leaving the window with nothing focused and the keystrokes going nowhere.
|
||||
|
||||
It binds SidebarRows rather than VisibleHosts, so the group headings are still in it. Two
|
||||
reasons, and neither is decoration: a grid showing every group at once has to say which cards
|
||||
belong to which, and a heading is the drop target that takes a host out of one group and into
|
||||
another. See VaultViewModel.RebuildSidebarRows for why a heading is a row rather than a
|
||||
container, and the code-behind for what a drag does with one.
|
||||
◆ CARDS ONLY, AND NO HEADINGS. It binds VisibleHosts rather than SidebarRows, which is the
|
||||
collection with the group headings folded into it. Those headings were a full-width fold-away
|
||||
bar between the cards — a chevron, a name and a count — and in a wrap of cards they read as a
|
||||
collapsed dropdown that had somehow been dealt the hosts: the moment a first group existed the
|
||||
grid stopped being a grid. Two controls saying "group" on one screen was one too many, and the
|
||||
cards above are the better of the two. SidebarRows and the fold are still the phone's, whose
|
||||
list has no room for a row of group cards; see Android's HostsScreen.
|
||||
|
||||
What a heading also was is the drop target, and that moved to the group cards rather than
|
||||
going away with it — see the note on them above, and the code-behind for the gesture. A host is
|
||||
taken back out of a group through its own editor, which is the one place "no group" can be said
|
||||
plainly.
|
||||
|
||||
Which group a card is in is on the card, as a chip. That is what the heading above it used to
|
||||
say, and one chip per card says it without an accordion.
|
||||
|
||||
SelectedItem is still SelectedSidebarRow and deliberately not SelectedHost, though every item
|
||||
here is now a host. That property swallows the null the ListBox writes back whenever the
|
||||
collection is rebuilt — which is every filter keystroke and every background sync — where
|
||||
binding the host selection directly would let that null through, and a null host selection
|
||||
closes the drawer and disarms a delete somebody is in the middle of confirming. See the
|
||||
property, and RebuildVisibleHosts on where the real clearing is done.
|
||||
-->
|
||||
<ListBox Classes="tiles" x:Name="HostGrid" Focusable="True"
|
||||
DragDrop.AllowDrop="True"
|
||||
ItemsSource="{Binding SidebarRows}"
|
||||
ItemsSource="{Binding VisibleHosts}"
|
||||
SelectedItem="{Binding SelectedSidebarRow}">
|
||||
|
||||
<ListBox.ItemsPanel>
|
||||
@@ -279,7 +312,8 @@
|
||||
one per card: a ContextMenu inside a DataTemplate would have the row for its data context,
|
||||
and the commands here are the vault's. The code-behind selects whatever was right-clicked
|
||||
before the menu opens, so the selection-based commands act on the card under the pointer,
|
||||
and cancels the menu outright over a group heading, which has no host to act on.
|
||||
and cancels the menu outright over the space around the cards, where there is no host to act
|
||||
on.
|
||||
-->
|
||||
<ListBox.ContextMenu>
|
||||
<ContextMenu>
|
||||
@@ -290,35 +324,9 @@
|
||||
</ContextMenu>
|
||||
</ListBox.ContextMenu>
|
||||
|
||||
<ListBox.DataTemplates>
|
||||
<ListBox.ItemTemplate>
|
||||
|
||||
<!--
|
||||
A heading, and it takes a whole row of the wrap rather than sitting in the flow as another
|
||||
card: it names the run of cards under it, and a heading the width of one card would read
|
||||
as the first of them. The stretch is what MinWidth buys — a WrapPanel gives a child the
|
||||
width it asks for and starts a new line when the line cannot hold it, so a child asking
|
||||
for more than one line holds is alone on its own.
|
||||
-->
|
||||
<DataTemplate DataType="vm:SidebarGroupHeader">
|
||||
<Button Classes="flat grouphead" MinWidth="640"
|
||||
Command="{Binding $parent[ListBox].((vm:VaultViewModel)DataContext).ToggleGroupCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
HorizontalContentAlignment="Stretch">
|
||||
<Grid ColumnDefinitions="Auto,Auto,*">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Chevron}"
|
||||
Foreground="{StaticResource TextFaint}" FontSize="9"
|
||||
VerticalAlignment="Center" Margin="0,0,6,0" />
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="{Binding Label}"
|
||||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Count}" FontSize="11"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
|
||||
Margin="8,0,0,0" HorizontalAlignment="Left" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="vm:HostRowViewModel">
|
||||
<DataTemplate x:DataType="vm:HostRowViewModel">
|
||||
<Border Classes="tile">
|
||||
<StackPanel Spacing="6">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
@@ -389,6 +397,26 @@
|
||||
VerticalAlignment="Top" Margin="6,3,0,0" />
|
||||
</Grid>
|
||||
|
||||
<!--
|
||||
The group this host is filed under, and only when it is filed under one — which is
|
||||
the same rule the group cards above follow, and the reason a keychain nobody has
|
||||
filed anything in looks exactly as it did before groups existed.
|
||||
|
||||
This is what the fold-away heading over a run of cards used to say. A chip on the
|
||||
card says it per host instead, which is the shape the rest of this card already is
|
||||
and the shape a wrap of cards can carry: it survives the cards being reflowed, it
|
||||
needs no row of its own, and it is what changes when a card is dropped onto a group.
|
||||
|
||||
Accent rather than plain, so the one chip that is a *place* is not read as another
|
||||
tag. A group whose name the vault cannot resolve leaves this empty rather than
|
||||
printing an id, for the reason a tag does. See HostRowViewModel.GroupLabel.
|
||||
-->
|
||||
<Border Classes="chip accent" Padding="5,0" HorizontalAlignment="Left"
|
||||
IsVisible="{Binding HasGroup}">
|
||||
<TextBlock Text="{Binding GroupLabel}" FontSize="9.5"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
The tags this host wears. Under the summary rather than beside it, because a host
|
||||
can wear several and the line above is three fixed-width facts where these are not.
|
||||
@@ -412,7 +440,7 @@
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
|
||||
</ListBox.DataTemplates>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<!--
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace DodoSSH.Client.App.Views;
|
||||
internal sealed partial class HostsScreen : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// How a host travels from the card it was picked up on to the heading it is dropped on.
|
||||
/// How a host travels from the card it was picked up on to the group card it is dropped on.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An in-process format carrying the row itself, rather than text carrying an id. The drag never leaves
|
||||
@@ -43,6 +43,21 @@ internal sealed partial class HostsScreen : UserControl
|
||||
/// </remarks>
|
||||
private const double DragThreshold = 5;
|
||||
|
||||
/// <summary>How close to the top or bottom of the grid a drag has to be held to scroll it.</summary>
|
||||
/// <remarks>
|
||||
/// Deeper than a card's own margin, because the band has to be reachable while the pointer is still
|
||||
/// carrying something the user is looking at — a band the width of a hairline would only be found by
|
||||
/// accident, and only by somebody who did not need it.
|
||||
/// </remarks>
|
||||
private const double EdgeBand = 48;
|
||||
|
||||
/// <summary>How far one drag event inside that band moves the grid.</summary>
|
||||
/// <remarks>
|
||||
/// Roughly a third of a card, so a pointer moving inside the band travels the grid at about the speed it
|
||||
/// is moving. A step of a whole card would jump the target out from under the pointer between two events.
|
||||
/// </remarks>
|
||||
private const double EdgeStep = 24;
|
||||
|
||||
/// <summary>The press a drag would start from, or null once it has become one or been let go of.</summary>
|
||||
/// <remarks>
|
||||
/// Held because <see cref="DragDrop.DoDragDropAsync"/> takes the press rather than the movement: the
|
||||
@@ -55,7 +70,7 @@ internal sealed partial class HostsScreen : UserControl
|
||||
|
||||
private Point origin;
|
||||
|
||||
/// <summary>The card or heading the pointer is currently over, while a drag is in flight.</summary>
|
||||
/// <summary>The group card the pointer is currently over, while a drag is in flight.</summary>
|
||||
private ListBoxItem? marked;
|
||||
|
||||
public HostsScreen()
|
||||
@@ -78,9 +93,22 @@ internal sealed partial class HostsScreen : UserControl
|
||||
HostGrid.PointerReleased += OnPointerReleased;
|
||||
HostGrid.PointerCaptureLost += OnPointerCaptureLost;
|
||||
|
||||
DragDrop.AddDragOverHandler(HostGrid, OnDragOver);
|
||||
DragDrop.AddDragLeaveHandler(HostGrid, OnDragLeave);
|
||||
DragDrop.AddDropHandler(HostGrid, OnDrop);
|
||||
// The host grid is where a drag starts and the group cards are where it lands. They used to be the
|
||||
// same control: the target was a heading among the cards, and with the headings gone the group cards
|
||||
// are the only thing on this screen that names a group. A card dropped onto another card is refused
|
||||
// rather than filed beside it — in a grid with no headings there is nothing to say which group that
|
||||
// would be, and a gesture whose result you cannot see before you let go is one that files machines
|
||||
// somewhere the user did not intend.
|
||||
DragDrop.AddDragOverHandler(GroupGrid, OnDragOver);
|
||||
DragDrop.AddDragLeaveHandler(GroupGrid, OnDragLeave);
|
||||
DragDrop.AddDropHandler(GroupGrid, OnDrop);
|
||||
|
||||
// Everywhere else the pointer can be during a drag, and it is a handler rather than the absence of
|
||||
// one because AllowDrop is an inherited property: it is set on the scroller so that a drag anywhere
|
||||
// over the grid is reported at all, and that makes every card inside it a drop target as far as the
|
||||
// platform is concerned. This is where all of them but a group card are turned down — and where a
|
||||
// drag held at the top or bottom edge pulls the grid towards the target.
|
||||
DragDrop.AddDragOverHandler(Scroll, OnDragOverScroll);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
@@ -117,9 +145,9 @@ internal sealed partial class HostsScreen : UserControl
|
||||
/// </remarks>
|
||||
private void OnHostActivated(object? sender, TappedEventArgs e)
|
||||
{
|
||||
// Only over a card. A double-tap on a group heading folds it and unfolds it again, and must not also
|
||||
// connect to whichever host was selected before — which is what an unguarded handler would do, on a
|
||||
// machine the user is not even pointing at.
|
||||
// Only over a card. A double-tap on the space around them must not connect to whichever host was
|
||||
// selected before — which is what an unguarded handler would do, on a machine the user is not even
|
||||
// pointing at.
|
||||
if (Vault is { } vault && RowUnder(e.Source) is HostRowViewModel)
|
||||
{
|
||||
_ = vault.ConnectCommand.ExecuteAsync(null);
|
||||
@@ -137,9 +165,9 @@ internal sealed partial class HostsScreen : UserControl
|
||||
/// against.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Cancelled outright over a group heading and over the space around the cards. Neither is a host, and a
|
||||
/// menu offering Connect, Edit and Delete over one would be three buttons that either do nothing or act
|
||||
/// on something else entirely.
|
||||
/// Cancelled outright over the space around the cards. That is not a host, and a menu offering Connect,
|
||||
/// Edit and Delete over it would be three buttons that either do nothing or act on something else
|
||||
/// entirely.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private void OnContextRequested(object? sender, ContextRequestedEventArgs e)
|
||||
@@ -233,9 +261,9 @@ internal sealed partial class HostsScreen : UserControl
|
||||
/// Says whether what is under the pointer would take this host, and marks it if it would.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A host over its own group's heading is refused, which is not pedantry: <c>DragDropEffects.None</c> is
|
||||
/// what turns the cursor into the "no" one, and a drag that looks like it would do something and then
|
||||
/// does nothing is worse than one that says so while it is still in the air.
|
||||
/// A host over the card of the group it is already in is refused, which is not pedantry:
|
||||
/// <c>DragDropEffects.None</c> is what turns the cursor into the "no" one, and a drag that looks like it
|
||||
/// would do something and then does nothing is worse than one that says so while it is still in the air.
|
||||
/// </remarks>
|
||||
private void OnDragOver(object? sender, DragEventArgs e)
|
||||
{
|
||||
@@ -254,6 +282,60 @@ internal sealed partial class HostsScreen : UserControl
|
||||
|
||||
private void OnDragLeave(object? sender, DragEventArgs e) => Unmark();
|
||||
|
||||
/// <summary>
|
||||
/// Carries the grid under a drag that is over the cards rather than over a group.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Without this the gesture is only available to whoever can see both ends of it.</b> The group cards
|
||||
/// are the first thing in the scrolling stack and the host being filed may be the fortieth card down, and
|
||||
/// a drag cannot use the wheel — the pointer button is held. So a drag held near the top edge pulls the
|
||||
/// grid down towards the target, which is what every file manager does with a drag near the edge of a
|
||||
/// list.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// A step per event rather than a timer, and that is a real limit rather than a simplification: a
|
||||
/// stationary pointer receives no drag events on any platform this runs on, so the scroll follows the
|
||||
/// pointer moving inside the band and stops when it stops. A timer would scroll on its own and would then
|
||||
/// need cancelling on the drop, on the leave, and on the drag that ends outside the window entirely.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Reached only when the group cards did not handle the event first, which is what makes refusing the
|
||||
/// drop here correct: the space around the cards is not a target, and saying so keeps the "no" cursor on
|
||||
/// everything that is not a group.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private void OnDragOverScroll(object? sender, DragEventArgs e)
|
||||
{
|
||||
if (e.DataTransfer.TryGetValue(HostFormat) is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
e.DragEffects = DragDropEffects.None;
|
||||
Unmark();
|
||||
|
||||
var at = e.GetPosition(Scroll).Y;
|
||||
var height = Scroll.Bounds.Height;
|
||||
|
||||
var step = at switch
|
||||
{
|
||||
_ when at < EdgeBand => -EdgeStep,
|
||||
_ when at > height - EdgeBand => EdgeStep,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
if (step == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var furthest = Math.Max(0, Scroll.Extent.Height - Scroll.Viewport.Height);
|
||||
|
||||
Scroll.Offset = Scroll.Offset.WithY(Math.Clamp(Scroll.Offset.Y + step, 0, furthest));
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Fire-and-forget, like every other command this control runs: the move writes to the vault and reports
|
||||
/// itself onto the status line, and a drop handler that awaited it would be an event handler returning a
|
||||
@@ -279,13 +361,12 @@ internal sealed partial class HostsScreen : UserControl
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// A heading is the obvious target and a card is the useful one: dropping onto a machine files the
|
||||
/// dragged host beside it, which means the whole band of cards under a heading is a target rather than
|
||||
/// one line of text. The ungrouped heading is a target like any other, and it is how a host is taken out
|
||||
/// of a group without opening the editor.
|
||||
/// One kind of target: a group card. It is the control that already answers "which group", it is drawn
|
||||
/// at the top of the screen where a drag can reach it from anywhere in the grid, and what it does when
|
||||
/// dropped on is what it says on it. The space around the cards takes nothing.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// A group the vault no longer has is read as no group at all, which is what the grid already does with
|
||||
/// A group the vault no longer has is read as no group at all, which is what the list already does with
|
||||
/// a dangling reference — see <c>VaultViewModel.RebuildSidebarRows</c>. That is decided in the command
|
||||
/// rather than here, so the rule has one home.
|
||||
/// </para>
|
||||
@@ -293,25 +374,13 @@ internal sealed partial class HostsScreen : UserControl
|
||||
private static DropTarget? Target(DragEventArgs e)
|
||||
{
|
||||
if (e.DataTransfer.TryGetValue(HostFormat) is not { } dragged
|
||||
|| Container(e.Source) is not { } container)
|
||||
|| Container(e.Source) is not { DataContext: HostGroupRowViewModel group } container
|
||||
|| dragged.Host.GroupId == group.EntityId)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Guid? group = container.DataContext switch
|
||||
{
|
||||
SidebarGroupHeader header => header.GroupId,
|
||||
HostRowViewModel row => row.Host.GroupId,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (container.DataContext is not (SidebarGroupHeader or HostRowViewModel)
|
||||
|| dragged.Host.GroupId == group)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DropTarget(dragged, group, container);
|
||||
return new DropTarget(dragged, group.EntityId, container);
|
||||
}
|
||||
|
||||
private void Mark(ListBoxItem container)
|
||||
@@ -353,5 +422,10 @@ internal sealed partial class HostsScreen : UserControl
|
||||
: null;
|
||||
|
||||
/// <summary>A drag in flight, and where it would land.</summary>
|
||||
private sealed record DropTarget(HostRowViewModel Host, Guid? GroupId, ListBoxItem Container);
|
||||
/// <remarks>
|
||||
/// The group is a <see cref="Guid"/> rather than a nullable one, which it was while the ungrouped
|
||||
/// heading was also a target. Every target is now a group card and every group card has an id; the way
|
||||
/// out of a group is the host's own editor, which is the one place "no group" can be said in words.
|
||||
/// </remarks>
|
||||
private sealed record DropTarget(HostRowViewModel Host, Guid GroupId, ListBoxItem Container);
|
||||
}
|
||||
|
||||
@@ -222,6 +222,31 @@ internal sealed partial class HostRowViewModel(
|
||||
/// <summary>Whether this row has a vault to name.</summary>
|
||||
internal bool HasVaultBadge => VaultBadge.Length > 0;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the group this host is filed under, or empty for a host that is in none.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// What the desktop's grid draws as a chip on the card. It is the one thing the fold-away group headings
|
||||
/// between the cards used to say, and the reason removing them cost nothing: a card that names its own
|
||||
/// group answers the question per host, where a heading answered it per run of cards and needed the grid
|
||||
/// to be sorted into runs to do it. The phone still draws headings — its list has no room for the row of
|
||||
/// group cards the desktop puts above the grid — so <see cref="SidebarGroupHeader"/> stays.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Resolved once when the list is built, like <see cref="TagLabels"/> and for the same two reasons: the
|
||||
/// row stores an id and a chip shows a name, and the answer cannot change without the list being rebuilt.
|
||||
/// <b>A group id that does not resolve leaves this empty</b> rather than printing the id — the reference
|
||||
/// is allowed to dangle, deleting a group deliberately does not rewrite the hosts in it, and "the group
|
||||
/// this names is not here" and "this names no group" are the same thing to look at. See
|
||||
/// <see cref="HostSecret.GroupId"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal string GroupLabel { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>Whether this host is filed under a group the vault can name.</summary>
|
||||
internal bool HasGroup => GroupLabel.Length > 0;
|
||||
|
||||
internal HostSecret Host => host.Secret;
|
||||
|
||||
/// <summary>
|
||||
@@ -1051,8 +1076,8 @@ internal sealed partial class VaultViewModel(
|
||||
"No hosts yet. Press + NEW HOST to add one, or import the machines already in this computer's "
|
||||
+ "~/.ssh/config from Preferences.",
|
||||
(_, not null, 0) =>
|
||||
"Nothing is filed under this group yet. Drag a host onto its heading in the grid, or choose the "
|
||||
+ "group in a host's own editor.",
|
||||
"Nothing is filed under this group yet. Press SHOW ALL, then drag a host card onto this group's "
|
||||
+ "card — or choose the group in a host's own editor.",
|
||||
_ => "No host matches that. The name, the address and the notes are all searched.",
|
||||
};
|
||||
|
||||
@@ -2396,6 +2421,12 @@ internal sealed partial class VaultViewModel(
|
||||
// Only when there is something to tell apart. A badge on every row of a
|
||||
// single-vault list is noise that says the same thing on all of them.
|
||||
VaultBadge = several ? vault.Name.ToUpperInvariant() : string.Empty,
|
||||
|
||||
// Every readable vault's groups, not the active one's, because a host in a team's
|
||||
// vault is filed under that team's group — and looked up here rather than on the row
|
||||
// for the reason the tag names are: the map is the list's, and a row that reached for
|
||||
// it would be a lookup per chip per redraw.
|
||||
GroupLabel = GroupLabelFor(item.Secret.GroupId),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -2712,6 +2743,17 @@ internal sealed partial class VaultViewModel(
|
||||
.OrderBy(label => label, StringComparer.CurrentCulture),
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// The name behind a host's group id, or empty where there is none to show.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Empty covers both "this host is in no group" and "the group it names is not in this vault any more",
|
||||
/// which is the same answer the list gives a dangling reference everywhere else. See
|
||||
/// <see cref="HostRowViewModel.GroupLabel"/>.
|
||||
/// </remarks>
|
||||
private string GroupLabelFor(Guid? groupId) =>
|
||||
groupId is { } id && groupsById.TryGetValue(id, out var group) ? group.Label : string.Empty;
|
||||
|
||||
/// <summary>Refills <see cref="Groups"/>, counting the hosts filed under each.</summary>
|
||||
private void RebuildGroups()
|
||||
{
|
||||
@@ -2895,10 +2937,10 @@ internal sealed partial class VaultViewModel(
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// What dragging a row onto a heading does, and the only thing in this application that changes a host
|
||||
/// without opening the editor. That is the justification for it existing at all: filing thirty imported
|
||||
/// machines meant thirty rounds of open, pick, save, and the field being changed is the one field of a
|
||||
/// host that is about arrangement rather than about the machine.
|
||||
/// What dragging a host card onto a group card does, and the only thing in this application that changes
|
||||
/// a host without opening the editor. That is the justification for it existing at all: filing thirty
|
||||
/// imported machines meant thirty rounds of open, pick, save, and the field being changed is the one
|
||||
/// field of a host that is about arrangement rather than about the machine.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// It writes the saved host rather than the editor's contents, and refuses while the editor is open. A
|
||||
|
||||
Reference in New Issue
Block a user