Доработан winui
This commit is contained in:
@@ -11,6 +11,12 @@ public interface IDockContent
|
||||
/// </summary>
|
||||
string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Устанавливает идентификатор контента.
|
||||
/// </summary>
|
||||
/// <param name="id">Новый идентификатор.</param>
|
||||
void SetId(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Получает заголовок, отображаемый пользователю на вкладке.
|
||||
/// </summary>
|
||||
|
||||
@@ -114,7 +114,16 @@ public static class DockOperations
|
||||
return root;
|
||||
}
|
||||
|
||||
// Если target был корнем, новая группа становится новым корнем
|
||||
if (target == root)
|
||||
{
|
||||
newGroup.Parent = null;
|
||||
return newGroup;
|
||||
}
|
||||
|
||||
// Эта точка недостижима при правильном использовании,
|
||||
// но добавляем для безопасности
|
||||
newGroup.Parent = null;
|
||||
return newGroup; // Новая группа стала корнем
|
||||
return newGroup;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -1,149 +1,65 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Lattice.Core.Docking.Abstractions;
|
||||
|
||||
namespace Lattice.Core.Docking.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Представляет автоскрываемую панель, которая может быть прикреплена к одной из сторон окна.
|
||||
/// Автоскрываемые панели скрываются, оставляя видимой только полоску-заголовок,
|
||||
/// и разворачиваются при наведении курсора или клике.
|
||||
/// Автоскрываемые панели скрываются, оставляя видимой только заголовок, и разворачиваются при наведении курсора или клике.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Автоскрываемые панели являются важным элементом современных IDE-подобных приложений,
|
||||
/// позволяя экономить пространство экрана при сохранении быстрого доступа к инструментам.
|
||||
/// </remarks>
|
||||
public class AutoHidePanel : INotifyPropertyChanged
|
||||
public class AutoHidePanel
|
||||
{
|
||||
/// <summary>
|
||||
/// Происходит при изменении значения свойства.
|
||||
/// </summary>
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private bool _isVisible = false;
|
||||
private double _slideOffset = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Получает уникальный идентификатор автоскрываемой панели.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Строковый идентификатор, сгенерированный с помощью GUID.
|
||||
/// </value>
|
||||
public string Id { get; } = Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает содержимое панели.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Объект, реализующий <see cref="Abstractions.IDockContent"/>.
|
||||
/// </value>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается при попытке установить значение null.
|
||||
/// </exception>
|
||||
public Abstractions.IDockContent Content
|
||||
{
|
||||
get => _content;
|
||||
set
|
||||
{
|
||||
if (_content != value)
|
||||
{
|
||||
_content = value ?? throw new ArgumentNullException(nameof(value));
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(Title));
|
||||
}
|
||||
}
|
||||
}
|
||||
private Abstractions.IDockContent _content;
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает сторону окна, к которой прикреплена панель.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Значение перечисления <see cref="DockSide"/>, указывающее сторону прикрепления.
|
||||
/// </value>
|
||||
public DockSide Side { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает ширину панели (для левой/правой сторон)
|
||||
/// или высоту (для верхней/нижней сторон).
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Размер панели в пикселях. Значение по умолчанию: 300.
|
||||
/// </value>
|
||||
public double Size { get; set; } = 300;
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает признак видимости панели.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// true, если панель развернута и видима; в противном случае false.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// При изменении этого свойства генерируется событие <see cref="PropertyChanged"/>.
|
||||
/// </remarks>
|
||||
public bool IsVisible
|
||||
{
|
||||
get => _isVisible;
|
||||
set
|
||||
{
|
||||
if (_isVisible != value)
|
||||
{
|
||||
_isVisible = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает смещение для анимации выезда/заезда панели.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Значение от 0.0 до 1.0, где 0.0 - полностью скрыта, 1.0 - полностью развернута.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Используется для плавной анимации отображения/скрытия панели.
|
||||
/// </remarks>
|
||||
public double SlideOffset
|
||||
{
|
||||
get => _slideOffset;
|
||||
set
|
||||
{
|
||||
if (Math.Abs(_slideOffset - value) > 0.001)
|
||||
{
|
||||
_slideOffset = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает заголовок панели.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Заголовок, взятый из содержимого панели.
|
||||
/// Если содержимое не установлено, возвращает "Auto-hide Panel".
|
||||
/// </value>
|
||||
public string Title => Content?.Title ?? "Auto-hide Panel";
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый экземпляр класса <see cref="AutoHidePanel"/>.
|
||||
/// </summary>
|
||||
/// <param name="content">Содержимое панели.</param>
|
||||
/// <param name="side">Сторона окна для прикрепления.</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, когда <paramref name="content"/> равен null.
|
||||
/// </exception>
|
||||
public AutoHidePanel(Abstractions.IDockContent content, DockSide side)
|
||||
/// <param name="side">Сторона окна для прикрепления панели.</param>
|
||||
/// <exception cref="ArgumentNullException">Выбрасывается, когда <paramref name="content"/> равен null.</exception>
|
||||
public AutoHidePanel(IDockContent content, DockSide side)
|
||||
{
|
||||
Content = content ?? throw new ArgumentNullException(nameof(content));
|
||||
Side = side;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает уникальный идентификатор панели.
|
||||
/// </summary>
|
||||
public string Id { get; } = Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Получает содержимое панели.
|
||||
/// </summary>
|
||||
public IDockContent Content { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает сторону окна, к которой прикреплена панель.
|
||||
/// </summary>
|
||||
public DockSide Side { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает размер панели в пикселях.
|
||||
/// Для левой/правой сторон - ширина, для верхней/нижней - высота.
|
||||
/// </summary>
|
||||
public double Size { get; set; } = 300;
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает значение, указывающее, видима ли панель.
|
||||
/// </summary>
|
||||
public bool IsVisible { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает смещение для анимации выезда/заезда панели.
|
||||
/// Значение от 0.0 (полностью скрыта) до 1.0 (полностью развернута).
|
||||
/// </summary>
|
||||
public double SlideOffset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает заголовок панели, взятый из содержимого.
|
||||
/// </summary>
|
||||
public string Title => Content?.Title ?? "Auto-hide Panel";
|
||||
|
||||
/// <summary>
|
||||
/// Переключает видимость панели.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Если панель была видимой, становится скрытой, и наоборот.
|
||||
/// </remarks>
|
||||
public void Toggle()
|
||||
{
|
||||
IsVisible = !IsVisible;
|
||||
@@ -164,15 +80,4 @@ public class AutoHidePanel : INotifyPropertyChanged
|
||||
{
|
||||
IsVisible = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Вызывает событие <see cref="PropertyChanged"/>.
|
||||
/// </summary>
|
||||
/// <param name="name">
|
||||
/// Имя изменившегося свойства. Если не указано, определяется автоматически.
|
||||
/// </param>
|
||||
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
}
|
||||
@@ -4,77 +4,40 @@ using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Lattice.Core.Docking.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Представляет узел дерева компоновки, который разделяет доступную область
|
||||
/// между двумя дочерними элементами. Этот класс является основным структурным
|
||||
/// элементом для создания сложных макетов с разделителями.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Каждая группа содержит два дочерних элемента (<see cref="First"/> и <see cref="Second"/>),
|
||||
/// которые могут быть либо другими группами (для создания вложенной структуры),
|
||||
/// либо листами (<see cref="DockLeaf"/>) с контентом.
|
||||
/// Направление разделения определяется свойством <see cref="Orientation"/>.
|
||||
/// </remarks>
|
||||
public class DockGroup : IDockElement, INotifyPropertyChanged
|
||||
{
|
||||
/// <summary>
|
||||
/// Происходит при изменении значения свойства.
|
||||
/// </summary>
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private double _splitRatio = 0.5;
|
||||
private string _id;
|
||||
private IDockElement _first;
|
||||
private IDockElement _second;
|
||||
private SplitDirection _orientation;
|
||||
private double _splitRatio = 0.5;
|
||||
private IDockElement? _parent;
|
||||
private double _width;
|
||||
private double _height;
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает уникальный идентификатор группы.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Строковый идентификатор, уникальный в пределах дерева компоновки.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Идентификатор используется для сериализации/десериализации макета,
|
||||
/// поиска элементов и отслеживания изменений в дереве.
|
||||
/// </remarks>
|
||||
public string Id
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public DockGroup(IDockElement first, IDockElement second, SplitDirection orientation)
|
||||
{
|
||||
get => _id;
|
||||
internal set
|
||||
First = first ?? throw new ArgumentNullException(nameof(first));
|
||||
Second = second ?? throw new ArgumentNullException(nameof(second));
|
||||
Orientation = orientation;
|
||||
}
|
||||
|
||||
public string Id { get; } = Guid.NewGuid().ToString();
|
||||
|
||||
public IDockElement? Parent
|
||||
{
|
||||
get => _parent;
|
||||
set
|
||||
{
|
||||
if (_id != value)
|
||||
if (_parent != value)
|
||||
{
|
||||
_id = value;
|
||||
_parent = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает родительский элемент в иерархии дерева компоновки.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Родительский элемент или null, если эта группа является корневой.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Это свойство управляется системой компоновки при добавлении или
|
||||
/// удалении элементов из дерева.
|
||||
/// </remarks>
|
||||
public IDockElement? Parent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает первый дочерний элемент (левую или верхнюю область).
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Элемент, занимающий первую часть разделенной области.
|
||||
/// </value>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается при попытке установить значение null.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// При установке нового значения автоматически обновляется свойство
|
||||
/// <see cref="Parent"/> у дочернего элемента.
|
||||
/// </remarks>
|
||||
public IDockElement First
|
||||
{
|
||||
get => _first;
|
||||
@@ -89,19 +52,6 @@ public class DockGroup : IDockElement, INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает второй дочерний элемент (правую или нижнюю область).
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Элемент, занимающий вторую часть разделенной области.
|
||||
/// </value>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается при попытке установить значение null.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// При установке нового значения автоматически обновляется свойство
|
||||
/// <see cref="Parent"/> у дочернего элемента.
|
||||
/// </remarks>
|
||||
public IDockElement Second
|
||||
{
|
||||
get => _second;
|
||||
@@ -116,42 +66,25 @@ public class DockGroup : IDockElement, INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает направление разделения данной группы.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Значение перечисления <see cref="SplitDirection"/>, указывающее,
|
||||
/// как разделена область: горизонтально или вертикально.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="SplitDirection.Horizontal"/> создает левую и правую области</item>
|
||||
/// <item><see cref="SplitDirection.Vertical"/> создает верхнюю и нижнюю области</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public SplitDirection Orientation { get; set; }
|
||||
public SplitDirection Orientation
|
||||
{
|
||||
get => _orientation;
|
||||
set
|
||||
{
|
||||
if (_orientation != value)
|
||||
{
|
||||
_orientation = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает соотношение разделения между первым и вторым элементами.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Значение от 0.0 до 1.0, где:
|
||||
/// <list type="bullet">
|
||||
/// <item>0.0 - вся область принадлежит второму элементу</item>
|
||||
/// <item>0.5 - область разделена поровну</item>
|
||||
/// <item>1.0 - вся область принадлежит первому элементу</item>
|
||||
/// </list>
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Изменение этого свойства вызывает событие <see cref="PropertyChanged"/>
|
||||
/// и может привести к перерисовке пользовательского интерфейса.
|
||||
/// </remarks>
|
||||
public double SplitRatio
|
||||
{
|
||||
get => _splitRatio;
|
||||
set
|
||||
{
|
||||
if (Math.Abs(_splitRatio - value) > double.Epsilon)
|
||||
if (Math.Abs(_splitRatio - value) > 0.001)
|
||||
{
|
||||
_splitRatio = value;
|
||||
OnPropertyChanged();
|
||||
@@ -159,96 +92,42 @@ public class DockGroup : IDockElement, INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает желаемую ширину элемента.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Ширина в пикселях или относительных единицах.
|
||||
/// </value>
|
||||
public double Width { get; set; }
|
||||
public double Width
|
||||
{
|
||||
get => _width;
|
||||
set
|
||||
{
|
||||
if (Math.Abs(_width - value) > 0.001)
|
||||
{
|
||||
_width = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает желаемую высоту элемента.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Высота в пикселях или относительных единицах.
|
||||
/// </value>
|
||||
public double Height { get; set; }
|
||||
public double Height
|
||||
{
|
||||
get => _height;
|
||||
set
|
||||
{
|
||||
if (Math.Abs(_height - value) > 0.001)
|
||||
{
|
||||
_height = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает минимально допустимую ширину элемента.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Минимальная ширина в пикселях, при которой элемент сохраняет функциональность.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Для группы минимальная ширина вычисляется как сумма минимальных ширин
|
||||
/// дочерних элементов при горизонтальной ориентации или максимум минимальных
|
||||
/// ширин при вертикальной ориентации.
|
||||
/// </remarks>
|
||||
public double MinWidth => Orientation == SplitDirection.Horizontal
|
||||
? First.MinWidth + Second.MinWidth
|
||||
: Math.Max(First.MinWidth, Second.MinWidth);
|
||||
|
||||
/// <summary>
|
||||
/// Получает минимально допустимую высоту элемента.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Минимальная высота в пикселях, при которой элемент сохраняет функциональность.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Для группы минимальная высота вычисляется как сумма минимальных высот
|
||||
/// дочерних элементов при вертикальной ориентации или максимум минимальных
|
||||
/// высот при горизонтальной ориентации.
|
||||
/// </remarks>
|
||||
public double MinHeight => Orientation == SplitDirection.Vertical
|
||||
? First.MinHeight + Second.MinHeight
|
||||
: Math.Max(First.MinHeight, Second.MinHeight);
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый экземпляр класса <see cref="DockGroup"/>.
|
||||
/// </summary>
|
||||
/// <param name="first">
|
||||
/// Первый дочерний элемент (левая или верхняя область).
|
||||
/// </param>
|
||||
/// <param name="second">
|
||||
/// Второй дочерний элемент (правая или нижняя область).
|
||||
/// </param>
|
||||
/// <param name="orientation">
|
||||
/// Направление разделения между дочерними элементами.
|
||||
/// </param>
|
||||
/// <param name="id">
|
||||
/// Уникальный идентификатор группы. Если не указан, генерируется новый GUID.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, когда <paramref name="first"/> или <paramref name="second"/>
|
||||
/// равны null.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// Конструктор автоматически устанавливает свойство <see cref="Parent"/>
|
||||
/// у дочерних элементов на текущую группу и генерирует уникальный идентификатор,
|
||||
/// если он не был предоставлен.
|
||||
/// </remarks>
|
||||
public DockGroup(IDockElement first, IDockElement second,
|
||||
SplitDirection orientation, string? id = null)
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
First = first ?? throw new ArgumentNullException(nameof(first));
|
||||
Second = second ?? throw new ArgumentNullException(nameof(second));
|
||||
Orientation = orientation;
|
||||
Id = id ?? Guid.NewGuid().ToString();
|
||||
|
||||
First.Parent = this;
|
||||
Second.Parent = this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Вызывает событие <see cref="PropertyChanged"/>.
|
||||
/// </summary>
|
||||
/// <param name="name">
|
||||
/// Имя изменившегося свойства. Если не указано, определяется автоматически.
|
||||
/// </param>
|
||||
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
@@ -5,85 +5,44 @@ using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Lattice.Core.Docking.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Представляет конечный узел (лист) дерева компоновки, который непосредственно
|
||||
/// содержит коллекцию вкладок с контентом. Этот класс является контейнером для
|
||||
/// отображаемого пользователю содержимого.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Лист является основным элементом, с которым взаимодействует пользователь
|
||||
/// при работе с документами или инструментальными панелями в IDE-подобных
|
||||
/// приложениях.
|
||||
/// </remarks>
|
||||
public class DockLeaf : IDockContainer, INotifyPropertyChanged
|
||||
{
|
||||
/// <summary>
|
||||
/// Происходит при изменении значения свойства.
|
||||
/// </summary>
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private readonly ObservableCollection<IDockContent> _items = new();
|
||||
private IDockContent? _activeContent;
|
||||
private string _id;
|
||||
private TabPlacement _tabPlacement = TabPlacement.Bottom;
|
||||
private IDockElement? _parent;
|
||||
private double _width;
|
||||
private double _height;
|
||||
private TabPlacement _tabPlacement = TabPlacement.Top;
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает уникальный идентификатор листа.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Строковый идентификатор, уникальный в пределах дерева компоновки.
|
||||
/// </value>
|
||||
public string Id
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public DockLeaf()
|
||||
{
|
||||
get => _id;
|
||||
internal set
|
||||
_items.CollectionChanged += (s, e) => OnPropertyChanged(nameof(Children));
|
||||
}
|
||||
|
||||
public string Id { get; } = Guid.NewGuid().ToString();
|
||||
|
||||
public IDockElement? Parent
|
||||
{
|
||||
get => _parent;
|
||||
set
|
||||
{
|
||||
if (_id != value)
|
||||
if (_parent != value)
|
||||
{
|
||||
_id = value;
|
||||
_parent = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает родительский элемент в иерархии дерева компоновки.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Родительский элемент или null, если этот лист является корневым.
|
||||
/// </value>
|
||||
public IDockElement? Parent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает список вкладок, содержащихся в данном контейнере.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Коллекция объектов, реализующих <see cref="IDockContent"/>.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Эта коллекция является наблюдаемой (ObservableCollection), что позволяет
|
||||
/// автоматически обновлять пользовательский интерфейс при добавлении или
|
||||
/// удалении вкладок.
|
||||
/// </remarks>
|
||||
public IList<IDockContent> Children => _items;
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает активную (выбранную) вкладку в контейнере.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Активная вкладка или null, если в контейнере нет вкладок.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// При установке нового значения проверяется, что вкладка действительно
|
||||
/// содержится в коллекции <see cref="Children"/>.
|
||||
/// Изменение этого свойства вызывает событие <see cref="PropertyChanged"/>.
|
||||
/// </remarks>
|
||||
public IDockContent? ActiveContent
|
||||
{
|
||||
get => _activeContent;
|
||||
set
|
||||
{
|
||||
if (value != null && !_items.Contains(value)) return;
|
||||
if (_activeContent != value)
|
||||
{
|
||||
_activeContent = value;
|
||||
@@ -92,48 +51,35 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает желаемую ширину элемента.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Ширина в пикселях или относительных единицах.
|
||||
/// </value>
|
||||
public double Width { get; set; }
|
||||
public double Width
|
||||
{
|
||||
get => _width;
|
||||
set
|
||||
{
|
||||
if (Math.Abs(_width - value) > 0.001)
|
||||
{
|
||||
_width = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает желаемую высоту элемента.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Высота в пикселях или относительных единицах.
|
||||
/// </value>
|
||||
public double Height { get; set; }
|
||||
public double Height
|
||||
{
|
||||
get => _height;
|
||||
set
|
||||
{
|
||||
if (Math.Abs(_height - value) > 0.001)
|
||||
{
|
||||
_height = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает минимально допустимую ширину элемента.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Минимальная ширина в пикселях. Значение по умолчанию: 100.
|
||||
/// </value>
|
||||
public double MinWidth { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает минимально допустимую высоту элемента.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Минимальная высота в пикселях. Значение по умолчанию: 100.
|
||||
/// </value>
|
||||
public double MinHeight { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает положение полосы вкладок в контейнере.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Значение перечисления <see cref="TabPlacement"/>, определяющее,
|
||||
/// где располагаются вкладки относительно содержимого.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Поддерживаются все четыре стороны: верх, низ, лево, право.
|
||||
/// </remarks>
|
||||
public TabPlacement TabPlacement
|
||||
{
|
||||
get => _tabPlacement;
|
||||
@@ -147,47 +93,10 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый экземпляр класса <see cref="DockLeaf"/>.
|
||||
/// </summary>
|
||||
/// <param name="id">
|
||||
/// Уникальный идентификатор листа. Если не указан, генерируется новый GUID.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// Создает пустой лист с коллекцией вкладок и генерирует уникальный
|
||||
/// идентификатор, если он не был предоставлен.
|
||||
/// </remarks>
|
||||
public DockLeaf(string? id = null)
|
||||
{
|
||||
_id = id ?? Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Вызывает событие <see cref="PropertyChanged"/>.
|
||||
/// </summary>
|
||||
/// <param name="name">
|
||||
/// Имя изменившегося свойства. Если не указано, определяется автоматически.
|
||||
/// </param>
|
||||
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Добавляет контент в контейнер и делает его активным.
|
||||
/// </summary>
|
||||
/// <param name="content">
|
||||
/// Контент для добавления.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// Если контент уже содержится в коллекции, он не добавляется повторно,
|
||||
/// но становится активным.
|
||||
/// Этот метод обновляет свойство <see cref="ActiveContent"/> и вызывает
|
||||
/// соответствующее событие изменения свойства.
|
||||
/// </remarks>
|
||||
public void AddContent(IDockContent content)
|
||||
{
|
||||
if (content == null) return;
|
||||
if (content == null)
|
||||
throw new ArgumentNullException(nameof(content));
|
||||
|
||||
if (!_items.Contains(content))
|
||||
{
|
||||
@@ -196,22 +105,10 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged
|
||||
ActiveContent = content;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удаляет контент из контейнера.
|
||||
/// </summary>
|
||||
/// <param name="content">
|
||||
/// Контент для удаления.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// Если удаляемый контент является активным, автоматически выбирается
|
||||
/// новая активная вкладка (следующая в списке или предыдущая, если удалена
|
||||
/// последняя).
|
||||
/// Если после удаления контейнер становится пустым, он может быть удален
|
||||
/// из дерева макета системой компоновки.
|
||||
/// </remarks>
|
||||
public void RemoveContent(IDockContent content)
|
||||
{
|
||||
if (content == null) return;
|
||||
if (content == null)
|
||||
throw new ArgumentNullException(nameof(content));
|
||||
|
||||
int index = _items.IndexOf(content);
|
||||
if (index == -1) return;
|
||||
@@ -226,4 +123,9 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged
|
||||
ActiveContent = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,10 @@ public class ContentRegistry
|
||||
if (factory == null)
|
||||
throw new ArgumentNullException(nameof(factory));
|
||||
|
||||
// Дополнительная проверка на пустую строку
|
||||
if (string.IsNullOrEmpty(contentTypeId.Trim()))
|
||||
throw new ArgumentException("Идентификатор типа контента не может быть пустой строкой.", nameof(contentTypeId));
|
||||
|
||||
if (_contentTypes.ContainsKey(contentTypeId))
|
||||
throw new ArgumentException($"Тип контента '{contentTypeId}' уже зарегистрирован.");
|
||||
|
||||
@@ -70,13 +74,7 @@ public class ContentRegistry
|
||||
throw new KeyNotFoundException($"Тип контента '{contentTypeId}' не зарегистрирован.");
|
||||
|
||||
var content = descriptor.Factory();
|
||||
|
||||
// Устанавливаем ID через рефлексию, если есть свойство Id
|
||||
var property = content.GetType().GetProperty("Id");
|
||||
if (property != null && property.CanWrite)
|
||||
{
|
||||
property.SetValue(content, id);
|
||||
}
|
||||
content.SetId(id);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user