Files
DodoSSH/src/DodoSSH.Client.App/Views/SafeAnswerFocus.cs
T

82 lines
3.0 KiB
C#

using System.ComponentModel;
using Avalonia.Controls;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace DodoSSH.Client.App.Views;
/// <summary>
/// Hands the keyboard to a confirmation's harmless answer when the question appears.
/// </summary>
/// <remarks>
/// <para>
/// For the questions drawn in place on a page — the deletion and the sign-out confirmations — which used to
/// appear with the keyboard still on the button that asked them, or nowhere. Tab then walked the page behind
/// the question, Enter did nothing useful, and somebody working from the keyboard had to reach for the mouse
/// to say no.
/// </para>
/// <para>
/// ◆ <b>The harmless answer, never the one the question is about.</b> Enter on a focused button presses it,
/// so the button that takes the keyboard is the one an accidental Enter is allowed to press: CANCEL. Putting it
/// on DELETE or SIGN OUT AND DELETE would make two keystrokes — the one that asked and the one after — a way to
/// destroy something without reading the sentence in between.
/// </para>
/// <para>
/// Each card follows its own question rather than the window following all of them, because the same card is
/// drawn in several places and only the one somebody can see should take the keyboard — which the posted
/// check on <c>IsEffectivelyVisible</c> decides, after the binding that reveals it has run. Subscribed only
/// while the card is in the tree: settings pages are rebuilt as they are visited, and a card that subscribed
/// for good would leave one handler on the long-lived view model per visit.
/// </para>
/// </remarks>
internal static class SafeAnswerFocus
{
internal static void Follow(Control card, string question, Func<Control> safeAnswer)
{
INotifyPropertyChanged? watched = null;
void OnChanged(object? sender, PropertyChangedEventArgs e)
{
if (!string.Equals(e.PropertyName, question, StringComparison.Ordinal))
{
return;
}
Dispatcher.UIThread.Post(
() =>
{
if (safeAnswer() is { IsEffectivelyVisible: true } answer)
{
answer.Focus();
}
},
DispatcherPriority.Loaded);
}
void Watch(INotifyPropertyChanged? next)
{
if (watched is not null)
{
watched.PropertyChanged -= OnChanged;
}
watched = next;
if (watched is not null)
{
watched.PropertyChanged += OnChanged;
}
}
card.AttachedToVisualTree += (_, _) => Watch(card.DataContext as INotifyPropertyChanged);
card.DetachedFromVisualTree += (_, _) => Watch(null);
card.DataContextChanged += (_, _) =>
{
if (card.IsAttachedToVisualTree())
{
Watch(card.DataContext as INotifyPropertyChanged);
}
};
}
}