Добавлен Studio

This commit is contained in:
2026-01-07 23:52:02 +03:00
parent ca5d912c9c
commit c3770c789b
19 changed files with 668 additions and 51 deletions

View File

@@ -0,0 +1,228 @@
using Lattice.Core.Abstractions;
using Lattice.Core.Models;
using Lattice.Core.Models.Enums;
using Lattice.Core.Persistence;
using Microsoft.Extensions.Logging;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Lattice.Core.Services;
/// <summary>
/// Реализация сервиса управления макетом.
/// </summary>
public class LayoutService : ILayoutService
{
private readonly ILogger? _logger;
private LayoutNode? _root;
/// <inheritdoc/>
public LayoutNode? Root => _root;
/// <inheritdoc/>
public event EventHandler? LayoutUpdated;
public LayoutService(ILogger<LayoutService>? logger = null)
{
_logger = logger;
}
/// <inheritdoc/>
public void Dock(LayoutNode source, LayoutNode target, DockDirection direction)
{
if (source == target) return;
_logger?.LogDebug("Начало трансформации дерева: {Source} -> {Target} ({Direction})", source.Name, target.Name, direction);
// 1. Извлекаем источник из его текущего места в дереве
Remove(source);
// 2. Если докинг в центр — это логика объединения (например, в TabView)
// В рамках Core это может означать добавление в тот же контейнер
if (direction == DockDirection.Center)
{
HandleCenterDock(source, target);
}
else
{
// 3. Создаем разделение (Split)
HandleSideDock(source, target, direction);
}
LayoutUpdated?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Логика добавления элемента в центральную часть (вкладки).
/// </summary>
private void HandleCenterDock(LayoutNode source, LayoutNode target)
{
if (target.Parent is SplitContainerNode parent)
{
parent.AddChild(source);
}
else if (target == _root)
{
// Если таргет - корень, и мы докаем в центр, создаем контейнер по умолчанию
var container = new SplitContainerNode(SplitOrientation.Horizontal);
_root = container;
container.AddChild(target);
container.AddChild(source);
}
}
/// <summary>
/// Логика разделения существующей области на две (Side Dock).
/// </summary>
private void HandleSideDock(LayoutNode source, LayoutNode target, DockDirection direction)
{
var orientation = (direction == DockDirection.Left || direction == DockDirection.Right)
? SplitOrientation.Horizontal
: SplitOrientation.Vertical;
var parent = target.Parent as SplitContainerNode;
// Создаем новый сплиттер, который заменит target
var newContainer = new SplitContainerNode(orientation);
if (parent != null)
{
// Заменяем target на новый контейнер в списке детей родителя
int index = parent.Children.IndexOf(target);
parent.Children[index] = newContainer;
newContainer.Parent = parent;
}
else
{
// Если родителя нет, значит target был корнем
_root = newContainer;
}
// Настраиваем порядок в новом сплиттере
if (direction == DockDirection.Left || direction == DockDirection.Top)
{
newContainer.AddChild(source);
newContainer.AddChild(target);
}
else
{
newContainer.AddChild(target);
newContainer.AddChild(source);
}
// Корректируем размеры (например, делим пополам)
source.WidthValue = 0.5;
target.WidthValue = 0.5;
source.IsWidthStar = true;
target.IsWidthStar = true;
}
/// <inheritdoc/>
public void Remove(LayoutNode node)
{
if (node.Parent is SplitContainerNode parent)
{
parent.Children.Remove(node);
node.Parent = null;
// Если в контейнере остался один элемент — убираем лишнюю вложенность
if (parent.Children.Count == 1)
{
CollapseContainer(parent);
}
}
else if (node == _root)
{
_root = null;
}
LayoutUpdated?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Убирает ненужные контейнеры, если в них остался только один элемент.
/// </summary>
private void CollapseContainer(SplitContainerNode container)
{
var lastChild = container.Children[0];
var parent = container.Parent as SplitContainerNode;
if (parent != null)
{
int index = parent.Children.IndexOf(container);
parent.Children[index] = lastChild;
lastChild.Parent = parent;
}
else
{
_root = lastChild;
lastChild.Parent = null;
}
}
/// <inheritdoc/>
public string SaveLayout()
{
if (_root == null) return string.Empty;
var options = GetJsonOptions();
try
{
string json = JsonSerializer.Serialize(_root, options);
_logger?.LogInformation("Макет успешно экспортирован в JSON. Длина: {Length}", json.Length);
return json;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Ошибка при сохранении макета Lattice");
return string.Empty;
}
}
/// <inheritdoc/>
public void LoadLayout(string jsonData)
{
if (string.IsNullOrWhiteSpace(jsonData)) return;
var options = GetJsonOptions();
try
{
var importedRoot = JsonSerializer.Deserialize<LayoutNode>(jsonData, options);
if (importedRoot != null)
{
// При загрузке нужно восстановить связи Parent, так как они не сериализуются (циклические ссылки)
RestoreParentLinks(importedRoot, null);
_root = importedRoot;
_logger?.LogInformation("Макет успешно загружен. Корневой узел: {Id}", _root.Id);
LayoutUpdated?.Invoke(this, EventArgs.Empty);
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "Ошибка при десериализации макета Lattice");
}
}
private JsonSerializerOptions GetJsonOptions()
{
return new JsonSerializerOptions
{
WriteIndented = true,
Converters = { new LayoutJsonConverter() },
// Игнорируем циклы, так как мы восстановим Parent вручную
ReferenceHandler = ReferenceHandler.IgnoreCycles
};
}
private void RestoreParentLinks(LayoutNode node, LayoutNode? parent)
{
node.Parent = parent;
if (node is SplitContainerNode container)
{
foreach (var child in container.Children)
{
RestoreParentLinks(child, container);
}
}
}
}