Files
cursor-lang/CursorLang/Views/LayoutPopupWindow.xaml.cs
T

148 lines
5.9 KiB
C#

using System.Windows;
using System.Windows.Interop;
using CursorLang.Interop;
using CursorLang.Models;
using CursorLang.ViewModels;
namespace CursorLang.Views;
/// <summary>
/// Всплывающая подсказка с коротким именем раскладки.
/// Отвечает только за показ, размер и место на экране: когда её убрать,
/// решает <see cref="Services.LayoutPopupService"/>.
/// </summary>
public partial class LayoutPopupWindow : Window
{
private readonly AppSettings _settings;
private Size _contentSize;
public LayoutPopupWindow(LayoutPopupViewModel viewModel, AppSettings settings)
{
InitializeComponent();
DataContext = viewModel;
_settings = settings;
// Создаём окно заранее, чтобы задать его границы до первого показа:
// иначе оно на мгновение появляется в размере по умолчанию
new WindowInteropHelper(this).EnsureHandle();
}
/// <summary>
/// Показывает подсказку в месте, заданном настройками.
/// </summary>
public void ShowPopup()
{
MeasureContent();
// Уже созданное окно ставим на место до показа, чтобы оно не мелькнуло
// на старой позиции; при самом первом вызове хэндла ещё нет
ApplyBounds();
Show();
// Повторяем после показа: до него окно подчиняется системному минимальному
// размеру окна и получается заметно крупнее текста
ApplyBounds();
}
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
PopupWindowNative.MakePassive(new WindowInteropHelper(this).Handle);
}
// Размер содержимого нужен до показа: от него зависит и размер окна,
// и позиция для углов. Рамок у окна нет, поэтому размер окна равен размеру
// содержимого
private void MeasureContent()
{
var content = (FrameworkElement)Content;
content.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
_contentSize = content.DesiredSize;
}
// Всё считается в физических пикселях: у мониторов разный масштаб, а
// Window.Left/Top/Width/Height пересчитываются по DPI того монитора,
// где окно сейчас, и на соседнем мониторе дают промах
private void ApplyBounds()
{
IntPtr handle = new WindowInteropHelper(this).Handle;
if (handle == IntPtr.Zero)
{
return;
}
if (_settings.PlacementMode == PopupPlacementMode.FixedPoint)
{
ApplyBoundsOnScreen(handle);
}
else
{
ApplyBoundsNearAnchor(handle, GetAnchor());
}
}
// Точка привязки: каретка в поле ввода либо курсор мыши. Курсор — это
// прямоугольник нулевого размера, поэтому расчёт углов у них общий
private PopupWindowNative.Rect GetAnchor()
{
if (_settings.PlacementMode == PopupPlacementMode.AtCaret &&
CaretNative.TryGetCaretRect() is { } caret)
{
return caret;
}
PopupWindowNative.Point cursor = PopupWindowNative.GetCursorPosition();
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 height = ToPixels(_contentSize.Height, scale);
int offset = ToPixels(_settings.CursorOffset, scale);
bool toRight = _settings.CursorCorner is CursorCorner.BottomRight or CursorCorner.TopRight;
bool toBottom = _settings.CursorCorner is CursorCorner.BottomRight or CursorCorner.BottomLeft;
int x = toRight ? anchor.Right + offset : anchor.Left - offset - width;
int y = toBottom ? anchor.Bottom + offset : anchor.Top - offset - height;
PopupWindowNative.SetBounds(handle, x, y, width, height);
}
private void ApplyBoundsOnScreen(IntPtr handle)
{
(PopupWindowNative.Rect work, double scale) = PopupWindowNative.GetActiveMonitorWorkArea();
int width = ToPixels(_contentSize.Width, scale);
int height = ToPixels(_contentSize.Height, scale);
int margin = ToPixels(_settings.ScreenMargin, scale);
(int x, int y) = _settings.ScreenPosition switch
{
ScreenPosition.TopLeft => (work.Left + margin, work.Top + margin),
ScreenPosition.TopRight => (work.Right - margin - width, work.Top + margin),
ScreenPosition.BottomLeft => (work.Left + margin, work.Bottom - margin - height),
ScreenPosition.BottomRight => (work.Right - margin - width, work.Bottom - margin - height),
_ => (work.Left + ((work.Right - work.Left - width) / 2),
work.Top + ((work.Bottom - work.Top - height) / 2)),
};
PopupWindowNative.SetBounds(handle, x, y, width, height);
}
private static int ToPixels(double wpfUnits, double scale) => (int)Math.Round(wpfUnits * scale);
}