Public Access
Give the phone the + it was promised, and the editors behind it
Steps 5 and 6 of docs/adding-hosts-on-the-phone.md, which finishes it. The phone can now put things in a keychain rather than only read one. The floating + arrives three designs after it was first asked for. It was refused twice on the honest grounds that it would open an editor this head had not got — and that stopped being true when steps 1 to 4 landed. It raises a sheet rather than a form, because "add" on this screen has been two operations since groups existed: a host, or a group to file hosts under. Both editors are cards in the list's own row, swapped for the list rather than stacked over it, following SnippetsScreen and for the reason written there: a form on top of the list hides what it is about. There is still no dialog and no editor screen anywhere on this head. The port and username boxes may be left empty, and the greyed text inside each is what the host will actually use — following the group picker as it moves, so the form says what leaving a box blank will do rather than making it a guess. The back gesture gains a guard above the switch rather than a case inside it. An editor is not a screen and has no entry there, and it is strictly nearer: the sheet sits over the editor's own screen, so back lowers whatever is topmost before it considers moving between screens. A group editor needs a way in, and the phone draws no groups panel — so the heading in the host list gains a pencil. A + that adds groups with no way to correct one is the same strange thing to ship as one that adds hosts with no way to correct one, and worse: a group's defaults are what every host beneath it falls back to. Scouting step 6 turned up a bug worth more than the prose it was looking for. ReloadGroupsAsync read the active vault alone, which was a cosmetic limitation while a group carried only a name — a teammate's host showed under UNGROUPED and nothing else was lost. Since a group began lending a port, a username and a binding, the same omission silently dropped all three: that host would dial 22 as nobody while the machine is on 2222 as deploy, with nothing on screen saying why. The resolution map now spans every readable vault. The editable list stays narrow, because a row shown across vaults needs a vault id for rename and delete and two vaults may hold groups with one name — but the map needs none of that, since it is only ever asked what an id says. An adversarial review of this change found something I had written a comment denying. The + was a child of the outer Panel rather than of the list's row, so it anchored to the bottom of the screen and sat squarely over the new EDIT button and the lower edge of CONNECT — and because a Panel hit-tests its last-declared child first, tapping the right end of EDIT would have raised the add sheet. The comment beside it claimed it was in the list's row precisely so that could not happen, and manual-checks 8.1 names that exact failure. It is in Grid.Row 2 now. The same pass caught a connect bar that was disabled rather than hidden while its own comment said hidden, a scrim that would have flashed its default pressed chrome across the screen, a fab comment claiming an accent fill no other button had when Button.primary has it, and a back-gesture remark naming two view-model properties that do not exist anywhere in the repo. That last one came from this plan, which invented IsVaultsSurface and IsConnectionsSurface, and pointed step 6 at a ConnectionsScreen.axaml that has never existed. Both are corrected in the plan rather than quietly worked around, along with a fourth "groups are flat" site in README.md that step 1 missed. The phone's rectangles are not measurable and structurally never will be: the layout suite is net10.0, the Android head is net10.0-android, and Avalonia's application is a one-shot process global — so a second head cannot share the process even if the reference were possible. Everything the sheet and both editors bind to is shared and is tested headlessly here; the pixels go to docs/manual-checks.md phase 8, nine checks, which is where this project already sends what it cannot assert. Tags remain the one thing asked for that did not ship. Tag is a full item kind and TagIds merges, encodes and resolves, but no screen draws a chip or offers to add one, so the tags a client can store are ones nothing here can see. Both editors carry the set through a save untouched so a client that can set them does not lose them. Recorded as half-shipped in docs/design-import-gaps.md rather than quietly left out. Verified by the whole suite: 1402 tests over nineteen projects, none failing. Both heads build. Nothing seen on a display — phase 8 is what that costs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
||||
xmlns:views="using:DodoSSH.Client.Android.Views"
|
||||
x:Class="DodoSSH.Client.Android.Views.HostsScreen"
|
||||
x:DataType="vm:VaultViewModel"
|
||||
Background="{StaticResource Canvas}">
|
||||
@@ -18,11 +19,27 @@
|
||||
|
||||
── v2 ────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
The rows became cards: rounded, inset from the edge, and filled when chosen rather than marked with a
|
||||
strip. What did not come across is the design's floating + button. It would open a host editor, and this
|
||||
head has none — hosts are created on the desktop and sync down. A button that opened nothing would be
|
||||
the one control on this screen that lied about what the phone can do; see docs/design-import-gaps.md.
|
||||
strip.
|
||||
|
||||
── v3 ────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
The design's floating + arrives, three versions after it was first asked for. It was refused twice on
|
||||
the grounds that it would open an editor this head had not got — hosts were created on the desktop and
|
||||
synced down — and that was true until the editors below existed. It opens a sheet rather than a form,
|
||||
because "add" on this screen has been two operations since groups existed: a host, or a group to file
|
||||
hosts under.
|
||||
|
||||
Both editors are cards in the list's own row, swapped for the list rather than stacked over it, which is
|
||||
what SnippetsScreen does and for the reason written there: a form on top of the list hides what it is
|
||||
about. There is no dialog and no editor screen anywhere on this head.
|
||||
-->
|
||||
|
||||
<!--
|
||||
The Panel is what lets the add sheet cover the screen. The + does not need it — it is layered inside the
|
||||
list's own Grid row, which is where it has to be so that the connect bar pushes it up rather than being
|
||||
covered by it. See the button itself.
|
||||
-->
|
||||
<Panel>
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
||||
|
||||
<!-- ============ filter ============ -->
|
||||
@@ -75,7 +92,7 @@
|
||||
</StackPanel>
|
||||
|
||||
<!-- ============ the list ============ -->
|
||||
<ListBox Grid.Row="2" Margin="0,6,0,0"
|
||||
<ListBox Grid.Row="2" Margin="0,6,0,0" IsVisible="{Binding !AnEditorIsOpen}"
|
||||
ItemsSource="{Binding SidebarRows}"
|
||||
SelectedItem="{Binding SelectedSidebarRow}"
|
||||
Background="Transparent" BorderThickness="0">
|
||||
@@ -106,10 +123,24 @@
|
||||
the filter, which is what a heading over a filtered list has to say.
|
||||
-->
|
||||
<DataTemplate DataType="vm:SidebarGroupHeader">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="8,12,8,5">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" Margin="8,12,8,5">
|
||||
<TextBlock Grid.Column="0" Classes="detail" Text="{Binding Chevron}" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="1" Classes="section" Text="{Binding Label}" Margin="7,0,0,0" />
|
||||
<TextBlock Grid.Column="2" Classes="detail" Text="{Binding Count}" FontSize="9" />
|
||||
<TextBlock Grid.Column="2" Classes="detail" Text="{Binding Count}" FontSize="9"
|
||||
VerticalAlignment="Center" />
|
||||
|
||||
<!--
|
||||
The phone's only way into a group's editor. The desktop reaches it from a groups panel this
|
||||
head does not draw, and a heading's own selection deliberately bounces back to the host — so
|
||||
without this a group could be created here and never corrected, which matters more for a group
|
||||
than for a host: its defaults are what every host beneath it falls back to.
|
||||
|
||||
Hidden on the ungrouped heading, which has no group behind it to edit.
|
||||
-->
|
||||
<Button Grid.Column="3" Classes="icon" FontSize="13" Content="✎" Margin="4,0,0,0"
|
||||
IsVisible="{Binding GroupId, Converter={x:Static ObjectConverters.IsNotNull}}"
|
||||
Command="{Binding $parent[views:HostsScreen].((vm:VaultViewModel)DataContext).EditGroupFromHeadingCommand}"
|
||||
CommandParameter="{Binding}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -158,12 +189,170 @@
|
||||
</ListBox.DataTemplates>
|
||||
</ListBox>
|
||||
|
||||
<!-- ============ the host editor ============ -->
|
||||
<!--
|
||||
In the list's row and swapped for it, following SnippetsScreen. Every field is the shared view model's
|
||||
— the same properties the desktop's sidebar binds — so the two heads cannot drift about what a host
|
||||
is or what saving one does.
|
||||
|
||||
◆ The port and username boxes may be left empty, and empty is not "unset": it means the host takes its
|
||||
group's. The watermark on each is what it will actually get, and it follows the group picker below as
|
||||
that moves, so the form says what leaving a box blank will do rather than making it a guess. See
|
||||
HostInheritance.
|
||||
-->
|
||||
<Border Grid.Row="2" Classes="card" Margin="12,0" IsVisible="{Binding IsEditing}"
|
||||
VerticalAlignment="Top">
|
||||
<ScrollViewer MaxHeight="560">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Classes="label" Text="HOST" />
|
||||
|
||||
<TextBox Classes="field" Text="{Binding EditorLabel}" PlaceholderText="name" />
|
||||
<TextBox Classes="field" Text="{Binding EditorHostname}" PlaceholderText="hostname or address" />
|
||||
|
||||
<Grid ColumnDefinitions="*,8,2*">
|
||||
<NumericUpDown Grid.Column="0" Value="{Binding EditorPort}" Minimum="1" Maximum="65535"
|
||||
FormatString="0" ShowButtonSpinner="False" MinHeight="44"
|
||||
PlaceholderText="{Binding EditorPortPlaceholder}" />
|
||||
<TextBox Grid.Column="2" Classes="field" Text="{Binding EditorUsername}"
|
||||
PlaceholderText="{Binding EditorUsernamePlaceholder}" />
|
||||
</Grid>
|
||||
|
||||
<TextBox Classes="field" Text="{Binding EditorNotes}" PlaceholderText="notes" />
|
||||
|
||||
<!--
|
||||
One picker for all of them, as on the desktop, which is what makes "a key or a credential, never
|
||||
both" impossible to express rather than merely invalid. The qualifier beside each label matters
|
||||
for the same reason it does there: a key called deploy and the deploy account's password are the
|
||||
ordinary case, and bare labels would offer two identical-looking entries.
|
||||
-->
|
||||
<TextBlock Classes="label" Text="AUTHENTICATION" Margin="0,4,0,0" />
|
||||
<ComboBox ItemsSource="{Binding EditorAuthenticationChoices}"
|
||||
SelectedItem="{Binding EditorSelectedAuthentication}"
|
||||
HorizontalAlignment="Stretch" MinHeight="44">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:AuthenticationChoice">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Classes="mono" FontSize="12" Text="{Binding Label}" />
|
||||
<TextBlock Classes="detail" FontSize="10" VerticalAlignment="Center"
|
||||
Text="{Binding Qualifier}"
|
||||
IsVisible="{Binding Qualifier, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Classes="label" Text="GROUP" Margin="0,4,0,0" />
|
||||
<ComboBox ItemsSource="{Binding EditorGroupChoices}"
|
||||
SelectedItem="{Binding EditorSelectedGroup}"
|
||||
HorizontalAlignment="Stretch" MinHeight="44">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:GroupChoice">
|
||||
<TextBlock Classes="mono" FontSize="12" Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<!--
|
||||
◆ The one control here that publishes something. Turning it on copies this host's address and
|
||||
port into plaintext columns the server can read, which is the single deliberate concession in
|
||||
the design — see ADR 0004 — so it says so rather than being a switch labelled "relay".
|
||||
-->
|
||||
<CheckBox IsChecked="{Binding EditorRelayEnabled}" MinHeight="44">
|
||||
<TextBlock Classes="mono" FontSize="11.5" Text="Reach this host through the server relay"
|
||||
TextWrapping="Wrap" />
|
||||
</CheckBox>
|
||||
<TextBlock Classes="body"
|
||||
Text="The relay dials on your behalf, so this host's address and port are stored on the server in the clear. Everything else about it stays encrypted. A relayed host also needs a port of its own rather than its group's." />
|
||||
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<Button Grid.Column="0" Classes="primary" Height="44" Content="SAVE"
|
||||
Command="{Binding SaveHostCommand}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||||
Command="{Binding CancelEditCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- ============ the group editor ============ -->
|
||||
<!--
|
||||
The same card in the same row, and a group is now enough of a thing to need one: a name, where it sits
|
||||
in the tree, and the three things hosts inside it fall back to. Every one of those may be left empty,
|
||||
and empty means the group lends nothing rather than that it has not been filled in.
|
||||
|
||||
The parent picker leaves out this group and everything beneath it, so a cycle cannot be made here. That
|
||||
is a courtesy rather than the guarantee — one assembled from two offline re-parents was never offered
|
||||
this list, which is why the walk itself stops at a repeat.
|
||||
-->
|
||||
<Border Grid.Row="2" Classes="card" Margin="12,0" IsVisible="{Binding IsEditingGroup}"
|
||||
VerticalAlignment="Top">
|
||||
<ScrollViewer MaxHeight="560">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Classes="label" Text="GROUP" />
|
||||
|
||||
<TextBox Classes="field" Text="{Binding GroupEditorLabel}" PlaceholderText="group name" />
|
||||
|
||||
<TextBlock Classes="label" Text="INSIDE" Margin="0,4,0,0" />
|
||||
<ComboBox ItemsSource="{Binding GroupEditorParentChoices}"
|
||||
SelectedItem="{Binding GroupEditorSelectedParent}"
|
||||
HorizontalAlignment="Stretch" MinHeight="44">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:GroupChoice">
|
||||
<TextBlock Classes="mono" FontSize="12" Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Classes="label" Text="HOSTS INSIDE FALL BACK TO" Margin="0,6,0,0" />
|
||||
|
||||
<Grid ColumnDefinitions="*,8,2*">
|
||||
<NumericUpDown Grid.Column="0" Value="{Binding GroupEditorDefaultPort}" Minimum="1"
|
||||
Maximum="65535" FormatString="0" ShowButtonSpinner="False" MinHeight="44"
|
||||
PlaceholderText="no default" />
|
||||
<TextBox Grid.Column="2" Classes="field" Text="{Binding GroupEditorDefaultUsername}"
|
||||
PlaceholderText="no default user" />
|
||||
</Grid>
|
||||
|
||||
<ComboBox ItemsSource="{Binding GroupEditorAuthenticationChoices}"
|
||||
SelectedItem="{Binding GroupEditorSelectedAuthentication}"
|
||||
HorizontalAlignment="Stretch" MinHeight="44">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:AuthenticationChoice">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Classes="mono" FontSize="12" Text="{Binding Label}" />
|
||||
<TextBlock Classes="detail" FontSize="10" VerticalAlignment="Center"
|
||||
Text="{Binding Qualifier}"
|
||||
IsVisible="{Binding Qualifier, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Classes="body"
|
||||
Text="A host inside this group uses these wherever it says nothing itself. They are read at connect time rather than copied in, so changing one here changes every host that never overrode it." />
|
||||
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<Button Grid.Column="0" Classes="primary" Height="44" Content="SAVE"
|
||||
Command="{Binding SaveGroupCommand}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||||
Command="{Binding CancelGroupEditCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- ============ connect ============ -->
|
||||
<!--
|
||||
Raised over the list when a host is chosen, because the desktop's right-hand column has nowhere to go
|
||||
at this width. It names the host: the selection that is obvious on a wide window is not obvious here.
|
||||
|
||||
Gone entirely while an editor is up, rather than merely greyed. The editor replaces the list above it,
|
||||
so a bar left in place would be a set of buttons about a host that is no longer on screen — and the
|
||||
one underneath the host editor would be about the very record being typed into. Disabling it was the
|
||||
first attempt and it is worse: a dimmed CONNECT under an open form reads as a screen that has broken
|
||||
rather than as one that is busy.
|
||||
-->
|
||||
<Border Grid.Row="3" IsVisible="{Binding SelectedHost, Converter={x:Static ObjectConverters.IsNotNull}}"
|
||||
<Border Grid.Row="3" IsVisible="{Binding ShowsConnectBar}"
|
||||
Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="0,1,0,0" Padding="14,12">
|
||||
<StackPanel Spacing="10">
|
||||
@@ -189,9 +378,100 @@
|
||||
|
||||
<Button Classes="primary" Content="CONNECT" Command="{Binding ConnectCommand}"
|
||||
IsEnabled="{Binding !IsBusy}" />
|
||||
|
||||
<!--
|
||||
Not asked for by the design, and here because a + that adds hosts with no way to correct one is a
|
||||
strange thing to ship. It costs nothing: the editor above serves both, so this is the same panel
|
||||
opened on an existing row.
|
||||
-->
|
||||
<Button Classes="secondary" Height="44" Content="EDIT"
|
||||
Command="{Binding EditSelectedHostCommand}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ============ add ============ -->
|
||||
<!--
|
||||
◆ The design's floating +, and the reason every version of this file until now said it could not be
|
||||
drawn.
|
||||
|
||||
<b>Grid.Row 2, inside the Grid — not the Panel around it — and the difference is the whole control.</b>
|
||||
The plan asked for the outer Panel so the button would not move. A Panel child aligned to the bottom is
|
||||
anchored to the bottom of the screen, and the connect bar is a row of that same Grid: the 56px circle
|
||||
would then sit squarely over EDIT and the lower edge of CONNECT, and because a Panel hit-tests its
|
||||
last-declared child first, tapping the right end of EDIT would raise the add sheet instead. Covering
|
||||
CONNECT is the one thing on this screen that must not happen.
|
||||
|
||||
In the list's row it rides up as the connect bar appears, which is what a floating button does on every
|
||||
other Android application and what a thumb reaches for. It moves; that is the cost, and it is the
|
||||
smaller one.
|
||||
|
||||
Hidden rather than disabled while anything is open over the list. A + that opens an editor on top of an
|
||||
open editor is a control with nothing to do, and disabling it would leave a large accented circle on
|
||||
screen saying so.
|
||||
-->
|
||||
<Button Grid.Row="2" Classes="fab" Content="+" Margin="0,0,18,18"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Bottom"
|
||||
IsVisible="{Binding !AnEditorIsOpen}"
|
||||
Command="{Binding OpenAddSheetCommand}" />
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- ============ the add sheet ============ -->
|
||||
<!--
|
||||
Follows HostKeySheet, this head's one existing sheet: a scrim, a bottom-aligned panel with the top two
|
||||
corners rounded, and a grab handle that is decoration. Two things it does differently, both because it
|
||||
is a menu rather than a decision.
|
||||
|
||||
It is dismissible. Tapping the scrim closes it, which HostKeySheet deliberately refuses — answering
|
||||
"do you trust this key" by tapping beside it is the swipe-to-dismiss that head was built to avoid.
|
||||
"Which kind of thing are you adding" has no wrong answer and no answer at all is one of them.
|
||||
|
||||
And it lives here rather than in PhoneShell. HostKeySheet sits up there because the files screen raises
|
||||
it too; nothing but this screen raises this. The cost is that the scrim covers the screen and not the
|
||||
64px nav bar underneath, so the four destinations stay lit and tappable — which for a menu is right:
|
||||
navigating away is a perfectly good way to decide not to add anything.
|
||||
-->
|
||||
<Panel IsVisible="{Binding IsAddSheetOpen}">
|
||||
|
||||
<!--
|
||||
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 CloseAddSheetCommand}" />
|
||||
|
||||
<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="ADD" FontSize="13" />
|
||||
|
||||
<Button Classes="row" Margin="0,10,0,0" Command="{Binding NewHostCommand}">
|
||||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="New host" />
|
||||
<TextBlock Classes="detail" FontSize="10.5"
|
||||
Text="A machine to connect to, filed under a group if you want one." />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button Classes="row" Command="{Binding NewGroupCommand}">
|
||||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="New group" />
|
||||
<TextBlock Classes="detail" FontSize="10.5"
|
||||
Text="A heading for hosts, and the port, user and key they fall back to." />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button Classes="secondary" Content="CANCEL" Margin="0,12,0,0"
|
||||
Command="{Binding CloseAddSheetCommand}" />
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Panel>
|
||||
|
||||
</Panel>
|
||||
|
||||
</UserControl>
|
||||
|
||||
@@ -168,6 +168,15 @@ internal sealed partial class PhoneShell : UserControl
|
||||
/// the host list it does nothing at all, so back still leaves the application from the screen the
|
||||
/// application opens on, which is what every other Android app does.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>v3 adds one guard above the switch rather than another case inside it.</b> The switch's first case
|
||||
/// is the membership test of <see cref="MainWindowViewModel.IsMoreSurface"/> minus <c>More</c> itself,
|
||||
/// and has to stay in step with it — a screen added to the hub and not to that case would trap the user
|
||||
/// on it. An editor is not a screen and has no entry there. It is also strictly nearer: the add sheet
|
||||
/// sits over the host editor's own screen, so back has to lower whatever is topmost before it considers
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private void OnBackRequested(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
@@ -183,6 +192,12 @@ internal sealed partial class PhoneShell : UserControl
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryCloseAnOpenEditor(current))
|
||||
{
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!current.IsShowingPages)
|
||||
{
|
||||
current.ShowScreenCommand.Execute(current.Screen);
|
||||
@@ -209,6 +224,51 @@ internal sealed partial class PhoneShell : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lowers whatever the hosts screen has raised over its list, topmost first.
|
||||
/// </summary>
|
||||
/// <returns>Whether anything was closed, and so whether back has been spent.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Order is the whole of it. The add sheet sits over the list and the two editors sit in place of it, so
|
||||
/// the sheet has to go first — closing an editor while a sheet was open would leave the sheet floating
|
||||
/// over a list nobody asked to see, and the second back would then close the sheet rather than the
|
||||
/// editor the user was looking at.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The editors are cancelled rather than merely hidden. Cancelling is what clears the boxes, and the
|
||||
/// host editor's boxes are the ones worth clearing: leaving a half-typed hostname behind would have the
|
||||
/// next NEW HOST open on somebody else's abandoned draft.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private static bool TryCloseAnOpenEditor(MainWindowViewModel current)
|
||||
{
|
||||
if (current.Vault is not { } vault)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (vault.IsAddSheetOpen)
|
||||
{
|
||||
vault.CloseAddSheetCommand.Execute(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (vault.IsEditing)
|
||||
{
|
||||
vault.CancelEditCommand.Execute(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (vault.IsEditingGroup)
|
||||
{
|
||||
vault.CancelGroupEditCommand.Execute(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Blocks screenshots and screen recording while the recovery code is on screen.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user