Public Access
Seven things reported from a day's use of v5b's session shell, and they are one commit because five of them are the same complaint from different angles: the window spends too much of itself on chrome describing the session, and the parts that are not chrome do not behave. ◆ THE HOST HEADER IS GONE, and that is a deliberate departure from the design. Terminal.dc.html and SFTP.dc.html both draw a 60-pixel row above the pane: the address on the left, a cross-surface button on the right. Both facts are worth having and the strip they sat in was not — the tab already names the host, and a full-width bar repeating it was the cheapest 60 pixels in the layout to give back. The address is now the first line of the sidebar and the button is stretched across the column under it, so nothing is lost and the pane is taller. Which word that button carries and which command it runs used to be handed in from the two usage sites in MainWindow.axaml, because the control was drawn twice. One sidebar cannot do that, so SessionCrossSurfaceLabel and OpenOtherSurfaceCommand resolve it in the shell — the same place SessionAddress already decides which surface's fact to read. The two directions underneath are untouched: SelectFilesHostAsync for a tab's host, OpenTerminalForFilesHostAsync for a fresh terminal at whatever SFTP has open. SessionHeader.axaml is deleted rather than left unused, and LayoutHarness stops subtracting its 60 pixels from every session screen's budget — the same treatment the retired window-wide tab strip got, and for the same reason: a constant for chrome nobody draws is a suite quietly measuring the wrong rectangle. ◆ AND THE SIDEBAR CLOSES, which the design has no state for at all. 300 pixels of an 1081-pixel minimum is a great deal to spend on a list that is often two rows long. The column now folds to a 34-pixel rail carrying the chevron that brings it back — a rail rather than nothing, because a panel that vanishes leaving no trace is one people report as lost rather than as closed. Both states live in the one control and swap on IsSessionSidebarOpen, so MainWindow's own "Auto" column takes whichever width is showing without knowing the state exists. Written through to ClientSettings.SessionSidebarOpen rather than held for the session. It is a decision about how much of the window a terminal gets, and one that had to be made again on every launch would not really be on offer. ---- THE FOUR SMALLER ONES ---- A SNIP LANDED IN A TERMINAL NOBODY COULD TYPE AT, and looked selected when it got there. Two causes with nothing in common. The click moved Win32 focus onto the sidebar row, and term.focus() in the page cannot take it back — only the host can, so the shell raises TerminalFocusRequested and the window answers with the same posted focus every other path here uses. The highlight was bash: xterm wraps a paste in bracketed-paste markers, readline marks what arrives inside them as an active region, and it stays in reverse video until the next keystroke. Right for a clipboard paste, wrong for a snippet picked off a sidebar. Single-line snips are typed rather than pasted now, which needs no markers; multi-line still pastes, because "runs three commands unasked" is the worse of the two failures and the markers are the whole of what prevents it. A BLACK BAR UNDER THE TERMINAL, on Windows. xterm.css paints its scrolling viewport #000 — its own comment explains why, and it is a macOS scrollbar concern. Everywhere else that black is covered by the rows, except along the bottom: the fit addon floors the row count, so the remainder below the last whole row is bare viewport, up to a line tall, against this page's #171a26. The light square at its right-hand end is where WebView2's classic scrollbar corner lands. The viewport is repainted in the page's own background, and the scrollbar with it — thin and in these colours rather than a grey Windows channel down the side of a near-black terminal, and kept rather than hidden, because a surface that scrolls with no sign that it does is worse than a quiet bar. THE PINS ROW DREW A TOFU BOX. U+E946 is not in the embedded Material Icons face at all — that file is the 2019 build and its cmap skips E944 and E946 — so the rail's Pins row and the hosts screen's own pin badge have both been drawing a missing-glyph rectangle since v5b picked the codepoint. push_pin in that vintage is U+F10D, verified against the file rather than against a codepoints table for a later release of the font. Every other icon codepoint in the repository was audited the same way; this was the only miss. THE KBD CHIP CUT THE CHORD IN HALF. 34 pixels is the design's width for a chip reading ⌘K, and this build substitutes CTRL K — six characters and a space, wider than 34 at 10.5 mono. MinWidth and padding instead, so the design's footprint survives for the day this face has a ⌘ to draw. ---- AND THE POPOVER UNDER THE USER CHIP ---- Reported as not matching the design, and it was not: Button.poprow set a corner radius and a padding and never touched the Background, so every row wore the Fluent theme's own #33FFFFFF button fill. Six raised pills stacked in a menu the design draws as six lines of text — and the hover rule underneath was already correct and simply invisible against a fill that never went away. Set on the ContentPresenter as well as on the Button, the same as Button.flat, because the theme binds its brush there and a Background set only on the control loses to it. The panel itself gets this window's own radius-12 card treatment through a FlyoutPresenter class rather than by widening the shared context-menu rule, and Vaults and Preferences stop being drawn one step dimmer than Settings and Logout, which read as two disabled entries in a menu of five live ones. ---- WHAT PROVES IT ---- Three tests in the layout suite, two of them checked against the defect they describe: the popover row's resting fill (fails with #33ffffff without the style), and the kbd chip against the natural width of its own text, measured on a detached copy because a TextBlock's DesiredSize is already clipped to what it was given and reports 34 inside a 34-pixel chip either way. SessionSidebarTests is new — the sidebar has never been laid out by a test, and it now holds a string of unbounded length beside a button that has to stay clickable. In the shell suite: the cross-surface row in both directions, the closed state surviving to disk, and the focus request being made when a snip lands and not made when it does not.
810 lines
53 KiB
XML
810 lines
53 KiB
XML
<UserControl xmlns="https://github.com/avaloniaui"
|
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
|
xmlns:views="using:DodoSSH.Client.App.Views"
|
|
x:Class="DodoSSH.Client.App.Views.HostsScreen"
|
|
x:DataType="vm:VaultViewModel">
|
|
|
|
<!--
|
|
The monogram converters, as resources rather than through x:Static: MonogramBrushConverter's own remarks
|
|
call for "a StaticResource-shaped reference" naming which half a card wants, and a resource entry is
|
|
what makes that literal — the markup below reads {StaticResource MonogramBg} and
|
|
{StaticResource MonogramFg} rather than a converter parameter that would mean nothing without reading
|
|
that file. Constructed with x:Arguments because the type takes its "which half" as a constructor
|
|
parameter, not a settable property — there is nothing to set once the instance exists.
|
|
-->
|
|
<UserControl.Resources>
|
|
<views:MonogramBrushConverter x:Key="MonogramBg">
|
|
<x:Arguments>
|
|
<x:Boolean>True</x:Boolean>
|
|
</x:Arguments>
|
|
</views:MonogramBrushConverter>
|
|
<views:MonogramBrushConverter x:Key="MonogramFg">
|
|
<x:Arguments>
|
|
<x:Boolean>False</x:Boolean>
|
|
</x:Arguments>
|
|
</views:MonogramBrushConverter>
|
|
</UserControl.Resources>
|
|
|
|
<!--
|
|
The hosts screen: every group as a heading, every host under it as a card, and a drawer for whichever one
|
|
is selected.
|
|
|
|
── v5: THE BOARD IS FLAT SECTIONS NOW, NOT A GRID OF GROUP CARDS ONE LEVEL AT A TIME. ──────────────────
|
|
The v4 shape was a directory pane: a wrap of group cards above the hosts, opened by a double-click, with
|
|
a breadcrumb trail above it saying where you were. That model traded a real cost for it — a keychain
|
|
filed under three groups took three clicks to see all of, and "where is everything" had no answer at
|
|
all, only "where am I right now". v5 draws every group at once, in label order, "No group" first, exactly
|
|
the flattening the phone's SidebarRows has always drawn — see VaultViewModel.HostSections and
|
|
VaultViewModel.FlattenIntoSections. A host is filed under a group through its own editor, or through the
|
|
chosen-hosts menu's "Change group…"; dragging a card onto a group card is gone with the cards it used to
|
|
land on. See HostsScreen.axaml.cs for the gestures this shape now needs — a shift-click's run and a
|
|
band's rectangle both have to reach across however many sections are on screen, where a single ListBox
|
|
used to answer both on its own.
|
|
|
|
── ONE SECTION, ONE ListBox, AND "Board" IS THE ItemsControl OF SECTIONS. ────────────────────────────────
|
|
Each HostSectionViewModel draws its own heading and its own ListBox of cards (Classes="sectioncards"),
|
|
which is what lets the wrap of cards stay an ordinary WrapPanel-of-cards-only rather than a single list
|
|
fighting to make a header row pretend to be as wide as the cards it sits above. "Board" is the outer
|
|
ItemsControl holding every section — named because the code-behind needs one focusable ancestor of every
|
|
card to hang KeyDown, tunnelled PointerPressed/ContextRequested and DoubleTapped from; see the remarks on
|
|
HostsScreen for why nothing here can be a single ListBox's own SelectedItem any more.
|
|
|
|
◆ WHY EVERY COMMAND REACHED FROM INSIDE A SECTION OR A CARD GOES THROUGH "#Board" RATHER THAN A PLAIN
|
|
{Binding}. This markup nests two DataTemplates deep by the time it reaches a card — HostSections's own
|
|
item template (x:DataType HostSectionViewModel) and then, inside that, a second ListBox's item template
|
|
(x:DataType HostRowViewModel) — so the ambient DataContext by the time a pencil button or a context menu
|
|
item is declared is a section or a row, never the vault. $parent[ItemsControl] cannot walk past the
|
|
inner ListBox to reach the outer one, because a ListBox answers "is this an ItemsControl" as truly as the
|
|
real target does and $parent stops at the nearest match; and a ContextMenu or MenuFlyout's content sits
|
|
in its own popup, inheriting whatever DataContext the control that opened it already has rather than
|
|
anything further up a visual chain. "#Board" sidesteps both: Avalonia's NameScope is the whole document's,
|
|
not each template's own, so an ElementName reference resolves the same way from inside a WrapPanel's card
|
|
or from inside a MenuFlyout's popup — see #Board.((vm:VaultViewModel)DataContext).SomeCommand throughout
|
|
this file. Kept as one idiom rather than mixing it with $parent[ItemsControl] where a single level of
|
|
nesting would technically allow it (the toolbar's Group ▾ and Tag ▾ flyouts are exactly that case),
|
|
because two techniques answering the same question is one more thing to get right per binding.
|
|
|
|
Cards split the two jobs the old 268-pixel list used to do at two-thirds size. The board 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 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 board 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 calendar and a share control in the toolbar,
|
|
and offers Serial beside Terminal, and colours the status dot amber for "reachable but not connected".
|
|
None of the four exists: there is one view, nothing dated, no sharing outside a team vault, no serial
|
|
transport in the SSH layer, and nothing here ever pings a host — a third dot colour would be a claim this
|
|
application does not check. See docs/design-import-gaps.md and hosts-v5-design-spec.md's deviations.
|
|
|
|
── 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 (directly at the root, or through #Board once a DataTemplate has
|
|
changed what is ambient).
|
|
|
|
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="*,Auto">
|
|
|
|
<Grid Grid.Column="0" RowDefinitions="Auto,Auto,Auto,*">
|
|
|
|
<!--
|
|
============ THE TOOLBAR ============
|
|
"Hosts" and the count chip on the left, the two narrowing flyouts and + New host on the right. 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 board of forty is a pair whose subject the user has to work out. The group's own
|
|
Edit/Move/Delete sit on its own heading's menu for the same reason.
|
|
-->
|
|
<Grid Grid.Row="0" Margin="16,20,16,16" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto">
|
|
|
|
<TextBlock Grid.Column="0" Text="Hosts" FontSize="33" FontWeight="Bold" LetterSpacing="-0.5"
|
|
Foreground="{StaticResource Text}" VerticalAlignment="Center" />
|
|
|
|
<!--
|
|
The count chip: every host in a shown vault, unfiltered — see HostBoardTotalCount. Deliberately not
|
|
narrowed by the find box or the two flyouts below, which already say "how many cards are on screen
|
|
right now" on each section's own heading; this says how big the keychain is.
|
|
-->
|
|
<Border Grid.Column="1" Margin="10,0,0,0" MinWidth="34" Height="30" CornerRadius="9" Padding="8,0"
|
|
Background="{StaticResource Chip}" VerticalAlignment="Center">
|
|
<TextBlock Classes="mono" Text="{Binding HostBoardTotalCount}" FontSize="12.5"
|
|
Foreground="{StaticResource TextDim}" HorizontalAlignment="Center"
|
|
VerticalAlignment="Center" />
|
|
</Border>
|
|
|
|
<!--
|
|
============ Group ▾ ============
|
|
Chips rather than checkable menu items, the same idiom the tab strip's vault menu uses and for the
|
|
same reason: nothing in this application uses a checkable MenuItem, and a chip that fills when worn
|
|
is this window's own vocabulary for on-and-off already. A host passes by being in a ticked group;
|
|
empty means every group. "New group…" lives at the foot rather than being a control of its own,
|
|
because the mock has no group-creation control and this is the nearest thing to where the design
|
|
put it — see hosts-v5-design-spec.md's deviations.
|
|
-->
|
|
<Button Grid.Column="3" Classes="ghost" Height="40" Margin="0,0,8,0" Content="Group ▾"
|
|
ToolTip.Tip="Narrow the board to one or more groups">
|
|
<Button.Flyout>
|
|
<Flyout Placement="BottomEdgeAlignedLeft">
|
|
<StackPanel Width="240" Spacing="8">
|
|
<TextBlock Classes="label" Text="FILTER BY GROUP" />
|
|
|
|
<ItemsControl ItemsSource="{Binding GroupFilterChoices}">
|
|
<ItemsControl.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:GroupFilterChoice">
|
|
<Button Classes="chiptoggle" Classes.worn="{Binding IsChecked}" Margin="0,0,0,4"
|
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
|
Command="{Binding #Board.((vm:VaultViewModel)DataContext).ToggleGroupFilterCommand}"
|
|
CommandParameter="{Binding}">
|
|
<TextBlock Text="{Binding Label}" FontSize="11" TextTrimming="CharacterEllipsis" />
|
|
</Button>
|
|
</DataTemplate>
|
|
</ItemsControl.ItemTemplate>
|
|
</ItemsControl>
|
|
|
|
<Button Classes="ghost" HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
|
Content="All groups" IsVisible="{Binding HasActiveGroupFilter}"
|
|
Command="{Binding ResetGroupFilterCommand}" />
|
|
|
|
<Border Height="1" Background="{StaticResource BorderSubtle}" />
|
|
|
|
<Button Classes="ghost" HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
|
Content="New group…" Command="{Binding NewGroupCommand}"
|
|
ToolTip.Tip="A heading for the board, and the port, username and key the hosts under it inherit." />
|
|
</StackPanel>
|
|
</Flyout>
|
|
</Button.Flyout>
|
|
</Button>
|
|
|
|
<!--
|
|
============ Tag ▾ ============
|
|
A host passes by wearing ANY ticked tag, not all of them — the same reading MatchesHostBoardFilters
|
|
gives it. No footer control: a tag is made from a host's own editor, not from this flyout.
|
|
-->
|
|
<Button Grid.Column="4" Classes="ghost" Height="40" Margin="0,0,8,0" Content="Tag ▾"
|
|
ToolTip.Tip="Narrow the board to one or more tags">
|
|
<Button.Flyout>
|
|
<Flyout Placement="BottomEdgeAlignedLeft">
|
|
<StackPanel Width="240" Spacing="8">
|
|
<TextBlock Classes="label" Text="FILTER BY TAG" />
|
|
|
|
<ItemsControl ItemsSource="{Binding TagFilterChoices}">
|
|
<ItemsControl.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:TagFilterChoice">
|
|
<Button Classes="chiptoggle" Classes.worn="{Binding IsChecked}" Margin="0,0,0,4"
|
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
|
Command="{Binding #Board.((vm:VaultViewModel)DataContext).ToggleTagFilterCommand}"
|
|
CommandParameter="{Binding}">
|
|
<TextBlock Text="{Binding Label}" FontSize="11" TextTrimming="CharacterEllipsis" />
|
|
</Button>
|
|
</DataTemplate>
|
|
</ItemsControl.ItemTemplate>
|
|
</ItemsControl>
|
|
|
|
<Button Classes="ghost" HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
|
Content="All tags" IsVisible="{Binding HasActiveTagFilter}"
|
|
Command="{Binding ResetTagFilterCommand}" />
|
|
</StackPanel>
|
|
</Flyout>
|
|
</Button.Flyout>
|
|
</Button>
|
|
|
|
<Button Grid.Column="5" Classes="accent" Height="40" FontSize="13.5" Content="+ New host"
|
|
Command="{Binding NewHostCommand}" />
|
|
|
|
</Grid>
|
|
|
|
<!--
|
|
============ FIND ============
|
|
One wide box, restyled but doing exactly what it always has: it narrows the board 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.
|
|
|
|
◆ IT IS ALSO THE WAY OUT OF THE SECTIONS. Typing here searches every section at once, whatever a
|
|
collapse fold or the two flyouts above have done to the board — the one thing on this screen that
|
|
crosses a group boundary, and it has to be: a search that looked only at what happens to be expanded
|
|
would say "no host matches that" about a host this keychain has got. See VaultViewModel.Matches.
|
|
|
|
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="1" Margin="16,0,16,16">
|
|
<TextBox x:Name="HostFilter" Text="{Binding HostFilter}" Height="40" CornerRadius="10"
|
|
FontFamily="{StaticResource MonoFont}"
|
|
PlaceholderText="Find a host by name, address or note… · Ctrl+K searches and connects" />
|
|
</Border>
|
|
|
|
<!--
|
|
============ THE PANELS ============
|
|
At most one of these is up at any moment — the view model disarms the others on the way up, see
|
|
MoveChosenHostsToVault, RegroupChosenHosts, DeleteChosenHosts, MoveGroup and DeleteGroup — and every
|
|
one of them sits here, above the board, rather than laid over it: a card over the cards would hide
|
|
the very ticks or the very group it is asking about.
|
|
-->
|
|
<StackPanel Grid.Row="2" Margin="16,0,16,12" Spacing="10">
|
|
|
|
<!--
|
|
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, on an Auto row rather than growing without limit — a pass that merged
|
|
twenty items must not push the board off the bottom of a screen nobody can scroll to.
|
|
-->
|
|
<Border Padding="14,10" Background="{StaticResource Panel}" CornerRadius="10"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="1" 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 Conflicts}">
|
|
<ItemsControl.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:ConflictRowViewModel">
|
|
<Border Margin="0,4" Padding="8" Background="{StaticResource Raised}" CornerRadius="6">
|
|
<StackPanel Spacing="4">
|
|
<TextBlock Text="{Binding Summary}" Foreground="{StaticResource Text}"
|
|
TextWrapping="Wrap" />
|
|
<SelectableTextBlock Classes="mono" Text="{Binding Detail}" FontSize="12"
|
|
Foreground="{StaticResource TextDim}"
|
|
IsVisible="{Binding HasDetail}" />
|
|
</StackPanel>
|
|
</Border>
|
|
</DataTemplate>
|
|
</ItemsControl.ItemTemplate>
|
|
</ItemsControl>
|
|
</ScrollViewer>
|
|
<Button Classes="ghost" Content="DISMISS ALL" HorizontalAlignment="Left"
|
|
Command="{Binding AcknowledgeAllConflictsCommand}" />
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!--
|
|
◆ ONE GROUP'S DELETION QUESTION, arrived at from a heading's own menu rather than from a card — the
|
|
group cards this used to sit under are gone, and Edit/Move/Delete moved to the heading. See
|
|
EditGroupFromHeading, MoveGroupFromHeading and DeleteGroupFromHeading.
|
|
-->
|
|
<Border Padding="10" Background="{StaticResource DangerWash}" CornerRadius="10"
|
|
IsVisible="{Binding IsConfirmingGroupDeletion}">
|
|
<views:ConfirmDeleteCard />
|
|
</Border>
|
|
|
|
<!--
|
|
◆ MOVING THE GROUP TO ANOTHER VAULT. A picker and two buttons rather than a question with a yes:
|
|
what is being asked is which vault, and a move is undone by moving it back — so it is not drawn in
|
|
the danger colours the deletion question above uses. It sits beside that question and never with
|
|
it: MoveGroup disarms a pending deletion and DeleteGroup folds this away, so at most one is drawn.
|
|
|
|
The sentence is what the panel is for. A group cannot go anywhere alone: the machines under it are
|
|
items of the vault it is leaving, and so are the groups nested inside it, so all of them are
|
|
re-sealed under the destination's key and all of them take new ids. What cannot come is the group
|
|
it is nested under, which is why it arrives at the top level.
|
|
-->
|
|
<Border Padding="10" Background="{StaticResource Panel}" CornerRadius="10"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="1" IsVisible="{Binding IsMovingGroup}">
|
|
<StackPanel Spacing="8">
|
|
<TextBlock Classes="label" Text="MOVE GROUP TO VAULT" />
|
|
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding MoveGroupVaultChoices}"
|
|
SelectedItem="{Binding SelectedMoveGroupVault}">
|
|
<ComboBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:VaultChoiceViewModel">
|
|
<TextBlock Text="{Binding Display}" FontSize="12" />
|
|
</DataTemplate>
|
|
</ComboBox.ItemTemplate>
|
|
</ComboBox>
|
|
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
|
Text="The group goes with the groups inside it and every host filed under them, all re-encrypted with the other vault's key. Any group it is nested under stays behind, so it arrives at the top level, and the hosts' tags stay behind with it." />
|
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
<Button Classes="accent" Content="MOVE" Command="{Binding ConfirmMoveGroupCommand}"
|
|
IsEnabled="{Binding !IsBusy}" />
|
|
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelMoveGroupCommand}" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!--
|
|
============ ◆ WHAT SAYS A SET IS UP ============
|
|
Ctrl, Shift and a band dragged across the board tick cards — see HostsScreen.axaml.cs — and this is
|
|
the whole of the chrome that arrives with them: how many, the way out, and where the actions are.
|
|
The phone raises a contextual action bar in the vault header's place for the same state; a window
|
|
with room for a context menu does not need one, and a strip of eight buttons over the board would
|
|
be a second home for entries the menu already has.
|
|
-->
|
|
<Grid ColumnDefinitions="Auto,Auto,*" IsVisible="{Binding IsChoosingHosts}">
|
|
<Border Grid.Column="0" Classes="chip accent" Padding="6,1" VerticalAlignment="Center">
|
|
<TextBlock Text="{Binding ChosenHostsSummary}" FontSize="9.5" />
|
|
</Border>
|
|
<Button Grid.Column="1" Classes="flat" Margin="6,0,0,0" Padding="5,1" FontSize="9.5"
|
|
Content="CLEAR" Command="{Binding ClearHostChoiceCommand}"
|
|
ToolTip.Tip="Takes the ticks off every card · Esc, or a click on the space around them" />
|
|
<TextBlock Grid.Column="2" Classes="hint" FontSize="10" Margin="8,0,0,0"
|
|
VerticalAlignment="Center" TextTrimming="CharacterEllipsis"
|
|
Text="right-click one of them for what can be done to all of them" />
|
|
</Grid>
|
|
|
|
<!--
|
|
◆ MOVING OR COPYING THEM TO ANOTHER VAULT. One panel for both verbs, because they differ in one
|
|
word and in the sentence under the picker; what varies is bound rather than duplicated. See
|
|
VaultViewModel.ChosenHostVaultPanelTitle and its two siblings.
|
|
-->
|
|
<Border Padding="10" Background="{StaticResource Panel}" CornerRadius="10"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="1"
|
|
IsVisible="{Binding IsSendingChosenHostsToAVault}">
|
|
<StackPanel Spacing="8">
|
|
<Grid ColumnDefinitions="Auto,*">
|
|
<TextBlock Grid.Column="0" Classes="label" Text="{Binding ChosenHostVaultPanelTitle}" />
|
|
<TextBlock Grid.Column="1" Classes="mono" Margin="8,0,0,0" FontSize="11"
|
|
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
|
|
Text="{Binding ChosenHostsSummary}" />
|
|
</Grid>
|
|
|
|
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding ChosenHostVaultChoices}"
|
|
SelectedItem="{Binding SelectedChosenHostVault}">
|
|
<ComboBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:VaultChoiceViewModel">
|
|
<TextBlock Text="{Binding Display}" FontSize="12" />
|
|
</DataTemplate>
|
|
</ComboBox.ItemTemplate>
|
|
</ComboBox>
|
|
|
|
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
|
Text="{Binding ChosenHostVaultPanelNote}" />
|
|
|
|
<!--
|
|
◆ THE KEY, and only for a move of exactly one host — which key to carry is a fact about one
|
|
machine, and a copy that took it away would leave the original unable to connect. Unticked,
|
|
because moving a key into a team's vault hands it to everybody holding that vault's key.
|
|
-->
|
|
<CheckBox IsVisible="{Binding HasAChosenBindingToBring}"
|
|
IsChecked="{Binding BringsTheChosenBindingAlong}">
|
|
<TextBlock FontSize="11.5" TextWrapping="Wrap" Text="{Binding ChosenBindingToBringQuestion}" />
|
|
</CheckBox>
|
|
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
|
IsVisible="{Binding HasAChosenBindingToBring}"
|
|
Text="{Binding ChosenBindingToBringNote}" />
|
|
|
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
<Button Classes="accent" Content="{Binding ChosenHostVaultPanelVerb}"
|
|
Command="{Binding ConfirmSendChosenHostsToAVaultCommand}"
|
|
IsEnabled="{Binding !IsBusy}" />
|
|
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelSendChosenHostsToAVaultCommand}" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!--
|
|
◆ FILING THEM UNDER A GROUP, which is what "Change group…" on the chosen-hosts menu offers instead
|
|
of a drag now that there is no group card to drop one on.
|
|
-->
|
|
<Border Padding="10" Background="{StaticResource Panel}" CornerRadius="10"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="1"
|
|
IsVisible="{Binding IsRegroupingChosenHosts}">
|
|
<StackPanel Spacing="8">
|
|
<Grid ColumnDefinitions="Auto,*">
|
|
<TextBlock Grid.Column="0" Classes="label" Text="CHANGE GROUP" />
|
|
<TextBlock Grid.Column="1" Classes="mono" Margin="8,0,0,0" FontSize="11"
|
|
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
|
|
Text="{Binding ChosenHostsSummary}" />
|
|
</Grid>
|
|
|
|
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding ChosenHostGroupChoices}"
|
|
SelectedItem="{Binding SelectedChosenHostGroup}">
|
|
<ComboBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:GroupChoice">
|
|
<TextBlock Text="{Binding Label}" FontSize="12" />
|
|
</DataTemplate>
|
|
</ComboBox.ItemTemplate>
|
|
</ComboBox>
|
|
|
|
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
|
Text="A group lends its port, its account and its key to every host filed under it that says nothing itself, so this can change what these machines dial. Nothing else about them moves." />
|
|
|
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
<Button Classes="accent" Content="FILE" Command="{Binding ConfirmRegroupChosenHostsCommand}"
|
|
IsEnabled="{Binding !IsBusy}" />
|
|
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelRegroupChosenHostsCommand}" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!--
|
|
◆ THE DELETION QUESTION, naming a count rather than a machine — which is the reason
|
|
DeletionTarget.ChosenHosts exists: six copies of "delete prod-db?" is not a confirmation anybody
|
|
reads. The shared card draws it, as it does for a group above and for one host in the drawer.
|
|
-->
|
|
<Border Padding="10" Background="{StaticResource DangerWash}" CornerRadius="10"
|
|
IsVisible="{Binding IsConfirmingChosenHostDeletion}">
|
|
<views:ConfirmDeleteCard />
|
|
</Border>
|
|
|
|
</StackPanel>
|
|
|
|
<!--
|
|
============ THE BOARD ============
|
|
A drop target that takes nothing itself — no group cards left to receive a drag. See
|
|
HostsScreen.axaml.cs.
|
|
-->
|
|
<ScrollViewer Grid.Row="3" x:Name="Scroll" HorizontalScrollBarVisibility="Disabled">
|
|
<StackPanel Margin="16,0,16,20" Spacing="16">
|
|
|
|
<!--
|
|
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 an ItemsControl is not by default. 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.
|
|
|
|
◆ EVERY GROUP, ONE AFTER ANOTHER, "No group" FIRST. Binds HostSections rather than VisibleHosts:
|
|
the old grid held one level of a tree at a time, and this holds every group's cards at once, the
|
|
way SidebarRows always has for the phone. A keychain nobody has filed anything in draws one
|
|
section with no heading at all — see HostSectionViewModel.HasHeader — which is what makes the
|
|
feature invisible until it is used.
|
|
-->
|
|
<ItemsControl x:Name="Board" Focusable="True" ItemsSource="{Binding HostSections}">
|
|
<ItemsControl.ItemsPanel>
|
|
<ItemsPanelTemplate><StackPanel Spacing="22" /></ItemsPanelTemplate>
|
|
</ItemsControl.ItemsPanel>
|
|
|
|
<ItemsControl.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:HostSectionViewModel">
|
|
<StackPanel Spacing="10">
|
|
|
|
<!--
|
|
============ THE SECTION HEADING ============
|
|
"{label} · {count}", the vault name beside it when more than one vault is in play, a
|
|
per-section collapse toggle, Collapse All / Expand All on the FIRST heading only — see
|
|
SidebarGroupHeader.IsFirstBoardSection — and a menu for Edit…/Move to another vault…/
|
|
Delete…, in that order, with no Open: the board draws every group at once now, so there
|
|
is nothing left to open one INTO. Absent entirely for the headerless section a groupless
|
|
keychain draws — see HasHeader on the type, and the remark on the item template above.
|
|
-->
|
|
<Grid IsVisible="{Binding HasHeader}" ColumnDefinitions="Auto,*,Auto,Auto">
|
|
|
|
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
|
<Button Classes="flat" Padding="2"
|
|
Command="{Binding #Board.((vm:VaultViewModel)DataContext).ToggleGroupCommand}"
|
|
CommandParameter="{Binding Header}"
|
|
ToolTip.Tip="{Binding Header.CollapseLabel}">
|
|
<TextBlock Text="{Binding Header.Chevron}" FontSize="11"
|
|
Foreground="{StaticResource TextGhost}" />
|
|
</Button>
|
|
<TextBlock FontSize="12.5" FontWeight="Medium" Foreground="{StaticResource TextGhost}">
|
|
<Run Text="{Binding Header.Label}" /><Run Text=" · " /><Run Text="{Binding Header.Count}" />
|
|
</TextBlock>
|
|
<TextBlock Classes="mono" Text="{Binding Header.VaultBadge}" FontSize="10.5"
|
|
Foreground="{StaticResource TextFaint}" TextTrimming="CharacterEllipsis"
|
|
IsVisible="{Binding Header.HasVaultBadge}" />
|
|
</StackPanel>
|
|
|
|
<!--
|
|
Bound on every heading and shown on the first, rather than a control of the board's
|
|
own above the sections — a virtualised ItemsControl has no clean way for one realized
|
|
row to ask "am I first", so HostSectionViewModel's own header carries the answer. See
|
|
VaultViewModel.RebuildHostSections and CollapseAllLabel.
|
|
-->
|
|
<Button Grid.Column="2" Classes="flat" Padding="6,2" Margin="0,0,10,0"
|
|
FontSize="12" Foreground="{StaticResource TextGhost}"
|
|
IsVisible="{Binding Header.IsFirstBoardSection}"
|
|
Content="{Binding #Board.((vm:VaultViewModel)DataContext).CollapseAllLabel}"
|
|
Command="{Binding #Board.((vm:VaultViewModel)DataContext).ToggleAllGroupsCommand}" />
|
|
|
|
<!--
|
|
Nothing to edit, move or delete about "No group" — it names no item, so the menu is
|
|
left off that heading altogether rather than opened onto three commands that would
|
|
each resolve to nothing. See VaultViewModel.GroupOf.
|
|
-->
|
|
<Button Grid.Column="3" Classes="flat paneicon" Content="⋯" Width="24" Height="24"
|
|
IsVisible="{Binding Header.GroupId, Converter={x:Static ObjectConverters.IsNotNull}}"
|
|
ToolTip.Tip="Edit, move or delete this group">
|
|
<Button.Flyout>
|
|
<MenuFlyout>
|
|
<MenuItem Header="Edit…"
|
|
Command="{Binding #Board.((vm:VaultViewModel)DataContext).EditGroupFromHeadingCommand}"
|
|
CommandParameter="{Binding Header}" />
|
|
<MenuItem Header="Move to another vault…"
|
|
Command="{Binding #Board.((vm:VaultViewModel)DataContext).MoveGroupFromHeadingCommand}"
|
|
CommandParameter="{Binding Header}" />
|
|
<Separator />
|
|
<MenuItem Header="Delete…"
|
|
Command="{Binding #Board.((vm:VaultViewModel)DataContext).DeleteGroupFromHeadingCommand}"
|
|
CommandParameter="{Binding Header}" />
|
|
</MenuFlyout>
|
|
</Button.Flyout>
|
|
</Button>
|
|
</Grid>
|
|
|
|
<!--
|
|
============ THE CARDS ============
|
|
A ListBox of its own per section — Classes="sectioncards" is what the code-behind finds
|
|
every card container through, across however many of these are realized. See the type
|
|
remarks on HostSectionViewModel for why this is a second list per group rather than one
|
|
list for the whole board.
|
|
|
|
◆ SELECTED IS ONE-WAY, WRITTEN BY HAND ON THE WAY OUT. See HostsScreen's own remarks:
|
|
a two-way binding shared by several independently-rebuilt lists would have each list's
|
|
Reset racing to null the one property every filter keystroke and every sync pass.
|
|
-->
|
|
<ListBox Classes="sectioncards" ItemsSource="{Binding Hosts}"
|
|
SelectedItem="{Binding #Board.((vm:VaultViewModel)DataContext).SelectedSidebarRow, Mode=OneWay}"
|
|
ToolTip.Tip="Double-press a card for a shell. Ctrl or Shift picks out several, and so does a band dragged across the space between them; right-click any of them for what can be done to the set.">
|
|
|
|
<ListBox.ItemsPanel>
|
|
<ItemsPanelTemplate><WrapPanel /></ItemsPanelTemplate>
|
|
</ListBox.ItemsPanel>
|
|
|
|
<!--
|
|
◆ TWO MENUS IN ONE, AND WHICH OF THEM IS DRAWN IS WHETHER ANYTHING IS TICKED. See
|
|
HostsScreen.axaml.cs's OnContextRequested for the half of this rule that lives in code:
|
|
a right click outside the ticked set drops the set first, so these entries and the
|
|
ones above are never both meaningful at once.
|
|
-->
|
|
<ListBox.ContextMenu>
|
|
<ContextMenu>
|
|
<MenuItem Header="Connect" IsVisible="{Binding !#Board.((vm:VaultViewModel)DataContext).IsChoosingHosts}"
|
|
Command="{Binding #Board.((vm:VaultViewModel)DataContext).ConnectCommand}" />
|
|
<MenuItem Header="Details…" IsVisible="{Binding !#Board.((vm:VaultViewModel)DataContext).IsChoosingHosts}"
|
|
Command="{Binding #Board.((vm:VaultViewModel)DataContext).OpenHostPaneCommand}" />
|
|
<MenuItem Header="Edit…" IsVisible="{Binding !#Board.((vm:VaultViewModel)DataContext).IsChoosingHosts}"
|
|
Command="{Binding #Board.((vm:VaultViewModel)DataContext).EditSelectedHostCommand}" />
|
|
<Separator IsVisible="{Binding !#Board.((vm:VaultViewModel)DataContext).IsChoosingHosts}" />
|
|
<MenuItem Header="Delete…" IsVisible="{Binding !#Board.((vm:VaultViewModel)DataContext).IsChoosingHosts}"
|
|
Command="{Binding #Board.((vm:VaultViewModel)DataContext).DeleteHostCommand}" />
|
|
|
|
<MenuItem Header="Connect" IsVisible="{Binding #Board.((vm:VaultViewModel)DataContext).HasOneChosenHost}"
|
|
Command="{Binding #Board.((vm:VaultViewModel)DataContext).ConnectToChosenHostCommand}" />
|
|
<MenuItem Header="Browse files" IsVisible="{Binding #Board.((vm:VaultViewModel)DataContext).HasOneChosenHost}"
|
|
Command="{Binding #Board.((vm:VaultViewModel)DataContext).BrowseChosenHostCommand}" />
|
|
<MenuItem Header="Edit…" IsVisible="{Binding #Board.((vm:VaultViewModel)DataContext).HasOneChosenHost}"
|
|
Command="{Binding #Board.((vm:VaultViewModel)DataContext).EditChosenHostCommand}" />
|
|
<MenuItem Header="Change group…" IsVisible="{Binding #Board.((vm:VaultViewModel)DataContext).IsChoosingHosts}"
|
|
Command="{Binding #Board.((vm:VaultViewModel)DataContext).RegroupChosenHostsCommand}" />
|
|
<MenuItem Header="Move to another vault…" IsVisible="{Binding #Board.((vm:VaultViewModel)DataContext).IsChoosingHosts}"
|
|
Command="{Binding #Board.((vm:VaultViewModel)DataContext).MoveChosenHostsToVaultCommand}" />
|
|
<MenuItem Header="Copy to another vault…" IsVisible="{Binding #Board.((vm:VaultViewModel)DataContext).IsChoosingHosts}"
|
|
Command="{Binding #Board.((vm:VaultViewModel)DataContext).CopyChosenHostsToVaultCommand}" />
|
|
<MenuItem Header="Duplicate" IsVisible="{Binding #Board.((vm:VaultViewModel)DataContext).IsChoosingHosts}"
|
|
Command="{Binding #Board.((vm:VaultViewModel)DataContext).DuplicateChosenHostsCommand}" />
|
|
<Separator IsVisible="{Binding #Board.((vm:VaultViewModel)DataContext).IsChoosingHosts}" />
|
|
<MenuItem Header="Delete…" IsVisible="{Binding #Board.((vm:VaultViewModel)DataContext).IsChoosingHosts}"
|
|
Command="{Binding #Board.((vm:VaultViewModel)DataContext).DeleteChosenHostsCommand}" />
|
|
</ContextMenu>
|
|
</ListBox.ContextMenu>
|
|
|
|
<ListBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:HostRowViewModel">
|
|
|
|
<!--
|
|
The full address is on the tooltip and nowhere else on the card — the design's own
|
|
rule, kept: a card read while scanning a section is read for its name and whether
|
|
it is live, and an address is what somebody reads once they have found the machine,
|
|
which the tooltip and the drawer both still say.
|
|
|
|
MinHeight rather than a fixed Height, unlike the mock's own 120: this codebase's
|
|
Border.tile is deliberately a fixed width and a free height (see App.axaml), so a
|
|
card wearing four tags grows a second line rather than clipping the fourth one — a
|
|
silent truncation is exactly the kind of thing the honesty rule in
|
|
HANDOFF-hosts-v5.md exists to catch, and it costs nothing here to avoid.
|
|
-->
|
|
<Border Classes="tile" Classes.chosen="{Binding IsChosen}" MinHeight="120"
|
|
ToolTip.Tip="{Binding Address}">
|
|
<Panel>
|
|
<Grid RowDefinitions="Auto,*,Auto">
|
|
|
|
<Grid Grid.Row="0" ColumnDefinitions="Auto,*">
|
|
|
|
<!--
|
|
The monogram avatar: two letters, stable-hashed into one of the mock's four
|
|
hue pairs — see HostRowViewModel.Monogram/MonogramHue and
|
|
MonogramBrushConverter, which holds the four pairs themselves. Kept beside
|
|
this one screen rather than in the shared palette, because a monogram's
|
|
colour is a fact about this card and nothing else in the theme reads it.
|
|
-->
|
|
<Border Grid.Column="0" Width="32" Height="32" CornerRadius="9"
|
|
Background="{Binding MonogramHue, Converter={StaticResource MonogramBg}}">
|
|
<TextBlock Classes="mono" Text="{Binding Monogram}" FontWeight="Bold"
|
|
FontSize="12.5" HorizontalAlignment="Center"
|
|
VerticalAlignment="Center"
|
|
Foreground="{Binding MonogramHue, Converter={StaticResource MonogramFg}}" />
|
|
</Border>
|
|
|
|
<StackPanel Grid.Column="1" Margin="10,0,0,0" VerticalAlignment="Center"
|
|
Spacing="3">
|
|
|
|
<!--
|
|
A Grid rather than a horizontal StackPanel, on both of these rows — a
|
|
horizontal StackPanel measures its children with infinite width, so
|
|
TextTrimming never fires and the name would run on past the card's own
|
|
border instead of trimming. See the longer note this carried in v4.
|
|
-->
|
|
<Grid ColumnDefinitions="*,Auto,Auto">
|
|
<TextBlock Grid.Column="0" Text="{Binding Label}" FontSize="17"
|
|
FontWeight="Bold" Foreground="{StaticResource Text}"
|
|
TextTrimming="CharacterEllipsis" />
|
|
|
|
<!--
|
|
The tick sits beside the dot rather than in place of it, the same
|
|
arrangement the phone's row uses: the dot says whether a shell is open
|
|
on this machine, and swapping it for the tick would hide that fact the
|
|
moment a card is chosen.
|
|
-->
|
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4"
|
|
Margin="6,0,0,0" VerticalAlignment="Center">
|
|
<TextBlock Text="✓" FontSize="11" IsVisible="{Binding IsChosen}"
|
|
Foreground="{StaticResource AccentText}" />
|
|
<!--
|
|
Two states, deliberately: green means a terminal is open on this host
|
|
right now, grey means there is not one. Never amber — nothing here
|
|
pings anything, and a third colour would be a claim this application
|
|
does not check. See Ellipse.dot in App.axaml and
|
|
hosts-v5-design-spec.md's deviations.
|
|
-->
|
|
<Ellipse Classes="dot" Classes.live="{Binding IsConnected}"
|
|
Width="9" Height="9" />
|
|
</StackPanel>
|
|
|
|
<Border Grid.Column="2" 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 Classes="mono" Text="{Binding Address}" FontSize="12.5"
|
|
Foreground="{StaticResource TextGhost}"
|
|
TextTrimming="CharacterEllipsis" />
|
|
|
|
<!--
|
|
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 board spanning several vaults it is not decoration.
|
|
-->
|
|
<TextBlock Classes="mono" Text="{Binding VaultBadge}" FontSize="10"
|
|
Foreground="{StaticResource TextFaint}"
|
|
TextTrimming="CharacterEllipsis"
|
|
IsVisible="{Binding HasVaultBadge}" />
|
|
</StackPanel>
|
|
</Grid>
|
|
|
|
<Grid Grid.Row="2" ColumnDefinitions="*,Auto" VerticalAlignment="Bottom"
|
|
Margin="0,8,0,0">
|
|
|
|
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="4"
|
|
VerticalAlignment="Bottom">
|
|
|
|
<!--
|
|
The pin badge: how many folders are pinned on this host, reading
|
|
HostRowViewModel.PinCount — which is Host.PinnedPaths.Count, see that
|
|
property's own remarks. Only when there is at least one; a badge reading
|
|
zero would be a fact nobody asked for.
|
|
-->
|
|
<Border Classes="chip" Height="19" CornerRadius="5" Padding="6,2"
|
|
IsVisible="{Binding HasPins}">
|
|
<StackPanel Orientation="Horizontal" Spacing="3">
|
|
<TextBlock Text="" FontFamily="{StaticResource IconFont}"
|
|
FontSize="11" Foreground="{StaticResource TextFaint}" />
|
|
<TextBlock Classes="mono" Text="{Binding PinCount}" FontSize="10.5"
|
|
Foreground="{StaticResource TextFaint}" />
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!--
|
|
Tag chips, bottom-left. Templated over plain strings rather than a typed
|
|
row, so no x:DataType is declared on this inner template — TagLabels is
|
|
already resolved names, not ids, see HostRowViewModel.TagLabels.
|
|
-->
|
|
<ItemsControl ItemsSource="{Binding TagLabels}" IsVisible="{Binding HasTags}">
|
|
<ItemsControl.ItemsPanel>
|
|
<ItemsPanelTemplate><WrapPanel /></ItemsPanelTemplate>
|
|
</ItemsControl.ItemsPanel>
|
|
<ItemsControl.ItemTemplate>
|
|
<DataTemplate>
|
|
<Border Classes="chip" Height="21" CornerRadius="5" Padding="6,2"
|
|
Margin="0,0,4,4">
|
|
<TextBlock Text="{Binding}" FontSize="11"
|
|
Foreground="{StaticResource TextFaint}" />
|
|
</Border>
|
|
</DataTemplate>
|
|
</ItemsControl.ItemTemplate>
|
|
</ItemsControl>
|
|
</StackPanel>
|
|
|
|
<!--
|
|
HostRowViewModel.LastConnectedText is filled from the connection log on
|
|
the hosts screen's activation and on SessionEnded, and restrung on a
|
|
one-minute tick while this screen is visible — see that property's own
|
|
remarks and VaultViewModel.RefreshLastConnectedAsync. This reads it as-is
|
|
rather than formatting anything here, and draws nothing while it is empty
|
|
rather than a dash that would claim "never connected" — a host never seen
|
|
in the log and a host whose vault this has not read yet look the same from
|
|
here, and neither is a claim this card can make. A connected host prints
|
|
nothing too, per decision 4: the dot above already says "right now".
|
|
-->
|
|
<TextBlock Grid.Column="1" Text="{Binding LastConnectedText}" FontSize="12"
|
|
Foreground="{StaticResource TextGhost}" VerticalAlignment="Bottom"
|
|
IsVisible="{Binding LastConnectedText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
|
</Grid>
|
|
</Grid>
|
|
|
|
<!--
|
|
◆ THE PENCIL, AND THE ONLY WAY THE DRAWER OPENS FROM THIS BOARD. See the note
|
|
on this in v4's own markup, kept: selecting a card does not open it any more,
|
|
and this is where the asking is. Bound through #Board rather than the old
|
|
$parent[ListBox] — see the remark at the top of this file for why a ListBox one
|
|
level further in than the old grid changes what that path would resolve to.
|
|
-->
|
|
<Button Classes="flat rowedit" Content="✎"
|
|
HorizontalAlignment="Right" VerticalAlignment="Bottom"
|
|
Command="{Binding #Board.((vm:VaultViewModel)DataContext).OpenHostPaneCommand}"
|
|
CommandParameter="{Binding}"
|
|
ToolTip.Tip="Opens this host's pane beside the board" />
|
|
</Panel>
|
|
</Border>
|
|
</DataTemplate>
|
|
</ListBox.ItemTemplate>
|
|
</ListBox>
|
|
|
|
</StackPanel>
|
|
</DataTemplate>
|
|
</ItemsControl.ItemTemplate>
|
|
</ItemsControl>
|
|
|
|
<!--
|
|
Nothing to show, and the reasons it can happen are different questions — see
|
|
VaultViewModel.NoHostBoardMessage for the one answered here per reason.
|
|
-->
|
|
<TextBlock Classes="hint" FontSize="12" TextWrapping="Wrap" MaxWidth="480"
|
|
HorizontalAlignment="Left" Margin="0,4,0,0"
|
|
IsVisible="{Binding !HasHostBoardEntries}"
|
|
Text="{Binding NoHostBoardMessage}" />
|
|
|
|
</StackPanel>
|
|
</ScrollViewer>
|
|
|
|
<!--
|
|
◆ THE BAND, drawn over the scroller rather than inside it.
|
|
|
|
It is a rectangle the pointer is dragging out right now, so it belongs in the viewport's coordinates
|
|
and not in the scrolling content's: a band drawn inside the stack would slide away from the pointer
|
|
the moment the board scrolled under it. Same row as the ScrollViewer and after it, which is what puts
|
|
it on top — a Grid cell stacks its children in declaration order.
|
|
|
|
IsHitTestVisible="False" is the whole of why the gesture works while it is up. The band is under the
|
|
pointer by definition, and a rectangle that took the pointer would end the drag it is drawing.
|
|
|
|
Positioned by Margin from the top left rather than in a Canvas, because the two alignments below make
|
|
the margin mean exactly "where the corner is"; see HostsScreen.axaml.cs, which is the only thing that
|
|
writes to it.
|
|
-->
|
|
<Border Grid.Row="3" x:Name="Band" IsVisible="False" IsHitTestVisible="False"
|
|
HorizontalAlignment="Left" VerticalAlignment="Top"
|
|
Background="{StaticResource AccentWash}" BorderBrush="{StaticResource Accent}"
|
|
BorderThickness="1" CornerRadius="2" />
|
|
|
|
</Grid>
|
|
|
|
<!--
|
|
The drawer, and it takes its 320 pixels only while it has been opened — which is what gives the board
|
|
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.
|
|
-->
|
|
<Panel Grid.Column="1" IsVisible="{Binding IsDrawerOpen}">
|
|
<views:HostDrawer x:Name="Drawer" />
|
|
</Panel>
|
|
|
|
</Grid>
|
|
|
|
</UserControl>
|