Доработан 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

@@ -2,7 +2,6 @@
using Lattice.Core.Docking.Engine;
using Lattice.Core.Docking.Models;
using Lattice.UI.Docking.Abstractions;
using Lattice.UI.Docking.Services;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
@@ -12,9 +11,22 @@ using System.Runtime.CompilerServices;
namespace Lattice.UI;
/// <summary>
/// Визуальный контрол для отображения группы разделения (сплиттера).
/// Реализует интерфейс <see cref="IDockGroupControl"/> для интеграции с системой докинга.
/// Визуальный контрол для отображения группы разделения (сплиттера) в системе докинга.
/// Реализует интерфейс <see cref="IDockGroupControl"/> для интеграции с системой докинга
/// и обеспечивает отображение двух дочерних элементов с разделителем между ними.
/// </summary>
/// <remarks>
/// <para>
/// Контрол <see cref="LatticeDockGroup"/> отвечает за визуальное представление узла
/// дерева компоновки, который разделяет доступное пространство между двумя дочерними
/// элементами. Поддерживает горизонтальное и вертикальное разделение с возможностью
/// изменения соотношения сторон через перетаскивание разделителя.
/// </para>
/// <para>
/// Контрол автоматически обновляет свое представление при изменении свойств модели
/// и обеспечивает двустороннюю привязку данных с объектом <see cref="DockGroup"/>.
/// </para>
/// </remarks>
public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
{
private readonly PropertyChangedEventHandler _modelPropertyChangedHandler;
@@ -24,18 +36,20 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
private ContentControl? _firstChildControl;
private ContentControl? _secondChildControl;
private LayoutManager? _layoutManager;
private DockDragDropService? _dragDropService;
private IDockContextManager? _contextManager;
private bool _isSelected;
private bool _isActive;
private bool _canDrag = true;
private bool _canDrop = true;
private double _splitRatio = 0.5;
private double _splitterSize = 4.0;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="LatticeDockGroup"/>.
/// </summary>
/// <remarks>
/// Конструктор устанавливает ключ стиля по умолчанию, инициализирует обработчик
/// изменений модели и подписывается на событие изменения контекста данных.
/// Созданный контрол готов к использованию после применения шаблона.
/// </remarks>
public LatticeDockGroup()
{
this.DefaultStyleKey = typeof(LatticeDockGroup);
@@ -43,7 +57,18 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
this.DataContextChanged += OnDataContextChanged;
}
/// <inheritdoc/>
/// <summary>
/// Получает или задает модель данных, связанную с этим контролом.
/// </summary>
/// <value>
/// Экземпляр <see cref="DockGroup"/>, представляющий узел разделения в дереве компоновки.
/// Может быть null, если контрол не связан с моделью.
/// </value>
/// <remarks>
/// При установке новой модели контрол автоматически подписывается на события
/// изменения свойств модели и обновляет свое визуальное представление.
/// При удалении модели происходит отписка от событий и очистка ресурсов.
/// </remarks>
public IDockElement? Model
{
get => _model;
@@ -57,7 +82,18 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <inheritdoc/>
/// <summary>
/// Получает или задает менеджер макета, к которому принадлежит этот контрол.
/// </summary>
/// <value>
/// Экземпляр <see cref="LayoutManager"/>, управляющий структурой док-системы.
/// Может быть null, если контрол не связан с менеджером макета.
/// </value>
/// <remarks>
/// Менеджер макета используется для выполнения операций с деревом компоновки,
/// таких как перемещение элементов, создание плавающих окон и управление
/// автоскрываемыми панелями.
/// </remarks>
public LayoutManager? LayoutManager
{
get => _layoutManager;
@@ -69,19 +105,17 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <inheritdoc/>
public IDockDragDropService? DragDropService
{
get => _dragDropService;
set
{
if (_dragDropService == value) return;
_dragDropService = value;
OnPropertyChanged(nameof(DragDropService));
}
}
/// <inheritdoc/>
/// <summary>
/// Получает или задает контекстный менеджер для этого контрола.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDockContextManager"/> или null, если менеджер не установлен.
/// </value>
/// <remarks>
/// Контекстный менеджер используется для отображения контекстных меню при щелчке
/// правой кнопкой мыши по контролу. Меню содержит команды, доступные для данного
/// элемента в текущем контексте.
/// </remarks>
public IDockContextManager? ContextManager
{
get => _contextManager;
@@ -93,7 +127,18 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <inheritdoc/>
/// <summary>
/// Получает или задает признак того, что контрол выбран.
/// </summary>
/// <value>
/// true, если контрол выбран; в противном случае false.
/// Значение по умолчанию: false.
/// </value>
/// <remarks>
/// Выделенный контрол обычно визуально отличается от других (например, имеет
/// выделенную границу или фон). В каждый момент времени может быть выделен
/// только один контрол в пределах контейнера.
/// </remarks>
public bool IsSelected
{
get => _isSelected;
@@ -105,7 +150,17 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <inheritdoc/>
/// <summary>
/// Получает или задает признак того, что контрол активен.
/// </summary>
/// <value>
/// true, если контрол активен; в противном случае false.
/// Значение по умолчанию: false.
/// </value>
/// <remarks>
/// Активный контрол получает фокус ввода и может обрабатывать команды клавиатуры.
/// Обычно соответствует последнему взаимодействию пользователя с элементом.
/// </remarks>
public bool IsActive
{
get => _isActive;
@@ -117,31 +172,20 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, 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/>
/// <summary>
/// Получает или задает ориентацию разделения группы.
/// </summary>
/// <value>
/// Направление разделения (горизонтальное или вертикальное).
/// </value>
/// <remarks>
/// Ориентация определяет, как расположены дочерние элементы относительно друг друга:
/// <list type="bullet">
/// <item><see cref="SplitDirection.Horizontal"/> - элементы расположены слева и справа</item>
/// <item><see cref="SplitDirection.Vertical"/> - элементы расположены сверху и снизу</item>
/// </list>
/// Изменение ориентации приводит к перестройке внутреннего макета контрола.
/// </remarks>
public SplitDirection Orientation
{
get => _model?.Orientation ?? SplitDirection.Horizontal;
@@ -155,7 +199,18 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <inheritdoc/>
/// <summary>
/// Получает или задает соотношение разделения между первым и вторым элементами.
/// </summary>
/// <value>
/// Значение от 0.0 до 1.0, где 0.5 означает равное разделение пространства.
/// Значение 0.0 отдает все пространство второму элементу, 1.0 - первому элементу.
/// </value>
/// <remarks>
/// Соотношение разделения определяет пропорции, в которых доступное пространство
/// распределяется между дочерними элементами. Изменение этого свойства приводит
/// к перестройке внутреннего макета и генерации события <see cref="SplitRatioChanged"/>.
/// </remarks>
public double SplitRatio
{
get => _splitRatio;
@@ -173,7 +228,17 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <inheritdoc/>
/// <summary>
/// Получает или задает размер разделителя в пикселях.
/// </summary>
/// <value>
/// Ширина разделителя в пикселях. Значение по умолчанию: 4.0.
/// </value>
/// <remarks>
/// Размер разделителя определяет область, доступную для перетаскивания пользователем
/// для изменения соотношения разделения. Увеличение размера облегчает взаимодействие,
/// но уменьшает полезное пространство для содержимого.
/// </remarks>
public double SplitterSize
{
get => _splitterSize;
@@ -187,19 +252,57 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <inheritdoc/>
/// <summary>
/// Получает контрол для первого дочернего элемента.
/// </summary>
/// <value>
/// Контрол, отображающий первый дочерний элемент, или null, если элемент не установлен.
/// </value>
/// <remarks>
/// Первый дочерний элемент занимает левую область при горизонтальной ориентации
/// или верхнюю область при вертикальной ориентации.
/// </remarks>
public IDockControl? FirstChild => _firstChildControl?.Content as IDockControl;
/// <inheritdoc/>
/// <summary>
/// Получает контрол для второго дочернего элемента.
/// </summary>
/// <value>
/// Контрол, отображающий второй дочерний элемент, или null, если элемент не установлен.
/// </value>
/// <remarks>
/// Второй дочерний элемент занимает правую область при горизонтальной ориентации
/// или нижнюю область при вертикальной ориентации.
/// </remarks>
public IDockControl? SecondChild => _secondChildControl?.Content as IDockControl;
/// <inheritdoc/>
/// <summary>
/// Происходит при изменении соотношения разделения между дочерними элементами.
/// </summary>
/// <remarks>
/// Событие генерируется при изменении свойства <see cref="SplitRatio"/>,
/// независимо от источника изменения (пользователь, программа или восстановление состояния).
/// Содержит информацию о новом соотношении и источнике изменения.
/// </remarks>
public event EventHandler<SplitRatioChangedEventArgs>? SplitRatioChanged;
/// <inheritdoc/>
/// <summary>
/// Происходит при изменении значения свойства.
/// </summary>
/// <remarks>
/// Событие реализует интерфейс <see cref="INotifyPropertyChanged"/> и используется
/// для уведомления системы привязки данных об изменениях свойств контрола.
/// </remarks>
public event PropertyChangedEventHandler? PropertyChanged;
/// <inheritdoc/>
/// <summary>
/// Вызывается при применении шаблона контрола.
/// </summary>
/// <remarks>
/// Метод получает ссылки на именованные части шаблона и инициализирует
/// внутренние структуры контрола. Вызывает обновление макета для корректного
/// отображения дочерних элементов.
/// </remarks>
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
@@ -211,11 +314,28 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
UpdateLayoutDefinitions();
}
/// <summary>
/// Обрабатывает изменение контекста данных контрола.
/// </summary>
/// <param name="sender">Источник события (контрол).</param>
/// <param name="args">Данные о изменении контекста.</param>
/// <remarks>
/// Метод автоматически устанавливает модель контрола на основе нового контекста данных,
/// если он является экземпляром <see cref="DockGroup"/>. Это позволяет использовать
/// привязку данных XAML для установки модели контрола.
/// </remarks>
private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
Model = args.NewValue as DockGroup;
}
/// <summary>
/// Присоединяет модель к контролу.
/// </summary>
/// <remarks>
/// Подписывается на события изменения свойств модели, устанавливает контекст данных
/// и инициализирует свойства контрола значениями из модели. Вызывает обновление макета.
/// </remarks>
private void AttachModel()
{
if (_model != null)
@@ -229,6 +349,13 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <summary>
/// Отсоединяет модель от контрола.
/// </summary>
/// <remarks>
/// Отписывается от событий изменения свойств модели, очищает контекст данных
/// и освобождает ресурсы, связанные с предыдущей моделью.
/// </remarks>
private void DetachModel()
{
if (_model != null)
@@ -238,6 +365,16 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <summary>
/// Обрабатывает изменения свойств модели.
/// </summary>
/// <param name="sender">Источник события (модель).</param>
/// <param name="e">Данные об изменении свойства.</param>
/// <remarks>
/// Реагирует на изменения ключевых свойств модели (Orientation, SplitRatio)
/// и обновляет соответствующие свойства и визуальное представление контрола.
/// Также уведомляет систему привязки данных об изменении свойств контрола.
/// </remarks>
private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
@@ -258,6 +395,14 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <summary>
/// Обновляет определения макета сетки на основе текущей ориентации и соотношения разделения.
/// </summary>
/// <remarks>
/// Метод перестраивает структуру строк и столбцов сетки в зависимости от ориентации
/// разделения и текущего соотношения между дочерними элементами. Обеспечивает
/// корректное позиционирование разделителя и дочерних контролов.
/// </remarks>
private void UpdateLayoutDefinitions()
{
if (_rootGrid == null || _model == null) return;
@@ -313,7 +458,15 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <inheritdoc/>
/// <summary>
/// Устанавливает дочерние контролы для отображения.
/// </summary>
/// <param name="firstChild">Контрол для первого элемента.</param>
/// <param name="secondChild">Контрол для второго элемента.</param>
/// <remarks>
/// Метод назначает контролы для визуального представления дочерних элементов группы.
/// После установки контролов обновляет макет для корректного отображения.
/// </remarks>
public void SetChildren(IDockControl? firstChild, IDockControl? secondChild)
{
if (_firstChildControl != null)
@@ -325,13 +478,27 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
UpdateLayoutDefinitions();
}
/// <inheritdoc/>
/// <summary>
/// Обновляет внешний вид контрола в соответствии с текущим состоянием модели.
/// </summary>
/// <remarks>
/// Вызывает перестройку макета сетки для синхронизации визуального представления
/// с текущими значениями свойств модели (ориентация, соотношение разделения).
/// </remarks>
public void Refresh()
{
UpdateLayoutDefinitions();
}
/// <inheritdoc/>
/// <summary>
/// Применяет указанную тему к контролу.
/// </summary>
/// <param name="theme">Тема для применения.</param>
/// <remarks>
/// Обновляет стили и параметры отображения контрола в соответствии с заданной темой.
/// В текущей реализации метод является заглушкой и должен быть расширен для
/// поддержки динамического изменения тем оформления.
/// </remarks>
public void ApplyTheme(IDockTheme theme)
{
// Применение темы к контролу
@@ -341,7 +508,14 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <inheritdoc/>
/// <summary>
/// Вызывается при изменении состояния модели для обновления UI.
/// </summary>
/// <param name="propertyName">Имя изменившегося свойства модели.</param>
/// <remarks>
/// Перенаправляет вызов в обработчик изменений модели, обеспечивая уведомление
/// контрола о конкретных изменениях в связанной модели данных.
/// </remarks>
public void OnModelPropertyChanged(string propertyName)
{
if (_model != null)
@@ -350,12 +524,27 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <summary>
/// Вызывает событие изменения свойства.
/// </summary>
/// <param name="propertyName">Имя изменившегося свойства.</param>
/// <remarks>
/// Используется для уведомления системы привязки данных об изменениях свойств
/// контрола. Если имя свойства не указано, автоматически определяется по имени
/// вызывающего члена.
/// </remarks>
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <inheritdoc/>
/// <summary>
/// Освобождает ресурсы, используемые этим экземпляром контрола.
/// </summary>
/// <remarks>
/// Выполняет отписку от событий модели, очистку ссылок и освобождение ресурсов.
/// После вызова этого метода контрол не должен использоваться.
/// </remarks>
public void Dispose()
{
if (!_disposed)

View File

@@ -1,6 +1,7 @@
using Lattice.Core.Docking.Abstractions;
using Lattice.Core.Docking.Engine;
using Lattice.Core.Docking.Models;
using Lattice.UI.Docking;
using Lattice.UI.Docking.Abstractions;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -39,7 +40,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
private bool _disposed;
private IDockElement? _model;
private LayoutManager? _layoutManager;
private IDockDragDropService? _dragDropService;
private IDockContextManager? _contextManager;
private bool _isSelected;
private bool _isActive;
@@ -109,27 +109,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
}
}
/// <summary>
/// Получает или задает сервис перетаскивания, используемый этим контролом.
/// </summary>
/// <value>
/// Реализация <see cref="IDockDragDropService"/> для обработки операций перетаскивания.
/// </value>
/// <remarks>
/// Сервис перетаскивания обеспечивает взаимодействие с системой drag-and-drop,
/// включая визуальную обратную связь и обработку событий.
/// </remarks>
public IDockDragDropService? DragDropService
{
get => _dragDropService;
set
{
if (_dragDropService == value) return;
_dragDropService = value;
OnPropertyChanged(nameof(DragDropService));
}
}
/// <summary>
/// Получает или задает контекстный менеджер для этого контрола.
/// </summary>

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);
}