Compare commits
2
Commits
240ffe7687
...
5ecc46f7a3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ecc46f7a3 | ||
|
|
ba0748920b |
@@ -0,0 +1,214 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Windows.Automation;
|
||||||
|
using System.Windows.Automation.Text;
|
||||||
|
using Accessibility;
|
||||||
|
|
||||||
|
namespace CursorLang.Interop;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет положение каретки в активном поле ввода — в том числе в чужом приложении.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Единого способа нет: классические Win32-приложения заводят системную каретку,
|
||||||
|
/// а Chrome, Electron и прочие рисуют её сами и сообщают положение только через
|
||||||
|
/// средства доступности. Поэтому сначала спрашиваем систему, затем — приложение.
|
||||||
|
/// </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>
|
||||||
|
/// Прямоугольник каретки в пикселях экрана или <c>null</c>, если активное
|
||||||
|
/// приложение не сообщает её положение.
|
||||||
|
/// </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>
|
||||||
|
/// Отсеивает заведомо неверные координаты.
|
||||||
|
/// </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>
|
||||||
|
private static readonly TimeSpan AutomationTimeout = TimeSpan.FromMilliseconds(150);
|
||||||
|
|
||||||
|
// Браузеры и другие приложения на своих движках рисуют каретку сами и
|
||||||
|
// сообщают её положение только через UI Automation. Запрос идёт в чужой
|
||||||
|
// процесс, поэтому он самый медленный и стоит последним
|
||||||
|
private static PopupWindowNative.Rect? TryGetAutomationCaret()
|
||||||
|
{
|
||||||
|
// Зависшее приложение не должно подвешивать подсказку вместе с собой:
|
||||||
|
// ждём ответ ограниченное время, иначе показываем у курсора
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// У каретки выделение пустое и прямоугольника не имеет,
|
||||||
|
// поэтому расширяем его до ближайшего символа
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
// Приложение закрылось или не отвечает — подсказку это ронять не должно
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Системная каретка: координаты приходят относительно окна, которому она принадлежит
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Каретка через средства доступности: сюда попадают браузеры и Electron
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
// Приложение объявило поддержку, но положение не отдало
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Marshal.ReleaseComObject(caret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Когда каретки нет, её прямоугольник приходит нулевой высоты.
|
||||||
|
// Судим только по высоте: нулевые координаты — это обычное начало пустого поля
|
||||||
|
private static bool IsEmpty(PopupWindowNative.Rect rect) => rect.Bottom - rect.Top <= 0;
|
||||||
|
}
|
||||||
@@ -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 привязана к потоку,
|
||||||
/// поэтому так её видно у любого приложения, а не только у своего.
|
/// поэтому так её видно у любого приложения, а не только у своего.
|
||||||
|
|||||||
@@ -8,13 +8,20 @@ public enum PopupPlacementMode
|
|||||||
/// <summary>Рядом с курсором мыши.</summary>
|
/// <summary>Рядом с курсором мыши.</summary>
|
||||||
AtCursor,
|
AtCursor,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Рядом с кареткой в активном поле ввода. Если приложение не сообщает
|
||||||
|
/// её положение, подсказка показывается у курсора мыши.
|
||||||
|
/// </summary>
|
||||||
|
AtCaret,
|
||||||
|
|
||||||
/// <summary>В заданной точке монитора с активным окном.</summary>
|
/// <summary>В заданной точке монитора с активным окном.</summary>
|
||||||
FixedPoint,
|
FixedPoint,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// С какой стороны от курсора показывать подсказку.
|
/// С какой стороны от курсора или каретки показывать подсказку.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
||||||
public enum CursorCorner
|
public enum CursorCorner
|
||||||
{
|
{
|
||||||
BottomRight,
|
BottomRight,
|
||||||
|
|||||||
@@ -76,11 +76,14 @@
|
|||||||
<data name="PopupPlacementMode_AtCursor" xml:space="preserve">
|
<data name="PopupPlacementMode_AtCursor" xml:space="preserve">
|
||||||
<value>Near the cursor</value>
|
<value>Near the cursor</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="PopupPlacementMode_AtCaret" xml:space="preserve">
|
||||||
|
<value>Near the text caret</value>
|
||||||
|
</data>
|
||||||
<data name="PopupPlacementMode_FixedPoint" xml:space="preserve">
|
<data name="PopupPlacementMode_FixedPoint" xml:space="preserve">
|
||||||
<value>Fixed point on screen</value>
|
<value>Fixed point on screen</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CursorCornerLabel" xml:space="preserve">
|
<data name="CursorCornerLabel" xml:space="preserve">
|
||||||
<value>Side of the cursor</value>
|
<value>Side</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CursorCorner_BottomRight" xml:space="preserve">
|
<data name="CursorCorner_BottomRight" xml:space="preserve">
|
||||||
<value>Bottom right</value>
|
<value>Bottom right</value>
|
||||||
@@ -95,7 +98,7 @@
|
|||||||
<value>Top left</value>
|
<value>Top left</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CursorOffsetLabel" xml:space="preserve">
|
<data name="CursorOffsetLabel" xml:space="preserve">
|
||||||
<value>Offset from cursor</value>
|
<value>Offset</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ScreenPositionLabel" xml:space="preserve">
|
<data name="ScreenPositionLabel" xml:space="preserve">
|
||||||
<value>Position on screen</value>
|
<value>Position on screen</value>
|
||||||
|
|||||||
@@ -76,11 +76,14 @@
|
|||||||
<data name="PopupPlacementMode_AtCursor" xml:space="preserve">
|
<data name="PopupPlacementMode_AtCursor" xml:space="preserve">
|
||||||
<value>Рядом с курсором</value>
|
<value>Рядом с курсором</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="PopupPlacementMode_AtCaret" xml:space="preserve">
|
||||||
|
<value>Рядом с кареткой ввода</value>
|
||||||
|
</data>
|
||||||
<data name="PopupPlacementMode_FixedPoint" xml:space="preserve">
|
<data name="PopupPlacementMode_FixedPoint" xml:space="preserve">
|
||||||
<value>В заданной точке экрана</value>
|
<value>В заданной точке экрана</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CursorCornerLabel" xml:space="preserve">
|
<data name="CursorCornerLabel" xml:space="preserve">
|
||||||
<value>Сторона от курсора</value>
|
<value>Сторона</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CursorCorner_BottomRight" xml:space="preserve">
|
<data name="CursorCorner_BottomRight" xml:space="preserve">
|
||||||
<value>Справа снизу</value>
|
<value>Справа снизу</value>
|
||||||
@@ -95,7 +98,7 @@
|
|||||||
<value>Слева сверху</value>
|
<value>Слева сверху</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CursorOffsetLabel" xml:space="preserve">
|
<data name="CursorOffsetLabel" xml:space="preserve">
|
||||||
<value>Отступ от курсора</value>
|
<value>Отступ</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ScreenPositionLabel" xml:space="preserve">
|
<data name="ScreenPositionLabel" xml:space="preserve">
|
||||||
<value>Позиция на экране</value>
|
<value>Позиция на экране</value>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -6,13 +6,21 @@ using System.Windows.Media;
|
|||||||
namespace CursorLang.Views;
|
namespace CursorLang.Views;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Показывает элемент, только если значение совпадает с параметром.
|
/// Показывает элемент, если значение совпадает с одним из перечисленных
|
||||||
/// Нужен, чтобы настройки курсора и экрана не показывались одновременно.
|
/// в параметре через запятую. Нужен, чтобы настройки точки привязки
|
||||||
|
/// и настройки экрана не показывались одновременно.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class EnumToVisibilityConverter : IValueConverter
|
public sealed class EnumToVisibilityConverter : IValueConverter
|
||||||
{
|
{
|
||||||
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>
|
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
value?.ToString() == parameter?.ToString() ? Visibility.Visible : Visibility.Collapsed;
|
{
|
||||||
|
string? current = value?.ToString();
|
||||||
|
bool matches = parameter?.ToString()?
|
||||||
|
.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Any(expected => expected == current) ?? false;
|
||||||
|
|
||||||
|
return matches ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
|
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
|
||||||
Binding.DoNothing;
|
Binding.DoNothing;
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ public partial class LayoutPopupWindow : Window
|
|||||||
|
|
||||||
DataContext = viewModel;
|
DataContext = viewModel;
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
|
|
||||||
|
// Создаём окно заранее, чтобы задать его границы до первого показа:
|
||||||
|
// иначе оно на мгновение появляется в размере по умолчанию
|
||||||
|
new WindowInteropHelper(this).EnsureHandle();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -70,20 +74,40 @@ public partial class LayoutPopupWindow : Window
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_settings.PlacementMode == PopupPlacementMode.AtCursor)
|
if (_settings.PlacementMode == PopupPlacementMode.FixedPoint)
|
||||||
{
|
|
||||||
ApplyBoundsAtCursor(handle);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
ApplyBoundsOnScreen(handle);
|
ApplyBoundsOnScreen(handle);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ApplyBoundsNearAnchor(handle, GetAnchor());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyBoundsAtCursor(IntPtr handle)
|
// Точка привязки: каретка в поле ввода либо курсор мыши. Курсор — это
|
||||||
|
// прямоугольник нулевого размера, поэтому расчёт углов у них общий
|
||||||
|
private PopupWindowNative.Rect GetAnchor()
|
||||||
{
|
{
|
||||||
|
if (_settings.PlacementMode == PopupPlacementMode.AtCaret &&
|
||||||
|
CaretNative.TryGetCaretRect() is { } caret)
|
||||||
|
{
|
||||||
|
return caret;
|
||||||
|
}
|
||||||
|
|
||||||
PopupWindowNative.Point cursor = PopupWindowNative.GetCursorPosition();
|
PopupWindowNative.Point cursor = PopupWindowNative.GetCursorPosition();
|
||||||
double scale = PopupWindowNative.GetScaleAt(cursor);
|
return new PopupWindowNative.Rect
|
||||||
|
{
|
||||||
|
Left = cursor.X,
|
||||||
|
Top = cursor.Y,
|
||||||
|
Right = cursor.X,
|
||||||
|
Bottom = cursor.Y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyBoundsNearAnchor(IntPtr handle, PopupWindowNative.Rect anchor)
|
||||||
|
{
|
||||||
|
var anchorPoint = new PopupWindowNative.Point { X = anchor.Left, Y = anchor.Top };
|
||||||
|
double scale = PopupWindowNative.GetScaleAt(anchorPoint);
|
||||||
|
|
||||||
int width = ToPixels(_contentSize.Width, scale);
|
int width = ToPixels(_contentSize.Width, scale);
|
||||||
int height = ToPixels(_contentSize.Height, scale);
|
int height = ToPixels(_contentSize.Height, scale);
|
||||||
@@ -92,8 +116,8 @@ public partial class LayoutPopupWindow : Window
|
|||||||
bool toRight = _settings.CursorCorner is CursorCorner.BottomRight or CursorCorner.TopRight;
|
bool toRight = _settings.CursorCorner is CursorCorner.BottomRight or CursorCorner.TopRight;
|
||||||
bool toBottom = _settings.CursorCorner is CursorCorner.BottomRight or CursorCorner.BottomLeft;
|
bool toBottom = _settings.CursorCorner is CursorCorner.BottomRight or CursorCorner.BottomLeft;
|
||||||
|
|
||||||
int x = toRight ? cursor.X + offset : cursor.X - offset - width;
|
int x = toRight ? anchor.Right + offset : anchor.Left - offset - width;
|
||||||
int y = toBottom ? cursor.Y + offset : cursor.Y - offset - height;
|
int y = toBottom ? anchor.Bottom + offset : anchor.Top - offset - height;
|
||||||
|
|
||||||
PopupWindowNative.SetBounds(handle, x, y, width, height);
|
PopupWindowNative.SetBounds(handle, x, y, width, height);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:local="clr-namespace:CursorLang.Views"
|
xmlns:local="clr-namespace:CursorLang.Views"
|
||||||
xmlns:models="clr-namespace:CursorLang.Models"
|
|
||||||
xmlns:vm="clr-namespace:CursorLang.ViewModels"
|
xmlns:vm="clr-namespace:CursorLang.ViewModels"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
d:DataContext="{d:DesignInstance Type=vm:SettingsViewModel}"
|
d:DataContext="{d:DesignInstance Type=vm:SettingsViewModel}"
|
||||||
@@ -82,13 +81,13 @@
|
|||||||
SelectedValuePath="Value"
|
SelectedValuePath="Value"
|
||||||
SelectedValue="{Binding Settings.PlacementMode}" />
|
SelectedValue="{Binding Settings.PlacementMode}" />
|
||||||
|
|
||||||
<!-- Режим «у курсора» -->
|
<!-- Привязка к точке: курсор или каретка -->
|
||||||
<TextBlock Grid.Row="1" Margin="0,12,12,0"
|
<TextBlock Grid.Row="1" Margin="0,12,12,0"
|
||||||
Style="{StaticResource FieldLabel}"
|
Style="{StaticResource FieldLabel}"
|
||||||
Text="{Binding Localization[CursorCornerLabel]}"
|
Text="{Binding Localization[CursorCornerLabel]}"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Converter={StaticResource EnumToVisibility},
|
Converter={StaticResource EnumToVisibility},
|
||||||
ConverterParameter={x:Static models:PopupPlacementMode.AtCursor}}" />
|
ConverterParameter='AtCursor,AtCaret'}" />
|
||||||
<ComboBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
<ComboBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||||
ItemsSource="{Binding CursorCorners}"
|
ItemsSource="{Binding CursorCorners}"
|
||||||
DisplayMemberPath="Display"
|
DisplayMemberPath="Display"
|
||||||
@@ -96,26 +95,26 @@
|
|||||||
SelectedValue="{Binding Settings.CursorCorner}"
|
SelectedValue="{Binding Settings.CursorCorner}"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Converter={StaticResource EnumToVisibility},
|
Converter={StaticResource EnumToVisibility},
|
||||||
ConverterParameter={x:Static models:PopupPlacementMode.AtCursor}}" />
|
ConverterParameter='AtCursor,AtCaret'}" />
|
||||||
|
|
||||||
<TextBlock Grid.Row="2" Margin="0,12,12,0"
|
<TextBlock Grid.Row="2" Margin="0,12,12,0"
|
||||||
Style="{StaticResource FieldLabel}"
|
Style="{StaticResource FieldLabel}"
|
||||||
Text="{Binding Localization[CursorOffsetLabel]}"
|
Text="{Binding Localization[CursorOffsetLabel]}"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Converter={StaticResource EnumToVisibility},
|
Converter={StaticResource EnumToVisibility},
|
||||||
ConverterParameter={x:Static models:PopupPlacementMode.AtCursor}}" />
|
ConverterParameter='AtCursor,AtCaret'}" />
|
||||||
<Slider Grid.Row="2" Grid.Column="1" Margin="0,12,0,0"
|
<Slider Grid.Row="2" Grid.Column="1" Margin="0,12,0,0"
|
||||||
Minimum="0" Maximum="80" TickFrequency="1"
|
Minimum="0" Maximum="80" TickFrequency="1"
|
||||||
Value="{Binding Settings.CursorOffset}"
|
Value="{Binding Settings.CursorOffset}"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Converter={StaticResource EnumToVisibility},
|
Converter={StaticResource EnumToVisibility},
|
||||||
ConverterParameter={x:Static models:PopupPlacementMode.AtCursor}}" />
|
ConverterParameter='AtCursor,AtCaret'}" />
|
||||||
<TextBlock Grid.Row="2" Grid.Column="2" Margin="12,12,0,0"
|
<TextBlock Grid.Row="2" Grid.Column="2" Margin="12,12,0,0"
|
||||||
Style="{StaticResource FieldValue}"
|
Style="{StaticResource FieldValue}"
|
||||||
Text="{Binding Settings.CursorOffset, StringFormat={}{0:F0}}"
|
Text="{Binding Settings.CursorOffset, StringFormat={}{0:F0}}"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Converter={StaticResource EnumToVisibility},
|
Converter={StaticResource EnumToVisibility},
|
||||||
ConverterParameter={x:Static models:PopupPlacementMode.AtCursor}}" />
|
ConverterParameter='AtCursor,AtCaret'}" />
|
||||||
|
|
||||||
<!-- Режим «фиксированная точка» -->
|
<!-- Режим «фиксированная точка» -->
|
||||||
<TextBlock Grid.Row="3" Margin="0,12,12,0"
|
<TextBlock Grid.Row="3" Margin="0,12,12,0"
|
||||||
@@ -123,7 +122,7 @@
|
|||||||
Text="{Binding Localization[ScreenPositionLabel]}"
|
Text="{Binding Localization[ScreenPositionLabel]}"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Converter={StaticResource EnumToVisibility},
|
Converter={StaticResource EnumToVisibility},
|
||||||
ConverterParameter={x:Static models:PopupPlacementMode.FixedPoint}}" />
|
ConverterParameter=FixedPoint}" />
|
||||||
<ComboBox Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
<ComboBox Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,12,0,0"
|
||||||
ItemsSource="{Binding ScreenPositions}"
|
ItemsSource="{Binding ScreenPositions}"
|
||||||
DisplayMemberPath="Display"
|
DisplayMemberPath="Display"
|
||||||
@@ -131,26 +130,26 @@
|
|||||||
SelectedValue="{Binding Settings.ScreenPosition}"
|
SelectedValue="{Binding Settings.ScreenPosition}"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Converter={StaticResource EnumToVisibility},
|
Converter={StaticResource EnumToVisibility},
|
||||||
ConverterParameter={x:Static models:PopupPlacementMode.FixedPoint}}" />
|
ConverterParameter=FixedPoint}" />
|
||||||
|
|
||||||
<TextBlock Grid.Row="4" Margin="0,12,12,0"
|
<TextBlock Grid.Row="4" Margin="0,12,12,0"
|
||||||
Style="{StaticResource FieldLabel}"
|
Style="{StaticResource FieldLabel}"
|
||||||
Text="{Binding Localization[ScreenMarginLabel]}"
|
Text="{Binding Localization[ScreenMarginLabel]}"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Converter={StaticResource EnumToVisibility},
|
Converter={StaticResource EnumToVisibility},
|
||||||
ConverterParameter={x:Static models:PopupPlacementMode.FixedPoint}}" />
|
ConverterParameter=FixedPoint}" />
|
||||||
<Slider Grid.Row="4" Grid.Column="1" Margin="0,12,0,0"
|
<Slider Grid.Row="4" Grid.Column="1" Margin="0,12,0,0"
|
||||||
Minimum="0" Maximum="200" TickFrequency="1"
|
Minimum="0" Maximum="200" TickFrequency="1"
|
||||||
Value="{Binding Settings.ScreenMargin}"
|
Value="{Binding Settings.ScreenMargin}"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Converter={StaticResource EnumToVisibility},
|
Converter={StaticResource EnumToVisibility},
|
||||||
ConverterParameter={x:Static models:PopupPlacementMode.FixedPoint}}" />
|
ConverterParameter=FixedPoint}" />
|
||||||
<TextBlock Grid.Row="4" Grid.Column="2" Margin="12,12,0,0"
|
<TextBlock Grid.Row="4" Grid.Column="2" Margin="12,12,0,0"
|
||||||
Style="{StaticResource FieldValue}"
|
Style="{StaticResource FieldValue}"
|
||||||
Text="{Binding Settings.ScreenMargin, StringFormat={}{0:F0}}"
|
Text="{Binding Settings.ScreenMargin, StringFormat={}{0:F0}}"
|
||||||
Visibility="{Binding Settings.PlacementMode,
|
Visibility="{Binding Settings.PlacementMode,
|
||||||
Converter={StaticResource EnumToVisibility},
|
Converter={StaticResource EnumToVisibility},
|
||||||
ConverterParameter={x:Static models:PopupPlacementMode.FixedPoint}}" />
|
ConverterParameter=FixedPoint}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user