Fix hotkeys running when window is in focus (Closes #1411) (#1413)

This commit is contained in:
2026-02-10 05:33:54 -08:00
parent 067da8a091
commit 5102ad0fe7
3 changed files with 515 additions and 341 deletions
@@ -1,3 +1,4 @@
using Daybreak.Linux.Utils;
using Daybreak.Shared.Models;
using Daybreak.Shared.Services.Keyboard;
using Microsoft.Extensions.Hosting;
@@ -5,149 +6,23 @@ using System.Runtime.InteropServices;
namespace Daybreak.Linux.Services.Keyboard;
public partial class KeyboardHookService : IHostedService, IKeyboardHookService, IDisposable
public sealed class KeyboardHookService : IHostedService, IKeyboardHookService, IDisposable
{
private const string X11Lib = "libX11.so.6";
private const string XtstLib = "libXtst.so.6";
// X11 event types
private const int KeyPress = 2;
private const int KeyRelease = 3;
// XRecord constants
private const int XRecordFromServer = 0;
private const int XRecordAllClients = 3;
// XK key codes (from X11/keysymdef.h)
private const uint XK_F1 = 0xffbe;
private const uint XK_F2 = 0xffbf;
private const uint XK_F3 = 0xffc0;
private const uint XK_F4 = 0xffc1;
private const uint XK_F5 = 0xffc2;
private const uint XK_F6 = 0xffc3;
private const uint XK_F7 = 0xffc4;
private const uint XK_F8 = 0xffc5;
private const uint XK_F9 = 0xffc6;
private const uint XK_F10 = 0xffc7;
private const uint XK_F11 = 0xffc8;
private const uint XK_F12 = 0xffc9;
// XRecordRange structure
[StructLayout(LayoutKind.Sequential)]
private struct XRecordRange
{
public XRecordRange8 core_requests;
public XRecordRange8 core_replies;
public XRecordExtRange ext_requests;
public XRecordExtRange ext_replies;
public XRecordRange8 delivered_events;
public XRecordRange8 device_events;
public XRecordRange8 errors;
public byte client_started;
public byte client_died;
}
[StructLayout(LayoutKind.Sequential)]
private struct XRecordRange8
{
public byte first;
public byte last;
}
[StructLayout(LayoutKind.Sequential)]
private struct XRecordExtRange
{
public XRecordRange16 ext_major;
public XRecordRange8 ext_minor;
}
[StructLayout(LayoutKind.Sequential)]
private struct XRecordRange16
{
public ushort first;
public ushort last;
}
// Callback delegate for XRecordEnableContext
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void XRecordInterceptProc(nint closure, ref XRecordInterceptData recorded_data);
// XRecordInterceptData structure
[StructLayout(LayoutKind.Sequential)]
private struct XRecordInterceptData
{
public nint id_base;
public nint server_time;
public nint client_seq;
public int category;
public byte client_swapped;
private byte pad1;
private byte pad2;
private byte pad3;
public nint data;
public uint data_len;
}
// X11 P/Invoke
[LibraryImport(X11Lib, EntryPoint = "XOpenDisplay")]
private static partial nint XOpenDisplay(nint display_name);
[LibraryImport(X11Lib, EntryPoint = "XCloseDisplay")]
private static partial int XCloseDisplay(nint display);
[LibraryImport(X11Lib, EntryPoint = "XFlush")]
private static partial int XFlush(nint display);
[LibraryImport(X11Lib, EntryPoint = "XKeycodeToKeysym")]
private static partial uint XKeycodeToKeysym(nint display, byte keycode, int index);
[LibraryImport(X11Lib, EntryPoint = "XFree")]
private static partial int XFree(nint data);
// XRecord extension P/Invoke
[LibraryImport(XtstLib, EntryPoint = "XRecordQueryVersion")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool XRecordQueryVersion(nint display, out int major, out int minor);
[LibraryImport(XtstLib, EntryPoint = "XRecordAllocRange")]
private static partial nint XRecordAllocRange();
[LibraryImport(XtstLib, EntryPoint = "XRecordCreateContext")]
private static partial ulong XRecordCreateContext(
nint display,
int flags,
nint[] client_specs,
int nclients,
nint[] ranges,
int nranges);
[LibraryImport(XtstLib, EntryPoint = "XRecordEnableContext")]
private static partial int XRecordEnableContext(
nint display,
ulong context,
XRecordInterceptProc callback,
nint closure);
[LibraryImport(XtstLib, EntryPoint = "XRecordDisableContext")]
private static partial int XRecordDisableContext(nint display, ulong context);
[LibraryImport(XtstLib, EntryPoint = "XRecordFreeContext")]
private static partial int XRecordFreeContext(nint display, ulong context);
// Instance fields
private nint controlDisplay;
private nint dataDisplay;
private ulong recordContext;
private Thread? recordThread;
private Task? recordTask;
private CancellationTokenSource? cts;
private bool isStarted;
private XRecordInterceptProc? callbackDelegate;
private NativeMethods.XRecordInterceptProc? callbackDelegate;
private ulong appWindowId;
public event EventHandler<KeyboardEventArgs>? KeyDown;
public event EventHandler<KeyboardEventArgs>? KeyUp;
Task IHostedService.StartAsync(CancellationToken cancellationToken)
{
this.appWindowId = FindAppWindowId();
return Task.CompletedTask;
}
@@ -167,8 +42,8 @@ public partial class KeyboardHookService : IHostedService, IKeyboardHookService,
// We need two display connections for XRecord:
// - controlDisplay: for controlling the recording (enable/disable)
// - dataDisplay: for receiving the recorded data (blocks in XRecordEnableContext)
this.controlDisplay = XOpenDisplay(nint.Zero);
this.dataDisplay = XOpenDisplay(nint.Zero);
this.controlDisplay = NativeMethods.XOpenDisplay(nint.Zero);
this.dataDisplay = NativeMethods.XOpenDisplay(nint.Zero);
if (this.controlDisplay == nint.Zero || this.dataDisplay == nint.Zero)
{
@@ -177,14 +52,14 @@ public partial class KeyboardHookService : IHostedService, IKeyboardHookService,
}
// Check if XRecord extension is available
if (!XRecordQueryVersion(this.controlDisplay, out _, out _))
if (!NativeMethods.XRecordQueryVersion(this.controlDisplay, out _, out _))
{
this.Cleanup();
return;
}
// Create a record range for keyboard events only
var rangePtr = XRecordAllocRange();
var rangePtr = NativeMethods.XRecordAllocRange();
if (rangePtr == nint.Zero)
{
this.Cleanup();
@@ -192,21 +67,21 @@ public partial class KeyboardHookService : IHostedService, IKeyboardHookService,
}
// Set up the range to capture keyboard events (KeyPress and KeyRelease)
var range = Marshal.PtrToStructure<XRecordRange>(rangePtr);
range.device_events.first = KeyPress;
range.device_events.last = KeyRelease;
var range = Marshal.PtrToStructure<NativeMethods.XRecordRange>(rangePtr);
range.device_events.first = NativeMethods.KeyPress;
range.device_events.last = NativeMethods.KeyRelease;
Marshal.StructureToPtr(range, rangePtr, false);
// Create client spec for all clients
var clientSpec = new nint[1];
clientSpec[0] = XRecordAllClients;
clientSpec[0] = NativeMethods.XRecordAllClients;
var ranges = new nint[1];
ranges[0] = rangePtr;
// Create the record context on the DATA display (not control)
// The context must be created and enabled on the same display
this.recordContext = XRecordCreateContext(
this.recordContext = NativeMethods.XRecordCreateContext(
this.dataDisplay,
0,
clientSpec,
@@ -214,7 +89,7 @@ public partial class KeyboardHookService : IHostedService, IKeyboardHookService,
ranges,
1);
XFree(rangePtr);
NativeMethods.XFree(rangePtr);
if (this.recordContext == 0)
{
@@ -226,12 +101,11 @@ public partial class KeyboardHookService : IHostedService, IKeyboardHookService,
this.callbackDelegate = this.RecordCallback;
this.cts = new CancellationTokenSource();
this.recordThread = new Thread(this.RecordLoop)
{
IsBackground = true,
Name = "X11 XRecord Event Loop"
};
this.recordThread.Start();
this.recordTask = Task.Factory.StartNew(
this.RecordLoop,
this.cts.Token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
this.isStarted = true;
}
@@ -248,11 +122,11 @@ public partial class KeyboardHookService : IHostedService, IKeyboardHookService,
// Disable the context to unblock XRecordEnableContext
if (this.controlDisplay != nint.Zero && this.recordContext != 0)
{
XRecordDisableContext(this.controlDisplay, this.recordContext);
XFlush(this.controlDisplay);
NativeMethods.XRecordDisableContext(this.controlDisplay, this.recordContext);
NativeMethods.XFlush(this.controlDisplay);
}
this.recordThread?.Join(TimeSpan.FromSeconds(2));
this.recordTask = null;
this.Cleanup();
this.isStarted = false;
@@ -262,19 +136,19 @@ public partial class KeyboardHookService : IHostedService, IKeyboardHookService,
{
if (this.controlDisplay != nint.Zero && this.recordContext != 0)
{
XRecordFreeContext(this.controlDisplay, this.recordContext);
NativeMethods.XRecordFreeContext(this.controlDisplay, this.recordContext);
this.recordContext = 0;
}
if (this.dataDisplay != nint.Zero)
{
XCloseDisplay(this.dataDisplay);
NativeMethods.XCloseDisplay(this.dataDisplay);
this.dataDisplay = nint.Zero;
}
if (this.controlDisplay != nint.Zero)
{
XCloseDisplay(this.controlDisplay);
NativeMethods.XCloseDisplay(this.controlDisplay);
this.controlDisplay = nint.Zero;
}
@@ -291,12 +165,18 @@ public partial class KeyboardHookService : IHostedService, IKeyboardHookService,
private void RecordLoop()
{
// This call blocks until XRecordDisableContext is called
XRecordEnableContext(this.dataDisplay, this.recordContext, this.callbackDelegate!, nint.Zero);
NativeMethods.XRecordEnableContext(this.dataDisplay, this.recordContext, this.callbackDelegate!, nint.Zero);
}
private void RecordCallback(nint closure, ref XRecordInterceptData data)
private void RecordCallback(nint closure, ref NativeMethods.XRecordInterceptData data)
{
if (data.category != XRecordFromServer || data.data == nint.Zero)
if (data.category != NativeMethods.XRecordFromServer || data.data == nint.Zero)
{
return;
}
// Only process events when our app window is focused
if (!this.IsAppWindowFocused())
{
return;
}
@@ -306,13 +186,13 @@ public partial class KeyboardHookService : IHostedService, IKeyboardHookService,
var eventType = Marshal.ReadByte(data.data, 0);
var keycode = Marshal.ReadByte(data.data, 1);
if (eventType is not KeyPress and not KeyRelease)
if (eventType is not NativeMethods.KeyPress and not NativeMethods.KeyRelease)
{
return;
}
// Convert keycode to keysym
var keysym = XKeycodeToKeysym(this.controlDisplay, keycode, 0);
var keysym = NativeMethods.XKeycodeToKeysym(this.controlDisplay, keycode, 0);
// Only process F1-F12 keys
if (!TryMapXKeyToVirtualKey(keysym, out var virtualKey))
@@ -320,7 +200,7 @@ public partial class KeyboardHookService : IHostedService, IKeyboardHookService,
return;
}
if (eventType == KeyPress)
if (eventType == NativeMethods.KeyPress)
{
this.KeyDown?.Invoke(this, new KeyboardEventArgs(virtualKey));
}
@@ -335,21 +215,143 @@ public partial class KeyboardHookService : IHostedService, IKeyboardHookService,
// Only map F1-F12 - these are the only keys we need to detect
virtualKey = keysym switch
{
XK_F1 => VirtualKey.F1,
XK_F2 => VirtualKey.F2,
XK_F3 => VirtualKey.F3,
XK_F4 => VirtualKey.F4,
XK_F5 => VirtualKey.F5,
XK_F6 => VirtualKey.F6,
XK_F7 => VirtualKey.F7,
XK_F8 => VirtualKey.F8,
XK_F9 => VirtualKey.F9,
XK_F10 => VirtualKey.F10,
XK_F11 => VirtualKey.F11,
XK_F12 => VirtualKey.F12,
NativeMethods.XK_F1 => VirtualKey.F1,
NativeMethods.XK_F2 => VirtualKey.F2,
NativeMethods.XK_F3 => VirtualKey.F3,
NativeMethods.XK_F4 => VirtualKey.F4,
NativeMethods.XK_F5 => VirtualKey.F5,
NativeMethods.XK_F6 => VirtualKey.F6,
NativeMethods.XK_F7 => VirtualKey.F7,
NativeMethods.XK_F8 => VirtualKey.F8,
NativeMethods.XK_F9 => VirtualKey.F9,
NativeMethods.XK_F10 => VirtualKey.F10,
NativeMethods.XK_F11 => VirtualKey.F11,
NativeMethods.XK_F12 => VirtualKey.F12,
_ => default
};
return virtualKey != default;
}
/// <summary>
/// Checks if the application window is currently focused.
/// </summary>
private bool IsAppWindowFocused()
{
if (this.controlDisplay == nint.Zero)
{
return false;
}
// Lazily get app window ID if not yet cached
if (this.appWindowId == 0)
{
this.appWindowId = FindAppWindowId();
if (this.appWindowId == 0)
{
return false;
}
}
if (NativeMethods.XGetInputFocus(this.controlDisplay, out var focusedWindow, out _) == 0)
{
return false;
}
// Check if focused window is the app window or a descendant of it
return this.IsWindowOrDescendant(focusedWindow, this.appWindowId);
}
/// <summary>
/// Checks if the focused window is the target window or a descendant (child) of it.
/// This handles cases where focus is on a child widget within the main window.
/// </summary>
private bool IsWindowOrDescendant(ulong focusedWindow, ulong targetWindow)
{
if (focusedWindow == 0)
{
return false;
}
var current = focusedWindow;
while (current != 0)
{
if (current == targetWindow)
{
return true;
}
// Get parent of current window
if (NativeMethods.XQueryTree(this.controlDisplay, current, out var root, out var parent, out var children, out _) == 0)
{
break;
}
// Free the children list if allocated
if (children != nint.Zero)
{
NativeMethods.XFree(children);
}
// Stop if we've reached the root
if (parent == root || parent == 0)
{
break;
}
current = parent;
}
return false;
}
/// <summary>
/// Finds the X11 window ID for the application using GTK's toplevel window list.
/// </summary>
private static ulong FindAppWindowId()
{
var gtkWindow = FindGtkWindowForCurrentProcess();
if (gtkWindow == nint.Zero)
{
return 0;
}
var gdkWindow = NativeMethods.gtk_widget_get_window(gtkWindow);
if (gdkWindow == nint.Zero)
{
return 0;
}
return NativeMethods.gdk_x11_window_get_xid(gdkWindow);
}
/// <summary>
/// Finds the GTK window belonging to the current process using gtk_window_list_toplevels.
/// </summary>
private static nint FindGtkWindowForCurrentProcess()
{
var list = NativeMethods.gtk_window_list_toplevels();
if (list == nint.Zero)
{
return nint.Zero;
}
var current = list;
nint result = nint.Zero;
while (current != nint.Zero)
{
var window = NativeMethods.G_list_data(current);
if (window != nint.Zero && NativeMethods.gtk_widget_get_visible(window))
{
// Return the first visible toplevel window (Photino creates one main window)
result = window;
break;
}
current = NativeMethods.G_list_next(current);
}
NativeMethods.g_list_free(list);
return result;
}
}
@@ -1,3 +1,4 @@
using Daybreak.Linux.Utils;
using Daybreak.Shared.Services.Window;
using Daybreak.Shared.Utils;
using Microsoft.Extensions.Logging;
@@ -15,23 +16,8 @@ namespace Daybreak.Linux.Services.Window;
/// This service requires GDK_BACKEND=x11 to be set when running under Wayland compositors.
/// Interactive drag/resize may not work properly under XWayland on some compositors.
/// </remarks>
internal sealed partial class WindowManipulationService(ILogger<WindowManipulationService> logger) : IWindowManipulationService
internal sealed class WindowManipulationService(ILogger<WindowManipulationService> logger) : IWindowManipulationService
{
private const string GtkLib = "libgtk-3.so.0";
private const string GdkLib = "libgdk-3.so.0";
private const string GlibLib = "libglib-2.0.so.0";
private const string X11Lib = "libX11.so.6";
// GDK window edge constants (from gdktypes.h)
private const int GDK_WINDOW_EDGE_NORTH_WEST = 0;
private const int GDK_WINDOW_EDGE_NORTH = 1;
private const int GDK_WINDOW_EDGE_NORTH_EAST = 2;
private const int GDK_WINDOW_EDGE_WEST = 3;
private const int GDK_WINDOW_EDGE_EAST = 4;
private const int GDK_WINDOW_EDGE_SOUTH_WEST = 5;
private const int GDK_WINDOW_EDGE_SOUTH = 6;
private const int GDK_WINDOW_EDGE_SOUTH_EAST = 7;
private readonly ILogger<WindowManipulationService> logger = logger;
private readonly int currentPid = Environment.ProcessId;
@@ -47,16 +33,16 @@ internal sealed partial class WindowManipulationService(ILogger<WindowManipulati
return;
}
var display = gdk_window_get_display(gdkWindow);
var seat = gdk_display_get_default_seat(display);
var device = gdk_seat_get_pointer(seat);
var display = NativeMethods.gdk_window_get_display(gdkWindow);
var seat = NativeMethods.gdk_display_get_default_seat(display);
var device = NativeMethods.gdk_seat_get_pointer(seat);
// Get root (screen) coordinates, not window-relative coordinates
gdk_device_get_position(device, out _, out var rootX, out var rootY);
NativeMethods.gdk_device_get_position(device, out _, out var rootX, out var rootY);
var timestamp = GetCurrentEventTime();
scopedLogger.LogDebug("DragWindow: root position=({rootX}, {rootY}), timestamp={timestamp}", rootX, rootY, timestamp);
gdk_window_begin_move_drag_for_device(gdkWindow, device, 1, rootX, rootY, timestamp);
NativeMethods.gdk_window_begin_move_drag_for_device(gdkWindow, device, 1, rootX, rootY, timestamp);
}
public void ResizeWindow(ResizeDirection direction)
@@ -69,18 +55,18 @@ internal sealed partial class WindowManipulationService(ILogger<WindowManipulati
return;
}
var display = gdk_window_get_display(gdkWindow);
var seat = gdk_display_get_default_seat(display);
var device = gdk_seat_get_pointer(seat);
var display = NativeMethods.gdk_window_get_display(gdkWindow);
var seat = NativeMethods.gdk_display_get_default_seat(display);
var device = NativeMethods.gdk_seat_get_pointer(seat);
// Get root (screen) coordinates, not window-relative coordinates
gdk_device_get_position(device, out _, out var rootX, out var rootY);
NativeMethods.gdk_device_get_position(device, out _, out var rootX, out var rootY);
var timestamp = GetCurrentEventTime();
var edge = MapResizeDirectionToGdkEdge(direction);
scopedLogger.LogDebug("ResizeWindow: edge={edge}, root position=({rootX}, {rootY}), timestamp={timestamp}", edge, rootX, rootY, timestamp);
gdk_window_begin_resize_drag_for_device(gdkWindow, edge, device, 1, rootX, rootY, timestamp);
NativeMethods.gdk_window_begin_resize_drag_for_device(gdkWindow, edge, device, 1, rootX, rootY, timestamp);
}
/// <summary>
@@ -100,7 +86,7 @@ internal sealed partial class WindowManipulationService(ILogger<WindowManipulati
var gtkWindow = FindGtkWindowForCurrentProcess();
if (gtkWindow != nint.Zero)
{
var gdkWindow = gtk_widget_get_window(gtkWindow);
var gdkWindow = NativeMethods.gtk_widget_get_window(gtkWindow);
if (gdkWindow != nint.Zero)
{
this.cachedGdkWindow = gdkWindow;
@@ -112,10 +98,10 @@ internal sealed partial class WindowManipulationService(ILogger<WindowManipulati
var x11Window = FindX11WindowByPid(this.currentPid);
if (x11Window != 0)
{
var display = gdk_display_get_default();
var display = NativeMethods.gdk_display_get_default();
if (display != nint.Zero)
{
var gdkWindow = gdk_x11_window_foreign_new_for_display(display, x11Window);
var gdkWindow = NativeMethods.gdk_x11_window_foreign_new_for_display(display, x11Window);
if (gdkWindow != nint.Zero)
{
this.cachedGdkWindow = gdkWindow;
@@ -133,7 +119,7 @@ internal sealed partial class WindowManipulationService(ILogger<WindowManipulati
/// </summary>
private static nint FindGtkWindowForCurrentProcess()
{
var list = gtk_window_list_toplevels();
var list = NativeMethods.gtk_window_list_toplevels();
if (list == nint.Zero)
{
return nint.Zero;
@@ -144,17 +130,17 @@ internal sealed partial class WindowManipulationService(ILogger<WindowManipulati
while (current != nint.Zero)
{
var window = G_list_data(current);
if (window != nint.Zero && gtk_widget_get_visible(window))
var window = NativeMethods.G_list_data(current);
if (window != nint.Zero && NativeMethods.gtk_widget_get_visible(window))
{
// Return the first visible toplevel window (Photino creates one main window)
result = window;
break;
}
current = G_list_next(current);
current = NativeMethods.G_list_next(current);
}
g_list_free(list);
NativeMethods.g_list_free(list);
return result;
}
@@ -163,7 +149,7 @@ internal sealed partial class WindowManipulationService(ILogger<WindowManipulati
/// </summary>
private static ulong FindX11WindowByPid(int targetPid)
{
var display = XOpenDisplay(nint.Zero);
var display = NativeMethods.XOpenDisplay(nint.Zero);
if (display == nint.Zero)
{
return 0;
@@ -171,12 +157,12 @@ internal sealed partial class WindowManipulationService(ILogger<WindowManipulati
try
{
var rootWindow = XDefaultRootWindow(display);
var rootWindow = NativeMethods.XDefaultRootWindow(display);
return FindWindowByPidRecursive(display, rootWindow, targetPid);
}
finally
{
XCloseDisplay(display);
NativeMethods.XCloseDisplay(display);
}
}
@@ -194,7 +180,7 @@ internal sealed partial class WindowManipulationService(ILogger<WindowManipulati
}
// Recurse into children
if (XQueryTree(display, window, out _, out _, out var children, out var nChildren) != 0 && children != nint.Zero)
if (NativeMethods.XQueryTree(display, window, out _, out _, out var children, out var nChildren) != 0 && children != nint.Zero)
{
try
{
@@ -212,7 +198,7 @@ internal sealed partial class WindowManipulationService(ILogger<WindowManipulati
}
finally
{
XFree(children);
NativeMethods.XFree(children);
}
}
@@ -221,40 +207,40 @@ internal sealed partial class WindowManipulationService(ILogger<WindowManipulati
private static int GetWindowPid(nint display, ulong window)
{
var pidAtom = XInternAtom(display, "_NET_WM_PID", true);
var pidAtom = NativeMethods.XInternAtom(display, "_NET_WM_PID", true);
if (pidAtom == 0)
{
return -1;
}
var result = XGetWindowProperty(
var result = NativeMethods.XGetWindowProperty(
display, window, pidAtom, 0, 1, false, 6 /* XA_CARDINAL */,
out _, out _, out var nitems, out _, out var data);
if (result != 0 || nitems == 0 || data == nint.Zero)
{
if (data != nint.Zero) XFree(data);
if (data != nint.Zero) NativeMethods.XFree(data);
return -1;
}
var pid = Marshal.ReadInt32(data);
XFree(data);
NativeMethods.XFree(data);
return pid;
}
private static bool HasWmState(nint display, ulong window)
{
var wmStateAtom = XInternAtom(display, "WM_STATE", true);
var wmStateAtom = NativeMethods.XInternAtom(display, "WM_STATE", true);
if (wmStateAtom == 0)
{
return false;
}
var result = XGetWindowProperty(
var result = NativeMethods.XGetWindowProperty(
display, window, wmStateAtom, 0, 0, false, 0 /* AnyPropertyType */,
out var actualType, out _, out _, out _, out var data);
if (data != nint.Zero) XFree(data);
if (data != nint.Zero) NativeMethods.XFree(data);
return result == 0 && actualType != 0;
}
@@ -262,141 +248,29 @@ internal sealed partial class WindowManipulationService(ILogger<WindowManipulati
{
return direction switch
{
ResizeDirection.Left => GDK_WINDOW_EDGE_WEST,
ResizeDirection.Right => GDK_WINDOW_EDGE_EAST,
ResizeDirection.Top => GDK_WINDOW_EDGE_NORTH,
ResizeDirection.TopLeft => GDK_WINDOW_EDGE_NORTH_WEST,
ResizeDirection.TopRight => GDK_WINDOW_EDGE_NORTH_EAST,
ResizeDirection.Bottom => GDK_WINDOW_EDGE_SOUTH,
ResizeDirection.BottomLeft => GDK_WINDOW_EDGE_SOUTH_WEST,
ResizeDirection.BottomRight => GDK_WINDOW_EDGE_SOUTH_EAST,
_ => GDK_WINDOW_EDGE_SOUTH_EAST
ResizeDirection.Left => NativeMethods.GDK_WINDOW_EDGE_WEST,
ResizeDirection.Right => NativeMethods.GDK_WINDOW_EDGE_EAST,
ResizeDirection.Top => NativeMethods.GDK_WINDOW_EDGE_NORTH,
ResizeDirection.TopLeft => NativeMethods.GDK_WINDOW_EDGE_NORTH_WEST,
ResizeDirection.TopRight => NativeMethods.GDK_WINDOW_EDGE_NORTH_EAST,
ResizeDirection.Bottom => NativeMethods.GDK_WINDOW_EDGE_SOUTH,
ResizeDirection.BottomLeft => NativeMethods.GDK_WINDOW_EDGE_SOUTH_WEST,
ResizeDirection.BottomRight => NativeMethods.GDK_WINDOW_EDGE_SOUTH_EAST,
_ => NativeMethods.GDK_WINDOW_EDGE_SOUTH_EAST
};
}
private static uint GetCurrentEventTime()
{
var currentEvent = gtk_get_current_event();
var currentEvent = NativeMethods.gtk_get_current_event();
if (currentEvent != nint.Zero)
{
var time = gdk_event_get_time(currentEvent);
gdk_event_free(currentEvent);
var time = NativeMethods.gdk_event_get_time(currentEvent);
NativeMethods.gdk_event_free(currentEvent);
return time;
}
// GDK_CURRENT_TIME = 0
return 0;
}
// GTK3 P/Invoke declarations
[LibraryImport(GtkLib, EntryPoint = "gtk_widget_get_window")]
private static partial nint gtk_widget_get_window(nint widget);
[LibraryImport(GtkLib, EntryPoint = "gtk_widget_get_visible")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool gtk_widget_get_visible(nint widget);
[LibraryImport(GtkLib, EntryPoint = "gtk_get_current_event")]
private static partial nint gtk_get_current_event();
[LibraryImport(GtkLib, EntryPoint = "gtk_window_list_toplevels")]
private static partial nint gtk_window_list_toplevels();
// GLib P/Invoke declarations
[LibraryImport(GlibLib, EntryPoint = "g_list_free")]
private static partial void g_list_free(nint list);
private static nint G_list_data(nint list) =>
list != nint.Zero ? Marshal.ReadIntPtr(list) : nint.Zero;
private static nint G_list_next(nint list) =>
list != nint.Zero ? Marshal.ReadIntPtr(list + nint.Size) : nint.Zero;
// GDK3 P/Invoke declarations
[LibraryImport(GdkLib, EntryPoint = "gdk_display_get_default")]
private static partial nint gdk_display_get_default();
[LibraryImport(GdkLib, EntryPoint = "gdk_window_get_display")]
private static partial nint gdk_window_get_display(nint window);
[LibraryImport(GdkLib, EntryPoint = "gdk_display_get_default_seat")]
private static partial nint gdk_display_get_default_seat(nint display);
[LibraryImport(GdkLib, EntryPoint = "gdk_seat_get_pointer")]
private static partial nint gdk_seat_get_pointer(nint seat);
[LibraryImport(GdkLib, EntryPoint = "gdk_device_get_position")]
private static partial void gdk_device_get_position(
nint device,
out nint screen,
out int x,
out int y);
[LibraryImport(GdkLib, EntryPoint = "gdk_window_begin_move_drag_for_device")]
private static partial void gdk_window_begin_move_drag_for_device(
nint window,
nint device,
int button,
int rootX,
int rootY,
uint timestamp);
[LibraryImport(GdkLib, EntryPoint = "gdk_window_begin_resize_drag_for_device")]
private static partial void gdk_window_begin_resize_drag_for_device(
nint window,
int edge,
nint device,
int button,
int rootX,
int rootY,
uint timestamp);
[LibraryImport(GdkLib, EntryPoint = "gdk_event_get_time")]
private static partial uint gdk_event_get_time(nint gdkEvent);
[LibraryImport(GdkLib, EntryPoint = "gdk_event_free")]
private static partial void gdk_event_free(nint gdkEvent);
[LibraryImport("libgdk-3.so.0", EntryPoint = "gdk_x11_window_foreign_new_for_display")]
private static partial nint gdk_x11_window_foreign_new_for_display(nint display, ulong window);
// X11 P/Invoke declarations
[LibraryImport(X11Lib, EntryPoint = "XOpenDisplay")]
private static partial nint XOpenDisplay(nint displayName);
[LibraryImport(X11Lib, EntryPoint = "XCloseDisplay")]
private static partial int XCloseDisplay(nint display);
[LibraryImport(X11Lib, EntryPoint = "XDefaultRootWindow")]
private static partial ulong XDefaultRootWindow(nint display);
[LibraryImport(X11Lib, EntryPoint = "XQueryTree")]
private static partial int XQueryTree(
nint display,
ulong window,
out ulong rootReturn,
out ulong parentReturn,
out nint childrenReturn,
out uint nChildrenReturn);
[LibraryImport(X11Lib, EntryPoint = "XFree")]
private static partial int XFree(nint data);
[LibraryImport(X11Lib, EntryPoint = "XInternAtom", StringMarshalling = StringMarshalling.Utf8)]
private static partial ulong XInternAtom(nint display, string atomName, [MarshalAs(UnmanagedType.Bool)] bool onlyIfExists);
[LibraryImport(X11Lib, EntryPoint = "XGetWindowProperty")]
private static partial int XGetWindowProperty(
nint display,
ulong window,
ulong property,
long longOffset,
long longLength,
[MarshalAs(UnmanagedType.Bool)] bool delete,
ulong reqType,
out ulong actualTypeReturn,
out int actualFormatReturn,
out ulong nitemsReturn,
out ulong bytesAfterReturn,
out nint propReturn);
}
+298
View File
@@ -0,0 +1,298 @@
using System.Runtime.InteropServices;
namespace Daybreak.Linux.Utils;
/// <summary>
/// Linux-specific native method declarations for X11, GTK3, GDK3, GLib, and XRecord APIs.
/// </summary>
public static partial class NativeMethods
{
#region Library Names
public const string X11Lib = "libX11.so.6";
public const string XtstLib = "libXtst.so.6";
public const string GtkLib = "libgtk-3.so.0";
public const string GdkLib = "libgdk-3.so.0";
public const string GlibLib = "libglib-2.0.so.0";
#endregion
#region X11 Constants
// X11 event types
public const int KeyPress = 2;
public const int KeyRelease = 3;
// XRecord constants
public const int XRecordFromServer = 0;
public const int XRecordAllClients = 3;
// XK key codes (from X11/keysymdef.h)
public const uint XK_F1 = 0xffbe;
public const uint XK_F2 = 0xffbf;
public const uint XK_F3 = 0xffc0;
public const uint XK_F4 = 0xffc1;
public const uint XK_F5 = 0xffc2;
public const uint XK_F6 = 0xffc3;
public const uint XK_F7 = 0xffc4;
public const uint XK_F8 = 0xffc5;
public const uint XK_F9 = 0xffc6;
public const uint XK_F10 = 0xffc7;
public const uint XK_F11 = 0xffc8;
public const uint XK_F12 = 0xffc9;
#endregion
#region GDK Constants
// GDK window edge constants (from gdktypes.h)
public const int GDK_WINDOW_EDGE_NORTH_WEST = 0;
public const int GDK_WINDOW_EDGE_NORTH = 1;
public const int GDK_WINDOW_EDGE_NORTH_EAST = 2;
public const int GDK_WINDOW_EDGE_WEST = 3;
public const int GDK_WINDOW_EDGE_EAST = 4;
public const int GDK_WINDOW_EDGE_SOUTH_WEST = 5;
public const int GDK_WINDOW_EDGE_SOUTH = 6;
public const int GDK_WINDOW_EDGE_SOUTH_EAST = 7;
#endregion
#region XRecord Structures
[StructLayout(LayoutKind.Sequential)]
public struct XRecordRange
{
public XRecordRange8 core_requests;
public XRecordRange8 core_replies;
public XRecordExtRange ext_requests;
public XRecordExtRange ext_replies;
public XRecordRange8 delivered_events;
public XRecordRange8 device_events;
public XRecordRange8 errors;
public byte client_started;
public byte client_died;
}
[StructLayout(LayoutKind.Sequential)]
public struct XRecordRange8
{
public byte first;
public byte last;
}
[StructLayout(LayoutKind.Sequential)]
public struct XRecordExtRange
{
public XRecordRange16 ext_major;
public XRecordRange8 ext_minor;
}
[StructLayout(LayoutKind.Sequential)]
public struct XRecordRange16
{
public ushort first;
public ushort last;
}
[StructLayout(LayoutKind.Sequential)]
public struct XRecordInterceptData
{
public nint id_base;
public nint server_time;
public nint client_seq;
public int category;
public byte client_swapped;
private byte pad1;
private byte pad2;
private byte pad3;
public nint data;
public uint data_len;
}
#endregion
#region Delegates
/// <summary>
/// Callback delegate for XRecordEnableContext.
/// </summary>
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void XRecordInterceptProc(nint closure, ref XRecordInterceptData recorded_data);
#endregion
#region X11 P/Invoke
[LibraryImport(X11Lib, EntryPoint = "XOpenDisplay")]
public static partial nint XOpenDisplay(nint display_name);
[LibraryImport(X11Lib, EntryPoint = "XCloseDisplay")]
public static partial int XCloseDisplay(nint display);
[LibraryImport(X11Lib, EntryPoint = "XFlush")]
public static partial int XFlush(nint display);
[LibraryImport(X11Lib, EntryPoint = "XKeycodeToKeysym")]
public static partial uint XKeycodeToKeysym(nint display, byte keycode, int index);
[LibraryImport(X11Lib, EntryPoint = "XFree")]
public static partial int XFree(nint data);
[LibraryImport(X11Lib, EntryPoint = "XGetInputFocus")]
public static partial int XGetInputFocus(nint display, out ulong focus_return, out int revert_to_return);
[LibraryImport(X11Lib, EntryPoint = "XQueryTree")]
public static partial int XQueryTree(
nint display,
ulong window,
out ulong root_return,
out ulong parent_return,
out nint children_return,
out uint nchildren_return);
[LibraryImport(X11Lib, EntryPoint = "XDefaultRootWindow")]
public static partial ulong XDefaultRootWindow(nint display);
[LibraryImport(X11Lib, EntryPoint = "XInternAtom", StringMarshalling = StringMarshalling.Utf8)]
public static partial ulong XInternAtom(nint display, string atomName, [MarshalAs(UnmanagedType.Bool)] bool onlyIfExists);
[LibraryImport(X11Lib, EntryPoint = "XGetWindowProperty")]
public static partial int XGetWindowProperty(
nint display,
ulong window,
ulong property,
long longOffset,
long longLength,
[MarshalAs(UnmanagedType.Bool)] bool delete,
ulong reqType,
out ulong actualTypeReturn,
out int actualFormatReturn,
out ulong nitemsReturn,
out ulong bytesAfterReturn,
out nint propReturn);
#endregion
#region XRecord P/Invoke
[LibraryImport(XtstLib, EntryPoint = "XRecordQueryVersion")]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool XRecordQueryVersion(nint display, out int major, out int minor);
[LibraryImport(XtstLib, EntryPoint = "XRecordAllocRange")]
public static partial nint XRecordAllocRange();
[LibraryImport(XtstLib, EntryPoint = "XRecordCreateContext")]
public static partial ulong XRecordCreateContext(
nint display,
int flags,
nint[] client_specs,
int nclients,
nint[] ranges,
int nranges);
[LibraryImport(XtstLib, EntryPoint = "XRecordEnableContext")]
public static partial int XRecordEnableContext(
nint display,
ulong context,
XRecordInterceptProc callback,
nint closure);
[LibraryImport(XtstLib, EntryPoint = "XRecordDisableContext")]
public static partial int XRecordDisableContext(nint display, ulong context);
[LibraryImport(XtstLib, EntryPoint = "XRecordFreeContext")]
public static partial int XRecordFreeContext(nint display, ulong context);
#endregion
#region GTK3 P/Invoke
[LibraryImport(GtkLib, EntryPoint = "gtk_widget_get_window")]
public static partial nint gtk_widget_get_window(nint widget);
[LibraryImport(GtkLib, EntryPoint = "gtk_widget_get_visible")]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool gtk_widget_get_visible(nint widget);
[LibraryImport(GtkLib, EntryPoint = "gtk_get_current_event")]
public static partial nint gtk_get_current_event();
[LibraryImport(GtkLib, EntryPoint = "gtk_window_list_toplevels")]
public static partial nint gtk_window_list_toplevels();
#endregion
#region GLib P/Invoke
[LibraryImport(GlibLib, EntryPoint = "g_list_free")]
public static partial void g_list_free(nint list);
/// <summary>
/// Gets the data pointer from a GList node.
/// </summary>
public static nint G_list_data(nint list) =>
list != nint.Zero ? Marshal.ReadIntPtr(list) : nint.Zero;
/// <summary>
/// Gets the next node pointer from a GList node.
/// </summary>
public static nint G_list_next(nint list) =>
list != nint.Zero ? Marshal.ReadIntPtr(list + nint.Size) : nint.Zero;
#endregion
#region GDK3 P/Invoke
[LibraryImport(GdkLib, EntryPoint = "gdk_display_get_default")]
public static partial nint gdk_display_get_default();
[LibraryImport(GdkLib, EntryPoint = "gdk_window_get_display")]
public static partial nint gdk_window_get_display(nint window);
[LibraryImport(GdkLib, EntryPoint = "gdk_display_get_default_seat")]
public static partial nint gdk_display_get_default_seat(nint display);
[LibraryImport(GdkLib, EntryPoint = "gdk_seat_get_pointer")]
public static partial nint gdk_seat_get_pointer(nint seat);
[LibraryImport(GdkLib, EntryPoint = "gdk_device_get_position")]
public static partial void gdk_device_get_position(
nint device,
out nint screen,
out int x,
out int y);
[LibraryImport(GdkLib, EntryPoint = "gdk_window_begin_move_drag_for_device")]
public static partial void gdk_window_begin_move_drag_for_device(
nint window,
nint device,
int button,
int rootX,
int rootY,
uint timestamp);
[LibraryImport(GdkLib, EntryPoint = "gdk_window_begin_resize_drag_for_device")]
public static partial void gdk_window_begin_resize_drag_for_device(
nint window,
int edge,
nint device,
int button,
int rootX,
int rootY,
uint timestamp);
[LibraryImport(GdkLib, EntryPoint = "gdk_event_get_time")]
public static partial uint gdk_event_get_time(nint gdkEvent);
[LibraryImport(GdkLib, EntryPoint = "gdk_event_free")]
public static partial void gdk_event_free(nint gdkEvent);
[LibraryImport(GdkLib, EntryPoint = "gdk_x11_window_foreign_new_for_display")]
public static partial nint gdk_x11_window_foreign_new_for_display(nint display, ulong window);
[LibraryImport(GdkLib, EntryPoint = "gdk_x11_window_get_xid")]
public static partial ulong gdk_x11_window_get_xid(nint gdkWindow);
#endregion
}