feat: 搭建工具箱核心服务层
新增工具、分类、组合、自动运行和日志等基础模型。 实现本地 JSON 配置读写、默认系统工具恢复、路径校验、统一启动服务、自动运行服务、托盘服务、开机自启服务和全局快捷键注册服务,为后续界面集成提供稳定边界。
This commit is contained in:
285
src/ToolboxApp/Services/HotkeyService.cs
Normal file
285
src/ToolboxApp/Services/HotkeyService.cs
Normal file
@@ -0,0 +1,285 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using ToolboxApp.Models;
|
||||
|
||||
namespace ToolboxApp.Services;
|
||||
|
||||
public sealed class HotkeyService : IDisposable
|
||||
{
|
||||
private const int WmHotkey = 0x0312;
|
||||
private readonly Dictionary<int, string> _registeredHotkeys = new();
|
||||
private HwndSource? _source;
|
||||
private Func<string, Task>? _onTriggered;
|
||||
private Action<LogMessage>? _log;
|
||||
private int _nextId = 1000;
|
||||
private nint _handle;
|
||||
|
||||
public void RegisterAll(
|
||||
Window window,
|
||||
IEnumerable<ToolItem> tools,
|
||||
bool enabled,
|
||||
Func<string, Task> onTriggered,
|
||||
Action<LogMessage> log)
|
||||
{
|
||||
UnregisterAll();
|
||||
|
||||
_onTriggered = onTriggered;
|
||||
_log = log;
|
||||
|
||||
var helper = new WindowInteropHelper(window);
|
||||
_handle = helper.Handle;
|
||||
if (_handle == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_source ??= HwndSource.FromHwnd(_handle);
|
||||
_source?.RemoveHook(WndProc);
|
||||
_source?.AddHook(WndProc);
|
||||
|
||||
foreach (var tool in tools)
|
||||
{
|
||||
tool.HotkeyStatus = string.IsNullOrWhiteSpace(tool.Hotkey) ? "未设置" : "等待注册";
|
||||
}
|
||||
|
||||
if (!enabled)
|
||||
{
|
||||
foreach (var tool in tools.Where(tool => !string.IsNullOrWhiteSpace(tool.Hotkey)))
|
||||
{
|
||||
tool.HotkeyStatus = "已禁用";
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var activeTools = tools
|
||||
.Where(tool => !tool.IsDeleted && !string.IsNullOrWhiteSpace(tool.Hotkey))
|
||||
.ToList();
|
||||
var conflicts = activeTools
|
||||
.GroupBy(tool => HotkeyParser.Normalize(tool.Hotkey!))
|
||||
.Where(group => group.Count() > 1)
|
||||
.SelectMany(group => group)
|
||||
.ToHashSet();
|
||||
|
||||
foreach (var conflict in conflicts)
|
||||
{
|
||||
conflict.HotkeyStatus = "内部冲突";
|
||||
log(CreateLog(LogLevel.Warning, $"快捷键内部冲突:{conflict.Hotkey},{conflict.Name}"));
|
||||
}
|
||||
|
||||
foreach (var tool in activeTools.Where(tool => !conflicts.Contains(tool)))
|
||||
{
|
||||
if (!HotkeyParser.TryParse(tool.Hotkey, out var modifiers, out var key))
|
||||
{
|
||||
tool.HotkeyStatus = "格式无效";
|
||||
log(CreateLog(LogLevel.Warning, $"快捷键格式无效:{tool.Name},{tool.Hotkey}"));
|
||||
continue;
|
||||
}
|
||||
|
||||
var id = _nextId++;
|
||||
if (RegisterHotKey(_handle, id, modifiers, key))
|
||||
{
|
||||
_registeredHotkeys[id] = tool.Id;
|
||||
tool.HotkeyStatus = "正常";
|
||||
}
|
||||
else
|
||||
{
|
||||
tool.HotkeyStatus = "系统注册失败";
|
||||
log(CreateLog(LogLevel.Warning, $"快捷键注册失败:{tool.Name},{tool.Hotkey}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UnregisterAll()
|
||||
{
|
||||
if (_handle != 0)
|
||||
{
|
||||
foreach (var id in _registeredHotkeys.Keys.ToList())
|
||||
{
|
||||
UnregisterHotKey(_handle, id);
|
||||
}
|
||||
}
|
||||
|
||||
_registeredHotkeys.Clear();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
UnregisterAll();
|
||||
_source?.RemoveHook(WndProc);
|
||||
}
|
||||
|
||||
private nint WndProc(nint hwnd, int msg, nint wParam, nint lParam, ref bool handled)
|
||||
{
|
||||
if (msg == WmHotkey && _registeredHotkeys.TryGetValue(wParam.ToInt32(), out var toolId))
|
||||
{
|
||||
handled = true;
|
||||
_ = TriggerAsync(toolId);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private async Task TriggerAsync(string toolId)
|
||||
{
|
||||
if (_onTriggered is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _onTriggered(toolId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log?.Invoke(CreateLog(LogLevel.Error, $"快捷键触发失败:{ex.Message}"));
|
||||
}
|
||||
}
|
||||
|
||||
private static LogMessage CreateLog(LogLevel level, string message)
|
||||
{
|
||||
return new LogMessage
|
||||
{
|
||||
Level = level,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool RegisterHotKey(nint hWnd, int id, uint fsModifiers, uint vk);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool UnregisterHotKey(nint hWnd, int id);
|
||||
}
|
||||
|
||||
public static class HotkeyParser
|
||||
{
|
||||
private const uint ModAlt = 0x0001;
|
||||
private const uint ModControl = 0x0002;
|
||||
private const uint ModShift = 0x0004;
|
||||
private const uint ModWin = 0x0008;
|
||||
|
||||
public static string Normalize(string hotkey)
|
||||
{
|
||||
return string.Join("+", hotkey.Split('+', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(NormalizePart)
|
||||
.Where(part => part.Length > 0)
|
||||
.OrderBy(GetPartOrder)
|
||||
.ThenBy(part => part)
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
public static bool TryParse(string? hotkey, out uint modifiers, out uint key)
|
||||
{
|
||||
modifiers = 0;
|
||||
key = 0;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(hotkey))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var keyPart = "";
|
||||
foreach (var rawPart in hotkey.Split('+', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
var part = NormalizePart(rawPart);
|
||||
switch (part)
|
||||
{
|
||||
case "Ctrl":
|
||||
modifiers |= ModControl;
|
||||
break;
|
||||
case "Alt":
|
||||
modifiers |= ModAlt;
|
||||
break;
|
||||
case "Shift":
|
||||
modifiers |= ModShift;
|
||||
break;
|
||||
case "Win":
|
||||
modifiers |= ModWin;
|
||||
break;
|
||||
default:
|
||||
keyPart = part;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (modifiers == 0 || string.IsNullOrWhiteSpace(keyPart))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
key = KeyToVirtualKey(keyPart);
|
||||
return key != 0;
|
||||
}
|
||||
|
||||
private static string NormalizePart(string value)
|
||||
{
|
||||
var part = value.Trim();
|
||||
return part.ToLowerInvariant() switch
|
||||
{
|
||||
"control" or "ctrl" => "Ctrl",
|
||||
"menu" or "alt" => "Alt",
|
||||
"shift" => "Shift",
|
||||
"windows" or "win" => "Win",
|
||||
_ => part.Length == 1 ? part.ToUpperInvariant() : part.ToUpperInvariant().StartsWith("F", StringComparison.Ordinal) ? part.ToUpperInvariant() : part
|
||||
};
|
||||
}
|
||||
|
||||
private static int GetPartOrder(string part)
|
||||
{
|
||||
return part switch
|
||||
{
|
||||
"Ctrl" => 0,
|
||||
"Alt" => 1,
|
||||
"Shift" => 2,
|
||||
"Win" => 3,
|
||||
_ => 4
|
||||
};
|
||||
}
|
||||
|
||||
private static uint KeyToVirtualKey(string keyPart)
|
||||
{
|
||||
if (keyPart.Length == 1)
|
||||
{
|
||||
var character = keyPart[0];
|
||||
if (character is >= 'A' and <= 'Z')
|
||||
{
|
||||
return character;
|
||||
}
|
||||
|
||||
if (character is >= '0' and <= '9')
|
||||
{
|
||||
return character;
|
||||
}
|
||||
}
|
||||
|
||||
if (keyPart.StartsWith("F", StringComparison.OrdinalIgnoreCase)
|
||||
&& int.TryParse(keyPart[1..], out var functionKey)
|
||||
&& functionKey is >= 1 and <= 24)
|
||||
{
|
||||
return (uint)(0x70 + functionKey - 1);
|
||||
}
|
||||
|
||||
return keyPart.ToLowerInvariant() switch
|
||||
{
|
||||
"escape" or "esc" => 0x1B,
|
||||
"tab" => 0x09,
|
||||
"space" => 0x20,
|
||||
"enter" => 0x0D,
|
||||
"backspace" => 0x08,
|
||||
"delete" => 0x2E,
|
||||
"insert" => 0x2D,
|
||||
"home" => 0x24,
|
||||
"end" => 0x23,
|
||||
"pageup" => 0x21,
|
||||
"pagedown" => 0x22,
|
||||
"left" => 0x25,
|
||||
"up" => 0x26,
|
||||
"right" => 0x27,
|
||||
"down" => 0x28,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user