155 lines
5.7 KiB
C#
155 lines
5.7 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.InteropServices;
|
|
using AppLog = ytLive.Helpers.AppLog;
|
|
|
|
namespace ytLive.Services;
|
|
|
|
public enum HotkeyId
|
|
{
|
|
SceneStarting = 1,
|
|
SceneLive = 2,
|
|
SceneBrb = 3,
|
|
SceneChat = 4,
|
|
SceneEnding = 5,
|
|
StartStopStream = 6,
|
|
ToggleMicMute = 7,
|
|
ToggleGameMute = 8,
|
|
TraxPlayPause = 9,
|
|
}
|
|
|
|
/// <summary>Registers/revokes the OS-level hotkeys. Seam so tests never touch
|
|
/// user32 (a real registration is global to the machine and needs a pump).</summary>
|
|
public interface IHotkeyRegistrar
|
|
{
|
|
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 modifiers, uint virtualKey)
|
|
=> RegisterHotKey(hwnd, (int)id, modifiers, virtualKey);
|
|
|
|
public void Unregister(IntPtr hwnd, HotkeyId id)
|
|
=> UnregisterHotKey(hwnd, (int)id);
|
|
|
|
[DllImport("user32.dll", SetLastError = true)]
|
|
private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
|
|
|
|
[DllImport("user32.dll", SetLastError = true)]
|
|
private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// TASK 20: global hotkeys — work while the app is unfocused (streamers are in-game).
|
|
/// 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
|
|
{
|
|
public const int WM_HOTKEY = 0x0312;
|
|
|
|
internal static Func<IHotkeyRegistrar>? RegistrarOverride;
|
|
|
|
internal static readonly Dictionary<HotkeyId, HotkeyChord> Defaults = new()
|
|
{
|
|
[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;
|
|
private readonly List<HotkeyId> _registered = new();
|
|
private IntPtr _hwnd;
|
|
private System.Windows.Interop.HwndSource? _source;
|
|
|
|
/// <summary>Raised on the UI thread for each WM_HOTKEY that carries a known id.</summary>
|
|
public event Action<HotkeyId>? Activated;
|
|
|
|
public GlobalHotkeyManager()
|
|
: this(RegistrarOverride?.Invoke() ?? new Win32HotkeyRegistrar())
|
|
{
|
|
}
|
|
|
|
internal GlobalHotkeyManager(IHotkeyRegistrar registrar) => _registrar = registrar;
|
|
|
|
/// <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);
|
|
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)
|
|
{
|
|
if (msg != WM_HOTKEY) return IntPtr.Zero;
|
|
var raw = wParam.ToInt64();
|
|
if (raw is < 1 or > int.MaxValue || !Enum.IsDefined(typeof(HotkeyId), (int)raw))
|
|
return IntPtr.Zero;
|
|
Activated?.Invoke((HotkeyId)raw);
|
|
handled = true;
|
|
return IntPtr.Zero;
|
|
}
|
|
|
|
public void Detach()
|
|
{
|
|
if (_hwnd != IntPtr.Zero)
|
|
foreach (var id in _registered)
|
|
_registrar.Unregister(_hwnd, id);
|
|
_registered.Clear();
|
|
_source?.RemoveHook(WndProc);
|
|
_source = null;
|
|
_hwnd = IntPtr.Zero;
|
|
}
|
|
}
|