Public Access
Choose more than one host card on the desktop, the way the phone already can
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.
This commit is contained in:
@@ -377,12 +377,164 @@
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="label" Text="HOSTS"
|
||||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
|
||||
|
||||
<!--
|
||||
============ ◆ WHAT SAYS A SET IS UP ============
|
||||
Ctrl, Shift and a band dragged across the grid tick cards — see HostsScreen.axaml.cs — and
|
||||
this is the whole of the chrome that arrives with them: how many, the way out, and where the
|
||||
actions are. The phone raises a contextual action bar in the vault header's place for the
|
||||
same state; a window with room for a context menu does not need one, and a strip of eight
|
||||
buttons over the grid would be a second home for entries the menu already has.
|
||||
|
||||
◆ THE MENU IS WHERE THE ACTIONS ARE, and the sentence says so because there is no other way
|
||||
to find that out. Everything else on this screen can be reached by looking at it.
|
||||
|
||||
A Grid rather than a horizontal StackPanel, for the reason the host card carries at length: a
|
||||
horizontal StackPanel measures its children with infinite width, so the sentence would never
|
||||
learn it is short of room and would run out over the count at the far end instead of
|
||||
trimming. At the window's minimum with the drawer open there is not room for all of it.
|
||||
-->
|
||||
<Grid Grid.Column="1" Margin="12,0" ColumnDefinitions="Auto,Auto,*"
|
||||
IsVisible="{Binding IsChoosingHosts}">
|
||||
<Border Grid.Column="0" Classes="chip accent" Padding="6,1" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding ChosenHostsSummary}" FontSize="9.5" />
|
||||
</Border>
|
||||
<Button Grid.Column="1" Classes="flat" Margin="6,0,0,0" Padding="5,1" FontSize="9.5"
|
||||
Content="CLEAR" Command="{Binding ClearHostChoiceCommand}"
|
||||
ToolTip.Tip="Takes the ticks off every card · Esc, or a click on the space around them" />
|
||||
<TextBlock Grid.Column="2" Classes="hint" FontSize="10" Margin="8,0,0,0"
|
||||
VerticalAlignment="Center" TextTrimming="CharacterEllipsis"
|
||||
Text="right-click one of them for what can be done to all of them" />
|
||||
</Grid>
|
||||
|
||||
<!-- The collection's own count, so it follows both filters with no second number to keep in step. -->
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding VisibleHosts.Count}"
|
||||
FontSize="11" Foreground="{StaticResource TextFaint}"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<!--
|
||||
============ ◆ WHAT THE SET'S MENU ASKED ============
|
||||
Three panels, at most one of them up, all three above the grid rather than over it — the same
|
||||
arrangement the GROUPS section uses and the phone's list uses, for the reason written there: a
|
||||
card laid over the cards hides the ticked ones, and which hosts are ticked is the information
|
||||
the question exists to give. The grid shortens instead.
|
||||
|
||||
Which of the three is showing is decided in the view model, because each disarms the other two
|
||||
on the way up. See MoveChosenHostsToVault, RegroupChosenHosts and DeleteChosenHosts.
|
||||
-->
|
||||
|
||||
<!--
|
||||
◆ MOVING OR COPYING THEM TO ANOTHER VAULT. One panel for both verbs, because they differ in one
|
||||
word and in the sentence under the picker; what varies is bound rather than duplicated. See
|
||||
VaultViewModel.ChosenHostVaultPanelTitle and its two siblings.
|
||||
|
||||
The sentence is not decoration: groups and tags are items of the vault being left, so neither
|
||||
can come along, and a host that arrived carrying either would point at something the
|
||||
destination does not contain.
|
||||
-->
|
||||
<Border Padding="10" Background="{StaticResource Panel}" CornerRadius="6"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="1"
|
||||
IsVisible="{Binding IsSendingChosenHostsToAVault}">
|
||||
<StackPanel Spacing="8">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<TextBlock Grid.Column="0" Classes="label"
|
||||
Text="{Binding ChosenHostVaultPanelTitle}" />
|
||||
<TextBlock Grid.Column="1" Classes="mono" Margin="8,0,0,0" FontSize="11"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
|
||||
Text="{Binding ChosenHostsSummary}" />
|
||||
</Grid>
|
||||
|
||||
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding ChosenHostVaultChoices}"
|
||||
SelectedItem="{Binding SelectedChosenHostVault}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:VaultChoiceViewModel">
|
||||
<TextBlock Text="{Binding Display}" FontSize="12" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
||||
Text="{Binding ChosenHostVaultPanelNote}" />
|
||||
|
||||
<!--
|
||||
◆ THE KEY, and only for a move of exactly one host — which key to carry is a fact about one
|
||||
machine, and a copy that took it away would leave the original unable to connect. Unticked,
|
||||
because moving a key into a team's vault hands it to everybody holding that vault's key.
|
||||
See VaultViewModel.BringsTheChosenBindingAlong.
|
||||
-->
|
||||
<CheckBox IsVisible="{Binding HasAChosenBindingToBring}"
|
||||
IsChecked="{Binding BringsTheChosenBindingAlong}">
|
||||
<TextBlock FontSize="11.5" TextWrapping="Wrap"
|
||||
Text="{Binding ChosenBindingToBringQuestion}" />
|
||||
</CheckBox>
|
||||
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
||||
IsVisible="{Binding HasAChosenBindingToBring}"
|
||||
Text="{Binding ChosenBindingToBringNote}" />
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="accent" Content="{Binding ChosenHostVaultPanelVerb}"
|
||||
Command="{Binding ConfirmSendChosenHostsToAVaultCommand}"
|
||||
IsEnabled="{Binding !IsBusy}" />
|
||||
<Button Classes="ghost" Content="CANCEL"
|
||||
Command="{Binding CancelSendChosenHostsToAVaultCommand}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
◆ FILING THEM UNDER A GROUP, which is what dragging the set onto a group card does without the
|
||||
picker. Both exist for the reason the single-host gesture and the host editor both do: a
|
||||
gesture is unreachable without a pointer, and a card that has scrolled out of sight cannot be
|
||||
dragged onto.
|
||||
|
||||
One keychain's groups. A selection spanning two is refused before this panel is drawn at all —
|
||||
see VaultViewModel.RegroupChosenHosts, which is the same refusal the drop makes.
|
||||
-->
|
||||
<Border Padding="10" Background="{StaticResource Panel}" CornerRadius="6"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="1"
|
||||
IsVisible="{Binding IsRegroupingChosenHosts}">
|
||||
<StackPanel Spacing="8">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<TextBlock Grid.Column="0" Classes="label" Text="CHANGE GROUP" />
|
||||
<TextBlock Grid.Column="1" Classes="mono" Margin="8,0,0,0" FontSize="11"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
|
||||
Text="{Binding ChosenHostsSummary}" />
|
||||
</Grid>
|
||||
|
||||
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding ChosenHostGroupChoices}"
|
||||
SelectedItem="{Binding SelectedChosenHostGroup}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:GroupChoice">
|
||||
<TextBlock Text="{Binding Label}" FontSize="12" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
||||
Text="A group lends its port, its account and its key to every host filed under it that says nothing itself, so this can change what these machines dial. Nothing else about them moves." />
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="accent" Content="FILE"
|
||||
Command="{Binding ConfirmRegroupChosenHostsCommand}"
|
||||
IsEnabled="{Binding !IsBusy}" />
|
||||
<Button Classes="ghost" Content="CANCEL"
|
||||
Command="{Binding CancelRegroupChosenHostsCommand}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
◆ THE DELETION QUESTION, naming a count rather than a machine — which is the reason
|
||||
DeletionTarget.ChosenHosts exists: six copies of "delete prod-db?" is not a confirmation
|
||||
anybody reads. The shared card draws it, as it does for a group above and for one host in the
|
||||
drawer; see ConfirmDeleteCard.
|
||||
-->
|
||||
<Border Padding="10" Background="{StaticResource DangerWash}" CornerRadius="6"
|
||||
IsVisible="{Binding IsConfirmingChosenHostDeletion}">
|
||||
<views:ConfirmDeleteCard />
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
Named because it is where keyboard focus lands when the terminal gives it back, and because
|
||||
every gesture on it is wired in the code-behind.
|
||||
@@ -426,7 +578,8 @@
|
||||
-->
|
||||
<ListBox Classes="tiles" x:Name="HostGrid" Focusable="True"
|
||||
ItemsSource="{Binding VisibleHosts}"
|
||||
SelectedItem="{Binding SelectedSidebarRow}">
|
||||
SelectedItem="{Binding SelectedSidebarRow}"
|
||||
ToolTip.Tip="Double-press a card for a shell. Ctrl or Shift picks out several, and so does a band dragged across the space between them; right-click any of them for what can be done to the set.">
|
||||
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate><WrapPanel /></ItemsPanelTemplate>
|
||||
@@ -449,13 +602,53 @@
|
||||
already selected whatever was right-clicked, which is exactly what OpenHostPane falls back
|
||||
to.
|
||||
-->
|
||||
<!--
|
||||
◆ TWO MENUS IN ONE, AND WHICH OF THEM IS DRAWN IS WHETHER ANYTHING IS TICKED.
|
||||
|
||||
Once Ctrl, Shift or a band has ticked cards, this menu is about the set and nothing else —
|
||||
and it has to be, because the entries above act on the vault's *selection*, which is one
|
||||
host. A Delete… that asked about the card under the pointer while six sat ticked behind the
|
||||
menu is the exact mistake the whole of OnContextRequested exists to prevent, arrived at from
|
||||
the other direction. Right-clicking a card that is not in the set drops the set first, so
|
||||
these entries and the ones above are never both about something; see the code-behind.
|
||||
|
||||
This is the only home for the seven, which is why it says what each of them takes: a set from
|
||||
two keychains, a set with a host in it that a newer version wrote, and a set of one are three
|
||||
different answers. The three single-host entries are collapsed rather than greyed for the
|
||||
reason the phone's sheet collapses them — a terminal, an SFTP session and a form are each
|
||||
about one machine, and there is no reading of "edit these six".
|
||||
|
||||
Delete is under a separator, as it is above and as the phone's sheet has it.
|
||||
-->
|
||||
<ListBox.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="Connect" Command="{Binding ConnectCommand}" />
|
||||
<MenuItem Header="Details…" Command="{Binding OpenHostPaneCommand}" />
|
||||
<MenuItem Header="Edit…" Command="{Binding EditSelectedHostCommand}" />
|
||||
<Separator />
|
||||
<MenuItem Header="Delete…" Command="{Binding DeleteHostCommand}" />
|
||||
<MenuItem Header="Connect" IsVisible="{Binding !IsChoosingHosts}"
|
||||
Command="{Binding ConnectCommand}" />
|
||||
<MenuItem Header="Details…" IsVisible="{Binding !IsChoosingHosts}"
|
||||
Command="{Binding OpenHostPaneCommand}" />
|
||||
<MenuItem Header="Edit…" IsVisible="{Binding !IsChoosingHosts}"
|
||||
Command="{Binding EditSelectedHostCommand}" />
|
||||
<Separator IsVisible="{Binding !IsChoosingHosts}" />
|
||||
<MenuItem Header="Delete…" IsVisible="{Binding !IsChoosingHosts}"
|
||||
Command="{Binding DeleteHostCommand}" />
|
||||
|
||||
<MenuItem Header="Connect" IsVisible="{Binding HasOneChosenHost}"
|
||||
Command="{Binding ConnectToChosenHostCommand}" />
|
||||
<MenuItem Header="Browse files" IsVisible="{Binding HasOneChosenHost}"
|
||||
Command="{Binding BrowseChosenHostCommand}" />
|
||||
<MenuItem Header="Edit…" IsVisible="{Binding HasOneChosenHost}"
|
||||
Command="{Binding EditChosenHostCommand}" />
|
||||
<MenuItem Header="Change group…" IsVisible="{Binding IsChoosingHosts}"
|
||||
Command="{Binding RegroupChosenHostsCommand}" />
|
||||
<MenuItem Header="Move to another vault…" IsVisible="{Binding IsChoosingHosts}"
|
||||
Command="{Binding MoveChosenHostsToVaultCommand}" />
|
||||
<MenuItem Header="Copy to another vault…" IsVisible="{Binding IsChoosingHosts}"
|
||||
Command="{Binding CopyChosenHostsToVaultCommand}" />
|
||||
<MenuItem Header="Duplicate" IsVisible="{Binding IsChoosingHosts}"
|
||||
Command="{Binding DuplicateChosenHostsCommand}" />
|
||||
<Separator IsVisible="{Binding IsChoosingHosts}" />
|
||||
<MenuItem Header="Delete…" IsVisible="{Binding IsChoosingHosts}"
|
||||
Command="{Binding DeleteChosenHostsCommand}" />
|
||||
</ContextMenu>
|
||||
</ListBox.ContextMenu>
|
||||
|
||||
@@ -466,7 +659,15 @@
|
||||
The address is on the tooltip and nowhere else on the card. It used to be the subtitle;
|
||||
see the note on the summary line below for what took its place and why it is not lost.
|
||||
-->
|
||||
<Border Classes="tile" ToolTip.Tip="{Binding Address}">
|
||||
<!--
|
||||
◆ Classes.chosen is the tick on the card, and it is a class rather than the list's own
|
||||
selection because they are two different things: the list holds one card, which is what
|
||||
the drawer and CONNECT are about, and the set is what Ctrl, Shift and the band build. A
|
||||
card can be in the set without being the selected one, which is what a run of six looks
|
||||
like. See VaultViewModel.ChooseHosts and Border.tile.chosen in App.axaml.
|
||||
-->
|
||||
<Border Classes="tile" Classes.chosen="{Binding IsChosen}"
|
||||
ToolTip.Tip="{Binding Address}">
|
||||
<!--
|
||||
A Panel so the pencil below can be drawn *over* the card rather than in a column of
|
||||
its own. A column would have cost the name 30 pixels of a 232-pixel tile — permanently,
|
||||
@@ -543,9 +744,21 @@
|
||||
now; grey means there is not one. It is deliberately not reachability — nothing
|
||||
here pings anything, and a dot that meant "up" would be a claim this application
|
||||
never checks.
|
||||
|
||||
◆ The tick sits beside it rather than in place of it, which is the arrangement
|
||||
the phone's row uses and for the same reason: the dot says whether a shell is
|
||||
open on this machine, and swapping it for the tick would make choosing a host
|
||||
hide a fact about it. It takes no width while it is collapsed, so a grid with
|
||||
nothing ticked is laid out exactly as it was.
|
||||
-->
|
||||
<Ellipse Grid.Column="2" Classes="dot" Classes.live="{Binding IsConnected}"
|
||||
VerticalAlignment="Top" Margin="6,3,0,0" />
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="4"
|
||||
VerticalAlignment="Top" Margin="6,2,0,0">
|
||||
<TextBlock Text="✓" FontSize="11" IsVisible="{Binding IsChosen}"
|
||||
Foreground="{StaticResource AccentText}"
|
||||
VerticalAlignment="Center" />
|
||||
<Ellipse Classes="dot" Classes.live="{Binding IsConnected}"
|
||||
VerticalAlignment="Center" Margin="0,1,0,0" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!--
|
||||
@@ -625,6 +838,26 @@
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!--
|
||||
◆ THE BAND, drawn over the scroller rather than inside it.
|
||||
|
||||
It is a rectangle the pointer is dragging out right now, so it belongs in the viewport's coordinates
|
||||
and not in the scrolling content's: a band drawn inside the stack would slide away from the pointer
|
||||
the moment the grid scrolled under it. Same row as the ScrollViewer and after it, which is what puts
|
||||
it on top — a Grid cell stacks its children in declaration order.
|
||||
|
||||
IsHitTestVisible="False" is the whole of why the gesture works while it is up. The band is under the
|
||||
pointer by definition, and a rectangle that took the pointer would end the drag it is drawing.
|
||||
|
||||
Positioned by Margin from the top left rather than in a Canvas, because the two alignments below make
|
||||
the margin mean exactly "where the corner is"; see HostsScreen.axaml.cs, which is the only thing that
|
||||
writes to it.
|
||||
-->
|
||||
<Border Grid.Row="3" x:Name="Band" IsVisible="False" IsHitTestVisible="False"
|
||||
HorizontalAlignment="Left" VerticalAlignment="Top"
|
||||
Background="{StaticResource AccentWash}" BorderBrush="{StaticResource Accent}"
|
||||
BorderThickness="1" CornerRadius="2" />
|
||||
|
||||
</Grid>
|
||||
|
||||
<!--
|
||||
|
||||
Reference in New Issue
Block a user