diff --git a/docfx/articles/dock-controls-reference.md b/docfx/articles/dock-controls-reference.md index 54fbafd68..968fff5bd 100644 --- a/docfx/articles/dock-controls-reference.md +++ b/docfx/articles/dock-controls-reference.md @@ -131,6 +131,10 @@ For behavior details and keyboard validation guidance, see [Accessibility and UI | `Orientation` | `Orientation` | Tab strip orientation. | | `MouseWheelScrollOrientation` | `Orientation` | Mouse-wheel scroll axis for tab overflow (`Horizontal` by default). | | `CreateButtonTheme` | `ControlTheme?` | Theme for the create document button. | +| `IconTemplate` | `object?` | Tab icon template used by `DocumentTabStripItem`. | +| `HeaderTemplate` | `IDataTemplate?` | Tab header template used by `DocumentTabStripItem`. | +| `ModifiedTemplate` | `IDataTemplate?` | Modified indicator template used by `DocumentTabStripItem`. | +| `CloseTemplate` | `IDataTemplate?` | Close template used by `DocumentTabStripItem`. | ### DocumentTabStripItem @@ -145,6 +149,9 @@ For behavior details and keyboard validation guidance, see [Accessibility and UI | --- | --- | --- | | `CanCreateItem` | `bool` | `true` when the new-tool button is available. | | `MouseWheelScrollOrientation` | `Orientation` | Mouse-wheel scroll axis for tab overflow (`Horizontal` by default). | +| `IconTemplate` | `object?` | Tab icon template used by `ToolTabStripItem`. | +| `HeaderTemplate` | `IDataTemplate?` | Tab header template used by `ToolTabStripItem`. | +| `ModifiedTemplate` | `IDataTemplate?` | Modified indicator template used by `ToolTabStripItem`. | ### ToolTabStripItem diff --git a/src/Dock.Avalonia.Themes.Fluent/Controls/DocumentControl.axaml b/src/Dock.Avalonia.Themes.Fluent/Controls/DocumentControl.axaml index cdbf6ca9b..f0c880ac7 100644 --- a/src/Dock.Avalonia.Themes.Fluent/Controls/DocumentControl.axaml +++ b/src/Dock.Avalonia.Themes.Fluent/Controls/DocumentControl.axaml @@ -34,6 +34,10 @@ ItemsSource="{Binding VisibleDockables}" SelectedItem="{Binding ActiveDockable, Mode=TwoWay}" CanCreateItem="{Binding CanCreateDocument}" + IconTemplate="{TemplateBinding IconTemplate}" + HeaderTemplate="{TemplateBinding HeaderTemplate}" + ModifiedTemplate="{TemplateBinding ModifiedTemplate}" + CloseTemplate="{TemplateBinding CloseTemplate}" IsActive="{TemplateBinding IsActive}" Orientation="{Binding TabsLayout, Converter={x:Static DocumentTabOrientationConverter.Instance}}" DockPanel.Dock="{Binding TabsLayout, Converter={x:Static DocumentTabDockConverter.Instance}}" @@ -88,6 +92,10 @@ ItemsSource="{Binding VisibleDockables}" SelectedItem="{Binding ActiveDockable, Mode=TwoWay}" CanCreateItem="{Binding CanCreateDocument}" + IconTemplate="{TemplateBinding IconTemplate}" + HeaderTemplate="{TemplateBinding HeaderTemplate}" + ModifiedTemplate="{TemplateBinding ModifiedTemplate}" + CloseTemplate="{TemplateBinding CloseTemplate}" IsActive="{TemplateBinding IsActive}" Orientation="{Binding TabsLayout, Converter={x:Static DocumentTabOrientationConverter.Instance}}" DockPanel.Dock="{Binding TabsLayout, Converter={x:Static DocumentTabDockConverter.Instance}}" diff --git a/src/Dock.Avalonia.Themes.Fluent/Controls/DocumentTabStripItem.axaml b/src/Dock.Avalonia.Themes.Fluent/Controls/DocumentTabStripItem.axaml index ef5d6cc1e..ef24dcb43 100644 --- a/src/Dock.Avalonia.Themes.Fluent/Controls/DocumentTabStripItem.axaml +++ b/src/Dock.Avalonia.Themes.Fluent/Controls/DocumentTabStripItem.axaml @@ -360,18 +360,18 @@ @@ -380,7 +380,7 @@ HorizontalAlignment="Right" VerticalAlignment="Center" Margin="{DynamicResource DockTabContentMargin}" - ContentTemplate="{Binding $parent[DocumentControl].CloseTemplate}" + ContentTemplate="{Binding $parent[DocumentTabStrip].CloseTemplate}" Content="{Binding}" /> diff --git a/src/Dock.Avalonia.Themes.Fluent/Controls/ToolControl.axaml b/src/Dock.Avalonia.Themes.Fluent/Controls/ToolControl.axaml index d6060b48a..f1933e8ea 100644 --- a/src/Dock.Avalonia.Themes.Fluent/Controls/ToolControl.axaml +++ b/src/Dock.Avalonia.Themes.Fluent/Controls/ToolControl.axaml @@ -46,6 +46,9 @@ diff --git a/src/Dock.Avalonia.Themes.Fluent/Controls/ToolTabStripItem.axaml b/src/Dock.Avalonia.Themes.Fluent/Controls/ToolTabStripItem.axaml index 699d8ae5c..292ed65d7 100644 --- a/src/Dock.Avalonia.Themes.Fluent/Controls/ToolTabStripItem.axaml +++ b/src/Dock.Avalonia.Themes.Fluent/Controls/ToolTabStripItem.axaml @@ -180,12 +180,15 @@ Orientation="Horizontal" Spacing="{DynamicResource DockTabContentSpacing}"> - - - diff --git a/src/Dock.Avalonia/Controls/DocumentTabStrip.axaml.cs b/src/Dock.Avalonia/Controls/DocumentTabStrip.axaml.cs index bba55f71d..11ebf74e4 100644 --- a/src/Dock.Avalonia/Controls/DocumentTabStrip.axaml.cs +++ b/src/Dock.Avalonia/Controls/DocumentTabStrip.axaml.cs @@ -79,6 +79,30 @@ public class DocumentTabStrip : TabStrip public static readonly StyledProperty CreateButtonThemeProperty = AvaloniaProperty.Register(nameof(CreateButtonTheme)); + /// + /// Defines the property. + /// + public static readonly StyledProperty IconTemplateProperty = + AvaloniaProperty.Register(nameof(IconTemplate)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty HeaderTemplateProperty = + AvaloniaProperty.Register(nameof(HeaderTemplate)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ModifiedTemplateProperty = + AvaloniaProperty.Register(nameof(ModifiedTemplate)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CloseTemplateProperty = + AvaloniaProperty.Register(nameof(CloseTemplate)); + /// /// Defines the property. /// @@ -142,6 +166,42 @@ public ControlTheme? CreateButtonTheme set => SetValue(CreateButtonThemeProperty, value); } + /// + /// Gets or sets tab icon template. + /// + public object? IconTemplate + { + get => GetValue(IconTemplateProperty); + set => SetValue(IconTemplateProperty, value); + } + + /// + /// Gets or sets tab header template. + /// + public IDataTemplate? HeaderTemplate + { + get => GetValue(HeaderTemplateProperty); + set => SetValue(HeaderTemplateProperty, value); + } + + /// + /// Gets or sets tab modified template. + /// + public IDataTemplate? ModifiedTemplate + { + get => GetValue(ModifiedTemplateProperty); + set => SetValue(ModifiedTemplateProperty, value); + } + + /// + /// Gets or sets tab close template. + /// + public IDataTemplate? CloseTemplate + { + get => GetValue(CloseTemplateProperty); + set => SetValue(CloseTemplateProperty, value); + } + /// /// Gets or sets the create button content template. /// diff --git a/src/Dock.Avalonia/Controls/ToolTabStrip.axaml.cs b/src/Dock.Avalonia/Controls/ToolTabStrip.axaml.cs index 96209fbf8..2263bba41 100644 --- a/src/Dock.Avalonia/Controls/ToolTabStrip.axaml.cs +++ b/src/Dock.Avalonia/Controls/ToolTabStrip.axaml.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Reactive; @@ -44,6 +45,24 @@ public class ToolTabStrip : TabStrip nameof(MouseWheelScrollOrientation), defaultValue: Orientation.Horizontal); + /// + /// Defines the property. + /// + public static readonly StyledProperty IconTemplateProperty = + AvaloniaProperty.Register(nameof(IconTemplate)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty HeaderTemplateProperty = + AvaloniaProperty.Register(nameof(HeaderTemplate)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ModifiedTemplateProperty = + AvaloniaProperty.Register(nameof(ModifiedTemplate)); + /// /// Gets or sets if tab strop dock can create new items. /// @@ -62,6 +81,33 @@ public Orientation MouseWheelScrollOrientation set => SetValue(MouseWheelScrollOrientationProperty, value); } + /// + /// Gets or sets tab icon template. + /// + public object? IconTemplate + { + get => GetValue(IconTemplateProperty); + set => SetValue(IconTemplateProperty, value); + } + + /// + /// Gets or sets tab header template. + /// + public IDataTemplate? HeaderTemplate + { + get => GetValue(HeaderTemplateProperty); + set => SetValue(HeaderTemplateProperty, value); + } + + /// + /// Gets or sets tab modified template. + /// + public IDataTemplate? ModifiedTemplate + { + get => GetValue(ModifiedTemplateProperty); + set => SetValue(ModifiedTemplateProperty, value); + } + /// protected override Type StyleKeyOverride => typeof(ToolTabStrip); diff --git a/tests/Dock.Avalonia.HeadlessTests/DocumentTabStripTemplateBindingTests.cs b/tests/Dock.Avalonia.HeadlessTests/DocumentTabStripTemplateBindingTests.cs new file mode 100644 index 000000000..895dcad7a --- /dev/null +++ b/tests/Dock.Avalonia.HeadlessTests/DocumentTabStripTemplateBindingTests.cs @@ -0,0 +1,147 @@ +using System.Linq; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Headless.XUnit; +using Avalonia.VisualTree; +using Dock.Avalonia.Controls; +using Dock.Model.Avalonia; +using Dock.Model.Avalonia.Controls; +using Dock.Model.Core; +using Xunit; + +namespace Dock.Avalonia.HeadlessTests; + +public class DocumentTabStripTemplateBindingTests +{ + [AvaloniaFact] + public void Standalone_DocumentTabStrip_Uses_All_Item_Templates() + { + var templates = CreateTemplates(); + var document = new Document { Id = "doc-1", Title = "Doc 1", IsModified = true }; + var tabStrip = new DocumentTabStrip + { + Width = 320, + Height = 48, + IconTemplate = templates.IconTemplate, + HeaderTemplate = templates.HeaderTemplate, + ModifiedTemplate = templates.ModifiedTemplate, + CloseTemplate = templates.CloseTemplate, + ItemsSource = new AvaloniaList { document } + }; + + var window = ShowInWindow(tabStrip); + try + { + var tabItem = GetTabItem(tabStrip, 0); + + Assert.Same(templates.IconTemplate, GetPresenter(tabItem, "PART_IconPresenter").ContentTemplate); + Assert.Same(templates.HeaderTemplate, GetPresenter(tabItem, "PART_HeaderPresenter").ContentTemplate); + Assert.Same(templates.ModifiedTemplate, GetPresenter(tabItem, "PART_ModifiedPresenter").ContentTemplate); + Assert.Same(templates.CloseTemplate, GetPresenter(tabItem, "PART_ClosePresenter").ContentTemplate); + + var renderedTexts = tabItem.GetVisualDescendants() + .OfType() + .Select(textBlock => textBlock.Text ?? string.Empty) + .ToArray(); + + Assert.Contains("icon:Doc 1", renderedTexts); + Assert.Contains("header:Doc 1", renderedTexts); + Assert.Contains("modified:Doc 1", renderedTexts); + Assert.Contains("close:Doc 1", renderedTexts); + } + finally + { + window.Close(); + } + } + + [AvaloniaFact] + public void DocumentControl_Forwards_Templates_To_DocumentTabStrip() + { + var templates = CreateTemplates(); + var factory = new Factory(); + var dock = new DocumentDock + { + Factory = factory, + LayoutMode = DocumentLayoutMode.Tabbed, + VisibleDockables = factory.CreateList() + }; + + var document = new Document { Id = "doc-1", Title = "Doc 1", IsModified = true }; + dock.VisibleDockables!.Add(document); + dock.ActiveDockable = document; + + var control = new DocumentControl + { + DataContext = dock, + IconTemplate = templates.IconTemplate, + HeaderTemplate = templates.HeaderTemplate, + ModifiedTemplate = templates.ModifiedTemplate, + CloseTemplate = templates.CloseTemplate + }; + + var window = ShowInWindow(control); + try + { + var tabStrip = control.GetVisualDescendants().OfType().FirstOrDefault(); + Assert.NotNull(tabStrip); + + Assert.Same(templates.IconTemplate, tabStrip!.IconTemplate); + Assert.Same(templates.HeaderTemplate, tabStrip.HeaderTemplate); + Assert.Same(templates.ModifiedTemplate, tabStrip.ModifiedTemplate); + Assert.Same(templates.CloseTemplate, tabStrip.CloseTemplate); + + var tabItem = GetTabItem(tabStrip, 0); + Assert.Same(templates.CloseTemplate, GetPresenter(tabItem, "PART_ClosePresenter").ContentTemplate); + } + finally + { + window.Close(); + } + } + + private static (IDataTemplate IconTemplate, IDataTemplate HeaderTemplate, IDataTemplate ModifiedTemplate, IDataTemplate CloseTemplate) CreateTemplates() + { + var iconTemplate = new FuncDataTemplate((dockable, _) => new TextBlock { Text = $"icon:{dockable.Title}" }, true); + var headerTemplate = new FuncDataTemplate((dockable, _) => new TextBlock { Text = $"header:{dockable.Title}" }, true); + var modifiedTemplate = new FuncDataTemplate((dockable, _) => new TextBlock { Text = $"modified:{dockable.Title}" }, true); + var closeTemplate = new FuncDataTemplate((dockable, _) => new TextBlock { Text = $"close:{dockable.Title}" }, true); + return (iconTemplate, headerTemplate, modifiedTemplate, closeTemplate); + } + + private static Window ShowInWindow(Control control) + { + var window = new Window + { + Width = 600, + Height = 400, + Content = control + }; + + window.Show(); + control.ApplyTemplate(); + window.UpdateLayout(); + control.UpdateLayout(); + return window; + } + + private static DocumentTabStripItem GetTabItem(DocumentTabStrip tabStrip, int index) + { + var tabItem = tabStrip.ContainerFromIndex(index) as DocumentTabStripItem; + Assert.NotNull(tabItem); + tabItem!.ApplyTemplate(); + tabItem.UpdateLayout(); + return tabItem; + } + + private static ContentPresenter GetPresenter(DocumentTabStripItem tabItem, string presenterName) + { + var presenter = tabItem.GetVisualDescendants() + .OfType() + .FirstOrDefault(candidate => candidate.Name == presenterName); + Assert.NotNull(presenter); + return presenter!; + } +} diff --git a/tests/Dock.Avalonia.HeadlessTests/ToolTabStripTemplateBindingTests.cs b/tests/Dock.Avalonia.HeadlessTests/ToolTabStripTemplateBindingTests.cs new file mode 100644 index 000000000..06df33bca --- /dev/null +++ b/tests/Dock.Avalonia.HeadlessTests/ToolTabStripTemplateBindingTests.cs @@ -0,0 +1,154 @@ +using System.Linq; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Headless.XUnit; +using Avalonia.VisualTree; +using Dock.Avalonia.Controls; +using Dock.Model.Avalonia; +using Dock.Model.Avalonia.Controls; +using Dock.Model.Core; +using Xunit; + +namespace Dock.Avalonia.HeadlessTests; + +public class ToolTabStripTemplateBindingTests +{ + [AvaloniaFact] + public void Standalone_ToolTabStrip_Uses_All_Item_Templates() + { + var templates = CreateTemplates(); + var tool = new Tool { Id = "tool-1", Title = "Tool 1", IsModified = true }; + var tool2 = new Tool { Id = "tool-2", Title = "Tool 2", IsModified = false }; + var tabStrip = new ToolTabStrip + { + Width = 320, + Height = 48, + SelectedIndex = 0, + IconTemplate = templates.IconTemplate, + HeaderTemplate = templates.HeaderTemplate, + ModifiedTemplate = templates.ModifiedTemplate, + ItemsSource = new AvaloniaList { tool, tool2 } + }; + + var window = ShowInWindow(tabStrip); + try + { + var tabItem = GetTabItem(tabStrip, 0); + Assert.Same(templates.IconTemplate, GetPresenter(tabItem, "PART_IconPresenter").ContentTemplate); + Assert.Same(templates.HeaderTemplate, GetPresenter(tabItem, "PART_HeaderPresenter").ContentTemplate); + Assert.Same(templates.ModifiedTemplate, GetPresenter(tabItem, "PART_ModifiedPresenter").ContentTemplate); + + var renderedTexts = tabItem.GetVisualDescendants() + .OfType() + .Select(textBlock => textBlock.Text ?? string.Empty) + .ToArray(); + + Assert.Contains("tool-icon:Tool 1", renderedTexts); + Assert.Contains("tool-header:Tool 1", renderedTexts); + Assert.Contains("tool-modified:Tool 1", renderedTexts); + } + finally + { + window.Close(); + } + } + + [AvaloniaFact] + public void ToolControl_Forwards_Templates_To_ToolTabStrip() + { + var templates = CreateTemplates(); + var factory = new Factory(); + var dock = new ToolDock + { + Factory = factory, + VisibleDockables = factory.CreateList() + }; + + var tool = new Tool { Id = "tool-1", Title = "Tool 1", IsModified = true }; + var tool2 = new Tool { Id = "tool-2", Title = "Tool 2", IsModified = false }; + dock.VisibleDockables!.Add(tool); + dock.VisibleDockables!.Add(tool2); + dock.ActiveDockable = tool; + + var control = new ToolControl + { + DataContext = dock, + IconTemplate = templates.IconTemplate, + HeaderTemplate = templates.HeaderTemplate, + ModifiedTemplate = templates.ModifiedTemplate + }; + + var window = ShowInWindow(control); + try + { + var tabStrip = control.GetVisualDescendants().OfType().FirstOrDefault(); + Assert.NotNull(tabStrip); + + Assert.Same(templates.IconTemplate, tabStrip!.IconTemplate); + Assert.Same(templates.HeaderTemplate, tabStrip.HeaderTemplate); + Assert.Same(templates.ModifiedTemplate, tabStrip.ModifiedTemplate); + + var tabItem = GetTabItem(tabStrip, 0); + Assert.Same(templates.IconTemplate, GetPresenter(tabItem, "PART_IconPresenter").ContentTemplate); + Assert.Same(templates.HeaderTemplate, GetPresenter(tabItem, "PART_HeaderPresenter").ContentTemplate); + Assert.Same(templates.ModifiedTemplate, GetPresenter(tabItem, "PART_ModifiedPresenter").ContentTemplate); + + var renderedTexts = tabItem.GetVisualDescendants() + .OfType() + .Select(textBlock => textBlock.Text ?? string.Empty) + .ToArray(); + + Assert.Contains("tool-icon:Tool 1", renderedTexts); + Assert.Contains("tool-header:Tool 1", renderedTexts); + Assert.Contains("tool-modified:Tool 1", renderedTexts); + } + finally + { + window.Close(); + } + } + + private static (IDataTemplate IconTemplate, IDataTemplate HeaderTemplate, IDataTemplate ModifiedTemplate) CreateTemplates() + { + var iconTemplate = new FuncDataTemplate((dockable, _) => new TextBlock { Text = $"tool-icon:{dockable.Title}" }, true); + var headerTemplate = new FuncDataTemplate((dockable, _) => new TextBlock { Text = $"tool-header:{dockable.Title}" }, true); + var modifiedTemplate = new FuncDataTemplate((dockable, _) => new TextBlock { Text = $"tool-modified:{dockable.Title}" }, true); + return (iconTemplate, headerTemplate, modifiedTemplate); + } + + private static Window ShowInWindow(Control control) + { + var window = new Window + { + Width = 600, + Height = 400, + Content = control + }; + + window.Show(); + control.ApplyTemplate(); + window.UpdateLayout(); + control.UpdateLayout(); + return window; + } + + private static ToolTabStripItem GetTabItem(ToolTabStrip tabStrip, int index) + { + var tabItem = tabStrip.ContainerFromIndex(index) as ToolTabStripItem; + Assert.NotNull(tabItem); + tabItem!.ApplyTemplate(); + tabItem.UpdateLayout(); + return tabItem; + } + + private static ContentPresenter GetPresenter(ToolTabStripItem tabItem, string presenterName) + { + var presenter = tabItem.GetVisualDescendants() + .OfType() + .FirstOrDefault(candidate => candidate.Name == presenterName); + Assert.NotNull(presenter); + return presenter!; + } +}