diff --git a/samples/AtomUI.Demo.Desktop/ShowCase/TabControlShowCase.axaml b/samples/AtomUI.Demo.Desktop/ShowCase/TabControlShowCase.axaml index 3236865a7..67f084a7d 100644 --- a/samples/AtomUI.Demo.Desktop/ShowCase/TabControlShowCase.axaml +++ b/samples/AtomUI.Demo.Desktop/ShowCase/TabControlShowCase.axaml @@ -59,6 +59,76 @@ + + + + Content of Tab Pane 1 + Content of Tab Pane 2 + Content of Tab Pane 3 + + + + + + + + + + + + + + Content of Tab Pane 1 + Content of Tab Pane 2 + Content of Tab Pane 3 + Content of Tab Pane 4 + Content of Tab Pane 5 + Content of Tab Pane 6 + Content of Tab Pane 7 + Content of Tab Pane 8 + Content of Tab Pane 9 + Content of Tab Pane 10 + Content of Tab Pane 11 + Content of Tab Pane 12 + Content of Tab Pane 13 + Content of Tab Pane 14 + Content of Tab Pane 15 + Content of Tab Pane 16 + Content of Tab Pane 17 + Content of Tab Pane 18 + Content of Tab Pane 19 + Content of Tab Pane 20 + + + + Content of Tab Pane 1 + Content of Tab Pane 2 + Content of Tab Pane 3 + Content of Tab Pane 4 + Content of Tab Pane 5 + Content of Tab Pane 6 + Content of Tab Pane 7 + Content of Tab Pane 8 + Content of Tab Pane 9 + Content of Tab Pane 10 + Content of Tab Pane 11 + Content of Tab Pane 12 + Content of Tab Pane 13 + Content of Tab Pane 14 + Content of Tab Pane 15 + Content of Tab Pane 16 + Content of Tab Pane 17 + Content of Tab Pane 18 + Content of Tab Pane 19 + Content of Tab Pane 20 + + + + diff --git a/src/AtomUI.Controls/TabControl/OverflowTabMenuItem.cs b/src/AtomUI.Controls/TabControl/BaseOverflowMenuItem.cs similarity index 60% rename from src/AtomUI.Controls/TabControl/OverflowTabMenuItem.cs rename to src/AtomUI.Controls/TabControl/BaseOverflowMenuItem.cs index 0754eedc7..33329ef3c 100644 --- a/src/AtomUI.Controls/TabControl/OverflowTabMenuItem.cs +++ b/src/AtomUI.Controls/TabControl/BaseOverflowMenuItem.cs @@ -6,12 +6,12 @@ using Avalonia.Threading; namespace AtomUI.Controls; -internal class OverflowTabMenuItem : MenuItem +internal class BaseOverflowMenuItem : MenuItem { #region 公共属性 - public static readonly DirectProperty IsClosableProperty = - AvaloniaProperty.RegisterDirect(nameof(IsClosable), + public static readonly DirectProperty IsClosableProperty = + AvaloniaProperty.RegisterDirect(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? CloseTab { @@ -40,29 +38,26 @@ internal class OverflowTabMenuItem : MenuItem protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); - _iconButton = e.NameScope.Find(OverflowTabMenuItemTheme.ItemCloseButtonPart); + _iconButton = e.NameScope.Find(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; } } \ No newline at end of file diff --git a/src/AtomUI.Controls/TabControl/OverflowTabMenuItemTheme.cs b/src/AtomUI.Controls/TabControl/BaseOverflowMenuItemTheme.cs similarity index 87% rename from src/AtomUI.Controls/TabControl/OverflowTabMenuItemTheme.cs rename to src/AtomUI.Controls/TabControl/BaseOverflowMenuItemTheme.cs index 76392a35a..5c08ae91e 100644 --- a/src/AtomUI.Controls/TabControl/OverflowTabMenuItemTheme.cs +++ b/src/AtomUI.Controls/TabControl/BaseOverflowMenuItemTheme.cs @@ -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((item, scope) => + return new FuncControlTemplate((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); } diff --git a/src/AtomUI.Controls/TabControl/TabControlOverflowMenuItem.cs b/src/AtomUI.Controls/TabControl/TabControlOverflowMenuItem.cs new file mode 100644 index 000000000..17c51607e --- /dev/null +++ b/src/AtomUI.Controls/TabControl/TabControlOverflowMenuItem.cs @@ -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(); + }); + } + } +} diff --git a/src/AtomUI.Controls/TabControl/TabControlScrollViewer.cs b/src/AtomUI.Controls/TabControl/TabControlScrollViewer.cs index 88455b34a..c23643cc7 100644 --- a/src/AtomUI.Controls/TabControl/TabControlScrollViewer.cs +++ b/src/AtomUI.Controls/TabControl/TabControlScrollViewer.cs @@ -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); + } + } + } + + } + } } \ No newline at end of file diff --git a/src/AtomUI.Controls/TabControl/TabStrip/TabStripOverflowMenuItem.cs b/src/AtomUI.Controls/TabControl/TabStrip/TabStripOverflowMenuItem.cs new file mode 100644 index 000000000..b473e9c27 --- /dev/null +++ b/src/AtomUI.Controls/TabControl/TabStrip/TabStripOverflowMenuItem.cs @@ -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(); + }); + } + } +} diff --git a/src/AtomUI.Controls/TabControl/TabStrip/TabStripScrollViewer.cs b/src/AtomUI.Controls/TabControl/TabStrip/TabStripScrollViewer.cs index 4a1c07b21..514e9a648 100644 --- a/src/AtomUI.Controls/TabControl/TabStrip/TabStripScrollViewer.cs +++ b/src/AtomUI.Controls/TabControl/TabStrip/TabStripScrollViewer.cs @@ -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) {