From 9db0875f9e8b679963b40fb71731ad4357224e1f Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Sun, 10 May 2026 21:26:37 -0400 Subject: [PATCH] Numpad 1-9 hotkeys toggle Nth participant's ISO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fast keyboard-driven ISO routing for operators with one hand on the keyboard during a show. Both NumPad1..9 and top-row 1..9 bind to ToggleByIndexCommand which resolves against the filtered+sorted ParticipantsView — index matches what's on screen, not the underlying storage order. Press a digit again to toggle off. Plays nice with sort modes: LoudestFirst means '1' is always whoever's loudest right now; Alphabetical lets you build muscle memory for recurring guests. Implementation: - New generic RelayCommand in RelayCommand.cs so XAML CommandParameter strings convert to the action's T (int / string / etc.). - ToggleByIndexCommand on MainViewModel iterates ParticipantsView, finds the Nth ParticipantViewModel, fires its ToggleIsoCommand if CanExecute. - 18 KeyBindings (9 NumPad + 9 D1-D9) in MainWindow.xaml's Window.InputBindings. - F1 help cheat sheet updated to mention the new range. --- src/TeamsISO.App/HelpWindow.xaml | 11 +++++-- src/TeamsISO.App/MainWindow.xaml | 20 ++++++++++++ src/TeamsISO.App/ViewModels/MainViewModel.cs | 27 +++++++++++++++++ src/TeamsISO.App/ViewModels/RelayCommand.cs | 32 ++++++++++++++++++++ 4 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/TeamsISO.App/HelpWindow.xaml b/src/TeamsISO.App/HelpWindow.xaml index 1e360e7..5bd6e42 100644 --- a/src/TeamsISO.App/HelpWindow.xaml +++ b/src/TeamsISO.App/HelpWindow.xaml @@ -120,12 +120,19 @@ Text="Ctrl + R" Style="{StaticResource Wd.Text.Mono}" Foreground="{DynamicResource Wd.Accent.Cyan}" - Margin="0,0,16,0"/> + Margin="0,0,16,6"/> + FontSize="12" + Margin="0,0,0,6"/> + diff --git a/src/TeamsISO.App/MainWindow.xaml b/src/TeamsISO.App/MainWindow.xaml index b00a708..fba8989 100644 --- a/src/TeamsISO.App/MainWindow.xaml +++ b/src/TeamsISO.App/MainWindow.xaml @@ -54,6 +54,26 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/TeamsISO.App/ViewModels/MainViewModel.cs b/src/TeamsISO.App/ViewModels/MainViewModel.cs index 426d4eb..c721abb 100644 --- a/src/TeamsISO.App/ViewModels/MainViewModel.cs +++ b/src/TeamsISO.App/ViewModels/MainViewModel.cs @@ -181,6 +181,14 @@ public sealed class MainViewModel : ObservableObject, IDisposable /// Save a PNG snapshot of every enabled participant's current frame. public RelayCommand SnapshotAllCommand { get; } + /// + /// Toggle the ISO for the Nth visible participant (1-based, matches the + /// numpad layout). Used by the NumPad1..NumPad9 hotkeys; resolves + /// against ParticipantsView so the index matches what the operator + /// sees in the current sort + filter. + /// + public RelayCommand ToggleByIndexCommand { get; } + /// /// Two-way bound to the quick-join input. Whatever the operator pastes /// gets handed to when the @@ -412,6 +420,25 @@ public sealed class MainViewModel : ObservableObject, IDisposable SnapshotAllCommand = new RelayCommand(SnapshotAll, () => Participants.Any(p => p.IsEnabled)); + ToggleByIndexCommand = new RelayCommand(s => + { + // Numpad / digit hotkeys pass "1".."9" as a string. Resolve + // against the filtered/sorted view so the index matches what + // the operator sees on screen, not the underlying storage order. + if (!int.TryParse(s, out var idx) || idx < 1 || idx > 9) return; + var i = 0; + foreach (var item in ParticipantsView) + { + if (item is not ParticipantViewModel p) continue; + if (++i == idx) + { + if (p.ToggleIsoCommand.CanExecute(null)) + p.ToggleIsoCommand.Execute(null); + break; + } + } + }); + JoinMeetingCommand = new RelayCommand(() => { // Trim + handle the operator pasting whitespace around the URL. diff --git a/src/TeamsISO.App/ViewModels/RelayCommand.cs b/src/TeamsISO.App/ViewModels/RelayCommand.cs index a859178..87416fa 100644 --- a/src/TeamsISO.App/ViewModels/RelayCommand.cs +++ b/src/TeamsISO.App/ViewModels/RelayCommand.cs @@ -23,6 +23,38 @@ public sealed class RelayCommand : ICommand public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); } +/// +/// Synchronous command that accepts a typed parameter. Used by hotkeys +/// that need to pass an index (e.g. NumPad1..NumPad9 → 1..9). The +/// parameter is converted from object via Convert.ChangeType so XAML +/// CommandParameter="1" works for int T. +/// +public sealed class RelayCommand : ICommand +{ + private readonly Action _execute; + private readonly Func? _canExecute; + + public RelayCommand(Action execute, Func? canExecute = null) + { + _execute = execute; + _canExecute = canExecute; + } + + public bool CanExecute(object? parameter) => _canExecute?.Invoke(Convert(parameter)) ?? true; + public void Execute(object? parameter) => _execute(Convert(parameter)); + + public event EventHandler? CanExecuteChanged; + public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); + + private static TValue Convert(object? value) + { + if (value is null) return default!; + if (value is TValue typed) return typed; + try { return (TValue)System.Convert.ChangeType(value, typeof(TValue)); } + catch { return default!; } + } +} + /// /// Async command that suppresses re-entrancy while running. ///