Files
cursor-lang/CursorLang/Views/LayoutPopupWindow.xaml.cs
T
2026-08-08 21:48:16 +05:00

112 lines
4.6 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;
public LayoutPopupWindow(LayoutPopupViewModel viewModel, AppSettings settings)
{
InitializeComponent();
DataContext = viewModel;
_settings = settings;
}
/// <summary>
/// Показывает подсказку в месте, заданном настройками.
/// </summary>
public void ShowPopup()
{
ResizeToContent();
// Для уже созданного окна двигаем до показа; при самом первом вызове
// хэндла ещё нет, и позиционирование выполнит OnSourceInitialized
MoveToTargetPosition();
Show();
}
// Хэндл создан, но окно ещё не отрисовано: здесь и настраиваем стили,
// и ставим окно на место, чтобы первый показ прошёл уже на нужном мониторе
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
PopupWindowNative.MakePassive(new WindowInteropHelper(this).Handle);
MoveToTargetPosition();
}
// Размер считаем сами, а не через SizeToContent: тот вычисляет его при создании
// окна, когда содержимое ещё не измерено, и первый показ выходит шире текста.
// Рамок у окна нет, поэтому его размер равен размеру содержимого
private void ResizeToContent()
{
var content = (FrameworkElement)Content;
content.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Width = content.DesiredSize.Width;
Height = content.DesiredSize.Height;
}
private void MoveToTargetPosition()
{
IntPtr handle = new WindowInteropHelper(this).Handle;
if (handle == IntPtr.Zero)
{
return;
}
(int x, int y) = _settings.PlacementMode == PopupPlacementMode.AtCursor
? GetCursorPosition()
: GetScreenPosition();
PopupWindowNative.MoveTo(handle, x, y);
}
// Отступы и размеры заданы в единицах WPF, а окно двигаем в пикселях,
// поэтому всё пересчитывается по масштабу нужного монитора
private (int X, int Y) GetCursorPosition()
{
PopupWindowNative.Point cursor = PopupWindowNative.GetCursorPosition();
double scale = PopupWindowNative.GetScaleAt(cursor);
int offset = (int)Math.Round(_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 ? cursor.X + offset : cursor.X - offset - ToPixels(Width, scale);
int y = toBottom ? cursor.Y + offset : cursor.Y - offset - ToPixels(Height, scale);
return (x, y);
}
private (int X, int Y) GetScreenPosition()
{
(PopupWindowNative.Rect work, double scale) = PopupWindowNative.GetActiveMonitorWorkArea();
int margin = (int)Math.Round(_settings.ScreenMargin * scale);
int width = ToPixels(Width, scale);
int height = ToPixels(Height, scale);
return _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)),
};
}
private static int ToPixels(double wpfUnits, double scale) => (int)Math.Round(wpfUnits * scale);
}