Public Access
The address at the head of the session column is the one place the desktop head prints the endpoint it actually dialled, and it was a TextBlock — which in Avalonia is a drawing of a string rather than a string. The next thing people do with an endpoint is paste it somewhere: into a second client, into a ticket, into the ssh command being written in the window beside this one. Retyping is how a digit goes missing from an address that is mostly digits. SelectableTextBlock, which is what the rest of this application already reaches for wherever a string is on screen to be taken away — a fingerprint, a public key, a recovery code, a snippet's command. Nothing else about the row moves: same font, weight, colour and trimming, same tooltip, same close button beside it. The sidebar is one control drawn on both surfaces, so SFTP gets it for the same reason it gets everything else in that block. The trimming and the selection do disagree at the long end, and the fix for that is not to drop the trimming: the ellipsis is there because a long account@host:port would otherwise push the close button off the edge of a panel whose whole point is that it can be got rid of. A drag reaches only the glyphs that were drawn, so a trimmed address has to be taken with select-all — and select-all takes the whole thing, because the ellipsis is a drawing and not a shorter value. That is the half a test can quietly lose, so it is the half the new test asserts, on the longest address the suite has rather than on one that fits and would pass either way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
187 lines
11 KiB
XML
187 lines
11 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.SessionSidebar"
|
|
x:Name="Root"
|
|
x:DataType="vm:MainWindowViewModel">
|
|
|
|
<!--
|
|
── v5b's session sidebar ────────────────────────────────────────────────────────────────────────────────
|
|
300 pixels, Sidebar background, a 1px left border — the design's own right-hand column on both the
|
|
terminal and the SFTP screen. It replaces the pin chip strip that used to sit above the terminal: see
|
|
MainWindowViewModel.ActiveTabPinnedPaths and OpenPinnedPathCommand, both reused here unchanged, and
|
|
MainWindow.axaml for where the old strip's Border used to live.
|
|
|
|
QUICK ACCESS is on both surfaces. SNIPS is the terminal's own — gated on <see cref="ShowsSnips"/>, which
|
|
the terminal usage in MainWindow.axaml sets true and the SFTP usage leaves false, rather than a second
|
|
copy of this file: the two sections share nothing surface-specific except which one is drawn at all.
|
|
|
|
Every row here is a command the shell already exposes for exactly this purpose — see
|
|
MainWindowViewModel.PinFolderFromSidebarCommand, AddSnippetFromSidebarCommand and InsertSnippetCommand —
|
|
so this control carries no logic of its own beyond the list it draws and the click it forwards.
|
|
|
|
── v5c-4: THE SESSION BLOCK AT THE HEAD, AND THE HEADER ROW THAT IS GONE ─────────────────────────────────
|
|
The 60-pixel host header that used to sit above the pane on both surfaces has been retired, and its two
|
|
contents moved up here: the address it printed, and the cross-surface button — "Open SFTP" from a
|
|
terminal, "Open terminal" from SFTP. Which of the two words it is and which command it runs are resolved
|
|
by the shell now rather than handed in from the two usage sites; see
|
|
MainWindowViewModel.SessionCrossSurfaceLabel and OpenOtherSurfaceCommand. The pane keeps that height.
|
|
|
|
The address is the fact the header row existed for, so it moves rather than disappears. It sits where the
|
|
QUICK ACCESS heading used to print the selected tab's short label — that label said less than the address
|
|
does and would be the same word twice beside it.
|
|
|
|
── AND THE COLUMN CLOSES ────────────────────────────────────────────────────────────────────────────────
|
|
300 pixels is a lot of a 1180-pixel window to give a list that is often two rows long, so the column
|
|
folds to a 34-pixel rail carrying the way back. A rail rather than nothing: a panel that vanishes without
|
|
trace is one people report as lost. Both halves live in this control and swap on
|
|
MainWindowViewModel.IsSessionSidebarOpen, so MainWindow.axaml's own "Auto" column takes whichever width
|
|
is showing without knowing anything about the state — and the pane beside it grows into what is freed.
|
|
-->
|
|
|
|
<Panel>
|
|
|
|
<!-- ============ THE RAIL, WHEN THE COLUMN IS CLOSED ============ -->
|
|
<!--
|
|
Painted and bordered like the open column so the closing reads as the same surface narrowing rather
|
|
than as one piece of furniture being swapped for another.
|
|
-->
|
|
<Border Width="34" Background="{StaticResource Sidebar}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="1,0,0,0"
|
|
IsVisible="{Binding !IsSessionSidebarOpen}">
|
|
<Button Classes="flat sidebargrip" VerticalAlignment="Top" Margin="0,20,0,0"
|
|
Command="{Binding ToggleSessionSidebarCommand}"
|
|
ToolTip.Tip="Show quick access, snips and the way across to the other surface">
|
|
<TextBlock Text="" FontFamily="{StaticResource IconFont}" FontSize="18"
|
|
Foreground="{StaticResource TextFaint}"
|
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
</Button>
|
|
</Border>
|
|
|
|
<!-- ============ THE COLUMN ============ -->
|
|
<Border Width="300" Background="{StaticResource Sidebar}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="1,0,0,0"
|
|
IsVisible="{Binding IsSessionSidebarOpen}">
|
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
|
<StackPanel Spacing="6" Margin="16,20">
|
|
|
|
<!-- ============ THE SESSION ============ -->
|
|
<!--
|
|
The address, and the button that closes the column. Both on one row, and the address is the
|
|
trimming one: a long account@host:port is exactly the string that would otherwise push the
|
|
close button off the edge of a panel whose whole point is that it can be got rid of.
|
|
|
|
SelectableTextBlock rather than TextBlock, which is what the rest of this application already
|
|
reaches for whenever a string is on screen to be taken somewhere else — a fingerprint, a public
|
|
key, a recovery code. This is the one place the shell prints the endpoint it actually dialled,
|
|
and the endpoint is a thing people paste: into another client, into a ticket, into the ssh
|
|
command they are writing beside this window. Retyping it off the screen is how a digit gets
|
|
dropped from an address that is mostly digits.
|
|
|
|
The trimming and the selection do disagree at the long end, and the ToolTip is why the
|
|
disagreement is tolerable rather than a trap: a drag reaches only the glyphs that were drawn,
|
|
so an address wearing an ellipsis has to be taken with select-all — Ctrl+A, then Ctrl+C — and
|
|
the hover already shows what the full string is, so nobody is copying half of one without
|
|
having been told there is a half.
|
|
-->
|
|
<Grid ColumnDefinitions="*,Auto" Margin="8,0,0,0">
|
|
<SelectableTextBlock Grid.Column="0" FontFamily="{StaticResource MonoFont}" FontWeight="Bold"
|
|
FontSize="12.5" Foreground="{StaticResource AccentText}"
|
|
VerticalAlignment="Center" TextTrimming="CharacterEllipsis"
|
|
Text="{Binding SessionAddress}" ToolTip.Tip="{Binding SessionAddress}" />
|
|
<Button Grid.Column="1" Classes="flat sidebargrip"
|
|
Command="{Binding ToggleSessionSidebarCommand}"
|
|
ToolTip.Tip="Close this column. The terminal takes the width, and the rail it leaves behind brings it back.">
|
|
<TextBlock Text="" FontFamily="{StaticResource IconFont}" FontSize="18"
|
|
Foreground="{StaticResource TextFaint}"
|
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
</Button>
|
|
</Grid>
|
|
|
|
<!--
|
|
The cross-surface button, stretched across the column rather than sized to its own caption: it
|
|
is the one action in this panel that is not a list row, and a 90-pixel button floating at the
|
|
left of a 300-pixel column would read as unfinished.
|
|
-->
|
|
<Button Classes="headerghost" HorizontalAlignment="Stretch" Margin="0,4,0,10"
|
|
Content="{Binding SessionCrossSurfaceLabel}"
|
|
Command="{Binding OpenOtherSurfaceCommand}" />
|
|
|
|
<!-- ============ QUICK ACCESS ============ -->
|
|
<TextBlock Classes="label" Text="QUICK ACCESS" FontSize="10" Margin="8,0" />
|
|
|
|
<ItemsControl ItemsSource="{Binding ActiveTabPinnedPaths}">
|
|
<ItemsControl.ItemTemplate>
|
|
<DataTemplate x:DataType="x:String">
|
|
<!--
|
|
A single level of $parent[ItemsControl] reaches the shell directly, the same way the old pin
|
|
strip's chips did: this ItemsControl's own DataContext is MainWindowViewModel, so one hop up
|
|
from the path's string DataContext lands on it.
|
|
-->
|
|
<Button Classes="sidebarrow"
|
|
Command="{Binding $parent[ItemsControl].((vm:MainWindowViewModel)DataContext).OpenPinnedPathCommand}"
|
|
CommandParameter="{Binding}"
|
|
ToolTip.Tip="{Binding}">
|
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
|
<TextBlock Text="" FontFamily="{StaticResource IconFont}" FontSize="15"
|
|
Foreground="{StaticResource AccentText}" VerticalAlignment="Center" />
|
|
<TextBlock FontFamily="{StaticResource MonoFont}" FontSize="12.5" Text="{Binding}"
|
|
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
|
</StackPanel>
|
|
</Button>
|
|
</DataTemplate>
|
|
</ItemsControl.ItemTemplate>
|
|
</ItemsControl>
|
|
|
|
<Button Classes="sidebaradd" Command="{Binding PinFolderFromSidebarCommand}"
|
|
ToolTip.Tip="Opens the active tab's host for editing, at QUICK ACCESS.">
|
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
|
<TextBlock Text="" FontFamily="{StaticResource IconFont}" FontSize="14"
|
|
VerticalAlignment="Center" />
|
|
<TextBlock Text="Pin folder" VerticalAlignment="Center" />
|
|
</StackPanel>
|
|
</Button>
|
|
|
|
<!-- ============ SNIPS (the terminal surface only) ============ -->
|
|
<StackPanel Spacing="6" Margin="0,16,0,0" IsVisible="{Binding #Root.ShowsSnips}">
|
|
|
|
<TextBlock Classes="label" Text="SNIPS" FontSize="10" Margin="8,0" />
|
|
|
|
<ItemsControl ItemsSource="{Binding SnippetsScreen.Visible}">
|
|
<ItemsControl.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:SnippetRowViewModel">
|
|
<Button Classes="sidebarrow"
|
|
Command="{Binding $parent[ItemsControl].((vm:MainWindowViewModel)DataContext).InsertSnippetCommand}"
|
|
CommandParameter="{Binding}"
|
|
ToolTip.Tip="{Binding Snippet.Command}">
|
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
|
<TextBlock Text="{}{ }" FontFamily="{StaticResource MonoFont}" FontWeight="Bold"
|
|
FontSize="11" Foreground="{StaticResource AccentText}"
|
|
VerticalAlignment="Center" />
|
|
<TextBlock FontFamily="{StaticResource MonoFont}" FontSize="12.5" Text="{Binding Label}"
|
|
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
|
</StackPanel>
|
|
</Button>
|
|
</DataTemplate>
|
|
</ItemsControl.ItemTemplate>
|
|
</ItemsControl>
|
|
|
|
<Button Classes="sidebaradd" Command="{Binding AddSnippetFromSidebarCommand}"
|
|
ToolTip.Tip="Opens the snippet editor.">
|
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
|
<TextBlock Text="" FontFamily="{StaticResource IconFont}" FontSize="14"
|
|
VerticalAlignment="Center" />
|
|
<TextBlock Text="Add Snip" VerticalAlignment="Center" />
|
|
</StackPanel>
|
|
</Button>
|
|
|
|
</StackPanel>
|
|
|
|
</StackPanel>
|
|
</ScrollViewer>
|
|
</Border>
|
|
|
|
</Panel>
|
|
|
|
</UserControl>
|