Merge pull request #93 from irihitech/drawer

Drawer
This commit is contained in:
Dong Bin
2024-02-06 22:13:05 +08:00
committed by GitHub
40 changed files with 1590 additions and 444 deletions

View File

@@ -1,25 +1,43 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Ursa.Demo.Dialogs"
x:DataType="local:DialogWithActionViewModel"
x:CompileBindings="True"
Background="{DynamicResource SemiYellow1}"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ursa.Demo.Dialogs.DialogWithAction">
<StackPanel Margin="24">
<TextBlock FontSize="16" FontWeight="600" Margin="8" Text="{Binding Title}"></TextBlock>
<Calendar SelectedDate="{Binding Date}" ></Calendar>
<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>
<UserControl
x:Class="Ursa.Demo.Dialogs.DialogWithAction"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Ursa.Demo.Dialogs"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
d:DesignHeight="450"
d:DesignWidth="800"
x:CompileBindings="True"
x:DataType="local:DialogWithActionViewModel"
Background="{DynamicResource SemiYellow1}"
mc:Ignorable="d">
<Grid Margin="24" RowDefinitions="Auto, *, Auto">
<TextBlock
Grid.Row="0"
Margin="8"
FontSize="16"
FontWeight="600"
Text="{Binding Title}" />
<Calendar
Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Top"
SelectedDate="{Binding Date}" />
<StackPanel
Grid.Row="2"
HorizontalAlignment="Right"
Orientation="Horizontal"
Spacing="8">
<Button Command="{Binding DialogCommand}" Content="Dialog" />
<Button Command="{Binding OKCommand}" Content="OK" />
<Button Command="{Binding CancelCommand}" Content="Cancel" />
<ComboBox>
<ComboBoxItem>A</ComboBoxItem>
<ComboBoxItem>B</ComboBoxItem>
<ComboBoxItem>C</ComboBoxItem>
</ComboBox>
</StackPanel>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -9,10 +9,5 @@
x:Class="Ursa.Demo.Dialogs.PlainDialog">
<StackPanel>
<Calendar SelectedDate="{Binding Date}" ></Calendar>
<ComboBox>
<ComboBoxItem>A</ComboBoxItem>
<ComboBoxItem>B</ComboBoxItem>
<ComboBoxItem>C</ComboBoxItem>
</ComboBox>
</StackPanel>
</UserControl>

View File

@@ -9,6 +9,7 @@ public static class MenuKeys
public const string MenuKeyClassInput = "Class Input";
public const string MenuKeyDialog = "Dialog";
public const string MenuKeyDivider = "Divider";
public const string MenuKeyDrawer = "Drawer";
public const string MenuKeyDualBadge = "DualBadge";
public const string MenuKeyEnumSelector = "EnumSelector";
public const string MenuKeyImageViewer = "ImageViewer";

View File

@@ -0,0 +1,99 @@
<UserControl
x:Class="Ursa.Demo.Pages.DrawerDemo"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:common="clr-namespace:Ursa.Common;assembly=Ursa"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:u="https://irihi.tech/ursa"
xmlns:vm="clr-namespace:Ursa.Demo.ViewModels;assembly=Ursa.Demo"
d:DesignHeight="450"
d:DesignWidth="800"
x:CompileBindings="True"
x:DataType="vm:DrawerDemoViewModel"
mc:Ignorable="d">
<Grid ColumnDefinitions="Auto, *">
<TabControl Grid.Column="0" Width="300">
<TabItem Header="Default">
<StackPanel>
<u:EnumSelector EnumType="common:Position" Value="{Binding SelectedPosition}" />
<ToggleSwitch
Content="Global/Local"
IsChecked="{Binding IsGlobal}"
OffContent="Local"
OnContent="Global" />
<ToggleSwitch
Content="ShowMask"
IsChecked="{Binding ShowMask}"
OffContent="No"
OnContent="Yes" />
<ToggleSwitch
Content="ClickOnMaskToClose"
IsChecked="{Binding CanCloseMaskToClose}"
OffContent="No"
OnContent="Yes" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="Buttons" />
<u:EnumSelector EnumType="{x:Type u:DialogButton}" Value="{Binding SelectedButton}" />
</StackPanel>
<Button Command="{Binding ShowDialogCommand}" Content="Show Default Drawer" />
<TextBlock>
<Run Text="Default Result: " />
<Run Text="{Binding DefaultResult}" />
</TextBlock>
<TextBlock>
<Run Text="Dialog Date: " />
<Run Text="{Binding Date}" />
</TextBlock>
</StackPanel>
</TabItem>
<TabItem Header="Custom">
<StackPanel>
<u:EnumSelector EnumType="common:Position" Value="{Binding SelectedPosition}" />
<ToggleSwitch
Content="Global/Local"
IsChecked="{Binding IsGlobal}"
OffContent="Local"
OnContent="Global" />
<ToggleSwitch
Content="ClickOnMaskToClose"
IsChecked="{Binding CanCloseMaskToClose}"
OffContent="No"
OnContent="Yes" />
<ToggleSwitch
Content="ShowMask"
IsChecked="{Binding ShowMask}"
OffContent="No"
OnContent="Yes" />
<Button Command="{Binding ShowCustomDialogCommand}" Content="Show Custom Drawer" />
<TextBlock>
<Run Text="Custom Result: " />
<Run Text="{Binding Result}" />
</TextBlock>
<TextBlock>
<Run Text="Dialog Date: " />
<Run Text="{Binding Date}" />
</TextBlock>
</StackPanel>
</TabItem>
</TabControl>
<Grid Grid.Column="1" ClipToBounds="True">
<Border
BorderBrush="{DynamicResource SemiGrey1}"
BorderThickness="1"
ClipToBounds="True"
CornerRadius="20">
<u:OverlayDialogHost HostId="LocalHost">
<u:OverlayDialogHost.DialogDataTemplates>
<DataTemplate DataType="x:String">
<TextBlock
Margin="24,24,48,24"
Foreground="Red"
Text="{Binding Path=.}" />
</DataTemplate>
</u:OverlayDialogHost.DialogDataTemplates>
</u:OverlayDialogHost>
</Border>
</Grid>
</Grid>
</UserControl>

View File

@@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Ursa.Demo.Pages;
public partial class DrawerDemo : UserControl
{
public DrawerDemo()
{
InitializeComponent();
}
}

View File

@@ -14,7 +14,7 @@
</UserControl.Resources>
<StackPanel HorizontalAlignment="Left" Spacing="16">
<ToggleSwitch Name="loading" Content="Toggle Loading" />
<u:EnumSelector Name="placement" EnumType="{x:Type common:IconPlacement}" />
<u:EnumSelector Name="placement" EnumType="{x:Type common:Position}" />
<u:IconButton
Content="Hello World"
IconPlacement="{Binding #placement.Value}"

View File

@@ -0,0 +1,67 @@
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Ursa.Common;
using Ursa.Controls;
using Ursa.Controls.Options;
using Ursa.Demo.Dialogs;
namespace Ursa.Demo.ViewModels;
public partial class DrawerDemoViewModel: ObservableObject
{
public ICommand ShowDialogCommand { get; set; }
public ICommand ShowCustomDialogCommand { get; set; }
[ObservableProperty] private Position _selectedPosition;
[ObservableProperty] private DialogButton _selectedButton;
[ObservableProperty] private bool _isGlobal;
[ObservableProperty] private bool _canCloseMaskToClose;
[ObservableProperty] private DialogResult? _defaultResult;
[ObservableProperty] private bool _result;
[ObservableProperty] private bool _showMask;
[ObservableProperty] private DateTime? _date;
public DrawerDemoViewModel()
{
ShowDialogCommand = new AsyncRelayCommand(ShowDefaultDialog);
ShowCustomDialogCommand = new AsyncRelayCommand(ShowCustomDrawer);
}
private async Task ShowDefaultDialog()
{
var vm = new PlainDialogViewModel();
DefaultResult = await Drawer.Show<PlainDialog, PlainDialogViewModel>(
vm,
IsGlobal ? null : "LocalHost",
new DefaultDrawerOptions()
{
Title = "Please select a date",
Position = SelectedPosition,
Buttons = SelectedButton,
CanClickOnMaskToClose = CanCloseMaskToClose,
ShowMask = ShowMask,
});
Date = vm.Date;
}
private async Task ShowCustomDrawer()
{
var vm = new DialogWithActionViewModel();
Result = await Drawer.ShowCustom<DialogWithAction, DialogWithActionViewModel, bool>(
vm,
IsGlobal ? null : "LocalHost",
new CustomDrawerOptions()
{
Position = SelectedPosition,
CanClickOnMaskToClose = CanCloseMaskToClose,
ShowMask = ShowMask,
});
Date = vm.Date;
}
}

View File

@@ -31,6 +31,7 @@ public class MainViewViewModel : ViewModelBase
MenuKeys.MenuKeyClassInput => new ClassInputDemoViewModel(),
MenuKeys.MenuKeyDialog => new DialogDemoViewModel(),
MenuKeys.MenuKeyDivider => new DividerDemoViewModel(),
MenuKeys.MenuKeyDrawer => new DrawerDemoViewModel(),
MenuKeys.MenuKeyDualBadge => new DualBadgeDemoViewModel(),
MenuKeys.MenuKeyEnumSelector => new EnumSelectorDemoViewModel(),
MenuKeys.MenuKeyImageViewer => new ImageViewerDemoViewModel(),

View File

@@ -18,6 +18,7 @@ public class MenuViewModel: ViewModelBase
new() { MenuHeader = "Class Input", Key = MenuKeys.MenuKeyClassInput, Status = "New" },
new() { MenuHeader = "Dialog", Key = MenuKeys.MenuKeyDialog },
new() { MenuHeader = "Divider", Key = MenuKeys.MenuKeyDivider },
new() { MenuHeader = "Drawer", Key = MenuKeys.MenuKeyDrawer },
new() { MenuHeader = "DualBadge", Key = MenuKeys.MenuKeyDualBadge },
new() { MenuHeader = "Enum Selector", Key = MenuKeys.MenuKeyEnumSelector },
new() { MenuHeader = "Icon Button", Key = MenuKeys.MenuKeyIconButton },

View File

@@ -7,10 +7,16 @@
<ControlTheme x:Key="{x:Type u:OverlayDialogHost}" TargetType="u:OverlayDialogHost">
<Setter Property="OverlayMaskBrush" Value="{DynamicResource OverlayDialogMaskBrush}" />
</ControlTheme>
<ControlTheme x:Key="{x:Type u:DialogControl}" TargetType="u:DialogControl">
<ControlTheme x:Key="{x:Type u:CustomDialogControl}" TargetType="u:CustomDialogControl">
<Setter Property="CornerRadius" Value="12" />
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Duration="0.2" Property="RenderTransform"/>
</Transitions>
</Setter>
<Setter Property="RenderTransform" Value="scale(1.0)"></Setter>
<Setter Property="Template">
<ControlTemplate TargetType="u:DialogControl">
<ControlTemplate TargetType="u:CustomDialogControl">
<Border
Margin="8"
Padding="0"
@@ -30,7 +36,7 @@
Content="{TemplateBinding Content}" />
<Grid Grid.Row="0" ColumnDefinitions="*, Auto">
<Panel
Name="{x:Static u:DialogControl.PART_TitleArea}"
Name="{x:Static u:DialogControlBase.PART_TitleArea}"
Grid.Column="0"
Grid.ColumnSpan="2"
Background="Transparent" />
@@ -46,12 +52,14 @@
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^[IsClosed=True]">
<Setter Property="RenderTransform" Value="scale(0.95)"/>
</Style>
<Style Selector="^ /template/ Panel#PART_TitleArea">
<Setter Property="ContextFlyout">
<MenuFlyout>
<MenuItem
Command="{Binding $parent[u:DialogControl].CloseDialog}"
CommandParameter="{x:Static u:DialogLayerChangeType.BringForward}"
Command="{Binding $parent[u:DialogControlBase].Close}"
Header="{DynamicResource STRING_MENU_DIALOG_CLOSE}">
<MenuItem.Icon>
<PathIcon
@@ -67,7 +75,7 @@
<Setter Property="ContextFlyout">
<MenuFlyout>
<MenuItem
Command="{Binding $parent[u:DialogControl].UpdateLayer}"
Command="{Binding $parent[u:DialogControlBase].UpdateLayer}"
CommandParameter="{x:Static u:DialogLayerChangeType.BringForward}"
Header="{DynamicResource STRING_MENU_BRING_FORWARD}">
<MenuItem.Icon>
@@ -78,7 +86,7 @@
</MenuItem.Icon>
</MenuItem>
<MenuItem
Command="{Binding $parent[u:DialogControl].UpdateLayer}"
Command="{Binding $parent[u:DialogControlBase].UpdateLayer}"
CommandParameter="{x:Static u:DialogLayerChangeType.BringToFront}"
Header="{DynamicResource STRING_MENU_BRING_TO_FRONT}">
<MenuItem.Icon>
@@ -89,7 +97,7 @@
</MenuItem.Icon>
</MenuItem>
<MenuItem
Command="{Binding $parent[u:DialogControl].UpdateLayer}"
Command="{Binding $parent[u:DialogControlBase].UpdateLayer}"
CommandParameter="{x:Static u:DialogLayerChangeType.SendBackward}"
Header="{DynamicResource STRING_MENU_SEND_BACKWARD}">
<MenuItem.Icon>
@@ -100,7 +108,7 @@
</MenuItem.Icon>
</MenuItem>
<MenuItem
Command="{Binding $parent[u:DialogControl].UpdateLayer}"
Command="{Binding $parent[u:DialogControlBase].UpdateLayer}"
CommandParameter="{x:Static u:DialogLayerChangeType.SendToBack}"
Header="{DynamicResource STRING_MENU_SEND_TO_BACK}">
<MenuItem.Icon>
@@ -111,8 +119,7 @@
</MenuItem.Icon>
</MenuItem>
<MenuItem
Command="{Binding $parent[u:DialogControl].CloseDialog}"
CommandParameter="{x:Static u:DialogLayerChangeType.BringForward}"
Command="{Binding $parent[u:DialogControlBase].Close}"
Header="{DynamicResource STRING_MENU_DIALOG_CLOSE}">
<MenuItem.Icon>
<PathIcon
@@ -128,6 +135,11 @@
<ControlTheme x:Key="{x:Type u:DefaultDialogControl}" TargetType="u:DefaultDialogControl">
<Setter Property="CornerRadius" Value="12" />
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Duration="0.2" Property="RenderTransform"/>
</Transitions>
</Setter>
<Setter Property="Template">
<ControlTemplate TargetType="u:DefaultDialogControl">
<Border
@@ -152,7 +164,7 @@
</ScrollViewer>
<Grid Grid.Row="0" ColumnDefinitions="Auto, *, Auto">
<Panel
Name="{x:Static u:DialogControl.PART_TitleArea}"
Name="{x:Static u:DialogControlBase.PART_TitleArea}"
Grid.Column="0"
Grid.ColumnSpan="3"
Background="Transparent" />
@@ -216,6 +228,9 @@
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^[IsClosed=True]">
<Setter Property="RenderTransform" Value="scale(0.95)"/>
</Style>
<Style Selector="^[Mode=None]">
<Style Selector="^ /template/ PathIcon#PART_Icon">
<Setter Property="IsVisible" Value="False" />
@@ -338,8 +353,7 @@
<Setter Property="ContextFlyout">
<MenuFlyout>
<MenuItem
Command="{Binding $parent[u:DialogControl].CloseDialog}"
CommandParameter="{x:Static u:DialogLayerChangeType.BringForward}"
Command="{Binding $parent[u:DialogControlBase].Close}"
Header="{DynamicResource STRING_MENU_DIALOG_CLOSE}">
<MenuItem.Icon>
<PathIcon
@@ -355,7 +369,7 @@
<Setter Property="ContextFlyout">
<MenuFlyout>
<MenuItem
Command="{Binding $parent[u:DialogControl].UpdateLayer}"
Command="{Binding $parent[u:DialogControlBase].UpdateLayer}"
CommandParameter="{x:Static u:DialogLayerChangeType.BringForward}"
Header="{DynamicResource STRING_MENU_BRING_FORWARD}">
<MenuItem.Icon>
@@ -366,7 +380,7 @@
</MenuItem.Icon>
</MenuItem>
<MenuItem
Command="{Binding $parent[u:DialogControl].UpdateLayer}"
Command="{Binding $parent[u:DialogControlBase].UpdateLayer}"
CommandParameter="{x:Static u:DialogLayerChangeType.BringToFront}"
Header="{DynamicResource STRING_MENU_BRING_TO_FRONT}">
<MenuItem.Icon>
@@ -377,7 +391,7 @@
</MenuItem.Icon>
</MenuItem>
<MenuItem
Command="{Binding $parent[u:DialogControl].UpdateLayer}"
Command="{Binding $parent[u:DialogControlBase].UpdateLayer}"
CommandParameter="{x:Static u:DialogLayerChangeType.SendBackward}"
Header="{DynamicResource STRING_MENU_SEND_BACKWARD}">
<MenuItem.Icon>
@@ -388,7 +402,7 @@
</MenuItem.Icon>
</MenuItem>
<MenuItem
Command="{Binding $parent[u:DialogControl].UpdateLayer}"
Command="{Binding $parent[u:DialogControlBase].UpdateLayer}"
CommandParameter="{x:Static u:DialogLayerChangeType.SendToBack}"
Header="{DynamicResource STRING_MENU_SEND_TO_BACK}">
<MenuItem.Icon>
@@ -399,7 +413,7 @@
</MenuItem.Icon>
</MenuItem>
<MenuItem
Command="{Binding $parent[u:DialogControl].CloseDialog}"
Command="{Binding $parent[u:DialogControlBase].Close}"
CommandParameter="{x:Static u:DialogLayerChangeType.BringForward}"
Header="{DynamicResource STRING_MENU_DIALOG_CLOSE}">
<MenuItem.Icon>

View File

@@ -0,0 +1,167 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:u="https://irihi.tech/ursa">
<ControlTheme TargetType="u:CustomDrawerControl" x:Key="{x:Type u:CustomDrawerControl}">
<Setter Property="VerticalAlignment" Value="Stretch"></Setter>
<Setter Property="HorizontalAlignment" Value="Stretch"></Setter>
<Setter Property="Template">
<ControlTemplate TargetType="u:CustomDrawerControl">
<Border Name="PART_Root"
Margin="8 -1 -1 -1"
Padding="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Classes="Shadow"
ClipToBounds="False"
CornerRadius="12 0 0 12"
BorderThickness="1 0 0 0"
IsHitTestVisible="True"
Theme="{DynamicResource CardBorder}">
<Border ClipToBounds="True" CornerRadius="{Binding #PART_Root.CornerRadius}">
<Grid RowDefinitions="Auto, *">
<ContentPresenter
Name="PART_ContentPresenter"
Grid.Row="0"
Grid.RowSpan="2"
Content="{TemplateBinding Content}" />
<Grid Grid.Row="0" ColumnDefinitions="*, Auto">
<Panel
Name="{x:Static u:DialogControlBase.PART_TitleArea}"
Grid.Column="0"
Grid.ColumnSpan="2"
Background="Transparent" />
<Button
Name="{x:Static u:MessageBoxWindow.PART_CloseButton}"
Grid.Column="1"
Margin="0,24,24,0"
DockPanel.Dock="Right"
Theme="{DynamicResource CloseButton}" />
</Grid>
</Grid>
</Border>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^[Position=Right] /template/ Border#PART_Root">
<Setter Property="Margin" Value="8 0 0 0" />
<Setter Property="CornerRadius" Value="12 0 0 12" />
<Setter Property="BorderThickness" Value="1 0 0 0" />
</Style>
<Style Selector="^[Position=Left] /template/ Border#PART_Root">
<Setter Property="Margin" Value="0 0 8 0" />
<Setter Property="CornerRadius" Value="0 12 12 0" />
<Setter Property="BorderThickness" Value="0 0 1 0" />
</Style>
<Style Selector="^[Position=Top] /template/ Border#PART_Root">
<Setter Property="Margin" Value="0 0 0 8" />
<Setter Property="CornerRadius" Value="0 0 12 12" />
<Setter Property="BorderThickness" Value="0 0 0 1" />
</Style>
<Style Selector="^[Position=Bottom] /template/ Border#PART_Root">
<Setter Property="Margin" Value="0 8 0 0" />
<Setter Property="CornerRadius" Value="12 12 0 0" />
<Setter Property="BorderThickness" Value="0 1 0 0" />
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type u:DefaultDrawerControl}" TargetType="u:DefaultDrawerControl">
<Setter Property="VerticalAlignment" Value="Stretch"></Setter>
<Setter Property="HorizontalAlignment" Value="Stretch"></Setter>
<Setter Property="Template">
<ControlTemplate TargetType="u:DefaultDrawerControl">
<Border Name="PART_Root"
Margin="8 -1 -1 -1"
Padding="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Classes="Shadow"
ClipToBounds="False"
CornerRadius="12 0 0 12"
BorderThickness="1 0 0 0"
IsHitTestVisible="True"
Theme="{DynamicResource CardBorder}">
<Border ClipToBounds="True" CornerRadius="{Binding #PART_Root.CornerRadius}">
<Grid RowDefinitions="Auto, *, Auto">
<ScrollViewer Grid.Row="1">
<ContentPresenter
Name="PART_ContentPresenter"
Grid.Row="1"
Margin="24,8"
Content="{TemplateBinding Content}" />
</ScrollViewer>
<Grid Grid.Row="0" ColumnDefinitions=" *, Auto">
<TextBlock
Name="PART_Title"
Grid.Column="0"
Margin="24,24,0,0"
VerticalAlignment="Center"
FontSize="16"
FontWeight="Bold"
IsHitTestVisible="False"
IsVisible="{TemplateBinding Title,
Converter={x:Static ObjectConverters.IsNotNull}}"
Text="{TemplateBinding Title}"
TextWrapping="Wrap" />
<Button
Name="{x:Static u:DrawerControlBase.PART_CloseButton}"
Grid.Column="1"
Margin="0,24,24,0"
DockPanel.Dock="Right"
Theme="{DynamicResource CloseButton}" />
</Grid>
<StackPanel
Grid.Row="2"
Margin="24,0,24,24"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Name="{x:Static u:DefaultDialogControl.PART_CancelButton}"
Margin="8,0,0,0"
Classes="Tertiary"
Content="{DynamicResource STRING_MENU_DIALOG_CANCEL}" />
<Button
Name="{x:Static u:DefaultDialogControl.PART_NoButton}"
Margin="8,0,0,0"
Classes="Danger"
Content="{DynamicResource STRING_MENU_DIALOG_NO}"
Theme="{DynamicResource SolidButton}" />
<Button
Name="{x:Static u:DefaultDialogControl.PART_YesButton}"
Margin="8,0,0,0"
Classes="Primary"
Content="{DynamicResource STRING_MENU_DIALOG_YES}"
Theme="{DynamicResource SolidButton}" />
<Button
Name="{x:Static u:DefaultDialogControl.PART_OKButton}"
Margin="8,0,0,0"
Classes="Primary"
Content="{DynamicResource STRING_MENU_DIALOG_OK}"
Theme="{DynamicResource SolidButton}" />
</StackPanel>
</Grid>
</Border>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^[Position=Right] /template/ Border#PART_Root">
<Setter Property="Margin" Value="8 0 0 0" />
<Setter Property="CornerRadius" Value="12 0 0 12" />
<Setter Property="BorderThickness" Value="1 0 0 0" />
</Style>
<Style Selector="^[Position=Left] /template/ Border#PART_Root">
<Setter Property="Margin" Value="0 0 8 0" />
<Setter Property="CornerRadius" Value="0 12 12 0" />
<Setter Property="BorderThickness" Value="0 0 1 0" />
</Style>
<Style Selector="^[Position=Top] /template/ Border#PART_Root">
<Setter Property="Margin" Value="0 0 0 8" />
<Setter Property="CornerRadius" Value="0 0 12 12" />
<Setter Property="BorderThickness" Value="0 0 0 1" />
</Style>
<Style Selector="^[Position=Bottom] /template/ Border#PART_Root">
<Setter Property="Margin" Value="0 8 0 0" />
<Setter Property="CornerRadius" Value="12 12 0 0" />
<Setter Property="BorderThickness" Value="0 1 0 0" />
</Style>
</ControlTheme>
</ResourceDictionary>

View File

@@ -165,7 +165,7 @@
<Grid RowDefinitions="Auto, *, Auto">
<Grid Grid.Row="0" ColumnDefinitions="Auto, *, Auto">
<Panel
Name="{x:Static u:DialogControl.PART_TitleArea}"
Name="{x:Static u:DialogControlBase.PART_TitleArea}"
Grid.Column="0"
Grid.ColumnSpan="3"
Background="Transparent" />
@@ -247,7 +247,7 @@
<Style Selector="^ /template/ Panel#PART_TitleArea">
<Setter Property="ContextFlyout">
<MenuFlyout>
<MenuItem Command="{Binding $parent[u:DialogControl].CloseDialog}" Header="{DynamicResource STRING_MENU_DIALOG_CLOSE}">
<MenuItem Command="{Binding $parent[u:CustomDialogControl].CloseDialog}" Header="{DynamicResource STRING_MENU_DIALOG_CLOSE}">
<MenuItem.Icon>
<PathIcon
Width="12"

View File

@@ -8,6 +8,7 @@
<ResourceInclude Source="Dialog.axaml" />
<ResourceInclude Source="DialogShared.axaml" />
<ResourceInclude Source="Divider.axaml" />
<ResourceInclude Source="Drawer.axaml" />
<ResourceInclude Source="DualBadge.axaml" />
<ResourceInclude Source="EnumSelector.axaml" />
<ResourceInclude Source="IconButton.axaml" />

View File

@@ -1,6 +1,6 @@
namespace Ursa.Common;
public enum IconPlacement
public enum Position
{
Left,
Top,

View File

@@ -0,0 +1,39 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Interactivity;
using Avalonia.Threading;
using Ursa.Common;
using Ursa.Controls.OverlayShared;
using Ursa.EventArgs;
namespace Ursa.Controls;
public class CustomDialogControl: DialogControlBase
{
internal bool IsCloseButtonVisible { get; set; }
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (_closeButton is not null)
{
_closeButton.IsVisible = IsCloseButtonVisible;
}
}
public override void Close()
{
if (DataContext is IDialogContext context)
{
context.Close();
}
else
{
OnElementClosing(this, null);
}
}
}

View File

@@ -5,16 +5,15 @@ using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Ursa.Common;
using Ursa.EventArgs;
namespace Ursa.Controls;
[TemplatePart(PART_OKButton, typeof(Button))]
[TemplatePart(PART_CancelButton, typeof(Button))]
[TemplatePart(PART_YesButton, typeof(Button))]
[TemplatePart(PART_NoButton, typeof(Button))]
[TemplatePart(PART_TitleArea, typeof(Panel))]
public class DefaultDialogControl: DialogControl
public class DefaultDialogControl: DialogControlBase
{
public const string PART_OKButton = "PART_OKButton";
public const string PART_CancelButton = "PART_CancelButton";
@@ -104,11 +103,6 @@ public class DefaultDialogControl: DialogControl
break;
}
}
private void SetVisibility(Button? button, bool visible)
{
if (button is not null) button.IsVisible = visible;
}
private void DefaultButtonsClose(object sender, RoutedEventArgs args)
{
@@ -116,24 +110,24 @@ public class DefaultDialogControl: DialogControl
{
if (button == _okButton)
{
OnDialogControlClosing(this, DialogResult.OK);
OnElementClosing(this, DialogResult.OK);
}
else if (button == _cancelButton)
{
OnDialogControlClosing(this, DialogResult.Cancel);
OnElementClosing(this, DialogResult.Cancel);
}
else if (button == _yesButton)
{
OnDialogControlClosing(this, DialogResult.Yes);
OnElementClosing(this, DialogResult.Yes);
}
else if (button == _noButton)
{
OnDialogControlClosing(this, DialogResult.No);
OnElementClosing(this, DialogResult.No);
}
}
}
internal override void CloseDialog()
public override void Close()
{
if (DataContext is IDialogContext context)
{
@@ -150,7 +144,7 @@ public class DefaultDialogControl: DialogControl
DialogButton.YesNoCancel => DialogResult.Cancel,
_ => DialogResult.None
};
OnDialogControlClosing(this, result);
OnElementClosing(this, result);
}
}
}

View File

@@ -1,167 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Interactivity;
using Avalonia.Threading;
using Ursa.Common;
namespace Ursa.Controls;
[TemplatePart(PART_CloseButton, typeof(Button))]
[TemplatePart(PART_TitleArea, typeof(Panel))]
[PseudoClasses(PC_Modal)]
public class DialogControl: ContentControl
{
public const string PART_CloseButton = "PART_CloseButton";
public const string PART_TitleArea = "PART_TitleArea";
public const string PC_Modal = ":modal";
protected internal Button? _closeButton;
private Panel? _titleArea;
internal HorizontalPosition HorizontalAnchor { get; set; } = HorizontalPosition.Center;
internal VerticalPosition VerticalAnchor { get; set; } = VerticalPosition.Center;
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; }
internal bool IsCloseButtonVisible { get; set; }
public event EventHandler<DialogLayerChangeEventArgs>? LayerChanged;
public event EventHandler<object?>? DialogControlClosing;
static DialogControl()
{
DataContextProperty.Changed.AddClassHandler<DialogControl, object?>((o, e) => o.OnDataContextChange(e));
}
private void OnDataContextChange(AvaloniaPropertyChangedEventArgs<object?> args)
{
if (args.OldValue.Value is IDialogContext oldContext)
{
oldContext.RequestClose-= OnContextRequestClose;
}
if (args.NewValue.Value is IDialogContext newContext)
{
newContext.RequestClose += OnContextRequestClose;
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
EventHelper.UnregisterClickEvent(OnCloseButtonClick, _closeButton);
_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);
if (_closeButton is not null)
{
_closeButton.IsVisible = IsCloseButtonVisible;
}
_titleArea?.AddHandler(PointerMovedEvent, OnTitlePointerMove, RoutingStrategies.Bubble);
_titleArea?.AddHandler(PointerPressedEvent, OnTitlePointerPressed, RoutingStrategies.Bubble);
_titleArea?.AddHandler(PointerReleasedEvent, OnTitlePointerRelease, RoutingStrategies.Bubble);
EventHelper.RegisterClickEvent(OnCloseButtonClick, _closeButton);
}
private void OnTitlePointerPressed(object sender, PointerPressedEventArgs e)
{
e.Source = this;
}
private void OnTitlePointerMove(object sender, PointerEventArgs e)
{
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(() =>
{
Dispatcher.UIThread.Invoke(CloseDialog);
});
void OnCloseHandler(object sender, object? args)
{
if (args is T result)
{
tcs.SetResult(result);
DialogControlClosing-= OnCloseHandler;
}
else
{
tcs.SetResult(default(T));
DialogControlClosing-= OnCloseHandler;
}
}
this.DialogControlClosing += OnCloseHandler;
return tcs.Task;
}
private void OnCloseButtonClick(object sender, RoutedEventArgs args) => CloseDialog();
private void OnContextRequestClose(object sender, object? args)
{
DialogControlClosing?.Invoke(this, args);
}
public void UpdateLayer(object? o)
{
if (o is DialogLayerChangeType t)
{
LayerChanged?.Invoke(this, new DialogLayerChangeEventArgs(t));
}
}
/// <summary>
/// Used for inherited classes to invoke the DialogControlClosing event.
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
protected internal virtual void OnDialogControlClosing(object sender, object? args)
{
DialogControlClosing?.Invoke(this, args);
}
internal void SetAsModal(bool modal)
{
PseudoClasses.Set(PC_Modal, modal);
}
/// <summary>
/// This method is exposed internally for closing the dialog from neither context nor closing by clicking on the close button.
/// It is also exposed to be bound to context flyout.
/// It is virtual because inherited classes may return a different result by default.
/// </summary>
internal virtual void CloseDialog()
{
if (DataContext is IDialogContext context)
{
context.Close();
}
else
{
DialogControlClosing?.Invoke(this, null);
}
}
}

View File

@@ -0,0 +1,87 @@
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Ursa.Common;
using Ursa.Controls.OverlayShared;
using Ursa.EventArgs;
namespace Ursa.Controls;
[TemplatePart(PART_CloseButton, typeof(Button))]
[TemplatePart(PART_TitleArea, typeof(Panel))]
[PseudoClasses(PC_Modal)]
public abstract class DialogControlBase: OverlayFeedbackElement
{
public const string PART_CloseButton = "PART_CloseButton";
public const string PART_TitleArea = "PART_TitleArea";
public const string PC_Modal = ":modal";
internal HorizontalPosition HorizontalAnchor { get; set; } = HorizontalPosition.Center;
internal VerticalPosition VerticalAnchor { get; set; } = VerticalPosition.Center;
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; }
internal bool CanLightDismiss { get; set; }
protected internal Button? _closeButton;
private Panel? _titleArea;
internal void SetAsModal(bool modal)
{
PseudoClasses.Set(PC_Modal, modal);
}
public static readonly RoutedEvent<DialogLayerChangeEventArgs> LayerChangedEvent = RoutedEvent.Register<CustomDialogControl, DialogLayerChangeEventArgs>(
nameof(LayerChanged), RoutingStrategies.Bubble);
public event EventHandler<DialogLayerChangeEventArgs> LayerChanged
{
add => AddHandler(LayerChangedEvent, value);
remove => RemoveHandler(LayerChangedEvent, value);
}
public void UpdateLayer(object? o)
{
if (o is DialogLayerChangeType t)
{
RaiseEvent(new DialogLayerChangeEventArgs(LayerChangedEvent, t));
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_titleArea?.RemoveHandler(PointerMovedEvent, OnTitlePointerMove);
_titleArea?.RemoveHandler(PointerPressedEvent, OnTitlePointerPressed);
_titleArea?.RemoveHandler(PointerReleasedEvent, OnTitlePointerRelease);
_titleArea = e.NameScope.Find<Panel>(PART_TitleArea);
_titleArea?.AddHandler(PointerMovedEvent, OnTitlePointerMove, RoutingStrategies.Bubble);
_titleArea?.AddHandler(PointerPressedEvent, OnTitlePointerPressed, RoutingStrategies.Bubble);
_titleArea?.AddHandler(PointerReleasedEvent, OnTitlePointerRelease, RoutingStrategies.Bubble);
EventHelper.UnregisterClickEvent(OnCloseButtonClick, _closeButton);
_closeButton = e.NameScope.Find<Button>(PART_CloseButton);
EventHelper.RegisterClickEvent(OnCloseButtonClick, _closeButton);
}
private void OnTitlePointerPressed(object sender, PointerPressedEventArgs e)
{
e.Source = this;
}
private void OnTitlePointerMove(object sender, PointerEventArgs e)
{
e.Source = this;
}
private void OnTitlePointerRelease(object sender, PointerReleasedEventArgs e)
{
e.Source = this;
}
private void OnCloseButtonClick(object sender, RoutedEventArgs args) => Close();
}

View File

@@ -1,12 +1,19 @@
using Avalonia.Interactivity;
namespace Ursa.Controls;
public class DialogLayerChangeEventArgs
public class DialogLayerChangeEventArgs: RoutedEventArgs
{
public DialogLayerChangeType ChangeType { get; }
public DialogLayerChangeEventArgs(DialogLayerChangeType type)
{
ChangeType = type;
}
public DialogLayerChangeEventArgs(RoutedEvent routedEvent, DialogLayerChangeType type): base(routedEvent)
{
ChangeType = type;
}
}
public enum DialogLayerChangeType

View File

@@ -26,4 +26,6 @@ public class OverlayDialogOptions
public DialogButton Buttons { get; set; } = DialogButton.OKCancel;
public string? Title { get; set; } = null;
public bool IsCloseButtonVisible { get; set; } = true;
public bool CanLightDismiss { get; set; }
}

View File

@@ -58,7 +58,7 @@ public static class OverlayDialog
{
var host = OverlayDialogManager.GetHost(hostId);
if (host is null) return;
var t = new DialogControl()
var t = new CustomDialogControl()
{
Content = new TView(),
DataContext = vm,
@@ -72,7 +72,7 @@ public static class OverlayDialog
{
var host = OverlayDialogManager.GetHost(hostId);
if (host is null) return;
var t = new DialogControl()
var t = new CustomDialogControl()
{
Content = control,
DataContext = vm,
@@ -89,7 +89,7 @@ public static class OverlayDialog
var view = host.GetDataTemplate(vm)?.Build(vm);
if (view is null) view = new ContentControl() { Padding = new Thickness(24) };
view.DataContext = vm;
var t = new DialogControl()
var t = new CustomDialogControl()
{
Content = view,
DataContext = vm,
@@ -135,7 +135,7 @@ public static class OverlayDialog
{
var host = OverlayDialogManager.GetHost(hostId);
if (host is null) return Task.FromResult(default(TResult));
var t = new DialogControl()
var t = new CustomDialogControl()
{
Content = new TView(),
DataContext = vm,
@@ -150,7 +150,7 @@ public static class OverlayDialog
{
var host = OverlayDialogManager.GetHost(hostId);
if (host is null) return Task.FromResult(default(TResult));
var t = new DialogControl()
var t = new CustomDialogControl()
{
Content = control,
DataContext = vm,
@@ -168,7 +168,7 @@ public static class OverlayDialog
var view = host.GetDataTemplate(vm)?.Build(vm);
if (view is null) view = new ContentControl() { Padding = new Thickness(24) };
view.DataContext = vm;
var t = new DialogControl()
var t = new CustomDialogControl()
{
Content = view,
DataContext = vm,
@@ -178,7 +178,7 @@ public static class OverlayDialog
return t.ShowAsync<TResult?>(token);
}
private static void ConfigureDialogControl(DialogControl control, OverlayDialogOptions? options)
private static void ConfigureDialogControl(CustomDialogControl control, OverlayDialogOptions? options)
{
options ??= OverlayDialogOptions.Default;
control.HorizontalAnchor = options.HorizontalAnchor;
@@ -191,6 +191,7 @@ public static class OverlayDialog
options.VerticalAnchor == VerticalPosition.Center ? null : options.VerticalOffset;
control.CanClickOnMaskToClose = options.CanClickOnMaskToClose;
control.IsCloseButtonVisible = options.IsCloseButtonVisible;
control.CanLightDismiss = options.CanLightDismiss;
}
private static void ConfigureDefaultDialogControl(DefaultDialogControl control, OverlayDialogOptions? options)
@@ -208,6 +209,7 @@ public static class OverlayDialog
control.Mode = options.Mode;
control.Buttons = options.Buttons;
control.Title = options.Title;
control.CanLightDismiss = options.CanLightDismiss;
}

View File

@@ -0,0 +1,27 @@
using Avalonia.Controls.Primitives;
namespace Ursa.Controls;
public class CustomDrawerControl: DrawerControlBase
{
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (_closeButton is not null)
{
_closeButton.IsVisible = IsCloseButtonVisible;
}
}
public override void Close()
{
if (DataContext is IDialogContext context)
{
context.Close();
}
else
{
OnElementClosing(this, null);
}
}
}

View File

@@ -0,0 +1,148 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Ursa.Common;
using Ursa.EventArgs;
namespace Ursa.Controls;
[TemplatePart(PART_YesButton, typeof(Button))]
[TemplatePart(PART_NoButton, typeof(Button))]
[TemplatePart(PART_OKButton, typeof(Button))]
[TemplatePart(PART_CancelButton, typeof(Button))]
public class DefaultDrawerControl: DrawerControlBase
{
public const string PART_YesButton = "PART_YesButton";
public const string PART_NoButton = "PART_NoButton";
public const string PART_OKButton = "PART_OKButton";
public const string PART_CancelButton = "PART_CancelButton";
private Button? _yesButton;
private Button? _noButton;
private Button? _okButton;
private Button? _cancelButton;
public static readonly StyledProperty<DialogButton> ButtonsProperty = AvaloniaProperty.Register<DefaultDrawerControl, DialogButton>(
nameof(Buttons), DialogButton.OKCancel);
public DialogButton Buttons
{
get => GetValue(ButtonsProperty);
set => SetValue(ButtonsProperty, value);
}
public static readonly StyledProperty<DialogMode> ModeProperty = AvaloniaProperty.Register<DefaultDrawerControl, DialogMode>(
nameof(Mode), DialogMode.None);
public DialogMode Mode
{
get => GetValue(ModeProperty);
set => SetValue(ModeProperty, value);
}
public static readonly StyledProperty<string?> TitleProperty = AvaloniaProperty.Register<DefaultDrawerControl, string?>(
nameof(Title));
public string? Title
{
get => GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
EventHelper.UnregisterClickEvent(OnDefaultButtonClick, _yesButton, _noButton, _okButton, _cancelButton);
_yesButton = e.NameScope.Find<Button>(PART_YesButton);
_noButton = e.NameScope.Find<Button>(PART_NoButton);
_okButton = e.NameScope.Find<Button>(PART_OKButton);
_cancelButton = e.NameScope.Find<Button>(PART_CancelButton);
EventHelper.RegisterClickEvent(OnDefaultButtonClick, _yesButton, _noButton, _okButton, _cancelButton);
SetButtonVisibility();
}
private void SetButtonVisibility()
{
bool isCloseButtonVisible = DataContext is IDialogContext || Buttons != DialogButton.YesNo;
SetVisibility(_closeButton, isCloseButtonVisible);
switch (Buttons)
{
case DialogButton.None:
SetVisibility(_okButton, false);
SetVisibility(_cancelButton, false);
SetVisibility(_yesButton, false);
SetVisibility(_noButton, false);
break;
case DialogButton.OK:
SetVisibility(_okButton, true);
SetVisibility(_cancelButton, false);
SetVisibility(_yesButton, false);
SetVisibility(_noButton, false);
break;
case DialogButton.OKCancel:
SetVisibility(_okButton, true);
SetVisibility(_cancelButton, true);
SetVisibility(_yesButton, false);
SetVisibility(_noButton, false);
break;
case DialogButton.YesNo:
SetVisibility(_okButton, false);
SetVisibility(_cancelButton, false);
SetVisibility(_yesButton, true);
SetVisibility(_noButton, true);
break;
case DialogButton.YesNoCancel:
SetVisibility(_okButton, false);
SetVisibility(_cancelButton, true);
SetVisibility(_yesButton, true);
SetVisibility(_noButton, true);
break;
}
}
private void OnDefaultButtonClick(object? sender, RoutedEventArgs e)
{
if (sender is Button button)
{
if (button == _okButton)
{
OnElementClosing(this, DialogResult.OK);
}
else if (button == _cancelButton)
{
OnElementClosing(this, DialogResult.Cancel);
}
else if (button == _yesButton)
{
OnElementClosing(this, DialogResult.Yes);
}
else if (button == _noButton)
{
OnElementClosing(this, DialogResult.No);
}
}
}
public override void Close()
{
if (DataContext is IDialogContext context)
{
context.Close();
}
else
{
DialogResult result = Buttons switch
{
DialogButton.None => DialogResult.None,
DialogButton.OK => DialogResult.OK,
DialogButton.OKCancel => DialogResult.Cancel,
DialogButton.YesNo => DialogResult.No,
DialogButton.YesNoCancel => DialogResult.Cancel,
_ => DialogResult.None
};
RaiseEvent(new ResultEventArgs(ClosedEvent, result));
}
}
}

View File

@@ -0,0 +1,145 @@
using Avalonia;
using Avalonia.Controls;
using Ursa.Common;
using Ursa.Controls.Options;
namespace Ursa.Controls;
public static class Drawer
{
public static Task<DialogResult> Show<TView, TViewModel>(TViewModel vm, string? hostId = null, DefaultDrawerOptions? options = null)
where TView: Control, new()
{
var host = OverlayDialogManager.GetHost(hostId);
if (host is null) return Task.FromResult(default(DialogResult));
var drawer = new DefaultDrawerControl()
{
Content = new TView(),
DataContext = vm,
};
ConfigureDefaultDrawer(drawer, options);
host.AddDrawer(drawer);
return drawer.ShowAsync<DialogResult>();
}
public static Task<DialogResult> Show(Control control, object? vm, string? hostId = null,
DefaultDrawerOptions? options = null)
{
var host = OverlayDialogManager.GetHost(hostId);
if (host is null) return Task.FromResult(default(DialogResult));
var drawer = new DefaultDrawerControl()
{
Content = control,
DataContext = vm,
};
ConfigureDefaultDrawer(drawer, options);
host.AddDrawer(drawer);
return drawer.ShowAsync<DialogResult>();
}
public static Task<DialogResult> Show(object? vm, string? hostId = null, DefaultDrawerOptions? options = null)
{
var host = OverlayDialogManager.GetHost(hostId);
if (host is null) return Task.FromResult(default(DialogResult));
var view = host.GetDataTemplate(vm)?.Build(vm);
if (view is null) view = new ContentControl() { Padding = new Thickness(24) };
view.DataContext = vm;
var drawer = new DefaultDrawerControl()
{
Content = view,
DataContext = vm,
};
ConfigureDefaultDrawer(drawer, options);
host.AddDrawer(drawer);
return drawer.ShowAsync<DialogResult>();
}
public static Task<TResult?> ShowCustom<TView, TViewModel, TResult>(TViewModel vm, string? hostId = null, CustomDrawerOptions? options = null)
where TView: Control, new()
{
var host = OverlayDialogManager.GetHost(hostId);
if (host is null) return Task.FromResult(default(TResult));
var dialog = new CustomDrawerControl()
{
Content = new TView(),
DataContext = vm,
};
ConfigureCustomDrawer(dialog, options);
host.AddDrawer(dialog);
return dialog.ShowAsync<TResult>();
}
public static Task<TResult?> ShowCustom<TResult>(Control control, object? vm, string? hostId = null, CustomDrawerOptions? options = null)
{
var host = OverlayDialogManager.GetHost(hostId);
if (host is null) return Task.FromResult(default(TResult));
var dialog = new CustomDrawerControl()
{
Content = control,
DataContext = vm,
};
ConfigureCustomDrawer(dialog, options);
host.AddDrawer(dialog);
return dialog.ShowAsync<TResult>();
}
public static Task<TResult?> ShowCustom<TResult>(object? vm, string? hostId = null, CustomDrawerOptions? options = null)
{
var host = OverlayDialogManager.GetHost(hostId);
if (host is null) return Task.FromResult(default(TResult));
var view = host.GetDataTemplate(vm)?.Build(vm);
if (view is null) view = new ContentControl() { Padding = new Thickness(24) };
view.DataContext = vm;
var dialog = new CustomDrawerControl()
{
Content = view,
DataContext = vm,
};
ConfigureCustomDrawer(dialog, options);
host.AddDrawer(dialog);
return dialog.ShowAsync<TResult>();
}
private static void ConfigureCustomDrawer(CustomDrawerControl drawer, CustomDrawerOptions? options)
{
options ??= CustomDrawerOptions.Default;
drawer.Position = options.Position;
drawer.CanClickOnMaskToClose = options.CanClickOnMaskToClose;
drawer.IsCloseButtonVisible = options.IsCloseButtonVisible;
drawer.ShowMask = options.ShowMask;
drawer.CanLightDismiss = options.CanLightDismiss;
if (options.Position == Position.Left || options.Position == Position.Right)
{
drawer.MinWidth = options.MinWidth ?? 0.0;
drawer.MaxWidth = options.MaxWidth ?? double.PositiveInfinity;
}
if (options.Position is Position.Top or Position.Bottom)
{
drawer.MinHeight = options.MinHeight ?? 0.0;
drawer.MaxHeight = options.MaxHeight ?? double.PositiveInfinity;
}
}
private static void ConfigureDefaultDrawer(DefaultDrawerControl drawer, DefaultDrawerOptions? options)
{
options ??= DefaultDrawerOptions.Default;
drawer.Position = options.Position;
drawer.CanClickOnMaskToClose = options.CanClickOnMaskToClose;
drawer.IsCloseButtonVisible = options.IsCloseButtonVisible;
drawer.Buttons = options.Buttons;
drawer.Title = options.Title;
drawer.ShowMask = options.ShowMask;
drawer.CanLightDismiss = options.CanLightDismiss;
if (options.Position == Position.Left || options.Position == Position.Right)
{
drawer.MinWidth = options.MinWidth ?? 0.0;
drawer.MaxWidth = options.MaxWidth ?? double.PositiveInfinity;
}
if (options.Position is Position.Top or Position.Bottom)
{
drawer.MinHeight = options.MinHeight ?? 0.0;
drawer.MaxHeight = options.MaxHeight ?? double.PositiveInfinity;
}
}
}

View File

@@ -0,0 +1,97 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Threading;
using Ursa.Common;
using Ursa.Controls.OverlayShared;
using Ursa.EventArgs;
namespace Ursa.Controls;
[TemplatePart(PART_CloseButton, typeof(Button))]
public abstract class DrawerControlBase: OverlayFeedbackElement
{
public const string PART_CloseButton = "PART_CloseButton";
internal bool CanClickOnMaskToClose { get; set; }
protected internal Button? _closeButton;
public static readonly StyledProperty<Position> PositionProperty =
AvaloniaProperty.Register<DrawerControlBase, Position>(
nameof(Position), defaultValue: Position.Right);
public Position Position
{
get => GetValue(PositionProperty);
set => SetValue(PositionProperty, value);
}
public static readonly StyledProperty<bool> IsOpenProperty = AvaloniaProperty.Register<DrawerControlBase, bool>(
nameof(IsOpen));
public bool IsOpen
{
get => GetValue(IsOpenProperty);
set => SetValue(IsOpenProperty, value);
}
public static readonly StyledProperty<bool> IsCloseButtonVisibleProperty =
AvaloniaProperty.Register<DrawerControlBase, bool>(
nameof(IsCloseButtonVisible), defaultValue: true);
public bool IsCloseButtonVisible
{
get => GetValue(IsCloseButtonVisibleProperty);
set => SetValue(IsCloseButtonVisibleProperty, value);
}
protected internal bool ShowMask { get; set; }
protected internal bool CanLightDismiss { get; set; }
static DrawerControlBase()
{
DataContextProperty.Changed.AddClassHandler<DrawerControlBase, object?>((o, e) => o.OnDataContextChange(e));
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
EventHelper.UnregisterClickEvent(OnCloseButtonClick, _closeButton);
_closeButton = e.NameScope.Find<Button>(PART_CloseButton);
EventHelper.RegisterClickEvent(OnCloseButtonClick, _closeButton);
}
private void OnDataContextChange(AvaloniaPropertyChangedEventArgs<object?> args)
{
if(args.OldValue.Value is IDialogContext oldContext)
{
oldContext.RequestClose -= OnContextRequestClose;
}
if(args.NewValue.Value is IDialogContext newContext)
{
newContext.RequestClose += OnContextRequestClose;
}
}
private void OnContextRequestClose(object sender, object? e)
{
RaiseEvent(new ResultEventArgs(ClosedEvent, e));
}
private void OnCloseButtonClick(object sender, RoutedEventArgs e) => Close();
public override void Close()
{
if (DataContext is IDialogContext context)
{
context.Close();
}
else
{
RaiseEvent(new ResultEventArgs(ClosedEvent, null));
}
}
}

View File

@@ -0,0 +1,17 @@
using Ursa.Common;
namespace Ursa.Controls.Options;
public class CustomDrawerOptions
{
internal static CustomDrawerOptions Default => new ();
public Position Position { get; set; } = Position.Right;
public bool CanClickOnMaskToClose { get; set; } = true;
public bool CanLightDismiss { get; set; } = false;
public bool ShowMask { get; set; } = true;
public bool IsCloseButtonVisible { get; set; } = true;
public double? MinWidth { get; set; } = null;
public double? MinHeight { get; set; } = null;
public double? MaxWidth { get; set; } = null;
public double? MaxHeight { get; set; } = null;
}

View File

@@ -0,0 +1,19 @@
using Ursa.Common;
namespace Ursa.Controls.Options;
public class DefaultDrawerOptions
{
internal static DefaultDrawerOptions Default => new ();
public Position Position { get; set; } = Position.Right;
public bool CanClickOnMaskToClose { get; set; } = true;
public bool CanLightDismiss { get; set; } = false;
public bool ShowMask { get; set; } = true;
public bool IsCloseButtonVisible { get; set; } = true;
public double? MinWidth { get; set; } = null;
public double? MinHeight { get; set; } = null;
public double? MaxWidth { get; set; } = null;
public double? MaxHeight { get; set; } = null;
public DialogButton Buttons { get; set; } = DialogButton.OKCancel;
public string? Title { get; set; }
}

View File

@@ -44,10 +44,10 @@ public class IconButton: Button
set => SetValue(IsLoadingProperty, value);
}
public static readonly StyledProperty<IconPlacement> IconPlacementProperty = AvaloniaProperty.Register<IconButton, IconPlacement>(
nameof(IconPlacement), defaultValue: IconPlacement.Left);
public static readonly StyledProperty<Position> IconPlacementProperty = AvaloniaProperty.Register<IconButton, Position>(
nameof(IconPlacement), defaultValue: Position.Left);
public IconPlacement IconPlacement
public Position IconPlacement
{
get => GetValue(IconPlacementProperty);
set => SetValue(IconPlacementProperty, value);
@@ -55,7 +55,7 @@ public class IconButton: Button
static IconButton()
{
IconPlacementProperty.Changed.AddClassHandler<IconButton, IconPlacement>((o, e) =>
IconPlacementProperty.Changed.AddClassHandler<IconButton, Position>((o, e) =>
{
o.SetPlacement(e.NewValue.Value, o.Icon);
});
@@ -71,7 +71,7 @@ public class IconButton: Button
SetPlacement(IconPlacement, Icon);
}
private void SetPlacement(IconPlacement placement, object? icon)
private void SetPlacement(Position placement, object? icon)
{
if (icon is null)
{
@@ -83,9 +83,9 @@ public class IconButton: Button
return;
}
PseudoClasses.Set(PC_Empty, false);
PseudoClasses.Set(PC_Left, placement == IconPlacement.Left);
PseudoClasses.Set(PC_Right, placement == IconPlacement.Right);
PseudoClasses.Set(PC_Top, placement == IconPlacement.Top);
PseudoClasses.Set(PC_Bottom, placement == IconPlacement.Bottom);
PseudoClasses.Set(PC_Left, placement == Position.Left);
PseudoClasses.Set(PC_Right, placement == Position.Right);
PseudoClasses.Set(PC_Top, placement == Position.Top);
PseudoClasses.Set(PC_Bottom, placement == Position.Bottom);
}
}

View File

@@ -0,0 +1,8 @@
using Avalonia.Controls.Primitives;
namespace Ursa.Controls.Layout;
public class DefaultDialogLayout: TemplatedControl
{
}

View File

@@ -2,6 +2,7 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Styling;
@@ -12,12 +13,11 @@ namespace Ursa.Controls;
/// <summary>
/// The messageBox used to display in OverlayDialogHost.
/// </summary>
[TemplatePart(PART_CloseButton, typeof(Button))]
[TemplatePart(PART_NoButton, typeof(Button))]
[TemplatePart(PART_OKButton, typeof(Button))]
[TemplatePart(PART_CancelButton, typeof(Button))]
[TemplatePart(PART_YesButton, typeof(Button))]
public class MessageBoxControl: DialogControl
public class MessageBoxControl: DialogControlBase
{
public const string PART_YesButton = "PART_YesButton";
public const string PART_NoButton = "PART_NoButton";
@@ -80,19 +80,19 @@ public class MessageBoxControl: DialogControl
{
if (button == _okButton)
{
OnDialogControlClosing(this, MessageBoxResult.OK);
OnElementClosing(this, MessageBoxResult.OK);
}
else if (button == _cancelButton)
{
OnDialogControlClosing(this, MessageBoxResult.Cancel);
OnElementClosing(this, MessageBoxResult.Cancel);
}
else if (button == _yesButton)
{
OnDialogControlClosing(this, MessageBoxResult.Yes);
OnElementClosing(this, MessageBoxResult.Yes);
}
else if (button == _noButton)
{
OnDialogControlClosing(this, MessageBoxResult.No);
OnElementClosing(this, MessageBoxResult.No);
}
}
}
@@ -127,13 +127,8 @@ public class MessageBoxControl: DialogControl
break;
}
}
private void SetVisibility(Button? button, bool visible)
{
if (button is not null) button.IsVisible = visible;
}
internal override void CloseDialog()
public override void Close()
{
MessageBoxResult result = Buttons switch
{
@@ -143,6 +138,6 @@ public class MessageBoxControl: DialogControl
MessageBoxButton.YesNoCancel => MessageBoxResult.Cancel,
_ => MessageBoxResult.None
};
OnDialogControlClosing(this, result);
OnElementClosing(this, result);
}
}

View File

@@ -1,97 +1,27 @@
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.Utilities;
using Ursa.Controls.OverlayShared;
using Ursa.Controls.Shapes;
using Ursa.EventArgs;
namespace Ursa.Controls;
public class OverlayDialogHost : Canvas
public partial class OverlayDialogHost
{
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)
public Thickness SnapThickness { get; set; } = new Thickness(0);
private static void ResetDialogPosition(DialogControlBase control, Size newSize)
{
var width = newSize.Width - control.Bounds.Width;
var height = newSize.Height - control.Bounds.Height;
@@ -116,16 +46,10 @@ public class OverlayDialogHost : Canvas
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.Source is DialogControlBase item)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
@@ -142,7 +66,7 @@ public class OverlayDialogHost : Canvas
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
if (e.Source is DialogControl item)
if (e.Source is DialogControlBase item)
{
_lastPoint = e.GetPosition(item);
}
@@ -150,49 +74,52 @@ public class OverlayDialogHost : Canvas
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
if (e.Source is DialogControl item)
if (e.Source is DialogControlBase item)
{
AnchorDialog(item);
AnchorAndUpdatePositionInfo(item);
}
}
internal void AddDialog(DialogControl control)
internal void AddDialog(DialogControlBase control)
{
PureRectangle? mask = null;
if (control.CanLightDismiss)
{
CreateOverlayMask(false, control.CanLightDismiss);
}
if (mask is not null)
{
Children.Add(mask);
}
this.Children.Add(control);
_dialogs.Add(control);
_layers.Add(new DialogPair(mask, control));
control.Measure(this.Bounds.Size);
control.Arrange(new Rect(control.DesiredSize));
SetToPosition(control);
control.DialogControlClosing += OnDialogControlClosing;
control.LayerChanged += OnDialogLayerChanged;
control.AddHandler(OverlayFeedbackElement.ClosedEvent, OnDialogControlClosing);
control.AddHandler(DialogControlBase.LayerChangedEvent, OnDialogLayerChanged);
ResetZIndices();
}
private void OnDialogControlClosing(object sender, object? e)
private async void OnDialogControlClosing(object sender, object? e)
{
if (sender is DialogControl control)
if (sender is DialogControlBase control)
{
var layer = _layers.FirstOrDefault(a => a.Element == control);
if (layer is null) return;
_layers.Remove(layer);
control.RemoveHandler(OverlayFeedbackElement.ClosedEvent, OnDialogControlClosing);
control.RemoveHandler(DialogControlBase.LayerChangedEvent, OnDialogLayerChanged);
Children.Remove(control);
control.DialogControlClosing -= OnDialogControlClosing;
control.LayerChanged -= OnDialogLayerChanged;
if (_dialogs.Contains(control))
if (layer.Mask is not null)
{
_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;
}
}
await _maskDisappearAnimation.RunAsync(layer.Mask);
Children.Remove(layer.Mask);
}
ResetZIndices();
}
}
@@ -201,94 +128,64 @@ public class OverlayDialogHost : Canvas
/// Add a dialog as a modal dialog to the host
/// </summary>
/// <param name="control"></param>
internal void AddModalDialog(DialogControl control)
internal void AddModalDialog(DialogControlBase control)
{
var mask = CreateOverlayMask(control.CanClickOnMaskToClose);
_masks.Add(mask);
_modalDialogs.Add(control);
var mask = CreateOverlayMask(true, control.CanClickOnMaskToClose);
_layers.Add(new DialogPair(mask, 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.DialogControlClosing += OnDialogControlClosing;
control.LayerChanged += OnDialogLayerChanged;
control.AddHandler(OverlayFeedbackElement.ClosedEvent, OnDialogControlClosing);
control.AddHandler(DialogControlBase.LayerChangedEvent, OnDialogLayerChanged);
_maskAppearAnimation.RunAsync(mask);
control.IsClosed = false;
}
// Handle dialog layer change event
private void OnDialogLayerChanged(object sender, DialogLayerChangeEventArgs e)
{
if (sender is not DialogControl control)
if (sender is not DialogControlBase control)
return;
if (!_dialogs.Contains(control))
return;
int index = _dialogs.IndexOf(control);
_dialogs.Remove(control);
var layer = _layers.FirstOrDefault(a => a.Element == control);
if (layer is null) return;
int index = _layers.IndexOf(layer);
_layers.Remove(layer);
int newIndex = index;
switch (e.ChangeType)
{
case DialogLayerChangeType.BringForward:
newIndex = MathUtilities.Clamp(index + 1, 0, _dialogs.Count);
newIndex = MathUtilities.Clamp(index + 1, 0, _layers.Count);
break;
case DialogLayerChangeType.SendBackward:
newIndex = MathUtilities.Clamp(index - 1, 0, _dialogs.Count);
newIndex = MathUtilities.Clamp(index - 1, 0, _layers.Count);
break;
case DialogLayerChangeType.BringToFront:
newIndex = _dialogs.Count;
newIndex = _layers.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;
}
_layers.Insert(newIndex, layer);
ResetZIndices();
}
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)
private void SetToPosition(DialogControlBase? control)
{
if (control is null) return;
double left = GetLeftPosition(control);
double top = GetTopPosition(control);
SetLeft(control, left);
SetTop(control, top);
AnchorDialog(control);
AnchorAndUpdatePositionInfo(control);
}
private void AnchorDialog(DialogControl control)
private void AnchorAndUpdatePositionInfo(DialogControlBase control)
{
control.ActualHorizontalAnchor = HorizontalPosition.Center;
control.ActualVerticalAnchor = VerticalPosition.Center;
@@ -329,7 +226,7 @@ public class OverlayDialogHost : Canvas
control.VerticalOffsetRatio = (top + bottom) == 0 ? 0 : top / (top + bottom);
}
private double GetLeftPosition(DialogControl control)
private double GetLeftPosition(DialogControlBase control)
{
double left = 0;
double offset = Math.Max(0, control.HorizontalOffset ?? 0);
@@ -355,9 +252,9 @@ public class OverlayDialogHost : Canvas
}
}
return left;
}
}
private double GetTopPosition(DialogControl control)
private double GetTopPosition(DialogControlBase control)
{
double top = 0;
double offset = Math.Max(0, control.VerticalOffset ?? 0);
@@ -380,22 +277,5 @@ public class OverlayDialogHost : Canvas
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;
}
}

View File

@@ -0,0 +1,152 @@
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Styling;
using Ursa.Common;
using Ursa.Controls.OverlayShared;
using Ursa.Controls.Shapes;
using Ursa.EventArgs;
namespace Ursa.Controls;
public partial class OverlayDialogHost
{
internal async void AddDrawer(DrawerControlBase control)
{
PureRectangle? mask = null;
if (control.ShowMask == false && control.CanLightDismiss)
{
mask = CreateOverlayMask(false, true);
}
else if (control.ShowMask)
{
mask = CreateOverlayMask(control.ShowMask, control.CanClickOnMaskToClose);
}
_layers.Add(new DialogPair(mask, control));
ResetZIndices();
if(mask is not null)this.Children.Add(mask);
this.Children.Add(control);
control.Measure(this.Bounds.Size);
control.Arrange(new Rect(control.DesiredSize));
SetDrawerPosition(control);
control.AddHandler(OverlayFeedbackElement.ClosedEvent, OnDrawerControlClosing);
var animation = CreateAnimation(control.Bounds.Size, control.Position, true);
if (mask is null)
{
await animation.RunAsync(control);
}
else
{
await Task.WhenAll(animation.RunAsync(control), _maskAppearAnimation.RunAsync(mask));
}
}
private void SetDrawerPosition(DrawerControlBase control)
{
if(control.Position is Position.Left or Position.Right)
{
control.Height = this.Bounds.Height;
}
if(control.Position is Position.Top or Position.Bottom)
{
control.Width = this.Bounds.Width;
}
}
private static void ResetDrawerPosition(DrawerControlBase control, Size newSize)
{
if (control.Position == Position.Right)
{
control.Height = newSize.Height;
SetLeft(control, newSize.Width - control.Bounds.Width);
}
else if (control.Position == Position.Left)
{
control.Height = newSize.Height;
SetLeft(control, 0);
}
else if (control.Position == Position.Top)
{
control.Width = newSize.Width;
SetTop(control, 0);
}
else
{
control.Width = newSize.Width;
SetTop(control, newSize.Height-control.Bounds.Height);
}
}
private Animation CreateAnimation(Size elementBounds, Position position, bool appear = true)
{
// left or top.
double source = 0;
double target = 0;
if (position == Position.Left)
{
source = appear ? -elementBounds.Width : 0;
target = appear ? 0 : -elementBounds.Width;
}
if (position == Position.Right)
{
source = appear ? Bounds.Width : Bounds.Width - elementBounds.Width;
target = appear ? Bounds.Width - elementBounds.Width : Bounds.Width;
}
if (position == Position.Top)
{
source = appear ? -elementBounds.Height : 0;
target = appear ? 0 : -elementBounds.Height;
}
if (position == Position.Bottom)
{
source = appear ? Bounds.Height : Bounds.Height - elementBounds.Height;
target = appear ? Bounds.Height - elementBounds.Height : Bounds.Height;
}
var targetProperty = position==Position.Left || position==Position.Right ? Canvas.LeftProperty : Canvas.TopProperty;
var animation = new Animation();
animation.Easing = new CubicEaseOut();
animation.FillMode = FillMode.Forward;
var keyFrame1 = new KeyFrame(){ Cue = new Cue(0.0) };
keyFrame1.Setters.Add(new Setter()
{ Property = targetProperty, Value = source });
var keyFrame2 = new KeyFrame() { Cue = new Cue(1.0) };
keyFrame2.Setters.Add(new Setter()
{ Property = targetProperty, Value = target });
animation.Children.Add(keyFrame1);
animation.Children.Add(keyFrame2);
animation.Duration = TimeSpan.FromSeconds(0.3);
return animation;
}
private async void OnDrawerControlClosing(object sender, ResultEventArgs e)
{
if (sender is DrawerControlBase control)
{
var layer = _layers.FirstOrDefault(a => a.Element == control);
if(layer is null) return;
_layers.Remove(layer);
control.RemoveHandler(OverlayFeedbackElement.ClosedEvent, OnDialogControlClosing);
control.RemoveHandler(DialogControlBase.LayerChangedEvent, OnDialogLayerChanged);
if (layer.Mask is not null)
{
layer.Mask.RemoveHandler(PointerPressedEvent, ClickMaskToCloseDialog);
var disappearAnimation = CreateAnimation(control.Bounds.Size, control.Position, false);
await Task.WhenAll(disappearAnimation.RunAsync(control), _maskDisappearAnimation.RunAsync(layer.Mask));
Children.Remove(layer.Mask);
}
else
{
var disappearAnimation = CreateAnimation(control.Bounds.Size, control.Position, false);
await disappearAnimation.RunAsync(control);
}
Children.Remove(control);
ResetZIndices();
}
}
}

View File

@@ -0,0 +1,176 @@
using Avalonia;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Media;
using Ursa.Controls.OverlayShared;
using Avalonia.Layout;
using Avalonia.Media.Immutable;
using Avalonia.Styling;
using Ursa.Controls.Shapes;
namespace Ursa.Controls;
public partial class OverlayDialogHost: Canvas
{
private static readonly Animation _maskAppearAnimation;
private static readonly Animation _maskDisappearAnimation;
private readonly List<DialogPair> _layers = new List<DialogPair>();
private class DialogPair
{
internal PureRectangle? Mask;
internal OverlayFeedbackElement Element;
public DialogPair(PureRectangle? mask, OverlayFeedbackElement element)
{
Mask = mask;
Element = element;
}
}
static OverlayDialogHost()
{
ClipToBoundsProperty.OverrideDefaultValue<OverlayDialogHost>(true);
_maskAppearAnimation = CreateOpacityAnimation(true);
_maskDisappearAnimation = CreateOpacityAnimation(false);
}
private static Animation CreateOpacityAnimation(bool appear)
{
var animation = new Animation();
animation.FillMode = FillMode.Forward;
var keyFrame1 = new KeyFrame{ Cue = new Cue(0.0) };
keyFrame1.Setters.Add(new Setter() { Property = OpacityProperty, Value = appear ? 0.0 : 1.0 });
var keyFrame2 = new KeyFrame{ Cue = new Cue(1.0) };
keyFrame2.Setters.Add(new Setter() { Property = OpacityProperty, Value = appear ? 1.0 : 0.0 });
animation.Children.Add(keyFrame1);
animation.Children.Add(keyFrame2);
animation.Duration = TimeSpan.FromSeconds(0.2);
return animation;
}
public string? HostId { get; set; }
public DataTemplates DialogDataTemplates { get; set; } = new DataTemplates();
public static readonly StyledProperty<IBrush?> OverlayMaskBrushProperty =
AvaloniaProperty.Register<OverlayDialogHost, IBrush?>(
nameof(OverlayMaskBrush));
public IBrush? OverlayMaskBrush
{
get => GetValue(OverlayMaskBrushProperty);
set => SetValue(OverlayMaskBrushProperty, value);
}
private PureRectangle CreateOverlayMask(bool modal, bool canCloseOnClick)
{
PureRectangle rec = new()
{
Width = this.Bounds.Width,
Height = this.Bounds.Height,
IsVisible = true,
};
if (modal)
{
rec[!PureRectangle.BackgroundProperty] = this[!OverlayMaskBrushProperty];
}
else if(canCloseOnClick)
{
rec.SetCurrentValue(Shape.FillProperty, Brushes.Transparent);
}
if (canCloseOnClick)
{
rec.AddHandler(PointerReleasedEvent, ClickMaskToCloseDialog);
}
return rec;
}
private void ClickMaskToCloseDialog(object sender, PointerReleasedEventArgs e)
{
if (sender is PureRectangle border)
{
var layer = _layers.FirstOrDefault(a => a.Mask == border);
if (layer is not null)
{
layer.Element.Close();
border.RemoveHandler(PointerReleasedEvent, ClickMaskToCloseDialog);
}
}
}
protected sealed override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
OverlayDialogManager.RegisterHost(this, HostId);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
OverlayDialogManager.UnregisterHost(HostId);
base.OnDetachedFromVisualTree(e);
}
protected sealed override void OnSizeChanged(SizeChangedEventArgs e)
{
base.OnSizeChanged(e);
for (int i = 0; i < _layers.Count; i++)
{
if (_layers[i].Mask is { } rect)
{
rect.Width = this.Bounds.Width;
rect.Height = this.Bounds.Height;
}
if (_layers[i].Element is DialogControlBase d)
{
ResetDialogPosition(d, e.NewSize);
}
else if (_layers[i].Element is DrawerControlBase drawer)
{
ResetDrawerPosition(drawer, e.NewSize);
}
}
}
private void ResetZIndices()
{
int index = 0;
for (int i = 0; i < _layers.Count; i++)
{
if(_layers[i].Mask is { } mask)
{
mask.ZIndex = index;
index++;
}
if(_layers[i].Element is { } dialog)
{
dialog.ZIndex = index;
index++;
}
}
}
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;
}
}

View File

@@ -0,0 +1,93 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Threading;
using Ursa.EventArgs;
namespace Ursa.Controls.OverlayShared;
public abstract class OverlayFeedbackElement: ContentControl
{
public static readonly StyledProperty<bool> IsClosedProperty =
AvaloniaProperty.Register<OverlayFeedbackElement, bool>(nameof(IsClosed), defaultValue: true);
public bool IsClosed
{
get => GetValue(IsClosedProperty);
set => SetValue(IsClosedProperty, value);
}
static OverlayFeedbackElement()
{
DataContextProperty.Changed.AddClassHandler<CustomDialogControl, object?>((o, e) => o.OnDataContextChange(e));
ClosedEvent.AddClassHandler<OverlayFeedbackElement>((o,e)=>o.OnClosed(e));
}
private void OnClosed(ResultEventArgs arg2)
{
SetCurrentValue(IsClosedProperty,true);
}
public static readonly RoutedEvent<ResultEventArgs> ClosedEvent = RoutedEvent.Register<DrawerControlBase, ResultEventArgs>(
nameof(Closed), RoutingStrategies.Bubble);
public event EventHandler<ResultEventArgs> Closed
{
add => AddHandler(ClosedEvent, value);
remove => RemoveHandler(ClosedEvent, value);
}
private void OnDataContextChange(AvaloniaPropertyChangedEventArgs<object?> args)
{
if (args.OldValue.Value is IDialogContext oldContext)
{
oldContext.RequestClose -= OnContextRequestClose;
}
if (args.NewValue.Value is IDialogContext newContext)
{
newContext.RequestClose += OnContextRequestClose;
}
}
protected virtual void OnElementClosing(object sender, object? args)
{
RaiseEvent(new ResultEventArgs(ClosedEvent, args));
}
private void OnContextRequestClose(object sender, object? args)
{
RaiseEvent(new ResultEventArgs(ClosedEvent, args));
}
public Task<T?> ShowAsync<T>(CancellationToken? token = default)
{
var tcs = new TaskCompletionSource<T?>();
token?.Register(() =>
{
Dispatcher.UIThread.Invoke(Close);
});
void OnCloseHandler(object sender, ResultEventArgs? args)
{
if (args?.Result is T result)
{
tcs.SetResult(result);
}
else
{
tcs.SetResult(default);
}
RemoveHandler(ClosedEvent, OnCloseHandler);
}
AddHandler(ClosedEvent, OnCloseHandler);
return tcs.Task;
}
protected static void SetVisibility(Button? button, bool visible)
{
if (button is not null) button.IsVisible = visible;
}
public abstract void Close();
}

View File

@@ -0,0 +1,30 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
namespace Ursa.Controls.Shapes;
/// <summary>
/// A rectangle, with no corner radius.
/// </summary>
public class PureRectangle: Control
{
public static readonly StyledProperty<IBrush?> BackgroundProperty = AvaloniaProperty.Register<PureRectangle, IBrush?>(
nameof(Background));
public IBrush? Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
static PureRectangle()
{
FocusableProperty.OverrideDefaultValue<PureRectangle>(false);
}
public override void Render(DrawingContext context)
{
context.DrawRectangle(Background, null, new Rect(Bounds.Size));
}
}

View File

@@ -0,0 +1,18 @@
using Avalonia.Interactivity;
namespace Ursa.EventArgs;
public class ResultEventArgs: RoutedEventArgs
{
public object? Result { get; set; }
public ResultEventArgs(object? result)
{
Result = result;
}
public ResultEventArgs(RoutedEvent routedEvent, object? result): base(routedEvent)
{
Result = result;
}
}