345 lines
14 KiB
C#
345 lines
14 KiB
C#
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;
|
||
}
|
||
} |