369 lines
13 KiB
C#
369 lines
13 KiB
C#
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;
|
||
}
|
||
} |