重构 TabControl 溢出上下文菜单

重构 TabControl 溢出上下文菜单
This commit is contained in:
polarboy 2024-08-04 16:00:44 +08:00
parent c939b25884
commit 75bc4e3baf
7 changed files with 234 additions and 31 deletions

View File

@ -59,6 +59,76 @@
</StackPanel>
</showcase:ShowCaseItem>
<showcase:ShowCaseItem
Title="Icon"
Description="The Tab with Icon.">
<StackPanel Orientation="Vertical" Spacing="20">
<atom:TabControl>
<atom:TabItem Header="Tab 1" Icon="{atom:IconProvider Kind=AppleOutlined}">Content of Tab Pane 1</atom:TabItem>
<atom:TabItem Header="Tab 2" Icon="{atom:IconProvider Kind=AndroidOutlined}">Content of Tab Pane 2</atom:TabItem>
<atom:TabItem Header="Tab 3" Icon="{atom:IconProvider Kind=WechatOutlined}">Content of Tab Pane 3</atom:TabItem>
</atom:TabControl>
<!-- -->
<!-- <atom:CardTabControl> -->
<!-- <atom:TabItem Header="Tab 1" Icon="{atom:IconProvider Kind=AppleOutlined}">Content of Tab Pane 1</atom:TabItem> -->
<!-- <atom:TabItem Header="Tab 2" Icon="{atom:IconProvider Kind=AndroidOutlined}">Content of Tab Pane 2</atom:TabItem> -->
<!-- <atom:TabItem Header="Tab 3" Icon="{atom:IconProvider Kind=WechatOutlined}">Content of Tab Pane 3</atom:TabItem> -->
<!-- </atom:CardTabControl> -->
</StackPanel>
</showcase:ShowCaseItem>
<showcase:ShowCaseItem
Title="Slide"
Description="In order to fit in more tabs, they can slide left and right (or up and down).">
<StackPanel Orientation="Vertical" Spacing="20">
<atom:TabControl>
<atom:TabItem Header="Tab 1">Content of Tab Pane 1</atom:TabItem>
<atom:TabItem Header="Tab 2">Content of Tab Pane 2</atom:TabItem>
<atom:TabItem Header="Tab 3">Content of Tab Pane 3</atom:TabItem>
<atom:TabItem Header="Tab 4">Content of Tab Pane 4</atom:TabItem>
<atom:TabItem Header="Tab 5">Content of Tab Pane 5</atom:TabItem>
<atom:TabItem Header="Tab 6">Content of Tab Pane 6</atom:TabItem>
<atom:TabItem Header="Tab 7">Content of Tab Pane 7</atom:TabItem>
<atom:TabItem Header="Tab 8">Content of Tab Pane 8</atom:TabItem>
<atom:TabItem Header="Tab 9">Content of Tab Pane 9</atom:TabItem>
<atom:TabItem Header="Tab 10">Content of Tab Pane 10</atom:TabItem>
<atom:TabItem Header="Tab 11">Content of Tab Pane 11</atom:TabItem>
<atom:TabItem Header="Tab 12">Content of Tab Pane 12</atom:TabItem>
<atom:TabItem Header="Tab 13">Content of Tab Pane 13</atom:TabItem>
<atom:TabItem Header="Tab 14">Content of Tab Pane 14</atom:TabItem>
<atom:TabItem Header="Tab 15">Content of Tab Pane 15</atom:TabItem>
<atom:TabItem Header="Tab 16">Content of Tab Pane 16</atom:TabItem>
<atom:TabItem Header="Tab 17">Content of Tab Pane 17</atom:TabItem>
<atom:TabItem Header="Tab 18">Content of Tab Pane 18</atom:TabItem>
<atom:TabItem Header="Tab 19">Content of Tab Pane 19</atom:TabItem>
<atom:TabItem Header="Tab 20">Content of Tab Pane 20</atom:TabItem>
</atom:TabControl>
<atom:CardTabControl>
<atom:TabItem Header="Tab 1">Content of Tab Pane 1</atom:TabItem>
<atom:TabItem Header="Tab 2">Content of Tab Pane 2</atom:TabItem>
<atom:TabItem Header="Tab 3">Content of Tab Pane 3</atom:TabItem>
<atom:TabItem Header="Tab 4">Content of Tab Pane 4</atom:TabItem>
<atom:TabItem Header="Tab 5">Content of Tab Pane 5</atom:TabItem>
<atom:TabItem Header="Tab 6">Content of Tab Pane 6</atom:TabItem>
<atom:TabItem Header="Tab 7">Content of Tab Pane 7</atom:TabItem>
<atom:TabItem Header="Tab 8">Content of Tab Pane 8</atom:TabItem>
<atom:TabItem Header="Tab 9">Content of Tab Pane 9</atom:TabItem>
<atom:TabItem Header="Tab 10">Content of Tab Pane 10</atom:TabItem>
<atom:TabItem Header="Tab 11">Content of Tab Pane 11</atom:TabItem>
<atom:TabItem Header="Tab 12">Content of Tab Pane 12</atom:TabItem>
<atom:TabItem Header="Tab 13">Content of Tab Pane 13</atom:TabItem>
<atom:TabItem Header="Tab 14">Content of Tab Pane 14</atom:TabItem>
<atom:TabItem Header="Tab 15">Content of Tab Pane 15</atom:TabItem>
<atom:TabItem Header="Tab 16">Content of Tab Pane 16</atom:TabItem>
<atom:TabItem Header="Tab 17">Content of Tab Pane 17</atom:TabItem>
<atom:TabItem Header="Tab 18">Content of Tab Pane 18</atom:TabItem>
<atom:TabItem Header="Tab 19">Content of Tab Pane 19</atom:TabItem>
<atom:TabItem Header="Tab 20">Content of Tab Pane 20</atom:TabItem>
</atom:CardTabControl>
</StackPanel>
</showcase:ShowCaseItem>
</showcase:ShowCasePanel>
</TabItem>

View File

@ -6,12 +6,12 @@ using Avalonia.Threading;
namespace AtomUI.Controls;
internal class OverflowTabMenuItem : MenuItem
internal class BaseOverflowMenuItem : MenuItem
{
#region
public static readonly DirectProperty<OverflowTabMenuItem, bool> IsClosableProperty =
AvaloniaProperty.RegisterDirect<OverflowTabMenuItem, bool>(nameof(IsClosable),
public static readonly DirectProperty<BaseOverflowMenuItem, bool> IsClosableProperty =
AvaloniaProperty.RegisterDirect<BaseOverflowMenuItem, bool>(nameof(IsClosable),
o => o.IsClosable,
(o, v) => o.IsClosable = v);
@ -24,8 +24,6 @@ internal class OverflowTabMenuItem : MenuItem
get => _isClosable;
set => SetAndRaise(IsClosableProperty, ref _isClosable, value);
}
public TabStripItem? TabStripItem { get; set; }
public event EventHandler<CloseTabRequestEventArgs>? CloseTab
{
@ -40,29 +38,26 @@ internal class OverflowTabMenuItem : MenuItem
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_iconButton = e.NameScope.Find<IconButton>(OverflowTabMenuItemTheme.ItemCloseButtonPart);
_iconButton = e.NameScope.Find<IconButton>(BaseOverflowMenuItemTheme.ItemCloseButtonPart);
if (_iconButton is not null) {
_iconButton.Click += (sender, args) =>
{
if (Parent is MenuBase menu) {
var eventArgs = new CloseTabRequestEventArgs(CloseTabEvent, TabStripItem!);
RaiseEvent(eventArgs);
Dispatcher.UIThread.Post(() =>
{
menu.Close();
});
}
NotifyCloseRequest();
};
}
}
protected virtual void NotifyCloseRequest()
{
}
}
internal class CloseTabRequestEventArgs : RoutedEventArgs
{
public CloseTabRequestEventArgs(RoutedEvent routedEvent, TabStripItem stripItem)
public CloseTabRequestEventArgs(RoutedEvent routedEvent, object tabItem)
: base(routedEvent)
{
TabStripItem = stripItem;
TabItem = tabItem;
}
public TabStripItem TabStripItem { get; }
public object TabItem { get; }
}

View File

@ -13,21 +13,21 @@ using Avalonia.Styling;
namespace AtomUI.Controls;
[ControlThemeProvider]
internal class OverflowTabMenuItemTheme : BaseControlTheme
internal class BaseOverflowMenuItemTheme : BaseControlTheme
{
public const string ItemDecoratorPart = "PART_ItemDecorator";
public const string MainContainerPart = "PART_MainContainer";
public const string ItemTextPresenterPart = "PART_ItemTextPresenter";
public const string ItemCloseButtonPart = "PART_ItemCloseIcon";
public OverflowTabMenuItemTheme()
: base(typeof(OverflowTabMenuItem))
public BaseOverflowMenuItemTheme()
: base(typeof(BaseOverflowMenuItem))
{
}
protected override IControlTemplate BuildControlTemplate()
{
return new FuncControlTemplate<OverflowTabMenuItem>((item, scope) =>
return new FuncControlTemplate<BaseOverflowMenuItem>((item, scope) =>
{
var container = new Border()
{
@ -61,8 +61,8 @@ internal class OverflowTabMenuItemTheme : BaseControlTheme
Grid.SetColumn(itemTextPresenter, 0);
TokenResourceBinder.CreateTokenBinding(itemTextPresenter, ContentPresenter.MarginProperty, MenuResourceKey.ItemMargin);
CreateTemplateParentBinding(itemTextPresenter, ContentPresenter.ContentProperty, OverflowTabMenuItem.HeaderProperty);
CreateTemplateParentBinding(itemTextPresenter, ContentPresenter.ContentTemplateProperty, OverflowTabMenuItem.HeaderTemplateProperty);
CreateTemplateParentBinding(itemTextPresenter, ContentPresenter.ContentProperty, BaseOverflowMenuItem.HeaderProperty);
CreateTemplateParentBinding(itemTextPresenter, ContentPresenter.ContentTemplateProperty, BaseOverflowMenuItem.HeaderTemplateProperty);
itemTextPresenter.RegisterInNameScope(scope);
@ -82,7 +82,7 @@ internal class OverflowTabMenuItemTheme : BaseControlTheme
};
CreateTemplateParentBinding(closeButton, IconButton.IsVisibleProperty, OverflowTabMenuItem.IsClosableProperty);
CreateTemplateParentBinding(closeButton, IconButton.IsVisibleProperty, BaseOverflowMenuItem.IsClosableProperty);
TokenResourceBinder.CreateGlobalTokenBinding(menuCloseIcon, PathIcon.NormalFilledBrushProperty, GlobalResourceKey.ColorIcon);
TokenResourceBinder.CreateGlobalTokenBinding(menuCloseIcon, PathIcon.ActiveFilledBrushProperty, GlobalResourceKey.ColorIconHover);
@ -110,7 +110,7 @@ internal class OverflowTabMenuItemTheme : BaseControlTheme
private void BuildCommonStyle(Style commonStyle)
{
commonStyle.Add(OverflowTabMenuItem.ForegroundProperty, MenuResourceKey.ItemColor);
commonStyle.Add(BaseOverflowMenuItem.ForegroundProperty, MenuResourceKey.ItemColor);
{
var borderStyle = new Style(selector => selector.Nesting().Template().Name(ItemDecoratorPart));
borderStyle.Add(Border.MinHeightProperty, MenuResourceKey.ItemHeight);
@ -122,7 +122,7 @@ internal class OverflowTabMenuItemTheme : BaseControlTheme
// Hover 状态
var hoverStyle = new Style(selector => selector.Nesting().Class(StdPseudoClass.PointerOver));
hoverStyle.Add(OverflowTabMenuItem.ForegroundProperty, MenuResourceKey.ItemHoverColor);
hoverStyle.Add(BaseOverflowMenuItem.ForegroundProperty, MenuResourceKey.ItemHoverColor);
{
var borderStyle = new Style(selector => selector.Nesting().Template().Name(ItemDecoratorPart));
borderStyle.Add(Border.BackgroundProperty, MenuResourceKey.ItemHoverBg);
@ -134,7 +134,7 @@ internal class OverflowTabMenuItemTheme : BaseControlTheme
private void BuildDisabledStyle()
{
var disabledStyle = new Style(selector => selector.Nesting().Class(StdPseudoClass.Disabled));
disabledStyle.Add(OverflowTabMenuItem.ForegroundProperty, MenuResourceKey.ItemDisabledColor);
disabledStyle.Add(BaseOverflowMenuItem.ForegroundProperty, MenuResourceKey.ItemDisabledColor);
Add(disabledStyle);
}

View File

@ -0,0 +1,22 @@
using Avalonia.Controls;
using Avalonia.Threading;
namespace AtomUI.Controls;
internal class TabControlOverflowMenuItem : BaseOverflowMenuItem
{
protected override Type StyleKeyOverride => typeof(BaseOverflowMenuItem);
public TabItem? TabItem { get; set; }
protected override void NotifyCloseRequest()
{
if (Parent is MenuBase menu) {
var eventArgs = new CloseTabRequestEventArgs(CloseTabEvent, TabItem!);
RaiseEvent(eventArgs);
Dispatcher.UIThread.Post(() =>
{
menu.Close();
});
}
}
}

View File

@ -1,4 +1,9 @@
namespace AtomUI.Controls;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Threading;
namespace AtomUI.Controls;
internal class TabControlScrollViewer : BaseTabScrollViewer
{
@ -9,4 +14,93 @@ internal class TabControlScrollViewer : BaseTabScrollViewer
#endregion
protected override Type StyleKeyOverride => typeof(BaseTabScrollViewer);
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (_menuIndicator is not null) {
_menuIndicator.Click += HandleMenuIndicator;
}
}
private void HandleMenuIndicator(object? sender, RoutedEventArgs args)
{
if (_menuFlyout is null) {
_menuFlyout = new MenuFlyout();
}
if (TabStripPlacement == Dock.Top || TabStripPlacement == Dock.Bottom) {
_menuFlyout.Placement = PlacementMode.Bottom;
} else {
_menuFlyout.Placement = PlacementMode.Right;
}
// 收集没有完全显示的 Tab 列表
_menuFlyout.Items.Clear();
if (TabControl is not null) {
for (int i = 0; i < TabControl.ItemCount; i++) {
var itemContainer = TabControl.ContainerFromIndex(i)!;
if (itemContainer is TabItem tabItem) {
var itemBounds = itemContainer.Bounds;
var left = Math.Floor(itemBounds.Left - Offset.X);
var right = Math.Floor(itemBounds.Right - Offset.X);
if (TabStripPlacement == Dock.Top || TabStripPlacement == Dock.Bottom) {
if (left < 0 || right > Viewport.Width) {
var menuItem = new TabControlOverflowMenuItem
{
Header = tabItem.Header,
TabItem = tabItem,
IsClosable = tabItem.IsClosable
};
menuItem.Click += HandleMenuItemClicked;
menuItem.CloseTab += HandleCloseTabRequest;
_menuFlyout.Items.Add(menuItem);
}
}
}
}
if (_menuFlyout.Items.Count > 0) {
_menuFlyout.ShowAt(_menuIndicator!);
}
}
}
private void HandleMenuItemClicked(object? sender, RoutedEventArgs args)
{
if (TabControl is not null) {
Dispatcher.UIThread.Post(sender =>
{
if (sender is TabControlOverflowMenuItem tabControlMenuItem) {
var tabItem = tabControlMenuItem.TabItem;
if (tabItem is not null) {
tabItem.BringIntoView();
TabControl.SelectedItem = tabItem;
}
}
}, sender);
}
}
private void HandleCloseTabRequest(object? sender, RoutedEventArgs args)
{
if (sender is TabControlOverflowMenuItem tabControlMenuItem) {
if (TabControl is not null) {
if (TabControl.SelectedItem is TabItem selectedItem) {
if (selectedItem == tabControlMenuItem.TabItem) {
var selectedIndex = TabControl.SelectedIndex;
object? newSelectedItem = null;
if (selectedIndex != 0) {
newSelectedItem = TabControl.Items[--selectedIndex];
}
TabControl.Items.Remove(tabControlMenuItem.TabItem);
TabControl.SelectedItem = newSelectedItem;
} else {
TabControl.Items.Remove(tabControlMenuItem.TabItem);
}
}
}
}
}
}

View File

@ -0,0 +1,22 @@
using Avalonia.Controls;
using Avalonia.Threading;
namespace AtomUI.Controls;
internal class TabStripOverflowMenuItem : BaseOverflowMenuItem
{
protected override Type StyleKeyOverride => typeof(BaseOverflowMenuItem);
public TabStripItem? TabStripItem { get; set; }
protected override void NotifyCloseRequest()
{
if (Parent is MenuBase menu) {
var eventArgs = new CloseTabRequestEventArgs(CloseTabEvent, TabStripItem!);
RaiseEvent(eventArgs);
Dispatcher.UIThread.Post(() =>
{
menu.Close();
});
}
}
}

View File

@ -46,7 +46,7 @@ internal class TabStripScrollViewer : BaseTabScrollViewer
var right = Math.Floor(itemBounds.Right - Offset.X);
if (TabStripPlacement == Dock.Top || TabStripPlacement == Dock.Bottom) {
if (left < 0 || right > Viewport.Width) {
var menuItem = new OverflowTabMenuItem()
var menuItem = new TabStripOverflowMenuItem()
{
Header = tabStripItem.Content,
TabStripItem = tabStripItem,
@ -71,7 +71,7 @@ internal class TabStripScrollViewer : BaseTabScrollViewer
if (TabStrip is not null) {
Dispatcher.UIThread.Post(sender =>
{
if (sender is OverflowTabMenuItem tabStripMenuItem) {
if (sender is TabStripOverflowMenuItem tabStripMenuItem) {
var tabStripItem = tabStripMenuItem.TabStripItem;
if (tabStripItem is not null) {
tabStripItem.BringIntoView();
@ -84,7 +84,7 @@ internal class TabStripScrollViewer : BaseTabScrollViewer
private void HandleCloseTabRequest(object? sender, RoutedEventArgs args)
{
if (sender is OverflowTabMenuItem tabStripMenuItem) {
if (sender is TabStripOverflowMenuItem tabStripMenuItem) {
if (TabStrip is not null) {
if (TabStrip.SelectedItem is TabStripItem selectedItem) {
if (selectedItem == tabStripMenuItem.TabStripItem) {