394 lines
11 KiB
C#
394 lines
11 KiB
C#
// Licensed to the .NET Foundation under one or more agreements.
|
|
// The .NET Foundation licenses this file to you under the MIT license.
|
|
|
|
using System.Collections.Concurrent;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace Microsoft.Maui.Platform.Linux.Services;
|
|
|
|
/// <summary>
|
|
/// Provides global hotkey registration and handling using X11.
|
|
/// </summary>
|
|
public class GlobalHotkeyService : IDisposable
|
|
{
|
|
private nint _display;
|
|
private nint _rootWindow;
|
|
private readonly ConcurrentDictionary<int, HotkeyRegistration> _registrations = new();
|
|
private int _nextId = 1;
|
|
private bool _disposed;
|
|
private Thread? _eventThread;
|
|
private bool _isListening;
|
|
|
|
/// <summary>
|
|
/// Event raised when a registered hotkey is pressed.
|
|
/// </summary>
|
|
public event EventHandler<HotkeyEventArgs>? HotkeyPressed;
|
|
|
|
/// <summary>
|
|
/// Initializes the global hotkey service.
|
|
/// </summary>
|
|
public void Initialize()
|
|
{
|
|
_display = XOpenDisplay(IntPtr.Zero);
|
|
if (_display == IntPtr.Zero)
|
|
{
|
|
throw new InvalidOperationException("Failed to open X display");
|
|
}
|
|
|
|
_rootWindow = XDefaultRootWindow(_display);
|
|
|
|
// Start listening for hotkeys in background
|
|
_isListening = true;
|
|
_eventThread = new Thread(ListenForHotkeys)
|
|
{
|
|
IsBackground = true,
|
|
Name = "GlobalHotkeyListener"
|
|
};
|
|
_eventThread.Start();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a global hotkey.
|
|
/// </summary>
|
|
/// <param name="key">The key code.</param>
|
|
/// <param name="modifiers">The modifier keys.</param>
|
|
/// <returns>A registration ID that can be used to unregister.</returns>
|
|
public int Register(HotkeyKey key, HotkeyModifiers modifiers)
|
|
{
|
|
if (_display == IntPtr.Zero)
|
|
{
|
|
throw new InvalidOperationException("Service not initialized");
|
|
}
|
|
|
|
int keyCode = XKeysymToKeycode(_display, (nint)key);
|
|
if (keyCode == 0)
|
|
{
|
|
throw new ArgumentException($"Invalid key: {key}");
|
|
}
|
|
|
|
uint modifierMask = GetModifierMask(modifiers);
|
|
|
|
// Register for all modifier combinations (with/without NumLock, CapsLock)
|
|
uint[] masks = GetModifierCombinations(modifierMask);
|
|
|
|
foreach (var mask in masks)
|
|
{
|
|
int result = XGrabKey(_display, keyCode, mask, _rootWindow, true, GrabModeAsync, GrabModeAsync);
|
|
if (result == 0)
|
|
{
|
|
Console.WriteLine($"Failed to grab key {key} with modifiers {modifiers}");
|
|
}
|
|
}
|
|
|
|
int id = _nextId++;
|
|
_registrations[id] = new HotkeyRegistration
|
|
{
|
|
Id = id,
|
|
KeyCode = keyCode,
|
|
Modifiers = modifierMask,
|
|
Key = key,
|
|
ModifierKeys = modifiers
|
|
};
|
|
|
|
XFlush(_display);
|
|
return id;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unregisters a global hotkey.
|
|
/// </summary>
|
|
/// <param name="id">The registration ID.</param>
|
|
public void Unregister(int id)
|
|
{
|
|
if (_registrations.TryRemove(id, out var registration))
|
|
{
|
|
uint[] masks = GetModifierCombinations(registration.Modifiers);
|
|
|
|
foreach (var mask in masks)
|
|
{
|
|
XUngrabKey(_display, registration.KeyCode, mask, _rootWindow);
|
|
}
|
|
|
|
XFlush(_display);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unregisters all global hotkeys.
|
|
/// </summary>
|
|
public void UnregisterAll()
|
|
{
|
|
foreach (var id in _registrations.Keys.ToList())
|
|
{
|
|
Unregister(id);
|
|
}
|
|
}
|
|
|
|
private void ListenForHotkeys()
|
|
{
|
|
while (_isListening && _display != IntPtr.Zero)
|
|
{
|
|
try
|
|
{
|
|
if (XPending(_display) > 0)
|
|
{
|
|
var xevent = new XEvent();
|
|
XNextEvent(_display, ref xevent);
|
|
|
|
if (xevent.type == KeyPress)
|
|
{
|
|
var keyEvent = xevent.KeyEvent;
|
|
ProcessKeyEvent(keyEvent.keycode, keyEvent.state);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Thread.Sleep(10);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"GlobalHotkeyService error: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ProcessKeyEvent(int keyCode, uint state)
|
|
{
|
|
// Remove NumLock and CapsLock from state for comparison
|
|
uint cleanState = state & ~(NumLockMask | CapsLockMask | ScrollLockMask);
|
|
|
|
foreach (var registration in _registrations.Values)
|
|
{
|
|
if (registration.KeyCode == keyCode &&
|
|
(registration.Modifiers == cleanState ||
|
|
registration.Modifiers == (cleanState & ~Mod2Mask))) // Mod2 is often NumLock
|
|
{
|
|
OnHotkeyPressed(registration);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnHotkeyPressed(HotkeyRegistration registration)
|
|
{
|
|
HotkeyPressed?.Invoke(this, new HotkeyEventArgs(
|
|
registration.Id,
|
|
registration.Key,
|
|
registration.ModifierKeys));
|
|
}
|
|
|
|
private uint GetModifierMask(HotkeyModifiers modifiers)
|
|
{
|
|
uint mask = 0;
|
|
if (modifiers.HasFlag(HotkeyModifiers.Shift)) mask |= ShiftMask;
|
|
if (modifiers.HasFlag(HotkeyModifiers.Control)) mask |= ControlMask;
|
|
if (modifiers.HasFlag(HotkeyModifiers.Alt)) mask |= Mod1Mask;
|
|
if (modifiers.HasFlag(HotkeyModifiers.Super)) mask |= Mod4Mask;
|
|
return mask;
|
|
}
|
|
|
|
private uint[] GetModifierCombinations(uint baseMask)
|
|
{
|
|
// Include combinations with NumLock and CapsLock
|
|
return new uint[]
|
|
{
|
|
baseMask,
|
|
baseMask | NumLockMask,
|
|
baseMask | CapsLockMask,
|
|
baseMask | NumLockMask | CapsLockMask
|
|
};
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
|
|
_isListening = false;
|
|
|
|
UnregisterAll();
|
|
|
|
if (_display != IntPtr.Zero)
|
|
{
|
|
XCloseDisplay(_display);
|
|
_display = IntPtr.Zero;
|
|
}
|
|
}
|
|
|
|
#region X11 Interop
|
|
|
|
private const int KeyPress = 2;
|
|
private const int GrabModeAsync = 1;
|
|
|
|
private const uint ShiftMask = 1 << 0;
|
|
private const uint LockMask = 1 << 1; // CapsLock
|
|
private const uint ControlMask = 1 << 2;
|
|
private const uint Mod1Mask = 1 << 3; // Alt
|
|
private const uint Mod2Mask = 1 << 4; // NumLock
|
|
private const uint Mod4Mask = 1 << 6; // Super
|
|
|
|
private const uint NumLockMask = Mod2Mask;
|
|
private const uint CapsLockMask = LockMask;
|
|
private const uint ScrollLockMask = 0; // Usually not used
|
|
|
|
[StructLayout(LayoutKind.Explicit)]
|
|
private struct XEvent
|
|
{
|
|
[FieldOffset(0)] public int type;
|
|
[FieldOffset(0)] public XKeyEvent KeyEvent;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct XKeyEvent
|
|
{
|
|
public int type;
|
|
public ulong serial;
|
|
public bool send_event;
|
|
public nint display;
|
|
public nint window;
|
|
public nint root;
|
|
public nint subwindow;
|
|
public ulong time;
|
|
public int x, y;
|
|
public int x_root, y_root;
|
|
public uint state;
|
|
public int keycode;
|
|
public bool same_screen;
|
|
}
|
|
|
|
[DllImport("libX11.so.6")]
|
|
private static extern nint XOpenDisplay(nint display);
|
|
|
|
[DllImport("libX11.so.6")]
|
|
private static extern void XCloseDisplay(nint display);
|
|
|
|
[DllImport("libX11.so.6")]
|
|
private static extern nint XDefaultRootWindow(nint display);
|
|
|
|
[DllImport("libX11.so.6")]
|
|
private static extern int XKeysymToKeycode(nint display, nint keysym);
|
|
|
|
[DllImport("libX11.so.6")]
|
|
private static extern int XGrabKey(nint display, int keycode, uint modifiers, nint grabWindow,
|
|
bool ownerEvents, int pointerMode, int keyboardMode);
|
|
|
|
[DllImport("libX11.so.6")]
|
|
private static extern int XUngrabKey(nint display, int keycode, uint modifiers, nint grabWindow);
|
|
|
|
[DllImport("libX11.so.6")]
|
|
private static extern int XPending(nint display);
|
|
|
|
[DllImport("libX11.so.6")]
|
|
private static extern int XNextEvent(nint display, ref XEvent xevent);
|
|
|
|
[DllImport("libX11.so.6")]
|
|
private static extern void XFlush(nint display);
|
|
|
|
#endregion
|
|
|
|
private class HotkeyRegistration
|
|
{
|
|
public int Id { get; set; }
|
|
public int KeyCode { get; set; }
|
|
public uint Modifiers { get; set; }
|
|
public HotkeyKey Key { get; set; }
|
|
public HotkeyModifiers ModifierKeys { get; set; }
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Event args for hotkey pressed events.
|
|
/// </summary>
|
|
public class HotkeyEventArgs : EventArgs
|
|
{
|
|
/// <summary>
|
|
/// Gets the registration ID.
|
|
/// </summary>
|
|
public int Id { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the key.
|
|
/// </summary>
|
|
public HotkeyKey Key { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the modifier keys.
|
|
/// </summary>
|
|
public HotkeyModifiers Modifiers { get; }
|
|
|
|
public HotkeyEventArgs(int id, HotkeyKey key, HotkeyModifiers modifiers)
|
|
{
|
|
Id = id;
|
|
Key = key;
|
|
Modifiers = modifiers;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hotkey modifier keys.
|
|
/// </summary>
|
|
[Flags]
|
|
public enum HotkeyModifiers
|
|
{
|
|
None = 0,
|
|
Shift = 1 << 0,
|
|
Control = 1 << 1,
|
|
Alt = 1 << 2,
|
|
Super = 1 << 3
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hotkey keys (X11 keysyms).
|
|
/// </summary>
|
|
public enum HotkeyKey : uint
|
|
{
|
|
// Letters
|
|
A = 0x61, B = 0x62, C = 0x63, D = 0x64, E = 0x65,
|
|
F = 0x66, G = 0x67, H = 0x68, I = 0x69, J = 0x6A,
|
|
K = 0x6B, L = 0x6C, M = 0x6D, N = 0x6E, O = 0x6F,
|
|
P = 0x70, Q = 0x71, R = 0x72, S = 0x73, T = 0x74,
|
|
U = 0x75, V = 0x76, W = 0x77, X = 0x78, Y = 0x79,
|
|
Z = 0x7A,
|
|
|
|
// Numbers
|
|
D0 = 0x30, D1 = 0x31, D2 = 0x32, D3 = 0x33, D4 = 0x34,
|
|
D5 = 0x35, D6 = 0x36, D7 = 0x37, D8 = 0x38, D9 = 0x39,
|
|
|
|
// Function keys
|
|
F1 = 0xFFBE, F2 = 0xFFBF, F3 = 0xFFC0, F4 = 0xFFC1,
|
|
F5 = 0xFFC2, F6 = 0xFFC3, F7 = 0xFFC4, F8 = 0xFFC5,
|
|
F9 = 0xFFC6, F10 = 0xFFC7, F11 = 0xFFC8, F12 = 0xFFC9,
|
|
|
|
// Special keys
|
|
Escape = 0xFF1B,
|
|
Tab = 0xFF09,
|
|
Return = 0xFF0D,
|
|
Space = 0x20,
|
|
BackSpace = 0xFF08,
|
|
Delete = 0xFFFF,
|
|
Insert = 0xFF63,
|
|
Home = 0xFF50,
|
|
End = 0xFF57,
|
|
PageUp = 0xFF55,
|
|
PageDown = 0xFF56,
|
|
|
|
// Arrow keys
|
|
Left = 0xFF51,
|
|
Up = 0xFF52,
|
|
Right = 0xFF53,
|
|
Down = 0xFF54,
|
|
|
|
// Media keys
|
|
AudioPlay = 0x1008FF14,
|
|
AudioStop = 0x1008FF15,
|
|
AudioPrev = 0x1008FF16,
|
|
AudioNext = 0x1008FF17,
|
|
AudioMute = 0x1008FF12,
|
|
AudioRaiseVolume = 0x1008FF13,
|
|
AudioLowerVolume = 0x1008FF11,
|
|
|
|
// Print screen
|
|
Print = 0xFF61
|
|
}
|