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;
}