using System.Runtime.InteropServices;
using XFEEDToolBox.Core.Session;
namespace XFEEDToolBox.Services;
public sealed class WindowsGlobalHotkeys : IHotkeyRegistry, IDisposable
{
private readonly nint window;
private readonly SubclassProc callback;
private readonly GlobalHotkeyBindings bindings;
private readonly Action<SessionOperation> action;
private bool disposed;
private static int nextSubclass = 0x584600;
private readonly nuint subclassId = (nuint)Interlocked.Increment(ref nextSubclass);
public WindowsGlobalHotkeys(nint window, Action<SessionOperation> action)
{
this.window = window; this.action = action; bindings = new(this); callback = OnMessage;
if (!SetWindowSubclass(window, callback, subclassId, 0)) throw new InvalidOperationException("无法在工具箱窗口接收全局快捷键。");
}
public HotkeySettings Settings => bindings.Settings;
public void Apply(HotkeySettings settings) { ObjectDisposedException.ThrowIf(disposed, this); bindings.Apply(settings); }
void IHotkeyRegistry.Register(int id, HotkeyGesture gesture)
{ if (!RegisterHotKey(window, id, gesture.Modifiers | 0x4000, gesture.Key)) throw new InvalidOperationException($"快捷键 {gesture} 无法注册,可能已被其他程序占用({Marshal.GetLastWin32Error()})。"); }
void IHotkeyRegistry.Unregister(int id) => UnregisterHotKey(window, id);
private nint OnMessage(nint hwnd, uint message, nuint wParam, nint lParam, nuint subclass, nuint data)
{
if (message == 0x0312 && !disposed && bindings.Resolve(unchecked((int)wParam)) is { } operation) { action(operation); return 0; }
return DefSubclassProc(hwnd, message, wParam, lParam);
}
public void Dispose()
{
if (disposed) return;
disposed = true; bindings.Dispose(); RemoveWindowSubclass(window, callback, subclassId); GC.KeepAlive(callback);
}
/// <summary>Hidden-window QA: posts only to our own window and uses a counting callback, never a game command.</summary>
public static async Task<object> ProbeAsync(nint window)
{
var received = new TaskCompletionSource<SessionOperation>(TaskCreationOptions.RunContinuationsAsynchronously);
var gesture = HotkeyGesture.Parse("Ctrl+Alt+Shift+F24");
using var probe = new WindowsGlobalHotkeys(window, operation => received.TrySetResult(operation));
try
{
probe.Apply(new(true, Restart: gesture.ToString()));
var id = probe.bindings.IdFor(SessionOperation.Restart)!.Value;
if (!PostMessage(window, 0x0312, (nuint)id, (nint)((gesture.Key << 16) | gesture.Modifiers))) throw new InvalidOperationException("无法向工具箱测试窗口投递快捷键消息。");
var action = await received.Task.WaitAsync(TimeSpan.FromSeconds(2));
probe.Dispose();
var released = RegisterHotKey(window, 0x5FFD, gesture.Modifiers | 0x4000, gesture.Key);
if (released) UnregisterHotKey(window, 0x5FFD);
return new { registered = true, messageDispatched = action == SessionOperation.Restart, released, gameActionInvoked = false };
}
catch (Exception e) { return new { registered = false, error = e.Message, gameActionInvoked = false }; }
}
private delegate nint SubclassProc(nint hwnd, uint msg, nuint wParam, nint lParam, nuint id, nuint data);
[DllImport("user32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool RegisterHotKey(nint hwnd, int id, uint modifiers, uint key);
[DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool UnregisterHotKey(nint hwnd, int id);
[DllImport("user32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool PostMessage(nint hwnd, uint msg, nuint wParam, nint lParam);
[DllImport("comctl32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SetWindowSubclass(nint hwnd, SubclassProc proc, nuint id, nuint data);
[DllImport("comctl32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool RemoveWindowSubclass(nint hwnd, SubclassProc proc, nuint id);
[DllImport("comctl32.dll")] private static extern nint DefSubclassProc(nint hwnd, uint msg, nuint wParam, nint lParam);
}
using System.Runtime.InteropServices;
using XFEEDToolBox.Core.Session;
namespace XFEEDToolBox.Services;
public sealed class WindowsGlobalHotkeys : IHotkeyRegistry, IDisposable
{
private readonly nint window;
private readonly SubclassProc callback;
private readonly GlobalHotkeyBindings bindings;
private readonly Action<SessionOperation> action;
private bool disposed;
private static int nextSubclass = 0x584600;
private readonly nuint subclassId = (nuint)Interlocked.Increment(ref nextSubclass);
public WindowsGlobalHotkeys(nint window, Action<SessionOperation> action)
{
this.window = window; this.action = action; bindings = new(this); callback = OnMessage;
if (!SetWindowSubclass(window, callback, subclassId, 0)) throw new InvalidOperationException("无法在工具箱窗口接收全局快捷键。");
}
public HotkeySettings Settings => bindings.Settings;
public void Apply(HotkeySettings settings) { ObjectDisposedException.ThrowIf(disposed, this); bindings.Apply(settings); }
void IHotkeyRegistry.Register(int id, HotkeyGesture gesture)
{ if (!RegisterHotKey(window, id, gesture.Modifiers | 0x4000, gesture.Key)) throw new InvalidOperationException($"快捷键 {gesture} 无法注册,可能已被其他程序占用({Marshal.GetLastWin32Error()})。"); }
void IHotkeyRegistry.Unregister(int id) => UnregisterHotKey(window, id);
private nint OnMessage(nint hwnd, uint message, nuint wParam, nint lParam, nuint subclass, nuint data)
{
if (message == 0x0312 && !disposed && bindings.Resolve(unchecked((int)wParam)) is { } operation) { action(operation); return 0; }
return DefSubclassProc(hwnd, message, wParam, lParam);
}
public void Dispose()
{
if (disposed) return;
disposed = true; bindings.Dispose(); RemoveWindowSubclass(window, callback, subclassId); GC.KeepAlive(callback);
}
/// <summary>Hidden-window QA: posts only to our own window and uses a counting callback, never a game command.</summary>
public static async Task<object> ProbeAsync(nint window)
{
var received = new TaskCompletionSource<SessionOperation>(TaskCreationOptions.RunContinuationsAsynchronously);
var gesture = HotkeyGesture.Parse("Ctrl+Alt+Shift+F24");
using var probe = new WindowsGlobalHotkeys(window, operation => received.TrySetResult(operation));
try
{
probe.Apply(new(true, Restart: gesture.ToString()));
var id = probe.bindings.IdFor(SessionOperation.Restart)!.Value;
if (!PostMessage(window, 0x0312, (nuint)id, (nint)((gesture.Key << 16) | gesture.Modifiers))) throw new InvalidOperationException("无法向工具箱测试窗口投递快捷键消息。");
var action = await received.Task.WaitAsync(TimeSpan.FromSeconds(2));
probe.Dispose();
var released = RegisterHotKey(window, 0x5FFD, gesture.Modifiers | 0x4000, gesture.Key);
if (released) UnregisterHotKey(window, 0x5FFD);
return new { registered = true, messageDispatched = action == SessionOperation.Restart, released, gameActionInvoked = false };
}
catch (Exception e) { return new { registered = false, error = e.Message, gameActionInvoked = false }; }
}
private delegate nint SubclassProc(nint hwnd, uint msg, nuint wParam, nint lParam, nuint id, nuint data);
[DllImport("user32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool RegisterHotKey(nint hwnd, int id, uint modifiers, uint key);
[DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool UnregisterHotKey(nint hwnd, int id);
[DllImport("user32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool PostMessage(nint hwnd, uint msg, nuint wParam, nint lParam);
[DllImport("comctl32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SetWindowSubclass(nint hwnd, SubclassProc proc, nuint id, nuint data);
[DllImport("comctl32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool RemoveWindowSubclass(nint hwnd, SubclassProc proc, nuint id);
[DllImport("comctl32.dll")] private static extern nint DefSubclassProc(nint hwnd, uint msg, nuint wParam, nint lParam);
}