TASK 20 step 2: hotkey config UI + modifier chord persistence + conflict detection
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user