Files
LlamaCasty/ViewModels/HotkeyConfigViewModel.cs

218 lines
6.5 KiB
C#

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;
/// <summary>One row in the hotkey config dialog — an action and its current chord.</summary>
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(),
};
}
/// <summary>
/// ViewModel for the hotkey config dialog. Manages the working copy of
/// bindings, capture mode, conflict detection, and save/cancel/reset.
/// </summary>
public sealed class HotkeyConfigViewModel : ViewModelBase
{
private readonly LayoutStore _store;
private readonly Dictionary<HotkeyId, HotkeyChord?> _original;
public ObservableCollection<HotkeyBindingRow> 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<HotkeyId, HotkeyChord?>();
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<HotkeyBindingRow>(
_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());
}
/// <summary>
/// Called by the dialog after it captures a key combo from the user.
/// Assigns the chord if free; shows a conflict warning otherwise.
/// </summary>
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;
}
/// <summary>Cancel capture mode without changing the binding.</summary>
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<HotkeyId, HotkeyChord?>();
foreach (var row in Rows)
bindings[row.Id] = row.Chord;
_store.SaveHotkeyBindings(bindings);
SaveRequested?.Invoke();
}
}