Public Access
Let a tap on the phone's host list mean connect
Choosing a machine raised the connect bar over the bottom of the list: a password box, CONNECT, EDIT, MOVE and DELETE. Five controls in the way of the one thing a tap on a machine's name obviously means. So the gestures split. A tap connects. A long press raises the bar, with all five. The pencil in the phone's header — its only persistent chrome — edits whichever host is chosen, which is the one of the five common enough to be worth a control that is always in the same place. The flag doing it is the desktop's own IsHostPaneOpen rather than a second one. That head made exactly this move when a selection stopped opening its drawer, and the question both are asking is "has somebody asked about this host" — answering it twice is how two heads come to disagree about what a selection means. One tap cannot finish: a host that authenticates with a typed password has nowhere on a list to be given one. That tap raises the bar with the box in it and says so, and a second tap with the box filled in connects. The branch is in the view model rather than in the head, because "can this machine be reached without asking for anything" is the same question the bar's own password box answers, and a copy of it in a view would be a second reading of a binding chain that has one. Two mechanics worth knowing. Avalonia raises Tapped on release whatever the press lasted, so a long press would open the bar and then connect — one touch firing both gestures — which is why HostsScreen tracks the hold and swallows the tap it precedes. And Holding only fires once IsHoldingEnabled is set, so that and the handler are attached together rather than one in markup and one in code. ConnectToRecent now opens the pane rather than selecting the row. On the phone it has to: a selection alone raises nothing now, so going back to a recent machine would land on a screen with nothing to press.
This commit is contained in:
@@ -31,6 +31,23 @@
|
||||
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.
|
||||
|
||||
── ◆ v4: A TAP CONNECTS, AND THE BAR IS WHAT A LONG PRESS IS FOR. ───────────────────────────────────────
|
||||
Choosing a row used to raise the connect bar over the bottom of the list — a password box, CONNECT, EDIT,
|
||||
MOVE and DELETE, five controls in the way of the one thing a tap on a machine's name means. So the
|
||||
gestures split, and each one now does what it looks like it does:
|
||||
|
||||
· a tap connects, and the bar never appears;
|
||||
· a long press asks about the host: the bar comes up, with all five;
|
||||
· the pencil in the phone's header edits whatever is chosen, which is the one action common enough to
|
||||
be worth a control that is always in the same place.
|
||||
|
||||
The one case a tap cannot finish is a host that authenticates with a typed password, since there is
|
||||
nowhere to type it. That tap raises the bar with the box in it and says so — see
|
||||
VaultViewModel.ConnectToRowAsync, which is where the branch lives and why it is not in this head.
|
||||
|
||||
It is the same move the desktop grid made when a selection stopped opening its drawer, and it reuses that
|
||||
head's flag rather than inventing a second one; see VaultViewModel.IsHostPaneOpen.
|
||||
-->
|
||||
|
||||
<!--
|
||||
@@ -180,9 +197,20 @@
|
||||
</StackPanel>
|
||||
|
||||
<!-- ============ the list ============ -->
|
||||
<ListBox Grid.Row="2" Margin="0,6,0,0" IsVisible="{Binding !AnEditorIsOpen}"
|
||||
<!--
|
||||
◆ Named, because the long press is attached from code-behind: the gesture only fires at all once
|
||||
IsHoldingEnabled is on, and setting the attached property and the handler together is what keeps those
|
||||
two from drifting apart. Tapped stays here, beside the bindings it is about.
|
||||
|
||||
Both are on the list rather than on the row, and the row stays a plain Grid rather than becoming a
|
||||
Button, for the reason FilesScreen writes out: a button as the item template swallows the press before
|
||||
the list sees it, so nothing is ever selected and every control that reads the selection stops working.
|
||||
Both handlers fire after the list has moved its selection, which is what lets them read it.
|
||||
-->
|
||||
<ListBox x:Name="Rows" Grid.Row="2" Margin="0,6,0,0" IsVisible="{Binding !AnEditorIsOpen}"
|
||||
ItemsSource="{Binding SidebarRows}"
|
||||
SelectedItem="{Binding SelectedSidebarRow}"
|
||||
Tapped="OnRowTapped"
|
||||
Background="Transparent" BorderThickness="0">
|
||||
|
||||
<ListBox.Styles>
|
||||
@@ -548,8 +576,10 @@
|
||||
|
||||
<!-- ============ 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.
|
||||
◆ Raised by a long press on a row, not by choosing one. A tap connects — see the v4 note at the top of
|
||||
this file — so what is left in here is everything a tap cannot be: the password for a host that wants
|
||||
one, and EDIT, MOVE and DELETE. It names the host, because the selection that is obvious on a wide
|
||||
window is not obvious under a thumb.
|
||||
|
||||
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
|
||||
|
||||
@@ -1,10 +1,109 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
using DodoSSH.Client.Shell.ViewModels;
|
||||
|
||||
namespace DodoSSH.Client.Android.Views;
|
||||
|
||||
/// <summary>Design 02 — the host list, and the connect bar that replaces the desktop's right column.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// ◆ <b>Two gestures on one list, and this file is the whole of the difference between them.</b> A tap on a
|
||||
/// host connects to it; a long press asks about it, which is what raises the bar. Why they were split is on
|
||||
/// the screen itself; what is here is the mechanics, and there are two of them worth knowing.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The handlers are on the <c>ListBox</c> rather than on the row, and the row stays a plain <c>Grid</c>.
|
||||
/// A button as the item template swallows the press before the list sees it — <c>FilesScreen</c> writes that
|
||||
/// out at length — leaving nothing selected and every control that reads the selection doing nothing. Both
|
||||
/// events fire after the list has moved its selection, which is what lets these read it.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed partial class HostsScreen : UserControl
|
||||
{
|
||||
public HostsScreen() => AvaloniaXamlLoader.Load(this);
|
||||
/// <summary>
|
||||
/// Whether the press that is about to end was long enough to have meant something else.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Avalonia raises <c>Tapped</c> on release whatever the press lasted, so without this a long press
|
||||
/// would open the bar and then connect — the two gestures firing one after the other on one touch, which
|
||||
/// is the one outcome that would make both of them untrustworthy. Set when the hold starts and cleared
|
||||
/// by the tap it suppresses, so it never survives the gesture that set it.
|
||||
/// </remarks>
|
||||
private bool held;
|
||||
|
||||
/// <remarks>
|
||||
/// The long press is attached here rather than in the markup so that it sits beside the property that
|
||||
/// makes it fire at all. <see cref="InputElement.IsHoldingEnabledProperty"/> is set rather than assumed:
|
||||
/// it is the whole of the gesture, and a default that changed would take it away silently — every tap
|
||||
/// would go on working and nothing would ever open the bar again.
|
||||
/// </remarks>
|
||||
public HostsScreen()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
|
||||
InputElement.SetIsHoldingEnabled(Rows, true);
|
||||
Rows.Holding += OnRowHeld;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the row that was tapped.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Guarded on the selection being a host rather than on what was under the finger. A tap on a group
|
||||
/// heading moves the list's selection and the view model bounces it straight back to whatever was chosen
|
||||
/// before — see <c>VaultViewModel.SelectedSidebarRow</c> — so reading the selection here answers "a host,
|
||||
/// or nothing" without this file needing to know that rule. The cost of getting it wrong is connecting to
|
||||
/// a machine the user was not pointing at.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Fire-and-forget, as the desktop grid's activation is: the command reports its own failures onto the
|
||||
/// status line — an unknown host key, a refused password — and awaiting it here would be an event handler
|
||||
/// returning a task nothing observes.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private void OnRowTapped(object? sender, TappedEventArgs e)
|
||||
{
|
||||
if (held)
|
||||
{
|
||||
held = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (DataContext is VaultViewModel { SelectedHost: { } row } vault)
|
||||
{
|
||||
_ = vault.ConnectToRowCommand.ExecuteAsync(row);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the bar about the row that was held.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// On <see cref="HoldingState.Started"/> rather than on completion, so the bar is up while the finger is
|
||||
/// still down. A long press that showed nothing until release would be a gesture with no way to tell it
|
||||
/// had been recognised, and the only feedback available on this list is the thing it does.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// It goes through <c>OpenHostPaneCommand</c>, which is the desktop's own "ask about this host" — the two
|
||||
/// heads raise different furniture from one flag rather than keeping a selection rule each.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private void OnRowHeld(object? sender, HoldingRoutedEventArgs e)
|
||||
{
|
||||
if (e.HoldingState != HoldingState.Started)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
held = true;
|
||||
|
||||
if (DataContext is VaultViewModel { SelectedHost: { } row } vault)
|
||||
{
|
||||
vault.OpenHostPaneCommand.Execute(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
<Border Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="0,0,0,1" Padding="14,0" Height="56"
|
||||
IsVisible="{Binding !IsMoreSurface}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto">
|
||||
|
||||
<!--
|
||||
Filled rather than outlined since v2. The mark is the one thing on this header that is not a
|
||||
@@ -100,7 +100,31 @@
|
||||
<TextBlock Classes="label" FontSize="9" Text="{Binding SyncLabel}" />
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="3" Classes="icon" Margin="4,0,0,0" Command="{Binding LockCommand}"
|
||||
<!--
|
||||
◆ THE PENCIL, and it is here rather than on the row for the reason the whole gesture split
|
||||
happened: choosing a host on this head no longer raises a bar carrying EDIT, because that bar
|
||||
was five controls over the bottom of the list in the way of a tap that means "connect". EDIT is
|
||||
the one of the five common enough to be worth a control that is always in the same place, so it
|
||||
is in the header — the phone's only piece of persistent chrome — and the long press still
|
||||
reaches the other four. See HostsScreen.axaml.
|
||||
|
||||
Two conditions, nested rather than combined, because Avalonia's bindings have no "and" and the
|
||||
two belong to different view models: which screen is showing is the shell's question, and
|
||||
whether there is a host to edit is the vault's. That is the same arrangement the header itself
|
||||
is wrapped in one level up.
|
||||
|
||||
Collapsed rather than disabled when there is nothing chosen. A greyed pencil sitting beside the
|
||||
vault's name on every screen would be a permanent reminder of a control that is only ever about
|
||||
one row.
|
||||
-->
|
||||
<Panel Grid.Column="3" IsVisible="{Binding IsHostsShowing}">
|
||||
<Button Classes="icon" Content="✎" FontSize="15" Margin="4,0,0,0"
|
||||
IsVisible="{Binding Vault.CanEditSelectedHost}"
|
||||
Command="{Binding Vault.EditSelectedHostCommand}"
|
||||
ToolTip.Tip="Edit the selected host" />
|
||||
</Panel>
|
||||
|
||||
<Button Grid.Column="4" Classes="icon" Margin="4,0,0,0" Command="{Binding LockCommand}"
|
||||
ToolTip.Tip="Lock the keychain">
|
||||
<TextBlock Text="LOCK" Classes="label" FontSize="8.5"
|
||||
Foreground="{StaticResource TextDim}" />
|
||||
|
||||
Reference in New Issue
Block a user