Public Access
Desktop: let the arrows choose the host the drawer acts on, and Enter connect to it
This commit is contained in:
1 parent
6c09bdecb6
commit
079914bfb4
2 files changed
+146
No files matched your search
@@ -122,6 +122,13 @@ internal sealed partial class HostsScreen : UserControl
|
|||||||
// touched; see KeyboardTarget for the other half of who has the keyboard on this screen.
|
// touched; see KeyboardTarget for the other half of who has the keyboard on this screen.
|
||||||
Board.KeyDown += OnGridKey;
|
Board.KeyDown += OnGridKey;
|
||||||
|
|
||||||
|
// The arrows' half of the selection, which the pointer's half writes by hand in OnPointerPressed. See
|
||||||
|
// OnCardSelectionChanged for why this is not simply a two-way binding.
|
||||||
|
Board.AddHandler(SelectingItemsControl.SelectionChangedEvent, OnCardSelectionChanged);
|
||||||
|
|
||||||
|
// Tunnelled, because a section's list answers Enter itself and nothing bubbles past it.
|
||||||
|
Board.AddHandler(KeyDownEvent, OnCardEnter, RoutingStrategies.Tunnel);
|
||||||
|
|
||||||
// ◆ The band is the scroller's rather than any one section list's, because the space it is dragged
|
// ◆ The band is the scroller's rather than any one section list's, because the space it is dragged
|
||||||
// out over is mostly not a list's: a WrapPanel of cards is exactly as tall as its cards, and the gaps
|
// out over is mostly not a list's: a WrapPanel of cards is exactly as tall as its cards, and the gaps
|
||||||
// between sections and below the last one belong to the stack around them. Tunnelled for the reason
|
// between sections and below the last one belong to the stack around them. Tunnelled for the reason
|
||||||
@@ -315,6 +322,16 @@ internal sealed partial class HostsScreen : UserControl
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An arrow with the board itself focused — which is where this screen puts the keyboard, see
|
||||||
|
// KeyboardTarget — goes into the cards: onto the selected one, or the first. Otherwise no arrow ever
|
||||||
|
// reaches a section's list, since none of them has the keyboard until a card has been clicked.
|
||||||
|
if (e.Key is Key.Up or Key.Down or Key.Left or Key.Right && ReferenceEquals(e.Source, Board))
|
||||||
|
{
|
||||||
|
e.Handled = EnterTheCards(vault);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (e.Key is Key.A && e.KeyModifiers.HasFlag(KeyModifiers.Control) && vault.HasHostBoardEntries)
|
if (e.Key is Key.A && e.KeyModifiers.HasFlag(KeyModifiers.Control) && vault.HasHostBoardEntries)
|
||||||
{
|
{
|
||||||
var order = vault.HostBoardOrder.ToList();
|
var order = vault.HostBoardOrder.ToList();
|
||||||
@@ -347,6 +364,80 @@ internal sealed partial class HostsScreen : UserControl
|
|||||||
/// on the empty space means is decided on the release. See <see cref="OnBandReleased"/>.
|
/// on the empty space means is decided on the release. See <see cref="OnBandReleased"/>.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
/// <summary>Enter on a card is the double-press, for somebody who got there with the arrows.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Only on a card, for the reason <see cref="OnHostActivated"/> gives: Enter with the board itself focused
|
||||||
|
/// would connect to whatever was selected before, which is not something anyone is looking at. And not
|
||||||
|
/// while cards are ticked, where the selection is not what the set is about.
|
||||||
|
/// </remarks>
|
||||||
|
private void OnCardEnter(object? sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key is not Key.Enter
|
||||||
|
|| e.KeyModifiers != KeyModifiers.None
|
||||||
|
|| Vault is not { IsChoosingHosts: false } vault
|
||||||
|
|| RowUnder(e.Source) is not HostRowViewModel row)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vault.SelectedHost = row;
|
||||||
|
|
||||||
|
_ = vault.ConnectCommand.ExecuteAsync(null);
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Writes a selection the arrows made in one section's list back to the vault.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// The cards' <c>SelectedItem</c> is one-way, for the reason the markup gives: several lists rebuilt
|
||||||
|
/// independently would each null the vault's selection on every filter keystroke and every sync pass. So
|
||||||
|
/// the pointer writes it by hand, and until this nothing wrote it for the arrows — a card lit by the
|
||||||
|
/// keyboard was not the one the drawer's CONNECT and DELETE then acted on, which is the same mistake the
|
||||||
|
/// right-click handler exists to prevent.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// ◆ Only from the list that has the keyboard, and only for a card. A rebuild's reset arrives as a change
|
||||||
|
/// to nothing, and the binding's own refills of the lists nobody is in arrive without the keyboard — both
|
||||||
|
/// are exactly the writes the one-way binding keeps out, and they stay out. A click arrives here too, after
|
||||||
|
/// <see cref="OnPointerPressed"/> has already written the same card, and changes nothing.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
private void OnCardSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (Vault is not { } vault
|
||||||
|
|| e.Source is not ListBox { IsKeyboardFocusWithin: true, SelectedItem: HostRowViewModel row }
|
||||||
|
|| ReferenceEquals(row, vault.SelectedHost))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vault.IsChoosingHosts)
|
||||||
|
{
|
||||||
|
vault.ClearHostChoiceCommand.Execute(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
anchor = row;
|
||||||
|
vault.SelectedHost = row;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Hands the keyboard from the board to a card: the selected one if it is drawn, else the first.</summary>
|
||||||
|
private bool EnterTheCards(VaultViewModel vault)
|
||||||
|
{
|
||||||
|
var cards = Board.GetVisualDescendants().OfType<ListBoxItem>().Where(card => card.IsEffectivelyVisible).ToList();
|
||||||
|
|
||||||
|
var card = cards.Find(card => ReferenceEquals(card.DataContext, vault.SelectedHost)) ?? cards.FirstOrDefault();
|
||||||
|
|
||||||
|
if (card?.DataContext is not HostRowViewModel row)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
vault.SelectedHost = row;
|
||||||
|
anchor = row;
|
||||||
|
|
||||||
|
return card.Focus(NavigationMethod.Directional);
|
||||||
|
}
|
||||||
|
|
||||||
private void OnBandPressed(object? sender, PointerPressedEventArgs e)
|
private void OnBandPressed(object? sender, PointerPressedEventArgs e)
|
||||||
{
|
{
|
||||||
EndBand();
|
EndBand();
|
||||||
|
|||||||
@@ -465,6 +465,61 @@ public sealed class HostGridTests : IAsyncLifetime
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// The arrows' half of the selection. The cards' <c>SelectedItem</c> is one-way, so a card the keyboard lit
|
||||||
|
/// used to stay out of the vault — and the drawer's CONNECT and DELETE then acted on the card clicked
|
||||||
|
/// before it, the mistake <see cref="ARightClickSelectsTheHostUnderThePointer"/> is about.
|
||||||
|
/// </remarks>
|
||||||
|
[Fact]
|
||||||
|
public async Task TheArrowsMoveTheVaultsSelection_AndEnterConnectsToIt()
|
||||||
|
{
|
||||||
|
await OnTheBoardAsync((screen, window) =>
|
||||||
|
{
|
||||||
|
var first = Row(vault, "prod-db");
|
||||||
|
var other = Row(vault, "stage-web");
|
||||||
|
|
||||||
|
Click(CardFor(screen, first), window);
|
||||||
|
vault.SelectedHost.ShouldBeSameAs(first);
|
||||||
|
|
||||||
|
window.KeyPressQwerty(PhysicalKey.ArrowRight, RawInputModifiers.None);
|
||||||
|
Dispatcher.UIThread.RunJobs();
|
||||||
|
|
||||||
|
vault.SelectedHost.ShouldBeSameAs(other, "the card the arrow lit, not the one clicked before it");
|
||||||
|
|
||||||
|
vault.ConnectCommand.ExecutionTask.ShouldBeNull();
|
||||||
|
|
||||||
|
window.KeyPressQwerty(PhysicalKey.Enter, RawInputModifiers.None);
|
||||||
|
Dispatcher.UIThread.RunJobs();
|
||||||
|
|
||||||
|
// The command, not a connection: these hosts were saved with nothing to sign in with, so it stops
|
||||||
|
// at saying so — about the card Enter was pressed on, which is the selection checked above.
|
||||||
|
vault.ConnectCommand.ExecutionTask.ShouldNotBeNull("Enter on a card is the double-press");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// The board is where this screen puts the keyboard, and none of the section lists inside it has it until
|
||||||
|
/// a card has been clicked — so without this an arrow pressed on arriving went nowhere at all.
|
||||||
|
/// </remarks>
|
||||||
|
[Fact]
|
||||||
|
public async Task AnArrowOnTheBoardItselfGoesIntoTheCards()
|
||||||
|
{
|
||||||
|
await OnTheBoardAsync((screen, window) =>
|
||||||
|
{
|
||||||
|
vault.SelectedHost = null;
|
||||||
|
|
||||||
|
screen.Board.Focus();
|
||||||
|
Dispatcher.UIThread.RunJobs();
|
||||||
|
|
||||||
|
window.KeyPressQwerty(PhysicalKey.ArrowDown, RawInputModifiers.None);
|
||||||
|
Dispatcher.UIThread.RunJobs();
|
||||||
|
|
||||||
|
var focused = window.FocusManager!.GetFocusedElement().ShouldBeOfType<ListBoxItem>();
|
||||||
|
|
||||||
|
vault.SelectedHost.ShouldNotBeNull().ShouldBeSameAs(focused.DataContext);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Helpers ----
|
// ---- Helpers ----
|
||||||
|
|
||||||
private static void RightClick(Visual card, Visual window)
|
private static void RightClick(Visual card, Visual window)
|
||||||
|
|||||||
Reference in new issue
Block a user