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,37 @@
namespace Lattice.Serialization.Docking;
/// <summary>
/// DTO для автоскрываемой панели (AutoHidePanel).
/// </summary>
public class AutoHidePanelDto
{
/// <summary>
/// Уникальный идентификатор панели.
/// </summary>
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// Ссылка на контент панели.
/// </summary>
public ContentReferenceDto Content { get; set; } = null!;
/// <summary>
/// Сторона прикрепления в виде строки.
/// </summary>
public string Side { get; set; } = string.Empty;
/// <summary>
/// Размер панели в пикселях.
/// </summary>
public double Size { get; set; } = 300;
/// <summary>
/// Показывает, видима ли панель.
/// </summary>
public bool IsVisible { get; set; } = false;
/// <summary>
/// Смещение для анимации (0.0 - 1.0).
/// </summary>
public double SlideOffset { get; set; } = 0.0;
}

View File

@@ -0,0 +1,32 @@
namespace Lattice.Serialization.Docking;
/// <summary>
/// DTO для ссылки на контент без сериализации самого контента.
/// </summary>
public class ContentReferenceDto
{
/// <summary>
/// Уникальный идентификатор контента.
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Тип контента (для восстановления через ContentRegistry).
/// </summary>
public string TypeId { get; set; } = string.Empty;
/// <summary>
/// Отображаемое название контента.
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// Показывает, можно ли закрыть контент.
/// </summary>
public bool CanClose { get; set; } = true;
/// <summary>
/// Дополнительные свойства контента для восстановления состояния.
/// </summary>
public Dictionary<string, object?> Properties { get; set; } = new();
}

View File

@@ -0,0 +1,37 @@
namespace Lattice.Serialization.Docking;
/// <summary>
/// Базовый DTO для элементов дерева компоновки.
/// </summary>
public abstract class ElementDto
{
/// <summary>
/// Уникальный идентификатор элемента.
/// </summary>
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// Тип элемента (для десериализации).
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// Ширина элемента.
/// </summary>
public double Width { get; set; }
/// <summary>
/// Высота элемента.
/// </summary>
public double Height { get; set; }
/// <summary>
/// Минимальная ширина элемента.
/// </summary>
public double MinWidth { get; set; }
/// <summary>
/// Минимальная высота элемента.
/// </summary>
public double MinHeight { get; set; }
}

View File

@@ -0,0 +1,27 @@
namespace Lattice.Serialization.Docking;
/// <summary>
/// DTO для группы разделения (DockGroup).
/// </summary>
public class GroupDto : ElementDto
{
/// <summary>
/// Первый дочерний элемент (левая или верхняя область).
/// </summary>
public ElementDto First { get; set; } = null!;
/// <summary>
/// Второй дочерний элемент (правая или нижняя область).
/// </summary>
public ElementDto Second { get; set; } = null!;
/// <summary>
/// Направление разделения в виде строки.
/// </summary>
public string Orientation { get; set; } = string.Empty;
/// <summary>
/// Соотношение разделения между первым и вторым элементами (0.0 - 1.0).
/// </summary>
public double SplitRatio { get; set; } = 0.5;
}

View File

@@ -0,0 +1,47 @@
namespace Lattice.Serialization.Docking;
/// <summary>
/// Data Transfer Object (DTO) для сериализации состояния макета док-системы.
/// Содержит все необходимые данные для сохранения и восстановления состояния макета.
/// </summary>
/// <remarks>
/// Этот DTO является независимым от формата сериализации (JSON, XML, Binary) и используется
/// как промежуточное представление между объектной моделью и сериализованными данными.
/// </remarks>
public class LayoutDto
{
/// <summary>
/// Версия формата DTO для контроля совместимости.
/// </summary>
public string Version { get; set; } = "1.0";
/// <summary>
/// Дата и время создания DTO в UTC.
/// </summary>
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
/// <summary>
/// Идентификатор приложения, создавшего DTO.
/// </summary>
public string? ApplicationId { get; set; }
/// <summary>
/// Корневой элемент дерева компоновки.
/// </summary>
public ElementDto? Root { get; set; }
/// <summary>
/// Список плавающих окон.
/// </summary>
public List<WindowDto> FloatingWindows { get; set; } = new();
/// <summary>
/// Список автоскрываемых панелей.
/// </summary>
public List<AutoHidePanelDto> AutoHidePanels { get; set; } = new();
/// <summary>
/// Дополнительные метаданные, специфичные для приложения.
/// </summary>
public Dictionary<string, string> Metadata { get; set; } = new();
}

View File

@@ -0,0 +1,22 @@
namespace Lattice.Serialization.Docking;
/// <summary>
/// DTO для контейнера вкладок (DockLeaf).
/// </summary>
public class LeafDto : ElementDto
{
/// <summary>
/// Список ссылок на контент, содержащийся в листе.
/// </summary>
public List<ContentReferenceDto> Contents { get; set; } = new();
/// <summary>
/// Идентификатор активного контента (если есть).
/// </summary>
public string? ActiveContentId { get; set; }
/// <summary>
/// Расположение вкладок в виде строки.
/// </summary>
public string TabPlacement { get; set; } = "Bottom";
}

View File

@@ -0,0 +1,52 @@
namespace Lattice.Serialization.Docking;
/// <summary>
/// DTO для плавающего окна (DockWindow).
/// </summary>
public class WindowDto
{
/// <summary>
/// Уникальный идентификатор окна.
/// </summary>
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// Позиция X окна на экране.
/// </summary>
public double X { get; set; }
/// <summary>
/// Позиция Y окна на экране.
/// </summary>
public double Y { get; set; }
/// <summary>
/// Ширина окна.
/// </summary>
public double Width { get; set; } = 800;
/// <summary>
/// Высота окна.
/// </summary>
public double Height { get; set; } = 600;
/// <summary>
/// Заголовок окна.
/// </summary>
public string Title { get; set; } = "Lattice Tool Window";
/// <summary>
/// Корневой элемент макета внутри окна.
/// </summary>
public ElementDto? Root { get; set; }
/// <summary>
/// Показывает, видимо ли окно.
/// </summary>
public bool IsVisible { get; set; } = true;
/// <summary>
/// Показывает, сфокусировано ли окно.
/// </summary>
public bool IsFocused { get; set; } = false;
}

View File

@@ -0,0 +1,103 @@
using Lattice.Core.Docking.Abstractions;
using Lattice.Core.Docking.Engine;
namespace Lattice.Serialization.Docking;
/// <summary>
/// Определяет контракт для сериализации и десериализации состояния макета док-системы.
/// Этот интерфейс позволяет реализовать различные форматы сериализации (JSON, XML, Binary)
/// и различные хранилища (файлы, базы данных, облако) без изменения основной логики.
/// </summary>
/// <remarks>
/// Реализации этого интерфейса должны преобразовывать объектную модель док-системы
/// в промежуточный DTO (<see cref="LayoutDto"/>), который затем сериализуется в целевой формат.
/// Это обеспечивает независимость формата сериализации от структуры DTO.
/// </remarks>
public interface ILayoutSerializer
{
/// <summary>
/// Получает уникальный идентификатор формата сериализации.
/// </summary>
/// <value>Строковый идентификатор формата, например "json", "xml", "binary".</value>
string FormatId { get; }
/// <summary>
/// Получает MIME-тип формата сериализации.
/// </summary>
/// <value>MIME-тип, например "application/json", "application/xml".</value>
string MimeType { get; }
/// <summary>
/// Получает расширение файла по умолчанию для данного формата.
/// </summary>
/// <value>Расширение файла, например ".json", ".xml".</value>
string DefaultFileExtension { get; }
/// <summary>
/// Сериализует состояние менеджера макета в массив байтов.
/// </summary>
/// <param name="manager">Менеджер макета для сериализации.</param>
/// <returns>Массив байтов, содержащий сериализованное состояние макета.</returns>
/// <exception cref="ArgumentNullException">Выбрасывается, если <paramref name="manager"/> равен null.</exception>
byte[] Serialize(LayoutManager manager);
/// <summary>
/// Сериализует состояние менеджера макета в строку.
/// </summary>
/// <param name="manager">Менеджер макета для сериализации.</param>
/// <returns>Строковое представление состояния макета.</returns>
/// <exception cref="ArgumentNullException">Выбрасывается, если <paramref name="manager"/> равен null.</exception>
string SerializeToString(LayoutManager manager);
/// <summary>
/// Сериализует состояние менеджера макета в поток.
/// </summary>
/// <param name="manager">Менеджер макета для сериализации.</param>
/// <param name="stream">Поток для записи сериализованных данных.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="manager"/> или <paramref name="stream"/> равен null.
/// </exception>
void SerializeToStream(LayoutManager manager, Stream stream);
/// <summary>
/// Десериализует состояние макета из массива байтов и восстанавливает его в менеджере.
/// </summary>
/// <param name="manager">Менеджер макета для восстановления состояния.</param>
/// <param name="data">Массив байтов с сериализованным состоянием макета.</param>
/// <param name="contentResolver">
/// Функция разрешения контента по идентификатору, используемая для восстановления
/// ссылок на контент в десериализованном состоянии.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="manager"/> или <paramref name="data"/> равен null.
/// </exception>
void Deserialize(LayoutManager manager, byte[] data, Func<string, IDockContent?> contentResolver);
/// <summary>
/// Десериализует состояние макета из строки и восстанавливает его в менеджере.
/// </summary>
/// <param name="manager">Менеджер макета для восстановления состояния.</param>
/// <param name="serializedData">Строка с сериализованным состоянием макета.</param>
/// <param name="contentResolver">
/// Функция разрешения контента по идентификатору, используемая для восстановления
/// ссылок на контент в десериализованном состоянии.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="manager"/> или <paramref name="serializedData"/> равен null.
/// </exception>
void DeserializeFromString(LayoutManager manager, string serializedData, Func<string, IDockContent?> contentResolver);
/// <summary>
/// Десериализует состояние макета из потока и восстанавливает его в менеджере.
/// </summary>
/// <param name="manager">Менеджер макета для восстановления состояния.</param>
/// <param name="stream">Поток с сериализованными данными.</param>
/// <param name="contentResolver">
/// Функция разрешения контента по идентификатору, используемая для восстановления
/// ссылок на контент в десериализованном состоянии.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="manager"/> или <paramref name="stream"/> равен null.
/// </exception>
void DeserializeFromStream(LayoutManager manager, Stream stream, Func<string, IDockContent?> contentResolver);
}

View File

@@ -0,0 +1,19 @@
namespace Lattice.Serialization.Docking;
/// <summary>
/// Интерфейс для контента, поддерживающего сериализацию дополнительного состояния.
/// </summary>
public interface ISerializableContent
{
/// <summary>
/// Получает состояние для сериализации.
/// </summary>
/// <returns>Словарь свойств и их значений.</returns>
Dictionary<string, object?> GetSerializableState();
/// <summary>
/// Восстанавливает состояние из десериализованных данных.
/// </summary>
/// <param name="state">Словарь свойств и их значений.</param>
void RestoreFromState(Dictionary<string, object?> state);
}

View File

@@ -0,0 +1,12 @@
<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.Docking\Lattice.Core.Docking.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,317 @@
using Lattice.Core.Docking.Abstractions;
using Lattice.Core.Docking.Engine;
using Lattice.Core.Docking.Models;
using System.Reflection;
namespace Lattice.Serialization.Docking;
/// <summary>
/// Предоставляет методы для преобразования между объектной моделью док-системы
/// и Data Transfer Objects (DTO) для сериализации.
/// </summary>
/// <remarks>
/// Этот класс является центральным местом для преобразования между различными представлениями
/// данных макета. Он обеспечивает независимость формата сериализации от внутренней структуры
/// данных и позволяет легко добавлять новые форматы без изменения логики преобразования.
/// </remarks>
public static class LayoutConverter
{
/// <summary>
/// Преобразует менеджер макета в DTO для сериализации.
/// </summary>
/// <param name="manager">Менеджер макета для преобразования.</param>
/// <returns>
/// DTO, содержащий все данные, необходимые для восстановления состояния макета.
/// </returns>
/// <exception cref="ArgumentNullException">Выбрасывается, если <paramref name="manager"/> равен null.</exception>
public static LayoutDto ConvertToDto(LayoutManager manager)
{
if (manager == null)
throw new ArgumentNullException(nameof(manager));
var dto = new LayoutDto
{
ApplicationId = GetApplicationIdentifier()
};
if (manager.Root != null)
{
dto.Root = ConvertElementToDto(manager.Root);
}
dto.FloatingWindows = manager.FloatingWindows
.Select(ConvertWindowToDto)
.ToList();
dto.AutoHidePanels = manager.AutoHidePanels
.Select(ConvertAutoHidePanelToDto)
.ToList();
return dto;
}
/// <summary>
/// Восстанавливает состояние менеджера макета из DTO.
/// </summary>
/// <param name="manager">Менеджер макета для восстановления состояния.</param>
/// <param name="dto">DTO, содержащий данные состояния макета.</param>
/// <param name="contentResolver">
/// Функция разрешения контента по идентификатору.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="manager"/> или <paramref name="dto"/> равен null.
/// </exception>
public static void RestoreFromDto(LayoutManager manager, LayoutDto dto,
Func<string, IDockContent?> contentResolver)
{
if (manager == null)
throw new ArgumentNullException(nameof(manager));
if (dto == null)
throw new ArgumentNullException(nameof(dto));
// Сбрасываем текущее состояние
manager.Reset();
// Восстанавливаем корневой элемент
if (dto.Root != null)
{
manager.Root = ConvertDtoToElement(dto.Root, contentResolver);
}
// Восстанавливаем плавающие окна
foreach (var windowDto in dto.FloatingWindows)
{
var window = new DockWindow
{
X = windowDto.X,
Y = windowDto.Y,
Width = windowDto.Width,
Height = windowDto.Height,
Title = windowDto.Title
};
if (windowDto.Root != null)
{
window.Root = ConvertDtoToElement(windowDto.Root, contentResolver);
}
manager.FloatingWindows.Add(window);
}
// Восстанавливаем автоскрываемые панели
foreach (var panelDto in dto.AutoHidePanels)
{
var content = contentResolver(panelDto.Content.Id);
if (content != null)
{
if (Enum.TryParse<DockSide>(panelDto.Side, out var side))
{
var panel = manager.AddAutoHidePanel(content, side);
panel.Size = panelDto.Size;
panel.IsVisible = panelDto.IsVisible;
panel.SlideOffset = panelDto.SlideOffset;
}
}
}
}
/// <summary>
/// Преобразует элемент дерева компоновки в DTO.
/// </summary>
private static ElementDto ConvertElementToDto(IDockElement element)
{
if (element is DockGroup group)
{
return new GroupDto
{
Id = group.Id,
Type = "group",
Width = group.Width,
Height = group.Height,
MinWidth = group.MinWidth,
MinHeight = group.MinHeight,
First = ConvertElementToDto(group.First),
Second = ConvertElementToDto(group.Second),
Orientation = group.Orientation.ToString(),
SplitRatio = group.SplitRatio
};
}
else if (element is DockLeaf leaf)
{
return new LeafDto
{
Id = leaf.Id,
Type = "leaf",
Width = leaf.Width,
Height = leaf.Height,
MinWidth = leaf.MinWidth,
MinHeight = leaf.MinHeight,
Contents = leaf.Children.Select(ConvertContentToDto).ToList(),
ActiveContentId = leaf.ActiveContent?.Id,
TabPlacement = leaf.TabPlacement.ToString()
};
}
throw new NotSupportedException($"Element type {element.GetType().Name} is not supported for serialization");
}
/// <summary>
/// Преобразует контент в DTO ссылки.
/// </summary>
private static ContentReferenceDto ConvertContentToDto(IDockContent content)
{
var dto = new ContentReferenceDto
{
Id = content.Id,
TypeId = GetContentTypeId(content),
Title = content.Title,
CanClose = content.CanClose
};
// Сохраняем дополнительные свойства, если контент поддерживает сериализацию состояния
if (content is ISerializableContent serializable)
{
dto.Properties = serializable.GetSerializableState();
}
return dto;
}
/// <summary>
/// Преобразует окно в DTO.
/// </summary>
private static WindowDto ConvertWindowToDto(DockWindow window)
{
return new WindowDto
{
Id = window.Id,
X = window.X,
Y = window.Y,
Width = window.Width,
Height = window.Height,
Title = window.Title,
Root = window.Root != null ? ConvertElementToDto(window.Root) : null
};
}
/// <summary>
/// Преобразует автоскрываемую панель в DTO.
/// </summary>
private static AutoHidePanelDto ConvertAutoHidePanelToDto(AutoHidePanel panel)
{
return new AutoHidePanelDto
{
Id = panel.Id,
Content = ConvertContentToDto(panel.Content),
Side = panel.Side.ToString(),
Size = panel.Size,
IsVisible = panel.IsVisible,
SlideOffset = panel.SlideOffset
};
}
/// <summary>
/// Преобразует DTO обратно в элемент дерева.
/// </summary>
private static IDockElement ConvertDtoToElement(ElementDto dto, Func<string, IDockContent?> contentResolver)
{
return dto switch
{
GroupDto groupDto => ConvertGroupDtoToElement(groupDto, contentResolver),
LeafDto leafDto => ConvertLeafDtoToElement(leafDto, contentResolver),
_ => throw new NotSupportedException($"Unsupported DTO type: {dto.Type}")
};
}
/// <summary>
/// Преобразует DTO группы в элемент.
/// </summary>
private static DockGroup ConvertGroupDtoToElement(GroupDto dto, Func<string, IDockContent?> contentResolver)
{
var group = new DockGroup(
ConvertDtoToElement(dto.First, contentResolver),
ConvertDtoToElement(dto.Second, contentResolver),
Enum.Parse<SplitDirection>(dto.Orientation))
{
Width = dto.Width,
Height = dto.Height,
SplitRatio = dto.SplitRatio
};
// Восстанавливаем ID
SetElementId(group, dto.Id);
return group;
}
/// <summary>
/// Преобразует DTO листа в элемент.
/// </summary>
private static DockLeaf ConvertLeafDtoToElement(LeafDto dto, Func<string, IDockContent?> contentResolver)
{
var leaf = new DockLeaf
{
Width = dto.Width,
Height = dto.Height,
MinWidth = dto.MinWidth,
MinHeight = dto.MinHeight,
TabPlacement = Enum.Parse<TabPlacement>(dto.TabPlacement)
};
// Восстанавливаем ID
SetElementId(leaf, dto.Id);
// Восстанавливаем контент
foreach (var contentRef in dto.Contents)
{
var content = contentResolver(contentRef.Id);
if (content != null)
{
// Восстанавливаем состояние контента, если он поддерживает десериализацию
if (content is ISerializableContent serializable)
{
serializable.RestoreFromState(contentRef.Properties);
}
leaf.AddContent(content);
}
}
// Восстанавливаем активный контент
if (dto.ActiveContentId != null)
{
leaf.ActiveContent = leaf.Children.FirstOrDefault(c => c.Id == dto.ActiveContentId);
}
return leaf;
}
/// <summary>
/// Получает идентификатор типа контента.
/// </summary>
private static string GetContentTypeId(IDockContent content)
{
// По умолчанию используем имя типа
return content.GetType().Name;
}
/// <summary>
/// Устанавливает ID элемента через рефлексию.
/// </summary>
private static void SetElementId(object element, string id)
{
var property = element.GetType().GetProperty("Id");
if (property != null && property.CanWrite && property.PropertyType == typeof(string))
{
property.SetValue(element, id);
}
}
/// <summary>
/// Получает идентификатор приложения.
/// </summary>
private static string GetApplicationIdentifier()
{
return Assembly.GetEntryAssembly()?.GetName().Name ?? "UnknownApp";
}
}