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. ///