Доработан winui

This commit is contained in:
2026-02-01 09:26:13 +03:00
parent 584df249f6
commit e8b4cb9881
26 changed files with 1842 additions and 2373 deletions

View File

@@ -1,41 +1,29 @@
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.Serialization.Docking")]
[assembly: InternalsVisibleTo("Lattice.UI.Docking.WinUI")]
namespace Lattice.Core.Docking.Engine;
/// <summary>
/// Центральный менеджер макета, управляющий всей структурой док-системы.
/// Координирует дерево компоновки, плавающие окна, автоскрываемые панели
/// и предоставляет API для манипуляции макетом.
/// и предоставляет API для манипуляции макетом. Использует кэширование
/// для оптимизации поиска элементов по идентификатору.
/// </summary>
/// <remarks>
/// Этот класс является основным координатором док-системы. Он управляет:
/// <list type="bullet">
/// <item>Деревом компоновки главного окна</item>
/// <item>Коллекцией плавающих окон</item>
/// <item>Коллекцией автоскрываемых панелей</item>
/// <item>Реестром типов контента</item>
/// </list>
/// Все изменения в структуре макета должны выполняться через методы этого класса.
/// </remarks>
public class LayoutManager
{
private readonly ObservableCollection<AutoHidePanel> _autoHidePanels = new();
private readonly Dictionary<string, IDockElement> _elementCache = new();
private IDockElement? _root;
/// <summary>
/// Получает или задает корневой элемент дерева компоновки главного окна.
/// При изменении значения генерируется событие <see cref="LayoutUpdated"/>.
/// </summary>
/// <value>
/// Корневой элемент или null, если макет пуст.
/// </value>
/// <remarks>
/// При изменении этого свойства генерируется событие <see cref="LayoutUpdated"/>.
/// </remarks>
public IDockElement? Root
{
get => _root;
@@ -52,34 +40,21 @@ public class LayoutManager
/// <summary>
/// Получает список активных плавающих окон.
/// </summary>
/// <value>
/// Коллекция объектов <see cref="DockWindow"/>, представляющих плавающие окна.
/// </value>
public List<DockWindow> FloatingWindows { get; } = new();
/// <summary>
/// Получает коллекцию автоскрываемых панелей.
/// </summary>
/// <value>
/// Доступная только для чтения коллекция объектов <see cref="AutoHidePanel"/>.
/// </value>
public ReadOnlyObservableCollection<AutoHidePanel> AutoHidePanels { get; }
/// <summary>
/// Получает или задает реестр типов контента.
/// </summary>
/// <value>
/// Реестр типов контента или null, если реестр не установлен.
/// </value>
public Services.ContentRegistry? ContentRegistry { get; set; }
public ContentRegistry? ContentRegistry { get; set; }
/// <summary>
/// Происходит при изменении структуры дерева компоновки.
/// </summary>
/// <remarks>
/// Событие генерируется при любых изменениях в дереве компоновки,
/// включая добавление, удаление или перемещение элементов.
/// </remarks>
public event Action? LayoutUpdated;
/// <summary>
@@ -100,12 +75,8 @@ public class LayoutManager
/// </summary>
/// <param name="content">Содержимое панели.</param>
/// <param name="side">Сторона окна для прикрепления панели.</param>
/// <returns>
/// Созданная автоскрываемая панель.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="content"/> равен null.
/// </exception>
/// <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));
@@ -120,12 +91,8 @@ public class LayoutManager
/// Удаляет автоскрываемую панель из коллекции.
/// </summary>
/// <param name="panel">Панель для удаления.</param>
/// <returns>
/// true, если панель была успешно удалена; в противном случае false.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="panel"/> равен null.
/// </exception>
/// <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));
@@ -143,10 +110,7 @@ public class LayoutManager
/// </summary>
/// <param name="contentTypeId">Идентификатор типа контента.</param>
/// <param name="id">Уникальный идентификатор документа.</param>
/// <returns>
/// Созданный контент или null, если ContentRegistry не установлен
/// или тип контента не зарегистрирован.
/// </returns>
/// <returns>Созданный контент или null, если ContentRegistry не установлен или тип контента не зарегистрирован.</returns>
public IDockContent? CreateDocument(string contentTypeId, string id)
{
if (ContentRegistry == null || !ContentRegistry.IsRegistered(contentTypeId))
@@ -161,22 +125,8 @@ public class LayoutManager
/// <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>
/// <remarks>
/// Метод выполняет следующие шаги:
/// <list type="number">
/// <item>Удаляет источник из текущего местоположения</item>
/// <item>Вставляет источник в новое местоположение относительно цели</item>
/// <item>Обновляет структуру дерева компоновки</item>
/// </list>
/// Если <paramref name="target"/> равен null, элемент помещается в новое плавающее окно.
/// </remarks>
/// <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)
{
@@ -210,7 +160,10 @@ public class LayoutManager
if (!sourceRemoved) return;
// 2. Вставляем в цель
// Обновляем кэш - удаляем перемещенный элемент
_elementCache.Remove(source.Id);
// 2. Вставляем в новое место
if (target == null)
{
// Создаем новое плавающее окно
@@ -228,6 +181,9 @@ public class LayoutManager
}
}
// Обновляем кэш для вставленного элемента
_elementCache[source.Id] = source;
LayoutUpdated?.Invoke();
}
@@ -235,9 +191,7 @@ public class LayoutManager
/// Удаляет элемент из всех плавающих окон.
/// </summary>
/// <param name="element">Элемент для удаления.</param>
/// <returns>
/// true, если элемент был найден и удален; в противном случае false.
/// </returns>
/// <returns>true, если элемент был найден и удален; в противном случае false.</returns>
private bool RemoveFromFloatingWindows(IDockElement element)
{
foreach (var win in FloatingWindows.ToArray())
@@ -277,36 +231,52 @@ public class LayoutManager
/// </summary>
/// <param name="element">Проверяемый элемент.</param>
/// <param name="ancestor">Предполагаемый предок.</param>
/// <returns>
/// true, если элемент является потомком предка; в противном случае false.
/// </returns>
/// <returns>true, если элемент является потомком предка; в противном случае false.</returns>
private bool IsDescendantOf(IDockElement element, IDockElement ancestor)
{
if (element == ancestor) return true;
if (ancestor is DockGroup group)
return IsDescendantOf(element, group.First) || IsDescendantOf(element, group.Second);
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>
/// <returns>Найденный элемент или null, если элемент с таким идентификатором не найден.</returns>
public IDockElement? FindById(string id)
{
if (string.IsNullOrEmpty(id)) return null;
var found = FindRecursive(Root, id);
if (found != null) return found;
// Проверка кэша
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) return found;
if (found != null)
{
_elementCache[id] = found;
return found;
}
}
return null;
}
@@ -315,9 +285,7 @@ public class LayoutManager
/// </summary>
/// <param name="node">Корневой узел поддерева для поиска.</param>
/// <param name="id">Идентификатор элемента для поиска.</param>
/// <returns>
/// Найденный элемент или null, если элемент не найден.
/// </returns>
/// <returns>Найденный элемент или null, если элемент не найден.</returns>
private IDockElement? FindRecursive(IDockElement? node, string id)
{
if (node == null) return null;
@@ -331,21 +299,14 @@ public class LayoutManager
/// <summary>
/// Сбрасывает макет к состоянию по умолчанию.
/// Очищает корневой элемент, плавающие окна, автоскрываемые панели и кэш.
/// </summary>
/// <remarks>
/// Метод выполняет следующие действия:
/// <list type="bullet">
/// <item>Очищает корневой элемент</item>
/// <item>Закрывает все плавающие окна</item>
/// <item>Удаляет все автоскрываемые панели</item>
/// <item>Генерирует соответствующие события</item>
/// </list>
/// </remarks>
public void Reset()
{
Root = null;
FloatingWindows.Clear();
_autoHidePanels.Clear();
_elementCache.Clear();
LayoutUpdated?.Invoke();
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
}
@@ -354,9 +315,7 @@ public class LayoutManager
/// Находит элемент по идентификатору в дереве компоновки.
/// </summary>
/// <param name="id">Идентификатор элемента для поиска.</param>
/// <returns>
/// Найденный элемент или null, если элемент с таким идентификатором не найден.
/// </returns>
/// <returns>Найденный элемент или null, если элемент с таким идентификатором не найден.</returns>
public IDockElement? FindElementById(string id)
{
return FindElementByIdRecursive(Root, id) ??
@@ -369,9 +328,7 @@ public class LayoutManager
/// </summary>
/// <param name="element">Корневой элемент поддерева для поиска.</param>
/// <param name="id">Идентификатор элемента для поиска.</param>
/// <returns>
/// Найденный элемент или null, если элемент не найден.
/// </returns>
/// <returns>Найденный элемент или null, если элемент не найден.</returns>
private IDockElement? FindElementByIdRecursive(IDockElement? element, string id)
{
if (element == null) return null;