using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Windows.Input; using ytLive.Helpers; using ytLive.Services; namespace ytLive.ViewModels; /// One row in the hotkey config dialog — an action and its current chord. public sealed class HotkeyBindingRow : ViewModelBase { public HotkeyId Id { get; } public string ActionName { get; } private HotkeyChord? _chord; public HotkeyChord? Chord { get => _chord; set { if (SetProperty(ref _chord, value)) OnPropertyChanged(nameof(DisplayText)); } } private bool _isCapturing; public bool IsCapturing { get => _isCapturing; set { if (SetProperty(ref _isCapturing, value)) OnPropertyChanged(nameof(DisplayText)); } } public string DisplayText => IsCapturing ? "▶ Press a key combination..." : HotkeyChord.DisplayString(Chord); public HotkeyBindingRow(HotkeyId id, HotkeyChord? chord) { Id = id; ActionName = GetActionName(id); Chord = chord; } private static string GetActionName(HotkeyId id) => id switch { HotkeyId.SceneStarting => "Scene: Starting", HotkeyId.SceneLive => "Scene: Live", HotkeyId.SceneBrb => "Scene: BRB", HotkeyId.SceneChat => "Scene: Chat", HotkeyId.SceneEnding => "Scene: Ending", HotkeyId.StartStopStream => "Start / Stop Stream", HotkeyId.ToggleMicMute => "Toggle Mic Mute", HotkeyId.ToggleGameMute => "Toggle Desktop Audio Mute", HotkeyId.TraxPlayPause => "TRAX Play / Pause", _ => id.ToString(), }; } /// /// ViewModel for the hotkey config dialog. Manages the working copy of /// bindings, capture mode, conflict detection, and save/cancel/reset. /// public sealed class HotkeyConfigViewModel : ViewModelBase { private readonly LayoutStore _store; private readonly Dictionary _original; public ObservableCollection Rows { get; } public event Action? SaveRequested; public event Action? CancelRequested; private string? _conflictMessage; public string? ConflictMessage { get => _conflictMessage; set => SetProperty(ref _conflictMessage, value); } private HotkeyBindingRow? _capturingRow; public HotkeyBindingRow? CapturingRow { get => _capturingRow; private set => SetProperty(ref _capturingRow, value); } public ICommand StartCaptureCommand { get; } public ICommand UnbindCommand { get; } public ICommand ResetToDefaultsCommand { get; } public ICommand SaveCommand { get; } public ICommand CancelCommand { get; } public HotkeyConfigViewModel(LayoutStore store) { _store = store; ConflictMessage = null; // Load saved bindings — keys present in DB are either custom or "none" (unbound). var saved = store.LoadHotkeyBindings(); // Build working copy: start from defaults, overlay saved, fill gaps. _original = new Dictionary(); foreach (HotkeyId id in Enum.GetValues(typeof(HotkeyId))) { if (saved.TryGetValue(id, out var chord)) _original[id] = chord; // custom or null (explicitly unbound) else _original[id] = GlobalHotkeyManager.Defaults.TryGetValue(id, out var def) ? def : null; } Rows = new ObservableCollection( _original.Select(kv => new HotkeyBindingRow(kv.Key, kv.Value))); StartCaptureCommand = new RelayCommand(row => { if (row is HotkeyBindingRow r) StartCapture(r); }); UnbindCommand = new RelayCommand(row => { if (row is HotkeyBindingRow r) Unbind(r); }); ResetToDefaultsCommand = new RelayCommand(_ => ResetToDefaults()); SaveCommand = new RelayCommand(_ => Save()); CancelCommand = new RelayCommand(_ => CancelRequested?.Invoke()); } /// /// Called by the dialog after it captures a key combo from the user. /// Assigns the chord if free; shows a conflict warning otherwise. /// public void AssignCapturedChord(HotkeyChord chord) { if (CapturingRow == null) return; // Same chord as current — no-op if (CapturingRow.Chord != null && CapturingRow.Chord.Equals(chord)) { CapturingRow.IsCapturing = false; CapturingRow = null; return; } // Conflict: another action already uses this chord var conflict = Rows.FirstOrDefault(r => r.Id != CapturingRow.Id && r.Chord != null && r.Chord.Equals(chord)); if (conflict != null) { ConflictMessage = $"{chord.ToDisplayString()} is already bound to {conflict.ActionName}. Unbind it first."; CapturingRow.IsCapturing = false; CapturingRow = null; return; } // Free — assign it ConflictMessage = null; CapturingRow.Chord = chord; CapturingRow.IsCapturing = false; CapturingRow = null; } /// Cancel capture mode without changing the binding. public void CancelCapture() { if (CapturingRow == null) return; CapturingRow.IsCapturing = false; CapturingRow = null; ConflictMessage = null; } private void StartCapture(HotkeyBindingRow row) { CancelCapture(); // cancel any in-progress capture first ConflictMessage = null; row.IsCapturing = true; CapturingRow = row; } private void Unbind(HotkeyBindingRow row) { CancelCapture(); ConflictMessage = null; row.Chord = null; } private void ResetToDefaults() { CancelCapture(); ConflictMessage = null; foreach (var row in Rows) { row.Chord = GlobalHotkeyManager.Defaults.TryGetValue(row.Id, out var def) ? def : null; } } private void Save() { CancelCapture(); ConflictMessage = null; var bindings = new Dictionary(); foreach (var row in Rows) bindings[row.Id] = row.Chord; _store.SaveHotkeyBindings(bindings); SaveRequested?.Invoke(); } }