Files
Lattice/Lattice.Core.Docking/Engine/LayoutManager.cs
2026-01-18 16:33:35 +03:00

369 lines
13 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 System.Collections.ObjectModel;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Lattice.Serialization.Docking")]
namespace Lattice.Core.Docking.Engine;
/// <summary>
/// Расширенный менеджер макета, поддерживающий автоскрываемые панели, группы документов
/// и расширенные операции управления макетом.
/// </summary>
/// <remarks>
/// Этот класс является центральным координатором всей док-системы, управляя деревом компоновки,
/// плавающими окнами, автоскрываемыми панелями и предоставляя API для манипуляции макетом.
/// </remarks>
public class LayoutManager
{
private readonly ObservableCollection<AutoHidePanel> _autoHidePanels = new();
/// <summary>
/// Корневой элемент главного окна IDE.
/// </summary>
public IDockElement? Root { get; internal set; }
/// <summary>
/// Список активных плавающих окон.
/// </summary>
public List<DockWindow> FloatingWindows { get; } = new();
/// <summary>
/// Коллекция автоскрываемых панелей.
/// </summary>
public ReadOnlyObservableCollection<AutoHidePanel> AutoHidePanels { get; }
/// <summary>
/// Реестр типов контента (опционально).
/// </summary>
public Services.ContentRegistry? ContentRegistry { get; set; }
/// <summary>
/// Уведомляет UI, что структура дерева изменилась.
/// </summary>
public event Action? LayoutUpdated;
/// <summary>
/// Уведомляет об изменении в коллекции автоскрываемых панелей.
/// </summary>
public event EventHandler? AutoHidePanelsChanged;
/// <summary>
/// Событие, возникающее при операции перетаскивания элемента.
/// </summary>
public event EventHandler<DragDropEventArgs>? DragDropOperation;
/// <summary>
/// Инициализирует новый экземпляр менеджера макета.
/// </summary>
public LayoutManager()
{
AutoHidePanels = new ReadOnlyObservableCollection<AutoHidePanel>(_autoHidePanels);
}
/// <summary>
/// Добавляет автоскрываемую панель.
/// </summary>
/// <param name="content">Содержимое панели.</param>
/// <param name="side">Сторона для прикрепления.</param>
/// <returns>Созданная автоскрываемая панель.</returns>
public AutoHidePanel AddAutoHidePanel(IDockContent content, DockSide side)
{
var panel = new AutoHidePanel(content, side);
_autoHidePanels.Add(panel);
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
return panel;
}
/// <summary>
/// Удаляет автоскрываемую панель.
/// </summary>
/// <param name="panel">Панель для удаления.</param>
public void RemoveAutoHidePanel(AutoHidePanel panel)
{
if (_autoHidePanels.Remove(panel))
{
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
}
}
/// <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>
public void Move(IDockElement source, IDockElement? target, DockPosition position, bool asDocument = false)
{
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;
// 2. Вставляем в цель
if (target == null)
{
FloatingWindows.Add(new DockWindow { Root = source as IDockElement });
}
else
{
if (IsDescendantOf(target, Root))
{
Root = DockOperations.Insert(target, source, position, Root!);
}
else
{
InsertIntoFloatingWindow(target, source, position);
}
}
LayoutUpdated?.Invoke();
}
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;
}
}
}
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);
return false;
}
/// <summary> Поиск элемента по ID во всех окнах. </summary>
public IDockElement? FindById(string id)
{
var found = FindRecursive(Root, id);
if (found != null) return found;
foreach (var win in FloatingWindows)
{
found = FindRecursive(win.Root, id);
if (found != null) return found;
}
return null;
}
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);
return null;
}
/// <summary>
/// Сбрасывает макет к состоянию по умолчанию.
/// </summary>
public void Reset()
{
Root = null;
FloatingWindows.Clear();
_autoHidePanels.Clear();
LayoutUpdated?.Invoke();
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
}
/// <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>
public IDockElement? FindElementById(string id)
{
return FindElementByIdRecursive(Root, id) ??
FloatingWindows.Select(w => FindElementByIdRecursive(w.Root, id))
.FirstOrDefault(result => result != 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;
}
}
/// <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;
}
}