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; /// /// Центральный менеджер макета, управляющий всей структурой док-системы. /// Координирует дерево компоновки, плавающие окна, автоскрываемые панели /// и предоставляет API для манипуляции макетом. /// /// /// Этот класс является основным координатором док-системы. Он управляет: /// /// Деревом компоновки главного окна /// Коллекцией плавающих окон /// Коллекцией автоскрываемых панелей /// Реестром типов контента /// /// Все изменения в структуре макета должны выполняться через методы этого класса. /// public class LayoutManager { private readonly ObservableCollection _autoHidePanels = new(); private IDockElement? _root; /// /// Получает или задает корневой элемент дерева компоновки главного окна. /// /// /// Корневой элемент или null, если макет пуст. /// /// /// При изменении этого свойства генерируется событие . /// public IDockElement? Root { get => _root; internal set { if (_root != value) { _root = value; LayoutUpdated?.Invoke(); } } } /// /// Получает список активных плавающих окон. /// /// /// Коллекция объектов , представляющих плавающие окна. /// public List FloatingWindows { get; } = new(); /// /// Получает коллекцию автоскрываемых панелей. /// /// /// Доступная только для чтения коллекция объектов . /// public ReadOnlyObservableCollection AutoHidePanels { get; } /// /// Получает или задает реестр типов контента. /// /// /// Реестр типов контента или null, если реестр не установлен. /// public Services.ContentRegistry? ContentRegistry { get; set; } /// /// Происходит при изменении структуры дерева компоновки. /// /// /// Событие генерируется при любых изменениях в дереве компоновки, /// включая добавление, удаление или перемещение элементов. /// public event Action? LayoutUpdated; /// /// Происходит при изменении коллекции автоскрываемых панелей. /// public event EventHandler? AutoHidePanelsChanged; /// /// Инициализирует новый экземпляр класса . /// public LayoutManager() { AutoHidePanels = new ReadOnlyObservableCollection(_autoHidePanels); } /// /// Добавляет автоскрываемую панель с указанным содержимым к заданной стороне окна. /// /// Содержимое панели. /// Сторона окна для прикрепления панели. /// /// Созданная автоскрываемая панель. /// /// /// Выбрасывается, когда равен null. /// 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; } /// /// Удаляет автоскрываемую панель из коллекции. /// /// Панель для удаления. /// /// true, если панель была успешно удалена; в противном случае false. /// /// /// Выбрасывается, когда равен null. /// 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; } /// /// Создает документ указанного типа контента с заданным идентификатором. /// /// Идентификатор типа контента. /// Уникальный идентификатор документа. /// /// Созданный контент или null, если ContentRegistry не установлен /// или тип контента не зарегистрирован. /// public IDockContent? CreateDocument(string contentTypeId, string id) { if (ContentRegistry == null || !ContentRegistry.IsRegistered(contentTypeId)) return null; return ContentRegistry.CreateContent(contentTypeId, id); } /// /// Выполняет перемещение элемента в макете относительно целевого элемента. /// /// Перемещаемый элемент. /// Целевой элемент, относительно которого выполняется перемещение. /// Позиция перемещения относительно цели. /// /// Если true, контент будет добавлен как документ в центральную область. /// В текущей реализации этот параметр не используется. /// /// /// Выбрасывается, когда равен null. /// /// /// Метод выполняет следующие шаги: /// /// Удаляет источник из текущего местоположения /// Вставляет источник в новое местоположение относительно цели /// Обновляет структуру дерева компоновки /// /// Если равен null, элемент помещается в новое плавающее окно. /// 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; // 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); } } LayoutUpdated?.Invoke(); } /// /// Удаляет элемент из всех плавающих окон. /// /// Элемент для удаления. /// /// true, если элемент был найден и удален; в противном случае false. /// 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; } } } /// /// Определяет, является ли элемент потомком указанного предка. /// /// Проверяемый элемент. /// Предполагаемый предок. /// /// true, если элемент является потомком предка; в противном случае false. /// 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; } /// /// Находит элемент по его идентификатору во всех окнах (главном и плавающих). /// /// Идентификатор элемента для поиска. /// /// Найденный элемент или null, если элемент с таким идентификатором не найден. /// public IDockElement? FindById(string id) { if (string.IsNullOrEmpty(id)) return null; 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; } /// /// Рекурсивно ищет элемент по идентификатору в поддереве. /// /// Корневой узел поддерева для поиска. /// Идентификатор элемента для поиска. /// /// Найденный элемент или null, если элемент не найден. /// 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; } /// /// Сбрасывает макет к состоянию по умолчанию. /// /// /// Метод выполняет следующие действия: /// /// Очищает корневой элемент /// Закрывает все плавающие окна /// Удаляет все автоскрываемые панели /// Генерирует соответствующие события /// /// public void Reset() { Root = null; FloatingWindows.Clear(); _autoHidePanels.Clear(); LayoutUpdated?.Invoke(); AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty); } /// /// Находит элемент по идентификатору в дереве компоновки. /// /// Идентификатор элемента для поиска. /// /// Найденный элемент или null, если элемент с таким идентификатором не найден. /// public IDockElement? FindElementById(string id) { return FindElementByIdRecursive(Root, id) ?? FloatingWindows.Select(w => FindElementByIdRecursive(w.Root, id)) .FirstOrDefault(result => result != null); } /// /// Рекурсивно ищет элемент по идентификатору в поддереве. /// /// Корневой элемент поддерева для поиска. /// Идентификатор элемента для поиска. /// /// Найденный элемент или 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; } }