Доработан Docking

This commit is contained in:
2026-01-27 05:17:35 +03:00
parent 33abd94f6e
commit 584df249f6
99 changed files with 2270 additions and 12792 deletions

View File

@@ -1,9 +1,9 @@
using Lattice.Core.Docking.Abstractions;
using Lattice.Core.Docking.Engine;
using Lattice.Core.Docking.Models;
using Lattice.UI.Docking.Abstractions;
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;
@@ -14,9 +14,20 @@ using System.Runtime.CompilerServices;
namespace Lattice.UI;
/// <summary>
/// Кастомный контрол вкладок с поддержкой всех позиций размещения панели вкладок.
/// Представляет кастомный контрол вкладок с поддержкой всех позиций размещения панели вкладок.
/// Реализует интерфейс <see cref="IDockLeafControl"/> для интеграции с системой докинга.
/// </summary>
/// <remarks>
/// <para>
/// Контрол обеспечивает отображение коллекции вкладок с возможностью навигации между ними,
/// закрытия вкладок и изменения порядка. Поддерживает все четыре позиции размещения панели
/// вкладок: сверху, снизу, слева и справа.
/// </para>
/// <para>
/// Контрол автоматически синхронизирует свое состояние с моделью данных <see cref="DockLeaf"/>
/// и обеспечивает двустороннюю привязку данных через механизм INotifyPropertyChanged.
/// </para>
/// </remarks>
public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
{
private readonly PropertyChangedEventHandler _modelPropertyChangedHandler;
@@ -26,12 +37,10 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
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 TabPlacement _tabPlacement = TabPlacement.Top;
private bool _showCloseButtons = true;
private bool _canReorderTabs = true;
@@ -43,11 +52,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
this.DefaultStyleKey = typeof(LatticeTabControl);
_modelPropertyChangedHandler = OnModelPropertyChanged;
this.DataContextChanged += OnDataContextChanged;
// Подписываемся на события
this.PointerPressed += OnPointerPressed;
this.PointerMoved += OnPointerMoved;
this.PointerReleased += OnPointerReleased;
}
/// <inheritdoc/>
@@ -76,18 +80,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <inheritdoc/>
public IDockDragDropService? DragDropService
{
get => _dragDropService;
set
{
if (_dragDropService == value) return;
_dragDropService = value;
OnPropertyChanged(nameof(DragDropService));
}
}
/// <inheritdoc/>
public IDockContextManager? ContextManager
{
@@ -124,40 +116,17 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <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;
get => _tabPlacement;
set
{
if (_model != null && _model.TabPlacement != value)
if (_tabPlacement != value)
{
_model.TabPlacement = value;
_tabPlacement = value;
UpdateTabPlacement();
OnPropertyChanged(nameof(TabPlacement));
}
}
}
@@ -168,10 +137,12 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
get => _showCloseButtons;
set
{
if (_showCloseButtons == value) return;
_showCloseButtons = value;
OnPropertyChanged(nameof(ShowCloseButtons));
UpdateTabHeaders();
if (_showCloseButtons != value)
{
_showCloseButtons = value;
OnPropertyChanged(nameof(ShowCloseButtons));
UpdateTabHeaders();
}
}
}
@@ -181,9 +152,11 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
get => _canReorderTabs;
set
{
if (_canReorderTabs == value) return;
_canReorderTabs = value;
OnPropertyChanged(nameof(CanReorderTabs));
if (_canReorderTabs != value)
{
_canReorderTabs = value;
OnPropertyChanged(nameof(CanReorderTabs));
}
}
}
@@ -230,30 +203,38 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
UpdateTabHeaders();
}
/// <summary>
/// Обрабатывает изменение контекста данных контрола.
/// </summary>
private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
Model = args.NewValue as DockLeaf;
}
/// <summary>
/// Присоединяет модель данных к контролу.
/// </summary>
private void AttachModel()
{
if (_model != null)
{
_model.PropertyChanged += _modelPropertyChangedHandler;
// Подписываемся на изменения коллекции
if (_model.Children is INotifyCollectionChanged notifyCollection)
{
notifyCollection.CollectionChanged += OnChildrenCollectionChanged;
}
// Устанавливаем DataContext для привязки в XAML
this.DataContext = _model;
_tabPlacement = _model.TabPlacement;
UpdateTabHeaders();
UpdateTabPlacement();
}
}
/// <summary>
/// Отсоединяет модель данных от контрола.
/// </summary>
private void DetachModel()
{
if (_model != null)
@@ -269,11 +250,15 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <summary>
/// Обрабатывает изменения свойств модели данных.
/// </summary>
private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(DockLeaf.TabPlacement):
_tabPlacement = _model?.TabPlacement ?? TabPlacement.Top;
OnPropertyChanged(nameof(TabPlacement));
UpdateTabPlacement();
break;
@@ -289,20 +274,24 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <summary>
/// Обрабатывает изменения коллекции вкладок.
/// </summary>
private void OnChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
UpdateTabHeaders();
}
/// <summary>
/// Обновляет положение панели вкладок.
/// </summary>
private void UpdateTabPlacement()
{
if (_rootGrid == null || _model == null) return;
// Очищаем все определения
_rootGrid.RowDefinitions.Clear();
_rootGrid.ColumnDefinitions.Clear();
// Настраиваем Grid в зависимости от позиции вкладок
switch (_model.TabPlacement)
{
case TabPlacement.Top:
@@ -330,21 +319,24 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
break;
}
// Обновляем позиции элементов
UpdateElementPositions();
}
/// <summary>
/// Обновляет ориентацию списка заголовков вкладок.
/// </summary>
/// <param name="orientation">Новая ориентация списка.</param>
private void UpdateHeaderListOrientation(Orientation orientation)
{
if (_tabHeaderList != null && _tabHeaderList.ItemsPanelRoot is ItemsPanelTemplate panelTemplate)
if (_tabHeaderList?.ItemsPanelRoot is StackPanel stackPanel)
{
if (panelTemplate.LoadContent() is StackPanel stackPanel)
{
stackPanel.Orientation = orientation;
}
stackPanel.Orientation = orientation;
}
}
/// <summary>
/// Обновляет позиции элементов в сетке.
/// </summary>
private void UpdateElementPositions()
{
if (_rootGrid == null || _tabHeaderList == null || _contentControl == null) return;
@@ -381,14 +373,15 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <summary>
/// Обновляет заголовки вкладок.
/// </summary>
private void UpdateTabHeaders()
{
if (_tabHeaderList == null || _model == null) return;
// Очищаем текущие элементы
_tabHeaderList.Items.Clear();
// Добавляем новые элементы
foreach (var content in _model.Children)
{
var item = CreateTabHeaderItem(content);
@@ -398,6 +391,11 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
UpdateSelectedTab();
}
/// <summary>
/// Создает элемент заголовка вкладки.
/// </summary>
/// <param name="content">Содержимое вкладки.</param>
/// <returns>Созданный элемент заголовка.</returns>
private ListBoxItem CreateTabHeaderItem(IDockContent content)
{
var item = new ListBoxItem
@@ -408,7 +406,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
VerticalContentAlignment = VerticalAlignment.Stretch
};
// Обработка клика для выбора вкладки
item.PointerPressed += (sender, e) =>
{
if (e.GetCurrentPoint(item).Properties.IsLeftButtonPressed)
@@ -418,26 +415,20 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
};
// Обработка клика на кнопке закрытия
if (_showCloseButtons && content.CanClose)
{
// Добавляем контекстное меню
item.ContextRequested += (sender, e) =>
{
ShowTabContextMenu(item, content, e);
};
}
return item;
}
/// <summary>
/// Создает содержимое заголовка вкладки.
/// </summary>
/// <param name="content">Содержимое вкладки.</param>
/// <returns>Созданное содержимое заголовка.</returns>
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,
@@ -447,7 +438,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
Grid.SetColumn(textBlock, 0);
grid.Children.Add(textBlock);
// Кнопка закрытия (если разрешено)
if (_showCloseButtons && content.CanClose)
{
var closeButton = new Button
@@ -475,11 +465,13 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
return grid;
}
/// <summary>
/// Обновляет выбранную вкладку.
/// </summary>
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)
@@ -488,13 +480,15 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
// Обновляем контент
if (_contentControl != null)
{
_contentControl.Content = _model.ActiveContent?.View;
}
}
/// <summary>
/// Обрабатывает изменение выбора вкладки.
/// </summary>
private void OnTabSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_tabHeaderList?.SelectedItem is ListBoxItem selectedItem &&
@@ -511,66 +505,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
// 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)
{
@@ -643,10 +577,9 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
/// <inheritdoc/>
public void ApplyTheme(IDockTheme theme)
{
// Применение темы к элементу
if (theme != null)
{
// TODO: Применить тему к стилям контрола
// TODO: Реализовать применение темы к стилям контрола
}
}
@@ -659,6 +592,9 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <summary>
/// Вызывает событие изменения свойства.
/// </summary>
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
@@ -676,11 +612,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
_tabHeaderList.SelectionChanged -= OnTabSelectionChanged;
}
// Отписываемся от событий указателя
this.PointerPressed -= OnPointerPressed;
this.PointerMoved -= OnPointerMoved;
this.PointerReleased -= OnPointerReleased;
_disposed = true;
GC.SuppressFinalize(this);
}