Files
Lattice/Lattice.UI.Docking.WinUI/Controls/LatticeDockGroup.cs
2026-01-27 06:07:15 +03:00

557 lines
26 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 Lattice.UI.Docking.Abstractions;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Lattice.UI;
/// <summary>
/// Визуальный контрол для отображения группы разделения (сплиттера) в системе докинга.
/// Реализует интерфейс <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;
private bool _disposed;
private DockGroup? _model;
private Grid? _rootGrid;
private ContentControl? _firstChildControl;
private ContentControl? _secondChildControl;
private LayoutManager? _layoutManager;
private IDockContextManager? _contextManager;
private bool _isSelected;
private bool _isActive;
private double _splitRatio = 0.5;
private double _splitterSize = 4.0;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="LatticeDockGroup"/>.
/// </summary>
/// <remarks>
/// Конструктор устанавливает ключ стиля по умолчанию, инициализирует обработчик
/// изменений модели и подписывается на событие изменения контекста данных.
/// Созданный контрол готов к использованию после применения шаблона.
/// </remarks>
public LatticeDockGroup()
{
this.DefaultStyleKey = typeof(LatticeDockGroup);
_modelPropertyChangedHandler = OnModelPropertyChanged;
this.DataContextChanged += OnDataContextChanged;
}
/// <summary>
/// Получает или задает модель данных, связанную с этим контролом.
/// </summary>
/// <value>
/// Экземпляр <see cref="DockGroup"/>, представляющий узел разделения в дереве компоновки.
/// Может быть null, если контрол не связан с моделью.
/// </value>
/// <remarks>
/// При установке новой модели контрол автоматически подписывается на события
/// изменения свойств модели и обновляет свое визуальное представление.
/// При удалении модели происходит отписка от событий и очистка ресурсов.
/// </remarks>
public IDockElement? Model
{
get => _model;
set
{
if (_model == value) return;
DetachModel();
_model = value as DockGroup;
AttachModel();
OnPropertyChanged(nameof(Model));
}
}
/// <summary>
/// Получает или задает менеджер макета, к которому принадлежит этот контрол.
/// </summary>
/// <value>
/// Экземпляр <see cref="LayoutManager"/>, управляющий структурой док-системы.
/// Может быть null, если контрол не связан с менеджером макета.
/// </value>
/// <remarks>
/// Менеджер макета используется для выполнения операций с деревом компоновки,
/// таких как перемещение элементов, создание плавающих окон и управление
/// автоскрываемыми панелями.
/// </remarks>
public LayoutManager? LayoutManager
{
get => _layoutManager;
set
{
if (_layoutManager == value) return;
_layoutManager = value;
OnPropertyChanged(nameof(LayoutManager));
}
}
/// <summary>
/// Получает или задает контекстный менеджер для этого контрола.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDockContextManager"/> или null, если менеджер не установлен.
/// </value>
/// <remarks>
/// Контекстный менеджер используется для отображения контекстных меню при щелчке
/// правой кнопкой мыши по контролу. Меню содержит команды, доступные для данного
/// элемента в текущем контексте.
/// </remarks>
public IDockContextManager? ContextManager
{
get => _contextManager;
set
{
if (_contextManager == value) return;
_contextManager = value;
OnPropertyChanged(nameof(ContextManager));
}
}
/// <summary>
/// Получает или задает признак того, что контрол выбран.
/// </summary>
/// <value>
/// true, если контрол выбран; в противном случае false.
/// Значение по умолчанию: false.
/// </value>
/// <remarks>
/// Выделенный контрол обычно визуально отличается от других (например, имеет
/// выделенную границу или фон). В каждый момент времени может быть выделен
/// только один контрол в пределах контейнера.
/// </remarks>
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected == value) return;
_isSelected = value;
OnPropertyChanged(nameof(IsSelected));
}
}
/// <summary>
/// Получает или задает признак того, что контрол активен.
/// </summary>
/// <value>
/// true, если контрол активен; в противном случае false.
/// Значение по умолчанию: false.
/// </value>
/// <remarks>
/// Активный контрол получает фокус ввода и может обрабатывать команды клавиатуры.
/// Обычно соответствует последнему взаимодействию пользователя с элементом.
/// </remarks>
public bool IsActive
{
get => _isActive;
set
{
if (_isActive == value) return;
_isActive = value;
OnPropertyChanged(nameof(IsActive));
}
}
/// <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;
set
{
if (_model != null && _model.Orientation != value)
{
_model.Orientation = value;
UpdateLayoutDefinitions();
}
}
}
/// <summary>
/// Получает или задает соотношение разделения между первым и вторым элементами.
/// </summary>
/// <value>
/// Значение от 0.0 до 1.0, где 0.5 означает равное разделение пространства.
/// Значение 0.0 отдает все пространство второму элементу, 1.0 - первому элементу.
/// </value>
/// <remarks>
/// Соотношение разделения определяет пропорции, в которых доступное пространство
/// распределяется между дочерними элементами. Изменение этого свойства приводит
/// к перестройке внутреннего макета и генерации события <see cref="SplitRatioChanged"/>.
/// </remarks>
public double SplitRatio
{
get => _splitRatio;
set
{
if (Math.Abs(_splitRatio - value) > 0.001)
{
_splitRatio = value;
UpdateLayoutDefinitions();
OnPropertyChanged(nameof(SplitRatio));
SplitRatioChanged?.Invoke(this,
new SplitRatioChangedEventArgs(value, SplitRatioChangeSource.Programmatic));
}
}
}
/// <summary>
/// Получает или задает размер разделителя в пикселях.
/// </summary>
/// <value>
/// Ширина разделителя в пикселях. Значение по умолчанию: 4.0.
/// </value>
/// <remarks>
/// Размер разделителя определяет область, доступную для перетаскивания пользователем
/// для изменения соотношения разделения. Увеличение размера облегчает взаимодействие,
/// но уменьшает полезное пространство для содержимого.
/// </remarks>
public double SplitterSize
{
get => _splitterSize;
set
{
if (Math.Abs(_splitterSize - value) > 0.001)
{
_splitterSize = value;
OnPropertyChanged(nameof(SplitterSize));
}
}
}
/// <summary>
/// Получает контрол для первого дочернего элемента.
/// </summary>
/// <value>
/// Контрол, отображающий первый дочерний элемент, или null, если элемент не установлен.
/// </value>
/// <remarks>
/// Первый дочерний элемент занимает левую область при горизонтальной ориентации
/// или верхнюю область при вертикальной ориентации.
/// </remarks>
public IDockControl? FirstChild => _firstChildControl?.Content as IDockControl;
/// <summary>
/// Получает контрол для второго дочернего элемента.
/// </summary>
/// <value>
/// Контрол, отображающий второй дочерний элемент, или null, если элемент не установлен.
/// </value>
/// <remarks>
/// Второй дочерний элемент занимает правую область при горизонтальной ориентации
/// или нижнюю область при вертикальной ориентации.
/// </remarks>
public IDockControl? SecondChild => _secondChildControl?.Content as IDockControl;
/// <summary>
/// Происходит при изменении соотношения разделения между дочерними элементами.
/// </summary>
/// <remarks>
/// Событие генерируется при изменении свойства <see cref="SplitRatio"/>,
/// независимо от источника изменения (пользователь, программа или восстановление состояния).
/// Содержит информацию о новом соотношении и источнике изменения.
/// </remarks>
public event EventHandler<SplitRatioChangedEventArgs>? SplitRatioChanged;
/// <summary>
/// Происходит при изменении значения свойства.
/// </summary>
/// <remarks>
/// Событие реализует интерфейс <see cref="INotifyPropertyChanged"/> и используется
/// для уведомления системы привязки данных об изменениях свойств контрола.
/// </remarks>
public event PropertyChangedEventHandler? PropertyChanged;
/// <summary>
/// Вызывается при применении шаблона контрола.
/// </summary>
/// <remarks>
/// Метод получает ссылки на именованные части шаблона и инициализирует
/// внутренние структуры контрола. Вызывает обновление макета для корректного
/// отображения дочерних элементов.
/// </remarks>
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_rootGrid = GetTemplateChild("PART_Grid") as Grid;
_firstChildControl = GetTemplateChild("PART_First") as ContentControl;
_secondChildControl = GetTemplateChild("PART_Second") as ContentControl;
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)
{
_model.PropertyChanged += _modelPropertyChangedHandler;
this.DataContext = _model;
// Инициализируем свойства из модели
_splitRatio = _model.SplitRatio;
UpdateLayoutDefinitions();
}
}
/// <summary>
/// Отсоединяет модель от контрола.
/// </summary>
/// <remarks>
/// Отписывается от событий изменения свойств модели, очищает контекст данных
/// и освобождает ресурсы, связанные с предыдущей моделью.
/// </remarks>
private void DetachModel()
{
if (_model != null)
{
_model.PropertyChanged -= _modelPropertyChangedHandler;
this.DataContext = null;
}
}
/// <summary>
/// Обрабатывает изменения свойств модели.
/// </summary>
/// <param name="sender">Источник события (модель).</param>
/// <param name="e">Данные об изменении свойства.</param>
/// <remarks>
/// Реагирует на изменения ключевых свойств модели (Orientation, SplitRatio)
/// и обновляет соответствующие свойства и визуальное представление контрола.
/// Также уведомляет систему привязки данных об изменении свойств контрола.
/// </remarks>
private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(DockGroup.Orientation):
OnPropertyChanged(nameof(Orientation));
UpdateLayoutDefinitions();
break;
case nameof(DockGroup.SplitRatio):
if (_model != null)
{
_splitRatio = _model.SplitRatio;
OnPropertyChanged(nameof(SplitRatio));
UpdateLayoutDefinitions();
}
break;
}
}
/// <summary>
/// Обновляет определения макета сетки на основе текущей ориентации и соотношения разделения.
/// </summary>
/// <remarks>
/// Метод перестраивает структуру строк и столбцов сетки в зависимости от ориентации
/// разделения и текущего соотношения между дочерними элементами. Обеспечивает
/// корректное позиционирование разделителя и дочерних контролов.
/// </remarks>
private void UpdateLayoutDefinitions()
{
if (_rootGrid == null || _model == null) return;
_rootGrid.ColumnDefinitions.Clear();
_rootGrid.RowDefinitions.Clear();
if (_model.Orientation == SplitDirection.Horizontal)
{
// Горизонтальное разделение
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition
{ Width = new GridLength(_model.SplitRatio, GridUnitType.Star) });
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition
{ Width = GridLength.Auto });
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition
{ Width = new GridLength(1 - _model.SplitRatio, GridUnitType.Star) });
// Устанавливаем позиции элементов
if (_firstChildControl != null)
{
Grid.SetColumn(_firstChildControl, 0);
Grid.SetRow(_firstChildControl, 0);
}
if (_secondChildControl != null)
{
Grid.SetColumn(_secondChildControl, 2);
Grid.SetRow(_secondChildControl, 0);
}
}
else
{
// Вертикальное разделение
_rootGrid.RowDefinitions.Add(new RowDefinition
{ Height = new GridLength(_model.SplitRatio, GridUnitType.Star) });
_rootGrid.RowDefinitions.Add(new RowDefinition
{ Height = GridLength.Auto });
_rootGrid.RowDefinitions.Add(new RowDefinition
{ Height = new GridLength(1 - _model.SplitRatio, GridUnitType.Star) });
// Устанавливаем позиции элементов
if (_firstChildControl != null)
{
Grid.SetRow(_firstChildControl, 0);
Grid.SetColumn(_firstChildControl, 0);
}
if (_secondChildControl != null)
{
Grid.SetRow(_secondChildControl, 2);
Grid.SetColumn(_secondChildControl, 0);
}
}
}
/// <summary>
/// Устанавливает дочерние контролы для отображения.
/// </summary>
/// <param name="firstChild">Контрол для первого элемента.</param>
/// <param name="secondChild">Контрол для второго элемента.</param>
/// <remarks>
/// Метод назначает контролы для визуального представления дочерних элементов группы.
/// После установки контролов обновляет макет для корректного отображения.
/// </remarks>
public void SetChildren(IDockControl? firstChild, IDockControl? secondChild)
{
if (_firstChildControl != null)
_firstChildControl.Content = firstChild;
if (_secondChildControl != null)
_secondChildControl.Content = secondChild;
UpdateLayoutDefinitions();
}
/// <summary>
/// Обновляет внешний вид контрола в соответствии с текущим состоянием модели.
/// </summary>
/// <remarks>
/// Вызывает перестройку макета сетки для синхронизации визуального представления
/// с текущими значениями свойств модели (ориентация, соотношение разделения).
/// </remarks>
public void Refresh()
{
UpdateLayoutDefinitions();
}
/// <summary>
/// Применяет указанную тему к контролу.
/// </summary>
/// <param name="theme">Тема для применения.</param>
/// <remarks>
/// Обновляет стили и параметры отображения контрола в соответствии с заданной темой.
/// В текущей реализации метод является заглушкой и должен быть расширен для
/// поддержки динамического изменения тем оформления.
/// </remarks>
public void ApplyTheme(IDockTheme theme)
{
// Применение темы к контролу
if (theme != null)
{
// TODO: Реализовать применение темы к стилям контрола
}
}
/// <summary>
/// Вызывается при изменении состояния модели для обновления UI.
/// </summary>
/// <param name="propertyName">Имя изменившегося свойства модели.</param>
/// <remarks>
/// Перенаправляет вызов в обработчик изменений модели, обеспечивая уведомление
/// контрола о конкретных изменениях в связанной модели данных.
/// </remarks>
public void OnModelPropertyChanged(string propertyName)
{
if (_model != null)
{
OnModelPropertyChanged(_model, new PropertyChangedEventArgs(propertyName));
}
}
/// <summary>
/// Вызывает событие изменения свойства.
/// </summary>
/// <param name="propertyName">Имя изменившегося свойства.</param>
/// <remarks>
/// Используется для уведомления системы привязки данных об изменениях свойств
/// контрола. Если имя свойства не указано, автоматически определяется по имени
/// вызывающего члена.
/// </remarks>
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <summary>
/// Освобождает ресурсы, используемые этим экземпляром контрола.
/// </summary>
/// <remarks>
/// Выполняет отписку от событий модели, очистку ссылок и освобождение ресурсов.
/// После вызова этого метода контрол не должен использоваться.
/// </remarks>
public void Dispose()
{
if (!_disposed)
{
DetachModel();
_disposed = true;
GC.SuppressFinalize(this);
}
}
}