using System.Windows; using System.Windows.Interop; using CursorLang.Interop; namespace CursorLang.Services; /// /// Решает, где появиться окну настроек: впервые за сеанс — по центру монитора, /// на котором пользователь работает, а затем — там, где он это окно оставил. /// /// /// Положение живёт только в памяти и между запусками не сохраняется: набор /// мониторов к следующему запуску может стать другим, а «по центру активного» /// верно всегда. /// public sealed class MainWindowPlacement { private PopupWindowNative.Point? _position; // Окно сообщает о переносе и тогда, когда двигаем его мы сами; // запоминать надо только то, что выбрал пользователь private bool _isPlacing; /// /// Берёт на себя размещение окна: ставит его на место к первому показу /// и следит за тем, куда пользователь его переносит. /// public void Attach(Window window) { window.SourceInitialized += OnSourceInitialized; window.LocationChanged += OnLocationChanged; } /// /// Возвращает окно на запомненное место, а если за этот сеанс его ещё /// не показывали — ставит по центру активного монитора. /// public void Apply(Window window) { // У свёрнутого окна нет осмысленных границ: его разворачивают на прежнем // месте, и разместить его можно только после этого if (window.WindowState != WindowState.Normal) { return; } IntPtr handle = new WindowInteropHelper(window).Handle; if (handle == IntPtr.Zero || WindowPlacementNative.TryGetBounds(handle) is not { } bounds) { return; } PopupWindowNative.Point? wanted = _position ?? CenterOnActiveMonitor(bounds); if (wanted is null || KeepOnScreen(wanted.Value, bounds) is not { } target) { return; } _isPlacing = true; try { PopupWindowNative.MoveTo(handle, target.X, target.Y); } finally { _isPlacing = false; } _position = target; } // Высота окна подстраивается под содержимое, и до первой раскладки она // неизвестна — по центру встал бы пустой каркас окна. Раскладку поэтому // просим посчитать сразу: окно к этому моменту ещё не показано, так что // на прежнем месте оно не мелькнёт private void OnSourceInitialized(object? sender, EventArgs e) { if (sender is not Window window) { return; } window.SourceInitialized -= OnSourceInitialized; window.UpdateLayout(); Apply(window); } private void OnLocationChanged(object? sender, EventArgs e) { if (_isPlacing || sender is not Window { WindowState: WindowState.Normal } window) { return; } IntPtr handle = new WindowInteropHelper(window).Handle; if (handle != IntPtr.Zero && WindowPlacementNative.TryGetBounds(handle) is { } bounds) { _position = new PopupWindowNative.Point { X = bounds.Left, Y = bounds.Top }; } } private static PopupWindowNative.Point? CenterOnActiveMonitor(PopupWindowNative.Rect bounds) { (PopupWindowNative.Rect work, _) = PopupWindowNative.GetActiveMonitorWorkArea(); if (IsEmpty(work)) { return null; } return new PopupWindowNative.Point { X = work.Left + (((work.Right - work.Left) - Width(bounds)) / 2), Y = work.Top + (((work.Bottom - work.Top) - Height(bounds)) / 2), }; } /// /// Подтягивает окно в рабочую область ближайшего монитора. /// /// /// Нужно в двух случаях. Монитор, на который окно поставил пользователь, /// за сеанс может быть отключён — возвращать окно на его место значило бы /// оставить пользователя без окна, поэтому запомненное положение здесь /// пожелание, а не приказ. А высота окна равна высоте содержимого и на /// невысоком мониторе может рабочую область превысить — тогда у окна, /// поставленного по центру, заголовок ушёл бы за верхний край. /// private static PopupWindowNative.Point? KeepOnScreen( PopupWindowNative.Point position, PopupWindowNative.Rect bounds) { int width = Width(bounds); int height = Height(bounds); var wanted = new PopupWindowNative.Rect { Left = position.X, Top = position.Y, Right = position.X + width, Bottom = position.Y + height, }; if (WindowPlacementNative.TryGetWorkAreaNear(wanted) is not { } work || IsEmpty(work)) { return null; } return new PopupWindowNative.Point { X = Math.Clamp(position.X, work.Left, Math.Max(work.Left, work.Right - width)), Y = Math.Clamp(position.Y, work.Top, Math.Max(work.Top, work.Bottom - height)), }; } private static int Width(PopupWindowNative.Rect rect) => rect.Right - rect.Left; private static int Height(PopupWindowNative.Rect rect) => rect.Bottom - rect.Top; private static bool IsEmpty(PopupWindowNative.Rect rect) => rect.Right <= rect.Left || rect.Bottom <= rect.Top; }