新增工具、分类、组合、自动运行和日志等基础模型。 实现本地 JSON 配置读写、默认系统工具恢复、路径校验、统一启动服务、自动运行服务、托盘服务、开机自启服务和全局快捷键注册服务,为后续界面集成提供稳定边界。
286 lines
7.8 KiB
C#
286 lines
7.8 KiB
C#
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
|
||
};
|
||
}
|
||
}
|