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.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.CompilerServices; namespace Lattice.Core.Docking.Models; /// /// Представляет конечный узел (лист) дерева компоновки, который непосредственно /// содержит коллекцию вкладок с контентом. Этот класс является контейнером для /// отображаемого пользователю содержимого. /// /// /// /// реализует интерфейсы , /// и , что позволяет ему: /// /// /// Управлять коллекцией вкладок /// Быть источником перетаскивания (как всего листа, так и отдельных вкладок) /// Принимать сброс других элементов или вкладок /// /// /// Лист является основным элементом, с которым взаимодействует пользователь /// при работе с документами или инструментальными панелями в IDE-подобных /// приложениях. /// /// public class DockLeaf : IDockContainer, INotifyPropertyChanged, IDragSource, IDropTarget { /// /// Событие, возникающее при изменении значения свойства. /// public event PropertyChangedEventHandler? PropertyChanged; private readonly ObservableCollection _items = new(); private IDockContent? _activeContent; private string _id; /// /// Получает уникальный идентификатор листа. /// /// /// Строковый идентификатор, уникальный в пределах дерева компоновки. /// public string Id { get => _id; internal set { if (_id != value) { _id = value; OnPropertyChanged(); } } } /// /// Получает или задает родительский элемент в иерархии дерева компоновки. /// /// /// Родительский элемент или null, если этот лист является корневым. /// public IDockElement? Parent { get; set; } /// /// Получает список вкладок, содержащихся в данном контейнере. /// /// /// Коллекция объектов, реализующих . /// /// /// Эта коллекция является наблюдаемой (ObservableCollection), что позволяет /// автоматически обновлять пользовательский интерфейс при добавлении или /// удалении вкладок. /// public IList Children => _items; /// /// Получает или задает активную (выбранную) вкладку в контейнере. /// /// /// Активная вкладка или null, если в контейнере нет вкладок. /// /// /// /// При установке нового значения проверяется, что вкладка действительно /// содержится в коллекции . /// /// /// Изменение этого свойства вызывает событие . /// /// public IDockContent? ActiveContent { get => _activeContent; set { if (value != null && !_items.Contains(value)) return; if (_activeContent != value) { _activeContent = value; OnPropertyChanged(); } } } /// /// Получает или задает желаемую ширину элемента. /// /// /// Ширина в пикселях или относительных единицах. /// public double Width { get; set; } /// /// Получает или задает желаемую высоту элемента. /// /// /// Высота в пикселях или относительных единицах. /// public double Height { get; set; } /// /// Получает или задает минимально допустимую ширину элемента. /// /// /// Минимальная ширина в пикселях. Значение по умолчанию: 100. /// public double MinWidth { get; set; } = 100; /// /// Получает или задает минимально допустимую высоту элемента. /// /// /// Минимальная высота в пикселях. Значение по умолчанию: 100. /// public double MinHeight { get; set; } = 100; /// /// Получает или задает положение полосы вкладок в контейнере. /// /// /// Значение перечисления , определяющее, /// где располагаются вкладки относительно содержимого. /// /// /// Поддерживаются все четыре стороны: верх, низ, лево, право. /// public TabPlacement TabPlacement { get; set; } = TabPlacement.Bottom; /// /// Инициализирует новый экземпляр класса . /// /// /// Уникальный идентификатор листа. Если не указан, генерируется новый GUID. /// /// /// Создает пустой лист с коллекцией вкладок и генерирует уникальный /// идентификатор, если он не был предоставлен. /// public DockLeaf(string? id = null) { _id = id ?? Guid.NewGuid().ToString(); } /// /// Вызывает событие . /// /// /// Имя изменившегося свойства. Если не указано, определяется автоматически. /// protected void OnPropertyChanged([CallerMemberName] string? name = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } /// /// Добавляет контент в контейнер и делает его активным. /// /// /// Контент для добавления. /// /// /// /// Если контент уже содержится в коллекции, он не добавляется повторно, /// но становится активным. /// /// /// Этот метод обновляет свойство и вызывает /// соответствующее событие изменения свойства. /// /// public void AddContent(IDockContent content) { if (!_items.Contains(content)) { _items.Add(content); } ActiveContent = content; } /// /// Удаляет контент из контейнера. /// /// /// Контент для удаления. /// /// /// /// Если удаляемый контент является активным, автоматически выбирается /// новая активная вкладка (следующая в списке или предыдущая, если удалена /// последняя). /// /// /// Если после удаления контейнер становится пустым, он может быть удален /// из дерева макета системой компоновки. /// /// public void RemoveContent(IDockContent content) { int index = _items.IndexOf(content); if (index == -1) return; _items.RemoveAt(index); if (ActiveContent == content) { if (_items.Count > 0) ActiveContent = _items[Math.Min(index, _items.Count - 1)]; else ActiveContent = null; } } #region Реализация IDragSource /// /// Определяет, может ли лист начать операцию перетаскивания. /// /// /// При успешном возврате содержит информацию о перетаскивании; /// в противном случае — null. /// /// /// true, если лист может начать перетаскивание; в противном случае — false. /// /// /// /// Лист может быть перетащен, если: /// /// /// Он имеет родителя (не является корневым) /// Или имеет хотя бы одну вкладку (не пустой) /// /// /// В зависимости от наличия активного контента создаются разные данные: /// /// /// /// Если есть активный контент - создается /// для перетаскивания конкретной вкладки /// /// /// Если нет активного контента - создается /// для перетаскивания всего листа /// /// /// public bool CanStartDrag(out DragInfo? dragInfo) { dragInfo = null; // DockLeaf можно перетаскивать if (Parent == null && Children.Count == 0) return false; // Не перетаскиваем пустые корневые листья object data; // Если есть активный контент, перетаскиваем контент, иначе перетаскиваем весь лист if (ActiveContent != null) { data = new ContentDragData { ElementId = Id, ContentId = ActiveContent.Id, ContentTitle = ActiveContent.Title, ContentType = ActiveContent.GetType().Name }; } else { data = new DockElementDragData { ElementId = Id, ElementType = GetType().Name, IsGroup = false, Width = Width, Height = Height }; } dragInfo = new DragInfo(data, DragDropEffects.Move | DragDropEffects.Copy, Point.Zero, this); return true; } /// /// Начинает операцию перетаскивания для листа. /// /// /// Информация о перетаскивании. /// /// /// Всегда возвращает true. /// /// /// Для этот метод не выполняет дополнительных действий. /// public bool StartDrag(DragInfo dragInfo) { // DockLeaf не требует дополнительной подготовки 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 DockElementDragData elementData) { // Можно принимать другие листы и группы return elementData.ElementType == nameof(DockLeaf) || elementData.ElementType == nameof(DockGroup); } else if (dropInfo.Data is ContentDragData contentData) { // Можно принимать контент для объединения вкладок return true; } return false; } /// /// Вызывается, когда перетаскиваемый объект находится над листом. /// /// /// Информация о текущем положении перетаскивания. /// /// /// /// В зависимости от типа данных устанавливаются разные предлагаемые эффекты: /// /// /// /// Для - эффект копирования (объединение вкладок) /// /// /// Для - эффект перемещения /// /// /// public void DragOver(DropInfo dropInfo) { if (CanAcceptDrop(dropInfo)) { if (dropInfo.Data is ContentDragData) { // Для контента предлагаем копирование (объединение вкладок) dropInfo.SuggestedEffects = DragDropEffects.Copy; } else { // Для элементов предлагаем перемещение dropInfo.SuggestedEffects = DragDropEffects.Move; } } else { dropInfo.SuggestedEffects = DragDropEffects.None; } } /// /// Вызывается, когда пользователь сбрасывает данные на лист. /// /// /// Информация о сбросе. /// /// /// Обработка сброса делегируется . /// public void Drop(DropInfo dropInfo) { // Обработка делегируется LayoutManager dropInfo.MarkAsHandled(); } /// /// Вызывается, когда перетаскиваемый объект покидает область листа. /// /// /// Очистка визуальной обратной связи выполняется в UI-слое. /// public void DragLeave() { // Очистка визуальной обратной связи } #endregion } /// /// Представляет данные для перетаскивания элементов док-системы (групп или листов). /// Используется при перетаскивании целых структурных элементов дерева компоновки. /// /// /// Этот класс сериализуется и передается между компонентами системы перетаскивания /// для идентификации перетаскиваемого элемента и его свойств. /// public class DockElementDragData { /// /// Получает или задает уникальный идентификатор элемента. /// /// /// Идентификатор элемента, соответствующий свойству . /// public string ElementId { get; set; } = string.Empty; /// /// Получает или задает тип элемента. /// /// /// Имя типа элемента (обычно "DockGroup" или "DockLeaf"). /// public string ElementType { get; set; } = string.Empty; /// /// Получает или задает значение, указывающее, является ли элемент группой. /// /// /// true, если элемент является ; false, если . /// public bool IsGroup { get; set; } /// /// Получает или задает идентификатор родительского элемента. /// /// /// Идентификатор родительского элемента или null, если элемент корневой. /// public string? ParentId { get; set; } /// /// Получает или задает ширину элемента. /// /// /// Текущая ширина элемента в пикселях. /// public double Width { get; set; } /// /// Получает или задает высоту элемента. /// /// /// Текущая высота элемента в пикселях. /// public double Height { get; set; } } /// /// Представляет данные для перетаскивания контента (вкладок). /// Используется при перетаскивании отдельных вкладок между контейнерами. /// /// /// Этот класс позволяет идентифицировать конкретную вкладку для операций /// объединения или перемещения между контейнерами. /// public class ContentDragData { /// /// Получает или задает идентификатор контейнера (листа), содержащего контент. /// /// /// Идентификатор , в котором находится перетаскиваемая вкладка. /// public string ElementId { get; set; } = string.Empty; /// /// Получает или задает уникальный идентификатор контента. /// /// /// Идентификатор контента, соответствующий свойству . /// public string ContentId { get; set; } = string.Empty; /// /// Получает или задает заголовок контента. /// /// /// Текст, отображаемый на вкладке. /// public string ContentTitle { get; set; } = string.Empty; /// /// Получает или задает тип контента. /// /// /// Имя типа контента (например, "TextEditor", "Toolbox", и т.д.). /// public string ContentType { get; set; } = string.Empty; /// /// Получает или задает значение, указывающее, можно ли закрыть контент. /// /// /// true, если контент можно закрыть; в противном случае — false. /// public bool CanClose { get; set; } = true; }