lightweight variant (#1)
Reviewed-on: #1 Co-authored-by: Aleksandr Neychev <alexnejchev73@gmail.com>
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
using System.Runtime.InteropServices;
|
||||
#if CARET_UI_AUTOMATION
|
||||
using System.Windows.Automation;
|
||||
using System.Windows.Automation.Text;
|
||||
#endif
|
||||
using Accessibility;
|
||||
|
||||
namespace CursorLang.Core.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// Locates the caret in the active input field — including one in another application.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// There is no single way to do it: classic Win32 applications create a system caret,
|
||||
/// while Chrome, Electron and others draw it themselves and report its position only
|
||||
/// through accessibility interfaces. So we ask the system first, then the application.
|
||||
///
|
||||
/// The UI Automation step is behind <c>CARET_UI_AUTOMATION</c>: it is the one part of
|
||||
/// the background process that reaches into the WPF half of the desktop runtime —
|
||||
/// TextPatternRange hands its rectangles back as System.Windows.Rect, which lives in
|
||||
/// WindowsBase — and it was measured at +3.9 MB private. It is also the last of the
|
||||
/// three steps and rarely reached. See the switch in CursorLang.Core.csproj.
|
||||
/// </remarks>
|
||||
internal static class CaretNative
|
||||
{
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool ClientToScreen(IntPtr hWnd, ref PopupWindowNative.Point lpPoint);
|
||||
|
||||
[DllImport("oleacc.dll")]
|
||||
private static extern int AccessibleObjectFromWindow(IntPtr hwnd, uint dwObjectId,
|
||||
ref Guid riid, out IAccessible ppvObject);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool GetWindowRect(IntPtr hWnd, out PopupWindowNative.Rect lpRect);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint GetDpiForWindow(IntPtr hWnd);
|
||||
|
||||
private const uint OBJID_CARET = 0xFFFFFFF8;
|
||||
private const int CHILDID_SELF = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The caret rectangle in screen pixels, or <c>null</c> when the active
|
||||
/// application does not report its position.
|
||||
/// </summary>
|
||||
internal static PopupWindowNative.Rect? TryGetCaretRect()
|
||||
{
|
||||
if (!ForegroundInputNative.TryGetInfo(out ForegroundInputNative.GuiThreadInfo info))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
PopupWindowNative.Rect? caret = TryGetSystemCaret(info)
|
||||
?? TryGetAccessibleCaret(info.hwndFocus)
|
||||
?? TryGetAutomationCaret();
|
||||
|
||||
return caret is null ? null : Validate(caret.Value, info.hwndFocus);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters out obviously wrong coordinates.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Some applications report the caret position in their own coordinate system or
|
||||
/// without accounting for display scaling, and the popup ends up far from the input
|
||||
/// field. The caret must be inside the input window — that is what we check, and
|
||||
/// before giving up we try to read the coordinates as unscaled ones.
|
||||
/// </remarks>
|
||||
private static PopupWindowNative.Rect? Validate(PopupWindowNative.Rect caret, IntPtr hwndFocus)
|
||||
{
|
||||
if (hwndFocus == IntPtr.Zero || !GetWindowRect(hwndFocus, out PopupWindowNative.Rect window))
|
||||
{
|
||||
return caret;
|
||||
}
|
||||
|
||||
return Validate(caret, window, GetDpiForWindow(hwndFocus) / 96.0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The same check over plain numbers: the input window bounds and the scale of
|
||||
/// its monitor are already known.
|
||||
/// </summary>
|
||||
internal static PopupWindowNative.Rect? Validate(
|
||||
PopupWindowNative.Rect caret, PopupWindowNative.Rect window, double scale)
|
||||
{
|
||||
if (IsInside(caret, window))
|
||||
{
|
||||
return caret;
|
||||
}
|
||||
|
||||
var scaled = new PopupWindowNative.Rect
|
||||
{
|
||||
Left = (int)(caret.Left * scale),
|
||||
Top = (int)(caret.Top * scale),
|
||||
Right = (int)(caret.Right * scale),
|
||||
Bottom = (int)(caret.Bottom * scale),
|
||||
};
|
||||
|
||||
return IsInside(scaled, window) ? scaled : null;
|
||||
}
|
||||
|
||||
internal static bool IsInside(PopupWindowNative.Rect inner, PopupWindowNative.Rect outer) =>
|
||||
inner.Left >= outer.Left && inner.Right <= outer.Right &&
|
||||
inner.Top >= outer.Top && inner.Bottom <= outer.Bottom;
|
||||
|
||||
#if CARET_UI_AUTOMATION
|
||||
/// <summary>How long we wait for another application to answer over UI Automation.</summary>
|
||||
private static readonly TimeSpan AutomationTimeout = TimeSpan.FromMilliseconds(150);
|
||||
|
||||
// Browsers and other applications with their own rendering engines draw the caret
|
||||
// themselves and report its position only through UI Automation. The request goes
|
||||
// into another process, so it is the slowest one and comes last
|
||||
private static PopupWindowNative.Rect? TryGetAutomationCaret()
|
||||
{
|
||||
// A hung application must not hang the popup along with it: we wait for the
|
||||
// answer for a limited time, otherwise we show the popup at the cursor
|
||||
Task<PopupWindowNative.Rect?> query = Task.Run(QueryAutomationCaret);
|
||||
return query.Wait(AutomationTimeout) ? query.Result : null;
|
||||
}
|
||||
|
||||
private static PopupWindowNative.Rect? QueryAutomationCaret()
|
||||
{
|
||||
try
|
||||
{
|
||||
AutomationElement focused = AutomationElement.FocusedElement;
|
||||
if (focused is null ||
|
||||
!focused.TryGetCurrentPattern(TextPattern.Pattern, out object pattern))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
TextPatternRange[] selection = ((TextPattern)pattern).GetSelection();
|
||||
if (selection.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// The caret has an empty selection and therefore no rectangle,
|
||||
// so we expand it to the nearest character
|
||||
TextPatternRange range = selection[0].Clone();
|
||||
range.ExpandToEnclosingUnit(TextUnit.Character);
|
||||
|
||||
System.Windows.Rect[] rectangles = range.GetBoundingRectangles();
|
||||
if (rectangles.Length == 0 || rectangles[0].Height <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
System.Windows.Rect caret = rectangles[0];
|
||||
return new PopupWindowNative.Rect
|
||||
{
|
||||
Left = (int)caret.Left,
|
||||
Top = (int)caret.Top,
|
||||
Right = (int)caret.Right,
|
||||
Bottom = (int)caret.Bottom,
|
||||
};
|
||||
}
|
||||
catch (Exception e) when (e is ElementNotAvailableException
|
||||
or InvalidOperationException
|
||||
or COMException)
|
||||
{
|
||||
// The application closed or stopped responding — that must not take the popup down
|
||||
return null;
|
||||
}
|
||||
}
|
||||
#else
|
||||
// Built without UI Automation: Chromium and Electron keep the system caret and MSAA
|
||||
// steps above, and where those stay silent the popup falls back to the cursor
|
||||
private static PopupWindowNative.Rect? TryGetAutomationCaret() => null;
|
||||
#endif
|
||||
|
||||
// The system caret: its coordinates come relative to the window that owns it
|
||||
private static PopupWindowNative.Rect? TryGetSystemCaret(ForegroundInputNative.GuiThreadInfo info)
|
||||
{
|
||||
if (info.hwndCaret == IntPtr.Zero || IsEmpty(info.rcCaret))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var topLeft = new PopupWindowNative.Point { X = info.rcCaret.Left, Y = info.rcCaret.Top };
|
||||
var bottomRight = new PopupWindowNative.Point { X = info.rcCaret.Right, Y = info.rcCaret.Bottom };
|
||||
if (!ClientToScreen(info.hwndCaret, ref topLeft) || !ClientToScreen(info.hwndCaret, ref bottomRight))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PopupWindowNative.Rect
|
||||
{
|
||||
Left = topLeft.X,
|
||||
Top = topLeft.Y,
|
||||
Right = bottomRight.X,
|
||||
Bottom = bottomRight.Y,
|
||||
};
|
||||
}
|
||||
|
||||
// The caret through accessibility interfaces: this is where browsers and Electron land
|
||||
private static PopupWindowNative.Rect? TryGetAccessibleCaret(IntPtr hwndFocus)
|
||||
{
|
||||
if (hwndFocus == IntPtr.Zero)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Guid iid = typeof(IAccessible).GUID;
|
||||
if (AccessibleObjectFromWindow(hwndFocus, OBJID_CARET, ref iid, out IAccessible caret) != 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
caret.accLocation(out int left, out int top, out int width, out int height, CHILDID_SELF);
|
||||
var rect = new PopupWindowNative.Rect
|
||||
{
|
||||
Left = left,
|
||||
Top = top,
|
||||
Right = left + width,
|
||||
Bottom = top + height,
|
||||
};
|
||||
|
||||
return IsEmpty(rect) ? null : rect;
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
// The application declared support but did not report the position
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.ReleaseComObject(caret);
|
||||
}
|
||||
}
|
||||
|
||||
// When there is no caret, its rectangle comes back with zero height.
|
||||
// We judge by height alone: zero coordinates are a normal start of an empty field
|
||||
internal static bool IsEmpty(PopupWindowNative.Rect rect) => rect.Bottom - rect.Top <= 0;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace CursorLang.Core.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// Input details of the active application: which window holds keyboard focus
|
||||
/// and where the caret is.
|
||||
/// </summary>
|
||||
internal static class ForegroundInputNative
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct GuiThreadInfo
|
||||
{
|
||||
public int cbSize;
|
||||
public uint flags;
|
||||
public IntPtr hwndActive;
|
||||
public IntPtr hwndFocus;
|
||||
public IntPtr hwndCapture;
|
||||
public IntPtr hwndMenuOwner;
|
||||
public IntPtr hwndMoveSize;
|
||||
public IntPtr hwndCaret;
|
||||
public PopupWindowNative.Rect rcCaret;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool GetGUIThreadInfo(uint idThread, ref GuiThreadInfo lpgui);
|
||||
|
||||
/// <summary>
|
||||
/// The input state of the foreground thread.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The zero thread identifier is not accidental: in modern applications the
|
||||
/// top-level window and the input window live in different threads, and the
|
||||
/// question has to be about the foreground as a whole.
|
||||
/// </remarks>
|
||||
internal static bool TryGetInfo(out GuiThreadInfo info)
|
||||
{
|
||||
info = new GuiThreadInfo { cbSize = Marshal.SizeOf<GuiThreadInfo>() };
|
||||
return GetGUIThreadInfo(0, ref info);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace CursorLang.Core.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// The right to bring a window to the foreground.
|
||||
/// </summary>
|
||||
internal static class ForegroundPermissionNative
|
||||
{
|
||||
/// <summary>ASFW_ANY — any process gets the right.</summary>
|
||||
private const uint AnyProcess = 0xFFFFFFFF;
|
||||
|
||||
/// <summary>
|
||||
/// Gives up our right to bring a window to the foreground in favour of other processes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Windows does not let just anyone change the foreground window: you have to be
|
||||
/// the process the user interacted with last. An instance started long ago is not
|
||||
/// one of those, and its window will come up only if the right is shared by the
|
||||
/// process the user has just launched.
|
||||
/// </remarks>
|
||||
internal static void GrantToAnyProcess() => AllowSetForegroundWindow(AnyProcess);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool AllowSetForegroundWindow(uint dwProcessId);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace CursorLang.Core.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// Win32 API for reading the layout of the active application.
|
||||
/// </summary>
|
||||
internal static class KeyboardLayoutNative
|
||||
{
|
||||
private const uint WmInputLangChangeRequest = 0x0050;
|
||||
|
||||
/// <summary>Take the next layout from the system list.</summary>
|
||||
private static readonly IntPtr InputLangChangeForward = new(0x0002);
|
||||
|
||||
/// <summary>HKL_NEXT — the same request in the language of older Windows versions.</summary>
|
||||
private static readonly IntPtr HklNext = new(1);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern IntPtr GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr lpdwProcessId);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetKeyboardLayout(uint idThread);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
/// <summary>
|
||||
/// The layout the user is currently typing with.
|
||||
/// </summary>
|
||||
internal static int GetActiveLocaleId() => GetLocaleIdOf(GetInputWindow());
|
||||
|
||||
/// <summary>
|
||||
/// Asks the active application to switch to the next layout from the system list.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Synthesizing a system shortcut such as Alt+Shift will not do: the user can
|
||||
/// reassign it in the Windows settings or turn it off entirely. A request sent as
|
||||
/// a message does not depend on those settings and works in another process.
|
||||
/// </remarks>
|
||||
internal static void RequestNextLayout()
|
||||
{
|
||||
IntPtr target = GetInputWindow();
|
||||
if (target == IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Both parameters mean the same thing: different Windows versions and different
|
||||
// UI frameworks look either at the flag or at lParam
|
||||
PostMessage(target, WmInputLangChangeRequest, InputLangChangeForward, HklNext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The window that owns keyboard input.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We ask the window with keyboard focus rather than the foreground window: in
|
||||
/// Windows 11 Notepad, the Start menu and other WinUI applications the input field
|
||||
/// lives in a separate thread, and the layout changes only for that thread. For the
|
||||
/// main window's thread it stays the same, and the switch goes unnoticed.
|
||||
/// </remarks>
|
||||
private static IntPtr GetInputWindow()
|
||||
{
|
||||
if (ForegroundInputNative.TryGetInfo(out ForegroundInputNative.GuiThreadInfo info) &&
|
||||
info.hwndFocus != IntPtr.Zero)
|
||||
{
|
||||
return info.hwndFocus;
|
||||
}
|
||||
|
||||
return GetForegroundWindow();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The locale identifier for a window. In Windows the layout is bound to a thread,
|
||||
/// so this reveals it for any application, not only for our own.
|
||||
/// </summary>
|
||||
internal static int GetLocaleIdOf(IntPtr hWnd)
|
||||
{
|
||||
uint threadId = GetWindowThreadProcessId(hWnd, IntPtr.Zero);
|
||||
return (int)GetKeyboardLayout(threadId).ToInt64() & 0xFFFF;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace CursorLang.Core.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// A system keyboard hook (WH_KEYBOARD_LL): sees key presses in every application
|
||||
/// and can keep them from going any further.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only the low-level hook is installed: a regular WH_KEYBOARD requires injecting a
|
||||
/// DLL into other processes, which is impossible for managed code. The callback
|
||||
/// arrives on the thread that installed the hook, and that thread must pump a message
|
||||
/// loop — hence the requirement to install the hook from the user interface thread.
|
||||
/// Returning control must not be delayed: once the system timeout expires, Windows
|
||||
/// silently removes the hook.
|
||||
/// </remarks>
|
||||
internal sealed class LowLevelKeyboardHook : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// A key event handler. Returns <c>true</c> when the event must be swallowed —
|
||||
/// then the foreground application will not see it.
|
||||
/// </summary>
|
||||
internal delegate bool KeyFilter(int virtualKey, bool isKeyDown);
|
||||
|
||||
private const int WhKeyboardLowLevel = 13;
|
||||
private const int HcAction = 0;
|
||||
|
||||
private const int WmKeyDown = 0x0100;
|
||||
private const int WmKeyUp = 0x0101;
|
||||
private const int WmSysKeyDown = 0x0104;
|
||||
private const int WmSysKeyUp = 0x0105;
|
||||
|
||||
/// <summary>The event came from SendInput rather than from a real key press.</summary>
|
||||
private const uint LowLevelKeyHookFlagInjected = 0x10;
|
||||
|
||||
private readonly KeyFilter _filter;
|
||||
|
||||
// The delegate lives in a field not for convenience: the only reference to it is
|
||||
// held by Win32, which the garbage collector knows nothing about, and without the
|
||||
// field the hook stops working after a random amount of time
|
||||
private readonly HookProc _callback;
|
||||
|
||||
private IntPtr _handle;
|
||||
|
||||
internal LowLevelKeyboardHook(KeyFilter filter)
|
||||
{
|
||||
_filter = filter;
|
||||
_callback = OnHookEvent;
|
||||
}
|
||||
|
||||
private delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
internal bool IsInstalled => _handle != IntPtr.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Installs the hook. Returns <c>false</c> when the system refuses.
|
||||
/// </summary>
|
||||
internal bool Install()
|
||||
{
|
||||
if (_handle != IntPtr.Zero)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
_handle = SetWindowsHookEx(WhKeyboardLowLevel, _callback, GetModuleHandle(null), 0);
|
||||
return _handle != IntPtr.Zero;
|
||||
}
|
||||
|
||||
internal void Uninstall()
|
||||
{
|
||||
if (_handle == IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UnhookWindowsHookEx(_handle);
|
||||
_handle = IntPtr.Zero;
|
||||
}
|
||||
|
||||
public void Dispose() => Uninstall();
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr CallNextHookEx(IntPtr hhk, int code, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern IntPtr GetModuleHandle(string? lpModuleName);
|
||||
|
||||
private IntPtr OnHookEvent(int code, IntPtr wParam, IntPtr lParam)
|
||||
{
|
||||
if (code != HcAction)
|
||||
{
|
||||
return CallNextHookEx(_handle, code, wParam, lParam);
|
||||
}
|
||||
|
||||
var message = (int)wParam;
|
||||
bool isKeyDown = message is WmKeyDown or WmSysKeyDown;
|
||||
bool isKeyUp = message is WmKeyUp or WmSysKeyUp;
|
||||
|
||||
var data = Marshal.PtrToStructure<KeyboardHookData>(lParam);
|
||||
|
||||
// Synthetic input comes from on-screen keyboards, text expanders and
|
||||
// automation tools: overriding what they do is none of our business
|
||||
bool injected = (data.flags & LowLevelKeyHookFlagInjected) != 0;
|
||||
|
||||
if ((isKeyDown || isKeyUp) && !injected && _filter((int)data.vkCode, isKeyDown))
|
||||
{
|
||||
// A non-zero result instead of CallNextHookEx breaks the chain: the event
|
||||
// will reach neither the application nor the Windows caps-lock handler
|
||||
return 1;
|
||||
}
|
||||
|
||||
return CallNextHookEx(_handle, code, wParam, lParam);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct KeyboardHookData
|
||||
{
|
||||
public uint vkCode;
|
||||
public uint scanCode;
|
||||
public uint flags;
|
||||
public uint time;
|
||||
public IntPtr dwExtraInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace CursorLang.Core.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// Answers whether the application runs from an MSIX package.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The same application runs both installed from the Store and simply unpacked
|
||||
/// into a folder. Some Windows features — startup through <c>StartupTask</c>, for
|
||||
/// instance — are available only to a package, and reaching for them without a
|
||||
/// check is not allowed: outside a package they throw.
|
||||
/// </remarks>
|
||||
internal static class PackageIdentityNative
|
||||
{
|
||||
/// <summary>APPMODEL_ERROR_NO_PACKAGE — the process runs outside a package.</summary>
|
||||
private const int NoPackage = 15700;
|
||||
|
||||
/// <summary>
|
||||
/// The application runs from an MSIX package.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The value is computed once: it cannot change during the lifetime of
|
||||
/// the process.
|
||||
/// </remarks>
|
||||
internal static bool IsPackaged { get; } = DetectPackage();
|
||||
|
||||
private static bool DetectPackage()
|
||||
{
|
||||
// The answer comes from the return code rather than from the name itself, so
|
||||
// no buffer is needed: with zero length a package replies complaining about space
|
||||
uint length = 0;
|
||||
return GetCurrentPackageFullName(ref length, IntPtr.Zero) != NoPackage;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern int GetCurrentPackageFullName(ref uint packageFullNameLength, IntPtr packageFullName);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace CursorLang.Core.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// Win32 API for the popup window: styles, positioning near the cursor
|
||||
/// and the scale of the monitor the cursor is on.
|
||||
/// </summary>
|
||||
internal static class PopupWindowNative
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct Point
|
||||
{
|
||||
public int X;
|
||||
public int Y;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool GetCursorPos(out Point lpPoint);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter,
|
||||
int X, int Y, int cx, int cy, uint uFlags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr MonitorFromPoint(Point pt, uint dwFlags);
|
||||
|
||||
[DllImport("shcore.dll")]
|
||||
private static extern int GetDpiForMonitor(IntPtr hMonitor, int dpiType, out uint dpiX, out uint dpiY);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr MonitorFromWindow(IntPtr hWnd, uint dwFlags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MonitorInfo lpmi);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct Rect
|
||||
{
|
||||
public int Left;
|
||||
public int Top;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MonitorInfo
|
||||
{
|
||||
public int cbSize;
|
||||
public Rect rcMonitor;
|
||||
public Rect rcWork;
|
||||
public uint dwFlags;
|
||||
}
|
||||
|
||||
private const uint SWP_NOSIZE = 0x0001;
|
||||
private const uint SWP_NOZORDER = 0x0004;
|
||||
private const uint SWP_NOACTIVATE = 0x0010;
|
||||
|
||||
private const uint MONITOR_DEFAULTTONEAREST = 2;
|
||||
private const int MDT_EFFECTIVE_DPI = 0;
|
||||
|
||||
internal static Point GetCursorPosition()
|
||||
{
|
||||
GetCursorPos(out Point cursor);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the window to a screen point without changing its size or z-order.
|
||||
/// The coordinates are physical pixels: monitors have different scaling, while
|
||||
/// Window.Left/Top are converted using the DPI of the monitor the window is on
|
||||
/// right now, which misses the target on a neighbouring monitor.
|
||||
/// </summary>
|
||||
internal static void MoveTo(IntPtr hWnd, int x, int y)
|
||||
{
|
||||
SetWindowPos(hWnd, IntPtr.Zero, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
|
||||
/// <summary>The scale of the monitor the point is on (1.0 at 96 DPI).</summary>
|
||||
internal static double GetScaleAt(Point point) =>
|
||||
GetScaleOf(MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST));
|
||||
|
||||
/// <summary>
|
||||
/// The work area of the monitor holding the active window — without the taskbar —
|
||||
/// and its scale. That is the monitor the user is working on right now.
|
||||
/// </summary>
|
||||
internal static (Rect WorkArea, double Scale) GetActiveMonitorWorkArea()
|
||||
{
|
||||
IntPtr monitor = MonitorFromWindow(GetForegroundWindow(), MONITOR_DEFAULTTONEAREST);
|
||||
|
||||
var info = new MonitorInfo { cbSize = Marshal.SizeOf<MonitorInfo>() };
|
||||
if (!GetMonitorInfo(monitor, ref info))
|
||||
{
|
||||
return (new Rect(), 1.0);
|
||||
}
|
||||
|
||||
return (info.rcWork, GetScaleOf(monitor));
|
||||
}
|
||||
|
||||
private static double GetScaleOf(IntPtr monitor)
|
||||
{
|
||||
if (GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, out uint dpiX, out _) < 0)
|
||||
{
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
return dpiX / 96.0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user