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;
///
/// Центральный менеджер макета, управляющий всей структурой док-системы.
/// Координирует дерево компоновки, плавающие окна, автоскрываемые панели
/// и предоставляет API для манипуляции макетом. Использует кэширование
/// для оптимизации поиска элементов по идентификатору.
///
public class LayoutManager
{
private readonly ObservableCollection _autoHidePanels = new();
private readonly Dictionary _elementCache = new();
private IDockElement? _root;
///
/// Получает или задает корневой элемент дерева компоновки главного окна.
/// При изменении значения генерируется событие .
///
public IDockElement? Root
{
get => _root;
internal set
{
if (_root != value)
{
_root = value;
LayoutUpdated?.Invoke();
}
}
}
///
/// Получает список активных плавающих окон.
///
public List FloatingWindows { get; } = new();
///
/// Получает коллекцию автоскрываемых панелей.
///
public ReadOnlyObservableCollection AutoHidePanels { get; }
///
/// Получает или задает реестр типов контента.
///
public ContentRegistry? ContentRegistry { get; set; }
///
/// Происходит при изменении структуры дерева компоновки.
///
public event Action? LayoutUpdated;
///
/// Происходит при изменении коллекции автоскрываемых панелей.
///
public event EventHandler? AutoHidePanelsChanged;
///
/// Инициализирует новый экземпляр класса .
///
public LayoutManager()
{
AutoHidePanels = new ReadOnlyObservableCollection(_autoHidePanels);
}
///
/// Добавляет автоскрываемую панель с указанным содержимым к заданной стороне окна.
///
/// Содержимое панели.
/// Сторона окна для прикрепления панели.
/// Созданная автоскрываемая панель.
/// Выбрасывается, когда равен null.
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;
}
///
/// Удаляет автоскрываемую панель из коллекции.
///
/// Панель для удаления.
/// true, если панель была успешно удалена; в противном случае false.
/// Выбрасывается, когда равен null.
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;
}
///
/// Создает документ указанного типа контента с заданным идентификатором.
///
/// Идентификатор типа контента.
/// Уникальный идентификатор документа.
/// Созданный контент или null, если ContentRegistry не установлен или тип контента не зарегистрирован.
public IDockContent? CreateDocument(string contentTypeId, string id)
{
if (ContentRegistry == null || !ContentRegistry.IsRegistered(contentTypeId))
return null;
return ContentRegistry.CreateContent(contentTypeId, id);
}
///
/// Выполняет перемещение элемента в макете относительно целевого элемента.
///
/// Перемещаемый элемент.
/// Целевой элемент, относительно которого выполняется перемещение.
/// Позиция перемещения относительно цели.
/// Если true, контент будет добавлен как документ в центральную область.
/// Выбрасывается, когда равен null.
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();
}
///
/// Удаляет элемент из всех плавающих окон.
///
/// Элемент для удаления.
/// true, если элемент был найден и удален; в противном случае false.
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;
}
///
/// Вставляет элемент в плавающее окно, содержащее целевой элемент.
///
/// Целевой элемент в плавающем окне.
/// Вставляемый элемент.
/// Позиция вставки.
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;
}
}
}
///
/// Определяет, является ли элемент потомком указанного предка.
///
/// Проверяемый элемент.
/// Предполагаемый предок.
/// true, если элемент является потомком предка; в противном случае false.
private bool IsDescendantOf(IDockElement element, IDockElement ancestor)
{
var current = element.Parent;
while (current != null)
{
if (current == ancestor)
return true;
current = current.Parent;
}
return false;
}
///
/// Находит элемент по его идентификатору во всех окнах (главном и плавающих).
/// Использует кэширование для оптимизации повторных поисков.
///
/// Идентификатор элемента для поиска.
/// Найденный элемент или null, если элемент с таким идентификатором не найден.
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;
}
///
/// Рекурсивно ищет элемент по идентификатору в поддереве.
///
/// Корневой узел поддерева для поиска.
/// Идентификатор элемента для поиска.
/// Найденный элемент или null, если элемент не найден.
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;
}
///
/// Сбрасывает макет к состоянию по умолчанию.
/// Очищает корневой элемент, плавающие окна, автоскрываемые панели и кэш.
///
public void Reset()
{
Root = null;
FloatingWindows.Clear();
_autoHidePanels.Clear();
_elementCache.Clear();
LayoutUpdated?.Invoke();
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
}
///
/// Находит элемент по идентификатору в дереве компоновки.
///
/// Идентификатор элемента для поиска.
/// Найденный элемент или null, если элемент с таким идентификатором не найден.
public IDockElement? FindElementById(string id)
{
return FindElementByIdRecursive(Root, id) ??
FloatingWindows.Select(w => FindElementByIdRecursive(w.Root, id))
.FirstOrDefault(result => result != null);
}
///
/// Рекурсивно ищет элемент по идентификатору в поддереве.
///
/// Корневой элемент поддерева для поиска.
/// Идентификатор элемента для поиска.
/// Найденный элемент или null, если элемент не найден.
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;
}
}