688 lines
20 KiB
C#
688 lines
20 KiB
C#
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);
|
||
}
|
||
}
|
||
} |