Public Access
Merge branch 'claude/host-connection-top-bar-25d04e'
This commit is contained in:
@@ -274,6 +274,12 @@ that design now; the desktop's own v2 is a 190-pixel labelled sidebar in place o
|
|||||||
search box in the titlebar, and session tabs as pills. Its light theme is not built — see
|
search box in the titlebar, and session tabs as pills. Its light theme is not built — see
|
||||||
[`docs/design-import-gaps.md`](docs/design-import-gaps.md) — so the application is dark on both.
|
[`docs/design-import-gaps.md`](docs/design-import-gaps.md) — so the application is dark on both.
|
||||||
|
|
||||||
|
**A connected phone shows one bar and then the terminal.** The header, the session strip and the bottom
|
||||||
|
bar are collapsed while a shell is up, and a single 52-pixel row replaces them: back on the left, the
|
||||||
|
sessions as pills, and a `+` on the right offering the three connections this application can make — a
|
||||||
|
shell, a host's files over SFTP, or a bucket. The system back gesture does what the arrow does, and lowers
|
||||||
|
that menu first if it is open.
|
||||||
|
|
||||||
Widening the sidebar moved the desktop window's minimum from `880x560` to `1016x574`, which leaves every
|
Widening the sidebar moved the desktop window's minimum from `880x560` to `1016x574`, which leaves every
|
||||||
screen exactly the width it was designed against.
|
screen exactly the width it was designed against.
|
||||||
|
|
||||||
|
|||||||
@@ -508,6 +508,14 @@ go at 360dp:
|
|||||||
drawn, because a modifier that is on and does not look on is how somebody sends `^L` to a database prompt
|
drawn, because a modifier that is on and does not look on is how somebody sends `^L` to a database prompt
|
||||||
believing they typed an `l`.
|
believing they typed an `l`.
|
||||||
|
|
||||||
|
**The surface has since taken the whole screen.** `PhoneShell` collapses the header, the session strip
|
||||||
|
and the bottom bar while a terminal is showing — one binding on `IsShowingPages` each — and the screen
|
||||||
|
draws a 52-pixel bar in their place: back, the session pills, and a `+` raising a sheet with the three
|
||||||
|
connections there are. That sheet is the head's first control that could be drawn over the renderer, so
|
||||||
|
it collapses it rather than covering it, exactly as the desktop's palette does; whether Android's
|
||||||
|
WebView actually composites above Avalonia content is still the unverified question recorded below, and
|
||||||
|
collapsing is correct under either answer.
|
||||||
|
|
||||||
All eight are done for the decided scope, and v2 has since gone past it — see the note under step 7.
|
All eight are done for the decided scope, and v2 has since gone past it — see the note under step 7.
|
||||||
|
|
||||||
What is left, in the order it matters:
|
What is left, in the order it matters:
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ the chrome, hosts and terminals, file transfer, the vault, teams, and preference
|
|||||||
> | v2 element | What ships instead |
|
> | v2 element | What ships instead |
|
||||||
> | --- | --- |
|
> | --- | --- |
|
||||||
> | The **FORWARDING** screen: local/remote/dynamic rules, toggles, bytes transferred | **Nothing, said out loud.** `ISshConnection` offers `OpenShellAsync` and nothing else, so there is no tunnel for a rule to run through; `SyncEntityType.PortForward = 9` is still reserved and still unused. The MORE screen carries a paragraph naming the absence, for the reason the desktop keeps TEAMS in its rail. |
|
> | The **FORWARDING** screen: local/remote/dynamic rules, toggles, bytes transferred | **Nothing, said out loud.** `ISshConnection` offers `OpenShellAsync` and nothing else, so there is no tunnel for a rule to run through; `SyncEntityType.PortForward = 9` is still reserved and still unused. The MORE screen carries a paragraph naming the absence, for the reason the desktop keeps TEAMS in its rail. |
|
||||||
> | `23 ms · fwd 5432` on the terminal's connection line | The account and endpoint actually dialled. There is no RTT measurement in SSH.NET and nothing forwards anything. |
|
> | `23 ms · fwd 5432` on the terminal's connection line | ◆ **The line is gone, and what was real on it moved.** There was never an RTT to draw — SSH.NET measures none — and nothing forwards anything, so what shipped was the account and endpoint actually dialled. In v3 a connected phone draws one 52-pixel bar and then the terminal, so a second 36-pixel row naming the machine is exactly the chrome that surface exists to give back: the address is on the connecting card, where it is read before anything has answered, and the shell's own prompt says it afterwards. The two text-size buttons that shared the line are pinned at the end of the accessory row, outside its scroller, which is what the line was protecting them from. |
|
||||||
> | `ED25519` badge and `SHA256:kQ9f…Zw2M` on every keychain card | `Detail`, which is what is genuinely known *about* an item. Unchanged from the first import: no algorithm field, no fingerprint, and computing either means parsing armour the type stores verbatim. |
|
> | `ED25519` badge and `SHA256:kQ9f…Zw2M` on every keychain card | `Detail`, which is what is genuinely known *about* an item. Unchanged from the first import: no algorithm field, no fingerprint, and computing either means parsing armour the type stores verbatim. |
|
||||||
> | An `agent` chip on a key | Omitted. There is no agent of any kind — see the first import's Vault section. |
|
> | An `agent` chip on a key | Omitted. There is no agent of any kind — see the first import's Vault section. |
|
||||||
> | Snippet cards footed with `edge-eu-1 · today 10:58 · exit 0` | The command and its badge. Nothing records where a snippet ran, when, or what it returned; the shell it is typed into never reports back. |
|
> | Snippet cards footed with `edge-eu-1 · today 10:58 · exit 0` | The command and its badge. Nothing records where a snippet ran, when, or what it returned; the shell it is typed into never reports back. |
|
||||||
|
|||||||
@@ -889,3 +889,66 @@ Focus a passphrase box, then turn the phone sideways.
|
|||||||
|
|
||||||
**Pass:** the box is still visible and still focused, and the shell is intact — the activity handles the
|
**Pass:** the box is still visible and still focused, and the shell is intact — the activity handles the
|
||||||
rotation rather than being recreated, and live shells survive it.
|
rotation rather than being recreated, and live shells survive it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 11 — The phone's terminal surface
|
||||||
|
|
||||||
|
Every check here needs a real device for the reason Phase 10's do, plus one of its own: the interesting
|
||||||
|
question on this screen is whether a native `WebView` composites above what Avalonia draws over it, and no
|
||||||
|
headless surface has a native view to answer with. `docs/android-port.md` still records that as unverified;
|
||||||
|
11.2 is the check that settles it.
|
||||||
|
|
||||||
|
### 11.1 A shell gets the screen · **the important one for chrome**
|
||||||
|
|
||||||
|
Open a shell from HOSTS.
|
||||||
|
|
||||||
|
**Pass:** the vault header, the session strip and the four-entry bottom bar are all gone. What is left is
|
||||||
|
one bar — a back arrow, the session pills, a `+` — and then the terminal down to the accessory keys. Press
|
||||||
|
back: all three come back, the tab is still in the strip and its dot is still green.
|
||||||
|
|
||||||
|
**Failure means:** one of the three rows is not bound on `IsShowingPages`, or the terminal is being reached
|
||||||
|
by a route that leaves `Surface` on `Page`.
|
||||||
|
|
||||||
|
### 11.2 The connect menu is not drawn over the renderer · **the important one**
|
||||||
|
|
||||||
|
With a shell showing output, press `+`.
|
||||||
|
|
||||||
|
**Pass:** the terminal's rectangle goes to the canvas colour and the sheet sits over it whole — scrim, three
|
||||||
|
rows and CANCEL, every one of them tappable, none of them sliced down the left edge. Tap the scrim: the
|
||||||
|
terminal comes back with its scrollback intact and the shell still running.
|
||||||
|
|
||||||
|
**Failure means:** `IsTerminalShowing` is not being cleared by `IsConnectSheetOpen` — or, if the sheet is
|
||||||
|
sliced *despite* the rectangle going blank, something else in that Panel is still showing. A sheet that
|
||||||
|
draws over live terminal output is the Android answer to the compositing question, and it means every
|
||||||
|
future sheet on this surface has to collapse the renderer too.
|
||||||
|
|
||||||
|
### 11.3 Back lowers the menu before it leaves the terminal
|
||||||
|
|
||||||
|
With the connect menu open, use the system back gesture.
|
||||||
|
|
||||||
|
**Pass:** the menu closes and the terminal is still showing. A second back leaves the terminal for the
|
||||||
|
screen it was opened over.
|
||||||
|
|
||||||
|
**Failure means:** the guard in `PhoneShell.axaml.cs` is below the surface check rather than above it, and
|
||||||
|
one gesture is spending two levels.
|
||||||
|
|
||||||
|
### 11.4 The two end buttons cannot be pushed off the bar
|
||||||
|
|
||||||
|
Open six or more shells.
|
||||||
|
|
||||||
|
**Pass:** the pills scroll under a fixed back arrow and a fixed `+`; neither ever leaves the screen, and
|
||||||
|
scrolling the pills to either end does not move them.
|
||||||
|
|
||||||
|
**Failure means:** a control was put inside the `ScrollViewer` rather than beside it.
|
||||||
|
|
||||||
|
### 11.5 The text-size buttons are always reachable
|
||||||
|
|
||||||
|
With a shell open, scroll the accessory key row to the far left and the far right.
|
||||||
|
|
||||||
|
**Pass:** `A−` and `A+` stay pinned at the right-hand end throughout, separated from the keys by the
|
||||||
|
hairline, and both are at least 38 tall. At the smallest and largest sizes the one that can do nothing is
|
||||||
|
visibly disabled rather than silently inert.
|
||||||
|
|
||||||
|
**Failure means:** they have been folded into the scrolling row — which is the arrangement the connection
|
||||||
|
line existed to avoid, and the reason it could be removed at all.
|
||||||
|
|||||||
@@ -21,6 +21,16 @@
|
|||||||
|
|
||||||
The order is the design's rather than the rail's. Terminal sits second, beside Hosts, because those two
|
The order is the design's rather than the rail's. Terminal sits second, beside Hosts, because those two
|
||||||
are the pair a session moves between; on the desktop the terminal is not a rail entry at all.
|
are the pair a session moves between; on the desktop the terminal is not a rail entry at all.
|
||||||
|
|
||||||
|
── a terminal gets the screen ─────────────────────────────────────────────────────────────────────────
|
||||||
|
Three of the four rows below stand down while a shell is showing: the header, the shells strip and the
|
||||||
|
bottom bar itself. All three are bound on IsShowingPages, which is the same question asked once — the
|
||||||
|
surface is either a page or a terminal, and these are the chrome a page has.
|
||||||
|
|
||||||
|
The arithmetic is why. Header 56, strip 46, bar 64, and the terminal's own two rows on top of that: at
|
||||||
|
360dp the shell was framed by about a third of the display, all of it about somewhere the user was not.
|
||||||
|
What takes their place is one 52-pixel bar drawn by the surface itself, carrying back on the left and
|
||||||
|
the sessions and a + across from it. See TerminalScreen.axaml.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
@@ -49,45 +59,54 @@
|
|||||||
Hidden behind MORE, and that is the design's arrangement rather than a saving. v2 gives every screen
|
Hidden behind MORE, and that is the design's arrangement rather than a saving. v2 gives every screen
|
||||||
one header carrying that screen's own name and its own actions — a back arrow, an add, a refresh —
|
one header carrying that screen's own name and its own actions — a back arrow, an add, a refresh —
|
||||||
so the five hub screens draw their own and this one stands down rather than stacking a second row of
|
so the five hub screens draw their own and this one stands down rather than stacking a second row of
|
||||||
chrome above theirs. It stays on the three destinations that are the product's top level, where the
|
chrome above theirs. It stays on the two destinations that are the product's top level, where the
|
||||||
vault's name and the sync light are the most useful thing a header could say.
|
vault's name and the sync light are the most useful thing a header could say.
|
||||||
|
|
||||||
|
Wrapped rather than given a second condition, because Avalonia's bindings have no "and": the wrapper
|
||||||
|
collapses it over a terminal, where the surface draws its own bar and the vault's name is not what
|
||||||
|
the user is looking at. That is one of three rows this Grid stands down while a shell is showing —
|
||||||
|
see the strip and the bottom bar below.
|
||||||
-->
|
-->
|
||||||
<Border Grid.Row="0" Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
<Panel Grid.Row="0" IsVisible="{Binding IsShowingPages}">
|
||||||
BorderThickness="0,0,0,1" Padding="14,0" Height="56" IsVisible="{Binding !IsMoreSurface}">
|
<Border Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
|
BorderThickness="0,0,0,1" Padding="14,0" Height="56"
|
||||||
|
IsVisible="{Binding !IsMoreSurface}">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Filled rather than outlined since v2. The mark is the one thing on this header that is not a
|
Filled rather than outlined since v2. The mark is the one thing on this header that is not a
|
||||||
fact about the vault, and the design gives it the accent as a solid tile — which is also what
|
fact about the vault, and the design gives it the accent as a solid tile — which is also what
|
||||||
the launcher icon draws, so the two agree.
|
the launcher icon draws, so the two agree.
|
||||||
-->
|
-->
|
||||||
<Border Grid.Column="0" Width="26" Height="26" CornerRadius="8"
|
<Border Grid.Column="0" Width="26" Height="26" CornerRadius="8"
|
||||||
Background="{StaticResource Accent}" VerticalAlignment="Center">
|
Background="{StaticResource Accent}" VerticalAlignment="Center">
|
||||||
<TextBlock Text=">_" Foreground="{StaticResource AccentInk}"
|
<TextBlock Text=">_" Foreground="{StaticResource AccentInk}"
|
||||||
FontFamily="{StaticResource MonoFont}" FontSize="10" FontWeight="Bold"
|
FontFamily="{StaticResource MonoFont}" FontSize="10" FontWeight="Bold"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<TextBlock Grid.Column="1" Classes="heading" Margin="10,0,8,0" FontSize="16"
|
<TextBlock Grid.Column="1" Classes="heading" Margin="10,0,8,0" FontSize="16"
|
||||||
Text="{Binding Vault.VaultName}" TextTrimming="CharacterEllipsis" />
|
Text="{Binding Vault.VaultName}" TextTrimming="CharacterEllipsis" />
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
The sync light, and it is green only when it has earned it — see SyncLabel. The design draws a
|
The sync light, and it is green only when it has earned it — see SyncLabel. The design draws
|
||||||
permanently green "Synced" here, which is the one claim on that mock-up this application will
|
a permanently green "Synced" here, which is the one claim on that mock-up this application
|
||||||
not make.
|
will not make.
|
||||||
-->
|
-->
|
||||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
||||||
<Ellipse Classes="dot" Classes.live="{Binding IsFullySynced}" Width="6" Height="6"
|
<Ellipse Classes="dot" Classes.live="{Binding IsFullySynced}" Width="6" Height="6"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
<TextBlock Classes="label" FontSize="9" Text="{Binding SyncLabel}" />
|
<TextBlock Classes="label" FontSize="9" Text="{Binding SyncLabel}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Button Grid.Column="3" Classes="icon" Margin="4,0,0,0" Command="{Binding LockCommand}"
|
<Button Grid.Column="3" Classes="icon" Margin="4,0,0,0" Command="{Binding LockCommand}"
|
||||||
ToolTip.Tip="Lock the keychain">
|
ToolTip.Tip="Lock the keychain">
|
||||||
<TextBlock Text="LOCK" Classes="label" FontSize="8.5" Foreground="{StaticResource TextDim}" />
|
<TextBlock Text="LOCK" Classes="label" FontSize="8.5"
|
||||||
</Button>
|
Foreground="{StaticResource TextDim}" />
|
||||||
</Grid>
|
</Button>
|
||||||
</Border>
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
<!-- The screens. Only one draws; which one is the shell's business. -->
|
<!-- The screens. Only one draws; which one is the shell's business. -->
|
||||||
<Panel Grid.Row="1">
|
<Panel Grid.Row="1">
|
||||||
@@ -180,41 +199,57 @@
|
|||||||
v2 draws the sessions as pills rather than as a labelled row, and drops the word SHELLS: with a
|
v2 draws the sessions as pills rather than as a labelled row, and drops the word SHELLS: with a
|
||||||
rounded chip carrying a live dot and a name, the label was spending nine characters of a 360dp row
|
rounded chip carrying a live dot and a name, the label was spending nine characters of a 360dp row
|
||||||
saying what the row already looks like.
|
saying what the row already looks like.
|
||||||
|
|
||||||
|
On every screen except the one it names. The terminal draws these same sessions in its own bar, and
|
||||||
|
two rows of the same pills — one of them 46 pixels of it — is the arrangement this surface exists to
|
||||||
|
stop. Wrapped rather than given a second condition, because the strip's own visibility is about
|
||||||
|
whether there are any tabs and this one is about which surface is up.
|
||||||
-->
|
-->
|
||||||
<Border Grid.Row="2" IsVisible="{Binding HasTabs}" Background="{StaticResource Sidebar}"
|
<Panel Grid.Row="2" IsVisible="{Binding IsShowingPages}">
|
||||||
BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0" Height="46">
|
<Border IsVisible="{Binding HasTabs}" Background="{StaticResource Sidebar}"
|
||||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0" Height="46">
|
||||||
<ItemsControl ItemsSource="{Binding Tabs}" Margin="12,0" VerticalAlignment="Center">
|
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl ItemsSource="{Binding Tabs}" Margin="12,0" VerticalAlignment="Center">
|
||||||
<ItemsPanelTemplate><StackPanel Orientation="Horizontal" Spacing="6" /></ItemsPanelTemplate>
|
<ItemsControl.ItemsPanel>
|
||||||
</ItemsControl.ItemsPanel>
|
<ItemsPanelTemplate><StackPanel Orientation="Horizontal" Spacing="6" /></ItemsPanelTemplate>
|
||||||
<ItemsControl.ItemTemplate>
|
</ItemsControl.ItemsPanel>
|
||||||
<DataTemplate x:DataType="vm:TerminalTabViewModel">
|
<ItemsControl.ItemTemplate>
|
||||||
<Button Classes="row" MinHeight="34" Padding="13,0" CornerRadius="9"
|
<DataTemplate x:DataType="vm:TerminalTabViewModel">
|
||||||
Background="{StaticResource Panel}" BorderBrush="{StaticResource BorderMid}"
|
<Button Classes="row" MinHeight="34" Padding="13,0" CornerRadius="9"
|
||||||
BorderThickness="1"
|
Background="{StaticResource Panel}" BorderBrush="{StaticResource BorderMid}"
|
||||||
Command="{Binding $parent[views:PhoneShell].((vm:MainWindowViewModel)DataContext).SelectTabCommand}"
|
BorderThickness="1"
|
||||||
CommandParameter="{Binding}">
|
Command="{Binding $parent[views:PhoneShell].((vm:MainWindowViewModel)DataContext).SelectTabCommand}"
|
||||||
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
|
CommandParameter="{Binding}">
|
||||||
<!--
|
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
|
||||||
Green only while there is a shell behind the tab. It used to be lit unconditionally,
|
<!--
|
||||||
which was true when a tab could not exist without a session; one can now — connecting
|
Green only while there is a shell behind the tab. It used to be lit unconditionally,
|
||||||
opens the tab first — and a dot that was green before anything had answered would be
|
which was true when a tab could not exist without a session; one can now —
|
||||||
the one thing on this strip claiming something untrue.
|
connecting opens the tab first — and a dot that was green before anything had
|
||||||
-->
|
answered would be the one thing on this strip claiming something untrue.
|
||||||
<Ellipse Classes="dot" Classes.live="{Binding IsLive}" Width="6" Height="6"
|
-->
|
||||||
VerticalAlignment="Center" />
|
<Ellipse Classes="dot" Classes.live="{Binding IsLive}" Width="6" Height="6"
|
||||||
<TextBlock Classes="mono" FontSize="11" Text="{Binding Label}" />
|
VerticalAlignment="Center" />
|
||||||
</StackPanel>
|
<TextBlock Classes="mono" FontSize="11" Text="{Binding Label}" />
|
||||||
</Button>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</Button>
|
||||||
</ItemsControl.ItemTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl.ItemTemplate>
|
||||||
</ScrollViewer>
|
</ItemsControl>
|
||||||
</Border>
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
<!-- ============ navigation ============ -->
|
<!-- ============ navigation ============ -->
|
||||||
<Border Grid.Row="3" Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
<!--
|
||||||
|
Gone while a terminal is showing, which is the whole of that surface's arrangement: the bar's four
|
||||||
|
destinations are replaced by a back arrow and a + that leads to three of them, both in the terminal's
|
||||||
|
own bar. See TerminalScreen.axaml.
|
||||||
|
|
||||||
|
This one is bound directly rather than wrapped — its visibility is a single question and it has no
|
||||||
|
second condition of its own to keep separate.
|
||||||
|
-->
|
||||||
|
<Border Grid.Row="3" IsVisible="{Binding IsShowingPages}"
|
||||||
|
Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
||||||
BorderThickness="0,1,0,0" Height="64">
|
BorderThickness="0,1,0,0" Height="64">
|
||||||
<Grid ColumnDefinitions="*,*,*,*">
|
<Grid ColumnDefinitions="*,*,*,*">
|
||||||
|
|
||||||
@@ -225,8 +260,13 @@
|
|||||||
<!--
|
<!--
|
||||||
The terminal is a surface rather than a page — see ShellSurface — so this one does not go
|
The terminal is a surface rather than a page — see ShellSurface — so this one does not go
|
||||||
through ShowScreen. Its own command is on the shell.
|
through ShowScreen. Its own command is on the shell.
|
||||||
|
|
||||||
|
The only entry here that never lights, and deliberately no longer tries: this bar is collapsed
|
||||||
|
while the terminal is showing, so IsCurrent could only ever be read as false. Binding it anyway
|
||||||
|
would be a rule about a state this control cannot be in. What marks the terminal as current is
|
||||||
|
the surface filling the screen.
|
||||||
-->
|
-->
|
||||||
<views:NavButton Grid.Column="1" Label="Terminal" Glyph="⌗" IsCurrent="{Binding IsTerminalSurface}"
|
<views:NavButton Grid.Column="1" Label="Terminal" Glyph="⌗"
|
||||||
Command="{Binding ShowTerminalCommand}" />
|
Command="{Binding ShowTerminalCommand}" />
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@@ -298,6 +298,13 @@ internal sealed partial class PhoneShell : UserControl
|
|||||||
/// moving between screens at all. Closing an editor is not the same refusal as leaving a host-key
|
/// moving between screens at all. Closing an editor is not the same refusal as leaving a host-key
|
||||||
/// decision alone — an editor is abandonable by design, and the CANCEL button beside it says so.
|
/// decision alone — an editor is abandonable by design, and the CANCEL button beside it says so.
|
||||||
/// </para>
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <b>The connect menu is a second such guard, and it matters more than the first.</b> A terminal now
|
||||||
|
/// fills the screen — no header, no bottom bar — so while that menu is up this gesture is the only way
|
||||||
|
/// off it other than the scrim and CANCEL. It is checked before the terminal is dismissed for the
|
||||||
|
/// reason it is drawn over it: back takes the topmost thing, and dismissing the surface underneath a
|
||||||
|
/// menu would take two, neither of them the one being looked at.
|
||||||
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
private void OnBackRequested(object? sender, RoutedEventArgs e)
|
private void OnBackRequested(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
@@ -313,6 +320,17 @@ internal sealed partial class PhoneShell : UserControl
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The connect menu, which is raised from the terminal's own bar and is the topmost thing the phone
|
||||||
|
// draws while it is up. Ahead of the editors below because it is nearer, and ahead of leaving the
|
||||||
|
// terminal because a gesture that dismissed the surface underneath a menu would close two things at
|
||||||
|
// once — and the one the user was looking at would not be either of them.
|
||||||
|
if (current.IsConnectSheetOpen)
|
||||||
|
{
|
||||||
|
current.CloseConnectSheetCommand.Execute(null);
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (TryCloseAnOpenEditor(current))
|
if (TryCloseAnOpenEditor(current))
|
||||||
{
|
{
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
|
|||||||
@@ -16,113 +16,119 @@
|
|||||||
|
|
||||||
The tab strip is horizontal-scrolling rather than wrapping. Wrapping would reflow the terminal every
|
The tab strip is horizontal-scrolling rather than wrapping. Wrapping would reflow the terminal every
|
||||||
time a tab opened, which is the one thing a terminal must not do while output is arriving.
|
time a tab opened, which is the one thing a terminal must not do while output is arriving.
|
||||||
|
|
||||||
|
── the screen a shell gets ───────────────────────────────────────────────────────────────────────────
|
||||||
|
A connected phone shows one bar and then the terminal. The vault header, the shells strip and the
|
||||||
|
four-entry bottom bar are all collapsed by PhoneShell while this surface is up, and what replaces them
|
||||||
|
is the row below: back, the sessions, and the way to open another one.
|
||||||
|
|
||||||
|
That is a trade, and the thing bought is the only one a terminal really wants. At 360dp the chrome this
|
||||||
|
screen used to sit inside came to 254 pixels of a roughly 780-pixel display — a third of it — and every
|
||||||
|
one of those rows was about somewhere the user was not. What is given up is the bottom bar's one-tap
|
||||||
|
reach to Hosts, Keychain and MORE; back and the + between them lead to all of it, and the system back
|
||||||
|
gesture does the same thing the arrow does.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
<Panel>
|
||||||
|
|
||||||
<!-- ============ tabs ============ -->
|
<Grid RowDefinitions="Auto,*,Auto">
|
||||||
|
|
||||||
|
<!-- ============ the bar ============ -->
|
||||||
<!--
|
<!--
|
||||||
v2 draws these as pills rather than as a segmented strip, so the row is transparent and each session
|
Everything the phone draws above a shell. It is a bar rather than a strip because it now carries the
|
||||||
carries its own outline. The close cross moved inside the pill with the name, which is what makes it
|
two controls the collapsed chrome took with it, one at each end, with the sessions between them.
|
||||||
read as one object you can dismiss rather than as two adjacent targets.
|
|
||||||
-->
|
|
||||||
<Border Grid.Row="0" Height="52">
|
|
||||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
|
||||||
<ItemsControl ItemsSource="{Binding Tabs}" Margin="12,0" VerticalAlignment="Center">
|
|
||||||
<ItemsControl.ItemsPanel>
|
|
||||||
<ItemsPanelTemplate><StackPanel Orientation="Horizontal" Spacing="6" /></ItemsPanelTemplate>
|
|
||||||
</ItemsControl.ItemsPanel>
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="vm:TerminalTabViewModel">
|
|
||||||
<!--
|
|
||||||
44 tall, where the session pills on the shell strip are 34. The difference is the close
|
|
||||||
cross: a pill you only select can be chip-sized, and a pill containing the control that ends
|
|
||||||
a shell cannot. This head's rule is 44 and this is the one control on the phone that is both
|
|
||||||
destructive and has no confirmation and no undo — see CloseTabAsync, which ends the session
|
|
||||||
the moment it is pressed.
|
|
||||||
-->
|
|
||||||
<Border Background="{StaticResource Panel}" BorderBrush="{StaticResource BorderMid}"
|
|
||||||
BorderThickness="1" CornerRadius="11" Height="44">
|
|
||||||
<StackPanel Orientation="Horizontal">
|
|
||||||
<Button Classes="row" MinHeight="42" Padding="13,0" CornerRadius="11"
|
|
||||||
Command="{Binding $parent[views:TerminalScreen].((vm:MainWindowViewModel)DataContext).SelectTabCommand}"
|
|
||||||
CommandParameter="{Binding}">
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
|
|
||||||
<!-- Green only while there is a shell behind it; see the same dot in PhoneShell. -->
|
|
||||||
<Ellipse Classes="dot" Classes.live="{Binding IsLive}" Width="6" Height="6"
|
|
||||||
VerticalAlignment="Center" />
|
|
||||||
<TextBlock Classes="mono" FontSize="12" FontWeight="SemiBold"
|
|
||||||
Text="{Binding Label}" />
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
<!--
|
|
||||||
The close cross is inside the tab, which the plan calls out: a strip-level close would
|
|
||||||
act on whichever tab happened to be selected, and on a phone that is a mis-tap away from
|
|
||||||
killing the wrong shell.
|
|
||||||
|
|
||||||
The hairline down its left edge is not decoration. The two targets are flush inside one
|
Both are outside the ScrollViewer deliberately. They are the way out of this surface and the way to
|
||||||
pill, so without a visible seam there is nothing telling a thumb where "switch to this
|
another host, and a tenth tab must not be able to push either of them off the right-hand edge.
|
||||||
shell" stops and "end it" starts.
|
|
||||||
-->
|
|
||||||
<Button Classes="row" MinHeight="42" Width="44" Padding="0" CornerRadius="0,11,11,0"
|
|
||||||
HorizontalContentAlignment="Center"
|
|
||||||
BorderBrush="{StaticResource BorderMid}" BorderThickness="1,0,0,0"
|
|
||||||
Command="{Binding $parent[views:TerminalScreen].((vm:MainWindowViewModel)DataContext).CloseTabCommand}"
|
|
||||||
CommandParameter="{Binding}">
|
|
||||||
<TextBlock Text="×" Foreground="{StaticResource TextFaint}" FontSize="14" />
|
|
||||||
</Button>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
</ScrollViewer>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- ============ the connection line ============ -->
|
|
||||||
<!--
|
|
||||||
The text-size buttons live here rather than in the accessory row below, and the row is the reason:
|
|
||||||
that one scrolls, so a key can be off-screen, and these two must not be — a terminal that is too
|
|
||||||
small to read is exactly the state in which hunting for the control that fixes it is worst.
|
|
||||||
|
|
||||||
A phone cannot press Ctrl+plus. The desktop head has that chord and needs no buttons; this head has
|
|
||||||
no keyboard to press it with, which is why the two heads differ here and nowhere else in this screen.
|
|
||||||
-->
|
-->
|
||||||
<!--
|
<Border Grid.Row="0" Height="52" Background="{StaticResource Chrome}"
|
||||||
The design's line here also carries a round-trip time and a forwarded port. Neither is drawn: the SSH
|
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
||||||
library offers no RTT measurement, and nothing in this application forwards anything. What is left is
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
the one fact that is real and is the one that matters — the account and endpoint actually dialled.
|
|
||||||
-->
|
|
||||||
<Border Grid.Row="1" Padding="16,5" Height="36"
|
|
||||||
IsVisible="{Binding SelectedTab, Converter={x:Static ObjectConverters.IsNotNull}}">
|
|
||||||
<Grid ColumnDefinitions="*,Auto,Auto">
|
|
||||||
<TextBlock Grid.Column="0" Classes="detail" FontSize="10.5" TextTrimming="CharacterEllipsis"
|
|
||||||
Foreground="{StaticResource TextDim}"
|
|
||||||
VerticalAlignment="Center" Text="{Binding SelectedTab.Address}" />
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Disabled at the ends rather than clamping silently. A button that keeps accepting taps and does
|
Back, and it goes to the page this terminal was opened over rather than to Hosts by name. The
|
||||||
nothing reads as the terminal having stopped responding, which is the one thing this screen must
|
system back gesture already does exactly that — see PhoneShell.axaml.cs — and an arrow that
|
||||||
never look like.
|
landed somewhere else would be the second of two answers to one question.
|
||||||
-->
|
-->
|
||||||
<Button Grid.Column="1" Classes="row" MinHeight="34" MinWidth="38" Padding="0"
|
<Button Grid.Column="0" Classes="icon" Content="←" Margin="4,0,0,0"
|
||||||
HorizontalContentAlignment="Center"
|
Command="{Binding ShowScreenCommand}" CommandParameter="{Binding Screen}" />
|
||||||
Command="{Binding ShrinkTerminalFontCommand}"
|
|
||||||
IsEnabled="{Binding CanShrinkTerminalFont}">
|
|
||||||
<TextBlock Classes="mono" FontSize="13" Text="A−" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button Grid.Column="2" Classes="row" MinHeight="34" MinWidth="38" Padding="0" Margin="4,0,0,0"
|
<!--
|
||||||
HorizontalContentAlignment="Center"
|
v2 draws these as pills rather than as a segmented strip, so the row is transparent and each
|
||||||
Command="{Binding EnlargeTerminalFontCommand}"
|
session carries its own outline. The close cross moved inside the pill with the name, which is
|
||||||
IsEnabled="{Binding CanEnlargeTerminalFont}">
|
what makes it read as one object you can dismiss rather than as two adjacent targets.
|
||||||
<TextBlock Classes="mono" FontSize="15" Text="A+" />
|
-->
|
||||||
</Button>
|
<ScrollViewer Grid.Column="1" HorizontalScrollBarVisibility="Auto"
|
||||||
|
VerticalScrollBarVisibility="Disabled">
|
||||||
|
<ItemsControl ItemsSource="{Binding Tabs}" Margin="6,0" VerticalAlignment="Center">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate><StackPanel Orientation="Horizontal" Spacing="6" /></ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:TerminalTabViewModel">
|
||||||
|
<!--
|
||||||
|
44 tall, where the session pills on the shell strip are 34. The difference is the close
|
||||||
|
cross: a pill you only select can be chip-sized, and a pill containing the control that ends
|
||||||
|
a shell cannot. This head's rule is 44 and this is the one control on the phone that is both
|
||||||
|
destructive and has no confirmation and no undo — see CloseTabAsync, which ends the session
|
||||||
|
the moment it is pressed.
|
||||||
|
-->
|
||||||
|
<Border Background="{StaticResource Panel}" BorderBrush="{StaticResource BorderMid}"
|
||||||
|
BorderThickness="1" CornerRadius="11" Height="44">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Button Classes="row" MinHeight="42" Padding="13,0" CornerRadius="11"
|
||||||
|
Command="{Binding $parent[views:TerminalScreen].((vm:MainWindowViewModel)DataContext).SelectTabCommand}"
|
||||||
|
CommandParameter="{Binding}">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
|
||||||
|
<!-- Green only while there is a shell behind it; see the same dot in PhoneShell. -->
|
||||||
|
<Ellipse Classes="dot" Classes.live="{Binding IsLive}" Width="6" Height="6"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<TextBlock Classes="mono" FontSize="12" FontWeight="SemiBold"
|
||||||
|
Text="{Binding Label}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<!--
|
||||||
|
The close cross is inside the tab, which the plan calls out: a strip-level close would
|
||||||
|
act on whichever tab happened to be selected, and on a phone that is a mis-tap away from
|
||||||
|
killing the wrong shell.
|
||||||
|
|
||||||
|
The hairline down its left edge is not decoration. The two targets are flush inside one
|
||||||
|
pill, so without a visible seam there is nothing telling a thumb where "switch to this
|
||||||
|
shell" stops and "end it" starts.
|
||||||
|
-->
|
||||||
|
<Button Classes="row" MinHeight="42" Width="44" Padding="0" CornerRadius="0,11,11,0"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
BorderBrush="{StaticResource BorderMid}" BorderThickness="1,0,0,0"
|
||||||
|
Command="{Binding $parent[views:TerminalScreen].((vm:MainWindowViewModel)DataContext).CloseTabCommand}"
|
||||||
|
CommandParameter="{Binding}">
|
||||||
|
<TextBlock Text="×" Foreground="{StaticResource TextFaint}" FontSize="14" />
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Another connection, and the three kinds this application can make. A menu rather than a straight
|
||||||
|
jump to Hosts because SFTP and S3 used to be two taps through the bottom bar's MORE, and the bar
|
||||||
|
is not on screen here — so the control that replaces it has to lead to all three or it has quietly
|
||||||
|
removed two of them.
|
||||||
|
|
||||||
|
The desktop's own + refuses a flyout on this reasoning, in TerminalTabs.axaml: a popup dropping
|
||||||
|
into the renderer's rectangle may or may not composite above a native child window, and that is
|
||||||
|
not a claim to make without a screenshot. It is answered here rather than dodged — this is a sheet
|
||||||
|
at the bottom of the screen and opening it collapses the renderer outright, exactly as the
|
||||||
|
palette does on the desktop. Nothing is drawn over the WebView.
|
||||||
|
-->
|
||||||
|
<Button Grid.Column="2" Classes="icon accent" Content="+" Margin="0,0,4,0"
|
||||||
|
Command="{Binding OpenConnectSheetCommand}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- ============ the renderer ============ -->
|
<!-- ============ the renderer ============ -->
|
||||||
<Panel Grid.Row="2">
|
<Panel Grid.Row="1">
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
The empty state, and it says what the surface is for rather than that it is empty. A phone opens
|
The empty state, and it says what the surface is for rather than that it is empty. A phone opens
|
||||||
@@ -131,7 +137,7 @@
|
|||||||
<StackPanel IsVisible="{Binding !HasTabs}" VerticalAlignment="Center" Margin="24" Spacing="10">
|
<StackPanel IsVisible="{Binding !HasTabs}" VerticalAlignment="Center" Margin="24" Spacing="10">
|
||||||
<TextBlock Classes="title" FontSize="13" Text="NO SHELL OPEN" />
|
<TextBlock Classes="title" FontSize="13" Text="NO SHELL OPEN" />
|
||||||
<TextBlock Classes="body"
|
<TextBlock Classes="body"
|
||||||
Text="Choose a host and press CONNECT. A shell opened here keeps running while the app is in the background, and keeps running after the keychain is locked — a notification says so for as long as one is alive." />
|
Text="Press + above, or choose a host and press CONNECT. A shell opened here keeps running while the app is in the background, and keeps running after the keychain is locked — a notification says so for as long as one is alive." />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
@@ -140,10 +146,17 @@
|
|||||||
hold the application still any more, so there is a stretch in which a tab is selected and there is
|
hold the application still any more, so there is a stretch in which a tab is selected and there is
|
||||||
nothing yet to render in it. A phone needs it more than a desktop does: mobile links are slower, and
|
nothing yet to render in it. A phone needs it more than a desktop does: mobile links are slower, and
|
||||||
the alternative is a black rectangle.
|
the alternative is a black rectangle.
|
||||||
|
|
||||||
|
It carries the address, which is where that fact went when the connection line was folded into the
|
||||||
|
bar above. This is the moment it is worth reading — what is being dialled, before anything has
|
||||||
|
answered — and once a shell is open its own prompt says the same thing more accurately than a header
|
||||||
|
derived from the keychain ever did.
|
||||||
-->
|
-->
|
||||||
<StackPanel IsVisible="{Binding IsConnectingShowing}" VerticalAlignment="Center" Margin="24"
|
<StackPanel IsVisible="{Binding IsConnectingShowing}" VerticalAlignment="Center" Margin="24"
|
||||||
Spacing="10">
|
Spacing="10">
|
||||||
<TextBlock Classes="title" FontSize="13" Text="{Binding SelectedTab.Label}" />
|
<TextBlock Classes="title" FontSize="13" Text="{Binding SelectedTab.Label}" />
|
||||||
|
<TextBlock Classes="detail" FontSize="11" Foreground="{StaticResource TextDim}"
|
||||||
|
TextWrapping="Wrap" Text="{Binding SelectedTab.Address}" />
|
||||||
<TextBlock Classes="body" Text="{Binding SelectedTab.Status}" />
|
<TextBlock Classes="body" Text="{Binding SelectedTab.Status}" />
|
||||||
<Button Classes="row" MinHeight="44" Padding="14,0" HorizontalAlignment="Left"
|
<Button Classes="row" MinHeight="44" Padding="14,0" HorizontalAlignment="Left"
|
||||||
Command="{Binding CloseTabCommand}" CommandParameter="{Binding SelectedTab}">
|
Command="{Binding CloseTabCommand}" CommandParameter="{Binding SelectedTab}">
|
||||||
@@ -153,13 +166,14 @@
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
Collapsed rather than merely covered when there is no pane to show. On Windows this control is a
|
Collapsed rather than merely covered when there is no pane to show. On Windows this control is a
|
||||||
native child window that composites above everything Avalonia draws, which is why the desktop head
|
native child view that composites above everything Avalonia draws, which is why the desktop head
|
||||||
hides it explicitly; whether Android's WebView does the same is recorded as unverified in
|
hides it explicitly; whether Android's WebView does the same is recorded as unverified in
|
||||||
docs/android-port.md. Hiding it either way costs nothing and is correct under both answers.
|
docs/android-port.md. Hiding it either way costs nothing and is correct under both answers.
|
||||||
|
|
||||||
IsTerminalShowing rather than HasTabs, which are no longer the same question: a tab that is still
|
IsTerminalShowing rather than HasTabs, which are no longer the same question: a tab that is still
|
||||||
connecting has no pane, and showing the renderer for it would show the previous session's output
|
connecting has no pane, and showing the renderer for it would show the previous session's output
|
||||||
under the name of a machine nothing has connected to yet.
|
under the name of a machine nothing has connected to yet. It is also what the connect sheet turns
|
||||||
|
off — see MainWindowViewModel.IsTerminalShowing.
|
||||||
|
|
||||||
v2 insets this behind a 14-pixel radius. Not done, and not an oversight: this is a native child view
|
v2 insets this behind a 14-pixel radius. Not done, and not an oversight: this is a native child view
|
||||||
composited above everything Avalonia draws, so a rounded Border behind it clips nothing — the
|
composited above everything Avalonia draws, so a rounded Border behind it clips nothing — the
|
||||||
@@ -178,13 +192,135 @@
|
|||||||
then release, because holding a modifier while typing is not possible one-thumbed.
|
then release, because holding a modifier while typing is not possible one-thumbed.
|
||||||
-->
|
-->
|
||||||
<!-- Only with a pane to type into: the keys send bytes at a session, and a connecting tab has none. -->
|
<!-- Only with a pane to type into: the keys send bytes at a session, and a connecting tab has none. -->
|
||||||
<Border Grid.Row="3" IsVisible="{Binding IsTerminalShowing}" Height="50">
|
<Border Grid.Row="2" IsVisible="{Binding IsTerminalShowing}" Height="50">
|
||||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<StackPanel x:Name="AccessoryKeys" Orientation="Horizontal" Spacing="5" Margin="12,0"
|
|
||||||
VerticalAlignment="Center" />
|
<ScrollViewer Grid.Column="0" HorizontalScrollBarVisibility="Auto"
|
||||||
</ScrollViewer>
|
VerticalScrollBarVisibility="Disabled">
|
||||||
|
<StackPanel x:Name="AccessoryKeys" Orientation="Horizontal" Spacing="5" Margin="12,0"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The text-size buttons, pinned at this row's right-hand end rather than scrolling with the keys
|
||||||
|
beside them.
|
||||||
|
|
||||||
|
They used to have a row of their own above the terminal, on the argument that the accessory row
|
||||||
|
scrolls and these two must never be off-screen — a terminal too small to read is exactly the state
|
||||||
|
in which hunting for the control that fixes it is worst. That argument is answered rather than
|
||||||
|
abandoned: outside the ScrollViewer they cannot scroll away, and the row they had costs 36 pixels
|
||||||
|
on a surface this change exists to give back.
|
||||||
|
|
||||||
|
A phone cannot press Ctrl+plus. The desktop head has that chord and needs no buttons; this head
|
||||||
|
has no keyboard to press it with, which is why the two heads differ here and nowhere else in this
|
||||||
|
screen.
|
||||||
|
|
||||||
|
Disabled at the ends rather than clamping silently. A button that keeps accepting taps and does
|
||||||
|
nothing reads as the terminal having stopped responding, which is the one thing this screen must
|
||||||
|
never look like.
|
||||||
|
-->
|
||||||
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="5" Margin="8,0,12,0"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<Border Width="1" Height="26" Background="{StaticResource Border}" Margin="0,0,3,0"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
|
||||||
|
<Button Classes="row" MinHeight="38" Height="38" MinWidth="40" Padding="0" CornerRadius="9"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
Background="{StaticResource Panel}" BorderBrush="{StaticResource BorderMid}"
|
||||||
|
BorderThickness="1"
|
||||||
|
Command="{Binding ShrinkTerminalFontCommand}"
|
||||||
|
IsEnabled="{Binding CanShrinkTerminalFont}">
|
||||||
|
<TextBlock Classes="mono" FontSize="13" Text="A−" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button Classes="row" MinHeight="38" Height="38" MinWidth="40" Padding="0" CornerRadius="9"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
Background="{StaticResource Panel}" BorderBrush="{StaticResource BorderMid}"
|
||||||
|
BorderThickness="1"
|
||||||
|
Command="{Binding EnlargeTerminalFontCommand}"
|
||||||
|
IsEnabled="{Binding CanEnlargeTerminalFont}">
|
||||||
|
<TextBlock Classes="mono" FontSize="15" Text="A+" />
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<!-- ============ the connect sheet ============ -->
|
||||||
|
<!--
|
||||||
|
Follows the add sheet on HostsScreen, which follows HostKeySheet: a scrim, a bottom-aligned panel with
|
||||||
|
the top two corners rounded, and a grab handle that is decoration. Dismissible, like the add sheet and
|
||||||
|
unlike the host-key one — "which kind of connection" has no wrong answer and no answer at all is one
|
||||||
|
of them.
|
||||||
|
|
||||||
|
It lives here rather than in PhoneShell for the reason the add sheet lives in its own screen: nothing
|
||||||
|
but this surface raises it. The scrim reaching only the screen area is not a compromise here the way
|
||||||
|
it was there — the bottom bar is collapsed while a terminal is showing, so the screen area is the
|
||||||
|
display.
|
||||||
|
|
||||||
|
Every row navigates away from the terminal. That is not a side effect of the menu, it is the menu:
|
||||||
|
each of the three destinations is a picker, and the shell they open lands back on this surface as a
|
||||||
|
new tab in the bar above.
|
||||||
|
-->
|
||||||
|
<Panel IsVisible="{Binding IsConnectSheetOpen}">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Declared before the sheet so the sheet draws over it: a Panel stacks its children in declaration
|
||||||
|
order. See the .scrim style for why a tap on it must not light anything up.
|
||||||
|
-->
|
||||||
|
<Button Classes="scrim" Command="{Binding CloseConnectSheetCommand}" />
|
||||||
|
|
||||||
|
<Border VerticalAlignment="Bottom" Background="{StaticResource Panel}"
|
||||||
|
BorderBrush="{StaticResource BorderMid}" BorderThickness="0,1,0,0"
|
||||||
|
CornerRadius="22,22,0,0" Padding="20,18,20,16">
|
||||||
|
<StackPanel Spacing="0">
|
||||||
|
|
||||||
|
<Border Width="38" Height="4" CornerRadius="2" Background="{StaticResource BorderMid}"
|
||||||
|
HorizontalAlignment="Center" Margin="0,0,0,16" />
|
||||||
|
|
||||||
|
<TextBlock Classes="title" Text="CONNECT" FontSize="13" />
|
||||||
|
|
||||||
|
<Button Classes="row" Margin="0,10,0,0" Command="{Binding ShowScreenCommand}"
|
||||||
|
CommandParameter="{x:Static vm:ShellScreen.Hosts}">
|
||||||
|
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||||||
|
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="Connect" />
|
||||||
|
<TextBlock Classes="detail" FontSize="10.5"
|
||||||
|
Text="Another shell, on this host or any other in the keychain." />
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
These two go through ShowFiles rather than ShowScreen, as the MORE hub's own rows do: one screen
|
||||||
|
over one view model, and which kind of remote it offers is the thing being chosen. It can refuse
|
||||||
|
— there is a single transfer session behind both — and refusing lands on the screen the open one
|
||||||
|
belongs to with a sentence saying why, which is a better place to read it than a sheet that has
|
||||||
|
just closed.
|
||||||
|
-->
|
||||||
|
<Button Classes="row" Command="{Binding ShowFilesCommand}"
|
||||||
|
CommandParameter="{x:Static vm:RemoteKind.Host}">
|
||||||
|
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||||||
|
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="Connect via SFTP" />
|
||||||
|
<TextBlock Classes="detail" FontSize="10.5" Text="Browse a host's files." />
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button Classes="row" Command="{Binding ShowFilesCommand}"
|
||||||
|
CommandParameter="{x:Static vm:RemoteKind.Bucket}">
|
||||||
|
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||||||
|
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="Connect via S3" />
|
||||||
|
<TextBlock Classes="detail" FontSize="10.5"
|
||||||
|
Text="Objects in an S3-compatible bucket from the keychain." />
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button Classes="secondary" Content="CANCEL" Margin="0,12,0,0"
|
||||||
|
Command="{Binding CloseConnectSheetCommand}" />
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
</Panel>
|
||||||
|
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -795,8 +795,15 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
|||||||
/// and a child window composites above everything its parent paints — so whatever Avalonia draws in the
|
/// and a child window composites above everything its parent paints — so whatever Avalonia draws in the
|
||||||
/// same rectangle is drawn underneath it and its buttons cannot be clicked. Anything that covers the
|
/// same rectangle is drawn underneath it and its buttons cannot be clicked. Anything that covers the
|
||||||
/// terminal's area has to collapse the terminal instead, and that is every one of the conditions here: a
|
/// terminal's area has to collapse the terminal instead, and that is every one of the conditions here: a
|
||||||
/// locked vault (the unlock card), the page area (every screen uses the full width), and the
|
/// locked vault (the unlock card), the page area (every screen uses the full width), the quick-connect
|
||||||
/// quick-connect palette.
|
/// palette, and the phone's connect sheet.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <b>The sheet is here rather than in <see cref="IsTerminalSurface"/>, and the palette is not.</b> The
|
||||||
|
/// palette replaces the whole surface, so collapsing everything the terminal half draws is right. The
|
||||||
|
/// sheet is raised from the terminal's own top bar and that bar has to stay on screen behind it —
|
||||||
|
/// dropping the surface would take the bar, the tabs and the phone's whole chrome with it and leave the
|
||||||
|
/// sheet floating over the page underneath. So only the renderer's rectangle is given up.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// <b>The terminal and the pages are exclusive, and that is the whole of the rule.</b> They share one
|
/// <b>The terminal and the pages are exclusive, and that is the whole of the rule.</b> They share one
|
||||||
@@ -825,7 +832,8 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
|||||||
/// safe — that detaches it and destroys the whole WebView2 process tree.
|
/// safe — that detaches it and destroys the whole WebView2 process tree.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
internal bool IsTerminalShowing => IsTerminalSurface && SelectedTab is { HasSession: true };
|
internal bool IsTerminalShowing =>
|
||||||
|
IsTerminalSurface && !IsConnectSheetOpen && SelectedTab is { HasSession: true };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the terminal half of the window is the half being shown, pane or no pane.
|
/// Whether the terminal half of the window is the half being shown, pane or no pane.
|
||||||
@@ -889,6 +897,52 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void ShowTerminal() => Surface = ShellSurface.Terminal;
|
private void ShowTerminal() => Surface = ShellSurface.Terminal;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the phone's connect menu is open over the terminal.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Drawn by the Android head alone, and shell state rather than something that view could hold on its
|
||||||
|
/// own for the reason <see cref="IsSearching"/> is: it has to collapse the renderer while it is up. See
|
||||||
|
/// <see cref="IsTerminalShowing"/>.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// It exists because the phone gives a terminal the whole screen. The bottom bar and the vault header
|
||||||
|
/// are gone while a shell is showing, so the three things that bar was the way to — a host, a host's
|
||||||
|
/// files, a bucket — need a way back that is not "leave the terminal first and remember what you were
|
||||||
|
/// doing". The menu is that, and every entry on it is one of the two navigation commands above.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool isConnectSheetOpen;
|
||||||
|
|
||||||
|
/// <summary>Raises the connect menu over the terminal.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Gated on the terminal surface rather than merely trusting its only button to be off screen otherwise.
|
||||||
|
/// The flag collapses the renderer, so one set while a page was showing would be a sheet nobody can see
|
||||||
|
/// holding a terminal hidden that nothing would put back.
|
||||||
|
/// </remarks>
|
||||||
|
[RelayCommand]
|
||||||
|
private void OpenConnectSheet()
|
||||||
|
{
|
||||||
|
if (!IsTerminalSurface)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsConnectSheetOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Lowers the connect menu, leaving the terminal where it was.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The scrim, the CANCEL row and the system back gesture all come here. Choosing an entry does not, and
|
||||||
|
/// does not need to: every entry navigates, and leaving the terminal surface lowers the sheet on its own
|
||||||
|
/// — see <see cref="OnSurfaceChanged"/>, which is what makes "the sheet is only ever up over a terminal"
|
||||||
|
/// true of routes nobody wrote it for.
|
||||||
|
/// </remarks>
|
||||||
|
[RelayCommand]
|
||||||
|
private void CloseConnectSheet() => IsConnectSheetOpen = false;
|
||||||
|
|
||||||
// ---- Open terminals ----
|
// ---- Open terminals ----
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -2533,7 +2587,22 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="OnScreenChanged" />
|
/// <inheritdoc cref="OnScreenChanged" />
|
||||||
partial void OnSurfaceChanged(ShellSurface value) => RaiseSurfaceState();
|
/// <remarks>
|
||||||
|
/// <b>The one place the connect sheet is lowered by something other than a tap.</b> Every way out of a
|
||||||
|
/// terminal ends here — a rail or bottom-bar destination, the files screen, the palette connecting to a
|
||||||
|
/// host, closing the last tab, a lock — and each of them would otherwise leave the flag set on a shell
|
||||||
|
/// showing a page. That is not merely untidy: the flag collapses the renderer, so the next return to the
|
||||||
|
/// terminal would draw the sheet again over a rectangle held blank by it.
|
||||||
|
/// </remarks>
|
||||||
|
partial void OnSurfaceChanged(ShellSurface value)
|
||||||
|
{
|
||||||
|
if (value is not ShellSurface.Terminal)
|
||||||
|
{
|
||||||
|
IsConnectSheetOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RaiseSurfaceState();
|
||||||
|
}
|
||||||
|
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Both changes raise the same set, and they have to: <see cref="IsHostsShowing"/> and its four siblings
|
/// Both changes raise the same set, and they have to: <see cref="IsHostsShowing"/> and its four siblings
|
||||||
@@ -2596,6 +2665,9 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
|||||||
|
|
||||||
partial void OnIsSearchingChanged(bool value) => RaiseTerminalState();
|
partial void OnIsSearchingChanged(bool value) => RaiseTerminalState();
|
||||||
|
|
||||||
|
/// <inheritdoc cref="OnIsSearchingChanged" />
|
||||||
|
partial void OnIsConnectSheetOpenChanged(bool value) => RaiseTerminalState();
|
||||||
|
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The unlock card and the confirmation swap, so arming one has to hide the other — see
|
/// The unlock card and the confirmation swap, so arming one has to hide the other — see
|
||||||
/// <see cref="IsAskingForThePassphrase"/>.
|
/// <see cref="IsAskingForThePassphrase"/>.
|
||||||
|
|||||||
@@ -682,6 +682,82 @@ public sealed class ShellFlowTests : IAsyncLifetime
|
|||||||
shell.Screen.ShouldBe(ShellScreen.Vault);
|
shell.Screen.ShouldBe(ShellScreen.Vault);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// The phone's connect menu is drawn over the terminal's own rectangle, so it obeys the rule the palette
|
||||||
|
/// does: whatever covers the renderer collapses it instead. The surface stays, because the bar the menu
|
||||||
|
/// was raised from is part of it — see <c>MainWindowViewModel.IsTerminalShowing</c>.
|
||||||
|
/// </remarks>
|
||||||
|
[Fact]
|
||||||
|
public async Task TheConnectSheet_HidesTheRendererAndLeavesTheSurfaceUnderIt()
|
||||||
|
{
|
||||||
|
var vault = await ReadyToConnectAsync();
|
||||||
|
|
||||||
|
await using var renderer = await FakeRenderer.AttachAsync(workspace, Token);
|
||||||
|
await vault.ConnectCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
shell.OpenConnectSheetCommand.Execute(null);
|
||||||
|
|
||||||
|
shell.IsConnectSheetOpen.ShouldBeTrue();
|
||||||
|
shell.IsTerminalShowing.ShouldBeFalse("the sheet draws over the renderer's rectangle");
|
||||||
|
shell.IsTerminalSurface.ShouldBeTrue("the bar the sheet was raised from is on that surface");
|
||||||
|
|
||||||
|
shell.CloseConnectSheetCommand.Execute(null);
|
||||||
|
|
||||||
|
shell.IsTerminalShowing.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// The flag holds the renderer blank, so one set while a page was showing would be a sheet nobody can
|
||||||
|
/// see keeping a terminal hidden that nothing would put back.
|
||||||
|
/// </remarks>
|
||||||
|
[Fact]
|
||||||
|
public async Task TheConnectSheet_RefusesToOpenOverAPage()
|
||||||
|
{
|
||||||
|
var vault = await ReadyToConnectAsync();
|
||||||
|
|
||||||
|
await using var renderer = await FakeRenderer.AttachAsync(workspace, Token);
|
||||||
|
await vault.ConnectCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
shell.ShowScreenCommand.Execute(ShellScreen.Vault);
|
||||||
|
|
||||||
|
shell.OpenConnectSheetCommand.Execute(null);
|
||||||
|
|
||||||
|
shell.IsConnectSheetOpen.ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// Every entry on the menu navigates, and none of them closes the sheet itself: leaving the terminal
|
||||||
|
/// surface is what lowers it. That is the guarantee worth a test — it is what makes routes nobody wrote
|
||||||
|
/// the sheet for, like closing the last tab or locking, safe.
|
||||||
|
/// </remarks>
|
||||||
|
[Fact]
|
||||||
|
public async Task LeavingTheTerminal_LowersTheConnectSheetHoweverItIsLeft()
|
||||||
|
{
|
||||||
|
var vault = await ReadyToConnectAsync();
|
||||||
|
|
||||||
|
await using var renderer = await FakeRenderer.AttachAsync(workspace, Token);
|
||||||
|
await vault.ConnectCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
// The menu's own second entry: one screen, over one view model, with the kind of remote chosen by
|
||||||
|
// the thing that navigates.
|
||||||
|
shell.OpenConnectSheetCommand.Execute(null);
|
||||||
|
shell.ShowFilesCommand.Execute(RemoteKind.Bucket);
|
||||||
|
|
||||||
|
shell.IsConnectSheetOpen.ShouldBeFalse();
|
||||||
|
shell.IsBucketsShowing.ShouldBeTrue();
|
||||||
|
|
||||||
|
// And a route the sheet was never wired to: back to the terminal, open it, then end the only shell
|
||||||
|
// there is.
|
||||||
|
shell.ShowTerminalCommand.Execute(null);
|
||||||
|
shell.OpenConnectSheetCommand.Execute(null);
|
||||||
|
shell.IsConnectSheetOpen.ShouldBeTrue();
|
||||||
|
|
||||||
|
await shell.CloseTabCommand.ExecuteAsync(shell.Tabs[0]);
|
||||||
|
|
||||||
|
shell.IsConnectSheetOpen.ShouldBeFalse("closing the last tab returns the surface to a page");
|
||||||
|
shell.IsShowingPages.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// A visible WebView with no pane in it reads as the application having broken, so this is the one
|
/// A visible WebView with no pane in it reads as the application having broken, so this is the one
|
||||||
/// transition that moves the surface back on its own.
|
/// transition that moves the surface back on its own.
|
||||||
|
|||||||
Reference in New Issue
Block a user