using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Windows.Threading; using TeamsISO.App.Services; namespace TeamsISO.App.ViewModels; /// /// View-model for the v2 Ctrl+K command palette. Owns the static list of /// commands the operator can invoke, plus a free-text filter that whittles /// the visible list down. /// /// The palette is the v2 redesign's navigation surface — it replaces the /// v1 rail's launch / hide / settings buttons (still discoverable in the /// 32px header) AND the buried-in-tabs operator actions like "Apply /// transcoder topology" or "Stop all ISOs" that previously needed /// hunting through menus. Type two letters, press Enter, action invokes. /// /// Match shape: case-insensitive Contains across Label + Category + the /// optional Keywords list. Fuzzy (Sublime / Linear style) matching is a /// future evolution if Contains proves insufficient; broadcasters have /// short attention budgets and Contains is the predictable answer. /// public sealed class CommandPaletteViewModel : ObservableObject { private readonly MainViewModel _main; private readonly Dispatcher _dispatcher; private readonly List _all; private string _filter = string.Empty; private PaletteCommand? _selected; public CommandPaletteViewModel(MainViewModel main, Dispatcher dispatcher) { _main = main; _dispatcher = dispatcher; _all = BuildCommands(); Visible = new ObservableCollection(_all); Selected = Visible.FirstOrDefault(); } /// Free-text filter. Empty string shows all commands. public string Filter { get => _filter; set { if (!SetField(ref _filter, value)) return; ApplyFilter(); } } /// Filtered command list, bound to the palette ListBox. public ObservableCollection Visible { get; } /// Currently-highlighted command. Driven by ↑/↓ in the search box and by mouse hover. public PaletteCommand? Selected { get => _selected; set => SetField(ref _selected, value); } /// /// Move the selection up or down within the visible list, wrapping at the /// edges. Called from the palette's PreviewKeyDown when the operator /// presses ↑ / ↓ while focus is in the search box. /// public void MoveSelection(int direction) { if (Visible.Count == 0) return; var idx = Selected is null ? -1 : Visible.IndexOf(Selected); idx = (idx + direction + Visible.Count) % Visible.Count; Selected = Visible[idx]; } /// Invoke the current selection's action. Returns true if something fired. public bool InvokeSelection() { var sel = Selected; if (sel is null) return false; try { sel.Invoke(); } catch (Exception ex) { _main.Toast.Warn($"{sel.Label}: {ex.Message}"); return false; } return true; } private void ApplyFilter() { var query = _filter.Trim(); var prevSelected = Selected; Visible.Clear(); if (string.IsNullOrEmpty(query)) { foreach (var c in _all) Visible.Add(c); } else { foreach (var c in _all) { if (Matches(c, query)) Visible.Add(c); } } Selected = Visible.Contains(prevSelected!) ? prevSelected : Visible.FirstOrDefault(); } internal static bool Matches(PaletteCommand c, string query) { if (c.Label.Contains(query, StringComparison.OrdinalIgnoreCase)) return true; if (c.Category.Contains(query, StringComparison.OrdinalIgnoreCase)) return true; // Keywords is a single space-separated string of synonyms — Contains // over the whole blob suffices for the operator's short-token typing. if (!string.IsNullOrEmpty(c.Keywords) && c.Keywords.Contains(query, StringComparison.OrdinalIgnoreCase)) { return true; } return false; } /// /// Build the static command list. Order within a category matters for /// keyboard-only operators: the most-frequent command of each category /// goes first. /// private List BuildCommands() { var vm = _main; return new List { // ─── QUICK ─── operator's top-of-mind verbs new("Quick", "Enable all online", "ISOs enable everyone start everything live", "Ctrl+E", () => InvokeIfReady(vm.EnableAllOnlineCommand)), new("Quick", "Stop all ISOs", "panic stop everything kill disable", "Ctrl+Shift+S", () => InvokeIfReady(vm.StopAllIsosCommand)), new("Quick", "Refresh discovery", "rediscover sources rebuild finder NDI", "Ctrl+R", () => InvokeIfReady(vm.RefreshDiscoveryCommand)), // ─── TEAMS ─── direct UIA orchestration new("Teams", "Mute / unmute", "microphone audio silence toggle", null, () => InvokeIfReady(vm.ToggleMuteCommand)), new("Teams", "Toggle camera", "video webcam on off", null, () => InvokeIfReady(vm.ToggleCameraCommand)), new("Teams", "Open share tray", "screen share present", null, () => InvokeIfReady(vm.OpenShareTrayCommand)), new("Teams", "Leave call", "exit end disconnect quit", null, () => InvokeIfReady(vm.LeaveCallCommand)), new("Teams", "Launch Microsoft Teams", "start open run app", null, () => RunOnUi(() => { if (!TeamsLauncher.IsRunning() && TeamsLauncher.TryLaunch(out _)) vm.Toast.Show("Launching Microsoft Teams…"); else TeamsLauncher.ShowWindows(); })), new("Teams", "Hide Teams windows", "minimize cloak", null, () => RunOnUi(() => { var n = TeamsLauncher.HideWindows(); vm.Toast.Show(n > 0 ? $"Hid {n} Teams window(s)" : "No Teams windows to hide"); })), new("Teams", "Show Teams windows", "restore unhide", null, () => RunOnUi(() => { var n = TeamsLauncher.ShowWindows(); vm.Toast.Show(n > 0 ? $"Restored {n} Teams window(s)" : "No Teams windows to restore"); })), // ─── NETWORK ─── new("Network", "Apply transcoder topology", "ndi groups isolate teamsiso-input private", null, () => InvokeIfReady(vm.Settings.ApplyTranscoderTopologyCommand)), // ─── APP ─── new("App", "Theme: dark", "appearance night mode", null, () => RunOnUi(() => ThemeManager.Current.Set("Dark"))), new("App", "Theme: light", "appearance day mode bright", null, () => RunOnUi(() => ThemeManager.Current.Set("Light"))), new("App", "Theme: follow Windows", "system auto", null, () => RunOnUi(() => ThemeManager.Current.Set("System"))), new("App", "Help", "shortcuts cheatsheet f1", "F1", () => InvokeIfReady(vm.ShowHelpCommand)), new("App", "Show notes", "show notes daily journal", null, () => InvokeIfReady(vm.ShowNotesCommand)), }; } private void RunOnUi(Action action) => _dispatcher.BeginInvoke(action); private static void InvokeIfReady(System.Windows.Input.ICommand cmd) { if (cmd?.CanExecute(null) == true) cmd.Execute(null); } } /// /// One command in the Ctrl+K palette. is an optional /// space of additional search terms — the operator might type "ndi" or /// "private" and still match "Apply transcoder topology". /// public sealed record PaletteCommand( string Category, string Label, string? Keywords, string? Shortcut, Action Invoke);