557 lines
26 KiB
C#
557 lines
26 KiB
C#
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);
|
||
}
|
||
}
|
||
} |