完成基础的 DropdownButton

This commit is contained in:
polarboy 2024-08-24 10:07:07 +08:00
parent ed23296d55
commit 32b0736746
5 changed files with 82 additions and 60 deletions

View File

@ -11,12 +11,19 @@
<showcase:ShowCaseItem
Title="Basic"
Description="The most basic dropdown menu.">
<atom:DropdownButton ButtonType="Link" TriggerType="Focus">
<atom:DropdownButton ButtonType="Link" TriggerType="Hover">
Hover me
<atom:DropdownButton.DropdownFlyout>
<atom:Flyout>
<TextBlock Width="200" Height="100" Padding="20">The most basic example.</TextBlock>
</atom:Flyout>
<atom:MenuFlyout>
<atom:MenuItem Header="Cut" InputGesture="Ctrl+X" Icon="{atom:IconProvider Kind=ScissorOutlined}" />
<atom:MenuItem Header="Copy" InputGesture="Ctrl+C" Icon="{atom:IconProvider Kind=CopyOutlined}" />
<atom:MenuItem Header="Delete" InputGesture="Ctrl+D" Icon="{atom:IconProvider Kind=DeleteOutlined}" />
<atom:MenuItem Header="Paste">
<atom:MenuItem Header="Paste" InputGesture="Ctrl+P"
Icon="{atom:IconProvider Kind=FileDoneOutlined}" />
<atom:MenuItem Header="Paste from History" InputGesture="Ctrl+Shift+V" />
</atom:MenuItem>
</atom:MenuFlyout>
</atom:DropdownButton.DropdownFlyout>
</atom:DropdownButton>
</showcase:ShowCaseItem>

View File

@ -35,14 +35,6 @@
</atom:FlyoutHost.Flyout>
<atom:Button>Hover me</atom:Button>
</atom:FlyoutHost>
<atom:FlyoutHost Trigger="Focus">
<atom:FlyoutHost.Flyout>
<atom:Flyout>
<TextBlock Width="200" Height="100" Padding="20">The most basic example.</TextBlock>
</atom:Flyout>
</atom:FlyoutHost.Flyout>
<atom:Button>Focus me</atom:Button>
</atom:FlyoutHost>
<atom:FlyoutHost Trigger="Click">
<atom:FlyoutHost.Flyout>
<atom:Flyout>

View File

@ -15,14 +15,12 @@ using Avalonia.Threading;
namespace AtomUI.Controls;
using FlyoutControl = Flyout;
public class DropdownButton : Button
{
#region
public static readonly StyledProperty<FlyoutControl?> DropdownFlyoutProperty =
AvaloniaProperty.Register<DropdownButton, FlyoutControl?>(nameof(DropdownFlyout));
public static readonly StyledProperty<MenuFlyout?> DropdownFlyoutProperty =
AvaloniaProperty.Register<DropdownButton, MenuFlyout?>(nameof(DropdownFlyout));
public static readonly StyledProperty<FlyoutTriggerType> TriggerTypeProperty =
AvaloniaProperty.Register<DropdownButton, FlyoutTriggerType>(nameof(TriggerType), FlyoutTriggerType.Click);
@ -51,7 +49,7 @@ public class DropdownButton : Button
public static readonly StyledProperty<int> MouseLeaveDelayProperty =
AvaloniaProperty.Register<DropdownButton, int>(nameof(MouseLeaveDelay), 100);
public FlyoutControl? DropdownFlyout
public MenuFlyout? DropdownFlyout
{
get => GetValue(DropdownFlyoutProperty);
set => SetValue(DropdownFlyoutProperty, value);
@ -117,6 +115,7 @@ public class DropdownButton : Button
private DispatcherTimer? _mouseLeaveDelayTimer;
private CompositeDisposable? _subscriptions;
private PathIcon? _openIndicatorIcon;
private IDisposable? _flyoutCloseDetectDisposable;
static DropdownButton()
{
@ -136,7 +135,7 @@ public class DropdownButton : Button
base.OnApplyTemplate(e);
TokenResourceBinder.CreateGlobalTokenBinding(this, MarginToAnchorProperty, GlobalTokenResourceKey.MarginXXS);
SetupTriggerHandler();
SetupFlyoutProperties();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
@ -162,6 +161,26 @@ public class DropdownButton : Button
BindUtils.RelayBind(this, IsShowArrowProperty, DropdownFlyout);
BindUtils.RelayBind(this, IsPointAtCenterProperty, DropdownFlyout);
BindUtils.RelayBind(this, MarginToAnchorProperty, DropdownFlyout);
DropdownFlyout.Opened += HandleFlyoutOpened;
}
}
private void HandleFlyoutOpened(object? sender, EventArgs e)
{
if (DropdownFlyout is IPopupHostProvider popupHostProvider) {
var host = popupHostProvider.PopupHost;
if (host is PopupRoot popupRoot) {
// 这里 PopupRoot 关闭的时候会被关闭,所以这里的事件处理器是不是不需要删除
popupRoot.PointerMoved += (o, args) =>
{
StopMouseLeaveTimer();
if (_flyoutCloseDetectDisposable is null) {
var inputManager = AvaloniaLocator.Current.GetService<IInputManager>()!;
_flyoutCloseDetectDisposable = inputManager.Process.Subscribe(DetectWhenToClosePopup);
}
};
}
}
}
@ -175,13 +194,6 @@ public class DropdownButton : Button
HandleAnchorTargetHover(args);
}
}));
} else if (TriggerType == FlyoutTriggerType.Focus) {
_subscriptions.Add(IsFocusedProperty.Changed.Subscribe(args =>
{
if (args.Sender == this) {
HandleAnchorTargetFocus(args);
}
}));
} else if (TriggerType == FlyoutTriggerType.Click) {
var inputManager = AvaloniaLocator.Current.GetService<IInputManager>()!;
_subscriptions.Add(inputManager.Process.Subscribe(HandleAnchorTargetClick));
@ -199,33 +211,46 @@ public class DropdownButton : Button
}
}
private void HandleAnchorTargetFocus(AvaloniaPropertyChangedEventArgs<bool> e)
private void DetectWhenToClosePopup(RawInputEventArgs args)
{
if (DropdownFlyout is not null) {
if (e.GetNewValue<bool>()) {
if (!DropdownFlyout.IsOpen) {
ShowFlyout();
if (args is RawPointerEventArgs pointerEventArgs) {
if (DropdownFlyout is null) {
return;
}
if (DropdownFlyout.IsOpen) {
var found = false;
if (pointerEventArgs.Root is PopupRoot popupRoot) {
var current = popupRoot.Parent;
while (current is not null) {
if (current == this) {
found = true;
}
current = current.Parent;
}
} else if (object.Equals(pointerEventArgs.Root, this)) {
found = true;
}
if (!found) {
HideFlyout();
}
} else {
HideFlyout();
}
}
}
private void HandleAnchorTargetClick(RawInputEventArgs args)
{
if (args is RawPointerEventArgs pointerEventArgs) {
if (DropdownFlyout is null) {
return;
}
if (pointerEventArgs.Type == RawPointerEventType.LeftButtonUp) {
if (DropdownFlyout is null) {
return;
}
if (!DropdownFlyout.IsOpen) {
var pos = this.TranslatePoint(new Point(0, 0), TopLevel.GetTopLevel(this)!);
if (!pos.HasValue) {
return;
}
var bounds = new Rect(pos.Value, Bounds.Size);
if (bounds.Contains(pointerEventArgs.Position)) {
ShowFlyout();
@ -246,7 +271,8 @@ public class DropdownButton : Button
if (DropdownFlyout is null) {
return;
}
// 防止干扰打开
_flyoutCloseDetectDisposable?.Dispose();
StopMouseEnterTimer();
StopMouseLeaveTimer();
DropdownFlyout.Hide();
@ -262,7 +288,8 @@ public class DropdownButton : Button
if (DropdownFlyout is null) {
return;
}
_flyoutCloseDetectDisposable?.Dispose();
_flyoutCloseDetectDisposable = null;
StopMouseEnterTimer();
if (MouseLeaveDelay == 0) {

View File

@ -18,8 +18,7 @@ using FlyoutControl = Flyout;
public enum FlyoutTriggerType
{
Hover,
Click,
Focus
Click
}
public class FlyoutHost : Control
@ -212,13 +211,6 @@ public class FlyoutHost : Control
HandleAnchorTargetHover(args);
}
});
} else if (Trigger == FlyoutTriggerType.Focus) {
_subscriptions.Add(IsFocusedProperty.Changed.Subscribe(args =>
{
if (args.Sender == AnchorTarget) {
HandleAnchorTargetFocus(args);
}
}));
} else if (Trigger == FlyoutTriggerType.Click) {
var inputManager = AvaloniaLocator.Current.GetService<IInputManager>()!;
_subscriptions.Add(inputManager.Process.Subscribe(HandleAnchorTargetClick));
@ -236,17 +228,6 @@ public class FlyoutHost : Control
}
}
private void HandleAnchorTargetFocus(AvaloniaPropertyChangedEventArgs<bool> e)
{
if (Flyout is not null) {
if (e.GetNewValue<bool>()) {
ShowFlyout();
} else {
HideFlyout();
}
}
}
private void HandleAnchorTargetClick(RawInputEventArgs args)
{
if (args is RawPointerEventArgs pointerEventArgs) {

View File

@ -526,4 +526,19 @@ public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider
//Add new classes
presenter.Classes.AddRange(classes);
}
internal bool InPopupRootBounds(Point position)
{
if (!IsOpen) {
return false;
}
// TODO 后期需要加入对 Overlay 的支持
if (Popup?.Host is PopupRoot root) {
// Get the popup root bounds and convert to screen coordinates
Console.WriteLine(position);
}
return true;
}
}