Public Access
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.
228 lines
14 KiB
XML
228 lines
14 KiB
XML
<UserControl xmlns="https://github.com/avaloniaui"
|
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
|
x:Class="DodoSSH.Client.App.Views.TerminalTabs"
|
|
x:DataType="vm:MainWindowViewModel">
|
|
|
|
<!--
|
|
The window's tab strip: three fixed tabs, then one per open terminal.
|
|
|
|
── 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.
|
|
|
|
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.
|
|
|
|
Two of the design's header controls are still absent: SPLIT and FORWARDS. Splits would need a second
|
|
pane geometry the renderer does not have, and port forwarding does not exist in the SSH layer. Two
|
|
disabled buttons would teach nobody anything; see docs/design-import-gaps.md.
|
|
|
|
An ItemsControl of buttons rather than a TabStrip, because the selection lives on the shell — a tab
|
|
outlives the vault that opened it — and a strip that owned its own selection would be a second copy of
|
|
that state.
|
|
|
|
── v2 ────────────────────────────────────────────────────────────────────────────────────────────────
|
|
Tabs became pills: taller, rounded, each with its own outline, on the sidebar's surface rather than the
|
|
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 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" 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>
|
|
<ItemsPanelTemplate>
|
|
<StackPanel Orientation="Horizontal" />
|
|
</ItemsPanelTemplate>
|
|
</ItemsControl.ItemsPanel>
|
|
<ItemsControl.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:TerminalTabViewModel">
|
|
|
|
<!--
|
|
The close box is inside the tab, not beside it. Beside it, the two were siblings in a grid:
|
|
the cross was as tall as the strip and sat outside the tab's own background, so it read as a
|
|
divider between tabs rather than as part of one, and the tab it belonged to was ambiguous
|
|
for the tab to its right.
|
|
|
|
Nested buttons work, and it is worth knowing why rather than assuming. Avalonia's
|
|
Button.OnPointerPressed checks IsLeftButtonPressed, takes the pointer capture and marks the
|
|
event handled — so a left press on the cross does not also select the tab. It deliberately
|
|
does not handle any other button, which is exactly what lets a middle press bubble out of
|
|
the cross and reach the handler below.
|
|
|
|
Marked active on IsShowing rather than on IsSelected, which are not the same question. The
|
|
selection survives navigating away — that is what makes the strip a way back to a terminal —
|
|
so a tab lit while preferences filled the window would be a second "you are here" mark
|
|
pointing at something nobody can see. See TerminalTabViewModel.IsShowing.
|
|
-->
|
|
<Button Classes="flat tab"
|
|
Classes.active="{Binding IsShowing}"
|
|
Command="{Binding $parent[ItemsControl].((vm:MainWindowViewModel)DataContext).SelectTabCommand}"
|
|
CommandParameter="{Binding}"
|
|
PointerPressed="OnTabPointerPressed"
|
|
ToolTip.Tip="{Binding Address}">
|
|
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
|
|
<!--
|
|
Green while the shell behind this tab is running, grey while it is connecting and once
|
|
it has ended. The pane keeps its scrollback either way, which is usually why somebody is
|
|
still looking at a tab whose dot has gone out.
|
|
-->
|
|
<Ellipse Classes="dot" Width="5" Height="5" Classes.live="{Binding IsLive}"
|
|
VerticalAlignment="Center" />
|
|
<TextBlock Text="{Binding Label}" VerticalAlignment="Center" />
|
|
|
|
<!--
|
|
What a tab with no pane has to say for itself: "connecting…" while the handshake runs,
|
|
and the refusal once one has failed. It is here rather than only on the card because the
|
|
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="10.5"
|
|
MaxWidth="180" TextTrimming="CharacterEllipsis"
|
|
Foreground="{StaticResource TextFaint}"
|
|
IsVisible="{Binding !HasSession}" />
|
|
|
|
<!--
|
|
Always drawn, never on hover only. The strip has no other close affordance, and one
|
|
that appears when the pointer is already over the tab cannot be found by somebody
|
|
looking for it.
|
|
-->
|
|
<Button Classes="flat close inline" Width="16" Height="16" Padding="0"
|
|
VerticalAlignment="Center"
|
|
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="10" HorizontalAlignment="Center"
|
|
VerticalAlignment="Center" />
|
|
</Button>
|
|
</StackPanel>
|
|
</Button>
|
|
|
|
</DataTemplate>
|
|
</ItemsControl.ItemTemplate>
|
|
</ItemsControl>
|
|
|
|
<!--
|
|
Opens the quick-connect palette, which is also what Ctrl+K does — so the tooltip can say that
|
|
honestly, and there is one way to start a connection rather than two that have to agree.
|
|
|
|
Not a MenuFlyout offering "SSH" and "local shell", which is the nicer-looking answer and is not
|
|
verifiably safe here: this strip sits directly above the WebView's rectangle, and whether a popup
|
|
dropping into it composites above a native child window depends on whether Avalonia gives it its
|
|
own platform window. docs/platform-flags.md records what this project already paid for treating a
|
|
rendering claim as settled without a screenshot. The palette has no such question — opening it
|
|
collapses the terminal outright.
|
|
-->
|
|
<Button Classes="flat tab plus" Width="30"
|
|
Command="{Binding ToggleSearchCommand}"
|
|
ToolTip.Tip="Open a connection · Ctrl+K">
|
|
<TextBlock Text="+" FontSize="15" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
</Button>
|
|
|
|
<!--
|
|
Nothing open, and this is where that is said. It is also the only place near the terminal that can
|
|
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="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"
|
|
IsVisible="{Binding !HasTabs}" />
|
|
|
|
</StackPanel>
|
|
</ScrollViewer>
|
|
</Border>
|
|
|
|
</UserControl>
|