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
}