Public Access
Merge branch 'main' into the Android head
Main grew the screens the host-management plan called for — hosts, pins, snippets, logs, import, teams — plus the ObjectStore and Import projects behind two of them, and moved WindowsDeviceKeyStore into the desktop head's Platform folder. Five of those view models landed in a directory this branch had already moved, so they join the rest in DodoSSH.Client.Shell: git spotted the rename and put them there, and the namespaces followed. Shell picks up ObjectStore and Import as a result, which the Android head then gets transitively and will use neither of at first — scoped storage means there is no ~/.ssh/config to import, and file transfer is out of its first scope. Desktop suites green at 155 and 64.
This commit is contained in:
@@ -5,17 +5,20 @@
|
||||
x:DataType="vm:MainWindowViewModel">
|
||||
|
||||
<!--
|
||||
The tab strip above the terminal.
|
||||
The tab strip, above every screen.
|
||||
|
||||
Every tab 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.
|
||||
|
||||
Three of the design's header controls are absent: SPLIT, FORWARDS and SNIPPETS. Splits would need a
|
||||
second pane geometry the renderer does not have, port forwarding does not exist in the SSH layer, and
|
||||
there is no snippet item type in the vault. Three disabled buttons would teach nobody anything; see
|
||||
docs/design-import-gaps.md.
|
||||
It spans the whole window rather than the hosts screen, which is what the strip 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
|
||||
@@ -24,10 +27,15 @@
|
||||
|
||||
<Border Height="34" Background="{StaticResource Chrome}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
<ScrollViewer Grid.Column="0" HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Disabled">
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
|
||||
<ItemsControl ItemsSource="{Binding Tabs}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
@@ -36,57 +44,85 @@
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TerminalTabViewModel">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
|
||||
<Button Grid.Column="0" Classes="flat tab"
|
||||
Command="{Binding $parent[ItemsControl].((vm:MainWindowViewModel)DataContext).SelectTabCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
Classes.active="{Binding IsSelected}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
|
||||
<!--
|
||||
Green while the shell behind this tab is running, grey 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" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<!--
|
||||
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.
|
||||
|
||||
<Button Grid.Column="1" Classes="flat close" Width="20"
|
||||
VerticalAlignment="Stretch"
|
||||
Command="{Binding $parent[ItemsControl].((vm:MainWindowViewModel)DataContext).CloseTabCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
ToolTip.Tip="Closes this terminal and ends its shell.">
|
||||
<TextBlock Text="✕" FontSize="10" HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Button>
|
||||
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.
|
||||
-->
|
||||
<Button Classes="flat tab"
|
||||
Classes.active="{Binding IsSelected}"
|
||||
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 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" />
|
||||
|
||||
<!--
|
||||
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="9" HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
<!--
|
||||
Nothing open, and this is where that is said. The strip stays rather than collapsing — a row of
|
||||
chrome that appears and disappears moves the terminal up and down by 34 pixels every time the last
|
||||
tab closes — and 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 Grid.Column="1" Classes="mono" FontSize="9.5"
|
||||
Text="no terminals open · choose a host and press Connect, or Ctrl+K"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" Margin="12,0"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
IsVisible="{Binding !HasTabs}" />
|
||||
<!--
|
||||
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.
|
||||
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding SelectedTab.Address}" FontSize="9.5"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" Margin="12,0"
|
||||
TextTrimming="CharacterEllipsis" MaxWidth="280"
|
||||
IsVisible="{Binding HasTabs}" />
|
||||
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="14" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Button>
|
||||
|
||||
</Grid>
|
||||
<!--
|
||||
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="9.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>
|
||||
|
||||
Reference in New Issue
Block a user