Public Access
1315 lines
84 KiB
XML
1315 lines
84 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.TransfersScreen"
|
||
x:DataType="vm:TransfersViewModel">
|
||
|
||
<!--
|
||
File transfer: this machine on the left, the host on the right, and the queue underneath.
|
||
|
||
Two things about this screen are worth knowing before reading the markup. It has its own CONNECT
|
||
button, because SSH.NET cannot open an SFTP subsystem on a transport that is already carrying a shell —
|
||
so browsing a host's files is a second authenticated connection rather than a second channel, and
|
||
pretending otherwise would hide a second login from the person whose audit log it appears in. And the
|
||
panes are symmetrical apart from one column: PERMS is remote-only, because a POSIX mode is not a fact
|
||
about a file on the machine this client is developed on.
|
||
|
||
── v3 ────────────────────────────────────────────────────────────────────────────────────────────────
|
||
◆ THE CONNECT BAR IS GONE, and everything it held is inside the right-hand pane now.
|
||
|
||
It was a 44-pixel strip across the top of the screen holding a heading, a picker, a password box and a
|
||
button — chrome spanning both panes to configure one of them, and drawn at full width whether or not
|
||
anything was ever going to be connected. Underneath it sat a pane that was empty for exactly the same
|
||
reason, saying so in a sentence nobody had to be told twice.
|
||
|
||
So the pane says it instead. Disconnected, the right-hand half is an invitation where the listing would
|
||
be: what this screen is for, what pressing the button will cost, and the button. Connected, the
|
||
invitation is replaced by the listing and the connection states itself in a strip above it, beside the
|
||
control that closes it — which is the arrangement the two facts were always about. The heading the bar
|
||
carried is not reprinted anywhere: the tab in the strip says SFTP or S3, and it says it whether or not
|
||
this screen is showing.
|
||
|
||
What the design has and this does not: `sftp over bastion-eu`, which needs jump hosts the connection
|
||
layer has not got. See docs/design-import-gaps.md.
|
||
-->
|
||
|
||
<!--
|
||
══ THE TWO LISTINGS' COLUMNS ═════════════════════════════════════════════════════════════════════════
|
||
◆ They were fixed pixel widths, and fixed was wrong at both ends of the range this screen actually gets.
|
||
|
||
The numbers — 20/*/60/56 here and 20/*/56/52/48 across the arrow column — were chosen against the 204
|
||
pixels the session shell's own QUICK ACCESS sidebar leaves a pane at the window's minimum. Measured,
|
||
they were wrong in both directions at once:
|
||
|
||
· MODIFIED prints "2026-08-21 11:36", which needs 116 pixels, and never had more than 48. So the
|
||
column was truncated at every width this screen has ever been drawn at — a maximised window showed
|
||
"2026-08" beside a NAME column with four hundred empty pixels in it. PERMS needs 60 for a
|
||
ten-character mode and had 48; SIZE needs 58 for "999.9 GB" and had 56.
|
||
· At 204 the remote pane's NAME column measured **zero pixels wide**. Twenty for the glyph plus
|
||
56+52+48 for the numbers is 176 of the 180 a row has, so the file names — the one column a file
|
||
browser exists to show — were not drawn at all.
|
||
|
||
Star columns with a MaxWidth fix both, because the cap is what makes "share the width" mean the right
|
||
thing at each end. Wide, the three numeric columns stop growing at what their own text needs and NAME
|
||
keeps every remaining pixel. Narrow, nothing has a MinWidth to overflow with, so all four give ground
|
||
together and NAME — weighted heaviest — keeps enough to read a name by.
|
||
|
||
MODIFIED carries the extra weight (1.6) so that it reaches its cap first: it is the widest content and
|
||
the column somebody is most often actually reading. The local pane's NAME is 3* against the remote's 2*,
|
||
which is the remote's PERMS weight folded into it — that is what makes SIZE and MODIFIED come out the
|
||
same width on both sides of the arrow column, so the same amount of a timestamp is visible in each. See
|
||
Timestamps.Format for why both panes write one the same way in the first place; a shared format read at
|
||
two different widths would have made that argument for nothing.
|
||
|
||
◆ FOUR COPIES OF THIS, and they have to stay in step: a heading row and a row template per pane. Avalonia
|
||
cannot share one ColumnDefinitions instance between two Grids — each mutates its own computed sizes —
|
||
and SharedSizeGroup only applies to Auto columns. What catches a copy that drifts is measurement rather
|
||
than discipline: ScreenLayoutTests holds every cell against the width its own text needs
|
||
(TheListingColumnsShowTheirContentInFullWhenThereIsRoom), the headings against each other
|
||
(TheListingHeadingsDoNotRunIntoEachOther), and NAME against zero at the narrowest budget
|
||
(TheNameColumnSurvivesTheNarrowestBudget).
|
||
-->
|
||
|
||
<UserControl.Styles>
|
||
<!--
|
||
── v5b ─────────────────────────────────────────────────────────────────────────────────────────────
|
||
A directory used to be marked by colour alone — this application shipped no icon set when that rule
|
||
was written. It ships one now: v5b embeds Material Icons as IconFont, already spent on the nav rail,
|
||
the drawer and the session shell's own sidebar, and per SFTP.dc.html a file row draws a 14px glyph of
|
||
its own — folder for a directory, and the closest classic Material Icons has to the design's own
|
||
Material-Symbols-only "draft": insert_drive_file, a plain document rather than a page with a folded
|
||
corner. Recorded as a deviation rather than silently swapped, because "draft" is simply not a glyph
|
||
this font contains.
|
||
|
||
So NAME's colour stopped being the directory's own mark — the glyph is now — and become what the
|
||
design says instead: a folder is white and medium weight, a file is TextDim and regular. Blue is not
|
||
spent here any more; Info keeps meaning "a distinct scope" everywhere else it already did, and this
|
||
screen no longer borrows it.
|
||
|
||
A second colour still comes from the mode, and it still overrides the first rather than competing with
|
||
it: an executable is Live green in NAME — "live, yours, something that runs" — regardless of whether
|
||
the row beside it is asking for the folder colours or the file ones. PERMS keeps its own, separate
|
||
amber for a mode anyone may write to, on the same reasoning as before: NAME says what a row is, PERMS
|
||
says what is notable about how it is set, and the two never compete for one TextBlock.
|
||
-->
|
||
<Style Selector="TextBlock.entry">
|
||
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||
</Style>
|
||
<Style Selector="TextBlock.entry.dir">
|
||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||
<Setter Property="FontWeight" Value="Medium" />
|
||
</Style>
|
||
<!--
|
||
Declared after .dir on purpose — Avalonia has no specificity, and an executable that is also somehow
|
||
the target row would need this rule to win. It never is in practice; see SftpEntry, which reads the
|
||
execute bit off a file's own mode and nothing else's.
|
||
-->
|
||
<Style Selector="TextBlock.entry.exec">
|
||
<Setter Property="Foreground" Value="{StaticResource Live}" />
|
||
</Style>
|
||
|
||
<!--
|
||
The glyph beside NAME. One quiet colour regardless of folder or file — the design's own renderVals
|
||
gives both the same default iconFg — because the glyph itself already says which one a row is; the
|
||
colour underneath it is furniture, on the same TextFaint-family step TextBlock.fieldglyph already uses
|
||
for a mark meant to be skipped rather than read.
|
||
-->
|
||
<Style Selector="TextBlock.entryicon">
|
||
<Setter Property="FontFamily" Value="{StaticResource IconFont}" />
|
||
<Setter Property="FontSize" Value="14" />
|
||
<Setter Property="Foreground" Value="{StaticResource TextGhost}" />
|
||
<Setter Property="VerticalAlignment" Value="Center" />
|
||
<Setter Property="HorizontalAlignment" Value="Center" />
|
||
</Style>
|
||
|
||
<!--
|
||
SIZE and MODIFIED, both quieter than the design's own literal values ask for — rgb(93,95,116) and
|
||
rgb(110,112,137) sit a step below every text colour this palette already names. Per
|
||
design-notes/v5b-fidelity-notes.md's own instruction for values this close, the nearest existing steps
|
||
are reused rather than two new keys added for a difference nobody would see: TextGhost for SIZE, which
|
||
the design draws a shade darker than MODIFIED, and TextFaint — one step lighter — for MODIFIED, which
|
||
keeps the two in the same order the design puts them in even though neither is the design's exact hex.
|
||
-->
|
||
<Style Selector="TextBlock.entrysize">
|
||
<Setter Property="Foreground" Value="{StaticResource TextGhost}" />
|
||
</Style>
|
||
<Style Selector="TextBlock.entrydate">
|
||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||
</Style>
|
||
|
||
<!--
|
||
Faint by default, as this column has always been: a mode is there so its absence would be noticed. It
|
||
steps up to amber only when it has something to say, which is the whole reason the default is quiet.
|
||
-->
|
||
<Style Selector="TextBlock.perms">
|
||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||
</Style>
|
||
<!--
|
||
Warn rather than WarnText, which is the muted amber a warning card writes its sentences in. At 9.5px
|
||
against TextFaint that one is a shade, not a signal, and a marker nobody notices is the same as no
|
||
marker at all.
|
||
-->
|
||
<Style Selector="TextBlock.perms.loose">
|
||
<Setter Property="Foreground" Value="{StaticResource Warn}" />
|
||
</Style>
|
||
|
||
<!--
|
||
── v5b: the TRANSFERS strip's own progress track ────────────────────────────────────────────────────
|
||
5px, 3px radius, Chip's own fill under an AccentGradient indicator — the design's own slim bar. A real
|
||
0% is what a queued transfer draws with this: the indicator's width is nothing, which reads exactly as
|
||
the design's own queued sample row does (a bar the same colour as its own track). No indeterminate
|
||
animation is drawn for it — a queued transfer really is at zero, so zero is the honest thing to show,
|
||
not a state this screen has to invent furniture for.
|
||
-->
|
||
<Style Selector="ProgressBar.transferbar">
|
||
<Setter Property="Height" Value="5" />
|
||
<Setter Property="MinHeight" Value="5" />
|
||
<Setter Property="CornerRadius" Value="3" />
|
||
<Setter Property="Background" Value="{StaticResource Chip}" />
|
||
<Setter Property="Foreground" Value="{StaticResource AccentGradient}" />
|
||
</Style>
|
||
|
||
<!--
|
||
The strip's own right-aligned status word. Quiet by default — a stopped transfer's "stopped" gets no
|
||
rule of its own and falls through to this — and three states get their own colour: in motion, kept, and
|
||
refused. See TransferRowViewModel.StatusWord for what word each state actually prints.
|
||
-->
|
||
<Style Selector="TextBlock.transferstatus">
|
||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||
</Style>
|
||
<Style Selector="TextBlock.transferstatus.running">
|
||
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||
</Style>
|
||
<Style Selector="TextBlock.transferstatus.done">
|
||
<Setter Property="Foreground" Value="{StaticResource Live}" />
|
||
</Style>
|
||
<Style Selector="TextBlock.transferstatus.failed">
|
||
<Setter Property="Foreground" Value="{StaticResource Danger}" />
|
||
</Style>
|
||
</UserControl.Styles>
|
||
|
||
<Grid RowDefinitions="*,Auto">
|
||
|
||
<!-- ============ The two panes ============ -->
|
||
<Grid Grid.Row="0" ColumnDefinitions="*,64,*">
|
||
|
||
<!-- ==== This machine ==== -->
|
||
<!--
|
||
AllowDrop on the pane rather than on the list, because an empty directory lays its ListBox out at
|
||
zero height behind the empty-state sentence — a handler on the list would have nothing to hit.
|
||
This side takes remote rows only; see TransfersScreen.axaml.cs.
|
||
-->
|
||
<Grid Grid.Column="0" x:Name="LocalPane" RowDefinitions="Auto,Auto,Auto,*" DragDrop.AllowDrop="True">
|
||
|
||
<!--
|
||
── v5b ─────────────────────────────────────────────────────────────────────────────────────────
|
||
Per SFTP.dc.html: a 10px tracked-out label beside the current path in mono 13.5, and the pane's
|
||
affordances restyled to quiet glyphs rather than dropped. "THIS MACHINE" becomes the design's own
|
||
"LOCAL" — nothing in this file's own comments ever argued for that particular wording over the
|
||
design's, unlike HOST/BUCKET across the arrow column, which keeps its own words for a reason
|
||
recorded where it is drawn.
|
||
|
||
UP is arrow_upward here and arrow_downward on the host/bucket pane below, matching the design's own
|
||
two glyphs exactly rather than picking one "up a directory" icon for both — both buttons do the same
|
||
thing (go to the parent directory), and the pair reading differently is the design's own choice
|
||
faithfully carried over rather than a functional difference invented to justify it.
|
||
-->
|
||
<!--
|
||
◆ THE PATH SITS IN THE STAR COLUMN, ALONE, AND THAT PLACEMENT IS LOAD-BEARING.
|
||
|
||
TextTrimming only acts when measure hands the block a finite width, and a horizontal StackPanel
|
||
never does — it measures every child at infinity, takes the full answer, and an Auto grid column
|
||
passes that on. With the path in a StackPanel beside the label, a directory deep enough — the
|
||
remote pane meets one on any real host, this pane on any machine whose profile path is long —
|
||
made the header wider than the pane and pushed the icon buttons past the window's own edge. The
|
||
layout suite caught it at the 472-pixel session budget, on the one machine whose home directory
|
||
was long enough to arm it. Star column: bounded width, working ellipsis, buttons that stay.
|
||
-->
|
||
<Border Grid.Row="0" Padding="0,0,0,10" BorderThickness="0">
|
||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||
<TextBlock Grid.Column="0" Classes="label" FontSize="10" Text="LOCAL"
|
||
VerticalAlignment="Center" Margin="0,0,12,0" />
|
||
<TextBlock Grid.Column="1" Classes="mono" FontSize="13.5" FontWeight="Medium"
|
||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center"
|
||
HorizontalAlignment="Left"
|
||
Text="{Binding LocalPath}" TextTrimming="CharacterEllipsis"
|
||
ToolTip.Tip="{Binding LocalPath}" />
|
||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="4" Margin="12,0,0,0">
|
||
<!--
|
||
The drives, because the breadcrumb cannot reach them: above C:\ is a list rather than a
|
||
directory. Without this the pane is stuck on whichever drive the user profile is on. Chips
|
||
rather than the pane's own quiet icon buttons, because a drive is named rather than a single
|
||
glyph — "C:" has no honest Material Icons equivalent.
|
||
-->
|
||
<ItemsControl ItemsSource="{Binding LocalRoots}" VerticalAlignment="Center">
|
||
<ItemsControl.ItemsPanel>
|
||
<ItemsPanelTemplate>
|
||
<StackPanel Orientation="Horizontal" Spacing="4" />
|
||
</ItemsPanelTemplate>
|
||
</ItemsControl.ItemsPanel>
|
||
<ItemsControl.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:CrumbViewModel">
|
||
<Button Classes="panechip" Content="{Binding Name}"
|
||
Command="{Binding $parent[ItemsControl].((vm:TransfersViewModel)DataContext).GoLocalCommand}"
|
||
CommandParameter="{Binding Path}" />
|
||
</DataTemplate>
|
||
</ItemsControl.ItemTemplate>
|
||
</ItemsControl>
|
||
<Button Classes="paneicon" Command="{Binding LocalUpCommand}"
|
||
ToolTip.Tip="Up one directory">
|
||
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="16" Text="" />
|
||
</Button>
|
||
<Button Classes="paneicon" Command="{Binding RefreshLocalCommand}"
|
||
ToolTip.Tip="Refresh">
|
||
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="16" Text="" />
|
||
</Button>
|
||
</StackPanel>
|
||
</Grid>
|
||
</Border>
|
||
|
||
<ItemsControl Grid.Row="1" ItemsSource="{Binding LocalTrail}" Margin="12,6,12,4">
|
||
<ItemsControl.ItemsPanel>
|
||
<ItemsPanelTemplate>
|
||
<WrapPanel Orientation="Horizontal" />
|
||
</ItemsPanelTemplate>
|
||
</ItemsControl.ItemsPanel>
|
||
<ItemsControl.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:CrumbViewModel">
|
||
<StackPanel Orientation="Horizontal">
|
||
<Button Classes="flat" Padding="3,1"
|
||
Command="{Binding $parent[ItemsControl].((vm:TransfersViewModel)DataContext).GoLocalCommand}"
|
||
CommandParameter="{Binding Path}">
|
||
<TextBlock Classes="mono" Text="{Binding Name}" FontSize="11"
|
||
Foreground="{StaticResource TextDim}" />
|
||
</Button>
|
||
<TextBlock Classes="mono" Text="›" FontSize="11"
|
||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||
</StackPanel>
|
||
</DataTemplate>
|
||
</ItemsControl.ItemTemplate>
|
||
</ItemsControl>
|
||
|
||
<!--
|
||
v5b: 20 for the glyph column rather than the design's bare icon with no gutter, because a Material
|
||
glyph centred with no reserved width drifts as the font's own advance width varies between folder
|
||
and insert_drive_file. 60/56 for SIZE/MODIFIED are narrower than the design's literal 80/78 — see
|
||
the row template below for why the whole column set was chosen against 204 pixels rather than the
|
||
design's own 1920-pixel canvas.
|
||
-->
|
||
<Grid Grid.Row="2" Margin="0,2,10,4">
|
||
<Grid.ColumnDefinitions>
|
||
<ColumnDefinition Width="20" />
|
||
<ColumnDefinition Width="3*" />
|
||
<ColumnDefinition Width="*" MaxWidth="66" />
|
||
<ColumnDefinition Width="1.6*" MaxWidth="124" />
|
||
</Grid.ColumnDefinitions>
|
||
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="9.5" Margin="8,0,8,0" />
|
||
<TextBlock Grid.Column="2" Classes="label" Text="SIZE" FontSize="9.5" Margin="8,0,0,0"
|
||
HorizontalAlignment="Right" TextTrimming="CharacterEllipsis" />
|
||
<TextBlock Grid.Column="3" Classes="label" Text="MODIFIED" FontSize="9.5" Margin="8,0,0,0"
|
||
HorizontalAlignment="Right" TextTrimming="CharacterEllipsis" />
|
||
</Grid>
|
||
|
||
<!--
|
||
h38, radius 7, Track for hover and selected — see ListBox.filerows in App.axaml — and no rowmark:
|
||
the design has nothing to distinguish "selected" from "hovered" beyond which one is currently true,
|
||
and :selected alone already carries that once the pointer moves on.
|
||
-->
|
||
<ListBox Grid.Row="3" x:Name="LocalList" Classes="filerows" ItemsSource="{Binding LocalEntries}"
|
||
SelectedItem="{Binding SelectedLocalEntry}">
|
||
<ListBox.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:LocalEntryRowViewModel">
|
||
<Grid Height="38" Margin="6,0,10,0">
|
||
<Grid.ColumnDefinitions>
|
||
<ColumnDefinition Width="24" />
|
||
<ColumnDefinition Width="20" />
|
||
<ColumnDefinition Width="3*" />
|
||
<ColumnDefinition Width="*" MaxWidth="66" />
|
||
<ColumnDefinition Width="1.6*" MaxWidth="124" />
|
||
</Grid.ColumnDefinitions>
|
||
<!-- The remote pane's own tick, on this side and for the same reasons. -->
|
||
<CheckBox Grid.Column="0" MinWidth="0" VerticalAlignment="Center"
|
||
IsChecked="{Binding IsChecked}"
|
||
Click="OnLocalRowChecked" />
|
||
<TextBlock Grid.Column="1" Classes="entryicon" Text="{Binding IconGlyph}" />
|
||
<TextBlock Grid.Column="2" Classes="mono entry" Classes.dir="{Binding IsNavigable}"
|
||
Text="{Binding Name}" FontSize="13.5"
|
||
Margin="10,0,8,0" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" />
|
||
<TextBlock Grid.Column="3" Classes="mono entrysize" Text="{Binding Size}" FontSize="12"
|
||
Margin="8,0,0,0" HorizontalAlignment="Right" VerticalAlignment="Center" />
|
||
<TextBlock Grid.Column="4" Classes="mono entrydate" Text="{Binding Modified}" FontSize="12"
|
||
Margin="8,0,0,0" HorizontalAlignment="Right" VerticalAlignment="Center" />
|
||
</Grid>
|
||
</DataTemplate>
|
||
</ListBox.ItemTemplate>
|
||
</ListBox>
|
||
|
||
<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}" />
|
||
|
||
<!--
|
||
The drop highlight, over the whole pane and last so it is on top.
|
||
|
||
IsHitTestVisible="False" is not optional. An overlay that takes part in hit testing swallows the
|
||
DragOver events underneath it the moment it appears — so the pointer leaves, the highlight never
|
||
clears, and the drop lands nowhere.
|
||
-->
|
||
<Border Grid.Row="0" Grid.RowSpan="4" IsHitTestVisible="False"
|
||
Background="{StaticResource AccentWash}" BorderBrush="{StaticResource Accent}"
|
||
BorderThickness="2" IsVisible="{Binding IsLocalDropTarget}" />
|
||
|
||
</Grid>
|
||
|
||
<!-- ==== The two directions ==== -->
|
||
<Border Grid.Column="1" Background="{StaticResource Sidebar}"
|
||
BorderBrush="{StaticResource Border}" BorderThickness="1,0">
|
||
<StackPanel VerticalAlignment="Center" Spacing="10" Margin="6">
|
||
<!--
|
||
Pointing at the pane the file is going to, which is the only reading that survives the panes
|
||
being side by side: the left-hand pane is this machine, so an upload points right.
|
||
-->
|
||
<Button Classes="ghost" Content="→" HorizontalAlignment="Stretch"
|
||
HorizontalContentAlignment="Center"
|
||
Command="{Binding UploadCommand}" IsEnabled="{Binding CanUpload}"
|
||
ToolTip.Tip="Upload the selected file to the directory showing on the host" />
|
||
<Button Classes="ghost" Content="←" HorizontalAlignment="Stretch"
|
||
HorizontalContentAlignment="Center"
|
||
Command="{Binding DownloadCommand}" IsEnabled="{Binding CanDownload}"
|
||
ToolTip.Tip="Download the selected file into the folder showing on this machine" />
|
||
</StackPanel>
|
||
</Border>
|
||
|
||
<!-- ==== The host ==== -->
|
||
<!--
|
||
Seven rows. Two of them are furniture over a listing rather than part of it: the connection's own
|
||
strip, and the row of things that can be done to whichever row is chosen. Both are inside this pane
|
||
rather than above both panes because both are about this one and nothing else. The two questions —
|
||
deleting, and renaming or moving — share a single row, since only one of the two is ever on screen.
|
||
-->
|
||
<Grid Grid.Column="2" x:Name="RemotePane" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,*"
|
||
DragDrop.AllowDrop="True">
|
||
|
||
<!--
|
||
── v5b ─────────────────────────────────────────────────────────────────────────────────────────
|
||
HOST/BUCKET keep their own words rather than adopting the design's generic "REMOTE": the comment
|
||
this replaced already carried a documented honesty reason — the pair is the difference between a
|
||
directory tree and a flat namespace with inferred folders — and that reason still holds, so only
|
||
the style moves to the design's 10px tracked label, not the wording.
|
||
|
||
UP is arrow_downward here, matching arrow_upward on the local pane above per the design's own two
|
||
distinct glyphs; see that pane's own remark on why the pair differs without the actions differing.
|
||
-->
|
||
<!--
|
||
◆ THE PATH IS TYPED HERE, and that is what the star column is now holding.
|
||
|
||
It was a TextBlock printing where the pane was. A breadcrumb only reaches what is under the
|
||
directory already listed, so crossing a host's filesystem was a walk to the common ancestor and
|
||
back down — a click and a round trip a level — and a path somebody already knew could not be
|
||
entered at all. Enter goes; Escape puts back what the pane is really showing. See
|
||
TransfersViewModel.RemotePathDraft for why the box holds its own value rather than RemotePath
|
||
itself, and SftpPath.Resolve for what an entry that is not absolute means.
|
||
|
||
Still alone in the star column, for the reason the local pane's header remark spells out: this box
|
||
has to be bounded, and an Auto column beside a StackPanel would measure it at its content's width
|
||
and push the buttons past the pane's edge.
|
||
|
||
DELETE has left this row. It acts on the selected row rather than on the pane, and it is one of
|
||
three that do now — see the item row below, which is where the three of them are together and
|
||
where there is room for three rather than for one.
|
||
-->
|
||
<Border Grid.Row="0" Padding="0,0,0,10" BorderThickness="0">
|
||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="12" VerticalAlignment="Center"
|
||
Margin="0,0,8,0">
|
||
<TextBlock Classes="label" FontSize="10" Text="HOST" VerticalAlignment="Center"
|
||
IsVisible="{Binding ShowsHostPicker}" />
|
||
<TextBlock Classes="label" FontSize="10" Text="BUCKET" VerticalAlignment="Center"
|
||
IsVisible="{Binding ShowsBucketPicker}" />
|
||
</StackPanel>
|
||
<TextBox Grid.Column="1" Classes="pathbox" VerticalAlignment="Center"
|
||
Text="{Binding RemotePathDraft}" IsEnabled="{Binding IsConnected}"
|
||
PlaceholderText="path"
|
||
ToolTip.Tip="Type a path and press Enter. A relative path is taken from here; ~ is the account's home directory.">
|
||
<!--
|
||
One word, because the only time this placeholder shows is on a pane with nothing connected,
|
||
where the box is disabled and the invitation below is doing the explaining. "/path/to/
|
||
somewhere" needs 146 pixels and had 91 at the session shell's narrowest, so it drew as a
|
||
path cut off mid-segment. What the box is for is in the tooltip, where there is room for it.
|
||
-->
|
||
<TextBox.KeyBindings>
|
||
<KeyBinding Gesture="Enter" Command="{Binding GoToTypedRemotePathCommand}" />
|
||
<KeyBinding Gesture="Escape" Command="{Binding RevertTypedRemotePathCommand}" />
|
||
</TextBox.KeyBindings>
|
||
</TextBox>
|
||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="4" Margin="8,0,0,0">
|
||
<Button Classes="paneicon" Command="{Binding RemoteUpCommand}"
|
||
IsEnabled="{Binding IsConnected}" ToolTip.Tip="Up one directory">
|
||
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="16" Text="" />
|
||
</Button>
|
||
<Button Classes="paneicon" Command="{Binding RefreshRemoteCommand}"
|
||
IsEnabled="{Binding IsConnected}" ToolTip.Tip="Refresh">
|
||
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="16" Text="" />
|
||
</Button>
|
||
</StackPanel>
|
||
</Grid>
|
||
</Border>
|
||
|
||
<!--
|
||
◆ WHAT IS OPEN, AND WHAT CLOSES IT. Only while something is.
|
||
|
||
v5b drops the account-at-host chip this row used to carry beside DISCONNECT: the session shell
|
||
prints the very same address beside this screen — in the sidebar's own session block since v5c-4
|
||
retired the header row that printed it above — see MainWindowViewModel.SessionAddress,
|
||
which already reads Transfers.ConnectedTo on the SFTP surface — and repeating it here stopped being
|
||
information and started being the thing squeezing DISCONNECT off the edge. At the session shell's
|
||
own narrower budget this pane is 204 pixels wide once QUICK ACCESS is showing beside it, where the
|
||
170-pixel chip this row used to carry would have taken most of that on its own.
|
||
|
||
The status line still is not here, for the reason the comment this replaced gave: there is no room
|
||
for a sentence beside anything else this row holds. It is at the foot of the screen instead — see
|
||
the queue's own strip.
|
||
-->
|
||
<Border Grid.Row="1" Padding="12,6" Background="{StaticResource Raised}"
|
||
BorderBrush="{StaticResource BorderSubtle}" BorderThickness="0,0,0,1"
|
||
IsVisible="{Binding IsConnected}">
|
||
<Button Classes="headerghost" HorizontalAlignment="Right" Content="DISCONNECT"
|
||
Command="{Binding DisconnectCommand}" />
|
||
</Border>
|
||
|
||
<!--
|
||
◆ WHAT CAN BE DONE TO THE ROW THAT IS CHOSEN. Only while something is connected.
|
||
|
||
Three buttons in one place, which is the point. DELETE used to sit alone in the pane header beside
|
||
UP and REFRESH — two things that move the pane and one that destroys a file, in one row — and
|
||
there was no rename and no move at all, so a file that had landed under the wrong name could only
|
||
be put right by deleting it and sending it again. These three act on the selection, so they belong
|
||
together and away from the two that act on the pane.
|
||
|
||
The name beside them is the row they are about, ellipsized. It says which row rather than which
|
||
path: the listing directly underneath has that row highlighted, so a full path here would be
|
||
repeating what the selection is already showing. The forms the buttons open do print the path,
|
||
because that is the moment something is committed rather than merely looked at.
|
||
|
||
The row goes while one of the three questions is up rather than sitting there greyed out, and so
|
||
does the breadcrumb below it — see ShowsRemoteItemActions. The form that opens is the answer to
|
||
what these buttons asked, and inside the session shell this pane is 204 pixels wide: the height
|
||
goes to the question or to the buttons that raised it, not to both.
|
||
-->
|
||
<Border Grid.Row="2" Padding="12,5" BorderBrush="{StaticResource BorderSubtle}"
|
||
BorderThickness="0,0,0,1" IsVisible="{Binding ShowsRemoteItemActions}">
|
||
<Grid ColumnDefinitions="*,Auto">
|
||
<!--
|
||
The chosen row's name, or the count once anything is ticked. The count wins because it is the
|
||
one the buttons beside it are actually about: with three rows gathered up, a name would be
|
||
describing whichever of them happens to be highlighted while DELETE takes all three.
|
||
-->
|
||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||
<Button Classes="ghost" Content="ALL" Command="{Binding ToggleAllRemoteCommand}"
|
||
ToolTip.Tip="Tick every row, or untick them all" />
|
||
<TextBlock Classes="mono" FontSize="11.5" VerticalAlignment="Center"
|
||
Foreground="{StaticResource TextFaint}" TextTrimming="CharacterEllipsis"
|
||
Text="{Binding SelectedRemoteName}"
|
||
IsVisible="{Binding !HasCheckedRemote}" />
|
||
<TextBlock Classes="mono" FontSize="11.5" VerticalAlignment="Center"
|
||
Foreground="{StaticResource AccentText}"
|
||
Text="{Binding CheckedRemoteLabel}"
|
||
IsVisible="{Binding HasCheckedRemote}" />
|
||
</StackPanel>
|
||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4" Margin="8,0,0,0">
|
||
<!--
|
||
◆ FOUR NOW, and this is the one that was missing: the PERMS column has been a true fact
|
||
about a file with nothing to be done about it since it arrived, so the commonest next act
|
||
after using this screen was opening a terminal to chmod what had just been uploaded. Dark on
|
||
a bucket rather than live-and-refused — an object has an access policy rather than nine bits;
|
||
see CanChangeRemotePermissions.
|
||
|
||
A lock rather than a key glyph: a key is what this application's keychain uses, and one here
|
||
would read as "which credential" over a row that has nothing to do with one.
|
||
-->
|
||
<!--
|
||
◆ EDIT, and it is first because it is the only one of the five that changes nothing on the
|
||
host by pressing it: it fetches a copy and opens it. Dark on a head with no way to open a
|
||
file or watch one — see CanEditRemote, which is what the phone fails rather than this
|
||
button being absent there for a reason nobody can see.
|
||
-->
|
||
<Button Classes="paneicon" Command="{Binding EditRemoteCommand}"
|
||
IsEnabled="{Binding CanEditRemote}"
|
||
ToolTip.Tip="Open in this machine's editor, and send each save back">
|
||
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="15" Text="" />
|
||
</Button>
|
||
<Button Classes="paneicon" Command="{Binding ChangeRemotePermissionsCommand}"
|
||
IsEnabled="{Binding CanChangeRemotePermissions}"
|
||
ToolTip.Tip="Permissions on the host">
|
||
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="15" Text="" />
|
||
</Button>
|
||
<Button Classes="paneicon" Command="{Binding RenameRemoteCommand}"
|
||
IsEnabled="{Binding CanRenameRemote}" ToolTip.Tip="Rename on the host">
|
||
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="15" Text="" />
|
||
</Button>
|
||
<Button Classes="paneicon" Command="{Binding MoveRemoteCommand}"
|
||
IsEnabled="{Binding CanMoveRemote}" ToolTip.Tip="Move to another directory">
|
||
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="15" Text="" />
|
||
</Button>
|
||
<Button Classes="paneicon danger" Command="{Binding DeleteRemoteCommand}"
|
||
IsEnabled="{Binding CanDeleteRemote}" ToolTip.Tip="Delete on the host">
|
||
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="15" Text="" />
|
||
</Button>
|
||
</StackPanel>
|
||
</Grid>
|
||
</Border>
|
||
|
||
<!--
|
||
◆ WHAT AN EDIT SESSION SAYS FOR ITSELF, and it is a strip rather than a question: nothing is
|
||
waiting on an answer, a file is simply open somewhere else and each save is going back. It shares
|
||
the questions' Grid row because it is exclusive with them — opening any of the three needs a
|
||
selection, and while one is up ShowsRemoteItemActions has taken the buttons away.
|
||
|
||
The log is newest-first and bounded. Watching means saving happens more than once, and each one is
|
||
a round trip somebody may want to see happened; an editor that autosaves would otherwise grow this
|
||
without limit. See TransfersViewModel.RemoteEditLog.
|
||
|
||
The conflict card inside it is the one state where a save is not written back. Three answers,
|
||
because those are the three that exist: send it anyway, throw away what was typed and take the
|
||
host's copy, or stop. There is deliberately no merge — this client has no way to do one, and a
|
||
button that pretended otherwise would be the fabrication this project's honesty rule forbids.
|
||
-->
|
||
<Border Grid.Row="3" Padding="12,10" Background="{StaticResource Raised}"
|
||
BorderBrush="{StaticResource BorderSubtle}" BorderThickness="0,0,0,1"
|
||
IsVisible="{Binding IsEditingRemote}">
|
||
<!--
|
||
◆ THE WHOLE STRIP SCROLLS, not just the log inside it, and that is what makes this safe at any
|
||
width. It is the tallest thing that can be in this Grid row — a heading, a path, a log, a
|
||
conflict card and a button — and unlike the three questions beside it, it is not modal: the
|
||
listing underneath stays live, because a file being open in an editor is no reason to stop
|
||
being able to browse. So it cannot be allowed to take the pane's height, and bounding the
|
||
container rather than each part means nothing added inside it later can undo that.
|
||
|
||
134 is measured. Inside the session shell this pane is 204x377, and ScreenLayoutTests draws
|
||
this in its worst case — the conflict card open with a session's worth of saves behind it.
|
||
-->
|
||
<ScrollViewer MaxHeight="134" VerticalScrollBarVisibility="Auto"
|
||
HorizontalScrollBarVisibility="Disabled">
|
||
<StackPanel Spacing="7">
|
||
<TextBlock Classes="heading" FontSize="14" TextWrapping="Wrap"
|
||
Text="{Binding RemoteEdit.Question}" />
|
||
<SelectableTextBlock Classes="mono" FontSize="11.5" TextWrapping="Wrap"
|
||
Foreground="{StaticResource TextFaint}"
|
||
Text="{Binding RemoteEdit.RemotePath}" />
|
||
|
||
<!-- Bounded inside a bounded strip: this keeps a long session's log from pushing the conflict
|
||
card and STOP EDITING out of view within the scroller. -->
|
||
<ScrollViewer MaxHeight="48" VerticalScrollBarVisibility="Auto"
|
||
HorizontalScrollBarVisibility="Disabled">
|
||
<ItemsControl ItemsSource="{Binding RemoteEditLog}">
|
||
<ItemsControl.ItemTemplate>
|
||
<DataTemplate x:DataType="x:String">
|
||
<TextBlock Classes="mono" FontSize="11" Margin="0,1"
|
||
Foreground="{StaticResource TextGhost}" TextTrimming="CharacterEllipsis"
|
||
Text="{Binding}" />
|
||
</DataTemplate>
|
||
</ItemsControl.ItemTemplate>
|
||
</ItemsControl>
|
||
</ScrollViewer>
|
||
|
||
<Border Padding="10,8" CornerRadius="8" Background="{StaticResource DangerWash}"
|
||
BorderBrush="{StaticResource DangerSoft}" BorderThickness="1"
|
||
IsVisible="{Binding HasRemoteEditConflict}">
|
||
<StackPanel Spacing="6">
|
||
<TextBlock Foreground="{StaticResource WarnText}" FontSize="12" TextWrapping="Wrap"
|
||
Text="{Binding RemoteEditConflict}" />
|
||
<WrapPanel>
|
||
<Button Classes="ghost" Content="UPLOAD ANYWAY" Margin="0,0,8,4"
|
||
Command="{Binding ForceSaveEditCommand}" />
|
||
<Button Classes="ghost" Content="RELOAD" Margin="0,0,0,4"
|
||
Command="{Binding ReloadEditCommand}" />
|
||
</WrapPanel>
|
||
</StackPanel>
|
||
</Border>
|
||
|
||
<Button Classes="ghost" Content="STOP EDITING" HorizontalAlignment="Left"
|
||
Command="{Binding StopEditingCommand}" />
|
||
</StackPanel>
|
||
</ScrollViewer>
|
||
</Border>
|
||
|
||
<!--
|
||
The question DELETE asks. It names the full path rather than the file, because a name is the half
|
||
that does not identify anything — and that is now the whole of how the question says which row it
|
||
is about: at the session shell's own 204-pixel width there is no height left for a listing under
|
||
it, so "the row is still on screen and still selected" is no longer true and the path in the card
|
||
is what carries the identity. See the rename form below, which is measured in the same shape.
|
||
|
||
This is the strongest warning on any of these screens, and deliberately: everything else this
|
||
application deletes is a tombstone against a copy the server still has, and a file on somebody's
|
||
host is bytes with nothing behind them.
|
||
|
||
A WrapPanel for the two buttons rather than a horizontal StackPanel — v5b's own change, made once
|
||
the session shell's sidebar could take this pane down to 204 pixels: "DELETE ON THE HOST" and
|
||
CANCEL side by side want closer to 220, so at the narrow width they wrap onto their own line
|
||
instead of one of them going off the edge, which the WrapPanel achieves for free.
|
||
-->
|
||
<Border Grid.Row="3" Padding="12,10" Background="{StaticResource DangerWash}"
|
||
BorderBrush="{StaticResource DangerSoft}" BorderThickness="0,0,0,1"
|
||
IsVisible="{Binding IsConfirmingRemoteDeletion}">
|
||
<StackPanel Spacing="7">
|
||
<TextBlock Classes="heading" FontSize="14" TextWrapping="Wrap"
|
||
Text="{Binding PendingRemoteDeletion.Question}" />
|
||
<!--
|
||
Every path, one per line, and never a count on its own: agreeing to "3 items" is agreeing to a
|
||
number. See RemoteDeletionRequest.Listing.
|
||
|
||
◆ 32, AND THE NUMBER IS MEASURED RATHER THAN CHOSEN. A ticked set has no ceiling, so this list
|
||
has to scroll, and the budget it scrolls inside is small: within the session shell this pane is
|
||
204 wide, where one remote path wraps to two lines, and the card also carries a heading, the
|
||
consequence and two buttons that wrap onto two rows at that width. 120 put the buttons outside
|
||
the window; 72 left CANCEL three pixels under it; 56 and 40 pushed the listing's own squeezed
|
||
row past the bottom — each caught by ScreenLayoutTests, which is why the five-path test exists
|
||
beside the single-path one rather than instead of it. 32 is one wrapped path, which is exactly
|
||
what the single-path question has always shown.
|
||
|
||
What that buys is a scrollbar rather than a taller card, and the heading carries the count so
|
||
the number is never the thing being scrolled for.
|
||
-->
|
||
<ScrollViewer MaxHeight="32" VerticalScrollBarVisibility="Auto"
|
||
HorizontalScrollBarVisibility="Disabled">
|
||
<SelectableTextBlock Classes="mono" FontSize="11.5" TextWrapping="Wrap"
|
||
Foreground="{StaticResource Danger}"
|
||
Text="{Binding PendingRemoteDeletion.Listing}" />
|
||
</ScrollViewer>
|
||
<TextBlock Foreground="{StaticResource WarnText}" FontSize="12" TextWrapping="Wrap"
|
||
Text="{Binding PendingRemoteDeletion.Consequence}" />
|
||
<WrapPanel>
|
||
<Button Classes="danger" Content="DELETE ON THE HOST" Margin="0,0,8,4"
|
||
Command="{Binding ConfirmDeleteRemoteCommand}" IsEnabled="{Binding !IsBusy}" />
|
||
<Button Classes="ghost" Content="CANCEL" Margin="0,0,0,4"
|
||
Command="{Binding CancelDeleteRemoteCommand}" />
|
||
</WrapPanel>
|
||
</StackPanel>
|
||
</Border>
|
||
|
||
<!--
|
||
◆ RENAME AND MOVE, in the row the deletion question uses and never at the same time as it.
|
||
|
||
One form for both, because they differ only in what the box starts out holding and in the words
|
||
around it — see RemoteMoveRequest, where both sets are chosen. It shares this Grid row with
|
||
the deletion card above: the three questions are mutually exclusive by construction, each one's
|
||
own opening clearing the others, so a row of its own would be a row that is empty whenever this
|
||
one is not.
|
||
|
||
It is not painted in the deletion card's danger colours, and that is deliberate rather than an
|
||
oversight. A rename is undone by renaming back; the deletion beside it is the one thing this
|
||
application does that nothing can walk back, and spending the same red on both would make the red
|
||
stop meaning anything.
|
||
|
||
Enter commits from inside the box, which is where the hands already are once an answer has been
|
||
typed.
|
||
|
||
◆ IT TAKES THE WHOLE PANE at the session shell's own 204-pixel width, and that is measured rather
|
||
than hoped: the listing's star row is squeezed to nothing while either question is open, exactly
|
||
as the deletion card above already squeezed it. There is no room for a form and a listing in 336
|
||
pixels, and the form carries the full path precisely so the row underneath does not have to be
|
||
visible for the question to be about something identifiable. That is why the item row and the
|
||
breadcrumb step aside instead — the height they free is what keeps the form itself un-clipped,
|
||
which is what ScreenLayoutTests measures.
|
||
-->
|
||
<Border Grid.Row="3" Padding="12,10" Background="{StaticResource Raised}"
|
||
BorderBrush="{StaticResource BorderSubtle}" BorderThickness="0,0,0,1"
|
||
IsVisible="{Binding IsMovingRemote}">
|
||
<StackPanel Spacing="7">
|
||
<TextBlock Classes="heading" FontSize="14" TextWrapping="Wrap"
|
||
Text="{Binding PendingRemoteMove.Question}" />
|
||
<SelectableTextBlock Classes="mono" FontSize="11.5" TextWrapping="Wrap"
|
||
Foreground="{StaticResource TextFaint}"
|
||
Text="{Binding PendingRemoteMove.FullPath}" />
|
||
<TextBox Text="{Binding RemoteMoveTarget}"
|
||
PlaceholderText="{Binding PendingRemoteMove.FieldHint}">
|
||
<TextBox.KeyBindings>
|
||
<KeyBinding Gesture="Enter" Command="{Binding ConfirmMoveRemoteCommand}" />
|
||
<KeyBinding Gesture="Escape" Command="{Binding CancelMoveRemoteCommand}" />
|
||
</TextBox.KeyBindings>
|
||
</TextBox>
|
||
<TextBlock Classes="hint" FontSize="11.5" TextWrapping="Wrap"
|
||
Text="{Binding PendingRemoteMove.Note}" />
|
||
<WrapPanel>
|
||
<Button Classes="accent" Content="{Binding PendingRemoteMove.ConfirmLabel}" Margin="0,0,8,4"
|
||
Command="{Binding ConfirmMoveRemoteCommand}" IsEnabled="{Binding !IsBusy}" />
|
||
<Button Classes="ghost" Content="CANCEL" Margin="0,0,0,4"
|
||
Command="{Binding CancelMoveRemoteCommand}" />
|
||
</WrapPanel>
|
||
</StackPanel>
|
||
</Border>
|
||
|
||
<!--
|
||
◆ THE MODE, as nine boxes and the number they add up to.
|
||
|
||
In the same Grid row as the other two questions and never at the same time as either — each one's
|
||
own opening clears the others, which is what makes one row enough for all three.
|
||
|
||
Boxes rather than a field to type 755 into, and that is the design rather than a preference: the
|
||
mode is already drawn as -rwxr-xr-x in the column this form was opened from, so a who-can-do-what
|
||
grid is the same representation the reader is already holding. An octal field would ask them to
|
||
translate something they can see into something they cannot check. The octal is printed underneath
|
||
as it changes, so the two readings check each other rather than one replacing the other.
|
||
|
||
The EXEC column is headed from the request rather than fixed, because on a directory that bit is
|
||
permission to enter it — the commonest misreading of a POSIX mode there is, and one word fixes it.
|
||
See RemotePermissionsRequest.ExecuteLabel.
|
||
|
||
APPLY is dead until something has actually changed. A round trip that sets a mode to the mode it
|
||
already has is not wrong, only pointless, and a live button over an untouched form invites it.
|
||
|
||
Not painted in the deletion card's red. Changing a mode is undone by changing it back; the red is
|
||
spent on the one thing here that nothing walks back, and spending it twice would make it stop
|
||
meaning anything — the same argument the rename form above makes for itself.
|
||
-->
|
||
<Border Grid.Row="3" Padding="12,10" Background="{StaticResource Raised}"
|
||
BorderBrush="{StaticResource BorderSubtle}" BorderThickness="0,0,0,1"
|
||
IsVisible="{Binding IsChangingRemotePermissions}">
|
||
<StackPanel Spacing="7">
|
||
<TextBlock Classes="heading" FontSize="14" TextWrapping="Wrap"
|
||
Text="{Binding PendingRemotePermissions.Question}" />
|
||
<SelectableTextBlock Classes="mono" FontSize="11.5" TextWrapping="Wrap"
|
||
Foreground="{StaticResource TextFaint}"
|
||
Text="{Binding PendingRemotePermissions.FullPath}" />
|
||
|
||
<Grid ColumnDefinitions="Auto,*,*,*" RowDefinitions="Auto,Auto,Auto,Auto" Margin="0,2">
|
||
<TextBlock Grid.Row="0" Grid.Column="1" Classes="hint" FontSize="10.5" Text="read"
|
||
HorizontalAlignment="Center" />
|
||
<TextBlock Grid.Row="0" Grid.Column="2" Classes="hint" FontSize="10.5" Text="write"
|
||
HorizontalAlignment="Center" />
|
||
<TextBlock Grid.Row="0" Grid.Column="3" Classes="hint" FontSize="10.5"
|
||
Text="{Binding PendingRemotePermissions.ExecuteLabel}"
|
||
HorizontalAlignment="Center" />
|
||
|
||
<TextBlock Grid.Row="1" Grid.Column="0" Classes="mono" FontSize="11.5" Text="owner"
|
||
VerticalAlignment="Center" Margin="0,0,10,0" />
|
||
<CheckBox Grid.Row="1" Grid.Column="1" HorizontalAlignment="Center" MinWidth="0"
|
||
IsChecked="{Binding PendingRemotePermissions.OwnerRead}" />
|
||
<CheckBox Grid.Row="1" Grid.Column="2" HorizontalAlignment="Center" MinWidth="0"
|
||
IsChecked="{Binding PendingRemotePermissions.OwnerWrite}" />
|
||
<CheckBox Grid.Row="1" Grid.Column="3" HorizontalAlignment="Center" MinWidth="0"
|
||
IsChecked="{Binding PendingRemotePermissions.OwnerExecute}" />
|
||
|
||
<TextBlock Grid.Row="2" Grid.Column="0" Classes="mono" FontSize="11.5" Text="group"
|
||
VerticalAlignment="Center" Margin="0,0,10,0" />
|
||
<CheckBox Grid.Row="2" Grid.Column="1" HorizontalAlignment="Center" MinWidth="0"
|
||
IsChecked="{Binding PendingRemotePermissions.GroupRead}" />
|
||
<CheckBox Grid.Row="2" Grid.Column="2" HorizontalAlignment="Center" MinWidth="0"
|
||
IsChecked="{Binding PendingRemotePermissions.GroupWrite}" />
|
||
<CheckBox Grid.Row="2" Grid.Column="3" HorizontalAlignment="Center" MinWidth="0"
|
||
IsChecked="{Binding PendingRemotePermissions.GroupExecute}" />
|
||
|
||
<TextBlock Grid.Row="3" Grid.Column="0" Classes="mono" FontSize="11.5" Text="others"
|
||
VerticalAlignment="Center" Margin="0,0,10,0" />
|
||
<CheckBox Grid.Row="3" Grid.Column="1" HorizontalAlignment="Center" MinWidth="0"
|
||
IsChecked="{Binding PendingRemotePermissions.OthersRead}" />
|
||
<CheckBox Grid.Row="3" Grid.Column="2" HorizontalAlignment="Center" MinWidth="0"
|
||
IsChecked="{Binding PendingRemotePermissions.OthersWrite}" />
|
||
<CheckBox Grid.Row="3" Grid.Column="3" HorizontalAlignment="Center" MinWidth="0"
|
||
IsChecked="{Binding PendingRemotePermissions.OthersExecute}" />
|
||
</Grid>
|
||
|
||
<SelectableTextBlock Classes="mono" FontSize="12.5"
|
||
Foreground="{StaticResource AccentText}"
|
||
Text="{Binding PendingRemotePermissions.Preview}" />
|
||
|
||
<WrapPanel>
|
||
<Button Classes="accent" Content="APPLY" Margin="0,0,8,4"
|
||
Command="{Binding ConfirmRemotePermissionsCommand}"
|
||
IsEnabled="{Binding PendingRemotePermissions.HasChanges}" />
|
||
<Button Classes="ghost" Content="CANCEL" Margin="0,0,0,4"
|
||
Command="{Binding CancelRemotePermissionsCommand}" />
|
||
</WrapPanel>
|
||
</StackPanel>
|
||
</Border>
|
||
|
||
<!--
|
||
Away while a question is up, for the reason the item row above states: the trail is where the pane
|
||
is and MKDIR makes a directory, and neither is what somebody answering a question about one row
|
||
is doing.
|
||
-->
|
||
<Grid Grid.Row="4" ColumnDefinitions="*,Auto" Margin="12,6,12,4"
|
||
IsVisible="{Binding !IsAskingAboutRemoteEntry}">
|
||
<ItemsControl Grid.Column="0" ItemsSource="{Binding RemoteTrail}" VerticalAlignment="Center">
|
||
<ItemsControl.ItemsPanel>
|
||
<ItemsPanelTemplate>
|
||
<WrapPanel Orientation="Horizontal" />
|
||
</ItemsPanelTemplate>
|
||
</ItemsControl.ItemsPanel>
|
||
<ItemsControl.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:CrumbViewModel">
|
||
<StackPanel Orientation="Horizontal">
|
||
<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="11"
|
||
Foreground="{StaticResource TextDim}" />
|
||
</Button>
|
||
</StackPanel>
|
||
</DataTemplate>
|
||
</ItemsControl.ItemTemplate>
|
||
</ItemsControl>
|
||
|
||
<!--
|
||
Making a directory sits here, beside the path it would be made in, rather than with the queue's
|
||
controls. It exists because the queue refuses to overwrite: without somewhere else to put a file,
|
||
"that name is already taken" is a dead end.
|
||
|
||
v5b narrows the box from 140 to 90: at the session shell's own 204-pixel width once QUICK ACCESS
|
||
is showing, 140 plus MKDIR plus the spacing between them ran past the pane's own edge. Still wide
|
||
enough for a real directory name to be typed and read back, which is what this box is for; a
|
||
name that outgrows it scrolls inside the box the way every TextBox already does.
|
||
|
||
◆ The placeholder says "directory" rather than "new directory", which is what it said until it
|
||
was measured: the longer text needs 101 pixels inside a box that has 74, so it drew as "new
|
||
directo" — a placeholder cut off mid-word, which reads as a rendering fault rather than as a
|
||
hint. MKDIR is right beside it and carries the "new" half already. The path box in the header
|
||
above lost the same argument and for the same reason; see its own PlaceholderText.
|
||
-->
|
||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6"
|
||
IsVisible="{Binding IsConnected}">
|
||
<TextBox Width="90" Text="{Binding NewRemoteFolder}" PlaceholderText="directory" />
|
||
<Button Classes="ghost" Content="MKDIR" Command="{Binding CreateRemoteFolderCommand}" />
|
||
</StackPanel>
|
||
</Grid>
|
||
|
||
<!--
|
||
v5b: PERMS keeps its own column — a real fact about a file on the host, dropped nowhere — but at
|
||
48 pixels rather than the drawer's own wider habit, and SIZE/MODIFIED shrink to match the local
|
||
pane's narrower numbers. See the local pane's own remark on why 204 rather than the design's 1920.
|
||
-->
|
||
<Grid Grid.Row="5" Margin="0,2,10,4">
|
||
<Grid.ColumnDefinitions>
|
||
<ColumnDefinition Width="20" />
|
||
<ColumnDefinition Width="2*" />
|
||
<ColumnDefinition Width="*" MaxWidth="66" />
|
||
<ColumnDefinition Width="1.6*" MaxWidth="124" />
|
||
<ColumnDefinition Width="*" MaxWidth="66" />
|
||
</Grid.ColumnDefinitions>
|
||
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="9.5" Margin="8,0,8,0" />
|
||
<TextBlock Grid.Column="2" Classes="label" Text="SIZE" FontSize="9.5" Margin="8,0,0,0"
|
||
HorizontalAlignment="Right" TextTrimming="CharacterEllipsis" />
|
||
<TextBlock Grid.Column="3" Classes="label" Text="MODIFIED" FontSize="9.5" Margin="8,0,0,0"
|
||
HorizontalAlignment="Right" TextTrimming="CharacterEllipsis" />
|
||
<TextBlock Grid.Column="4" Classes="label" Text="PERMS" FontSize="9.5" Margin="6,0,0,0"
|
||
HorizontalAlignment="Right" TextTrimming="CharacterEllipsis" />
|
||
</Grid>
|
||
|
||
<ListBox Grid.Row="6" x:Name="RemoteList" Classes="filerows" ItemsSource="{Binding RemoteEntries}"
|
||
SelectedItem="{Binding SelectedRemoteEntry}">
|
||
<ListBox.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:RemoteEntryRowViewModel">
|
||
<Grid Height="38" Margin="6,0,10,0">
|
||
<Grid.ColumnDefinitions>
|
||
<ColumnDefinition Width="24" />
|
||
<ColumnDefinition Width="20" />
|
||
<ColumnDefinition Width="2*" />
|
||
<ColumnDefinition Width="*" MaxWidth="66" />
|
||
<ColumnDefinition Width="1.6*" MaxWidth="124" />
|
||
<ColumnDefinition Width="*" MaxWidth="66" />
|
||
</Grid.ColumnDefinitions>
|
||
<!--
|
||
◆ THE TICK IS NOT THE LIST'S SELECTION, and the two answer different questions. The
|
||
selection is "the row I am looking at", which is what rename, move and permissions act on
|
||
— each takes a form naming one path and is one-at-a-time by construction. This is "the
|
||
rows I have gathered up", which is what DOWNLOAD and DELETE act on, and it survives the
|
||
highlight moving.
|
||
|
||
It is drawn rather than left to ctrl-click for a reason beyond taste: the phone head shares
|
||
this view model and has neither ctrl nor shift, so a multi-selection has to be a tap target
|
||
there whatever this pane does. One shape on both heads, and a state somebody can see.
|
||
|
||
Click is handled rather than bound one-way, because the count above the list is a property
|
||
of the collection rather than of any row — see TransfersViewModel.RemoteChecksChanged.
|
||
-->
|
||
<CheckBox Grid.Column="0" MinWidth="0" VerticalAlignment="Center"
|
||
IsChecked="{Binding IsChecked}"
|
||
Click="OnRemoteRowChecked" />
|
||
<TextBlock Grid.Column="1" Classes="entryicon" Text="{Binding IconGlyph}" />
|
||
<TextBlock Grid.Column="2" Classes="mono entry" Classes.dir="{Binding IsNavigable}"
|
||
Classes.exec="{Binding IsExecutable}"
|
||
Text="{Binding Name}" FontSize="13.5"
|
||
Margin="10,0,8,0" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" />
|
||
<TextBlock Grid.Column="3" Classes="mono entrysize" Text="{Binding Size}" FontSize="12"
|
||
Margin="8,0,0,0" HorizontalAlignment="Right" VerticalAlignment="Center" />
|
||
<TextBlock Grid.Column="4" Classes="mono entrydate" Text="{Binding Modified}" FontSize="12"
|
||
Margin="8,0,0,0" HorizontalAlignment="Right" VerticalAlignment="Center" />
|
||
<TextBlock Grid.Column="5" Classes="mono perms" Classes.loose="{Binding IsWorldWritable}"
|
||
Text="{Binding Permissions}" FontSize="10" Margin="6,0,0,0"
|
||
HorizontalAlignment="Right" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" />
|
||
</Grid>
|
||
</DataTemplate>
|
||
</ListBox.ItemTemplate>
|
||
</ListBox>
|
||
|
||
<!--
|
||
A directory with nothing in it. It no longer has to say anything about not being connected: that
|
||
state is the invitation below, which covers this pane whole.
|
||
-->
|
||
<TextBlock Grid.Row="6" Classes="hint" FontSize="12" Margin="24" MaxWidth="300"
|
||
HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center"
|
||
Text="Nothing in this directory." IsVisible="{Binding !HasRemoteEntries}" />
|
||
|
||
<!--
|
||
============ THE INVITATION, which is what this screen opens on ============
|
||
|
||
Everything the connect bar used to hold, in the half of the screen it was always about. It covers
|
||
the pane from below the header to the bottom, opaquely: the breadcrumb, the column headings and
|
||
the empty listing are all still laid out underneath, and all three are furniture for a listing
|
||
that does not exist yet.
|
||
|
||
Two steps, not one. The first is an invitation with a button — a name for what this screen does, a
|
||
sentence about what it costs, and one thing to press — and the picker is the second. A combo box
|
||
sitting open in the middle of an empty pane would be a form with no question above it; the reason
|
||
the pane is empty is the question, and that is what the first step says.
|
||
|
||
◆ The two steps also keep this inside its budget, which is the pane's height less whatever the
|
||
queue has taken — 268 pixels with three transfers on it. The sentence and the button go away when
|
||
the picker arrives, so the tall shape is the form rather than form-plus-prose, and neither shape
|
||
reaches the floor. Anything added here has to hold that; the layout suite measures both.
|
||
-->
|
||
<!--
|
||
ShowsRemoteInvitation rather than !IsConnected, which are no longer the same question. Pressing
|
||
DISCONNECT empties this pane, so an invitation over it covers nothing; a connection lost
|
||
underneath somebody leaves the listing exactly where it was, and covering that would hide the one
|
||
thing the strip below is offering to put them back into. See TransfersViewModel.ShowsRemoteContents.
|
||
-->
|
||
<Border Grid.Row="1" Grid.RowSpan="6" Background="{StaticResource Canvas}"
|
||
IsVisible="{Binding ShowsRemoteInvitation}">
|
||
<StackPanel Spacing="10" Margin="20" MaxWidth="320"
|
||
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||
|
||
<!--
|
||
The mark the hosts screen puts on a group, at the size an empty state can carry one. Still a
|
||
hand-picked glyph rather than one of the Material Icons codepoints the file rows now use — this
|
||
invitation predates the app's own IconFont and is outside wave C's restyle, which touched the
|
||
pane headers, the rows and the TRANSFERS strip and not this card — and ▤ already means "a place
|
||
things are kept" without borrowing from a font built for a different visual language.
|
||
-->
|
||
<Border Width="44" Height="44" CornerRadius="12" HorizontalAlignment="Center"
|
||
Background="{StaticResource Raised}" BorderBrush="{StaticResource Border}"
|
||
BorderThickness="1">
|
||
<TextBlock Text="▤" FontSize="18" Foreground="{StaticResource TextDim}"
|
||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||
</Border>
|
||
|
||
<TextBlock Classes="heading" FontSize="16" TextAlignment="Center"
|
||
Text="Connect to a host" IsVisible="{Binding ShowsHostPicker}" />
|
||
<TextBlock Classes="heading" FontSize="16" TextAlignment="Center"
|
||
Text="Open a bucket" IsVisible="{Binding ShowsBucketChoice}" />
|
||
<!--
|
||
A different heading rather than the same one over an empty picker, which is what this screen
|
||
used to do: SELECT BUCKET opened a combo box with nothing in it and no sentence anywhere saying
|
||
where a bucket comes from. From here that is indistinguishable from an application that cannot
|
||
add one — and this is the screen somebody looking for S3 arrives at.
|
||
-->
|
||
<TextBlock Classes="heading" FontSize="16" TextAlignment="Center"
|
||
Text="No buckets yet" IsVisible="{Binding ShowsNoBuckets}" />
|
||
|
||
<!--
|
||
The sentence, and the button under it. Both go away once the picker is up — by then they have
|
||
been read or they have not — which is why they are one collapsing group rather than two
|
||
elements each answering the same question.
|
||
|
||
The sentence is the one the empty pane used to carry, and it is kept rather than dropped
|
||
because it is the one thing about this screen that surprises people: browsing files is a
|
||
second login, and it shows up in the host's auth log as one. The bucket's counterpart says
|
||
the opposite thing for the opposite reason — there is no session to open at all, so nothing
|
||
is dialled until a listing is asked for.
|
||
-->
|
||
<StackPanel Spacing="10" IsVisible="{Binding !IsChoosingRemote}">
|
||
<TextBlock Classes="hint" FontSize="12" TextAlignment="Center"
|
||
IsVisible="{Binding ShowsHostPicker}"
|
||
Text="Browsing a host's files opens its own SFTP connection, so the host records a second login — it is not the channel a terminal uses." />
|
||
<TextBlock Classes="hint" FontSize="12" TextAlignment="Center"
|
||
IsVisible="{Binding ShowsBucketChoice}"
|
||
Text="A bucket has nothing to connect to: the keys and the endpoint are tested by the first listing, not by opening a session." />
|
||
<!--
|
||
Where a bucket comes from, said on the screen that needs one. It names the keychain rather
|
||
than only offering a button, because the button is a shortcut and the sentence is what makes
|
||
the bucket findable again next week without it.
|
||
-->
|
||
<TextBlock Classes="hint" FontSize="12" TextAlignment="Center"
|
||
IsVisible="{Binding ShowsNoBuckets}"
|
||
Text="A bucket is an item in your keychain — the bucket name, its keys and its endpoint, encrypted like everything else there. Add one and it appears here." />
|
||
<Button Classes="accent" HorizontalAlignment="Center" Content="SELECT HOST"
|
||
IsVisible="{Binding ShowsHostPicker}"
|
||
Command="{Binding BeginChoosingRemoteCommand}" />
|
||
<Button Classes="accent" HorizontalAlignment="Center" Content="SELECT BUCKET"
|
||
IsVisible="{Binding ShowsBucketChoice}"
|
||
Command="{Binding BeginChoosingRemoteCommand}" />
|
||
<Button Classes="accent" HorizontalAlignment="Center" Content="+ BUCKET"
|
||
IsVisible="{Binding ShowsNoBuckets}"
|
||
Command="{Binding AddBucketCommand}"
|
||
ToolTip.Tip="Goes to the keychain with the bucket editor open." />
|
||
</StackPanel>
|
||
|
||
<!-- ==== The picker, once it has been asked for ==== -->
|
||
<StackPanel Spacing="8" IsVisible="{Binding IsChoosingRemote}">
|
||
|
||
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding Hosts}"
|
||
SelectedItem="{Binding SelectedHost}"
|
||
IsVisible="{Binding ShowsHostPicker}"
|
||
PlaceholderText="choose a host">
|
||
<ComboBox.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:HostRowViewModel">
|
||
<StackPanel>
|
||
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="12"
|
||
Foreground="{StaticResource Text}" />
|
||
<TextBlock Classes="mono" Text="{Binding Address}" FontSize="10"
|
||
Foreground="{StaticResource TextFaint}" />
|
||
</StackPanel>
|
||
</DataTemplate>
|
||
</ComboBox.ItemTemplate>
|
||
</ComboBox>
|
||
|
||
<!--
|
||
Gated on there being one, not merely on this being the S3 screen. An empty picker is the
|
||
thing that made this screen look like it could not add a bucket, and the invitation above
|
||
now sends somebody to the keychain instead of opening this.
|
||
-->
|
||
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding Buckets}"
|
||
SelectedItem="{Binding SelectedBucket}"
|
||
IsVisible="{Binding ShowsBucketChoice}"
|
||
PlaceholderText="choose a bucket">
|
||
<ComboBox.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:ObjectStoreRowViewModel">
|
||
<StackPanel>
|
||
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="12"
|
||
Foreground="{StaticResource Text}" />
|
||
<TextBlock Classes="mono" Text="{Binding Description}" FontSize="10"
|
||
Foreground="{StaticResource TextFaint}" />
|
||
</StackPanel>
|
||
</DataTemplate>
|
||
</ComboBox.ItemTemplate>
|
||
</ComboBox>
|
||
|
||
<!--
|
||
Only for a host bound to nothing, exactly as the hosts screen's box is — and it is a
|
||
different box holding a different value. This connection authenticates separately, so a
|
||
password typed to open a terminal was never offered here.
|
||
-->
|
||
<TextBox PasswordChar="•" Text="{Binding TypedPassword}" PlaceholderText="password"
|
||
IsVisible="{Binding SelectedHostAsksForAPassword}" />
|
||
|
||
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Center">
|
||
<Button Classes="accent" Content="{Binding ConnectLabel}"
|
||
Command="{Binding ConnectCommand}" IsEnabled="{Binding !IsBusy}" />
|
||
<Button Classes="ghost" Content="CANCEL"
|
||
Command="{Binding CancelChoosingRemoteCommand}" />
|
||
</StackPanel>
|
||
|
||
</StackPanel>
|
||
|
||
<!--
|
||
The status line, in the state where the connected strip above is not on screen. Wrapped rather
|
||
than trimmed, unlike its counterpart up there: what lands here is a refusal with a reason in
|
||
it — a server's own sentence about why a connection did not open — and an ellipsis through the
|
||
middle of that is a message that has been shown without being said.
|
||
-->
|
||
<TextBlock Classes="hint" FontSize="11.5" TextAlignment="Center" Text="{Binding Status}" />
|
||
|
||
</StackPanel>
|
||
</Border>
|
||
|
||
<!--
|
||
Two highlights rather than one, because refusing is worth showing. Something dragged over a
|
||
disconnected pane has to say so under the pointer — a pane that lights up nowhere reads as a
|
||
window that has stopped answering, and the answer arriving after the drop is the answer arriving
|
||
too late. See the local pane for why neither may hit-test.
|
||
-->
|
||
<Border Grid.Row="0" Grid.RowSpan="7" IsHitTestVisible="False"
|
||
Background="{StaticResource AccentWash}" BorderBrush="{StaticResource Accent}"
|
||
BorderThickness="2" IsVisible="{Binding IsRemoteDropTarget}" />
|
||
|
||
<Border Grid.Row="0" Grid.RowSpan="7" IsHitTestVisible="False"
|
||
Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
|
||
BorderThickness="2" IsVisible="{Binding IsRemoteDropRefused}">
|
||
<TextBlock Classes="hint" Text="Connect to a host first." FontSize="12"
|
||
Foreground="{StaticResource Danger}"
|
||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||
</Border>
|
||
|
||
</Grid>
|
||
|
||
</Grid>
|
||
|
||
<!--
|
||
============ The TRANSFERS strip ============
|
||
── v5b ─────────────────────────────────────────────────────────────────────────────────────────────
|
||
DeepChrome and a top border, per SFTP.dc.html, in place of the plain Sidebar fill this used to carry —
|
||
the same surface the session shell's own header and status bar are painted in, which is what fuses the
|
||
strip into that frame rather than leaving it read as another pane. Two Borders share this row and
|
||
exactly one is ever visible: the design's own header — TRANSFERS plus a count chip — with the queue
|
||
underneath while there is one, and a single quiet line otherwise. That is the "collapse" wave C's own
|
||
brief asks for: not zero height, because the honest sentence that used to live inside the full strip
|
||
still has somewhere to be read, but far short of the full header-plus-rows shape.
|
||
|
||
The design's own aggregate throughput readout — "8.4 MB/s" beside an upward arrow — is not drawn.
|
||
TransferRowViewModel.Progress computes a rate per transfer, off TransferSnapshot.BytesPerSecond, and
|
||
nothing anywhere in this queue sums those into one number for the whole strip; inventing one here would
|
||
be exactly the fabricated fact this project's honesty rule forbids. Recorded as a deviation rather than
|
||
silently dropped.
|
||
-->
|
||
<Border Grid.Row="1" Background="{StaticResource DeepChrome}" BorderBrush="{StaticResource Border}"
|
||
BorderThickness="0,1,0,0" MaxHeight="196" IsVisible="{Binding HasTransfers}">
|
||
<Grid RowDefinitions="Auto,Auto,*,Auto" Margin="16,12">
|
||
|
||
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*">
|
||
<TextBlock Grid.Column="0" Classes="label" FontSize="10" Text="TRANSFERS" VerticalAlignment="Center" />
|
||
<!--
|
||
h18, radius 5, Chip fill with no border — the design's own count chip, distinct from Border.chip's
|
||
usual bordered-and-unfilled shape elsewhere in this application. ActiveTransfersLabel is
|
||
"N active", off TransfersViewModel.ActiveTransfers: queued counts as active there for the reason
|
||
its own remark gives, which matches what this chip is naming.
|
||
-->
|
||
<Border Grid.Column="1" Height="18" CornerRadius="5" Background="{StaticResource Chip}"
|
||
Padding="8,0" Margin="10,0,0,0" VerticalAlignment="Center">
|
||
<TextBlock Classes="mono" FontSize="10.5" FontWeight="Medium"
|
||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
|
||
Text="{Binding ActiveTransfersLabel}" />
|
||
</Border>
|
||
</Grid>
|
||
|
||
<!--
|
||
◆ THE STATUS LINE, in the state where the remote pane has no room for one — unchanged in meaning
|
||
from the strip this replaced, only in paint. Right-aligned beside the queue's own "one at a time"
|
||
policy sentence, which stays for the reason it always did: the queue refuses to overwrite, and
|
||
nothing else on this screen says so.
|
||
-->
|
||
<Grid Grid.Row="1" ColumnDefinitions="*,Auto" Margin="0,8,0,10">
|
||
<TextBlock Grid.Column="0" Classes="mono" FontSize="10.5" Foreground="{StaticResource TextGhost}"
|
||
Text="one at a time · nothing lands at its final name until it is complete" />
|
||
<TextBlock Grid.Column="1" Classes="hint" FontSize="11" Margin="16,0,0,0"
|
||
HorizontalAlignment="Right" VerticalAlignment="Center"
|
||
TextTrimming="CharacterEllipsis" TextWrapping="NoWrap"
|
||
Text="{Binding Status}" IsVisible="{Binding IsConnected}" />
|
||
</Grid>
|
||
|
||
<!--
|
||
Virtualised: a folder download queues a row per file, and a stack panel inside an outer scroller
|
||
laid out and drew every one of them, thousands deep. The scroller is the control's own so the panel
|
||
knows the viewport it has to fill and realises only those rows.
|
||
-->
|
||
<ItemsControl Grid.Row="2" ItemsSource="{Binding Transfers}">
|
||
<ItemsControl.Template>
|
||
<ControlTemplate>
|
||
<ScrollViewer>
|
||
<ItemsPresenter Name="PART_ItemsPresenter" ItemsPanel="{TemplateBinding ItemsPanel}" />
|
||
</ScrollViewer>
|
||
</ControlTemplate>
|
||
</ItemsControl.Template>
|
||
<ItemsControl.ItemsPanel>
|
||
<ItemsPanelTemplate>
|
||
<VirtualizingStackPanel />
|
||
</ItemsPanelTemplate>
|
||
</ItemsControl.ItemsPanel>
|
||
<ItemsControl.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:TransferRowViewModel">
|
||
<!--
|
||
Per SFTP.dc.html: a 320-pixel ellipsized "source → destination" label, a slim progress track
|
||
and a right-aligned status word — see TransferRowViewModel.Label/StatusWord. STOP, RETRY/
|
||
RESUME and DISCARD are the app's own, kept per wave C's brief rather than dropped for having
|
||
no equivalent in the design's static mock: icon-only now, restyled to the pane headers' own
|
||
quiet-glyph idiom, with a tooltip carrying the word the button used to print.
|
||
-->
|
||
<Grid ColumnDefinitions="320,*,64,Auto" Margin="0,5" ColumnSpacing="14">
|
||
<TextBlock Grid.Column="0" Classes="mono" FontSize="12"
|
||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center"
|
||
Text="{Binding Label}" TextTrimming="CharacterEllipsis" />
|
||
|
||
<ProgressBar Grid.Column="1" Classes="transferbar" Minimum="0" Maximum="100"
|
||
Value="{Binding Percent}" VerticalAlignment="Center"
|
||
ToolTip.Tip="{Binding Progress}" />
|
||
|
||
<TextBlock Grid.Column="2" Classes="mono transferstatus"
|
||
Classes.running="{Binding IsInProgress}" Classes.done="{Binding IsDone}"
|
||
Classes.failed="{Binding HasFailed}" FontSize="12" FontWeight="Medium"
|
||
Text="{Binding StatusWord}" TextAlignment="Right" VerticalAlignment="Center" />
|
||
|
||
<StackPanel Grid.Column="3" Orientation="Horizontal" Spacing="4">
|
||
<Button Classes="paneicon" Width="22" Height="22" IsVisible="{Binding IsRunning}"
|
||
ToolTip.Tip="Stop this transfer"
|
||
Command="{Binding $parent[ItemsControl].((vm:TransfersViewModel)DataContext).CancelTransferCommand}"
|
||
CommandParameter="{Binding}">
|
||
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="13" Text="" />
|
||
</Button>
|
||
<Button Classes="paneicon" Width="22" Height="22" IsVisible="{Binding CanRetry}"
|
||
ToolTip.Tip="{Binding RetryLabel}"
|
||
Command="{Binding $parent[ItemsControl].((vm:TransfersViewModel)DataContext).RetryTransferCommand}"
|
||
CommandParameter="{Binding}">
|
||
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="13" Text="" />
|
||
</Button>
|
||
<Button Classes="paneicon danger" Width="22" Height="22" IsVisible="{Binding IsFinished}"
|
||
ToolTip.Tip="Discard this row"
|
||
Command="{Binding $parent[ItemsControl].((vm:TransfersViewModel)DataContext).DiscardTransferCommand}"
|
||
CommandParameter="{Binding}">
|
||
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="13" Text="" />
|
||
</Button>
|
||
</StackPanel>
|
||
</Grid>
|
||
</DataTemplate>
|
||
</ItemsControl.ItemTemplate>
|
||
</ItemsControl>
|
||
|
||
<Button Grid.Row="3" Classes="ghost" Content="CLEAR FINISHED" Margin="0,10,0,0"
|
||
HorizontalAlignment="Left" Command="{Binding ClearCompletedCommand}" />
|
||
|
||
</Grid>
|
||
</Border>
|
||
|
||
<!--
|
||
The collapsed shape: no header, no count chip, no rows — just the honest sentence the full strip
|
||
carried before anything was ever queued, kept per wave C's own brief rather than dropped along with the
|
||
rest of the strip's furniture.
|
||
-->
|
||
<Border Grid.Row="1" Background="{StaticResource DeepChrome}" BorderBrush="{StaticResource Border}"
|
||
BorderThickness="0,1,0,0" Padding="16,12" IsVisible="{Binding !HasTransfers}">
|
||
<TextBlock Classes="hint" FontSize="11.5" Foreground="{StaticResource TextFaint}"
|
||
Text="Nothing queued. Choose a file in either pane and press the arrow pointing the way you want it to go." />
|
||
</Border>
|
||
|
||
<!--
|
||
First contact and a changed key, over the whole screen. The same two refusals a terminal makes, and
|
||
they arrive here on their own because this is a separate connection — a host trusted for a shell is
|
||
trusted for this too, but a host nobody has connected to at all is met here first.
|
||
-->
|
||
<Border Grid.Row="0" Grid.RowSpan="2" Background="{StaticResource Canvas}"
|
||
IsVisible="{Binding HasPendingHostKey}">
|
||
<Border Classes="card">
|
||
<StackPanel Spacing="12">
|
||
<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="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}" />
|
||
<Button Classes="ghost" Content="CANCEL" Command="{Binding RejectHostKeyCommand}" />
|
||
</StackPanel>
|
||
</StackPanel>
|
||
</Border>
|
||
</Border>
|
||
|
||
<Border Grid.Row="0" Grid.RowSpan="2" Background="{StaticResource Canvas}"
|
||
IsVisible="{Binding HasHostKeyMismatch}">
|
||
<Border Classes="card" BorderBrush="{StaticResource DangerSoft}">
|
||
<StackPanel Spacing="12">
|
||
<TextBlock Classes="heading" Text="The host key has changed" Foreground="{StaticResource Danger}" />
|
||
<TextBlock Classes="hint" Text="{Binding HostKeyMismatch}" />
|
||
<TextBlock Classes="hint"
|
||
Text="Nothing was connected, and there is no way to continue from here. If the server was legitimately rebuilt, edit the host on the Hosts screen and choose Forget host key." />
|
||
<Button Classes="ghost" Content="CLOSE" Command="{Binding RejectHostKeyCommand}"
|
||
HorizontalAlignment="Left" />
|
||
</StackPanel>
|
||
</Border>
|
||
</Border>
|
||
|
||
</Grid>
|
||
|
||
</UserControl>
|