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);