feat: advanced dialog layout.

This commit is contained in:
rabbitism
2024-02-02 19:58:52 +08:00
parent 6e8ab8c16d
commit 75503b8055
9 changed files with 260 additions and 32 deletions

View File

@@ -119,6 +119,10 @@ public class DialogDemoViewModel: ObservableObject
Mode = SelectedMode,
Buttons = SelectedButton,
CanClickOnMaskToClose = CanCloseMaskToClose,
HorizontalAnchor = HorizontalPosition.Right,
HorizontalOffset = 50,
VerticalAnchor = VerticalPosition.Top,
VerticalOffset = 50,
}
);
Date = vm.Date;

View File

@@ -93,7 +93,7 @@
<converters:ViewLocator />
</ContentControl.ContentTemplate>
</ContentControl>
<u:OverlayDialogHost Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Grid.ColumnSpan="2"/>
<u:OverlayDialogHost Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Grid.ColumnSpan="2" SnapThickness="20"/>
</Grid>
</UserControl>

View File

@@ -46,6 +46,23 @@
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^ /template/ Panel#PART_TitleArea">
<Setter Property="ContextFlyout">
<MenuFlyout>
<MenuItem
Command="{Binding $parent[u:DialogControl].CloseDialog}"
CommandParameter="{x:Static u:DialogLayerChangeType.BringForward}"
Header="{DynamicResource STRING_MENU_DIALOG_CLOSE}">
<MenuItem.Icon>
<PathIcon
Width="12"
Height="12"
Data="{DynamicResource WindowCloseIconGlyph}" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Setter>
</Style>
<Style Selector="^:not(:modal) /template/ Panel#PART_TitleArea">
<Setter Property="ContextFlyout">
<MenuFlyout>
@@ -93,6 +110,17 @@
Data="{DynamicResource DialogArrangeSendToBackGlyph}" />
</MenuItem.Icon>
</MenuItem>
<MenuItem
Command="{Binding $parent[u:DialogControl].CloseDialog}"
CommandParameter="{x:Static u:DialogLayerChangeType.BringForward}"
Header="{DynamicResource STRING_MENU_DIALOG_CLOSE}">
<MenuItem.Icon>
<PathIcon
Width="12"
Height="12"
Data="{DynamicResource WindowCloseIconGlyph}" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Setter>
</Style>
@@ -115,11 +143,13 @@
Theme="{DynamicResource CardBorder}">
<Border ClipToBounds="True" CornerRadius="{TemplateBinding CornerRadius}">
<Grid RowDefinitions="Auto, *, Auto">
<ContentPresenter
Name="PART_ContentPresenter"
Grid.Row="1"
Margin="24,8"
Content="{TemplateBinding Content}" />
<ScrollViewer Grid.Row="1">
<ContentPresenter
Name="PART_ContentPresenter"
Grid.Row="1"
Margin="24,8"
Content="{TemplateBinding Content}" />
</ScrollViewer>
<Grid Grid.Row="0" ColumnDefinitions="Auto, *, Auto">
<Panel
Name="{x:Static u:DialogControl.PART_TitleArea}"
@@ -304,7 +334,23 @@
<Setter Property="theme:ClassHelper.Classes" Value="Tertiary" />
</Style>
</Style>
<Style Selector="^ /template/ Panel#PART_TitleArea">
<Setter Property="ContextFlyout">
<MenuFlyout>
<MenuItem
Command="{Binding $parent[u:DialogControl].CloseDialog}"
CommandParameter="{x:Static u:DialogLayerChangeType.BringForward}"
Header="{DynamicResource STRING_MENU_DIALOG_CLOSE}">
<MenuItem.Icon>
<PathIcon
Width="12"
Height="12"
Data="{DynamicResource WindowCloseIconGlyph}" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Setter>
</Style>
<Style Selector="^:not(:modal) /template/ Panel#PART_TitleArea">
<Setter Property="ContextFlyout">
<MenuFlyout>
@@ -352,6 +398,17 @@
Data="{DynamicResource DialogArrangeSendToBackGlyph}" />
</MenuItem.Icon>
</MenuItem>
<MenuItem
Command="{Binding $parent[u:DialogControl].CloseDialog}"
CommandParameter="{x:Static u:DialogLayerChangeType.BringForward}"
Header="{DynamicResource STRING_MENU_DIALOG_CLOSE}">
<MenuItem.Icon>
<PathIcon
Width="12"
Height="12"
Data="{DynamicResource WindowCloseIconGlyph}" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Setter>
</Style>

View File

@@ -81,7 +81,7 @@
</Grid>
<StackPanel
Grid.Row="2"
Margin="0,0,24,24"
Margin="24,0,24,24"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
@@ -244,6 +244,20 @@
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^ /template/ Panel#PART_TitleArea">
<Setter Property="ContextFlyout">
<MenuFlyout>
<MenuItem Command="{Binding $parent[u:DialogControl].CloseDialog}" Header="{DynamicResource STRING_MENU_DIALOG_CLOSE}">
<MenuItem.Icon>
<PathIcon
Width="12"
Height="12"
Data="{DynamicResource WindowCloseIconGlyph}" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Setter>
</Style>
<Style Selector="^[MessageIcon=None] /template/ PathIcon#PART_Icon">
<Setter Property="IsVisible" Value="False" />
</Style>

View File

@@ -9,4 +9,5 @@
<x:String x:Key="STRING_MENU_DIALOG_CANCEL">Cancel</x:String>
<x:String x:Key="STRING_MENU_DIALOG_YES">Yes</x:String>
<x:String x:Key="STRING_MENU_DIALOG_NO">No</x:String>
<x:String x:Key="STRING_MENU_DIALOG_CLOSE">Close</x:String>
</ResourceDictionary>

View File

@@ -9,4 +9,5 @@
<x:String x:Key="STRING_MENU_DIALOG_CANCEL">取消</x:String>
<x:String x:Key="STRING_MENU_DIALOG_YES">是</x:String>
<x:String x:Key="STRING_MENU_DIALOG_NO">否</x:String>
<x:String x:Key="STRING_MENU_DIALOG_CLOSE">关闭</x:String>
</ResourceDictionary>

View File

@@ -3,6 +3,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Interactivity;
using Ursa.Common;
@@ -22,8 +23,12 @@ public class DialogControl: ContentControl
internal HorizontalPosition HorizontalAnchor { get; set; }
internal VerticalPosition VerticalAnchor { get; set; }
internal double? InitialHorizontalOffset { get; set; }
internal double? InitialVerticalOffset { get; set; }
internal HorizontalPosition ActualHorizontalAnchor { get; set; }
internal VerticalPosition ActualVerticalAnchor { get; set; }
internal double? HorizontalOffset { get; set; }
internal double? VerticalOffset { get; set; }
internal double? HorizontalOffsetRatio { get; set; }
internal double? VerticalOffsetRatio { get; set; }
internal bool CanClickOnMaskToClose { get; set; }
public event EventHandler<DialogLayerChangeEventArgs>? LayerChanged;
@@ -55,6 +60,7 @@ public class DialogControl: ContentControl
_titleArea?.RemoveHandler(PointerMovedEvent, OnTitlePointerMove);
_titleArea?.RemoveHandler(PointerPressedEvent, OnTitlePointerPressed);
_titleArea?.RemoveHandler(PointerReleasedEvent, OnTitlePointerRelease);
_closeButton = e.NameScope.Find<Button>(PART_CloseButton);
_titleArea = e.NameScope.Find<Panel>(PART_TitleArea);
@@ -62,6 +68,7 @@ public class DialogControl: ContentControl
_titleArea?.AddHandler(PointerMovedEvent, OnTitlePointerMove, RoutingStrategies.Bubble);
_titleArea?.AddHandler(PointerPressedEvent, OnTitlePointerPressed, RoutingStrategies.Bubble);
_titleArea?.AddHandler(PointerReleasedEvent, OnTitlePointerRelease, RoutingStrategies.Bubble);
EventHelper.RegisterClickEvent(Close, _closeButton);
}
@@ -75,11 +82,16 @@ public class DialogControl: ContentControl
e.Source = this;
}
private void OnTitlePointerRelease(object sender, PointerReleasedEventArgs e)
{
e.Source = this;
}
public Task<T?> ShowAsync<T>(CancellationToken? token = default)
{
var tcs = new TaskCompletionSource<T?>();
token?.Register(Close);
token?.Register(CloseDialog);
void OnCloseHandler(object sender, object? args)
{
if (args is T result)
@@ -137,7 +149,7 @@ public class DialogControl: ContentControl
PseudoClasses.Set(PC_Modal, modal);
}
public void Close()
public void CloseDialog()
{
if (this.DataContext is IDialogContext context)
{

View File

@@ -183,9 +183,11 @@ public static class OverlayDialog
if (options is null) options = new OverlayDialogOptions();
control.HorizontalAnchor = options.HorizontalAnchor;
control.VerticalAnchor = options.VerticalAnchor;
control.InitialHorizontalOffset =
control.ActualHorizontalAnchor = options.HorizontalAnchor;
control.ActualVerticalAnchor = options.VerticalAnchor;
control.HorizontalOffset =
control.HorizontalAnchor == HorizontalPosition.Center ? null : options.HorizontalOffset;
control.InitialVerticalOffset =
control.VerticalOffset =
options.VerticalAnchor == VerticalPosition.Center ? null : options.VerticalOffset;
control.CanClickOnMaskToClose = options.CanClickOnMaskToClose;
}
@@ -195,9 +197,11 @@ public static class OverlayDialog
if (options is null) options = new OverlayDialogOptions();
control.HorizontalAnchor = options.HorizontalAnchor;
control.VerticalAnchor = options.VerticalAnchor;
control.InitialHorizontalOffset =
control.ActualHorizontalAnchor = options.HorizontalAnchor;
control.ActualVerticalAnchor = options.VerticalAnchor;
control.HorizontalOffset =
control.HorizontalAnchor == HorizontalPosition.Center ? null : options.HorizontalOffset;
control.InitialVerticalOffset =
control.VerticalOffset =
options.VerticalAnchor == VerticalPosition.Center ? null : options.VerticalOffset;
control.CanClickOnMaskToClose = options.CanClickOnMaskToClose;
control.Mode = options.Mode;

View File

@@ -1,4 +1,5 @@
using Avalonia;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
@@ -21,6 +22,7 @@ public class OverlayDialogHost : Canvas
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?>(
@@ -56,7 +58,7 @@ public class OverlayDialogHost : Canvas
{
int i = _masks.IndexOf(border);
DialogControl dialog = _modalDialogs[i];
dialog.Close();
dialog.CloseDialog();
border.RemoveHandler(PointerReleasedEvent, ClickBorderToCloseDialog);
}
}
@@ -75,6 +77,44 @@ public class OverlayDialogHost : Canvas
_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)
@@ -85,7 +125,6 @@ public class OverlayDialogHost : Canvas
protected override void OnPointerMoved(PointerEventArgs e)
{
base.OnPointerMoved(e);
if (e.Source is DialogControl item)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
@@ -95,28 +134,35 @@ public class OverlayDialogHost : Canvas
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);
Canvas.SetLeft(item, left);
Canvas.SetTop(item, top);
SetLeft(item, left);
SetTop(item, top);
}
}
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
// base.OnPointerPressed(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));
SetToCenter(control);
SetToPosition(control);
control.DialogControlClosing += OnDialogControlClosing;
control.LayerChanged += OnDialogLayerChanged;
ResetZIndices();
@@ -126,7 +172,7 @@ public class OverlayDialogHost : Canvas
{
if (sender is DialogControl control)
{
this.Children.Remove(control);
Children.Remove(control);
control.DialogControlClosing -= OnDialogControlClosing;
control.LayerChanged -= OnDialogLayerChanged;
if (_dialogs.Contains(control))
@@ -170,7 +216,7 @@ public class OverlayDialogHost : Canvas
this.Children.Add(control);
control.Measure(this.Bounds.Size);
control.Arrange(new Rect(control.DesiredSize));
SetToCenter(control);
SetToPosition(control);
control.DialogControlClosing += OnDialogControlClosing;
control.LayerChanged += OnDialogLayerChanged;
}
@@ -232,16 +278,105 @@ public class OverlayDialogHost : Canvas
}
}
private void SetToCenter(DialogControl? control)
private void SetToPosition(DialogControl? control)
{
// return;
if (control is null) return;
double left = (this.Bounds.Width - control.Bounds.Width) / 2;
double top = (this.Bounds.Height - control.Bounds.Height) / 2;
left = MathUtilities.Clamp(left, 0, Bounds.Width);
top = MathUtilities.Clamp(top, 0, Bounds.Height);
Canvas.SetLeft(control, left);
Canvas.SetTop(control, top);
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 > Bounds.Height - 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 > Bounds.Width - 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 / (left + right);
control.VerticalOffsetRatio = 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)