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; } }