fixed bug - careet in notepad and menu start OS
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 привязана к потоку,
|
||||||
/// поэтому так её видно у любого приложения, а не только у своего.
|
/// поэтому так её видно у любого приложения, а не только у своего.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user