fixed bug - careet in notepad and menu start OS

This commit is contained in:
2026-08-08 23:00:37 +05:00
parent ba0748920b
commit 5ecc46f7a3
4 changed files with 113 additions and 33 deletions
+48 -28
View File
@@ -15,29 +15,6 @@ namespace CursorLang.Interop;
/// </remarks> /// </remarks>
internal static class CaretNative internal static class CaretNative
{ {
[StructLayout(LayoutKind.Sequential)]
private 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);
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr lpdwProcessId);
[DllImport("user32.dll")] [DllImport("user32.dll")]
private static extern bool ClientToScreen(IntPtr hWnd, ref PopupWindowNative.Point lpPoint); private static extern bool ClientToScreen(IntPtr hWnd, ref PopupWindowNative.Point lpPoint);
@@ -45,6 +22,12 @@ internal static class CaretNative
private static extern int AccessibleObjectFromWindow(IntPtr hwnd, uint dwObjectId, private static extern int AccessibleObjectFromWindow(IntPtr hwnd, uint dwObjectId,
ref Guid riid, out IAccessible ppvObject); 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 uint OBJID_CARET = 0xFFFFFFF8;
private const int CHILDID_SELF = 0; private const int CHILDID_SELF = 0;
@@ -54,18 +37,55 @@ internal static class CaretNative
/// </summary> /// </summary>
internal static PopupWindowNative.Rect? TryGetCaretRect() internal static PopupWindowNative.Rect? TryGetCaretRect()
{ {
var info = new GuiThreadInfo { cbSize = Marshal.SizeOf<GuiThreadInfo>() }; if (!ForegroundInputNative.TryGetInfo(out ForegroundInputNative.GuiThreadInfo info))
uint threadId = GetWindowThreadProcessId(GetForegroundWindow(), IntPtr.Zero);
if (!GetGUIThreadInfo(threadId, ref info))
{ {
return null; return null;
} }
return TryGetSystemCaret(info) PopupWindowNative.Rect? caret = TryGetSystemCaret(info)
?? TryGetAccessibleCaret(info.hwndFocus) ?? TryGetAccessibleCaret(info.hwndFocus)
?? TryGetAutomationCaret(); ?? TryGetAutomationCaret();
return caret is null ? null : Validate(caret.Value, info.hwndFocus);
} }
/// <summary>
/// Отсеивает заведомо неверные координаты.
/// </summary>
/// <remarks>
/// Часть приложений отдаёт положение каретки в своей системе координат либо
/// без учёта масштаба экрана, и подсказка уезжает далеко от поля ввода.
/// Каретка обязана находиться внутри окна ввода — это и проверяем, а перед
/// отказом пробуем истолковать координаты как немасштабированные.
/// </remarks>
private static PopupWindowNative.Rect? Validate(PopupWindowNative.Rect caret, IntPtr hwndFocus)
{
if (hwndFocus == IntPtr.Zero || !GetWindowRect(hwndFocus, out PopupWindowNative.Rect window))
{
return caret;
}
if (IsInside(caret, window))
{
return caret;
}
double scale = GetDpiForWindow(hwndFocus) / 96.0;
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;
}
private 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;
/// <summary>Сколько ждём ответа от чужого приложения по UI Automation.</summary> /// <summary>Сколько ждём ответа от чужого приложения по UI Automation.</summary>
private static readonly TimeSpan AutomationTimeout = TimeSpan.FromMilliseconds(150); private static readonly TimeSpan AutomationTimeout = TimeSpan.FromMilliseconds(150);
@@ -127,7 +147,7 @@ internal static class CaretNative
} }
// Системная каретка: координаты приходят относительно окна, которому она принадлежит // Системная каретка: координаты приходят относительно окна, которому она принадлежит
private static PopupWindowNative.Rect? TryGetSystemCaret(GuiThreadInfo info) private static PopupWindowNative.Rect? TryGetSystemCaret(ForegroundInputNative.GuiThreadInfo info)
{ {
if (info.hwndCaret == IntPtr.Zero || IsEmpty(info.rcCaret)) if (info.hwndCaret == IntPtr.Zero || IsEmpty(info.rcCaret))
{ {
@@ -0,0 +1,41 @@
using System.Runtime.InteropServices;
namespace CursorLang.Interop;
/// <summary>
/// Сведения о вводе в активном приложении: какое окно держит фокус клавиатуры
/// и где находится каретка.
/// </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>
/// Состояние ввода потока переднего плана.
/// </summary>
/// <remarks>
/// Нулевой идентификатор потока не случаен: у современных приложений окно
/// верхнего уровня и окно ввода живут в разных потоках, и спрашивать нужно
/// именно про передний план целиком.
/// </remarks>
internal static bool TryGetInfo(out GuiThreadInfo info)
{
info = new GuiThreadInfo { cbSize = Marshal.SizeOf<GuiThreadInfo>() };
return GetGUIThreadInfo(0, ref info);
}
}
+21 -1
View File
@@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
namespace CursorLang.Interop; namespace CursorLang.Interop;
/// <summary> /// <summary>
/// Win32 API для определения раскладки активного окна. /// Win32 API для определения раскладки активного приложения.
/// </summary> /// </summary>
internal static class KeyboardLayoutNative internal static class KeyboardLayoutNative
{ {
@@ -16,6 +16,26 @@ internal static class KeyboardLayoutNative
[DllImport("user32.dll")] [DllImport("user32.dll")]
private static extern IntPtr GetKeyboardLayout(uint idThread); private static extern IntPtr GetKeyboardLayout(uint idThread);
/// <summary>
/// Раскладка, которой сейчас печатает пользователь.
/// </summary>
/// <remarks>
/// Спрашиваем окно с фокусом клавиатуры, а не окно верхнего плана: у Блокнота
/// Windows 11, меню «Пуск» и прочих приложений на WinUI поле ввода живёт в
/// отдельном потоке, и раскладка меняется только у него. У потока главного
/// окна она остаётся прежней, и переключение проходит незамеченным.
/// </remarks>
internal static int GetActiveLocaleId()
{
if (ForegroundInputNative.TryGetInfo(out ForegroundInputNative.GuiThreadInfo info) &&
info.hwndFocus != IntPtr.Zero)
{
return GetLocaleIdOf(info.hwndFocus);
}
return GetLocaleIdOf(GetForegroundWindow());
}
/// <summary> /// <summary>
/// Идентификатор локали для окна. Раскладка в Windows привязана к потоку, /// Идентификатор локали для окна. Раскладка в Windows привязана к потоку,
/// поэтому так её видно у любого приложения, а не только у своего. /// поэтому так её видно у любого приложения, а не только у своего.
+3 -4
View File
@@ -37,13 +37,12 @@ public sealed class KeyboardLayoutService : IKeyboardLayoutService, IDisposable
public event EventHandler<LayoutChangedEventArgs>? LayoutChanged; public event EventHandler<LayoutChangedEventArgs>? LayoutChanged;
public KeyboardLayout Current => public KeyboardLayout Current => KeyboardLayout.FromLocaleId(KeyboardLayoutNative.GetActiveLocaleId());
KeyboardLayout.FromLocaleId(KeyboardLayoutNative.GetLocaleIdOf(KeyboardLayoutNative.GetForegroundWindow()));
public void Start() public void Start()
{ {
_lastForegroundWindow = KeyboardLayoutNative.GetForegroundWindow(); _lastForegroundWindow = KeyboardLayoutNative.GetForegroundWindow();
_lastLocaleId = KeyboardLayoutNative.GetLocaleIdOf(_lastForegroundWindow); _lastLocaleId = KeyboardLayoutNative.GetActiveLocaleId();
_pollTimer.Start(); _pollTimer.Start();
} }
@@ -66,7 +65,7 @@ public sealed class KeyboardLayoutService : IKeyboardLayoutService, IDisposable
bool appSwitched = foreground != _lastForegroundWindow; bool appSwitched = foreground != _lastForegroundWindow;
_lastForegroundWindow = foreground; _lastForegroundWindow = foreground;
int localeId = KeyboardLayoutNative.GetLocaleIdOf(foreground); int localeId = KeyboardLayoutNative.GetActiveLocaleId();
if (localeId == _lastLocaleId) if (localeId == _lastLocaleId)
{ {
return; return;