Public Access
Nightly 0.0.0-alpha.0.133 died before its first frame. The long press I added attached itself in HostsScreen's constructor through the field the Avalonia name generator declares for `x:Name` — and that field is assigned by the generated InitializeComponent, which no view in this repository calls. Every one of them loads its XAML directly. So the field compiles, resolves in the editor, and is null at run time; PhoneShell builds this control on the way up, so the NullReferenceException took the launch rather than the hosts screen. PhoneShell and TerminalScreen both use FindControl, and PhoneShell carries a <remarks> saying exactly this and naming exactly this consequence. I read neither and wrote the field. So the rule is in docs/platform-flags.md now as well. A comment on the control that already got it right is not where somebody writing a new one is looking, which is the whole of why two correct examples and one warning were not enough. And phase 8 opens with "it launches at all". Nothing on this head is covered by a test — no test project, no headless surface — so a view that throws while being built takes the launch with it and no gate anywhere says so. Thirty seconds, and it would have caught this one before it was published.
124 lines
5.4 KiB
C#
124 lines
5.4 KiB
C#
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
|
|
{
|
|
/// <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>
|
|
/// <para>
|
|
/// 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.
|
|
/// </para>
|
|
/// <para>
|
|
/// ◆ <b><c>FindControl</c> rather than the field the name generator declares for <c>x:Name</c></b>,
|
|
/// which is the same rule <c>PhoneShell</c> and <c>TerminalScreen</c> already carry, and which this
|
|
/// file learned the hard way. That field is
|
|
/// assigned by the generated <c>InitializeComponent</c>, and no view on this head calls it — they load
|
|
/// their XAML directly, as above. So the field compiles, is null at run time, and the first thing
|
|
/// touching it throws. Here that was a <see cref="NullReferenceException"/> in this constructor, which
|
|
/// is a crash before the first frame: the phone shell builds this control on the way up, so the
|
|
/// application died on launch rather than on the hosts screen.
|
|
/// </para>
|
|
/// </remarks>
|
|
public HostsScreen()
|
|
{
|
|
AvaloniaXamlLoader.Load(this);
|
|
|
|
var rows = this.FindControl<ListBox>("Rows")!;
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|