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; /// /// Реализация сервиса управления макетом. /// public class LayoutService : ILayoutService { private readonly ILogger? _logger; private LayoutNode? _root; /// public LayoutNode? Root => _root; /// public event EventHandler? LayoutUpdated; public LayoutService(ILogger? logger = null) { _logger = logger; } /// 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); } /// /// Логика добавления элемента в центральную часть (вкладки). /// 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); } } /// /// Логика разделения существующей области на две (Side Dock). /// 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; } /// 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); } /// /// Убирает ненужные контейнеры, если в них остался только один элемент. /// 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; } } /// 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; } } /// public void LoadLayout(string jsonData) { if (string.IsNullOrWhiteSpace(jsonData)) return; var options = GetJsonOptions(); try { var importedRoot = JsonSerializer.Deserialize(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); } } } }