406 lines
13 KiB
C#
406 lines
13 KiB
C#
using Avalonia;
|
|
using Avalonia.Animation;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Controls.Templates;
|
|
using Avalonia.Input;
|
|
using Avalonia.Layout;
|
|
using Avalonia.Media;
|
|
using Avalonia.Utilities;
|
|
|
|
namespace Ursa.Controls;
|
|
|
|
public class OverlayDialogHost : Canvas
|
|
{
|
|
private readonly List<DialogControl> _dialogs = new();
|
|
private readonly List<DialogControl> _modalDialogs = new();
|
|
private readonly List<Border> _masks = new();
|
|
|
|
public string? HostId { get; set; }
|
|
|
|
private Point _lastPoint;
|
|
|
|
|
|
public DataTemplates DialogDataTemplates { get; set; } = new DataTemplates();
|
|
public Thickness SnapThickness { get; set; } = new Thickness(0);
|
|
|
|
public static readonly StyledProperty<IBrush?> OverlayMaskBrushProperty =
|
|
AvaloniaProperty.Register<OverlayDialogHost, IBrush?>(
|
|
nameof(OverlayMaskBrush));
|
|
|
|
public IBrush? OverlayMaskBrush
|
|
{
|
|
get => GetValue(OverlayMaskBrushProperty);
|
|
set => SetValue(OverlayMaskBrushProperty, value);
|
|
}
|
|
|
|
private Border CreateOverlayMask(bool canCloseOnClick)
|
|
{
|
|
Border border = new()
|
|
{
|
|
HorizontalAlignment = HorizontalAlignment.Stretch,
|
|
VerticalAlignment = VerticalAlignment.Stretch,
|
|
Width = this.Bounds.Width,
|
|
Height = this.Bounds.Height,
|
|
[!BackgroundProperty] = this[!OverlayMaskBrushProperty],
|
|
IsVisible = true,
|
|
};
|
|
if (canCloseOnClick)
|
|
{
|
|
border.AddHandler(PointerReleasedEvent, ClickBorderToCloseDialog);
|
|
}
|
|
return border;
|
|
}
|
|
|
|
private void ClickBorderToCloseDialog(object sender, PointerReleasedEventArgs e)
|
|
{
|
|
if (sender is Border border)
|
|
{
|
|
int i = _masks.IndexOf(border);
|
|
DialogControl dialog = _modalDialogs[i];
|
|
dialog.CloseDialog();
|
|
border.RemoveHandler(PointerReleasedEvent, ClickBorderToCloseDialog);
|
|
}
|
|
}
|
|
|
|
protected sealed override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
|
{
|
|
base.OnAttachedToVisualTree(e);
|
|
OverlayDialogManager.RegisterHost(this, HostId);
|
|
}
|
|
|
|
protected sealed override void OnSizeChanged(SizeChangedEventArgs e)
|
|
{
|
|
base.OnSizeChanged(e);
|
|
for (int i = 0; i < _masks.Count; i++)
|
|
{
|
|
_masks[i].Width = this.Bounds.Width;
|
|
_masks[i].Height = this.Bounds.Height;
|
|
}
|
|
|
|
var oldSize = e.PreviousSize;
|
|
var newSize = e.NewSize;
|
|
foreach (var dialog in _dialogs)
|
|
{
|
|
ResetDialogPosition(dialog, oldSize, newSize);
|
|
}
|
|
|
|
foreach (var modalDialog in _modalDialogs)
|
|
{
|
|
ResetDialogPosition(modalDialog, oldSize, newSize);
|
|
}
|
|
}
|
|
|
|
private void ResetDialogPosition(DialogControl control, Size oldSize, Size newSize)
|
|
{
|
|
var width = newSize.Width - control.Bounds.Width;
|
|
var height = newSize.Height - control.Bounds.Height;
|
|
var newLeft = width * control.HorizontalOffsetRatio??0;
|
|
var newTop = height * control.VerticalOffsetRatio??0;
|
|
if(control.ActualHorizontalAnchor == HorizontalPosition.Left)
|
|
{
|
|
newLeft = 0;
|
|
}
|
|
if (control.ActualHorizontalAnchor == HorizontalPosition.Right)
|
|
{
|
|
newLeft = newSize.Width - control.Bounds.Width;
|
|
}
|
|
if (control.ActualVerticalAnchor == VerticalPosition.Top)
|
|
{
|
|
newTop = 0;
|
|
}
|
|
if (control.ActualVerticalAnchor == VerticalPosition.Bottom)
|
|
{
|
|
newTop = newSize.Height - control.Bounds.Height;
|
|
}
|
|
SetLeft(control, Math.Max(0.0, newLeft));
|
|
SetTop(control, Math.Max(0.0, newTop));
|
|
}
|
|
|
|
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
|
{
|
|
OverlayDialogManager.UnregisterHost(HostId);
|
|
base.OnDetachedFromVisualTree(e);
|
|
}
|
|
|
|
protected override void OnPointerMoved(PointerEventArgs e)
|
|
{
|
|
if (e.Source is DialogControl item)
|
|
{
|
|
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
|
{
|
|
var p = e.GetPosition(this);
|
|
var left = p.X - _lastPoint.X;
|
|
var top = p.Y - _lastPoint.Y;
|
|
left = MathUtilities.Clamp(left, 0, Bounds.Width - item.Bounds.Width);
|
|
top = MathUtilities.Clamp(top, 0, Bounds.Height - item.Bounds.Height);
|
|
SetLeft(item, left);
|
|
SetTop(item, top);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
|
{
|
|
if (e.Source is DialogControl item)
|
|
{
|
|
_lastPoint = e.GetPosition(item);
|
|
}
|
|
}
|
|
|
|
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
|
{
|
|
if (e.Source is DialogControl item)
|
|
{
|
|
AnchorDialog(item);
|
|
}
|
|
}
|
|
|
|
internal void AddDialog(DialogControl control)
|
|
{
|
|
this.Children.Add(control);
|
|
_dialogs.Add(control);
|
|
control.Measure(this.Bounds.Size);
|
|
control.Arrange(new Rect(control.DesiredSize));
|
|
SetToPosition(control);
|
|
control.AddHandler(DialogControl.ClosedEvent, OnDialogControlClosing);
|
|
control.AddHandler(DialogControl.LayerChangedEvent, OnDialogLayerChanged);
|
|
ResetZIndices();
|
|
}
|
|
|
|
internal void AddDrawer(DrawerControlBase control)
|
|
{
|
|
|
|
}
|
|
|
|
private void OnDialogControlClosing(object sender, object? e)
|
|
{
|
|
if (sender is DialogControl control)
|
|
{
|
|
Children.Remove(control);
|
|
control.RemoveHandler(DialogControl.ClosedEvent, OnDialogControlClosing);
|
|
control.RemoveHandler(DialogControl.LayerChangedEvent, OnDialogLayerChanged);
|
|
if (_dialogs.Contains(control))
|
|
{
|
|
_dialogs.Remove(control);
|
|
}
|
|
else if (_modalDialogs.Contains(control))
|
|
{
|
|
_modalDialogs.Remove(control);
|
|
if (_masks.Count > 0)
|
|
{
|
|
var last = _masks.Last();
|
|
this.Children.Remove(last);
|
|
_masks.Remove(last);
|
|
if (_masks.Count > 0)
|
|
{
|
|
_masks.Last().IsVisible = true;
|
|
}
|
|
}
|
|
}
|
|
ResetZIndices();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a dialog as a modal dialog to the host
|
|
/// </summary>
|
|
/// <param name="control"></param>
|
|
internal void AddModalDialog(DialogControl control)
|
|
{
|
|
var mask = CreateOverlayMask(control.CanClickOnMaskToClose);
|
|
_masks.Add(mask);
|
|
_modalDialogs.Add(control);
|
|
control.SetAsModal(true);
|
|
for (int i = 0; i < _masks.Count-1; i++)
|
|
{
|
|
_masks[i].Opacity = 0.5;
|
|
}
|
|
ResetZIndices();
|
|
this.Children.Add(mask);
|
|
this.Children.Add(control);
|
|
control.Measure(this.Bounds.Size);
|
|
control.Arrange(new Rect(control.DesiredSize));
|
|
SetToPosition(control);
|
|
control.AddHandler(DialogControl.ClosedEvent, OnDialogControlClosing);
|
|
control.AddHandler(DialogControl.LayerChangedEvent, OnDialogLayerChanged);
|
|
}
|
|
|
|
// Handle dialog layer change event
|
|
private void OnDialogLayerChanged(object sender, DialogLayerChangeEventArgs e)
|
|
{
|
|
if (sender is not DialogControl control)
|
|
return;
|
|
if (!_dialogs.Contains(control))
|
|
return;
|
|
int index = _dialogs.IndexOf(control);
|
|
_dialogs.Remove(control);
|
|
int newIndex = index;
|
|
switch (e.ChangeType)
|
|
{
|
|
case DialogLayerChangeType.BringForward:
|
|
newIndex = MathUtilities.Clamp(index + 1, 0, _dialogs.Count);
|
|
break;
|
|
case DialogLayerChangeType.SendBackward:
|
|
newIndex = MathUtilities.Clamp(index - 1, 0, _dialogs.Count);
|
|
break;
|
|
case DialogLayerChangeType.BringToFront:
|
|
newIndex = _dialogs.Count;
|
|
break;
|
|
case DialogLayerChangeType.SendToBack:
|
|
newIndex = 0;
|
|
break;
|
|
}
|
|
|
|
_dialogs.Insert(newIndex, control);
|
|
for (int i = 0; i < _dialogs.Count; i++)
|
|
{
|
|
_dialogs[i].ZIndex = i;
|
|
}
|
|
|
|
for (int i = 0; i < _masks.Count * 2; i += 2)
|
|
{
|
|
_masks[i].ZIndex = _dialogs.Count + i;
|
|
_modalDialogs[i].ZIndex = _dialogs.Count + i + 1;
|
|
}
|
|
|
|
}
|
|
|
|
private void ResetZIndices()
|
|
{
|
|
int index = 0;
|
|
for ( int i = 0; i< _dialogs.Count; i++)
|
|
{
|
|
_dialogs[i].ZIndex = index;
|
|
index++;
|
|
}
|
|
for(int i = 0; i< _masks.Count; i++)
|
|
{
|
|
_masks[i].ZIndex = index;
|
|
index++;
|
|
_modalDialogs[i].ZIndex = index;
|
|
index++;
|
|
}
|
|
}
|
|
|
|
private void SetToPosition(DialogControl? control)
|
|
{
|
|
if (control is null) return;
|
|
double left = GetLeftPosition(control);
|
|
double top = GetTopPosition(control);
|
|
SetLeft(control, left);
|
|
SetTop(control, top);
|
|
AnchorDialog(control);
|
|
}
|
|
|
|
private void AnchorDialog(DialogControl control)
|
|
{
|
|
control.ActualHorizontalAnchor = HorizontalPosition.Center;
|
|
control.ActualVerticalAnchor = VerticalPosition.Center;
|
|
double left = GetLeft(control);
|
|
double top = GetTop(control);
|
|
double right = Bounds.Width - left - control.Bounds.Width;
|
|
double bottom = Bounds.Height - top - control.Bounds.Height;
|
|
if(top < SnapThickness.Top)
|
|
{
|
|
SetTop(control, 0);
|
|
control.ActualVerticalAnchor = VerticalPosition.Top;
|
|
control.VerticalOffsetRatio = 0;
|
|
}
|
|
if(bottom < SnapThickness.Bottom)
|
|
{
|
|
SetTop(control, Bounds.Height - control.Bounds.Height);
|
|
control.ActualVerticalAnchor = VerticalPosition.Bottom;
|
|
control.VerticalOffsetRatio = 1;
|
|
}
|
|
if(left < SnapThickness.Left)
|
|
{
|
|
SetLeft(control, 0);
|
|
control.ActualHorizontalAnchor = HorizontalPosition.Left;
|
|
control.HorizontalOffsetRatio = 0;
|
|
}
|
|
if(right < SnapThickness.Right)
|
|
{
|
|
SetLeft(control, Bounds.Width - control.Bounds.Width);
|
|
control.ActualHorizontalAnchor = HorizontalPosition.Right;
|
|
control.HorizontalOffsetRatio = 1;
|
|
}
|
|
left = GetLeft(control);
|
|
top = GetTop(control);
|
|
right = Bounds.Width - left - control.Bounds.Width;
|
|
bottom = Bounds.Height - top - control.Bounds.Height;
|
|
|
|
control.HorizontalOffsetRatio = (left + right) == 0 ? 0 : left / (left + right);
|
|
control.VerticalOffsetRatio = (top + bottom) == 0 ? 0 : top / (top + bottom);
|
|
}
|
|
|
|
private double GetLeftPosition(DialogControl control)
|
|
{
|
|
double left = 0;
|
|
double offset = Math.Max(0, control.HorizontalOffset ?? 0);
|
|
left = this.Bounds.Width - control.Bounds.Width;
|
|
if (control.HorizontalAnchor == HorizontalPosition.Center)
|
|
{
|
|
left *= 0.5;
|
|
(double min, double max) = MathUtilities.GetMinMax(0, Bounds.Width * 0.5);
|
|
left = MathUtilities.Clamp(left, min, max);
|
|
}
|
|
else if (control.HorizontalAnchor == HorizontalPosition.Left)
|
|
{
|
|
(double min, double max) = MathUtilities.GetMinMax(0, offset);
|
|
left = MathUtilities.Clamp(left, min, max);
|
|
}
|
|
else if (control.HorizontalAnchor == HorizontalPosition.Right)
|
|
{
|
|
double leftOffset = Bounds.Width - control.Bounds.Width - offset;
|
|
leftOffset = Math.Max(0, leftOffset);
|
|
if(control.HorizontalOffset.HasValue)
|
|
{
|
|
left = MathUtilities.Clamp(left, 0, leftOffset);
|
|
}
|
|
}
|
|
return left;
|
|
}
|
|
|
|
private double GetTopPosition(DialogControl control)
|
|
{
|
|
double top = 0;
|
|
double offset = Math.Max(0, control.VerticalOffset ?? 0);
|
|
top = this.Bounds.Height - control.Bounds.Height;
|
|
if (control.VerticalAnchor == VerticalPosition.Center)
|
|
{
|
|
top *= 0.5;
|
|
(double min, double max) = MathUtilities.GetMinMax(0, Bounds.Height * 0.5);
|
|
top = MathUtilities.Clamp(top, min, max);
|
|
}
|
|
else if (control.VerticalAnchor == VerticalPosition.Top)
|
|
{
|
|
top = MathUtilities.Clamp(top, 0, offset);
|
|
}
|
|
else if (control.VerticalAnchor == VerticalPosition.Bottom)
|
|
{
|
|
var topOffset = Math.Max(0, Bounds.Height - control.Bounds.Height - offset);
|
|
top = MathUtilities.Clamp(top, 0, topOffset);
|
|
}
|
|
return top;
|
|
}
|
|
|
|
internal IDataTemplate? GetDataTemplate(object? o)
|
|
{
|
|
if (o is null) return null;
|
|
IDataTemplate? result = null;
|
|
var templates = this.DialogDataTemplates;
|
|
result = templates.FirstOrDefault(a => a.Match(o));
|
|
if (result != null) return result;
|
|
var keys = this.Resources.Keys;
|
|
foreach (var key in keys)
|
|
{
|
|
if (Resources.TryGetValue(key, out var value) && value is IDataTemplate t)
|
|
{
|
|
result = t;
|
|
break;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
} |