Files
Lattice/Lattice.Core.Docking/Engine/LayoutManager.cs
2026-02-01 09:26:13 +03:00

345 lines
14 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.Docking.Abstractions;
using Lattice.Core.Docking.Models;
using Lattice.Core.Docking.Services;
using System.Collections.ObjectModel;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Lattice.UI.Docking.WinUI")]
namespace Lattice.Core.Docking.Engine;
/// <summary>
/// Центральный менеджер макета, управляющий всей структурой док-системы.
/// Координирует дерево компоновки, плавающие окна, автоскрываемые панели
/// и предоставляет API для манипуляции макетом. Использует кэширование
/// для оптимизации поиска элементов по идентификатору.
/// </summary>
public class LayoutManager
{
private readonly ObservableCollection<AutoHidePanel> _autoHidePanels = new();
private readonly Dictionary<string, IDockElement> _elementCache = new();
private IDockElement? _root;
/// <summary>
/// Получает или задает корневой элемент дерева компоновки главного окна.
/// При изменении значения генерируется событие <see cref="LayoutUpdated"/>.
/// </summary>
public IDockElement? Root
{
get => _root;
internal set
{
if (_root != value)
{
_root = value;
LayoutUpdated?.Invoke();
}
}
}
/// <summary>
/// Получает список активных плавающих окон.
/// </summary>
public List<DockWindow> FloatingWindows { get; } = new();
/// <summary>
/// Получает коллекцию автоскрываемых панелей.
/// </summary>
public ReadOnlyObservableCollection<AutoHidePanel> AutoHidePanels { get; }
/// <summary>
/// Получает или задает реестр типов контента.
/// </summary>
public ContentRegistry? ContentRegistry { get; set; }
/// <summary>
/// Происходит при изменении структуры дерева компоновки.
/// </summary>
public event Action? LayoutUpdated;
/// <summary>
/// Происходит при изменении коллекции автоскрываемых панелей.
/// </summary>
public event EventHandler? AutoHidePanelsChanged;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="LayoutManager"/>.
/// </summary>
public LayoutManager()
{
AutoHidePanels = new ReadOnlyObservableCollection<AutoHidePanel>(_autoHidePanels);
}
/// <summary>
/// Добавляет автоскрываемую панель с указанным содержимым к заданной стороне окна.
/// </summary>
/// <param name="content">Содержимое панели.</param>
/// <param name="side">Сторона окна для прикрепления панели.</param>
/// <returns>Созданная автоскрываемая панель.</returns>
/// <exception cref="ArgumentNullException">Выбрасывается, когда <paramref name="content"/> равен null.</exception>
public AutoHidePanel AddAutoHidePanel(IDockContent content, DockSide side)
{
if (content == null) throw new ArgumentNullException(nameof(content));
var panel = new AutoHidePanel(content, side);
_autoHidePanels.Add(panel);
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
return panel;
}
/// <summary>
/// Удаляет автоскрываемую панель из коллекции.
/// </summary>
/// <param name="panel">Панель для удаления.</param>
/// <returns>true, если панель была успешно удалена; в противном случае false.</returns>
/// <exception cref="ArgumentNullException">Выбрасывается, когда <paramref name="panel"/> равен null.</exception>
public bool RemoveAutoHidePanel(AutoHidePanel panel)
{
if (panel == null) throw new ArgumentNullException(nameof(panel));
if (_autoHidePanels.Remove(panel))
{
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
return true;
}
return false;
}
/// <summary>
/// Создает документ указанного типа контента с заданным идентификатором.
/// </summary>
/// <param name="contentTypeId">Идентификатор типа контента.</param>
/// <param name="id">Уникальный идентификатор документа.</param>
/// <returns>Созданный контент или null, если ContentRegistry не установлен или тип контента не зарегистрирован.</returns>
public IDockContent? CreateDocument(string contentTypeId, string id)
{
if (ContentRegistry == null || !ContentRegistry.IsRegistered(contentTypeId))
return null;
return ContentRegistry.CreateContent(contentTypeId, id);
}
/// <summary>
/// Выполняет перемещение элемента в макете относительно целевого элемента.
/// </summary>
/// <param name="source">Перемещаемый элемент.</param>
/// <param name="target">Целевой элемент, относительно которого выполняется перемещение.</param>
/// <param name="position">Позиция перемещения относительно цели.</param>
/// <param name="asDocument">Если true, контент будет добавлен как документ в центральную область.</param>
/// <exception cref="ArgumentNullException">Выбрасывается, когда <paramref name="source"/> равен null.</exception>
public void Move(IDockElement source, IDockElement? target,
DockPosition position, bool asDocument = false)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (source == target) return;
// 1. Удаляем источник из текущего местоположения
bool sourceRemoved = false;
if (Root != null && IsDescendantOf(source, Root))
{
Root = DockOperations.Remove(source, Root);
sourceRemoved = true;
}
else
{
sourceRemoved = RemoveFromFloatingWindows(source);
}
if (!sourceRemoved)
{
// Проверяем автоскрываемые панели
var autoHidePanel = _autoHidePanels.FirstOrDefault(p => p.Content == source);
if (autoHidePanel != null)
{
_autoHidePanels.Remove(autoHidePanel);
sourceRemoved = true;
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
}
}
if (!sourceRemoved) return;
// Обновляем кэш - удаляем перемещенный элемент
_elementCache.Remove(source.Id);
// 2. Вставляем в новое место
if (target == null)
{
// Создаем новое плавающее окно
FloatingWindows.Add(new DockWindow { Root = source });
}
else
{
if (Root != null && IsDescendantOf(target, Root))
{
Root = DockOperations.Insert(target, source, position, Root);
}
else
{
InsertIntoFloatingWindow(target, source, position);
}
}
// Обновляем кэш для вставленного элемента
_elementCache[source.Id] = source;
LayoutUpdated?.Invoke();
}
/// <summary>
/// Удаляет элемент из всех плавающих окон.
/// </summary>
/// <param name="element">Элемент для удаления.</param>
/// <returns>true, если элемент был найден и удален; в противном случае false.</returns>
private bool RemoveFromFloatingWindows(IDockElement element)
{
foreach (var win in FloatingWindows.ToArray())
{
if (win.Root != null && IsDescendantOf(element, win.Root))
{
win.Root = DockOperations.Remove(element, win.Root);
if (win.Root == null)
FloatingWindows.Remove(win);
return true;
}
}
return false;
}
/// <summary>
/// Вставляет элемент в плавающее окно, содержащее целевой элемент.
/// </summary>
/// <param name="target">Целевой элемент в плавающем окне.</param>
/// <param name="source">Вставляемый элемент.</param>
/// <param name="position">Позиция вставки.</param>
private void InsertIntoFloatingWindow(IDockElement target, IDockElement source,
DockPosition position)
{
foreach (var win in FloatingWindows)
{
if (win.Root != null && IsDescendantOf(target, win.Root))
{
win.Root = DockOperations.Insert(target, source, position, win.Root);
return;
}
}
}
/// <summary>
/// Определяет, является ли элемент потомком указанного предка.
/// </summary>
/// <param name="element">Проверяемый элемент.</param>
/// <param name="ancestor">Предполагаемый предок.</param>
/// <returns>true, если элемент является потомком предка; в противном случае false.</returns>
private bool IsDescendantOf(IDockElement element, IDockElement ancestor)
{
var current = element.Parent;
while (current != null)
{
if (current == ancestor)
return true;
current = current.Parent;
}
return false;
}
/// <summary>
/// Находит элемент по его идентификатору во всех окнах (главном и плавающих).
/// Использует кэширование для оптимизации повторных поисков.
/// </summary>
/// <param name="id">Идентификатор элемента для поиска.</param>
/// <returns>Найденный элемент или null, если элемент с таким идентификатором не найден.</returns>
public IDockElement? FindById(string id)
{
if (string.IsNullOrEmpty(id)) return null;
// Проверка кэша
if (_elementCache.TryGetValue(id, out var cached))
return cached;
// Поиск в основном дереве
var found = FindRecursive(Root, id);
if (found != null)
{
_elementCache[id] = found;
return found;
}
// Поиск в плавающих окнах
foreach (var win in FloatingWindows)
{
found = FindRecursive(win.Root, id);
if (found != null)
{
_elementCache[id] = found;
return found;
}
}
return null;
}
/// <summary>
/// Рекурсивно ищет элемент по идентификатору в поддереве.
/// </summary>
/// <param name="node">Корневой узел поддерева для поиска.</param>
/// <param name="id">Идентификатор элемента для поиска.</param>
/// <returns>Найденный элемент или null, если элемент не найден.</returns>
private IDockElement? FindRecursive(IDockElement? node, string id)
{
if (node == null) return null;
if (node.Id == id) return node;
if (node is DockGroup g)
return FindRecursive(g.First, id) ?? FindRecursive(g.Second, id);
return null;
}
/// <summary>
/// Сбрасывает макет к состоянию по умолчанию.
/// Очищает корневой элемент, плавающие окна, автоскрываемые панели и кэш.
/// </summary>
public void Reset()
{
Root = null;
FloatingWindows.Clear();
_autoHidePanels.Clear();
_elementCache.Clear();
LayoutUpdated?.Invoke();
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Находит элемент по идентификатору в дереве компоновки.
/// </summary>
/// <param name="id">Идентификатор элемента для поиска.</param>
/// <returns>Найденный элемент или null, если элемент с таким идентификатором не найден.</returns>
public IDockElement? FindElementById(string id)
{
return FindElementByIdRecursive(Root, id) ??
FloatingWindows.Select(w => FindElementByIdRecursive(w.Root, id))
.FirstOrDefault(result => result != null);
}
/// <summary>
/// Рекурсивно ищет элемент по идентификатору в поддереве.
/// </summary>
/// <param name="element">Корневой элемент поддерева для поиска.</param>
/// <param name="id">Идентификатор элемента для поиска.</param>
/// <returns>Найденный элемент или null, если элемент не найден.</returns>
private IDockElement? FindElementByIdRecursive(IDockElement? element, string id)
{
if (element == null) return null;
if (element.Id == id) return element;
if (element is DockGroup group)
{
return FindElementByIdRecursive(group.First, id) ??
FindElementByIdRecursive(group.Second, id);
}
return null;
}
}