971 lines
27 KiB
C#
971 lines
27 KiB
C#
using System;
|
|
using System.Diagnostics;
|
|
using System.Reflection;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Controls.Primitives;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Animation;
|
|
using System.Windows.Media.Effects;
|
|
|
|
namespace WPFluent.Controls.Primitives;
|
|
|
|
public class ThemeShadowChrome : Decorator
|
|
{
|
|
static ThemeShadowChrome()
|
|
{
|
|
s_bg1 = new SolidColorBrush(Colors.Black) { Opacity = 0.11d };
|
|
s_bg2 = new SolidColorBrush(Colors.Black) { Opacity = 0.13d };
|
|
s_bg3 = new SolidColorBrush(Colors.Black) { Opacity = 0.18d };
|
|
s_bg4 = new SolidColorBrush(Colors.Black) { Opacity = 0.22d };
|
|
|
|
s_bg1.Freeze();
|
|
s_bg2.Freeze();
|
|
s_bg3.Freeze();
|
|
s_bg4.Freeze();
|
|
}
|
|
|
|
public ThemeShadowChrome()
|
|
{
|
|
#if NET462_OR_NEWER
|
|
_bitmapCache = new BitmapCache(VisualTreeHelper.GetDpi(this).PixelsPerDip);
|
|
#else
|
|
_bitmapCache = new BitmapCache();
|
|
#endif
|
|
_background = new Grid
|
|
{
|
|
CacheMode = _bitmapCache,
|
|
Focusable = false,
|
|
IsHitTestVisible = false,
|
|
SnapsToDevicePixels = false
|
|
};
|
|
AddVisualChild(_background);
|
|
|
|
SizeChanged += OnSizeChanged;
|
|
Loaded += OnLoaded;
|
|
}
|
|
|
|
public static readonly DependencyProperty IsShadowEnabledProperty =
|
|
DependencyProperty.Register(nameof(IsShadowEnabled), typeof(bool), typeof(ThemeShadowChrome), new PropertyMetadata(true, OnIsShadowEnabledChanged));
|
|
|
|
public bool IsShadowEnabled
|
|
{
|
|
get => (bool)GetValue(IsShadowEnabledProperty);
|
|
set => SetValue(IsShadowEnabledProperty, value);
|
|
}
|
|
|
|
private static void OnIsShadowEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
((ThemeShadowChrome)d).OnIsShadowEnabledChanged();
|
|
}
|
|
|
|
private void OnIsShadowEnabledChanged()
|
|
{
|
|
if (IsInitialized)
|
|
{
|
|
if (IsShadowEnabled)
|
|
{
|
|
EnsureShadows();
|
|
Debug.Assert(_background.Children.Count == 0);
|
|
_background.Children.Add(_shadow1);
|
|
_background.Children.Add(_shadow2);
|
|
_background.Visibility = Visibility.Visible;
|
|
}
|
|
else
|
|
{
|
|
_background.Children.Clear();
|
|
_background.Visibility = Visibility.Collapsed;
|
|
}
|
|
|
|
OnVisualParentChanged();
|
|
UpdatePopupMargin();
|
|
}
|
|
}
|
|
|
|
public static readonly DependencyProperty DepthProperty =
|
|
DependencyProperty.Register(
|
|
nameof(Depth),
|
|
typeof(double),
|
|
typeof(ThemeShadowChrome),
|
|
new PropertyMetadata(32d, OnDepthChanged));
|
|
|
|
public double Depth
|
|
{
|
|
get => (double)GetValue(DepthProperty);
|
|
set => SetValue(DepthProperty, value);
|
|
}
|
|
|
|
private static void OnDepthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
((ThemeShadowChrome)d).OnDepthChanged();
|
|
}
|
|
|
|
private void OnDepthChanged()
|
|
{
|
|
if (IsInitialized)
|
|
{
|
|
UpdateShadow1();
|
|
UpdateShadow2();
|
|
UpdatePopupMargin();
|
|
}
|
|
}
|
|
|
|
public static readonly DependencyProperty CornerRadiusProperty =
|
|
DependencyProperty.Register(nameof(CornerRadius), typeof(CornerRadius), typeof(ThemeShadowChrome), new PropertyMetadata(new CornerRadius(), OnCornerRadiusChanged));
|
|
|
|
public CornerRadius CornerRadius
|
|
{
|
|
get => (CornerRadius)GetValue(CornerRadiusProperty);
|
|
set => SetValue(CornerRadiusProperty, value);
|
|
}
|
|
|
|
private static void OnCornerRadiusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
((ThemeShadowChrome)d).OnCornerRadiusChanged(e);
|
|
}
|
|
|
|
private void OnCornerRadiusChanged(DependencyPropertyChangedEventArgs e)
|
|
{
|
|
var cornerRadius = (CornerRadius)e.NewValue;
|
|
|
|
if (_shadow1 != null)
|
|
{
|
|
_shadow1.CornerRadius = cornerRadius;
|
|
}
|
|
|
|
if (_shadow2 != null)
|
|
{
|
|
_shadow2.CornerRadius = cornerRadius;
|
|
}
|
|
}
|
|
|
|
private static readonly DependencyProperty PopupMarginProperty =
|
|
DependencyProperty.Register(nameof(PopupMargin), typeof(Thickness), typeof(ThemeShadowChrome), new PropertyMetadata(new Thickness(), OnPopupMarginChanged));
|
|
|
|
private Thickness PopupMargin
|
|
{
|
|
get => (Thickness)GetValue(PopupMarginProperty);
|
|
set => SetValue(PopupMarginProperty, value);
|
|
}
|
|
|
|
private static void OnPopupMarginChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
((ThemeShadowChrome)d).OnPopupMarginChanged(e);
|
|
}
|
|
|
|
private void OnPopupMarginChanged(DependencyPropertyChangedEventArgs e)
|
|
{
|
|
ApplyPopupMargin();
|
|
}
|
|
|
|
private void UpdatePopupMargin()
|
|
{
|
|
if (IsShadowEnabled)
|
|
{
|
|
var depth = Depth;
|
|
var radius = 0.9d * depth;
|
|
var offset = 0.4d * depth;
|
|
|
|
PopupMargin = new Thickness(
|
|
radius,
|
|
radius,
|
|
radius,
|
|
radius + offset);
|
|
}
|
|
else
|
|
{
|
|
ClearValue(PopupMarginProperty);
|
|
}
|
|
}
|
|
|
|
private void ApplyPopupMargin()
|
|
{
|
|
if (_parentPopupControl != null)
|
|
{
|
|
if (ReadLocalValue(PopupMarginProperty) == DependencyProperty.UnsetValue)
|
|
{
|
|
_parentPopupControl.ClearMargin();
|
|
}
|
|
else
|
|
{
|
|
_parentPopupControl.SetMargin(PopupMargin);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override int VisualChildrenCount =>
|
|
IsShadowEnabled ? Child == null ? 1 : 2 : base.VisualChildrenCount;
|
|
|
|
protected override void OnVisualParentChanged(DependencyObject oldParent)
|
|
{
|
|
base.OnVisualParentChanged(oldParent);
|
|
|
|
if (IsInitialized)
|
|
{
|
|
OnVisualParentChanged();
|
|
}
|
|
}
|
|
|
|
protected override Visual GetVisualChild(int index)
|
|
{
|
|
if (IsShadowEnabled)
|
|
{
|
|
if (index == 0)
|
|
{
|
|
return _background;
|
|
}
|
|
else if (index == 1 && Child != null)
|
|
{
|
|
return Child;
|
|
}
|
|
|
|
throw new ArgumentOutOfRangeException(nameof(index));
|
|
}
|
|
else
|
|
{
|
|
return base.GetVisualChild(index);
|
|
}
|
|
}
|
|
|
|
protected override void OnInitialized(EventArgs e)
|
|
{
|
|
base.OnInitialized(e);
|
|
|
|
OnIsShadowEnabledChanged();
|
|
}
|
|
|
|
protected override Size MeasureOverride(Size constraint)
|
|
{
|
|
if (IsShadowEnabled)
|
|
{
|
|
_background.Measure(constraint);
|
|
}
|
|
|
|
return base.MeasureOverride(constraint);
|
|
}
|
|
|
|
protected override Size ArrangeOverride(Size arrangeSize)
|
|
{
|
|
if (IsShadowEnabled)
|
|
{
|
|
_background.Arrange(new Rect(arrangeSize));
|
|
}
|
|
|
|
return base.ArrangeOverride(arrangeSize);
|
|
}
|
|
|
|
#if NET462_OR_NEWER
|
|
protected override void OnDpiChanged(DpiScale oldDpi, DpiScale newDpi)
|
|
{
|
|
base.OnDpiChanged(oldDpi, newDpi);
|
|
|
|
_bitmapCache.RenderAtScale = newDpi.PixelsPerDip;
|
|
}
|
|
#endif
|
|
|
|
private void OnVisualParentChanged()
|
|
{
|
|
if (IsShadowEnabled)
|
|
{
|
|
PopupControl parentPopupControl = null!;
|
|
|
|
var visualParent = VisualParent;
|
|
if (visualParent is ContextMenu contextMenu)
|
|
{
|
|
parentPopupControl = new PopupControl(contextMenu);
|
|
}
|
|
else if (visualParent is ToolTip toolTip)
|
|
{
|
|
parentPopupControl = new PopupControl(toolTip);
|
|
}
|
|
else if (FindParentPopup(this) is Popup parentPopup)
|
|
{
|
|
parentPopupControl = new PopupControl(parentPopup);
|
|
}
|
|
|
|
SetParentPopupControl(parentPopupControl!);
|
|
}
|
|
else
|
|
{
|
|
SetParentPopupControl(null!);
|
|
}
|
|
}
|
|
|
|
private void EnsureShadows()
|
|
{
|
|
if (_shadow1 == null)
|
|
{
|
|
_shadow1 = CreateShadowElement();
|
|
UpdateShadow1();
|
|
}
|
|
|
|
if (_shadow2 == null)
|
|
{
|
|
_shadow2 = CreateShadowElement();
|
|
UpdateShadow2();
|
|
}
|
|
}
|
|
|
|
private Border CreateShadowElement()
|
|
{
|
|
return new Border
|
|
{
|
|
CornerRadius = CornerRadius,
|
|
Effect = new BlurEffect(),
|
|
RenderTransform = new TranslateTransform()
|
|
};
|
|
}
|
|
|
|
private void UpdateShadow1()
|
|
{
|
|
if (_shadow1 != null)
|
|
{
|
|
var depth = Depth;
|
|
|
|
var effect = (BlurEffect)_shadow1.Effect;
|
|
effect.Radius = 0.9 * depth;
|
|
|
|
var transform = (TranslateTransform)_shadow1.RenderTransform;
|
|
transform.Y = 0.4 * depth;
|
|
|
|
_shadow1.Background = depth >= 32 ? s_bg4 : s_bg2;
|
|
}
|
|
}
|
|
|
|
private void UpdateShadow2()
|
|
{
|
|
if (_shadow2 != null)
|
|
{
|
|
var depth = Depth;
|
|
|
|
var effect = (BlurEffect)_shadow2.Effect;
|
|
effect.Radius = 0.225 * depth;
|
|
|
|
var transform = (TranslateTransform)_shadow2.RenderTransform;
|
|
transform.Y = 0.075 * depth;
|
|
|
|
_shadow2.Background = depth >= 32 ? s_bg3 : s_bg1;
|
|
}
|
|
}
|
|
|
|
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
|
{
|
|
ClearMarginAdjustment();
|
|
UpdateLayout();
|
|
AdjustMargin();
|
|
}
|
|
|
|
private void OnLoaded(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (IsVisible)
|
|
{
|
|
AdjustMargin();
|
|
}
|
|
}
|
|
|
|
private void AdjustMargin()
|
|
{
|
|
if (_parentPopupControl != null)
|
|
{
|
|
var margin = Margin;
|
|
if (margin != new Thickness() && VisualParent is UIElement parent)
|
|
{
|
|
var parentWidth = parent.RenderSize.Width;
|
|
var shadowWidth = ActualWidth;
|
|
if (parentWidth > 0 && shadowWidth > 0)
|
|
{
|
|
if (parentWidth < shadowWidth + margin.Left + margin.Right)
|
|
{
|
|
var leftRightMargin = (parentWidth - shadowWidth) / 2;
|
|
var adjustedMargin = new Thickness(leftRightMargin, margin.Top, leftRightMargin, margin.Bottom);
|
|
var marginAnim = new ThicknessAnimation(adjustedMargin, TimeSpan.Zero);
|
|
BeginAnimation(MarginProperty, marginAnim);
|
|
UpdateLayout();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ClearMarginAdjustment()
|
|
{
|
|
BeginAnimation(MarginProperty, null);
|
|
}
|
|
|
|
private void SetParentPopupControl(PopupControl value)
|
|
{
|
|
if (_parentPopupControl == value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_popupPositioner != null)
|
|
{
|
|
_popupPositioner.Dispose();
|
|
_popupPositioner = null!;
|
|
}
|
|
|
|
if (_parentPopupControl != null)
|
|
{
|
|
_parentPopupControl.Opened -= OnParentPopupControlOpened;
|
|
_parentPopupControl.Closed -= OnParentPopupControlClosed;
|
|
_parentPopupControl.ClearMargin();
|
|
_parentPopupControl.Dispose();
|
|
}
|
|
|
|
_parentPopupControl = value;
|
|
|
|
if (_parentPopupControl != null)
|
|
{
|
|
_parentPopupControl.Opened += OnParentPopupControlOpened;
|
|
_parentPopupControl.Closed += OnParentPopupControlClosed;
|
|
ApplyPopupMargin();
|
|
}
|
|
}
|
|
|
|
private void OnParentPopupControlOpened(object? sender, EventArgs e)
|
|
{
|
|
if (_popupPositioner != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_parentPopupControl != null)
|
|
{
|
|
if (_parentPopupControl.Control is { } control)
|
|
{
|
|
if (control is ToolTip toolTip && toolTip.PlacementTarget is Thumb thumb && thumb.TemplatedParent is Slider)
|
|
{
|
|
// Do not reposition slider auto tool tip
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
var popup = control as Popup ?? control.Parent as Popup;
|
|
if (popup != null && PopupPositioner.IsSupported)
|
|
{
|
|
_popupPositioner = new PopupPositioner(popup);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_popupPositioner == null)
|
|
{
|
|
PositionParentPopupControl();
|
|
}
|
|
}
|
|
|
|
private void OnParentPopupControlClosed(object? sender, EventArgs e)
|
|
{
|
|
ClearMarginAdjustment();
|
|
ResetTransform();
|
|
}
|
|
|
|
private void PositionParentPopupControl()
|
|
{
|
|
var popup = _parentPopupControl;
|
|
if (popup != null)
|
|
{
|
|
Debug.Assert(IsShadowEnabled);
|
|
|
|
CustomPlacementMode? placement = null;
|
|
|
|
switch (popup.Placement)
|
|
{
|
|
case PlacementMode.Bottom:
|
|
placement = CustomPlacementMode.BottomEdgeAlignedLeft;
|
|
break;
|
|
|
|
case PlacementMode.Top:
|
|
placement = CustomPlacementMode.TopEdgeAlignedLeft;
|
|
break;
|
|
|
|
case PlacementMode.Custom:
|
|
if (TryGetCustomPlacementMode(out var customPlacement))
|
|
{
|
|
placement = customPlacement;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (placement.HasValue)
|
|
{
|
|
if (!EnsureEdgesAligned(placement.Value))
|
|
{
|
|
if (placement == CustomPlacementMode.BottomEdgeAlignedLeft)
|
|
{
|
|
if (shouldAlignRightEdges())
|
|
{
|
|
EnsureEdgesAligned(CustomPlacementMode.BottomEdgeAlignedRight);
|
|
}
|
|
}
|
|
else if (placement == CustomPlacementMode.TopEdgeAlignedLeft)
|
|
{
|
|
if (shouldAlignRightEdges())
|
|
{
|
|
EnsureEdgesAligned(CustomPlacementMode.TopEdgeAlignedRight);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool shouldAlignRightEdges()
|
|
{
|
|
var target = popup.PlacementTarget;
|
|
if (target != null)
|
|
{
|
|
var targetWidth = target.RenderSize.Width;
|
|
if (ActualWidth > 0 && targetWidth > 0)
|
|
{
|
|
if (ActualWidth == targetWidth)
|
|
{
|
|
return true;
|
|
}
|
|
else if (ActualWidth > targetWidth)
|
|
{
|
|
if (TryGetOffsetToTarget(InterestPoint.TopRight, InterestPoint.TopRight, out var offset))
|
|
{
|
|
if (offset.X < 0)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool TryGetCustomPlacementMode(out CustomPlacementMode placement)
|
|
{
|
|
if (TryGetCustomPlacementMode(_parentPopupControl?.Control!, out placement))
|
|
{
|
|
return true;
|
|
}
|
|
if (TryGetCustomPlacementMode(VisualParent, out placement))
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private bool TryGetCustomPlacementMode(DependencyObject element, out CustomPlacementMode placement)
|
|
{
|
|
if (element != null &&
|
|
element.ReadLocalValue(CustomPopupPlacementHelper.PlacementProperty) != DependencyProperty.UnsetValue)
|
|
{
|
|
placement = CustomPopupPlacementHelper.GetPlacement(element);
|
|
return true;
|
|
}
|
|
|
|
placement = default;
|
|
return false;
|
|
}
|
|
|
|
private bool TryGetOffsetToTarget(
|
|
InterestPoint targetInterestPoint,
|
|
InterestPoint childInterestPoint,
|
|
out Vector offset)
|
|
{
|
|
var popup = _parentPopupControl;
|
|
if (popup != null)
|
|
{
|
|
var target = popup.PlacementTarget;
|
|
if (target != null)
|
|
{
|
|
if (IsVisible && target.IsVisible)
|
|
{
|
|
offset = Helper.GetOffset(this, childInterestPoint, target, targetInterestPoint, popup.PlacementRectangle);
|
|
|
|
if (Math.Abs(offset.X) < 0.5)
|
|
{
|
|
offset.X = 0;
|
|
}
|
|
|
|
if (Math.Abs(offset.Y) < 0.5)
|
|
{
|
|
offset.Y = 0;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
offset = default;
|
|
return false;
|
|
}
|
|
|
|
private bool EnsureEdgesAligned(CustomPlacementMode placement)
|
|
{
|
|
Vector offsetToTarget;
|
|
var translation = s_noTranslation;
|
|
|
|
switch (placement)
|
|
{
|
|
case CustomPlacementMode.TopEdgeAlignedLeft:
|
|
if (TryGetOffsetToTarget(InterestPoint.TopLeft, InterestPoint.BottomLeft, out offsetToTarget))
|
|
{
|
|
translation = getTranslation(true, true, offsetToTarget);
|
|
}
|
|
break;
|
|
|
|
case CustomPlacementMode.TopEdgeAlignedRight:
|
|
if (TryGetOffsetToTarget(InterestPoint.TopRight, InterestPoint.BottomRight, out offsetToTarget))
|
|
{
|
|
translation = getTranslation(true, false, offsetToTarget);
|
|
}
|
|
break;
|
|
|
|
case CustomPlacementMode.BottomEdgeAlignedLeft:
|
|
if (TryGetOffsetToTarget(InterestPoint.BottomLeft, InterestPoint.TopLeft, out offsetToTarget))
|
|
{
|
|
translation = getTranslation(false, true, offsetToTarget);
|
|
}
|
|
break;
|
|
|
|
case CustomPlacementMode.BottomEdgeAlignedRight:
|
|
if (TryGetOffsetToTarget(InterestPoint.BottomRight, InterestPoint.TopRight, out offsetToTarget))
|
|
{
|
|
translation = getTranslation(false, false, offsetToTarget);
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (translation != s_noTranslation)
|
|
{
|
|
SetupTransform(translation);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
ResetTransform();
|
|
return false;
|
|
}
|
|
|
|
Vector getTranslation(bool top, bool left, Vector offset)
|
|
{
|
|
double offsetX = 0;
|
|
double offsetY = 0;
|
|
|
|
if (left && offset.X > 0 ||
|
|
!left && offset.X < 0 ||
|
|
Math.Abs(offset.X) < 0.5)
|
|
{
|
|
offsetX = -offset.X;
|
|
}
|
|
|
|
if (top && offset.Y < PopupMargin.Top ||
|
|
!top && offset.Y > -PopupMargin.Bottom ||
|
|
Math.Abs(offset.Y) < 0.5)
|
|
{
|
|
offsetY = -offset.Y;
|
|
}
|
|
|
|
return new Vector(offsetX, offsetY);
|
|
}
|
|
}
|
|
|
|
private void SetupTransform(Vector translation)
|
|
{
|
|
if (_transform == null)
|
|
{
|
|
_transform = new TranslateTransform();
|
|
RenderTransform = _transform;
|
|
}
|
|
_transform.X = translation.X;
|
|
_transform.Y = translation.Y;
|
|
}
|
|
|
|
private void ResetTransform()
|
|
{
|
|
if (_transform != null)
|
|
{
|
|
_transform.ClearValue(TranslateTransform.XProperty);
|
|
_transform.ClearValue(TranslateTransform.YProperty);
|
|
}
|
|
}
|
|
|
|
private Popup FindParentPopup(FrameworkElement element)
|
|
{
|
|
var parent = element.Parent;
|
|
if (parent is Popup popup)
|
|
{
|
|
return popup;
|
|
}
|
|
else if (parent is FrameworkElement fe)
|
|
{
|
|
return FindParentPopup(fe);
|
|
}
|
|
else
|
|
{
|
|
if (VisualTreeHelper.GetParent(element) is FrameworkElement visualParent)
|
|
{
|
|
return FindParentPopup(visualParent);
|
|
}
|
|
}
|
|
return null!;
|
|
}
|
|
|
|
private class PopupControl : IDisposable
|
|
{
|
|
private ContextMenu _contextMenu = null!;
|
|
private ToolTip _toolTip = null!;
|
|
private Popup _popup = null!;
|
|
|
|
public PopupControl(ContextMenu contextMenu)
|
|
{
|
|
_contextMenu = contextMenu;
|
|
_contextMenu.Opened += OnOpened;
|
|
_contextMenu.Closed += OnClosed;
|
|
}
|
|
|
|
public PopupControl(ToolTip toolTip)
|
|
{
|
|
_toolTip = toolTip;
|
|
_toolTip.Opened += OnOpened;
|
|
_toolTip.Closed += OnClosed;
|
|
}
|
|
|
|
public PopupControl(Popup popup)
|
|
{
|
|
_popup = popup;
|
|
_popup.Opened += OnOpened;
|
|
_popup.Closed += OnClosed;
|
|
}
|
|
|
|
public FrameworkElement Control =>
|
|
_contextMenu as FrameworkElement ??
|
|
_toolTip as FrameworkElement ??
|
|
_popup as FrameworkElement;
|
|
|
|
public PlacementMode Placement
|
|
{
|
|
get
|
|
{
|
|
if (_contextMenu != null)
|
|
{
|
|
return _contextMenu.Placement;
|
|
}
|
|
if (_toolTip != null)
|
|
{
|
|
return _toolTip.Placement;
|
|
}
|
|
if (_popup != null)
|
|
{
|
|
return _popup.Placement;
|
|
}
|
|
return default;
|
|
}
|
|
}
|
|
|
|
public UIElement PlacementTarget
|
|
{
|
|
get
|
|
{
|
|
if (_contextMenu != null)
|
|
{
|
|
return _contextMenu.PlacementTarget;
|
|
}
|
|
if (_toolTip != null)
|
|
{
|
|
return _toolTip.PlacementTarget;
|
|
}
|
|
if (_popup != null)
|
|
{
|
|
return (_popup.PlacementTarget ??
|
|
VisualTreeHelper.GetParent(_popup) as UIElement)!;
|
|
}
|
|
return null!;
|
|
}
|
|
}
|
|
|
|
public Rect PlacementRectangle
|
|
{
|
|
get
|
|
{
|
|
if (_contextMenu != null)
|
|
{
|
|
return _contextMenu.PlacementRectangle;
|
|
}
|
|
if (_toolTip != null)
|
|
{
|
|
return _toolTip.PlacementRectangle;
|
|
}
|
|
if (_popup != null)
|
|
{
|
|
return _popup.PlacementRectangle;
|
|
}
|
|
return Rect.Empty;
|
|
}
|
|
}
|
|
|
|
private FrameworkElement? ChildAsFE =>
|
|
_contextMenu as FrameworkElement ??
|
|
_toolTip as FrameworkElement ??
|
|
_popup?.Child as FrameworkElement;
|
|
|
|
public event EventHandler? Opened;
|
|
|
|
public event EventHandler? Closed;
|
|
|
|
public void SetMargin(Thickness margin)
|
|
{
|
|
var child = ChildAsFE;
|
|
if (child != null)
|
|
{
|
|
child.Margin = margin;
|
|
}
|
|
}
|
|
|
|
public void ClearMargin()
|
|
{
|
|
ChildAsFE?.ClearValue(MarginProperty);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_contextMenu != null)
|
|
{
|
|
_contextMenu.Opened -= OnOpened;
|
|
_contextMenu.Closed -= OnClosed;
|
|
_contextMenu = null!;
|
|
}
|
|
else if (_toolTip != null)
|
|
{
|
|
_toolTip.Opened -= OnOpened;
|
|
_toolTip.Closed -= OnClosed;
|
|
_toolTip = null!;
|
|
}
|
|
else if (_popup != null)
|
|
{
|
|
_popup.Opened -= OnOpened;
|
|
_popup.Closed -= OnClosed;
|
|
_popup = null!;
|
|
}
|
|
}
|
|
|
|
private void OnOpened(object? sender, EventArgs e)
|
|
{
|
|
Opened?.Invoke(this, e);
|
|
}
|
|
|
|
private void OnClosed(object? sender, EventArgs e)
|
|
{
|
|
Closed?.Invoke(this, e);
|
|
}
|
|
}
|
|
|
|
private readonly Grid _background = null!;
|
|
private readonly BitmapCache _bitmapCache = null!;
|
|
private Border _shadow1 = null!;
|
|
private Border _shadow2 = null!;
|
|
private PopupControl _parentPopupControl = null!;
|
|
private TranslateTransform _transform = null!;
|
|
private PopupPositioner _popupPositioner = null!;
|
|
|
|
private static readonly Brush s_bg1, s_bg2, s_bg3, s_bg4;
|
|
private static readonly Vector s_noTranslation = new(0, 0);
|
|
}
|
|
|
|
internal static class PopupHelper
|
|
{
|
|
public static void Reposition(this Popup popup)
|
|
{
|
|
if (popup is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(popup));
|
|
}
|
|
|
|
if (PopupPositioner.GetPositioner(popup) is { } positioner)
|
|
{
|
|
positioner.Reposition();
|
|
}
|
|
else
|
|
{
|
|
if (s_reposition.Value is { } reposition)
|
|
{
|
|
reposition(popup);
|
|
}
|
|
else
|
|
{
|
|
var offset = popup.HorizontalOffset;
|
|
popup.SetCurrentValue(Popup.HorizontalOffsetProperty, offset + 0.1);
|
|
popup.InvalidateProperty(Popup.HorizontalOffsetProperty);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static Action<Popup> CreateRepositionDelegate()
|
|
{
|
|
try
|
|
{
|
|
return DelegateHelper.CreateDelegate<Action<Popup>>(
|
|
typeof(Popup),
|
|
nameof(Reposition),
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
}
|
|
catch
|
|
{
|
|
return null!;
|
|
}
|
|
}
|
|
|
|
private static readonly Lazy<Action<Popup>> s_reposition = new Lazy<Action<Popup>>(CreateRepositionDelegate);
|
|
}
|
|
|
|
file static class Helper
|
|
{
|
|
public static Vector GetOffset(
|
|
UIElement element1,
|
|
InterestPoint interestPoint1,
|
|
UIElement element2,
|
|
InterestPoint interestPoint2,
|
|
Rect element2Bounds)
|
|
{
|
|
var point = element1.TranslatePoint(GetPoint(element1, interestPoint1), element2);
|
|
if (element2Bounds.IsEmpty)
|
|
{
|
|
return point - GetPoint(element2, interestPoint2);
|
|
}
|
|
else
|
|
{
|
|
return point - GetPoint(element2Bounds, interestPoint2);
|
|
}
|
|
}
|
|
|
|
private static Point GetPoint(UIElement element, InterestPoint interestPoint)
|
|
{
|
|
return GetPoint(new Rect(element.RenderSize), interestPoint);
|
|
}
|
|
|
|
private static Point GetPoint(Rect rect, InterestPoint interestPoint)
|
|
{
|
|
switch (interestPoint)
|
|
{
|
|
case InterestPoint.TopLeft:
|
|
return rect.TopLeft;
|
|
|
|
case InterestPoint.TopRight:
|
|
return rect.TopRight;
|
|
|
|
case InterestPoint.BottomLeft:
|
|
return rect.BottomLeft;
|
|
|
|
case InterestPoint.BottomRight:
|
|
return rect.BottomRight;
|
|
|
|
case InterestPoint.Center:
|
|
return new Point(rect.Left + rect.Width / 2,
|
|
rect.Top + rect.Height / 2);
|
|
|
|
default:
|
|
throw new ArgumentOutOfRangeException(nameof(interestPoint));
|
|
}
|
|
}
|
|
}
|