using Lattice.Core.Docking.Abstractions; using Lattice.Core.DragDrop.Abstractions; using Lattice.Core.DragDrop.Enums; using Lattice.Core.DragDrop.Models; using Lattice.Core.Geometry; using System.ComponentModel; using System.Runtime.CompilerServices; namespace Lattice.Core.Docking.Models; /// /// Представляет узел дерева компоновки, который разделяет доступную область /// между двумя дочерними элементами. Этот класс является основным структурным /// элементом для создания сложных макетов с разделителями. /// /// /// /// реализует как (для /// возможности перетаскивания всей группы), так и /// (для возможности сброса на группу), что делает его полностью интегрированным /// в систему перетаскивания док-системы. /// /// /// Каждая группа содержит два дочерних элемента ( и /// ), которые могут быть либо другими группами (для /// создания вложенной структуры), либо листами () /// с контентом. Направление разделения определяется свойством /// . /// /// public class DockGroup : IDockElement, IDragSource, IDropTarget, INotifyPropertyChanged { /// /// Событие, возникающее при изменении значения свойства. /// public event PropertyChangedEventHandler? PropertyChanged; private double _splitRatio = 0.5; private string _id; /// /// Получает уникальный идентификатор группы. /// /// /// Строковый идентификатор, уникальный в пределах дерева компоновки. /// /// /// Идентификатор используется для сериализации/десериализации макета, /// поиска элементов и отслеживания изменений в дереве. /// public string Id { get => _id; internal set { if (_id != value) { _id = value; OnPropertyChanged(); } } } /// /// Получает или задает родительский элемент в иерархии дерева компоновки. /// /// /// Родительский элемент или null, если эта группа является корневой. /// /// /// Это свойство управляется системой компоновки при добавлении или /// удалении элементов из дерева. /// public IDockElement? Parent { get; set; } /// /// Получает или задает первый дочерний элемент (левую или верхнюю область). /// /// /// Элемент, занимающий первую часть разделенной области. /// /// /// Выбрасывается при попытке установить значение null. /// /// /// При установке нового значения автоматически обновляется свойство /// у дочернего элемента. /// public IDockElement First { get; set; } /// /// Получает или задает второй дочерний элемент (правую или нижнюю область). /// /// /// Элемент, занимающий вторую часть разделенной области. /// /// /// Выбрасывается при попытке установить значение null. /// /// /// При установке нового значения автоматически обновляется свойство /// у дочернего элемента. /// public IDockElement Second { get; set; } /// /// Получает или задает направление разделения данной группы. /// /// /// Значение перечисления , указывающее, /// как разделена область: горизонтально или вертикально. /// /// /// /// создает левую и правую области. /// /// /// создает верхнюю и нижнюю области. /// /// public SplitDirection Orientation { get; set; } /// /// Получает или задает соотношение разделения между первым и вторым элементами. /// /// /// Значение от 0.0 до 1.0, где: /// /// 0.0 - вся область принадлежит второму элементу /// 0.5 - область разделена поровну /// 1.0 - вся область принадлежит первому элементу /// /// /// /// Изменение этого свойства вызывает событие /// и может привести к перерисовке пользовательского интерфейса. /// public double SplitRatio { get => _splitRatio; set { if (Math.Abs(_splitRatio - value) > double.Epsilon) { _splitRatio = value; OnPropertyChanged(); } } } /// /// Получает или задает желаемую ширину элемента. /// /// /// Ширина в пикселях или относительных единицах. /// public double Width { get; set; } /// /// Получает или задает желаемую высоту элемента. /// /// /// Высота в пикселях или относительных единицах. /// public double Height { get; set; } /// /// Получает минимально допустимую ширину элемента. /// /// /// Минимальная ширина в пикселях, при которой элемент сохраняет функциональность. /// /// /// Для группы минимальная ширина вычисляется как сумма минимальных ширин /// дочерних элементов при горизонтальной ориентации или максимум минимальных /// ширин при вертикальной ориентации. /// public double MinWidth => Orientation == SplitDirection.Horizontal ? First.MinWidth + Second.MinWidth : Math.Max(First.MinWidth, Second.MinWidth); /// /// Получает минимально допустимую высоту элемента. /// /// /// Минимальная высота в пикселях, при которой элемент сохраняет функциональность. /// /// /// Для группы минимальная высота вычисляется как сумма минимальных высот /// дочерних элементов при вертикальной ориентации или максимум минимальных /// высот при горизонтальной ориентации. /// public double MinHeight => Orientation == SplitDirection.Vertical ? First.MinHeight + Second.MinHeight : Math.Max(First.MinHeight, Second.MinHeight); /// /// Инициализирует новый экземпляр класса . /// /// /// Первый дочерний элемент (левая или верхняя область). /// /// /// Второй дочерний элемент (правая или нижняя область). /// /// /// Направление разделения между дочерними элементами. /// /// /// Уникальный идентификатор группы. Если не указан, генерируется новый GUID. /// /// /// Выбрасывается, когда или /// равны null. /// /// /// Конструктор автоматически устанавливает свойство /// у дочерних элементов на текущую группу и генерирует уникальный идентификатор, /// если он не был предоставлен. /// public DockGroup(IDockElement first, IDockElement second, SplitDirection orientation, string? id = null) { First = first ?? throw new ArgumentNullException(nameof(first)); Second = second ?? throw new ArgumentNullException(nameof(second)); Orientation = orientation; Id = id ?? Guid.NewGuid().ToString(); First.Parent = this; Second.Parent = this; } /// /// Вызывает событие . /// /// /// Имя изменившегося свойства. Если не указано, определяется автоматически. /// protected void OnPropertyChanged([CallerMemberName] string? name = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } #region Реализация IDragSource /// /// Определяет, может ли группа начать операцию перетаскивания. /// /// /// При успешном возврате содержит информацию о перетаскивании; /// в противном случае — null. /// /// /// true, если группа может начать перетаскивание; в противном случае — false. /// /// /// /// Группа может быть перетащена только если она не является корневым /// элементом дерева (имеет родителя). /// /// /// При успешной проверке метод заполняет /// данными типа . /// /// public bool CanStartDrag(out DragInfo? dragInfo) { dragInfo = null; // DockGroup можно перетаскивать только если он не является корневым элементом if (Parent == null) return false; // Создаем данные для перетаскивания var data = new DockElementDragData { ElementId = Id, ElementType = GetType().Name, IsGroup = true }; dragInfo = new DragInfo(data, DragDropEffects.Move, Point.Zero, this); return true; } /// /// Начинает операцию перетаскивания для группы. /// /// /// Информация о перетаскивании, полученная из . /// /// /// Всегда возвращает true, так как группа не требует специальной подготовки /// для начала перетаскивания. /// /// /// Для этот метод не выполняет дополнительных действий, /// так как все необходимые данные уже содержатся в . /// public bool StartDrag(DragInfo dragInfo) { // DockGroup не требует дополнительной подготовки для перетаскивания return true; } /// /// Вызывается при завершении операции перетаскивания. /// /// /// Исходная информация о перетаскивании. /// /// /// Эффекты, которые были применены при сбросе. /// /// /// /// Этот метод вызывается после того, как операция перетаскивания была /// завершена (успешно или неуспешно). /// /// /// Для этот метод не выполняет действий, так как /// все изменения в структуре дерева уже обработаны . /// /// public void DragCompleted(DragInfo dragInfo, DragDropEffects effects) { // Если группа была перемещена, ничего не делаем - LayoutManager уже обработал изменение } /// /// Вызывается при отмене операции перетаскивания. /// /// /// Исходная информация о перетаскивании. /// /// /// Для отмена перетаскивания не требует специальных /// действий, так как структура дерева не была изменена. /// public void DragCancelled(DragInfo dragInfo) { // Отмена перетаскивания не требует действий } #endregion #region Реализация IDropTarget /// /// Определяет, может ли группа принять сбрасываемые данные. /// /// /// Информация о потенциальном сбросе. /// /// /// true, если группа может принять данные; в противном случае — false. /// /// /// /// Группа может принимать только данные типа /// для элементов док-системы ( или ). /// /// /// Группа не может принять сброс самой себя (проверяется по идентификатору). /// /// public bool CanAcceptDrop(DropInfo dropInfo) { if (dropInfo.Data is not DockElementDragData dragData) return false; // Нельзя сбросить элемент на самого себя if (dragData.ElementId == Id) return false; // Можно принимать только элементы док-системы return dragData.ElementType == nameof(DockGroup) || dragData.ElementType == nameof(DockLeaf); } /// /// Вызывается, когда перетаскиваемый объект находится над группой. /// /// /// Информация о текущем положении перетаскивания. /// /// /// /// Этот метод вызывается постоянно, пока пользователь перемещает объект /// над целью. Для группы он устанавливает предлагаемые эффекты в /// . /// /// /// Если группа может принять сброс, предлагается эффект перемещения; /// в противном случае эффекты не предлагаются. /// /// public void DragOver(DropInfo dropInfo) { if (CanAcceptDrop(dropInfo)) { dropInfo.SuggestedEffects = DragDropEffects.Move; } else { dropInfo.SuggestedEffects = DragDropEffects.None; } } /// /// Вызывается, когда пользователь сбрасывает данные на группу. /// /// /// Информация о сбросе. /// /// /// /// Для обработка сброса делегируется /// , поэтому метод просто помечает операцию /// как обработанную. /// /// /// Фактическое изменение структуры дерева выполняется менеджером макета /// на основе данных из . /// /// public void Drop(DropInfo dropInfo) { // Обработка сброса делегируется LayoutManager dropInfo.MarkAsHandled(); } /// /// Вызывается, когда перетаскиваемый объект покидает область группы. /// /// /// Для группы этот метод не выполняет действий, так как очистка визуальной /// обратной связи выполняется в UI-слое. /// public void DragLeave() { // Очистка визуальной обратной связи (будет выполнена в UI слое) } #endregion }