A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

324 lines
13 KiB

using System.Linq;
using Avalonia.Collections;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.VisualTree;
using Avalonia.Automation;
using Avalonia.Controls.Metadata;
using Avalonia.Reactive;
using Avalonia.Interactivity;
namespace Avalonia.Controls
{
/// <summary>
/// A tab control that displays a tab strip along with the content of the selected tab.
/// </summary>
[TemplatePart("PART_ItemsPresenter", typeof(ItemsPresenter))]
[TemplatePart("PART_SelectedContentHost", typeof(ContentPresenter))]
public class TabControl : SelectingItemsControl, IContentPresenterHost
{
private object? _selectedContent;
private IDataTemplate? _selectedContentTemplate;
private CompositeDisposable? _selectedItemSubscriptions;
/// <summary>
/// Defines the <see cref="TabStripPlacement"/> property.
/// </summary>
public static readonly StyledProperty<Dock> TabStripPlacementProperty =
AvaloniaProperty.Register<TabControl, Dock>(nameof(TabStripPlacement), defaultValue: Dock.Top);
/// <summary>
/// Defines the <see cref="HorizontalContentAlignment"/> property.
/// </summary>
public static readonly StyledProperty<HorizontalAlignment> HorizontalContentAlignmentProperty =
ContentControl.HorizontalContentAlignmentProperty.AddOwner<TabControl>();
/// <summary>
/// Defines the <see cref="VerticalContentAlignment"/> property.
/// </summary>
public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
ContentControl.VerticalContentAlignmentProperty.AddOwner<TabControl>();
/// <summary>
/// Defines the <see cref="ContentTemplate"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate?> ContentTemplateProperty =
ContentControl.ContentTemplateProperty.AddOwner<TabControl>();
/// <summary>
/// The selected content property
/// </summary>
public static readonly DirectProperty<TabControl, object?> SelectedContentProperty =
AvaloniaProperty.RegisterDirect<TabControl, object?>(nameof(SelectedContent), o => o.SelectedContent);
/// <summary>
/// The selected content template property
/// </summary>
public static readonly DirectProperty<TabControl, IDataTemplate?> SelectedContentTemplateProperty =
AvaloniaProperty.RegisterDirect<TabControl, IDataTemplate?>(nameof(SelectedContentTemplate), o => o.SelectedContentTemplate);
/// <summary>
/// The default value for the <see cref="ItemsControl.ItemsPanel"/> property.
/// </summary>
private static readonly FuncTemplate<Panel?> DefaultPanel =
new(() => new WrapPanel());
/// <summary>
/// Initializes static members of the <see cref="TabControl"/> class.
/// </summary>
static TabControl()
{
SelectionModeProperty.OverrideDefaultValue<TabControl>(SelectionMode.AlwaysSelected);
ItemsPanelProperty.OverrideDefaultValue<TabControl>(DefaultPanel);
AffectsMeasure<TabControl>(TabStripPlacementProperty);
SelectedItemProperty.Changed.AddClassHandler<TabControl>((x, e) => x.UpdateSelectedContent());
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<TabControl>(AutomationControlType.Tab);
}
/// <summary>
/// Gets or sets the horizontal alignment of the content within the control.
/// </summary>
public HorizontalAlignment HorizontalContentAlignment
{
get => GetValue(HorizontalContentAlignmentProperty);
set => SetValue(HorizontalContentAlignmentProperty, value);
}
/// <summary>
/// Gets or sets the vertical alignment of the content within the control.
/// </summary>
public VerticalAlignment VerticalContentAlignment
{
get => GetValue(VerticalContentAlignmentProperty);
set => SetValue(VerticalContentAlignmentProperty, value);
}
/// <summary>
/// Gets or sets the tabstrip placement of the TabControl.
/// </summary>
public Dock TabStripPlacement
{
get => GetValue(TabStripPlacementProperty);
set => SetValue(TabStripPlacementProperty, value);
}
/// <summary>
/// Gets or sets the default data template used to display the content of the selected tab.
/// </summary>
public IDataTemplate? ContentTemplate
{
get => GetValue(ContentTemplateProperty);
set => SetValue(ContentTemplateProperty, value);
}
/// <summary>
/// Gets or sets the content of the selected tab.
/// </summary>
/// <value>
/// The content of the selected tab.
/// </value>
public object? SelectedContent
{
get => _selectedContent;
internal set => SetAndRaise(SelectedContentProperty, ref _selectedContent, value);
}
/// <summary>
/// Gets or sets the content template for the selected tab.
/// </summary>
/// <value>
/// The content template of the selected tab.
/// </value>
public IDataTemplate? SelectedContentTemplate
{
get => _selectedContentTemplate;
internal set => SetAndRaise(SelectedContentTemplateProperty, ref _selectedContentTemplate, value);
}
internal ItemsPresenter? ItemsPresenterPart { get; private set; }
internal ContentPresenter? ContentPart { get; private set; }
/// <inheritdoc/>
IAvaloniaList<ILogical> IContentPresenterHost.LogicalChildren => LogicalChildren;
/// <inheritdoc/>
bool IContentPresenterHost.RegisterContentPresenter(ContentPresenter presenter)
{
return RegisterContentPresenter(presenter);
}
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new TabItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<TabItem>(item, out recycleKey);
}
protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index)
{
base.PrepareContainerForItemOverride(element, item, index);
if (element is TabItem tabItem)
{
tabItem.TabStripPlacement = TabStripPlacement;
}
if (index == SelectedIndex)
{
UpdateSelectedContent(element);
}
}
protected override void ContainerIndexChangedOverride(Control container, int oldIndex, int newIndex)
{
base.ContainerIndexChangedOverride(container, oldIndex, newIndex);
var selectedIndex = SelectedIndex;
if (selectedIndex == oldIndex || selectedIndex == newIndex)
UpdateSelectedContent();
}
protected internal override void ClearContainerForItemOverride(Control element)
{
base.ClearContainerForItemOverride(element);
UpdateSelectedContent();
}
protected override bool ShouldTriggerSelection(Visual selectable, PointerEventArgs eventArgs) =>
eventArgs.Properties.PointerUpdateKind is PointerUpdateKind.LeftButtonPressed or PointerUpdateKind.LeftButtonReleased && base.ShouldTriggerSelection(selectable, eventArgs);
public override bool UpdateSelectionFromEvent(Control container, RoutedEventArgs eventArgs)
{
if (eventArgs is GotFocusEventArgs { NavigationMethod: not NavigationMethod.Directional })
{
return false;
}
return base.UpdateSelectionFromEvent(container, eventArgs);
}
private void UpdateSelectedContent(Control? container = null)
{
_selectedItemSubscriptions?.Dispose();
_selectedItemSubscriptions = null;
if (SelectedIndex == -1)
{
SelectedContent = SelectedContentTemplate = null;
}
else
{
container ??= ContainerFromIndex(SelectedIndex);
if (container != null)
{
if (SelectedContentTemplate != SelectContentTemplate(container.GetValue(ContentTemplateProperty)))
{
// If the value of SelectedContentTemplate is about to change, clear it first. This ensures
// that the template is not reused as soon as SelectedContent changes in the statement below
// this block, and also that controls generated from it are unloaded before SelectedContent
// (which is typically their DataContext) changes.
SelectedContentTemplate = null;
}
_selectedItemSubscriptions = new CompositeDisposable(
container.GetObservable(ContentControl.ContentProperty).Subscribe(v => SelectedContent = v),
container.GetObservable(ContentControl.ContentTemplateProperty).Subscribe(v => SelectedContentTemplate = SelectContentTemplate(v)));
// Note how we fall back to our own ContentTemplate if the container doesn't specify one
IDataTemplate? SelectContentTemplate(IDataTemplate? containerTemplate) => containerTemplate ?? ContentTemplate;
}
}
}
/// <summary>
/// Called when an <see cref="ContentPresenter"/> is registered with the control.
/// </summary>
/// <param name="presenter">The presenter.</param>
protected virtual bool RegisterContentPresenter(ContentPresenter presenter)
{
if (presenter.Name == "PART_SelectedContentHost")
{
ContentPart = presenter;
return true;
}
return false;
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
ItemsPresenterPart = e.NameScope.Find<ItemsPresenter>("PART_ItemsPresenter");
ItemsPresenterPart?.ApplyTemplate();
UpdateTabStripPlacement();
// Set TabNavigation to Once on the panel if not already set and
// forward the TabOnceActiveElement to the panel.
if (ItemsPresenterPart?.Panel is { } panel)
{
if (!panel.IsSet(KeyboardNavigation.TabNavigationProperty))
panel.SetCurrentValue(
KeyboardNavigation.TabNavigationProperty,
KeyboardNavigationMode.Once);
KeyboardNavigation.SetTabOnceActiveElement(
panel,
KeyboardNavigation.GetTabOnceActiveElement(this));
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == TabStripPlacementProperty)
{
RefreshContainers();
}
else if (change.Property == ContentTemplateProperty)
{
var newTemplate = change.GetNewValue<IDataTemplate?>();
if (SelectedContentTemplate != newTemplate &&
ContainerFromIndex(SelectedIndex) is { } container &&
container.GetValue(ContentControl.ContentTemplateProperty) == null)
{
SelectedContentTemplate = newTemplate; // See also UpdateSelectedContent
}
}
else if (change.Property == KeyboardNavigation.TabOnceActiveElementProperty &&
ItemsPresenterPart?.Panel is { } panel)
{
// Forward TabOnceActiveElement to the panel.
KeyboardNavigation.SetTabOnceActiveElement(
panel,
change.GetNewValue<IInputElement?>());
}
}
private void UpdateTabStripPlacement()
{
var controls = ItemsPresenterPart?.Panel?.Children;
if (controls is null)
{
return;
}
foreach (var control in controls)
{
if (control is TabItem tabItem)
{
tabItem.TabStripPlacement = TabStripPlacement;
}
}
}
}
}