TASK 20 step 1: global hotkeys F1-F9 (scenes, stream, mutes, TRAX)
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
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 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);
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public sealed class GlobalHotkeyManager
|
||||
{
|
||||
public const int WM_HOTKEY = 0x0312;
|
||||
|
||||
internal static Func<IHotkeyRegistrar>? RegistrarOverride;
|
||||
|
||||
private static readonly Dictionary<HotkeyId, uint> 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<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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user