DragAndDrop core

This commit is contained in:
FrigaT
2026-01-18 16:33:35 +03:00
parent 9ea82af329
commit 79bdd8bc62
229 changed files with 21214 additions and 2494 deletions

View File

@@ -0,0 +1,8 @@
namespace Lattice.Core.Docking.Abstractions;
public interface IDockCommand : System.Windows.Input.ICommand
{
string Name { get; }
string Icon { get; }
string GestureText { get; }
}

View File

@@ -0,0 +1,24 @@
using Lattice.Core.Docking.Models;
namespace Lattice.Core.Docking.Abstractions;
/// <summary>
/// Интерфейс для элементов (листьев дерева), которые физически содержат внутри себя коллекцию вкладок.
/// </summary>
public interface IDockContainer : IDockElement
{
/// <summary> Список вкладок, находящихся в данном контейнере. </summary>
IList<IDockContent> Children { get; }
/// <summary> Ссылка на текущую выбранную и отображаемую вкладку. </summary>
IDockContent? ActiveContent { get; set; }
/// <summary> Добавляет контент в контейнер и делает его активным. </summary>
void AddContent(IDockContent content);
/// <summary> Удаляет контент. Если Children становится пустым, контейнер может быть удален из дерева макета. </summary>
void RemoveContent(IDockContent content);
/// <summary> Положение вкладок в интерфейсе. </summary>
TabPlacement TabPlacement { get; set; }
}

View File

@@ -0,0 +1,25 @@
namespace Lattice.Core.Docking.Abstractions;
/// <summary>
/// Описывает объект содержимого (вкладку), который может быть размещен внутри IDockContainer.
/// </summary>
public interface IDockContent
{
/// <summary> Уникальный идентификатор контента (например, путь к файлу или ID инструмента). </summary>
string Id { get; }
/// <summary> Заголовок, отображаемый пользователю в интерфейсе (на вкладке). </summary>
string Title { get; }
/// <summary>
/// Сам визуальный элемент (например, Microsoft.UI.Xaml.UIElement).
/// Lattice просто отображает этот объект в теле вкладки.
/// </summary>
object View { get; set; }
/// <summary> Флаг, определяющий доступность кнопки закрытия для пользователя. </summary>
bool CanClose { get; }
/// <summary> Вызывается системой при попытке закрытия контента. Возвращает true, если закрытие разрешено. </summary>
bool OnClosing();
}

View File

@@ -0,0 +1,25 @@
namespace Lattice.Core.Docking.Abstractions;
/// <summary>
/// Базовый интерфейс для любого элемента, который может быть частью дерева компоновки Lattice.
/// </summary>
public interface IDockElement
{
/// <summary> Уникальный идентификатор элемента. </summary>
string Id { get; }
/// <summary> Родительский элемент в иерархии. Если null — элемент является корневым. </summary>
IDockElement? Parent { get; set; }
/// <summary> Желаемая ширина элемента в относительных или абсолютных единицах. </summary>
double Width { get; set; }
/// <summary> Желаемая высота элемента в относительных или абсолютных единицах. </summary>
double Height { get; set; }
/// <summary> Минимально допустимая ширина, при которой элемент сохраняет функциональность. </summary>
double MinWidth { get; }
/// <summary> Минимально допустимая высота, при которой элемент сохраняет функциональность. </summary>
double MinHeight { get; }
}

View File

@@ -0,0 +1,19 @@
using Lattice.Core.DragDrop.Abstractions;
namespace Lattice.Core.Docking.Abstractions;
/// <summary>
/// Расширяет интерфейс элемента док-системы для поддержки операций перетаскивания.
/// </summary>
public interface IDockElementDragSource : IDockElement, IDragSource
{
/// <summary>
/// Получает или устанавливает признак того, что элемент можно перетаскивать.
/// </summary>
bool CanDrag { get; set; }
/// <summary>
/// Получает тип данных для перетаскивания этого элемента.
/// </summary>
string DragDataType { get; }
}

View File

@@ -0,0 +1,19 @@
using Lattice.Core.DragDrop.Abstractions;
namespace Lattice.Core.Docking.Abstractions;
/// <summary>
/// Расширяет интерфейс элемента док-системы для возможности быть целью сброса.
/// </summary>
public interface IDockElementDropTarget : IDockElement, IDropTarget
{
/// <summary>
/// Получает или устанавливает признак того, что элемент может принимать сброс.
/// </summary>
bool CanDrop { get; set; }
/// <summary>
/// Получает типы данных, которые может принимать элемент.
/// </summary>
IEnumerable<string> AcceptableDropTypes { get; }
}

View File

@@ -0,0 +1,76 @@
using Lattice.Core.Docking.Models;
using Lattice.Core.Geometry;
namespace Lattice.Core.Docking.Abstractions;
/// <summary>
/// Предоставляет абстракцию для операции перетаскивания в док-системе.
/// Эта абстракция позволяет отделить логику перетаскивания от конкретной UI-платформы.
/// </summary>
public interface IDragService
{
/// <summary>
/// Начинает операцию перетаскивания указанного элемента.
/// </summary>
/// <param name="element">Элемент для перетаскивания.</param>
/// <param name="visualFeedback">Визуальная обратная связь (зависит от платформы).</param>
void StartDrag(IDockElement element, object? visualFeedback = null);
/// <summary>
/// Обновляет позицию перетаскивания.
/// </summary>
/// <param name="x">Координата X.</param>
/// <param name="y">Координата Y.</param>
void UpdateDrag(double x, double y);
/// <summary>
/// Завершает операцию перетаскивания.
/// </summary>
/// <param name="x">Координата X завершения.</param>
/// <param name="y">Координата Y завершения.</param>
void EndDrag(double x, double y);
/// <summary>
/// Отменяет операцию перетаскивания.
/// </summary>
void CancelDrag();
}
/// <summary>
/// Представляет область для сброса при операции перетаскивания.
/// </summary>
public class DropArea
{
/// <summary>
/// Целевой элемент для сброса.
/// </summary>
public IDockElement Target { get; set; }
/// <summary>
/// Позиция сброса относительно цели.
/// </summary>
public DockPosition Position { get; set; }
/// <summary>
/// Границы области в экранных координатах.
/// </summary>
public Rect Bounds { get; set; }
/// <summary>
/// Видимость области (для анимации).
/// </summary>
public double Visibility { get; set; } = 0.0;
/// <summary>
/// Инициализирует новый экземпляр области сброса.
/// </summary>
/// <param name="target">Целевой элемент.</param>
/// <param name="position">Позиция сброса.</param>
/// <param name="bounds">Границы области.</param>
public DropArea(IDockElement target, DockPosition position, Rect bounds)
{
Target = target;
Position = position;
Bounds = bounds;
}
}

View File

@@ -0,0 +1,89 @@
using Lattice.Core.Docking.Abstractions;
using Lattice.Core.Docking.Models;
namespace Lattice.Core.Docking.Engine;
/// <summary>
/// Статический движок для манипуляции иерархией дерева компоновки.
/// Содержит чистые алгоритмы трансформации графа.
/// </summary>
public static class DockOperations
{
/// <summary>
/// Извлекает элемент из дерева. Если родительская группа остается с одним ребенком,
/// она удаляется, а ребенок занимает её место.
/// </summary>
/// <param name="element">Элемент для удаления.</param>
/// <param name="root">Текущий корень дерева.</param>
/// <returns>Новый корень дерева после оптимизации.</returns>
public static IDockElement? Remove(IDockElement element, IDockElement root)
{
if (element == root) return null;
var parent = element.Parent as DockGroup;
if (parent == null) return root;
// Определяем "выжившего" соседа
var sibling = (parent.First == element) ? parent.Second : parent.First;
var grandParent = parent.Parent as DockGroup;
if (grandParent != null)
{
// Переподключаем соседа напрямую к дедушке
if (grandParent.First == parent) grandParent.First = sibling;
else grandParent.Second = sibling;
sibling.Parent = grandParent;
return root;
}
// Если дедушки нет, сосед становится новым корнем
sibling.Parent = null;
return sibling;
}
/// <summary>
/// Вставляет элемент в дерево, создавая новую группу разделения или объединяя контент.
/// </summary>
public static IDockElement Insert(IDockElement target, IDockElement source, DockPosition pos, IDockElement root)
{
// Случай 1: Объединение вкладок в центре
if (pos == DockPosition.Center)
{
if (target is IDockContainer targetContainer && source is IDockContainer sourceContainer)
{
var items = new List<IDockContent>(sourceContainer.Children);
foreach (var item in items)
{
sourceContainer.RemoveContent(item);
targetContainer.AddContent(item);
}
}
return root;
}
// Случай 2: Разделение (Split)
var direction = (pos == DockPosition.Left || pos == DockPosition.Right)
? SplitDirection.Horizontal : SplitDirection.Vertical;
bool sourceIsFirst = (pos == DockPosition.Left || pos == DockPosition.Top);
var oldParent = target.Parent;
// Создаем новую группу. Источник и цель делят пространство 50/50
var newGroup = sourceIsFirst
? new DockGroup(source, target, direction) { SplitRatio = 0.5 }
: new DockGroup(target, source, direction) { SplitRatio = 0.5 };
if (oldParent is DockGroup gp)
{
if (gp.First == target) gp.First = newGroup;
else gp.Second = newGroup;
newGroup.Parent = gp;
return root;
}
newGroup.Parent = null;
return newGroup; // Новая группа стала корнем
}
}

View File

@@ -0,0 +1,369 @@
using Lattice.Core.Docking.Abstractions;
using Lattice.Core.Docking.Models;
using System.Collections.ObjectModel;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Lattice.Serialization.Docking")]
namespace Lattice.Core.Docking.Engine;
/// <summary>
/// Расширенный менеджер макета, поддерживающий автоскрываемые панели, группы документов
/// и расширенные операции управления макетом.
/// </summary>
/// <remarks>
/// Этот класс является центральным координатором всей док-системы, управляя деревом компоновки,
/// плавающими окнами, автоскрываемыми панелями и предоставляя API для манипуляции макетом.
/// </remarks>
public class LayoutManager
{
private readonly ObservableCollection<AutoHidePanel> _autoHidePanels = new();
/// <summary>
/// Корневой элемент главного окна IDE.
/// </summary>
public IDockElement? Root { get; internal set; }
/// <summary>
/// Список активных плавающих окон.
/// </summary>
public List<DockWindow> FloatingWindows { get; } = new();
/// <summary>
/// Коллекция автоскрываемых панелей.
/// </summary>
public ReadOnlyObservableCollection<AutoHidePanel> AutoHidePanels { get; }
/// <summary>
/// Реестр типов контента (опционально).
/// </summary>
public Services.ContentRegistry? ContentRegistry { get; set; }
/// <summary>
/// Уведомляет UI, что структура дерева изменилась.
/// </summary>
public event Action? LayoutUpdated;
/// <summary>
/// Уведомляет об изменении в коллекции автоскрываемых панелей.
/// </summary>
public event EventHandler? AutoHidePanelsChanged;
/// <summary>
/// Событие, возникающее при операции перетаскивания элемента.
/// </summary>
public event EventHandler<DragDropEventArgs>? DragDropOperation;
/// <summary>
/// Инициализирует новый экземпляр менеджера макета.
/// </summary>
public LayoutManager()
{
AutoHidePanels = new ReadOnlyObservableCollection<AutoHidePanel>(_autoHidePanels);
}
/// <summary>
/// Добавляет автоскрываемую панель.
/// </summary>
/// <param name="content">Содержимое панели.</param>
/// <param name="side">Сторона для прикрепления.</param>
/// <returns>Созданная автоскрываемая панель.</returns>
public AutoHidePanel AddAutoHidePanel(IDockContent content, DockSide side)
{
var panel = new AutoHidePanel(content, side);
_autoHidePanels.Add(panel);
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
return panel;
}
/// <summary>
/// Удаляет автоскрываемую панель.
/// </summary>
/// <param name="panel">Панель для удаления.</param>
public void RemoveAutoHidePanel(AutoHidePanel panel)
{
if (_autoHidePanels.Remove(panel))
{
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
}
}
/// <summary>
/// Создает документ из зарегистрированного типа контента.
/// </summary>
/// <param name="contentTypeId">Идентификатор типа контента.</param>
/// <param name="id">Уникальный идентификатор документа.</param>
/// <returns>Созданный контент или null, если ContentRegistry не установлен.</returns>
public IDockContent? CreateDocument(string contentTypeId, string id)
{
if (ContentRegistry == null || !ContentRegistry.IsRegistered(contentTypeId))
return null;
return ContentRegistry.CreateContent(contentTypeId, id);
}
/// <summary>
/// Основной метод перемещения элементов в макете.
/// </summary>
/// <param name="source">Что перетаскиваем.</param>
/// <param name="target">Куда приземляем.</param>
/// <param name="position">Позиция относительно цели.</param>
/// <param name="asDocument">
/// Если true, контент будет добавлен как документ в центральную область.
/// </param>
public void Move(IDockElement source, IDockElement? target, DockPosition position, bool asDocument = false)
{
if (source == target) return;
// 1. Удаляем источник из текущего местоположения
bool sourceRemoved = false;
if (Root != null && IsDescendantOf(source, Root))
{
Root = DockOperations.Remove(source, Root);
sourceRemoved = true;
}
else
{
sourceRemoved = RemoveFromFloatingWindows(source);
}
if (!sourceRemoved)
{
// Проверяем автоскрываемые панели
var autoHidePanel = _autoHidePanels.FirstOrDefault(p => p.Content == source);
if (autoHidePanel != null)
{
_autoHidePanels.Remove(autoHidePanel);
sourceRemoved = true;
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
}
}
if (!sourceRemoved) return;
// 2. Вставляем в цель
if (target == null)
{
FloatingWindows.Add(new DockWindow { Root = source as IDockElement });
}
else
{
if (IsDescendantOf(target, Root))
{
Root = DockOperations.Insert(target, source, position, Root!);
}
else
{
InsertIntoFloatingWindow(target, source, position);
}
}
LayoutUpdated?.Invoke();
}
private bool RemoveFromFloatingWindows(IDockElement element)
{
foreach (var win in FloatingWindows.ToArray())
{
if (win.Root != null && IsDescendantOf(element, win.Root))
{
win.Root = DockOperations.Remove(element, win.Root);
if (win.Root == null)
FloatingWindows.Remove(win);
return true;
}
}
return false;
}
private void InsertIntoFloatingWindow(IDockElement target, IDockElement source, DockPosition position)
{
foreach (var win in FloatingWindows)
{
if (win.Root != null && IsDescendantOf(target, win.Root))
{
win.Root = DockOperations.Insert(target, source, position, win.Root);
return;
}
}
}
private bool IsDescendantOf(IDockElement element, IDockElement ancestor)
{
if (element == ancestor) return true;
if (ancestor is DockGroup group)
return IsDescendantOf(element, group.First) || IsDescendantOf(element, group.Second);
return false;
}
/// <summary> Поиск элемента по ID во всех окнах. </summary>
public IDockElement? FindById(string id)
{
var found = FindRecursive(Root, id);
if (found != null) return found;
foreach (var win in FloatingWindows)
{
found = FindRecursive(win.Root, id);
if (found != null) return found;
}
return null;
}
private IDockElement? FindRecursive(IDockElement? node, string id)
{
if (node == null || node.Id == id) return node;
if (node is DockGroup g) return FindRecursive(g.First, id) ?? FindRecursive(g.Second, id);
return null;
}
/// <summary>
/// Сбрасывает макет к состоянию по умолчанию.
/// </summary>
public void Reset()
{
Root = null;
FloatingWindows.Clear();
_autoHidePanels.Clear();
LayoutUpdated?.Invoke();
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Обрабатывает операцию перетаскивания между элементами.
/// </summary>
/// <param name="source">Источник перетаскивания.</param>
/// <param name="target">Цель сброса.</param>
/// <param name="position">Позиция сброса относительно цели.</param>
/// <param name="data">Данные перетаскивания.</param>
/// <returns>true, если операция успешно выполнена; иначе false.</returns>
public bool HandleDragDrop(IDockElement source, IDockElement target,
DockPosition position, object data)
{
try
{
if (source == target)
return false;
// Определяем тип операции на основе данных
if (data is ContentDragData contentData)
{
return HandleContentDragDrop(contentData, target, position);
}
else if (data is DockElementDragData elementData)
{
return HandleElementDragDrop(elementData, target, position);
}
return false;
}
catch (Exception ex)
{
DragDropOperation?.Invoke(this, new DragDropEventArgs(
source, target, position, false, ex.Message));
return false;
}
}
private bool HandleContentDragDrop(ContentDragData data, IDockElement target, DockPosition position)
{
// Находим исходный контейнер с контентом
var sourceContainer = FindElementById(data.ElementId) as IDockContainer;
if (sourceContainer == null)
return false;
// Находим контент
var content = sourceContainer.Children.FirstOrDefault(c => c.Id == data.ContentId);
if (content == null)
return false;
if (target is IDockContainer targetContainer && position == DockPosition.Center)
{
// Объединение вкладок
sourceContainer.RemoveContent(content);
targetContainer.AddContent(content);
DragDropOperation?.Invoke(this, new DragDropEventArgs(
sourceContainer as IDockElement ?? sourceContainer as IDockElement,
target, position, true, "Content merged"));
return true;
}
return false;
}
private bool HandleElementDragDrop(DockElementDragData data, IDockElement target, DockPosition position)
{
// Находим перетаскиваемый элемент
var sourceElement = FindElementById(data.ElementId);
if (sourceElement == null)
return false;
// Выполняем перемещение
Move(sourceElement, target, position);
DragDropOperation?.Invoke(this, new DragDropEventArgs(
sourceElement, target, position, true, "Element moved"));
return true;
}
/// <summary>
/// Находит элемент по идентификатору.
/// </summary>
public IDockElement? FindElementById(string id)
{
return FindElementByIdRecursive(Root, id) ??
FloatingWindows.Select(w => FindElementByIdRecursive(w.Root, id))
.FirstOrDefault(result => result != null);
}
private IDockElement? FindElementByIdRecursive(IDockElement? element, string id)
{
if (element == null) return null;
if (element.Id == id) return element;
if (element is DockGroup group)
{
return FindElementByIdRecursive(group.First, id) ??
FindElementByIdRecursive(group.Second, id);
}
return null;
}
}
/// <summary>
/// Аргументы события операции перетаскивания.
/// </summary>
public class DragDropEventArgs : EventArgs
{
/// <summary> Источник перетаскивания. </summary>
public IDockElement Source { get; }
/// <summary> Цель сброса. </summary>
public IDockElement Target { get; }
/// <summary> Позиция сброса. </summary>
public DockPosition Position { get; }
/// <summary> Показывает, была ли операция успешной. </summary>
public bool Success { get; }
/// <summary> Сообщение о результате операции. </summary>
public string Message { get; }
/// <summary> Время выполнения операции. </summary>
public DateTime Timestamp { get; }
public DragDropEventArgs(IDockElement source, IDockElement target,
DockPosition position, bool success, string message)
{
Source = source;
Target = target;
Position = position;
Success = success;
Message = message;
Timestamp = DateTime.UtcNow;
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Lattice.Core.Geometry\Lattice.Core.Geometry.csproj" />
<ProjectReference Include="..\Lattice.Core.DragDrop\Lattice.Core.DragDrop.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,114 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Lattice.Core.Docking.Models;
/// <summary>
/// Представляет автоскрываемую панель, которая может быть прикреплена к одной из сторон окна.
/// Автоскрываемые панели скрываются, оставляя только заголовок, и появляются при наведении курсора.
/// </summary>
/// <remarks>
/// Автоскрываемые панели являются ключевым элементом интерфейса современных IDE,
/// позволяя экономить пространство экрана при сохранении быстрого доступа к инструментам.
/// </remarks>
public class AutoHidePanel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? name = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
private bool _isVisible = false;
private double _slideOffset = 0;
/// <summary>
/// Уникальный идентификатор автоскрываемой панели.
/// </summary>
public string Id { get; } = Guid.NewGuid().ToString();
/// <summary>
/// Содержимое панели.
/// </summary>
public Abstractions.IDockContent Content { get; set; }
/// <summary>
/// Сторона окна, к которой прикреплена панель.
/// </summary>
public DockSide Side { get; set; }
/// <summary>
/// Ширина панели (для левой/правой сторон) или высота (для верхней/нижней сторон).
/// </summary>
public double Size { get; set; } = 300;
/// <summary>
/// Признак видимости панели.
/// </summary>
public bool IsVisible
{
get => _isVisible;
set
{
if (_isVisible != value)
{
_isVisible = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Смещение для анимации выезда/заезда панели (0-1).
/// </summary>
public double SlideOffset
{
get => _slideOffset;
set
{
if (Math.Abs(_slideOffset - value) > 0.001)
{
_slideOffset = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Заголовок панели (обычно берется из содержимого).
/// </summary>
public string Title => Content?.Title ?? "Auto-hide Panel";
/// <summary>
/// Инициализирует новый экземпляр автоскрываемой панели.
/// </summary>
/// <param name="content">Содержимое панели.</param>
/// <param name="side">Сторона окна для прикрепления.</param>
public AutoHidePanel(Abstractions.IDockContent content, DockSide side)
{
Content = content ?? throw new ArgumentNullException(nameof(content));
Side = side;
}
/// <summary>
/// Переключает видимость панели.
/// </summary>
public void Toggle()
{
IsVisible = !IsVisible;
}
/// <summary>
/// Показывает панель.
/// </summary>
public void Show()
{
IsVisible = true;
}
/// <summary>
/// Скрывает панель.
/// </summary>
public void Hide()
{
IsVisible = false;
}
}

View File

@@ -0,0 +1,444 @@
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;
/// <summary>
/// Представляет узел дерева компоновки, который разделяет доступную область
/// между двумя дочерними элементами. Этот класс является основным структурным
/// элементом для создания сложных макетов с разделителями.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="DockGroup"/> реализует как <see cref="IDragSource"/> (для
/// возможности перетаскивания всей группы), так и <see cref="IDropTarget"/>
/// (для возможности сброса на группу), что делает его полностью интегрированным
/// в систему перетаскивания док-системы.
/// </para>
/// <para>
/// Каждая группа содержит два дочерних элемента (<see cref="First"/> и
/// <see cref="Second"/>), которые могут быть либо другими группами (для
/// создания вложенной структуры), либо листами (<see cref="DockLeaf"/>)
/// с контентом. Направление разделения определяется свойством
/// <see cref="Orientation"/>.
/// </para>
/// </remarks>
public class DockGroup : IDockElement, IDragSource, IDropTarget, INotifyPropertyChanged
{
/// <summary>
/// Событие, возникающее при изменении значения свойства.
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;
private double _splitRatio = 0.5;
private string _id;
/// <summary>
/// Получает уникальный идентификатор группы.
/// </summary>
/// <value>
/// Строковый идентификатор, уникальный в пределах дерева компоновки.
/// </value>
/// <remarks>
/// Идентификатор используется для сериализации/десериализации макета,
/// поиска элементов и отслеживания изменений в дереве.
/// </remarks>
public string Id
{
get => _id;
internal set
{
if (_id != value)
{
_id = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Получает или задает родительский элемент в иерархии дерева компоновки.
/// </summary>
/// <value>
/// Родительский элемент или null, если эта группа является корневой.
/// </value>
/// <remarks>
/// Это свойство управляется системой компоновки при добавлении или
/// удалении элементов из дерева.
/// </remarks>
public IDockElement? Parent { get; set; }
/// <summary>
/// Получает или задает первый дочерний элемент (левую или верхнюю область).
/// </summary>
/// <value>
/// Элемент, занимающий первую часть разделенной области.
/// </value>
/// <exception cref="ArgumentNullException">
/// Выбрасывается при попытке установить значение null.
/// </exception>
/// <remarks>
/// При установке нового значения автоматически обновляется свойство
/// <see cref="Parent"/> у дочернего элемента.
/// </remarks>
public IDockElement First { get; set; }
/// <summary>
/// Получает или задает второй дочерний элемент (правую или нижнюю область).
/// </summary>
/// <value>
/// Элемент, занимающий вторую часть разделенной области.
/// </value>
/// <exception cref="ArgumentNullException">
/// Выбрасывается при попытке установить значение null.
/// </exception>
/// <remarks>
/// При установке нового значения автоматически обновляется свойство
/// <see cref="Parent"/> у дочернего элемента.
/// </remarks>
public IDockElement Second { get; set; }
/// <summary>
/// Получает или задает направление разделения данной группы.
/// </summary>
/// <value>
/// Значение перечисления <see cref="SplitDirection"/>, указывающее,
/// как разделена область: горизонтально или вертикально.
/// </value>
/// <remarks>
/// <para>
/// <see cref="SplitDirection.Horizontal"/> создает левую и правую области.
/// </para>
/// <para>
/// <see cref="SplitDirection.Vertical"/> создает верхнюю и нижнюю области.
/// </para>
/// </remarks>
public SplitDirection Orientation { get; set; }
/// <summary>
/// Получает или задает соотношение разделения между первым и вторым элементами.
/// </summary>
/// <value>
/// Значение от 0.0 до 1.0, где:
/// <list type="bullet">
/// <item>0.0 - вся область принадлежит второму элементу</item>
/// <item>0.5 - область разделена поровну</item>
/// <item>1.0 - вся область принадлежит первому элементу</item>
/// </list>
/// </value>
/// <remarks>
/// Изменение этого свойства вызывает событие <see cref="PropertyChanged"/>
/// и может привести к перерисовке пользовательского интерфейса.
/// </remarks>
public double SplitRatio
{
get => _splitRatio;
set
{
if (Math.Abs(_splitRatio - value) > double.Epsilon)
{
_splitRatio = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Получает или задает желаемую ширину элемента.
/// </summary>
/// <value>
/// Ширина в пикселях или относительных единицах.
/// </value>
public double Width { get; set; }
/// <summary>
/// Получает или задает желаемую высоту элемента.
/// </summary>
/// <value>
/// Высота в пикселях или относительных единицах.
/// </value>
public double Height { get; set; }
/// <summary>
/// Получает минимально допустимую ширину элемента.
/// </summary>
/// <value>
/// Минимальная ширина в пикселях, при которой элемент сохраняет функциональность.
/// </value>
/// <remarks>
/// Для группы минимальная ширина вычисляется как сумма минимальных ширин
/// дочерних элементов при горизонтальной ориентации или максимум минимальных
/// ширин при вертикальной ориентации.
/// </remarks>
public double MinWidth => Orientation == SplitDirection.Horizontal
? First.MinWidth + Second.MinWidth
: Math.Max(First.MinWidth, Second.MinWidth);
/// <summary>
/// Получает минимально допустимую высоту элемента.
/// </summary>
/// <value>
/// Минимальная высота в пикселях, при которой элемент сохраняет функциональность.
/// </value>
/// <remarks>
/// Для группы минимальная высота вычисляется как сумма минимальных высот
/// дочерних элементов при вертикальной ориентации или максимум минимальных
/// высот при горизонтальной ориентации.
/// </remarks>
public double MinHeight => Orientation == SplitDirection.Vertical
? First.MinHeight + Second.MinHeight
: Math.Max(First.MinHeight, Second.MinHeight);
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DockGroup"/>.
/// </summary>
/// <param name="first">
/// Первый дочерний элемент (левая или верхняя область).
/// </param>
/// <param name="second">
/// Второй дочерний элемент (правая или нижняя область).
/// </param>
/// <param name="orientation">
/// Направление разделения между дочерними элементами.
/// </param>
/// <param name="id">
/// Уникальный идентификатор группы. Если не указан, генерируется новый GUID.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="first"/> или <paramref name="second"/>
/// равны null.
/// </exception>
/// <remarks>
/// Конструктор автоматически устанавливает свойство <see cref="Parent"/>
/// у дочерних элементов на текущую группу и генерирует уникальный идентификатор,
/// если он не был предоставлен.
/// </remarks>
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;
}
/// <summary>
/// Вызывает событие <see cref="PropertyChanged"/>.
/// </summary>
/// <param name="name">
/// Имя изменившегося свойства. Если не указано, определяется автоматически.
/// </param>
protected void OnPropertyChanged([CallerMemberName] string? name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
#region Реализация IDragSource
/// <summary>
/// Определяет, может ли группа начать операцию перетаскивания.
/// </summary>
/// <param name="dragInfo">
/// При успешном возврате содержит информацию о перетаскивании;
/// в противном случае — null.
/// </param>
/// <returns>
/// true, если группа может начать перетаскивание; в противном случае — false.
/// </returns>
/// <remarks>
/// <para>
/// Группа может быть перетащена только если она не является корневым
/// элементом дерева (имеет родителя).
/// </para>
/// <para>
/// При успешной проверке метод заполняет <paramref name="dragInfo"/>
/// данными типа <see cref="DockElementDragData"/>.
/// </para>
/// </remarks>
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;
}
/// <summary>
/// Начинает операцию перетаскивания для группы.
/// </summary>
/// <param name="dragInfo">
/// Информация о перетаскивании, полученная из <see cref="CanStartDrag"/>.
/// </param>
/// <returns>
/// Всегда возвращает true, так как группа не требует специальной подготовки
/// для начала перетаскивания.
/// </returns>
/// <remarks>
/// Для <see cref="DockGroup"/> этот метод не выполняет дополнительных действий,
/// так как все необходимые данные уже содержатся в <paramref name="dragInfo"/>.
/// </remarks>
public bool StartDrag(DragInfo dragInfo)
{
// DockGroup не требует дополнительной подготовки для перетаскивания
return true;
}
/// <summary>
/// Вызывается при завершении операции перетаскивания.
/// </summary>
/// <param name="dragInfo">
/// Исходная информация о перетаскивании.
/// </param>
/// <param name="effects">
/// Эффекты, которые были применены при сбросе.
/// </param>
/// <remarks>
/// <para>
/// Этот метод вызывается после того, как операция перетаскивания была
/// завершена (успешно или неуспешно).
/// </para>
/// <para>
/// Для <see cref="DockGroup"/> этот метод не выполняет действий, так как
/// все изменения в структуре дерева уже обработаны <see cref="LayoutManager"/>.
/// </para>
/// </remarks>
public void DragCompleted(DragInfo dragInfo, DragDropEffects effects)
{
// Если группа была перемещена, ничего не делаем - LayoutManager уже обработал изменение
}
/// <summary>
/// Вызывается при отмене операции перетаскивания.
/// </summary>
/// <param name="dragInfo">
/// Исходная информация о перетаскивании.
/// </param>
/// <remarks>
/// Для <see cref="DockGroup"/> отмена перетаскивания не требует специальных
/// действий, так как структура дерева не была изменена.
/// </remarks>
public void DragCancelled(DragInfo dragInfo)
{
// Отмена перетаскивания не требует действий
}
#endregion
#region Реализация IDropTarget
/// <summary>
/// Определяет, может ли группа принять сбрасываемые данные.
/// </summary>
/// <param name="dropInfo">
/// Информация о потенциальном сбросе.
/// </param>
/// <returns>
/// true, если группа может принять данные; в противном случае — false.
/// </returns>
/// <remarks>
/// <para>
/// Группа может принимать только данные типа <see cref="DockElementDragData"/>
/// для элементов док-системы (<see cref="DockGroup"/> или <see cref="DockLeaf"/>).
/// </para>
/// <para>
/// Группа не может принять сброс самой себя (проверяется по идентификатору).
/// </para>
/// </remarks>
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);
}
/// <summary>
/// Вызывается, когда перетаскиваемый объект находится над группой.
/// </summary>
/// <param name="dropInfo">
/// Информация о текущем положении перетаскивания.
/// </param>
/// <remarks>
/// <para>
/// Этот метод вызывается постоянно, пока пользователь перемещает объект
/// над целью. Для группы он устанавливает предлагаемые эффекты в
/// <see cref="DropInfo.SuggestedEffects"/>.
/// </para>
/// <para>
/// Если группа может принять сброс, предлагается эффект перемещения;
/// в противном случае эффекты не предлагаются.
/// </para>
/// </remarks>
public void DragOver(DropInfo dropInfo)
{
if (CanAcceptDrop(dropInfo))
{
dropInfo.SuggestedEffects = DragDropEffects.Move;
}
else
{
dropInfo.SuggestedEffects = DragDropEffects.None;
}
}
/// <summary>
/// Вызывается, когда пользователь сбрасывает данные на группу.
/// </summary>
/// <param name="dropInfo">
/// Информация о сбросе.
/// </param>
/// <remarks>
/// <para>
/// Для <see cref="DockGroup"/> обработка сброса делегируется
/// <see cref="LayoutManager"/>, поэтому метод просто помечает операцию
/// как обработанную.
/// </para>
/// <para>
/// Фактическое изменение структуры дерева выполняется менеджером макета
/// на основе данных из <paramref name="dropInfo"/>.
/// </para>
/// </remarks>
public void Drop(DropInfo dropInfo)
{
// Обработка сброса делегируется LayoutManager
dropInfo.MarkAsHandled();
}
/// <summary>
/// Вызывается, когда перетаскиваемый объект покидает область группы.
/// </summary>
/// <remarks>
/// Для группы этот метод не выполняет действий, так как очистка визуальной
/// обратной связи выполняется в UI-слое.
/// </remarks>
public void DragLeave()
{
// Очистка визуальной обратной связи (будет выполнена в UI слое)
}
#endregion
}

View File

@@ -0,0 +1,580 @@
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;
/// <summary>
/// Представляет конечный узел (лист) дерева компоновки, который непосредственно
/// содержит коллекцию вкладок с контентом. Этот класс является контейнером для
/// отображаемого пользователю содержимого.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="DockLeaf"/> реализует интерфейсы <see cref="IDockContainer"/>,
/// <see cref="IDragSource"/> и <see cref="IDropTarget"/>, что позволяет ему:
/// </para>
/// <list type="bullet">
/// <item>Управлять коллекцией вкладок</item>
/// <item>Быть источником перетаскивания (как всего листа, так и отдельных вкладок)</item>
/// <item>Принимать сброс других элементов или вкладок</item>
/// </list>
/// <para>
/// Лист является основным элементом, с которым взаимодействует пользователь
/// при работе с документами или инструментальными панелями в IDE-подобных
/// приложениях.
/// </para>
/// </remarks>
public class DockLeaf : IDockContainer, INotifyPropertyChanged, IDragSource, IDropTarget
{
/// <summary>
/// Событие, возникающее при изменении значения свойства.
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;
private readonly ObservableCollection<IDockContent> _items = new();
private IDockContent? _activeContent;
private string _id;
/// <summary>
/// Получает уникальный идентификатор листа.
/// </summary>
/// <value>
/// Строковый идентификатор, уникальный в пределах дерева компоновки.
/// </value>
public string Id
{
get => _id;
internal set
{
if (_id != value)
{
_id = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Получает или задает родительский элемент в иерархии дерева компоновки.
/// </summary>
/// <value>
/// Родительский элемент или null, если этот лист является корневым.
/// </value>
public IDockElement? Parent { get; set; }
/// <summary>
/// Получает список вкладок, содержащихся в данном контейнере.
/// </summary>
/// <value>
/// Коллекция объектов, реализующих <see cref="IDockContent"/>.
/// </value>
/// <remarks>
/// Эта коллекция является наблюдаемой (ObservableCollection), что позволяет
/// автоматически обновлять пользовательский интерфейс при добавлении или
/// удалении вкладок.
/// </remarks>
public IList<IDockContent> Children => _items;
/// <summary>
/// Получает или задает активную (выбранную) вкладку в контейнере.
/// </summary>
/// <value>
/// Активная вкладка или null, если в контейнере нет вкладок.
/// </value>
/// <remarks>
/// <para>
/// При установке нового значения проверяется, что вкладка действительно
/// содержится в коллекции <see cref="Children"/>.
/// </para>
/// <para>
/// Изменение этого свойства вызывает событие <see cref="PropertyChanged"/>.
/// </para>
/// </remarks>
public IDockContent? ActiveContent
{
get => _activeContent;
set
{
if (value != null && !_items.Contains(value)) return;
if (_activeContent != value)
{
_activeContent = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Получает или задает желаемую ширину элемента.
/// </summary>
/// <value>
/// Ширина в пикселях или относительных единицах.
/// </value>
public double Width { get; set; }
/// <summary>
/// Получает или задает желаемую высоту элемента.
/// </summary>
/// <value>
/// Высота в пикселях или относительных единицах.
/// </value>
public double Height { get; set; }
/// <summary>
/// Получает или задает минимально допустимую ширину элемента.
/// </summary>
/// <value>
/// Минимальная ширина в пикселях. Значение по умолчанию: 100.
/// </value>
public double MinWidth { get; set; } = 100;
/// <summary>
/// Получает или задает минимально допустимую высоту элемента.
/// </summary>
/// <value>
/// Минимальная высота в пикселях. Значение по умолчанию: 100.
/// </value>
public double MinHeight { get; set; } = 100;
/// <summary>
/// Получает или задает положение полосы вкладок в контейнере.
/// </summary>
/// <value>
/// Значение перечисления <see cref="TabPlacement"/>, определяющее,
/// где располагаются вкладки относительно содержимого.
/// </value>
/// <remarks>
/// Поддерживаются все четыре стороны: верх, низ, лево, право.
/// </remarks>
public TabPlacement TabPlacement { get; set; } = TabPlacement.Bottom;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DockLeaf"/>.
/// </summary>
/// <param name="id">
/// Уникальный идентификатор листа. Если не указан, генерируется новый GUID.
/// </param>
/// <remarks>
/// Создает пустой лист с коллекцией вкладок и генерирует уникальный
/// идентификатор, если он не был предоставлен.
/// </remarks>
public DockLeaf(string? id = null)
{
_id = id ?? Guid.NewGuid().ToString();
}
/// <summary>
/// Вызывает событие <see cref="PropertyChanged"/>.
/// </summary>
/// <param name="name">
/// Имя изменившегося свойства. Если не указано, определяется автоматически.
/// </param>
protected void OnPropertyChanged([CallerMemberName] string? name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
/// <summary>
/// Добавляет контент в контейнер и делает его активным.
/// </summary>
/// <param name="content">
/// Контент для добавления.
/// </param>
/// <remarks>
/// <para>
/// Если контент уже содержится в коллекции, он не добавляется повторно,
/// но становится активным.
/// </para>
/// <para>
/// Этот метод обновляет свойство <see cref="ActiveContent"/> и вызывает
/// соответствующее событие изменения свойства.
/// </para>
/// </remarks>
public void AddContent(IDockContent content)
{
if (!_items.Contains(content))
{
_items.Add(content);
}
ActiveContent = content;
}
/// <summary>
/// Удаляет контент из контейнера.
/// </summary>
/// <param name="content">
/// Контент для удаления.
/// </param>
/// <remarks>
/// <para>
/// Если удаляемый контент является активным, автоматически выбирается
/// новая активная вкладка (следующая в списке или предыдущая, если удалена
/// последняя).
/// </para>
/// <para>
/// Если после удаления контейнер становится пустым, он может быть удален
/// из дерева макета системой компоновки.
/// </para>
/// </remarks>
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
/// <summary>
/// Определяет, может ли лист начать операцию перетаскивания.
/// </summary>
/// <param name="dragInfo">
/// При успешном возврате содержит информацию о перетаскивании;
/// в противном случае — null.
/// </param>
/// <returns>
/// true, если лист может начать перетаскивание; в противном случае — false.
/// </returns>
/// <remarks>
/// <para>
/// Лист может быть перетащен, если:
/// </para>
/// <list type="bullet">
/// <item>Он имеет родителя (не является корневым)</item>
/// <item>Или имеет хотя бы одну вкладку (не пустой)</item>
/// </list>
/// <para>
/// В зависимости от наличия активного контента создаются разные данные:
/// </para>
/// <list type="bullet">
/// <item>
/// Если есть активный контент - создается <see cref="ContentDragData"/>
/// для перетаскивания конкретной вкладки
/// </item>
/// <item>
/// Если нет активного контента - создается <see cref="DockElementDragData"/>
/// для перетаскивания всего листа
/// </item>
/// </list>
/// </remarks>
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;
}
/// <summary>
/// Начинает операцию перетаскивания для листа.
/// </summary>
/// <param name="dragInfo">
/// Информация о перетаскивании.
/// </param>
/// <returns>
/// Всегда возвращает true.
/// </returns>
/// <remarks>
/// Для <see cref="DockLeaf"/> этот метод не выполняет дополнительных действий.
/// </remarks>
public bool StartDrag(DragInfo dragInfo)
{
// DockLeaf не требует дополнительной подготовки
return true;
}
/// <summary>
/// Вызывается при завершении операции перетаскивания.
/// </summary>
/// <param name="dragInfo">
/// Исходная информация о перетаскивании.
/// </param>
/// <param name="effects">
/// Эффекты, которые были применены при сбросе.
/// </param>
/// <remarks>
/// Для <see cref="DockLeaf"/> этот метод не выполняет действий.
/// </remarks>
public void DragCompleted(DragInfo dragInfo, DragDropEffects effects)
{
// Если лист был перемещен или скопирован, LayoutManager уже обработал это
}
/// <summary>
/// Вызывается при отмене операции перетаскивания.
/// </summary>
/// <param name="dragInfo">
/// Исходная информация о перетаскивании.
/// </param>
/// <remarks>
/// Для <see cref="DockLeaf"/> отмена перетаскивания не требует действий.
/// </remarks>
public void DragCancelled(DragInfo dragInfo)
{
// Отмена не требует действий
}
#endregion
#region Реализация IDropTarget
/// <summary>
/// Определяет, может ли лист принять сбрасываемые данные.
/// </summary>
/// <param name="dropInfo">
/// Информация о потенциальном сбросе.
/// </param>
/// <returns>
/// true, если лист может принять данные; в противном случае — false.
/// </returns>
/// <remarks>
/// Лист может принимать:
/// <list type="bullet">
/// <item>
/// <see cref="DockElementDragData"/> для других листов и групп
/// (для объединения или разделения)
/// </item>
/// <item>
/// <see cref="ContentDragData"/> для вкладок (для объединения вкладок)
/// </item>
/// </list>
/// </remarks>
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;
}
/// <summary>
/// Вызывается, когда перетаскиваемый объект находится над листом.
/// </summary>
/// <param name="dropInfo">
/// Информация о текущем положении перетаскивания.
/// </param>
/// <remarks>
/// <para>
/// В зависимости от типа данных устанавливаются разные предлагаемые эффекты:
/// </para>
/// <list type="bullet">
/// <item>
/// Для <see cref="ContentDragData"/> - эффект копирования (объединение вкладок)
/// </item>
/// <item>
/// Для <see cref="DockElementDragData"/> - эффект перемещения
/// </item>
/// </list>
/// </remarks>
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;
}
}
/// <summary>
/// Вызывается, когда пользователь сбрасывает данные на лист.
/// </summary>
/// <param name="dropInfo">
/// Информация о сбросе.
/// </param>
/// <remarks>
/// Обработка сброса делегируется <see cref="LayoutManager"/>.
/// </remarks>
public void Drop(DropInfo dropInfo)
{
// Обработка делегируется LayoutManager
dropInfo.MarkAsHandled();
}
/// <summary>
/// Вызывается, когда перетаскиваемый объект покидает область листа.
/// </summary>
/// <remarks>
/// Очистка визуальной обратной связи выполняется в UI-слое.
/// </remarks>
public void DragLeave()
{
// Очистка визуальной обратной связи
}
#endregion
}
/// <summary>
/// Представляет данные для перетаскивания элементов док-системы (групп или листов).
/// Используется при перетаскивании целых структурных элементов дерева компоновки.
/// </summary>
/// <remarks>
/// Этот класс сериализуется и передается между компонентами системы перетаскивания
/// для идентификации перетаскиваемого элемента и его свойств.
/// </remarks>
public class DockElementDragData
{
/// <summary>
/// Получает или задает уникальный идентификатор элемента.
/// </summary>
/// <value>
/// Идентификатор элемента, соответствующий свойству <see cref="IDockElement.Id"/>.
/// </value>
public string ElementId { get; set; } = string.Empty;
/// <summary>
/// Получает или задает тип элемента.
/// </summary>
/// <value>
/// Имя типа элемента (обычно "DockGroup" или "DockLeaf").
/// </value>
public string ElementType { get; set; } = string.Empty;
/// <summary>
/// Получает или задает значение, указывающее, является ли элемент группой.
/// </summary>
/// <value>
/// true, если элемент является <see cref="DockGroup"/>; false, если <see cref="DockLeaf"/>.
/// </value>
public bool IsGroup { get; set; }
/// <summary>
/// Получает или задает идентификатор родительского элемента.
/// </summary>
/// <value>
/// Идентификатор родительского элемента или null, если элемент корневой.
/// </value>
public string? ParentId { get; set; }
/// <summary>
/// Получает или задает ширину элемента.
/// </summary>
/// <value>
/// Текущая ширина элемента в пикселях.
/// </value>
public double Width { get; set; }
/// <summary>
/// Получает или задает высоту элемента.
/// </summary>
/// <value>
/// Текущая высота элемента в пикселях.
/// </value>
public double Height { get; set; }
}
/// <summary>
/// Представляет данные для перетаскивания контента (вкладок).
/// Используется при перетаскивании отдельных вкладок между контейнерами.
/// </summary>
/// <remarks>
/// Этот класс позволяет идентифицировать конкретную вкладку для операций
/// объединения или перемещения между контейнерами.
/// </remarks>
public class ContentDragData
{
/// <summary>
/// Получает или задает идентификатор контейнера (листа), содержащего контент.
/// </summary>
/// <value>
/// Идентификатор <see cref="DockLeaf"/>, в котором находится перетаскиваемая вкладка.
/// </value>
public string ElementId { get; set; } = string.Empty;
/// <summary>
/// Получает или задает уникальный идентификатор контента.
/// </summary>
/// <value>
/// Идентификатор контента, соответствующий свойству <see cref="IDockContent.Id"/>.
/// </value>
public string ContentId { get; set; } = string.Empty;
/// <summary>
/// Получает или задает заголовок контента.
/// </summary>
/// <value>
/// Текст, отображаемый на вкладке.
/// </value>
public string ContentTitle { get; set; } = string.Empty;
/// <summary>
/// Получает или задает тип контента.
/// </summary>
/// <value>
/// Имя типа контента (например, "TextEditor", "Toolbox", и т.д.).
/// </value>
public string ContentType { get; set; } = string.Empty;
/// <summary>
/// Получает или задает значение, указывающее, можно ли закрыть контент.
/// </summary>
/// <value>
/// true, если контент можно закрыть; в противном случае — false.
/// </value>
public bool CanClose { get; set; } = true;
}

View File

@@ -0,0 +1,13 @@
namespace Lattice.Core.Docking.Models;
/// <summary>
/// Определяет позицию вставки при операции Drag-and-Drop.
/// </summary>
public enum DockPosition
{
Left,
Right,
Top,
Bottom,
Center,
}

View File

@@ -0,0 +1,19 @@
namespace Lattice.Core.Docking.Models;
/// <summary>
/// Определяет стороны окна, к которым могут быть прикреплены автоскрываемые панели.
/// </summary>
public enum DockSide
{
/// <summary> Левая сторона окна. </summary>
Left,
/// <summary> Правая сторона окна. </summary>
Right,
/// <summary> Верхняя сторона окна. </summary>
Top,
/// <summary> Нижняя сторона окна. </summary>
Bottom
}

View File

@@ -0,0 +1,23 @@
using Lattice.Core.Docking.Abstractions;
namespace Lattice.Core.Docking.Models;
/// <summary>
/// Описывает состояние плавающего окна в системе Lattice.
/// </summary>
public class DockWindow
{
/// <summary> Уникальный ID окна для сохранения его позиции в конфиге. </summary>
public string Id { get; } = Guid.NewGuid().ToString();
/// <summary> Корневой элемент макета внутри данного окна. </summary>
public IDockElement? Root { get; set; }
public double X { get; set; }
public double Y { get; set; }
public double Width { get; set; } = 800;
public double Height { get; set; } = 600;
/// <summary> Заголовок окна (обычно берется из активного контента). </summary>
public string Title { get; set; } = "Lattice Tool Window";
}

View File

@@ -0,0 +1,12 @@
namespace Lattice.Core.Docking.Models;
/// <summary>
/// Перечисление направлений разделения пространства внутри группы.
/// </summary>
public enum SplitDirection
{
/// <summary> Разделение по горизонтали (создает левую и правую области). </summary>
Horizontal,
/// <summary> Разделение по вертикали (создает верхнюю и нижнюю области). </summary>
Vertical
}

View File

@@ -0,0 +1,12 @@
namespace Lattice.Core.Docking.Models;
/// <summary>
/// Определяет положение полосы вкладок в контейнере.
/// </summary>
public enum TabPlacement
{
Top,
Bottom,
Left,
Right,
}

View File

@@ -0,0 +1,31 @@
namespace Lattice.Core.Docking.Serialization;
/// <summary>
/// Абстракция для сериализации и десериализации состояния макета док-системы.
/// Позволяет сохранять и восстанавливать расположение панелей, окон и их состояние.
/// </summary>
/// <remarks>
/// Эта абстракция позволяет реализовать различные форматы сериализации (JSON, XML, бинарный)
/// и различные хранилища (файлы, базы данных, облако) без изменения основной логики док-системы.
/// </remarks>
public interface ILayoutSerializer
{
/// <summary>
/// Сериализует состояние менеджера макета в строку.
/// </summary>
/// <param name="manager">Менеджер макета для сериализации.</param>
/// <returns>Строковое представление состояния макета.</returns>
string Serialize(Engine.LayoutManager manager);
/// <summary>
/// Десериализует состояние макета из строки и восстанавливает его в менеджере.
/// </summary>
/// <param name="manager">Менеджер макета для восстановления состояния.</param>
/// <param name="serializedLayout">Сериализованное состояние макета.</param>
/// <param name="contentResolver">
/// Функция разрешения контента по идентификатору, используемая для восстановления
/// ссылок на контент в десериализованном состоянии.
/// </param>
void Deserialize(Engine.LayoutManager manager, string serializedLayout,
Func<string, Abstractions.IDockContent?> contentResolver);
}

View File

@@ -0,0 +1,19 @@
namespace Lattice.Core.Docking.Serialization;
/// <summary>
/// Контракт для объектов, которые могут предоставлять состояние для сериализации.
/// </summary>
public interface ISerializableLayout
{
/// <summary>
/// Получает состояние для сериализации.
/// </summary>
/// <returns>Объект состояния, готовый к сериализации.</returns>
object GetSerializableState();
/// <summary>
/// Восстанавливает состояние из десериализованного объекта.
/// </summary>
/// <param name="state">Десериализованное состояние.</param>
void RestoreFromState(object state);
}

View File

@@ -0,0 +1,158 @@
namespace Lattice.Core.Docking.Services;
/// <summary>
/// Реестр типов содержимого, который позволяет создавать экземпляры контента по типу.
/// Этот сервис является центральным для динамического создания панелей инструментов и документов в IDE.
/// </summary>
/// <remarks>
/// Реализует шаблон "Фабрика" для создания экземпляров <see cref="Abstractions.IDockContent"/>.
/// Позволяет регистрировать фабричные методы для различных типов контента, что обеспечивает
/// позднее связывание и возможность плагинной архитектуры.
/// </remarks>
public class ContentRegistry
{
private readonly Dictionary<string, ContentDescriptor> _contentTypes = new();
/// <summary>
/// Регистрирует фабричный метод для создания контента указанного типа.
/// </summary>
/// <typeparam name="T">Тип контента, реализующий <see cref="Abstractions.IDockContent"/>.</typeparam>
/// <param name="contentTypeId">Уникальный идентификатор типа контента.</param>
/// <param name="factory">Фабричный метод для создания экземпляров контента.</param>
/// <param name="metadata">Метаданные типа контента (опционально).</param>
/// <exception cref="ArgumentNullException">Выбрасывается, если contentTypeId или factory равны null.</exception>
/// <exception cref="ArgumentException">Выбрасывается, если contentTypeId уже зарегистрирован.</exception>
public void Register<T>(string contentTypeId, Func<T> factory, ContentMetadata? metadata = null)
where T : Abstractions.IDockContent
{
if (string.IsNullOrWhiteSpace(contentTypeId))
throw new ArgumentNullException(nameof(contentTypeId));
if (factory == null)
throw new ArgumentNullException(nameof(factory));
if (_contentTypes.ContainsKey(contentTypeId))
throw new ArgumentException($"Content type '{contentTypeId}' is already registered.");
_contentTypes[contentTypeId] = new ContentDescriptor(
typeof(T),
() => factory(),
metadata ?? new ContentMetadata(contentTypeId, typeof(T).Name)
);
}
/// <summary>
/// Создает новый экземпляр контента указанного типа с заданным идентификатором.
/// </summary>
/// <param name="contentTypeId">Идентификатор типа контента.</param>
/// <param name="id">Уникальный идентификатор для создаваемого экземпляра контента.</param>
/// <returns>Новый экземпляр контента.</returns>
/// <exception cref="KeyNotFoundException">Выбрасывается, если тип контента не зарегистрирован.</exception>
public Abstractions.IDockContent CreateContent(string contentTypeId, string id)
{
if (!_contentTypes.TryGetValue(contentTypeId, out var descriptor))
throw new KeyNotFoundException($"Content type '{contentTypeId}' is not registered.");
var content = descriptor.Factory();
// Устанавливаем ID через рефлексию, если есть свойство Id
var property = content.GetType().GetProperty("Id");
if (property != null && property.CanWrite)
{
property.SetValue(content, id);
}
return content;
}
/// <summary>
/// Получает метаданные для указанного типа контента.
/// </summary>
/// <param name="contentTypeId">Идентификатор типа контента.</param>
/// <returns>Метаданные типа контента или null, если тип не найден.</returns>
public ContentMetadata? GetMetadata(string contentTypeId)
{
return _contentTypes.TryGetValue(contentTypeId, out var descriptor)
? descriptor.Metadata
: null;
}
/// <summary>
/// Получает все зарегистрированные типы контента.
/// </summary>
/// <returns>Коллекция идентификаторов зарегистрированных типов контента.</returns>
public IEnumerable<string> GetRegisteredTypes() => _contentTypes.Keys;
/// <summary>
/// Проверяет, зарегистрирован ли указанный тип контента.
/// </summary>
public bool IsRegistered(string contentTypeId) => _contentTypes.ContainsKey(contentTypeId);
/// <summary>
/// Дескриптор типа контента, содержащий информацию о фабричном методе и метаданных.
/// </summary>
private class ContentDescriptor
{
public Type ContentType { get; }
public Func<Abstractions.IDockContent> Factory { get; }
public ContentMetadata Metadata { get; }
public ContentDescriptor(Type contentType, Func<Abstractions.IDockContent> factory, ContentMetadata metadata)
{
ContentType = contentType;
Factory = factory;
Metadata = metadata;
}
}
}
/// <summary>
/// Метаданные типа контента, предоставляющие дополнительную информацию для отображения в UI.
/// </summary>
public class ContentMetadata
{
/// <summary>
/// Идентификатор типа контента.
/// </summary>
public string ContentTypeId { get; }
/// <summary>
/// Отображаемое имя типа контента.
/// </summary>
public string DisplayName { get; set; }
/// <summary>
/// Описание типа контента.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Имя ресурса для иконки (опционально).
/// </summary>
public string? IconResource { get; set; }
/// <summary>
/// Признак того, что контент является документом (а не инструментальной панелью).
/// </summary>
public bool IsDocument { get; set; }
/// <summary>
/// Минимальная ширина контента в пикселях.
/// </summary>
public double DefaultWidth { get; set; } = 300;
/// <summary>
/// Минимальная высота контента в пикселях.
/// </summary>
public double DefaultHeight { get; set; } = 200;
/// <summary>
/// Инициализирует новый экземпляр метаданных контента.
/// </summary>
/// <param name="contentTypeId">Идентификатор типа контента.</param>
/// <param name="displayName">Отображаемое имя.</param>
public ContentMetadata(string contentTypeId, string displayName)
{
ContentTypeId = contentTypeId;
DisplayName = displayName;
Description = string.Empty;
}
}