Доработан Docking

This commit is contained in:
2026-01-27 05:17:35 +03:00
parent 33abd94f6e
commit 584df249f6
99 changed files with 2270 additions and 12792 deletions

View File

@@ -8,54 +8,87 @@ using System.Runtime.CompilerServices;
namespace Lattice.Core.Docking.Engine;
/// <summary>
/// Расширенный менеджер макета, поддерживающий автоскрываемые панели, группы документов
/// и расширенные операции управления макетом.
/// Центральный менеджер макета, управляющий всей структурой док-системы.
/// Координирует дерево компоновки, плавающие окна, автоскрываемые панели
/// и предоставляет API для манипуляции макетом.
/// </summary>
/// <remarks>
/// Этот класс является центральным координатором всей док-системы, управляя деревом компоновки,
/// плавающими окнами, автоскрываемыми панелями и предоставляя API для манипуляции макетом.
/// Этот класс является основным координатором док-системы. Он управляет:
/// <list type="bullet">
/// <item>Деревом компоновки главного окна</item>
/// <item>Коллекцией плавающих окон</item>
/// <item>Коллекцией автоскрываемых панелей</item>
/// <item>Реестром типов контента</item>
/// </list>
/// Все изменения в структуре макета должны выполняться через методы этого класса.
/// </remarks>
public class LayoutManager
{
private readonly ObservableCollection<AutoHidePanel> _autoHidePanels = new();
private IDockElement? _root;
/// <summary>
/// Корневой элемент главного окна IDE.
/// Получает или задает корневой элемент дерева компоновки главного окна.
/// </summary>
public IDockElement? Root { get; internal set; }
/// <value>
/// Корневой элемент или null, если макет пуст.
/// </value>
/// <remarks>
/// При изменении этого свойства генерируется событие <see cref="LayoutUpdated"/>.
/// </remarks>
public IDockElement? Root
{
get => _root;
internal set
{
if (_root != value)
{
_root = value;
LayoutUpdated?.Invoke();
}
}
}
/// <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; }
/// <summary>
/// Уведомляет UI, что структура дерева изменилась.
/// Происходит при изменении структуры дерева компоновки.
/// </summary>
/// <remarks>
/// Событие генерируется при любых изменениях в дереве компоновки,
/// включая добавление, удаление или перемещение элементов.
/// </remarks>
public event Action? LayoutUpdated;
/// <summary>
/// Уведомляет об изменении в коллекции автоскрываемых панелей.
/// Происходит при изменении коллекции автоскрываемых панелей.
/// </summary>
public event EventHandler? AutoHidePanelsChanged;
/// <summary>
/// Событие, возникающее при операции перетаскивания элемента.
/// </summary>
public event EventHandler<DragDropEventArgs>? DragDropOperation;
/// <summary>
/// Инициализирует новый экземпляр менеджера макета.
/// Инициализирует новый экземпляр класса <see cref="LayoutManager"/>.
/// </summary>
public LayoutManager()
{
@@ -63,13 +96,20 @@ public class LayoutManager
}
/// <summary>
/// Добавляет автоскрываемую панель.
/// Добавляет автоскрываемую панель с указанным содержимым к заданной стороне окна.
/// </summary>
/// <param name="content">Содержимое панели.</param>
/// <param name="side">Сторона для прикрепления.</param>
/// <returns>Созданная автоскрываемая панель.</returns>
/// <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);
@@ -77,23 +117,36 @@ public class LayoutManager
}
/// <summary>
/// Удаляет автоскрываемую панель.
/// Удаляет автоскрываемую панель из коллекции.
/// </summary>
/// <param name="panel">Панель для удаления.</param>
public void RemoveAutoHidePanel(AutoHidePanel panel)
/// <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>
/// <returns>
/// Созданный контент или null, если ContentRegistry не установлен
/// или тип контента не зарегистрирован.
/// </returns>
public IDockContent? CreateDocument(string contentTypeId, string id)
{
if (ContentRegistry == null || !ContentRegistry.IsRegistered(contentTypeId))
@@ -103,16 +156,31 @@ public class LayoutManager
}
/// <summary>
/// Основной метод перемещения элементов в макете.
/// Выполняет перемещение элемента в макете относительно целевого элемента.
/// </summary>
/// <param name="source">Что перетаскиваем.</param>
/// <param name="target">Куда приземляем.</param>
/// <param name="position">Позиция относительно цели.</param>
/// <param name="source">Перемещаемый элемент.</param>
/// <param name="target">Целевой элемент, относительно которого выполняется перемещение.</param>
/// <param name="position">Позиция перемещения относительно цели.</param>
/// <param name="asDocument">
/// Если true, контент будет добавлен как документ в центральную область.
/// В текущей реализации этот параметр не используется.
/// </param>
public void Move(IDockElement source, IDockElement? target, DockPosition position, bool asDocument = false)
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="source"/> равен null.
/// </exception>
/// <remarks>
/// Метод выполняет следующие шаги:
/// <list type="number">
/// <item>Удаляет источник из текущего местоположения</item>
/// <item>Вставляет источник в новое местоположение относительно цели</item>
/// <item>Обновляет структуру дерева компоновки</item>
/// </list>
/// Если <paramref name="target"/> равен null, элемент помещается в новое плавающее окно.
/// </remarks>
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. Удаляем источник из текущего местоположения
@@ -145,13 +213,14 @@ public class LayoutManager
// 2. Вставляем в цель
if (target == null)
{
FloatingWindows.Add(new DockWindow { Root = source as IDockElement });
// Создаем новое плавающее окно
FloatingWindows.Add(new DockWindow { Root = source });
}
else
{
if (IsDescendantOf(target, Root))
if (Root != null && IsDescendantOf(target, Root))
{
Root = DockOperations.Insert(target, source, position, Root!);
Root = DockOperations.Insert(target, source, position, Root);
}
else
{
@@ -162,6 +231,13 @@ public class LayoutManager
LayoutUpdated?.Invoke();
}
/// <summary>
/// Удаляет элемент из всех плавающих окон.
/// </summary>
/// <param name="element">Элемент для удаления.</param>
/// <returns>
/// true, если элемент был найден и удален; в противном случае false.
/// </returns>
private bool RemoveFromFloatingWindows(IDockElement element)
{
foreach (var win in FloatingWindows.ToArray())
@@ -177,7 +253,14 @@ public class LayoutManager
return false;
}
private void InsertIntoFloatingWindow(IDockElement target, IDockElement source, DockPosition position)
/// <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)
{
@@ -189,6 +272,14 @@ public class LayoutManager
}
}
/// <summary>
/// Определяет, является ли элемент потомком указанного предка.
/// </summary>
/// <param name="element">Проверяемый элемент.</param>
/// <param name="ancestor">Предполагаемый предок.</param>
/// <returns>
/// true, если элемент является потомком предка; в противном случае false.
/// </returns>
private bool IsDescendantOf(IDockElement element, IDockElement ancestor)
{
if (element == ancestor) return true;
@@ -197,9 +288,17 @@ public class LayoutManager
return false;
}
/// <summary> Поиск элемента по ID во всех окнах. </summary>
/// <summary>
/// Находит элемент по его идентификатору во всех окнах (главном и плавающих).
/// </summary>
/// <param name="id">Идентификатор элемента для поиска.</param>
/// <returns>
/// Найденный элемент или null, если элемент с таким идентификатором не найден.
/// </returns>
public IDockElement? FindById(string id)
{
if (string.IsNullOrEmpty(id)) return null;
var found = FindRecursive(Root, id);
if (found != null) return found;
@@ -211,16 +310,37 @@ public class LayoutManager
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 || node.Id == id) return node;
if (node is DockGroup g) return FindRecursive(g.First, id) ?? FindRecursive(g.Second, 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>
/// <remarks>
/// Метод выполняет следующие действия:
/// <list type="bullet">
/// <item>Очищает корневой элемент</item>
/// <item>Закрывает все плавающие окна</item>
/// <item>Удаляет все автоскрываемые панели</item>
/// <item>Генерирует соответствующие события</item>
/// </list>
/// </remarks>
public void Reset()
{
Root = null;
@@ -231,86 +351,12 @@ public class LayoutManager
}
/// <summary>
/// Обрабатывает операцию перетаскивания между элементами.
/// </summary>
/// <param name="source">Источник перетаскивания.</param>
/// <param name="target">Цель сброса.</param>
/// <param name="position">Позиция сброса относительно цели.</param>
/// <param name="data">Данные перетаскивания.</param>
/// <returns>true, если операция успешно выполнена; иначе false.</returns>
public bool HandleDragDrop(IDockElement source, IDockElement target,
DockPosition position, object data)
{
try
{
if (source == target)
return false;
// Определяем тип операции на основе данных
if (data is ContentDragData contentData)
{
return HandleContentDragDrop(contentData, target, position);
}
else if (data is DockElementDragData elementData)
{
return HandleElementDragDrop(elementData, target, position);
}
return false;
}
catch (Exception ex)
{
DragDropOperation?.Invoke(this, new DragDropEventArgs(
source, target, position, false, ex.Message));
return false;
}
}
private bool HandleContentDragDrop(ContentDragData data, IDockElement target, DockPosition position)
{
// Находим исходный контейнер с контентом
var sourceContainer = FindElementById(data.ElementId) as IDockContainer;
if (sourceContainer == null)
return false;
// Находим контент
var content = sourceContainer.Children.FirstOrDefault(c => c.Id == data.ContentId);
if (content == null)
return false;
if (target is IDockContainer targetContainer && position == DockPosition.Center)
{
// Объединение вкладок
sourceContainer.RemoveContent(content);
targetContainer.AddContent(content);
DragDropOperation?.Invoke(this, new DragDropEventArgs(
sourceContainer as IDockElement ?? sourceContainer as IDockElement,
target, position, true, "Content merged"));
return true;
}
return false;
}
private bool HandleElementDragDrop(DockElementDragData data, IDockElement target, DockPosition position)
{
// Находим перетаскиваемый элемент
var sourceElement = FindElementById(data.ElementId);
if (sourceElement == null)
return false;
// Выполняем перемещение
Move(sourceElement, target, position);
DragDropOperation?.Invoke(this, new DragDropEventArgs(
sourceElement, target, position, true, "Element moved"));
return true;
}
/// <summary>
/// Находит элемент по идентификатору.
/// Находит элемент по идентификатору в дереве компоновки.
/// </summary>
/// <param name="id">Идентификатор элемента для поиска.</param>
/// <returns>
/// Найденный элемент или null, если элемент с таким идентификатором не найден.
/// </returns>
public IDockElement? FindElementById(string id)
{
return FindElementByIdRecursive(Root, id) ??
@@ -318,6 +364,14 @@ public class LayoutManager
.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;
@@ -331,39 +385,4 @@ public class LayoutManager
return null;
}
}
/// <summary>
/// Аргументы события операции перетаскивания.
/// </summary>
public class DragDropEventArgs : EventArgs
{
/// <summary> Источник перетаскивания. </summary>
public IDockElement Source { get; }
/// <summary> Цель сброса. </summary>
public IDockElement Target { get; }
/// <summary> Позиция сброса. </summary>
public DockPosition Position { get; }
/// <summary> Показывает, была ли операция успешной. </summary>
public bool Success { get; }
/// <summary> Сообщение о результате операции. </summary>
public string Message { get; }
/// <summary> Время выполнения операции. </summary>
public DateTime Timestamp { get; }
public DragDropEventArgs(IDockElement source, IDockElement target,
DockPosition position, bool success, string message)
{
Source = source;
Target = target;
Position = position;
Success = success;
Message = message;
Timestamp = DateTime.UtcNow;
}
}