229 lines
7.6 KiB
C#
229 lines
7.6 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|
||
}
|