TASK 20 step 2: hotkey config UI + modifier chord persistence + conflict detection
This commit is contained in:
+53
-22
@@ -22,14 +22,14 @@ public enum HotkeyId
|
||||
/// user32 (a real registration is global to the machine and needs a pump).</summary>
|
||||
public interface IHotkeyRegistrar
|
||||
{
|
||||
bool Register(IntPtr hwnd, HotkeyId id, uint virtualKey);
|
||||
bool Register(IntPtr hwnd, HotkeyId id, uint modifiers, uint virtualKey);
|
||||
void Unregister(IntPtr hwnd, HotkeyId id);
|
||||
}
|
||||
|
||||
public sealed class Win32HotkeyRegistrar : IHotkeyRegistrar
|
||||
{
|
||||
public bool Register(IntPtr hwnd, HotkeyId id, uint virtualKey)
|
||||
=> RegisterHotKey(hwnd, (int)id, 0, virtualKey);
|
||||
public bool Register(IntPtr hwnd, HotkeyId id, uint modifiers, uint virtualKey)
|
||||
=> RegisterHotKey(hwnd, (int)id, modifiers, virtualKey);
|
||||
|
||||
public void Unregister(IntPtr hwnd, HotkeyId id)
|
||||
=> UnregisterHotKey(hwnd, (int)id);
|
||||
@@ -46,6 +46,7 @@ public sealed class Win32HotkeyRegistrar : IHotkeyRegistrar
|
||||
/// Defaults: F1-F5 stage/transition the five canonical scenes, F6 start/end stream,
|
||||
/// F7 mute mic, F8 mute desktop audio, F9 TRAX play/pause.
|
||||
/// Attach after the window has a source (OnSourceInitialized); Detach on close.
|
||||
/// Pass a custom bindings dictionary to override defaults.
|
||||
/// </summary>
|
||||
public sealed class GlobalHotkeyManager
|
||||
{
|
||||
@@ -53,17 +54,17 @@ public sealed class GlobalHotkeyManager
|
||||
|
||||
internal static Func<IHotkeyRegistrar>? RegistrarOverride;
|
||||
|
||||
private static readonly Dictionary<HotkeyId, uint> Defaults = new()
|
||||
internal static readonly Dictionary<HotkeyId, HotkeyChord> Defaults = new()
|
||||
{
|
||||
[HotkeyId.SceneStarting] = 0x70, // F1
|
||||
[HotkeyId.SceneLive] = 0x71, // F2
|
||||
[HotkeyId.SceneBrb] = 0x72, // F3
|
||||
[HotkeyId.SceneChat] = 0x73, // F4
|
||||
[HotkeyId.SceneEnding] = 0x74, // F5
|
||||
[HotkeyId.StartStopStream] = 0x75, // F6
|
||||
[HotkeyId.ToggleMicMute] = 0x76, // F7
|
||||
[HotkeyId.ToggleGameMute] = 0x77, // F8
|
||||
[HotkeyId.TraxPlayPause] = 0x78, // F9
|
||||
[HotkeyId.SceneStarting] = new HotkeyChord(0, 0x70), // F1
|
||||
[HotkeyId.SceneLive] = new HotkeyChord(0, 0x71), // F2
|
||||
[HotkeyId.SceneBrb] = new HotkeyChord(0, 0x72), // F3
|
||||
[HotkeyId.SceneChat] = new HotkeyChord(0, 0x73), // F4
|
||||
[HotkeyId.SceneEnding] = new HotkeyChord(0, 0x74), // F5
|
||||
[HotkeyId.StartStopStream]= new HotkeyChord(0, 0x75), // F6
|
||||
[HotkeyId.ToggleMicMute] = new HotkeyChord(0, 0x76), // F7
|
||||
[HotkeyId.ToggleGameMute] = new HotkeyChord(0, 0x77), // F8
|
||||
[HotkeyId.TraxPlayPause] = new HotkeyChord(0, 0x78), // F9
|
||||
};
|
||||
|
||||
private readonly IHotkeyRegistrar _registrar;
|
||||
@@ -81,22 +82,52 @@ public sealed class GlobalHotkeyManager
|
||||
|
||||
internal GlobalHotkeyManager(IHotkeyRegistrar registrar) => _registrar = registrar;
|
||||
|
||||
public void Attach(System.Windows.Window window)
|
||||
/// <summary>
|
||||
/// Attach to a window and register hotkeys. If <paramref name="bindings"/> is
|
||||
/// non-null, it overrides the defaults for the provided entries; any HotkeyId
|
||||
/// not present in the dictionary falls back to the built-in default.
|
||||
/// </summary>
|
||||
public void Attach(System.Windows.Window window, Dictionary<HotkeyId, HotkeyChord>? bindings = null)
|
||||
{
|
||||
_hwnd = new System.Windows.Interop.WindowInteropHelper(window).Handle;
|
||||
_source = System.Windows.Interop.HwndSource.FromHwnd(_hwnd);
|
||||
_source?.AddHook(WndProc);
|
||||
foreach (var (id, vk) in Defaults)
|
||||
{
|
||||
if (_registrar.Register(_hwnd, id, vk))
|
||||
_registered.Add(id);
|
||||
else
|
||||
AppLog.Write($"Hotkey: {(HotkeyId)id} (vk {vk}) not registered — likely owned by another app");
|
||||
}
|
||||
RegisterAll(bindings);
|
||||
window.Closed += OnWindowClosed;
|
||||
AppLog.Write($"hotkey: attached, {_registered.Count} registered");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hot-swap bindings: detach current registrations, re-register with new
|
||||
/// bindings. Call after the user saves custom hotkey config.
|
||||
/// </summary>
|
||||
public void Reattach(System.Windows.Window window, Dictionary<HotkeyId, HotkeyChord> bindings)
|
||||
{
|
||||
Detach();
|
||||
_hwnd = new System.Windows.Interop.WindowInteropHelper(window).Handle;
|
||||
_source = System.Windows.Interop.HwndSource.FromHwnd(_hwnd);
|
||||
_source?.AddHook(WndProc);
|
||||
RegisterAll(bindings);
|
||||
AppLog.Write($"hotkey: reattached, {_registered.Count} registered");
|
||||
}
|
||||
|
||||
private void RegisterAll(Dictionary<HotkeyId, HotkeyChord>? bindings)
|
||||
{
|
||||
foreach (HotkeyId id in Enum.GetValues(typeof(HotkeyId)))
|
||||
{
|
||||
var chord = bindings != null && bindings.TryGetValue(id, out var custom)
|
||||
? custom
|
||||
: Defaults.TryGetValue(id, out var def) ? def : null;
|
||||
|
||||
if (chord == null) continue; // unbound action
|
||||
|
||||
if (_registrar.Register(_hwnd, id, chord.Modifiers, chord.VirtualKey))
|
||||
_registered.Add(id);
|
||||
else
|
||||
AppLog.Write($"Hotkey: {id} ({chord.ToDisplayString()}) not registered — likely owned by another app");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWindowClosed(object? sender, EventArgs e) => Detach();
|
||||
|
||||
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||
@@ -110,7 +141,7 @@ public sealed class GlobalHotkeyManager
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
private void Detach()
|
||||
public void Detach()
|
||||
{
|
||||
if (_hwnd != IntPtr.Zero)
|
||||
foreach (var id in _registered)
|
||||
|
||||
Reference in New Issue
Block a user