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:
@@ -625,6 +625,155 @@
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
◆ THE PENCIL ON A HOST CARD, which is the only way the drawer opens from the grid.
|
||||
|
||||
It exists because selecting a host stopped opening it. A card that took 304 pixels off the grid the
|
||||
moment it was touched charged the price of the pane for the act of choosing — most of which is arrowing
|
||||
past machines you did not want — so opening is now a thing you ask for and this is the asking. See
|
||||
VaultViewModel.IsHostPaneOpen.
|
||||
|
||||
Hidden until the pointer is on the card, and shown on the selected one whether or not the pointer is
|
||||
there, because the selected card is the one whose pane you are most likely to want and a control that
|
||||
vanished from under the card you just clicked would have to be hunted for. Both are declared after the
|
||||
base rule: Avalonia has no specificity and settles two matching rules by declaration order, which this
|
||||
file records three times over.
|
||||
|
||||
IsVisible rather than Opacity. A button at zero opacity is still a button — it takes the click, and the
|
||||
card underneath does not — so half the grid would swallow presses aimed at selecting a host. The space
|
||||
is held open by the Panel around it in the item template rather than by this, so the card does not
|
||||
reflow as the pointer crosses it; see HostsScreen.axaml.
|
||||
-->
|
||||
<Style Selector="Button.rowedit">
|
||||
<Setter Property="Width" Value="24" />
|
||||
<Setter Property="Height" Value="24" />
|
||||
<Setter Property="CornerRadius" Value="6" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="Button.rowedit /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="CornerRadius" Value="6" />
|
||||
</Style>
|
||||
<Style Selector="Button.rowedit:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource ChromeHover}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:pointerover Button.rowedit">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:selected Button.rowedit">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
◆ THE DRAWER'S FURNITURE: a card, its heading, and the rows inside it.
|
||||
|
||||
The hosts drawer is a column of grouped fields rather than a run of labelled controls — address here,
|
||||
the general facts here, the connection and its credentials here — which is what the design draws and
|
||||
what a 304-pixel column needs to stay readable: three cards is three things to find, where fourteen
|
||||
stacked controls is fourteen.
|
||||
|
||||
Raised on Sidebar, which is the one pairing that works in this column. Panel and Chrome are the same
|
||||
value as each other and near enough Sidebar to vanish against it, and Field is the well a control sits
|
||||
in — a card drawn in it would read as one enormous input.
|
||||
-->
|
||||
<Style Selector="Border.section">
|
||||
<Setter Property="Background" Value="{StaticResource Raised}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
<Setter Property="Padding" Value="12" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
A card's heading, and deliberately not TextBlock.label. That style is the small tracked-out capitals
|
||||
this design uses for a *section of a screen* — GROUPS, HOSTS, the vault's categories — and three of
|
||||
them inside a 304-pixel column would claim the same weight as the screen headings beside it. These are
|
||||
captions on a card, so they are sentence case at the body size.
|
||||
-->
|
||||
<Style Selector="TextBlock.sectionhead">
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
◆ A ROW IN THE DETAIL PANE, AND IT IS A BUTTON THAT LOOKS LIKE A FIELD.
|
||||
|
||||
The design draws every fact about a host as a filled box, which is the shape of an input — so a reader
|
||||
arrives expecting to type in one. They cannot: saving here is a whole-host operation that validates the
|
||||
key-or-credential exclusion and writes one encrypted payload, so a box that committed on every
|
||||
keystroke would be a save per character and a half-typed hostname on the wire.
|
||||
|
||||
Rather than draw an input that refuses the pointer, each row *is* the door to the editor. Pressing any
|
||||
of them opens the host editor, which is the same three cards with real boxes in them — so the thing
|
||||
that looked editable turns out to be editable, one step further along, and nothing on screen is a
|
||||
control that does nothing. The alternative was a flat read-only pane with one EDIT button, which is
|
||||
what this replaced.
|
||||
|
||||
It borrows the TextBox's own geometry rather than sharing it, for the reason Button.chiptoggle borrows
|
||||
Border.chip's: a Button is not a TextBox and Avalonia's selectors are structural. The numbers are
|
||||
repeated and the two have to move together, which is why they are adjacent in this file.
|
||||
-->
|
||||
<Style Selector="Button.fieldrow">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="MinHeight" Value="32" />
|
||||
<Setter Property="Padding" Value="8,5" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||
</Style>
|
||||
<Style Selector="Button.fieldrow /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource Field}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="4" />
|
||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||
</Style>
|
||||
<Style Selector="Button.fieldrow:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource Hover}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderHover}" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
The glyph at the head of such a row. A fixed width so that four rows in a card line their text up, and
|
||||
the faintest step in the ramp because it is a mark to skip rather than a fact to read: what the row
|
||||
says is to its right.
|
||||
-->
|
||||
<Style Selector="TextBlock.fieldglyph">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="Width" Value="16" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
The two buttons in the drawer's header: the overflow menu and the arrow that puts the drawer away.
|
||||
Chrome rather than controls — they are the frame around whichever panel is showing, which is why they
|
||||
are flat and quiet until the pointer finds them.
|
||||
-->
|
||||
<Style Selector="Button.paneicon">
|
||||
<Setter Property="Width" Value="26" />
|
||||
<Setter Property="Height" Value="26" />
|
||||
<Setter Property="CornerRadius" Value="6" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
<Style Selector="Button.paneicon /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="CornerRadius" Value="6" />
|
||||
</Style>
|
||||
<Style Selector="Button.paneicon:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource ChromeHover}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
The status dot, in one place rather than as a converter in code.
|
||||
|
||||
|
||||
@@ -18,329 +18,592 @@
|
||||
Renamed with the job rather than kept as HostSidebar, unlike NavRail, which kept its name when it only
|
||||
changed width. This one changed what it holds, which side it is on, and whether it is there at all.
|
||||
|
||||
── A HEADER, A BODY THAT SCROLLS, AND A FOOTER. ─────────────────────────────────────────────────────
|
||||
Three rows rather than one column, which is the shape the v4 design draws and the one change here that
|
||||
is structural rather than cosmetic. The header names what the drawer is about and carries the two
|
||||
controls that are true of every panel — the overflow menu and the arrow that puts the drawer away. 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 is never below the fold — which it was,
|
||||
on a host with fifteen tags.
|
||||
|
||||
── EXACTLY ONE OF THE THREE PANELS IS SHOWING ───────────────────────────────────────────────────────
|
||||
Detail, host editor, group editor. They are exclusive by construction rather than by three flags that
|
||||
could disagree: IsShowingHostDetail is defined as "neither editor is open and something is selected",
|
||||
so no two of these can be true at once. The whole control collapses when none of them is — see
|
||||
could disagree: IsShowingHostDetail is defined as "neither editor is open and the pane has been asked
|
||||
for", so no two of these can be true at once. The whole control collapses when none of them is — see
|
||||
IsDrawerOpen, which the hosts screen binds — rather than standing there empty, because an empty
|
||||
300-pixel column beside a grid is 300 pixels the grid could have had.
|
||||
|
||||
◆ AND IT IS ASKED FOR NOW, rather than arriving with a selection. Touching a card used to open this,
|
||||
which charged the width of the pane for the act of choosing; the pencil on a card is the asking. See
|
||||
VaultViewModel.IsHostPaneOpen and the item template in HostsScreen.axaml.
|
||||
|
||||
── THE THREE PANELS ARE THE SAME THREE CARDS ────────────────────────────────────────────────────────
|
||||
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, so a box that looks editable turns out to be,
|
||||
one step along; see Button.fieldrow in App.axaml for why they are not inputs that save as you type.
|
||||
|
||||
Its data context is the VaultViewModel, so every binding here is a property of the vault. The hosts
|
||||
screen hands it over.
|
||||
|
||||
Nothing in here may be laid over the terminal's rectangle: it is a column of the hosts screen, and the
|
||||
hosts screen is a sibling of the WebView. See MainWindow.axaml's occlusion rule.
|
||||
|
||||
── WHAT THE DESIGN DRAWS HERE AND THIS PANE HAS NOT GOT ─────────────────────────────────────────────
|
||||
Share this host, Add Telnet, "SSH ID, Certificate, FIDO2", the backspace-key mapping row and the vault
|
||||
picker's chevron. Five controls with nothing behind them: 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. They are listed in
|
||||
docs/design-import-gaps.md with what ships instead, and none of them is drawn disabled.
|
||||
-->
|
||||
|
||||
<Border Width="304" Background="{StaticResource Sidebar}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="1,0,0,0">
|
||||
|
||||
<!--
|
||||
◆ IT SCROLLS AS A WHOLE, and the host editor no longer carries a MaxHeight of its own.
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
|
||||
The old column gave the editor 300 pixels and let the list above have the rest, so the editor had to
|
||||
be bounded separately. Here the drawer is the only thing in its column, so one ScrollViewer over all
|
||||
three panels is both simpler and more honest: whichever panel is up gets the whole height, and the
|
||||
one that overflows is the one that scrolls.
|
||||
<!-- ============ THE HEADER ============ -->
|
||||
<!--
|
||||
One row for all three panels, which is why what it says is on the view model rather than repeated
|
||||
three times here. See VaultViewModel.DrawerTitle.
|
||||
|
||||
The cost is the one the old note recorded and it has not changed: the layout harness skips anything
|
||||
with a ScrollViewer in its ancestry — see LayoutHarness.IsScrollable — so from here on it certifies
|
||||
that this pane fits the column rather than that every field inside it does. That is the true claim
|
||||
about a pane that scrolls, and the tag picker is why it has to scroll: its height is a chip per tag
|
||||
in the keychain, wrapped, so no fixed height holds it for somebody with fifteen.
|
||||
-->
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled">
|
||||
<Panel>
|
||||
The subtitle is the keychain this host is filed in, and the design's chevron beside it is not drawn:
|
||||
an item cannot be moved between vaults — the two are encrypted under different keys, so moving one
|
||||
is a delete and a retype — and a picker offering the move would be offering something no layer below
|
||||
this can do.
|
||||
-->
|
||||
<Border Grid.Row="0" Padding="14,10" Background="{StaticResource Panel}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
||||
<Grid ColumnDefinitions="*,Auto,Auto">
|
||||
|
||||
<!-- ============ WHAT THIS HOST IS ============ -->
|
||||
<StackPanel Margin="16" Spacing="12" IsVisible="{Binding IsShowingHostDetail}">
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Ellipse Classes="dot" Classes.live="{Binding SelectedHost.IsConnected}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Text="{Binding SelectedHost.Label}" FontSize="16" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource Text}" VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<StackPanel Grid.Column="0" VerticalAlignment="Center" Spacing="1">
|
||||
<TextBlock Text="{Binding DrawerTitle}" FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Text="{Binding DrawerSubtitle}" FontSize="11"
|
||||
Foreground="{StaticResource TextFaint}" TextTrimming="CharacterEllipsis"
|
||||
IsVisible="{Binding DrawerSubtitle,
|
||||
Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
</StackPanel>
|
||||
|
||||
<SelectableTextBlock Classes="mono" Text="{Binding SelectedHost.Address}" FontSize="12"
|
||||
<!--
|
||||
The host's own two actions, behind a menu rather than as a row of buttons under the pane. They
|
||||
are what EDIT and DELETE were; a pane whose footer is CONNECT has one action worth a button, and
|
||||
the other two are things you go looking for. It hides with the deletion question for the reason
|
||||
the buttons did — see VaultViewModel.ShowsHostPaneActions.
|
||||
-->
|
||||
<Button Grid.Column="1" Classes="flat paneicon" Content="⋯"
|
||||
IsVisible="{Binding ShowsHostPaneActions}"
|
||||
ToolTip.Tip="More things to do with this host">
|
||||
<Button.Flyout>
|
||||
<MenuFlyout>
|
||||
<MenuItem Header="Edit…" Command="{Binding EditSelectedHostCommand}" />
|
||||
<Separator />
|
||||
<MenuItem Header="Delete…" Command="{Binding DeleteHostCommand}" />
|
||||
</MenuFlyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
|
||||
<Button Grid.Column="2" Classes="flat paneicon" Content="⇥"
|
||||
Command="{Binding CloseDrawerCommand}"
|
||||
ToolTip.Tip="Closes this pane and gives the grid the width back. The host stays selected." />
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
◆ THE BODY SCROLLS AND THE ROWS ABOVE AND BELOW IT DO NOT.
|
||||
|
||||
The layout harness skips anything with a ScrollViewer in its ancestry — see
|
||||
LayoutHarness.IsScrollable — so from here on it certifies that the header, the footer and the
|
||||
column itself fit, rather than that every field inside the cards does. That is the true claim about
|
||||
a pane that scrolls, and it is a better one than before: what the harness now measures includes the
|
||||
button each panel exists to offer, which is exactly the control that used to be able to fall off the
|
||||
bottom. The tag picker is why the middle has to scroll at all — its height is a chip per tag in the
|
||||
keychain, wrapped, so no fixed height holds it for somebody with fifteen.
|
||||
-->
|
||||
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Disabled">
|
||||
<Panel>
|
||||
|
||||
<!-- ============ WHAT THIS HOST IS ============ -->
|
||||
<StackPanel Margin="12" Spacing="10" IsVisible="{Binding IsShowingHostDetail}">
|
||||
|
||||
<!--
|
||||
============ ADDRESS ============
|
||||
The hostname alone, beside the same mark the host's card in the grid carries. The account and
|
||||
the port are in the connection card below rather than crammed into one user@host:port string:
|
||||
this is the pane with room to separate them, and the two are edited in different boxes.
|
||||
-->
|
||||
<Border Classes="section">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Classes="sectionhead" Text="Address" />
|
||||
<Grid ColumnDefinitions="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>
|
||||
<Button Grid.Column="1" Classes="fieldrow" Margin="10,0,0,0"
|
||||
Command="{Binding EditSelectedHostCommand}"
|
||||
ToolTip.Tip="Opens this host's editor.">
|
||||
<TextBlock Classes="mono" Text="{Binding SelectedHost.Host.Hostname}" FontSize="12"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
============ GENERAL ============
|
||||
The name, where it is filed, what it wears, and whatever was written about it. Notes are the
|
||||
one row that is not always there: an empty box labelled nothing is a row that says a host has
|
||||
no notes, which is not a fact anybody came here for.
|
||||
-->
|
||||
<Border Classes="section">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Classes="sectionhead" Text="General" Margin="0,0,0,2" />
|
||||
|
||||
<Button Classes="fieldrow" Command="{Binding EditSelectedHostCommand}">
|
||||
<TextBlock Text="{Binding SelectedHost.Label}" TextTrimming="CharacterEllipsis" />
|
||||
</Button>
|
||||
|
||||
<!--
|
||||
The group, with the same mark its card carries in the grid. A host in none says so rather
|
||||
than showing an empty box, for the reason the notes row is absent: blank and "none" look
|
||||
identical and only one of them is an answer.
|
||||
-->
|
||||
<Button Classes="fieldrow" Command="{Binding EditSelectedHostCommand}">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<TextBlock Grid.Column="0" Classes="fieldglyph" Text="▤" />
|
||||
<TextBlock Grid.Column="1" Text="{Binding SelectedHost.GroupLabel}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
IsVisible="{Binding SelectedHost.HasGroup}" />
|
||||
<TextBlock Grid.Column="1" Text="No group"
|
||||
Foreground="{StaticResource TextFaint}"
|
||||
IsVisible="{Binding !SelectedHost.HasGroup}" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<!--
|
||||
The tags it wears, as the same chips the card draws — repeated rather than shared with the
|
||||
card's template because the two are different shapes and a shared template would have to
|
||||
be told which.
|
||||
-->
|
||||
<Button Classes="fieldrow" Command="{Binding EditSelectedHostCommand}">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<TextBlock Grid.Column="0" Classes="fieldglyph" Text="#" VerticalAlignment="Top"
|
||||
Margin="0,3,0,0" />
|
||||
<ItemsControl Grid.Column="1" ItemsSource="{Binding SelectedHost.TagLabels}"
|
||||
IsVisible="{Binding SelectedHost.HasTags}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><WrapPanel /></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<Border Classes="chip" Padding="6,1" Margin="0,0,4,2">
|
||||
<TextBlock Text="{Binding}" FontSize="9.5" />
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBlock Grid.Column="1" Text="No tags" Foreground="{StaticResource TextFaint}"
|
||||
IsVisible="{Binding !SelectedHost.HasTags}" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Button Classes="fieldrow" Command="{Binding EditSelectedHostCommand}"
|
||||
IsVisible="{Binding SelectedHost.Host.Notes,
|
||||
Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<TextBlock Grid.Column="0" Classes="fieldglyph" Text="✎" VerticalAlignment="Top"
|
||||
Margin="0,2,0,0" />
|
||||
<TextBlock Grid.Column="1" Text="{Binding SelectedHost.Host.Notes}" FontSize="12"
|
||||
Foreground="{StaticResource TextDim}" TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<!--
|
||||
Which of the three ways this host authenticates, and where it came from. The note rather than
|
||||
the one-word Authentication the card shows: a host that inherits its group's key is the case
|
||||
where the word alone is misleading, and there is room for the sentence here.
|
||||
-->
|
||||
<TextBlock Classes="hint" FontSize="12" Text="{Binding SelectedHostAuthenticationNote}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
The tags it wears, as the same chips the card draws. Repeated rather than shared with the card's
|
||||
template because the two are different shapes — the card wraps them under a two-line summary and
|
||||
this is a column 304 wide — and a shared template would have to be told which.
|
||||
-->
|
||||
<ItemsControl ItemsSource="{Binding SelectedHost.TagLabels}"
|
||||
IsVisible="{Binding SelectedHost.HasTags}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><WrapPanel /></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<Border Classes="chip" Padding="6,1" Margin="0,0,4,4">
|
||||
<TextBlock Text="{Binding}" FontSize="9.5" />
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<!--
|
||||
============ CONNECTION ============
|
||||
What this host dials and what it authenticates with — the two things CONNECT in the footer is
|
||||
about, which is why they share a card with the box that some hosts need filled in.
|
||||
|
||||
<SelectableTextBlock Text="{Binding SelectedHost.Host.Notes}" FontSize="12"
|
||||
Foreground="{StaticResource TextDim}" TextWrapping="Wrap"
|
||||
IsVisible="{Binding SelectedHost.Host.Notes,
|
||||
Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
Every value here is the *resolved* one, so a host that states nothing of its own shows what it
|
||||
would actually use rather than a blank. Where that came from a group the row says "inherited"
|
||||
beside it: the number is the same either way and the edit is not, because clearing a group's
|
||||
default moves every host that never overrode it.
|
||||
-->
|
||||
<Border Classes="section">
|
||||
<StackPanel Spacing="6">
|
||||
|
||||
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="0,2" />
|
||||
<Grid ColumnDefinitions="Auto,72,*">
|
||||
<TextBlock Grid.Column="0" Classes="sectionhead" Text="SSH on"
|
||||
VerticalAlignment="Center" />
|
||||
<Button Grid.Column="1" Classes="fieldrow" Margin="8,0"
|
||||
Command="{Binding EditSelectedHostCommand}">
|
||||
<TextBlock Classes="mono" Text="{Binding SelectedHostPortLabel}" FontSize="12" />
|
||||
</Button>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="port" Foreground="{StaticResource TextDim}" />
|
||||
<TextBlock Text="inherited" FontSize="11" Foreground="{StaticResource TextFaint}"
|
||||
IsVisible="{Binding SelectedHostPortIsInherited}"
|
||||
ToolTip.Tip="This host states no port of its own and takes its group's." />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!--
|
||||
Connecting, and the box a host that wants a typed password needs. A sentence in the box's place
|
||||
when it does not, because "nothing needs typing" and "something needs typing and the box has not
|
||||
appeared" look identical and only one of them is fine.
|
||||
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="0,4" />
|
||||
|
||||
It is here rather than in a bar across the top of the screen, which is where it used to be: the
|
||||
password belongs to the host, and a box at the top of a grid of forty machines is one whose
|
||||
subject you have to work out. That move is also what this arrangement is for — the bar had one
|
||||
row and had to fit the box, the tick, the note and CONNECT along it; a column has room to put
|
||||
the tick under the box it qualifies, which is where it reads as a property of the password
|
||||
rather than as a fourth control in a row.
|
||||
<TextBlock Classes="sectionhead" Text="Credentials" Margin="0,0,0,2" />
|
||||
|
||||
REMEMBER travels with the box and hides with it. It is the two-step chore the box's tooltip used
|
||||
to describe — add a password under Keychain, then bind the host to it — done from the one place
|
||||
that already has the password, and it takes effect only once the remote has accepted it.
|
||||
-->
|
||||
<TextBox Text="{Binding ConnectPassword}" PlaceholderText="password"
|
||||
PasswordChar="•" HorizontalAlignment="Stretch"
|
||||
IsVisible="{Binding SelectedHostAsksForAPassword}"
|
||||
ToolTip.Tip="Typed each time unless REMEMBER is ticked, in which case it is saved to your keychain and bound to this host once the connection succeeds." />
|
||||
<CheckBox IsChecked="{Binding RemembersConnectPassword}"
|
||||
IsVisible="{Binding SelectedHostAsksForAPassword}"
|
||||
ToolTip.Tip="Saves this password to your keychain, bound to this host, so it is not asked for again. It syncs to your other machines, and only happens if the connection works.">
|
||||
<TextBlock Text="Remember this password" Classes="hint" FontSize="12" />
|
||||
</CheckBox>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<Button Grid.Column="0" Classes="fieldrow" Command="{Binding EditSelectedHostCommand}">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<TextBlock Grid.Column="0" Classes="fieldglyph" Text="@" />
|
||||
<TextBlock Grid.Column="1" Text="{Binding SelectedHostUsernameLabel}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</Grid>
|
||||
</Button>
|
||||
<TextBlock Grid.Column="1" Text="inherited" FontSize="11" Margin="6,0,0,0"
|
||||
VerticalAlignment="Center" Foreground="{StaticResource TextFaint}"
|
||||
IsVisible="{Binding SelectedHostUsernameIsInherited}"
|
||||
ToolTip.Tip="This host states no account of its own and takes its group's." />
|
||||
</Grid>
|
||||
|
||||
<!--
|
||||
The box a host that wants a typed password needs, and it is a real one — the only input in
|
||||
this pane, because what it holds is not part of the host and is never saved with it.
|
||||
|
||||
It is here rather than in a bar across the top of the screen, which is where it used to be:
|
||||
the password belongs to the host, and a box at the top of a grid of forty machines is one
|
||||
whose subject you have to work out. A column also has room to put REMEMBER under the box it
|
||||
qualifies, which is where it reads as a property of the password rather than as a fourth
|
||||
control in a row.
|
||||
|
||||
REMEMBER travels with the box and hides with it. It is the two-step chore the box's tooltip
|
||||
used to describe — add a password under Keychain, then bind the host to it — done from the
|
||||
one place that already has the password, and it takes effect only once the remote has
|
||||
accepted it.
|
||||
-->
|
||||
<TextBox Text="{Binding ConnectPassword}" PlaceholderText="password"
|
||||
PasswordChar="•" HorizontalAlignment="Stretch"
|
||||
IsVisible="{Binding SelectedHostAsksForAPassword}"
|
||||
ToolTip.Tip="Typed each time unless REMEMBER is ticked, in which case it is saved to your keychain and bound to this host once the connection succeeds." />
|
||||
<CheckBox IsChecked="{Binding RemembersConnectPassword}"
|
||||
IsVisible="{Binding SelectedHostAsksForAPassword}"
|
||||
ToolTip.Tip="Saves this password to your keychain, bound to this host, so it is not asked for again. It syncs to your other machines, and only happens if the connection works.">
|
||||
<TextBlock Text="Remember this password" Classes="hint" FontSize="12" />
|
||||
</CheckBox>
|
||||
|
||||
<!--
|
||||
What it authenticates with instead, named. The row is the item's own label and the sentence
|
||||
under it is which of the three ways this is and where the binding came from — the note
|
||||
rather than the one word the card shows, because a host that inherits its group's key is
|
||||
the case where the word alone is misleading and there is room for the sentence here.
|
||||
-->
|
||||
<Button Classes="fieldrow" Command="{Binding EditSelectedHostCommand}"
|
||||
IsVisible="{Binding !SelectedHostAsksForAPassword}">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<!--
|
||||
◆ rather than the ⚿ the nav rail marks the keychain with, and that is a rendering
|
||||
decision rather than a semantic one: this application substitutes the design's fonts
|
||||
for Inter and the system monospace stack — see MonoFont in Palette.axaml — and U+26BF
|
||||
is outside what either supplies, so it lands on whatever the platform's fallback has.
|
||||
Every other glyph in this pane is from Geometric Shapes, which both faces carry.
|
||||
-->
|
||||
<TextBlock Grid.Column="0" Classes="fieldglyph" Text="◆" />
|
||||
<TextBlock Grid.Column="1" Text="{Binding SelectedHostBindingLabel}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<TextBlock Classes="hint" FontSize="11" Text="{Binding SelectedHostAuthenticationNote}"
|
||||
IsVisible="{Binding SelectedHostAuthenticationNote,
|
||||
Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||
Text="Double-clicking the card does the same as CONNECT. The terminal opens as a tab in the strip above and stays there while you look at anything else." />
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" IsVisible="{Binding ShowsHostActions}">
|
||||
<Button Classes="accent" Content="CONNECT" Command="{Binding ConnectCommand}"
|
||||
IsEnabled="{Binding !IsBusy}" />
|
||||
<Button Classes="ghost" Content="EDIT" Command="{Binding EditSelectedHostCommand}" />
|
||||
<Button Classes="ghost" Content="DELETE" Command="{Binding DeleteHostCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- ============ THE HOST EDITOR ============ -->
|
||||
<!--
|
||||
Swapped for the buttons rather than stacked under them, as it always was, so DELETE cannot be
|
||||
pressed again while its own question is on screen. See VaultViewModel.ShowsHostActions.
|
||||
The editor doubles as the "add" form; there is no separate dialog. Same three cards as the pane
|
||||
above, with the boxes real — which is what makes pressing a row up there lead somewhere that
|
||||
looks like where it was pressed.
|
||||
-->
|
||||
<StackPanel Margin="12" Spacing="10" IsVisible="{Binding IsEditing}">
|
||||
|
||||
<Border Classes="section">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Classes="sectionhead" Text="Address" />
|
||||
<Grid ColumnDefinitions="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>
|
||||
<TextBox Grid.Column="1" Margin="10,0,0,0" Text="{Binding EditorHostname}"
|
||||
PlaceholderText="hostname or address" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="section">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Classes="sectionhead" Text="General" Margin="0,0,0,2" />
|
||||
|
||||
<TextBox Text="{Binding EditorLabel}" PlaceholderText="name" />
|
||||
|
||||
<!--
|
||||
Which group this host is filed under. Inside the encrypted payload like everything else
|
||||
here, so the server learns nothing about how the estate is organised — and a group the
|
||||
vault no longer has keeps a placeholder entry, so that editing the port cannot quietly
|
||||
unfile the host.
|
||||
-->
|
||||
<ComboBox ItemsSource="{Binding EditorGroupChoices}"
|
||||
SelectedItem="{Binding EditorSelectedGroup}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:GroupChoice">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<!--
|
||||
The tags this host wears. Chips that toggle rather than a multi-select list, because a chip
|
||||
is what a tag looks like on the card in the grid — a list of names to tick would make the
|
||||
user match an entry to a chip they can already see.
|
||||
|
||||
The box under them creates one and puts it on straight away. That is where a tag is usually
|
||||
wanted: while tagging a host and finding it does not exist yet. Unlike every other field
|
||||
here it writes to the keychain immediately, because a host can only name a tag that has an
|
||||
id — so cancelling this editor leaves the tag behind, which is honest rather than hidden.
|
||||
Renaming and deleting are on the keychain screen, where every other item kind is managed.
|
||||
-->
|
||||
<ItemsControl ItemsSource="{Binding EditorTagChoices}" IsVisible="{Binding HasTagChoices}"
|
||||
Margin="0,2,0,0">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><WrapPanel /></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TagChoice">
|
||||
<!--
|
||||
Worn is filled, unworn is outlined. One control per tag with two states rather than a
|
||||
checkbox beside a label: the state and the name occupy the same object, so a row of
|
||||
them reads as the host's tags rather than as a form about them.
|
||||
-->
|
||||
<Button Classes="chiptoggle" Classes.worn="{Binding IsWorn}" Margin="0,0,4,4"
|
||||
Command="{Binding $parent[ItemsControl].((vm:VaultViewModel)DataContext).ToggleEditorTagCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<TextBlock Text="{Binding Label}" FontSize="10.5" />
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Grid ColumnDefinitions="*,6,Auto">
|
||||
<TextBox Grid.Column="0" Text="{Binding EditorNewTag}" PlaceholderText="new tag">
|
||||
<TextBox.KeyBindings>
|
||||
<KeyBinding Gesture="Enter" Command="{Binding AddEditorTagCommand}" />
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
<Button Grid.Column="2" Classes="ghost" Content="ADD"
|
||||
Command="{Binding AddEditorTagCommand}" />
|
||||
</Grid>
|
||||
|
||||
<TextBox Text="{Binding EditorNotes}" PlaceholderText="notes" AcceptsReturn="True"
|
||||
Height="56" TextWrapping="Wrap" />
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="section">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Classes="sectionhead" Text="Connection" Margin="0,0,0,2" />
|
||||
|
||||
<!--
|
||||
Both boxes are allowed to be empty, and empty means "take the group's" rather than "unset".
|
||||
The watermark is what the host will actually use if it is left that way, which is why it is
|
||||
bound rather than literal: it changes when the group picker above moves.
|
||||
-->
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<NumericUpDown Grid.Column="0" Value="{Binding EditorPort}" Minimum="1" Maximum="65535"
|
||||
FormatString="0" ShowButtonSpinner="False"
|
||||
PlaceholderText="{Binding EditorPortPlaceholder}" />
|
||||
<TextBox Grid.Column="2" Text="{Binding EditorUsername}"
|
||||
PlaceholderText="{Binding EditorUsernamePlaceholder}" />
|
||||
</Grid>
|
||||
|
||||
<!--
|
||||
How this host authenticates: a typed password, one of the vault's keys, or one of its
|
||||
credentials. Part of the host rather than of the connection, so it follows the host to
|
||||
every machine; a host bound to something since deleted keeps a placeholder entry here, so
|
||||
that editing the port cannot quietly turn it back into a typed-password host.
|
||||
|
||||
One control for all three, which is what makes "a key or a credential, never both"
|
||||
impossible to express rather than merely invalid. The qualifier beside each label is not
|
||||
decoration: a key called "deploy" and the deploy account's password are the ordinary case,
|
||||
and bare labels would offer two identical-looking entries that authenticate completely
|
||||
differently.
|
||||
-->
|
||||
<ComboBox ItemsSource="{Binding EditorAuthenticationChoices}"
|
||||
SelectedItem="{Binding EditorSelectedAuthentication}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:AuthenticationChoice">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
<TextBlock Text="{Binding Qualifier}" Classes="hint" FontSize="11"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding Qualifier, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<CheckBox IsChecked="{Binding EditorRelayEnabled}"
|
||||
Content="Connect through the server relay" />
|
||||
<!--
|
||||
Stated at the moment the decision is made, which is the only place it means anything. With
|
||||
relay off the server stores no address at all; with it on the server must be able to
|
||||
resolve the target, or it becomes an authenticated open proxy into the operator's network.
|
||||
-->
|
||||
<TextBlock Classes="hint" FontSize="11"
|
||||
Text="The relay stores this host's address on the server in plain text. Everything else stays encrypted." />
|
||||
|
||||
<!--
|
||||
Withdrawing host key trust lives here, in the host's own settings, because a changed host
|
||||
key is refused outright with no way to continue past it — so a legitimately rebuilt server
|
||||
needs somewhere deliberate to be re-approved from, and that somewhere must not be the
|
||||
warning itself. It takes effect when clicked rather than on Save, and the status line says
|
||||
so; it is not a field of the host.
|
||||
-->
|
||||
<Button Classes="danger" Content="FORGET HOST KEY" HorizontalAlignment="Left"
|
||||
Margin="0,4,0,0"
|
||||
Command="{Binding ForgetHostKeyCommand}"
|
||||
IsVisible="{Binding CanForgetHostKey}"
|
||||
ToolTip.Tip="Removes the pinned key for this host's address, so the next connection asks you to check its fingerprint again." />
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!-- ============ THE GROUP EDITOR ============ -->
|
||||
<!--
|
||||
Here rather than on the Keychain screen, because a group is not a secret — it is how this
|
||||
screen's grid is arranged, and the arranging belongs beside the thing arranged. Filing a host
|
||||
into one is done in the host's own editor above, for the same reason its key and its password
|
||||
are.
|
||||
|
||||
One form for both adding and renaming; the header says which of the two is about to happen, and
|
||||
so does the footer's button. The four fields under the name are what the hosts inside inherit
|
||||
when they say nothing themselves; every one of them may be left empty, and empty means "lend
|
||||
nothing" rather than "unset". The parent picker leaves out this group and everything beneath it,
|
||||
so a cycle cannot be made here — which is a courtesy rather than the guarantee, because one
|
||||
assembled offline on two machines was never offered this list. See HostInheritance.
|
||||
-->
|
||||
<StackPanel Margin="12" Spacing="10" IsVisible="{Binding IsEditingGroup}">
|
||||
|
||||
<Border Classes="section">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Classes="sectionhead" Text="Group" Margin="0,0,0,2" />
|
||||
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||
Text="A heading for the grid, and the defaults every host under it inherits. Which group a host is in is part of the host, and stays encrypted." />
|
||||
|
||||
<TextBox Text="{Binding GroupEditorLabel}" PlaceholderText="group name" />
|
||||
|
||||
<ComboBox ItemsSource="{Binding GroupEditorParentChoices}"
|
||||
SelectedItem="{Binding GroupEditorSelectedParent}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:GroupChoice">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="section">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Classes="sectionhead" Text="What its hosts inherit" Margin="0,0,0,2" />
|
||||
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<NumericUpDown Grid.Column="0" Value="{Binding GroupEditorDefaultPort}" Minimum="1"
|
||||
Maximum="65535" FormatString="0" ShowButtonSpinner="False"
|
||||
PlaceholderText="default port" />
|
||||
<TextBox Grid.Column="2" Text="{Binding GroupEditorDefaultUsername}"
|
||||
PlaceholderText="default username" />
|
||||
</Grid>
|
||||
|
||||
<ComboBox ItemsSource="{Binding GroupEditorAuthenticationChoices}"
|
||||
SelectedItem="{Binding GroupEditorSelectedAuthentication}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:AuthenticationChoice">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
<TextBlock Text="{Binding Qualifier}" Classes="hint" FontSize="11"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding Qualifier, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</Panel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ============ THE FOOTER ============ -->
|
||||
<!--
|
||||
One row, and exactly one of its four contents is showing — the same by-construction exclusivity the
|
||||
panels above have, from the same flags. It is what each panel is for: connecting, saving a host,
|
||||
saving a group, or answering the question about deleting one.
|
||||
|
||||
◆ THE QUESTION TAKES CONNECT'S PLACE rather than stacking under it, as it always did with the row of
|
||||
buttons this footer replaced, so that DELETE cannot be pressed again while its own question is on
|
||||
screen. See VaultViewModel.ShowsHostPaneActions.
|
||||
-->
|
||||
<Border Grid.Row="2" Padding="12" Background="{StaticResource Panel}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0">
|
||||
<Panel>
|
||||
|
||||
<Button Classes="accent" Content="Connect" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center" Padding="10,7" FontSize="12.5"
|
||||
Command="{Binding ConnectCommand}" IsEnabled="{Binding !IsBusy}"
|
||||
IsVisible="{Binding ShowsHostPaneActions}" />
|
||||
|
||||
<Border Padding="10" Background="{StaticResource DangerWash}" CornerRadius="6"
|
||||
IsVisible="{Binding IsConfirmingHostDeletion}">
|
||||
<views:ConfirmDeleteCard />
|
||||
</Border>
|
||||
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||
Text="Double-clicking the card does the same as CONNECT. The terminal opens as a tab in the strip above and stays there while you look at anything else." />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!-- ============ THE HOST EDITOR ============ -->
|
||||
<!--
|
||||
The editor doubles as the "add" form; there is no separate dialog.
|
||||
-->
|
||||
<StackPanel Margin="16" Spacing="6" IsVisible="{Binding IsEditing}">
|
||||
|
||||
<TextBlock Classes="label" Text="HOST" Foreground="{StaticResource TextDim}" Margin="0,0,0,4" />
|
||||
|
||||
<TextBox Text="{Binding EditorLabel}" PlaceholderText="name" />
|
||||
<TextBox Text="{Binding EditorHostname}" PlaceholderText="hostname or address" />
|
||||
<!--
|
||||
Both boxes are allowed to be empty, and empty means "take the group's" rather than "unset". The
|
||||
watermark is what the host will actually use if it is left that way, which is why it is bound
|
||||
rather than literal: it changes when the group picker below moves.
|
||||
-->
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<NumericUpDown Grid.Column="0" Value="{Binding EditorPort}" Minimum="1" Maximum="65535"
|
||||
FormatString="0" ShowButtonSpinner="False"
|
||||
PlaceholderText="{Binding EditorPortPlaceholder}" />
|
||||
<TextBox Grid.Column="2" Text="{Binding EditorUsername}"
|
||||
PlaceholderText="{Binding EditorUsernamePlaceholder}" />
|
||||
</Grid>
|
||||
<TextBox Text="{Binding EditorNotes}" PlaceholderText="notes" AcceptsReturn="True"
|
||||
Height="56" TextWrapping="Wrap" />
|
||||
<!--
|
||||
How this host authenticates: a typed password, one of the vault's keys, or one of its credentials.
|
||||
Part of the host rather than of the connection, so it follows the host to every machine; a host
|
||||
bound to something since deleted keeps a placeholder entry here, so that editing the port cannot
|
||||
quietly turn it back into a typed-password host.
|
||||
|
||||
One control for all three, which is what makes "a key or a credential, never both" impossible to
|
||||
express rather than merely invalid. The qualifier beside each label is not decoration: a key called
|
||||
"deploy" and the deploy account's password are the ordinary case, and bare labels would offer two
|
||||
identical-looking entries that authenticate completely differently.
|
||||
-->
|
||||
<ComboBox ItemsSource="{Binding EditorAuthenticationChoices}"
|
||||
SelectedItem="{Binding EditorSelectedAuthentication}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:AuthenticationChoice">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
<TextBlock Text="{Binding Qualifier}" Classes="hint" FontSize="11"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding Qualifier, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<!--
|
||||
Which group this host is filed under. Inside the encrypted payload like everything else here, so
|
||||
the server learns nothing about how the estate is organised — and a group the vault no longer has
|
||||
keeps a placeholder entry, so that editing the port cannot quietly unfile the host.
|
||||
-->
|
||||
<ComboBox ItemsSource="{Binding EditorGroupChoices}"
|
||||
SelectedItem="{Binding EditorSelectedGroup}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:GroupChoice">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<!--
|
||||
The tags this host wears. Chips that toggle rather than a multi-select list, because a chip is
|
||||
what a tag looks like on the card in the grid — a list of names to tick would make the user
|
||||
match an entry to a chip they can already see.
|
||||
|
||||
The box under them creates one and puts it on straight away. That is where a tag is usually
|
||||
wanted: while tagging a host and finding it does not exist yet. Unlike every other field here it
|
||||
writes to the keychain immediately, because a host can only name a tag that has an id — so
|
||||
cancelling this editor leaves the tag behind, which is honest rather than hidden. Renaming and
|
||||
deleting are on the keychain screen, where every other item kind is managed.
|
||||
-->
|
||||
<ItemsControl ItemsSource="{Binding EditorTagChoices}" IsVisible="{Binding HasTagChoices}"
|
||||
Margin="0,4,0,0">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><WrapPanel /></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TagChoice">
|
||||
<!--
|
||||
Worn is filled, unworn is outlined. One control per tag with two states rather than a
|
||||
checkbox beside a label: the state and the name occupy the same object, so a row of them
|
||||
reads as the host's tags rather than as a form about them.
|
||||
-->
|
||||
<Button Classes="chiptoggle" Classes.worn="{Binding IsWorn}" Margin="0,0,4,4"
|
||||
Command="{Binding $parent[ItemsControl].((vm:VaultViewModel)DataContext).ToggleEditorTagCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<TextBlock Text="{Binding Label}" FontSize="10.5" />
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Grid ColumnDefinitions="*,6,Auto">
|
||||
<TextBox Grid.Column="0" Text="{Binding EditorNewTag}" PlaceholderText="new tag">
|
||||
<TextBox.KeyBindings>
|
||||
<KeyBinding Gesture="Enter" Command="{Binding AddEditorTagCommand}" />
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
<Button Grid.Column="2" Classes="ghost" Content="ADD" Command="{Binding AddEditorTagCommand}" />
|
||||
</Grid>
|
||||
|
||||
<CheckBox IsChecked="{Binding EditorRelayEnabled}"
|
||||
Content="Connect through the server relay" />
|
||||
<!--
|
||||
Stated at the moment the decision is made, which is the only place it means anything. With
|
||||
relay off the server stores no address at all; with it on the server must be able to resolve
|
||||
the target, or it becomes an authenticated open proxy into the operator's network.
|
||||
-->
|
||||
<TextBlock Classes="hint" FontSize="11"
|
||||
Text="The relay stores this host's address on the server in plain text. Everything else stays encrypted." />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" IsVisible="{Binding IsEditing}">
|
||||
<Button Classes="accent" Content="SAVE" Command="{Binding SaveHostCommand}" />
|
||||
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelEditCommand}" />
|
||||
</StackPanel>
|
||||
<!--
|
||||
Withdrawing host key trust lives here, in the host's own settings, because a changed host key
|
||||
is refused outright with no way to continue past it — so a legitimately rebuilt server needs
|
||||
somewhere deliberate to be re-approved from, and that somewhere must not be the warning
|
||||
itself. It takes effect when clicked rather than on Save, and the status line says so; it is
|
||||
not a field of the host.
|
||||
-->
|
||||
<Button Classes="danger" Content="FORGET HOST KEY" HorizontalAlignment="Left"
|
||||
Command="{Binding ForgetHostKeyCommand}"
|
||||
IsVisible="{Binding CanForgetHostKey}"
|
||||
ToolTip.Tip="Removes the pinned key for this host's address, so the next connection asks you to check its fingerprint again." />
|
||||
</StackPanel>
|
||||
|
||||
<!-- ============ THE GROUP EDITOR ============ -->
|
||||
<!--
|
||||
Here rather than on the Keychain screen, because a group is not a secret — it is how this screen's
|
||||
grid is arranged, and the arranging belongs beside the thing arranged. Filing a host into one is
|
||||
done in the host's own editor above, for the same reason its key and its password are.
|
||||
|
||||
One form for both adding and renaming; GroupSaveLabel is what says which of the two is about to
|
||||
happen. The four fields under the name are what the hosts inside inherit when they say nothing
|
||||
themselves; every one of them may be left empty, and empty means "lend nothing" rather than
|
||||
"unset". The parent picker leaves out this group and everything beneath it, so a cycle cannot be
|
||||
made here — which is a courtesy rather than the guarantee, because one assembled offline on two
|
||||
machines was never offered this list. See HostInheritance.
|
||||
-->
|
||||
<StackPanel Margin="16" Spacing="6" IsVisible="{Binding IsEditingGroup}">
|
||||
|
||||
<TextBlock Classes="label" Text="GROUP" Foreground="{StaticResource TextDim}" Margin="0,0,0,4" />
|
||||
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||
Text="A heading for the grid, and the defaults every host under it inherits. Which group a host is in is part of the host, and stays encrypted." />
|
||||
|
||||
<TextBox Text="{Binding GroupEditorLabel}" PlaceholderText="group name" />
|
||||
|
||||
<ComboBox ItemsSource="{Binding GroupEditorParentChoices}"
|
||||
SelectedItem="{Binding GroupEditorSelectedParent}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:GroupChoice">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<NumericUpDown Grid.Column="0" Value="{Binding GroupEditorDefaultPort}" Minimum="1"
|
||||
Maximum="65535" FormatString="0" ShowButtonSpinner="False"
|
||||
PlaceholderText="default port" />
|
||||
<TextBox Grid.Column="2" Text="{Binding GroupEditorDefaultUsername}"
|
||||
PlaceholderText="default username" />
|
||||
</Grid>
|
||||
|
||||
<ComboBox ItemsSource="{Binding GroupEditorAuthenticationChoices}"
|
||||
SelectedItem="{Binding GroupEditorSelectedAuthentication}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:AuthenticationChoice">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
<TextBlock Text="{Binding Qualifier}" Classes="hint" FontSize="11"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding Qualifier, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" IsVisible="{Binding IsEditingGroup}">
|
||||
<Button Classes="accent" Content="{Binding GroupSaveLabel}"
|
||||
Command="{Binding SaveGroupCommand}" />
|
||||
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelGroupEditCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</Border>
|
||||
|
||||
</Panel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
</UserControl>
|
||||
|
||||
@@ -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