Merge pull request #657 from irihitech/popconfirm

New Control: PopConfirm
This commit is contained in:
Zhang Dian
2025-04-25 12:29:07 +08:00
committed by GitHub
15 changed files with 618 additions and 5 deletions

View File

@@ -0,0 +1,72 @@
<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:u="https://irihi.tech/ursa"
xmlns:viewModels="clr-namespace:Ursa.Demo.ViewModels"
mc:Ignorable="d" d:DesignWidth="800"
d:DesignHeight="450"
x:DataType="viewModels:PopConfirmDemoViewModel"
x:Class="Ursa.Demo.Pages.PopConfirmDemo">
<StackPanel HorizontalAlignment="Left">
<Border Theme="{DynamicResource CardBorder}">
<u:Form>
<u:ControlClassesInput
Name="classInput"
u:FormItem.Label="Control Classes"
Width="240">
</u:ControlClassesInput>
<u:EnumSelector
Name="placement"
Width="240"
u:FormItem.Label="Popup Placement"
EnumType="{x:Type PlacementMode}"
Value="{x:Static PlacementMode.BottomEdgeAlignedLeft}" />
</u:Form>
</Border>
<TextBlock Text="Default PopConfirm" Margin="0 16" />
<u:PopConfirm PopupHeader="确定是否要保存此修改?"
HorizontalAlignment="Left"
PopupContent="此修改将不可逆"
Placement="{Binding #placement.Value}"
ConfirmCommand="{Binding ConfirmCommand}"
CancelCommand="{Binding Path=CancelCommand}">
<Button Content="Hello World" />
</u:PopConfirm>
<TextBlock Text="GotFocus to open, LostFocus to close" Margin="0 16" />
<u:PopConfirm PopupHeader="确定是否要保存此修改?"
PopupContent="此修改将不可逆"
TriggerMode="Focus, Click"
HorizontalAlignment="Left"
Placement="{Binding #placement.Value}"
u:ControlClassesInput.Source="{Binding #classInput}"
ConfirmCommand="{Binding ConfirmCommand}"
CancelCommand="{Binding Path=CancelCommand}">
<Button Content="Hello World" />
</u:PopConfirm>
<TextBlock
Text="Non-button control as main element"
Margin="0 16" />
<u:PopConfirm PopupHeader="确定是否要保存此修改?"
PopupContent="此修改将不可逆"
TriggerMode="Click"
HorizontalAlignment="Left"
u:ControlClassesInput.Source="{Binding #classInput}"
Placement="{Binding #placement.Value}"
ConfirmCommand="{Binding ConfirmCommand}"
CancelCommand="{Binding Path=CancelCommand}">
<TextBlock Text="Hello World" />
</u:PopConfirm>
<TextBlock Text="Asynchronized command support"
Margin="0 16" />
<u:PopConfirm PopupHeader="确定是否要保存此修改?"
PopupContent="此修改将不可逆"
HorizontalAlignment="Left"
Placement="{Binding #placement.Value}"
u:ControlClassesInput.Source="{Binding #classInput}"
ConfirmCommand="{Binding AsyncConfirmCommand}"
CancelCommand="{Binding Path=AsyncCancelCommand}">
<Button Content="Hello World" />
</u:PopConfirm>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,25 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Ursa.Controls;
using Ursa.Demo.ViewModels;
namespace Ursa.Demo.Pages;
public partial class PopConfirmDemo : UserControl
{
public PopConfirmDemo()
{
InitializeComponent();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (this.DataContext is not PopConfirmDemoViewModel vm) return;
var manager = WindowToastManager.TryGetToastManager(TopLevel.GetTopLevel(this), out var m)
? m
: new WindowToastManager(TopLevel.GetTopLevel(this));
vm.ToastManager = manager;
}
}

View File

@@ -69,6 +69,7 @@ public partial class MainViewViewModel : ViewModelBase
MenuKeys.MenuKeyNumPad => new NumPadDemoViewModel(), MenuKeys.MenuKeyNumPad => new NumPadDemoViewModel(),
MenuKeys.MenuKeyPagination => new PaginationDemoViewModel(), MenuKeys.MenuKeyPagination => new PaginationDemoViewModel(),
MenuKeys.MenuKeyPinCode => new PinCodeDemoViewModel(), MenuKeys.MenuKeyPinCode => new PinCodeDemoViewModel(),
MenuKeys.MenuKeyPopConfirm => new PopConfirmDemoViewModel(),
MenuKeys.MenuKeyRangeSlider => new RangeSliderDemoViewModel(), MenuKeys.MenuKeyRangeSlider => new RangeSliderDemoViewModel(),
MenuKeys.MenuKeyRating => new RatingDemoViewModel(), MenuKeys.MenuKeyRating => new RatingDemoViewModel(),
MenuKeys.MenuKeyScrollToButton => new ScrollToButtonDemoViewModel(), MenuKeys.MenuKeyScrollToButton => new ScrollToButtonDemoViewModel(),

View File

@@ -44,6 +44,7 @@ public class MenuViewModel : ViewModelBase
new() { MenuHeader = "Loading", Key = MenuKeys.MenuKeyLoading, Status = "Updated" }, new() { MenuHeader = "Loading", Key = MenuKeys.MenuKeyLoading, Status = "Updated" },
new() { MenuHeader = "Message Box", Key = MenuKeys.MenuKeyMessageBox }, new() { MenuHeader = "Message Box", Key = MenuKeys.MenuKeyMessageBox },
new() { MenuHeader = "Notification", Key = MenuKeys.MenuKeyNotification }, new() { MenuHeader = "Notification", Key = MenuKeys.MenuKeyNotification },
new() { MenuHeader = "PopConfirm", Key = MenuKeys.MenuKeyPopConfirm },
new() { MenuHeader = "Toast", Key = MenuKeys.MenuKeyToast }, new() { MenuHeader = "Toast", Key = MenuKeys.MenuKeyToast },
new() { MenuHeader = "Skeleton", Key = MenuKeys.MenuKeySkeleton }, new() { MenuHeader = "Skeleton", Key = MenuKeys.MenuKeySkeleton },
} }
@@ -135,6 +136,7 @@ public static class MenuKeys
public const string MenuKeyNumPad = "NumPad"; public const string MenuKeyNumPad = "NumPad";
public const string MenuKeyPagination = "Pagination"; public const string MenuKeyPagination = "Pagination";
public const string MenuKeyPinCode = "PinCode"; public const string MenuKeyPinCode = "PinCode";
public const string MenuKeyPopConfirm = "PopConfirm";
public const string MenuKeyRangeSlider = "RangeSlider"; public const string MenuKeyRangeSlider = "RangeSlider";
public const string MenuKeyRating = "Rating"; public const string MenuKeyRating = "Rating";
public const string MenuKeyScrollToButton = "ScrollToButton"; public const string MenuKeyScrollToButton = "ScrollToButton";

View File

@@ -0,0 +1,48 @@
using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia.Controls.Notifications;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Ursa.Controls;
namespace Ursa.Demo.ViewModels;
public class PopConfirmDemoViewModel : ObservableObject
{
public PopConfirmDemoViewModel()
{
AsyncConfirmCommand = new AsyncRelayCommand(OnConfirmAsync);
AsyncCancelCommand = new RelayCommand(OnCancelAsync);
ConfirmCommand = new RelayCommand(OnConfirm);
CancelCommand = new RelayCommand(OnCancel);
}
internal WindowToastManager? ToastManager { get; set; }
public ICommand ConfirmCommand { get; }
public ICommand CancelCommand { get; }
public ICommand AsyncConfirmCommand { get; }
public ICommand AsyncCancelCommand { get; }
private void OnCancel()
{
ToastManager?.Show(new Toast("Canceled"), NotificationType.Error, classes: ["Light"]);
}
private void OnConfirm()
{
ToastManager?.Show(new Toast("Confirmed"), NotificationType.Success, classes: ["Light"]);
}
private async Task OnConfirmAsync()
{
await Task.Delay(3000);
ToastManager?.Show(new Toast("Async Confirmed"), NotificationType.Success, classes: ["Light"]);
}
private void OnCancelAsync()
{
ToastManager?.Show(new Toast("Async Canceled"), NotificationType.Error, classes: ["Light"]);
}
}

View File

@@ -0,0 +1,135 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:u="https://irihi.tech/ursa"
xmlns:helpers="clr-namespace:Irihi.Avalonia.Shared.Helpers;assembly=Irihi.Avalonia.Shared">
<!-- Add Resources Here -->
<ControlTheme x:Key="{x:Type u:PopConfirm}"
TargetType="u:PopConfirm">
<Setter Property="Placement" Value="BottomEdgeAlignedLeft" />
<Setter Property="Template">
<ControlTemplate TargetType="u:PopConfirm">
<Panel>
<ContentPresenter
Name="PART_ContentPresenter"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
<Popup
IsLightDismissEnabled="True"
IsOpen="{Binding IsDropdownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
OverlayInputPassThroughElement="{Binding #PART_ContentPresenter}"
Name="{x:Static u:PopConfirm.PART_Popup}"
Placement="{TemplateBinding Placement}">
<Border
MaxWidth="{DynamicResource PopConfirmBorderMaxWidth}"
Padding="{DynamicResource PopConfirmBorderPadding}"
Margin="{DynamicResource PopConfirmBorderMargin}"
Background="{DynamicResource PopConfirmBackground}"
BorderBrush="{DynamicResource PopConfirmBorderBrush}"
BorderThickness="{DynamicResource PopConfirmBorderThickness}"
CornerRadius="{DynamicResource PopConfirmBorderCornerRadius}"
BoxShadow="{DynamicResource PopConfirmBorderBoxShadows}">
<Grid
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
RowDefinitions="*, Auto"
ColumnDefinitions="Auto, *, Auto">
<!-- Icon Area -->
<Panel
Grid.Column="0"
Margin="{DynamicResource PopConfirmIconMargin}"
VerticalAlignment="Top">
<ContentPresenter
Content="{TemplateBinding Icon}" />
<PathIcon
Name="PART_BuildInIcon"
Theme="{DynamicResource InnerPathIcon}"
Classes="ExtraLarge"
Data="{DynamicResource BannerWarningIconGeometry}"
Foreground="{DynamicResource BannerWarningBorderBrush}"
IsVisible="{TemplateBinding Icon, Converter={x:Static ObjectConverters.IsNull}}" />
</Panel>
<StackPanel
Grid.Row="0"
Grid.Column="1"
Spacing="8">
<ContentPresenter
Content="{TemplateBinding PopupHeader}"
ContentTemplate="{TemplateBinding PopupHeaderTemplate}"
TextTrimming="CharacterEllipsis"
TextElement.FontSize="{DynamicResource PopConfirmTitleFontSize}"
TextElement.FontWeight="{DynamicResource PopConfirmTitleFontWeight}" />
<ContentPresenter
Content="{TemplateBinding PopupContent}"
ContentTemplate="{TemplateBinding PopupContentTemplate}"
Foreground="{DynamicResource PopConfirmContentForeground}"
TextWrapping="Wrap" />
</StackPanel>
<Button
Name="{x:Static u:PopConfirm.PART_CloseButton}"
Grid.Row="0"
Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Theme="{DynamicResource OverlayCloseButton}"
Command="{TemplateBinding CancelCommand}"
CommandParameter="{TemplateBinding CancelCommandParameter}" />
<StackPanel
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="3"
Margin="0 25 0 0"
Spacing="8"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button
Name="{x:Static u:PopConfirm.PART_CancelButton}"
Content="{DynamicResource STRING_MENU_DIALOG_CANCEL}"
Command="{TemplateBinding CancelCommand}"
CommandParameter="{TemplateBinding CancelCommandParameter}" />
<Button
Name="{x:Static u:PopConfirm.PART_ConfirmButton}"
Theme="{DynamicResource SolidButton}"
Content="{DynamicResource STRING_MENU_DIALOG_OK}"
Command="{TemplateBinding ConfirmCommand}"
CommandParameter="{TemplateBinding ConfirmCommandParameter}" />
</StackPanel>
</Grid>
</Border>
</Popup>
</Panel>
</ControlTemplate>
</Setter>
<Style Selector="^ /template/ Button#PART_CancelButton">
<Setter Property="helpers:ClassHelper.Classes" Value="Tertiary" />
</Style>
<Style Selector="^ /template/ Button#PART_ConfirmButton">
<Setter Property="helpers:ClassHelper.Classes" Value="Primary" />
</Style>
<Style Selector="^.Information">
<Style Selector="^ /template/ PathIcon#PART_BuildInIcon">
<Setter Property="Data" Value="{DynamicResource BannerInformationIconGeometry}" />
<Setter Property="Foreground" Value="{DynamicResource BannerInformationBorderBrush}" />
</Style>
</Style>
<Style Selector="^.Success">
<Style Selector="^ /template/ PathIcon#PART_BuildInIcon">
<Setter Property="Data" Value="{DynamicResource BannerSuccessIconGeometry}" />
<Setter Property="Foreground" Value="{DynamicResource BannerSuccessBorderBrush}" />
</Style>
</Style>
<Style Selector="^.Warning">
<Style Selector="^ /template/ PathIcon#PART_BuildInIcon">
<Setter Property="Data" Value="{DynamicResource BannerWarningIconGeometry}" />
<Setter Property="Foreground" Value="{DynamicResource BannerWarningBorderBrush}" />
</Style>
</Style>
<Style Selector="^.Error">
<Style Selector="^ /template/ PathIcon#PART_BuildInIcon">
<Setter Property="Data" Value="{DynamicResource BannerErrorIconGeometry}" />
<Setter Property="Foreground" Value="{DynamicResource BannerErrorBorderBrush}" />
</Style>
</Style>
</ControlTheme>
</ResourceDictionary>

View File

@@ -36,26 +36,27 @@
<ResourceInclude Source="NumPad.axaml" /> <ResourceInclude Source="NumPad.axaml" />
<ResourceInclude Source="NumberDisplayer.axaml" /> <ResourceInclude Source="NumberDisplayer.axaml" />
<ResourceInclude Source="Pagination.axaml" /> <ResourceInclude Source="Pagination.axaml" />
<ResourceInclude Source="PathPicker.axaml"/>
<ResourceInclude Source="PinCode.axaml" />
<ResourceInclude Source="PopConfirm.axaml" />
<ResourceInclude Source="RangeSlider.axaml" /> <ResourceInclude Source="RangeSlider.axaml" />
<ResourceInclude Source="Rating.axaml" /> <ResourceInclude Source="Rating.axaml" />
<ResourceInclude Source="Resizer.axaml" /> <ResourceInclude Source="Resizer.axaml" />
<ResourceInclude Source="ScrollToButton.axaml" /> <ResourceInclude Source="ScrollToButton.axaml" />
<ResourceInclude Source="SelectionList.axaml" /> <ResourceInclude Source="SelectionList.axaml" />
<ResourceInclude Source="Skeleton.axaml" />
<ResourceInclude Source="SplashWindow.axaml"/>
<ResourceInclude Source="TagInput.axaml" /> <ResourceInclude Source="TagInput.axaml" />
<ResourceInclude Source="ThemeSelector.axaml" /> <ResourceInclude Source="ThemeSelector.axaml" />
<ResourceInclude Source="TimePicker.axaml" /> <ResourceInclude Source="TimePicker.axaml" />
<ResourceInclude Source="TimeRangePicker.axaml" /> <ResourceInclude Source="TimeRangePicker.axaml" />
<ResourceInclude Source="Timeline.axaml" /> <ResourceInclude Source="Timeline.axaml" />
<ResourceInclude Source="TreeComboBox.axaml"/> <ResourceInclude Source="TreeComboBox.axaml"/>
<ResourceInclude Source="Skeleton.axaml" />
<ResourceInclude Source="TwoTonePathIcon.axaml" /> <ResourceInclude Source="TwoTonePathIcon.axaml" />
<ResourceInclude Source="Toast.axaml" /> <ResourceInclude Source="Toast.axaml" />
<ResourceInclude Source="ToolBar.axaml" /> <ResourceInclude Source="ToolBar.axaml" />
<ResourceInclude Source="TimeBox.axaml"/> <ResourceInclude Source="TimeBox.axaml"/>
<ResourceInclude Source="UrsaView.axaml" /> <ResourceInclude Source="UrsaView.axaml" />
<ResourceInclude Source="UrsaWindow.axaml"/> <ResourceInclude Source="UrsaWindow.axaml"/>
<ResourceInclude Source="PinCode.axaml" />
<ResourceInclude Source="PathPicker.axaml"/>
<ResourceInclude Source="SplashWindow.axaml"/>
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>

View File

@@ -0,0 +1,6 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="PopConfirmBackground">#43444A</SolidColorBrush>
<SolidColorBrush x:Key="PopConfirmBorderBrush" Opacity="0.08" Color="White" />
<SolidColorBrush x:Key="PopConfirmContentForeground" Opacity="0.6" Color="#F9F9F9" />
<BoxShadows x:Key="PopConfirmBorderBoxShadows">inset 0 0 0 1 #1AFFFFFF, 0 4 14 #40000000</BoxShadows>
</ResourceDictionary>

View File

@@ -15,6 +15,7 @@
<ResourceInclude Source="NavigationMenu.axaml" /> <ResourceInclude Source="NavigationMenu.axaml" />
<ResourceInclude Source="NotificationShared.axaml" /> <ResourceInclude Source="NotificationShared.axaml" />
<ResourceInclude Source="Pagination.axaml" /> <ResourceInclude Source="Pagination.axaml" />
<ResourceInclude Source="PopConfirm.axaml" />
<ResourceInclude Source="Rating.axaml" /> <ResourceInclude Source="Rating.axaml" />
<ResourceInclude Source="SelectionList.axaml" /> <ResourceInclude Source="SelectionList.axaml" />
<ResourceInclude Source="Skeleton.axaml" /> <ResourceInclude Source="Skeleton.axaml" />

View File

@@ -0,0 +1,6 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="PopConfirmBackground">White</SolidColorBrush>
<SolidColorBrush x:Key="PopConfirmBorderBrush" Opacity="0.08" Color="#1C1F23" />
<SolidColorBrush x:Key="PopConfirmContentForeground" Opacity="0.62" Color="#1C1F23" />
<BoxShadows x:Key="PopConfirmBorderBoxShadows">0 0 1 #4A000000, 0 4 14 #1A000000</BoxShadows>
</ResourceDictionary>

View File

@@ -15,6 +15,7 @@
<ResourceInclude Source="NavigationMenu.axaml" /> <ResourceInclude Source="NavigationMenu.axaml" />
<ResourceInclude Source="NotificationShared.axaml" /> <ResourceInclude Source="NotificationShared.axaml" />
<ResourceInclude Source="Pagination.axaml" /> <ResourceInclude Source="Pagination.axaml" />
<ResourceInclude Source="PopConfirm.axaml" />
<ResourceInclude Source="Rating.axaml" /> <ResourceInclude Source="Rating.axaml" />
<ResourceInclude Source="SelectionList.axaml" /> <ResourceInclude Source="SelectionList.axaml" />
<ResourceInclude Source="Skeleton.axaml" /> <ResourceInclude Source="Skeleton.axaml" />

View File

@@ -0,0 +1,11 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<x:Double x:Key="PopConfirmBorderMaxWidth">400</x:Double>
<Thickness x:Key="PopConfirmBorderPadding">20 24 24 24</Thickness>
<Thickness x:Key="PopConfirmBorderMargin">4</Thickness>
<Thickness x:Key="PopConfirmBorderThickness">1</Thickness>
<CornerRadius x:Key="PopConfirmBorderCornerRadius">6</CornerRadius>
<Thickness x:Key="PopConfirmIconMargin">0 0 12 0</Thickness>
<x:Double x:Key="PopConfirmTitleFontSize">16</x:Double>
<FontWeight x:Key="PopConfirmTitleFontWeight">600</FontWeight>
</ResourceDictionary>

View File

@@ -16,8 +16,9 @@
<ResourceInclude Source="NavigationMenu.axaml" /> <ResourceInclude Source="NavigationMenu.axaml" />
<ResourceInclude Source="Notification.axaml" /> <ResourceInclude Source="Notification.axaml" />
<ResourceInclude Source="Pagination.axaml" /> <ResourceInclude Source="Pagination.axaml" />
<ResourceInclude Source="PopConfirm.axaml" />
<ResourceInclude Source="Rating.axaml" /> <ResourceInclude Source="Rating.axaml" />
<ResourceInclude Source="Resizer.axaml"/> <ResourceInclude Source="Resizer.axaml" />
<ResourceInclude Source="ScrollToButton.axaml" /> <ResourceInclude Source="ScrollToButton.axaml" />
<ResourceInclude Source="TagInput.axaml" /> <ResourceInclude Source="TagInput.axaml" />
<ResourceInclude Source="Skeleton.axaml" /> <ResourceInclude Source="Skeleton.axaml" />

View File

@@ -0,0 +1,295 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Windows.Input;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Irihi.Avalonia.Shared.Helpers;
namespace Ursa.Controls;
[PseudoClasses(PC_DropdownOpen, PC_Icon)]
public class PopConfirm : ContentControl
{
public const string PART_CloseButton = "PART_CloseButton";
public const string PART_ConfirmButton = "PART_ConfirmButton";
public const string PART_CancelButton = "PART_CancelButton";
public const string PART_Popup = "PART_Popup";
public const string PC_DropdownOpen = ":dropdownopen";
public const string PC_Icon = ":icon";
public static readonly StyledProperty<object?> PopupHeaderProperty = AvaloniaProperty.Register<PopConfirm, object?>(
nameof(PopupHeader));
public static readonly StyledProperty<IDataTemplate?> PopupHeaderTemplateProperty =
AvaloniaProperty.Register<PopConfirm, IDataTemplate?>(
nameof(PopupHeaderTemplate));
public static readonly StyledProperty<object?> PopupContentProperty =
AvaloniaProperty.Register<PopConfirm, object?>(
nameof(PopupContent));
public static readonly StyledProperty<IDataTemplate?> PopupContentTemplateProperty =
AvaloniaProperty.Register<PopConfirm, IDataTemplate?>(
nameof(PopupContentTemplate));
public static readonly StyledProperty<ICommand?> ConfirmCommandProperty =
AvaloniaProperty.Register<PopConfirm, ICommand?>(
nameof(ConfirmCommand));
public static readonly StyledProperty<ICommand?> CancelCommandProperty =
AvaloniaProperty.Register<PopConfirm, ICommand?>(
nameof(CancelCommand));
public static readonly StyledProperty<object?> ConfirmCommandParameterProperty =
AvaloniaProperty.Register<PopConfirm, object?>(
nameof(ConfirmCommandParameter));
public static readonly StyledProperty<object?> CancelCommandParameterProperty =
AvaloniaProperty.Register<PopConfirm, object?>(
nameof(CancelCommandParameter));
public static readonly StyledProperty<PopConfirmTriggerMode> TriggerModeProperty =
AvaloniaProperty.Register<PopConfirm, PopConfirmTriggerMode>(
nameof(TriggerMode), PopConfirmTriggerMode.Click);
public static readonly StyledProperty<bool> HandleAsyncCommandProperty =
AvaloniaProperty.Register<PopConfirm, bool>(
nameof(HandleAsyncCommand), true);
public static readonly StyledProperty<bool> IsDropdownOpenProperty = AvaloniaProperty.Register<PopConfirm, bool>(
nameof(IsDropdownOpen));
public static readonly StyledProperty<PlacementMode> PlacementProperty =
Popup.PlacementProperty.AddOwner<PopConfirm>(new StyledPropertyMetadata<PlacementMode>());
public static readonly StyledProperty<object?> IconProperty = Banner.IconProperty.AddOwner<PopConfirm>();
private Button? _closeButton;
private Button? _cancelButton;
private IDisposable? _childChangeDisposable;
private Button? _confirmButton;
private Popup? _popup;
static PopConfirm()
{
IsDropdownOpenProperty.AffectsPseudoClass<PopConfirm>(PC_DropdownOpen);
TriggerModeProperty.Changed.AddClassHandler<PopConfirm, PopConfirmTriggerMode>((pop, args) =>
pop.OnTriggerModeChanged(args));
}
public object? Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
public object? PopupHeader
{
get => GetValue(PopupHeaderProperty);
set => SetValue(PopupHeaderProperty, value);
}
public IDataTemplate? PopupHeaderTemplate
{
get => GetValue(PopupHeaderTemplateProperty);
set => SetValue(PopupHeaderTemplateProperty, value);
}
public object? PopupContent
{
get => GetValue(PopupContentProperty);
set => SetValue(PopupContentProperty, value);
}
public IDataTemplate? PopupContentTemplate
{
get => GetValue(PopupContentTemplateProperty);
set => SetValue(PopupContentTemplateProperty, value);
}
public ICommand? ConfirmCommand
{
get => GetValue(ConfirmCommandProperty);
set => SetValue(ConfirmCommandProperty, value);
}
public ICommand? CancelCommand
{
get => GetValue(CancelCommandProperty);
set => SetValue(CancelCommandProperty, value);
}
public object? ConfirmCommandParameter
{
get => GetValue(ConfirmCommandParameterProperty);
set => SetValue(ConfirmCommandParameterProperty, value);
}
public object? CancelCommandParameter
{
get => GetValue(CancelCommandParameterProperty);
set => SetValue(CancelCommandParameterProperty, value);
}
public PopConfirmTriggerMode TriggerMode
{
get => GetValue(TriggerModeProperty);
set => SetValue(TriggerModeProperty, value);
}
public bool HandleAsyncCommand
{
get => GetValue(HandleAsyncCommandProperty);
set => SetValue(HandleAsyncCommandProperty, value);
}
public bool IsDropdownOpen
{
get => GetValue(IsDropdownOpenProperty);
set => SetValue(IsDropdownOpenProperty, value);
}
public PlacementMode Placement
{
get => GetValue(PlacementProperty);
set => SetValue(PlacementProperty, value);
}
private void OnTriggerModeChanged(AvaloniaPropertyChangedEventArgs<PopConfirmTriggerMode> args)
{
var child = Presenter?.Child;
TeardownChildrenEventSubscriptions(child);
SetupChildrenEventSubscriptions(child, args.GetNewValue<PopConfirmTriggerMode>());
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_closeButton = e.NameScope.Find<Button>(PART_CloseButton);
_confirmButton = e.NameScope.Find<Button>(PART_ConfirmButton);
_cancelButton = e.NameScope.Find<Button>(PART_CancelButton);
_popup = e.NameScope.Find<Popup>(PART_Popup);
Button.ClickEvent.AddHandler(OnButtonClicked, _closeButton, _cancelButton, _confirmButton);
}
private void OnButtonClicked(object sender, RoutedEventArgs e)
{
if (!HandleAsyncCommand) _popup?.SetValue(Popup.IsOpenProperty, false);
// This is a hack for MVVM toolkit and Prism that uses INotifyPropertyChanged for async command. It counts the number of
// IsRunning property changes to determine when the command is finished.
if (sender is Button { Command: { } command and (INotifyPropertyChanged or IDisposable) } button)
{
var count = 0;
void OnCanExecuteChanged(object? _, System.EventArgs a)
{
count++;
if (count < 2) return;
var canExecute = command.CanExecute(button.CommandParameter);
if (canExecute)
{
_popup?.SetValue(Popup.IsOpenProperty, false);
}
command.CanExecuteChanged -= OnCanExecuteChanged;
}
command.CanExecuteChanged += OnCanExecuteChanged;
}
else
{
_popup?.SetValue(Popup.IsOpenProperty, false);
}
}
protected override bool RegisterContentPresenter(ContentPresenter presenter)
{
var result = base.RegisterContentPresenter(presenter);
if (result)
_childChangeDisposable = presenter.GetPropertyChangedObservable(ContentPresenter.ChildProperty)
.Subscribe(OnChildChanged);
return result;
}
private void OnChildChanged(AvaloniaPropertyChangedEventArgs arg)
{
TeardownChildrenEventSubscriptions(arg.GetOldValue<Control?>());
SetupChildrenEventSubscriptions(arg.GetNewValue<Control?>(), TriggerMode);
}
private void SetupChildrenEventSubscriptions(Control? child, PopConfirmTriggerMode mode)
{
if (child is null) return;
if (mode.HasFlag(PopConfirmTriggerMode.Click))
{
if (child is Button button)
Button.ClickEvent.AddHandler(OnMainButtonClicked, button);
else
PointerPressedEvent.AddHandler(OnMainElementPressed, child);
}
if (mode.HasFlag(PopConfirmTriggerMode.Focus))
{
GotFocusEvent.AddHandler(OnMainElementGotFocus, child);
LostFocusEvent.AddHandler(OnMainElementLostFocus, child);
}
}
private void OnMainElementLostFocus(object sender, RoutedEventArgs e)
{
var newFocus = TopLevel.GetTopLevel(this)?.FocusManager?.GetFocusedElement();
if (newFocus is Visual v && _popup?.IsInsidePopup(v) == true) return;
SetCurrentValue(IsDropdownOpenProperty, false);
}
private bool _suppressButtonClickEvent;
private void OnMainElementGotFocus(object sender, GotFocusEventArgs e)
{
Debug.WriteLine("Got Focus");
if (TriggerMode.HasFlag(PopConfirmTriggerMode.Click) && TriggerMode.HasFlag(PopConfirmTriggerMode.Focus))
{
_suppressButtonClickEvent = true;
}
SetCurrentValue(IsDropdownOpenProperty, true);
}
private void TeardownChildrenEventSubscriptions(Control? child)
{
if (child is null) return;
PointerPressedEvent.RemoveHandler(OnMainElementPressed, child);
Button.ClickEvent.RemoveHandler(OnMainButtonClicked, child);
GotFocusEvent.RemoveHandler(OnMainElementGotFocus, child);
LostFocusEvent.RemoveHandler(OnMainElementLostFocus, child);
}
private void OnMainButtonClicked(object sender, RoutedEventArgs e)
{
Debug.WriteLine("Main Button Clicked");
if (!_suppressButtonClickEvent)
{
SetCurrentValue(IsDropdownOpenProperty, !IsDropdownOpen);
}
_suppressButtonClickEvent = false;
}
private void OnMainElementPressed(object sender, PointerPressedEventArgs e)
{
SetCurrentValue(IsDropdownOpenProperty, !IsDropdownOpen);
}
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_childChangeDisposable?.Dispose();
base.OnDetachedFromLogicalTree(e);
}
}

View File

@@ -0,0 +1,8 @@
namespace Ursa.Controls;
[Flags]
public enum PopConfirmTriggerMode
{
Click = 1,
Focus = 2,
}