133 lines
4.7 KiB
C#
133 lines
4.7 KiB
C#
using System.Windows;
|
|
using System.Windows.Interop;
|
|
using CursorLang.Interop;
|
|
using CursorLang.Models;
|
|
using CursorLang.Services;
|
|
using CursorLang.ViewModels;
|
|
|
|
namespace CursorLang.Views;
|
|
|
|
/// <summary>
|
|
/// The popup with the short name of the layout.
|
|
/// Responsible only for showing it, its size and its place on screen: when to take it
|
|
/// down is decided by <see cref="Services.LayoutPopupService"/>.
|
|
/// </summary>
|
|
public partial class LayoutPopupWindow : Window, ILayoutPopupWindow
|
|
{
|
|
private readonly AppSettings _settings;
|
|
private Size _contentSize;
|
|
|
|
public LayoutPopupWindow(LayoutPopupViewModel viewModel, AppSettings settings)
|
|
{
|
|
InitializeComponent();
|
|
|
|
DataContext = viewModel;
|
|
_settings = settings;
|
|
|
|
// We create the window in advance to set its bounds before the first show:
|
|
// otherwise it appears at the default size for a moment
|
|
new WindowInteropHelper(this).EnsureHandle();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shows the popup at the place set by the settings.
|
|
/// </summary>
|
|
public void ShowPopup()
|
|
{
|
|
MeasureContent();
|
|
|
|
// An already created window is put in place before the show so that it does
|
|
// not flash in its old position; on the very first call there is no handle yet
|
|
ApplyBounds();
|
|
Show();
|
|
|
|
// We repeat it after the show: before that the window is subject to the system
|
|
// minimum window size and comes out noticeably larger than its text
|
|
ApplyBounds();
|
|
}
|
|
|
|
protected override void OnSourceInitialized(EventArgs e)
|
|
{
|
|
base.OnSourceInitialized(e);
|
|
|
|
PopupWindowNative.MakePassive(new WindowInteropHelper(this).Handle);
|
|
}
|
|
|
|
// The content size is needed before the show: both the window size and the corner
|
|
// position depend on it. The window has no frame, so the window size equals the
|
|
// content size
|
|
private void MeasureContent()
|
|
{
|
|
var content = (FrameworkElement)Content;
|
|
content.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
|
|
|
_contentSize = content.DesiredSize;
|
|
}
|
|
|
|
// Everything is computed in physical pixels: monitors have different scaling,
|
|
// while Window.Left/Top/Width/Height are converted using the DPI of the monitor
|
|
// the window is on right now, which misses the target on a neighbouring monitor
|
|
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());
|
|
}
|
|
}
|
|
|
|
// The anchor point: the caret in the input field or the mouse cursor. The cursor
|
|
// is a rectangle of zero size, so the corner computation is shared by both
|
|
private PopupWindowNative.Rect GetAnchor()
|
|
{
|
|
if (_settings.PlacementMode == PopupPlacementMode.AtCaret &&
|
|
CaretNative.TryGetCaretRect() is { } caret)
|
|
{
|
|
return caret;
|
|
}
|
|
|
|
return PopupLayout.AsAnchor(PopupWindowNative.GetCursorPosition());
|
|
}
|
|
|
|
private void ApplyBoundsNearAnchor(IntPtr handle, PopupWindowNative.Rect anchor)
|
|
{
|
|
var anchorPoint = new PopupWindowNative.Point { X = anchor.Left, Y = anchor.Top };
|
|
double scale = PopupWindowNative.GetScaleAt(anchorPoint);
|
|
|
|
// Every anchor mode has a side and an offset of its own
|
|
bool atCaret = _settings.PlacementMode == PopupPlacementMode.AtCaret;
|
|
AnchorSide side = atCaret ? _settings.CaretSide : _settings.CursorSide;
|
|
int offset = PopupLayout.ToPixels(atCaret ? _settings.CaretOffset : _settings.CursorOffset, scale);
|
|
|
|
int width = PopupLayout.ToPixels(_contentSize.Width, scale);
|
|
int height = PopupLayout.ToPixels(_contentSize.Height, scale);
|
|
|
|
PopupWindowNative.Point position = PopupLayout.NearAnchor(anchor, side, offset, width, height);
|
|
|
|
PopupWindowNative.SetBounds(handle, position.X, position.Y, width, height);
|
|
}
|
|
|
|
private void ApplyBoundsOnScreen(IntPtr handle)
|
|
{
|
|
(PopupWindowNative.Rect work, double scale) = PopupWindowNative.GetActiveMonitorWorkArea();
|
|
|
|
int width = PopupLayout.ToPixels(_contentSize.Width, scale);
|
|
int height = PopupLayout.ToPixels(_contentSize.Height, scale);
|
|
int margin = PopupLayout.ToPixels(_settings.ScreenMargin, scale);
|
|
|
|
PopupWindowNative.Point position =
|
|
PopupLayout.OnScreen(work, _settings.ScreenPosition, margin, width, height);
|
|
|
|
PopupWindowNative.SetBounds(handle, position.X, position.Y, width, height);
|
|
}
|
|
}
|