Phase 4: 全局快捷键拦截 (Win32 API)
- 新增 HotKeyManager (Helpers/), 使用 user32.dll RegisterHotKey/UnregisterHotKey 注册系统级快捷键 - 支持 Ctrl/Alt/Shift/Win 修饰键组合, 单字符键和命名键(F1-F12等)解析 - MainWindow.OnSourceInitialized 挂载 HwndSource.AddHook 拦截 WM_HOTKEY 消息 - 启动时自动注册所有含快捷键的工具, 工具增删改后自动重新注册 - 快捷键冲突时记录 Win32 错误码, 无效格式打印警告 - 新增 12 个 HotKeyManager 单元测试 (73 tests total)
This commit is contained in:
135
PersonalToolBox.Tests/Helpers/HotKeyManagerTests.cs
Normal file
135
PersonalToolBox.Tests/Helpers/HotKeyManagerTests.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using Moq;
|
||||
using PersonalToolBox.Helpers;
|
||||
using PersonalToolBox.Models;
|
||||
using PersonalToolBox.Services;
|
||||
|
||||
namespace PersonalToolBox.Tests.Helpers;
|
||||
|
||||
public class HotKeyManagerTests
|
||||
{
|
||||
[Fact]
|
||||
public void TryParseHotKey_ValidCtrlAltT_ReturnsCorrectModifiersAndVk()
|
||||
{
|
||||
var result = HotKeyManager.TryParseHotKey("Ctrl+Alt+T", out uint mod, out uint vk);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.NotEqual(0u, mod);
|
||||
// 'T' → VK = 0x54
|
||||
Assert.Equal((uint)'T', vk);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseHotKey_ValidCtrlShiftF1_ReturnsCorrectVk()
|
||||
{
|
||||
var result = HotKeyManager.TryParseHotKey("Ctrl+Shift+F1", out uint mod, out uint vk);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.NotEqual(0u, mod);
|
||||
// F1 → VK_F1 = 0x70
|
||||
Assert.Equal(0x70u, vk);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseHotKey_OnlyModifier_ReturnsFalse()
|
||||
{
|
||||
var result = HotKeyManager.TryParseHotKey("Ctrl", out _, out _);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseHotKey_OnlyKey_ReturnsFalse()
|
||||
{
|
||||
var result = HotKeyManager.TryParseHotKey("T", out _, out _);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseHotKey_Empty_ReturnsFalse()
|
||||
{
|
||||
var result = HotKeyManager.TryParseHotKey("", out _, out _);
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseHotKey_Null_ReturnsFalse()
|
||||
{
|
||||
var result = HotKeyManager.TryParseHotKey(null!, out _, out _);
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseHotKey_Whitespace_ReturnsFalse()
|
||||
{
|
||||
var result = HotKeyManager.TryParseHotKey(" ", out _, out _);
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseHotKey_WinKey_B_ReturnsCorrectVk()
|
||||
{
|
||||
var result = HotKeyManager.TryParseHotKey("Win+B", out uint mod, out uint vk);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal((uint)'B', vk);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseHotKey_UnknownModifier_ReturnsFalse()
|
||||
{
|
||||
var result = HotKeyManager.TryParseHotKey("Foo+T", out _, out _);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterAll_SkipsEmptyHotKeys()
|
||||
{
|
||||
var logMock = new Mock<ILogService>();
|
||||
var processMock = new Mock<IProcessExecutionService>();
|
||||
var manager = new HotKeyManager(logMock.Object, processMock.Object);
|
||||
|
||||
var tools = new[]
|
||||
{
|
||||
new ToolItem { Name = "NoHotkey", HotKey = "" },
|
||||
new ToolItem { Name = "BlankHotkey", HotKey = " " }
|
||||
};
|
||||
|
||||
// Should not throw
|
||||
manager.RegisterAll(tools);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterAll_InvalidFormat_LogsWarning()
|
||||
{
|
||||
var logMock = new Mock<ILogService>();
|
||||
var processMock = new Mock<IProcessExecutionService>();
|
||||
var manager = new HotKeyManager(logMock.Object, processMock.Object);
|
||||
|
||||
// Initialize needs a hwnd, but RegisterAll calls Register which checks _hwnd
|
||||
// So calling RegisterAll without Initialize should be a no-op
|
||||
var tools = new[]
|
||||
{
|
||||
new ToolItem { Name = "Bad", HotKey = "not-a-valid-key" }
|
||||
};
|
||||
|
||||
// hwnd is IntPtr.Zero, so should skip silently
|
||||
manager.RegisterAll(tools);
|
||||
|
||||
// hwnd is zero, so Register is skipped entirely
|
||||
logMock.Verify(x => x.Warning(It.IsAny<string>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryHandleMessage_NonWMHOTKEY_ReturnsFalse()
|
||||
{
|
||||
var logMock = new Mock<ILogService>();
|
||||
var processMock = new Mock<IProcessExecutionService>();
|
||||
var manager = new HotKeyManager(logMock.Object, processMock.Object);
|
||||
|
||||
var handled = manager.TryHandleMessage(9999, IntPtr.Zero, IntPtr.Zero);
|
||||
|
||||
Assert.False(handled);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user