Public Access
Make the host pane something you ask for, and draw it as cards
THE DRAWER USED TO ARRIVE WITH THE SELECTION. IsDrawerOpen read "a host is selected", so touching any card took 304 pixels off the grid — including every card arrowed past on the way to the one somebody wanted. Choosing among forty machines was charged the price of the pane for one of them. A pencil now appears on the card under the pointer and on the selected card, and that is what opens it; IsHostPaneOpen is the flag, and the grid's context menu gained Details… so the pane is reachable without a pointer, which a hover-only control is not. Once open it follows the selection rather than pinning the host it was opened on: a pane about one host beside a grid marking a different one is two answers to the same question. Losing the selection closes it and clears the flag, or a filter matching nothing would leave the pane armed to spring open again on the next card merely selected — which is the behaviour the pencil exists to remove. The pencil is 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, for a control that is only there while the pointer is; the dot and the pencil stack in the two corners of that edge instead. IsVisible and not opacity, because a button at zero opacity still takes the click and the card underneath does not. A HEADER, A BODY THAT SCROLLS, AND A FOOTER, which is the one structural change in the pane. The header names what the drawer is about and carries the two things true of every panel; the footer carries the one thing each panel is for — CONNECT, or SAVE, or the question about deleting. Only the middle scrolls, so the button somebody came here to press can no longer be below the fold, which CONNECT could be on a host with fifteen tags. That also widens what the layout harness certifies: it skips anything inside a ScrollViewer, and the control each panel exists to offer is now outside one. THE SAME THREE CARDS TWICE. Address, General, Connection — first as rows stating what the host is, then as boxes for changing it. The detail pane's rows are buttons that open the editor: the design draws every fact as a filled box, and rather than draw an input that refuses the pointer, pressing one leads to the same card with a real box in it. Nothing here saves as you type, and that is not timidity — saving validates the key-or-credential exclusion and writes one encrypted payload, so a box committing per keystroke would be a save per character and a half-typed hostname on the wire. Every value the pane prints is the resolved one, and says "inherited" beside it where a group supplied it. The number is the same either way and the edit is not: clearing a group's default moves every host that never overrode it. A HOST CARD IS TWO LINES AND NO CHIPS. The subtitle is now "ssh, root, pci, eu-west-1" — the transport, the resolved account, then every tag — replacing both the user@host:port line and the wrapped row of tag chips under it. The address went to the card's tooltip rather than nowhere: a card is read while scanning forty machines, where the name and the kind of machine are what is being looked for, and an address is what you read once you have found it. "ssh" is a constant today and is printed anyway, which is the one thing here that argues with this codebase's own rule about constants dressed as readings. It is the first item of a list whose other items vary, and a list beginning with the account on one card and a tag on the next has no shape to scan. The remark on HostRowViewModel.Summary says so rather than leaving it to be discovered. WHAT THE DESIGN DRAWS AND THIS PANE HAS NOT GOT: Share this host, Add Telnet, "SSH ID, Certificate, FIDO2", the backspace-key mapping row, the vault picker's chevron and Show more. Sharing is per vault and not per item, every session here is an SSH channel, there are no identity or certificate item types, nothing carries a terminal setting to the renderer, and an item cannot be moved between vaults at all. Six controls with nothing behind them, listed in docs/design-import-gaps.md with what ships instead, and none drawn disabled. The credentials row is marked with ◆ rather than the ⚿ the nav rail uses for the keychain. U+26BF is outside both faces this application substitutes for the design's fonts, so it lands on whatever the platform's fallback has; every other glyph in the pane is from Geometric Shapes, which both carry.
This commit is contained in:
@@ -16,7 +16,12 @@
|
||||
|
||||
Cards split the two jobs. The grid is about all of them — how many groups there are, which machines are
|
||||
in one, which have a shell open — and it gets the full width to say so. The drawer is about one, and it
|
||||
is only there when there is one. See HostDrawer and VaultViewModel.IsDrawerOpen.
|
||||
is only there when it has been asked for. See HostDrawer and VaultViewModel.IsDrawerOpen.
|
||||
|
||||
◆ ASKED FOR, AND IT USED TO ARRIVE WITH THE SELECTION. Touching any card opened the drawer, so choosing
|
||||
a host cost 304 pixels of grid every time — including for every card passed over on the way to the one
|
||||
somebody wanted. The pencil on a card is what opens it now; selecting is free again. See the item
|
||||
template below and VaultViewModel.IsHostPaneOpen.
|
||||
|
||||
── WHAT IS DELIBERATELY NOT HERE ────────────────────────────────────────────────────────────────────
|
||||
The design this was drawn from puts a view-mode switch, a tag filter, a calendar and a share control in
|
||||
@@ -315,9 +320,17 @@
|
||||
and cancels the menu outright over the space around the cards, where there is no host to act
|
||||
on.
|
||||
-->
|
||||
<!--
|
||||
Details is here as well as on the pencil, and that is not a duplicate for its own sake: the
|
||||
pencil appears on hover, so it is unreachable without a pointer, and this menu is what a
|
||||
keyboard and the context key can get to. It passes no parameter — the code-behind has
|
||||
already selected whatever was right-clicked, which is exactly what OpenHostPane falls back
|
||||
to.
|
||||
-->
|
||||
<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}" />
|
||||
@@ -327,116 +340,148 @@
|
||||
<ListBox.ItemTemplate>
|
||||
|
||||
<DataTemplate x:DataType="vm:HostRowViewModel">
|
||||
<Border Classes="tile">
|
||||
<StackPanel Spacing="6">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<!--
|
||||
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}">
|
||||
<!--
|
||||
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,
|
||||
for a control that is only there while the pointer is — and the name is the thing the
|
||||
grid exists to be scanned by.
|
||||
-->
|
||||
<Panel>
|
||||
<StackPanel Spacing="6">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
<Border Grid.Column="0" Classes="tileicon" Background="{StaticResource Chip}">
|
||||
<TextBlock Classes="mono" Text=">_" FontSize="11" FontWeight="Bold"
|
||||
Foreground="{StaticResource AccentText}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<Border Grid.Column="0" Classes="tileicon" Background="{StaticResource Chip}">
|
||||
<TextBlock Classes="mono" Text=">_" FontSize="11" FontWeight="Bold"
|
||||
Foreground="{StaticResource AccentText}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1" Margin="10,0,0,0" VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<!--
|
||||
◆ A GRID AND NOT A HORIZONTAL StackPanel, on both of these rows, and the two
|
||||
look interchangeable until the text is too long for the card.
|
||||
<StackPanel Grid.Column="1" Margin="10,0,0,0" VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<!--
|
||||
◆ A GRID AND NOT A HORIZONTAL StackPanel, on both of these rows, and the two
|
||||
look interchangeable until the text is too long for the card.
|
||||
|
||||
A horizontal StackPanel measures every child with infinite width, so a
|
||||
TextBlock inside one never learns it is short of room and TextTrimming never
|
||||
fires — the text simply runs on past the card's border and over the card beside
|
||||
it. The card is a fixed 248 and a host name is not, so that is the ordinary
|
||||
case rather than the edge one.
|
||||
A horizontal StackPanel measures every child with infinite width, so a
|
||||
TextBlock inside one never learns it is short of room and TextTrimming never
|
||||
fires — the text simply runs on past the card's border and over the card beside
|
||||
it. The card is a fixed 248 and a host name is not, so that is the ordinary
|
||||
case rather than the edge one.
|
||||
|
||||
The star column is the part that gives way and the Auto column is the part that
|
||||
must not: a badge saying "not synced" and the word that says how a host
|
||||
authenticates are both short and both meaningless trimmed, where a hostname
|
||||
with its tail cut is still the machine you were looking for.
|
||||
-->
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Label}" FontWeight="Medium"
|
||||
Foreground="{StaticResource Text}"
|
||||
The star column is the part that gives way and the Auto column is the part that
|
||||
must not: a badge saying "not synced" and the word that says how a host
|
||||
authenticates are both short and both meaningless trimmed, where a hostname
|
||||
with its tail cut is still the machine you were looking for.
|
||||
-->
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Label}" FontWeight="Medium"
|
||||
Foreground="{StaticResource Text}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<Border Grid.Column="1" Classes="chip warn" Padding="4,0" Margin="6,0,0,0"
|
||||
IsVisible="{Binding Badge, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<TextBlock Text="{Binding Badge}" FontSize="9.5" />
|
||||
</Border>
|
||||
</Grid>
|
||||
<!--
|
||||
◆ ONE COMMA-SEPARATED LINE: the transport, the account, and every tag this host
|
||||
wears. It replaces two things — the address-and-authentication row that was here,
|
||||
and the row of tag chips that used to sit under the whole card.
|
||||
|
||||
What went is the address, and it went to the tooltip above rather than nowhere.
|
||||
A card is read while scanning forty machines, and at that moment the name and
|
||||
what kind of machine it is are what is being looked for; an address is what you
|
||||
read once you have found it, which is what the drawer is for. The word saying
|
||||
how it authenticates went with it, for a smaller reason: it was there to warn
|
||||
that a password box would be needed, and the box is in the pane beside that
|
||||
warning now.
|
||||
|
||||
See HostRowViewModel.Summary, and the remark there on why "ssh" is printed at
|
||||
all when every host in this product is one.
|
||||
-->
|
||||
<TextBlock Text="{Binding Summary}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextFaint}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<Border Grid.Column="1" Classes="chip warn" Padding="4,0" Margin="6,0,0,0"
|
||||
IsVisible="{Binding Badge, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<TextBlock Text="{Binding Badge}" FontSize="9.5" />
|
||||
</Border>
|
||||
</Grid>
|
||||
<!--
|
||||
Which vault this host is in, and only when there is more than one to be in. It
|
||||
decides who else can see the host and where an edit goes back to, so on a grid
|
||||
spanning several vaults it is not decoration.
|
||||
-->
|
||||
<TextBlock Classes="mono" Text="{Binding VaultBadge}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextFaint}"
|
||||
IsVisible="{Binding HasVaultBadge}" />
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
The address and how it authenticates, on one line. Which of the three ways is
|
||||
here because only one of them wants a password typed, and an empty password box
|
||||
on a key-authenticated host is otherwise indistinguishable from one somebody
|
||||
forgot to fill in.
|
||||
Connected, and nothing more. Green means a terminal is open on this host right
|
||||
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.
|
||||
-->
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="{Binding Address}"
|
||||
FontSize="10.5" Foreground="{StaticResource TextFaint}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Authentication}"
|
||||
FontSize="10.5" Margin="6,0,0,0"
|
||||
Foreground="{StaticResource TextFaint}" />
|
||||
</Grid>
|
||||
<!--
|
||||
Which vault this host is in, and only when there is more than one to be in. It
|
||||
decides who else can see the host and where an edit goes back to, so on a grid
|
||||
spanning several vaults it is not decoration.
|
||||
-->
|
||||
<TextBlock Classes="mono" Text="{Binding VaultBadge}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextFaint}"
|
||||
IsVisible="{Binding HasVaultBadge}" />
|
||||
</StackPanel>
|
||||
<Ellipse Grid.Column="2" Classes="dot" Classes.live="{Binding IsConnected}"
|
||||
VerticalAlignment="Top" Margin="6,3,0,0" />
|
||||
</Grid>
|
||||
|
||||
<!--
|
||||
Connected, and nothing more. Green means a terminal is open on this host right
|
||||
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 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.
|
||||
-->
|
||||
<Ellipse Grid.Column="2" Classes="dot" Classes.live="{Binding IsConnected}"
|
||||
VerticalAlignment="Top" Margin="6,3,0,0" />
|
||||
</Grid>
|
||||
<Border Classes="chip accent" Padding="5,0" HorizontalAlignment="Left"
|
||||
IsVisible="{Binding HasGroup}">
|
||||
<TextBlock Text="{Binding GroupLabel}" FontSize="9.5"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
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.
|
||||
<!--
|
||||
A wrapped row of tag chips was here. The tags are in the summary line above now,
|
||||
after the account, which is what took the card from three rows and a wrap down to
|
||||
two: a chip and a comma-separated name say the same thing, and only one of them
|
||||
needs a row of its own. Chips are still what a tag looks like where it is *chosen* —
|
||||
the editor's picker in the drawer, and the phone's own list. See
|
||||
HostRowViewModel.Summary.
|
||||
-->
|
||||
</StackPanel>
|
||||
|
||||
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.
|
||||
<!--
|
||||
◆ THE PENCIL, AND THE ONLY WAY THE DRAWER OPENS FROM THIS GRID.
|
||||
|
||||
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>
|
||||
Selecting a card no longer opens it. That used to be automatic, which meant the grid
|
||||
gave up 304 pixels the moment anybody touched a card — including every card arrowed
|
||||
past on the way to the one they wanted. Opening the pane is a thing you ask for now,
|
||||
and this is where the asking is. See VaultViewModel.IsHostPaneOpen.
|
||||
|
||||
<!--
|
||||
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.
|
||||
Bottom right, under the status dot rather than beside it, and over the card rather
|
||||
than in a column: the two marks a card can carry on that edge are "a shell is open
|
||||
here" and "open this one's pane", and stacking them is what keeps both out of the
|
||||
name's width. It appears on hover and on the selected card; see Button.rowedit in
|
||||
App.axaml for why that is IsVisible rather than an opacity.
|
||||
|
||||
Names, not ids: a tag the vault cannot resolve is left out rather than drawn. See
|
||||
HostRowViewModel.TagLabels.
|
||||
-->
|
||||
<ItemsControl ItemsSource="{Binding TagLabels}" IsVisible="{Binding HasTags}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><WrapPanel /></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<Border Classes="chip" Padding="5,0" Margin="0,0,4,2">
|
||||
<TextBlock Text="{Binding}" FontSize="9.5" />
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
The row is the command's parameter rather than being left to the selection. A button
|
||||
inside a ListBoxItem takes the press itself, and whether the item is also selected by
|
||||
it is the theme's business — so a command reading SelectedHost would open the pane on
|
||||
whichever card happened to be lit, which on the first click of a session is none of
|
||||
them.
|
||||
-->
|
||||
<Button Classes="flat rowedit" Content="✎"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Bottom"
|
||||
Command="{Binding $parent[ListBox].((vm:VaultViewModel)DataContext).OpenHostPaneCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
ToolTip.Tip="Opens this host's pane beside the grid" />
|
||||
</Panel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -461,8 +506,9 @@
|
||||
</Grid>
|
||||
|
||||
<!--
|
||||
The drawer, and it takes its 304 pixels only while there is something in it — which is what gives the
|
||||
grid the full width for most of the time anybody is looking at it.
|
||||
The drawer, and it takes its 304 pixels only while it has been opened — which is what gives the grid
|
||||
the full width for most of the time anybody is looking at it, now including all the time somebody is
|
||||
choosing between forty machines.
|
||||
|
||||
Wrapped so that its own IsVisible is the shell's binding and its data context is the vault; see
|
||||
MainWindow.axaml's note on why the two cannot be put on one element.
|
||||
|
||||
Reference in New Issue
Block a user