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,
}
/// Registers/revokes the OS-level hotkeys. Seam so tests never touch
/// user32 (a real registration is global to the machine and needs a pump).
public interface IHotkeyRegistrar
{
bool Register(IntPtr hwnd, HotkeyId id, 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 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);
}
///
/// 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.
///
public sealed class GlobalHotkeyManager
{
public const int WM_HOTKEY = 0x0312;
internal static Func? RegistrarOverride;
private static readonly Dictionary 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
};
private readonly IHotkeyRegistrar _registrar;
private readonly List _registered = new();
private IntPtr _hwnd;
private System.Windows.Interop.HwndSource? _source;
/// Raised on the UI thread for each WM_HOTKEY that carries a known id.
public event Action? Activated;
public GlobalHotkeyManager()
: this(RegistrarOverride?.Invoke() ?? new Win32HotkeyRegistrar())
{
}
internal GlobalHotkeyManager(IHotkeyRegistrar registrar) => _registrar = registrar;
public void Attach(System.Windows.Window window)
{
_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");
}
window.Closed += OnWindowClosed;
AppLog.Write($"hotkey: attached, {_registered.Count} registered");
}
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;
}
private 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;
}
}