Files
Lattice/Lattice.UI.Docking.WinUI/Controls/LatticeTabControl.cs
2026-01-18 16:33:35 +03:00

688 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Lattice.Core.Docking.Abstractions;
using Lattice.Core.Docking.Engine;
using Lattice.Core.Docking.Models;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
namespace Lattice.UI;
/// <summary>
/// Кастомный контрол вкладок с поддержкой всех позиций размещения панели вкладок.
/// Реализует интерфейс <see cref="IDockLeafControl"/> для интеграции с системой докинга.
/// </summary>
public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
{
private readonly PropertyChangedEventHandler _modelPropertyChangedHandler;
private bool _disposed;
private DockLeaf? _model;
private Grid? _rootGrid;
private ListBox? _tabHeaderList;
private ContentControl? _contentControl;
private LayoutManager? _layoutManager;
private IDockDragDropService? _dragDropService;
private IDockContextManager? _contextManager;
private bool _isSelected;
private bool _isActive;
private bool _canDrag = true;
private bool _canDrop = true;
private bool _showCloseButtons = true;
private bool _canReorderTabs = true;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="LatticeTabControl"/>.
/// </summary>
public LatticeTabControl()
{
this.DefaultStyleKey = typeof(LatticeTabControl);
_modelPropertyChangedHandler = OnModelPropertyChanged;
this.DataContextChanged += OnDataContextChanged;
// Подписываемся на события
this.PointerPressed += OnPointerPressed;
this.PointerMoved += OnPointerMoved;
this.PointerReleased += OnPointerReleased;
}
/// <inheritdoc/>
public IDockElement? Model
{
get => _model;
set
{
if (_model == value) return;
DetachModel();
_model = value as DockLeaf;
AttachModel();
OnPropertyChanged(nameof(Model));
}
}
/// <inheritdoc/>
public LayoutManager? LayoutManager
{
get => _layoutManager;
set
{
if (_layoutManager == value) return;
_layoutManager = value;
OnPropertyChanged(nameof(LayoutManager));
}
}
/// <inheritdoc/>
public IDockDragDropService? DragDropService
{
get => _dragDropService;
set
{
if (_dragDropService == value) return;
_dragDropService = value;
OnPropertyChanged(nameof(DragDropService));
}
}
/// <inheritdoc/>
public IDockContextManager? ContextManager
{
get => _contextManager;
set
{
if (_contextManager == value) return;
_contextManager = value;
OnPropertyChanged(nameof(ContextManager));
}
}
/// <inheritdoc/>
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected == value) return;
_isSelected = value;
OnPropertyChanged(nameof(IsSelected));
}
}
/// <inheritdoc/>
public bool IsActive
{
get => _isActive;
set
{
if (_isActive == value) return;
_isActive = value;
OnPropertyChanged(nameof(IsActive));
}
}
/// <inheritdoc/>
public bool CanDrag
{
get => _canDrag;
set
{
if (_canDrag == value) return;
_canDrag = value;
OnPropertyChanged(nameof(CanDrag));
}
}
/// <inheritdoc/>
public bool CanDrop
{
get => _canDrop;
set
{
if (_canDrop == value) return;
_canDrop = value;
OnPropertyChanged(nameof(CanDrop));
}
}
/// <inheritdoc/>
public TabPlacement TabPlacement
{
get => _model?.TabPlacement ?? TabPlacement.Top;
set
{
if (_model != null && _model.TabPlacement != value)
{
_model.TabPlacement = value;
UpdateTabPlacement();
}
}
}
/// <inheritdoc/>
public bool ShowCloseButtons
{
get => _showCloseButtons;
set
{
if (_showCloseButtons == value) return;
_showCloseButtons = value;
OnPropertyChanged(nameof(ShowCloseButtons));
UpdateTabHeaders();
}
}
/// <inheritdoc/>
public bool CanReorderTabs
{
get => _canReorderTabs;
set
{
if (_canReorderTabs == value) return;
_canReorderTabs = value;
OnPropertyChanged(nameof(CanReorderTabs));
}
}
/// <inheritdoc/>
public IDockContent? ActiveContent
{
get => _model?.ActiveContent;
set
{
if (_model != null)
{
_model.ActiveContent = value;
}
}
}
/// <inheritdoc/>
public event EventHandler<ActiveContentChangedEventArgs>? ActiveContentChanged;
/// <inheritdoc/>
public event EventHandler<ContentClosingEventArgs>? ContentClosing;
/// <inheritdoc/>
public event EventHandler<TabsReorderedEventArgs>? TabsReordered;
/// <inheritdoc/>
public event PropertyChangedEventHandler? PropertyChanged;
/// <inheritdoc/>
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_rootGrid = GetTemplateChild("PART_RootGrid") as Grid;
_tabHeaderList = GetTemplateChild("PART_TabHeaderList") as ListBox;
_contentControl = GetTemplateChild("PART_ContentControl") as ContentControl;
if (_tabHeaderList != null)
{
_tabHeaderList.SelectionChanged += OnTabSelectionChanged;
}
UpdateTabPlacement();
UpdateTabHeaders();
}
private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
Model = args.NewValue as DockLeaf;
}
private void AttachModel()
{
if (_model != null)
{
_model.PropertyChanged += _modelPropertyChangedHandler;
// Подписываемся на изменения коллекции
if (_model.Children is INotifyCollectionChanged notifyCollection)
{
notifyCollection.CollectionChanged += OnChildrenCollectionChanged;
}
// Устанавливаем DataContext для привязки в XAML
this.DataContext = _model;
UpdateTabHeaders();
UpdateTabPlacement();
}
}
private void DetachModel()
{
if (_model != null)
{
_model.PropertyChanged -= _modelPropertyChangedHandler;
if (_model.Children is INotifyCollectionChanged notifyCollection)
{
notifyCollection.CollectionChanged -= OnChildrenCollectionChanged;
}
this.DataContext = null;
}
}
private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(DockLeaf.TabPlacement):
OnPropertyChanged(nameof(TabPlacement));
UpdateTabPlacement();
break;
case nameof(DockLeaf.ActiveContent):
OnPropertyChanged(nameof(ActiveContent));
UpdateSelectedTab();
break;
case nameof(DockLeaf.Children):
UpdateTabHeaders();
break;
}
}
private void OnChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
UpdateTabHeaders();
}
private void UpdateTabPlacement()
{
if (_rootGrid == null || _model == null) return;
// Очищаем все определения
_rootGrid.RowDefinitions.Clear();
_rootGrid.ColumnDefinitions.Clear();
// Настраиваем Grid в зависимости от позиции вкладок
switch (_model.TabPlacement)
{
case TabPlacement.Top:
_rootGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
_rootGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
UpdateHeaderListOrientation(Orientation.Horizontal);
break;
case TabPlacement.Bottom:
_rootGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
_rootGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
UpdateHeaderListOrientation(Orientation.Horizontal);
break;
case TabPlacement.Left:
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
UpdateHeaderListOrientation(Orientation.Vertical);
break;
case TabPlacement.Right:
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
UpdateHeaderListOrientation(Orientation.Vertical);
break;
}
// Обновляем позиции элементов
UpdateElementPositions();
}
private void UpdateHeaderListOrientation(Orientation orientation)
{
if (_tabHeaderList != null && _tabHeaderList.ItemsPanelRoot is ItemsPanelTemplate panelTemplate)
{
if (panelTemplate.LoadContent() is StackPanel stackPanel)
{
stackPanel.Orientation = orientation;
}
}
}
private void UpdateElementPositions()
{
if (_rootGrid == null || _tabHeaderList == null || _contentControl == null) return;
switch (_model?.TabPlacement)
{
case TabPlacement.Top:
Grid.SetRow(_tabHeaderList, 0);
Grid.SetRow(_contentControl, 1);
Grid.SetColumn(_tabHeaderList, 0);
Grid.SetColumn(_contentControl, 0);
break;
case TabPlacement.Bottom:
Grid.SetRow(_contentControl, 0);
Grid.SetRow(_tabHeaderList, 1);
Grid.SetColumn(_contentControl, 0);
Grid.SetColumn(_tabHeaderList, 0);
break;
case TabPlacement.Left:
Grid.SetColumn(_tabHeaderList, 0);
Grid.SetColumn(_contentControl, 1);
Grid.SetRow(_tabHeaderList, 0);
Grid.SetRow(_contentControl, 0);
break;
case TabPlacement.Right:
Grid.SetColumn(_contentControl, 0);
Grid.SetColumn(_tabHeaderList, 1);
Grid.SetRow(_contentControl, 0);
Grid.SetRow(_tabHeaderList, 0);
break;
}
}
private void UpdateTabHeaders()
{
if (_tabHeaderList == null || _model == null) return;
// Очищаем текущие элементы
_tabHeaderList.Items.Clear();
// Добавляем новые элементы
foreach (var content in _model.Children)
{
var item = CreateTabHeaderItem(content);
_tabHeaderList.Items.Add(item);
}
UpdateSelectedTab();
}
private ListBoxItem CreateTabHeaderItem(IDockContent content)
{
var item = new ListBoxItem
{
Content = CreateTabHeaderContent(content),
Tag = content,
HorizontalContentAlignment = HorizontalAlignment.Stretch,
VerticalContentAlignment = VerticalAlignment.Stretch
};
// Обработка клика для выбора вкладки
item.PointerPressed += (sender, e) =>
{
if (e.GetCurrentPoint(item).Properties.IsLeftButtonPressed)
{
ActiveContent = content;
e.Handled = true;
}
};
// Обработка клика на кнопке закрытия
if (_showCloseButtons && content.CanClose)
{
// Добавляем контекстное меню
item.ContextRequested += (sender, e) =>
{
ShowTabContextMenu(item, content, e);
};
}
return item;
}
private UIElement CreateTabHeaderContent(IDockContent content)
{
var grid = new Grid();
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
// Заголовок вкладки
var textBlock = new TextBlock
{
Text = content.Title,
Margin = new Thickness(8, 4, 8, 4),
VerticalAlignment = VerticalAlignment.Center
};
Grid.SetColumn(textBlock, 0);
grid.Children.Add(textBlock);
// Кнопка закрытия (если разрешено)
if (_showCloseButtons && content.CanClose)
{
var closeButton = new Button
{
Content = "×",
FontSize = 16,
Width = 24,
Height = 24,
Margin = new Thickness(2),
Padding = new Thickness(0),
Background = new SolidColorBrush(Microsoft.UI.Colors.Transparent),
BorderThickness = new Thickness(0)
};
closeButton.Click += (sender, e) =>
{
CloseContent(content);
e.Handled = true;
};
Grid.SetColumn(closeButton, 1);
grid.Children.Add(closeButton);
}
return grid;
}
private void UpdateSelectedTab()
{
if (_tabHeaderList == null || _model == null) return;
// Находим элемент, соответствующий активному контенту
foreach (var item in _tabHeaderList.Items)
{
if (item is ListBoxItem listBoxItem && listBoxItem.Tag is IDockContent content)
{
listBoxItem.IsSelected = content == _model.ActiveContent;
}
}
// Обновляем контент
if (_contentControl != null)
{
_contentControl.Content = _model.ActiveContent?.View;
}
}
private void OnTabSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_tabHeaderList?.SelectedItem is ListBoxItem selectedItem &&
selectedItem.Tag is IDockContent content)
{
var oldContent = ActiveContent;
ActiveContent = content;
if (oldContent != content)
{
ActiveContentChanged?.Invoke(this,
new ActiveContentChangedEventArgs(oldContent, content));
}
}
}
// Drag-and-Drop для переупорядочивания вкладок
private ListBoxItem? _draggedItem;
private Point _dragStartPoint;
private void OnPointerPressed(object sender, PointerRoutedEventArgs e)
{
if (!_canReorderTabs) return;
var pointerPoint = e.GetCurrentPoint(this);
_dragStartPoint = new Point(pointerPoint.Position.X, pointerPoint.Position.Y);
// Находим элемент под курсором
var element = VisualTreeHelper.FindElementsInHostCoordinates(
pointerPoint.Position, this).FirstOrDefault();
if (element is ListBoxItem listBoxItem)
{
_draggedItem = listBoxItem;
}
}
private void OnPointerMoved(object sender, PointerRoutedEventArgs e)
{
if (_draggedItem == null || !_canReorderTabs) return;
var pointerPoint = e.GetCurrentPoint(this);
var currentPoint = new Point(pointerPoint.Position.X, pointerPoint.Position.Y);
// Проверяем, достаточно ли переместили для начала перетаскивания
var distance = Math.Sqrt(
Math.Pow(currentPoint.X - _dragStartPoint.X, 2) +
Math.Pow(currentPoint.Y - _dragStartPoint.Y, 2));
if (distance > 10 && _draggedItem.Tag is IDockContent content)
{
// Начинаем операцию перетаскивания
StartTabDrag(_draggedItem, content);
_draggedItem = null;
}
}
private void OnPointerReleased(object sender, PointerRoutedEventArgs e)
{
_draggedItem = null;
}
private void StartTabDrag(ListBoxItem item, IDockContent content)
{
// TODO: Реализовать перетаскивание вкладок
// Для этого нужно использовать IDockDragDropService
}
private void ShowTabContextMenu(ListBoxItem item, IDockContent content, ContextRequestedEventArgs e)
{
if (_contextManager == null) return;
var position = e.GetPosition(this);
_contextManager.ShowContextMenu(this, position.X, position.Y);
}
/// <inheritdoc/>
public void AddContent(IDockContent content)
{
if (_model != null && !_model.Children.Contains(content))
{
_model.AddContent(content);
UpdateTabHeaders();
}
}
/// <inheritdoc/>
public void RemoveContent(IDockContent content)
{
if (_model != null && _model.Children.Contains(content))
{
_model.RemoveContent(content);
UpdateTabHeaders();
}
}
/// <inheritdoc/>
public bool CloseContent(IDockContent content)
{
var args = new ContentClosingEventArgs(content);
ContentClosing?.Invoke(this, args);
if (!args.Cancel)
{
RemoveContent(content);
return true;
}
return false;
}
/// <inheritdoc/>
public void CloseAllExcept(IDockContent exceptContent)
{
if (_model == null) return;
var itemsToClose = _model.Children
.Where(c => c != exceptContent)
.ToList();
foreach (var content in itemsToClose)
{
CloseContent(content);
}
}
/// <inheritdoc/>
public void CloseAll()
{
if (_model == null) return;
var itemsToClose = _model.Children.ToList();
foreach (var content in itemsToClose)
{
CloseContent(content);
}
}
/// <inheritdoc/>
public void Refresh()
{
UpdateTabHeaders();
UpdateTabPlacement();
}
/// <inheritdoc/>
public void ApplyTheme(IDockTheme theme)
{
// Применение темы к элементу
if (theme != null)
{
// TODO: Применить тему к стилям контрола
}
}
/// <inheritdoc/>
public void OnModelPropertyChanged(string propertyName)
{
if (_model != null)
{
OnModelPropertyChanged(_model, new PropertyChangedEventArgs(propertyName));
}
}
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <inheritdoc/>
public void Dispose()
{
if (!_disposed)
{
DetachModel();
if (_tabHeaderList != null)
{
_tabHeaderList.SelectionChanged -= OnTabSelectionChanged;
}
// Отписываемся от событий указателя
this.PointerPressed -= OnPointerPressed;
this.PointerMoved -= OnPointerMoved;
this.PointerReleased -= OnPointerReleased;
_disposed = true;
GC.SuppressFinalize(this);
}
}
}