using Avalonia.Controls; using Avalonia.Input; using Avalonia.Markup.Xaml; using DodoSSH.Client.Shell.ViewModels; namespace DodoSSH.Client.Android.Views; /// Design 02 — the host list, and the connect bar that replaces the desktop's right column. /// /// /// ◆ Two gestures on one list, and this file is the whole of the difference between them. 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. /// /// /// The handlers are on the ListBox rather than on the row, and the row stays a plain Grid. /// A button as the item template swallows the press before the list sees it — FilesScreen 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. /// /// internal sealed partial class HostsScreen : UserControl { /// /// Whether the press that is about to end was long enough to have meant something else. /// /// /// Avalonia raises Tapped 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. /// private bool held; /// /// /// The long press is attached here rather than in the markup so that it sits beside the property that /// makes it fire at all. 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. /// /// /// ◆ FindControl rather than the field the name generator declares for x:Name, /// which is the same rule PhoneShell and TerminalScreen already carry, and which this /// file learned the hard way. That field is /// assigned by the generated InitializeComponent, 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 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. /// /// public HostsScreen() { AvaloniaXamlLoader.Load(this); var rows = this.FindControl("Rows")!; InputElement.SetIsHoldingEnabled(rows, true); rows.Holding += OnRowHeld; } /// /// Connects to the row that was tapped. /// /// /// /// 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 VaultViewModel.SelectedSidebarRow — 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. /// /// /// 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. /// /// private void OnRowTapped(object? sender, TappedEventArgs e) { if (held) { held = false; return; } if (DataContext is VaultViewModel { SelectedHost: { } row } vault) { _ = vault.ConnectToRowCommand.ExecuteAsync(row); } } /// /// Raises the bar about the row that was held. /// /// /// /// On 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. /// /// /// It goes through OpenHostPaneCommand, 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. /// /// 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); } } }