Compare commits
14 Commits
b6de0543b7
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e8b4cb9881 | |||
| 584df249f6 | |||
| 33abd94f6e | |||
| a902474345 | |||
| 0e050b452a | |||
| 6ad7b5dcdb | |||
| bbb20edb03 | |||
| 2bd7d3c474 | |||
| be12154262 | |||
| a6ee6fcb36 | |||
|
|
79bdd8bc62 | ||
| 9ea82af329 | |||
| c3770c789b | |||
| ca5d912c9c |
23
Lattice.Core.Docking/Abstractions/IDockCommand.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace Lattice.Core.Docking.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Определяет контракт для команды в системе докинга.
|
||||
/// Команды представляют действия, которые могут быть выполнены над элементами док-системы.
|
||||
/// </summary>
|
||||
public interface IDockCommand : System.Windows.Input.ICommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Получает отображаемое имя команды.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает идентификатор ресурса для иконки команды.
|
||||
/// </summary>
|
||||
string Icon { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает текстовое представление жеста (горячей клавиши) для команды.
|
||||
/// </summary>
|
||||
string GestureText { get; }
|
||||
}
|
||||
38
Lattice.Core.Docking/Abstractions/IDockContainer.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Lattice.Core.Docking.Models;
|
||||
|
||||
namespace Lattice.Core.Docking.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Определяет контракт для контейнеров, содержащих коллекцию вкладок.
|
||||
/// Контейнеры являются листьями дерева компоновки и непосредственно отображают содержимое.
|
||||
/// </summary>
|
||||
public interface IDockContainer : IDockElement
|
||||
{
|
||||
/// <summary>
|
||||
/// Получает список вкладок, находящихся в данном контейнере.
|
||||
/// </summary>
|
||||
IList<IDockContent> Children { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает текущую активную (выбранную) вкладку.
|
||||
/// </summary>
|
||||
IDockContent? ActiveContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Добавляет контент в контейнер и делает его активным.
|
||||
/// </summary>
|
||||
/// <param name="content">Контент для добавления.</param>
|
||||
void AddContent(IDockContent content);
|
||||
|
||||
/// <summary>
|
||||
/// Удаляет контент из контейнера. Если коллекция становится пустой,
|
||||
/// контейнер может быть удален из дерева макета.
|
||||
/// </summary>
|
||||
/// <param name="content">Контент для удаления.</param>
|
||||
void RemoveContent(IDockContent content);
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает положение панели вкладок в интерфейсе.
|
||||
/// </summary>
|
||||
TabPlacement TabPlacement { get; set; }
|
||||
}
|
||||
43
Lattice.Core.Docking/Abstractions/IDockContent.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
namespace Lattice.Core.Docking.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Определяет контракт для содержимого (вкладки), которое может быть размещено внутри контейнера.
|
||||
/// </summary>
|
||||
public interface IDockContent
|
||||
{
|
||||
/// <summary>
|
||||
/// Получает уникальный идентификатор контента.
|
||||
/// Используется для идентификации вкладки в системе.
|
||||
/// </summary>
|
||||
string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Устанавливает идентификатор контента.
|
||||
/// </summary>
|
||||
/// <param name="id">Новый идентификатор.</param>
|
||||
void SetId(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Получает заголовок, отображаемый пользователю на вкладке.
|
||||
/// </summary>
|
||||
string Title { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает визуальный элемент для отображения в теле вкладки.
|
||||
/// </summary>
|
||||
object View { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает значение, указывающее, можно ли закрыть вкладку.
|
||||
/// </summary>
|
||||
bool CanClose { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Вызывается системой при попытке закрытия контента.
|
||||
/// Позволяет выполнить дополнительные проверки или сохранить состояние.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// true, если закрытие разрешено; в противном случае false.
|
||||
/// </returns>
|
||||
bool OnClosing();
|
||||
}
|
||||
91
Lattice.Core.Docking/Abstractions/IDockElement.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
namespace Lattice.Core.Docking.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Базовый интерфейс для любого элемента, являющегося частью дерева компоновки.
|
||||
/// Определяет общие свойства и методы для всех элементов док-системы.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Элементы док-системы образуют древовидную структуру, где каждый элемент может иметь
|
||||
/// родителя и дочерние элементы. Эта иерархия используется для организации пространства
|
||||
/// главного окна и плавающих окон в IDE-подобных приложениях.
|
||||
/// </remarks>
|
||||
public interface IDockElement
|
||||
{
|
||||
/// <summary>
|
||||
/// Получает уникальный идентификатор элемента.
|
||||
/// Используется для поиска элементов, сериализации состояния и отслеживания изменений.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Строковый идентификатор, гарантированно уникальный в пределах дерева компоновки.
|
||||
/// Обычно представляет собой GUID в строковом формате.
|
||||
/// </value>
|
||||
string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает родительский элемент в иерархии дерева компоновки.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Родительский элемент или null, если элемент является корневым.
|
||||
/// Это свойство управляется системой компоновки при добавлении или удалении элементов.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Изменение этого свойства вручную может привести к нарушению целостности дерева.
|
||||
/// Для манипуляции структурой дерева следует использовать методы <see cref="DockOperations"/>.
|
||||
/// </remarks>
|
||||
IDockElement? Parent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает желаемую ширину элемента.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Ширина элемента в пикселях или относительных единицах.
|
||||
/// Может быть выражена как абсолютное значение (в пикселях) или как пропорция
|
||||
/// (например, 0.5 для 50% доступного пространства).
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Фактическая ширина элемента определяется родительским контейнером с учетом
|
||||
/// минимальных размеров и соотношений разделения.
|
||||
/// </remarks>
|
||||
double Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает желаемую высоту элемента.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Высота элемента в пикселях или относительных единицах.
|
||||
/// Может быть выражена как абсолютное значение (в пикселях) или как пропорция.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Фактическая высота элемента определяется родительским контейнером с учетом
|
||||
/// минимальных размеров и соотношений разделения.
|
||||
/// </remarks>
|
||||
double Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает минимально допустимую ширину элемента.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Минимальная ширина элемента в пикселях, при которой элемент сохраняет
|
||||
/// базовую функциональность и читаемость содержимого.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Система компоновки не позволит уменьшить элемент ниже этого значения.
|
||||
/// Для групп разделения минимальная ширина вычисляется рекурсивно на основе
|
||||
/// минимальных размеров дочерних элементов.
|
||||
/// </remarks>
|
||||
double MinWidth { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает минимально допустимую высоту элемента.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Минимальная высота элемента в пикселях, при которой элемент сохраняет
|
||||
/// базовую функциональность и читаемость содержимого.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// Система компоновки не позволит уменьшить элемент ниже этого значения.
|
||||
/// Для групп разделения минимальная высота вычисляется рекурсивно на основе
|
||||
/// минимальных размеров дочерних элементов.
|
||||
/// </remarks>
|
||||
double MinHeight { get; }
|
||||
}
|
||||
129
Lattice.Core.Docking/Engine/DockOperations.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using Lattice.Core.Docking.Abstractions;
|
||||
using Lattice.Core.Docking.Models;
|
||||
|
||||
namespace Lattice.Core.Docking.Engine;
|
||||
|
||||
/// <summary>
|
||||
/// Предоставляет статические методы для манипуляции иерархией дерева компоновки.
|
||||
/// Содержит чистые алгоритмы трансформации графа без зависимости от UI.
|
||||
/// </summary>
|
||||
public static class DockOperations
|
||||
{
|
||||
/// <summary>
|
||||
/// Извлекает элемент из дерева компоновки.
|
||||
/// Если родительская группа остается с одним ребенком, она удаляется,
|
||||
/// а оставшийся ребенок занимает её место в иерархии.
|
||||
/// </summary>
|
||||
/// <param name="element">Элемент для удаления из дерева.</param>
|
||||
/// <param name="root">Текущий корневой элемент дерева.</param>
|
||||
/// <returns>
|
||||
/// Новый корневой элемент дерева после удаления и оптимизации структуры.
|
||||
/// Возвращает null, если дерево становится пустым.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, когда <paramref name="root"/> равен null.
|
||||
/// </exception>
|
||||
public static IDockElement? Remove(IDockElement element, IDockElement root)
|
||||
{
|
||||
if (element == null) throw new ArgumentNullException(nameof(element));
|
||||
if (root == null) throw new ArgumentNullException(nameof(root));
|
||||
|
||||
if (element == root) return null;
|
||||
|
||||
var parent = element.Parent as DockGroup;
|
||||
if (parent == null) return root;
|
||||
|
||||
// Определяем "выжившего" соседа
|
||||
var sibling = (parent.First == element) ? parent.Second : parent.First;
|
||||
var grandParent = parent.Parent as DockGroup;
|
||||
|
||||
if (grandParent != null)
|
||||
{
|
||||
// Переподключаем соседа напрямую к дедушке
|
||||
if (grandParent.First == parent) grandParent.First = sibling;
|
||||
else grandParent.Second = sibling;
|
||||
|
||||
sibling.Parent = grandParent;
|
||||
return root;
|
||||
}
|
||||
|
||||
// Если дедушки нет, сосед становится новым корнем
|
||||
sibling.Parent = null;
|
||||
return sibling;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Вставляет элемент в дерево компоновки относительно целевого элемента.
|
||||
/// Создает новую группу разделения или объединяет контент в зависимости от позиции.
|
||||
/// </summary>
|
||||
/// <param name="target">Целевой элемент, относительно которого выполняется вставка.</param>
|
||||
/// <param name="source">Вставляемый элемент.</param>
|
||||
/// <param name="pos">Позиция вставки относительно целевого элемента.</param>
|
||||
/// <param name="root">Текущий корневой элемент дерева.</param>
|
||||
/// <returns>
|
||||
/// Новый корневой элемент дерева после вставки и оптимизации структуры.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, когда <paramref name="target"/>, <paramref name="source"/>
|
||||
/// или <paramref name="root"/> равны null.
|
||||
/// </exception>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Выбрасывается, когда <paramref name="pos"/> имеет недопустимое значение.
|
||||
/// </exception>
|
||||
public static IDockElement Insert(IDockElement target, IDockElement source,
|
||||
DockPosition pos, IDockElement root)
|
||||
{
|
||||
if (target == null) throw new ArgumentNullException(nameof(target));
|
||||
if (source == null) throw new ArgumentNullException(nameof(source));
|
||||
if (root == null) throw new ArgumentNullException(nameof(root));
|
||||
|
||||
// Случай 1: Объединение вкладок в центре
|
||||
if (pos == DockPosition.Center)
|
||||
{
|
||||
if (target is IDockContainer targetContainer && source is IDockContainer sourceContainer)
|
||||
{
|
||||
// Переносим все вкладки из источника в целевой контейнер
|
||||
var items = new List<IDockContent>(sourceContainer.Children);
|
||||
foreach (var item in items)
|
||||
{
|
||||
sourceContainer.RemoveContent(item);
|
||||
targetContainer.AddContent(item);
|
||||
}
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
// Случай 2: Разделение (Split)
|
||||
var direction = (pos == DockPosition.Left || pos == DockPosition.Right)
|
||||
? SplitDirection.Horizontal : SplitDirection.Vertical;
|
||||
|
||||
bool sourceIsFirst = (pos == DockPosition.Left || pos == DockPosition.Top);
|
||||
|
||||
var oldParent = target.Parent;
|
||||
|
||||
// Создаем новую группу. Источник и цель делят пространство 50/50
|
||||
var newGroup = sourceIsFirst
|
||||
? new DockGroup(source, target, direction) { SplitRatio = 0.5 }
|
||||
: new DockGroup(target, source, direction) { SplitRatio = 0.5 };
|
||||
|
||||
if (oldParent is DockGroup gp)
|
||||
{
|
||||
if (gp.First == target) gp.First = newGroup;
|
||||
else gp.Second = newGroup;
|
||||
newGroup.Parent = gp;
|
||||
return root;
|
||||
}
|
||||
|
||||
// Если target был корнем, новая группа становится новым корнем
|
||||
if (target == root)
|
||||
{
|
||||
newGroup.Parent = null;
|
||||
return newGroup;
|
||||
}
|
||||
|
||||
// Эта точка недостижима при правильном использовании,
|
||||
// но добавляем для безопасности
|
||||
newGroup.Parent = null;
|
||||
return newGroup;
|
||||
}
|
||||
}
|
||||
345
Lattice.Core.Docking/Engine/LayoutManager.cs
Normal file
@@ -0,0 +1,345 @@
|
||||
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.UI.Docking.WinUI")]
|
||||
|
||||
namespace Lattice.Core.Docking.Engine;
|
||||
|
||||
/// <summary>
|
||||
/// Центральный менеджер макета, управляющий всей структурой док-системы.
|
||||
/// Координирует дерево компоновки, плавающие окна, автоскрываемые панели
|
||||
/// и предоставляет API для манипуляции макетом. Использует кэширование
|
||||
/// для оптимизации поиска элементов по идентификатору.
|
||||
/// </summary>
|
||||
public class LayoutManager
|
||||
{
|
||||
private readonly ObservableCollection<AutoHidePanel> _autoHidePanels = new();
|
||||
private readonly Dictionary<string, IDockElement> _elementCache = new();
|
||||
private IDockElement? _root;
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает корневой элемент дерева компоновки главного окна.
|
||||
/// При изменении значения генерируется событие <see cref="LayoutUpdated"/>.
|
||||
/// </summary>
|
||||
public IDockElement? Root
|
||||
{
|
||||
get => _root;
|
||||
internal set
|
||||
{
|
||||
if (_root != value)
|
||||
{
|
||||
_root = value;
|
||||
LayoutUpdated?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает список активных плавающих окон.
|
||||
/// </summary>
|
||||
public List<DockWindow> FloatingWindows { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Получает коллекцию автоскрываемых панелей.
|
||||
/// </summary>
|
||||
public ReadOnlyObservableCollection<AutoHidePanel> AutoHidePanels { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает реестр типов контента.
|
||||
/// </summary>
|
||||
public ContentRegistry? ContentRegistry { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Происходит при изменении структуры дерева компоновки.
|
||||
/// </summary>
|
||||
public event Action? LayoutUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Происходит при изменении коллекции автоскрываемых панелей.
|
||||
/// </summary>
|
||||
public event EventHandler? AutoHidePanelsChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый экземпляр класса <see cref="LayoutManager"/>.
|
||||
/// </summary>
|
||||
public LayoutManager()
|
||||
{
|
||||
AutoHidePanels = new ReadOnlyObservableCollection<AutoHidePanel>(_autoHidePanels);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Добавляет автоскрываемую панель с указанным содержимым к заданной стороне окна.
|
||||
/// </summary>
|
||||
/// <param name="content">Содержимое панели.</param>
|
||||
/// <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);
|
||||
return panel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удаляет автоскрываемую панель из коллекции.
|
||||
/// </summary>
|
||||
/// <param name="panel">Панель для удаления.</param>
|
||||
/// <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>
|
||||
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>
|
||||
/// <exception cref="ArgumentNullException">Выбрасывается, когда <paramref name="source"/> равен null.</exception>
|
||||
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;
|
||||
|
||||
// Обновляем кэш - удаляем перемещенный элемент
|
||||
_elementCache.Remove(source.Id);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем кэш для вставленного элемента
|
||||
_elementCache[source.Id] = source;
|
||||
|
||||
LayoutUpdated?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удаляет элемент из всех плавающих окон.
|
||||
/// </summary>
|
||||
/// <param name="element">Элемент для удаления.</param>
|
||||
/// <returns>true, если элемент был найден и удален; в противном случае false.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
if (win.Root != null && IsDescendantOf(target, win.Root))
|
||||
{
|
||||
win.Root = DockOperations.Insert(target, source, position, win.Root);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Определяет, является ли элемент потомком указанного предка.
|
||||
/// </summary>
|
||||
/// <param name="element">Проверяемый элемент.</param>
|
||||
/// <param name="ancestor">Предполагаемый предок.</param>
|
||||
/// <returns>true, если элемент является потомком предка; в противном случае false.</returns>
|
||||
private bool IsDescendantOf(IDockElement element, IDockElement ancestor)
|
||||
{
|
||||
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>
|
||||
public IDockElement? FindById(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id)) return null;
|
||||
|
||||
// Проверка кэша
|
||||
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)
|
||||
{
|
||||
_elementCache[id] = found;
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
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) 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>
|
||||
public void Reset()
|
||||
{
|
||||
Root = null;
|
||||
FloatingWindows.Clear();
|
||||
_autoHidePanels.Clear();
|
||||
_elementCache.Clear();
|
||||
LayoutUpdated?.Invoke();
|
||||
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Находит элемент по идентификатору в дереве компоновки.
|
||||
/// </summary>
|
||||
/// <param name="id">Идентификатор элемента для поиска.</param>
|
||||
/// <returns>Найденный элемент или null, если элемент с таким идентификатором не найден.</returns>
|
||||
public IDockElement? FindElementById(string id)
|
||||
{
|
||||
return FindElementByIdRecursive(Root, id) ??
|
||||
FloatingWindows.Select(w => FindElementByIdRecursive(w.Root, id))
|
||||
.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;
|
||||
if (element.Id == id) return element;
|
||||
|
||||
if (element is DockGroup group)
|
||||
{
|
||||
return FindElementByIdRecursive(group.First, id) ??
|
||||
FindElementByIdRecursive(group.Second, id);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
12
Lattice.Core.Docking/Lattice.Core.Docking.csproj
Normal file
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Lattice.Core.Geometry\Lattice.Core.Geometry.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
83
Lattice.Core.Docking/Models/AutoHidePanel.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using Lattice.Core.Docking.Abstractions;
|
||||
|
||||
namespace Lattice.Core.Docking.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Представляет автоскрываемую панель, которая может быть прикреплена к одной из сторон окна.
|
||||
/// Автоскрываемые панели скрываются, оставляя видимой только заголовок, и разворачиваются при наведении курсора или клике.
|
||||
/// </summary>
|
||||
public class AutoHidePanel
|
||||
{
|
||||
/// <summary>
|
||||
/// Инициализирует новый экземпляр класса <see cref="AutoHidePanel"/>.
|
||||
/// </summary>
|
||||
/// <param name="content">Содержимое панели.</param>
|
||||
/// <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>
|
||||
public void Toggle()
|
||||
{
|
||||
IsVisible = !IsVisible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Показывает панель.
|
||||
/// </summary>
|
||||
public void Show()
|
||||
{
|
||||
IsVisible = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Скрывает панель.
|
||||
/// </summary>
|
||||
public void Hide()
|
||||
{
|
||||
IsVisible = false;
|
||||
}
|
||||
}
|
||||
133
Lattice.Core.Docking/Models/DockGroup.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using Lattice.Core.Docking.Abstractions;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Lattice.Core.Docking.Models;
|
||||
|
||||
public class DockGroup : IDockElement, INotifyPropertyChanged
|
||||
{
|
||||
private IDockElement _first;
|
||||
private IDockElement _second;
|
||||
private SplitDirection _orientation;
|
||||
private double _splitRatio = 0.5;
|
||||
private IDockElement? _parent;
|
||||
private double _width;
|
||||
private double _height;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public DockGroup(IDockElement first, IDockElement second, SplitDirection orientation)
|
||||
{
|
||||
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 (_parent != value)
|
||||
{
|
||||
_parent = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IDockElement First
|
||||
{
|
||||
get => _first;
|
||||
set
|
||||
{
|
||||
if (_first != value)
|
||||
{
|
||||
_first = value ?? throw new ArgumentNullException(nameof(value));
|
||||
_first.Parent = this;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IDockElement Second
|
||||
{
|
||||
get => _second;
|
||||
set
|
||||
{
|
||||
if (_second != value)
|
||||
{
|
||||
_second = value ?? throw new ArgumentNullException(nameof(value));
|
||||
_second.Parent = this;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SplitDirection Orientation
|
||||
{
|
||||
get => _orientation;
|
||||
set
|
||||
{
|
||||
if (_orientation != value)
|
||||
{
|
||||
_orientation = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public double SplitRatio
|
||||
{
|
||||
get => _splitRatio;
|
||||
set
|
||||
{
|
||||
if (Math.Abs(_splitRatio - value) > 0.001)
|
||||
{
|
||||
_splitRatio = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public double Width
|
||||
{
|
||||
get => _width;
|
||||
set
|
||||
{
|
||||
if (Math.Abs(_width - value) > 0.001)
|
||||
{
|
||||
_width = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public double Height
|
||||
{
|
||||
get => _height;
|
||||
set
|
||||
{
|
||||
if (Math.Abs(_height - value) > 0.001)
|
||||
{
|
||||
_height = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public double MinWidth => Orientation == SplitDirection.Horizontal
|
||||
? First.MinWidth + Second.MinWidth
|
||||
: Math.Max(First.MinWidth, Second.MinWidth);
|
||||
|
||||
public double MinHeight => Orientation == SplitDirection.Vertical
|
||||
? First.MinHeight + Second.MinHeight
|
||||
: Math.Max(First.MinHeight, Second.MinHeight);
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
131
Lattice.Core.Docking/Models/DockLeaf.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using Lattice.Core.Docking.Abstractions;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Lattice.Core.Docking.Models;
|
||||
|
||||
public class DockLeaf : IDockContainer, INotifyPropertyChanged
|
||||
{
|
||||
private readonly ObservableCollection<IDockContent> _items = new();
|
||||
private IDockContent? _activeContent;
|
||||
private IDockElement? _parent;
|
||||
private double _width;
|
||||
private double _height;
|
||||
private TabPlacement _tabPlacement = TabPlacement.Top;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public DockLeaf()
|
||||
{
|
||||
_items.CollectionChanged += (s, e) => OnPropertyChanged(nameof(Children));
|
||||
}
|
||||
|
||||
public string Id { get; } = Guid.NewGuid().ToString();
|
||||
|
||||
public IDockElement? Parent
|
||||
{
|
||||
get => _parent;
|
||||
set
|
||||
{
|
||||
if (_parent != value)
|
||||
{
|
||||
_parent = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IList<IDockContent> Children => _items;
|
||||
|
||||
public IDockContent? ActiveContent
|
||||
{
|
||||
get => _activeContent;
|
||||
set
|
||||
{
|
||||
if (_activeContent != value)
|
||||
{
|
||||
_activeContent = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public double Width
|
||||
{
|
||||
get => _width;
|
||||
set
|
||||
{
|
||||
if (Math.Abs(_width - value) > 0.001)
|
||||
{
|
||||
_width = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public double Height
|
||||
{
|
||||
get => _height;
|
||||
set
|
||||
{
|
||||
if (Math.Abs(_height - value) > 0.001)
|
||||
{
|
||||
_height = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public double MinWidth { get; set; } = 100;
|
||||
public double MinHeight { get; set; } = 100;
|
||||
|
||||
public TabPlacement TabPlacement
|
||||
{
|
||||
get => _tabPlacement;
|
||||
set
|
||||
{
|
||||
if (_tabPlacement != value)
|
||||
{
|
||||
_tabPlacement = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AddContent(IDockContent content)
|
||||
{
|
||||
if (content == null)
|
||||
throw new ArgumentNullException(nameof(content));
|
||||
|
||||
if (!_items.Contains(content))
|
||||
{
|
||||
_items.Add(content);
|
||||
}
|
||||
ActiveContent = content;
|
||||
}
|
||||
|
||||
public void RemoveContent(IDockContent content)
|
||||
{
|
||||
if (content == null)
|
||||
throw new ArgumentNullException(nameof(content));
|
||||
|
||||
int index = _items.IndexOf(content);
|
||||
if (index == -1) return;
|
||||
|
||||
_items.RemoveAt(index);
|
||||
|
||||
if (ActiveContent == content)
|
||||
{
|
||||
if (_items.Count > 0)
|
||||
ActiveContent = _items[Math.Min(index, _items.Count - 1)];
|
||||
else
|
||||
ActiveContent = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
33
Lattice.Core.Docking/Models/DockPosition.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
namespace Lattice.Core.Docking.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Определяет позицию вставки элемента относительно целевого элемента.
|
||||
/// Используется при операциях перемещения и вставки элементов в дерево компоновки.
|
||||
/// </summary>
|
||||
public enum DockPosition
|
||||
{
|
||||
/// <summary>
|
||||
/// Слева от целевого элемента.
|
||||
/// </summary>
|
||||
Left,
|
||||
|
||||
/// <summary>
|
||||
/// Справа от целевого элемента.
|
||||
/// </summary>
|
||||
Right,
|
||||
|
||||
/// <summary>
|
||||
/// Сверху от целевого элемента.
|
||||
/// </summary>
|
||||
Top,
|
||||
|
||||
/// <summary>
|
||||
/// Снизу от целевого элемента.
|
||||
/// </summary>
|
||||
Bottom,
|
||||
|
||||
/// <summary>
|
||||
/// В центре целевого элемента (для объединения вкладок).
|
||||
/// </summary>
|
||||
Center,
|
||||
}
|
||||
27
Lattice.Core.Docking/Models/DockSide.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace Lattice.Core.Docking.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Определяет стороны окна, к которым могут быть прикреплены автоскрываемые панели.
|
||||
/// </summary>
|
||||
public enum DockSide
|
||||
{
|
||||
/// <summary>
|
||||
/// Левая сторона окна.
|
||||
/// </summary>
|
||||
Left,
|
||||
|
||||
/// <summary>
|
||||
/// Правая сторона окна.
|
||||
/// </summary>
|
||||
Right,
|
||||
|
||||
/// <summary>
|
||||
/// Верхняя сторона окна.
|
||||
/// </summary>
|
||||
Top,
|
||||
|
||||
/// <summary>
|
||||
/// Нижняя сторона окна.
|
||||
/// </summary>
|
||||
Bottom
|
||||
}
|
||||
68
Lattice.Core.Docking/Models/DockWindow.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using Lattice.Core.Docking.Abstractions;
|
||||
|
||||
namespace Lattice.Core.Docking.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Представляет плавающее окно в системе докинга.
|
||||
/// Плавающие окна могут перемещаться по экрану независимо от главного окна.
|
||||
/// </summary>
|
||||
public class DockWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// Получает уникальный идентификатор окна.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Строковый идентификатор, сгенерированный с помощью GUID.
|
||||
/// Используется для сохранения позиции и размера окна в конфигурации.
|
||||
/// </value>
|
||||
public string Id { get; } = Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает корневой элемент макета внутри данного окна.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Корневой элемент дерева компоновки плавающего окна.
|
||||
/// </value>
|
||||
public IDockElement? Root { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает позицию X окна на экране.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Координата X левого верхнего угла окна в пикселях.
|
||||
/// </value>
|
||||
public double X { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает позицию Y окна на экране.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Координата Y левого верхнего угла окна в пикселях.
|
||||
/// </value>
|
||||
public double Y { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает ширину окна.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Ширина окна в пикселях. Значение по умолчанию: 800.
|
||||
/// </value>
|
||||
public double Width { get; set; } = 800;
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает высоту окна.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Высота окна в пикселях. Значение по умолчанию: 600.
|
||||
/// </value>
|
||||
public double Height { get; set; } = 600;
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает заголовок окна.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Текст заголовка окна. Обычно берется из активного контента.
|
||||
/// Значение по умолчанию: "Lattice Tool Window".
|
||||
/// </value>
|
||||
public string Title { get; set; } = "Lattice Tool Window";
|
||||
}
|
||||
17
Lattice.Core.Docking/Models/SplitDirection.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace Lattice.Core.Docking.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Определяет направление разделения пространства внутри группы.
|
||||
/// </summary>
|
||||
public enum SplitDirection
|
||||
{
|
||||
/// <summary>
|
||||
/// Разделение по горизонтали (создает левую и правую области).
|
||||
/// </summary>
|
||||
Horizontal,
|
||||
|
||||
/// <summary>
|
||||
/// Разделение по вертикали (создает верхнюю и нижнюю области).
|
||||
/// </summary>
|
||||
Vertical
|
||||
}
|
||||
27
Lattice.Core.Docking/Models/TabPlacement.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace Lattice.Core.Docking.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Определяет положение полосы вкладок в контейнере.
|
||||
/// </summary>
|
||||
public enum TabPlacement
|
||||
{
|
||||
/// <summary>
|
||||
/// Вкладки располагаются сверху.
|
||||
/// </summary>
|
||||
Top,
|
||||
|
||||
/// <summary>
|
||||
/// Вкладки располагаются снизу.
|
||||
/// </summary>
|
||||
Bottom,
|
||||
|
||||
/// <summary>
|
||||
/// Вкладки располагаются слева.
|
||||
/// </summary>
|
||||
Left,
|
||||
|
||||
/// <summary>
|
||||
/// Вкладки располагаются справа.
|
||||
/// </summary>
|
||||
Right,
|
||||
}
|
||||
40
Lattice.Core.Docking/Serialization/ILayoutSerializer.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
namespace Lattice.Core.Docking.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Определяет контракт для сериализации и десериализации состояния макета док-системы.
|
||||
/// Позволяет сохранять и восстанавливать расположение панелей, окон и их состояние.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Эта абстракция позволяет реализовать различные форматы сериализации (JSON, XML, бинарный)
|
||||
/// и различные хранилища (файлы, базы данных, облако) без изменения основной логики док-системы.
|
||||
/// </remarks>
|
||||
public interface ILayoutSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Сериализует состояние менеджера макета в строку.
|
||||
/// </summary>
|
||||
/// <param name="manager">Менеджер макета для сериализации.</param>
|
||||
/// <returns>
|
||||
/// Строковое представление состояния макета.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, когда <paramref name="manager"/> равен null.
|
||||
/// </exception>
|
||||
string Serialize(Engine.LayoutManager manager);
|
||||
|
||||
/// <summary>
|
||||
/// Десериализует состояние макета из строки и восстанавливает его в менеджере.
|
||||
/// </summary>
|
||||
/// <param name="manager">Менеджер макета для восстановления состояния.</param>
|
||||
/// <param name="serializedLayout">Сериализованное состояние макета.</param>
|
||||
/// <param name="contentResolver">
|
||||
/// Функция разрешения контента по идентификатору, используемая для восстановления
|
||||
/// ссылок на контент в десериализованном состоянии.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, когда <paramref name="manager"/> или <paramref name="serializedLayout"/>
|
||||
/// равны null.
|
||||
/// </exception>
|
||||
void Deserialize(Engine.LayoutManager manager, string serializedLayout,
|
||||
Func<string, Abstractions.IDockContent?> contentResolver);
|
||||
}
|
||||
24
Lattice.Core.Docking/Serialization/ISerializableLayout.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace Lattice.Core.Docking.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Определяет контракт для объектов, которые могут предоставлять состояние для сериализации.
|
||||
/// </summary>
|
||||
public interface ISerializableLayout
|
||||
{
|
||||
/// <summary>
|
||||
/// Получает состояние объекта для сериализации.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Объект состояния, готовый к сериализации.
|
||||
/// </returns>
|
||||
object GetSerializableState();
|
||||
|
||||
/// <summary>
|
||||
/// Восстанавливает состояние объекта из десериализованного объекта.
|
||||
/// </summary>
|
||||
/// <param name="state">Десериализованное состояние.</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, когда <paramref name="state"/> равен null.
|
||||
/// </exception>
|
||||
void RestoreFromState(object state);
|
||||
}
|
||||
235
Lattice.Core.Docking/Services/ContentRegistry.cs
Normal file
@@ -0,0 +1,235 @@
|
||||
namespace Lattice.Core.Docking.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Реестр типов содержимого, который позволяет создавать экземпляры контента по типу.
|
||||
/// Этот сервис является центральным для динамического создания панелей инструментов и документов.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Реализует шаблон "Фабрика" для создания экземпляров <see cref="Abstractions.IDockContent"/>.
|
||||
/// Позволяет регистрировать фабричные методы для различных типов контента, что обеспечивает
|
||||
/// позднее связывание и возможность плагинной архитектуры.
|
||||
/// </remarks>
|
||||
public class ContentRegistry
|
||||
{
|
||||
private readonly Dictionary<string, ContentDescriptor> _contentTypes = new();
|
||||
|
||||
/// <summary>
|
||||
/// Регистрирует фабричный метод для создания контента указанного типа.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">
|
||||
/// Тип контента, реализующий <see cref="Abstractions.IDockContent"/>.
|
||||
/// </typeparam>
|
||||
/// <param name="contentTypeId">Уникальный идентификатор типа контента.</param>
|
||||
/// <param name="factory">Фабричный метод для создания экземпляров контента.</param>
|
||||
/// <param name="metadata">Метаданные типа контента (опционально).</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, если <paramref name="contentTypeId"/> или <paramref name="factory"/>
|
||||
/// равны null.
|
||||
/// </exception>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Выбрасывается, если <paramref name="contentTypeId"/> уже зарегистрирован.
|
||||
/// </exception>
|
||||
public void Register<T>(string contentTypeId, Func<T> factory, ContentMetadata? metadata = null)
|
||||
where T : Abstractions.IDockContent
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(contentTypeId))
|
||||
throw new ArgumentNullException(nameof(contentTypeId));
|
||||
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}' уже зарегистрирован.");
|
||||
|
||||
_contentTypes[contentTypeId] = new ContentDescriptor(
|
||||
typeof(T),
|
||||
() => factory(),
|
||||
metadata ?? new ContentMetadata(contentTypeId, typeof(T).Name)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создает новый экземпляр контента указанного типа с заданным идентификатором.
|
||||
/// </summary>
|
||||
/// <param name="contentTypeId">Идентификатор типа контента.</param>
|
||||
/// <param name="id">Уникальный идентификатор для создаваемого экземпляра контента.</param>
|
||||
/// <returns>
|
||||
/// Новый экземпляр контента.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, если <paramref name="contentTypeId"/> равен null или пустой строке.
|
||||
/// </exception>
|
||||
/// <exception cref="KeyNotFoundException">
|
||||
/// Выбрасывается, если тип контента не зарегистрирован.
|
||||
/// </exception>
|
||||
public Abstractions.IDockContent CreateContent(string contentTypeId, string id)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(contentTypeId))
|
||||
throw new ArgumentNullException(nameof(contentTypeId));
|
||||
|
||||
if (!_contentTypes.TryGetValue(contentTypeId, out var descriptor))
|
||||
throw new KeyNotFoundException($"Тип контента '{contentTypeId}' не зарегистрирован.");
|
||||
|
||||
var content = descriptor.Factory();
|
||||
content.SetId(id);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает метаданные для указанного типа контента.
|
||||
/// </summary>
|
||||
/// <param name="contentTypeId">Идентификатор типа контента.</param>
|
||||
/// <returns>
|
||||
/// Метаданные типа контента или null, если тип не найден.
|
||||
/// </returns>
|
||||
public ContentMetadata? GetMetadata(string contentTypeId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(contentTypeId))
|
||||
return null;
|
||||
|
||||
return _contentTypes.TryGetValue(contentTypeId, out var descriptor)
|
||||
? descriptor.Metadata
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает все зарегистрированные типы контента.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Коллекция идентификаторов зарегистрированных типов контента.
|
||||
/// </returns>
|
||||
public IEnumerable<string> GetRegisteredTypes() => _contentTypes.Keys;
|
||||
|
||||
/// <summary>
|
||||
/// Проверяет, зарегистрирован ли указанный тип контента.
|
||||
/// </summary>
|
||||
/// <param name="contentTypeId">Идентификатор типа контента.</param>
|
||||
/// <returns>
|
||||
/// true, если тип контента зарегистрирован; в противном случае false.
|
||||
/// </returns>
|
||||
public bool IsRegistered(string contentTypeId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(contentTypeId))
|
||||
return false;
|
||||
|
||||
return _contentTypes.ContainsKey(contentTypeId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Представляет дескриптор типа контента, содержащий информацию о фабричном методе и метаданных.
|
||||
/// </summary>
|
||||
private class ContentDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
/// Получает тип контента.
|
||||
/// </summary>
|
||||
public Type ContentType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает фабричный метод для создания экземпляров контента.
|
||||
/// </summary>
|
||||
public Func<Abstractions.IDockContent> Factory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает метаданные типа контента.
|
||||
/// </summary>
|
||||
public ContentMetadata Metadata { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый экземпляр класса <see cref="ContentDescriptor"/>.
|
||||
/// </summary>
|
||||
/// <param name="contentType">Тип контента.</param>
|
||||
/// <param name="factory">Фабричный метод.</param>
|
||||
/// <param name="metadata">Метаданные.</param>
|
||||
public ContentDescriptor(Type contentType, Func<Abstractions.IDockContent> factory,
|
||||
ContentMetadata metadata)
|
||||
{
|
||||
ContentType = contentType;
|
||||
Factory = factory;
|
||||
Metadata = metadata;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Представляет метаданные типа контента, предоставляющие дополнительную информацию для отображения в UI.
|
||||
/// </summary>
|
||||
public class ContentMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Получает идентификатор типа контента.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Уникальный строковый идентификатор типа контента.
|
||||
/// </value>
|
||||
public string ContentTypeId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает отображаемое имя типа контента.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Имя типа контента, отображаемое пользователю.
|
||||
/// </value>
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает описание типа контента.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Текстовое описание функциональности контента.
|
||||
/// </value>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает имя ресурса для иконки типа контента.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Имя ресурса иконки или null, если иконка не определена.
|
||||
/// </value>
|
||||
public string? IconResource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает значение, указывающее, является ли контент документом
|
||||
/// (а не инструментальной панелью).
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// true, если контент является документом; в противном случае false.
|
||||
/// </value>
|
||||
public bool IsDocument { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает ширину контента по умолчанию.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Ширина контента в пикселях. Значение по умолчанию: 300.
|
||||
/// </value>
|
||||
public double DefaultWidth { get; set; } = 300;
|
||||
|
||||
/// <summary>
|
||||
/// Получает или задает высоту контента по умолчанию.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// Высота контента в пикселях. Значение по умолчанию: 200.
|
||||
/// </value>
|
||||
public double DefaultHeight { get; set; } = 200;
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый экземпляр класса <see cref="ContentMetadata"/>.
|
||||
/// </summary>
|
||||
/// <param name="contentTypeId">Идентификатор типа контента.</param>
|
||||
/// <param name="displayName">Отображаемое имя типа контента.</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, если <paramref name="contentTypeId"/> или <paramref name="displayName"/>
|
||||
/// равны null.
|
||||
/// </exception>
|
||||
public ContentMetadata(string contentTypeId, string displayName)
|
||||
{
|
||||
ContentTypeId = contentTypeId ?? throw new ArgumentNullException(nameof(contentTypeId));
|
||||
DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName));
|
||||
Description = string.Empty;
|
||||
}
|
||||
}
|
||||
9
Lattice.Core.Geometry/Lattice.Core.Geometry.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
79
Lattice.Core.Geometry/Point.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
namespace Lattice.Core.Geometry;
|
||||
|
||||
/// <summary>
|
||||
/// Представляет точку в двумерном пространстве с координатами X и Y.
|
||||
/// Эта структура является платформонезависимой и может использоваться
|
||||
/// во всех слоях системы Lattice.
|
||||
/// </summary>
|
||||
public struct Point : IEquatable<Point>
|
||||
{
|
||||
/// <summary>
|
||||
/// Получает точку с координатами (0, 0).
|
||||
/// </summary>
|
||||
public static readonly Point Zero = new(0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// Координата X (горизонтальная).
|
||||
/// </summary>
|
||||
public double X { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Координата Y (вертикальная).
|
||||
/// </summary>
|
||||
public double Y { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новую точку с указанными координатами.
|
||||
/// </summary>
|
||||
/// <param name="x">Координата X.</param>
|
||||
/// <param name="y">Координата Y.</param>
|
||||
public Point(double x, double y)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создает точку из System.Drawing.Point.
|
||||
/// </summary>
|
||||
public static Point FromDrawingPoint(System.Drawing.Point point) =>
|
||||
new(point.X, point.Y);
|
||||
|
||||
/// <summary>
|
||||
/// Преобразует точку в System.Drawing.Point.
|
||||
/// </summary>
|
||||
public System.Drawing.Point ToDrawingPoint() =>
|
||||
new((int)X, (int)Y);
|
||||
|
||||
/// <summary>
|
||||
/// Определяет, равна ли эта точка другой точке.
|
||||
/// </summary>
|
||||
public bool Equals(Point other) =>
|
||||
Math.Abs(X - other.X) < double.Epsilon &&
|
||||
Math.Abs(Y - other.Y) < double.Epsilon;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) =>
|
||||
obj is Point point && Equals(point);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(X, Y);
|
||||
|
||||
/// <summary>
|
||||
/// Определяет, равны ли две точки.
|
||||
/// </summary>
|
||||
public static bool operator ==(Point left, Point right) =>
|
||||
left.Equals(right);
|
||||
|
||||
/// <summary>
|
||||
/// Определяет, не равны ли две точки.
|
||||
/// </summary>
|
||||
public static bool operator !=(Point left, Point right) =>
|
||||
!left.Equals(right);
|
||||
|
||||
/// <summary>
|
||||
/// Возвращает строковое представление точки.
|
||||
/// </summary>
|
||||
public override string ToString() => $"{X}, {Y}";
|
||||
}
|
||||
153
Lattice.Core.Geometry/Rect.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
namespace Lattice.Core.Geometry;
|
||||
|
||||
/// <summary>
|
||||
/// Представляет прямоугольник в двумерном пространстве с позицией и размерами.
|
||||
/// Эта структура является платформонезависимой и может использоваться
|
||||
/// во всех слоях системы Lattice.
|
||||
/// </summary>
|
||||
public struct Rect : IEquatable<Rect>
|
||||
{
|
||||
/// <summary>
|
||||
/// Получает пустой прямоугольник (позиция (0, 0), размеры (0, 0)).
|
||||
/// </summary>
|
||||
public static readonly Rect Empty = new(0, 0, 0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// Координата X левого верхнего угла прямоугольника.
|
||||
/// </summary>
|
||||
public double X { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Координата Y левого верхнего угла прямоугольника.
|
||||
/// </summary>
|
||||
public double Y { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ширина прямоугольника.
|
||||
/// </summary>
|
||||
public double Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Высота прямоугольника.
|
||||
/// </summary>
|
||||
public double Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает координату X правого края прямоугольника.
|
||||
/// </summary>
|
||||
public double Right => X + Width;
|
||||
|
||||
/// <summary>
|
||||
/// Получает координату Y нижнего края прямоугольника.
|
||||
/// </summary>
|
||||
public double Bottom => Y + Height;
|
||||
|
||||
/// <summary>
|
||||
/// Получает левый верхний угол прямоугольника.
|
||||
/// </summary>
|
||||
public Point TopLeft => new(X, Y);
|
||||
|
||||
/// <summary>
|
||||
/// Получает правый нижний угол прямоугольника.
|
||||
/// </summary>
|
||||
public Point BottomRight => new(Right, Bottom);
|
||||
|
||||
/// <summary>
|
||||
/// Получает центр прямоугольника.
|
||||
/// </summary>
|
||||
public Point Center => new(X + Width / 2, Y + Height / 2);
|
||||
|
||||
/// <summary>
|
||||
/// Получает площадь прямоугольника.
|
||||
/// </summary>
|
||||
public double Area => Width * Height;
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый прямоугольник с указанными параметрами.
|
||||
/// </summary>
|
||||
/// <param name="x">Координата X.</param>
|
||||
/// <param name="y">Координата Y.</param>
|
||||
/// <param name="width">Ширина.</param>
|
||||
/// <param name="height">Высота.</param>
|
||||
public Rect(double x, double y, double width, double height)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Width = width;
|
||||
Height = height;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый прямоугольник с указанной позицией и размером.
|
||||
/// </summary>
|
||||
/// <param name="location">Позиция прямоугольника.</param>
|
||||
/// <param name="size">Размер прямоугольника.</param>
|
||||
public Rect(Point location, Size size)
|
||||
{
|
||||
X = location.X;
|
||||
Y = location.Y;
|
||||
Width = size.Width;
|
||||
Height = size.Height;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создает прямоугольник из System.Drawing.Rectangle.
|
||||
/// </summary>
|
||||
public static Rect FromDrawingRectangle(System.Drawing.Rectangle rect) =>
|
||||
new(rect.X, rect.Y, rect.Width, rect.Height);
|
||||
|
||||
/// <summary>
|
||||
/// Преобразует прямоугольник в System.Drawing.Rectangle.
|
||||
/// </summary>
|
||||
public System.Drawing.Rectangle ToDrawingRectangle() =>
|
||||
new((int)X, (int)Y, (int)Width, (int)Height);
|
||||
|
||||
/// <summary>
|
||||
/// Проверяет, содержит ли прямоугольник указанную точку.
|
||||
/// </summary>
|
||||
public bool Contains(Point point) =>
|
||||
point.X >= X && point.X <= Right &&
|
||||
point.Y >= Y && point.Y <= Bottom;
|
||||
|
||||
/// <summary>
|
||||
/// Проверяет, пересекается ли этот прямоугольник с другим.
|
||||
/// </summary>
|
||||
public bool Intersects(Rect other) =>
|
||||
X < other.Right && Right > other.X &&
|
||||
Y < other.Bottom && Bottom > other.Y;
|
||||
|
||||
/// <summary>
|
||||
/// Определяет, равен ли этот прямоугольник другому прямоугольнику.
|
||||
/// </summary>
|
||||
public bool Equals(Rect other) =>
|
||||
Math.Abs(X - other.X) < double.Epsilon &&
|
||||
Math.Abs(Y - other.Y) < double.Epsilon &&
|
||||
Math.Abs(Width - other.Width) < double.Epsilon &&
|
||||
Math.Abs(Height - other.Height) < double.Epsilon;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) =>
|
||||
obj is Rect rect && Equals(rect);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(X, Y, Width, Height);
|
||||
|
||||
/// <summary>
|
||||
/// Определяет, равны ли два прямоугольника.
|
||||
/// </summary>
|
||||
public static bool operator ==(Rect left, Rect right) =>
|
||||
left.Equals(right);
|
||||
|
||||
/// <summary>
|
||||
/// Определяет, не равны ли два прямоугольника.
|
||||
/// </summary>
|
||||
public static bool operator !=(Rect left, Rect right) =>
|
||||
!left.Equals(right);
|
||||
|
||||
/// <summary>
|
||||
/// Возвращает строковое представление прямоугольника.
|
||||
/// </summary>
|
||||
public override string ToString() =>
|
||||
$"[X={X:F2}, Y={Y:F2}, Width={Width:F2}, Height={Height:F2}]";
|
||||
}
|
||||
84
Lattice.Core.Geometry/Size.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
namespace Lattice.Core.Geometry;
|
||||
|
||||
/// <summary>
|
||||
/// Представляет размеры в двумерном пространстве с шириной и высотой.
|
||||
/// Эта структура является платформонезависимой и может использоваться
|
||||
/// во всех слоях системы Lattice.
|
||||
/// </summary>
|
||||
public struct Size : IEquatable<Size>
|
||||
{
|
||||
/// <summary>
|
||||
/// Получает размер с нулевой шириной и высотой.
|
||||
/// </summary>
|
||||
public static readonly Size Zero = new(0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// Ширина.
|
||||
/// </summary>
|
||||
public double Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Высота.
|
||||
/// </summary>
|
||||
public double Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает признак того, что размер является пустым (нулевая ширина или высота).
|
||||
/// </summary>
|
||||
public bool IsEmpty => Width <= 0 || Height <= 0;
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый размер с указанными значениями.
|
||||
/// </summary>
|
||||
/// <param name="width">Ширина.</param>
|
||||
/// <param name="height">Высота.</param>
|
||||
public Size(double width, double height)
|
||||
{
|
||||
Width = width;
|
||||
Height = height;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создает размер из System.Drawing.Size.
|
||||
/// </summary>
|
||||
public static Size FromDrawingSize(System.Drawing.Size size) =>
|
||||
new(size.Width, size.Height);
|
||||
|
||||
/// <summary>
|
||||
/// Преобразует размер в System.Drawing.Size.
|
||||
/// </summary>
|
||||
public System.Drawing.Size ToDrawingSize() =>
|
||||
new((int)Width, (int)Height);
|
||||
|
||||
/// <summary>
|
||||
/// Определяет, равен ли этот размер другому размеру.
|
||||
/// </summary>
|
||||
public bool Equals(Size other) =>
|
||||
Math.Abs(Width - other.Width) < double.Epsilon &&
|
||||
Math.Abs(Height - other.Height) < double.Epsilon;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) =>
|
||||
obj is Size size && Equals(size);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(Width, Height);
|
||||
|
||||
/// <summary>
|
||||
/// Определяет, равны ли два размера.
|
||||
/// </summary>
|
||||
public static bool operator ==(Size left, Size right) =>
|
||||
left.Equals(right);
|
||||
|
||||
/// <summary>
|
||||
/// Определяет, не равны ли два размера.
|
||||
/// </summary>
|
||||
public static bool operator !=(Size left, Size right) =>
|
||||
!left.Equals(right);
|
||||
|
||||
/// <summary>
|
||||
/// Возвращает строковое представление размера.
|
||||
/// </summary>
|
||||
public override string ToString() => $"{Width} × {Height}";
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
namespace Lattice.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Сервис управления контекстом приложения и связанными командами.
|
||||
/// </summary>
|
||||
public interface IContextService
|
||||
{
|
||||
/// <summary>
|
||||
/// Имя текущего активного контекста.
|
||||
/// </summary>
|
||||
string CurrentContext { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Возникает при смене фокуса между вкладками с разными ContextGroup.
|
||||
/// </summary>
|
||||
event EventHandler<string>? ContextChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Устанавливает активный контекст. Вызывается UI-слоем при активации вкладки.
|
||||
/// </summary>
|
||||
void SetContext(string contextGroup);
|
||||
|
||||
/// <summary>
|
||||
/// Проверяет, должна ли команда быть видимой в текущем контексте.
|
||||
/// </summary>
|
||||
bool IsCommandVisible(string commandId, string commandContext);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
namespace Lattice.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Описывает компонент, который может быть размещен внутри узла компоновки Lattice.
|
||||
/// </summary>
|
||||
public interface IDockableComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Уникальный строковый идентификатор компонента (например, "SolutionExplorer").
|
||||
/// </summary>
|
||||
string UniqueId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Заголовок, отображаемый на вкладке или в заголовке панели.
|
||||
/// </summary>
|
||||
string DisplayName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Ключ иконки (для Segoe Fluent Icons или путей к ресурсам).
|
||||
/// </summary>
|
||||
string? IconKey { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Группа контекста (например, "CodeEditor", "Debugger").
|
||||
/// Определяет, какие панели инструментов будут активны.
|
||||
/// </summary>
|
||||
string ContextGroup { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Указывает, разрешено ли закрывать данный компонент пользователем.
|
||||
/// </summary>
|
||||
bool CanClose { get; }
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
namespace Lattice.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Представляет базовый элемент иерархии компоновки Lattice.
|
||||
/// </summary>
|
||||
public interface ILayoutElement
|
||||
{
|
||||
/// <summary>
|
||||
/// Уникальный идентификатор элемента.
|
||||
/// </summary>
|
||||
Guid Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Имя элемента для отображения или идентификации в логах.
|
||||
/// </summary>
|
||||
string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Значение ширины (в пикселях или долях "star").
|
||||
/// </summary>
|
||||
double WidthValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Указывает, является ли ширина пропорциональной (star).
|
||||
/// </summary>
|
||||
bool IsWidthStar { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Значение высоты (в пикселях или долях "star").
|
||||
/// </summary>
|
||||
double HeightValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Указывает, является ли высота пропорциональной (star).
|
||||
/// </summary>
|
||||
bool IsHeightStar { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Родительский элемент в дереве компоновки.
|
||||
/// </summary>
|
||||
ILayoutElement? Parent { get; set; }
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using Lattice.Core.Models;
|
||||
using Lattice.Core.Models.Enums;
|
||||
|
||||
namespace Lattice.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Сервис управления жизненным циклом макета приложения.
|
||||
/// </summary>
|
||||
public interface ILayoutService
|
||||
{
|
||||
/// <summary>
|
||||
/// Текущий корневой узел всей структуры окон.
|
||||
/// </summary>
|
||||
LayoutNode? Root { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Событие, возникающее при любом изменении структуры (докинг, закрытие, изменение размеров).
|
||||
/// </summary>
|
||||
event EventHandler? LayoutUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Перемещает узел в указанную позицию относительно целевого узла.
|
||||
/// </summary>
|
||||
void Dock(LayoutNode source, LayoutNode target, DockDirection direction);
|
||||
|
||||
/// <summary>
|
||||
/// Удаляет узел из макета (например, при закрытии вкладки).
|
||||
/// </summary>
|
||||
void Remove(LayoutNode node);
|
||||
|
||||
/// <summary>
|
||||
/// Импортирует структуру макета из снапшота.
|
||||
/// </summary>
|
||||
void LoadLayout(string jsonData);
|
||||
|
||||
/// <summary>
|
||||
/// Экспортирует текущую структуру в строку для сохранения.
|
||||
/// </summary>
|
||||
string SaveLayout();
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
using Lattice.Core.Abstractions;
|
||||
|
||||
namespace Lattice.Core.Context;
|
||||
|
||||
/// <summary>
|
||||
/// Реализация сервиса управления контекстом приложения.
|
||||
/// </summary>
|
||||
public class ContextManager : IContextService
|
||||
{
|
||||
private string _currentContext = "Common";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string CurrentContext => _currentContext;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<string>? ContextChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetContext(string contextGroup)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(contextGroup)) contextGroup = "Common";
|
||||
|
||||
if (_currentContext != contextGroup)
|
||||
{
|
||||
_currentContext = contextGroup;
|
||||
ContextChanged?.Invoke(this, contextGroup);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsCommandVisible(string commandId, string commandContext)
|
||||
{
|
||||
// Базовая логика: команда видима, если её контекст совпадает с текущим
|
||||
// или если команда помечена как общая ("Common" или "Global").
|
||||
return commandContext == "Common" ||
|
||||
commandContext == "Global" ||
|
||||
commandContext == _currentContext;
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
using Lattice.Core.Abstractions;
|
||||
using Lattice.Core.Models;
|
||||
using Lattice.Core.Models.Enums;
|
||||
using Lattice.Core.Persistence;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Lattice.Core.Engine;
|
||||
|
||||
/// <summary>
|
||||
/// Реализация сервиса управления макетом.
|
||||
/// </summary>
|
||||
public class LayoutManager : ILayoutService
|
||||
{
|
||||
private readonly ILogger? _logger;
|
||||
private LayoutNode? _root;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public LayoutNode? Root => _root;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler? LayoutUpdated;
|
||||
|
||||
public LayoutManager(ILogger<LayoutManager>? logger = null)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dock(LayoutNode source, LayoutNode target, DockDirection direction)
|
||||
{
|
||||
if (source == target) return;
|
||||
|
||||
_logger?.LogDebug("Начало трансформации дерева: {Source} -> {Target} ({Direction})", source.Name, target.Name, direction);
|
||||
|
||||
// 1. Извлекаем источник из его текущего места в дереве
|
||||
Remove(source);
|
||||
|
||||
// 2. Если докинг в центр — это логика объединения (например, в TabView)
|
||||
// В рамках Core это может означать добавление в тот же контейнер
|
||||
if (direction == DockDirection.Center)
|
||||
{
|
||||
HandleCenterDock(source, target);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 3. Создаем разделение (Split)
|
||||
HandleSideDock(source, target, direction);
|
||||
}
|
||||
|
||||
LayoutUpdated?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Логика добавления элемента в центральную часть (вкладки).
|
||||
/// </summary>
|
||||
private void HandleCenterDock(LayoutNode source, LayoutNode target)
|
||||
{
|
||||
if (target.Parent is SplitContainerNode parent)
|
||||
{
|
||||
parent.AddChild(source);
|
||||
}
|
||||
else if (target == _root)
|
||||
{
|
||||
// Если таргет - корень, и мы докаем в центр, создаем контейнер по умолчанию
|
||||
var container = new SplitContainerNode(SplitOrientation.Horizontal);
|
||||
_root = container;
|
||||
container.AddChild(target);
|
||||
container.AddChild(source);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Логика разделения существующей области на две (Side Dock).
|
||||
/// </summary>
|
||||
private void HandleSideDock(LayoutNode source, LayoutNode target, DockDirection direction)
|
||||
{
|
||||
var orientation = (direction == DockDirection.Left || direction == DockDirection.Right)
|
||||
? SplitOrientation.Horizontal
|
||||
: SplitOrientation.Vertical;
|
||||
|
||||
var parent = target.Parent as SplitContainerNode;
|
||||
|
||||
// Создаем новый сплиттер, который заменит target
|
||||
var newContainer = new SplitContainerNode(orientation);
|
||||
|
||||
if (parent != null)
|
||||
{
|
||||
// Заменяем target на новый контейнер в списке детей родителя
|
||||
int index = parent.Children.IndexOf(target);
|
||||
parent.Children[index] = newContainer;
|
||||
newContainer.Parent = parent;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Если родителя нет, значит target был корнем
|
||||
_root = newContainer;
|
||||
}
|
||||
|
||||
// Настраиваем порядок в новом сплиттере
|
||||
if (direction == DockDirection.Left || direction == DockDirection.Top)
|
||||
{
|
||||
newContainer.AddChild(source);
|
||||
newContainer.AddChild(target);
|
||||
}
|
||||
else
|
||||
{
|
||||
newContainer.AddChild(target);
|
||||
newContainer.AddChild(source);
|
||||
}
|
||||
|
||||
// Корректируем размеры (например, делим пополам)
|
||||
source.WidthValue = 0.5;
|
||||
target.WidthValue = 0.5;
|
||||
source.IsWidthStar = true;
|
||||
target.IsWidthStar = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Remove(LayoutNode node)
|
||||
{
|
||||
if (node.Parent is SplitContainerNode parent)
|
||||
{
|
||||
parent.Children.Remove(node);
|
||||
node.Parent = null;
|
||||
|
||||
// Если в контейнере остался один элемент — убираем лишнюю вложенность
|
||||
if (parent.Children.Count == 1)
|
||||
{
|
||||
CollapseContainer(parent);
|
||||
}
|
||||
}
|
||||
else if (node == _root)
|
||||
{
|
||||
_root = null;
|
||||
}
|
||||
|
||||
LayoutUpdated?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Убирает ненужные контейнеры, если в них остался только один элемент.
|
||||
/// </summary>
|
||||
private void CollapseContainer(SplitContainerNode container)
|
||||
{
|
||||
var lastChild = container.Children[0];
|
||||
var parent = container.Parent as SplitContainerNode;
|
||||
|
||||
if (parent != null)
|
||||
{
|
||||
int index = parent.Children.IndexOf(container);
|
||||
parent.Children[index] = lastChild;
|
||||
lastChild.Parent = parent;
|
||||
}
|
||||
else
|
||||
{
|
||||
_root = lastChild;
|
||||
lastChild.Parent = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string SaveLayout()
|
||||
{
|
||||
if (_root == null) return string.Empty;
|
||||
|
||||
var options = GetJsonOptions();
|
||||
try
|
||||
{
|
||||
string json = JsonSerializer.Serialize(_root, options);
|
||||
_logger?.LogInformation("Макет успешно экспортирован в JSON. Длина: {Length}", json.Length);
|
||||
return json;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Ошибка при сохранении макета Lattice");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void LoadLayout(string jsonData)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(jsonData)) return;
|
||||
|
||||
var options = GetJsonOptions();
|
||||
try
|
||||
{
|
||||
var importedRoot = JsonSerializer.Deserialize<LayoutNode>(jsonData, options);
|
||||
if (importedRoot != null)
|
||||
{
|
||||
// При загрузке нужно восстановить связи Parent, так как они не сериализуются (циклические ссылки)
|
||||
RestoreParentLinks(importedRoot, null);
|
||||
_root = importedRoot;
|
||||
_logger?.LogInformation("Макет успешно загружен. Корневой узел: {Id}", _root.Id);
|
||||
LayoutUpdated?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Ошибка при десериализации макета Lattice");
|
||||
}
|
||||
}
|
||||
|
||||
private JsonSerializerOptions GetJsonOptions()
|
||||
{
|
||||
return new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
Converters = { new LayoutJsonConverter() },
|
||||
// Игнорируем циклы, так как мы восстановим Parent вручную
|
||||
ReferenceHandler = ReferenceHandler.IgnoreCycles
|
||||
};
|
||||
}
|
||||
|
||||
private void RestoreParentLinks(LayoutNode node, LayoutNode? parent)
|
||||
{
|
||||
node.Parent = parent;
|
||||
if (node is SplitContainerNode container)
|
||||
{
|
||||
foreach (var child in container.Children)
|
||||
{
|
||||
RestoreParentLinks(child, container);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Поддержка LTS версий и актуальной на 2026 год .NET 10 -->
|
||||
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>Lattice.Core</AssemblyName>
|
||||
<RootNamespace>Lattice.Core</RootNamespace>
|
||||
|
||||
<!-- Метаданные разработчика -->
|
||||
<Authors>FrigaT</Authors>
|
||||
<Company>FrigaT</Company>
|
||||
<RepositoryUrl>https://git.frigat.duckdns.org/FrigaT/Lattice</RepositoryUrl>
|
||||
<PackageProjectUrl>https://git.frigat.duckdns.org/FrigaT/Lattice</PackageProjectUrl>
|
||||
<Description>Core docking and layout engine for Lattice UI (WinUI 3 / Uno Platform).</Description>
|
||||
|
||||
<!-- Совместимость с Uno Platform (Trimming и AOT) -->
|
||||
<IsTrimmable Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsTrimmable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,32 +0,0 @@
|
||||
namespace Lattice.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Определение действия (команды), которое может быть отображено в интерфейсе.
|
||||
/// </summary>
|
||||
public record ActionDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Уникальный идентификатор команды.
|
||||
/// </summary>
|
||||
public string Id { get; init; } = Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Текст кнопки.
|
||||
/// </summary>
|
||||
public string Label { get; init; } = "Action";
|
||||
|
||||
/// <summary>
|
||||
/// Группа контекста, к которой привязана кнопка (например, "CodeEditor").
|
||||
/// </summary>
|
||||
public string TargetContext { get; init; } = "Common";
|
||||
|
||||
/// <summary>
|
||||
/// Указывает, активна ли кнопка в данный момент.
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Подсказка (Tooltip).
|
||||
/// </summary>
|
||||
public string Tooltip { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using Lattice.Core.Abstractions;
|
||||
|
||||
namespace Lattice.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Узел, представляющий конечный контент (вкладку, панель инструментов или документ).
|
||||
/// </summary>
|
||||
public class ContentNode : LayoutNode
|
||||
{
|
||||
/// <summary>
|
||||
/// Ссылка на визуальный или логический компонент, закрепленный в этом узле.
|
||||
/// </summary>
|
||||
public IDockableComponent? Component { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Указывает, является ли данный узел частью основной рабочей области документов.
|
||||
/// </summary>
|
||||
public bool IsDocumentArea { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый экземпляр <see cref="ContentNode"/> на основе компонента.
|
||||
/// </summary>
|
||||
/// <param name="component">Компонент содержимого.</param>
|
||||
public ContentNode(IDockableComponent component)
|
||||
{
|
||||
Component = component;
|
||||
Name = component.DisplayName;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace Lattice.Core.Models.Enums;
|
||||
|
||||
public enum DockDirection
|
||||
{
|
||||
Center,
|
||||
Left,
|
||||
Right,
|
||||
Top,
|
||||
Bottom,
|
||||
Floating,
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace Lattice.Core.Models.Enums;
|
||||
|
||||
public enum SplitOrientation
|
||||
{
|
||||
/// <summary>
|
||||
/// Элементы располагаются друг за другом по горизонтали
|
||||
/// </summary>
|
||||
Horizontal,
|
||||
/// <summary>
|
||||
/// Элементы располагаются друг за другом по вертикали
|
||||
/// </summary>
|
||||
Vertical,
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using Lattice.Core.Abstractions;
|
||||
|
||||
namespace Lattice.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Абстрактный базовый класс для всех узлов дерева компоновки.
|
||||
/// </summary>
|
||||
public abstract class LayoutNode : ILayoutElement
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public Guid Id { get; } = Guid.NewGuid();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public double WidthValue { get; set; } = 1.0;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsWidthStar { get; set; } = true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public double HeightValue { get; set; } = 1.0;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsHeightStar { get; set; } = true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ILayoutElement? Parent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Возвращает строковое представление узла для отладки.
|
||||
/// </summary>
|
||||
public override string ToString() => $"{GetType().Name} [{Name}] ({Id.ToString()[..4]})";
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
using Lattice.Core.Models.Enums;
|
||||
|
||||
namespace Lattice.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Узел-контейнер, разделяющий пространство между дочерними элементами в определенной ориентации.
|
||||
/// </summary>
|
||||
public class SplitContainerNode : LayoutNode
|
||||
{
|
||||
/// <summary>
|
||||
/// Ориентация разделения (горизонтальная или вертикальная).
|
||||
/// </summary>
|
||||
public SplitOrientation Orientation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Список дочерних узлов, находящихся внутри данного контейнера.
|
||||
/// </summary>
|
||||
public List<LayoutNode> Children { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый экземпляр <see cref="SplitContainerNode"/>.
|
||||
/// </summary>
|
||||
/// <param name="orientation">Ориентация контейнера.</param>
|
||||
public SplitContainerNode(SplitOrientation orientation)
|
||||
{
|
||||
Orientation = orientation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Добавляет дочерний узел в контейнер и устанавливает связь с родителем.
|
||||
/// </summary>
|
||||
/// <param name="child">Узел для добавления.</param>
|
||||
public void AddChild(LayoutNode child)
|
||||
{
|
||||
child.Parent = this;
|
||||
Children.Add(child);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Lattice.Core.Models
|
||||
{
|
||||
internal class WorkspaceSnapshot
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using Lattice.Core.Models;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Lattice.Core.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Конвертер для полиморфной сериализации и десериализации узлов дерева Lattice.
|
||||
/// </summary>
|
||||
public class LayoutJsonConverter : JsonConverter<LayoutNode>
|
||||
{
|
||||
public override LayoutNode? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
using var jsonDoc = JsonDocument.ParseValue(ref reader);
|
||||
var rootElement = jsonDoc.RootElement;
|
||||
|
||||
// Определяем тип узла по наличию специфических свойств
|
||||
if (rootElement.TryGetProperty("Orientation", out _))
|
||||
{
|
||||
return JsonSerializer.Deserialize<SplitContainerNode>(rootElement.GetRawText(), options);
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<ContentNode>(rootElement.GetRawText(), options);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, LayoutNode value, JsonSerializerOptions options)
|
||||
{
|
||||
// Используем стандартную сериализацию для конкретных типов
|
||||
JsonSerializer.Serialize(writer, (object)value, value.GetType(), options);
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
# Lattice.Core
|
||||
|
||||
[](#)
|
||||
[](git.frigat.duckdns.org)
|
||||
[](#)
|
||||
|
||||
**Lattice.Core** — это платформонезависимое ядро (Layout Engine) для построения сложных интерфейсов с системой докинга в стиле Visual Studio 2026.
|
||||
|
||||
Библиотека является частью экосистемы **Lattice** и отвечает исключительно за математику макета, управление деревом узлов и контекстное состояние, не имея зависимостей от конкретных UI-фреймворков.
|
||||
|
||||
## 🚀 Особенности
|
||||
|
||||
- **Агностическая архитектура**: Полная совместимость с .NET 8+, WinUI 3 и Uno Platform.
|
||||
- **Древовидная компоновка**: Управление интерфейсом через узлы (`Split` и `Content`).
|
||||
- **Context-Aware System**: Встроенный сервис отслеживания контекста для динамического переключения панелей инструментов.
|
||||
- **Smart Docking**: Алгоритмы автоматического разделения зон и схлопывания пустых контейнеров.
|
||||
- **JSON Persistence**: Полиморфная сериализация макетов для сохранения и загрузки состояний пользователя.
|
||||
|
||||
## 📁 Структура проекта
|
||||
|
||||
* `Abstractions/` — Интерфейсы для расширения системы.
|
||||
* `Models/` — Базовые сущности дерева (узлы, направления, ориентация).
|
||||
* `Engine/` — `LayoutManager`, реализующий логику трансформации дерева.
|
||||
* `Context/` — Сервисы управления активными состояниями и командами.
|
||||
* `Persistence/` — Логика сохранения макета в JSON.
|
||||
|
||||
## 🛠 Использование
|
||||
|
||||
### Создание базового макета
|
||||
|
||||
```csharp
|
||||
var layoutManager = new LayoutManager();
|
||||
|
||||
// Создаем контентные узлы
|
||||
var explorer = new ContentNode(new MyToolComponent("Solution Explorer", "Explorer"));
|
||||
var editor = new ContentNode(new MyDocumentComponent("Main.cs", "CodeEditor"));
|
||||
|
||||
// Устанавливаем редактор как корень
|
||||
layoutManager.SetRoot(editor);
|
||||
|
||||
// Прикрепляем проводник слева от редактора
|
||||
layoutManager.Dock(explorer, editor, DockDirection.Left);
|
||||
|
||||
//Переключение контекста
|
||||
var contextService = new ContextManager();
|
||||
|
||||
// Вызывается при активации вкладки в UI
|
||||
contextService.SetContext("CodeEditor");
|
||||
|
||||
// Проверка видимости команд в текущем контексте
|
||||
bool isDebugVisible = contextService.IsCommandVisible("btnDebug", "CodeEditor");
|
||||
```
|
||||
15
Lattice.Example.DragDrop/App.xaml
Normal file
@@ -0,0 +1,15 @@
|
||||
<Application
|
||||
x:Class="Lattice.Example.DragDrop.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Lattice.Example.DragDrop">
|
||||
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<!-- Fluent Theme Resources -->
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
33
Lattice.Example.DragDrop/App.xaml.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Lattice.Themes;
|
||||
using Lattice.Themes.Fluent;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Lattice.Example.DragDrop;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
private Window? _window;
|
||||
|
||||
public App()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
// Регистрируем Fluent тему
|
||||
var themeManager = ThemeManager.Current;
|
||||
themeManager.RegisterTheme(new FluentThemePack(false)); // Light тема
|
||||
themeManager.RegisterTheme(new FluentThemePack(true)); // Dark тема
|
||||
|
||||
// Применяем тему по умолчанию
|
||||
themeManager.ApplyTheme("Fluent Dark");
|
||||
|
||||
// Создаем главное окно
|
||||
_window = new MainWindow();
|
||||
_window.Activate();
|
||||
|
||||
// Регистрируем окно в трекере
|
||||
WindowTracker.Register(_window);
|
||||
}
|
||||
}
|
||||
BIN
Lattice.Example.DragDrop/Assets/LockScreenLogo.scale-200.png
Normal file
|
After Width: | Height: | Size: 432 B |
BIN
Lattice.Example.DragDrop/Assets/SplashScreen.scale-200.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
Lattice.Example.DragDrop/Assets/Square150x150Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
Lattice.Example.DragDrop/Assets/Square44x44Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 637 B |
|
After Width: | Height: | Size: 283 B |
BIN
Lattice.Example.DragDrop/Assets/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 456 B |
BIN
Lattice.Example.DragDrop/Assets/Wide310x150Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
64
Lattice.Example.DragDrop/Lattice.Example.DragDrop.csproj
Normal file
@@ -0,0 +1,64 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<RootNamespace>Lattice.Example.DragDrop</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Platforms>x86;x64;ARM64</Platforms>
|
||||
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
|
||||
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<WinUISDKReferences>false</WinUISDKReferences>
|
||||
<EnableMsixTooling>true</EnableMsixTooling>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Assets\SplashScreen.scale-200.png" />
|
||||
<Content Include="Assets\LockScreenLogo.scale-200.png" />
|
||||
<Content Include="Assets\Square150x150Logo.scale-200.png" />
|
||||
<Content Include="Assets\Square44x44Logo.scale-200.png" />
|
||||
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
|
||||
<Content Include="Assets\StoreLogo.png" />
|
||||
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Manifest Include="$(ApplicationManifest)" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
|
||||
Tools extension to be activated for this project even if the Windows App SDK Nuget
|
||||
package has not yet been restored.
|
||||
-->
|
||||
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||
<ProjectCapability Include="Msix" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260101001" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Lattice.Themes.Core\Lattice.Themes.Core.csproj" />
|
||||
<ProjectReference Include="..\Lattice.Themes.Fluent\Lattice.Themes.Fluent.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
|
||||
Explorer "Package and Publish" context menu entry to be enabled for this project even if
|
||||
the Windows App SDK Nuget package has not yet been restored.
|
||||
-->
|
||||
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Publish Properties -->
|
||||
<PropertyGroup>
|
||||
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
|
||||
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
|
||||
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
|
||||
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
141
Lattice.Example.DragDrop/MainWindow.xaml
Normal file
@@ -0,0 +1,141 @@
|
||||
<Window
|
||||
x:Class="Lattice.Example.DragDrop.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:lattice="using:Lattice.UI.DragDrop.WinUI"
|
||||
Title="Drag Drop Demo"
|
||||
>
|
||||
|
||||
<Grid Background="#F0F2F5">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Инструкция -->
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="Просто перетащите элементы справа влево!"
|
||||
FontSize="14" Margin="20" HorizontalAlignment="Center"
|
||||
FontWeight="SemiBold"/>
|
||||
|
||||
<!-- Основное содержимое -->
|
||||
<Grid Grid.Row="1" Margin="20">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- ЦЕЛЕВАЯ ЗОНА (куда бросаем) -->
|
||||
<Border Grid.Column="0"
|
||||
Background="White"
|
||||
CornerRadius="10"
|
||||
BorderThickness="2"
|
||||
BorderBrush="#4CAF50"
|
||||
Padding="20"
|
||||
Margin="0,0,10,0"
|
||||
lattice:DragDropProperties.IsDropTarget="True">
|
||||
|
||||
<StackPanel>
|
||||
<TextBlock Text="🟢 ЦЕЛЕВАЯ ЗОНА"
|
||||
FontSize="16" FontWeight="Bold"
|
||||
Foreground="#4CAF50"
|
||||
Margin="0,0,0,15"/>
|
||||
|
||||
<TextBlock x:Name="DropInfoText"
|
||||
Text="Бросьте сюда элементы"
|
||||
FontSize="14"
|
||||
Foreground="#666"
|
||||
TextWrapping="Wrap"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ЗОНА С ЭЛЕМЕНТАМИ (откуда тянем) -->
|
||||
<StackPanel Grid.Column="1"
|
||||
Background="White"
|
||||
CornerRadius="10"
|
||||
Padding="20"
|
||||
Margin="10,0,0,0">
|
||||
|
||||
<TextBlock Text="📦 ЭЛЕМЕНТЫ ДЛЯ ПЕРЕТАСКИВАНИЯ"
|
||||
FontSize="16" FontWeight="Bold"
|
||||
Foreground="#2196F3"
|
||||
Margin="0,0,0,15"/>
|
||||
|
||||
<!-- 1. TextBlock элемент -->
|
||||
<Border Padding="15"
|
||||
Background="#E3F2FD"
|
||||
CornerRadius="8"
|
||||
BorderThickness="1"
|
||||
BorderBrush="#90CAF9"
|
||||
Margin="0,0,0,10"
|
||||
lattice:DragDropProperties.IsDragSource="True"
|
||||
lattice:DragDropProperties.DragData="TextBlock Element">
|
||||
|
||||
<TextBlock Text="📝 Это TextBlock"
|
||||
FontSize="14"
|
||||
Foreground="#1565C0"
|
||||
FontWeight="SemiBold"/>
|
||||
</Border>
|
||||
|
||||
<!-- 2. Border элемент -->
|
||||
<Border Padding="15"
|
||||
Background="#E8F5E9"
|
||||
CornerRadius="8"
|
||||
BorderThickness="1"
|
||||
BorderBrush="#A5D6A7"
|
||||
Margin="0,0,0,10"
|
||||
lattice:DragDropProperties.IsDragSource="True"
|
||||
lattice:DragDropProperties.DragData="Border Element">
|
||||
|
||||
<StackPanel>
|
||||
<TextBlock Text="🟩 Это Border"
|
||||
FontSize="14"
|
||||
Foreground="#2E7D32"
|
||||
FontWeight="SemiBold"/>
|
||||
<TextBlock Text="С рамкой и заливкой"
|
||||
FontSize="12"
|
||||
Foreground="#666"
|
||||
Margin="0,5,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 3. Grid элемент -->
|
||||
<Border Padding="15"
|
||||
Background="#FFF3E0"
|
||||
CornerRadius="8"
|
||||
BorderThickness="1"
|
||||
BorderBrush="#FFCC80"
|
||||
lattice:DragDropProperties.IsDragSource="True"
|
||||
lattice:DragDropProperties.DragData="Grid Element">
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Иконка -->
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="🔲"
|
||||
FontSize="18"
|
||||
Margin="0,0,10,0"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<!-- Контент -->
|
||||
<StackPanel Grid.Column="1">
|
||||
<TextBlock Text="Это Grid"
|
||||
FontSize="14"
|
||||
Foreground="#EF6C00"
|
||||
FontWeight="SemiBold"/>
|
||||
<TextBlock Text="С несколькими колонками"
|
||||
FontSize="12"
|
||||
Foreground="#666"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
17
Lattice.Example.DragDrop/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Lattice.UI.DragDrop.WinUI;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Lattice.Example.DragDrop;
|
||||
|
||||
public sealed partial class MainWindow : Window
|
||||
{
|
||||
private bool _isInitialized = false;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
XamlInitializer.Initialize(this);
|
||||
Title = "✅ Drag & Drop работает! Перетащите элементы →";
|
||||
}
|
||||
}
|
||||
51
Lattice.Example.DragDrop/Package.appxmanifest
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
IgnorableNamespaces="uap rescap">
|
||||
|
||||
<Identity
|
||||
Name="dcfd6640-86d9-4ce7-bc17-24685f01b577"
|
||||
Publisher="CN=frost"
|
||||
Version="1.0.0.0" />
|
||||
|
||||
<mp:PhoneIdentity PhoneProductId="dcfd6640-86d9-4ce7-bc17-24685f01b577" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
|
||||
<Properties>
|
||||
<DisplayName>Lattice.Example.DragDrop</DisplayName>
|
||||
<PublisherDisplayName>frost</PublisherDisplayName>
|
||||
<Logo>Assets\StoreLogo.png</Logo>
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
<Resource Language="x-generate"/>
|
||||
</Resources>
|
||||
|
||||
<Applications>
|
||||
<Application Id="App"
|
||||
Executable="$targetnametoken$.exe"
|
||||
EntryPoint="$targetentrypoint$">
|
||||
<uap:VisualElements
|
||||
DisplayName="Lattice.Example.DragDrop"
|
||||
Description="Lattice.Example.DragDrop"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||
Square44x44Logo="Assets\Square44x44Logo.png">
|
||||
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
|
||||
<uap:SplashScreen Image="Assets\SplashScreen.png" />
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
</Capabilities>
|
||||
</Package>
|
||||
10
Lattice.Example.DragDrop/Properties/launchSettings.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Lattice.Example.DragDrop (Package)": {
|
||||
"commandName": "MsixPackage"
|
||||
},
|
||||
"Lattice.Example.DragDrop (Unpackaged)": {
|
||||
"commandName": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Lattice.Example.DragDrop/app.manifest
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="Lattice.Example.DragDrop.app"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
|
||||
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
|
||||
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
19
Lattice.IDE/App.xaml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Application
|
||||
x:Class="Lattice.IDE.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:lt="using:Lattice.Themes"
|
||||
xmlns:local="using:Lattice.IDE">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<ResourceDictionary Source="ms-appx:///Lattice.UI.Docking.WinUI/Themes/Generic.xaml" />
|
||||
|
||||
<!-- Other merged dictionaries here -->
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<!-- Other app resources here -->
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
37
Lattice.IDE/App.xaml.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Lattice.Themes;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||
|
||||
namespace Lattice.IDE
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides application-specific behavior to supplement the default Application class.
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
private Window? _window;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the singleton application object. This is the first line of authored code
|
||||
/// executed, and as such is the logical equivalent of main() or WinMain().
|
||||
/// </summary>
|
||||
public App()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the application is launched.
|
||||
/// </summary>
|
||||
/// <param name="args">Details about the launch request and process.</param>
|
||||
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
|
||||
{
|
||||
ThemeManager.Current.ApplyTheme(new FluentThemePack());
|
||||
|
||||
_window = new MainWindow();
|
||||
_window.Activate();
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
Lattice.IDE/Assets/LockScreenLogo.scale-200.png
Normal file
|
After Width: | Height: | Size: 432 B |
BIN
Lattice.IDE/Assets/SplashScreen.scale-200.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
Lattice.IDE/Assets/Square150x150Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
Lattice.IDE/Assets/Square44x44Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 637 B |
|
After Width: | Height: | Size: 283 B |
BIN
Lattice.IDE/Assets/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 456 B |
BIN
Lattice.IDE/Assets/Wide310x150Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
58
Lattice.IDE/Controls/EditorView.xaml
Normal file
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<UserControl
|
||||
x:Class="Lattice.IDE.Controls.EditorView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Lattice.IDE.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid Background="{ThemeResource Lattice.Brush.Background.Secondary}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<!-- Полоса номеров строк -->
|
||||
<ColumnDefinition Width="*"/>
|
||||
<!-- Текст кода -->
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Левая панель с номерами строк -->
|
||||
<StackPanel Grid.Column="0" Background="{ThemeResource Lattice.Brush.Background.Primary}" Padding="10,5">
|
||||
<TextBlock Text="1" Foreground="Gray" FontFamily="Cascadia Code, Consolas"/>
|
||||
<TextBlock Text="2" Foreground="Gray" FontFamily="Cascadia Code, Consolas"/>
|
||||
<TextBlock Text="3" Foreground="Gray" FontFamily="Cascadia Code, Consolas"/>
|
||||
<TextBlock Text="4" Foreground="Gray" FontFamily="Cascadia Code, Consolas"/>
|
||||
<TextBlock Text="5" Foreground="Gray" FontFamily="Cascadia Code, Consolas"/>
|
||||
<TextBlock Text="6" Foreground="Gray" FontFamily="Cascadia Code, Consolas"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Основная область редактирования -->
|
||||
<TextBox Grid.Column="1"
|
||||
AcceptsReturn="True"
|
||||
IsSpellCheckEnabled="False"
|
||||
TextWrapping="NoWrap"
|
||||
FontFamily="Cascadia Code, Consolas"
|
||||
FontSize="14"
|
||||
BorderThickness="0"
|
||||
Padding="10"
|
||||
Background="Transparent"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Auto"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||
xml:space="preserve">
|
||||
<TextBox.Text>using System;
|
||||
using Lattice.Core;
|
||||
|
||||
namespace Lattice.IDE.Demo;
|
||||
|
||||
public class Program
|
||||
{
|
||||
public void Main()
|
||||
{
|
||||
Console.WriteLine("Hello, Lattice 2026!");
|
||||
}
|
||||
}
|
||||
</TextBox.Text>
|
||||
</TextBox>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
15
Lattice.IDE/Controls/EditorView.xaml.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||
|
||||
namespace Lattice.IDE.Controls
|
||||
{
|
||||
public sealed partial class EditorView : UserControl
|
||||
{
|
||||
public EditorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
40
Lattice.IDE/Controls/SolutionExplorerView.xaml
Normal file
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<UserControl
|
||||
x:Class="Lattice.IDE.Controls.SolutionExplorerView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Lattice.IDE.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid Background="{ThemeResource LayerFillColorDefaultBrush}" Padding="10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="SOLUTION 'LATTICE' (2026)"
|
||||
FontSize="11"
|
||||
FontWeight="Bold"
|
||||
Opacity="0.6"/>
|
||||
|
||||
<TreeView Grid.Row="1" Margin="0,10,0,0">
|
||||
<TreeView.RootNodes>
|
||||
<TreeViewNode Content="Lattice.Core.Docking" IsExpanded="True">
|
||||
<TreeViewNode.Children>
|
||||
<TreeViewNode Content="Models" />
|
||||
<TreeViewNode Content="Engine" />
|
||||
</TreeViewNode.Children>
|
||||
</TreeViewNode>
|
||||
<TreeViewNode Content="Lattice.UI.Docking.WinUI" IsExpanded="True">
|
||||
<TreeViewNode.Children>
|
||||
<TreeViewNode Content="Controls" />
|
||||
<TreeViewNode Content="Themes" />
|
||||
</TreeViewNode.Children>
|
||||
</TreeViewNode>
|
||||
</TreeView.RootNodes>
|
||||
</TreeView>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
15
Lattice.IDE/Controls/SolutionExplorerView.xaml.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||
|
||||
namespace Lattice.IDE.Controls
|
||||
{
|
||||
public sealed partial class SolutionExplorerView : UserControl
|
||||
{
|
||||
public SolutionExplorerView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
81
Lattice.IDE/Lattice.IDE.csproj
Normal file
@@ -0,0 +1,81 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFrameworks>net8.0-windows10.0.19041.0;net9.0-windows10.0.19041.0;net10.0-windows10.0.19041.0</TargetFrameworks>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<RootNamespace>Lattice.IDE</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Platforms>x86;x64;ARM64</Platforms>
|
||||
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
|
||||
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<WinUISDKReferences>false</WinUISDKReferences>
|
||||
<EnableMsixTooling>true</EnableMsixTooling>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Controls\EditorView.xaml" />
|
||||
<None Remove="Controls\SolutionExplorerView.xaml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Assets\SplashScreen.scale-200.png" />
|
||||
<Content Include="Assets\LockScreenLogo.scale-200.png" />
|
||||
<Content Include="Assets\Square150x150Logo.scale-200.png" />
|
||||
<Content Include="Assets\Square44x44Logo.scale-200.png" />
|
||||
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
|
||||
<Content Include="Assets\StoreLogo.png" />
|
||||
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Manifest Include="$(ApplicationManifest)" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
|
||||
Tools extension to be activated for this project even if the Windows App SDK Nuget
|
||||
package has not yet been restored.
|
||||
-->
|
||||
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||
<ProjectCapability Include="Msix" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Lattice.Core.Docking\Lattice.Core.Docking.csproj" />
|
||||
<ProjectReference Include="..\Lattice.Themes.Core\Lattice.Themes.Core.csproj" />
|
||||
<ProjectReference Include="..\Lattice.Themes.Fluent\Lattice.Themes.Fluent.csproj" />
|
||||
<ProjectReference Include="..\Lattice.Themes.VS2026\Lattice.Themes.VS2026.csproj" />
|
||||
<ProjectReference Include="..\Lattice.UI.Docking.WinUI\Lattice.UI.Docking.WinUI.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\EditorView.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\SolutionExplorerView.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
|
||||
Explorer "Package and Publish" context menu entry to be enabled for this project even if
|
||||
the Windows App SDK Nuget package has not yet been restored.
|
||||
-->
|
||||
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Publish Properties -->
|
||||
<PropertyGroup>
|
||||
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
|
||||
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
|
||||
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
|
||||
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
28
Lattice.IDE/Layout/DemoContent.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Lattice.Core.Docking.Abstractions;
|
||||
|
||||
namespace Lattice.IDE;
|
||||
|
||||
/// <summary>
|
||||
/// Реализация контента для демонстрации, принимающая любой UI-объект.
|
||||
/// </summary>
|
||||
public class DemoContent : IDockContent
|
||||
{
|
||||
public string Id { get; }
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Сюда мы передаем наш UserControl (SolutionExplorerView и т.д.)
|
||||
/// </summary>
|
||||
public object View { get; set; }
|
||||
|
||||
public bool CanClose { get; set; } = true;
|
||||
|
||||
public DemoContent(string id, string title, object view)
|
||||
{
|
||||
Id = id;
|
||||
Title = title;
|
||||
View = view;
|
||||
}
|
||||
|
||||
public bool OnClosing() => true;
|
||||
}
|
||||
29
Lattice.IDE/MainWindow.xaml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Window
|
||||
x:Class="Lattice.IDE.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Lattice.IDE"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:lattice="using:Lattice.UI"
|
||||
mc:Ignorable="d"
|
||||
Title="Lattice.IDE">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Меню управления Demo -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" Padding="10" Background="{ThemeResource SystemControlBackgroundChromeMediumLowBrush}">
|
||||
<Button Content="Fluent UI Theme" Click="SetFluentTheme"/>
|
||||
<Button Content="VS 2026 Theme" Click="SetVSTheme"/>
|
||||
<TextBlock Text="Lattice IDE Demo 2026" VerticalAlignment="Center" Margin="20,0" FontWeight="Bold"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Lattice Docking Host -->
|
||||
<lattice:LatticeDockHost x:Name="DockHost" Grid.Row="1" />
|
||||
</Grid>
|
||||
</Window>
|
||||
64
Lattice.IDE/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using Lattice.Core.Docking.Engine;
|
||||
using Lattice.Core.Docking.Models;
|
||||
using Lattice.IDE.Controls;
|
||||
using Lattice.Themes;
|
||||
using Microsoft.UI.Xaml;
|
||||
// Ïðåäïîëîæèì, ÷òî VS2026Theme òîæå ðåàëèçîâàí àíàëîãè÷íî Fluent
|
||||
// using Lattice.Themes.VisualStudio2026;
|
||||
|
||||
namespace Lattice.IDE;
|
||||
|
||||
public sealed partial class MainWindow : Window
|
||||
{
|
||||
private LayoutManager _manager;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
WindowTracker.Register(this);
|
||||
SystemBackdrop = new Microsoft.UI.Xaml.Media.MicaBackdrop();
|
||||
InitLattice();
|
||||
}
|
||||
|
||||
private void InitLattice()
|
||||
{
|
||||
_manager = new LayoutManager();
|
||||
|
||||
// Ñîçäàåì êîíòåíò íà îñíîâå XAML UserControls
|
||||
var solutionExplorer = new DemoContent(
|
||||
"sln",
|
||||
"Solution Explorer",
|
||||
new SolutionExplorerView()
|
||||
);
|
||||
|
||||
var editor = new DemoContent(
|
||||
"code_01",
|
||||
"Program.cs",
|
||||
new EditorView()
|
||||
);
|
||||
|
||||
// Ñîáèðàåì äåðåâî (êàê ðàíüøå)
|
||||
var leftLeaf = new DockLeaf();
|
||||
leftLeaf.AddContent(solutionExplorer);
|
||||
|
||||
var centerLeaf = new DockLeaf()
|
||||
{
|
||||
TabPlacement = TabPlacement.Top,
|
||||
};
|
||||
centerLeaf.AddContent(editor);
|
||||
|
||||
var rootGroup = new DockGroup(leftLeaf, centerLeaf, SplitDirection.Horizontal)
|
||||
{
|
||||
SplitRatio = 0.25
|
||||
};
|
||||
|
||||
_manager.SetRoot(rootGroup);
|
||||
DockHost.Manager = _manager;
|
||||
}
|
||||
|
||||
private void SetFluentTheme(object sender, RoutedEventArgs e) =>
|
||||
ThemeManager.Current.ApplyTheme(new FluentThemePack());
|
||||
|
||||
private void SetVSTheme(object sender, RoutedEventArgs e) =>
|
||||
ThemeManager.Current.ApplyTheme(new VS2026ThemePack());
|
||||
}
|
||||
51
Lattice.IDE/Package.appxmanifest
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
IgnorableNamespaces="uap rescap">
|
||||
|
||||
<Identity
|
||||
Name="c6596033-98b6-4778-8280-bc9256b9be07"
|
||||
Publisher="CN=frost"
|
||||
Version="1.0.0.0" />
|
||||
|
||||
<mp:PhoneIdentity PhoneProductId="c6596033-98b6-4778-8280-bc9256b9be07" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
|
||||
<Properties>
|
||||
<DisplayName>Lattice.IDE</DisplayName>
|
||||
<PublisherDisplayName>frost</PublisherDisplayName>
|
||||
<Logo>Assets\StoreLogo.png</Logo>
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
<Resource Language="x-generate"/>
|
||||
</Resources>
|
||||
|
||||
<Applications>
|
||||
<Application Id="App"
|
||||
Executable="$targetnametoken$.exe"
|
||||
EntryPoint="$targetentrypoint$">
|
||||
<uap:VisualElements
|
||||
DisplayName="Lattice.IDE"
|
||||
Description="Lattice.IDE"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||
Square44x44Logo="Assets\Square44x44Logo.png">
|
||||
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
|
||||
<uap:SplashScreen Image="Assets\SplashScreen.png" />
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
</Capabilities>
|
||||
</Package>
|
||||
10
Lattice.IDE/Properties/launchSettings.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Lattice.IDE (Package)": {
|
||||
"commandName": "MsixPackage"
|
||||
},
|
||||
"Lattice.IDE (Unpackaged)": {
|
||||
"commandName": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Lattice.IDE/app.manifest
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="Lattice.IDE.app"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
|
||||
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
|
||||
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
30
Lattice.Layout.UI.WinUI/Controls/WinUIGroupControl.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Lattice.Layout.UI.WinUI.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Контрол для отображения группы вкладок.
|
||||
/// Содержит заголовки вкладок и область содержимого.
|
||||
/// </summary>
|
||||
public sealed class WinUIGroupControl : Grid
|
||||
{
|
||||
/// <summary>
|
||||
/// Контрол TabView, содержащий вкладки и их содержимое.
|
||||
/// </summary>
|
||||
public TabView TabView { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт новый экземпляр <see cref="WinUIGroupControl"/>.
|
||||
/// </summary>
|
||||
public WinUIGroupControl()
|
||||
{
|
||||
TabView = new TabView
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
VerticalAlignment = VerticalAlignment.Stretch
|
||||
};
|
||||
|
||||
Children.Add(TabView);
|
||||
}
|
||||
}
|
||||
134
Lattice.Layout.UI.WinUI/Controls/WinUIItemControl.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using System;
|
||||
|
||||
namespace Lattice.Layout.UI.WinUI.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Контрол для отображения содержимого конечного элемента раскладки.
|
||||
/// Является контейнером для реального UI-контента, подставляемого через ContentResolver.
|
||||
/// </summary>
|
||||
public sealed class WinUIItemControl : ContentControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Идентификатор содержимого, связанного с данным элементом.
|
||||
/// Используется для подстановки реального UI-контрола через ContentResolver.
|
||||
/// </summary>
|
||||
public string? ContentId
|
||||
{
|
||||
get => (string?)GetValue(ContentIdProperty);
|
||||
set => SetValue(ContentIdProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ContentIdProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(ContentId),
|
||||
typeof(string),
|
||||
typeof(WinUIItemControl),
|
||||
new PropertyMetadata(default(string), OnContentIdChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Делегат, который должен вернуть реальный UI-контент по ContentId.
|
||||
/// Устанавливается WinUILayoutHost.
|
||||
/// </summary>
|
||||
public Func<string, UIElement?>? ContentResolver
|
||||
{
|
||||
get => (Func<string, UIElement?>?)GetValue(ContentResolverProperty);
|
||||
set => SetValue(ContentResolverProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ContentResolverProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(ContentResolver),
|
||||
typeof(Func<string, UIElement?>),
|
||||
typeof(WinUIItemControl),
|
||||
new PropertyMetadata(null, OnContentResolverChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Вызывается, когда контент успешно загружен.
|
||||
/// </summary>
|
||||
public event Action<WinUIItemControl>? ContentLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// Вызывается, когда контент был очищен (Detach).
|
||||
/// </summary>
|
||||
public event Action<WinUIItemControl>? ContentCleared;
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт новый экземпляр <see cref="WinUIItemControl"/>.
|
||||
/// </summary>
|
||||
public WinUIItemControl()
|
||||
{
|
||||
HorizontalContentAlignment = HorizontalAlignment.Stretch;
|
||||
VerticalContentAlignment = VerticalAlignment.Stretch;
|
||||
|
||||
// Fallback-контент, если ContentResolver не установлен
|
||||
Content = CreatePlaceholder();
|
||||
}
|
||||
|
||||
private static void OnContentIdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is WinUIItemControl control)
|
||||
control.TryLoadContent();
|
||||
}
|
||||
|
||||
private static void OnContentResolverChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is WinUIItemControl control)
|
||||
control.TryLoadContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Пытается загрузить реальный контент по ContentId.
|
||||
/// </summary>
|
||||
private void TryLoadContent()
|
||||
{
|
||||
if (ContentId is null || ContentResolver is null)
|
||||
{
|
||||
Content = CreatePlaceholder();
|
||||
return;
|
||||
}
|
||||
|
||||
var resolved = ContentResolver(ContentId);
|
||||
|
||||
if (resolved is null)
|
||||
{
|
||||
Content = CreatePlaceholder($"Контент '{ContentId}' не найден");
|
||||
return;
|
||||
}
|
||||
|
||||
Content = resolved;
|
||||
ContentLoaded?.Invoke(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Очищает контент (используется визуалом при Detach).
|
||||
/// </summary>
|
||||
public void ClearContent()
|
||||
{
|
||||
Content = CreatePlaceholder();
|
||||
ContentCleared?.Invoke(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт placeholder-контент, отображаемый до загрузки реального UI.
|
||||
/// </summary>
|
||||
private static UIElement CreatePlaceholder(string? message = null)
|
||||
{
|
||||
return new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Microsoft.UI.Colors.Transparent),
|
||||
BorderBrush = new SolidColorBrush(Microsoft.UI.Colors.Gray),
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(8),
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = message ?? "Нет содержимого",
|
||||
Opacity = 0.6,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
134
Lattice.Layout.UI.WinUI/Controls/WinUILayoutHost.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
using Lattice.Layout.Abstractions;
|
||||
using Lattice.Layout.UI.Docking;
|
||||
using Lattice.Layout.UI.WinUI.Docking;
|
||||
using Lattice.Layout.UI.WinUI.Rendering;
|
||||
using Lattice.Layout.UI.WinUI.Visuals;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System;
|
||||
|
||||
namespace Lattice.Layout.UI.WinUI.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// WinUI-контрол, отображающий дерево раскладки.
|
||||
/// Оборачивает LayoutRenderer и размещает визуальное дерево внутри себя.
|
||||
/// </summary>
|
||||
public sealed class WinUILayoutHost : UserControl, ILayoutView
|
||||
{
|
||||
/// <summary>
|
||||
/// Слой, в котором размещается визуальное дерево раскладки.
|
||||
/// </summary>
|
||||
public Grid LayoutLayer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Слой для отображения подсветки зон докинга.
|
||||
/// </summary>
|
||||
public DockOverlayHost OverlayLayer { get; }
|
||||
|
||||
private readonly LayoutRenderer _renderer;
|
||||
private readonly WinUIVisualFactory _factory;
|
||||
|
||||
/// <summary>
|
||||
/// Функция, возвращающая UI-содержимое по ContentId.
|
||||
/// Используется визуальными элементами для подстановки реального контрола.
|
||||
/// </summary>
|
||||
public Func<string, UIElement>? ContentResolver { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Корневой элемент раскладки, который необходимо отобразить.
|
||||
/// </summary>
|
||||
public ILayoutRoot? Root
|
||||
{
|
||||
get => (ILayoutRoot?)GetValue(RootProperty);
|
||||
set => SetValue(RootProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Свойство зависимости для <see cref="Root"/>.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty RootProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(Root),
|
||||
typeof(ILayoutRoot),
|
||||
typeof(WinUILayoutHost),
|
||||
new PropertyMetadata(null, OnRootChanged));
|
||||
|
||||
private static void OnRootChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is WinUILayoutHost host)
|
||||
{
|
||||
host.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт новый экземпляр <see cref="WinUILayoutHost"/>.
|
||||
/// </summary>
|
||||
public WinUILayoutHost()
|
||||
{
|
||||
LayoutLayer = new Grid();
|
||||
OverlayLayer = new DockOverlayHost();
|
||||
|
||||
var rootGrid = new Grid();
|
||||
rootGrid.Children.Add(LayoutLayer);
|
||||
rootGrid.Children.Add(OverlayLayer);
|
||||
|
||||
Content = rootGrid;
|
||||
|
||||
_factory = new WinUIVisualFactory();
|
||||
_renderer = new LayoutRenderer(_factory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Выполняет полную перерисовку визуального дерева.
|
||||
/// </summary>
|
||||
public void Refresh()
|
||||
{
|
||||
LayoutLayer.Children.Clear();
|
||||
|
||||
if (Root?.Child is null)
|
||||
return;
|
||||
|
||||
var visual = _renderer.Build(Root.Child);
|
||||
visual.Attach();
|
||||
|
||||
switch (visual)
|
||||
{
|
||||
case WinUISplitVisual splitVisual:
|
||||
LayoutLayer.Children.Add(splitVisual.Control);
|
||||
break;
|
||||
|
||||
case WinUIGroupVisual groupVisual:
|
||||
LayoutLayer.Children.Add(groupVisual.Control);
|
||||
break;
|
||||
|
||||
case WinUIItemVisual itemVisual:
|
||||
LayoutLayer.Children.Add(itemVisual.Control);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Показывает подсветку зоны докинга для указанной цели.
|
||||
/// </summary>
|
||||
public void ShowDockOverlay(DockTarget target)
|
||||
{
|
||||
if (target.Visual is not IWinUIVisual winuiVisual)
|
||||
return;
|
||||
|
||||
var control = winuiVisual.Control;
|
||||
|
||||
var bounds = control.TransformToVisual(LayoutLayer)
|
||||
.TransformBounds(new Windows.Foundation.Rect(0, 0, control.ActualWidth, control.ActualHeight));
|
||||
|
||||
OverlayLayer.ShowOverlay(target, bounds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Скрывает подсветку зон докинга.
|
||||
/// </summary>
|
||||
public void HideDockOverlay()
|
||||
{
|
||||
OverlayLayer.HideOverlay();
|
||||
}
|
||||
}
|
||||
85
Lattice.Layout.UI.WinUI/Controls/WinUISplitControl.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Lattice.Layout.UI.WinUI.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Контейнер для отображения сплит-элемента раскладки.
|
||||
/// Использует Grid и автоматически создаёт строки/столбцы под детей.
|
||||
/// </summary>
|
||||
public sealed class WinUISplitControl : Grid
|
||||
{
|
||||
/// <summary>
|
||||
/// Ориентация сплита (горизонтальная или вертикальная).
|
||||
/// </summary>
|
||||
public Orientation LayoutOrientation
|
||||
{
|
||||
get => (Orientation)GetValue(LayoutOrientationProperty);
|
||||
set => SetValue(LayoutOrientationProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty LayoutOrientationProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(LayoutOrientation),
|
||||
typeof(Orientation),
|
||||
typeof(WinUISplitControl),
|
||||
new PropertyMetadata(Orientation.Horizontal, OnOrientationChanged));
|
||||
|
||||
private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is WinUISplitControl control)
|
||||
control.RebuildGrid();
|
||||
}
|
||||
|
||||
public WinUISplitControl()
|
||||
{
|
||||
Loaded += (_, _) => RebuildGrid();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Перестраивает структуру Grid в зависимости от ориентации и количества детей.
|
||||
/// </summary>
|
||||
public void RebuildGrid()
|
||||
{
|
||||
RowDefinitions.Clear();
|
||||
ColumnDefinitions.Clear();
|
||||
|
||||
if (Children.Count == 0)
|
||||
return;
|
||||
|
||||
if (LayoutOrientation == Orientation.Horizontal)
|
||||
{
|
||||
// Горизонтальный сплит → столбцы
|
||||
for (int i = 0; i < Children.Count; i++)
|
||||
{
|
||||
ColumnDefinitions.Add(new ColumnDefinition
|
||||
{
|
||||
Width = new GridLength(1, GridUnitType.Star)
|
||||
});
|
||||
|
||||
if (Children[i] is FrameworkElement fe) Grid.SetColumn(fe, i);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Вертикальный сплит → строки
|
||||
for (int i = 0; i < Children.Count; i++)
|
||||
{
|
||||
RowDefinitions.Add(new RowDefinition
|
||||
{
|
||||
Height = new GridLength(1, GridUnitType.Star)
|
||||
});
|
||||
|
||||
if (Children[i] is FrameworkElement fe) Grid.SetColumn(fe, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Добавляет дочерний элемент и перестраивает Grid.
|
||||
/// </summary>
|
||||
public new void ChildrenChanged()
|
||||
{
|
||||
RebuildGrid();
|
||||
}
|
||||
}
|
||||
83
Lattice.Layout.UI.WinUI/Docking/DockOverlay.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using Lattice.Layout.Abstractions;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Lattice.Layout.UI.WinUI.Docking;
|
||||
|
||||
/// <summary>
|
||||
/// Полупрозрачная подсветка зоны докинга.
|
||||
/// </summary>
|
||||
public sealed class DockOverlay : Control
|
||||
{
|
||||
public static readonly DependencyProperty ZoneProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(Zone),
|
||||
typeof(DockZone),
|
||||
typeof(DockOverlay),
|
||||
new PropertyMetadata(DockZone.Center, OnVisualPropertyChanged));
|
||||
|
||||
public static readonly DependencyProperty BoundsProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(Bounds),
|
||||
typeof(Rect),
|
||||
typeof(DockOverlay),
|
||||
new PropertyMetadata(Rect.Empty, OnVisualPropertyChanged));
|
||||
|
||||
/// <summary>
|
||||
/// Зона докинга, которую нужно подсветить.
|
||||
/// </summary>
|
||||
public DockZone Zone
|
||||
{
|
||||
get => (DockZone)GetValue(ZoneProperty);
|
||||
set => SetValue(ZoneProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Прямоугольник зоны в координатах родительского контейнера.
|
||||
/// </summary>
|
||||
public Rect Bounds
|
||||
{
|
||||
get => (Rect)GetValue(BoundsProperty);
|
||||
set => SetValue(BoundsProperty, value);
|
||||
}
|
||||
|
||||
private Border? _border;
|
||||
|
||||
public DockOverlay()
|
||||
{
|
||||
IsHitTestVisible = false;
|
||||
DefaultStyleKey = typeof(DockOverlay);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
_border = GetTemplateChild("PART_Border") as Border;
|
||||
UpdateVisual();
|
||||
}
|
||||
|
||||
private static void OnVisualPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is DockOverlay overlay)
|
||||
{
|
||||
overlay.UpdateVisual();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateVisual()
|
||||
{
|
||||
if (_border is null)
|
||||
return;
|
||||
|
||||
Canvas.SetLeft(this, Bounds.X);
|
||||
Canvas.SetTop(this, Bounds.Y);
|
||||
Width = Bounds.Width;
|
||||
Height = Bounds.Height;
|
||||
|
||||
_border.BorderBrush = new SolidColorBrush(Microsoft.UI.Colors.DeepSkyBlue);
|
||||
_border.BorderThickness = new Thickness(2);
|
||||
_border.Background = new SolidColorBrush(Microsoft.UI.Colors.LightSkyBlue) { Opacity = 0.25 };
|
||||
}
|
||||
}
|
||||
47
Lattice.Layout.UI.WinUI/Docking/DockOverlayHost.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Lattice.Layout.UI.Docking;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Lattice.Layout.UI.WinUI.Docking;
|
||||
|
||||
/// <summary>
|
||||
/// Контейнер для отображения подсветки зон докинга.
|
||||
/// Обычно используется как Overlay-слой внутри WinUILayoutHost.
|
||||
/// </summary>
|
||||
public sealed class DockOverlayHost : Canvas
|
||||
{
|
||||
private DockOverlay? _currentOverlay;
|
||||
|
||||
public DockOverlayHost()
|
||||
{
|
||||
IsHitTestVisible = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отображает подсветку для указанной цели докинга.
|
||||
/// </summary>
|
||||
public void ShowOverlay(DockTarget target, Rect bounds)
|
||||
{
|
||||
if (_currentOverlay is null)
|
||||
{
|
||||
_currentOverlay = new DockOverlay();
|
||||
Children.Add(_currentOverlay);
|
||||
}
|
||||
|
||||
_currentOverlay.Zone = target.Zone;
|
||||
_currentOverlay.Bounds = bounds;
|
||||
_currentOverlay.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Скрывает подсветку зоны докинга.
|
||||
/// </summary>
|
||||
public void HideOverlay()
|
||||
{
|
||||
if (_currentOverlay is not null)
|
||||
{
|
||||
_currentOverlay.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
78
Lattice.Layout.UI.WinUI/Docking/DockZoneHitTester.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using Lattice.Layout.UI.Docking;
|
||||
using Lattice.Layout.UI.WinUI.Controls;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Lattice.Layout.UI.WinUI.Docking;
|
||||
|
||||
/// <summary>
|
||||
/// Выполняет hit-test для определения зоны докинга в WinUI.
|
||||
/// </summary>
|
||||
public static class DockZoneHitTester
|
||||
{
|
||||
/// <summary>
|
||||
/// Выполняет hit-test по экранной точке и возвращает цель докинга.
|
||||
/// </summary>
|
||||
/// <param name="host">WinUI-хост раскладки.</param>
|
||||
/// <param name="screenPoint">Точка в координатах окна.</param>
|
||||
public static DockTarget? HitTest(WinUILayoutHost host, Point screenPoint)
|
||||
{
|
||||
if (host is null)
|
||||
throw new ArgumentNullException(nameof(host));
|
||||
|
||||
// Предполагаем, что LayoutLayer — основной слой, в котором живёт визуальное дерево.
|
||||
var layoutLayer = host.LayoutLayer;
|
||||
if (layoutLayer is null)
|
||||
return null;
|
||||
|
||||
// Переводим координаты в систему координат layoutLayer.
|
||||
var elements = VisualTreeHelper.FindElementsInHostCoordinates(screenPoint, host.LayoutLayer);
|
||||
|
||||
var firstElement = elements.FirstOrDefault();
|
||||
|
||||
if (firstElement is null)
|
||||
return null;
|
||||
|
||||
// Ищем ближайший IWinUIVisual.
|
||||
var visual = FindVisual(firstElement);
|
||||
if (visual is null)
|
||||
return null;
|
||||
|
||||
if (visual is not ILayoutVisual layoutVisual)
|
||||
return null;
|
||||
|
||||
// Вычисляем зону докинга для найденного контрола.
|
||||
var control = visual.Control;
|
||||
|
||||
var bounds = control.TransformToVisual(layoutLayer)
|
||||
.TransformBounds(new Rect(0, 0, control.ActualWidth, control.ActualHeight));
|
||||
|
||||
var localX = screenPoint.X - bounds.X;
|
||||
var localY = screenPoint.Y - bounds.Y;
|
||||
|
||||
var zone = DockingUtils.GetZone(localX, localY, bounds.Width, bounds.Height);
|
||||
|
||||
return new DockTarget(layoutVisual, zone);
|
||||
}
|
||||
|
||||
private static IWinUIVisual? FindVisual(UIElement element)
|
||||
{
|
||||
DependencyObject? current = element;
|
||||
|
||||
while (current is not null)
|
||||
{
|
||||
if (current is FrameworkElement fe && fe.DataContext is IWinUIVisual ctxVisual)
|
||||
return ctxVisual;
|
||||
|
||||
if (current is IWinUIVisual winuiVisual)
|
||||
return winuiVisual;
|
||||
|
||||
current = VisualTreeHelper.GetParent(current);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
15
Lattice.Layout.UI.WinUI/Docking/IWinUIVisual.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Lattice.Layout.UI.WinUI.Docking;
|
||||
|
||||
/// <summary>
|
||||
/// Интерфейс для визуальных элементов WinUI, соответствующих элементам раскладки.
|
||||
/// Нужен для hit-test и расчёта зон докинга.
|
||||
/// </summary>
|
||||
public interface IWinUIVisual
|
||||
{
|
||||
/// <summary>
|
||||
/// Реальный WinUI-элемент, отображающий данный визуальный элемент раскладки.
|
||||
/// </summary>
|
||||
FrameworkElement Control { get; }
|
||||
}
|
||||
118
Lattice.Layout.UI.WinUI/Helpers/LayoutHostExtensions.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using Lattice.Layout.Abstractions;
|
||||
using Lattice.Layout.UI.WinUI.Controls;
|
||||
using Microsoft.UI.Xaml;
|
||||
using System;
|
||||
|
||||
namespace Lattice.Layout.UI.WinUI;
|
||||
|
||||
/// <summary>
|
||||
/// Набор вспомогательных методов для упрощённой работы с визуальным хостом раскладки.
|
||||
/// Позволяет быстро подключать LayoutManager, обновлять UI и связывать содержимое.
|
||||
/// </summary>
|
||||
public static class LayoutHostExtensions
|
||||
{
|
||||
// ------------------------------------------------------------------------
|
||||
// 1. Подключение LayoutManager к любому ILayoutView
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Подписывает визуальный хост на события изменения раскладки.
|
||||
/// При каждом изменении модели вызывается <see cref="ILayoutView.Refresh"/>.
|
||||
/// </summary>
|
||||
/// <param name="view">Визуальный хост раскладки.</param>
|
||||
/// <param name="manager">Менеджер раскладки.</param>
|
||||
public static void BindToManager(this ILayoutView view, ILayoutManager manager)
|
||||
{
|
||||
manager.LayoutChanged += (_, _) => view.Refresh();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 2. Полная инициализация раскладки (Root + Manager)
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Устанавливает корневой элемент раскладки, подключает менеджер и выполняет начальную отрисовку.
|
||||
/// </summary>
|
||||
/// <param name="view">Визуальный хост раскладки.</param>
|
||||
/// <param name="root">Корневой элемент раскладки.</param>
|
||||
/// <param name="manager">Менеджер раскладки.</param>
|
||||
public static void UseLayout(this ILayoutView view, ILayoutRoot root, ILayoutManager manager)
|
||||
{
|
||||
view.Root = root;
|
||||
view.BindToManager(manager);
|
||||
view.Refresh();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 3. Удобный fluent-API
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Устанавливает корневой элемент раскладки и возвращает хост для fluent-цепочек.
|
||||
/// </summary>
|
||||
public static T WithRoot<T>(this T view, ILayoutRoot root)
|
||||
where T : ILayoutView
|
||||
{
|
||||
view.Root = root;
|
||||
return view;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Подключает менеджер раскладки и возвращает хост для fluent-цепочек.
|
||||
/// </summary>
|
||||
public static T WithManager<T>(this T view, ILayoutManager manager)
|
||||
where T : ILayoutView
|
||||
{
|
||||
view.BindToManager(manager);
|
||||
return view;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Выполняет начальную отрисовку и возвращает хост для fluent-цепочек.
|
||||
/// </summary>
|
||||
public static T Initialize<T>(this T view)
|
||||
where T : ILayoutView
|
||||
{
|
||||
view.Refresh();
|
||||
return view;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 4. Поддержка WinUILayoutHost: резолвер контента
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Устанавливает функцию, которая по ContentId возвращает реальный UI-элемент.
|
||||
/// Используется для отображения содержимого вкладок.
|
||||
/// </summary>
|
||||
/// <param name="host">WinUI-хост раскладки.</param>
|
||||
/// <param name="resolver">Функция, возвращающая UIElement по ContentId.</param>
|
||||
public static void UseContentResolver(this WinUILayoutHost host, Func<string, UIElement> resolver)
|
||||
{
|
||||
host.ContentResolver = resolver;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 5. Полная WinUI-инициализация (Root + Manager + ContentResolver)
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Полностью инициализирует WinUI-хост раскладки:
|
||||
/// устанавливает корень, подключает менеджер, задаёт резолвер содержимого и выполняет отрисовку.
|
||||
/// </summary>
|
||||
/// <param name="host">WinUI-хост раскладки.</param>
|
||||
/// <param name="root">Корневой элемент раскладки.</param>
|
||||
/// <param name="manager">Менеджер раскладки.</param>
|
||||
/// <param name="resolver">Функция получения UI-содержимого по ContentId.</param>
|
||||
public static void UseLayout(
|
||||
this WinUILayoutHost host,
|
||||
ILayoutRoot root,
|
||||
ILayoutManager manager,
|
||||
Func<string, UIElement> resolver)
|
||||
{
|
||||
host.Root = root;
|
||||
host.BindToManager(manager);
|
||||
host.ContentResolver = resolver;
|
||||
host.Refresh();
|
||||
}
|
||||
}
|
||||
18
Lattice.Layout.UI.WinUI/Lattice.Layout.UI.WinUI.csproj
Normal file
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0-windows10.0.19041.0;net9.0-windows10.0.19041.0;net10.0-windows10.0.19041.0</TargetFrameworks>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<RootNamespace>Lattice.Layout.UI.WinUI</RootNamespace>
|
||||
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<WinUISDKReferences>false</WinUISDKReferences>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Lattice.Layout.UI\Lattice.Layout.UI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
30
Lattice.Layout.UI.WinUI/Rendering/WinUIVisualFactory.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Lattice.Layout.Abstractions;
|
||||
using Lattice.Layout.UI.WinUI.Visuals;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Lattice.Layout.UI.WinUI.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Фабрика визуальных элементов для WinUI.
|
||||
/// Создаёт визуальные представления сплитов, групп и элементов.
|
||||
/// </summary>
|
||||
public sealed class WinUIVisualFactory : ILayoutVisualFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ILayoutVisual CreateSplit(ILayoutSplit split, IReadOnlyList<ILayoutVisual> children)
|
||||
{
|
||||
return new WinUISplitVisual(split, children);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ILayoutVisual CreateGroup(ILayoutGroup group, IReadOnlyList<ILayoutVisual> items)
|
||||
{
|
||||
return new WinUIGroupVisual(group, items);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ILayoutVisual CreateItem(ILayoutItem item)
|
||||
{
|
||||
return new WinUIItemVisual(item);
|
||||
}
|
||||
}
|
||||
81
Lattice.Layout.UI.WinUI/Visuals/WinUIGroupVisual.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using Lattice.Layout.Abstractions;
|
||||
using Lattice.Layout.UI.WinUI.Controls;
|
||||
using Lattice.Layout.UI.WinUI.Docking;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Lattice.Layout.UI.WinUI.Visuals;
|
||||
|
||||
/// <summary>
|
||||
/// Визуальное представление группы вкладок для WinUI.
|
||||
/// Управляет <see cref="WinUIGroupControl"/> и вкладками <see cref="TabViewItem"/>.
|
||||
/// </summary>
|
||||
public sealed class WinUIGroupVisual : LayoutVisual, IWinUIVisual
|
||||
{
|
||||
/// <summary>
|
||||
/// Реальный WinUI-контрол группы вкладок.
|
||||
/// </summary>
|
||||
public WinUIGroupControl GroupControl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Контрол, используемый докингом и рендерером.
|
||||
/// </summary>
|
||||
public FrameworkElement Control => GroupControl;
|
||||
|
||||
/// <summary>
|
||||
/// Визуальные элементы вкладок.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ILayoutVisual> Items { get; }
|
||||
|
||||
public WinUIGroupVisual(ILayoutGroup model, IReadOnlyList<ILayoutVisual> items)
|
||||
: base(model)
|
||||
{
|
||||
Items = items;
|
||||
GroupControl = new WinUIGroupControl();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Attach()
|
||||
{
|
||||
// Полная очистка
|
||||
GroupControl.TabView.TabItems.Clear();
|
||||
|
||||
foreach (var visual in Items)
|
||||
{
|
||||
visual.Attach();
|
||||
|
||||
if (visual is WinUIItemVisual itemVisual)
|
||||
{
|
||||
var tab = new TabViewItem
|
||||
{
|
||||
Header = itemVisual.Header,
|
||||
Content = itemVisual.Control // безопасно, т.к. Control — UserControl
|
||||
};
|
||||
|
||||
GroupControl.TabView.TabItems.Add(tab);
|
||||
}
|
||||
}
|
||||
|
||||
// Активная вкладка по модели
|
||||
if (Model is ILayoutGroup group && group.ActiveItem is not null)
|
||||
{
|
||||
var index = group.Items is IList<ILayoutItem> list
|
||||
? list.IndexOf(group.ActiveItem)
|
||||
: group.Items.ToList().IndexOf(group.ActiveItem);
|
||||
|
||||
if (index >= 0 && index < GroupControl.TabView.TabItems.Count)
|
||||
GroupControl.TabView.SelectedIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Detach()
|
||||
{
|
||||
foreach (var visual in Items)
|
||||
visual.Detach();
|
||||
|
||||
GroupControl.TabView.TabItems.Clear();
|
||||
}
|
||||
}
|
||||
51
Lattice.Layout.UI.WinUI/Visuals/WinUIItemVisual.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Lattice.Layout.Abstractions;
|
||||
using Lattice.Layout.UI.WinUI.Controls;
|
||||
using Lattice.Layout.UI.WinUI.Docking;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Lattice.Layout.UI.WinUI.Visuals;
|
||||
|
||||
/// <summary>
|
||||
/// Визуальное представление конечного элемента раскладки для WinUI.
|
||||
/// Оборачивает содержимое в <see cref="WinUIItemControl"/>.
|
||||
/// </summary>
|
||||
public sealed class WinUIItemVisual : LayoutVisual, IWinUIVisual
|
||||
{
|
||||
/// <summary>
|
||||
/// Реальный WinUI-контрол, отображающий элемент.
|
||||
/// </summary>
|
||||
public WinUIItemControl ItemControl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Контрол, используемый докингом и рендерером.
|
||||
/// </summary>
|
||||
public FrameworkElement Control => ItemControl;
|
||||
|
||||
/// <summary>
|
||||
/// Заголовок вкладки или элемента.
|
||||
/// </summary>
|
||||
public string Header => ((ILayoutItem)Model).Title;
|
||||
|
||||
public WinUIItemVisual(ILayoutItem model)
|
||||
: base(model)
|
||||
{
|
||||
ItemControl = new WinUIItemControl
|
||||
{
|
||||
ContentId = model.ContentId
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Attach()
|
||||
{
|
||||
// Здесь можно привязать реальное содержимое по ContentId через Shell/Service.
|
||||
// Например:
|
||||
// ItemControl.Content = resolver(((ILayoutItem)Model).ContentId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Detach()
|
||||
{
|
||||
ItemControl.Content = null;
|
||||
}
|
||||
}
|
||||
82
Lattice.Layout.UI.WinUI/Visuals/WinUISplitVisual.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using Lattice.Layout.Abstractions;
|
||||
using Lattice.Layout.UI.WinUI.Controls;
|
||||
using Lattice.Layout.UI.WinUI.Docking;
|
||||
using Microsoft.UI.Xaml;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Lattice.Layout.UI.WinUI.Visuals;
|
||||
|
||||
/// <summary>
|
||||
/// Визуальное представление сплит-элемента для WinUI.
|
||||
/// Управляет жизненным циклом соответствующего <see cref="WinUISplitControl"/>.
|
||||
/// </summary>
|
||||
public sealed class WinUISplitVisual : LayoutVisual, IWinUIVisual
|
||||
{
|
||||
/// <summary>
|
||||
/// Реальный WinUI-контрол, отображающий сплит.
|
||||
/// </summary>
|
||||
public WinUISplitControl SplitControl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Контрол, используемый рендерером и докингом.
|
||||
/// </summary>
|
||||
public FrameworkElement Control => SplitControl;
|
||||
|
||||
/// <summary>
|
||||
/// Дочерние визуальные элементы.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ILayoutVisual> Children { get; }
|
||||
|
||||
public WinUISplitVisual(ILayoutSplit model, IReadOnlyList<ILayoutVisual> children)
|
||||
: base(model)
|
||||
{
|
||||
Children = children;
|
||||
|
||||
SplitControl = new WinUISplitControl
|
||||
{
|
||||
LayoutOrientation = model.Orientation switch
|
||||
{
|
||||
Lattice.Layout.Abstractions.Orientation.Horizontal => Microsoft.UI.Xaml.Controls.Orientation.Horizontal,
|
||||
Lattice.Layout.Abstractions.Orientation.Vertical => Microsoft.UI.Xaml.Controls.Orientation.Vertical,
|
||||
_ => Microsoft.UI.Xaml.Controls.Orientation.Horizontal
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Attach()
|
||||
{
|
||||
SplitControl.Children.Clear();
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
child.Attach();
|
||||
|
||||
switch (child)
|
||||
{
|
||||
case WinUISplitVisual splitVisual:
|
||||
SplitControl.Children.Add(splitVisual.Control);
|
||||
break;
|
||||
|
||||
case WinUIGroupVisual groupVisual:
|
||||
SplitControl.Children.Add(groupVisual.Control);
|
||||
break;
|
||||
|
||||
case WinUIItemVisual itemVisual:
|
||||
SplitControl.Children.Add(itemVisual.Control);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
SplitControl.RebuildGrid();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Detach()
|
||||
{
|
||||
foreach (var child in Children)
|
||||
child.Detach();
|
||||
|
||||
SplitControl.Children.Clear();
|
||||
}
|
||||
}
|
||||
147
Lattice.Serialization.Docking.Json/JsonLayoutSerializer.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using Lattice.Core.Docking.Abstractions;
|
||||
using Lattice.Core.Docking.Engine;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Lattice.Serialization.Docking;
|
||||
|
||||
/// <summary>
|
||||
/// Реализация сериализатора макета док-системы в формате JSON с использованием System.Text.Json.
|
||||
/// Предоставляет высокопроизводительную сериализацию и десериализацию с поддержкой
|
||||
/// опций кастомизации через <see cref="JsonSerializerOptions"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Этот сериализатор использует преобразователь <see cref="LayoutConverter"/> для
|
||||
/// трансформации между объектной моделью и DTO, что обеспечивает независимость
|
||||
/// формата JSON от внутренней структуры данных.
|
||||
/// </remarks>
|
||||
public class JsonLayoutSerializer : ILayoutSerializer
|
||||
{
|
||||
private readonly JsonSerializerOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый экземпляр JSON-сериализатора с настройками по умолчанию.
|
||||
/// </summary>
|
||||
public JsonLayoutSerializer() : this(null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый экземпляр JSON-сериализатора с указанными опциями.
|
||||
/// </summary>
|
||||
/// <param name="options">
|
||||
/// Опции сериализации JSON. Если не указаны, используются стандартные опции.
|
||||
/// </param>
|
||||
public JsonLayoutSerializer(JsonSerializerOptions? options)
|
||||
{
|
||||
_options = options ?? CreateDefaultOptions();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string FormatId => "json";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string MimeType => "application/json";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DefaultFileExtension => ".json";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte[] Serialize(LayoutManager manager)
|
||||
{
|
||||
if (manager == null)
|
||||
throw new ArgumentNullException(nameof(manager));
|
||||
|
||||
var dto = LayoutConverter.ConvertToDto(manager);
|
||||
return JsonSerializer.SerializeToUtf8Bytes(dto, _options);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string SerializeToString(LayoutManager manager)
|
||||
{
|
||||
if (manager == null)
|
||||
throw new ArgumentNullException(nameof(manager));
|
||||
|
||||
var dto = LayoutConverter.ConvertToDto(manager);
|
||||
return JsonSerializer.Serialize(dto, _options);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SerializeToStream(LayoutManager manager, Stream stream)
|
||||
{
|
||||
if (manager == null)
|
||||
throw new ArgumentNullException(nameof(manager));
|
||||
if (stream == null)
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
|
||||
var dto = LayoutConverter.ConvertToDto(manager);
|
||||
JsonSerializer.Serialize(stream, dto, _options);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Deserialize(LayoutManager manager, byte[] data, Func<string, IDockContent?> contentResolver)
|
||||
{
|
||||
if (manager == null)
|
||||
throw new ArgumentNullException(nameof(manager));
|
||||
if (data == null)
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
|
||||
var dto = JsonSerializer.Deserialize<LayoutDto>(data, _options);
|
||||
if (dto == null)
|
||||
throw new InvalidOperationException("Failed to deserialize layout data");
|
||||
|
||||
LayoutConverter.RestoreFromDto(manager, dto, contentResolver);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void DeserializeFromString(LayoutManager manager, string serializedData,
|
||||
Func<string, IDockContent?> contentResolver)
|
||||
{
|
||||
if (manager == null)
|
||||
throw new ArgumentNullException(nameof(manager));
|
||||
if (serializedData == null)
|
||||
throw new ArgumentNullException(nameof(serializedData));
|
||||
|
||||
var dto = JsonSerializer.Deserialize<LayoutDto>(serializedData, _options);
|
||||
if (dto == null)
|
||||
throw new InvalidOperationException("Failed to deserialize layout string");
|
||||
|
||||
LayoutConverter.RestoreFromDto(manager, dto, contentResolver);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void DeserializeFromStream(LayoutManager manager, Stream stream,
|
||||
Func<string, IDockContent?> contentResolver)
|
||||
{
|
||||
if (manager == null)
|
||||
throw new ArgumentNullException(nameof(manager));
|
||||
if (stream == null)
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
|
||||
var dto = JsonSerializer.Deserialize<LayoutDto>(stream, _options);
|
||||
if (dto == null)
|
||||
throw new InvalidOperationException("Failed to deserialize layout stream");
|
||||
|
||||
LayoutConverter.RestoreFromDto(manager, dto, contentResolver);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создает стандартные опции сериализации JSON.
|
||||
/// </summary>
|
||||
/// <returns>Экземпляр <see cref="JsonSerializerOptions"/> с настройками по умолчанию.</returns>
|
||||
private static JsonSerializerOptions CreateDefaultOptions()
|
||||
{
|
||||
return new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
ReferenceHandler = ReferenceHandler.IgnoreCycles,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
AllowTrailingCommas = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
}
|
||||
}
|
||||
48
Lattice.Serialization.Docking.Json/JsonSerializerOptions.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Lattice.Serialization.Docking;
|
||||
|
||||
/// <summary>
|
||||
/// Предоставляет предварительно настроенные опции сериализации JSON для различных сценариев.
|
||||
/// </summary>
|
||||
public static class JsonSerializerOptionsPresets
|
||||
{
|
||||
/// <summary>
|
||||
/// Получает опции для красивого форматирования с отступами (для конфигурационных файлов).
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions PrettyPrint => new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() },
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Получает опции для компактного форматирования (для сетевой передачи).
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions Compact => new()
|
||||
{
|
||||
WriteIndented = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() },
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault,
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Получает опции для строгого форматирования (для валидации схемы).
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions Strict => new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() },
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.Never,
|
||||
IgnoreReadOnlyProperties = false,
|
||||
IgnoreReadOnlyFields = true,
|
||||
IncludeFields = false,
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.Default
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Lattice.Serialization.Docking\Lattice.Serialization.Docking.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
37
Lattice.Serialization.Docking/DTO/AutoHidePanelDto.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
namespace Lattice.Serialization.Docking;
|
||||
|
||||
/// <summary>
|
||||
/// DTO для автоскрываемой панели (AutoHidePanel).
|
||||
/// </summary>
|
||||
public class AutoHidePanelDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Уникальный идентификатор панели.
|
||||
/// </summary>
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Ссылка на контент панели.
|
||||
/// </summary>
|
||||
public ContentReferenceDto Content { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Сторона прикрепления в виде строки.
|
||||
/// </summary>
|
||||
public string Side { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Размер панели в пикселях.
|
||||
/// </summary>
|
||||
public double Size { get; set; } = 300;
|
||||
|
||||
/// <summary>
|
||||
/// Показывает, видима ли панель.
|
||||
/// </summary>
|
||||
public bool IsVisible { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Смещение для анимации (0.0 - 1.0).
|
||||
/// </summary>
|
||||
public double SlideOffset { get; set; } = 0.0;
|
||||
}
|
||||
32
Lattice.Serialization.Docking/DTO/ContentReferenceDto.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
namespace Lattice.Serialization.Docking;
|
||||
|
||||
/// <summary>
|
||||
/// DTO для ссылки на контент без сериализации самого контента.
|
||||
/// </summary>
|
||||
public class ContentReferenceDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Уникальный идентификатор контента.
|
||||
/// </summary>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Тип контента (для восстановления через ContentRegistry).
|
||||
/// </summary>
|
||||
public string TypeId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Отображаемое название контента.
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Показывает, можно ли закрыть контент.
|
||||
/// </summary>
|
||||
public bool CanClose { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Дополнительные свойства контента для восстановления состояния.
|
||||
/// </summary>
|
||||
public Dictionary<string, object?> Properties { get; set; } = new();
|
||||
}
|
||||
37
Lattice.Serialization.Docking/DTO/ElementDto.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
namespace Lattice.Serialization.Docking;
|
||||
|
||||
/// <summary>
|
||||
/// Базовый DTO для элементов дерева компоновки.
|
||||
/// </summary>
|
||||
public abstract class ElementDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Уникальный идентификатор элемента.
|
||||
/// </summary>
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Тип элемента (для десериализации).
|
||||
/// </summary>
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Ширина элемента.
|
||||
/// </summary>
|
||||
public double Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Высота элемента.
|
||||
/// </summary>
|
||||
public double Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Минимальная ширина элемента.
|
||||
/// </summary>
|
||||
public double MinWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Минимальная высота элемента.
|
||||
/// </summary>
|
||||
public double MinHeight { get; set; }
|
||||
}
|
||||
27
Lattice.Serialization.Docking/DTO/GroupDto.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace Lattice.Serialization.Docking;
|
||||
|
||||
/// <summary>
|
||||
/// DTO для группы разделения (DockGroup).
|
||||
/// </summary>
|
||||
public class GroupDto : ElementDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Первый дочерний элемент (левая или верхняя область).
|
||||
/// </summary>
|
||||
public ElementDto First { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Второй дочерний элемент (правая или нижняя область).
|
||||
/// </summary>
|
||||
public ElementDto Second { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Направление разделения в виде строки.
|
||||
/// </summary>
|
||||
public string Orientation { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Соотношение разделения между первым и вторым элементами (0.0 - 1.0).
|
||||
/// </summary>
|
||||
public double SplitRatio { get; set; } = 0.5;
|
||||
}
|
||||
47
Lattice.Serialization.Docking/DTO/LayoutDto.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
namespace Lattice.Serialization.Docking;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object (DTO) для сериализации состояния макета док-системы.
|
||||
/// Содержит все необходимые данные для сохранения и восстановления состояния макета.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Этот DTO является независимым от формата сериализации (JSON, XML, Binary) и используется
|
||||
/// как промежуточное представление между объектной моделью и сериализованными данными.
|
||||
/// </remarks>
|
||||
public class LayoutDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Версия формата DTO для контроля совместимости.
|
||||
/// </summary>
|
||||
public string Version { get; set; } = "1.0";
|
||||
|
||||
/// <summary>
|
||||
/// Дата и время создания DTO в UTC.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Идентификатор приложения, создавшего DTO.
|
||||
/// </summary>
|
||||
public string? ApplicationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Корневой элемент дерева компоновки.
|
||||
/// </summary>
|
||||
public ElementDto? Root { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Список плавающих окон.
|
||||
/// </summary>
|
||||
public List<WindowDto> FloatingWindows { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Список автоскрываемых панелей.
|
||||
/// </summary>
|
||||
public List<AutoHidePanelDto> AutoHidePanels { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Дополнительные метаданные, специфичные для приложения.
|
||||
/// </summary>
|
||||
public Dictionary<string, string> Metadata { get; set; } = new();
|
||||
}
|
||||
22
Lattice.Serialization.Docking/DTO/LeafDto.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace Lattice.Serialization.Docking;
|
||||
|
||||
/// <summary>
|
||||
/// DTO для контейнера вкладок (DockLeaf).
|
||||
/// </summary>
|
||||
public class LeafDto : ElementDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Список ссылок на контент, содержащийся в листе.
|
||||
/// </summary>
|
||||
public List<ContentReferenceDto> Contents { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Идентификатор активного контента (если есть).
|
||||
/// </summary>
|
||||
public string? ActiveContentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Расположение вкладок в виде строки.
|
||||
/// </summary>
|
||||
public string TabPlacement { get; set; } = "Bottom";
|
||||
}
|
||||
52
Lattice.Serialization.Docking/DTO/WindowDto.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
namespace Lattice.Serialization.Docking;
|
||||
|
||||
/// <summary>
|
||||
/// DTO для плавающего окна (DockWindow).
|
||||
/// </summary>
|
||||
public class WindowDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Уникальный идентификатор окна.
|
||||
/// </summary>
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Позиция X окна на экране.
|
||||
/// </summary>
|
||||
public double X { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Позиция Y окна на экране.
|
||||
/// </summary>
|
||||
public double Y { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ширина окна.
|
||||
/// </summary>
|
||||
public double Width { get; set; } = 800;
|
||||
|
||||
/// <summary>
|
||||
/// Высота окна.
|
||||
/// </summary>
|
||||
public double Height { get; set; } = 600;
|
||||
|
||||
/// <summary>
|
||||
/// Заголовок окна.
|
||||
/// </summary>
|
||||
public string Title { get; set; } = "Lattice Tool Window";
|
||||
|
||||
/// <summary>
|
||||
/// Корневой элемент макета внутри окна.
|
||||
/// </summary>
|
||||
public ElementDto? Root { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Показывает, видимо ли окно.
|
||||
/// </summary>
|
||||
public bool IsVisible { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Показывает, сфокусировано ли окно.
|
||||
/// </summary>
|
||||
public bool IsFocused { get; set; } = false;
|
||||
}
|
||||
103
Lattice.Serialization.Docking/ILayoutSerializer.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using Lattice.Core.Docking.Abstractions;
|
||||
using Lattice.Core.Docking.Engine;
|
||||
|
||||
namespace Lattice.Serialization.Docking;
|
||||
|
||||
/// <summary>
|
||||
/// Определяет контракт для сериализации и десериализации состояния макета док-системы.
|
||||
/// Этот интерфейс позволяет реализовать различные форматы сериализации (JSON, XML, Binary)
|
||||
/// и различные хранилища (файлы, базы данных, облако) без изменения основной логики.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Реализации этого интерфейса должны преобразовывать объектную модель док-системы
|
||||
/// в промежуточный DTO (<see cref="LayoutDto"/>), который затем сериализуется в целевой формат.
|
||||
/// Это обеспечивает независимость формата сериализации от структуры DTO.
|
||||
/// </remarks>
|
||||
public interface ILayoutSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Получает уникальный идентификатор формата сериализации.
|
||||
/// </summary>
|
||||
/// <value>Строковый идентификатор формата, например "json", "xml", "binary".</value>
|
||||
string FormatId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает MIME-тип формата сериализации.
|
||||
/// </summary>
|
||||
/// <value>MIME-тип, например "application/json", "application/xml".</value>
|
||||
string MimeType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Получает расширение файла по умолчанию для данного формата.
|
||||
/// </summary>
|
||||
/// <value>Расширение файла, например ".json", ".xml".</value>
|
||||
string DefaultFileExtension { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Сериализует состояние менеджера макета в массив байтов.
|
||||
/// </summary>
|
||||
/// <param name="manager">Менеджер макета для сериализации.</param>
|
||||
/// <returns>Массив байтов, содержащий сериализованное состояние макета.</returns>
|
||||
/// <exception cref="ArgumentNullException">Выбрасывается, если <paramref name="manager"/> равен null.</exception>
|
||||
byte[] Serialize(LayoutManager manager);
|
||||
|
||||
/// <summary>
|
||||
/// Сериализует состояние менеджера макета в строку.
|
||||
/// </summary>
|
||||
/// <param name="manager">Менеджер макета для сериализации.</param>
|
||||
/// <returns>Строковое представление состояния макета.</returns>
|
||||
/// <exception cref="ArgumentNullException">Выбрасывается, если <paramref name="manager"/> равен null.</exception>
|
||||
string SerializeToString(LayoutManager manager);
|
||||
|
||||
/// <summary>
|
||||
/// Сериализует состояние менеджера макета в поток.
|
||||
/// </summary>
|
||||
/// <param name="manager">Менеджер макета для сериализации.</param>
|
||||
/// <param name="stream">Поток для записи сериализованных данных.</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, если <paramref name="manager"/> или <paramref name="stream"/> равен null.
|
||||
/// </exception>
|
||||
void SerializeToStream(LayoutManager manager, Stream stream);
|
||||
|
||||
/// <summary>
|
||||
/// Десериализует состояние макета из массива байтов и восстанавливает его в менеджере.
|
||||
/// </summary>
|
||||
/// <param name="manager">Менеджер макета для восстановления состояния.</param>
|
||||
/// <param name="data">Массив байтов с сериализованным состоянием макета.</param>
|
||||
/// <param name="contentResolver">
|
||||
/// Функция разрешения контента по идентификатору, используемая для восстановления
|
||||
/// ссылок на контент в десериализованном состоянии.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, если <paramref name="manager"/> или <paramref name="data"/> равен null.
|
||||
/// </exception>
|
||||
void Deserialize(LayoutManager manager, byte[] data, Func<string, IDockContent?> contentResolver);
|
||||
|
||||
/// <summary>
|
||||
/// Десериализует состояние макета из строки и восстанавливает его в менеджере.
|
||||
/// </summary>
|
||||
/// <param name="manager">Менеджер макета для восстановления состояния.</param>
|
||||
/// <param name="serializedData">Строка с сериализованным состоянием макета.</param>
|
||||
/// <param name="contentResolver">
|
||||
/// Функция разрешения контента по идентификатору, используемая для восстановления
|
||||
/// ссылок на контент в десериализованном состоянии.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, если <paramref name="manager"/> или <paramref name="serializedData"/> равен null.
|
||||
/// </exception>
|
||||
void DeserializeFromString(LayoutManager manager, string serializedData, Func<string, IDockContent?> contentResolver);
|
||||
|
||||
/// <summary>
|
||||
/// Десериализует состояние макета из потока и восстанавливает его в менеджере.
|
||||
/// </summary>
|
||||
/// <param name="manager">Менеджер макета для восстановления состояния.</param>
|
||||
/// <param name="stream">Поток с сериализованными данными.</param>
|
||||
/// <param name="contentResolver">
|
||||
/// Функция разрешения контента по идентификатору, используемая для восстановления
|
||||
/// ссылок на контент в десериализованном состоянии.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, если <paramref name="manager"/> или <paramref name="stream"/> равен null.
|
||||
/// </exception>
|
||||
void DeserializeFromStream(LayoutManager manager, Stream stream, Func<string, IDockContent?> contentResolver);
|
||||
}
|
||||
19
Lattice.Serialization.Docking/ISerializableContent.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace Lattice.Serialization.Docking;
|
||||
|
||||
/// <summary>
|
||||
/// Интерфейс для контента, поддерживающего сериализацию дополнительного состояния.
|
||||
/// </summary>
|
||||
public interface ISerializableContent
|
||||
{
|
||||
/// <summary>
|
||||
/// Получает состояние для сериализации.
|
||||
/// </summary>
|
||||
/// <returns>Словарь свойств и их значений.</returns>
|
||||
Dictionary<string, object?> GetSerializableState();
|
||||
|
||||
/// <summary>
|
||||
/// Восстанавливает состояние из десериализованных данных.
|
||||
/// </summary>
|
||||
/// <param name="state">Словарь свойств и их значений.</param>
|
||||
void RestoreFromState(Dictionary<string, object?> state);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Lattice.Core.Docking\Lattice.Core.Docking.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||