Files
Lattice/Lattice.Core/Services/LayoutService.cs
2026-01-07 23:54:00 +03:00

229 lines
7.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}
}
}