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; /// /// Визуальный контрол для отображения группы разделения (сплиттера) в системе докинга. /// Реализует интерфейс для интеграции с системой докинга /// и обеспечивает отображение двух дочерних элементов с разделителем между ними. /// /// /// /// Контрол отвечает за визуальное представление узла /// дерева компоновки, который разделяет доступное пространство между двумя дочерними /// элементами. Поддерживает горизонтальное и вертикальное разделение с возможностью /// изменения соотношения сторон через перетаскивание разделителя. /// /// /// Контрол автоматически обновляет свое представление при изменении свойств модели /// и обеспечивает двустороннюю привязку данных с объектом . /// /// 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; /// /// Инициализирует новый экземпляр класса . /// /// /// Конструктор устанавливает ключ стиля по умолчанию, инициализирует обработчик /// изменений модели и подписывается на событие изменения контекста данных. /// Созданный контрол готов к использованию после применения шаблона. /// public LatticeDockGroup() { this.DefaultStyleKey = typeof(LatticeDockGroup); _modelPropertyChangedHandler = OnModelPropertyChanged; this.DataContextChanged += OnDataContextChanged; } /// /// Получает или задает модель данных, связанную с этим контролом. /// /// /// Экземпляр , представляющий узел разделения в дереве компоновки. /// Может быть null, если контрол не связан с моделью. /// /// /// При установке новой модели контрол автоматически подписывается на события /// изменения свойств модели и обновляет свое визуальное представление. /// При удалении модели происходит отписка от событий и очистка ресурсов. /// public IDockElement? Model { get => _model; set { if (_model == value) return; DetachModel(); _model = value as DockGroup; AttachModel(); OnPropertyChanged(nameof(Model)); } } /// /// Получает или задает менеджер макета, к которому принадлежит этот контрол. /// /// /// Экземпляр , управляющий структурой док-системы. /// Может быть null, если контрол не связан с менеджером макета. /// /// /// Менеджер макета используется для выполнения операций с деревом компоновки, /// таких как перемещение элементов, создание плавающих окон и управление /// автоскрываемыми панелями. /// public LayoutManager? LayoutManager { get => _layoutManager; set { if (_layoutManager == value) return; _layoutManager = value; OnPropertyChanged(nameof(LayoutManager)); } } /// /// Получает или задает контекстный менеджер для этого контрола. /// /// /// Экземпляр или null, если менеджер не установлен. /// /// /// Контекстный менеджер используется для отображения контекстных меню при щелчке /// правой кнопкой мыши по контролу. Меню содержит команды, доступные для данного /// элемента в текущем контексте. /// public IDockContextManager? ContextManager { get => _contextManager; set { if (_contextManager == value) return; _contextManager = value; OnPropertyChanged(nameof(ContextManager)); } } /// /// Получает или задает признак того, что контрол выбран. /// /// /// true, если контрол выбран; в противном случае false. /// Значение по умолчанию: false. /// /// /// Выделенный контрол обычно визуально отличается от других (например, имеет /// выделенную границу или фон). В каждый момент времени может быть выделен /// только один контрол в пределах контейнера. /// public bool IsSelected { get => _isSelected; set { if (_isSelected == value) return; _isSelected = value; OnPropertyChanged(nameof(IsSelected)); } } /// /// Получает или задает признак того, что контрол активен. /// /// /// true, если контрол активен; в противном случае false. /// Значение по умолчанию: false. /// /// /// Активный контрол получает фокус ввода и может обрабатывать команды клавиатуры. /// Обычно соответствует последнему взаимодействию пользователя с элементом. /// public bool IsActive { get => _isActive; set { if (_isActive == value) return; _isActive = value; OnPropertyChanged(nameof(IsActive)); } } /// /// Получает или задает ориентацию разделения группы. /// /// /// Направление разделения (горизонтальное или вертикальное). /// /// /// Ориентация определяет, как расположены дочерние элементы относительно друг друга: /// /// - элементы расположены слева и справа /// - элементы расположены сверху и снизу /// /// Изменение ориентации приводит к перестройке внутреннего макета контрола. /// public SplitDirection Orientation { get => _model?.Orientation ?? SplitDirection.Horizontal; set { if (_model != null && _model.Orientation != value) { _model.Orientation = value; UpdateLayoutDefinitions(); } } } /// /// Получает или задает соотношение разделения между первым и вторым элементами. /// /// /// Значение от 0.0 до 1.0, где 0.5 означает равное разделение пространства. /// Значение 0.0 отдает все пространство второму элементу, 1.0 - первому элементу. /// /// /// Соотношение разделения определяет пропорции, в которых доступное пространство /// распределяется между дочерними элементами. Изменение этого свойства приводит /// к перестройке внутреннего макета и генерации события . /// 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)); } } } /// /// Получает или задает размер разделителя в пикселях. /// /// /// Ширина разделителя в пикселях. Значение по умолчанию: 4.0. /// /// /// Размер разделителя определяет область, доступную для перетаскивания пользователем /// для изменения соотношения разделения. Увеличение размера облегчает взаимодействие, /// но уменьшает полезное пространство для содержимого. /// public double SplitterSize { get => _splitterSize; set { if (Math.Abs(_splitterSize - value) > 0.001) { _splitterSize = value; OnPropertyChanged(nameof(SplitterSize)); } } } /// /// Получает контрол для первого дочернего элемента. /// /// /// Контрол, отображающий первый дочерний элемент, или null, если элемент не установлен. /// /// /// Первый дочерний элемент занимает левую область при горизонтальной ориентации /// или верхнюю область при вертикальной ориентации. /// public IDockControl? FirstChild => _firstChildControl?.Content as IDockControl; /// /// Получает контрол для второго дочернего элемента. /// /// /// Контрол, отображающий второй дочерний элемент, или null, если элемент не установлен. /// /// /// Второй дочерний элемент занимает правую область при горизонтальной ориентации /// или нижнюю область при вертикальной ориентации. /// public IDockControl? SecondChild => _secondChildControl?.Content as IDockControl; /// /// Происходит при изменении соотношения разделения между дочерними элементами. /// /// /// Событие генерируется при изменении свойства , /// независимо от источника изменения (пользователь, программа или восстановление состояния). /// Содержит информацию о новом соотношении и источнике изменения. /// public event EventHandler? SplitRatioChanged; /// /// Происходит при изменении значения свойства. /// /// /// Событие реализует интерфейс и используется /// для уведомления системы привязки данных об изменениях свойств контрола. /// public event PropertyChangedEventHandler? PropertyChanged; /// /// Вызывается при применении шаблона контрола. /// /// /// Метод получает ссылки на именованные части шаблона и инициализирует /// внутренние структуры контрола. Вызывает обновление макета для корректного /// отображения дочерних элементов. /// 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(); } /// /// Обрабатывает изменение контекста данных контрола. /// /// Источник события (контрол). /// Данные о изменении контекста. /// /// Метод автоматически устанавливает модель контрола на основе нового контекста данных, /// если он является экземпляром . Это позволяет использовать /// привязку данных XAML для установки модели контрола. /// private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args) { Model = args.NewValue as DockGroup; } /// /// Присоединяет модель к контролу. /// /// /// Подписывается на события изменения свойств модели, устанавливает контекст данных /// и инициализирует свойства контрола значениями из модели. Вызывает обновление макета. /// private void AttachModel() { if (_model != null) { _model.PropertyChanged += _modelPropertyChangedHandler; this.DataContext = _model; // Инициализируем свойства из модели _splitRatio = _model.SplitRatio; UpdateLayoutDefinitions(); } } /// /// Отсоединяет модель от контрола. /// /// /// Отписывается от событий изменения свойств модели, очищает контекст данных /// и освобождает ресурсы, связанные с предыдущей моделью. /// private void DetachModel() { if (_model != null) { _model.PropertyChanged -= _modelPropertyChangedHandler; this.DataContext = null; } } /// /// Обрабатывает изменения свойств модели. /// /// Источник события (модель). /// Данные об изменении свойства. /// /// Реагирует на изменения ключевых свойств модели (Orientation, SplitRatio) /// и обновляет соответствующие свойства и визуальное представление контрола. /// Также уведомляет систему привязки данных об изменении свойств контрола. /// 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; } } /// /// Обновляет определения макета сетки на основе текущей ориентации и соотношения разделения. /// /// /// Метод перестраивает структуру строк и столбцов сетки в зависимости от ориентации /// разделения и текущего соотношения между дочерними элементами. Обеспечивает /// корректное позиционирование разделителя и дочерних контролов. /// 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); } } } /// /// Устанавливает дочерние контролы для отображения. /// /// Контрол для первого элемента. /// Контрол для второго элемента. /// /// Метод назначает контролы для визуального представления дочерних элементов группы. /// После установки контролов обновляет макет для корректного отображения. /// public void SetChildren(IDockControl? firstChild, IDockControl? secondChild) { if (_firstChildControl != null) _firstChildControl.Content = firstChild; if (_secondChildControl != null) _secondChildControl.Content = secondChild; UpdateLayoutDefinitions(); } /// /// Обновляет внешний вид контрола в соответствии с текущим состоянием модели. /// /// /// Вызывает перестройку макета сетки для синхронизации визуального представления /// с текущими значениями свойств модели (ориентация, соотношение разделения). /// public void Refresh() { UpdateLayoutDefinitions(); } /// /// Применяет указанную тему к контролу. /// /// Тема для применения. /// /// Обновляет стили и параметры отображения контрола в соответствии с заданной темой. /// В текущей реализации метод является заглушкой и должен быть расширен для /// поддержки динамического изменения тем оформления. /// public void ApplyTheme(IDockTheme theme) { // Применение темы к контролу if (theme != null) { // TODO: Реализовать применение темы к стилям контрола } } /// /// Вызывается при изменении состояния модели для обновления UI. /// /// Имя изменившегося свойства модели. /// /// Перенаправляет вызов в обработчик изменений модели, обеспечивая уведомление /// контрола о конкретных изменениях в связанной модели данных. /// 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)); } /// /// Освобождает ресурсы, используемые этим экземпляром контрола. /// /// /// Выполняет отписку от событий модели, очистку ссылок и освобождение ресурсов. /// После вызова этого метода контрол не должен использоваться. /// public void Dispose() { if (!_disposed) { DetachModel(); _disposed = true; GC.SuppressFinalize(this); } } }