7a4c516a20
- 后端:新增 CloudSync 认证/权限/端点/服务与 DTO - 数据:新增用户/会话/安全策略实体与 EF Core migrations - 前端:新增云同步设置 UI、客户端与本地存储;Vite 支持 maui 构建输出到 wwwroot - 桌面端:新增 Avalonia 项目、内置 WebServer、托盘与 Windows 全局热键 - 发布/构建:新增 Windows/Linux 发布脚本与统一入口;调整 MAUI 资源与安装包配置 - 文档:同步更新 README/docs 与协作规则
265 lines
7.2 KiB
C#
265 lines
7.2 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Diagnostics;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
|
|
namespace Hua.Todo.Avalonia.Services.Platforms;
|
|
|
|
/// <summary>
|
|
/// Windows 平台全局热键服务实现。
|
|
/// 通过 <c>RegisterHotKey</c> 将热键注册到专用线程的消息队列中,避免依赖窗口句柄与 WndProc Hook。
|
|
/// </summary>
|
|
public sealed class WindowsGlobalHotKeyService : IGlobalHotKeyService, IDisposable
|
|
{
|
|
private const int HotKeyId = 9000;
|
|
private const uint WmHotKey = 0x0312;
|
|
private const uint WmQuit = 0x0012;
|
|
private const uint WmAppExecute = 0x8001;
|
|
|
|
public const uint ModAlt = 0x0001;
|
|
public const uint ModControl = 0x0002;
|
|
public const uint ModShift = 0x0004;
|
|
public const uint ModWin = 0x0008;
|
|
|
|
private readonly ConcurrentDictionary<int, Action> _actions = new();
|
|
private int _actionId;
|
|
private Thread? _thread;
|
|
private uint _threadId;
|
|
private ManualResetEventSlim? _threadStarted;
|
|
|
|
private Action? _callback;
|
|
private bool _isRegistered;
|
|
private uint _currentModifiers;
|
|
private uint _currentKey;
|
|
private bool _isDisposed;
|
|
|
|
/// <inheritdoc />
|
|
public bool IsSupported => OperatingSystem.IsWindows();
|
|
|
|
/// <inheritdoc />
|
|
public void RegisterHotKey(string modifiers, string key, Action callback)
|
|
{
|
|
if (!IsSupported) return;
|
|
|
|
_callback = callback;
|
|
_currentModifiers = ParseModifiers(modifiers);
|
|
_currentKey = ParseKey(key);
|
|
|
|
StartThreadIfNeeded();
|
|
InvokeOnHotKeyThread(() =>
|
|
{
|
|
UnregisterHotKeyInternal();
|
|
|
|
if (_currentKey == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (RegisterHotKey(IntPtr.Zero, HotKeyId, _currentModifiers, _currentKey))
|
|
{
|
|
_isRegistered = true;
|
|
}
|
|
else
|
|
{
|
|
Debug.WriteLine("[WindowsGlobalHotKeyService] Failed to register hotkey.");
|
|
}
|
|
});
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void UnregisterHotKey()
|
|
{
|
|
if (!IsSupported) return;
|
|
|
|
StartThreadIfNeeded();
|
|
InvokeOnHotKeyThread(UnregisterHotKeyInternal);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void UpdateHotKey(string modifiers, string key)
|
|
{
|
|
if (_callback == null) return;
|
|
RegisterHotKey(modifiers, key, _callback);
|
|
}
|
|
|
|
private void UnregisterHotKeyInternal()
|
|
{
|
|
if (_isRegistered)
|
|
{
|
|
UnregisterHotKey(IntPtr.Zero, HotKeyId);
|
|
_isRegistered = false;
|
|
}
|
|
}
|
|
|
|
private void StartThreadIfNeeded()
|
|
{
|
|
if (_thread != null) return;
|
|
|
|
_threadStarted = new ManualResetEventSlim(false);
|
|
_thread = new Thread(MessageLoop)
|
|
{
|
|
IsBackground = true,
|
|
Name = "Hua.Todo HotKey Thread"
|
|
};
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
_thread.SetApartmentState(ApartmentState.STA);
|
|
}
|
|
_thread.Start();
|
|
_threadStarted.Wait();
|
|
}
|
|
|
|
private void MessageLoop()
|
|
{
|
|
_threadId = GetCurrentThreadId();
|
|
_threadStarted?.Set();
|
|
|
|
while (GetMessage(out var msg, IntPtr.Zero, 0, 0) != 0)
|
|
{
|
|
if (msg.message == WmHotKey && msg.wParam == (UIntPtr)HotKeyId)
|
|
{
|
|
try
|
|
{
|
|
_callback?.Invoke();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.WriteLine($"[WindowsGlobalHotKeyService] Hotkey callback failed: {ex}");
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (msg.message == WmAppExecute)
|
|
{
|
|
var id = unchecked((int)msg.wParam);
|
|
if (_actions.TryRemove(id, out var action))
|
|
{
|
|
try
|
|
{
|
|
action();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.WriteLine($"[WindowsGlobalHotKeyService] Action execution failed: {ex}");
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (msg.message == WmQuit)
|
|
{
|
|
break;
|
|
}
|
|
|
|
TranslateMessage(ref msg);
|
|
DispatchMessage(ref msg);
|
|
}
|
|
|
|
_threadStarted?.Dispose();
|
|
_threadStarted = null;
|
|
}
|
|
|
|
private void InvokeOnHotKeyThread(Action action)
|
|
{
|
|
var id = Interlocked.Increment(ref _actionId);
|
|
_actions[id] = action;
|
|
|
|
if (!PostThreadMessage(_threadId, WmAppExecute, (UIntPtr)id, IntPtr.Zero))
|
|
{
|
|
_actions.TryRemove(id, out _);
|
|
}
|
|
}
|
|
|
|
private static uint ParseModifiers(string modifiers)
|
|
{
|
|
uint mod = 0;
|
|
if (string.IsNullOrWhiteSpace(modifiers)) return mod;
|
|
|
|
var parts = modifiers.Split(',');
|
|
foreach (var part in parts)
|
|
{
|
|
var p = part.Trim();
|
|
if (p.Equals("Control", StringComparison.OrdinalIgnoreCase)) mod |= ModControl;
|
|
if (p.Equals("Alt", StringComparison.OrdinalIgnoreCase)) mod |= ModAlt;
|
|
if (p.Equals("Shift", StringComparison.OrdinalIgnoreCase)) mod |= ModShift;
|
|
if (p.Equals("Windows", StringComparison.OrdinalIgnoreCase)) mod |= ModWin;
|
|
}
|
|
|
|
return mod;
|
|
}
|
|
|
|
private static uint ParseKey(string key)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(key)) return 0;
|
|
|
|
if (key.Length == 1)
|
|
{
|
|
var c = char.ToUpperInvariant(key[0]);
|
|
if (c is >= 'A' and <= 'Z') return c;
|
|
if (c is >= '0' and <= '9') return c;
|
|
}
|
|
|
|
return 0x58; // Default 'X'
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Dispose()
|
|
{
|
|
if (_isDisposed) return;
|
|
_isDisposed = true;
|
|
|
|
try
|
|
{
|
|
UnregisterHotKey();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
if (_threadId != 0)
|
|
{
|
|
PostThreadMessage(_threadId, WmQuit, UIntPtr.Zero, IntPtr.Zero);
|
|
}
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct Msg
|
|
{
|
|
public IntPtr hwnd;
|
|
public uint message;
|
|
public UIntPtr wParam;
|
|
public IntPtr lParam;
|
|
public uint time;
|
|
public POINT pt;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct POINT
|
|
{
|
|
public int x;
|
|
public int y;
|
|
}
|
|
|
|
[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);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern sbyte GetMessage(out Msg lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern bool TranslateMessage(ref Msg lpMsg);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern IntPtr DispatchMessage(ref Msg lpMsg);
|
|
|
|
[DllImport("user32.dll", SetLastError = true)]
|
|
private static extern bool PostThreadMessage(uint idThread, uint msg, UIntPtr wParam, IntPtr lParam);
|
|
|
|
[DllImport("kernel32.dll")]
|
|
private static extern uint GetCurrentThreadId();
|
|
}
|