Files
cursor-lang/CursorLang/Services/MainWindowPlacement.cs
T
2026-08-09 01:55:45 +05:00

161 lines
6.5 KiB
C#

using System.Windows;
using System.Windows.Interop;
using CursorLang.Interop;
namespace CursorLang.Services;
/// <summary>
/// Решает, где появиться окну настроек: впервые за сеанс — по центру монитора,
/// на котором пользователь работает, а затем — там, где он это окно оставил.
/// </summary>
/// <remarks>
/// Положение живёт только в памяти и между запусками не сохраняется: набор
/// мониторов к следующему запуску может стать другим, а «по центру активного»
/// верно всегда.
/// </remarks>
public sealed class MainWindowPlacement
{
private PopupWindowNative.Point? _position;
// Окно сообщает о переносе и тогда, когда двигаем его мы сами;
// запоминать надо только то, что выбрал пользователь
private bool _isPlacing;
/// <summary>
/// Берёт на себя размещение окна: ставит его на место к первому показу
/// и следит за тем, куда пользователь его переносит.
/// </summary>
public void Attach(Window window)
{
window.SourceInitialized += OnSourceInitialized;
window.LocationChanged += OnLocationChanged;
}
/// <summary>
/// Возвращает окно на запомненное место, а если за этот сеанс его ещё
/// не показывали — ставит по центру активного монитора.
/// </summary>
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),
};
}
/// <summary>
/// Подтягивает окно в рабочую область ближайшего монитора.
/// </summary>
/// <remarks>
/// Нужно в двух случаях. Монитор, на который окно поставил пользователь,
/// за сеанс может быть отключён — возвращать окно на его место значило бы
/// оставить пользователя без окна, поэтому запомненное положение здесь
/// пожелание, а не приказ. А высота окна равна высоте содержимого и на
/// невысоком мониторе может рабочую область превысить — тогда у окна,
/// поставленного по центру, заголовок ушёл бы за верхний край.
/// </remarks>
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;
}