feat: implement multi layer dialog.

This commit is contained in:
rabbitism
2024-01-23 22:48:30 +08:00
parent 61ebba897b
commit c8540feeb3
15 changed files with 231 additions and 104 deletions

View File

@@ -7,10 +7,11 @@
x:CompileBindings="True"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ursa.Demo.Dialogs.DialogWithAction">
<StackPanel Margin="0 16 0 0">
<TextBlock Text="{Binding Title}"></TextBlock>
<StackPanel Margin="8">
<TextBlock Classes="Strong" Margin="8" Text="{Binding Title}"></TextBlock>
<Calendar SelectedDate="{Binding Date}" ></Calendar>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" Spacing="20">
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" Spacing="8">
<Button Content="Dialog" Command="{Binding DialogCommand}"></Button>
<Button Content="OK" Command="{Binding OKCommand}"></Button>
<Button Content="Cancel" Command="{Binding CancelCommand}"></Button>
</StackPanel>

View File

@@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;
using System.Windows.Input;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -16,10 +17,13 @@ public partial class DialogWithActionViewModel: ObservableObject, IDialogContext
public ICommand OKCommand { get; set; }
public ICommand CancelCommand { get; set; }
public ICommand DialogCommand { get; set; }
public DialogWithActionViewModel()
{
OKCommand = new RelayCommand(OK);
CancelCommand = new RelayCommand(Cancel);
DialogCommand = new AsyncRelayCommand(ShowDialog);
Title = "Please select a date";
Date = DateTime.Now;
}
@@ -33,4 +37,9 @@ public partial class DialogWithActionViewModel: ObservableObject, IDialogContext
{
Closed?.Invoke(this, false);
}
private async Task ShowDialog()
{
await DialogBox.ShowOverlayModalAsync<DialogWithAction, DialogWithActionViewModel, bool>(new DialogWithActionViewModel(), "GlobalHost");
}
}

View File

@@ -21,15 +21,16 @@
<Run Text="Date: "></Run>
<Run Text="{Binding DialogViewModel.Date}"></Run>
</TextBlock>
<Button Command="{Binding ShowGlobalDialogCommand}">Show Dialog</Button>
<Button Command="{Binding ShowLocalOverlayDialogCommand}">Show Local Overlay Dialog</Button>
<Button Command="{Binding ShowGlobalOverlayDialogCommand}"> Show Global Overlay Dialog </Button>
<Button Command="{Binding ShowGlobalModalDialogCommand}">Show Modal Dialog</Button>
<Button Command="{Binding ShowLocalOverlayModalDialogCommand}">Show Local Overlay Modal Dialog</Button>
<Button Command="{Binding ShowGlobalOverlayModalDialogCommand}"> Show Global Overlay Modal Dialog </Button>
<Button Command="{Binding ShowGlobalOverlayDialogCommand}">Show Global Overlay Dialog</Button>
</StackPanel>
<Grid Grid.Column="1">
<Button
HorizontalAlignment="Center"
VerticalAlignment="Center"
Command="{Binding ShowLocalOverlayDialogCommand}">
Command="{Binding ShowLocalOverlayModalDialogCommand}">
Show Local Overlay Dialog
</Button>
<u:OverlayDialogHost HostId="LocalHost" />

View File

@@ -11,9 +11,11 @@ namespace Ursa.Demo.ViewModels;
public class DialogDemoViewModel: ObservableObject
{
public ICommand ShowLocalOverlayDialogCommand { get; }
public ICommand ShowLocalOverlayModalDialogCommand { get; }
public ICommand ShowGlobalOverlayModalDialogCommand { get; }
public ICommand ShowGlobalModalDialogCommand { get; }
public ICommand ShowGlobalOverlayDialogCommand { get; }
public ICommand ShowGlobalDialogCommand { get; }
private object? _result;
@@ -35,27 +37,33 @@ public class DialogDemoViewModel: ObservableObject
public DialogDemoViewModel()
{
ShowLocalOverlayDialogCommand = new AsyncRelayCommand(ShowLocalOverlayDialog);
ShowGlobalOverlayDialogCommand = new AsyncRelayCommand(ShowGlobalOverlayDialog);
ShowGlobalDialogCommand = new AsyncRelayCommand(ShowGlobalDialog);
ShowLocalOverlayModalDialogCommand = new AsyncRelayCommand(ShowLocalOverlayModalDialog);
ShowGlobalOverlayModalDialogCommand = new AsyncRelayCommand(ShowGlobalOverlayModalDialog);
ShowGlobalModalDialogCommand = new AsyncRelayCommand(ShowGlobalModalDialog);
ShowGlobalOverlayDialogCommand = new RelayCommand(ShowGlobalOverlayDialog);
}
private async Task ShowGlobalDialog()
private void ShowGlobalOverlayDialog()
{
var result = await DialogBox.ShowAsync<DialogWithAction, DialogWithActionViewModel, bool>(DialogViewModel);
DialogBox.ShowOverlay<DialogWithAction, DialogWithActionViewModel>(new DialogWithActionViewModel(), "GlobalHost");
}
private async Task ShowGlobalModalDialog()
{
var result = await DialogBox.ShowModalAsync<DialogWithAction, DialogWithActionViewModel, bool>(DialogViewModel);
Result = result;
}
private async Task ShowGlobalOverlayDialog()
private async Task ShowGlobalOverlayModalDialog()
{
Result = await DialogBox.ShowOverlayAsync<DialogWithAction, DialogWithActionViewModel, bool>(DialogViewModel, "GlobalHost");
Result = await DialogBox.ShowOverlayModalAsync<DialogWithAction, DialogWithActionViewModel, bool>(DialogViewModel, "GlobalHost");
}
private async Task ShowLocalOverlayDialog()
private async Task ShowLocalOverlayModalDialog()
{
var vm = new DialogWithActionViewModel();
var result = await DialogBox.ShowOverlayAsync<DialogWithAction, DialogWithActionViewModel, bool>(
DialogViewModel, "LocalHost");
var result = await DialogBox.ShowOverlayModalAsync<DialogWithAction, DialogWithActionViewModel, bool>(
DialogViewModel, "LocalHost", new DialogOptions(){ ExtendToClientArea = true });
Result = result;
}
}

View File

@@ -3,38 +3,58 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:u="https://irihi.tech/ursa">
<!-- Add Resources Here -->
<ControlTheme x:Key="{x:Type u:OverlayDialogHost}" TargetType="u:OverlayDialogHost">
<Setter Property="OverlayMaskBrush" Value="{DynamicResource OverlayDialogMaskBrush}"></Setter>
</ControlTheme>
<ControlTheme x:Key="{x:Type u:DialogControl}" TargetType="u:DialogControl">
<Setter Property="Template">
<ControlTemplate TargetType="u:DialogControl">
<Border
Padding="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Classes="Hover"
IsHitTestVisible="True"
Padding="2"
Theme="{DynamicResource CardBorder}">
<Grid RowDefinitions="Auto, *, Auto">
<DockPanel
Name="{x:Static u:DialogControl.PART_TitleArea}"
Grid.Row="0"
LastChildFill="False"
Background="Transparent">
<TextBlock DockPanel.Dock="Left" Text="Title" Margin="8 8 0 0" />
<ContentPresenter
Name="PART_ContentPresenter"
Grid.Row="1"
Content="{TemplateBinding Content}" />
<Grid Grid.Row="0" ColumnDefinitions="*, Auto">
<Panel
Name="{x:Static u:DialogControl.PART_TitleArea}"
Grid.Column="0"
Grid.ColumnSpan="2"
Background="Transparent" >
<Panel.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Bring to Top"></MenuItem>
</MenuFlyout>
</Panel.ContextFlyout>
</Panel>
<TextBlock
Grid.Column="0"
Margin="8,8,0,0"
DockPanel.Dock="Left"
Text="{TemplateBinding Title}"
Classes="Strong"
IsVisible="{TemplateBinding Title, Converter={x:Static ObjectConverters.IsNotNull}}"
/>
<Button
Name="{x:Static u:MessageBoxWindow.PART_CloseButton}"
DockPanel.Dock="Right"
Grid.Column="1"
Margin="0,4,4,0"
Theme="{DynamicResource CloseButton}">
</Button>
</DockPanel>
<ContentPresenter
Grid.Row="0"
Grid.RowSpan="3"
Content="{TemplateBinding Content}" />
DockPanel.Dock="Right"
Theme="{DynamicResource CloseButton}" />
</Grid>
</Grid>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:extend /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Grid.Row" Value="0" />
<Setter Property="Grid.RowSpan" Value="3" />
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type u:DialogWindow}" TargetType="u:DialogWindow">
@@ -69,16 +89,17 @@
<Panel Margin="{TemplateBinding WindowDecorationMargin}" Background="Transparent" />
<ChromeOverlayLayer />
<Grid RowDefinitions="Auto, *, Auto">
<Button
Name="{x:Static u:DialogWindow.PART_CloseButton}"
HorizontalAlignment="Right"
VerticalAlignment="Top">
Close
</Button>
<ContentPresenter
Grid.Row="0"
Grid.RowSpan="2"
Content="{TemplateBinding Content}" />
<Button
Name="{x:Static u:DialogWindow.PART_CloseButton}"
Grid.Row="0"
Margin="0,4,4,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Theme="{DynamicResource CloseButton}" />
</Grid>
</Panel>
</ControlTemplate>

View File

@@ -0,0 +1,5 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Add Resources Here -->
<SolidColorBrush x:Key="OverlayDialogMaskBrush" Color="#FFA7ABB0" Opacity="0.2"></SolidColorBrush>
</ResourceDictionary>

View File

@@ -4,6 +4,7 @@
<MergeResourceInclude Source="Badge.axaml" />
<MergeResourceInclude Source="Banner.axaml" />
<MergeResourceInclude Source="ButtonGroup.axaml" />
<MergeResourceInclude Source="Dialog.axaml" />
<MergeResourceInclude Source="Divider.axaml" />
<MergeResourceInclude Source="DualBadge.axaml" />
<MergeResourceInclude Source="IPv4Box.axaml" />

View File

@@ -0,0 +1,5 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Add Resources Here -->
<SolidColorBrush x:Key="OverlayDialogMaskBrush" Color="#FF555B61" Opacity="0.2"></SolidColorBrush>
</ResourceDictionary>

View File

@@ -4,6 +4,7 @@
<MergeResourceInclude Source="Badge.axaml" />
<MergeResourceInclude Source="Banner.axaml" />
<MergeResourceInclude Source="ButtonGroup.axaml" />
<MergeResourceInclude Source="Dialog.axaml" />
<MergeResourceInclude Source="Divider.axaml" />
<MergeResourceInclude Source="DualBadge.axaml" />
<MergeResourceInclude Source="IPv4Box.axaml" />

View File

@@ -1,15 +0,0 @@
namespace Ursa.Common;
public enum DialogIcon
{
Asterisk, // Same as Information
Error,
Exclamation, // Same as Warning
Hand, // Same as Error
Information,
None,
Question,
Stop, // Same as Error
Warning,
Success,
}

View File

@@ -8,7 +8,7 @@ namespace Ursa.Controls;
public static class DialogBox
{
public static async Task<TResult?> ShowAsync<TView, TViewModel, TResult>(TViewModel vm)
public static async Task<TResult?> ShowModalAsync<TView, TViewModel, TResult>(TViewModel vm)
where TView : Control, new()
{
var window = new DialogWindow()
@@ -37,28 +37,68 @@ public static class DialogBox
}
}
public static async Task<TResult> ShowAsync<TView, TViewModel, TResult>(Window owner, TViewModel vm) where
public static async Task<TResult> ShowModalAsync<TView, TViewModel, TResult>(Window owner, TViewModel vm) where
TView: Control, new()
{
var window = new DialogWindow();
window.Content = new TView();
window.DataContext = vm;
var window = new DialogWindow
{
Content = new TView() { DataContext = vm },
DataContext = vm
};
return await window.ShowDialog<TResult>(owner);
}
public static Task<TResult> ShowOverlayAsync<TView, TViewModel, TResult>(TViewModel vm, string hostId)
public static Task<TResult> ShowOverlayModalAsync<TView, TViewModel, TResult>(TViewModel vm, string hostId)
where TView : Control, new()
where TViewModel: new()
{
var t = new DialogControl()
{
Content = new TView(){ DataContext = vm },
DataContext = vm,
};
t.DataContext = vm;
var host = OverlayDialogManager.GetOverlayDialogHost(hostId);
host?.AddDialog(t);
host?.AddModalDialog(t);
return t.ShowAsync<TResult>();
}
public static Task<TResult> ShowOverlayModalAsync<TView, TViewModel, TResult>(TViewModel vm, string hostId,
DialogOptions options)
where TView : Control, new()
{
var t = new DialogControl()
{
Content = new TView() { DataContext = vm },
DataContext = vm,
ExtendToClientArea = options.ExtendToClientArea,
Title = options.Title,
};
var host = OverlayDialogManager.GetOverlayDialogHost(hostId);
host?.AddModalDialog(t);
return t.ShowAsync<TResult>();
}
public static void ShowOverlay<TView, TViewModel>(TViewModel vm, string hostId)
where TView: Control, new()
{
var t = new DialogControl()
{
Content = new TView() { DataContext = vm },
DataContext = vm,
};
var host = OverlayDialogManager.GetOverlayDialogHost(hostId);
host?.AddDialog(t);
}
public static void ShowOverlay<TView, TViewModel>(TViewModel vm, string hostId, DialogOptions options)
where TView: Control, new()
{
var t = new DialogControl()
{
Content = new TView() { DataContext = vm },
DataContext = vm,
};
var host = OverlayDialogManager.GetOverlayDialogHost(hostId);
host?.AddModalDialog(t);
}
}

View File

@@ -9,18 +9,39 @@ namespace Ursa.Controls;
[TemplatePart(PART_CloseButton, typeof(Button))]
[TemplatePart(PART_TitleArea, typeof(Panel))]
[PseudoClasses(PC_ExtendClientArea)]
public class DialogControl: ContentControl
{
public const string PART_CloseButton = "PART_CloseButton";
public const string PART_TitleArea = "PART_TitleArea";
public const string PC_ExtendClientArea = ":extend";
private Button? _closeButton;
private Panel? _titleArea;
public event EventHandler<object?>? OnClose;
public static readonly StyledProperty<string?> TitleProperty = AvaloniaProperty.Register<DialogControl, string?>(
nameof(Title));
public string? Title
{
get => GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public static readonly StyledProperty<bool> ExtendToClientAreaProperty = AvaloniaProperty.Register<DialogControl, bool>(
nameof(ExtendToClientArea));
public bool ExtendToClientArea
{
get => GetValue(ExtendToClientAreaProperty);
set => SetValue(ExtendToClientAreaProperty, value);
}
static DialogControl()
{
DataContextProperty.Changed.AddClassHandler<DialogControl, object?>((o, e) => o.OnDataContextChange(e));
ExtendToClientAreaProperty.Changed.AddClassHandler<DialogControl, bool>((o, e) => o.PseudoClasses.Set(PC_ExtendClientArea, e.NewValue.Value));
}
private void OnDataContextChange(AvaloniaPropertyChangedEventArgs<object?> args)
@@ -34,7 +55,6 @@ public class DialogControl: ContentControl
{
newContext.Closed += Close;
}
}

View File

@@ -1,6 +1,11 @@
using Ursa.Common;
namespace Ursa.Controls;
public record DialogOptions
{
public bool ShowCloseButton { get; set; } = true;
public string? Title { get; set; }
public bool ExtendToClientArea { get; set; } = false;
public DialogButton DefaultButtons { get; set; } = DialogButton.OKCancel;
}

View File

@@ -1,4 +1,5 @@
using Avalonia.Controls;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
@@ -15,21 +16,21 @@ public class DialogWindow: Window
private Button? _closeButton;
protected override void OnDataContextBeginUpdate()
static DialogWindow()
{
base.OnDataContextBeginUpdate();
if (DataContext is IDialogContext context)
{
context.Closed-= OnDialogClose;
}
DataContextProperty.Changed.AddClassHandler<DialogWindow, object?>((o, e) => o.OnDataContextChange(e));
}
protected override void OnDataContextEndUpdate()
private void OnDataContextChange(AvaloniaPropertyChangedEventArgs<object?> args)
{
base.OnDataContextEndUpdate();
if (DataContext is IDialogContext context)
if (args.OldValue.Value is IDialogContext oldContext)
{
context.Closed += OnDialogClose;
oldContext.Closed-= OnDialogClose;
}
if (args.NewValue.Value is IDialogContext newContext)
{
newContext.Closed += OnDialogClose;
}
}

View File

@@ -15,39 +15,44 @@ public class OverlayDialogHost: Canvas
{
private readonly List<DialogControl> _dialogs = new();
private readonly List<DialogControl> _modalDialogs = new();
private readonly List<Border> _masks = new();
public static readonly StyledProperty<string> HostIdProperty = AvaloniaProperty.Register<OverlayDialogHost, string>(
nameof(HostId));
public string HostId
{
get => GetValue(HostIdProperty);
set => SetValue(HostIdProperty, value);
}
public string? HostId { get; set; }
private Point _lastPoint;
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() => new()
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
Width = this.Bounds.Width,
Height = this.Bounds.Height,
[!BackgroundProperty] = this[!OverlayMaskBrushProperty],
IsVisible = true,
};
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
OverlayDialogManager.RegisterOverlayDialogHost(this, HostId);
this.Children.Add(new Border()
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
Background = Brushes.Black,
Opacity = 0.3,
IsVisible = false,
});
}
protected override void OnSizeChanged(SizeChangedEventArgs e)
{
base.OnSizeChanged(e);
if (this.Children.Count > 0)
for (int i = 0; i < _masks.Count; i++)
{
this.Children[0].Width = this.Bounds.Width;
this.Children[0].Height = this.Bounds.Height;
_masks[i].Width = this.Bounds.Width;
_masks[i].Width = this.Bounds.Height;
}
}
@@ -87,11 +92,8 @@ public class OverlayDialogHost: Canvas
public void AddDialog(DialogControl control)
{
this.Children.Add(control);
_dialogs.Add(control);
control.OnClose += OnDialogClose;
if (this.Children.Count > 1)
{
this.Children[0].IsVisible = true;
}
}
private void OnDialogClose(object sender, object? e)
@@ -100,16 +102,38 @@ public class OverlayDialogHost: Canvas
{
this.Children.Remove(control);
control.OnClose -= OnDialogClose;
if (this.Children.Count == 1)
if (_dialogs.Contains(control))
{
this.Children[0].IsVisible = false;
_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;
}
}
}
}
}
public void AddModalDialog(DialogControl control)
{
var mask = CreateOverlayMask();
this.Children.Add(mask);
this.Children.Add(control);
_modalDialogs.Add(control);
for (int i = 0; i < _masks.Count; i++)
{
_masks[i].IsVisible = false;
}
_masks.Add(mask);
control.OnClose += OnDialogClose;
}
}