861 lines
30 KiB
C#
861 lines
30 KiB
C#
|
|
using WPFluent.Interop;
|
|
using WPFluent.Interop.WinDef;
|
|
|
|
using System;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Reflection;
|
|
using System.Runtime.InteropServices;
|
|
using System.Windows;
|
|
using System.Windows.Controls.Primitives;
|
|
using System.Windows.Interop;
|
|
using System.Windows.Media;
|
|
|
|
using System.Windows.Threading;
|
|
|
|
namespace WPFluent.Controls.Primitives;
|
|
|
|
internal class PopupPositioner : DependencyObject, IDisposable
|
|
{
|
|
static PopupPositioner()
|
|
{
|
|
IsSupported = Delegates.GetPlacementInternal != null &&
|
|
Delegates.GetDropOpposite != null &&
|
|
Delegates.GetPlacementTargetInterestPoints != null &&
|
|
Delegates.GetChildInterestPoints != null &&
|
|
Delegates.GetScreenBounds != null;
|
|
}
|
|
|
|
public PopupPositioner(Popup popup)
|
|
{
|
|
if (!IsSupported)
|
|
{
|
|
throw new NotSupportedException();
|
|
}
|
|
|
|
_popup = popup;
|
|
_secHelper = new PopupSecurityHelper();
|
|
|
|
SetPositioner(popup, this);
|
|
|
|
popup.Opened += OnPopupOpened;
|
|
popup.Closed += OnPopupClosed;
|
|
|
|
if (popup.IsOpen)
|
|
{
|
|
OnPopupOpened(null, EventArgs.Empty);
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_isDisposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_isDisposed = true;
|
|
|
|
if (_popup != null)
|
|
{
|
|
_popup.Opened -= OnPopupOpened;
|
|
_popup.Closed -= OnPopupClosed;
|
|
_popup.ClearValue(PositionerProperty);
|
|
}
|
|
|
|
OnPopupClosed(null, EventArgs.Empty);
|
|
}
|
|
|
|
public static bool IsSupported { get; }
|
|
|
|
#region Popup Members
|
|
|
|
public bool IsOpen => _popup.IsOpen;
|
|
|
|
public PlacementMode Placement => _popup.Placement;
|
|
|
|
/// <summary>
|
|
/// Tooltips should show on Keyboard focus.
|
|
/// Chooses the behavior of where the Popup should be placed on screen.
|
|
/// Takes into account TreatMousePlacementAsBottom to place tooltips correctly on keyboard focus.
|
|
/// </summary>
|
|
internal PlacementMode PlacementInternal => Delegates.GetPlacementInternal!(_popup);
|
|
|
|
public CustomPopupPlacementCallback CustomPopupPlacementCallback => _popup.CustomPopupPlacementCallback;
|
|
|
|
public double HorizontalOffset => _popup.HorizontalOffset;
|
|
public double VerticalOffset => _popup.VerticalOffset;
|
|
|
|
internal bool DropOpposite => Delegates.GetDropOpposite!(_popup);
|
|
|
|
private void OnWindowResize(object? sender, AutoResizedEventArgs e)
|
|
{
|
|
if (_positionInfo == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (e.Size != _positionInfo.ChildSize)
|
|
{
|
|
_positionInfo.ChildSize = e.Size;
|
|
|
|
// Reposition the popup
|
|
Reposition();
|
|
}
|
|
}
|
|
|
|
internal void Reposition()
|
|
{
|
|
if (IsOpen && _secHelper.IsWindowAlive())
|
|
{
|
|
if (CheckAccess())
|
|
{
|
|
UpdatePosition();
|
|
}
|
|
else
|
|
{
|
|
Dispatcher.BeginInvoke(DispatcherPriority.Normal, new DispatcherOperationCallback(delegate (object param)
|
|
{
|
|
Debug.Assert(CheckAccess(), "AsyncReposition not called on the dispatcher thread.");
|
|
|
|
Reposition();
|
|
|
|
return null;
|
|
}), null);
|
|
}
|
|
}
|
|
}
|
|
|
|
// This struct is returned by GetPointCombination to indicate
|
|
// which points on the target can align with points on the child
|
|
private struct PointCombination
|
|
{
|
|
public PointCombination(InterestPoint targetInterestPoint, InterestPoint childInterestPoint)
|
|
{
|
|
TargetInterestPoint = targetInterestPoint;
|
|
ChildInterestPoint = childInterestPoint;
|
|
}
|
|
|
|
public InterestPoint TargetInterestPoint;
|
|
public InterestPoint ChildInterestPoint;
|
|
}
|
|
|
|
private class PositionInfo
|
|
{
|
|
// The position of the upper left corner of the popup after nudging
|
|
public int X;
|
|
|
|
public int Y;
|
|
|
|
// The size of the popup
|
|
public Size ChildSize;
|
|
}
|
|
|
|
// To position the popup, we find the InterestPoints of the placement rectangle/point
|
|
// in the screen coordinate space. We also find the InterestPoints of the child in
|
|
// the popup's space. Then we attempt all valid combinations of matching InterestPoints
|
|
// (based on PlacementMode) to find the position that best fits on the screen.
|
|
// NOTE: any reference to the screen implies the monitor for full trust and
|
|
// the browser area for partial trust
|
|
private void UpdatePosition()
|
|
{
|
|
if (_popupRoot == null)
|
|
return;
|
|
|
|
var placement = PlacementInternal;
|
|
|
|
// Get a list of the corners of the target/child in screen space
|
|
var placementTargetInterestPoints = GetPlacementTargetInterestPoints(placement);
|
|
var childInterestPoints = GetChildInterestPoints(placement);
|
|
|
|
// Find bounds of screen and child in screen space
|
|
var targetBounds = GetBounds(placementTargetInterestPoints);
|
|
Rect screenBounds;
|
|
var childBounds = GetBounds(childInterestPoints);
|
|
|
|
var childArea = childBounds.Width * childBounds.Height;
|
|
|
|
var windowRect = _secHelper.GetWindowRect();
|
|
_positionInfo ??= new PositionInfo();
|
|
_positionInfo.X = (int)windowRect.X;
|
|
_positionInfo.Y = (int)windowRect.Y;
|
|
_positionInfo.ChildSize = windowRect.Size;
|
|
|
|
// Rank possible positions
|
|
var bestIndex = -1;
|
|
Vector bestTranslation = new(_positionInfo.X, _positionInfo.Y);
|
|
double bestScore = -1;
|
|
var bestAxis = PopupPrimaryAxis.None;
|
|
|
|
int positions;
|
|
|
|
CustomPopupPlacement[] customPlacements = null!;
|
|
|
|
// Find the number of possible positions
|
|
if (placement == PlacementMode.Custom)
|
|
{
|
|
var customCallback = CustomPopupPlacementCallback;
|
|
if (customCallback != null)
|
|
{
|
|
customPlacements = customCallback(childBounds.Size, targetBounds.Size, new Point(HorizontalOffset, VerticalOffset));
|
|
}
|
|
positions = customPlacements == null ? 0 : customPlacements.Length;
|
|
|
|
// Return if callback closed the popup
|
|
if (!IsOpen)
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
positions = GetNumberOfCombinations(placement);
|
|
}
|
|
|
|
// Try each position until the best one is found
|
|
for (var i = 0; i < positions; i++)
|
|
{
|
|
Vector popupTranslation;
|
|
|
|
PopupPrimaryAxis axis;
|
|
|
|
// Get the ith Position to rank
|
|
if (placement == PlacementMode.Custom)
|
|
{
|
|
// The custom callback only calculates relative to 0,0
|
|
// so the placementTarget's top/left need to be re-applied.
|
|
popupTranslation = (Vector)placementTargetInterestPoints[(int)InterestPoint.TopLeft]
|
|
+ (Vector)customPlacements![i].Point; // vector from origin
|
|
|
|
axis = customPlacements[i].PrimaryAxis;
|
|
}
|
|
else
|
|
{
|
|
var pointCombination = GetPointCombination(placement, i, out axis);
|
|
|
|
var targetInterestPoint = pointCombination.TargetInterestPoint;
|
|
var childInterestPoint = pointCombination.ChildInterestPoint;
|
|
|
|
// Compute the vector from the screen origin to the top left corner of the popup
|
|
// that will cause the the two interest points to overlap
|
|
popupTranslation = placementTargetInterestPoints[(int)targetInterestPoint]
|
|
- childInterestPoints[(int)childInterestPoint];
|
|
}
|
|
|
|
// Find percent of popup on screen by translating the popup bounds
|
|
// and calculating the percent of the bounds that is on screen
|
|
// Note: this score is based on the percent of the popup that is on screen
|
|
// not the percent of the child that is on screen. For certain
|
|
// scenarios, this may produce in counter-intuitive results.
|
|
// If this is a problem, more complex scoring is needed
|
|
var tranlsatedChildBounds = Rect.Offset(childBounds, popupTranslation);
|
|
screenBounds = GetScreenBounds(targetBounds, placementTargetInterestPoints[(int)InterestPoint.TopLeft]);
|
|
var currentIntersection = Rect.Intersect(screenBounds, tranlsatedChildBounds);
|
|
|
|
// Calculate area of intersection
|
|
var score = currentIntersection != Rect.Empty ? currentIntersection.Width * currentIntersection.Height : 0;
|
|
|
|
// If current score is better than the best score so far, save the position info
|
|
if (score - bestScore > Tolerance)
|
|
{
|
|
bestIndex = i;
|
|
bestTranslation = popupTranslation;
|
|
bestScore = score;
|
|
bestAxis = axis;
|
|
|
|
// Stop when we find a popup that is completely on screen
|
|
if (Math.Abs(score - childArea) < Tolerance)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check to see if the pop needs to be nudged onto the screen.
|
|
// Popups are not nudged if their axes do not align with the screen axes
|
|
|
|
// Use the size of the popupRoot in case it is clipping the popup content
|
|
var transformToDevice = _secHelper.GetTransformToDevice();
|
|
childBounds = new Rect((Size)transformToDevice.Transform((Point)GetChildSize()));
|
|
|
|
childBounds.Offset(bestTranslation);
|
|
|
|
var childTranslation = (Vector)transformToDevice.Transform(GetChildTranslation());
|
|
childBounds.Offset(childTranslation);
|
|
|
|
screenBounds = GetScreenBounds(targetBounds, placementTargetInterestPoints[(int)InterestPoint.TopLeft]);
|
|
var intersection = Rect.Intersect(screenBounds, childBounds);
|
|
|
|
// See if width/height of intersection are less than child's
|
|
if (Math.Abs(intersection.Width - childBounds.Width) > Tolerance ||
|
|
Math.Abs(intersection.Height - childBounds.Height) > Tolerance)
|
|
{
|
|
// Nudge Horizontally
|
|
var topLeft = placementTargetInterestPoints[(int)InterestPoint.TopLeft];
|
|
var topRight = placementTargetInterestPoints[(int)InterestPoint.TopRight];
|
|
|
|
// Create a vector pointing from the top of the placement target to the bottom
|
|
// to determine which direction the popup should be nudged in.
|
|
// If the vector is zero (NaN's after normalization), nudge horizontally
|
|
var horizontalAxis = topRight - topLeft;
|
|
horizontalAxis.Normalize();
|
|
|
|
// See if target's horizontal axis is aligned with screen
|
|
// (For opaque windows always translate horizontally)
|
|
if (!IsTransparent || double.IsNaN(horizontalAxis.Y) || Math.Abs(horizontalAxis.Y) < Tolerance)
|
|
{
|
|
// Nudge horizontally
|
|
if (childBounds.Right > screenBounds.Right)
|
|
{
|
|
bestTranslation.X = screenBounds.Right - childBounds.Width;
|
|
bestTranslation.X -= childTranslation.X;
|
|
}
|
|
else if (childBounds.Left < screenBounds.Left)
|
|
{
|
|
bestTranslation.X = screenBounds.Left;
|
|
bestTranslation.X -= childTranslation.X;
|
|
}
|
|
}
|
|
else if (IsTransparent && Math.Abs(horizontalAxis.X) < Tolerance)
|
|
{
|
|
// Nudge vertically, limit horizontally
|
|
if (childBounds.Bottom > screenBounds.Bottom)
|
|
{
|
|
bestTranslation.Y = screenBounds.Bottom - childBounds.Height;
|
|
bestTranslation.Y -= childTranslation.Y;
|
|
}
|
|
else if (childBounds.Top < screenBounds.Top)
|
|
{
|
|
bestTranslation.Y = screenBounds.Top;
|
|
bestTranslation.Y -= childTranslation.Y;
|
|
}
|
|
}
|
|
|
|
// Nudge Vertically
|
|
var bottomLeft = placementTargetInterestPoints[(int)InterestPoint.BottomLeft];
|
|
|
|
// Create a vector pointing from the top of the placement target to the bottom
|
|
// to determine which direction the popup should be nudged in
|
|
// If the vector is zero (NaN's after normalization), nudge vertically
|
|
var verticalAxis = topLeft - bottomLeft;
|
|
verticalAxis.Normalize();
|
|
|
|
// Axis is aligned with screen, nudge
|
|
if (!IsTransparent || double.IsNaN(verticalAxis.X) || Math.Abs(verticalAxis.X) < Tolerance)
|
|
{
|
|
if (childBounds.Bottom > screenBounds.Bottom)
|
|
{
|
|
bestTranslation.Y = screenBounds.Bottom - childBounds.Height;
|
|
bestTranslation.Y -= childTranslation.Y;
|
|
}
|
|
else if (childBounds.Top < screenBounds.Top)
|
|
{
|
|
bestTranslation.Y = screenBounds.Top;
|
|
bestTranslation.Y -= childTranslation.Y;
|
|
}
|
|
}
|
|
else if (IsTransparent && Math.Abs(verticalAxis.Y) < Tolerance)
|
|
{
|
|
if (childBounds.Right > screenBounds.Right)
|
|
{
|
|
bestTranslation.X = screenBounds.Right - childBounds.Width;
|
|
bestTranslation.X -= childTranslation.X;
|
|
}
|
|
else if (childBounds.Left < screenBounds.Left)
|
|
{
|
|
bestTranslation.X = screenBounds.Left;
|
|
bestTranslation.X -= childTranslation.X;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Finally, take the best position and apply it to the popup
|
|
var bestX = DoubleUtil.DoubleToInt(bestTranslation.X);
|
|
var bestY = DoubleUtil.DoubleToInt(bestTranslation.Y);
|
|
if (bestX != _positionInfo.X || bestY != _positionInfo.Y)
|
|
{
|
|
_positionInfo.X = bestX;
|
|
_positionInfo.Y = bestY;
|
|
_secHelper.SetPopupPos(true, bestX, bestY, false, 0, 0);
|
|
}
|
|
|
|
Size GetChildSize()
|
|
{
|
|
if (_popup.Child is { } child)
|
|
{
|
|
return child.RenderSize;
|
|
}
|
|
return _popupRoot.RenderSize;
|
|
}
|
|
|
|
Point GetChildTranslation()
|
|
{
|
|
if (_popup.Child is { } child)
|
|
{
|
|
return child.TranslatePoint(new Point(), _popupRoot);
|
|
}
|
|
return new Point();
|
|
}
|
|
}
|
|
|
|
private Point[] GetPlacementTargetInterestPoints(PlacementMode placement)
|
|
{
|
|
return Delegates.GetPlacementTargetInterestPoints!.Invoke(_popup, placement);
|
|
}
|
|
|
|
// Returns the ith possible alignment for the given PlacementMode
|
|
private PointCombination GetPointCombination(PlacementMode placement, int i, out PopupPrimaryAxis axis)
|
|
{
|
|
Debug.Assert(i >= 0 && i < GetNumberOfCombinations(placement));
|
|
|
|
var dropFromRight = SystemParameters.MenuDropAlignment;
|
|
|
|
switch (placement)
|
|
{
|
|
case PlacementMode.Bottom:
|
|
case PlacementMode.Mouse:
|
|
axis = PopupPrimaryAxis.Horizontal;
|
|
if (dropFromRight)
|
|
{
|
|
if (i == 0) return new PointCombination(InterestPoint.BottomRight, InterestPoint.TopRight);
|
|
if (i == 1) return new PointCombination(InterestPoint.TopRight, InterestPoint.BottomRight);
|
|
}
|
|
else
|
|
{
|
|
if (i == 0) return new PointCombination(InterestPoint.BottomLeft, InterestPoint.TopLeft);
|
|
if (i == 1) return new PointCombination(InterestPoint.TopLeft, InterestPoint.BottomLeft);
|
|
}
|
|
break;
|
|
|
|
case PlacementMode.Top:
|
|
axis = PopupPrimaryAxis.Horizontal;
|
|
if (dropFromRight)
|
|
{
|
|
if (i == 0) return new PointCombination(InterestPoint.TopRight, InterestPoint.BottomRight);
|
|
if (i == 1) return new PointCombination(InterestPoint.BottomRight, InterestPoint.TopRight);
|
|
}
|
|
else
|
|
{
|
|
if (i == 0) return new PointCombination(InterestPoint.TopLeft, InterestPoint.BottomLeft);
|
|
if (i == 1) return new PointCombination(InterestPoint.BottomLeft, InterestPoint.TopLeft);
|
|
}
|
|
break;
|
|
|
|
case PlacementMode.Right:
|
|
case PlacementMode.Left:
|
|
axis = PopupPrimaryAxis.Vertical;
|
|
dropFromRight |= DropOpposite;
|
|
|
|
if (dropFromRight && placement == PlacementMode.Right ||
|
|
!dropFromRight && placement == PlacementMode.Left)
|
|
{
|
|
if (i == 0) return new PointCombination(InterestPoint.TopLeft, InterestPoint.TopRight);
|
|
if (i == 1) return new PointCombination(InterestPoint.BottomLeft, InterestPoint.BottomRight);
|
|
if (i == 2) return new PointCombination(InterestPoint.TopRight, InterestPoint.TopLeft);
|
|
if (i == 3) return new PointCombination(InterestPoint.BottomRight, InterestPoint.BottomLeft);
|
|
}
|
|
else
|
|
{
|
|
if (i == 0) return new PointCombination(InterestPoint.TopRight, InterestPoint.TopLeft);
|
|
if (i == 1) return new PointCombination(InterestPoint.BottomRight, InterestPoint.BottomLeft);
|
|
if (i == 2) return new PointCombination(InterestPoint.TopLeft, InterestPoint.TopRight);
|
|
if (i == 3) return new PointCombination(InterestPoint.BottomLeft, InterestPoint.BottomRight);
|
|
}
|
|
break;
|
|
|
|
case PlacementMode.Relative:
|
|
case PlacementMode.RelativePoint:
|
|
case PlacementMode.MousePoint:
|
|
case PlacementMode.AbsolutePoint:
|
|
axis = PopupPrimaryAxis.Horizontal;
|
|
if (dropFromRight)
|
|
{
|
|
if (i == 0) return new PointCombination(InterestPoint.TopLeft, InterestPoint.TopRight);
|
|
if (i == 1) return new PointCombination(InterestPoint.TopLeft, InterestPoint.TopLeft);
|
|
if (i == 2) return new PointCombination(InterestPoint.TopLeft, InterestPoint.BottomRight);
|
|
if (i == 3) return new PointCombination(InterestPoint.TopLeft, InterestPoint.BottomLeft);
|
|
}
|
|
else
|
|
{
|
|
if (i == 0) return new PointCombination(InterestPoint.TopLeft, InterestPoint.TopLeft);
|
|
if (i == 1) return new PointCombination(InterestPoint.TopLeft, InterestPoint.TopRight);
|
|
if (i == 2) return new PointCombination(InterestPoint.TopLeft, InterestPoint.BottomLeft);
|
|
if (i == 3) return new PointCombination(InterestPoint.TopLeft, InterestPoint.BottomRight);
|
|
}
|
|
break;
|
|
|
|
case PlacementMode.Center:
|
|
axis = PopupPrimaryAxis.None;
|
|
return new PointCombination(InterestPoint.Center, InterestPoint.Center);
|
|
|
|
case PlacementMode.Absolute:
|
|
case PlacementMode.Custom:
|
|
default:
|
|
axis = PopupPrimaryAxis.None;
|
|
return new PointCombination(InterestPoint.TopLeft, InterestPoint.TopLeft);
|
|
}
|
|
|
|
return new PointCombination(InterestPoint.TopLeft, InterestPoint.TopRight);
|
|
}
|
|
|
|
// Retrieves a list of the interesting points of the popups child in the popup window space
|
|
private Point[] GetChildInterestPoints(PlacementMode placement)
|
|
{
|
|
return Delegates.GetChildInterestPoints!(_popup, placement);
|
|
}
|
|
|
|
// Gets the smallest rectangle that contains all points in the list
|
|
private Rect GetBounds(Point[] interestPoints)
|
|
{
|
|
double left, right, top, bottom;
|
|
|
|
left = right = interestPoints[0].X;
|
|
top = bottom = interestPoints[0].Y;
|
|
|
|
for (var i = 1; i < interestPoints.Length; i++)
|
|
{
|
|
var x = interestPoints[i].X;
|
|
var y = interestPoints[i].Y;
|
|
if (x < left) left = x;
|
|
if (x > right) right = x;
|
|
if (y < top) top = y;
|
|
if (y > bottom) bottom = y;
|
|
}
|
|
return new Rect(left, top, right - left, bottom - top);
|
|
}
|
|
|
|
// Gets the number of InterestPoint combinations for the given placement
|
|
private static int GetNumberOfCombinations(PlacementMode placement)
|
|
{
|
|
switch (placement)
|
|
{
|
|
case PlacementMode.Bottom:
|
|
case PlacementMode.Top:
|
|
case PlacementMode.Mouse:
|
|
return 2;
|
|
|
|
case PlacementMode.Right:
|
|
case PlacementMode.Left:
|
|
case PlacementMode.RelativePoint:
|
|
case PlacementMode.MousePoint:
|
|
case PlacementMode.AbsolutePoint:
|
|
return 4;
|
|
|
|
case PlacementMode.Custom:
|
|
return 0;
|
|
|
|
case PlacementMode.Absolute:
|
|
case PlacementMode.Relative:
|
|
case PlacementMode.Center:
|
|
default:
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
private Rect GetScreenBounds(Rect boundingBox, Point p)
|
|
{
|
|
return Delegates.GetScreenBounds!(_popup, boundingBox, p);
|
|
}
|
|
|
|
private bool IsTransparent => _popup.AllowsTransparency;
|
|
|
|
internal const double Tolerance = 1.0e-2; // allow errors in double calculations
|
|
|
|
private PositionInfo _positionInfo = null!;
|
|
|
|
private FrameworkElement _popupRoot = null!;
|
|
|
|
private PopupSecurityHelper _secHelper = null!;
|
|
|
|
private class PopupSecurityHelper
|
|
{
|
|
internal PopupSecurityHelper()
|
|
{
|
|
}
|
|
|
|
internal bool AttachedToWindow => _window != null;
|
|
|
|
internal void AttachToWindow(HwndSource window, AutoResizedEventHandler handler)
|
|
{
|
|
if (_window == null)
|
|
{
|
|
_window = window;
|
|
|
|
window.AutoResized += handler;
|
|
}
|
|
else
|
|
{
|
|
Debug.Assert(_window == window);
|
|
}
|
|
}
|
|
|
|
internal void DetachFromWindow(AutoResizedEventHandler onAutoResizedEventHandler)
|
|
{
|
|
if (_window != null)
|
|
{
|
|
var hwnd = _window;
|
|
|
|
_window = null!;
|
|
|
|
hwnd.AutoResized -= onAutoResizedEventHandler;
|
|
}
|
|
}
|
|
|
|
internal bool IsWindowAlive()
|
|
{
|
|
if (_window != null)
|
|
{
|
|
var hwnd = _window;
|
|
return hwnd != null && !hwnd.IsDisposed;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
internal void SetPopupPos(bool position, int x, int y, bool size, int width, int height)
|
|
{
|
|
User32.SET_WINDOW_POS_FLAGS flags = User32.SET_WINDOW_POS_FLAGS.SWP_NOZORDER | User32.SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE;
|
|
if (!position)
|
|
{
|
|
flags |= User32.SET_WINDOW_POS_FLAGS.SWP_NOMOVE;
|
|
}
|
|
if (!size)
|
|
{
|
|
flags |= User32.SET_WINDOW_POS_FLAGS.SWP_NOSIZE;
|
|
}
|
|
|
|
User32.SetWindowPos(Handle, IntPtr.Zero, x, y, width, height, flags);
|
|
}
|
|
|
|
internal unsafe Rect GetWindowRect()
|
|
{
|
|
RECT rect = new(0, 0, 0, 0);
|
|
|
|
if (IsWindowAlive())
|
|
{
|
|
GetWindowRect(_window.CreateHandleRef(), ref rect);
|
|
}
|
|
|
|
return rect.ToRect();
|
|
}
|
|
|
|
internal Matrix GetTransformToDevice()
|
|
{
|
|
CompositionTarget ct = _window.CompositionTarget;
|
|
if (ct != null)
|
|
{
|
|
try
|
|
{
|
|
return ct.TransformToDevice;
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
}
|
|
}
|
|
|
|
return Matrix.Identity;
|
|
}
|
|
|
|
internal Matrix GetTransformFromDevice()
|
|
{
|
|
CompositionTarget ct = _window.CompositionTarget;
|
|
if (ct != null)
|
|
{
|
|
try
|
|
{
|
|
return ct.TransformFromDevice;
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
}
|
|
}
|
|
|
|
return Matrix.Identity;
|
|
}
|
|
|
|
private static void GetWindowRect(HandleRef hWnd, [In, Out] ref RECT rect)
|
|
{
|
|
if (!_GetWindowRect(hWnd, ref rect))
|
|
{
|
|
throw new Win32Exception();
|
|
}
|
|
}
|
|
|
|
[DllImport("user32.dll", EntryPoint = "GetWindowRect", ExactSpelling = true, CharSet = CharSet.Auto, SetLastError = true)]
|
|
private static extern bool _GetWindowRect(HandleRef hWnd, [In, Out] ref RECT rect);
|
|
|
|
private static IntPtr GetHandle(HwndSource hwnd)
|
|
{
|
|
// add hook to the popup's window
|
|
return hwnd != null ? hwnd.Handle : IntPtr.Zero;
|
|
}
|
|
|
|
private IntPtr Handle
|
|
{
|
|
get
|
|
{
|
|
return GetHandle(_window);
|
|
}
|
|
}
|
|
|
|
private HwndSource _window = null!;
|
|
}
|
|
|
|
#endregion Popup Members
|
|
|
|
#region Positioner
|
|
|
|
private static readonly DependencyProperty PositionerProperty =
|
|
DependencyProperty.RegisterAttached(
|
|
"Positioner",
|
|
typeof(PopupPositioner),
|
|
typeof(PopupPositioner),
|
|
new PropertyMetadata(OnPositionerChanged));
|
|
|
|
internal static PopupPositioner GetPositioner(Popup popup)
|
|
{
|
|
return (PopupPositioner)popup.GetValue(PositionerProperty);
|
|
}
|
|
|
|
private static void SetPositioner(Popup popup, PopupPositioner value)
|
|
{
|
|
popup.SetValue(PositionerProperty, value);
|
|
}
|
|
|
|
private static void OnPositionerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if (e.OldValue is PopupPositioner oldValue)
|
|
{
|
|
oldValue.Dispose();
|
|
}
|
|
}
|
|
|
|
#endregion Positioner
|
|
|
|
private void OnPopupOpened(object? sender, EventArgs e)
|
|
{
|
|
if (!_secHelper.AttachedToWindow &&
|
|
_popup.Child is { } child &&
|
|
PresentationSource.FromVisual(child) is HwndSource window)
|
|
{
|
|
_secHelper.AttachToWindow(window, OnWindowResize);
|
|
_popupRoot = (window.RootVisual as FrameworkElement)!;
|
|
Debug.Assert(_popupRoot != null && _popupRoot.GetType().Name == "PopupRoot");
|
|
|
|
DependencyPropertyDescriptor.FromProperty(Popup.ChildProperty, typeof(Popup)).AddValueChanged(_popup, OnPopupPropertyChanged);
|
|
DependencyPropertyDescriptor.FromProperty(Popup.PlacementProperty, typeof(Popup)).AddValueChanged(_popup, OnPopupPropertyChanged);
|
|
DependencyPropertyDescriptor.FromProperty(Popup.HorizontalOffsetProperty, typeof(Popup)).AddValueChanged(_popup, OnPopupPropertyChanged);
|
|
DependencyPropertyDescriptor.FromProperty(Popup.VerticalOffsetProperty, typeof(Popup)).AddValueChanged(_popup, OnPopupPropertyChanged);
|
|
DependencyPropertyDescriptor.FromProperty(Popup.PlacementRectangleProperty, typeof(Popup)).AddValueChanged(_popup, OnPopupPropertyChanged);
|
|
|
|
Reposition();
|
|
}
|
|
}
|
|
|
|
private void OnPopupClosed(object? sender, EventArgs e)
|
|
{
|
|
if (_secHelper.AttachedToWindow)
|
|
{
|
|
DependencyPropertyDescriptor.FromProperty(Popup.ChildProperty, typeof(Popup)).RemoveValueChanged(_popup, OnPopupPropertyChanged);
|
|
DependencyPropertyDescriptor.FromProperty(Popup.PlacementProperty, typeof(Popup)).RemoveValueChanged(_popup, OnPopupPropertyChanged);
|
|
DependencyPropertyDescriptor.FromProperty(Popup.HorizontalOffsetProperty, typeof(Popup)).RemoveValueChanged(_popup, OnPopupPropertyChanged);
|
|
DependencyPropertyDescriptor.FromProperty(Popup.VerticalOffsetProperty, typeof(Popup)).RemoveValueChanged(_popup, OnPopupPropertyChanged);
|
|
DependencyPropertyDescriptor.FromProperty(Popup.PlacementRectangleProperty, typeof(Popup)).RemoveValueChanged(_popup, OnPopupPropertyChanged);
|
|
|
|
_secHelper.DetachFromWindow(OnWindowResize);
|
|
_popupRoot = null!;
|
|
_positionInfo = null!;
|
|
}
|
|
}
|
|
|
|
private void OnPopupPropertyChanged(object? sender, EventArgs e)
|
|
{
|
|
Reposition();
|
|
}
|
|
|
|
private readonly Popup _popup = null!;
|
|
private bool _isDisposed;
|
|
|
|
private static class Delegates
|
|
{
|
|
static Delegates()
|
|
{
|
|
try
|
|
{
|
|
GetPlacementInternal = DelegateHelper.CreatePropertyGetter<Popup, PlacementMode>(
|
|
nameof(PlacementInternal),
|
|
BindingFlags.Instance | BindingFlags.NonPublic,
|
|
true);
|
|
|
|
GetDropOpposite = DelegateHelper.CreatePropertyGetter<Popup, bool>(
|
|
nameof(DropOpposite),
|
|
BindingFlags.Instance | BindingFlags.NonPublic,
|
|
true);
|
|
|
|
GetPlacementTargetInterestPoints = DelegateHelper.CreateDelegate<Func<Popup, PlacementMode, Point[]>>(
|
|
typeof(Popup),
|
|
nameof(GetPlacementTargetInterestPoints),
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
|
|
GetChildInterestPoints = DelegateHelper.CreateDelegate<Func<Popup, PlacementMode, Point[]>>(
|
|
typeof(Popup),
|
|
nameof(GetChildInterestPoints),
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
|
|
GetScreenBounds = DelegateHelper.CreateDelegate<Func<Popup, Rect, Point, Rect>>(
|
|
typeof(Popup),
|
|
nameof(GetScreenBounds),
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
public static Func<Popup, PlacementMode>? GetPlacementInternal { get; }
|
|
|
|
public static Func<Popup, bool>? GetDropOpposite { get; }
|
|
|
|
public static Func<Popup, PlacementMode, Point[]>? GetPlacementTargetInterestPoints { get; }
|
|
public static Func<Popup, PlacementMode, Point[]>? GetChildInterestPoints { get; }
|
|
public static Func<Popup, Rect, Point, Rect>? GetScreenBounds { get; }
|
|
}
|
|
}
|
|
|
|
internal enum InterestPoint
|
|
{
|
|
TopLeft = 0,
|
|
TopRight = 1,
|
|
BottomLeft = 2,
|
|
BottomRight = 3,
|
|
Center = 4,
|
|
}
|
|
|
|
public static class DoubleUtil
|
|
{
|
|
public static int DoubleToInt(double val)
|
|
{
|
|
if (!(0d < val))
|
|
{
|
|
return (int)(val - 0.5d);
|
|
}
|
|
|
|
return (int)(val + 0.5);
|
|
}
|
|
}
|
|
|
|
internal static class PointUtil
|
|
{
|
|
internal static Rect ToRect(this RECT rc)
|
|
{
|
|
Rect rect = new()
|
|
{
|
|
X = rc.Left,
|
|
Y = rc.Top,
|
|
Width = rc.Right - rc.Left,
|
|
Height = rc.Bottom - rc.Top
|
|
};
|
|
|
|
return rect;
|
|
}
|
|
}
|