feat: implement most feature.

This commit is contained in:
Dong Bin
2025-04-18 17:32:19 +08:00
parent 713c1731c9
commit 2a081c0f95
4 changed files with 157 additions and 101 deletions

View File

@@ -10,9 +10,8 @@
x:Class="Ursa.Demo.Pages.PopConfirmDemo"> x:Class="Ursa.Demo.Pages.PopConfirmDemo">
<StackPanel HorizontalAlignment="Left"> <StackPanel HorizontalAlignment="Left">
<u:PopConfirm PopupHeader="Header" PopupContent="Content" <u:PopConfirm PopupHeader="Header" PopupContent="Content"
ConfirmCommand="{Binding Path=ConfirmCommand}" ConfirmCommand="{Binding ConfirmCommand}"
CancelCommand="{Binding Path=CancelCommand}" CancelCommand="{Binding Path=CancelCommand}" >
>
<Button Content="Hello World"></Button> <Button Content="Hello World"></Button>
</u:PopConfirm> </u:PopConfirm>
</StackPanel> </StackPanel>

View File

@@ -6,27 +6,38 @@
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate TargetType="u:PopConfirm"> <ControlTemplate TargetType="u:PopConfirm">
<Panel> <Panel>
<ContentPresenter Name="PART_ContentPresenter" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}"/> <ContentPresenter
Name="PART_ContentPresenter"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
<Popup <Popup
IsLightDismissEnabled="True" IsLightDismissEnabled="True"
IsOpen="{TemplateBinding IsDropdownOpen, Mode=TwoWay}"
OverlayInputPassThroughElement="{Binding #PART_ContentPresenter}" OverlayInputPassThroughElement="{Binding #PART_ContentPresenter}"
Name="{x:Static u:PopConfirm.PART_Popup}" Name="{x:Static u:PopConfirm.PART_Popup}"
Placement="Bottom" > Placement="{TemplateBinding Placement}" >
<Border Theme="{DynamicResource CardBorder}"> <Border Theme="{DynamicResource CardBorder}">
<StackPanel> <StackPanel>
<ContentPresenter Content="{TemplateBinding PopupHeader}" ContentTemplate="{TemplateBinding PopupHeaderTemplate}"/> <ContentPresenter
<ContentPresenter Content="{TemplateBinding PopupContent}" ContentTemplate="{TemplateBinding PopupContentTemplate}"/> Content="{TemplateBinding PopupHeader}"
<StackPanel Orientation="Horizontal"> ContentTemplate="{TemplateBinding PopupHeaderTemplate}"/>
<Button <ContentPresenter
Name="{x:Static u:PopConfirm.PART_ConfirmButton}" Content="{TemplateBinding PopupContent}"
Content="Confirm" ContentTemplate="{TemplateBinding PopupContentTemplate}"/>
Command="{TemplateBinding ConfirmCommand}" <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
CommandParameter="{TemplateBinding ConfirmCommandParameter}"/>
<Button <Button
Name="{x:Static u:PopConfirm.PART_CancelButton}" Name="{x:Static u:PopConfirm.PART_CancelButton}"
Content="Cancel" Margin="0 0 8 0"
Content="{DynamicResource STRING_MENU_DIALOG_CANCEL}"
Command="{TemplateBinding CancelCommand}" Command="{TemplateBinding CancelCommand}"
CommandParameter="{TemplateBinding CancelCommandParameter}"/> 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> </StackPanel>
</StackPanel> </StackPanel>
</Border> </Border>

View File

@@ -1,142 +1,163 @@
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics;
using System.Windows.Input; using System.Windows.Input;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Irihi.Avalonia.Shared.Helpers; using Irihi.Avalonia.Shared.Helpers;
namespace Ursa.Controls; namespace Ursa.Controls;
public class PopConfirm: ContentControl [PseudoClasses(PC_DropdownOpen)]
public class PopConfirm : ContentControl
{ {
public const string PART_ConfirmButton = "PART_ConfirmButton"; public const string PART_ConfirmButton = "PART_ConfirmButton";
public const string PART_CancelButton = "PART_CancelButton"; public const string PART_CancelButton = "PART_CancelButton";
public const string PART_Popup = "PART_Popup"; public const string PART_Popup = "PART_Popup";
public const string PC_DropdownOpen = ":dropdownopen";
private Button? _confirmButton;
private Button? _cancelButton;
private Popup? _popup;
public static readonly StyledProperty<object?> PopupHeaderProperty = AvaloniaProperty.Register<PopConfirm, object?>( public static readonly StyledProperty<object?> PopupHeaderProperty = AvaloniaProperty.Register<PopConfirm, object?>(
nameof(PopupHeader)); 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));
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>();
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? PopupHeader public object? PopupHeader
{ {
get => GetValue(PopupHeaderProperty); get => GetValue(PopupHeaderProperty);
set => SetValue(PopupHeaderProperty, value); set => SetValue(PopupHeaderProperty, value);
} }
public static readonly StyledProperty<IDataTemplate?> PopupHeaderTemplateProperty = AvaloniaProperty.Register<PopConfirm, IDataTemplate?>(
nameof(PopupHeaderTemplate));
public IDataTemplate? PopupHeaderTemplate public IDataTemplate? PopupHeaderTemplate
{ {
get => GetValue(PopupHeaderTemplateProperty); get => GetValue(PopupHeaderTemplateProperty);
set => SetValue(PopupHeaderTemplateProperty, value); set => SetValue(PopupHeaderTemplateProperty, value);
} }
public static readonly StyledProperty<object?> PopupContentProperty = AvaloniaProperty.Register<PopConfirm, object?>(
nameof(PopupContent));
public object? PopupContent public object? PopupContent
{ {
get => GetValue(PopupContentProperty); get => GetValue(PopupContentProperty);
set => SetValue(PopupContentProperty, value); set => SetValue(PopupContentProperty, value);
} }
public static readonly StyledProperty<IDataTemplate?> PopupContentTemplateProperty = AvaloniaProperty.Register<PopConfirm, IDataTemplate?>(
nameof(PopupContentTemplate));
public IDataTemplate? PopupContentTemplate public IDataTemplate? PopupContentTemplate
{ {
get => GetValue(PopupContentTemplateProperty); get => GetValue(PopupContentTemplateProperty);
set => SetValue(PopupContentTemplateProperty, value); set => SetValue(PopupContentTemplateProperty, value);
} }
public static readonly StyledProperty<ICommand?> ConfirmCommandProperty = AvaloniaProperty.Register<PopConfirm, ICommand?>(
nameof(ConfirmCommand));
public ICommand? ConfirmCommand public ICommand? ConfirmCommand
{ {
get => GetValue(ConfirmCommandProperty); get => GetValue(ConfirmCommandProperty);
set => SetValue(ConfirmCommandProperty, value); set => SetValue(ConfirmCommandProperty, value);
} }
public static readonly StyledProperty<ICommand?> CancelCommandProperty = AvaloniaProperty.Register<PopConfirm, ICommand?>(
nameof(CancelCommand));
public ICommand? CancelCommand public ICommand? CancelCommand
{ {
get => GetValue(CancelCommandProperty); get => GetValue(CancelCommandProperty);
set => SetValue(CancelCommandProperty, value); set => SetValue(CancelCommandProperty, value);
} }
public static readonly StyledProperty<object?> ConfirmCommandParameterProperty = AvaloniaProperty.Register<PopConfirm, object?>(
nameof(ConfirmCommandParameter));
public object? ConfirmCommandParameter public object? ConfirmCommandParameter
{ {
get => GetValue(ConfirmCommandParameterProperty); get => GetValue(ConfirmCommandParameterProperty);
set => SetValue(ConfirmCommandParameterProperty, value); set => SetValue(ConfirmCommandParameterProperty, value);
} }
public static readonly StyledProperty<object?> CancelCommandParameterProperty = AvaloniaProperty.Register<PopConfirm, object?>(
nameof(CancelCommandParameter));
public object? CancelCommandParameter public object? CancelCommandParameter
{ {
get => GetValue(CancelCommandParameterProperty); get => GetValue(CancelCommandParameterProperty);
set => SetValue(CancelCommandParameterProperty, value); set => SetValue(CancelCommandParameterProperty, value);
} }
public static readonly StyledProperty<PopConfirmTriggerMode> TriggerModeProperty =
AvaloniaProperty.Register<PopConfirm, PopConfirmTriggerMode>(
nameof(TriggerMode));
public PopConfirmTriggerMode TriggerMode public PopConfirmTriggerMode TriggerMode
{ {
get => GetValue(TriggerModeProperty); get => GetValue(TriggerModeProperty);
set => SetValue(TriggerModeProperty, value); set => SetValue(TriggerModeProperty, value);
} }
public static readonly StyledProperty<bool> HandleAsyncCommandProperty = AvaloniaProperty.Register<PopConfirm, bool>(
nameof(HandleAsyncCommand), true);
public bool HandleAsyncCommand public bool HandleAsyncCommand
{ {
get => GetValue(HandleAsyncCommandProperty); get => GetValue(HandleAsyncCommandProperty);
set => SetValue(HandleAsyncCommandProperty, value); set => SetValue(HandleAsyncCommandProperty, value);
} }
static PopConfirm() public bool IsDropdownOpen
{ {
ConfirmCommandProperty.Changed.AddClassHandler<PopConfirm, ICommand?>((popconfirm, args) => popconfirm.OnCommandChanged(args)); get => GetValue(IsDropdownOpenProperty);
set => SetValue(IsDropdownOpenProperty, value);
} }
private void OnCommandChanged(AvaloniaPropertyChangedEventArgs<ICommand?> args) public PlacementMode Placement
{ {
get => GetValue(PlacementProperty);
set => SetValue(PlacementProperty, value);
} }
public PopConfirm() private void OnTriggerModeChanged(AvaloniaPropertyChangedEventArgs<PopConfirmTriggerMode> args)
{ {
var child = Presenter?.Child;
} TeardownChildrenEventSubscriptions(child, args.GetOldValue<PopConfirmTriggerMode>());
SetupChildrenEventSubscriptions(child, args.GetNewValue<PopConfirmTriggerMode>());
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
}
private void OnContentChanged()
{
} }
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
@@ -148,76 +169,102 @@ public class PopConfirm: ContentControl
Button.ClickEvent.AddHandler(OnButtonClicked, _confirmButton, _cancelButton); Button.ClickEvent.AddHandler(OnButtonClicked, _confirmButton, _cancelButton);
} }
private void OnButtonClicked(object sender, RoutedEventArgs e) private async void OnButtonClicked(object sender, RoutedEventArgs e)
{ {
if (!HandleAsyncCommand) if (!HandleAsyncCommand) _popup?.SetValue(Popup.IsOpenProperty, false);
{
_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 // 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. // IsRunning property changes to determine when the command is finished.
if (sender is Button button && button.Command is { } command and INotifyPropertyChanged) if (sender is Button button && button.Command is { } command and (INotifyPropertyChanged or IDisposable))
{ {
var count = 0; var count = 0;
void OnCanExecuteChanged(object? _, System.EventArgs e) void OnCanExecuteChanged(object? _, System.EventArgs e)
{ {
count++; count++;
if (count != 2) return; if (count != 2) return;
var canExecute = command.CanExecute(button.CommandParameter); var canExecute = command.CanExecute(button.CommandParameter);
if (canExecute == true) if (canExecute)
{ {
command.CanExecuteChanged -= OnCanExecuteChanged; command.CanExecuteChanged -= OnCanExecuteChanged;
_popup?.SetValue(Popup.IsOpenProperty, false); _popup?.SetValue(Popup.IsOpenProperty, false);
} }
} }
command.CanExecuteChanged += OnCanExecuteChanged; command.CanExecuteChanged += OnCanExecuteChanged;
} }
// TODO: This is a hack for ReactiveUI that ReactiveCommand is an IDisposable.
else if (sender is Button b2 && b2.Command is { } command2 and IDisposable)
{
var count = 0;
void OnCanExecuteChanged2(object? _, System.EventArgs e)
{
count++;
if (count != 2) return;
var canExecute = command2.CanExecute(b2.CommandParameter);
if (canExecute == true)
{
command2.CanExecuteChanged -= OnCanExecuteChanged2;
_popup?.SetValue(Popup.IsOpenProperty, false);
}
}
command2.CanExecuteChanged += OnCanExecuteChanged2;
}
else else
{ {
_popup?.SetValue(Popup.IsOpenProperty, false); _popup?.SetValue(Popup.IsOpenProperty, false);
} }
} }
private IDisposable? _childChangeDisposable;
protected override bool RegisterContentPresenter(ContentPresenter presenter) protected override bool RegisterContentPresenter(ContentPresenter presenter)
{ {
var result = base.RegisterContentPresenter(presenter); var result = base.RegisterContentPresenter(presenter);
_childChangeDisposable = presenter.GetPropertyChangedObservable(ContentPresenter.ChildProperty).Subscribe(OnChildChanged); _childChangeDisposable = presenter.GetPropertyChangedObservable(ContentPresenter.ChildProperty)
.Subscribe(OnChildChanged);
return result; return result;
} }
private void OnChildChanged(AvaloniaPropertyChangedEventArgs arg) private void OnChildChanged(AvaloniaPropertyChangedEventArgs arg)
{ {
TeardownChildrenEventSubscriptions(arg.GetOldValue<Control?>(), TriggerMode);
SetupChildrenEventSubscriptions(arg.GetNewValue<Control?>(), TriggerMode);
if (arg.GetNewValue<Control?>() is null) return; if (arg.GetNewValue<Control?>() is null) return;
if (arg.GetNewValue<Control?>() is Button button) if (arg.GetNewValue<Control?>() is Button button)
button.Click += (o, e) => { SetCurrentValue(IsDropdownOpenProperty, true); };
}
private void SetupChildrenEventSubscriptions(Control? child, PopConfirmTriggerMode mode)
{
if (child is null) return;
if (mode == PopConfirmTriggerMode.Tap)
{ {
button.Click+= (o, e) => if (child is Button button)
{ Button.ClickEvent.AddHandler(OnMainButtonClicked, button);
_popup?.SetValue(Popup.IsOpenProperty, !_popup.IsOpen); else
}; PointerPressedEvent.AddHandler(OnMainElementPressed, child);
}
else if (mode == 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 void OnMainElementGotFocus(object sender, GotFocusEventArgs e)
{
SetCurrentValue(IsDropdownOpenProperty, true);
}
private void TeardownChildrenEventSubscriptions(Control? child, PopConfirmTriggerMode mode)
{
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)
{
SetCurrentValue(IsDropdownOpenProperty, !IsDropdownOpen);
}
private void OnMainElementPressed(object sender, PointerPressedEventArgs e)
{
SetCurrentValue(IsDropdownOpenProperty, !IsDropdownOpen);
}
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{ {
_childChangeDisposable?.Dispose(); _childChangeDisposable?.Dispose();

View File

@@ -3,6 +3,5 @@ namespace Ursa.Controls;
public enum PopConfirmTriggerMode public enum PopConfirmTriggerMode
{ {
Tap, Tap,
Hover,
Focus, Focus,
} }