using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using Avalonia.VisualTree;
using DodoSSH.Client.Shell.ViewModels;
namespace DodoSSH.Client.Android.Views;
/// Design 02 — the host list, and the two gestures that are the whole of this file.
///
///
/// ◆ A tap connects, a long press chooses, and once anything is chosen a tap ticks instead. The
/// branch in the middle of that is why the tap is handled here rather than bound in the markup: which of
/// the two a tap means depends on whether the screen is in selection mode, and Avalonia's bindings cannot
/// ask.
///
///
/// The handlers are on the ListBox rather than on the row, and the row stays a plain
/// Border. A button as the item template swallows the press before the list sees it —
/// FilesScreen writes that out at length — so neither handler would ever run.
///
///
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 tick the row and then the tap it becomes would untick it again — one touch doing a thing and
/// undoing it, which is the one outcome that would make both gestures 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;
///
///
/// Both gestures are attached here rather than in the markup so that they sit beside the property that
/// makes one of them fire at all. is set rather than
/// assumed: it is the whole of the long press, and a default that changed would take it away silently —
/// every tap would go on working and nothing would ever choose a host 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;
var sheet = this.FindControl("PasswordSheet")!;
var box = this.FindControl("PasswordBox")!;
sheet.PropertyChanged += (_, e) =>
{
if (e.Property == IsVisibleProperty && sheet.IsVisible)
{
FocusThePasswordBox(box);
}
};
}
///
/// Puts the focus, and so the keyboard, in the connect sheet's box as the sheet opens.
///
///
/// The sheet exists for that one box, and it used to open with nothing focused — a second tap before
/// typing could start (audit M12). Posted at Loaded priority because the box is not yet laid out
/// on the turn the sheet's visibility changes, and focusing a control with no bounds raises no keyboard.
///
private static void FocusThePasswordBox(TextBox box) =>
Dispatcher.UIThread.Post(() => box.Focus(NavigationMethod.Tab), DispatcherPriority.Loaded);
///
/// Connects to the row that was tapped, or ticks it where hosts are already ticked.
///
///
///
/// ◆ The branch is the selection, not the row. Once anything is chosen the screen is in selection
/// mode — the bar across the top says so — and in that mode a tap adds and removes rather than
/// connecting. That is what every Android list does, and the alternative is worse than a rule to
/// remember: a tap that connected while five machines sat ticked would open a terminal on top of a
/// selection somebody was halfway through building.
///
///
/// Fire-and-forget where it connects, 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 not VaultViewModel vault || RowUnder(e) is not { } row)
{
return;
}
if (vault.IsChoosingHosts)
{
vault.ToggleHostChoiceCommand.Execute(row);
return;
}
_ = vault.ConnectToRowCommand.ExecuteAsync(row);
}
/// Puts a tick against the row that was held, entering selection mode with it.
///
/// 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.
///
private void OnRowHeld(object? sender, HoldingRoutedEventArgs e)
{
if (e.HoldingState != HoldingState.Started)
{
return;
}
held = true;
if (DataContext is VaultViewModel vault && RowUnder(e) is { } row)
{
vault.ChooseHostCommand.Execute(row);
}
}
///
/// The host the gesture landed on, or null where it landed on something that is not one.
///
///
///
/// ◆ Read off the element under the finger rather than off the list's selection, which is what both
/// handlers used to do. The selection was defensible while a tap only ever connected: a tap on a
/// group heading moves the selection and the view model bounces it straight back — see
/// VaultViewModel.SelectedSidebarRow — so reading it answered "a host, or nothing" for free.
/// It stops being defensible the moment a tap can tick one: a heading would then bounce the selection to
/// whichever host was last chosen and tick that, which is a machine the user was not pointing
/// at going into a set they are about to delete.
///
///
/// The ancestor rather than e.Source itself, because the source is whatever leaf the finger
/// landed on — a tag chip's TextBlock has a string for a data context, and the row's own
/// Ellipse has the row. The is the one element in that chain whose data
/// context is always the list's item, whatever kind it is.
///
///
private static HostRowViewModel? RowUnder(RoutedEventArgs e) =>
(e.Source as Visual)?.FindAncestorOfType(includeSelf: true)?.DataContext
as HostRowViewModel;
}