完成二级菜单弹出

This commit is contained in:
polarboy 2024-07-26 09:28:59 +08:00
parent 5a7cabdb6b
commit 5b6ba00df7
9 changed files with 113 additions and 276 deletions

View File

@ -10,13 +10,13 @@
<showcase:ShowCasePanel>
<atom:Menu>
<atom:MenuItem Header="_File">
<atom:MenuItem Header="New Text File" InputGesture="Ctrl+N" />
<atom:MenuItem Header="New File" InputGesture="Ctrl+Alt+N" />
<atom:MenuItem Header="New Window" InputGesture="Ctrl+Shift+N" />
<atom:MenuItem Header="New Text File" InputGesture="Ctrl+N" ToggleType="Radio" GroupName="Group1"/>
<atom:MenuItem Header="New File" InputGesture="Ctrl+Alt+N" ToggleType="Radio" GroupName="Group1"/>
<atom:MenuItem Header="New Window" InputGesture="Ctrl+Shift+N" ToggleType="Radio" GroupName="Group1"/>
<atom:MenuSeparator />
<atom:MenuItem Header="Save" InputGesture="Ctrl+S" />
<atom:MenuItem Header="Save As..." InputGesture="Ctrl+Shift+S" />
<atom:MenuItem Header="Save All" InputGesture="Ctrl+K" />
<atom:MenuItem Header="Save" InputGesture="Ctrl+S" ToggleType="CheckBox" />
<atom:MenuItem Header="Save As..." InputGesture="Ctrl+Shift+S" ToggleType="CheckBox" Icon="{atom:IconProvider Kind=GithubOutlined}" />
<atom:MenuItem Header="Save All" InputGesture="Ctrl+K" ToggleType="CheckBox" Icon="{atom:IconProvider Kind=CheckOutlined}"/>
<atom:MenuSeparator />
<atom:MenuItem Header="Exit" />
</atom:MenuItem>

View File

@ -188,9 +188,10 @@ public class CheckBox : AvaloniaCheckBox,
var checkMarkPenWidth = 2;
var checkMarkPen = new Pen(IndicatorCheckedMarkBrush, 2);
var checkMarkBounds = checkMarkGeometry.GetRenderBounds(checkMarkPen);
var deltaSize = (CheckIndicatorSize - checkMarkBounds.Width) / 2;
var offsetX = deltaSize - checkMarkPenWidth - penWidth;
var offsetY = deltaSize - checkMarkPenWidth - penWidth;
var deltaWidth = (CheckIndicatorSize - checkMarkBounds.Width) / 2;
var deltaHeight = (CheckIndicatorSize - checkMarkBounds.Height) / 2;
var offsetX = indicatorRect.X + deltaWidth - checkMarkPenWidth - penWidth;
var offsetY = indicatorRect.Y + deltaHeight - checkMarkPenWidth - penWidth * 2;
checkMarkGeometry.Transform = new TranslateTransform(offsetX, offsetY);
context.DrawGeometry(null, checkMarkPen, checkMarkGeometry);
} else if (_styleState.HasFlag(ControlStyleState.Indeterminate)) {

View File

@ -1,7 +1,6 @@
using AtomUI.Utils;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.LogicalTree;
namespace AtomUI.Controls;

View File

@ -0,0 +1,19 @@
using Avalonia.Controls.Platform;
using Avalonia.Input;
namespace AtomUI.Controls;
public class MenuInteractionHandler : DefaultMenuInteractionHandler
{
public MenuInteractionHandler(bool isContextMenu)
: base(isContextMenu)
{
}
public MenuInteractionHandler(bool isContextMenu,
IInputManager? inputManager,
Action<Action, TimeSpan> delayRun)
: base(isContextMenu, inputManager, delayRun)
{
}
}

View File

@ -1,5 +1,4 @@
using AtomUI.Controls.Utils;
using AtomUI.Media;
using AtomUI.Media;
using AtomUI.Styling;
using AtomUI.Utils;
using Avalonia;
@ -11,7 +10,6 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
namespace AtomUI.Controls;
@ -37,6 +35,9 @@ public class MenuItem : AvaloniaMenuItem, IControlCustomStyle
private readonly IControlCustomStyle _customStyle;
private ContentPresenter? _topLevelContentPresenter;
private ContentControl? _togglePresenter;
private Popup? _popup;
internal static PlatformKeyGestureConverter KeyGestureConverter = new PlatformKeyGestureConverter();
static MenuItem()
@ -72,7 +73,10 @@ public class MenuItem : AvaloniaMenuItem, IControlCustomStyle
{
if (IsTopLevel) {
_topLevelContentPresenter = scope.Find<ContentPresenter>(TopLevelMenuItemTheme.HeaderPresenterPart);
} else {
_togglePresenter = scope.Find<ContentControl>(MenuItemTheme.TogglePresenterPart);
}
HandleToggleTypeChanged();
_customStyle.SetupTransitions();
UpdatePseudoClasses();
}
@ -89,6 +93,27 @@ public class MenuItem : AvaloniaMenuItem, IControlCustomStyle
BindUtils.CreateTokenBinding(pathIcon, PathIcon.HeightProperty, MenuResourceKey.ItemIconSize);
BindUtils.CreateTokenBinding(pathIcon, PathIcon.NormalFilledBrushProperty, MenuResourceKey.ItemColor);
}
} else if (e.Property == ToggleTypeProperty) {
HandleToggleTypeChanged();
}
}
private void HandleToggleTypeChanged()
{
if (IsTopLevel || _togglePresenter is null) {
return;
}
if (ToggleType == MenuItemToggleType.None) {
if (_togglePresenter.Presenter is not null) {
_togglePresenter.Presenter.IsVisible = false;
}
} else if (ToggleType == MenuItemToggleType.CheckBox) {
_togglePresenter.Content = new CheckBox();
_togglePresenter.IsVisible = true;
} else if (ToggleType == MenuItemToggleType.Radio) {
_togglePresenter.Content = new RadioButton();
_togglePresenter.IsVisible = true;
}
}

View File

@ -22,6 +22,8 @@ internal class MenuItemTheme : ControlTheme
public const string ItemTextPresenterPart = "Part_ItemTextPresenter";
public const string InputGestureTextPart = "Part_InputGestureText";
public const string MenuIndicatorIconPart = "Part_MenuIndicatorIcon";
public const string PopupPart = "PART_Popup";
public const string ItemsPresenterPart = "PART_ItemsPresenter";
public MenuItemTheme()
: base(typeof(MenuItem))
@ -32,6 +34,8 @@ internal class MenuItemTheme : ControlTheme
{
return new FuncControlTemplate<MenuItem>((item, scope) =>
{
// 仅仅为了把 Popup 包进来,没有其他什么作用
var layoutWrapper = new Panel();
var container = new Border()
{
Name = ItemDecoratorPart
@ -97,7 +101,8 @@ internal class MenuItemTheme : ControlTheme
Name = ItemTextPresenterPart,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Center,
RecognizesAccessKey = true
RecognizesAccessKey = true,
IsHitTestVisible = false
};
Grid.SetColumn(itemTextPresenter, 2);
BindUtils.CreateTokenBinding(itemTextPresenter, ContentPresenter.MarginProperty, MenuResourceKey.ItemMargin);
@ -135,18 +140,59 @@ internal class MenuItemTheme : ControlTheme
Grid.SetColumn(menuIndicatorIcon, 4);
menuIndicatorIcon.RegisterInNameScope(scope);
var popup = CreateMenuPopup();
layout.Children.Add(togglePresenter);
layout.Children.Add(iconPresenter);
layout.Children.Add(itemTextPresenter);
layout.Children.Add(inputGestureText);
layout.Children.Add(menuIndicatorIcon);
layout.Children.Add(popup);
container.Child = layout;
return container;
layoutWrapper.Children.Add(container);
return layoutWrapper;
});
}
private Popup CreateMenuPopup()
{
var popup = new Popup()
{
Name = PopupPart,
WindowManagerAddShadowHint = false,
IsLightDismissEnabled = false,
Placement = PlacementMode.RightEdgeAlignedTop,
};
var border = new Border();
BindUtils.CreateTokenBinding(border, Border.BackgroundProperty, GlobalResourceKey.ColorBgContainer);
BindUtils.CreateTokenBinding(border, Border.CornerRadiusProperty, MenuResourceKey.MenuPopupBorderRadius);
BindUtils.CreateTokenBinding(border, Border.MinWidthProperty, MenuResourceKey.MenuPopupMinWidth);
BindUtils.CreateTokenBinding(border, Border.MaxWidthProperty, MenuResourceKey.MenuPopupMaxWidth);
BindUtils.CreateTokenBinding(border, Border.MinHeightProperty, MenuResourceKey.MenuPopupMinHeight);
BindUtils.CreateTokenBinding(border, Border.MaxHeightProperty, MenuResourceKey.MenuPopupMaxHeight);
BindUtils.CreateTokenBinding(border, Border.PaddingProperty, MenuResourceKey.MenuPopupContentPadding);
var scrollViewer = new MenuScrollViewer();
var itemsPresenter = new ItemsPresenter
{
Name = ItemsPresenterPart,
};
CreateTemplateParentBinding(itemsPresenter, ItemsPresenter.ItemsPanelProperty, MenuItem.ItemsPanelProperty);
Grid.SetIsSharedSizeScope(itemsPresenter, true);
scrollViewer.Content = itemsPresenter;
border.Child = scrollViewer;
popup.Child = border;
BindUtils.CreateTokenBinding(popup, Popup.MarginToAnchorProperty, MenuResourceKey.TopLevelItemPopupMarginToAnchor);
BindUtils.CreateTokenBinding(popup, Popup.MaskShadowsProperty, MenuResourceKey.MenuPopupBoxShadows);
CreateTemplateParentBinding(popup, Popup.IsOpenProperty, MenuItem.IsSubMenuOpenProperty, BindingMode.TwoWay);
return popup;
}
protected override void BuildStyles()
{
var commonStyle = new Style(selector => selector.Nesting());

View File

@ -39,33 +39,35 @@ public class TopLevelMenuItemTheme : ControlTheme
RecognizesAccessKey = true,
};
// TODO 后面需要评估一下,能直接绑定到对象,是否还需要这样通过模板绑定
CreateTemplateParentBinding(contentPresenter, ContentPresenter.ContentProperty, MenuItem.HeaderProperty);
CreateTemplateParentBinding(contentPresenter, ContentPresenter.ContentTemplateProperty, MenuItem.HeaderTemplateProperty);
CreateTemplateParentBinding(contentPresenter, ContentPresenter.CornerRadiusProperty, MenuItem.CornerRadiusProperty);
CreateTemplateParentBinding(contentPresenter, ContentPresenter.PaddingProperty, MenuItem.PaddingProperty);
CreateTemplateParentBinding(contentPresenter, ContentPresenter.MinHeightProperty, MenuItem.MinHeightProperty);
CreateTemplateParentBinding(contentPresenter, ContentPresenter.FontSizeProperty, MenuItem.FontSizeProperty);
contentPresenter.RegisterInNameScope(scope);
panel.Children.Add(contentPresenter);
var popup = CreateMenuPopup(menuItem);
var popup = CreateMenuPopup();
panel.Children.Add(popup);
return panel;
});
}
private Popup CreateMenuPopup(MenuItem menuItem)
private Popup CreateMenuPopup()
{
var popup = new Popup()
{
Name = PopupPart,
WindowManagerAddShadowHint = false,
IsLightDismissEnabled = true,
IsLightDismissEnabled = false,
Placement = PlacementMode.BottomEdgeAlignedLeft,
OverlayInputPassThroughElement = menuItem,
};
var border = new Border();
BindUtils.CreateTokenBinding(border, Border.BackgroundProperty, GlobalResourceKey.ColorBgContainer);
BindUtils.CreateTokenBinding(border, Border.CornerRadiusProperty, MenuResourceKey.MenuPopupBorderRadius);
BindUtils.CreateTokenBinding(border, Border.MinWidthProperty, MenuResourceKey.MenuPopupMinWidth);

View File

@ -1,216 +0,0 @@
using Avalonia;
using Avalonia.Automation.Peers;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.VisualTree;
namespace AtomUI.Controls;
/// <summary>
/// The root window of a <see cref="Popup"/>.
/// </summary>
public sealed class PopupRoot : WindowBase, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost
{
/// <summary>
/// Defines the <see cref="Transform"/> property.
/// </summary>
public static readonly StyledProperty<Transform?> TransformProperty =
AvaloniaProperty.Register<PopupRoot, Transform?>(nameof(Transform));
/// <summary>
/// Defines the <see cref="WindowManagerAddShadowHint"/> property.
/// </summary>
public static readonly StyledProperty<bool> WindowManagerAddShadowHintProperty =
Popup.WindowManagerAddShadowHintProperty.AddOwner<PopupRoot>();
private PopupPositionerParameters _positionerParameters;
internal PopupPositionerParameters PositionerParameters => _positionerParameters;
/// <summary>
/// Initializes static members of the <see cref="PopupRoot"/> class.
/// </summary>
static PopupRoot()
{
BackgroundProperty.OverrideDefaultValue(typeof(PopupRoot), Brushes.Transparent);
}
/// <summary>
/// Initializes a new instance of the <see cref="PopupRoot"/> class.
/// </summary>
public PopupRoot(TopLevel parent, IPopupImpl impl)
: this(parent, impl,null)
{
#if DEBUG
this.AttachDevTools();
#endif
}
/// <summary>
/// Initializes a new instance of the <see cref="PopupRoot"/> class.
/// </summary>
/// <param name="parent">The popup parent.</param>
/// <param name="impl">The popup implementation.</param>
/// <param name="dependencyResolver">
/// The dependency resolver to use. If null the default dependency resolver will be used.
/// </param>
public PopupRoot(TopLevel parent, IPopupImpl impl, IAvaloniaDependencyResolver? dependencyResolver)
: base(impl, dependencyResolver)
{
ParentTopLevel = parent;
impl.SetWindowManagerAddShadowHint(WindowManagerAddShadowHint);
}
/// <summary>
/// Gets the platform-specific window implementation.
/// </summary>
public new IPopupImpl? PlatformImpl => (IPopupImpl?)base.PlatformImpl;
/// <summary>
/// Gets or sets a transform that will be applied to the popup.
/// </summary>
public Transform? Transform
{
get => GetValue(TransformProperty);
set => SetValue(TransformProperty, value);
}
/// <summary>
/// Gets or sets a hint to the window manager that a shadow should be added to the popup.
/// </summary>
public bool WindowManagerAddShadowHint
{
get => GetValue(WindowManagerAddShadowHintProperty);
set => SetValue(WindowManagerAddShadowHintProperty, value);
}
/// <summary>
/// Gets the parent control in the event route.
/// </summary>
/// <remarks>
/// Popup events are passed to their parent window. This facilitates this.
/// </remarks>
/// TODO 需要评估影响,可能有严重错误!!!
//internal override Interactive? InteractiveParent => (Interactive?)Parent;
/// <summary>
/// Gets the control that is hosting the popup root.
/// </summary>
Visual? IHostedVisualTreeRoot.Host
{
get
{
// If the parent is attached to a visual tree, then return that. However the parent
// will possibly be a standalone Popup (i.e. a Popup not attached to a visual tree,
// created by e.g. a ContextMenu): if this is the case, return the ParentTopLevel
// if set. This helps to allow the focus manager to restore the focus to the outer
// scope when the popup is closed.
var parentVisual = Parent as Visual;
if (parentVisual?.GetVisualRoot() is not null)
return parentVisual;
return ParentTopLevel ?? parentVisual;
}
}
/// <summary>
/// Gets the styling parent of the popup root.
/// </summary>
IStyleHost? IStyleHost.StylingParent => Parent;
public TopLevel ParentTopLevel { get; }
/// <inheritdoc/>
public void Dispose()
{
PlatformImpl?.Dispose();
}
private void UpdatePosition()
{
PlatformImpl?.PopupPositioner?.Update(_positionerParameters);
}
public void ConfigurePosition(Visual target, PlacementMode placement, Point offset,
PopupAnchor anchor = PopupAnchor.None,
PopupGravity gravity = PopupGravity.None,
PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All,
Rect? rect = null)
{
_positionerParameters.ConfigurePosition(ParentTopLevel, target,
placement, offset, anchor, gravity, constraintAdjustment, rect, FlowDirection);
if (_positionerParameters.Size != default) {
UpdatePosition();
}
}
public void SetChild(Control? control) => Content = control;
Visual IPopupHost.HostedVisualTreeRoot => this;
protected override Size MeasureOverride(Size availableSize)
{
var maxAutoSize = PlatformImpl?.MaxAutoSizeHint ?? Size.Infinity;
var constraint = availableSize;
if (double.IsInfinity(constraint.Width))
{
constraint = constraint.WithWidth(maxAutoSize.Width);
}
if (double.IsInfinity(constraint.Height))
{
constraint = constraint.WithHeight(maxAutoSize.Height);
}
var measured = base.MeasureOverride(constraint);
var width = measured.Width;
var height = measured.Height;
var widthCache = Width;
var heightCache = Height;
if (!double.IsNaN(widthCache))
{
width = widthCache;
}
width = Math.Min(width, MaxWidth);
width = Math.Max(width, MinWidth);
if (!double.IsNaN(heightCache))
{
height = heightCache;
}
height = Math.Min(height, MaxHeight);
height = Math.Max(height, MinHeight);
return new Size(width, height);
}
protected override sealed Size ArrangeSetBounds(Size size)
{
_positionerParameters.Size = size;
UpdatePosition();
return ClientSize;
}
protected override AutomationPeer OnCreateAutomationPeer()
{
return new PopupRootAutomationPeer(this);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == WindowManagerAddShadowHintProperty)
{
PlatformImpl?.SetWindowManagerAddShadowHint(change.GetNewValue<bool>());
}
}
}

View File

@ -1,39 +0,0 @@
using Avalonia.Automation.Peers;
namespace AtomUI.Controls;
public class PopupRootAutomationPeer : WindowBaseAutomationPeer
{
public PopupRootAutomationPeer(PopupRoot owner)
: base(owner)
{
if (owner.IsVisible) {
StartTrackingFocus();
} else {
owner.Opened += OnOpened;
}
owner.Closed += OnClosed;
}
protected override bool IsContentElementCore() => false;
protected override bool IsControlElementCore() => false;
protected override AutomationPeer? GetParentCore()
{
var parent = base.GetParentCore();
return parent;
}
private void OnOpened(object? sender, EventArgs e)
{
((PopupRoot)Owner).Opened -= OnOpened;
StartTrackingFocus();
}
private void OnClosed(object? sender, EventArgs e)
{
((PopupRoot)Owner).Closed -= OnClosed;
StopTrackingFocus();
}
}