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:
@@ -35,13 +35,40 @@
|
||||
wraps in the application and does not wrap in the harness makes every measured height too small, which
|
||||
is the one way this kind of test lies quietly.
|
||||
-->
|
||||
<!--
|
||||
◆ THE BASE SIZE, and it is set here rather than on the window.
|
||||
|
||||
Every explicit FontSize below is a step off a base nothing was stating: a bare TextBlock took
|
||||
TextElement's own default of 12, which meant the one number the whole scale is measured from lived in
|
||||
Avalonia rather than in this file. Raising the scale meant naming it.
|
||||
|
||||
Declared on Window *and* on UserControl, which is not redundancy. FontSize inherits, so the window
|
||||
alone would reach everything the application draws — but the layout harness hosts a UserControl in a
|
||||
plain `new Window()` it constructs itself, and that window has no style of this application's on it.
|
||||
Without the second selector the harness would measure every screen a point smaller than it ships,
|
||||
which is the failure mode App.axaml's own note at the top of this block was written about: a test that
|
||||
lays out text smaller than the application does reports heights that are all slightly too small, and
|
||||
it does it silently.
|
||||
|
||||
A selector on TextBlock would have been the obvious way and is wrong. Style setters beat inherited
|
||||
values in Avalonia, so `Selector="TextBlock"` would win over the size a Button sets on itself and
|
||||
reach the TextBlock inside that button's template — collapsing every deliberate step below back to one
|
||||
number.
|
||||
-->
|
||||
<Style Selector="Window">
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
</Style>
|
||||
<Style Selector="UserControl">
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock.hint">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.heading">
|
||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||
<Setter Property="FontSize" Value="18" />
|
||||
<Setter Property="FontSize" Value="19" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
</Style>
|
||||
<Style Selector="Border.card">
|
||||
@@ -74,7 +101,7 @@
|
||||
-->
|
||||
<Style Selector="TextBlock.label">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||
<Setter Property="FontSize" Value="10" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="LetterSpacing" Value="1.2" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||||
@@ -93,7 +120,7 @@
|
||||
</Style>
|
||||
<Style Selector="Border.chip > TextBlock">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||
<Setter Property="FontSize" Value="10" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="FontWeight" Value="Medium" />
|
||||
<Setter Property="LetterSpacing" Value="0.5" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||||
@@ -215,7 +242,7 @@
|
||||
<!-- Every button in this window is small, mono and tracked out; only the colours differ. -->
|
||||
<Style Selector="Button.ghost, Button.accent, Button.danger">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||
<Setter Property="FontSize" Value="10.5" />
|
||||
<Setter Property="FontSize" Value="11.5" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="LetterSpacing" Value="0.8" />
|
||||
<Setter Property="Padding" Value="10,5" />
|
||||
@@ -259,7 +286,7 @@
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||
<Setter Property="FontSize" Value="10" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="FontWeight" Value="Medium" />
|
||||
<Setter Property="LetterSpacing" Value="0.5" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||||
@@ -289,7 +316,7 @@
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="FontWeight" Value="Medium" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||||
</Style>
|
||||
@@ -315,7 +342,7 @@
|
||||
because a number that lit with its row would compete with the word beside it for the same emphasis.
|
||||
-->
|
||||
<Style Selector="TextBlock.navicon">
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="Width" Value="20" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
@@ -325,7 +352,7 @@
|
||||
</Style>
|
||||
<Style Selector="TextBlock.navcount">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||
<Setter Property="FontSize" Value="10" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
@@ -343,7 +370,7 @@
|
||||
<Setter Property="Margin" Value="0,0,6,0" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="FontWeight" Value="Medium" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||||
</Style>
|
||||
@@ -375,6 +402,19 @@
|
||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
One of the strip's three fixed tabs — Vaults, SFTP, S3. A pill in every respect except that it has no
|
||||
close box, so it takes its padding back on the right: the base rule is short there to leave room for
|
||||
the cross a terminal tab carries inside itself, and a fixed tab with the same asymmetry sits visibly
|
||||
off-centre beside one that has a reason for it.
|
||||
|
||||
Nothing else differs, deliberately. These are tabs and have to read as tabs — the whole point of the
|
||||
strip is that "where the window is" is one row of one kind of control.
|
||||
-->
|
||||
<Style Selector="Button.tab.fixed">
|
||||
<Setter Property="Padding" Value="12,0" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
The button that opens a connection. A tab in every respect but the marks a tab carries: no active
|
||||
state, because it is never the thing showing, and no outline, because it is not one of the things
|
||||
@@ -411,7 +451,7 @@
|
||||
<Style Selector="Button.choice">
|
||||
<Setter Property="Padding" Value="10,5" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||
<Setter Property="FontSize" Value="9.5" />
|
||||
<Setter Property="FontSize" Value="10.5" />
|
||||
<Setter Property="LetterSpacing" Value="0.5" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||||
</Style>
|
||||
@@ -461,9 +501,9 @@
|
||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||
<Setter Property="SelectionBrush" Value="{StaticResource AccentSoft}" />
|
||||
<Setter Property="CaretBrush" Value="{StaticResource Accent}" />
|
||||
<Setter Property="MinHeight" Value="28" />
|
||||
<Setter Property="MinHeight" Value="30" />
|
||||
<Setter Property="Padding" Value="8,4" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
</Style>
|
||||
<Style Selector="TextBox /template/ Border#PART_BorderElement">
|
||||
<Setter Property="Background" Value="{StaticResource Field}" />
|
||||
@@ -512,6 +552,76 @@
|
||||
<Setter Property="Background" Value="{StaticResource AccentWash}" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
◆ A LIST OF TILES, which is the hosts screen's grid of group and host cards.
|
||||
|
||||
The three rules above paint the *item* — a full-bleed rectangle behind whatever the template draws —
|
||||
and that is right for a row and wrong for a card: a square wash behind a 10-pixel-rounded tile shows
|
||||
as four grey corners, and it does it on hover as well as on selection, which is most of the time the
|
||||
pointer is anywhere near the grid. So a tiles list clears all three and the tile paints its own
|
||||
states.
|
||||
|
||||
Declared after them and not before. Avalonia has no specificity: two rules matching one element are
|
||||
settled by declaration order, so an exception stated above the rule it excepts does nothing at all.
|
||||
That trap is recorded twice more in this file, for Button.tab and for Border.rowmark.
|
||||
-->
|
||||
<Style Selector="ListBox.tiles > ListBoxItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</Style>
|
||||
<Style Selector="ListBox.tiles > ListBoxItem:selected /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</Style>
|
||||
<Style Selector="ListBox.tiles > ListBoxItem:selected:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
The card itself. A fixed width and a free height, which is the pair that makes a WrapPanel of these
|
||||
into a grid: equal columns, and a card that grew a third line of tags is taller than its neighbours
|
||||
rather than narrower.
|
||||
|
||||
◆ 232 IS DERIVED, and the arithmetic is written out because getting it wrong is invisible. The grid's
|
||||
column at the window's minimum is 1016 less the rail's 190 and the drawer's 304, which is 522. The
|
||||
scrolling stack inside it takes 16 of margin on each side, and the vertical scrollbar takes its own —
|
||||
call the usable width 490. A WrapPanel fits floor(490 / (Width + 10)) per row, so two columns needs
|
||||
Width no more than 235.
|
||||
|
||||
The first number here was 248, from the same reasoning with the two margins left out. It laid out
|
||||
cleanly and the layout harness passed it, because the harness asks whether a control is inside the
|
||||
window and not how many of them fit on a line — so the grid quietly became one column wide at exactly
|
||||
the size this application guarantees, which is the shape the cards exist to avoid.
|
||||
-->
|
||||
<Style Selector="Border.tile">
|
||||
<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,10" />
|
||||
<Setter Property="Width" Value="232" />
|
||||
<Setter Property="Margin" Value="0,0,10,10" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:pointerover Border.tile">
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderHover}" />
|
||||
</Style>
|
||||
<!--
|
||||
Selected is the accent outline *and* the filled surface, which is the same pair the nav rail and the
|
||||
tab strip use for "you are here". A tile marked by its border alone is legible on a card you are
|
||||
looking at and invisible in peripheral vision, which is where a selected card usually is once the
|
||||
drawer beside it has opened.
|
||||
-->
|
||||
<Style Selector="ListBoxItem:selected Border.tile">
|
||||
<Setter Property="Background" Value="{StaticResource Active}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Accent}" />
|
||||
</Style>
|
||||
|
||||
<!-- The square a tile carries on its left: a group's mark, or a host's prompt. -->
|
||||
<Style Selector="Border.tileicon">
|
||||
<Setter Property="Width" Value="32" />
|
||||
<Setter Property="Height" Value="32" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
The status dot, in one place rather than as a converter in code.
|
||||
|
||||
@@ -558,7 +668,7 @@
|
||||
|
||||
Set from the code-behind rather than by a binding, for the reason the whole gesture is code: the rows
|
||||
are rebuilt from scratch on every filter keystroke and every sync pass, so a flag on the view model
|
||||
would be state the list throws away halfway through the drag. See HostSidebar.axaml.cs.
|
||||
would be state the list throws away halfway through the drag. See HostsScreen.axaml.cs.
|
||||
-->
|
||||
<Style Selector="ListBoxItem.droptarget /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource AccentWash}" />
|
||||
@@ -577,7 +687,7 @@
|
||||
<Setter Property="CornerRadius" Value="4" />
|
||||
</Style>
|
||||
<Style Selector="MenuItem">
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||
</Style>
|
||||
|
||||
@@ -602,16 +712,16 @@
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||
<Setter Property="CornerRadius" Value="4" />
|
||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||
<Setter Property="MinHeight" Value="28" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="MinHeight" Value="30" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
</Style>
|
||||
<Style Selector="NumericUpDown">
|
||||
<Setter Property="Background" Value="{StaticResource Field}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||
<Setter Property="CornerRadius" Value="4" />
|
||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||
<Setter Property="MinHeight" Value="28" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="MinHeight" Value="30" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
</Style>
|
||||
<!--
|
||||
The foreground goes on the content presenter as well as on the control, for the same reason the
|
||||
@@ -621,7 +731,7 @@
|
||||
-->
|
||||
<Style Selector="CheckBox">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
</Style>
|
||||
<Style Selector="CheckBox /template/ ContentPresenter#PART_ContentPresenter">
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
<!--
|
||||
The question in front of deleting something in the vault.
|
||||
|
||||
One control used in two places — the host sidebar, where it takes the place of the row of buttons that
|
||||
opened it, and the vault screen's detail pane, where it takes the place of EDIT and DELETE. The two
|
||||
moments are different and what has to be said is not, which is why this is a shared control rather than
|
||||
two blocks that would drift apart. The sign-out confirmation is the same arrangement, for the same
|
||||
reason; see SignOutCard.
|
||||
One control used in three places — the hosts drawer, where it takes the place of the row of buttons that
|
||||
opened it; the hosts screen's GROUPS section, where it takes the place of that group's EDIT and DELETE;
|
||||
and the vault screen's detail pane. The three moments are different and what has to be said is not,
|
||||
which is why this is a shared control rather than three blocks that would drift apart. The sign-out
|
||||
confirmation is the same arrangement, for the same reason; see SignOutCard.
|
||||
|
||||
A bare StackPanel and not a card, because the two hosts frame it themselves: the sidebar puts it in the
|
||||
strip along its bottom edge, and the vault screen in a column that scrolls.
|
||||
A bare StackPanel and not a card, because all three hosts frame it themselves — each wraps it in its own
|
||||
DangerWash border in the place its buttons were.
|
||||
|
||||
Everything it says is something the view model can answer. The question names the item, the consequence
|
||||
knows whether this machine can push a tombstone yet, and the line in the box is a count of the hosts
|
||||
@@ -24,10 +24,10 @@
|
||||
|
||||
<StackPanel Spacing="8">
|
||||
|
||||
<TextBlock Classes="heading" FontSize="13" TextWrapping="Wrap"
|
||||
<TextBlock Classes="heading" FontSize="14" TextWrapping="Wrap"
|
||||
Text="{Binding PendingDeletion.Question}" />
|
||||
|
||||
<TextBlock Foreground="{StaticResource WarnText}" FontSize="11" TextWrapping="Wrap"
|
||||
<TextBlock Foreground="{StaticResource WarnText}" FontSize="12" TextWrapping="Wrap"
|
||||
Text="{Binding PendingDeletion.Consequence}" />
|
||||
|
||||
<!--
|
||||
@@ -37,7 +37,7 @@
|
||||
<Border Background="{StaticResource Panel}" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="1" CornerRadius="4" Padding="8,6"
|
||||
IsVisible="{Binding PendingDeletion.HasUsage, FallbackValue=False}">
|
||||
<TextBlock Foreground="{StaticResource Info}" FontSize="11" TextWrapping="Wrap"
|
||||
<TextBlock Foreground="{StaticResource Info}" FontSize="12" TextWrapping="Wrap"
|
||||
Text="{Binding PendingDeletion.Usage}" />
|
||||
</Border>
|
||||
|
||||
|
||||
@@ -6,8 +6,9 @@ namespace DodoSSH.Client.App.Views;
|
||||
/// The question in front of deleting a host, a key or a password.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Its data context is the <c>VaultViewModel</c>, in both of the places it is shown, so every binding in the
|
||||
/// markup is a property of the vault. See <see cref="HostSidebar"/> and <see cref="VaultScreen"/>.
|
||||
/// Its data context is the <c>VaultViewModel</c>, in all three of the places it is shown, so every binding
|
||||
/// in the markup is a property of the vault. See <see cref="HostDrawer"/>, <see cref="HostsScreen"/> and
|
||||
/// <see cref="VaultScreen"/>.
|
||||
/// </remarks>
|
||||
internal sealed partial class ConfirmDeleteCard : UserControl
|
||||
{
|
||||
|
||||
@@ -29,10 +29,10 @@
|
||||
Margin="24">
|
||||
|
||||
<StackPanel Spacing="6" HorizontalAlignment="Center">
|
||||
<TextBlock Classes="mono" Text="{Binding SelectedTab.Label}" FontSize="15" FontWeight="Medium"
|
||||
<TextBlock Classes="mono" Text="{Binding SelectedTab.Label}" FontSize="16" FontWeight="Medium"
|
||||
Foreground="{StaticResource Text}" HorizontalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Classes="mono" Text="{Binding SelectedTab.Address}" FontSize="10.5"
|
||||
<TextBlock Classes="mono" Text="{Binding SelectedTab.Address}" FontSize="11.5"
|
||||
Foreground="{StaticResource TextFaint}" HorizontalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
@@ -42,17 +42,17 @@
|
||||
the reason, in the palette's red, because it is the only place the reason will be after the user
|
||||
navigates away from the screen that started the connection.
|
||||
-->
|
||||
<TextBlock Classes="mono" Text="{Binding SelectedTab.Status}" FontSize="11"
|
||||
<TextBlock Classes="mono" Text="{Binding SelectedTab.Status}" FontSize="12"
|
||||
Foreground="{StaticResource Accent}" HorizontalAlignment="Center"
|
||||
TextWrapping="Wrap" TextAlignment="Center"
|
||||
IsVisible="{Binding SelectedTab.IsConnecting}" />
|
||||
|
||||
<SelectableTextBlock Text="{Binding SelectedTab.Status}" FontSize="12"
|
||||
<SelectableTextBlock Text="{Binding SelectedTab.Status}" FontSize="13"
|
||||
Foreground="{StaticResource Danger}" HorizontalAlignment="Center"
|
||||
TextWrapping="Wrap" TextAlignment="Center"
|
||||
IsVisible="{Binding SelectedTab.IsFailed}" />
|
||||
|
||||
<TextBlock Classes="hint" FontSize="10.5" TextAlignment="Center" HorizontalAlignment="Center"
|
||||
<TextBlock Classes="hint" FontSize="11.5" TextAlignment="Center" HorizontalAlignment="Center"
|
||||
Text="Nothing else is waiting for this. Every other screen still works, and the strip above says how this one is getting on."
|
||||
IsVisible="{Binding SelectedTab.IsConnecting}" />
|
||||
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
<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.HostDrawer"
|
||||
x:DataType="vm:VaultViewModel">
|
||||
|
||||
<!--
|
||||
Everything about one thing: what the selected host is, the editor for it, or the editor for a group.
|
||||
|
||||
── THIS WAS HostSidebar, AND THE HOST LIST IS NOT IN IT ANY MORE. ───────────────────────────────────
|
||||
It used to be a 268-pixel column on the left holding a filter box, the list of every host, and the
|
||||
editor underneath. That column was doing two jobs at two-thirds size — choosing among forty machines,
|
||||
and editing one of them — and it did the first one badly enough that the second was the only reason to
|
||||
keep it narrow. The list is now a grid of cards filling the screen, so this control kept the half that
|
||||
is about one host and moved to the right, which is where the thing you selected belongs.
|
||||
|
||||
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.
|
||||
|
||||
── 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
|
||||
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.
|
||||
|
||||
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.
|
||||
-->
|
||||
|
||||
<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.
|
||||
|
||||
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 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>
|
||||
|
||||
<!-- ============ 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>
|
||||
|
||||
<SelectableTextBlock Classes="mono" Text="{Binding SelectedHost.Address}" FontSize="12"
|
||||
Foreground="{StaticResource TextDim}" TextWrapping="Wrap" />
|
||||
|
||||
<!--
|
||||
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}" />
|
||||
|
||||
<!--
|
||||
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>
|
||||
|
||||
<SelectableTextBlock Text="{Binding SelectedHost.Host.Notes}" FontSize="12"
|
||||
Foreground="{StaticResource TextDim}" TextWrapping="Wrap"
|
||||
IsVisible="{Binding SelectedHost.Host.Notes,
|
||||
Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
|
||||
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="0,2" />
|
||||
|
||||
<!--
|
||||
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.
|
||||
|
||||
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.
|
||||
-->
|
||||
<TextBox Text="{Binding ConnectPassword}" PlaceholderText="password (not stored)"
|
||||
PasswordChar="•" HorizontalAlignment="Stretch"
|
||||
IsVisible="{Binding 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." />
|
||||
|
||||
<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>
|
||||
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<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">
|
||||
<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">
|
||||
<Button Classes="accent" Content="{Binding GroupSaveLabel}"
|
||||
Command="{Binding SaveGroupCommand}" />
|
||||
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelGroupEditCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</Panel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,23 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace DodoSSH.Client.App.Views;
|
||||
|
||||
/// <summary>
|
||||
/// The column beside the hosts grid: what the selected host is, or one of the two editors.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This was <c>HostSidebar</c>, which held the host list as well and sat on the left. The list is a grid of
|
||||
/// cards on <see cref="HostsScreen"/> now, so everything that made that control need code — the double-tap
|
||||
/// that connects, the right click that moves the selection before the menu opens, and the drag that files a
|
||||
/// host into a group — went with the list. What is left is markup, which is why this class is empty.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Its data context is the <c>VaultViewModel</c>, so every binding in the markup is a property of the vault
|
||||
/// rather than of the shell. <see cref="HostsScreen"/> hands it over.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed partial class HostDrawer : UserControl
|
||||
{
|
||||
public HostDrawer() => InitializeComponent();
|
||||
}
|
||||
@@ -1,366 +0,0 @@
|
||||
<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.HostSidebar"
|
||||
x:DataType="vm:VaultViewModel">
|
||||
|
||||
<!--
|
||||
The host list, and the editor for whichever host is open.
|
||||
|
||||
This was the top half of VaultColumn. The design gives hosts their own column beside the terminal and
|
||||
puts everything else in the vault screen, which is the split this control and VaultScreen are — and it
|
||||
is a better split than the one it replaces: the hosts list is the thing you look at while you work, and
|
||||
the keys and passwords behind it are the thing you go and manage.
|
||||
|
||||
Its data context is the VaultViewModel, so every binding here is a property of the vault. The shell
|
||||
hands it over; see MainWindow.
|
||||
|
||||
The editor stays in this column rather than moving into the terminal's half of the window, and that is
|
||||
an occlusion constraint rather than a preference: the terminal's WebView is a native child window that
|
||||
composites above anything Avalonia draws in the same rectangle, so a form laid over there would render
|
||||
underneath it with its buttons unclickable. It is also why this control is measurable at all — no part
|
||||
of it is the WebView, so the layout harness can lay it out headlessly.
|
||||
-->
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto,Auto" Background="{StaticResource Sidebar}">
|
||||
|
||||
<!--
|
||||
The filter. It narrows this list 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.
|
||||
-->
|
||||
<Border Grid.Row="0" Padding="10,8" BorderBrush="{StaticResource BorderSubtle}" BorderThickness="0,0,0,1">
|
||||
<TextBox x:Name="HostFilter" Text="{Binding HostFilter}" PlaceholderText="filter hosts" FontSize="11"
|
||||
FontFamily="{StaticResource MonoFont}" MinHeight="26" Padding="8,3" />
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
One heading, which names the vault while there is one and says ALL VAULTS once a team's is readable
|
||||
too — a heading that went on naming the personal vault over a list containing a team's hosts would be
|
||||
a quiet lie, so the rows carry the vault name instead. The chevron folds the list away; the count is
|
||||
the collection's own, so it follows the filter without a second number to keep in step.
|
||||
-->
|
||||
<Button Grid.Row="1" Classes="flat grouphead" Command="{Binding ToggleHostsCommand}"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch">
|
||||
<Grid ColumnDefinitions="Auto,Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="▾" Foreground="{StaticResource TextFaint}" FontSize="8"
|
||||
VerticalAlignment="Center" Margin="0,0,6,0"
|
||||
IsVisible="{Binding AreHostsExpanded}" />
|
||||
<TextBlock Grid.Column="0" Text="▸" Foreground="{StaticResource TextFaint}" FontSize="8"
|
||||
VerticalAlignment="Center" Margin="0,0,6,0"
|
||||
IsVisible="{Binding !AreHostsExpanded}" />
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="{Binding HostsHeading}"
|
||||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding VisibleHosts.Count}" FontSize="10"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<!--
|
||||
Named because it is where keyboard focus lands when the user leaves the terminal.
|
||||
|
||||
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.
|
||||
-->
|
||||
<ListBox Grid.Row="2" x:Name="HostList" Focusable="True"
|
||||
DragDrop.AllowDrop="True"
|
||||
IsVisible="{Binding AreHostsExpanded}"
|
||||
ItemsSource="{Binding SidebarRows}"
|
||||
SelectedItem="{Binding SelectedSidebarRow}">
|
||||
|
||||
<!--
|
||||
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
|
||||
row: a ContextMenu inside a DataTemplate would have the row for its data context, and the commands
|
||||
here are the vault's — the same three the buttons at the foot of this column run. The code-behind
|
||||
selects whatever was right-clicked before the menu opens, so the selection-based commands act on the
|
||||
row under the pointer, and cancels the menu outright over a group heading, which has no host to act
|
||||
on. See HostSidebar.axaml.cs.
|
||||
|
||||
The same commands as the buttons, deliberately: a second path to deleting a host would be a second
|
||||
place for the confirmation to be forgotten.
|
||||
-->
|
||||
<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>
|
||||
|
||||
<!--
|
||||
Two kinds of row in one list, chosen by type. It has to be one ListBox: it owns the selection and it
|
||||
is where keyboard focus lands when the terminal gives it back, neither of which survives a list per
|
||||
group. A vault with no groups produces no heading rows at all, so this is the list it always was.
|
||||
|
||||
The heading is a row rather than a container, which means the control will happily select it. That is
|
||||
turned back into the previous host selection in the view model — see SelectedSidebarRow — because
|
||||
CONNECT, EDIT and DELETE all act on a host and a highlighted heading is not one.
|
||||
-->
|
||||
<ListBox.DataTemplates>
|
||||
|
||||
<DataTemplate DataType="vm:SidebarGroupHeader">
|
||||
<Button Classes="flat grouphead" Command="{Binding $parent[ListBox].((vm:VaultViewModel)DataContext).ToggleGroupCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Chevron}" Foreground="{StaticResource TextFaint}"
|
||||
FontSize="8" 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="10"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="vm:HostRowViewModel">
|
||||
<Grid ColumnDefinitions="Auto,Auto,*" Margin="0,5,10,5">
|
||||
|
||||
<!-- The accent strip a selected row carries; see the style in App.axaml. -->
|
||||
<Border Grid.Column="0" Classes="rowmark" />
|
||||
|
||||
<!--
|
||||
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="1" Classes="dot" Classes.live="{Binding IsConnected}"
|
||||
Margin="8,5,8,0" VerticalAlignment="Top" />
|
||||
|
||||
<StackPanel Grid.Column="2" Spacing="1">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Classes="mono" Text="{Binding Label}" Foreground="{StaticResource Text}"
|
||||
FontSize="11.5" FontWeight="Medium"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<Border Classes="chip warn" Padding="4,0"
|
||||
IsVisible="{Binding Badge, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<TextBlock Text="{Binding Badge}" FontSize="8.5" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Classes="mono" Text="{Binding Address}" FontSize="9.5"
|
||||
Foreground="{StaticResource TextFaint}" TextTrimming="CharacterEllipsis" />
|
||||
<!--
|
||||
Which of the three ways this host authenticates. In the list because only one of them wants
|
||||
the password box filled in, and an empty box on a key-authenticated host is otherwise
|
||||
indistinguishable from one somebody forgot to fill in.
|
||||
-->
|
||||
<TextBlock Classes="mono" Text="{Binding Authentication}" FontSize="9.5"
|
||||
Foreground="{StaticResource TextFaint}" />
|
||||
<!--
|
||||
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 list that spans
|
||||
several vaults it is not decoration.
|
||||
-->
|
||||
<TextBlock Classes="mono" Text="{Binding VaultBadge}" FontSize="9.5"
|
||||
Foreground="{StaticResource TextFaint}"
|
||||
IsVisible="{Binding HasVaultBadge}" />
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
The tags this host wears. A third line rather than more on the second, because a host can
|
||||
wear several and the second line's three facts are fixed-width where these are not — one
|
||||
long tag would push the vault badge off the end of a 268-pixel column.
|
||||
|
||||
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}"
|
||||
Margin="0,1,0,0">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><WrapPanel /></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<Border Classes="chip" Padding="5,0" Margin="0,0,4,0">
|
||||
<TextBlock Text="{Binding}" FontSize="8.5" />
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
</ListBox.DataTemplates>
|
||||
</ListBox>
|
||||
|
||||
<!--
|
||||
The editor doubles as the "add" form; there is no separate dialog.
|
||||
|
||||
◆ It scrolls, and it did not until the tag picker arrived. The reason is that a picker's height is not
|
||||
a property of the design: it is a chip per tag in the keychain, wrapped, so somebody with fifteen tags
|
||||
has an editor half again as tall as somebody with three. No fixed height holds that, and trimming
|
||||
other fields to buy room only moves the failure to whoever has sixteen.
|
||||
|
||||
The cost is real and worth naming. The layout harness skips any control 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.
|
||||
The MaxHeight is stated rather than left to the row for the same reason: it is what keeps the pane
|
||||
itself inside the window, which is the part the harness still watches. 300 is what the minimum window
|
||||
affords once the filter, the heading and the button strip have taken theirs, with enough left that the
|
||||
host list above does not vanish — the point of a column this narrow is that the list stays visible
|
||||
while a host in it is edited.
|
||||
-->
|
||||
<Border Grid.Row="3" Padding="10" Background="{StaticResource Chrome}"
|
||||
BorderBrush="{StaticResource BorderSubtle}" BorderThickness="0,1,0,0"
|
||||
IsVisible="{Binding IsEditing}">
|
||||
<ScrollViewer MaxHeight="300" HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Spacing="6">
|
||||
<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="48" 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="10"
|
||||
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 row two inches above — 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="9.5" />
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Grid ColumnDefinitions="*,6,Auto">
|
||||
<TextBox Grid.Column="0" Text="{Binding EditorNewTag}" PlaceholderText="new tag"
|
||||
FontSize="11" MinHeight="26" Padding="8,3">
|
||||
<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="10"
|
||||
Text="The relay stores this host's address on the server in plain text. Everything else stays encrypted." />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<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>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="4" Padding="10,8" BorderBrush="{StaticResource BorderSubtle}" BorderThickness="0,1,0,0"
|
||||
IsVisible="{Binding ShowsHostActions}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="ghost" Content="+ NEW HOST" Command="{Binding NewHostCommand}" />
|
||||
<Button Classes="ghost" Content="EDIT" Command="{Binding EditSelectedHostCommand}" />
|
||||
<Button Classes="ghost" Content="DELETE" Command="{Binding DeleteHostCommand}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
The question DELETE asks, in the place the buttons were rather than under them. This strip is at the
|
||||
bottom edge of a column whose middle is a list that has already taken every spare pixel, so a second
|
||||
block below the first would push its own buttons off the window — the same reasoning that swaps the
|
||||
unlock card for the sign-out card rather than stacking them. Swapping also means DELETE cannot be
|
||||
pressed again while its own question is up; see VaultViewModel.ShowsHostActions.
|
||||
-->
|
||||
<Border Grid.Row="4" Padding="10,8" Background="{StaticResource DangerWash}"
|
||||
BorderBrush="{StaticResource BorderSubtle}" BorderThickness="0,1,0,0"
|
||||
IsVisible="{Binding IsConfirmingHostDeletion}">
|
||||
<views:ConfirmDeleteCard />
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
@@ -1,340 +0,0 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.VisualTree;
|
||||
using DodoSSH.Client.Shell.ViewModels;
|
||||
|
||||
namespace DodoSSH.Client.App.Views;
|
||||
|
||||
/// <summary>
|
||||
/// The host list beside the terminal, and the editor for whichever host is open.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Its data context is the <c>VaultViewModel</c>, so every binding in the markup is a property of the vault
|
||||
/// rather than of the shell. The shell hands it over; see <see cref="MainWindow"/>.
|
||||
/// </remarks>
|
||||
internal sealed partial class HostSidebar : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// How a host travels from the row it was picked up on to the heading it is dropped on.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An in-process format carrying the row itself, rather than text carrying an id. The drag never leaves
|
||||
/// this window — there is nothing outside it that could accept a host — and the row is what the drop
|
||||
/// needs: it knows which vault the edit has to return to, which an id on its own does not.
|
||||
/// </remarks>
|
||||
private static readonly DataFormat<HostRowViewModel> HostFormat =
|
||||
DataFormat.CreateInProcessFormat<HostRowViewModel>("dodossh-host-row");
|
||||
|
||||
/// <summary>How far the pointer has to travel before a press becomes a drag.</summary>
|
||||
/// <remarks>
|
||||
/// A threshold, because a press on this list is nearly always a click: selecting a host, or the first
|
||||
/// half of the double-click that connects. Starting a drag on the press itself would turn every one of
|
||||
/// those into a drag gesture the user never asked for.
|
||||
/// </remarks>
|
||||
private const double DragThreshold = 5;
|
||||
|
||||
/// <summary>The press a drag would start from, or null once it has become one or been let go of.</summary>
|
||||
/// <remarks>
|
||||
/// Held because <see cref="DragDrop.DoDragDropAsync"/> takes the press rather than the movement: the
|
||||
/// gesture belongs to the pointer that went down, and the platform needs that event to hand the drag
|
||||
/// over to the operating system.
|
||||
/// </remarks>
|
||||
private PointerPressedEventArgs? press;
|
||||
|
||||
private HostRowViewModel? pickedUp;
|
||||
|
||||
private Point origin;
|
||||
|
||||
/// <summary>The row the pointer is currently over, while a drag is in flight.</summary>
|
||||
private ListBoxItem? marked;
|
||||
|
||||
public HostSidebar()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// Wired here rather than in the markup because it is a gesture rather than a binding, which is how
|
||||
// the transfers screen opens a directory too. Double-clicking a machine to get a shell on it is what
|
||||
// every other client of this kind does, and the CONNECT button stays: it is the one that has the
|
||||
// password box beside it, and a host that asks for a password still needs it typed first.
|
||||
HostList.DoubleTapped += OnHostActivated;
|
||||
|
||||
// Tunnelled, so the row under the pointer is read before the ListBox has answered the press itself.
|
||||
// Bubbling would work for the drag but not for the menu: by then the control has already decided
|
||||
// what is selected, and the menu is about to open against it.
|
||||
HostList.AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel);
|
||||
HostList.AddHandler(ContextRequestedEvent, OnContextRequested, RoutingStrategies.Tunnel);
|
||||
|
||||
HostList.PointerMoved += OnPointerMoved;
|
||||
HostList.PointerReleased += OnPointerReleased;
|
||||
HostList.PointerCaptureLost += OnPointerCaptureLost;
|
||||
|
||||
DragDrop.AddDragOverHandler(HostList, OnDragOver);
|
||||
DragDrop.AddDragLeaveHandler(HostList, OnDragLeave);
|
||||
DragDrop.AddDropHandler(HostList, OnDrop);
|
||||
}
|
||||
|
||||
private VaultViewModel? Vault => DataContext as VaultViewModel;
|
||||
|
||||
/// <remarks>
|
||||
/// Fire-and-forget, as the transfers screen's is: the command reports its own failures onto the status
|
||||
/// line — an unknown host key, a refused password — and awaiting it here would mean an event handler
|
||||
/// returning a task nothing observes.
|
||||
/// </remarks>
|
||||
private void OnHostActivated(object? sender, TappedEventArgs e)
|
||||
{
|
||||
if (Vault is { } vault)
|
||||
{
|
||||
_ = vault.ConnectCommand.ExecuteAsync(null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Points the menu at whatever was right-clicked.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The menu's three commands all read the vault's host selection, and a right click does not move it —
|
||||
/// which would mean a menu that quietly acted on whichever host happened to be selected instead of the
|
||||
/// one under the pointer. Deleting the wrong machine is the version of that mistake worth designing
|
||||
/// against.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Cancelled outright over a group heading and over the empty space below the rows. Neither is a host,
|
||||
/// and a menu offering Connect, Edit and Delete over one would be three buttons that either do nothing or
|
||||
/// act on something else entirely.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private void OnContextRequested(object? sender, ContextRequestedEventArgs e)
|
||||
{
|
||||
if (Vault is not { } vault || RowUnder(e.Source) is not HostRowViewModel row)
|
||||
{
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
vault.SelectedSidebarRow = row;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Remembered rather than acted on. Whether this press is a click or the start of a drag is not known
|
||||
/// until the pointer moves, so this is the point at which both are still possible.
|
||||
/// </remarks>
|
||||
private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
press = null;
|
||||
pickedUp = null;
|
||||
|
||||
if (!e.GetCurrentPoint(HostList).Properties.IsLeftButtonPressed
|
||||
|| RowUnder(e.Source) is not HostRowViewModel row)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
press = e;
|
||||
pickedUp = row;
|
||||
origin = e.GetPosition(HostList);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The drag is started from the remembered press once the pointer has travelled far enough — see
|
||||
/// <see cref="DragThreshold"/>. Fire-and-forget, because the drag loop runs for as long as the user holds
|
||||
/// the button and an event handler cannot wait on that; what happens after it is only clearing the mark.
|
||||
/// </remarks>
|
||||
private void OnPointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (press is not { } pressed || pickedUp is not { } row)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e.GetCurrentPoint(HostList).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
Forget();
|
||||
return;
|
||||
}
|
||||
|
||||
var moved = e.GetPosition(HostList) - origin;
|
||||
|
||||
if (Math.Abs(moved.X) < DragThreshold && Math.Abs(moved.Y) < DragThreshold)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Forget();
|
||||
|
||||
_ = DragAsync(pressed, row);
|
||||
}
|
||||
|
||||
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e) => Forget();
|
||||
|
||||
private void OnPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e) => Forget();
|
||||
|
||||
/// <summary>Carries one host row for as long as the user holds it.</summary>
|
||||
private async Task DragAsync(PointerPressedEventArgs pressed, HostRowViewModel row)
|
||||
{
|
||||
var carried = new DataTransfer();
|
||||
carried.Add(DataTransferItem.Create(HostFormat, row));
|
||||
|
||||
try
|
||||
{
|
||||
// ConfigureAwait(true): what follows touches the list's own containers, and those are the UI
|
||||
// thread's.
|
||||
await DragDrop
|
||||
.DoDragDropAsync(pressed, carried, DragDropEffects.Move)
|
||||
.ConfigureAwait(true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Whatever the drop did or did not do. A mark left behind would be a row that looks like a
|
||||
// target for a drag that ended somewhere else entirely.
|
||||
Unmark();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Says whether the row under the pointer would take this host, and marks it if it would.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A host over its own group's heading is refused, which is not pedantry: <c>DragDropEffects.None</c> is
|
||||
/// what turns the cursor into the "no" one, and a drag that looks like it would do something and then
|
||||
/// does nothing is worse than one that says so while it is still in the air.
|
||||
/// </remarks>
|
||||
private void OnDragOver(object? sender, DragEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
|
||||
if (Target(e) is not { } target)
|
||||
{
|
||||
e.DragEffects = DragDropEffects.None;
|
||||
Unmark();
|
||||
return;
|
||||
}
|
||||
|
||||
e.DragEffects = DragDropEffects.Move;
|
||||
Mark(target.Container);
|
||||
}
|
||||
|
||||
private void OnDragLeave(object? sender, DragEventArgs e) => Unmark();
|
||||
|
||||
/// <remarks>
|
||||
/// Fire-and-forget, like every other command this control runs: the move writes to the vault and reports
|
||||
/// itself onto the status line, and a drop handler that awaited it would be an event handler returning a
|
||||
/// task nothing observes.
|
||||
/// </remarks>
|
||||
private void OnDrop(object? sender, DragEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
Unmark();
|
||||
|
||||
if (Vault is not { } vault || Target(e) is not { } target)
|
||||
{
|
||||
e.DragEffects = DragDropEffects.None;
|
||||
return;
|
||||
}
|
||||
|
||||
e.DragEffects = DragDropEffects.Move;
|
||||
vault.MoveHostToGroupCommand.Execute(new HostGroupMove(target.Host, target.GroupId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Where a drag currently is, or null if it is over nothing that would take it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// A heading is the obvious target and a host is the useful one: dropping onto a machine files the
|
||||
/// dragged host beside it, which means the whole band of rows under a heading is a target rather than
|
||||
/// one seven-pixel line of text. The ungrouped heading is a target like any other, and it is how a host
|
||||
/// is taken out of a group without opening the editor.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// A group the vault no longer has is read as no group at all, which is what the list already does with
|
||||
/// a dangling reference — see <c>VaultViewModel.RebuildSidebarRows</c>. That is decided in the command
|
||||
/// rather than here, so the rule has one home.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private static DropTarget? Target(DragEventArgs e)
|
||||
{
|
||||
if (e.DataTransfer.TryGetValue(HostFormat) is not { } dragged
|
||||
|| Container(e.Source) is not { } container)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Guid? group = container.DataContext switch
|
||||
{
|
||||
SidebarGroupHeader header => header.GroupId,
|
||||
HostRowViewModel row => row.Host.GroupId,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (container.DataContext is not (SidebarGroupHeader or HostRowViewModel)
|
||||
|| dragged.Host.GroupId == group)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DropTarget(dragged, group, container);
|
||||
}
|
||||
|
||||
private void Mark(ListBoxItem container)
|
||||
{
|
||||
if (ReferenceEquals(marked, container))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Unmark();
|
||||
|
||||
marked = container;
|
||||
marked.Classes.Add("droptarget");
|
||||
}
|
||||
|
||||
private void Unmark()
|
||||
{
|
||||
marked?.Classes.Remove("droptarget");
|
||||
marked = null;
|
||||
}
|
||||
|
||||
/// <summary>Lets go of a press that turned out not to be a drag, or has become one.</summary>
|
||||
private void Forget()
|
||||
{
|
||||
press = null;
|
||||
pickedUp = null;
|
||||
}
|
||||
|
||||
/// <summary>The view model of the list row an event happened on, if it happened on one.</summary>
|
||||
private static object? RowUnder(object? source) => Container(source)?.DataContext;
|
||||
|
||||
/// <remarks>
|
||||
/// Walks up from whatever was actually hit — a text block, a border, the row's own grid — because that is
|
||||
/// what an event's source is. Anything not inside a row, which is the empty space below the last one,
|
||||
/// yields null.
|
||||
/// </remarks>
|
||||
private static ListBoxItem? Container(object? source) => source is Visual visual
|
||||
? visual.FindAncestorOfType<ListBoxItem>(includeSelf: true)
|
||||
: null;
|
||||
|
||||
/// <summary>A drag in flight, and where it would land.</summary>
|
||||
private sealed record DropTarget(HostRowViewModel Host, Guid? GroupId, ListBoxItem Container);
|
||||
|
||||
/// <summary>
|
||||
/// Where the keyboard should land when the terminal hands it back.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Exposed as a property rather than left for the window to find by name, because the name is inside
|
||||
/// this control's template and the window cannot see it.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// It has to be a control that is on screen. <c>Focus()</c> on a collapsed control is measurably a no-op
|
||||
/// and is not replayed when the control is revealed, so handing the keyboard to a folded-away list would
|
||||
/// swallow it: the terminal would let go and nothing would take it. Folding the host list is the one way
|
||||
/// a user can put this control into that state, so that is the case this answer has to cover — the
|
||||
/// filter box is always there, and it is a perfectly good place for a keyboard to arrive.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal IInputElement KeyboardTarget =>
|
||||
HostList.IsEffectivelyVisible ? HostList : HostFilter;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -1,27 +1,357 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.VisualTree;
|
||||
using DodoSSH.Client.Shell.ViewModels;
|
||||
|
||||
namespace DodoSSH.Client.App.Views;
|
||||
|
||||
/// <summary>
|
||||
/// The hosts screen: the host list, and an overview of the one that is selected.
|
||||
/// The grid of groups and hosts, and everything on it that is a gesture rather than a binding.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Its data context is the shell rather than the vault, unlike <see cref="HostSidebar"/> and
|
||||
/// <see cref="VaultScreen"/>. The sidebar is handed the vault from inside the markup; everything else here
|
||||
/// reaches it through <c>Vault.*</c>. That split is not tidiness — this element's visibility is the shell's
|
||||
/// business and the sidebar's bindings are the vault's, and an element carrying both resolves the first
|
||||
/// against the second, where it does not exist.
|
||||
/// <para>
|
||||
/// All of this was <c>HostSidebar</c>'s until the host list became a grid of cards. It moved with the list
|
||||
/// rather than staying with the editor: every handler here is about the thing that was clicked, dragged or
|
||||
/// right-clicked, and the drawer beside the grid has none of those. See <see cref="HostDrawer"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Its data context is the <c>VaultViewModel</c>, as <see cref="VaultScreen"/>'s is, so every binding in the
|
||||
/// markup is a property of the vault. The window hands it over; see <see cref="MainWindow"/>. The drawer
|
||||
/// beside the grid inherits the same one.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed partial class HostsScreen : UserControl
|
||||
{
|
||||
public HostsScreen() => InitializeComponent();
|
||||
|
||||
/// <summary>Where the keyboard lands when this screen is the one showing.</summary>
|
||||
/// <summary>
|
||||
/// How a host travels from the card it was picked up on to the heading it is dropped on.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Forwarded to the sidebar, which answers for itself: the host list can be folded away, and
|
||||
/// <c>Focus()</c> on a collapsed control is measurably a no-op that is not replayed when the control is
|
||||
/// revealed. Nothing in the right column can take the keyboard — it is a heading and three sentences.
|
||||
/// An in-process format carrying the row itself, rather than text carrying an id. The drag never leaves
|
||||
/// this window — there is nothing outside it that could accept a host — and the row is what the drop
|
||||
/// needs: it knows which vault the edit has to return to, which an id on its own does not.
|
||||
/// </remarks>
|
||||
internal IInputElement KeyboardTarget => Sidebar.KeyboardTarget;
|
||||
private static readonly DataFormat<HostRowViewModel> HostFormat =
|
||||
DataFormat.CreateInProcessFormat<HostRowViewModel>("dodossh-host-row");
|
||||
|
||||
/// <summary>How far the pointer has to travel before a press becomes a drag.</summary>
|
||||
/// <remarks>
|
||||
/// A threshold, because a press on this grid is nearly always a click: selecting a host, or the first
|
||||
/// half of the double-click that connects. Starting a drag on the press itself would turn every one of
|
||||
/// those into a drag gesture the user never asked for.
|
||||
/// </remarks>
|
||||
private const double DragThreshold = 5;
|
||||
|
||||
/// <summary>The press a drag would start from, or null once it has become one or been let go of.</summary>
|
||||
/// <remarks>
|
||||
/// Held because <see cref="DragDrop.DoDragDropAsync"/> takes the press rather than the movement: the
|
||||
/// gesture belongs to the pointer that went down, and the platform needs that event to hand the drag
|
||||
/// over to the operating system.
|
||||
/// </remarks>
|
||||
private PointerPressedEventArgs? press;
|
||||
|
||||
private HostRowViewModel? pickedUp;
|
||||
|
||||
private Point origin;
|
||||
|
||||
/// <summary>The card or heading the pointer is currently over, while a drag is in flight.</summary>
|
||||
private ListBoxItem? marked;
|
||||
|
||||
public HostsScreen()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// Wired here rather than in the markup because it is a gesture rather than a binding, which is how
|
||||
// the transfers screen opens a directory too. Double-clicking a machine to get a shell on it is what
|
||||
// every other client of this kind does, and CONNECT stays: it is the one in the drawer with the
|
||||
// password box above it, and a host that asks for a password still needs it typed first.
|
||||
HostGrid.DoubleTapped += OnHostActivated;
|
||||
|
||||
// Tunnelled, so the card under the pointer is read before the ListBox has answered the press itself.
|
||||
// Bubbling would work for the drag but not for the menu: by then the control has already decided
|
||||
// what is selected, and the menu is about to open against it.
|
||||
HostGrid.AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel);
|
||||
HostGrid.AddHandler(ContextRequestedEvent, OnContextRequested, RoutingStrategies.Tunnel);
|
||||
|
||||
HostGrid.PointerMoved += OnPointerMoved;
|
||||
HostGrid.PointerReleased += OnPointerReleased;
|
||||
HostGrid.PointerCaptureLost += OnPointerCaptureLost;
|
||||
|
||||
DragDrop.AddDragOverHandler(HostGrid, OnDragOver);
|
||||
DragDrop.AddDragLeaveHandler(HostGrid, OnDragLeave);
|
||||
DragDrop.AddDropHandler(HostGrid, OnDrop);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Null before the window has handed one over, and while the previewer is showing this control with no
|
||||
/// data context at all. Every handler below checks rather than assuming.
|
||||
/// </remarks>
|
||||
private VaultViewModel? Vault => DataContext as VaultViewModel;
|
||||
|
||||
/// <summary>
|
||||
/// Where the keyboard should land when the terminal hands it back.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Exposed as a property rather than left for the window to find by name, because the name is inside
|
||||
/// this control's template and the window cannot see it.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// It has to be a control the keyboard can actually go to. <c>Focus()</c> on a collapsed control is
|
||||
/// measurably a no-op and is not replayed when the control is revealed, so handing the keyboard to
|
||||
/// something that is not there would swallow it: the terminal would let go and nothing would take it.
|
||||
/// The grid no longer folds away as the sidebar's list could, but an empty grid is still a
|
||||
/// <c>ListBox</c> with no item to take focus — and an empty grid is exactly what a filter that matches
|
||||
/// nothing produces, which is a state somebody typing is very likely to be in. The find box is the
|
||||
/// answer then, and it is a good one: it is where they were typing.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal IInputElement KeyboardTarget =>
|
||||
Vault is { HasVisibleHosts: true } ? HostGrid : HostFilter;
|
||||
|
||||
/// <remarks>
|
||||
/// Fire-and-forget, as the transfers screen's is: the command reports its own failures onto the status
|
||||
/// line — an unknown host key, a refused password — and awaiting it here would mean an event handler
|
||||
/// returning a task nothing observes.
|
||||
/// </remarks>
|
||||
private void OnHostActivated(object? sender, TappedEventArgs e)
|
||||
{
|
||||
// Only over a card. A double-tap on a group heading folds it and unfolds it again, and must not also
|
||||
// connect to whichever host was selected before — which is what an unguarded handler would do, on a
|
||||
// machine the user is not even pointing at.
|
||||
if (Vault is { } vault && RowUnder(e.Source) is HostRowViewModel)
|
||||
{
|
||||
_ = vault.ConnectCommand.ExecuteAsync(null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Points the menu at whatever was right-clicked.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The menu's three commands all read the vault's host selection, and a right click does not move it —
|
||||
/// which would mean a menu that quietly acted on whichever host happened to be selected instead of the
|
||||
/// one under the pointer. Deleting the wrong machine is the version of that mistake worth designing
|
||||
/// against.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Cancelled outright over a group heading and over the space around the cards. Neither is a host, and a
|
||||
/// menu offering Connect, Edit and Delete over one would be three buttons that either do nothing or act
|
||||
/// on something else entirely.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private void OnContextRequested(object? sender, ContextRequestedEventArgs e)
|
||||
{
|
||||
if (Vault is not { } vault || RowUnder(e.Source) is not HostRowViewModel row)
|
||||
{
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
vault.SelectedSidebarRow = row;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Remembered rather than acted on. Whether this press is a click or the start of a drag is not known
|
||||
/// until the pointer moves, so this is the point at which both are still possible.
|
||||
/// </remarks>
|
||||
private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
press = null;
|
||||
pickedUp = null;
|
||||
|
||||
if (!e.GetCurrentPoint(HostGrid).Properties.IsLeftButtonPressed
|
||||
|| RowUnder(e.Source) is not HostRowViewModel row)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
press = e;
|
||||
pickedUp = row;
|
||||
origin = e.GetPosition(HostGrid);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The drag is started from the remembered press once the pointer has travelled far enough — see
|
||||
/// <see cref="DragThreshold"/>. Fire-and-forget, because the drag loop runs for as long as the user holds
|
||||
/// the button and an event handler cannot wait on that; what happens after it is only clearing the mark.
|
||||
/// </remarks>
|
||||
private void OnPointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (press is not { } pressed || pickedUp is not { } row)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e.GetCurrentPoint(HostGrid).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
Forget();
|
||||
return;
|
||||
}
|
||||
|
||||
var moved = e.GetPosition(HostGrid) - origin;
|
||||
|
||||
if (Math.Abs(moved.X) < DragThreshold && Math.Abs(moved.Y) < DragThreshold)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Forget();
|
||||
|
||||
_ = DragAsync(pressed, row);
|
||||
}
|
||||
|
||||
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e) => Forget();
|
||||
|
||||
private void OnPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e) => Forget();
|
||||
|
||||
/// <summary>Carries one host for as long as the user holds it.</summary>
|
||||
private async Task DragAsync(PointerPressedEventArgs pressed, HostRowViewModel row)
|
||||
{
|
||||
var carried = new DataTransfer();
|
||||
carried.Add(DataTransferItem.Create(HostFormat, row));
|
||||
|
||||
try
|
||||
{
|
||||
// ConfigureAwait(true): what follows touches the grid's own containers, and those are the UI
|
||||
// thread's.
|
||||
await DragDrop
|
||||
.DoDragDropAsync(pressed, carried, DragDropEffects.Move)
|
||||
.ConfigureAwait(true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Whatever the drop did or did not do. A mark left behind would be a card that looks like a
|
||||
// target for a drag that ended somewhere else entirely.
|
||||
Unmark();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Says whether what is under the pointer would take this host, and marks it if it would.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A host over its own group's heading is refused, which is not pedantry: <c>DragDropEffects.None</c> is
|
||||
/// what turns the cursor into the "no" one, and a drag that looks like it would do something and then
|
||||
/// does nothing is worse than one that says so while it is still in the air.
|
||||
/// </remarks>
|
||||
private void OnDragOver(object? sender, DragEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
|
||||
if (Target(e) is not { } target)
|
||||
{
|
||||
e.DragEffects = DragDropEffects.None;
|
||||
Unmark();
|
||||
return;
|
||||
}
|
||||
|
||||
e.DragEffects = DragDropEffects.Move;
|
||||
Mark(target.Container);
|
||||
}
|
||||
|
||||
private void OnDragLeave(object? sender, DragEventArgs e) => Unmark();
|
||||
|
||||
/// <remarks>
|
||||
/// Fire-and-forget, like every other command this control runs: the move writes to the vault and reports
|
||||
/// itself onto the status line, and a drop handler that awaited it would be an event handler returning a
|
||||
/// task nothing observes.
|
||||
/// </remarks>
|
||||
private void OnDrop(object? sender, DragEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
Unmark();
|
||||
|
||||
if (Vault is not { } vault || Target(e) is not { } target)
|
||||
{
|
||||
e.DragEffects = DragDropEffects.None;
|
||||
return;
|
||||
}
|
||||
|
||||
e.DragEffects = DragDropEffects.Move;
|
||||
vault.MoveHostToGroupCommand.Execute(new HostGroupMove(target.Host, target.GroupId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Where a drag currently is, or null if it is over nothing that would take it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// A heading is the obvious target and a card is the useful one: dropping onto a machine files the
|
||||
/// dragged host beside it, which means the whole band of cards under a heading is a target rather than
|
||||
/// one line of text. The ungrouped heading is a target like any other, and it is how a host is taken out
|
||||
/// of a group without opening the editor.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// A group the vault no longer has is read as no group at all, which is what the grid already does with
|
||||
/// a dangling reference — see <c>VaultViewModel.RebuildSidebarRows</c>. That is decided in the command
|
||||
/// rather than here, so the rule has one home.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private static DropTarget? Target(DragEventArgs e)
|
||||
{
|
||||
if (e.DataTransfer.TryGetValue(HostFormat) is not { } dragged
|
||||
|| Container(e.Source) is not { } container)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Guid? group = container.DataContext switch
|
||||
{
|
||||
SidebarGroupHeader header => header.GroupId,
|
||||
HostRowViewModel row => row.Host.GroupId,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (container.DataContext is not (SidebarGroupHeader or HostRowViewModel)
|
||||
|| dragged.Host.GroupId == group)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DropTarget(dragged, group, container);
|
||||
}
|
||||
|
||||
private void Mark(ListBoxItem container)
|
||||
{
|
||||
if (ReferenceEquals(marked, container))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Unmark();
|
||||
|
||||
marked = container;
|
||||
marked.Classes.Add("droptarget");
|
||||
}
|
||||
|
||||
private void Unmark()
|
||||
{
|
||||
marked?.Classes.Remove("droptarget");
|
||||
marked = null;
|
||||
}
|
||||
|
||||
/// <summary>Lets go of a press that turned out not to be a drag, or has become one.</summary>
|
||||
private void Forget()
|
||||
{
|
||||
press = null;
|
||||
pickedUp = null;
|
||||
}
|
||||
|
||||
/// <summary>The view model of the grid item an event happened on, if it happened on one.</summary>
|
||||
private static object? RowUnder(object? source) => Container(source)?.DataContext;
|
||||
|
||||
/// <remarks>
|
||||
/// Walks up from whatever was actually hit — a text block, a border, the card's own grid — because that
|
||||
/// is what an event's source is. Anything not inside an item, which is the space around the cards,
|
||||
/// yields null.
|
||||
/// </remarks>
|
||||
private static ListBoxItem? Container(object? source) => source is Visual visual
|
||||
? visual.FindAncestorOfType<ListBoxItem>(includeSelf: true)
|
||||
: null;
|
||||
|
||||
/// <summary>A drag in flight, and where it would land.</summary>
|
||||
private sealed record DropTarget(HostRowViewModel Host, Guid? GroupId, ListBoxItem Container);
|
||||
}
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
<Border Grid.Row="0" Padding="14,0" Height="44"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center">
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="IMPORT SSH CONFIG" FontSize="11"
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="IMPORT SSH CONFIG" FontSize="12"
|
||||
FontWeight="SemiBold" LetterSpacing="1" Foreground="{StaticResource Text}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding ConfigPath}" FontSize="9.5"
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding ConfigPath}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextFaint}" Margin="10,0" VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<Button Grid.Column="2" Classes="ghost" Content="SCAN" Command="{Binding ScanCommand}"
|
||||
@@ -34,7 +34,7 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<TextBlock Grid.Row="1" Classes="hint" Text="{Binding Status}" FontSize="11" Margin="14,12,14,0"
|
||||
<TextBlock Grid.Row="1" Classes="hint" Text="{Binding Status}" FontSize="12" Margin="14,12,14,0"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<!--
|
||||
@@ -48,7 +48,7 @@
|
||||
<ItemsControl ItemsSource="{Binding Warnings}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<TextBlock Text="{Binding}" Foreground="{StaticResource WarnText}" FontSize="10"
|
||||
<TextBlock Text="{Binding}" Foreground="{StaticResource WarnText}" FontSize="11"
|
||||
TextWrapping="Wrap" Margin="0,2" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
@@ -58,10 +58,10 @@
|
||||
<Grid Grid.Row="3" RowDefinitions="Auto,*" Margin="0,12,0,0" IsVisible="{Binding HasRows}">
|
||||
|
||||
<Grid Grid.Row="0" ColumnDefinitions="34,1.1*,1.4*,1.6*,96" Margin="14,0,14,6">
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="8.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="ADDRESS" FontSize="8.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="3" Classes="label" Text="AUTHENTICATION" FontSize="8.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="4" Classes="label" Text="STATE" FontSize="8.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="ADDRESS" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="3" Classes="label" Text="AUTHENTICATION" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="4" Classes="label" Text="STATE" FontSize="9.5" LetterSpacing="1" />
|
||||
</Grid>
|
||||
|
||||
<ScrollViewer Grid.Row="1">
|
||||
@@ -71,21 +71,21 @@
|
||||
<StackPanel Margin="14,0">
|
||||
<Grid ColumnDefinitions="34,1.1*,1.4*,1.6*,96" Margin="0,7">
|
||||
<CheckBox Grid.Column="0" IsChecked="{Binding IsSelected}" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Alias}" FontSize="11"
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Alias}" FontSize="12"
|
||||
FontWeight="Medium" Foreground="{StaticResource Text}" Margin="0,0,8,0"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Address}" FontSize="9.5"
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Address}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextDim}" Margin="0,0,8,0"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Authentication}" FontSize="9.5"
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Authentication}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextFaint}" Margin="0,0,8,0"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||||
<Border Grid.Column="4" Classes="chip" HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center" IsVisible="{Binding HasBadge}">
|
||||
<TextBlock Text="{Binding Badge}" FontSize="8.5" />
|
||||
<TextBlock Text="{Binding Badge}" FontSize="9.5" />
|
||||
</Border>
|
||||
</Grid>
|
||||
<TextBlock Classes="hint" Text="{Binding Warnings}" FontSize="9.5" Margin="34,0,0,8"
|
||||
<TextBlock Classes="hint" Text="{Binding Warnings}" FontSize="10.5" Margin="34,0,0,8"
|
||||
TextWrapping="Wrap" Foreground="{StaticResource WarnText}"
|
||||
IsVisible="{Binding HasWarnings}" />
|
||||
</StackPanel>
|
||||
@@ -104,7 +104,7 @@
|
||||
the difference between a bookmark that connects and one that asks for a password, and somebody
|
||||
who is not told will conclude the import was broken.
|
||||
-->
|
||||
<TextBlock Classes="hint" FontSize="10" TextWrapping="Wrap"
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||
Text="Key files are not read. Where ssh_config names an IdentityFile the path is recorded as a note, and the host asks for a password until you bind it to a key in your keychain. Nothing here reaches into ~/.ssh for private key material." />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Classes="accent" Content="{Binding ImportLabel}" Command="{Binding ImportCommand}"
|
||||
|
||||
@@ -24,10 +24,10 @@
|
||||
<Border Grid.Row="0" Padding="14,0" Height="44"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center">
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="HOST KEYS" FontSize="11"
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="HOST KEYS" FontSize="12"
|
||||
FontWeight="SemiBold" LetterSpacing="1" Foreground="{StaticResource Text}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Summary}" FontSize="9.5"
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Summary}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextFaint}" Margin="10,0,0,0"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||||
|
||||
@@ -43,12 +43,12 @@
|
||||
|
||||
<Grid Grid.Row="1" ColumnDefinitions="2,1.4*,58,104,*,96" Margin="0,6,14,6"
|
||||
IsVisible="{Binding HasVisiblePins}">
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="HOST" FontSize="8.5" LetterSpacing="1"
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="HOST" FontSize="9.5" LetterSpacing="1"
|
||||
Margin="12,0,8,0" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="PORT" FontSize="8.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="3" Classes="label" Text="ALGORITHM" FontSize="8.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="4" Classes="label" Text="FINGERPRINT" FontSize="8.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="5" Classes="label" Text="APPROVED" FontSize="8.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="PORT" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="3" Classes="label" Text="ALGORITHM" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="4" Classes="label" Text="FINGERPRINT" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="5" Classes="label" Text="APPROVED" FontSize="9.5" LetterSpacing="1" />
|
||||
</Grid>
|
||||
|
||||
<ListBox Grid.Row="2" x:Name="PinList" Focusable="True"
|
||||
@@ -58,12 +58,12 @@
|
||||
<DataTemplate x:DataType="vm:KnownHostRowViewModel">
|
||||
<Grid ColumnDefinitions="2,1.4*,58,104,*,96" Margin="0,7,14,7">
|
||||
<Border Grid.Column="0" Classes="rowmark" />
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Host}" FontSize="11"
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Host}" FontSize="12"
|
||||
FontWeight="Medium" Foreground="{StaticResource Text}" Margin="12,0,8,0"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Port}" FontSize="9.5"
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Port}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Algorithm}" FontSize="9"
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Algorithm}" FontSize="10"
|
||||
Foreground="{StaticResource TextDim}" Margin="0,0,8,0"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||||
<!--
|
||||
@@ -72,17 +72,17 @@
|
||||
published; an ellipsis in the middle turns that into a glance, which is the habit the whole
|
||||
mechanism exists to replace.
|
||||
-->
|
||||
<TextBlock Grid.Column="4" Classes="mono" Text="{Binding Fingerprint}" FontSize="9.5"
|
||||
<TextBlock Grid.Column="4" Classes="mono" Text="{Binding Fingerprint}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextFaint}" Margin="0,0,8,0"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="5" Classes="mono" Text="{Binding Approved}" FontSize="9"
|
||||
<TextBlock Grid.Column="5" Classes="mono" Text="{Binding Approved}" FontSize="10"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<TextBlock Grid.Row="2" Classes="hint" Text="{Binding EmptyMessage}" FontSize="11"
|
||||
<TextBlock Grid.Row="2" Classes="hint" Text="{Binding EmptyMessage}" FontSize="12"
|
||||
Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
TextAlignment="Center" MaxWidth="340"
|
||||
IsVisible="{Binding !HasVisiblePins}" />
|
||||
@@ -94,12 +94,12 @@
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="14,16" Spacing="6">
|
||||
|
||||
<TextBlock Classes="hint" FontSize="11"
|
||||
<TextBlock Classes="hint" FontSize="12"
|
||||
Text="Choose a pinned key to see it in full, and to withdraw it."
|
||||
IsVisible="{Binding !HasSelection}" />
|
||||
|
||||
<StackPanel Spacing="6" IsVisible="{Binding HasSelection}">
|
||||
<TextBlock Classes="mono" Text="{Binding Selected.Label}" FontSize="12"
|
||||
<TextBlock Classes="mono" Text="{Binding Selected.Label}" FontSize="13"
|
||||
FontWeight="SemiBold" Foreground="{StaticResource Text}" TextWrapping="Wrap" />
|
||||
|
||||
<Border Classes="chip warn" HorizontalAlignment="Left"
|
||||
@@ -111,22 +111,22 @@
|
||||
<Border Background="{StaticResource Raised}" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="1" CornerRadius="4" Padding="8">
|
||||
<SelectableTextBlock Classes="mono" Text="{Binding Selected.Fingerprint}"
|
||||
FontSize="9.5" Foreground="{StaticResource TextDim}"
|
||||
FontSize="10.5" Foreground="{StaticResource TextDim}"
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
|
||||
<TextBlock Classes="label" Text="APPROVED" Margin="0,12,0,4" />
|
||||
<TextBlock Classes="mono" Text="{Binding Selected.Approved}" FontSize="10"
|
||||
<TextBlock Classes="mono" Text="{Binding Selected.Approved}" FontSize="11"
|
||||
Foreground="{StaticResource TextDim}" />
|
||||
<!--
|
||||
Said rather than implied. No vault item carries a timestamp, so this date is read back out of
|
||||
the item's own version 7 id — which records when the pin was created and knows nothing about
|
||||
it being re-approved since. Presenting that as "last used" would be inventing a fact.
|
||||
-->
|
||||
<TextBlock Classes="hint" FontSize="10" TextWrapping="Wrap"
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||
Text="Taken from the item's identifier, so it is when this key was first approved — not when it was last checked. Nothing here records that." />
|
||||
|
||||
<TextBlock Classes="hint" FontSize="10" TextWrapping="Wrap" Margin="0,12,0,0"
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap" Margin="0,12,0,0"
|
||||
Text="A pin outlives whatever it was approved for: deleting a host leaves it, and so does changing a host's address. That is deliberate — trust is about the endpoint, not the bookmark." />
|
||||
|
||||
<Button Classes="danger" Content="FORGET THIS HOST KEY" Margin="0,12,0,0"
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<Border Grid.Row="0" Padding="14,0" Height="44"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
||||
<Grid ColumnDefinitions="Auto,Auto,Auto,*,Auto" VerticalAlignment="Center">
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="LOGS" FontSize="11" FontWeight="SemiBold"
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="LOGS" FontSize="12" FontWeight="SemiBold"
|
||||
LetterSpacing="1" Foreground="{StaticResource Text}" VerticalAlignment="Center"
|
||||
Margin="0,0,14,0" />
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
Command="{Binding ShowSectionCommand}"
|
||||
CommandParameter="{x:Static vm:LogSection.Activity}" />
|
||||
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Status}" FontSize="9.5"
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Status}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextFaint}" Margin="14,0,0,0"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||||
|
||||
@@ -52,12 +52,12 @@
|
||||
|
||||
<Grid Grid.Row="0" ColumnDefinitions="1.2*,1.6*,88,72,90,*" Margin="14,6,14,6"
|
||||
IsVisible="{Binding HasConnections}">
|
||||
<TextBlock Grid.Column="0" Classes="label" Text="HOST" FontSize="8.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="ADDRESS" FontSize="8.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="LASTED" FontSize="8.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="3" Classes="label" Text="KIND" FontSize="8.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="4" Classes="label" Text="STARTED" FontSize="8.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="5" Classes="label" Text="FROM" FontSize="8.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="0" Classes="label" Text="HOST" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="ADDRESS" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="LASTED" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="3" Classes="label" Text="KIND" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="4" Classes="label" Text="STARTED" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="5" Classes="label" Text="FROM" FontSize="9.5" LetterSpacing="1" />
|
||||
</Grid>
|
||||
|
||||
<ListBox Grid.Row="1" x:Name="ConnectionList" Focusable="True"
|
||||
@@ -67,23 +67,23 @@
|
||||
<Grid ColumnDefinitions="1.2*,1.6*,88,72,90,*" Margin="0,6,14,6">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6" Margin="14,0,8,0">
|
||||
<Ellipse Classes="dot" Classes.live="{Binding IsLive}" VerticalAlignment="Center" />
|
||||
<TextBlock Classes="mono" Text="{Binding HostLabel}" FontSize="11" FontWeight="Medium"
|
||||
<TextBlock Classes="mono" Text="{Binding HostLabel}" FontSize="12" FontWeight="Medium"
|
||||
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Address}" FontSize="9.5"
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Address}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextDim}" Margin="0,0,8,0"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" Text="{Binding Duration}" FontSize="9.5"
|
||||
<TextBlock Classes="mono" Text="{Binding Duration}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextDim}" />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Kind}" FontSize="9"
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Kind}" FontSize="10"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="4" Classes="mono" Text="{Binding Started}" FontSize="9"
|
||||
<TextBlock Grid.Column="4" Classes="mono" Text="{Binding Started}" FontSize="10"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||
<StackPanel Grid.Column="5" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" Text="{Binding DeviceName}" FontSize="9"
|
||||
<TextBlock Classes="mono" Text="{Binding DeviceName}" FontSize="10"
|
||||
Foreground="{StaticResource TextFaint}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<!--
|
||||
@@ -91,7 +91,7 @@
|
||||
here; one that was refused says so, and that is the row worth finding in a long list.
|
||||
-->
|
||||
<Border Classes="chip warn" Padding="4,0" IsVisible="{Binding HasOutcome}">
|
||||
<TextBlock Text="{Binding Outcome}" FontSize="8.5" />
|
||||
<TextBlock Text="{Binding Outcome}" FontSize="9.5" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
@@ -99,7 +99,7 @@
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<TextBlock Grid.Row="1" Classes="hint" Text="{Binding EmptyMessage}" FontSize="11"
|
||||
<TextBlock Grid.Row="1" Classes="hint" Text="{Binding EmptyMessage}" FontSize="12"
|
||||
Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
TextAlignment="Center" MaxWidth="420"
|
||||
IsVisible="{Binding !HasConnections}" />
|
||||
@@ -110,40 +110,40 @@
|
||||
|
||||
<Grid Grid.Row="0" ColumnDefinitions="1.2*,90,96,*,90" Margin="14,6,14,6"
|
||||
IsVisible="{Binding HasActivity}">
|
||||
<TextBlock Grid.Column="0" Classes="label" Text="ITEM" FontSize="8.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="TYPE" FontSize="8.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="WHAT" FontSize="8.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="3" Classes="label" Text="FIELDS" FontSize="8.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="4" Classes="label" Text="WHEN" FontSize="8.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="0" Classes="label" Text="ITEM" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="TYPE" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="WHAT" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="3" Classes="label" Text="FIELDS" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="4" Classes="label" Text="WHEN" FontSize="9.5" LetterSpacing="1" />
|
||||
</Grid>
|
||||
|
||||
<ListBox Grid.Row="1" x:Name="ActivityList" Focusable="True" ItemsSource="{Binding Activity}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ActivityLogRowViewModel">
|
||||
<Grid ColumnDefinitions="1.2*,90,96,*,90" Margin="14,6,14,6">
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="{Binding ItemLabel}" FontSize="11"
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="{Binding ItemLabel}" FontSize="12"
|
||||
FontWeight="Medium" Foreground="{StaticResource Text}" Margin="0,0,8,0"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding ItemKind}" FontSize="9"
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding ItemKind}" FontSize="10"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Operation}" FontSize="9.5"
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Operation}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
|
||||
<!--
|
||||
The names of the fields that changed, and never what they changed to. A log that recorded
|
||||
an old password would be a plaintext credential store with a vault drawn around it.
|
||||
-->
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding ChangedFields}" FontSize="9.5"
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding ChangedFields}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextFaint}" Margin="0,0,8,0"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center"
|
||||
IsVisible="{Binding HasChangedFields}" />
|
||||
<TextBlock Grid.Column="4" Classes="mono" Text="{Binding At}" FontSize="9"
|
||||
<TextBlock Grid.Column="4" Classes="mono" Text="{Binding At}" FontSize="10"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<TextBlock Grid.Row="1" Classes="hint" Text="{Binding EmptyMessage}" FontSize="11"
|
||||
<TextBlock Grid.Row="1" Classes="hint" Text="{Binding EmptyMessage}" FontSize="12"
|
||||
Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
TextAlignment="Center" MaxWidth="420"
|
||||
IsVisible="{Binding !HasActivity}" />
|
||||
|
||||
@@ -76,33 +76,57 @@
|
||||
<Panel Grid.Row="1">
|
||||
|
||||
<!-- The unlocked application. -->
|
||||
<Grid ColumnDefinitions="Auto,*" IsVisible="{Binding IsUnlocked}">
|
||||
|
||||
<views:NavRail Grid.Column="0" />
|
||||
<Grid RowDefinitions="Auto,*" IsVisible="{Binding IsUnlocked}">
|
||||
|
||||
<!--
|
||||
The rail is full height and the strip is not, so the strip spans exactly the area it navigates.
|
||||
The other arrangement — strip above rail — would put a row of tabs over a column of destinations
|
||||
they have nothing to do with.
|
||||
◆ THE STRIP IS ABOVE THE RAIL, and it used to be beside it.
|
||||
|
||||
It was the other way round for a reason that stopped being true: while every tab was a terminal,
|
||||
the strip navigated only the area to the right of a full-height rail, and putting it over the rail
|
||||
would have been a row of tabs above a column of destinations they had nothing to do with.
|
||||
|
||||
The three fixed tabs are what changed that. The rail is now one tab's contents rather than the
|
||||
window's own furniture — Vaults owns it, SFTP and S3 do not have it, and a terminal does not
|
||||
either — so a rail drawn beside the strip would outrank the thing that decides whether it is
|
||||
showing at all. Above and full width is the arrangement that matches what selects what.
|
||||
-->
|
||||
<Grid Grid.Column="1" RowDefinitions="Auto,*">
|
||||
<views:TerminalTabs Grid.Row="0" />
|
||||
|
||||
<views:TerminalTabs Grid.Row="0" />
|
||||
<Grid Grid.Row="1" ColumnDefinitions="Auto,*">
|
||||
|
||||
<Panel Grid.Row="1">
|
||||
<!--
|
||||
The Vaults tab's own navigation, and it collapses with that tab. Its width is 190 either way, so
|
||||
SFTP, S3 and a terminal each get the full window rather than the 826 a page gets.
|
||||
-->
|
||||
<views:NavRail Grid.Column="0" IsVisible="{Binding IsVaultsTab}" />
|
||||
|
||||
<Panel Grid.Column="1">
|
||||
|
||||
<!-- ============ THE PAGES ============ -->
|
||||
<Panel IsVisible="{Binding IsShowingPages}">
|
||||
|
||||
<!--
|
||||
Bound directly rather than wrapped, unlike the two below it: this screen's data context is
|
||||
the shell's, so IsHostsScreen resolves. It hands the vault to the sidebar from inside its
|
||||
own markup.
|
||||
Wrapped, like every other screen here. It used to be bound directly — its data context was
|
||||
the shell's, so IsHostsScreen resolved on the same element — and that was only so it could
|
||||
hand the vault to the host sidebar from inside its own markup. The sidebar is gone and the
|
||||
drawer that replaced it is a plain child, so the screen takes the vault like the rest and
|
||||
its visibility goes on the wrapper.
|
||||
-->
|
||||
<views:HostsScreen x:Name="HostsPane" IsVisible="{Binding IsHostsScreen}" />
|
||||
<Panel IsVisible="{Binding IsHostsScreen}">
|
||||
<views:HostsScreen x:Name="HostsPane" DataContext="{Binding Vault}" />
|
||||
</Panel>
|
||||
|
||||
<!-- ============ FILES ============ -->
|
||||
<!-- ============ SFTP ============ -->
|
||||
<!--
|
||||
Inside this Panel although it is a tab rather than a rail screen, and that is not an
|
||||
oversight. IsShowingPages means "the Avalonia page area, not the WebView", which is the
|
||||
occlusion question and is true of all three fixed tabs; which of them is showing is the
|
||||
separate question each child below answers. Keeping the two apart is what lets the terminal
|
||||
stay collapsed under one rule rather than under four.
|
||||
|
||||
What differs from a rail screen is only the rail: NavRail collapses on IsVaultsTab above, so
|
||||
this screen is laid out at the full window width.
|
||||
|
||||
Wrapped rather than bound directly, for the same reason the vault screen is: this element's
|
||||
visibility is the shell's business and its data context is the transfers view model, and
|
||||
putting both on one element resolves IsVisible against that view model, where
|
||||
@@ -114,10 +138,10 @@
|
||||
|
||||
<!-- ============ S3 ============ -->
|
||||
<!--
|
||||
The same screen as FILES above, over the same view model, because an object store and an
|
||||
The same screen as SFTP above, over the same view model, because an object store and an
|
||||
SFTP host are both an IRemoteFileStore and everything below the picker was written once.
|
||||
What differs is which picker is offered, and that is decided by the destination rather than
|
||||
by a toggle inside the screen — see ShowFiles, and the sidebar entry that calls it.
|
||||
by a toggle inside the screen — see ShowFiles, and the tab in the strip that calls it.
|
||||
-->
|
||||
<Panel IsVisible="{Binding IsBucketsScreen}">
|
||||
<views:TransfersScreen DataContext="{Binding Transfers}" />
|
||||
@@ -266,7 +290,7 @@
|
||||
<Border Background="{StaticResource Raised}" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="1" CornerRadius="6" Padding="14">
|
||||
<SelectableTextBlock Classes="mono" Text="{Binding RecoveryCode}"
|
||||
FontSize="16" Foreground="{StaticResource Accent}"
|
||||
FontSize="17" Foreground="{StaticResource Accent}"
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
<CheckBox IsChecked="{Binding RecoveryCodeWrittenDown}"
|
||||
|
||||
@@ -18,10 +18,20 @@
|
||||
each carries a count, which is the one genuinely new fact: how many hosts, how many keys, how many pins
|
||||
is a question you would otherwise have to open the screen to answer.
|
||||
|
||||
A count is drawn only where one is real. SFTP, Logs and Preferences have none — a transfer queue's depth
|
||||
is not "how many files this screen holds", a log has no total until it is read, and preferences are not
|
||||
counted — so those three show nothing rather than a zero. The design draws a number on every row; a zero
|
||||
beside Logs would be a fact this application never computed.
|
||||
A count is drawn only where one is real. Logs and Preferences have none — a log has no total until it is
|
||||
read, and preferences are not counted — so those two show nothing rather than a zero. The design draws a
|
||||
number on every row; a zero beside Logs would be a fact this application never computed.
|
||||
|
||||
── SFTP AND S3 ARE NOT HERE, and that is the tab strip's doing. ──────────────────────────────────────
|
||||
Both were rail entries until the strip grew fixed tabs for them. They are the two destinations that are
|
||||
not about the keychain — they are a place you leave the keychain to work in, and you stay there while a
|
||||
transfer runs — which is exactly what a tab is for and what a rail entry is not. The rail is drawn only
|
||||
under the Vaults tab now, so an entry here for either of them would be a route out of the tab it lives
|
||||
in. See MainWindowViewModel.IsVaultsTab.
|
||||
|
||||
What this costs is that the S3 count has nowhere to go: the strip's tabs are one word each, and the rail
|
||||
was where "how many buckets" was printed. It is on the S3 screen itself, which is where somebody
|
||||
counting buckets is going anyway.
|
||||
|
||||
One of the destinations — TEAM — reaches a screen that says it is not built. It is in the list anyway
|
||||
rather than dropped, and the reasoning is in ShellScreen: the milestones are public, the screen behind it
|
||||
@@ -61,32 +71,6 @@
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Button Classes="flat nav" Classes.active="{Binding IsTransfersShowing}"
|
||||
Command="{Binding ShowFilesCommand}"
|
||||
CommandParameter="{x:Static vm:RemoteKind.Host}"
|
||||
ToolTip.Tip="Move files to and from a host over SFTP">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="navicon" Text="⇅" />
|
||||
<TextBlock Grid.Column="1" Classes="navlabel" Text="SFTP" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<!--
|
||||
Buckets are their own destination now, as they are on the phone, rather than a toggle inside the
|
||||
files screen. Same screen behind both — an object store and an SFTP host are both an
|
||||
IRemoteFileStore — and the entry chosen is what decides which picker is offered. See ShowFiles.
|
||||
-->
|
||||
<Button Classes="flat nav" Classes.active="{Binding IsBucketsShowing}"
|
||||
Command="{Binding ShowFilesCommand}"
|
||||
CommandParameter="{x:Static vm:RemoteKind.Bucket}"
|
||||
ToolTip.Tip="Objects in an S3-compatible bucket from your keychain">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="navicon" Text="◳" />
|
||||
<TextBlock Grid.Column="1" Classes="navlabel" Text="S3" />
|
||||
<TextBlock Grid.Column="2" Classes="navcount" Text="{Binding Vault.ObjectStores.Count}" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<!--
|
||||
TotalItemCount is keys plus passwords and deliberately excludes buckets, which used to disagree
|
||||
with the list under it. It no longer does: buckets have their own entry above, so this number and
|
||||
|
||||
@@ -18,14 +18,14 @@
|
||||
<StackPanel MaxWidth="560" Margin="28,26" HorizontalAlignment="Left">
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="mono" Name="TitleText" FontSize="16" FontWeight="SemiBold" LetterSpacing="0.5"
|
||||
<TextBlock Classes="mono" Name="TitleText" FontSize="17" FontWeight="SemiBold" LetterSpacing="0.5"
|
||||
Foreground="{StaticResource Text}" VerticalAlignment="Center" />
|
||||
<Border Classes="chip warn">
|
||||
<TextBlock Name="MilestoneText" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Name="SummaryText" Classes="hint" FontSize="12" Margin="0,10,0,0" />
|
||||
<TextBlock Name="SummaryText" Classes="hint" FontSize="13" Margin="0,10,0,0" />
|
||||
|
||||
<TextBlock Classes="label" Text="WHAT IS MISSING" Margin="0,24,0,8" />
|
||||
<ItemsControl Name="MissingList">
|
||||
@@ -34,7 +34,7 @@
|
||||
<Grid ColumnDefinitions="Auto,*" Margin="0,0,0,7">
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="·" Foreground="{StaticResource TextFaint}"
|
||||
Margin="0,0,8,0" />
|
||||
<TextBlock Grid.Column="1" Classes="hint" FontSize="11" Text="{Binding}" />
|
||||
<TextBlock Grid.Column="1" Classes="hint" FontSize="12" Text="{Binding}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
@@ -42,10 +42,10 @@
|
||||
|
||||
<Border Background="{StaticResource Raised}" BorderBrush="{StaticResource Border}" BorderThickness="1"
|
||||
CornerRadius="4" Padding="12" Margin="0,18,0,0">
|
||||
<TextBlock Name="InsteadText" Classes="hint" FontSize="11" />
|
||||
<TextBlock Name="InsteadText" Classes="hint" FontSize="12" />
|
||||
</Border>
|
||||
|
||||
<TextBlock Classes="hint" FontSize="10" Margin="0,14,0,0"
|
||||
<TextBlock Classes="hint" FontSize="11" Margin="0,14,0,0"
|
||||
Text="Everything this design asked for and this build does not have is written down in docs/design-import-gaps.md, with which project each piece would land in." />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
@@ -28,14 +28,14 @@
|
||||
<ScrollViewer>
|
||||
<StackPanel MaxWidth="620" Margin="28,26" HorizontalAlignment="Left">
|
||||
|
||||
<TextBlock Classes="mono" Text="THIS MACHINE" FontSize="13" FontWeight="SemiBold"
|
||||
<TextBlock Classes="mono" Text="THIS MACHINE" FontSize="14" FontWeight="SemiBold"
|
||||
LetterSpacing="1" Foreground="{StaticResource Text}" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,12,0,0">
|
||||
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="Unlock with Windows Hello" Foreground="{StaticResource Text}" FontSize="12"
|
||||
<TextBlock Text="Unlock with Windows Hello" Foreground="{StaticResource Text}" FontSize="13"
|
||||
FontWeight="Medium" />
|
||||
<TextBlock Classes="hint" FontSize="10"
|
||||
<TextBlock Classes="hint" FontSize="11"
|
||||
Text="Registers this machine so a later launch can open the keychain with a Windows confirmation instead of your passphrase. Your passphrase keeps working." />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Classes="accent" Content="REGISTER"
|
||||
@@ -55,13 +55,13 @@
|
||||
</Grid>
|
||||
|
||||
<!-- Neither flag is set on a machine that cannot keep a key at all, and that is worth saying. -->
|
||||
<TextBlock Classes="hint" FontSize="10" Margin="0,8,0,0"
|
||||
<TextBlock Classes="hint" FontSize="11" Margin="0,8,0,0"
|
||||
Text="This machine has nowhere to keep a device key, so the keychain will keep asking for your passphrase. That needs a TPM and a Windows keystore willing to release the key."
|
||||
IsVisible="{Binding HasNoDeviceKeyOption}" />
|
||||
|
||||
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="0,20" />
|
||||
|
||||
<TextBlock Classes="mono" Text="TERMINAL" FontSize="13" FontWeight="SemiBold"
|
||||
<TextBlock Classes="mono" Text="TERMINAL" FontSize="14" FontWeight="SemiBold"
|
||||
LetterSpacing="1" Foreground="{StaticResource Text}" />
|
||||
|
||||
<!--
|
||||
@@ -73,16 +73,16 @@
|
||||
-->
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,12,0,0">
|
||||
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="Text size" Foreground="{StaticResource Text}" FontSize="12"
|
||||
<TextBlock Text="Text size" Foreground="{StaticResource Text}" FontSize="13"
|
||||
FontWeight="Medium" />
|
||||
<TextBlock Classes="hint" FontSize="10"
|
||||
<TextBlock Classes="hint" FontSize="11"
|
||||
Text="How large a terminal draws, in pixels. Ctrl+plus and Ctrl+minus do the same while a terminal has focus, and Ctrl+0 puts it back. It resizes the grid rather than magnifying it, so the remote is told how many columns it now has — which is also why it stops before the columns run out." />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6" VerticalAlignment="Top">
|
||||
<Button Classes="ghost" Content="A−" Command="{Binding ShrinkTerminalFontCommand}"
|
||||
IsEnabled="{Binding CanShrinkTerminalFont}"
|
||||
ToolTip.Tip="Smaller · Ctrl+minus" />
|
||||
<TextBlock Classes="mono" FontSize="12" MinWidth="26" VerticalAlignment="Center"
|
||||
<TextBlock Classes="mono" FontSize="13" MinWidth="26" VerticalAlignment="Center"
|
||||
TextAlignment="Center" Foreground="{StaticResource Text}"
|
||||
Text="{Binding TerminalFontSize}" />
|
||||
<Button Classes="ghost" Content="A+" Command="{Binding EnlargeTerminalFontCommand}"
|
||||
@@ -95,14 +95,14 @@
|
||||
|
||||
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="0,20" />
|
||||
|
||||
<TextBlock Classes="mono" Text="KEYCHAIN" FontSize="13" FontWeight="SemiBold"
|
||||
<TextBlock Classes="mono" Text="KEYCHAIN" FontSize="14" FontWeight="SemiBold"
|
||||
LetterSpacing="1" Foreground="{StaticResource Text}" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,12,0,0">
|
||||
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="Lock the keychain" Foreground="{StaticResource Text}" FontSize="12"
|
||||
<TextBlock Text="Lock the keychain" Foreground="{StaticResource Text}" FontSize="13"
|
||||
FontWeight="Medium" />
|
||||
<TextBlock Classes="hint" FontSize="10"
|
||||
<TextBlock Classes="hint" FontSize="11"
|
||||
Text="Closes the keychain and forgets every key it held. Shells you have open keep running and reappear when you unlock — locked describes the keychain, not this machine's access to your hosts." />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Classes="ghost" Content="LOCK NOW" Command="{Binding LockCommand}" />
|
||||
@@ -110,9 +110,9 @@
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,14,0,0">
|
||||
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="Synchronise" Foreground="{StaticResource Text}" FontSize="12"
|
||||
<TextBlock Text="Synchronise" Foreground="{StaticResource Text}" FontSize="13"
|
||||
FontWeight="Medium" />
|
||||
<TextBlock Classes="hint" FontSize="10"
|
||||
<TextBlock Classes="hint" FontSize="11"
|
||||
Text="Runs a pass now. One runs on its own when the keychain opens, straight after any change, and every minute while it stays open — and a pass that finds this machine offline signs it back in from the session it remembered, so nothing here depends on being pressed." />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6">
|
||||
@@ -125,9 +125,9 @@
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,14,0,0">
|
||||
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="Import from ~/.ssh/config" Foreground="{StaticResource Text}" FontSize="12"
|
||||
<TextBlock Text="Import from ~/.ssh/config" Foreground="{StaticResource Text}" FontSize="13"
|
||||
FontWeight="Medium" />
|
||||
<TextBlock Classes="hint" FontSize="10"
|
||||
<TextBlock Classes="hint" FontSize="11"
|
||||
Text="Reads this machine's OpenSSH configuration and offers what it finds. It shows you the list first and stores nothing until you say so, and it does not read any private key — where a key file is named, the path is recorded as a note." />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Classes="ghost" Content="IMPORT HOSTS"
|
||||
@@ -137,17 +137,17 @@
|
||||
|
||||
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="0,20" />
|
||||
|
||||
<TextBlock Classes="mono" Text="ACCOUNT" FontSize="13" FontWeight="SemiBold"
|
||||
<TextBlock Classes="mono" Text="ACCOUNT" FontSize="14" FontWeight="SemiBold"
|
||||
LetterSpacing="1" Foreground="{StaticResource Text}" />
|
||||
|
||||
<TextBlock Classes="mono" Text="{Binding AccountName}" FontSize="11" Margin="0,8,0,0"
|
||||
<TextBlock Classes="mono" Text="{Binding AccountName}" FontSize="12" Margin="0,8,0,0"
|
||||
Foreground="{StaticResource Info}" TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,12,0,0">
|
||||
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="Sign out of this machine" Foreground="{StaticResource Text}" FontSize="12"
|
||||
<TextBlock Text="Sign out of this machine" Foreground="{StaticResource Text}" FontSize="13"
|
||||
FontWeight="Medium" />
|
||||
<TextBlock Classes="hint" FontSize="10"
|
||||
<TextBlock Classes="hint" FontSize="11"
|
||||
Text="Deletes this machine's copy of the keychain and withdraws its device key, so it goes back to knowing nothing. The keychain stays on the server; signing in again brings it back. Use this to hand a machine on, or to enrol a different account." />
|
||||
</StackPanel>
|
||||
<!--
|
||||
@@ -168,9 +168,9 @@
|
||||
|
||||
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="0,20" />
|
||||
|
||||
<TextBlock Classes="mono" Text="NOT BUILT YET" FontSize="13" FontWeight="SemiBold"
|
||||
<TextBlock Classes="mono" Text="NOT BUILT YET" FontSize="14" FontWeight="SemiBold"
|
||||
LetterSpacing="1" Foreground="{StaticResource TextDim}" />
|
||||
<TextBlock Classes="hint" FontSize="11" Margin="0,8,0,0"
|
||||
<TextBlock Classes="hint" FontSize="12" Margin="0,8,0,0"
|
||||
Text="These are on the design and have nothing behind them. They are listed rather than left out, so that what this screen does not do is as legible as what it does. The full list, and what each would take, is in docs/design-import-gaps.md." />
|
||||
|
||||
<ItemsControl Margin="0,12,0,0">
|
||||
|
||||
@@ -37,11 +37,11 @@
|
||||
|
||||
<Border Padding="12,10" BorderBrush="{StaticResource BorderSubtle}" BorderThickness="0,0,0,1">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text=">" FontSize="12"
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text=">" FontSize="13"
|
||||
Foreground="{StaticResource Accent}" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" x:Name="Query" Text="{Binding SearchText}"
|
||||
PlaceholderText="search hosts" Margin="8,0,0,0"
|
||||
FontFamily="{StaticResource MonoFont}" FontSize="13"
|
||||
FontFamily="{StaticResource MonoFont}" FontSize="14"
|
||||
Background="Transparent" BorderThickness="0" />
|
||||
</Grid>
|
||||
</Border>
|
||||
@@ -63,24 +63,24 @@
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,8,12,8">
|
||||
<Border Grid.Column="0" Classes="rowmark" />
|
||||
<StackPanel Grid.Column="1" Spacing="1" Margin="12,0,0,0">
|
||||
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="12" FontWeight="Medium"
|
||||
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="13" FontWeight="Medium"
|
||||
Foreground="{StaticResource Text}" />
|
||||
<TextBlock Classes="mono" Text="{Binding Address}" FontSize="9.5"
|
||||
<TextBlock Classes="mono" Text="{Binding Address}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextFaint}" />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Authentication}" FontSize="9"
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Authentication}" FontSize="10"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<TextBlock Classes="hint" FontSize="11" Margin="14,16"
|
||||
<TextBlock Classes="hint" FontSize="12" Margin="14,16"
|
||||
Text="No host matches that."
|
||||
IsVisible="{Binding !HasSearchResults}" />
|
||||
|
||||
<Border Padding="12,7" BorderBrush="{StaticResource BorderSubtle}" BorderThickness="0,1,0,0">
|
||||
<TextBlock Classes="mono" FontSize="9" Foreground="{StaticResource TextFaint}"
|
||||
<TextBlock Classes="mono" FontSize="10" Foreground="{StaticResource TextFaint}"
|
||||
Text="↑ ↓ to choose · ENTER or click to connect · ESC to close" />
|
||||
</Border>
|
||||
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
|
||||
<StackPanel Spacing="10">
|
||||
|
||||
<TextBlock Classes="heading" FontSize="15" Text="Sign out of this machine?" />
|
||||
<TextBlock Classes="heading" FontSize="16" Text="Sign out of this machine?" />
|
||||
|
||||
<TextBlock Text="{Binding SignOutWarning}" Foreground="{StaticResource WarnText}"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||
<TextBlock Classes="hint" FontSize="12" TextWrapping="Wrap"
|
||||
Text="This deletes this machine's copy of the keychain — the profile, the cached hosts, keys and passwords, and this machine's device key. Your keychain is on the server and is not touched: signing in again brings it all back." />
|
||||
|
||||
<Border Background="{StaticResource Panel}" BorderBrush="{StaticResource Border}"
|
||||
@@ -38,7 +38,7 @@
|
||||
the tab strip and the phone's own lock screen all paint green. -->
|
||||
<TextBlock Text="{Binding LiveSessionSummary}" Foreground="{StaticResource Live}"
|
||||
FontWeight="SemiBold" TextWrapping="Wrap" />
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||
<TextBlock Classes="hint" FontSize="12" TextWrapping="Wrap"
|
||||
Text="Signing out does not close them, exactly as locking does not. Quit DodoSSH to end them." />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
@@ -48,7 +48,7 @@
|
||||
a connection this machine holds rather than a window it shows: a transfer in flight authenticated
|
||||
before any of this and keeps writing into its part file afterwards.
|
||||
-->
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||
<TextBlock Classes="hint" FontSize="12" TextWrapping="Wrap"
|
||||
IsVisible="{Binding Transfers.IsConnected, FallbackValue=False}"
|
||||
Text="A file-transfer session is open on this machine. Signing out does not close it either — it goes when DodoSSH does." />
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelSignOutCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Classes="hint" FontSize="10" TextWrapping="Wrap"
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||
Text="Your session at the identity provider is not ended by this — DodoSSH has no way to end it — so on a machine that is not yours, sign out there too." />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
@@ -24,10 +24,10 @@
|
||||
<Border Grid.Row="0" Padding="14,0" Height="44"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center">
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="SNIPPETS" FontSize="11"
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="SNIPPETS" FontSize="12"
|
||||
FontWeight="SemiBold" LetterSpacing="1" Foreground="{StaticResource Text}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Status}" FontSize="9.5"
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Status}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextFaint}" Margin="10,0,0,0"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<Border Grid.Column="0" Classes="rowmark" />
|
||||
<StackPanel Grid.Column="1" Margin="12,0,0,0" Spacing="2">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="11" FontWeight="Medium"
|
||||
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="12" FontWeight="Medium"
|
||||
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
|
||||
<!--
|
||||
The flag, where the decision is made. A snippet that presses Enter for you is not the
|
||||
@@ -57,18 +57,18 @@
|
||||
them.
|
||||
-->
|
||||
<Border Classes="chip warn" Padding="4,0" IsVisible="{Binding RunsOnInsert}">
|
||||
<TextBlock Text="runs immediately" FontSize="8.5" />
|
||||
<TextBlock Text="runs immediately" FontSize="9.5" />
|
||||
</Border>
|
||||
<Border Classes="chip warn" Padding="4,0"
|
||||
IsVisible="{Binding Badge, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<TextBlock Text="{Binding Badge}" FontSize="8.5" />
|
||||
<TextBlock Text="{Binding Badge}" FontSize="9.5" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<!--
|
||||
Newlines shown as ⏎ rather than dropped. A three-line snippet flattened into one run of
|
||||
text reads as a single command, which is the thing being decided about on this row.
|
||||
-->
|
||||
<TextBlock Classes="mono" Text="{Binding Preview}" FontSize="9.5"
|
||||
<TextBlock Classes="mono" Text="{Binding Preview}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextFaint}" TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
@@ -76,7 +76,7 @@
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<TextBlock Grid.Row="1" Classes="hint" Text="{Binding EmptyMessage}" FontSize="11"
|
||||
<TextBlock Grid.Row="1" Classes="hint" Text="{Binding EmptyMessage}" FontSize="12"
|
||||
Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
TextAlignment="Center" MaxWidth="360"
|
||||
IsVisible="{Binding !HasVisible}" />
|
||||
@@ -109,12 +109,12 @@
|
||||
-->
|
||||
<TextBox Text="{Binding EditorCommand}" PlaceholderText="the command" AcceptsReturn="True"
|
||||
Height="140" TextWrapping="NoWrap" FontFamily="{StaticResource MonoFont}"
|
||||
FontSize="11" />
|
||||
FontSize="12" />
|
||||
<TextBox Text="{Binding EditorNotes}" PlaceholderText="notes" AcceptsReturn="True"
|
||||
Height="48" TextWrapping="Wrap" />
|
||||
<CheckBox IsChecked="{Binding EditorRunsOnInsert}"
|
||||
Content="Press Enter after inserting this" />
|
||||
<TextBlock Classes="hint" FontSize="10" TextWrapping="Wrap"
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||
Text="Off means the command is typed at the prompt and waits for you. That single Enter is the only thing standing between a saved command and a running one, so leave it off unless you meant it." />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="accent" Content="SAVE" Command="{Binding SaveCommand}" />
|
||||
@@ -125,23 +125,23 @@
|
||||
<!-- ============ The selected snippet ============ -->
|
||||
<StackPanel Spacing="6" IsVisible="{Binding !IsEditing}">
|
||||
|
||||
<TextBlock Classes="hint" FontSize="11"
|
||||
<TextBlock Classes="hint" FontSize="12"
|
||||
Text="Choose a snippet to see it in full and put it into a terminal."
|
||||
IsVisible="{Binding !HasSelection}" />
|
||||
|
||||
<StackPanel Spacing="6" IsVisible="{Binding HasSelection}">
|
||||
<TextBlock Classes="mono" Text="{Binding Selected.Label}" FontSize="12"
|
||||
<TextBlock Classes="mono" Text="{Binding Selected.Label}" FontSize="13"
|
||||
FontWeight="SemiBold" Foreground="{StaticResource Text}" TextWrapping="Wrap" />
|
||||
|
||||
<TextBlock Classes="label" Text="COMMAND" Margin="0,10,0,4" />
|
||||
<Border Background="{StaticResource Raised}" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="1" CornerRadius="4" Padding="8">
|
||||
<SelectableTextBlock Classes="mono" Text="{Binding Selected.Snippet.Command}"
|
||||
FontSize="9.5" Foreground="{StaticResource TextDim}"
|
||||
FontSize="10.5" Foreground="{StaticResource TextDim}"
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
|
||||
<TextBlock Classes="mono" Text="{Binding Selected.Snippet.Notes}" FontSize="10"
|
||||
<TextBlock Classes="mono" Text="{Binding Selected.Snippet.Notes}" FontSize="11"
|
||||
Foreground="{StaticResource TextFaint}" TextWrapping="Wrap" Margin="0,6,0,0"
|
||||
IsVisible="{Binding Selected.Snippet.Notes, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
IsVisible="{Binding SelectionRuns}" IsEnabled="{Binding CanInsert}"
|
||||
ToolTip.Tip="Types the command and presses Enter. Offered because this snippet is marked as one that runs." />
|
||||
|
||||
<TextBlock Classes="hint" FontSize="10" TextWrapping="Wrap" Margin="0,10,0,0"
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap" Margin="0,10,0,0"
|
||||
Text="Whatever is in the terminal receives this. Nothing here can tell whether that is a shell prompt, an editor, or a password prompt with the echo off — so check the tab before you insert." />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="5" VerticalAlignment="Center"
|
||||
Margin="0,0,14,0" IsVisible="{Binding SelectedTab, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<Ellipse Classes="dot" Classes.live="{Binding SelectedTab.IsLive}" VerticalAlignment="Center" />
|
||||
<TextBlock Classes="mono" Text="{Binding SelectedTab.Address}" FontSize="9.5"
|
||||
<TextBlock Classes="mono" Text="{Binding SelectedTab.Address}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
Everything the vault has to say. Trimmed rather than wrapped, because this bar is one line high and
|
||||
a message that grew it would move the whole window's contents up.
|
||||
-->
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Vault.Status}" FontSize="9.5"
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Vault.Status}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
<Border Grid.Row="0" Padding="14,0" BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
||||
<Grid ColumnDefinitions="*,Auto" VerticalAlignment="Center">
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="TEAMS" FontSize="11" FontWeight="SemiBold"
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="TEAMS" FontSize="12" FontWeight="SemiBold"
|
||||
LetterSpacing="1" Foreground="{StaticResource Text}" VerticalAlignment="Center" />
|
||||
<Button Grid.Column="1" Classes="ghost" Content="NEW"
|
||||
Command="{Binding NewTeamCommand}" IsEnabled="{Binding !IsBusy}" />
|
||||
@@ -41,18 +41,18 @@
|
||||
<DataTemplate x:DataType="vm:TeamRowViewModel">
|
||||
<StackPanel Spacing="2" Margin="0,3">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Name}" FontSize="12" FontWeight="Medium"
|
||||
<TextBlock Grid.Column="0" Text="{Binding Name}" FontSize="13" FontWeight="Medium"
|
||||
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Role}" FontSize="9"
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Role}" FontSize="10"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
<TextBlock Classes="hint" FontSize="10" Text="{Binding Detail}" />
|
||||
<TextBlock Classes="hint" FontSize="11" Text="{Binding Detail}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<TextBlock Classes="hint" FontSize="10" Margin="14,12" TextWrapping="Wrap"
|
||||
<TextBlock Classes="hint" FontSize="11" Margin="14,12" TextWrapping="Wrap"
|
||||
IsVisible="{Binding !HasTeams}"
|
||||
Text="No teams yet. A team is what makes a vault shareable: its vaults can be opened by every member you wrap a key to." />
|
||||
</StackPanel>
|
||||
@@ -65,7 +65,7 @@
|
||||
<TextBlock Classes="label" Text="NEW TEAM" />
|
||||
<TextBox PlaceholderText="Name" Text="{Binding NewTeamName}" />
|
||||
<TextBox PlaceholderText="slug-for-urls" Text="{Binding NewTeamSlug}" />
|
||||
<TextBlock Classes="hint" FontSize="9.5" TextWrapping="Wrap"
|
||||
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
||||
Text="The slug is lowercase letters, digits and hyphens, and has to be unique across this server." />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="accent" Content="CREATE" Command="{Binding CreateTeamCommand}"
|
||||
@@ -82,7 +82,7 @@
|
||||
<Grid Grid.Column="1" RowDefinitions="44,*,Auto">
|
||||
|
||||
<Border Grid.Row="0" Padding="14,0" BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
||||
<TextBlock Classes="mono" Text="{Binding SelectedTeam.Name}" FontSize="11" FontWeight="SemiBold"
|
||||
<TextBlock Classes="mono" Text="{Binding SelectedTeam.Name}" FontSize="12" FontWeight="SemiBold"
|
||||
LetterSpacing="1" Foreground="{StaticResource Text}" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
|
||||
@@ -100,13 +100,13 @@
|
||||
<DataTemplate x:DataType="vm:TeamMemberRowViewModel">
|
||||
<Grid ColumnDefinitions="*,150,Auto" Margin="0,3">
|
||||
<StackPanel Grid.Column="0" Spacing="2">
|
||||
<TextBlock Text="{Binding Name}" FontSize="12" FontWeight="Medium"
|
||||
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="Medium"
|
||||
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Classes="hint" FontSize="10" Text="{Binding Email}" />
|
||||
<TextBlock Classes="hint" FontSize="11" Text="{Binding Email}" />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="1" Classes="hint" FontSize="10" VerticalAlignment="Center"
|
||||
<TextBlock Grid.Column="1" Classes="hint" FontSize="11" VerticalAlignment="Center"
|
||||
Text="{Binding KeyState}" TextWrapping="Wrap" />
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Role}" FontSize="9"
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Role}" FontSize="10"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
|
||||
Margin="10,0,0,0" />
|
||||
</Grid>
|
||||
@@ -124,7 +124,7 @@
|
||||
ToolTip.Tip="Removes the selected member and withdraws every vault key they hold from this team. It blocks future reads only — anything already on their machine stays there, so rotate the credentials that matter." />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Classes="hint" FontSize="9.5" TextWrapping="Wrap"
|
||||
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
||||
IsVisible="{Binding CanAdministerSelected}"
|
||||
Text="Adding somebody lets the server serve them this team's vaults. It does not let them read one: a vault key can only be wrapped by a machine that already holds it, which is what SHARE KEY below does." />
|
||||
</StackPanel>
|
||||
@@ -146,15 +146,15 @@
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TeamVaultRowViewModel">
|
||||
<StackPanel Spacing="2" Margin="0,3">
|
||||
<TextBlock Text="{Binding Name}" FontSize="12" FontWeight="Medium"
|
||||
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="Medium"
|
||||
Foreground="{StaticResource Text}" />
|
||||
<TextBlock Classes="hint" FontSize="10" Text="{Binding State}" />
|
||||
<TextBlock Classes="hint" FontSize="11" Text="{Binding State}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<TextBlock Classes="hint" FontSize="10" TextWrapping="Wrap"
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||
IsVisible="{Binding !HasSelection}"
|
||||
Text="Select a team to see its vaults." />
|
||||
|
||||
@@ -167,20 +167,20 @@
|
||||
ToolTip.Tip="Withdraws the selected member's key to the selected vault. Blocks future reads only." />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Classes="hint" FontSize="9.5" TextWrapping="Wrap"
|
||||
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
||||
Text="Sharing verifies the recipient's key against the key log, which proves this server has been consistent with itself — not that the key is the right person's. Compare the fingerprint with them over a channel this server does not carry before sharing anything that matters." />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<TextBlock Grid.Row="1" Classes="hint" FontSize="11" Margin="20" TextWrapping="Wrap"
|
||||
<TextBlock Grid.Row="1" Classes="hint" FontSize="12" Margin="20" TextWrapping="Wrap"
|
||||
VerticalAlignment="Top" IsVisible="{Binding !HasSelection}"
|
||||
Text="Create a team on the left, or wait to be added to one. A team owns vaults; a vault's key is what makes its contents readable, and that key is handed out by people rather than by the server." />
|
||||
|
||||
<Border Grid.Row="2" Padding="14,10" BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0"
|
||||
IsVisible="{Binding Status, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<TextBlock Classes="hint" FontSize="10.5" Text="{Binding Status}" TextWrapping="Wrap" />
|
||||
<TextBlock Classes="hint" FontSize="11.5" Text="{Binding Status}" TextWrapping="Wrap" />
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
|
||||
@@ -5,14 +5,36 @@
|
||||
x:DataType="vm:MainWindowViewModel">
|
||||
|
||||
<!--
|
||||
The tab strip, above every screen.
|
||||
The window's tab strip: three fixed tabs, then one per open terminal.
|
||||
|
||||
Every tab is one pane in the one WebView, so switching is a single frame telling the page which pane to
|
||||
── IT IS NOT ONLY TERMINALS ANY MORE, and the type is still called TerminalTabs. ─────────────────────
|
||||
Vaults, SFTP and S3 sit at the head of the strip and are always there. The name stays because the strip
|
||||
is named in MainWindow, in the layout harness's height budget and in its own suite, and renaming a type
|
||||
to track what it grew into is a rename across four files that leaves the product identical. What the
|
||||
name now under-describes is written here instead.
|
||||
|
||||
── THE THREE FIXED TABS ─────────────────────────────────────────────────────────────────────────────
|
||||
None of the three can be closed, and that is the difference between them and a terminal rather than a
|
||||
styling choice. A terminal tab owns a shell and closing it ends that shell; these three own nothing —
|
||||
they are three places this window goes, and a close box on one would be asking whether to destroy a
|
||||
destination.
|
||||
|
||||
Vaults is first and is the only one with anything under it: the nav rail, and whichever of its screens
|
||||
the rail points at. SFTP and S3 were rail entries until this strip existed, and they moved because they
|
||||
are the two destinations you *stay in* while something runs. The rail is drawn only under Vaults; see
|
||||
MainWindowViewModel.IsVaultsTab for why that is expressed as a page test rather than as a surface.
|
||||
|
||||
S3 carries no count although the rail entry it replaces did. There is room for one, and a number on two
|
||||
of five tabs reads as a fact about those two rather than as the tab's own state — a terminal tab has
|
||||
nothing to count, and the eye reads the strip left to right expecting the same shape.
|
||||
|
||||
── THE TERMINAL TABS ────────────────────────────────────────────────────────────────────────────────
|
||||
Every one is one pane in the one WebView, so switching is a single frame telling the page which pane to
|
||||
show — nothing is created, nothing is destroyed, and the shell behind a hidden pane goes on running and
|
||||
goes on producing output. That is what makes tabs cost almost nothing here, and it is also why closing
|
||||
one is the only thing in this application that deliberately ends a session.
|
||||
|
||||
It spans the whole window rather than the hosts screen, which is what the strip is for: a connection you
|
||||
The strip spans the whole window rather than one screen, which is what it is for: a connection you
|
||||
opened stays visible and one click away while you are looking at a transfer, a key, or preferences.
|
||||
Clicking a tab switches the window's surface to that terminal — see MainWindowViewModel.ShellSurface.
|
||||
|
||||
@@ -26,23 +48,76 @@
|
||||
|
||||
── v2 ────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Tabs became pills: taller, rounded, each with its own outline, on the sidebar's surface rather than the
|
||||
chrome's. The design also puts a "Hosts" pill at the head of this strip and hides the sidebar while a
|
||||
session is showing, so that pill is the only way back. This window keeps the sidebar up instead — it is
|
||||
beside both surfaces rather than inside one — so the way back from a terminal is every destination at
|
||||
once rather than a single pill leading to one of them. That makes the design's pill redundant, and a
|
||||
redundant control on a strip this narrow is one the user has to rule out before finding the tabs.
|
||||
chrome's. The design puts a "Hosts" pill at the head of this strip and hides the sidebar while a session
|
||||
is showing, so that pill is the only way back. The three fixed tabs are that idea taken at its word and
|
||||
one step further: the rail is not hidden, but it belongs to the Vaults tab, and the head of the strip is
|
||||
where you go to get back to it.
|
||||
-->
|
||||
|
||||
<Border Height="42" Background="{StaticResource Sidebar}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
||||
|
||||
<!--
|
||||
Everything in one scrolling row: the tabs, then the button that opens another, then the sentence for
|
||||
when there are none. The strip stays rather than collapsing — a row of chrome that appears and
|
||||
disappears would move every screen up and down by 34 pixels each time the last tab closed.
|
||||
Everything in one scrolling row: the three fixed tabs, a rule, the terminals, the button that opens
|
||||
another, then the sentence for when there are none. The strip stays rather than collapsing — a row of
|
||||
chrome that appears and disappears would move every screen up and down by 42 pixels each time the last
|
||||
tab closed.
|
||||
-->
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<StackPanel Orientation="Horizontal" Margin="8,0,0,0">
|
||||
|
||||
<!--
|
||||
The three that are always here. Buttons with no close box, marked active from the shell's own
|
||||
state rather than holding a selection of their own — the same reason the terminal tabs below are
|
||||
buttons and not a TabStrip.
|
||||
|
||||
Each is lit by a different property and the three are exclusive by construction: IsVaultsTab is
|
||||
"a page, and not one of these two", and the other two are the existing IsTransfersShowing and
|
||||
IsBucketsShowing that both heads already use. Nothing here can light two at once.
|
||||
-->
|
||||
<Button Classes="flat tab fixed" Classes.active="{Binding IsVaultsTab}"
|
||||
Command="{Binding ShowVaultsCommand}"
|
||||
ToolTip.Tip="Your keychain: hosts, keys, pins, snippets and logs">
|
||||
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
|
||||
<TextBlock Text="▦" FontSize="13" VerticalAlignment="Center" />
|
||||
<TextBlock Text="Vaults" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button Classes="flat tab fixed" Classes.active="{Binding IsTransfersShowing}"
|
||||
Command="{Binding ShowFilesCommand}"
|
||||
CommandParameter="{x:Static vm:RemoteKind.Host}"
|
||||
ToolTip.Tip="Move files to and from a host over SFTP">
|
||||
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
|
||||
<TextBlock Text="⇅" FontSize="13" VerticalAlignment="Center" />
|
||||
<TextBlock Text="SFTP" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!--
|
||||
The same screen as SFTP over the same view model — an object store and an SFTP host are both an
|
||||
IRemoteFileStore — and a separate tab anyway, because which picker is offered is decided by the
|
||||
destination rather than by a toggle inside the screen. See ShowFiles, which also explains why
|
||||
pressing this while an SFTP session is open refuses instead of arriving.
|
||||
-->
|
||||
<Button Classes="flat tab fixed" Classes.active="{Binding IsBucketsShowing}"
|
||||
Command="{Binding ShowFilesCommand}"
|
||||
CommandParameter="{x:Static vm:RemoteKind.Bucket}"
|
||||
ToolTip.Tip="Objects in an S3-compatible bucket from your keychain">
|
||||
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
|
||||
<TextBlock Text="◳" FontSize="13" VerticalAlignment="Center" />
|
||||
<TextBlock Text="S3" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!--
|
||||
What separates the fixed tabs from the terminals. Without it the strip is five pills of the same
|
||||
shape and the user has to read all five to learn that three of them are places and two are
|
||||
machines. It is a rule rather than a gap because a gap at this width reads as the strip having
|
||||
been laid out carelessly.
|
||||
-->
|
||||
<Border Width="1" Height="18" Margin="6,0,10,0" VerticalAlignment="Center"
|
||||
Background="{StaticResource Border}" />
|
||||
|
||||
<ItemsControl ItemsSource="{Binding Tabs}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
@@ -92,7 +167,7 @@
|
||||
whole point of not blocking the window is that the user is somewhere else — the strip is
|
||||
the one piece of chrome that is on screen wherever that is.
|
||||
-->
|
||||
<TextBlock Text="{Binding Status}" VerticalAlignment="Center" FontSize="9.5"
|
||||
<TextBlock Text="{Binding Status}" VerticalAlignment="Center" FontSize="10.5"
|
||||
MaxWidth="180" TextTrimming="CharacterEllipsis"
|
||||
Foreground="{StaticResource TextFaint}"
|
||||
IsVisible="{Binding !HasSession}" />
|
||||
@@ -107,7 +182,7 @@
|
||||
Command="{Binding $parent[ItemsControl].((vm:MainWindowViewModel)DataContext).CloseTabCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
ToolTip.Tip="Closes this terminal and ends its shell. Middle-click the tab does the same.">
|
||||
<TextBlock Text="✕" FontSize="9" HorizontalAlignment="Center"
|
||||
<TextBlock Text="✕" FontSize="10" HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
@@ -131,7 +206,7 @@
|
||||
<Button Classes="flat tab plus" Width="30"
|
||||
Command="{Binding ToggleSearchCommand}"
|
||||
ToolTip.Tip="Open a connection · Ctrl+K">
|
||||
<TextBlock Text="+" FontSize="14" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
<TextBlock Text="+" FontSize="15" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Button>
|
||||
|
||||
<!--
|
||||
@@ -139,7 +214,7 @@
|
||||
carry a sentence at all: the rectangle below is a native child window, and anything Avalonia draws
|
||||
in it is drawn underneath.
|
||||
-->
|
||||
<TextBlock Classes="mono" FontSize="9.5"
|
||||
<TextBlock Classes="mono" FontSize="10.5"
|
||||
Text="no terminals open · press + or Ctrl+K, or choose a host and press Connect"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" Margin="12,0"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
same one the launcher icon carries, so the three cannot drift.
|
||||
-->
|
||||
<Border Width="20" Height="20" CornerRadius="6" Background="{StaticResource Accent}">
|
||||
<TextBlock Classes="mono" Text=">_" FontSize="9" FontWeight="Bold"
|
||||
<TextBlock Classes="mono" Text=">_" FontSize="10" FontWeight="Bold"
|
||||
Foreground="{StaticResource AccentInk}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
@@ -37,7 +37,7 @@
|
||||
The product's name is the one string in this bar that is not machine-shaped, so v2 sets it in the
|
||||
sans face while the address, the account and the fingerprint beside it stay monospaced.
|
||||
-->
|
||||
<TextBlock Text="DodoSSH" FontSize="13" FontWeight="SemiBold"
|
||||
<TextBlock Text="DodoSSH" FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource Text}" VerticalAlignment="Center" />
|
||||
<!--
|
||||
The design puts an organisation here — "dodotech / platform". There are no organisations: the
|
||||
@@ -48,7 +48,7 @@
|
||||
<Border Classes="chip" IsVisible="{Binding IsUnlocked}">
|
||||
<TextBlock Text="{Binding Vault.VaultName}" />
|
||||
</Border>
|
||||
<TextBlock Classes="mono" Text="{Binding AccountName}" FontSize="10"
|
||||
<TextBlock Classes="mono" Text="{Binding AccountName}" FontSize="11"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis" MaxWidth="220" />
|
||||
</StackPanel>
|
||||
@@ -72,18 +72,18 @@
|
||||
<Border Background="{StaticResource Field}" BorderBrush="{StaticResource BorderMid}"
|
||||
BorderThickness="1" CornerRadius="8" Padding="10,0">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="⌕" FontSize="12"
|
||||
<TextBlock Grid.Column="0" Text="⌕" FontSize="13"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||
<!--
|
||||
"Search or connect…", which is what it does: the palette connects on Enter. Not the design's
|
||||
wider promise of running a command — a snippet is inserted from its own screen, and a box that
|
||||
offered to run one would be offering something this palette does not do.
|
||||
-->
|
||||
<TextBlock Grid.Column="1" Text="Search or connect…" FontSize="12" Margin="8,0"
|
||||
<TextBlock Grid.Column="1" Text="Search or connect…" FontSize="13" Margin="8,0"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||
<Border Grid.Column="2" BorderBrush="{StaticResource BorderMid}" BorderThickness="1"
|
||||
CornerRadius="4" Padding="5,1" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" Text="CTRL K" FontSize="9"
|
||||
<TextBlock Classes="mono" Text="CTRL K" FontSize="10"
|
||||
Foreground="{StaticResource TextFaint}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
@@ -101,7 +101,7 @@
|
||||
IsVisible="{Binding IsUnlocked}"
|
||||
ToolTip.Tip="Green means a connection is held and nothing this machine changed is still waiting to be sent. It does not mean a colleague's change has arrived — that is pulled on a timer.">
|
||||
<Ellipse Classes="dot" Classes.live="{Binding IsFullySynced}" VerticalAlignment="Center" />
|
||||
<TextBlock Classes="mono" Text="{Binding SyncLabel}" FontSize="8.5" FontWeight="SemiBold"
|
||||
<TextBlock Classes="mono" Text="{Binding SyncLabel}" FontSize="9.5" FontWeight="SemiBold"
|
||||
LetterSpacing="1" Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
@@ -111,17 +111,17 @@
|
||||
<StackPanel Orientation="Horizontal" Spacing="2">
|
||||
<Button Classes="flat" Width="26" Height="24" Click="OnMinimise"
|
||||
ToolTip.Tip="Minimise">
|
||||
<TextBlock Text="–" FontSize="12" Foreground="{StaticResource TextDim}"
|
||||
<TextBlock Text="–" FontSize="13" Foreground="{StaticResource TextDim}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Button>
|
||||
<Button Classes="flat" Width="26" Height="24" Click="OnToggleMaximised"
|
||||
ToolTip.Tip="Maximise">
|
||||
<TextBlock Text="▢" FontSize="10" Foreground="{StaticResource TextDim}"
|
||||
<TextBlock Text="▢" FontSize="11" Foreground="{StaticResource TextDim}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Button>
|
||||
<Button Classes="flat close" Width="26" Height="24" Click="OnClose"
|
||||
ToolTip.Tip="Close DodoSSH. This ends every shell it has open.">
|
||||
<TextBlock Text="✕" FontSize="11" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
<TextBlock Text="✕" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
|
||||
@@ -79,21 +79,22 @@
|
||||
|
||||
<!--
|
||||
The screen names whichever remote it is offering, because since v2 it is reached as two
|
||||
destinations rather than one: SFTP and S3 are separate entries in the sidebar and this control
|
||||
draws both. A single "FILES" heading over a bucket picker would name neither of them.
|
||||
destinations rather than one: SFTP and S3 are separate tabs in the strip — they were rail entries
|
||||
until v3 — and this control draws both. A single "FILES" heading over a bucket picker would name
|
||||
neither of them.
|
||||
-->
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="SFTP" FontSize="11" FontWeight="SemiBold"
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="SFTP" FontSize="12" FontWeight="SemiBold"
|
||||
LetterSpacing="1" Foreground="{StaticResource Text}" VerticalAlignment="Center"
|
||||
Margin="0,0,12,0" IsVisible="{Binding ShowsHostPicker}" />
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="S3" FontSize="11" FontWeight="SemiBold"
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="S3" FontSize="12" FontWeight="SemiBold"
|
||||
LetterSpacing="1" Foreground="{StaticResource Text}" VerticalAlignment="Center"
|
||||
Margin="0,0,12,0" IsVisible="{Binding ShowsBucketPicker}" />
|
||||
|
||||
<!--
|
||||
The HOST / BUCKET pair that used to sit here is gone: which sort of remote this screen offers is
|
||||
now the destination you chose in the sidebar, and a toggle that silently moved you to the other
|
||||
one would leave the lit sidebar entry naming a screen you are no longer on. What sets it is
|
||||
ShowFiles on the shell, which is what the sidebar calls.
|
||||
now the tab you chose, and a toggle that silently moved you to the other one would leave the lit
|
||||
tab naming a screen you are no longer on. What sets it is ShowFiles on the shell, which is what
|
||||
the tab calls.
|
||||
-->
|
||||
|
||||
<ComboBox Grid.Column="2" ItemsSource="{Binding Hosts}"
|
||||
@@ -104,9 +105,9 @@
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:HostRowViewModel">
|
||||
<StackPanel>
|
||||
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="11"
|
||||
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="12"
|
||||
Foreground="{StaticResource Text}" />
|
||||
<TextBlock Classes="mono" Text="{Binding Address}" FontSize="9"
|
||||
<TextBlock Classes="mono" Text="{Binding Address}" FontSize="10"
|
||||
Foreground="{StaticResource TextFaint}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
@@ -121,9 +122,9 @@
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ObjectStoreRowViewModel">
|
||||
<StackPanel>
|
||||
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="11"
|
||||
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="12"
|
||||
Foreground="{StaticResource Text}" />
|
||||
<TextBlock Classes="mono" Text="{Binding Description}" FontSize="9"
|
||||
<TextBlock Classes="mono" Text="{Binding Description}" FontSize="10"
|
||||
Foreground="{StaticResource TextFaint}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
@@ -160,7 +161,7 @@
|
||||
mostly read before connecting; it is not now, because refusing to switch between SFTP and S3 while
|
||||
a session is live reports itself here, which is precisely when the chip is on screen.
|
||||
-->
|
||||
<TextBlock Grid.Column="6" Classes="hint" Text="{Binding Status}" FontSize="10.5"
|
||||
<TextBlock Grid.Column="6" Classes="hint" Text="{Binding Status}" FontSize="11.5"
|
||||
Margin="12,0,0,0" VerticalAlignment="Center" TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap" />
|
||||
|
||||
@@ -219,10 +220,10 @@
|
||||
<Button Classes="flat" Padding="3,1"
|
||||
Command="{Binding $parent[ItemsControl].((vm:TransfersViewModel)DataContext).GoLocalCommand}"
|
||||
CommandParameter="{Binding Path}">
|
||||
<TextBlock Classes="mono" Text="{Binding Name}" FontSize="10"
|
||||
<TextBlock Classes="mono" Text="{Binding Name}" FontSize="11"
|
||||
Foreground="{StaticResource TextDim}" />
|
||||
</Button>
|
||||
<TextBlock Classes="mono" Text="›" FontSize="10"
|
||||
<TextBlock Classes="mono" Text="›" FontSize="11"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
@@ -230,9 +231,9 @@
|
||||
</ItemsControl>
|
||||
|
||||
<Grid Grid.Row="2" ColumnDefinitions="2,*,84,110" Margin="0,2,12,4">
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="8.5" Margin="12,0,8,0" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="SIZE" FontSize="8.5" />
|
||||
<TextBlock Grid.Column="3" Classes="label" Text="MODIFIED" FontSize="8.5" />
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="9.5" Margin="12,0,8,0" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="SIZE" FontSize="9.5" />
|
||||
<TextBlock Grid.Column="3" Classes="label" Text="MODIFIED" FontSize="9.5" />
|
||||
</Grid>
|
||||
|
||||
<ListBox Grid.Row="3" x:Name="LocalList" ItemsSource="{Binding LocalEntries}"
|
||||
@@ -242,18 +243,18 @@
|
||||
<Grid ColumnDefinitions="2,*,84,110" Margin="0,5,12,5">
|
||||
<Border Grid.Column="0" Classes="rowmark" />
|
||||
<TextBlock Grid.Column="1" Classes="mono entry" Classes.dir="{Binding IsNavigable}"
|
||||
Text="{Binding Name}" FontSize="11"
|
||||
Text="{Binding Name}" FontSize="12"
|
||||
Margin="12,0,8,0" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Size}" FontSize="9.5"
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Size}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Modified}" FontSize="9.5"
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Modified}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<TextBlock Grid.Row="3" Classes="hint" FontSize="11" Margin="24" MaxWidth="260"
|
||||
<TextBlock Grid.Row="3" Classes="hint" FontSize="12" Margin="24" MaxWidth="260"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center"
|
||||
Text="Nothing in this folder. Use the trail above to go somewhere else."
|
||||
IsVisible="{Binding !HasLocalEntries}" />
|
||||
@@ -322,12 +323,12 @@
|
||||
BorderBrush="{StaticResource DangerSoft}" BorderThickness="0,0,0,1"
|
||||
IsVisible="{Binding IsConfirmingRemoteDeletion}">
|
||||
<StackPanel Spacing="7">
|
||||
<TextBlock Classes="heading" FontSize="13" TextWrapping="Wrap"
|
||||
<TextBlock Classes="heading" FontSize="14" TextWrapping="Wrap"
|
||||
Text="{Binding PendingRemoteDeletion.Question}" />
|
||||
<SelectableTextBlock Classes="mono" FontSize="10.5" TextWrapping="Wrap"
|
||||
<SelectableTextBlock Classes="mono" FontSize="11.5" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource Danger}"
|
||||
Text="{Binding PendingRemoteDeletion.FullPath}" />
|
||||
<TextBlock Foreground="{StaticResource WarnText}" FontSize="11" TextWrapping="Wrap"
|
||||
<TextBlock Foreground="{StaticResource WarnText}" FontSize="12" TextWrapping="Wrap"
|
||||
Text="{Binding PendingRemoteDeletion.Consequence}" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Classes="danger" Content="DELETE ON THE HOST"
|
||||
@@ -347,12 +348,12 @@
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:CrumbViewModel">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Classes="mono" Text="/" FontSize="10"
|
||||
<TextBlock Classes="mono" Text="/" FontSize="11"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||
<Button Classes="flat" Padding="3,1"
|
||||
Command="{Binding $parent[ItemsControl].((vm:TransfersViewModel)DataContext).GoRemoteCommand}"
|
||||
CommandParameter="{Binding Path}">
|
||||
<TextBlock Classes="mono" Text="{Binding Name}" FontSize="10"
|
||||
<TextBlock Classes="mono" Text="{Binding Name}" FontSize="11"
|
||||
Foreground="{StaticResource TextDim}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
@@ -373,10 +374,10 @@
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="3" ColumnDefinitions="2,*,84,110,92" Margin="0,2,12,4">
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="8.5" Margin="12,0,8,0" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="SIZE" FontSize="8.5" />
|
||||
<TextBlock Grid.Column="3" Classes="label" Text="MODIFIED" FontSize="8.5" />
|
||||
<TextBlock Grid.Column="4" Classes="label" Text="PERMS" FontSize="8.5" />
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="9.5" Margin="12,0,8,0" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="SIZE" FontSize="9.5" />
|
||||
<TextBlock Grid.Column="3" Classes="label" Text="MODIFIED" FontSize="9.5" />
|
||||
<TextBlock Grid.Column="4" Classes="label" Text="PERMS" FontSize="9.5" />
|
||||
</Grid>
|
||||
|
||||
<ListBox Grid.Row="4" x:Name="RemoteList" ItemsSource="{Binding RemoteEntries}"
|
||||
@@ -387,14 +388,14 @@
|
||||
<Border Grid.Column="0" Classes="rowmark" />
|
||||
<TextBlock Grid.Column="1" Classes="mono entry" Classes.dir="{Binding IsNavigable}"
|
||||
Classes.exec="{Binding IsExecutable}"
|
||||
Text="{Binding Name}" FontSize="11"
|
||||
Text="{Binding Name}" FontSize="12"
|
||||
Margin="12,0,8,0" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Size}" FontSize="9.5"
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Size}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Modified}" FontSize="9.5"
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Modified}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="4" Classes="mono perms" Classes.loose="{Binding IsWorldWritable}"
|
||||
Text="{Binding Permissions}" FontSize="9.5" VerticalAlignment="Center" />
|
||||
Text="{Binding Permissions}" FontSize="10.5" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
@@ -403,10 +404,10 @@
|
||||
<StackPanel Grid.Row="4" Spacing="10" Margin="24" MaxWidth="300"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
IsVisible="{Binding !HasRemoteEntries}">
|
||||
<TextBlock Classes="hint" FontSize="11" TextAlignment="Center"
|
||||
<TextBlock Classes="hint" FontSize="12" TextAlignment="Center"
|
||||
Text="Connect to a host to browse its files. This opens its own SFTP connection, so the host records a second login — it is not the same channel as a terminal."
|
||||
IsVisible="{Binding !IsConnected}" />
|
||||
<TextBlock Classes="hint" FontSize="11" TextAlignment="Center"
|
||||
<TextBlock Classes="hint" FontSize="12" TextAlignment="Center"
|
||||
Text="Nothing in this directory."
|
||||
IsVisible="{Binding IsConnected}" />
|
||||
</StackPanel>
|
||||
@@ -424,7 +425,7 @@
|
||||
<Border Grid.Row="0" Grid.RowSpan="5" IsHitTestVisible="False"
|
||||
Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
|
||||
BorderThickness="2" IsVisible="{Binding IsRemoteDropRefused}">
|
||||
<TextBlock Classes="hint" Text="Connect to a host first." FontSize="11"
|
||||
<TextBlock Classes="hint" Text="Connect to a host first." FontSize="12"
|
||||
Foreground="{StaticResource Danger}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
@@ -441,7 +442,7 @@
|
||||
<Border Grid.Row="0" Padding="12,7">
|
||||
<Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="label" Text="TRANSFERS" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="1" Classes="mono" FontSize="9.5" Margin="10,0,0,0"
|
||||
<TextBlock Grid.Column="1" Classes="mono" FontSize="10.5" Margin="10,0,0,0"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
|
||||
Text="one at a time · nothing lands at its final name until it is complete" />
|
||||
</Grid>
|
||||
@@ -449,7 +450,7 @@
|
||||
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<StackPanel>
|
||||
<TextBlock Classes="hint" FontSize="10.5" Margin="12,4,12,14"
|
||||
<TextBlock Classes="hint" FontSize="11.5" Margin="12,4,12,14"
|
||||
IsVisible="{Binding !HasTransfers}"
|
||||
Text="Nothing queued. Choose a file in either pane and press the arrow pointing the way you want it to go." />
|
||||
|
||||
@@ -457,12 +458,12 @@
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TransferRowViewModel">
|
||||
<Grid ColumnDefinitions="16,150,*,190,Auto" Margin="12,4">
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="{Binding Arrow}" FontSize="11"
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="{Binding Arrow}" FontSize="12"
|
||||
Foreground="{StaticResource Accent}" VerticalAlignment="Center" />
|
||||
<StackPanel Grid.Column="1" Margin="0,0,8,0">
|
||||
<TextBlock Classes="mono" Text="{Binding Name}" FontSize="10.5"
|
||||
<TextBlock Classes="mono" Text="{Binding Name}" FontSize="11.5"
|
||||
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Classes="mono" Text="{Binding Path}" FontSize="9"
|
||||
<TextBlock Classes="mono" Text="{Binding Path}" FontSize="10"
|
||||
Foreground="{StaticResource TextFaint}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
@@ -472,7 +473,7 @@
|
||||
Foreground="{StaticResource Accent}"
|
||||
Background="{StaticResource Raised}" />
|
||||
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Progress}" FontSize="9.5"
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Progress}" FontSize="10.5"
|
||||
Margin="10,0" VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Foreground="{StaticResource TextDim}" />
|
||||
@@ -517,7 +518,7 @@
|
||||
<TextBlock Classes="heading" Text="Check this host's fingerprint" />
|
||||
<TextBlock Classes="hint"
|
||||
Text="This host has not been seen before. Compare the fingerprint with what the operator published. Trusting it here also trusts it for terminals, and on your other machines." />
|
||||
<SelectableTextBlock Classes="mono" FontSize="11" Foreground="{StaticResource Text}"
|
||||
<SelectableTextBlock Classes="mono" FontSize="12" Foreground="{StaticResource Text}"
|
||||
TextWrapping="Wrap" Text="{Binding PendingHostKey.Fingerprint}" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Classes="accent" Content="TRUST AND CONNECT" Command="{Binding TrustHostKeyCommand}" />
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Classes="hint" Text="{Binding StatusMessage}" TextWrapping="Wrap" />
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||
<TextBlock Classes="hint" FontSize="12" TextWrapping="Wrap"
|
||||
Text="This works with no network: the salt and the wrapped key are already on this machine." />
|
||||
|
||||
<!--
|
||||
@@ -74,7 +74,7 @@
|
||||
the tab strip and the phone's own lock screen all paint green. -->
|
||||
<TextBlock Text="{Binding LiveSessionSummary}" Foreground="{StaticResource Live}"
|
||||
FontWeight="SemiBold" TextWrapping="Wrap" />
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||
<TextBlock Classes="hint" FontSize="12" TextWrapping="Wrap"
|
||||
Text="Locking closes the keychain, not your terminals: a job you started keeps running, and its output is waiting behind this screen. It also means this machine still holds an open, authenticated channel to those hosts — locked describes the keychain, not the connections. Quit DodoSSH to end them." />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
@@ -89,7 +89,7 @@
|
||||
<Border Height="1" Background="{StaticResource BorderSubtle}" />
|
||||
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||
<TextBlock Classes="hint" FontSize="12" TextWrapping="Wrap"
|
||||
Text="Forgotten your passphrase? Nothing can recover it — not even whoever runs the server. What you can do is reset this machine and sign in again; the keychain is on the server and comes back." />
|
||||
<Button Classes="ghost" Content="RESET THIS MACHINE"
|
||||
Command="{Binding SignOutCommand}" HorizontalAlignment="Left" />
|
||||
|
||||
@@ -11,7 +11,7 @@ internal sealed partial class UnlockCard : UserControl
|
||||
/// Where the keyboard goes when the vault is locked.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Exposed the way <see cref="HostSidebar.KeyboardTarget"/> is, and for the same reason: the window
|
||||
/// Exposed the way <see cref="HostsScreen.KeyboardTarget"/> is, and for the same reason: the window
|
||||
/// owns the focus policy — it has to take the keyboard off the terminal's native child window first —
|
||||
/// and the control it hands it to belongs to whichever screen is showing.
|
||||
/// </remarks>
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
-->
|
||||
<StackPanel Orientation="Horizontal" Margin="14,2" Spacing="7">
|
||||
<Ellipse Width="6" Height="6" Fill="{StaticResource Accent}" VerticalAlignment="Center" />
|
||||
<TextBlock Classes="mono" Text="{Binding HostsHeading}" FontSize="10"
|
||||
<TextBlock Classes="mono" Text="{Binding HostsHeading}" FontSize="11"
|
||||
Foreground="{StaticResource Text}" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
@@ -131,11 +131,11 @@
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:VaultChoiceViewModel">
|
||||
<TextBlock Text="{Binding Display}" FontSize="11" />
|
||||
<TextBlock Text="{Binding Display}" FontSize="12" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<TextBlock Classes="hint" FontSize="9.5" TextWrapping="Wrap"
|
||||
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
||||
Text="An item filed into a team's vault is readable by everyone holding that vault's key. It defaults to your own and never moves on its own." />
|
||||
</StackPanel>
|
||||
|
||||
@@ -158,21 +158,37 @@
|
||||
|
||||
<Border Grid.Row="0" Padding="14,0" Height="44"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
||||
<Grid ColumnDefinitions="Auto,Auto,*,Auto" VerticalAlignment="Center">
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="{Binding SectionTitle}" FontSize="11"
|
||||
<!--
|
||||
◆ THE SUMMARY IS THE COLUMN THAT GIVES WAY, and that is what stops this header overflowing again.
|
||||
|
||||
It used to be Auto,Auto,*,Auto — a fixed title, a fixed summary, slack, then the buttons — so the
|
||||
slack column was the only thing absorbing a change of width, and the strip fell off the right edge
|
||||
the moment the five buttons wanted more than it had. That is not hypothetical: it is why GENERATE
|
||||
lost the word KEY (see below), and it happened again the moment the type scale went up a point.
|
||||
Buying pixels by shortening a caption fixes one instance of a shape that keeps producing them.
|
||||
|
||||
So the summary sits in the star column and trims, and the buttons are Auto and always get their
|
||||
full width. That is the rule worth encoding rather than the pixels: a trimmed summary is a fact
|
||||
you can read by widening the window, and a clipped button is a dead end. The titlebar's search box
|
||||
is arranged this way for the same reason.
|
||||
-->
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center">
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="{Binding SectionTitle}" FontSize="12"
|
||||
FontWeight="SemiBold" LetterSpacing="1" Foreground="{StaticResource Text}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding SectionSummary}" FontSize="9.5"
|
||||
Foreground="{StaticResource TextFaint}" Margin="10,0,0,0" VerticalAlignment="Center" />
|
||||
<StackPanel Grid.Column="3" Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding SectionSummary}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextFaint}" Margin="10,0,10,0" VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6">
|
||||
<!--
|
||||
Always offered. Every category left on this screen is one things can be added to — the one
|
||||
that was not, HOST KEYS, is now its own screen, and a pin still cannot be typed in there
|
||||
either. See KnownHostsScreen.
|
||||
|
||||
GENERATE lost the word KEY when a fifth button arrived: five of these overflow an 1016-wide
|
||||
window's header by a few pixels, and the layout suite catches it. Its tooltip carries what the
|
||||
word did, and the two key buttons are adjacent, so which one generates is not in doubt.
|
||||
GENERATE carries no KEY, which it lost when a fifth button arrived and the strip could still
|
||||
overflow. The arrangement above is what keeps it inside now; the short caption stays because
|
||||
its tooltip carries what the word did and the two key buttons are adjacent, so which one
|
||||
generates is not in doubt.
|
||||
-->
|
||||
<Button Classes="ghost" Content="GENERATE" Command="{Binding NewGeneratedKeyCommand}"
|
||||
ToolTip.Tip="Makes a new key pair here, so the private half never becomes a file on this disk." />
|
||||
@@ -196,11 +212,11 @@
|
||||
-->
|
||||
<Grid Grid.Row="1" ColumnDefinitions="2,1.3*,74,*,88" Margin="0,6,14,6"
|
||||
IsVisible="{Binding HasVaultItems}">
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="8.5" LetterSpacing="1"
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="9.5" LetterSpacing="1"
|
||||
Margin="12,0,8,0" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="TYPE" FontSize="8.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="3" Classes="label" Text="DETAIL" FontSize="8.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="4" Classes="label" Text="STATE" FontSize="8.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="TYPE" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="3" Classes="label" Text="DETAIL" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="4" Classes="label" Text="STATE" FontSize="9.5" LetterSpacing="1" />
|
||||
</Grid>
|
||||
|
||||
<ListBox Grid.Row="2" x:Name="ItemList" Focusable="True"
|
||||
@@ -210,17 +226,17 @@
|
||||
<DataTemplate x:DataType="vm:VaultItemRowViewModel">
|
||||
<Grid ColumnDefinitions="2,1.3*,74,*,88" Margin="0,7,14,7">
|
||||
<Border Grid.Column="0" Classes="rowmark" />
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Name}" FontSize="11"
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Name}" FontSize="12"
|
||||
FontWeight="Medium" Foreground="{StaticResource Text}" Margin="12,0,8,0"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Type}" FontSize="9"
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Type}" FontSize="10"
|
||||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Detail}" FontSize="9.5"
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Detail}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextFaint}" Margin="0,0,8,0"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||||
<Border Grid.Column="4" Classes="chip warn" HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center" IsVisible="{Binding HasBadge}">
|
||||
<TextBlock Text="{Binding Badge}" FontSize="8.5" />
|
||||
<TextBlock Text="{Binding Badge}" FontSize="9.5" />
|
||||
</Border>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
@@ -231,7 +247,7 @@
|
||||
The empty state says which category is empty and what to do about it, rather than showing an empty
|
||||
grid that reads as a list still loading.
|
||||
-->
|
||||
<TextBlock Grid.Row="2" Classes="hint" Text="{Binding EmptySectionMessage}" FontSize="11"
|
||||
<TextBlock Grid.Row="2" Classes="hint" Text="{Binding EmptySectionMessage}" FontSize="12"
|
||||
Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
TextAlignment="Center" MaxWidth="320"
|
||||
IsVisible="{Binding !HasVaultItems}" />
|
||||
@@ -245,12 +261,12 @@
|
||||
<StackPanel Margin="14,16">
|
||||
|
||||
<!-- Nothing selected. -->
|
||||
<TextBlock Classes="hint" FontSize="11"
|
||||
<TextBlock Classes="hint" FontSize="12"
|
||||
Text="Choose something on the left to see what is known about it."
|
||||
IsVisible="{Binding !HasSelectedVaultItem}" />
|
||||
|
||||
<StackPanel Spacing="6" IsVisible="{Binding HasSelectedVaultItem}">
|
||||
<TextBlock Classes="mono" Text="{Binding SelectedVaultItem.Name}" FontSize="12"
|
||||
<TextBlock Classes="mono" Text="{Binding SelectedVaultItem.Name}" FontSize="13"
|
||||
FontWeight="SemiBold" Foreground="{StaticResource Text}" TextWrapping="Wrap" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Border Classes="chip">
|
||||
@@ -265,7 +281,7 @@
|
||||
<Border Background="{StaticResource Raised}" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="1" CornerRadius="4" Padding="8">
|
||||
<SelectableTextBlock Classes="mono" Text="{Binding SelectedVaultItem.Detail}"
|
||||
FontSize="9.5" Foreground="{StaticResource TextDim}"
|
||||
FontSize="10.5" Foreground="{StaticResource TextDim}"
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
|
||||
@@ -275,7 +291,7 @@
|
||||
nothing can exercise a credential without a host to exercise it against. Rather than five
|
||||
empty rows, this says what is missing in one line.
|
||||
-->
|
||||
<TextBlock Classes="hint" FontSize="9.5" Margin="0,12,0,0"
|
||||
<TextBlock Classes="hint" FontSize="10.5" Margin="0,12,0,0"
|
||||
Text="Keychain items record no author, no timestamps and no sharing yet, so there is nothing more to show here." />
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,14,0,0"
|
||||
@@ -336,7 +352,7 @@
|
||||
</StackPanel>
|
||||
|
||||
<TextBox Text="{Binding GenerateComment}" PlaceholderText="name — also the key's comment" />
|
||||
<TextBlock Classes="hint" FontSize="9.5" TextWrapping="Wrap"
|
||||
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
||||
Text="This is what the key is called here and what is written into it, so the line on a host says where it came from." />
|
||||
|
||||
<!--
|
||||
@@ -344,7 +360,7 @@
|
||||
bcrypt_pbkdf, which .NET has no primitive for — and the defence it buys is one this product
|
||||
already makes: a passphrase protects a key file on a disk, and this key is never on one.
|
||||
-->
|
||||
<TextBlock Classes="hint" FontSize="9.5" TextWrapping="Wrap" Margin="0,4,0,0"
|
||||
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap" Margin="0,4,0,0"
|
||||
Text="The key file itself has no passphrase. Your keychain passphrase is what protects it, and it never reaches the server in a form it can read." />
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,8,0,0">
|
||||
@@ -366,13 +382,13 @@
|
||||
<TextBox Text="{Binding KeyEditorPrivateKey}"
|
||||
PlaceholderText="-----BEGIN OPENSSH PRIVATE KEY-----"
|
||||
AcceptsReturn="True" Height="96" TextWrapping="NoWrap"
|
||||
FontFamily="{StaticResource MonoFont}" FontSize="10.5" />
|
||||
FontFamily="{StaticResource MonoFont}" FontSize="11.5" />
|
||||
<TextBox Text="{Binding KeyEditorPassphrase}"
|
||||
PlaceholderText="passphrase, if the key has one" PasswordChar="•" />
|
||||
<TextBox Text="{Binding KeyEditorPublicKey}" PlaceholderText="public half (optional)" />
|
||||
<TextBox Text="{Binding KeyEditorNotes}" PlaceholderText="notes" AcceptsReturn="True"
|
||||
Height="44" TextWrapping="Wrap" />
|
||||
<TextBlock Classes="hint" FontSize="9.5"
|
||||
<TextBlock Classes="hint" FontSize="10.5"
|
||||
Text="The key and its passphrase are encrypted here and never reach the server in a form it can read. Storing both together is the point of a keychain: on a disk the passphrase protects the key, and in here your keychain passphrase protects both." />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="accent" Content="SAVE" Command="{Binding SaveKeyCommand}" />
|
||||
@@ -399,7 +415,7 @@
|
||||
<TextBox Text="{Binding CredentialEditorPassword}" PlaceholderText="password" PasswordChar="•" />
|
||||
<TextBox Text="{Binding CredentialEditorNotes}" PlaceholderText="notes" AcceptsReturn="True"
|
||||
Height="44" TextWrapping="Wrap" />
|
||||
<TextBlock Classes="hint" FontSize="9.5"
|
||||
<TextBlock Classes="hint" FontSize="10.5"
|
||||
Text="Encrypted here and never sent to the server in a form it can read. Bind a host to this in the host's own editor, under how it authenticates." />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="accent" Content="SAVE" Command="{Binding SaveCredentialCommand}" />
|
||||
@@ -419,7 +435,7 @@
|
||||
<KeyBinding Gesture="Enter" Command="{Binding SaveTagCommand}" />
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
<TextBlock Classes="hint" FontSize="9.5"
|
||||
<TextBlock Classes="hint" FontSize="10.5"
|
||||
Text="Renaming a tag changes it everywhere at once. No host is rewritten — each one names this tag rather than repeating its name." />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="accent" Content="SAVE" Command="{Binding SaveTagCommand}" />
|
||||
@@ -452,11 +468,11 @@
|
||||
Said where the decision is made. Getting this wrong produces a DNS failure whose message
|
||||
mentions neither buckets nor this setting, which is the worst kind of thing to leave to a guess.
|
||||
-->
|
||||
<TextBlock Classes="hint" FontSize="9.5"
|
||||
<TextBlock Classes="hint" FontSize="10.5"
|
||||
Text="Off for Amazon S3. On for most self-hosted services — MinIO and Ceph have no wildcard DNS, so the bucket cannot be a subdomain." />
|
||||
<TextBox Text="{Binding BucketEditorNotes}" PlaceholderText="notes" AcceptsReturn="True"
|
||||
Height="44" TextWrapping="Wrap" />
|
||||
<TextBlock Classes="hint" FontSize="9.5"
|
||||
<TextBlock Classes="hint" FontSize="10.5"
|
||||
Text="Encrypted here, keys and endpoint alike, and never sent to the server in a form it can read. Pick this bucket on the Files screen to browse it." />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="accent" Content="SAVE" Command="{Binding SaveObjectStoreCommand}" />
|
||||
|
||||
Reference in New Issue
Block a user