Public Access
Move SFTP and S3 into the tab strip, and the host list into a card grid
Four asks in one pass over the desktop head, and two of them are furniture moving rather than anything new. THE STRIP IS THE WINDOW'S NOW, not the terminal's. Vaults, SFTP and S3 sit at its head and cannot be closed, and open terminals follow them. SFTP and S3 left the nav rail to get there, which is the one semantic change: they are the two destinations you stay in while something runs, and a rail entry is for somewhere you go and come back from. So the rail belongs to the Vaults tab and collapses with it, which also means SFTP, S3 and a terminal each get the full 1016 rather than the 826 a page gets. The tab is expressed as "a page, and not one of the two the strip took" rather than as a fourth ShellSurface. Both are still ShellScreen members and have to be — that is what they are on the phone, where they are two rows in a hub — so a surface each would have been a second way to say a thing Screen already says. IsTransfersShowing and IsBucketsShowing light the other two tabs unchanged. What is new is one field: the page Vaults comes back to, because it is the one tab with sub-navigation and therefore the one that can return to the wrong place. That is not the hidden field ShellSurface argues against — that one would be a second copy of "which page"; this is a tab remembering its own. THE HOSTS SCREEN IS A GRID, and the 268-pixel sidebar is gone. That column was choosing among forty machines and editing one of them at two-thirds width, and it was narrow so the editor beneath it could be a column at all. Cards took the first job at full width; a 304-pixel drawer took the second and collapses when nothing is selected. Pressing a group card narrows the grid; SHOW ALL is the way back. The group editor moved into the drawer as well, which finally makes IsEditingGroup mean the same thing on both heads — it was the phone's alone, because the desktop's editor was a bar that was always on screen. AreHostsExpanded and ToggleHosts went with the control that used them. They folded the whole list away under one heading, an affordance that existed because the column was narrow. Folding a single group is a different thing and is still here. THE TYPE SCALE IS A POINT LARGER and the text ramp is white. The base size was never stated anywhere — a bare TextBlock took TextElement's default of 12 — so raising the scale meant naming it, on Window and on UserControl. The second selector is not redundancy: the layout harness hosts a UserControl in a window it builds itself, and without it the suite would measure every screen a point smaller than it ships, silently. A selector on TextBlock would have been the obvious way and is wrong, because a style setter beats an inherited value and would collapse every deliberate step back to one number. #E3E7F4 is a blue-tinted white on blue-black surfaces, which costs contrast twice — once for being darker than white and once for sharing a hue with what it is drawn on. Pure white is 18.3:1 against the canvas where that was 15.5:1. Every step below moved with the top, so the intervals the design chose are kept and TextDim clears 9:1 against 6.4:1. The palette is shared, so the phone has both changes too. TWO DEFECTS THE HARNESS STRUCTURALLY CANNOT SEE, found by rendering the screen rather than by measuring it, and both now covered. The tile was 232 and was first written as 248, from arithmetic that left out the scrolling stack's own margins. Every layout test passed — the harness asks whether a control is inside the window, never how many fit on a line — so the grid quietly became one column wide at exactly the minimum this application guarantees, which is the shape cards exist to avoid. TheHostsGridKeepsTwoColumnsAtTheMinimumWithTheDrawerOpen counts columns instead, and fails at 248. And a card's text ran past its own border, because a horizontal StackPanel measures children with infinite width: a TextBlock inside one never learns it is short of room, so TextTrimming never fires. Both card rows are grids with a star column that gives way and an Auto column that does not — a hostname with its tail cut is still the machine you were looking for, where a badge or the word naming an auth method is not. The keychain header changed shape for the same class of reason. It was Auto,Auto,*,Auto with the buttons last, so the slack column was the only thing absorbing a change of width and five buttons fell off the right edge the moment the type grew. That is how GENERATE lost the word KEY once already. The summary sits in the star column and trims now, so the buttons always get their width. HostSidebarTests became HostGridTests and moved to the grid with the gestures it drives. docs/design-import-gaps.md gains a v3 section naming the five toolbar controls in the design with nothing behind them — a view-mode switch, a tag filter, a calendar, a share control and Serial — and manual-checks.md and the README follow the controls that moved.
This commit is contained in:
@@ -3,86 +3,106 @@
|
||||
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
||||
xmlns:views="using:DodoSSH.Client.App.Views"
|
||||
x:Class="DodoSSH.Client.App.Views.HostsScreen"
|
||||
x:DataType="vm:MainWindowViewModel">
|
||||
x:DataType="vm:VaultViewModel">
|
||||
|
||||
<!--
|
||||
The hosts screen: the list of machines, and what this application has to say about the one that is
|
||||
selected.
|
||||
The hosts screen: every group, then every host, as cards — and a drawer for whichever one is selected.
|
||||
|
||||
It used to be the list beside a terminal, and the terminal is no longer here. The tab strip is above
|
||||
every screen now, so a terminal is a surface the whole window switches to rather than a column on this
|
||||
one — see MainWindowViewModel.ShellSurface. What that leaves this screen is the thing its name always
|
||||
promised: an overview.
|
||||
── IT WAS A 268-PIXEL LIST BESIDE A MOSTLY EMPTY COLUMN. ────────────────────────────────────────────
|
||||
That list was doing two jobs at two-thirds size. It had to be narrow so the editor beneath it could be
|
||||
a column, and being narrow is what made forty machines a scroll rather than a glance: a host row was a
|
||||
name, an address and an auth word stacked in 268 pixels, and the column beside it repeated all three
|
||||
for the one that happened to be selected.
|
||||
|
||||
In its own file, rather than left in MainWindow.axaml, because nothing inside that window can be laid
|
||||
out by a test — WebView2's adapter refuses the headless session's thread — so markup that stays there
|
||||
is markup nobody can measure. The four blocks in the right column are exactly the ones that most needed
|
||||
measuring: two host key prompts and a conflict log, all three of which appear only in states a person
|
||||
has to reproduce by hand.
|
||||
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.
|
||||
|
||||
Its data context is the shell, not the vault, so that the sidebar can be handed the vault and everything
|
||||
else can bind Vault.* — the same split MainWindow.axaml had. See MainWindow.axaml's own note on why the
|
||||
two cannot be put on one element.
|
||||
── 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
|
||||
the toolbar, and offers Serial beside Terminal. None of the five exists: there is one view, no saved
|
||||
filters, nothing dated, no sharing outside a team vault, and no serial transport in the SSH layer. Five
|
||||
disabled controls would teach nobody anything. See docs/design-import-gaps.md.
|
||||
|
||||
── ITS DATA CONTEXT IS THE VAULT, and it used to be the shell. ──────────────────────────────────────
|
||||
The shell was needed only so that the old sidebar could be handed the vault on its own element while
|
||||
everything around it bound Vault.*. The drawer needs no such thing — it is a child of this control and
|
||||
inherits what this control has — so the indirection went with the sidebar, and every binding in this
|
||||
file is now a property of the vault.
|
||||
|
||||
That is worth more than the tidiness: a control whose data context is the shell cannot be laid out by
|
||||
the layout harness without building an entire MainWindowViewModel, and this is the screen with the most
|
||||
worth measuring. The window wraps it in a Panel that carries IsHostsScreen, which is the same pattern
|
||||
every other screen there already uses — see MainWindow.axaml's note on why the two cannot go on one
|
||||
element.
|
||||
-->
|
||||
|
||||
<Grid ColumnDefinitions="268,*">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
|
||||
<views:HostSidebar Grid.Column="0" x:Name="Sidebar" DataContext="{Binding Vault}" />
|
||||
|
||||
<Grid Grid.Column="1" RowDefinitions="Auto,Auto,*,Auto">
|
||||
<Grid Grid.Column="0" RowDefinitions="Auto,Auto,Auto,*">
|
||||
|
||||
<!--
|
||||
Connecting. A password box only for a host that asks to be — a host bound to a stored credential or
|
||||
a key wants nothing typed here — and a sentence in its place when it does not, because "nothing
|
||||
needs typing" and "something needs typing and the box has not appeared yet" look identical and only
|
||||
one of them is fine.
|
||||
============ FIND ============
|
||||
One wide box, which is what the width bought. It narrows the grid below and nothing else — the
|
||||
connect path, the selection and the pinned host key list all read the unfiltered collection — so a
|
||||
filter left in the box can hide a host but can never break one.
|
||||
|
||||
Ctrl+K is named on it because the palette is the other way to reach a host by typing, and somebody
|
||||
who has found this box should know about the one that also connects on Enter.
|
||||
-->
|
||||
<Border Grid.Row="0" Padding="12,8" Background="{StaticResource Panel}"
|
||||
<Border Grid.Row="0" Padding="16,12" Background="{StaticResource Panel}"
|
||||
BorderBrush="{StaticResource BorderSubtle}" BorderThickness="0,0,0,1">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBox Text="{Binding Vault.ConnectPassword}" PlaceholderText="password (not stored)"
|
||||
PasswordChar="•" Width="200" VerticalAlignment="Center"
|
||||
IsVisible="{Binding Vault.SelectedHostAsksForAPassword}"
|
||||
ToolTip.Tip="Typed each time and never stored. To stop typing it, add a password under Keychain and bind this host to it in the host's own editor." />
|
||||
<TextBlock Text="{Binding Vault.SelectedHostAuthenticationNote}" Classes="hint"
|
||||
FontSize="11" VerticalAlignment="Center"
|
||||
IsVisible="{Binding !Vault.SelectedHostAsksForAPassword}" />
|
||||
<Button Classes="accent" Content="CONNECT" Command="{Binding Vault.ConnectCommand}"
|
||||
IsEnabled="{Binding !Vault.IsBusy}" />
|
||||
<TextBox x:Name="HostFilter" Text="{Binding HostFilter}"
|
||||
FontFamily="{StaticResource MonoFont}"
|
||||
PlaceholderText="Find a host by name, address or note… · Ctrl+K searches and connects" />
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
============ THE TOOLBAR ============
|
||||
Only what makes something new. EDIT and DELETE are not here: they act on one host, and one host is
|
||||
what the drawer is about — a pair of buttons over a grid of forty is a pair whose subject the user
|
||||
has to work out. The group's own EDIT and DELETE sit beside the group cards for the same reason.
|
||||
-->
|
||||
<Border Grid.Row="1" Padding="16,8" BorderBrush="{StaticResource BorderSubtle}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="accent" Content="+ NEW HOST" Command="{Binding NewHostCommand}" />
|
||||
<Button Classes="ghost" Content="+ NEW GROUP" Command="{Binding NewGroupCommand}"
|
||||
ToolTip.Tip="A heading for this grid, and the port, username and key the hosts under it inherit." />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Row="1">
|
||||
<StackPanel Grid.Row="2">
|
||||
|
||||
<!--
|
||||
Host key prompts. Unknown and changed look deliberately different: one is a decision, the other is
|
||||
a refusal. Presenting a changed key with a "continue" button is how users are taught to click
|
||||
through the one warning that matters.
|
||||
-->
|
||||
<Border Padding="12,10" Background="{StaticResource WarnWash}"
|
||||
<Border Padding="16,10" Background="{StaticResource WarnWash}"
|
||||
BorderBrush="{StaticResource WarnSoft}" BorderThickness="0,0,0,1"
|
||||
IsVisible="{Binding Vault.HasPendingHostKey}">
|
||||
IsVisible="{Binding HasPendingHostKey}">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="This host has not been seen before. Check the fingerprint against what the server's operator published."
|
||||
Foreground="{StaticResource WarnText}" TextWrapping="Wrap" />
|
||||
<SelectableTextBlock Classes="mono" Text="{Binding Vault.PendingHostKey.Fingerprint}"
|
||||
<SelectableTextBlock Classes="mono" Text="{Binding PendingHostKey.Fingerprint}"
|
||||
Foreground="{StaticResource Warn}" TextWrapping="Wrap" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Classes="accent" Content="TRUST AND CONNECT"
|
||||
Command="{Binding Vault.TrustHostKeyCommand}" />
|
||||
Command="{Binding TrustHostKeyCommand}" />
|
||||
<Button Classes="ghost" Content="CANCEL"
|
||||
Command="{Binding Vault.RejectHostKeyCommand}" />
|
||||
Command="{Binding RejectHostKeyCommand}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Padding="12,10" Background="{StaticResource DangerWash}"
|
||||
<Border Padding="16,10" Background="{StaticResource DangerWash}"
|
||||
BorderBrush="{StaticResource DangerSoft}" BorderThickness="0,0,0,1"
|
||||
IsVisible="{Binding Vault.HasHostKeyMismatch}">
|
||||
IsVisible="{Binding HasHostKeyMismatch}">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="The host key changed and the connection was refused."
|
||||
Foreground="{StaticResource Danger}" FontWeight="SemiBold" />
|
||||
<SelectableTextBlock Text="{Binding Vault.HostKeyMismatch}"
|
||||
<SelectableTextBlock Text="{Binding HostKeyMismatch}"
|
||||
Foreground="{StaticResource Danger}" TextWrapping="Wrap" />
|
||||
<TextBlock Text="If the server was legitimately rebuilt, edit the host and choose "Forget host key" first. There is deliberately no way to continue from here."
|
||||
Foreground="{StaticResource WarnText}" TextWrapping="Wrap" />
|
||||
@@ -93,20 +113,18 @@
|
||||
The conflict log. The merge is only allowed to pick a winner because the value it overrode is kept
|
||||
and shown; without this panel it would be last-writer-wins with a longer explanation.
|
||||
|
||||
Bounded and scrollable, which it was not while it lived in the window. It sits on an Auto row above
|
||||
a star row, and an ItemsControl with no ceiling grows without limit — so a pass that merged twenty
|
||||
items pushed everything below it off the bottom of a screen nobody could scroll. It went unnoticed
|
||||
for as long as it did because no test could lay this markup out; that is the other half of why this
|
||||
file exists.
|
||||
Bounded and scrollable. It sits on an Auto row above a star row, and an ItemsControl with no
|
||||
ceiling grows without limit — so a pass that merged twenty items would push the grid off the
|
||||
bottom of a screen nobody could scroll.
|
||||
-->
|
||||
<Border Padding="12,10" Background="{StaticResource Panel}"
|
||||
<Border Padding="16,10" Background="{StaticResource Panel}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1"
|
||||
IsVisible="{Binding Vault.HasConflicts}">
|
||||
IsVisible="{Binding HasConflicts}">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="Some changes could not be merged automatically."
|
||||
Foreground="{StaticResource Info}" FontWeight="SemiBold" />
|
||||
<ScrollViewer MaxHeight="180" HorizontalScrollBarVisibility="Disabled">
|
||||
<ItemsControl ItemsSource="{Binding Vault.Conflicts}">
|
||||
<ItemsControl ItemsSource="{Binding Conflicts}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ConflictRowViewModel">
|
||||
<Border Margin="0,4" Padding="8" Background="{StaticResource Raised}"
|
||||
@@ -114,7 +132,7 @@
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="{Binding Summary}" Foreground="{StaticResource Text}"
|
||||
TextWrapping="Wrap" />
|
||||
<SelectableTextBlock Classes="mono" Text="{Binding Detail}" FontSize="11"
|
||||
<SelectableTextBlock Classes="mono" Text="{Binding Detail}" FontSize="12"
|
||||
Foreground="{StaticResource TextDim}"
|
||||
IsVisible="{Binding HasDetail}" />
|
||||
</StackPanel>
|
||||
@@ -124,174 +142,304 @@
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
<Button Classes="ghost" Content="DISMISS ALL" HorizontalAlignment="Left"
|
||||
Command="{Binding Vault.AcknowledgeAllConflictsCommand}" />
|
||||
Command="{Binding AcknowledgeAllConflictsCommand}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
The overview proper: what is known about the host the list has selected.
|
||||
|
||||
Every fact here is one the sidebar already computes, and that is deliberate. This column was a
|
||||
terminal until this screen stopped hosting one, and filling it with something that needed new state
|
||||
would be inventing a feature to fill a rectangle. What it is for is the question the screen now has
|
||||
to answer — "which machine is this, and how will it let me in" — before the answer scrolls past in a
|
||||
list of forty.
|
||||
-->
|
||||
<ScrollViewer Grid.Row="2" HorizontalScrollBarVisibility="Disabled">
|
||||
<Panel Margin="24">
|
||||
|
||||
<StackPanel Spacing="10" HorizontalAlignment="Left" VerticalAlignment="Top"
|
||||
IsVisible="{Binding Vault.SelectedHost, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Classes="heading" Text="{Binding Vault.SelectedHost.Label}"
|
||||
VerticalAlignment="Center" />
|
||||
<Border Classes="chip" VerticalAlignment="Center"
|
||||
IsVisible="{Binding Vault.SelectedHost.IsConnected}">
|
||||
<TextBlock Text="CONNECTED" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<SelectableTextBlock Classes="mono" Text="{Binding Vault.SelectedHost.Address}"
|
||||
Foreground="{StaticResource TextDim}" />
|
||||
|
||||
<TextBlock Classes="hint" Text="{Binding Vault.SelectedHost.Authentication}" />
|
||||
|
||||
<TextBlock Classes="hint" FontSize="11" MaxWidth="440" TextWrapping="Wrap"
|
||||
Text="Press CONNECT, or double-click the host in the list. The terminal opens in the strip above and stays there while you look at anything else." />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Classes="hint" HorizontalAlignment="Left" VerticalAlignment="Top"
|
||||
MaxWidth="440" TextWrapping="Wrap"
|
||||
Text="Choose a host on the left to see what it is and how it authenticates. Ctrl+K searches them by name."
|
||||
IsVisible="{Binding Vault.SelectedHost, Converter={x:Static ObjectConverters.IsNull}}" />
|
||||
|
||||
</Panel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!--
|
||||
Groups: making them, renaming them, and taking them away.
|
||||
|
||||
Here rather than on the Keychain screen, because a group is not a secret — it is how this screen's
|
||||
list is arranged, and the arranging belongs beside the thing arranged. Filing a host into one is done
|
||||
in the host's own editor, on the left, for the same reason its key and its password are.
|
||||
|
||||
One text box for both adding and renaming. A group has exactly one field, so a separate rename form
|
||||
would be this box with a different heading; GroupSaveLabel is what says which of the two is about to
|
||||
happen.
|
||||
-->
|
||||
<Border Grid.Row="3" Padding="12,10" Background="{StaticResource Panel}"
|
||||
BorderBrush="{StaticResource BorderSubtle}" BorderThickness="0,1,0,0">
|
||||
<StackPanel Spacing="8">
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Classes="label" Text="GROUPS" Foreground="{StaticResource TextDim}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Classes="hint" FontSize="10.5" VerticalAlignment="Center" TextWrapping="Wrap"
|
||||
Text="Headings for the list on the left. Which group a host is in is part of the host, and stays encrypted." />
|
||||
</StackPanel>
|
||||
<!-- ============ THE GRID ============ -->
|
||||
<ScrollViewer Grid.Row="3" HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="16,14" Spacing="16">
|
||||
|
||||
<!-- ============ GROUPS ============ -->
|
||||
<!--
|
||||
Horizontal, because a group is a name and a count: a vertical list of one-line rows would take a
|
||||
third of this column to say what a row of chips says in one line.
|
||||
Absent entirely from a vault nobody has filed anything in, which is what makes groups cost
|
||||
nothing to ignore — the same rule the host list has always followed for its headings.
|
||||
-->
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"
|
||||
IsVisible="{Binding Vault.HasGroups}">
|
||||
<ListBox ItemsSource="{Binding Vault.Groups}" SelectedItem="{Binding Vault.SelectedGroup}"
|
||||
Background="Transparent" MaxHeight="72">
|
||||
<StackPanel Spacing="8" IsVisible="{Binding HasGroups}">
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="label" Text="GROUPS"
|
||||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
|
||||
|
||||
<!--
|
||||
The group's own two actions, beside the group cards rather than in the toolbar, because they
|
||||
act on the selected card and this is where the selection is made. Hidden rather than
|
||||
disabled while DELETE's question is up, as every other pair in this application is, so it
|
||||
cannot be pressed twice — see VaultViewModel.ShowsGroupActions.
|
||||
-->
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="ghost" Content="SHOW ALL"
|
||||
Command="{Binding ClearGroupFilterCommand}"
|
||||
IsVisible="{Binding IsFilteredByGroup}"
|
||||
ToolTip.Tip="Stops narrowing the grid to one group." />
|
||||
<Button Classes="ghost" Content="EDIT" Command="{Binding EditGroupCommand}"
|
||||
IsVisible="{Binding ShowsGroupActions}" />
|
||||
<Button Classes="ghost" Content="DELETE" Command="{Binding DeleteGroupCommand}"
|
||||
IsVisible="{Binding ShowsGroupActions}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Border Padding="10" Background="{StaticResource DangerWash}" CornerRadius="6"
|
||||
IsVisible="{Binding IsConfirmingGroupDeletion}">
|
||||
<views:ConfirmDeleteCard />
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
A ListBox rather than an ItemsControl of buttons, and that is what marks the chosen group for
|
||||
free: selection is a state the control already has, and the tile style draws it the same way
|
||||
every other list in this application draws a selected row. Buttons would have needed a
|
||||
Classes.active binding per card and a second copy of "which one".
|
||||
|
||||
SelectedItem is GroupFilter and not SelectedGroup. The two are nearly the same thing here —
|
||||
GroupFilter assigns SelectedGroup — but only one of them may narrow the grid; see the property
|
||||
for why the phone's own use of SelectedGroup must not.
|
||||
-->
|
||||
<ListBox Classes="tiles" x:Name="GroupGrid"
|
||||
ItemsSource="{Binding Groups}"
|
||||
SelectedItem="{Binding GroupFilter}">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate><WrapPanel /></ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:HostGroupRowViewModel">
|
||||
<StackPanel Margin="2,4" Spacing="1">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Classes="mono" Text="{Binding Label}" Foreground="{StaticResource Text}"
|
||||
FontSize="11.5" />
|
||||
<Border Classes="chip warn" Padding="4,0"
|
||||
IsVisible="{Binding Badge, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<TextBlock Text="{Binding Badge}" FontSize="8.5" />
|
||||
<Border Classes="tile">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<Border Grid.Column="0" Classes="tileicon" Background="{StaticResource Accent}">
|
||||
<TextBlock Text="▤" FontSize="14" Foreground="{StaticResource AccentInk}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<TextBlock Classes="mono" Text="{Binding Description}" FontSize="9.5"
|
||||
Foreground="{StaticResource TextFaint}" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Margin="10,0,0,0" VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<!-- A Grid rather than a horizontal StackPanel; see the note on the host card. -->
|
||||
<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>
|
||||
<TextBlock Text="{Binding Description}" FontSize="11"
|
||||
Foreground="{StaticResource TextFaint}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</ScrollViewer>
|
||||
|
||||
<!--
|
||||
A group is no longer only a name, so this is no longer only a box. 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 Spacing="6" IsVisible="{Binding Vault.ShowsGroupActions}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBox Text="{Binding Vault.GroupEditorLabel}" PlaceholderText="group name" Width="180"
|
||||
FontSize="11" MinHeight="26" Padding="8,3" />
|
||||
<ComboBox ItemsSource="{Binding Vault.GroupEditorParentChoices}"
|
||||
SelectedItem="{Binding Vault.GroupEditorSelectedParent}"
|
||||
MinWidth="150" FontSize="11" MinHeight="26">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:GroupChoice">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<NumericUpDown Value="{Binding Vault.GroupEditorDefaultPort}" Minimum="1" Maximum="65535"
|
||||
FormatString="0" ShowButtonSpinner="False" PlaceholderText="default port"
|
||||
Width="120" FontSize="11" MinHeight="26" />
|
||||
<TextBox Text="{Binding Vault.GroupEditorDefaultUsername}"
|
||||
PlaceholderText="default username" Width="150"
|
||||
FontSize="11" MinHeight="26" Padding="8,3" />
|
||||
<ComboBox ItemsSource="{Binding Vault.GroupEditorAuthenticationChoices}"
|
||||
SelectedItem="{Binding Vault.GroupEditorSelectedAuthentication}"
|
||||
MinWidth="180" FontSize="11" MinHeight="26">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:AuthenticationChoice">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
<TextBlock Text="{Binding Qualifier}" Classes="hint" FontSize="10"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding Qualifier, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="ghost" Content="{Binding Vault.GroupSaveLabel}"
|
||||
Command="{Binding Vault.SaveGroupCommand}" />
|
||||
<Button Classes="ghost" Content="EDIT SELECTED" Command="{Binding Vault.EditGroupCommand}" />
|
||||
<Button Classes="ghost" Content="DELETE" Command="{Binding Vault.DeleteGroupCommand}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
Swapped for the buttons rather than stacked under them, as the sidebar's own question is, so
|
||||
DELETE cannot be pressed again while its answer is on screen. It asks its own question only: the
|
||||
two panels share one pending deletion, and the sidebar checks the same way.
|
||||
-->
|
||||
<Border Padding="8" Background="{StaticResource DangerWash}" CornerRadius="4"
|
||||
IsVisible="{Binding Vault.IsConfirmingGroupDeletion}">
|
||||
<views:ConfirmDeleteCard DataContext="{Binding Vault}" />
|
||||
</Border>
|
||||
<!-- ============ HOSTS ============ -->
|
||||
<StackPanel Spacing="8">
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="label" Text="HOSTS"
|
||||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
|
||||
<!-- The collection's own count, so it follows both filters with no second number to keep in step. -->
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding VisibleHosts.Count}"
|
||||
FontSize="11" Foreground="{StaticResource TextFaint}"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<!--
|
||||
Named because it is where keyboard focus lands when the terminal gives it back, and because
|
||||
every gesture on it is wired in the code-behind.
|
||||
|
||||
Focusable, which a ListBox is not by default — Avalonia leaves focus to the items and an empty
|
||||
list has none. Without it the release-the-keyboard path is a measured no-op: it takes Win32
|
||||
focus off the terminal's child window and then calls Focus() on something that refuses it,
|
||||
leaving the window with nothing focused and the keystrokes going nowhere.
|
||||
|
||||
It binds SidebarRows rather than VisibleHosts, so the group headings are still in it. Two
|
||||
reasons, and neither is decoration: a grid showing every group at once has to say which cards
|
||||
belong to which, and a heading is the drop target that takes a host out of one group and into
|
||||
another. See VaultViewModel.RebuildSidebarRows for why a heading is a row rather than a
|
||||
container, and the code-behind for what a drag does with one.
|
||||
-->
|
||||
<ListBox Classes="tiles" x:Name="HostGrid" Focusable="True"
|
||||
DragDrop.AllowDrop="True"
|
||||
ItemsSource="{Binding SidebarRows}"
|
||||
SelectedItem="{Binding SelectedSidebarRow}">
|
||||
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate><WrapPanel /></ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
|
||||
<!--
|
||||
The three things you can do to a host, on the host itself.
|
||||
|
||||
On the list rather than in the item template, and that is what makes it one menu rather than
|
||||
one per card: a ContextMenu inside a DataTemplate would have the row for its data context,
|
||||
and the commands here are the vault's. The code-behind selects whatever was right-clicked
|
||||
before the menu opens, so the selection-based commands act on the card under the pointer,
|
||||
and cancels the menu outright over a group heading, which has no host to act on.
|
||||
-->
|
||||
<ListBox.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="Connect" Command="{Binding ConnectCommand}" />
|
||||
<MenuItem Header="Edit…" Command="{Binding EditSelectedHostCommand}" />
|
||||
<Separator />
|
||||
<MenuItem Header="Delete…" Command="{Binding DeleteHostCommand}" />
|
||||
</ContextMenu>
|
||||
</ListBox.ContextMenu>
|
||||
|
||||
<ListBox.DataTemplates>
|
||||
|
||||
<!--
|
||||
A heading, and it takes a whole row of the wrap rather than sitting in the flow as another
|
||||
card: it names the run of cards under it, and a heading the width of one card would read
|
||||
as the first of them. The stretch is what MinWidth buys — a WrapPanel gives a child the
|
||||
width it asks for and starts a new line when the line cannot hold it, so a child asking
|
||||
for more than one line holds is alone on its own.
|
||||
-->
|
||||
<DataTemplate DataType="vm:SidebarGroupHeader">
|
||||
<Button Classes="flat grouphead" MinWidth="640"
|
||||
Command="{Binding $parent[ListBox].((vm:VaultViewModel)DataContext).ToggleGroupCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
HorizontalContentAlignment="Stretch">
|
||||
<Grid ColumnDefinitions="Auto,Auto,*">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Chevron}"
|
||||
Foreground="{StaticResource TextFaint}" FontSize="9"
|
||||
VerticalAlignment="Center" Margin="0,0,6,0" />
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="{Binding Label}"
|
||||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Count}" FontSize="11"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
|
||||
Margin="8,0,0,0" HorizontalAlignment="Left" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="vm:HostRowViewModel">
|
||||
<Border Classes="tile">
|
||||
<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>
|
||||
|
||||
<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.
|
||||
|
||||
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>
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<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>
|
||||
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<Ellipse Grid.Column="2" Classes="dot" Classes.live="{Binding IsConnected}"
|
||||
VerticalAlignment="Top" Margin="6,3,0,0" />
|
||||
</Grid>
|
||||
|
||||
<!--
|
||||
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.
|
||||
|
||||
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>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
|
||||
</ListBox.DataTemplates>
|
||||
</ListBox>
|
||||
|
||||
<!--
|
||||
Nothing to show, and the two reasons it can happen are different questions. An empty keychain
|
||||
is an invitation; a filter that matches nothing is a filter that has gone too far, and saying
|
||||
"add a host" to somebody who has thirty would be answering the wrong one.
|
||||
-->
|
||||
<TextBlock Classes="hint" FontSize="12" TextWrapping="Wrap" MaxWidth="480"
|
||||
HorizontalAlignment="Left" Margin="0,4,0,0"
|
||||
IsVisible="{Binding !HasVisibleHosts}"
|
||||
Text="{Binding NoVisibleHostsMessage}" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
|
||||
</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.
|
||||
|
||||
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.
|
||||
-->
|
||||
<Panel Grid.Column="1" IsVisible="{Binding IsDrawerOpen}">
|
||||
<views:HostDrawer x:Name="Drawer" />
|
||||
</Panel>
|
||||
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
|
||||
Reference in New Issue
Block a user