完成基础的 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 <showcase:ShowCaseItem
Title="Basic" Title="Basic"
Description="The most basic dropdown menu."> Description="The most basic dropdown menu.">
<atom:DropdownButton ButtonType="Link" TriggerType="Focus"> <atom:DropdownButton ButtonType="Link" TriggerType="Hover">
Hover me Hover me
<atom:DropdownButton.DropdownFlyout> <atom:DropdownButton.DropdownFlyout>
<atom:Flyout> <atom:MenuFlyout>
<TextBlock Width="200" Height="100" Padding="20">The most basic example.</TextBlock> <atom:MenuItem Header="Cut" InputGesture="Ctrl+X" Icon="{atom:IconProvider Kind=ScissorOutlined}" />
</atom:Flyout> <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.DropdownFlyout>
</atom:DropdownButton> </atom:DropdownButton>
</showcase:ShowCaseItem> </showcase:ShowCaseItem>

View File

@ -35,14 +35,6 @@
</atom:FlyoutHost.Flyout> </atom:FlyoutHost.Flyout>
<atom:Button>Hover me</atom:Button> <atom:Button>Hover me</atom:Button>
</atom:FlyoutHost> </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 Trigger="Click">
<atom:FlyoutHost.Flyout> <atom:FlyoutHost.Flyout>
<atom:Flyout> <atom:Flyout>

View File

@ -15,14 +15,12 @@ using Avalonia.Threading;
namespace AtomUI.Controls; namespace AtomUI.Controls;
using FlyoutControl = Flyout;
public class DropdownButton : Button public class DropdownButton : Button
{ {
#region #region
public static readonly StyledProperty<FlyoutControl?> DropdownFlyoutProperty = public static readonly StyledProperty<MenuFlyout?> DropdownFlyoutProperty =
AvaloniaProperty.Register<DropdownButton, FlyoutControl?>(nameof(DropdownFlyout)); AvaloniaProperty.Register<DropdownButton, MenuFlyout?>(nameof(DropdownFlyout));
public static readonly StyledProperty<FlyoutTriggerType> TriggerTypeProperty = public static readonly StyledProperty<FlyoutTriggerType> TriggerTypeProperty =
AvaloniaProperty.Register<DropdownButton, FlyoutTriggerType>(nameof(TriggerType), FlyoutTriggerType.Click); AvaloniaProperty.Register<DropdownButton, FlyoutTriggerType>(nameof(TriggerType), FlyoutTriggerType.Click);
@ -51,7 +49,7 @@ public class DropdownButton : Button
public static readonly StyledProperty<int> MouseLeaveDelayProperty = public static readonly StyledProperty<int> MouseLeaveDelayProperty =
AvaloniaProperty.Register<DropdownButton, int>(nameof(MouseLeaveDelay), 100); AvaloniaProperty.Register<DropdownButton, int>(nameof(MouseLeaveDelay), 100);
public FlyoutControl? DropdownFlyout public MenuFlyout? DropdownFlyout
{ {
get => GetValue(DropdownFlyoutProperty); get => GetValue(DropdownFlyoutProperty);
set => SetValue(DropdownFlyoutProperty, value); set => SetValue(DropdownFlyoutProperty, value);
@ -117,6 +115,7 @@ public class DropdownButton : Button
private DispatcherTimer? _mouseLeaveDelayTimer; private DispatcherTimer? _mouseLeaveDelayTimer;
private CompositeDisposable? _subscriptions; private CompositeDisposable? _subscriptions;
private PathIcon? _openIndicatorIcon; private PathIcon? _openIndicatorIcon;
private IDisposable? _flyoutCloseDetectDisposable;
static DropdownButton() static DropdownButton()
{ {
@ -136,7 +135,7 @@ public class DropdownButton : Button
base.OnApplyTemplate(e); base.OnApplyTemplate(e);
TokenResourceBinder.CreateGlobalTokenBinding(this, MarginToAnchorProperty, GlobalTokenResourceKey.MarginXXS); TokenResourceBinder.CreateGlobalTokenBinding(this, MarginToAnchorProperty, GlobalTokenResourceKey.MarginXXS);
SetupTriggerHandler(); SetupFlyoutProperties();
} }
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
@ -162,6 +161,26 @@ public class DropdownButton : Button
BindUtils.RelayBind(this, IsShowArrowProperty, DropdownFlyout); BindUtils.RelayBind(this, IsShowArrowProperty, DropdownFlyout);
BindUtils.RelayBind(this, IsPointAtCenterProperty, DropdownFlyout); BindUtils.RelayBind(this, IsPointAtCenterProperty, DropdownFlyout);
BindUtils.RelayBind(this, MarginToAnchorProperty, 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); HandleAnchorTargetHover(args);
} }
})); }));
} else if (TriggerType == FlyoutTriggerType.Focus) {
_subscriptions.Add(IsFocusedProperty.Changed.Subscribe(args =>
{
if (args.Sender == this) {
HandleAnchorTargetFocus(args);
}
}));
} else if (TriggerType == FlyoutTriggerType.Click) { } else if (TriggerType == FlyoutTriggerType.Click) {
var inputManager = AvaloniaLocator.Current.GetService<IInputManager>()!; var inputManager = AvaloniaLocator.Current.GetService<IInputManager>()!;
_subscriptions.Add(inputManager.Process.Subscribe(HandleAnchorTargetClick)); _subscriptions.Add(inputManager.Process.Subscribe(HandleAnchorTargetClick));
@ -199,27 +211,40 @@ public class DropdownButton : Button
} }
} }
private void HandleAnchorTargetFocus(AvaloniaPropertyChangedEventArgs<bool> e) private void DetectWhenToClosePopup(RawInputEventArgs args)
{ {
if (DropdownFlyout is not null) { if (args is RawPointerEventArgs pointerEventArgs) {
if (e.GetNewValue<bool>()) { if (DropdownFlyout is null) {
if (!DropdownFlyout.IsOpen) { return;
ShowFlyout();
} }
} else { 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(); HideFlyout();
} }
} }
} }
}
private void HandleAnchorTargetClick(RawInputEventArgs args) private void HandleAnchorTargetClick(RawInputEventArgs args)
{ {
if (args is RawPointerEventArgs pointerEventArgs) { if (args is RawPointerEventArgs pointerEventArgs) {
if (pointerEventArgs.Type == RawPointerEventType.LeftButtonUp) {
if (DropdownFlyout is null) { if (DropdownFlyout is null) {
return; return;
} }
if (pointerEventArgs.Type == RawPointerEventType.LeftButtonUp) {
if (!DropdownFlyout.IsOpen) { if (!DropdownFlyout.IsOpen) {
var pos = this.TranslatePoint(new Point(0, 0), TopLevel.GetTopLevel(this)!); var pos = this.TranslatePoint(new Point(0, 0), TopLevel.GetTopLevel(this)!);
if (!pos.HasValue) { if (!pos.HasValue) {
@ -246,7 +271,8 @@ public class DropdownButton : Button
if (DropdownFlyout is null) { if (DropdownFlyout is null) {
return; return;
} }
// 防止干扰打开
_flyoutCloseDetectDisposable?.Dispose();
StopMouseEnterTimer(); StopMouseEnterTimer();
StopMouseLeaveTimer(); StopMouseLeaveTimer();
DropdownFlyout.Hide(); DropdownFlyout.Hide();
@ -262,7 +288,8 @@ public class DropdownButton : Button
if (DropdownFlyout is null) { if (DropdownFlyout is null) {
return; return;
} }
_flyoutCloseDetectDisposable?.Dispose();
_flyoutCloseDetectDisposable = null;
StopMouseEnterTimer(); StopMouseEnterTimer();
if (MouseLeaveDelay == 0) { if (MouseLeaveDelay == 0) {

View File

@ -18,8 +18,7 @@ using FlyoutControl = Flyout;
public enum FlyoutTriggerType public enum FlyoutTriggerType
{ {
Hover, Hover,
Click, Click
Focus
} }
public class FlyoutHost : Control public class FlyoutHost : Control
@ -212,13 +211,6 @@ public class FlyoutHost : Control
HandleAnchorTargetHover(args); HandleAnchorTargetHover(args);
} }
}); });
} else if (Trigger == FlyoutTriggerType.Focus) {
_subscriptions.Add(IsFocusedProperty.Changed.Subscribe(args =>
{
if (args.Sender == AnchorTarget) {
HandleAnchorTargetFocus(args);
}
}));
} else if (Trigger == FlyoutTriggerType.Click) { } else if (Trigger == FlyoutTriggerType.Click) {
var inputManager = AvaloniaLocator.Current.GetService<IInputManager>()!; var inputManager = AvaloniaLocator.Current.GetService<IInputManager>()!;
_subscriptions.Add(inputManager.Process.Subscribe(HandleAnchorTargetClick)); _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) private void HandleAnchorTargetClick(RawInputEventArgs args)
{ {
if (args is RawPointerEventArgs pointerEventArgs) { if (args is RawPointerEventArgs pointerEventArgs) {

View File

@ -526,4 +526,19 @@ public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider
//Add new classes //Add new classes
presenter.Classes.AddRange(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;
}
} }