DragAndDrop core
8
Lattice.Core.Docking/Abstractions/IDockCommand.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Lattice.Core.Docking.Abstractions;
|
||||||
|
|
||||||
|
public interface IDockCommand : System.Windows.Input.ICommand
|
||||||
|
{
|
||||||
|
string Name { get; }
|
||||||
|
string Icon { get; }
|
||||||
|
string GestureText { get; }
|
||||||
|
}
|
||||||
24
Lattice.Core.Docking/Abstractions/IDockContainer.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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>
|
||||||
|
void AddContent(IDockContent content);
|
||||||
|
|
||||||
|
/// <summary> Удаляет контент. Если Children становится пустым, контейнер может быть удален из дерева макета. </summary>
|
||||||
|
void RemoveContent(IDockContent content);
|
||||||
|
|
||||||
|
/// <summary> Положение вкладок в интерфейсе. </summary>
|
||||||
|
TabPlacement TabPlacement { get; set; }
|
||||||
|
}
|
||||||
25
Lattice.Core.Docking/Abstractions/IDockContent.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
namespace Lattice.Core.Docking.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Описывает объект содержимого (вкладку), который может быть размещен внутри IDockContainer.
|
||||||
|
/// </summary>
|
||||||
|
public interface IDockContent
|
||||||
|
{
|
||||||
|
/// <summary> Уникальный идентификатор контента (например, путь к файлу или ID инструмента). </summary>
|
||||||
|
string Id { get; }
|
||||||
|
|
||||||
|
/// <summary> Заголовок, отображаемый пользователю в интерфейсе (на вкладке). </summary>
|
||||||
|
string Title { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сам визуальный элемент (например, Microsoft.UI.Xaml.UIElement).
|
||||||
|
/// Lattice просто отображает этот объект в теле вкладки.
|
||||||
|
/// </summary>
|
||||||
|
object View { get; set; }
|
||||||
|
|
||||||
|
/// <summary> Флаг, определяющий доступность кнопки закрытия для пользователя. </summary>
|
||||||
|
bool CanClose { get; }
|
||||||
|
|
||||||
|
/// <summary> Вызывается системой при попытке закрытия контента. Возвращает true, если закрытие разрешено. </summary>
|
||||||
|
bool OnClosing();
|
||||||
|
}
|
||||||
25
Lattice.Core.Docking/Abstractions/IDockElement.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
namespace Lattice.Core.Docking.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Базовый интерфейс для любого элемента, который может быть частью дерева компоновки Lattice.
|
||||||
|
/// </summary>
|
||||||
|
public interface IDockElement
|
||||||
|
{
|
||||||
|
/// <summary> Уникальный идентификатор элемента. </summary>
|
||||||
|
string Id { get; }
|
||||||
|
|
||||||
|
/// <summary> Родительский элемент в иерархии. Если null — элемент является корневым. </summary>
|
||||||
|
IDockElement? Parent { get; set; }
|
||||||
|
|
||||||
|
/// <summary> Желаемая ширина элемента в относительных или абсолютных единицах. </summary>
|
||||||
|
double Width { get; set; }
|
||||||
|
|
||||||
|
/// <summary> Желаемая высота элемента в относительных или абсолютных единицах. </summary>
|
||||||
|
double Height { get; set; }
|
||||||
|
|
||||||
|
/// <summary> Минимально допустимая ширина, при которой элемент сохраняет функциональность. </summary>
|
||||||
|
double MinWidth { get; }
|
||||||
|
|
||||||
|
/// <summary> Минимально допустимая высота, при которой элемент сохраняет функциональность. </summary>
|
||||||
|
double MinHeight { get; }
|
||||||
|
}
|
||||||
19
Lattice.Core.Docking/Abstractions/IDockElementDragSource.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using Lattice.Core.DragDrop.Abstractions;
|
||||||
|
|
||||||
|
namespace Lattice.Core.Docking.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Расширяет интерфейс элемента док-системы для поддержки операций перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public interface IDockElementDragSource : IDockElement, IDragSource
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или устанавливает признак того, что элемент можно перетаскивать.
|
||||||
|
/// </summary>
|
||||||
|
bool CanDrag { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает тип данных для перетаскивания этого элемента.
|
||||||
|
/// </summary>
|
||||||
|
string DragDataType { get; }
|
||||||
|
}
|
||||||
19
Lattice.Core.Docking/Abstractions/IDockElementDropTarget.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using Lattice.Core.DragDrop.Abstractions;
|
||||||
|
|
||||||
|
namespace Lattice.Core.Docking.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Расширяет интерфейс элемента док-системы для возможности быть целью сброса.
|
||||||
|
/// </summary>
|
||||||
|
public interface IDockElementDropTarget : IDockElement, IDropTarget
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или устанавливает признак того, что элемент может принимать сброс.
|
||||||
|
/// </summary>
|
||||||
|
bool CanDrop { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает типы данных, которые может принимать элемент.
|
||||||
|
/// </summary>
|
||||||
|
IEnumerable<string> AcceptableDropTypes { get; }
|
||||||
|
}
|
||||||
76
Lattice.Core.Docking/Abstractions/IDragService.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
using Lattice.Core.Docking.Models;
|
||||||
|
using Lattice.Core.Geometry;
|
||||||
|
|
||||||
|
namespace Lattice.Core.Docking.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Предоставляет абстракцию для операции перетаскивания в док-системе.
|
||||||
|
/// Эта абстракция позволяет отделить логику перетаскивания от конкретной UI-платформы.
|
||||||
|
/// </summary>
|
||||||
|
public interface IDragService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Начинает операцию перетаскивания указанного элемента.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="element">Элемент для перетаскивания.</param>
|
||||||
|
/// <param name="visualFeedback">Визуальная обратная связь (зависит от платформы).</param>
|
||||||
|
void StartDrag(IDockElement element, object? visualFeedback = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Обновляет позицию перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="x">Координата X.</param>
|
||||||
|
/// <param name="y">Координата Y.</param>
|
||||||
|
void UpdateDrag(double x, double y);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Завершает операцию перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="x">Координата X завершения.</param>
|
||||||
|
/// <param name="y">Координата Y завершения.</param>
|
||||||
|
void EndDrag(double x, double y);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отменяет операцию перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
void CancelDrag();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Представляет область для сброса при операции перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public class DropArea
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Целевой элемент для сброса.
|
||||||
|
/// </summary>
|
||||||
|
public IDockElement Target { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Позиция сброса относительно цели.
|
||||||
|
/// </summary>
|
||||||
|
public DockPosition Position { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Границы области в экранных координатах.
|
||||||
|
/// </summary>
|
||||||
|
public Rect Bounds { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Видимость области (для анимации).
|
||||||
|
/// </summary>
|
||||||
|
public double Visibility { get; set; } = 0.0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр области сброса.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="target">Целевой элемент.</param>
|
||||||
|
/// <param name="position">Позиция сброса.</param>
|
||||||
|
/// <param name="bounds">Границы области.</param>
|
||||||
|
public DropArea(IDockElement target, DockPosition position, Rect bounds)
|
||||||
|
{
|
||||||
|
Target = target;
|
||||||
|
Position = position;
|
||||||
|
Bounds = bounds;
|
||||||
|
}
|
||||||
|
}
|
||||||
89
Lattice.Core.Docking/Engine/DockOperations.cs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
using Lattice.Core.Docking.Abstractions;
|
||||||
|
using Lattice.Core.Docking.Models;
|
||||||
|
|
||||||
|
namespace Lattice.Core.Docking.Engine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Статический движок для манипуляции иерархией дерева компоновки.
|
||||||
|
/// Содержит чистые алгоритмы трансформации графа.
|
||||||
|
/// </summary>
|
||||||
|
public static class DockOperations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Извлекает элемент из дерева. Если родительская группа остается с одним ребенком,
|
||||||
|
/// она удаляется, а ребенок занимает её место.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="element">Элемент для удаления.</param>
|
||||||
|
/// <param name="root">Текущий корень дерева.</param>
|
||||||
|
/// <returns>Новый корень дерева после оптимизации.</returns>
|
||||||
|
public static IDockElement? Remove(IDockElement element, IDockElement 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>
|
||||||
|
public static IDockElement Insert(IDockElement target, IDockElement source, DockPosition pos, IDockElement 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
newGroup.Parent = null;
|
||||||
|
return newGroup; // Новая группа стала корнем
|
||||||
|
}
|
||||||
|
}
|
||||||
369
Lattice.Core.Docking/Engine/LayoutManager.cs
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
using Lattice.Core.Docking.Abstractions;
|
||||||
|
using Lattice.Core.Docking.Models;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("Lattice.Serialization.Docking")]
|
||||||
|
|
||||||
|
namespace Lattice.Core.Docking.Engine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Расширенный менеджер макета, поддерживающий автоскрываемые панели, группы документов
|
||||||
|
/// и расширенные операции управления макетом.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот класс является центральным координатором всей док-системы, управляя деревом компоновки,
|
||||||
|
/// плавающими окнами, автоскрываемыми панелями и предоставляя API для манипуляции макетом.
|
||||||
|
/// </remarks>
|
||||||
|
public class LayoutManager
|
||||||
|
{
|
||||||
|
private readonly ObservableCollection<AutoHidePanel> _autoHidePanels = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Корневой элемент главного окна IDE.
|
||||||
|
/// </summary>
|
||||||
|
public IDockElement? Root { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Список активных плавающих окон.
|
||||||
|
/// </summary>
|
||||||
|
public List<DockWindow> FloatingWindows { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Коллекция автоскрываемых панелей.
|
||||||
|
/// </summary>
|
||||||
|
public ReadOnlyObservableCollection<AutoHidePanel> AutoHidePanels { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Реестр типов контента (опционально).
|
||||||
|
/// </summary>
|
||||||
|
public Services.ContentRegistry? ContentRegistry { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Уведомляет UI, что структура дерева изменилась.
|
||||||
|
/// </summary>
|
||||||
|
public event Action? LayoutUpdated;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Уведомляет об изменении в коллекции автоскрываемых панелей.
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler? AutoHidePanelsChanged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Событие, возникающее при операции перетаскивания элемента.
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<DragDropEventArgs>? DragDropOperation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр менеджера макета.
|
||||||
|
/// </summary>
|
||||||
|
public LayoutManager()
|
||||||
|
{
|
||||||
|
AutoHidePanels = new ReadOnlyObservableCollection<AutoHidePanel>(_autoHidePanels);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Добавляет автоскрываемую панель.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">Содержимое панели.</param>
|
||||||
|
/// <param name="side">Сторона для прикрепления.</param>
|
||||||
|
/// <returns>Созданная автоскрываемая панель.</returns>
|
||||||
|
public AutoHidePanel AddAutoHidePanel(IDockContent content, DockSide side)
|
||||||
|
{
|
||||||
|
var panel = new AutoHidePanel(content, side);
|
||||||
|
_autoHidePanels.Add(panel);
|
||||||
|
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Удаляет автоскрываемую панель.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="panel">Панель для удаления.</param>
|
||||||
|
public void RemoveAutoHidePanel(AutoHidePanel panel)
|
||||||
|
{
|
||||||
|
if (_autoHidePanels.Remove(panel))
|
||||||
|
{
|
||||||
|
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создает документ из зарегистрированного типа контента.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="contentTypeId">Идентификатор типа контента.</param>
|
||||||
|
/// <param name="id">Уникальный идентификатор документа.</param>
|
||||||
|
/// <returns>Созданный контент или null, если ContentRegistry не установлен.</returns>
|
||||||
|
public IDockContent? CreateDocument(string contentTypeId, string id)
|
||||||
|
{
|
||||||
|
if (ContentRegistry == null || !ContentRegistry.IsRegistered(contentTypeId))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return ContentRegistry.CreateContent(contentTypeId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Основной метод перемещения элементов в макете.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">Что перетаскиваем.</param>
|
||||||
|
/// <param name="target">Куда приземляем.</param>
|
||||||
|
/// <param name="position">Позиция относительно цели.</param>
|
||||||
|
/// <param name="asDocument">
|
||||||
|
/// Если true, контент будет добавлен как документ в центральную область.
|
||||||
|
/// </param>
|
||||||
|
public void Move(IDockElement source, IDockElement? target, DockPosition position, bool asDocument = false)
|
||||||
|
{
|
||||||
|
if (source == target) return;
|
||||||
|
|
||||||
|
// 1. Удаляем источник из текущего местоположения
|
||||||
|
bool sourceRemoved = false;
|
||||||
|
|
||||||
|
if (Root != null && IsDescendantOf(source, Root))
|
||||||
|
{
|
||||||
|
Root = DockOperations.Remove(source, Root);
|
||||||
|
sourceRemoved = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sourceRemoved = RemoveFromFloatingWindows(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sourceRemoved)
|
||||||
|
{
|
||||||
|
// Проверяем автоскрываемые панели
|
||||||
|
var autoHidePanel = _autoHidePanels.FirstOrDefault(p => p.Content == source);
|
||||||
|
if (autoHidePanel != null)
|
||||||
|
{
|
||||||
|
_autoHidePanels.Remove(autoHidePanel);
|
||||||
|
sourceRemoved = true;
|
||||||
|
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sourceRemoved) return;
|
||||||
|
|
||||||
|
// 2. Вставляем в цель
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
FloatingWindows.Add(new DockWindow { Root = source as IDockElement });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (IsDescendantOf(target, Root))
|
||||||
|
{
|
||||||
|
Root = DockOperations.Insert(target, source, position, Root!);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
InsertIntoFloatingWindow(target, source, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LayoutUpdated?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool RemoveFromFloatingWindows(IDockElement element)
|
||||||
|
{
|
||||||
|
foreach (var win in FloatingWindows.ToArray())
|
||||||
|
{
|
||||||
|
if (win.Root != null && IsDescendantOf(element, win.Root))
|
||||||
|
{
|
||||||
|
win.Root = DockOperations.Remove(element, win.Root);
|
||||||
|
if (win.Root == null)
|
||||||
|
FloatingWindows.Remove(win);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InsertIntoFloatingWindow(IDockElement target, IDockElement source, DockPosition position)
|
||||||
|
{
|
||||||
|
foreach (var win in FloatingWindows)
|
||||||
|
{
|
||||||
|
if (win.Root != null && IsDescendantOf(target, win.Root))
|
||||||
|
{
|
||||||
|
win.Root = DockOperations.Insert(target, source, position, win.Root);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsDescendantOf(IDockElement element, IDockElement ancestor)
|
||||||
|
{
|
||||||
|
if (element == ancestor) return true;
|
||||||
|
if (ancestor is DockGroup group)
|
||||||
|
return IsDescendantOf(element, group.First) || IsDescendantOf(element, group.Second);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Поиск элемента по ID во всех окнах. </summary>
|
||||||
|
public IDockElement? FindById(string id)
|
||||||
|
{
|
||||||
|
var found = FindRecursive(Root, id);
|
||||||
|
if (found != null) return found;
|
||||||
|
|
||||||
|
foreach (var win in FloatingWindows)
|
||||||
|
{
|
||||||
|
found = FindRecursive(win.Root, id);
|
||||||
|
if (found != null) return found;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IDockElement? FindRecursive(IDockElement? node, string id)
|
||||||
|
{
|
||||||
|
if (node == null || node.Id == id) return node;
|
||||||
|
if (node is DockGroup g) return FindRecursive(g.First, id) ?? FindRecursive(g.Second, id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сбрасывает макет к состоянию по умолчанию.
|
||||||
|
/// </summary>
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
Root = null;
|
||||||
|
FloatingWindows.Clear();
|
||||||
|
_autoHidePanels.Clear();
|
||||||
|
LayoutUpdated?.Invoke();
|
||||||
|
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Обрабатывает операцию перетаскивания между элементами.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">Источник перетаскивания.</param>
|
||||||
|
/// <param name="target">Цель сброса.</param>
|
||||||
|
/// <param name="position">Позиция сброса относительно цели.</param>
|
||||||
|
/// <param name="data">Данные перетаскивания.</param>
|
||||||
|
/// <returns>true, если операция успешно выполнена; иначе false.</returns>
|
||||||
|
public bool HandleDragDrop(IDockElement source, IDockElement target,
|
||||||
|
DockPosition position, object data)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (source == target)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Определяем тип операции на основе данных
|
||||||
|
if (data is ContentDragData contentData)
|
||||||
|
{
|
||||||
|
return HandleContentDragDrop(contentData, target, position);
|
||||||
|
}
|
||||||
|
else if (data is DockElementDragData elementData)
|
||||||
|
{
|
||||||
|
return HandleElementDragDrop(elementData, target, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
DragDropOperation?.Invoke(this, new DragDropEventArgs(
|
||||||
|
source, target, position, false, ex.Message));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HandleContentDragDrop(ContentDragData data, IDockElement target, DockPosition position)
|
||||||
|
{
|
||||||
|
// Находим исходный контейнер с контентом
|
||||||
|
var sourceContainer = FindElementById(data.ElementId) as IDockContainer;
|
||||||
|
if (sourceContainer == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Находим контент
|
||||||
|
var content = sourceContainer.Children.FirstOrDefault(c => c.Id == data.ContentId);
|
||||||
|
if (content == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (target is IDockContainer targetContainer && position == DockPosition.Center)
|
||||||
|
{
|
||||||
|
// Объединение вкладок
|
||||||
|
sourceContainer.RemoveContent(content);
|
||||||
|
targetContainer.AddContent(content);
|
||||||
|
|
||||||
|
DragDropOperation?.Invoke(this, new DragDropEventArgs(
|
||||||
|
sourceContainer as IDockElement ?? sourceContainer as IDockElement,
|
||||||
|
target, position, true, "Content merged"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HandleElementDragDrop(DockElementDragData data, IDockElement target, DockPosition position)
|
||||||
|
{
|
||||||
|
// Находим перетаскиваемый элемент
|
||||||
|
var sourceElement = FindElementById(data.ElementId);
|
||||||
|
if (sourceElement == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Выполняем перемещение
|
||||||
|
Move(sourceElement, target, position);
|
||||||
|
|
||||||
|
DragDropOperation?.Invoke(this, new DragDropEventArgs(
|
||||||
|
sourceElement, target, position, true, "Element moved"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Находит элемент по идентификатору.
|
||||||
|
/// </summary>
|
||||||
|
public IDockElement? FindElementById(string id)
|
||||||
|
{
|
||||||
|
return FindElementByIdRecursive(Root, id) ??
|
||||||
|
FloatingWindows.Select(w => FindElementByIdRecursive(w.Root, id))
|
||||||
|
.FirstOrDefault(result => result != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IDockElement? FindElementByIdRecursive(IDockElement? element, string id)
|
||||||
|
{
|
||||||
|
if (element == null) return null;
|
||||||
|
if (element.Id == id) return element;
|
||||||
|
|
||||||
|
if (element is DockGroup group)
|
||||||
|
{
|
||||||
|
return FindElementByIdRecursive(group.First, id) ??
|
||||||
|
FindElementByIdRecursive(group.Second, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Аргументы события операции перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public class DragDropEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary> Источник перетаскивания. </summary>
|
||||||
|
public IDockElement Source { get; }
|
||||||
|
|
||||||
|
/// <summary> Цель сброса. </summary>
|
||||||
|
public IDockElement Target { get; }
|
||||||
|
|
||||||
|
/// <summary> Позиция сброса. </summary>
|
||||||
|
public DockPosition Position { get; }
|
||||||
|
|
||||||
|
/// <summary> Показывает, была ли операция успешной. </summary>
|
||||||
|
public bool Success { get; }
|
||||||
|
|
||||||
|
/// <summary> Сообщение о результате операции. </summary>
|
||||||
|
public string Message { get; }
|
||||||
|
|
||||||
|
/// <summary> Время выполнения операции. </summary>
|
||||||
|
public DateTime Timestamp { get; }
|
||||||
|
|
||||||
|
public DragDropEventArgs(IDockElement source, IDockElement target,
|
||||||
|
DockPosition position, bool success, string message)
|
||||||
|
{
|
||||||
|
Source = source;
|
||||||
|
Target = target;
|
||||||
|
Position = position;
|
||||||
|
Success = success;
|
||||||
|
Message = message;
|
||||||
|
Timestamp = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Lattice.Core.Docking/Lattice.Core.Docking.csproj
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<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" />
|
||||||
|
<ProjectReference Include="..\Lattice.Core.DragDrop\Lattice.Core.DragDrop.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
114
Lattice.Core.Docking/Models/AutoHidePanel.cs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Lattice.Core.Docking.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Представляет автоскрываемую панель, которая может быть прикреплена к одной из сторон окна.
|
||||||
|
/// Автоскрываемые панели скрываются, оставляя только заголовок, и появляются при наведении курсора.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Автоскрываемые панели являются ключевым элементом интерфейса современных IDE,
|
||||||
|
/// позволяя экономить пространство экрана при сохранении быстрого доступа к инструментам.
|
||||||
|
/// </remarks>
|
||||||
|
public class AutoHidePanel : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
protected void OnPropertyChanged([CallerMemberName] string? name = null) =>
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||||
|
|
||||||
|
private bool _isVisible = false;
|
||||||
|
private double _slideOffset = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Уникальный идентификатор автоскрываемой панели.
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; } = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Содержимое панели.
|
||||||
|
/// </summary>
|
||||||
|
public Abstractions.IDockContent Content { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сторона окна, к которой прикреплена панель.
|
||||||
|
/// </summary>
|
||||||
|
public DockSide Side { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ширина панели (для левой/правой сторон) или высота (для верхней/нижней сторон).
|
||||||
|
/// </summary>
|
||||||
|
public double Size { get; set; } = 300;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Признак видимости панели.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsVisible
|
||||||
|
{
|
||||||
|
get => _isVisible;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_isVisible != value)
|
||||||
|
{
|
||||||
|
_isVisible = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Смещение для анимации выезда/заезда панели (0-1).
|
||||||
|
/// </summary>
|
||||||
|
public double SlideOffset
|
||||||
|
{
|
||||||
|
get => _slideOffset;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (Math.Abs(_slideOffset - value) > 0.001)
|
||||||
|
{
|
||||||
|
_slideOffset = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Заголовок панели (обычно берется из содержимого).
|
||||||
|
/// </summary>
|
||||||
|
public string Title => Content?.Title ?? "Auto-hide Panel";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр автоскрываемой панели.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">Содержимое панели.</param>
|
||||||
|
/// <param name="side">Сторона окна для прикрепления.</param>
|
||||||
|
public AutoHidePanel(Abstractions.IDockContent content, DockSide side)
|
||||||
|
{
|
||||||
|
Content = content ?? throw new ArgumentNullException(nameof(content));
|
||||||
|
Side = side;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Переключает видимость панели.
|
||||||
|
/// </summary>
|
||||||
|
public void Toggle()
|
||||||
|
{
|
||||||
|
IsVisible = !IsVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Показывает панель.
|
||||||
|
/// </summary>
|
||||||
|
public void Show()
|
||||||
|
{
|
||||||
|
IsVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Скрывает панель.
|
||||||
|
/// </summary>
|
||||||
|
public void Hide()
|
||||||
|
{
|
||||||
|
IsVisible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
444
Lattice.Core.Docking/Models/DockGroup.cs
Normal file
@@ -0,0 +1,444 @@
|
|||||||
|
using Lattice.Core.Docking.Abstractions;
|
||||||
|
using Lattice.Core.DragDrop.Abstractions;
|
||||||
|
using Lattice.Core.DragDrop.Enums;
|
||||||
|
using Lattice.Core.DragDrop.Models;
|
||||||
|
using Lattice.Core.Geometry;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Lattice.Core.Docking.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Представляет узел дерева компоновки, который разделяет доступную область
|
||||||
|
/// между двумя дочерними элементами. Этот класс является основным структурным
|
||||||
|
/// элементом для создания сложных макетов с разделителями.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="DockGroup"/> реализует как <see cref="IDragSource"/> (для
|
||||||
|
/// возможности перетаскивания всей группы), так и <see cref="IDropTarget"/>
|
||||||
|
/// (для возможности сброса на группу), что делает его полностью интегрированным
|
||||||
|
/// в систему перетаскивания док-системы.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Каждая группа содержит два дочерних элемента (<see cref="First"/> и
|
||||||
|
/// <see cref="Second"/>), которые могут быть либо другими группами (для
|
||||||
|
/// создания вложенной структуры), либо листами (<see cref="DockLeaf"/>)
|
||||||
|
/// с контентом. Направление разделения определяется свойством
|
||||||
|
/// <see cref="Orientation"/>.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public class DockGroup : IDockElement, IDragSource, IDropTarget, INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Событие, возникающее при изменении значения свойства.
|
||||||
|
/// </summary>
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
private double _splitRatio = 0.5;
|
||||||
|
private string _id;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает уникальный идентификатор группы.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Строковый идентификатор, уникальный в пределах дерева компоновки.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Идентификатор используется для сериализации/десериализации макета,
|
||||||
|
/// поиска элементов и отслеживания изменений в дереве.
|
||||||
|
/// </remarks>
|
||||||
|
public string Id
|
||||||
|
{
|
||||||
|
get => _id;
|
||||||
|
internal set
|
||||||
|
{
|
||||||
|
if (_id != value)
|
||||||
|
{
|
||||||
|
_id = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает родительский элемент в иерархии дерева компоновки.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Родительский элемент или null, если эта группа является корневой.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Это свойство управляется системой компоновки при добавлении или
|
||||||
|
/// удалении элементов из дерева.
|
||||||
|
/// </remarks>
|
||||||
|
public IDockElement? Parent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает первый дочерний элемент (левую или верхнюю область).
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Элемент, занимающий первую часть разделенной области.
|
||||||
|
/// </value>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается при попытке установить значение null.
|
||||||
|
/// </exception>
|
||||||
|
/// <remarks>
|
||||||
|
/// При установке нового значения автоматически обновляется свойство
|
||||||
|
/// <see cref="Parent"/> у дочернего элемента.
|
||||||
|
/// </remarks>
|
||||||
|
public IDockElement First { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает второй дочерний элемент (правую или нижнюю область).
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Элемент, занимающий вторую часть разделенной области.
|
||||||
|
/// </value>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается при попытке установить значение null.
|
||||||
|
/// </exception>
|
||||||
|
/// <remarks>
|
||||||
|
/// При установке нового значения автоматически обновляется свойство
|
||||||
|
/// <see cref="Parent"/> у дочернего элемента.
|
||||||
|
/// </remarks>
|
||||||
|
public IDockElement Second { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает направление разделения данной группы.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Значение перечисления <see cref="SplitDirection"/>, указывающее,
|
||||||
|
/// как разделена область: горизонтально или вертикально.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="SplitDirection.Horizontal"/> создает левую и правую области.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="SplitDirection.Vertical"/> создает верхнюю и нижнюю области.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public SplitDirection Orientation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает соотношение разделения между первым и вторым элементами.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Значение от 0.0 до 1.0, где:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>0.0 - вся область принадлежит второму элементу</item>
|
||||||
|
/// <item>0.5 - область разделена поровну</item>
|
||||||
|
/// <item>1.0 - вся область принадлежит первому элементу</item>
|
||||||
|
/// </list>
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Изменение этого свойства вызывает событие <see cref="PropertyChanged"/>
|
||||||
|
/// и может привести к перерисовке пользовательского интерфейса.
|
||||||
|
/// </remarks>
|
||||||
|
public double SplitRatio
|
||||||
|
{
|
||||||
|
get => _splitRatio;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (Math.Abs(_splitRatio - value) > double.Epsilon)
|
||||||
|
{
|
||||||
|
_splitRatio = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает желаемую ширину элемента.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Ширина в пикселях или относительных единицах.
|
||||||
|
/// </value>
|
||||||
|
public double Width { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает желаемую высоту элемента.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Высота в пикселях или относительных единицах.
|
||||||
|
/// </value>
|
||||||
|
public double Height { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает минимально допустимую ширину элемента.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Минимальная ширина в пикселях, при которой элемент сохраняет функциональность.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Для группы минимальная ширина вычисляется как сумма минимальных ширин
|
||||||
|
/// дочерних элементов при горизонтальной ориентации или максимум минимальных
|
||||||
|
/// ширин при вертикальной ориентации.
|
||||||
|
/// </remarks>
|
||||||
|
public double MinWidth => Orientation == SplitDirection.Horizontal
|
||||||
|
? First.MinWidth + Second.MinWidth
|
||||||
|
: Math.Max(First.MinWidth, Second.MinWidth);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает минимально допустимую высоту элемента.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Минимальная высота в пикселях, при которой элемент сохраняет функциональность.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Для группы минимальная высота вычисляется как сумма минимальных высот
|
||||||
|
/// дочерних элементов при вертикальной ориентации или максимум минимальных
|
||||||
|
/// высот при горизонтальной ориентации.
|
||||||
|
/// </remarks>
|
||||||
|
public double MinHeight => Orientation == SplitDirection.Vertical
|
||||||
|
? First.MinHeight + Second.MinHeight
|
||||||
|
: Math.Max(First.MinHeight, Second.MinHeight);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="DockGroup"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="first">
|
||||||
|
/// Первый дочерний элемент (левая или верхняя область).
|
||||||
|
/// </param>
|
||||||
|
/// <param name="second">
|
||||||
|
/// Второй дочерний элемент (правая или нижняя область).
|
||||||
|
/// </param>
|
||||||
|
/// <param name="orientation">
|
||||||
|
/// Направление разделения между дочерними элементами.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="id">
|
||||||
|
/// Уникальный идентификатор группы. Если не указан, генерируется новый GUID.
|
||||||
|
/// </param>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, когда <paramref name="first"/> или <paramref name="second"/>
|
||||||
|
/// равны null.
|
||||||
|
/// </exception>
|
||||||
|
/// <remarks>
|
||||||
|
/// Конструктор автоматически устанавливает свойство <see cref="Parent"/>
|
||||||
|
/// у дочерних элементов на текущую группу и генерирует уникальный идентификатор,
|
||||||
|
/// если он не был предоставлен.
|
||||||
|
/// </remarks>
|
||||||
|
public DockGroup(IDockElement first, IDockElement second, SplitDirection orientation, string? id = null)
|
||||||
|
{
|
||||||
|
First = first ?? throw new ArgumentNullException(nameof(first));
|
||||||
|
Second = second ?? throw new ArgumentNullException(nameof(second));
|
||||||
|
Orientation = orientation;
|
||||||
|
Id = id ?? Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
First.Parent = this;
|
||||||
|
Second.Parent = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывает событие <see cref="PropertyChanged"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">
|
||||||
|
/// Имя изменившегося свойства. Если не указано, определяется автоматически.
|
||||||
|
/// </param>
|
||||||
|
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Реализация IDragSource
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет, может ли группа начать операцию перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dragInfo">
|
||||||
|
/// При успешном возврате содержит информацию о перетаскивании;
|
||||||
|
/// в противном случае — null.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// true, если группа может начать перетаскивание; в противном случае — false.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Группа может быть перетащена только если она не является корневым
|
||||||
|
/// элементом дерева (имеет родителя).
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// При успешной проверке метод заполняет <paramref name="dragInfo"/>
|
||||||
|
/// данными типа <see cref="DockElementDragData"/>.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public bool CanStartDrag(out DragInfo? dragInfo)
|
||||||
|
{
|
||||||
|
dragInfo = null;
|
||||||
|
|
||||||
|
// DockGroup можно перетаскивать только если он не является корневым элементом
|
||||||
|
if (Parent == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Создаем данные для перетаскивания
|
||||||
|
var data = new DockElementDragData
|
||||||
|
{
|
||||||
|
ElementId = Id,
|
||||||
|
ElementType = GetType().Name,
|
||||||
|
IsGroup = true
|
||||||
|
};
|
||||||
|
|
||||||
|
dragInfo = new DragInfo(data, DragDropEffects.Move, Point.Zero, this);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Начинает операцию перетаскивания для группы.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dragInfo">
|
||||||
|
/// Информация о перетаскивании, полученная из <see cref="CanStartDrag"/>.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// Всегда возвращает true, так как группа не требует специальной подготовки
|
||||||
|
/// для начала перетаскивания.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Для <see cref="DockGroup"/> этот метод не выполняет дополнительных действий,
|
||||||
|
/// так как все необходимые данные уже содержатся в <paramref name="dragInfo"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public bool StartDrag(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
// DockGroup не требует дополнительной подготовки для перетаскивания
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается при завершении операции перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dragInfo">
|
||||||
|
/// Исходная информация о перетаскивании.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="effects">
|
||||||
|
/// Эффекты, которые были применены при сбросе.
|
||||||
|
/// </param>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Этот метод вызывается после того, как операция перетаскивания была
|
||||||
|
/// завершена (успешно или неуспешно).
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Для <see cref="DockGroup"/> этот метод не выполняет действий, так как
|
||||||
|
/// все изменения в структуре дерева уже обработаны <see cref="LayoutManager"/>.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public void DragCompleted(DragInfo dragInfo, DragDropEffects effects)
|
||||||
|
{
|
||||||
|
// Если группа была перемещена, ничего не делаем - LayoutManager уже обработал изменение
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается при отмене операции перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dragInfo">
|
||||||
|
/// Исходная информация о перетаскивании.
|
||||||
|
/// </param>
|
||||||
|
/// <remarks>
|
||||||
|
/// Для <see cref="DockGroup"/> отмена перетаскивания не требует специальных
|
||||||
|
/// действий, так как структура дерева не была изменена.
|
||||||
|
/// </remarks>
|
||||||
|
public void DragCancelled(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
// Отмена перетаскивания не требует действий
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Реализация IDropTarget
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет, может ли группа принять сбрасываемые данные.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dropInfo">
|
||||||
|
/// Информация о потенциальном сбросе.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// true, если группа может принять данные; в противном случае — false.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Группа может принимать только данные типа <see cref="DockElementDragData"/>
|
||||||
|
/// для элементов док-системы (<see cref="DockGroup"/> или <see cref="DockLeaf"/>).
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Группа не может принять сброс самой себя (проверяется по идентификатору).
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public bool CanAcceptDrop(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
if (dropInfo.Data is not DockElementDragData dragData)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Нельзя сбросить элемент на самого себя
|
||||||
|
if (dragData.ElementId == Id)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Можно принимать только элементы док-системы
|
||||||
|
return dragData.ElementType == nameof(DockGroup) || dragData.ElementType == nameof(DockLeaf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается, когда перетаскиваемый объект находится над группой.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dropInfo">
|
||||||
|
/// Информация о текущем положении перетаскивания.
|
||||||
|
/// </param>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Этот метод вызывается постоянно, пока пользователь перемещает объект
|
||||||
|
/// над целью. Для группы он устанавливает предлагаемые эффекты в
|
||||||
|
/// <see cref="DropInfo.SuggestedEffects"/>.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Если группа может принять сброс, предлагается эффект перемещения;
|
||||||
|
/// в противном случае эффекты не предлагаются.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public void DragOver(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
if (CanAcceptDrop(dropInfo))
|
||||||
|
{
|
||||||
|
dropInfo.SuggestedEffects = DragDropEffects.Move;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dropInfo.SuggestedEffects = DragDropEffects.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается, когда пользователь сбрасывает данные на группу.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dropInfo">
|
||||||
|
/// Информация о сбросе.
|
||||||
|
/// </param>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Для <see cref="DockGroup"/> обработка сброса делегируется
|
||||||
|
/// <see cref="LayoutManager"/>, поэтому метод просто помечает операцию
|
||||||
|
/// как обработанную.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Фактическое изменение структуры дерева выполняется менеджером макета
|
||||||
|
/// на основе данных из <paramref name="dropInfo"/>.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public void Drop(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
// Обработка сброса делегируется LayoutManager
|
||||||
|
dropInfo.MarkAsHandled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается, когда перетаскиваемый объект покидает область группы.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Для группы этот метод не выполняет действий, так как очистка визуальной
|
||||||
|
/// обратной связи выполняется в UI-слое.
|
||||||
|
/// </remarks>
|
||||||
|
public void DragLeave()
|
||||||
|
{
|
||||||
|
// Очистка визуальной обратной связи (будет выполнена в UI слое)
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
580
Lattice.Core.Docking/Models/DockLeaf.cs
Normal file
@@ -0,0 +1,580 @@
|
|||||||
|
using Lattice.Core.Docking.Abstractions;
|
||||||
|
using Lattice.Core.DragDrop.Abstractions;
|
||||||
|
using Lattice.Core.DragDrop.Enums;
|
||||||
|
using Lattice.Core.DragDrop.Models;
|
||||||
|
using Lattice.Core.Geometry;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Lattice.Core.Docking.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Представляет конечный узел (лист) дерева компоновки, который непосредственно
|
||||||
|
/// содержит коллекцию вкладок с контентом. Этот класс является контейнером для
|
||||||
|
/// отображаемого пользователю содержимого.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="DockLeaf"/> реализует интерфейсы <see cref="IDockContainer"/>,
|
||||||
|
/// <see cref="IDragSource"/> и <see cref="IDropTarget"/>, что позволяет ему:
|
||||||
|
/// </para>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>Управлять коллекцией вкладок</item>
|
||||||
|
/// <item>Быть источником перетаскивания (как всего листа, так и отдельных вкладок)</item>
|
||||||
|
/// <item>Принимать сброс других элементов или вкладок</item>
|
||||||
|
/// </list>
|
||||||
|
/// <para>
|
||||||
|
/// Лист является основным элементом, с которым взаимодействует пользователь
|
||||||
|
/// при работе с документами или инструментальными панелями в IDE-подобных
|
||||||
|
/// приложениях.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public class DockLeaf : IDockContainer, INotifyPropertyChanged, IDragSource, IDropTarget
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Событие, возникающее при изменении значения свойства.
|
||||||
|
/// </summary>
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
private readonly ObservableCollection<IDockContent> _items = new();
|
||||||
|
private IDockContent? _activeContent;
|
||||||
|
private string _id;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает уникальный идентификатор листа.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Строковый идентификатор, уникальный в пределах дерева компоновки.
|
||||||
|
/// </value>
|
||||||
|
public string Id
|
||||||
|
{
|
||||||
|
get => _id;
|
||||||
|
internal set
|
||||||
|
{
|
||||||
|
if (_id != value)
|
||||||
|
{
|
||||||
|
_id = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает родительский элемент в иерархии дерева компоновки.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Родительский элемент или null, если этот лист является корневым.
|
||||||
|
/// </value>
|
||||||
|
public IDockElement? Parent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает список вкладок, содержащихся в данном контейнере.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Коллекция объектов, реализующих <see cref="IDockContent"/>.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Эта коллекция является наблюдаемой (ObservableCollection), что позволяет
|
||||||
|
/// автоматически обновлять пользовательский интерфейс при добавлении или
|
||||||
|
/// удалении вкладок.
|
||||||
|
/// </remarks>
|
||||||
|
public IList<IDockContent> Children => _items;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает активную (выбранную) вкладку в контейнере.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Активная вкладка или null, если в контейнере нет вкладок.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// При установке нового значения проверяется, что вкладка действительно
|
||||||
|
/// содержится в коллекции <see cref="Children"/>.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Изменение этого свойства вызывает событие <see cref="PropertyChanged"/>.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public IDockContent? ActiveContent
|
||||||
|
{
|
||||||
|
get => _activeContent;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value != null && !_items.Contains(value)) return;
|
||||||
|
if (_activeContent != value)
|
||||||
|
{
|
||||||
|
_activeContent = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает желаемую ширину элемента.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Ширина в пикселях или относительных единицах.
|
||||||
|
/// </value>
|
||||||
|
public double Width { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает желаемую высоту элемента.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Высота в пикселях или относительных единицах.
|
||||||
|
/// </value>
|
||||||
|
public double Height { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает минимально допустимую ширину элемента.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Минимальная ширина в пикселях. Значение по умолчанию: 100.
|
||||||
|
/// </value>
|
||||||
|
public double MinWidth { get; set; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает минимально допустимую высоту элемента.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Минимальная высота в пикселях. Значение по умолчанию: 100.
|
||||||
|
/// </value>
|
||||||
|
public double MinHeight { get; set; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает положение полосы вкладок в контейнере.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Значение перечисления <see cref="TabPlacement"/>, определяющее,
|
||||||
|
/// где располагаются вкладки относительно содержимого.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Поддерживаются все четыре стороны: верх, низ, лево, право.
|
||||||
|
/// </remarks>
|
||||||
|
public TabPlacement TabPlacement { get; set; } = TabPlacement.Bottom;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="DockLeaf"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">
|
||||||
|
/// Уникальный идентификатор листа. Если не указан, генерируется новый GUID.
|
||||||
|
/// </param>
|
||||||
|
/// <remarks>
|
||||||
|
/// Создает пустой лист с коллекцией вкладок и генерирует уникальный
|
||||||
|
/// идентификатор, если он не был предоставлен.
|
||||||
|
/// </remarks>
|
||||||
|
public DockLeaf(string? id = null)
|
||||||
|
{
|
||||||
|
_id = id ?? Guid.NewGuid().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывает событие <see cref="PropertyChanged"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">
|
||||||
|
/// Имя изменившегося свойства. Если не указано, определяется автоматически.
|
||||||
|
/// </param>
|
||||||
|
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Добавляет контент в контейнер и делает его активным.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">
|
||||||
|
/// Контент для добавления.
|
||||||
|
/// </param>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Если контент уже содержится в коллекции, он не добавляется повторно,
|
||||||
|
/// но становится активным.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Этот метод обновляет свойство <see cref="ActiveContent"/> и вызывает
|
||||||
|
/// соответствующее событие изменения свойства.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public void AddContent(IDockContent content)
|
||||||
|
{
|
||||||
|
if (!_items.Contains(content))
|
||||||
|
{
|
||||||
|
_items.Add(content);
|
||||||
|
}
|
||||||
|
ActiveContent = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Удаляет контент из контейнера.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">
|
||||||
|
/// Контент для удаления.
|
||||||
|
/// </param>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Если удаляемый контент является активным, автоматически выбирается
|
||||||
|
/// новая активная вкладка (следующая в списке или предыдущая, если удалена
|
||||||
|
/// последняя).
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Если после удаления контейнер становится пустым, он может быть удален
|
||||||
|
/// из дерева макета системой компоновки.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public void RemoveContent(IDockContent 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Реализация IDragSource
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет, может ли лист начать операцию перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dragInfo">
|
||||||
|
/// При успешном возврате содержит информацию о перетаскивании;
|
||||||
|
/// в противном случае — null.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// true, если лист может начать перетаскивание; в противном случае — false.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Лист может быть перетащен, если:
|
||||||
|
/// </para>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>Он имеет родителя (не является корневым)</item>
|
||||||
|
/// <item>Или имеет хотя бы одну вкладку (не пустой)</item>
|
||||||
|
/// </list>
|
||||||
|
/// <para>
|
||||||
|
/// В зависимости от наличия активного контента создаются разные данные:
|
||||||
|
/// </para>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>
|
||||||
|
/// Если есть активный контент - создается <see cref="ContentDragData"/>
|
||||||
|
/// для перетаскивания конкретной вкладки
|
||||||
|
/// </item>
|
||||||
|
/// <item>
|
||||||
|
/// Если нет активного контента - создается <see cref="DockElementDragData"/>
|
||||||
|
/// для перетаскивания всего листа
|
||||||
|
/// </item>
|
||||||
|
/// </list>
|
||||||
|
/// </remarks>
|
||||||
|
public bool CanStartDrag(out DragInfo? dragInfo)
|
||||||
|
{
|
||||||
|
dragInfo = null;
|
||||||
|
|
||||||
|
// DockLeaf можно перетаскивать
|
||||||
|
if (Parent == null && Children.Count == 0)
|
||||||
|
return false; // Не перетаскиваем пустые корневые листья
|
||||||
|
|
||||||
|
object data;
|
||||||
|
|
||||||
|
// Если есть активный контент, перетаскиваем контент, иначе перетаскиваем весь лист
|
||||||
|
if (ActiveContent != null)
|
||||||
|
{
|
||||||
|
data = new ContentDragData
|
||||||
|
{
|
||||||
|
ElementId = Id,
|
||||||
|
ContentId = ActiveContent.Id,
|
||||||
|
ContentTitle = ActiveContent.Title,
|
||||||
|
ContentType = ActiveContent.GetType().Name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
data = new DockElementDragData
|
||||||
|
{
|
||||||
|
ElementId = Id,
|
||||||
|
ElementType = GetType().Name,
|
||||||
|
IsGroup = false,
|
||||||
|
Width = Width,
|
||||||
|
Height = Height
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
dragInfo = new DragInfo(data, DragDropEffects.Move | DragDropEffects.Copy, Point.Zero, this);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Начинает операцию перетаскивания для листа.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dragInfo">
|
||||||
|
/// Информация о перетаскивании.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// Всегда возвращает true.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Для <see cref="DockLeaf"/> этот метод не выполняет дополнительных действий.
|
||||||
|
/// </remarks>
|
||||||
|
public bool StartDrag(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
// DockLeaf не требует дополнительной подготовки
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается при завершении операции перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dragInfo">
|
||||||
|
/// Исходная информация о перетаскивании.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="effects">
|
||||||
|
/// Эффекты, которые были применены при сбросе.
|
||||||
|
/// </param>
|
||||||
|
/// <remarks>
|
||||||
|
/// Для <see cref="DockLeaf"/> этот метод не выполняет действий.
|
||||||
|
/// </remarks>
|
||||||
|
public void DragCompleted(DragInfo dragInfo, DragDropEffects effects)
|
||||||
|
{
|
||||||
|
// Если лист был перемещен или скопирован, LayoutManager уже обработал это
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается при отмене операции перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dragInfo">
|
||||||
|
/// Исходная информация о перетаскивании.
|
||||||
|
/// </param>
|
||||||
|
/// <remarks>
|
||||||
|
/// Для <see cref="DockLeaf"/> отмена перетаскивания не требует действий.
|
||||||
|
/// </remarks>
|
||||||
|
public void DragCancelled(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
// Отмена не требует действий
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Реализация IDropTarget
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет, может ли лист принять сбрасываемые данные.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dropInfo">
|
||||||
|
/// Информация о потенциальном сбросе.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// true, если лист может принять данные; в противном случае — false.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Лист может принимать:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>
|
||||||
|
/// <see cref="DockElementDragData"/> для других листов и групп
|
||||||
|
/// (для объединения или разделения)
|
||||||
|
/// </item>
|
||||||
|
/// <item>
|
||||||
|
/// <see cref="ContentDragData"/> для вкладок (для объединения вкладок)
|
||||||
|
/// </item>
|
||||||
|
/// </list>
|
||||||
|
/// </remarks>
|
||||||
|
public bool CanAcceptDrop(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
if (dropInfo.Data is DockElementDragData elementData)
|
||||||
|
{
|
||||||
|
// Можно принимать другие листы и группы
|
||||||
|
return elementData.ElementType == nameof(DockLeaf) ||
|
||||||
|
elementData.ElementType == nameof(DockGroup);
|
||||||
|
}
|
||||||
|
else if (dropInfo.Data is ContentDragData contentData)
|
||||||
|
{
|
||||||
|
// Можно принимать контент для объединения вкладок
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается, когда перетаскиваемый объект находится над листом.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dropInfo">
|
||||||
|
/// Информация о текущем положении перетаскивания.
|
||||||
|
/// </param>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// В зависимости от типа данных устанавливаются разные предлагаемые эффекты:
|
||||||
|
/// </para>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>
|
||||||
|
/// Для <see cref="ContentDragData"/> - эффект копирования (объединение вкладок)
|
||||||
|
/// </item>
|
||||||
|
/// <item>
|
||||||
|
/// Для <see cref="DockElementDragData"/> - эффект перемещения
|
||||||
|
/// </item>
|
||||||
|
/// </list>
|
||||||
|
/// </remarks>
|
||||||
|
public void DragOver(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
if (CanAcceptDrop(dropInfo))
|
||||||
|
{
|
||||||
|
if (dropInfo.Data is ContentDragData)
|
||||||
|
{
|
||||||
|
// Для контента предлагаем копирование (объединение вкладок)
|
||||||
|
dropInfo.SuggestedEffects = DragDropEffects.Copy;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Для элементов предлагаем перемещение
|
||||||
|
dropInfo.SuggestedEffects = DragDropEffects.Move;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dropInfo.SuggestedEffects = DragDropEffects.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается, когда пользователь сбрасывает данные на лист.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dropInfo">
|
||||||
|
/// Информация о сбросе.
|
||||||
|
/// </param>
|
||||||
|
/// <remarks>
|
||||||
|
/// Обработка сброса делегируется <see cref="LayoutManager"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public void Drop(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
// Обработка делегируется LayoutManager
|
||||||
|
dropInfo.MarkAsHandled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается, когда перетаскиваемый объект покидает область листа.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Очистка визуальной обратной связи выполняется в UI-слое.
|
||||||
|
/// </remarks>
|
||||||
|
public void DragLeave()
|
||||||
|
{
|
||||||
|
// Очистка визуальной обратной связи
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Представляет данные для перетаскивания элементов док-системы (групп или листов).
|
||||||
|
/// Используется при перетаскивании целых структурных элементов дерева компоновки.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот класс сериализуется и передается между компонентами системы перетаскивания
|
||||||
|
/// для идентификации перетаскиваемого элемента и его свойств.
|
||||||
|
/// </remarks>
|
||||||
|
public class DockElementDragData
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает уникальный идентификатор элемента.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Идентификатор элемента, соответствующий свойству <see cref="IDockElement.Id"/>.
|
||||||
|
/// </value>
|
||||||
|
public string ElementId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает тип элемента.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Имя типа элемента (обычно "DockGroup" или "DockLeaf").
|
||||||
|
/// </value>
|
||||||
|
public string ElementType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает значение, указывающее, является ли элемент группой.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// true, если элемент является <see cref="DockGroup"/>; false, если <see cref="DockLeaf"/>.
|
||||||
|
/// </value>
|
||||||
|
public bool IsGroup { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает идентификатор родительского элемента.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Идентификатор родительского элемента или null, если элемент корневой.
|
||||||
|
/// </value>
|
||||||
|
public string? ParentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает ширину элемента.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Текущая ширина элемента в пикселях.
|
||||||
|
/// </value>
|
||||||
|
public double Width { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает высоту элемента.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Текущая высота элемента в пикселях.
|
||||||
|
/// </value>
|
||||||
|
public double Height { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Представляет данные для перетаскивания контента (вкладок).
|
||||||
|
/// Используется при перетаскивании отдельных вкладок между контейнерами.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот класс позволяет идентифицировать конкретную вкладку для операций
|
||||||
|
/// объединения или перемещения между контейнерами.
|
||||||
|
/// </remarks>
|
||||||
|
public class ContentDragData
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает идентификатор контейнера (листа), содержащего контент.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Идентификатор <see cref="DockLeaf"/>, в котором находится перетаскиваемая вкладка.
|
||||||
|
/// </value>
|
||||||
|
public string ElementId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает уникальный идентификатор контента.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Идентификатор контента, соответствующий свойству <see cref="IDockContent.Id"/>.
|
||||||
|
/// </value>
|
||||||
|
public string ContentId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает заголовок контента.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Текст, отображаемый на вкладке.
|
||||||
|
/// </value>
|
||||||
|
public string ContentTitle { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает тип контента.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Имя типа контента (например, "TextEditor", "Toolbox", и т.д.).
|
||||||
|
/// </value>
|
||||||
|
public string ContentType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает значение, указывающее, можно ли закрыть контент.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// true, если контент можно закрыть; в противном случае — false.
|
||||||
|
/// </value>
|
||||||
|
public bool CanClose { get; set; } = true;
|
||||||
|
}
|
||||||
13
Lattice.Core.Docking/Models/DockPosition.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Lattice.Core.Docking.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет позицию вставки при операции Drag-and-Drop.
|
||||||
|
/// </summary>
|
||||||
|
public enum DockPosition
|
||||||
|
{
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Top,
|
||||||
|
Bottom,
|
||||||
|
Center,
|
||||||
|
}
|
||||||
19
Lattice.Core.Docking/Models/DockSide.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
namespace Lattice.Core.Docking.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет стороны окна, к которым могут быть прикреплены автоскрываемые панели.
|
||||||
|
/// </summary>
|
||||||
|
public enum DockSide
|
||||||
|
{
|
||||||
|
/// <summary> Левая сторона окна. </summary>
|
||||||
|
Left,
|
||||||
|
|
||||||
|
/// <summary> Правая сторона окна. </summary>
|
||||||
|
Right,
|
||||||
|
|
||||||
|
/// <summary> Верхняя сторона окна. </summary>
|
||||||
|
Top,
|
||||||
|
|
||||||
|
/// <summary> Нижняя сторона окна. </summary>
|
||||||
|
Bottom
|
||||||
|
}
|
||||||
23
Lattice.Core.Docking/Models/DockWindow.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Lattice.Core.Docking.Abstractions;
|
||||||
|
|
||||||
|
namespace Lattice.Core.Docking.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Описывает состояние плавающего окна в системе Lattice.
|
||||||
|
/// </summary>
|
||||||
|
public class DockWindow
|
||||||
|
{
|
||||||
|
/// <summary> Уникальный ID окна для сохранения его позиции в конфиге. </summary>
|
||||||
|
public string Id { get; } = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
/// <summary> Корневой элемент макета внутри данного окна. </summary>
|
||||||
|
public IDockElement? Root { get; set; }
|
||||||
|
|
||||||
|
public double X { get; set; }
|
||||||
|
public double Y { get; set; }
|
||||||
|
public double Width { get; set; } = 800;
|
||||||
|
public double Height { get; set; } = 600;
|
||||||
|
|
||||||
|
/// <summary> Заголовок окна (обычно берется из активного контента). </summary>
|
||||||
|
public string Title { get; set; } = "Lattice Tool Window";
|
||||||
|
}
|
||||||
12
Lattice.Core.Docking/Models/SplitDirection.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Lattice.Core.Docking.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Перечисление направлений разделения пространства внутри группы.
|
||||||
|
/// </summary>
|
||||||
|
public enum SplitDirection
|
||||||
|
{
|
||||||
|
/// <summary> Разделение по горизонтали (создает левую и правую области). </summary>
|
||||||
|
Horizontal,
|
||||||
|
/// <summary> Разделение по вертикали (создает верхнюю и нижнюю области). </summary>
|
||||||
|
Vertical
|
||||||
|
}
|
||||||
12
Lattice.Core.Docking/Models/TabPlacement.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Lattice.Core.Docking.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет положение полосы вкладок в контейнере.
|
||||||
|
/// </summary>
|
||||||
|
public enum TabPlacement
|
||||||
|
{
|
||||||
|
Top,
|
||||||
|
Bottom,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
31
Lattice.Core.Docking/Serialization/ILayoutSerializer.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
namespace Lattice.Core.Docking.Serialization;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Абстракция для сериализации и десериализации состояния макета док-системы.
|
||||||
|
/// Позволяет сохранять и восстанавливать расположение панелей, окон и их состояние.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Эта абстракция позволяет реализовать различные форматы сериализации (JSON, XML, бинарный)
|
||||||
|
/// и различные хранилища (файлы, базы данных, облако) без изменения основной логики док-системы.
|
||||||
|
/// </remarks>
|
||||||
|
public interface ILayoutSerializer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Сериализует состояние менеджера макета в строку.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="manager">Менеджер макета для сериализации.</param>
|
||||||
|
/// <returns>Строковое представление состояния макета.</returns>
|
||||||
|
string Serialize(Engine.LayoutManager manager);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Десериализует состояние макета из строки и восстанавливает его в менеджере.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="manager">Менеджер макета для восстановления состояния.</param>
|
||||||
|
/// <param name="serializedLayout">Сериализованное состояние макета.</param>
|
||||||
|
/// <param name="contentResolver">
|
||||||
|
/// Функция разрешения контента по идентификатору, используемая для восстановления
|
||||||
|
/// ссылок на контент в десериализованном состоянии.
|
||||||
|
/// </param>
|
||||||
|
void Deserialize(Engine.LayoutManager manager, string serializedLayout,
|
||||||
|
Func<string, Abstractions.IDockContent?> contentResolver);
|
||||||
|
}
|
||||||
19
Lattice.Core.Docking/Serialization/ISerializableLayout.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
namespace Lattice.Core.Docking.Serialization;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Контракт для объектов, которые могут предоставлять состояние для сериализации.
|
||||||
|
/// </summary>
|
||||||
|
public interface ISerializableLayout
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получает состояние для сериализации.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Объект состояния, готовый к сериализации.</returns>
|
||||||
|
object GetSerializableState();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Восстанавливает состояние из десериализованного объекта.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="state">Десериализованное состояние.</param>
|
||||||
|
void RestoreFromState(object state);
|
||||||
|
}
|
||||||
158
Lattice.Core.Docking/Services/ContentRegistry.cs
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
namespace Lattice.Core.Docking.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Реестр типов содержимого, который позволяет создавать экземпляры контента по типу.
|
||||||
|
/// Этот сервис является центральным для динамического создания панелей инструментов и документов в IDE.
|
||||||
|
/// </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">Выбрасывается, если contentTypeId или factory равны null.</exception>
|
||||||
|
/// <exception cref="ArgumentException">Выбрасывается, если 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 (_contentTypes.ContainsKey(contentTypeId))
|
||||||
|
throw new ArgumentException($"Content type '{contentTypeId}' is already registered.");
|
||||||
|
|
||||||
|
_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="KeyNotFoundException">Выбрасывается, если тип контента не зарегистрирован.</exception>
|
||||||
|
public Abstractions.IDockContent CreateContent(string contentTypeId, string id)
|
||||||
|
{
|
||||||
|
if (!_contentTypes.TryGetValue(contentTypeId, out var descriptor))
|
||||||
|
throw new KeyNotFoundException($"Content type '{contentTypeId}' is not registered.");
|
||||||
|
|
||||||
|
var content = descriptor.Factory();
|
||||||
|
// Устанавливаем ID через рефлексию, если есть свойство Id
|
||||||
|
var property = content.GetType().GetProperty("Id");
|
||||||
|
if (property != null && property.CanWrite)
|
||||||
|
{
|
||||||
|
property.SetValue(content, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает метаданные для указанного типа контента.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="contentTypeId">Идентификатор типа контента.</param>
|
||||||
|
/// <returns>Метаданные типа контента или null, если тип не найден.</returns>
|
||||||
|
public ContentMetadata? GetMetadata(string contentTypeId)
|
||||||
|
{
|
||||||
|
return _contentTypes.TryGetValue(contentTypeId, out var descriptor)
|
||||||
|
? descriptor.Metadata
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает все зарегистрированные типы контента.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Коллекция идентификаторов зарегистрированных типов контента.</returns>
|
||||||
|
public IEnumerable<string> GetRegisteredTypes() => _contentTypes.Keys;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Проверяет, зарегистрирован ли указанный тип контента.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsRegistered(string contentTypeId) => _contentTypes.ContainsKey(contentTypeId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Дескриптор типа контента, содержащий информацию о фабричном методе и метаданных.
|
||||||
|
/// </summary>
|
||||||
|
private class ContentDescriptor
|
||||||
|
{
|
||||||
|
public Type ContentType { get; }
|
||||||
|
public Func<Abstractions.IDockContent> Factory { get; }
|
||||||
|
public ContentMetadata Metadata { get; }
|
||||||
|
|
||||||
|
public ContentDescriptor(Type contentType, Func<Abstractions.IDockContent> factory, ContentMetadata metadata)
|
||||||
|
{
|
||||||
|
ContentType = contentType;
|
||||||
|
Factory = factory;
|
||||||
|
Metadata = metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Метаданные типа контента, предоставляющие дополнительную информацию для отображения в UI.
|
||||||
|
/// </summary>
|
||||||
|
public class ContentMetadata
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Идентификатор типа контента.
|
||||||
|
/// </summary>
|
||||||
|
public string ContentTypeId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отображаемое имя типа контента.
|
||||||
|
/// </summary>
|
||||||
|
public string DisplayName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Описание типа контента.
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Имя ресурса для иконки (опционально).
|
||||||
|
/// </summary>
|
||||||
|
public string? IconResource { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Признак того, что контент является документом (а не инструментальной панелью).
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDocument { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Минимальная ширина контента в пикселях.
|
||||||
|
/// </summary>
|
||||||
|
public double DefaultWidth { get; set; } = 300;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Минимальная высота контента в пикселях.
|
||||||
|
/// </summary>
|
||||||
|
public double DefaultHeight { get; set; } = 200;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр метаданных контента.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="contentTypeId">Идентификатор типа контента.</param>
|
||||||
|
/// <param name="displayName">Отображаемое имя.</param>
|
||||||
|
public ContentMetadata(string contentTypeId, string displayName)
|
||||||
|
{
|
||||||
|
ContentTypeId = contentTypeId;
|
||||||
|
DisplayName = displayName;
|
||||||
|
Description = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
115
Lattice.Core.DragDrop.Tests/DragDropServiceTests.cs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
using Lattice.Core.DragDrop.Abstractions;
|
||||||
|
using Lattice.Core.DragDrop.Enums;
|
||||||
|
using Lattice.Core.DragDrop.Models;
|
||||||
|
using Lattice.Core.DragDrop.Services;
|
||||||
|
using Lattice.Core.Geometry;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Lattice.Core.DragDrop.Tests;
|
||||||
|
|
||||||
|
public class DragDropServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void StartDrag_WithValidSource_StartsDragOperation()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var service = new DragDropService();
|
||||||
|
var mockSource = new Mock<IDragSource>();
|
||||||
|
var dragInfo = new DragInfo("test", DragDropEffects.Copy, new Point(0, 0));
|
||||||
|
|
||||||
|
mockSource.Setup(s => s.CanStartDrag(out dragInfo)).Returns(true);
|
||||||
|
mockSource.Setup(s => s.StartDrag(It.IsAny<DragInfo>())).Returns(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = service.StartDrag(mockSource.Object, new Point(0, 0));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.True(service.IsDragActive);
|
||||||
|
Assert.NotNull(service.CurrentDragInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RegisterDropTarget_ReturnsValidId()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var service = new DragDropService();
|
||||||
|
var mockTarget = new Mock<IDropTarget>();
|
||||||
|
var bounds = new Rect(0, 0, 100, 100);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var id = service.RegisterDropTarget(mockTarget.Object, bounds);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(id);
|
||||||
|
Assert.NotEmpty(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateDrag_WithValidDropTarget_CallsDragOver()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var service = new DragDropService();
|
||||||
|
var mockSource = new Mock<IDragSource>();
|
||||||
|
var mockTarget = new Mock<IDropTarget>();
|
||||||
|
|
||||||
|
var dragInfo = new DragInfo("test", DragDropEffects.Copy, new Point(0, 0));
|
||||||
|
mockSource.Setup(s => s.CanStartDrag(out dragInfo)).Returns(true);
|
||||||
|
mockSource.Setup(s => s.StartDrag(It.IsAny<DragInfo>())).Returns(true);
|
||||||
|
|
||||||
|
var targetId = service.RegisterDropTarget(mockTarget.Object, new Rect(0, 0, 100, 100));
|
||||||
|
service.StartDrag(mockSource.Object, new Point(0, 0));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
service.UpdateDrag(new Point(50, 50));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
mockTarget.Verify(t => t.DragOver(It.IsAny<DropInfo>()), Times.AtLeastOnce());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EndDrag_WithValidDrop_CallsDrop()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var service = new DragDropService();
|
||||||
|
var mockSource = new Mock<IDragSource>();
|
||||||
|
var mockTarget = new Mock<IDropTarget>();
|
||||||
|
|
||||||
|
var dragInfo = new DragInfo("test", DragDropEffects.Copy, new Point(0, 0));
|
||||||
|
mockSource.Setup(s => s.CanStartDrag(out dragInfo)).Returns(true);
|
||||||
|
mockSource.Setup(s => s.StartDrag(It.IsAny<DragInfo>())).Returns(true);
|
||||||
|
|
||||||
|
service.RegisterDropTarget(mockTarget.Object, new Rect(0, 0, 100, 100));
|
||||||
|
service.StartDrag(mockSource.Object, new Point(0, 0));
|
||||||
|
service.UpdateDrag(new Point(50, 50));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var effects = service.EndDrag(new Point(50, 50));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
mockTarget.Verify(t => t.Drop(It.IsAny<DropInfo>()), Times.Once());
|
||||||
|
Assert.False(service.IsDragActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CancelDrag_WithActiveDrag_CallsDragCancelled()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var service = new DragDropService();
|
||||||
|
var mockSource = new Mock<IDragSource>();
|
||||||
|
var dragInfo = new DragInfo("test", DragDropEffects.Copy, new Point(0, 0));
|
||||||
|
|
||||||
|
mockSource.Setup(s => s.CanStartDrag(out dragInfo)).Returns(true);
|
||||||
|
mockSource.Setup(s => s.StartDrag(It.IsAny<DragInfo>())).Returns(true);
|
||||||
|
|
||||||
|
service.StartDrag(mockSource.Object, new Point(0, 0));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
service.CancelDrag();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
mockSource.Verify(s => s.DragCancelled(It.IsAny<DragInfo>()), Times.Once());
|
||||||
|
Assert.False(service.IsDragActive);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||||
|
<PackageReference Include="Moq" Version="4.20.70" />
|
||||||
|
<PackageReference Include="xunit" Version="2.6.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Lattice.Core.DragDrop\Lattice.Core.DragDrop.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
28
Lattice.Core.DragDrop/Abstractions/IAsyncDragSource.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
namespace Lattice.Core.DragDrop.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет контракт для объектов, которые могут быть источником данных
|
||||||
|
/// в операции перетаскивания с поддержкой асинхронных операций.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAsyncDragSource : IDragSource
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет, может ли объект начать операцию перетаскивания (асинхронно).
|
||||||
|
/// </summary>
|
||||||
|
Task<(bool CanStart, Models.DragInfo? DragInfo)> CanStartDragAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Начинает операцию перетаскивания (асинхронно).
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> StartDragAsync(Models.DragInfo dragInfo);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается при завершении операции перетаскивания (асинхронно).
|
||||||
|
/// </summary>
|
||||||
|
Task DragCompletedAsync(Models.DragInfo dragInfo, Enums.DragDropEffects effects);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается при отмене операции перетаскивания (асинхронно).
|
||||||
|
/// </summary>
|
||||||
|
Task DragCancelledAsync(Models.DragInfo dragInfo);
|
||||||
|
}
|
||||||
28
Lattice.Core.DragDrop/Abstractions/IAsyncDropTarget.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
namespace Lattice.Core.DragDrop.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет контракт для объектов, которые могут принимать сбрасываемые данные
|
||||||
|
/// в операции перетаскивания с поддержкой асинхронных операций.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAsyncDropTarget : IDropTarget
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет, может ли объект принять сбрасываемые данные (асинхронно).
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> CanAcceptDropAsync(Models.DropInfo dropInfo);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается, когда перетаскиваемый объект находится над целью (асинхронно).
|
||||||
|
/// </summary>
|
||||||
|
Task DragOverAsync(Models.DropInfo dropInfo);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается, когда пользователь сбрасывает данные на цель (асинхронно).
|
||||||
|
/// </summary>
|
||||||
|
Task DropAsync(Models.DropInfo dropInfo);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается, когда перетаскиваемый объект покидает область цели (асинхронно).
|
||||||
|
/// </summary>
|
||||||
|
Task DragLeaveAsync();
|
||||||
|
}
|
||||||
64
Lattice.Core.DragDrop/Abstractions/IDragSource.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
namespace Lattice.Core.DragDrop.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет контракт для объектов, которые могут быть источником данных
|
||||||
|
/// в операции перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Объекты, реализующие этот интерфейс, могут инициировать операции перетаскивания
|
||||||
|
/// и предоставлять данные для передачи другим элементам через механизм drag-and-drop.
|
||||||
|
/// </remarks>
|
||||||
|
public interface IDragSource
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет, может ли объект начать операцию перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dragInfo">
|
||||||
|
/// Информация о перетаскивании, которая будет заполнена данными, если операция разрешена.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// true, если объект может начать перетаскивание; в противном случае — false.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот метод вызывается системой перетаскивания для проверки возможности
|
||||||
|
/// начала операции. Если метод возвращает true, он должен заполнить
|
||||||
|
/// <paramref name="dragInfo"/> необходимыми данными.
|
||||||
|
/// </remarks>
|
||||||
|
bool CanStartDrag(out Models.DragInfo? dragInfo);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Начинает операцию перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dragInfo">Информация о перетаскивании.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// true, если операция перетаскивания успешно начата; в противном случае — false.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот метод вызывается, когда пользователь начинает перетаскивание элемента.
|
||||||
|
/// Реализация должна подготовить данные для перетаскивания и, возможно,
|
||||||
|
/// создать визуальное представление перетаскиваемого объекта.
|
||||||
|
/// </remarks>
|
||||||
|
bool StartDrag(Models.DragInfo dragInfo);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается при завершении операции перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dragInfo">Исходная информация о перетаскивании.</param>
|
||||||
|
/// <param name="effects">Эффекты, которые были применены при сбросе.</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот метод вызывается после завершения операции перетаскивания
|
||||||
|
/// (успешного или неуспешного). Реализация может выполнить очистку
|
||||||
|
/// или обновить состояние на основе результата операции.
|
||||||
|
/// </remarks>
|
||||||
|
void DragCompleted(Models.DragInfo dragInfo, Enums.DragDropEffects effects);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается при отмене операции перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dragInfo">Исходная информация о перетаскивании.</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот метод вызывается, когда операция перетаскивания была отменена
|
||||||
|
/// пользователем (например, нажатием клавиши Escape).
|
||||||
|
/// </remarks>
|
||||||
|
void DragCancelled(Models.DragInfo dragInfo);
|
||||||
|
}
|
||||||
55
Lattice.Core.DragDrop/Abstractions/IDropTarget.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
namespace Lattice.Core.DragDrop.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет контракт для объектов, которые могут принимать сбрасываемые данные
|
||||||
|
/// в операции перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Объекты, реализующие этот интерфейс, могут обрабатывать данные, сброшенные
|
||||||
|
/// пользователем, и предоставлять визуальную обратную связь во время перетаскивания.
|
||||||
|
/// </remarks>
|
||||||
|
public interface IDropTarget
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет, может ли объект принять сбрасываемые данные.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dropInfo">Информация о потенциальном сбросе.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// true, если объект может принять данные; в противном случае — false.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот метод вызывается, когда перетаскиваемый объект находится над целью.
|
||||||
|
/// Реализация должна проверить, совместимы ли данные с целью, и установить
|
||||||
|
/// предлагаемые эффекты в <paramref name="dropInfo"/>.
|
||||||
|
/// </remarks>
|
||||||
|
bool CanAcceptDrop(Models.DropInfo dropInfo);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается, когда перетаскиваемый объект находится над целью.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dropInfo">Информация о текущем положении перетаскивания.</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот метод вызывается постоянно, пока пользователь перемещает объект над целью.
|
||||||
|
/// Реализация может обновить визуальную обратную связь или изменить предлагаемые эффекты.
|
||||||
|
/// </remarks>
|
||||||
|
void DragOver(Models.DropInfo dropInfo);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается, когда пользователь сбрасывает данные на цель.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dropInfo">Информация о сбросе.</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот метод вызывается, когда пользователь отпускает кнопку мыши над целью.
|
||||||
|
/// Реализация должна обработать принятие данных и выполнить соответствующее действие.
|
||||||
|
/// </remarks>
|
||||||
|
void Drop(Models.DropInfo dropInfo);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается, когда перетаскиваемый объект покидает область цели.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот метод вызывается, когда пользователь перемещает объект за пределы цели.
|
||||||
|
/// Реализация должна очистить любую визуальную обратную связь, установленную ранее.
|
||||||
|
/// </remarks>
|
||||||
|
void DragLeave();
|
||||||
|
}
|
||||||
102
Lattice.Core.DragDrop/Enums/DragDropEffects.cs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
namespace Lattice.Core.DragDrop.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет эффекты, которые могут быть применены при операции перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот перечисление используется для указания допустимых операций перетаскивания
|
||||||
|
/// и передачи информации о результате операции между источником и целью.
|
||||||
|
/// </remarks>
|
||||||
|
[Flags]
|
||||||
|
public enum DragDropEffects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Операция перетаскивания не разрешена.
|
||||||
|
/// </summary>
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Данные копируются из источника в цель.
|
||||||
|
/// </summary>
|
||||||
|
Copy = 1 << 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Данные перемещаются из источника в цель.
|
||||||
|
/// </summary>
|
||||||
|
Move = 1 << 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создается ссылка на исходные данные.
|
||||||
|
/// </summary>
|
||||||
|
Link = 1 << 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Целевой элемент может прокручиваться во время перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
Scroll = 1 << 3,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Комбинированный эффект копирования и перемещения.
|
||||||
|
/// </summary>
|
||||||
|
CopyOrMove = Copy | Move,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Все эффекты разрешены.
|
||||||
|
/// </summary>
|
||||||
|
All = Copy | Move | Link | Scroll
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Расширения для работы с DragDropEffects.
|
||||||
|
/// </summary>
|
||||||
|
public static class DragDropEffectsExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Проверяет, содержит ли эффекты указанный эффект.
|
||||||
|
/// </summary>
|
||||||
|
public static bool HasEffect(this DragDropEffects effects, DragDropEffects effect)
|
||||||
|
{
|
||||||
|
return (effects & effect) == effect;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Проверяет, содержат ли эффекты копирование.
|
||||||
|
/// </summary>
|
||||||
|
public static bool CanCopy(this DragDropEffects effects)
|
||||||
|
{
|
||||||
|
return effects.HasEffect(DragDropEffects.Copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Проверяет, содержат ли эффекты перемещение.
|
||||||
|
/// </summary>
|
||||||
|
public static bool CanMove(this DragDropEffects effects)
|
||||||
|
{
|
||||||
|
return effects.HasEffect(DragDropEffects.Move);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Проверяет, содержат ли эффекты ссылку.
|
||||||
|
/// </summary>
|
||||||
|
public static bool CanLink(this DragDropEffects effects)
|
||||||
|
{
|
||||||
|
return effects.HasEffect(DragDropEffects.Link);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает наиболее подходящий эффект на основе модификаторов клавиатуры.
|
||||||
|
/// </summary>
|
||||||
|
public static DragDropEffects GetEffectFromKeys(bool controlKey, bool shiftKey, bool altKey)
|
||||||
|
{
|
||||||
|
if (controlKey && altKey)
|
||||||
|
return DragDropEffects.Link;
|
||||||
|
if (controlKey)
|
||||||
|
return DragDropEffects.Copy;
|
||||||
|
if (shiftKey)
|
||||||
|
return DragDropEffects.Move;
|
||||||
|
if (altKey)
|
||||||
|
return DragDropEffects.Link;
|
||||||
|
|
||||||
|
return DragDropEffects.Move; // По умолчанию
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Lattice.Core.DragDrop/Enums/DropPosition.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Lattice.Core.DragDrop.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Позиция сброса относительно цели.
|
||||||
|
/// </summary>
|
||||||
|
public enum DropPosition
|
||||||
|
{
|
||||||
|
Inside,
|
||||||
|
Top,
|
||||||
|
Bottom,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Center
|
||||||
|
}
|
||||||
85
Lattice.Core.DragDrop/Exceptions/DragDropException.cs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
namespace Lattice.Core.DragDrop.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Исключение, возникающее при ошибках в системе перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public class DragDropException : Exception
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Код ошибки.
|
||||||
|
/// </summary>
|
||||||
|
public string ErrorCode { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="DragDropException"/>.
|
||||||
|
/// </summary>
|
||||||
|
public DragDropException()
|
||||||
|
: base("Drag & Drop operation failed.")
|
||||||
|
{
|
||||||
|
ErrorCode = "DRAGDROP_0001";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="DragDropException"/> с указанным сообщением.
|
||||||
|
/// </summary>
|
||||||
|
public DragDropException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
ErrorCode = "DRAGDROP_0002";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="DragDropException"/> с кодом ошибки.
|
||||||
|
/// </summary>
|
||||||
|
public DragDropException(string errorCode, string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
ErrorCode = errorCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="DragDropException"/>
|
||||||
|
/// с указанным сообщением и внутренним исключением.
|
||||||
|
/// </summary>
|
||||||
|
public DragDropException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
ErrorCode = "DRAGDROP_0003";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="DragDropException"/>
|
||||||
|
/// с кодом ошибки, сообщением и внутренним исключением.
|
||||||
|
/// </summary>
|
||||||
|
public DragDropException(string errorCode, string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
ErrorCode = errorCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Коды ошибок Drag & Drop системы.
|
||||||
|
/// </summary>
|
||||||
|
public static class DragDropErrorCodes
|
||||||
|
{
|
||||||
|
// Общие ошибки
|
||||||
|
public const string OperationAlreadyActive = "DRAGDROP_1001";
|
||||||
|
public const string OperationNotActive = "DRAGDROP_1002";
|
||||||
|
public const string InvalidData = "DRAGDROP_1003";
|
||||||
|
public const string Timeout = "DRAGDROP_1004";
|
||||||
|
|
||||||
|
// Ошибки источников
|
||||||
|
public const string SourceCannotDrag = "DRAGDROP_2001";
|
||||||
|
public const string SourceStartFailed = "DRAGDROP_2002";
|
||||||
|
|
||||||
|
// Ошибки целей
|
||||||
|
public const string TargetNotFound = "DRAGDROP_3001";
|
||||||
|
public const string TargetCannotAccept = "DRAGDROP_3002";
|
||||||
|
public const string TargetDropFailed = "DRAGDROP_3003";
|
||||||
|
|
||||||
|
// Ошибки системы
|
||||||
|
public const string SystemNotInitialized = "DRAGDROP_4001";
|
||||||
|
public const string SystemDisposed = "DRAGDROP_4002";
|
||||||
|
public const string MemoryAllocationFailed = "DRAGDROP_4003";
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
namespace Lattice.Core.DragDrop.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Методы расширения для регистрации сервисов перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Добавляет сервис перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serviceCollection">Коллекция сервисов.</param>
|
||||||
|
/// <returns>Коллекция сервисов.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Реализация DI должна быть предоставлена конкретным приложением.
|
||||||
|
/// </remarks>
|
||||||
|
public static object AddDragDropService(this object serviceCollection)
|
||||||
|
{
|
||||||
|
// Реализация регистрации сервиса должна быть в конкретном приложении
|
||||||
|
// Это абстрактный метод для поддержки DI без зависимостей
|
||||||
|
return serviceCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Добавляет сервис перетаскивания с конфигурацией.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serviceCollection">Коллекция сервисов.</param>
|
||||||
|
/// <param name="configure">Действие конфигурации.</param>
|
||||||
|
/// <returns>Коллекция сервисов.</returns>
|
||||||
|
public static object AddDragDropService(
|
||||||
|
this object serviceCollection,
|
||||||
|
Action<DragDropServiceOptions> configure)
|
||||||
|
{
|
||||||
|
var options = new DragDropServiceOptions();
|
||||||
|
configure(options);
|
||||||
|
|
||||||
|
// Реализация регистрации с опциями должна быть в конкретном приложении
|
||||||
|
return serviceCollection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Опции конфигурации сервиса перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public class DragDropServiceOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Порог начала перетаскивания в пикселях.
|
||||||
|
/// </summary>
|
||||||
|
public double DragStartThreshold { get; set; } = 3.0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Включить ведение журнала операций.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableLogging { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Включить автоматическую очистку неиспользуемых целей.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableAutoCleanup { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Интервал автоматической очистки в миллисекундах.
|
||||||
|
/// </summary>
|
||||||
|
public int AutoCleanupInterval { get; set; } = 60000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Включить асинхронную обработку операций.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableAsyncOperations { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Время ожидания асинхронных операций в миллисекундах.
|
||||||
|
/// </summary>
|
||||||
|
public int AsyncOperationTimeout { get; set; } = 5000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Включить сбор статистики.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableStatistics { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Включить проверку типов данных.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableTypeChecking { get; set; } = true;
|
||||||
|
}
|
||||||
20
Lattice.Core.DragDrop/Lattice.Core.DragDrop.csproj
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
|
<PackageId>Lattice.Core.DragDrop</PackageId>
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
<Authors>FrigaT</Authors>
|
||||||
|
<Description>Professional drag-and-drop system for Lattice UI Framework</Description>
|
||||||
|
<PackageTags>ui;framework;drag;drop;docking;toolbox</PackageTags>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Lattice.Core.Geometry\Lattice.Core.Geometry.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
227
Lattice.Core.DragDrop/Models/DragInfo.cs
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
using Lattice.Core.Geometry;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace Lattice.Core.DragDrop.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Содержит информацию о начале операции перетаскивания.
|
||||||
|
/// Этот класс передается от источника перетаскивания к системе перетаскивания
|
||||||
|
/// для инициализации и управления операцией.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="DragInfo"/> является ключевым компонентом системы перетаскивания,
|
||||||
|
/// инкапсулирующим все необходимые данные для начала операции. Он содержит:
|
||||||
|
/// </para>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>Данные для передачи</item>
|
||||||
|
/// <item>Разрешенные эффекты перетаскивания</item>
|
||||||
|
/// <item>Начальную позицию операции</item>
|
||||||
|
/// <item>Ссылку на источник перетаскивания</item>
|
||||||
|
/// <item>Дополнительные параметры операции</item>
|
||||||
|
/// </list>
|
||||||
|
/// <para>
|
||||||
|
/// Этот класс используется как внутренний механизм передачи данных между
|
||||||
|
/// <see cref="Abstractions.IDragSource"/> и системой управления перетаскиванием.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public class DragInfo : IDisposable, ICloneable
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<string, object> _parameters = new();
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает данные, которые передаются в операции перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Объект, содержащий данные для передачи. Может быть любого типа,
|
||||||
|
/// поддерживаемого системой перетаскивания.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Эти данные будут доступны цели сброса через <see cref="DropInfo.Data"/>.
|
||||||
|
/// Важно, чтобы данные были сериализуемыми, если операция перетаскивания
|
||||||
|
/// может выходить за пределы процесса приложения.
|
||||||
|
/// </remarks>
|
||||||
|
public object Data { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает разрешенные эффекты для этой операции перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Комбинация флагов <see cref="Enums.DragDropEffects"/>, определяющая,
|
||||||
|
/// какие операции разрешены для этого перетаскивания.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот параметр используется системой для фильтрации допустимых операций
|
||||||
|
/// и предоставления соответствующей визуальной обратной связи пользователю.
|
||||||
|
/// </remarks>
|
||||||
|
public Enums.DragDropEffects AllowedEffects { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает начальную позицию операции перетаскивания в координатах экрана.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Точка в экранных координатах, где была начата операция перетаскивания.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Эта позиция используется для вычисления смещения при создании визуального
|
||||||
|
/// представления перетаскивания и для определения порога начала операции.
|
||||||
|
/// </remarks>
|
||||||
|
public Point StartPosition { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает источник перетаскивания, который инициировал операцию.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Объект, реализующий <see cref="Abstractions.IDragSource"/>, или null,
|
||||||
|
/// если источник не доступен или не требуется.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Эта ссылка может использоваться для уведомления источника о результате
|
||||||
|
/// операции перетаскивания (завершении или отмене).
|
||||||
|
/// </remarks>
|
||||||
|
public object? Source { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает дополнительные параметры, специфичные для конкретной
|
||||||
|
/// реализации перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Словарь, содержащий пары ключ-значение с дополнительными параметрами.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Используется для передачи контекстной информации, которая не входит
|
||||||
|
/// в стандартный набор свойств, но может быть полезной для обработки
|
||||||
|
/// операции перетаскивания.
|
||||||
|
/// </remarks>
|
||||||
|
public IReadOnlyDictionary<string, object> Parameters => _parameters;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="DragInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">
|
||||||
|
/// Данные, которые передаются в операции перетаскивания.
|
||||||
|
/// Не может быть null.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="allowedEffects">
|
||||||
|
/// Разрешенные эффекты для этой операции перетаскивания.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="startPosition">
|
||||||
|
/// Начальная позиция операции перетаскивания в координатах экрана.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="source">
|
||||||
|
/// Источник перетаскивания, который инициировал операцию. Может быть null.
|
||||||
|
/// </param>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, когда <paramref name="data"/> равен null.
|
||||||
|
/// </exception>
|
||||||
|
/// <remarks>
|
||||||
|
/// Конструктор создает экземпляр <see cref="DragInfo"/> с указанными
|
||||||
|
/// параметрами и инициализирует коллекцию параметров пустым словарем.
|
||||||
|
/// </remarks>
|
||||||
|
public DragInfo(object data, Enums.DragDropEffects allowedEffects, Point startPosition, object? source = null)
|
||||||
|
{
|
||||||
|
Data = data ?? throw new ArgumentNullException(nameof(data));
|
||||||
|
AllowedEffects = allowedEffects;
|
||||||
|
StartPosition = startPosition;
|
||||||
|
Source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создает новый экземпляр <see cref="DragInfo"/> с теми же данными,
|
||||||
|
/// но новой позицией.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="newPosition">
|
||||||
|
/// Новая позиция для информации о перетаскивании.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// Новый экземпляр <see cref="DragInfo"/> с обновленной позицией.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот метод используется для обновления информации о перетаскивании
|
||||||
|
/// при перемещении курсора, сохраняя исходные данные и параметры.
|
||||||
|
/// </remarks>
|
||||||
|
public DragInfo CloneWithPosition(Point newPosition)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
var clone = new DragInfo(Data, AllowedEffects, newPosition, Source);
|
||||||
|
|
||||||
|
foreach (var kvp in _parameters)
|
||||||
|
{
|
||||||
|
clone._parameters[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создает новый экземпляр <see cref="DragInfo"/> с теми же данными.
|
||||||
|
/// </summary>
|
||||||
|
public DragInfo Clone() => new DragInfo(Data, AllowedEffects, StartPosition, Source);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
object ICloneable.Clone() => this.Clone();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или дополнительные параметры, специфичные для конкретной
|
||||||
|
/// реализации перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public T? GetParameter<T>(string key, T? defaultValue = default)
|
||||||
|
{
|
||||||
|
if (Parameters.TryGetValue(key, out var value) && value is T typedValue)
|
||||||
|
{
|
||||||
|
return typedValue;
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или дополнительные параметры, специфичные для конкретной
|
||||||
|
/// реализации перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryGetParameter<T>(string key, out T? value)
|
||||||
|
{
|
||||||
|
value = default;
|
||||||
|
|
||||||
|
if (_parameters.TryGetValue(key, out var objValue) && objValue is T typedValue)
|
||||||
|
{
|
||||||
|
value = typedValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Задает дополнительные параметры, специфичные для конкретной
|
||||||
|
/// реализации перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public void SetParameter<T>(string key, T value)
|
||||||
|
{
|
||||||
|
_parameters[key] = value!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Освобождает ресурсы.
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
|
_parameters.Clear();
|
||||||
|
_disposed = true;
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThrowIfDisposed()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
throw new ObjectDisposedException(nameof(DragInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
~DragInfo()
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
269
Lattice.Core.DragDrop/Models/DropInfo.cs
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
using Lattice.Core.DragDrop.Enums;
|
||||||
|
using Lattice.Core.Geometry;
|
||||||
|
|
||||||
|
namespace Lattice.Core.DragDrop.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Содержит информацию о потенциальном или фактическом сбросе в операции перетаскивания.
|
||||||
|
/// Этот класс используется для передачи данных между системой перетаскивания
|
||||||
|
/// и целью сброса (<see cref="Abstractions.IDropTarget"/>).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="DropInfo"/> предоставляет цель сброса всей необходимой информацией
|
||||||
|
/// для принятия решения о возможности сброса и выполнения соответствующей операции.
|
||||||
|
/// Ключевые аспекты включают:
|
||||||
|
/// </para>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>Предлагаемые для сброса данные</item>
|
||||||
|
/// <item>Текущую позицию курсора</item>
|
||||||
|
/// <item>Разрешенные эффекты от источника</item>
|
||||||
|
/// <item>Предлагаемые эффекты для сброса</item>
|
||||||
|
/// <item>Ссылку на цель сброса</item>
|
||||||
|
/// <item>Флаг обработки операции</item>
|
||||||
|
/// </list>
|
||||||
|
/// <para>
|
||||||
|
/// Этот класс является изменяемым, позволяя цели сброса обновлять предлагаемые
|
||||||
|
/// эффекты и помечать операцию как обработанную.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public class DropInfo
|
||||||
|
{
|
||||||
|
private DragDropEffects _effects = DragDropEffects.None;
|
||||||
|
public DropPosition DropPosition { get; set; } = DropPosition.Inside;
|
||||||
|
public bool ShowVisualFeedback { get; set; } = true;
|
||||||
|
public object? VisualFeedbackData { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает данные, которые предлагаются для сброса.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Данные, переданные от источника перетаскивания, или null, если данные
|
||||||
|
/// не доступны или операция была отменена.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Эти данные соответствуют свойству <see cref="DragInfo.Data"/> из
|
||||||
|
/// исходной информации о перетаскивании.
|
||||||
|
/// </remarks>
|
||||||
|
public object? Data { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает текущую позицию курсора в координатах экрана.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Точка в экранных координатах, представляющая текущее положение курсора
|
||||||
|
/// мыши во время операции перетаскивания.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Эта позиция используется для определения точного места сброса и может
|
||||||
|
/// влиять на предлагаемые эффекты (например, различные операции для
|
||||||
|
/// разных областей цели сброса).
|
||||||
|
/// </remarks>
|
||||||
|
public Point Position { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает разрешенные эффекты от источника перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Комбинация флагов <see cref="Enums.DragDropEffects"/>, определяющая,
|
||||||
|
/// какие операции разрешил источник.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Цель сброса должна уважать эти ограничения и не предлагать эффекты,
|
||||||
|
/// которые не разрешены источником.
|
||||||
|
/// </remarks>
|
||||||
|
public Enums.DragDropEffects AllowedEffects { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает предлагаемые эффекты для операции сброса.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Комбинация флагов <see cref="Enums.DragDropEffects"/>, предлагаемая
|
||||||
|
/// целью сброса. По умолчанию равно <see cref="Enums.DragDropEffects.None"/>.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Цель сброса должна установить это свойство в методе <see cref="Abstractions.IDropTarget.DragOver"/>
|
||||||
|
/// на основе анализа предоставленных данных и текущего контекста.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Если цель не устанавливает это свойство, система перетаскивания
|
||||||
|
/// будет использовать эффекты по умолчанию.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public Enums.DragDropEffects SuggestedEffects
|
||||||
|
{
|
||||||
|
get => _effects;
|
||||||
|
set => _effects = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает цель сброса, которая обрабатывает эту информацию.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Объект, реализующий <see cref="Abstractions.IDropTarget"/>, или null,
|
||||||
|
/// если цель не определена.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Эта ссылка позволяет системе идентифицировать, какая цель обрабатывает
|
||||||
|
/// информацию о сбросе, и используется для отслеживания изменений цели
|
||||||
|
/// во время операции перетаскивания.
|
||||||
|
/// </remarks>
|
||||||
|
public object? Target { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает дополнительные параметры, специфичные для конкретной
|
||||||
|
/// реализации перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Словарь, содержащий пары ключ-значение с дополнительными параметрами.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Может использоваться для передачи контекстной информации между
|
||||||
|
/// различными компонентами системы перетаскивания или для хранения
|
||||||
|
/// временных данных во время обработки операции.
|
||||||
|
/// </remarks>
|
||||||
|
public Dictionary<string, object> Parameters { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает значение, указывающее, был ли сброс уже обработан.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// true, если операция сброса была помечена как обработанная;
|
||||||
|
/// в противном случае — false.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Это свойство используется для предотвращения множественной обработки
|
||||||
|
/// одной и той же операции сброса. После вызова метода <see cref="MarkAsHandled"/>,
|
||||||
|
/// свойство становится true.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Система перетаскивания может проверять это свойство, чтобы определить,
|
||||||
|
/// нужно ли выполнять дополнительную обработку по умолчанию.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public bool Handled { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает дополнительные параметры, специфичные для конкретной
|
||||||
|
/// реализации перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public T? GetParameter<T>(string key, T? defaultValue = default)
|
||||||
|
{
|
||||||
|
if (Parameters.TryGetValue(key, out var value) && value is T typedValue)
|
||||||
|
{
|
||||||
|
return typedValue;
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает дополнительные параметры, специфичные для конкретной
|
||||||
|
/// реализации перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryGetParameter<T>(string key, out T? value)
|
||||||
|
{
|
||||||
|
value = default;
|
||||||
|
|
||||||
|
if (Parameters.TryGetValue(key, out var objValue) && objValue is T typedValue)
|
||||||
|
{
|
||||||
|
value = typedValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Задает дополнительные параметры, специфичные для конкретной
|
||||||
|
/// реализации перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public void SetParameter<T>(string key, T value)
|
||||||
|
{
|
||||||
|
Parameters[key] = value!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="DropInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">
|
||||||
|
/// Данные, которые предлагаются для сброса. Может быть null.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="position">
|
||||||
|
/// Текущая позиция курсора в координатах экрана.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="allowedEffects">
|
||||||
|
/// Разрешенные эффекты от источника перетаскивания.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="target">
|
||||||
|
/// Цель сброса, которая обрабатывает эту информацию. Может быть null.
|
||||||
|
/// </param>
|
||||||
|
/// <remarks>
|
||||||
|
/// Конструктор создает экземпляр <see cref="DropInfo"/> с указанными
|
||||||
|
/// параметрами, инициализирует коллекцию параметров пустым словарем
|
||||||
|
/// и устанавливает флаг <see cref="Handled"/> в false.
|
||||||
|
/// </remarks>
|
||||||
|
public DropInfo(object? data, Point position, Enums.DragDropEffects allowedEffects, object? target = null)
|
||||||
|
{
|
||||||
|
Data = data;
|
||||||
|
Position = position;
|
||||||
|
AllowedEffects = allowedEffects;
|
||||||
|
Target = target;
|
||||||
|
Parameters = new Dictionary<string, object>();
|
||||||
|
Handled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Помечает сброс как обработанный.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Этот метод должен вызываться целью сброса в методе <see cref="Abstractions.IDropTarget.Drop"/>,
|
||||||
|
/// если она успешно обработала операцию сброса.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// После вызова этого метода свойство <see cref="Handled"/> становится true,
|
||||||
|
/// что сигнализирует системе перетаскивания о том, что дополнительная
|
||||||
|
/// обработка не требуется.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public void MarkAsHandled()
|
||||||
|
{
|
||||||
|
Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создает новый экземпляр <see cref="DropInfo"/> с теми же данными,
|
||||||
|
/// но новой позицией.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="newPosition">
|
||||||
|
/// Новая позиция для информации о сбросе.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// Новый экземпляр <see cref="DropInfo"/> с обновленной позицией.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот метод используется для обновления информации о сбросе при
|
||||||
|
/// перемещении курсора, сохраняя исходные данные и параметры.
|
||||||
|
/// </remarks>
|
||||||
|
public DropInfo WithPosition(Point newPosition)
|
||||||
|
{
|
||||||
|
return new DropInfo(Data, newPosition, AllowedEffects, Target)
|
||||||
|
{
|
||||||
|
Parameters = new Dictionary<string, object>(Parameters),
|
||||||
|
SuggestedEffects = _effects,
|
||||||
|
DropPosition = DropPosition,
|
||||||
|
ShowVisualFeedback = ShowVisualFeedback,
|
||||||
|
VisualFeedbackData = VisualFeedbackData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Проверка установки эффекта перетаскивания в разрешенные эффекты.
|
||||||
|
/// </summary>
|
||||||
|
public bool CanAcceptEffect(Enums.DragDropEffects effect)
|
||||||
|
{
|
||||||
|
return (AllowedEffects & effect) != Enums.DragDropEffects.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
832
Lattice.Core.DragDrop/README.md
Normal file
@@ -0,0 +1,832 @@
|
|||||||
|
# Lattice.Core.DragDrop
|
||||||
|
|
||||||
|
Профессиональная, асинхронная система перетаскивания для .NET приложений. Полностью потокобезопасная, расширяемая архитектура с поддержкой кросс-платформенности.
|
||||||
|
|
||||||
|
## 📋 Особенности
|
||||||
|
|
||||||
|
- ✅ **Полная асинхронная поддержка** - async/await для всех операций
|
||||||
|
- ✅ **Потокобезопасность** - `ReaderWriterLockSlim` для эффективной синхронизации
|
||||||
|
- ✅ **Производительность** - Оптимизированные алгоритмы, кэширование, минимальные аллокации
|
||||||
|
- ✅ **Расширяемость** - Легкая интеграция с любыми UI фреймворками
|
||||||
|
- ✅ **Надежность** - Таймауты, обработка ошибок, корректное освобождение ресурсов
|
||||||
|
- ✅ **Статистика** - Встроенный мониторинг производительности
|
||||||
|
|
||||||
|
## 🏗️ Архитектура
|
||||||
|
|
||||||
|
### Основные компоненты
|
||||||
|
|
||||||
|
```
|
||||||
|
Lattice.Core.DragDrop/
|
||||||
|
├── Abstractions/ # Интерфейсы
|
||||||
|
│ ├── IDragSource.cs # Источник перетаскивания (синхронный)
|
||||||
|
│ ├── IAsyncDragSource.cs # Асинхронный источник
|
||||||
|
│ ├── IDropTarget.cs # Цель сброса (синхронная)
|
||||||
|
│ └── IAsyncDropTarget.cs # Асинхронная цель
|
||||||
|
├── Enums/ # Перечисления
|
||||||
|
├── Exceptions/ # Исключения с кодами ошибок
|
||||||
|
├── Extensions/ # Расширения для DI
|
||||||
|
├── Models/ # Модели данных
|
||||||
|
│ ├── DragInfo.cs # Информация о перетаскивании
|
||||||
|
│ └── DropInfo.cs # Информация о сбросе
|
||||||
|
├── Services/ # Сервисы
|
||||||
|
│ ├── IDragDropService.cs # Основной интерфейс
|
||||||
|
│ ├── DragDropService.cs # Реализация сервиса
|
||||||
|
│ └── EventArgs/ # Аргументы событий
|
||||||
|
└── Utilities/ # Утилиты и фабрики
|
||||||
|
├── DragDropUtilities.cs # Синхронные утилиты
|
||||||
|
└── AsyncDragDropUtilities.cs # Асинхронные утилиты
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Быстрый старт
|
||||||
|
|
||||||
|
### 1. Установка
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Добавьте проект Lattice.Core.DragDrop в ваше решение
|
||||||
|
// или создайте NuGet пакет
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Базовое использование
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Lattice.Core.DragDrop;
|
||||||
|
using Lattice.Core.DragDrop.Abstractions;
|
||||||
|
using Lattice.Core.DragDrop.Services;
|
||||||
|
using Lattice.Core.Geometry;
|
||||||
|
|
||||||
|
// Создаем сервис
|
||||||
|
var dragDropService = new DragDropService();
|
||||||
|
|
||||||
|
// Создаем простой источник перетаскивания
|
||||||
|
var dragSource = DragDropUtilities.CreateSimpleDragSource(
|
||||||
|
dataProvider: () => "Example Data",
|
||||||
|
canDrag: () => true,
|
||||||
|
onCompleted: (dragInfo, effects) =>
|
||||||
|
Console.WriteLine($"Drag completed with effects: {effects}"),
|
||||||
|
onCancelled: dragInfo =>
|
||||||
|
Console.WriteLine("Drag cancelled")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Создаем простую цель сброса
|
||||||
|
var dropTarget = DragDropUtilities.CreateSimpleDropTarget(
|
||||||
|
canAccept: dropInfo => dropInfo.Data is string,
|
||||||
|
onDragOver: dropInfo =>
|
||||||
|
dropInfo.SuggestedEffects = DragDropEffects.Copy,
|
||||||
|
onDrop: dropInfo =>
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Dropped: {dropInfo.Data}");
|
||||||
|
dropInfo.MarkAsHandled();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Регистрируем цель
|
||||||
|
string targetId = dragDropService.RegisterDropTarget(
|
||||||
|
dropTarget,
|
||||||
|
new Rect(100, 100, 300, 200)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Начинаем перетаскивание
|
||||||
|
bool started = dragDropService.StartDrag(
|
||||||
|
dragSource,
|
||||||
|
new Point(50, 50)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (started)
|
||||||
|
{
|
||||||
|
// Обновляем позицию
|
||||||
|
dragDropService.UpdateDrag(new Point(150, 150));
|
||||||
|
|
||||||
|
// Завершаем
|
||||||
|
var effects = dragDropService.EndDrag(new Point(200, 200));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📖 Подробное руководство
|
||||||
|
|
||||||
|
### Сервис перетаскивания
|
||||||
|
|
||||||
|
Основной класс системы - `DragDropService`, реализующий `IDragDropService`.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Создание с кастомными настройками
|
||||||
|
var service = new DragDropService(options =>
|
||||||
|
{
|
||||||
|
options.DragStartThreshold = 5.0;
|
||||||
|
options.EnableAsyncOperations = true;
|
||||||
|
options.AsyncOperationTimeout = 3000;
|
||||||
|
options.EnableAutoCleanup = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Свойства
|
||||||
|
bool isActive = service.IsDragActive; // Активна ли операция
|
||||||
|
DragInfo? currentDrag = service.CurrentDragInfo; // Текущая информация
|
||||||
|
double threshold = service.DragStartThreshold; // Порог начала
|
||||||
|
|
||||||
|
// События
|
||||||
|
service.DragStarted += OnDragStarted;
|
||||||
|
service.DragUpdated += OnDragUpdated;
|
||||||
|
service.DragCompleted += OnDragCompleted;
|
||||||
|
service.DragCancelled += OnDragCancelled;
|
||||||
|
service.ErrorOccurred += OnErrorOccurred;
|
||||||
|
|
||||||
|
// Регистрация целей
|
||||||
|
string id = service.RegisterDropTarget(
|
||||||
|
target, // IDropTarget
|
||||||
|
bounds, // Rect
|
||||||
|
priority: 1, // Приоритет (выше = выше приоритет)
|
||||||
|
group: "main" // Группа для группового удаления
|
||||||
|
);
|
||||||
|
|
||||||
|
// Обновление границ
|
||||||
|
service.UpdateDropTargetBounds(id, newBounds);
|
||||||
|
|
||||||
|
// Удаление
|
||||||
|
service.UnregisterDropTarget(id);
|
||||||
|
service.UnregisterDropTargetsInGroup("main");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Асинхронное использование
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Асинхронные методы
|
||||||
|
bool started = await service.StartDragAsync(source, startPosition);
|
||||||
|
await service.UpdateDragAsync(currentPosition);
|
||||||
|
DragDropEffects effects = await service.EndDragAsync(dropPosition);
|
||||||
|
await service.CancelDragAsync();
|
||||||
|
|
||||||
|
// Статистика
|
||||||
|
var stats = service.GetStats();
|
||||||
|
Console.WriteLine($"Operations: {stats.TotalDragOperations}");
|
||||||
|
Console.WriteLine($"Success rate: {stats.SuccessfulDrops}/{stats.TotalDragOperations}");
|
||||||
|
Console.WriteLine($"Avg time: {stats.AverageOperationTime.TotalMilliseconds}ms");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Создание кастомных источников и целей
|
||||||
|
|
||||||
|
#### Синхронная реализация
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class FileDragSource : IDragSource
|
||||||
|
{
|
||||||
|
private readonly FileInfo _file;
|
||||||
|
|
||||||
|
public FileDragSource(FileInfo file) => _file = file;
|
||||||
|
|
||||||
|
public bool CanStartDrag(out DragInfo? dragInfo)
|
||||||
|
{
|
||||||
|
// Проверяем условия
|
||||||
|
if (!_file.Exists || _file.Length > 100 * 1024 * 1024) // 100 MB limit
|
||||||
|
{
|
||||||
|
dragInfo = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем DragInfo
|
||||||
|
dragInfo = new DragInfo(
|
||||||
|
data: _file,
|
||||||
|
allowedEffects: DragDropEffects.Copy | DragDropEffects.Move,
|
||||||
|
startPosition: Point.Zero,
|
||||||
|
source: this
|
||||||
|
);
|
||||||
|
|
||||||
|
// Добавляем дополнительные параметры
|
||||||
|
dragInfo.SetParameter("FileSize", _file.Length);
|
||||||
|
dragInfo.SetParameter("MimeType", GetMimeType(_file));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool StartDrag(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
// Подготовка к перетаскиванию
|
||||||
|
// Можно создать визуальное представление и т.д.
|
||||||
|
Console.WriteLine($"Starting drag of {_file.Name}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DragCompleted(DragInfo dragInfo, DragDropEffects effects)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"File drag completed with {effects}");
|
||||||
|
|
||||||
|
if (effects == DragDropEffects.Move)
|
||||||
|
{
|
||||||
|
// Файл был перемещен - возможно, удалить оригинал
|
||||||
|
// _file.Delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DragCancelled(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
Console.WriteLine("File drag cancelled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Асинхронная реализация
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class DatabaseItemDragSource : IAsyncDragSource
|
||||||
|
{
|
||||||
|
private readonly DatabaseService _db;
|
||||||
|
private readonly int _itemId;
|
||||||
|
|
||||||
|
public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Асинхронные проверки
|
||||||
|
var canDrag = await _db.CanDragItemAsync(_itemId);
|
||||||
|
if (!canDrag) return (false, null);
|
||||||
|
|
||||||
|
// Асинхронная загрузка данных
|
||||||
|
var data = await _db.GetItemForDragAsync(_itemId);
|
||||||
|
if (data == null) return (false, null);
|
||||||
|
|
||||||
|
var dragInfo = new DragInfo(
|
||||||
|
data: data,
|
||||||
|
allowedEffects: DragDropEffects.Copy | DragDropEffects.Move,
|
||||||
|
startPosition: Point.Zero,
|
||||||
|
source: this
|
||||||
|
);
|
||||||
|
|
||||||
|
return (true, dragInfo);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Логирование ошибки
|
||||||
|
return (false, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> StartDragAsync(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
// Асинхронная подготовка
|
||||||
|
return Task.FromResult(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects)
|
||||||
|
{
|
||||||
|
// Асинхронная обработка завершения
|
||||||
|
await _db.LogDragOperationAsync(_itemId, effects);
|
||||||
|
|
||||||
|
if (effects == DragDropEffects.Move)
|
||||||
|
{
|
||||||
|
await _db.MarkItemAsMovedAsync(_itemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DragCancelledAsync(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Синхронные методы для совместимости
|
||||||
|
public bool CanStartDrag(out DragInfo? dragInfo)
|
||||||
|
{
|
||||||
|
var result = Task.Run(() => CanStartDragAsync()).GetAwaiter().GetResult();
|
||||||
|
dragInfo = result.DragInfo;
|
||||||
|
return result.CanStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... остальные синхронные методы
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Работа с моделями данных
|
||||||
|
|
||||||
|
#### DragInfo
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Создание
|
||||||
|
var dragInfo = new DragInfo(
|
||||||
|
data: myObject,
|
||||||
|
allowedEffects: DragDropEffects.Copy | DragDropEffects.Move,
|
||||||
|
startPosition: new Point(x, y),
|
||||||
|
source: this
|
||||||
|
);
|
||||||
|
|
||||||
|
// Параметры
|
||||||
|
dragInfo.SetParameter("Timestamp", DateTime.UtcNow);
|
||||||
|
dragInfo.SetParameter("UserId", currentUser.Id);
|
||||||
|
|
||||||
|
// Получение параметров
|
||||||
|
if (dragInfo.TryGetParameter<string>("Category", out var category))
|
||||||
|
{
|
||||||
|
// Используем категорию
|
||||||
|
}
|
||||||
|
|
||||||
|
// Клонирование с новой позицией
|
||||||
|
var updatedDragInfo = dragInfo.CloneWithPosition(newPosition);
|
||||||
|
|
||||||
|
// Очистка ресурсов
|
||||||
|
dragInfo.Dispose();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DropInfo
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Создается сервисом автоматически
|
||||||
|
// Работа с DropInfo в методах цели:
|
||||||
|
|
||||||
|
public void DragOver(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
// Проверяем данные
|
||||||
|
if (dropInfo.Data is MyDataType myData)
|
||||||
|
{
|
||||||
|
// Определяем позицию относительно цели
|
||||||
|
dropInfo.DropPosition = CalculateDropPosition(dropInfo.Position);
|
||||||
|
|
||||||
|
// Предлагаем эффекты
|
||||||
|
if (CanAcceptData(myData))
|
||||||
|
{
|
||||||
|
dropInfo.SuggestedEffects = DragDropEffects.Move;
|
||||||
|
dropInfo.ShowVisualFeedback = true;
|
||||||
|
dropInfo.VisualFeedbackData = CreatePreview(myData);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dropInfo.SuggestedEffects = DragDropEffects.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Drop(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
if (dropInfo.Data is MyDataType myData)
|
||||||
|
{
|
||||||
|
// Обработка сброса
|
||||||
|
ProcessDrop(myData, dropInfo.DropPosition);
|
||||||
|
|
||||||
|
// Помечаем как обработанное
|
||||||
|
dropInfo.MarkAsHandled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Утилиты и фабрики
|
||||||
|
|
||||||
|
#### Синхронные утилиты
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Простые реализации
|
||||||
|
var simpleSource = DragDropUtilities.CreateSimpleDragSource(
|
||||||
|
() => data,
|
||||||
|
() => true,
|
||||||
|
(dragInfo, effects) => Console.WriteLine("Completed"),
|
||||||
|
dragInfo => Console.WriteLine("Cancelled")
|
||||||
|
);
|
||||||
|
|
||||||
|
var simpleTarget = DragDropUtilities.CreateSimpleDropTarget(
|
||||||
|
dropInfo => dropInfo.Data != null,
|
||||||
|
dropInfo => dropInfo.SuggestedEffects = DragDropEffects.Copy,
|
||||||
|
dropInfo => Console.WriteLine($"Dropped: {dropInfo.Data}"),
|
||||||
|
() => Console.WriteLine("Drag left")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Геометрия
|
||||||
|
double distance = DragDropUtilities.CalculateDistance(p1, p2);
|
||||||
|
bool exceeded = DragDropUtilities.HasExceededDragThreshold(start, current, threshold);
|
||||||
|
DropPosition position = DragDropUtilities.GetDropPosition(point, bounds, edgeThreshold);
|
||||||
|
|
||||||
|
// Проверка совместимости
|
||||||
|
bool compatible = DragDropUtilities.AreEffectsCompatible(sourceEffects, targetEffects);
|
||||||
|
bool typeMatch = DragDropUtilities.IsDataCompatible(data, new[] { typeof(string), typeof(int) });
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Асинхронные утилиты
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Асинхронные реализации
|
||||||
|
var asyncSource = AsyncDragDropUtilities.CreateAsyncDragSource(
|
||||||
|
async () => await LoadDataAsync(),
|
||||||
|
async () => await CanDragAsync(),
|
||||||
|
async (dragInfo, effects) => await OnCompletedAsync(dragInfo, effects),
|
||||||
|
async dragInfo => await OnCancelledAsync(dragInfo)
|
||||||
|
);
|
||||||
|
|
||||||
|
var asyncTarget = AsyncDragDropUtilities.CreateAsyncDropTarget(
|
||||||
|
async dropInfo => await CanAcceptAsync(dropInfo.Data),
|
||||||
|
async dropInfo => await OnDragOverAsync(dropInfo),
|
||||||
|
async dropInfo => await OnDropAsync(dropInfo),
|
||||||
|
async () => await OnDragLeaveAsync()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Адаптеры для синхронных интерфейсов
|
||||||
|
IAsyncDragSource asyncFromSync = AsyncDragDropUtilities.CreateAsyncAdapter(syncSource);
|
||||||
|
IAsyncDropTarget asyncTargetFromSync = AsyncDragDropUtilities.CreateAsyncAdapter(syncTarget);
|
||||||
|
|
||||||
|
// Комбинированные реализации (fallback стратегия)
|
||||||
|
var combined = AsyncDragDropUtilities.Combine(
|
||||||
|
syncSource,
|
||||||
|
asyncSource,
|
||||||
|
preferAsync: true // При ошибке в async использует sync
|
||||||
|
);
|
||||||
|
|
||||||
|
// Таймауты
|
||||||
|
var result = await AsyncDragDropUtilities.ExecuteWithTimeoutAsync(
|
||||||
|
task: LongOperationAsync(),
|
||||||
|
timeout: TimeSpan.FromSeconds(5),
|
||||||
|
defaultValue: fallbackValue
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Обработка ошибок
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Подписка на ошибки
|
||||||
|
service.ErrorOccurred += (sender, e) =>
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error in {e.Operation}: {e.Exception.Message}");
|
||||||
|
|
||||||
|
// Коды ошибок определены в DragDropErrorCodes
|
||||||
|
switch (e.ErrorCode)
|
||||||
|
{
|
||||||
|
case DragDropErrorCodes.Timeout:
|
||||||
|
Console.WriteLine("Operation timed out");
|
||||||
|
break;
|
||||||
|
case DragDropErrorCodes.SourceCannotDrag:
|
||||||
|
Console.WriteLine("Source cannot drag");
|
||||||
|
break;
|
||||||
|
case DragDropErrorCodes.TargetCannotAccept:
|
||||||
|
Console.WriteLine("Target cannot accept");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Использование в коде
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await service.StartDragAsync(source, position);
|
||||||
|
}
|
||||||
|
catch (DragDropException ex)
|
||||||
|
{
|
||||||
|
// Обработка специфичных для DragDrop ошибок
|
||||||
|
Console.WriteLine($"DragDrop error {ex.ErrorCode}: {ex.Message}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Обработка других ошибок
|
||||||
|
Console.WriteLine($"General error: {ex.Message}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Интеграция с UI фреймворками
|
||||||
|
|
||||||
|
### Базовый адаптер для WinUI/WPF
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class UIElementDragSource : IAsyncDragSource
|
||||||
|
{
|
||||||
|
private readonly FrameworkElement _element;
|
||||||
|
private readonly Func<object> _dataProvider;
|
||||||
|
|
||||||
|
public UIElementDragSource(FrameworkElement element, Func<object> dataProvider)
|
||||||
|
{
|
||||||
|
_element = element;
|
||||||
|
_dataProvider = dataProvider;
|
||||||
|
|
||||||
|
// Подписка на события
|
||||||
|
_element.PointerPressed += OnPointerPressed;
|
||||||
|
_element.PointerMoved += OnPointerMoved;
|
||||||
|
_element.PointerReleased += OnPointerReleased;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Point _dragStartPosition;
|
||||||
|
private bool _isDragging;
|
||||||
|
|
||||||
|
private void OnPointerPressed(object sender, PointerRoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var point = e.GetCurrentPoint(_element);
|
||||||
|
_dragStartPosition = new Point(point.Position.X, point.Position.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnPointerMoved(object sender, PointerRoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_isDragging) return;
|
||||||
|
|
||||||
|
var point = e.GetCurrentPoint(_element);
|
||||||
|
var current = new Point(point.Position.X, point.Position.Y);
|
||||||
|
|
||||||
|
var distance = Math.Sqrt(
|
||||||
|
Math.Pow(current.X - _dragStartPosition.X, 2) +
|
||||||
|
Math.Pow(current.Y - _dragStartPosition.Y, 2));
|
||||||
|
|
||||||
|
if (distance > 3.0) // Порог
|
||||||
|
{
|
||||||
|
_isDragging = true;
|
||||||
|
|
||||||
|
// Начинаем перетаскивание через сервис
|
||||||
|
var service = GetDragDropService();
|
||||||
|
await service.StartDragAsync(this, ConvertToScreen(_dragStartPosition));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
|
||||||
|
{
|
||||||
|
var data = _dataProvider();
|
||||||
|
if (data == null) return (false, null);
|
||||||
|
|
||||||
|
var dragInfo = new DragInfo(
|
||||||
|
data,
|
||||||
|
DragDropEffects.Copy | DragDropEffects.Move,
|
||||||
|
Point.Zero,
|
||||||
|
this
|
||||||
|
);
|
||||||
|
|
||||||
|
return (true, dragInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... остальная реализация
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Тестирование
|
||||||
|
|
||||||
|
### Примеры модульных тестов
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[TestClass]
|
||||||
|
public class DragDropServiceTests
|
||||||
|
{
|
||||||
|
private DragDropService _service;
|
||||||
|
private Mock<IAsyncDragSource> _mockSource;
|
||||||
|
private Mock<IAsyncDropTarget> _mockTarget;
|
||||||
|
|
||||||
|
[TestInitialize]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_service = new DragDropService();
|
||||||
|
_mockSource = new Mock<IAsyncDragSource>();
|
||||||
|
_mockTarget = new Mock<IAsyncDropTarget>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task StartDrag_ValidSource_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dragInfo = new DragInfo("test", DragDropEffects.Copy, Point.Zero);
|
||||||
|
_mockSource.Setup(s => s.CanStartDragAsync())
|
||||||
|
.ReturnsAsync((true, dragInfo));
|
||||||
|
_mockSource.Setup(s => s.StartDragAsync(It.IsAny<DragInfo>()))
|
||||||
|
.ReturnsAsync(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.StartDragAsync(_mockSource.Object, Point.Zero);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(result);
|
||||||
|
Assert.IsTrue(_service.IsDragActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task UpdateDrag_FindsTarget_CallsDragOver()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var targetId = _service.RegisterDropTarget(
|
||||||
|
_mockTarget.Object,
|
||||||
|
new Rect(0, 0, 100, 100)
|
||||||
|
);
|
||||||
|
|
||||||
|
await StartTestDrag();
|
||||||
|
|
||||||
|
_mockTarget.Setup(t => t.CanAcceptDropAsync(It.IsAny<DropInfo>()))
|
||||||
|
.ReturnsAsync(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _service.UpdateDragAsync(new Point(50, 50));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockTarget.Verify(t => t.DragOverAsync(It.IsAny<DropInfo>()), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartTestDrag()
|
||||||
|
{
|
||||||
|
var dragInfo = new DragInfo("test", DragDropEffects.Copy, Point.Zero);
|
||||||
|
_mockSource.Setup(s => s.CanStartDragAsync())
|
||||||
|
.ReturnsAsync((true, dragInfo));
|
||||||
|
_mockSource.Setup(s => s.StartDragAsync(It.IsAny<DragInfo>()))
|
||||||
|
.ReturnsAsync(true);
|
||||||
|
|
||||||
|
await _service.StartDragAsync(_mockSource.Object, Point.Zero);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Мониторинг и производительность
|
||||||
|
|
||||||
|
### Сбор статистики
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Получение статистики
|
||||||
|
var stats = service.GetStats();
|
||||||
|
|
||||||
|
Console.WriteLine($"Total operations: {stats.TotalDragOperations}");
|
||||||
|
Console.WriteLine($"Successful: {stats.SuccessfulDrops}");
|
||||||
|
Console.WriteLine($"Cancelled: {stats.CancelledOperations}");
|
||||||
|
Console.WriteLine($"Errors: {stats.ErrorCount}");
|
||||||
|
Console.WriteLine($"Avg time: {stats.AverageOperationTime.TotalMilliseconds}ms");
|
||||||
|
|
||||||
|
// Мониторинг в реальном времени
|
||||||
|
private Stopwatch _operationTimer;
|
||||||
|
|
||||||
|
service.DragStarted += (s, e) =>
|
||||||
|
{
|
||||||
|
_operationTimer = Stopwatch.StartNew();
|
||||||
|
Console.WriteLine($"Drag started from {e.DragInfo.Source}");
|
||||||
|
};
|
||||||
|
|
||||||
|
service.DragCompleted += (s, e) =>
|
||||||
|
{
|
||||||
|
_operationTimer.Stop();
|
||||||
|
Console.WriteLine($"Drag completed in {_operationTimer.ElapsedMilliseconds}ms");
|
||||||
|
|
||||||
|
if (service.EnableAsyncOperations)
|
||||||
|
{
|
||||||
|
var stats = service.GetStats();
|
||||||
|
Console.WriteLine($"Success rate: {(double)stats.SuccessfulDrops / stats.TotalDragOperations:P}");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Оптимизация производительности
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 1. Настройка параметров
|
||||||
|
var service = new DragDropService(options =>
|
||||||
|
{
|
||||||
|
options.DragStartThreshold = 4.0; // Увеличить порог для предотвращения случайных перетаскиваний
|
||||||
|
options.AsyncOperationTimeout = 2000; // Уменьшить таймаут для отзывчивости
|
||||||
|
options.EnableAutoCleanup = true; // Автоочистка неиспользуемых целей
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Группировка целей
|
||||||
|
_service.RegisterDropTarget(target1, bounds1, group: "toolbox");
|
||||||
|
_service.RegisterDropTarget(target2, bounds2, group: "toolbox");
|
||||||
|
// Быстрое удаление всех целей группы
|
||||||
|
_service.UnregisterDropTargetsInGroup("toolbox");
|
||||||
|
|
||||||
|
// 3. Приоритеты для оптимизации поиска
|
||||||
|
_service.RegisterDropTarget(importantTarget, bounds, priority: 100); // Высокий приоритет
|
||||||
|
_service.RegisterDropTarget(defaultTarget, bounds, priority: 0); // Низкий приоритет
|
||||||
|
|
||||||
|
// 4. Периодическая очистка
|
||||||
|
service.ClearAllDropTargets(); // При смене контекста
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Продвинутые сценарии
|
||||||
|
|
||||||
|
### Переупорядочивание элементов
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ReorderableListDropTarget : IAsyncDropTarget
|
||||||
|
{
|
||||||
|
private readonly IList<object> _items;
|
||||||
|
|
||||||
|
public async Task<bool> CanAcceptDropAsync(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
return dropInfo.Data is object && _items.Contains(dropInfo.Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DropAsync(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
var item = dropInfo.Data;
|
||||||
|
var insertIndex = CalculateInsertIndex(dropInfo);
|
||||||
|
|
||||||
|
// Удаляем из старой позиции
|
||||||
|
_items.Remove(item);
|
||||||
|
|
||||||
|
// Вставляем в новую позицию
|
||||||
|
if (insertIndex < _items.Count)
|
||||||
|
_items.Insert(insertIndex, item);
|
||||||
|
else
|
||||||
|
_items.Add(item);
|
||||||
|
|
||||||
|
dropInfo.MarkAsHandled();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int CalculateInsertIndex(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
// Логика определения позиции вставки на основе dropInfo.Position
|
||||||
|
// и визуального расположения элементов
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Мультиселект и групповое перетаскивание
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class MultiSelectionDragSource : IAsyncDragSource
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<object> _selectedItems;
|
||||||
|
|
||||||
|
public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
|
||||||
|
{
|
||||||
|
if (!_selectedItems.Any()) return (false, null);
|
||||||
|
|
||||||
|
// Создаем коллекцию для перетаскивания
|
||||||
|
var dragData = new DragItemCollection(_selectedItems);
|
||||||
|
|
||||||
|
var dragInfo = new DragInfo(
|
||||||
|
dragData,
|
||||||
|
DragDropEffects.Copy | DragDropEffects.Move,
|
||||||
|
Point.Zero,
|
||||||
|
this
|
||||||
|
);
|
||||||
|
|
||||||
|
dragInfo.SetParameter("ItemCount", _selectedItems.Count());
|
||||||
|
dragInfo.SetParameter("IsMultiSelect", true);
|
||||||
|
|
||||||
|
return (true, dragInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 API Reference
|
||||||
|
|
||||||
|
### Основные интерфейсы
|
||||||
|
|
||||||
|
#### IDragDropService
|
||||||
|
```csharp
|
||||||
|
bool IsDragActive { get; }
|
||||||
|
DragInfo? CurrentDragInfo { get; }
|
||||||
|
IDropTarget? CurrentDropTarget { get; }
|
||||||
|
double DragStartThreshold { get; set; }
|
||||||
|
bool EnableAsyncOperations { get; set; }
|
||||||
|
|
||||||
|
// Регистрация целей
|
||||||
|
string RegisterDropTarget(IDropTarget target, Rect bounds, int priority = 0, string? group = null);
|
||||||
|
bool UpdateDropTargetBounds(string id, Rect bounds);
|
||||||
|
bool UnregisterDropTarget(string id);
|
||||||
|
void UnregisterDropTargetsInGroup(string group);
|
||||||
|
|
||||||
|
// Асинхронные операции
|
||||||
|
Task<bool> StartDragAsync(IDragSource source, Point startPosition);
|
||||||
|
Task UpdateDragAsync(Point position);
|
||||||
|
Task<DragDropEffects> EndDragAsync(Point position);
|
||||||
|
Task CancelDragAsync();
|
||||||
|
|
||||||
|
// Синхронные операции
|
||||||
|
bool StartDrag(IDragSource source, Point startPosition);
|
||||||
|
void UpdateDrag(Point position);
|
||||||
|
DragDropEffects EndDrag(Point position);
|
||||||
|
void CancelDrag();
|
||||||
|
|
||||||
|
// Утилиты
|
||||||
|
void ClearAllDropTargets();
|
||||||
|
DragDropStats GetStats();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### IAsyncDragSource
|
||||||
|
```csharp
|
||||||
|
Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync();
|
||||||
|
Task<bool> StartDragAsync(DragInfo dragInfo);
|
||||||
|
Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects);
|
||||||
|
Task DragCancelledAsync(DragInfo dragInfo);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### IAsyncDropTarget
|
||||||
|
```csharp
|
||||||
|
Task<bool> CanAcceptDropAsync(DropInfo dropInfo);
|
||||||
|
Task DragOverAsync(DropInfo dropInfo);
|
||||||
|
Task DropAsync(DropInfo dropInfo);
|
||||||
|
Task DragLeaveAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Перечисления
|
||||||
|
|
||||||
|
#### DragDropEffects
|
||||||
|
```csharp
|
||||||
|
[Flags]
|
||||||
|
None = 0
|
||||||
|
Copy = 1 << 0 // Копирование данных
|
||||||
|
Move = 1 << 1 // Перемещение данных
|
||||||
|
Link = 1 << 2 // Ссылка на данные
|
||||||
|
CopyOrMove = Copy | Move
|
||||||
|
All = Copy | Move | Link
|
||||||
|
|
||||||
|
// Методы расширения:
|
||||||
|
bool CanCopy(this DragDropEffects effects)
|
||||||
|
bool CanMove(this DragDropEffects effects)
|
||||||
|
bool CanLink(this DragDropEffects effects)
|
||||||
|
DragDropEffects GetEffectFromKeys(bool controlKey, bool shiftKey, bool altKey)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DropPosition
|
||||||
|
```csharp
|
||||||
|
Inside // Внутри элемента
|
||||||
|
Top // Сверху
|
||||||
|
Bottom // Снизу
|
||||||
|
Left // Слева
|
||||||
|
Right // Справа
|
||||||
|
Center // По центру
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔮 Планы развития
|
||||||
|
|
||||||
|
1. **Интеграция с популярными UI фреймворками** (WinUI, Uno Platform, Avalonia)
|
||||||
|
2. **Поддержка жестов** (тач, мультитач)
|
||||||
|
3. **Виртуализация** для работы с большими наборами данных
|
||||||
|
4. **Продвинутые визуальные эффекты** (анимации, превью)
|
||||||
|
5. **Source Generators** для автоматической генерации кода
|
||||||
|
6. **Инструменты разработчика** (дебаггер, профилировщик)
|
||||||
829
Lattice.Core.DragDrop/Services/DragDropService.cs
Normal file
@@ -0,0 +1,829 @@
|
|||||||
|
namespace Lattice.Core.DragDrop.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Реализация сервиса управления операциями перетаскивания.
|
||||||
|
/// Полностью потокобезопасная реализация с поддержкой async/await.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DragDropService : IDragDropService
|
||||||
|
{
|
||||||
|
#region Nested Types
|
||||||
|
|
||||||
|
private sealed class DropTargetInfo : IDisposable
|
||||||
|
{
|
||||||
|
public required Abstractions.IDropTarget Target { get; init; }
|
||||||
|
public required Geometry.Rect Bounds { get; set; }
|
||||||
|
public required int Priority { get; init; }
|
||||||
|
public required string? Group { get; init; }
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public DateTime LastAccessTime { get; set; } = DateTime.UtcNow;
|
||||||
|
public int UsageCount { get; set; }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Target is IDisposable disposable)
|
||||||
|
disposable.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DragOperationContext : IDisposable
|
||||||
|
{
|
||||||
|
public Abstractions.IDragSource? Source { get; set; }
|
||||||
|
public Models.DragInfo? DragInfo { get; set; }
|
||||||
|
public Abstractions.IDropTarget? CurrentDropTarget { get; set; }
|
||||||
|
public CancellationTokenSource? CancellationTokenSource { get; set; }
|
||||||
|
public DateTime StartTime { get; set; } = DateTime.UtcNow;
|
||||||
|
public bool ThresholdExceeded { get; set; }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
DragInfo?.Dispose();
|
||||||
|
CancellationTokenSource?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Fields
|
||||||
|
|
||||||
|
private readonly Dictionary<string, DropTargetInfo> _dropTargets = new();
|
||||||
|
private readonly ReaderWriterLockSlim _dropTargetsLock = new(LockRecursionPolicy.NoRecursion);
|
||||||
|
private readonly object _dragOperationLock = new();
|
||||||
|
|
||||||
|
private DragOperationContext? _currentDragOperation;
|
||||||
|
private Timer? _cleanupTimer;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
private int _totalDragOperations;
|
||||||
|
private int _successfulDrops;
|
||||||
|
private int _cancelledOperations;
|
||||||
|
private int _errorCount;
|
||||||
|
private long _totalOperationTicks;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Events
|
||||||
|
|
||||||
|
private event EventHandler<DragStartedEventArgs>? _dragStarted;
|
||||||
|
private event EventHandler<DragUpdatedEventArgs>? _dragUpdated;
|
||||||
|
private event EventHandler<DropTargetChangedEventArgs>? _dropTargetChanged;
|
||||||
|
private event EventHandler<DragCompletedEventArgs>? _dragCompleted;
|
||||||
|
private event EventHandler<DragCancelledEventArgs>? _dragCancelled;
|
||||||
|
private event EventHandler<DragDropErrorEventArgs>? _errorOccurred;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
public bool IsDragActive => Volatile.Read(ref _currentDragOperation) != null;
|
||||||
|
|
||||||
|
public Models.DragInfo? CurrentDragInfo => _currentDragOperation?.DragInfo;
|
||||||
|
|
||||||
|
public Abstractions.IDropTarget? CurrentDropTarget => _currentDragOperation?.CurrentDropTarget;
|
||||||
|
|
||||||
|
public double DragStartThreshold { get; set; } = 3.0;
|
||||||
|
|
||||||
|
public bool EnableAsyncOperations { get; set; } = true;
|
||||||
|
|
||||||
|
public int AsyncOperationTimeout { get; set; } = 5000;
|
||||||
|
|
||||||
|
public event EventHandler<DragStartedEventArgs> DragStarted
|
||||||
|
{
|
||||||
|
add => _dragStarted += value;
|
||||||
|
remove => _dragStarted -= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<DragUpdatedEventArgs> DragUpdated
|
||||||
|
{
|
||||||
|
add => _dragUpdated += value;
|
||||||
|
remove => _dragUpdated -= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<DropTargetChangedEventArgs> DropTargetChanged
|
||||||
|
{
|
||||||
|
add => _dropTargetChanged += value;
|
||||||
|
remove => _dropTargetChanged -= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<DragCompletedEventArgs> DragCompleted
|
||||||
|
{
|
||||||
|
add => _dragCompleted += value;
|
||||||
|
remove => _dragCompleted -= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<DragCancelledEventArgs> DragCancelled
|
||||||
|
{
|
||||||
|
add => _dragCancelled += value;
|
||||||
|
remove => _dragCancelled -= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<DragDropErrorEventArgs> ErrorOccurred
|
||||||
|
{
|
||||||
|
add => _errorOccurred += value;
|
||||||
|
remove => _errorOccurred -= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Constructor
|
||||||
|
|
||||||
|
public DragDropService()
|
||||||
|
{
|
||||||
|
// Инициализация таймера очистки (каждые 5 минут)
|
||||||
|
_cleanupTimer = new Timer(CleanupExpiredTargets, null,
|
||||||
|
TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Registration Methods
|
||||||
|
|
||||||
|
public string RegisterDropTarget(Abstractions.IDropTarget target, Geometry.Rect bounds, int priority = 0, string? group = null)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
if (target == null) throw new ArgumentNullException(nameof(target));
|
||||||
|
|
||||||
|
var id = Guid.NewGuid().ToString("N");
|
||||||
|
var info = new DropTargetInfo
|
||||||
|
{
|
||||||
|
Target = target,
|
||||||
|
Bounds = bounds,
|
||||||
|
Priority = priority,
|
||||||
|
Group = group,
|
||||||
|
Id = id
|
||||||
|
};
|
||||||
|
|
||||||
|
_dropTargetsLock.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_dropTargets[id] = info;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_dropTargetsLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool UpdateDropTargetBounds(string id, Geometry.Rect bounds)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
_dropTargetsLock.EnterUpgradeableReadLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_dropTargets.TryGetValue(id, out var info))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_dropTargetsLock.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
info.Bounds = bounds;
|
||||||
|
info.LastAccessTime = DateTime.UtcNow;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_dropTargetsLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_dropTargetsLock.ExitUpgradeableReadLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool UnregisterDropTarget(string id)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
_dropTargetsLock.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_dropTargets.Remove(id, out var info))
|
||||||
|
{
|
||||||
|
info.Dispose();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_dropTargetsLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UnregisterDropTargetsInGroup(string group)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
if (string.IsNullOrEmpty(group)) return;
|
||||||
|
|
||||||
|
_dropTargetsLock.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var idsToRemove = new List<string>();
|
||||||
|
|
||||||
|
foreach (var kvp in _dropTargets)
|
||||||
|
{
|
||||||
|
if (kvp.Value.Group == group)
|
||||||
|
{
|
||||||
|
idsToRemove.Add(kvp.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var id in idsToRemove)
|
||||||
|
{
|
||||||
|
if (_dropTargets.Remove(id, out var info))
|
||||||
|
{
|
||||||
|
info.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_dropTargetsLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Async Operations
|
||||||
|
|
||||||
|
public async Task<bool> StartDragAsync(Abstractions.IDragSource source, Geometry.Point startPosition)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
if (source == null) throw new ArgumentNullException(nameof(source));
|
||||||
|
|
||||||
|
lock (_dragOperationLock)
|
||||||
|
{
|
||||||
|
if (_currentDragOperation != null)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _totalDragOperations);
|
||||||
|
|
||||||
|
Models.DragInfo? dragInfo = null;
|
||||||
|
|
||||||
|
// Проверка возможности начала перетаскивания
|
||||||
|
if (EnableAsyncOperations && source is Abstractions.IAsyncDragSource asyncSource)
|
||||||
|
{
|
||||||
|
var result = await ExecuteWithTimeoutAsync(
|
||||||
|
asyncSource.CanStartDragAsync(),
|
||||||
|
"CanStartDragAsync",
|
||||||
|
source);
|
||||||
|
|
||||||
|
if (!result.CanStart || result.DragInfo == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
dragInfo = result.DragInfo;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!source.CanStartDrag(out dragInfo) || dragInfo == null)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var updatedDragInfo = dragInfo.CloneWithPosition(startPosition);
|
||||||
|
|
||||||
|
// Начало перетаскивания
|
||||||
|
bool started;
|
||||||
|
|
||||||
|
if (EnableAsyncOperations && source is Abstractions.IAsyncDragSource asyncSource2)
|
||||||
|
{
|
||||||
|
started = await ExecuteWithTimeoutAsync(
|
||||||
|
asyncSource2.StartDragAsync(updatedDragInfo),
|
||||||
|
"StartDragAsync",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
started = source.StartDrag(updatedDragInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!started)
|
||||||
|
{
|
||||||
|
updatedDragInfo.Dispose();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_dragOperationLock)
|
||||||
|
{
|
||||||
|
_currentDragOperation = new DragOperationContext
|
||||||
|
{
|
||||||
|
Source = source,
|
||||||
|
DragInfo = updatedDragInfo,
|
||||||
|
CancellationTokenSource = new CancellationTokenSource()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вызов события
|
||||||
|
_dragStarted?.Invoke(this, new DragStartedEventArgs(updatedDragInfo, startPosition));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _errorCount);
|
||||||
|
HandleError(ex, "StartDragAsync", source);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateDragAsync(Geometry.Point position)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
DragOperationContext? context;
|
||||||
|
lock (_dragOperationLock)
|
||||||
|
{
|
||||||
|
context = _currentDragOperation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context?.DragInfo == null || context.Source == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Проверка порога начала
|
||||||
|
if (!context.ThresholdExceeded && DragStartThreshold > 0)
|
||||||
|
{
|
||||||
|
var distance = CalculateDistance(context.DragInfo.StartPosition, position);
|
||||||
|
if (distance < DragStartThreshold)
|
||||||
|
return;
|
||||||
|
|
||||||
|
context.ThresholdExceeded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var updatedDragInfo = context.DragInfo.CloneWithPosition(position);
|
||||||
|
context.DragInfo.Dispose();
|
||||||
|
context.DragInfo = updatedDragInfo;
|
||||||
|
|
||||||
|
// Поиск новой цели сброса
|
||||||
|
var newDropTarget = await FindDropTargetAsync(position, updatedDragInfo);
|
||||||
|
|
||||||
|
// Обработка смены цели
|
||||||
|
if (context.CurrentDropTarget != newDropTarget?.Target)
|
||||||
|
{
|
||||||
|
if (context.CurrentDropTarget != null)
|
||||||
|
{
|
||||||
|
await ExecuteTargetOperationAsync(
|
||||||
|
context.CurrentDropTarget,
|
||||||
|
t => t.DragLeaveAsync(),
|
||||||
|
t => t.DragLeave(),
|
||||||
|
"DragLeave");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.CurrentDropTarget = newDropTarget?.Target;
|
||||||
|
|
||||||
|
if (newDropTarget != null)
|
||||||
|
{
|
||||||
|
newDropTarget.UsageCount++;
|
||||||
|
_dropTargetChanged?.Invoke(this, new DropTargetChangedEventArgs(
|
||||||
|
updatedDragInfo, newDropTarget.Target, newDropTarget.Bounds));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Уведомление текущей цели
|
||||||
|
if (context.CurrentDropTarget != null)
|
||||||
|
{
|
||||||
|
var dropInfo = new Models.DropInfo(
|
||||||
|
updatedDragInfo.Data,
|
||||||
|
position,
|
||||||
|
updatedDragInfo.AllowedEffects,
|
||||||
|
context.CurrentDropTarget);
|
||||||
|
|
||||||
|
await ExecuteTargetOperationAsync(
|
||||||
|
context.CurrentDropTarget,
|
||||||
|
t => t.DragOverAsync(dropInfo),
|
||||||
|
t => t.DragOver(dropInfo),
|
||||||
|
"DragOver");
|
||||||
|
}
|
||||||
|
|
||||||
|
_dragUpdated?.Invoke(this, new DragUpdatedEventArgs(updatedDragInfo, position));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _errorCount);
|
||||||
|
HandleError(ex, "UpdateDragAsync", context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Enums.DragDropEffects> EndDragAsync(Geometry.Point position)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
DragOperationContext? context;
|
||||||
|
lock (_dragOperationLock)
|
||||||
|
{
|
||||||
|
context = _currentDragOperation;
|
||||||
|
_currentDragOperation = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context == null || context.DragInfo == null || context.Source == null)
|
||||||
|
{
|
||||||
|
Reset();
|
||||||
|
return Enums.DragDropEffects.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var effects = Enums.DragDropEffects.None;
|
||||||
|
var operationTime = DateTime.UtcNow - context.StartTime;
|
||||||
|
Interlocked.Add(ref _totalOperationTicks, operationTime.Ticks);
|
||||||
|
|
||||||
|
// Выполнение сброса
|
||||||
|
if (context.CurrentDropTarget != null)
|
||||||
|
{
|
||||||
|
var dropInfo = new Models.DropInfo(
|
||||||
|
context.DragInfo.Data,
|
||||||
|
position,
|
||||||
|
context.DragInfo.AllowedEffects,
|
||||||
|
context.CurrentDropTarget);
|
||||||
|
|
||||||
|
await ExecuteTargetOperationAsync(
|
||||||
|
context.CurrentDropTarget,
|
||||||
|
t => t.DropAsync(dropInfo),
|
||||||
|
t => t.Drop(dropInfo),
|
||||||
|
"Drop");
|
||||||
|
|
||||||
|
if (dropInfo.Handled)
|
||||||
|
{
|
||||||
|
effects = dropInfo.SuggestedEffects;
|
||||||
|
Interlocked.Increment(ref _successfulDrops);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Уведомление источника
|
||||||
|
await ExecuteSourceOperationAsync(
|
||||||
|
context.Source,
|
||||||
|
s => s.DragCompletedAsync(context.DragInfo, effects),
|
||||||
|
s => s.DragCompleted(context.DragInfo, effects),
|
||||||
|
"DragCompleted",
|
||||||
|
effects);
|
||||||
|
|
||||||
|
// Событие завершения
|
||||||
|
_dragCompleted?.Invoke(this, new DragCompletedEventArgs(
|
||||||
|
context.DragInfo, position, effects));
|
||||||
|
|
||||||
|
return effects;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _errorCount);
|
||||||
|
HandleError(ex, "EndDragAsync", context);
|
||||||
|
return Enums.DragDropEffects.None;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
context.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CancelDragAsync()
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
DragOperationContext? context;
|
||||||
|
lock (_dragOperationLock)
|
||||||
|
{
|
||||||
|
context = _currentDragOperation;
|
||||||
|
_currentDragOperation = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context == null || context.DragInfo == null || context.Source == null)
|
||||||
|
{
|
||||||
|
Reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
context.CancellationTokenSource?.Cancel();
|
||||||
|
Interlocked.Increment(ref _cancelledOperations);
|
||||||
|
|
||||||
|
await ExecuteSourceOperationAsync(
|
||||||
|
context.Source,
|
||||||
|
s => s.DragCancelledAsync(context.DragInfo),
|
||||||
|
s => s.DragCancelled(context.DragInfo),
|
||||||
|
"DragCancelled");
|
||||||
|
|
||||||
|
_dragCancelled?.Invoke(this, new DragCancelledEventArgs(context.DragInfo));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _errorCount);
|
||||||
|
HandleError(ex, "CancelDragAsync", context);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
context.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Synchronous Operations (for compatibility)
|
||||||
|
|
||||||
|
public bool StartDrag(Abstractions.IDragSource source, Geometry.Point startPosition)
|
||||||
|
{
|
||||||
|
return Task.Run(() => StartDragAsync(source, startPosition)).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateDrag(Geometry.Point position)
|
||||||
|
{
|
||||||
|
Task.Run(() => UpdateDragAsync(position)).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Enums.DragDropEffects EndDrag(Geometry.Point position)
|
||||||
|
{
|
||||||
|
return Task.Run(() => EndDragAsync(position)).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CancelDrag()
|
||||||
|
{
|
||||||
|
Task.Run(CancelDragAsync).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Utility Methods
|
||||||
|
|
||||||
|
public void ClearAllDropTargets()
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
_dropTargetsLock.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var info in _dropTargets.Values)
|
||||||
|
{
|
||||||
|
info.Dispose();
|
||||||
|
}
|
||||||
|
_dropTargets.Clear();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_dropTargetsLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DragDropStats GetStats()
|
||||||
|
{
|
||||||
|
return new DragDropStats
|
||||||
|
{
|
||||||
|
TotalDragOperations = _totalDragOperations,
|
||||||
|
SuccessfulDrops = _successfulDrops,
|
||||||
|
CancelledOperations = _cancelledOperations,
|
||||||
|
ErrorCount = _errorCount,
|
||||||
|
RegisteredTargets = _dropTargets.Count,
|
||||||
|
AverageOperationTime = _totalDragOperations > 0
|
||||||
|
? TimeSpan.FromTicks(_totalOperationTicks / _totalDragOperations)
|
||||||
|
: TimeSpan.Zero
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Private Helper Methods
|
||||||
|
|
||||||
|
private async Task<DropTargetInfo?> FindDropTargetAsync(Geometry.Point position, Models.DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
DropTargetInfo? bestTarget = null;
|
||||||
|
|
||||||
|
_dropTargetsLock.EnterReadLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var info in _dropTargets.Values)
|
||||||
|
{
|
||||||
|
if (!info.Bounds.Contains(position))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var dropInfo = new Models.DropInfo(
|
||||||
|
dragInfo.Data,
|
||||||
|
position,
|
||||||
|
dragInfo.AllowedEffects,
|
||||||
|
info.Target);
|
||||||
|
|
||||||
|
bool canAccept;
|
||||||
|
|
||||||
|
if (EnableAsyncOperations && info.Target is Abstractions.IAsyncDropTarget asyncTarget)
|
||||||
|
{
|
||||||
|
canAccept = await ExecuteWithTimeoutAsync(
|
||||||
|
asyncTarget.CanAcceptDropAsync(dropInfo),
|
||||||
|
"CanAcceptDropAsync",
|
||||||
|
info.Target);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
canAccept = info.Target.CanAcceptDrop(dropInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canAccept)
|
||||||
|
{
|
||||||
|
info.LastAccessTime = DateTime.UtcNow;
|
||||||
|
|
||||||
|
if (bestTarget == null || info.Priority > bestTarget.Priority)
|
||||||
|
{
|
||||||
|
bestTarget = info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_dropTargetsLock.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteTargetOperationAsync(
|
||||||
|
Abstractions.IDropTarget target,
|
||||||
|
Func<Abstractions.IAsyncDropTarget, Task> asyncOperation,
|
||||||
|
Action<Abstractions.IDropTarget> syncOperation,
|
||||||
|
string operationName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (EnableAsyncOperations && target is Abstractions.IAsyncDropTarget asyncTarget)
|
||||||
|
{
|
||||||
|
await ExecuteWithTimeoutAsync(
|
||||||
|
asyncOperation(asyncTarget),
|
||||||
|
$"{operationName}Async",
|
||||||
|
target);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
syncOperation(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _errorCount);
|
||||||
|
HandleError(ex, operationName, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteSourceOperationAsync(
|
||||||
|
Abstractions.IDragSource source,
|
||||||
|
Func<Abstractions.IAsyncDragSource, Task> asyncOperation,
|
||||||
|
Action<Abstractions.IDragSource> syncOperation,
|
||||||
|
string operationName,
|
||||||
|
Enums.DragDropEffects effects = Enums.DragDropEffects.None)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (EnableAsyncOperations && source is Abstractions.IAsyncDragSource asyncSource)
|
||||||
|
{
|
||||||
|
await ExecuteWithTimeoutAsync(
|
||||||
|
asyncOperation(asyncSource),
|
||||||
|
$"{operationName}Async",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
syncOperation(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _errorCount);
|
||||||
|
HandleError(ex, operationName, source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<T> ExecuteWithTimeoutAsync<T>(Task<T> task, string operationName, object? context = null)
|
||||||
|
{
|
||||||
|
if (AsyncOperationTimeout <= 0)
|
||||||
|
return await task;
|
||||||
|
|
||||||
|
var timeoutTask = Task.Delay(AsyncOperationTimeout);
|
||||||
|
var completedTask = await Task.WhenAny(task, timeoutTask);
|
||||||
|
|
||||||
|
if (completedTask == timeoutTask)
|
||||||
|
{
|
||||||
|
throw new TimeoutException($"{operationName} timed out after {AsyncOperationTimeout}ms");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteWithTimeoutAsync(Task task, string operationName, object? context = null)
|
||||||
|
{
|
||||||
|
if (AsyncOperationTimeout <= 0)
|
||||||
|
{
|
||||||
|
await task;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeoutTask = Task.Delay(AsyncOperationTimeout);
|
||||||
|
var completedTask = await Task.WhenAny(task, timeoutTask);
|
||||||
|
|
||||||
|
if (completedTask == timeoutTask)
|
||||||
|
{
|
||||||
|
throw new TimeoutException($"{operationName} timed out after {AsyncOperationTimeout}ms");
|
||||||
|
}
|
||||||
|
|
||||||
|
await task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double CalculateDistance(Geometry.Point p1, Geometry.Point p2)
|
||||||
|
{
|
||||||
|
var dx = p2.X - p1.X;
|
||||||
|
var dy = p2.Y - p1.Y;
|
||||||
|
return Math.Sqrt(dx * dx + dy * dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Reset()
|
||||||
|
{
|
||||||
|
lock (_dragOperationLock)
|
||||||
|
{
|
||||||
|
_currentDragOperation?.Dispose();
|
||||||
|
_currentDragOperation = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CleanupExpiredTargets(object? state)
|
||||||
|
{
|
||||||
|
var expirationTime = DateTime.UtcNow.AddMinutes(-10); // Цели старше 10 минут
|
||||||
|
|
||||||
|
_dropTargetsLock.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var idsToRemove = new List<string>();
|
||||||
|
|
||||||
|
foreach (var kvp in _dropTargets)
|
||||||
|
{
|
||||||
|
if (kvp.Value.LastAccessTime < expirationTime)
|
||||||
|
{
|
||||||
|
idsToRemove.Add(kvp.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var id in idsToRemove)
|
||||||
|
{
|
||||||
|
if (_dropTargets.Remove(id, out var info))
|
||||||
|
{
|
||||||
|
info.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_dropTargetsLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleError(Exception exception, string operation, object? context = null)
|
||||||
|
{
|
||||||
|
_errorOccurred?.Invoke(this, new DragDropErrorEventArgs(exception, operation, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThrowIfDisposed()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
throw new ObjectDisposedException(GetType().Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region IDisposable Implementation
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
|
lock (_dragOperationLock)
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
|
_cleanupTimer?.Dispose();
|
||||||
|
_cleanupTimer = null;
|
||||||
|
|
||||||
|
if (_currentDragOperation != null)
|
||||||
|
{
|
||||||
|
_currentDragOperation.CancellationTokenSource?.Cancel();
|
||||||
|
_currentDragOperation.Dispose();
|
||||||
|
_currentDragOperation = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClearAllDropTargets();
|
||||||
|
|
||||||
|
_dropTargetsLock.Dispose();
|
||||||
|
|
||||||
|
// Очистка событий
|
||||||
|
_dragStarted = null;
|
||||||
|
_dragUpdated = null;
|
||||||
|
_dropTargetChanged = null;
|
||||||
|
_dragCompleted = null;
|
||||||
|
_dragCancelled = null;
|
||||||
|
_errorOccurred = null;
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Lattice.Core.DragDrop.Models;
|
||||||
|
|
||||||
|
namespace Lattice.Core.DragDrop.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Аргументы события отмены перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public class DragCancelledEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Информация о перетаскивании.
|
||||||
|
/// </summary>
|
||||||
|
public DragInfo DragInfo { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="DragCancelledEventArgs"/>.
|
||||||
|
/// </summary>
|
||||||
|
public DragCancelledEventArgs(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
DragInfo = dragInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using Lattice.Core.DragDrop.Models;
|
||||||
|
using Lattice.Core.Geometry;
|
||||||
|
|
||||||
|
namespace Lattice.Core.DragDrop.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Аргументы события завершения перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public class DragCompletedEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Информация о перетаскивании.
|
||||||
|
/// </summary>
|
||||||
|
public DragInfo DragInfo { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Позиция завершения перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public Point DropPosition { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Примененные эффекты перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public Enums.DragDropEffects Effects { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="DragCompletedEventArgs"/>.
|
||||||
|
/// </summary>
|
||||||
|
public DragCompletedEventArgs(DragInfo dragInfo, Point dropPosition, Enums.DragDropEffects effects)
|
||||||
|
{
|
||||||
|
DragInfo = dragInfo;
|
||||||
|
DropPosition = dropPosition;
|
||||||
|
Effects = effects;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace Lattice.Core.DragDrop.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Аргументы события ошибки в операции перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public class DragDropErrorEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Ошибка, которая произошла.
|
||||||
|
/// </summary>
|
||||||
|
public Exception Exception { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Операция, во время которой произошла ошибка.
|
||||||
|
/// </summary>
|
||||||
|
public string Operation { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Контекст операции.
|
||||||
|
/// </summary>
|
||||||
|
public object? Context { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="DragDropErrorEventArgs"/>.
|
||||||
|
/// </summary>
|
||||||
|
public DragDropErrorEventArgs(Exception exception, string operation, object? context = null)
|
||||||
|
{
|
||||||
|
Exception = exception;
|
||||||
|
Operation = operation;
|
||||||
|
Context = context;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Lattice.Core.DragDrop.Models;
|
||||||
|
using Lattice.Core.Geometry;
|
||||||
|
|
||||||
|
namespace Lattice.Core.DragDrop.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Аргументы события начала перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public class DragStartedEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Информация о перетаскивании.
|
||||||
|
/// </summary>
|
||||||
|
public DragInfo DragInfo { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Начальная позиция перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public Point StartPosition { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="DragStartedEventArgs"/>.
|
||||||
|
/// </summary>
|
||||||
|
public DragStartedEventArgs(DragInfo dragInfo, Point startPosition)
|
||||||
|
{
|
||||||
|
DragInfo = dragInfo;
|
||||||
|
StartPosition = startPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Lattice.Core.DragDrop.Models;
|
||||||
|
using Lattice.Core.Geometry;
|
||||||
|
|
||||||
|
namespace Lattice.Core.DragDrop.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Аргументы события обновления перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public class DragUpdatedEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Информация о перетаскивании.
|
||||||
|
/// </summary>
|
||||||
|
public DragInfo DragInfo { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Текущая позиция перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public Point Position { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="DragUpdatedEventArgs"/>.
|
||||||
|
/// </summary>
|
||||||
|
public DragUpdatedEventArgs(DragInfo dragInfo, Point position)
|
||||||
|
{
|
||||||
|
DragInfo = dragInfo;
|
||||||
|
Position = position;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using Lattice.Core.DragDrop.Models;
|
||||||
|
using Lattice.Core.Geometry;
|
||||||
|
|
||||||
|
namespace Lattice.Core.DragDrop.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Аргументы события изменения цели сброса.
|
||||||
|
/// </summary>
|
||||||
|
public class DropTargetChangedEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Информация о перетаскивании.
|
||||||
|
/// </summary>
|
||||||
|
public DragInfo DragInfo { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Новая цель сброса.
|
||||||
|
/// </summary>
|
||||||
|
public Abstractions.IDropTarget Target { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Границы цели.
|
||||||
|
/// </summary>
|
||||||
|
public Rect TargetBounds { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="DropTargetChangedEventArgs"/>.
|
||||||
|
/// </summary>
|
||||||
|
public DropTargetChangedEventArgs(DragInfo dragInfo, Abstractions.IDropTarget target, Rect targetBounds)
|
||||||
|
{
|
||||||
|
DragInfo = dragInfo;
|
||||||
|
Target = target;
|
||||||
|
TargetBounds = targetBounds;
|
||||||
|
}
|
||||||
|
}
|
||||||
174
Lattice.Core.DragDrop/Services/IDragDropService.cs
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
namespace Lattice.Core.DragDrop.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Предоставляет централизованный сервис для управления операциями перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public interface IDragDropService : IDisposable
|
||||||
|
{
|
||||||
|
#region Свойства
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Активна ли операция перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
bool IsDragActive { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Информация о текущей операции.
|
||||||
|
/// </summary>
|
||||||
|
Models.DragInfo? CurrentDragInfo { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Текущая цель сброса.
|
||||||
|
/// </summary>
|
||||||
|
Abstractions.IDropTarget? CurrentDropTarget { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Порог начала перетаскивания в пикселях.
|
||||||
|
/// </summary>
|
||||||
|
double DragStartThreshold { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Включены ли асинхронные операции.
|
||||||
|
/// </summary>
|
||||||
|
bool EnableAsyncOperations { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Максимальное время ожидания асинхронной операции (мс).
|
||||||
|
/// </summary>
|
||||||
|
int AsyncOperationTimeout { get; set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region События
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Событие начала операции перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
event EventHandler<DragStartedEventArgs> DragStarted;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Событие обновления позиции перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
event EventHandler<DragUpdatedEventArgs> DragUpdated;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Событие изменения цели сброса.
|
||||||
|
/// </summary>
|
||||||
|
event EventHandler<DropTargetChangedEventArgs> DropTargetChanged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Событие завершения операции перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
event EventHandler<DragCompletedEventArgs> DragCompleted;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Событие отмены операции перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
event EventHandler<DragCancelledEventArgs> DragCancelled;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Событие ошибки в операции перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
event EventHandler<DragDropErrorEventArgs> ErrorOccurred;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Регистрация целей сброса
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Регистрирует цель сброса.
|
||||||
|
/// </summary>
|
||||||
|
string RegisterDropTarget(Abstractions.IDropTarget target, Geometry.Rect bounds, int priority = 0, string? group = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Обновляет границы цели сброса.
|
||||||
|
/// </summary>
|
||||||
|
bool UpdateDropTargetBounds(string id, Geometry.Rect bounds);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отменяет регистрацию цели сброса.
|
||||||
|
/// </summary>
|
||||||
|
bool UnregisterDropTarget(string id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отменяет регистрацию всех целей в группе.
|
||||||
|
/// </summary>
|
||||||
|
void UnregisterDropTargetsInGroup(string group);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Асинхронные операции
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Начинает операцию перетаскивания (асинхронно).
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> StartDragAsync(Abstractions.IDragSource source, Geometry.Point startPosition);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Обновляет позицию перетаскивания (асинхронно).
|
||||||
|
/// </summary>
|
||||||
|
Task UpdateDragAsync(Geometry.Point position);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Завершает операцию перетаскивания (асинхронно).
|
||||||
|
/// </summary>
|
||||||
|
Task<Enums.DragDropEffects> EndDragAsync(Geometry.Point position);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отменяет операцию перетаскивания (асинхронно).
|
||||||
|
/// </summary>
|
||||||
|
Task CancelDragAsync();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Синхронные операции (для обратной совместимости)
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Начинает операцию перетаскивания (синхронно).
|
||||||
|
/// </summary>
|
||||||
|
bool StartDrag(Abstractions.IDragSource source, Geometry.Point startPosition);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Обновляет позицию перетаскивания (синхронно).
|
||||||
|
/// </summary>
|
||||||
|
void UpdateDrag(Geometry.Point position);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Завершает операцию перетаскивания (синхронно).
|
||||||
|
/// </summary>
|
||||||
|
Enums.DragDropEffects EndDrag(Geometry.Point position);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отменяет операцию перетаскивания (синхронно).
|
||||||
|
/// </summary>
|
||||||
|
void CancelDrag();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Утилиты
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Очищает все зарегистрированные цели.
|
||||||
|
/// </summary>
|
||||||
|
void ClearAllDropTargets();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает статистику использования.
|
||||||
|
/// </summary>
|
||||||
|
DragDropStats GetStats();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Статистика использования Drag & Drop.
|
||||||
|
/// </summary>
|
||||||
|
public class DragDropStats
|
||||||
|
{
|
||||||
|
public int TotalDragOperations { get; set; }
|
||||||
|
public int SuccessfulDrops { get; set; }
|
||||||
|
public int CancelledOperations { get; set; }
|
||||||
|
public int ErrorCount { get; set; }
|
||||||
|
public int RegisteredTargets { get; set; }
|
||||||
|
public TimeSpan AverageOperationTime { get; set; }
|
||||||
|
}
|
||||||
713
Lattice.Core.DragDrop/Utilities/AsyncDragDropUtilities.cs
Normal file
@@ -0,0 +1,713 @@
|
|||||||
|
using Lattice.Core.DragDrop.Abstractions;
|
||||||
|
using Lattice.Core.DragDrop.Enums;
|
||||||
|
using Lattice.Core.DragDrop.Models;
|
||||||
|
|
||||||
|
namespace Lattice.Core.DragDrop.Utilities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Предоставляет утилитарные методы и фабричные методы для работы с системой перетаскивания с поддержкой async.
|
||||||
|
/// </summary>
|
||||||
|
public static class AsyncDragDropUtilities
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Создает асинхронную реализацию источника перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public static IAsyncDragSource CreateAsyncDragSource(
|
||||||
|
Func<Task<object>> dataProviderAsync,
|
||||||
|
Func<Task<bool>>? canDragAsync = null,
|
||||||
|
Func<DragInfo, DragDropEffects, Task>? onCompletedAsync = null,
|
||||||
|
Func<DragInfo, Task>? onCancelledAsync = null)
|
||||||
|
{
|
||||||
|
return new AsyncDragSourceWrapper(dataProviderAsync, canDragAsync, onCompletedAsync, onCancelledAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создает асинхронную реализацию цели сброса.
|
||||||
|
/// </summary>
|
||||||
|
public static IAsyncDropTarget CreateAsyncDropTarget(
|
||||||
|
Func<DropInfo, Task<bool>>? canAcceptAsync = null,
|
||||||
|
Func<DropInfo, Task>? onDragOverAsync = null,
|
||||||
|
Func<DropInfo, Task>? onDropAsync = null,
|
||||||
|
Func<Task>? onDragLeaveAsync = null)
|
||||||
|
{
|
||||||
|
return new AsyncDropTargetWrapper(canAcceptAsync, onDragOverAsync, onDropAsync, onDragLeaveAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создает адаптер для преобразования синхронного источника в асинхронный.
|
||||||
|
/// </summary>
|
||||||
|
public static IAsyncDragSource CreateAsyncAdapter(IDragSource syncSource)
|
||||||
|
{
|
||||||
|
return new SyncToAsyncDragSourceAdapter(syncSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создает адаптер для преобразования синхронной цели в асинхронную.
|
||||||
|
/// </summary>
|
||||||
|
public static IAsyncDropTarget CreateAsyncAdapter(IDropTarget syncTarget)
|
||||||
|
{
|
||||||
|
return new SyncToAsyncDropTargetAdapter(syncTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Обертки-реализации
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Обертка для создания асинхронного источника перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class AsyncDragSourceWrapper : IAsyncDragSource
|
||||||
|
{
|
||||||
|
private readonly Func<Task<object>> _dataProviderAsync;
|
||||||
|
private readonly Func<Task<bool>>? _canDragAsync;
|
||||||
|
private readonly Func<DragInfo, DragDropEffects, Task>? _onCompletedAsync;
|
||||||
|
private readonly Func<DragInfo, Task>? _onCancelledAsync;
|
||||||
|
|
||||||
|
public AsyncDragSourceWrapper(
|
||||||
|
Func<Task<object>> dataProviderAsync,
|
||||||
|
Func<Task<bool>>? canDragAsync = null,
|
||||||
|
Func<DragInfo, DragDropEffects, Task>? onCompletedAsync = null,
|
||||||
|
Func<DragInfo, Task>? onCancelledAsync = null)
|
||||||
|
{
|
||||||
|
_dataProviderAsync = dataProviderAsync ?? throw new ArgumentNullException(nameof(dataProviderAsync));
|
||||||
|
_canDragAsync = canDragAsync;
|
||||||
|
_onCompletedAsync = onCompletedAsync;
|
||||||
|
_onCancelledAsync = onCancelledAsync;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Проверяем, может ли начаться перетаскивание
|
||||||
|
if (_canDragAsync != null)
|
||||||
|
{
|
||||||
|
var canDrag = await _canDragAsync().ConfigureAwait(false);
|
||||||
|
if (!canDrag)
|
||||||
|
return (false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем данные
|
||||||
|
var data = await _dataProviderAsync().ConfigureAwait(false);
|
||||||
|
if (data == null)
|
||||||
|
return (false, null);
|
||||||
|
|
||||||
|
// Создаем информацию о перетаскивании
|
||||||
|
var dragInfo = DragDropUtilities.CreateDragInfo(
|
||||||
|
data,
|
||||||
|
Geometry.Point.Zero,
|
||||||
|
DragDropEffects.Copy | DragDropEffects.Move,
|
||||||
|
this);
|
||||||
|
|
||||||
|
return (true, dragInfo);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return (false, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> StartDragAsync(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
// Базовая реализация всегда разрешает начало
|
||||||
|
return Task.FromResult(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects)
|
||||||
|
{
|
||||||
|
if (_onCompletedAsync != null)
|
||||||
|
{
|
||||||
|
await _onCompletedAsync(dragInfo, effects).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DragCancelledAsync(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
if (_onCancelledAsync != null)
|
||||||
|
{
|
||||||
|
await _onCancelledAsync(dragInfo).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Синхронная реализация (для IDragSource)
|
||||||
|
|
||||||
|
public bool CanStartDrag(out DragInfo? dragInfo)
|
||||||
|
{
|
||||||
|
// Для синхронного вызова используем Task.Result
|
||||||
|
var result = Task.Run(() => CanStartDragAsync()).GetAwaiter().GetResult();
|
||||||
|
dragInfo = result.DragInfo;
|
||||||
|
return result.CanStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool StartDrag(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
return Task.Run(() => StartDragAsync(dragInfo)).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DragCompleted(DragInfo dragInfo, DragDropEffects effects)
|
||||||
|
{
|
||||||
|
Task.Run(() => DragCompletedAsync(dragInfo, effects)).Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DragCancelled(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
Task.Run(() => DragCancelledAsync(dragInfo)).Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Обертка для создания асинхронной цели сброса.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class AsyncDropTargetWrapper : IAsyncDropTarget
|
||||||
|
{
|
||||||
|
private readonly Func<DropInfo, Task<bool>>? _canAcceptAsync;
|
||||||
|
private readonly Func<DropInfo, Task>? _onDragOverAsync;
|
||||||
|
private readonly Func<DropInfo, Task>? _onDropAsync;
|
||||||
|
private readonly Func<Task>? _onDragLeaveAsync;
|
||||||
|
|
||||||
|
public AsyncDropTargetWrapper(
|
||||||
|
Func<DropInfo, Task<bool>>? canAcceptAsync = null,
|
||||||
|
Func<DropInfo, Task>? onDragOverAsync = null,
|
||||||
|
Func<DropInfo, Task>? onDropAsync = null,
|
||||||
|
Func<Task>? onDragLeaveAsync = null)
|
||||||
|
{
|
||||||
|
_canAcceptAsync = canAcceptAsync;
|
||||||
|
_onDragOverAsync = onDragOverAsync;
|
||||||
|
_onDropAsync = onDropAsync;
|
||||||
|
_onDragLeaveAsync = onDragLeaveAsync;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CanAcceptDropAsync(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_canAcceptAsync != null)
|
||||||
|
{
|
||||||
|
return await _canAcceptAsync(dropInfo).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
return true; // По умолчанию принимаем все
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false; // При ошибке не принимаем
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DragOverAsync(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_onDragOverAsync != null)
|
||||||
|
{
|
||||||
|
await _onDragOverAsync(dropInfo).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Игнорируем ошибки в обработчике
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DropAsync(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_onDropAsync != null)
|
||||||
|
{
|
||||||
|
await _onDropAsync(dropInfo).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Игнорируем ошибки в обработчике
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DragLeaveAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_onDragLeaveAsync != null)
|
||||||
|
{
|
||||||
|
await _onDragLeaveAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Игнорируем ошибки в обработчике
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Синхронная реализация (для IDropTarget)
|
||||||
|
|
||||||
|
public bool CanAcceptDrop(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
return Task.Run(() => CanAcceptDropAsync(dropInfo)).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DragOver(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
Task.Run(() => DragOverAsync(dropInfo)).Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Drop(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
Task.Run(() => DropAsync(dropInfo)).Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DragLeave()
|
||||||
|
{
|
||||||
|
Task.Run(DragLeaveAsync).Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Адаптер для преобразования синхронного источника в асинхронный.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class SyncToAsyncDragSourceAdapter : IAsyncDragSource
|
||||||
|
{
|
||||||
|
private readonly IDragSource _syncSource;
|
||||||
|
|
||||||
|
public SyncToAsyncDragSourceAdapter(IDragSource syncSource)
|
||||||
|
{
|
||||||
|
_syncSource = syncSource ?? throw new ArgumentNullException(nameof(syncSource));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
|
||||||
|
{
|
||||||
|
return await Task.Run(() =>
|
||||||
|
{
|
||||||
|
var canStart = _syncSource.CanStartDrag(out var dragInfo);
|
||||||
|
return (canStart, dragInfo);
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> StartDragAsync(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
return await Task.Run(() => _syncSource.StartDrag(dragInfo)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects)
|
||||||
|
{
|
||||||
|
await Task.Run(() => _syncSource.DragCompleted(dragInfo, effects)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DragCancelledAsync(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
await Task.Run(() => _syncSource.DragCancelled(dragInfo)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Синхронная реализация (делегируем синхронному источнику)
|
||||||
|
|
||||||
|
public bool CanStartDrag(out DragInfo? dragInfo)
|
||||||
|
{
|
||||||
|
return _syncSource.CanStartDrag(out dragInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool StartDrag(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
return _syncSource.StartDrag(dragInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DragCompleted(DragInfo dragInfo, DragDropEffects effects)
|
||||||
|
{
|
||||||
|
_syncSource.DragCompleted(dragInfo, effects);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DragCancelled(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
_syncSource.DragCancelled(dragInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Адаптер для преобразования синхронной цели в асинхронную.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class SyncToAsyncDropTargetAdapter : IAsyncDropTarget
|
||||||
|
{
|
||||||
|
private readonly IDropTarget _syncTarget;
|
||||||
|
|
||||||
|
public SyncToAsyncDropTargetAdapter(IDropTarget syncTarget)
|
||||||
|
{
|
||||||
|
_syncTarget = syncTarget ?? throw new ArgumentNullException(nameof(syncTarget));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CanAcceptDropAsync(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
return await Task.Run(() => _syncTarget.CanAcceptDrop(dropInfo)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DragOverAsync(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
await Task.Run(() => _syncTarget.DragOver(dropInfo)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DropAsync(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
await Task.Run(() => _syncTarget.Drop(dropInfo)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DragLeaveAsync()
|
||||||
|
{
|
||||||
|
await Task.Run(() => _syncTarget.DragLeave()).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Синхронная реализация (делегируем синхронной цели)
|
||||||
|
|
||||||
|
public bool CanAcceptDrop(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
return _syncTarget.CanAcceptDrop(dropInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DragOver(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
_syncTarget.DragOver(dropInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Drop(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
_syncTarget.Drop(dropInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DragLeave()
|
||||||
|
{
|
||||||
|
_syncTarget.DragLeave();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Утилитарные методы
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Выполняет асинхронную операцию с таймаутом.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<T> ExecuteWithTimeoutAsync<T>(
|
||||||
|
Task<T> task,
|
||||||
|
TimeSpan timeout,
|
||||||
|
T defaultValue = default!)
|
||||||
|
{
|
||||||
|
if (timeout <= TimeSpan.Zero)
|
||||||
|
return await task.ConfigureAwait(false);
|
||||||
|
|
||||||
|
var delayTask = Task.Delay(timeout);
|
||||||
|
var completedTask = await Task.WhenAny(task, delayTask).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (completedTask == delayTask)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await task.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Выполняет асинхронную операцию с таймаутом.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<bool> ExecuteWithTimeoutAsync(
|
||||||
|
Task task,
|
||||||
|
TimeSpan timeout)
|
||||||
|
{
|
||||||
|
if (timeout <= TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
await task.ConfigureAwait(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var delayTask = Task.Delay(timeout);
|
||||||
|
var completedTask = await Task.WhenAny(task, delayTask).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return completedTask == task;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Выполняет асинхронную операцию с таймаутом и обработкой ошибок.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<T?> ExecuteSafeWithTimeoutAsync<T>(
|
||||||
|
Task<T> task,
|
||||||
|
TimeSpan timeout,
|
||||||
|
Func<Exception, T?> errorHandler = null) where T : class
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (timeout <= TimeSpan.Zero)
|
||||||
|
return await task.ConfigureAwait(false);
|
||||||
|
|
||||||
|
var delayTask = Task.Delay(timeout);
|
||||||
|
var completedTask = await Task.WhenAny(task, delayTask).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (completedTask == delayTask)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await task.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return errorHandler?.Invoke(ex) ?? default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создает комбинированный источник из синхронного и асинхронного.
|
||||||
|
/// </summary>
|
||||||
|
public static IAsyncDragSource Combine(
|
||||||
|
IDragSource syncSource,
|
||||||
|
IAsyncDragSource asyncSource,
|
||||||
|
bool preferAsync = true)
|
||||||
|
{
|
||||||
|
return new CombinedDragSource(syncSource, asyncSource, preferAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создает комбинированную цель из синхронной и асинхронной.
|
||||||
|
/// </summary>
|
||||||
|
public static IAsyncDropTarget Combine(
|
||||||
|
IDropTarget syncTarget,
|
||||||
|
IAsyncDropTarget asyncTarget,
|
||||||
|
bool preferAsync = true)
|
||||||
|
{
|
||||||
|
return new CombinedDropTarget(syncTarget, asyncTarget, preferAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Комбинированные реализации
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Комбинированный источник, поддерживающий как синхронный, так и асинхронный API.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class CombinedDragSource : IAsyncDragSource
|
||||||
|
{
|
||||||
|
private readonly IDragSource _syncSource;
|
||||||
|
private readonly IAsyncDragSource _asyncSource;
|
||||||
|
private readonly bool _preferAsync;
|
||||||
|
|
||||||
|
public CombinedDragSource(IDragSource syncSource, IAsyncDragSource asyncSource, bool preferAsync)
|
||||||
|
{
|
||||||
|
_syncSource = syncSource ?? throw new ArgumentNullException(nameof(syncSource));
|
||||||
|
_asyncSource = asyncSource ?? throw new ArgumentNullException(nameof(asyncSource));
|
||||||
|
_preferAsync = preferAsync;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
|
||||||
|
{
|
||||||
|
if (_preferAsync)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _asyncSource.CanStartDragAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// В случае ошибки пробуем синхронную версию
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Используем синхронную версию в отдельной задаче
|
||||||
|
return await Task.Run(() =>
|
||||||
|
{
|
||||||
|
var canStart = _syncSource.CanStartDrag(out var dragInfo);
|
||||||
|
return (canStart, dragInfo);
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> StartDragAsync(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
if (_preferAsync)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _asyncSource.StartDragAsync(dragInfo).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// В случае ошибки пробуем синхронную версию
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Task.Run(() => _syncSource.StartDrag(dragInfo)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects)
|
||||||
|
{
|
||||||
|
if (_preferAsync)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _asyncSource.DragCompletedAsync(dragInfo, effects).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// В случае ошибки пробуем синхронную версию
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Run(() => _syncSource.DragCompleted(dragInfo, effects)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DragCancelledAsync(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
if (_preferAsync)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _asyncSource.DragCancelledAsync(dragInfo).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// В случае ошибки пробуем синхронную версию
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Run(() => _syncSource.DragCancelled(dragInfo)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Синхронная реализация
|
||||||
|
|
||||||
|
public bool CanStartDrag(out DragInfo? dragInfo)
|
||||||
|
{
|
||||||
|
return _syncSource.CanStartDrag(out dragInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool StartDrag(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
return _syncSource.StartDrag(dragInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DragCompleted(DragInfo dragInfo, DragDropEffects effects)
|
||||||
|
{
|
||||||
|
_syncSource.DragCompleted(dragInfo, effects);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DragCancelled(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
_syncSource.DragCancelled(dragInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Комбинированная цель, поддерживающая как синхронный, так и асинхронный API.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class CombinedDropTarget : IAsyncDropTarget
|
||||||
|
{
|
||||||
|
private readonly IDropTarget _syncTarget;
|
||||||
|
private readonly IAsyncDropTarget _asyncTarget;
|
||||||
|
private readonly bool _preferAsync;
|
||||||
|
|
||||||
|
public CombinedDropTarget(IDropTarget syncTarget, IAsyncDropTarget asyncTarget, bool preferAsync)
|
||||||
|
{
|
||||||
|
_syncTarget = syncTarget ?? throw new ArgumentNullException(nameof(syncTarget));
|
||||||
|
_asyncTarget = asyncTarget ?? throw new ArgumentNullException(nameof(asyncTarget));
|
||||||
|
_preferAsync = preferAsync;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CanAcceptDropAsync(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
if (_preferAsync)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _asyncTarget.CanAcceptDropAsync(dropInfo).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// В случае ошибки пробуем синхронную версию
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Task.Run(() => _syncTarget.CanAcceptDrop(dropInfo)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DragOverAsync(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
if (_preferAsync)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _asyncTarget.DragOverAsync(dropInfo).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// В случае ошибки пробуем синхронную версию
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Run(() => _syncTarget.DragOver(dropInfo)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DropAsync(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
if (_preferAsync)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _asyncTarget.DropAsync(dropInfo).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// В случае ошибки пробуем синхронную версию
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Run(() => _syncTarget.Drop(dropInfo)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DragLeaveAsync()
|
||||||
|
{
|
||||||
|
if (_preferAsync)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _asyncTarget.DragLeaveAsync().ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// В случае ошибки пробуем синхронную версию
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Run(() => _syncTarget.DragLeave()).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Синхронная реализация
|
||||||
|
|
||||||
|
public bool CanAcceptDrop(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
return _syncTarget.CanAcceptDrop(dropInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DragOver(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
_syncTarget.DragOver(dropInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Drop(DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
_syncTarget.Drop(dropInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DragLeave()
|
||||||
|
{
|
||||||
|
_syncTarget.DragLeave();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
275
Lattice.Core.DragDrop/Utilities/DragDropUtilities.cs
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
namespace Lattice.Core.DragDrop.Utilities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Утилиты для работы с системой перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public static class DragDropUtilities
|
||||||
|
{
|
||||||
|
#region Effect Utilities
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Проверяет, совместимы ли эффекты источника и цели.
|
||||||
|
/// </summary>
|
||||||
|
public static bool AreEffectsCompatible(Enums.DragDropEffects sourceEffects, Enums.DragDropEffects targetEffects)
|
||||||
|
{
|
||||||
|
if (sourceEffects == Enums.DragDropEffects.None || targetEffects == Enums.DragDropEffects.None)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return (sourceEffects & targetEffects) != Enums.DragDropEffects.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает наиболее подходящий эффект на основе доступных.
|
||||||
|
/// </summary>
|
||||||
|
public static Enums.DragDropEffects GetBestEffect(Enums.DragDropEffects available, Enums.DragDropEffects preferred)
|
||||||
|
{
|
||||||
|
if ((available & preferred) != Enums.DragDropEffects.None)
|
||||||
|
return available & preferred;
|
||||||
|
|
||||||
|
if ((available & Enums.DragDropEffects.Move) != Enums.DragDropEffects.None)
|
||||||
|
return Enums.DragDropEffects.Move;
|
||||||
|
|
||||||
|
if ((available & Enums.DragDropEffects.Copy) != Enums.DragDropEffects.None)
|
||||||
|
return Enums.DragDropEffects.Copy;
|
||||||
|
|
||||||
|
if ((available & Enums.DragDropEffects.Link) != Enums.DragDropEffects.None)
|
||||||
|
return Enums.DragDropEffects.Link;
|
||||||
|
|
||||||
|
return Enums.DragDropEffects.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Geometry Utilities
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вычисляет расстояние между двумя точками.
|
||||||
|
/// </summary>
|
||||||
|
public static double CalculateDistance(Geometry.Point p1, Geometry.Point p2)
|
||||||
|
{
|
||||||
|
var dx = p2.X - p1.X;
|
||||||
|
var dy = p2.Y - p1.Y;
|
||||||
|
return Math.Sqrt(dx * dx + dy * dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Проверяет, превысило ли перемещение пороговое значение.
|
||||||
|
/// </summary>
|
||||||
|
public static bool HasExceededDragThreshold(Geometry.Point startPoint, Geometry.Point currentPoint, double threshold)
|
||||||
|
{
|
||||||
|
var distance = CalculateDistance(startPoint, currentPoint);
|
||||||
|
return distance >= threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет позицию сброса относительно прямоугольника.
|
||||||
|
/// </summary>
|
||||||
|
public static Enums.DropPosition GetDropPosition(Geometry.Point point, Geometry.Rect bounds, double edgeThreshold = 20.0)
|
||||||
|
{
|
||||||
|
if (!bounds.Contains(new Geometry.Point(point.X, point.Y)))
|
||||||
|
return Enums.DropPosition.Inside;
|
||||||
|
|
||||||
|
var relativeX = (point.X - bounds.X) / bounds.Width;
|
||||||
|
var relativeY = (point.Y - bounds.Y) / bounds.Height;
|
||||||
|
|
||||||
|
if (relativeX < edgeThreshold / bounds.Width)
|
||||||
|
return Enums.DropPosition.Left;
|
||||||
|
if (relativeX > 1 - edgeThreshold / bounds.Width)
|
||||||
|
return Enums.DropPosition.Right;
|
||||||
|
if (relativeY < edgeThreshold / bounds.Height)
|
||||||
|
return Enums.DropPosition.Top;
|
||||||
|
if (relativeY > 1 - edgeThreshold / bounds.Height)
|
||||||
|
return Enums.DropPosition.Bottom;
|
||||||
|
|
||||||
|
return Enums.DropPosition.Center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Factory Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создает информацию о перетаскивании.
|
||||||
|
/// </summary>
|
||||||
|
public static Models.DragInfo CreateDragInfo(
|
||||||
|
object data,
|
||||||
|
Geometry.Point startPosition,
|
||||||
|
Enums.DragDropEffects allowedEffects = Enums.DragDropEffects.Copy | Enums.DragDropEffects.Move,
|
||||||
|
object? source = null,
|
||||||
|
Dictionary<string, object>? parameters = null)
|
||||||
|
{
|
||||||
|
var dragInfo = new Models.DragInfo(data, allowedEffects, startPosition, source);
|
||||||
|
|
||||||
|
if (parameters != null)
|
||||||
|
{
|
||||||
|
foreach (var param in parameters)
|
||||||
|
{
|
||||||
|
dragInfo.SetParameter(param.Key, param.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dragInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создает простую реализацию источника перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public static Abstractions.IDragSource CreateSimpleDragSource(
|
||||||
|
Func<object> dataProvider,
|
||||||
|
Func<bool>? canDrag = null,
|
||||||
|
Action<Models.DragInfo, Enums.DragDropEffects>? onCompleted = null,
|
||||||
|
Action<Models.DragInfo>? onCancelled = null)
|
||||||
|
{
|
||||||
|
return new SimpleDragSource(dataProvider, canDrag, onCompleted, onCancelled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создает простую реализацию цели сброса.
|
||||||
|
/// </summary>
|
||||||
|
public static Abstractions.IDropTarget CreateSimpleDropTarget(
|
||||||
|
Func<Models.DropInfo, bool>? canAccept = null,
|
||||||
|
Action<Models.DropInfo>? onDragOver = null,
|
||||||
|
Action<Models.DropInfo>? onDrop = null,
|
||||||
|
Action? onDragLeave = null)
|
||||||
|
{
|
||||||
|
return new SimpleDropTarget(canAccept, onDragOver, onDrop, onDragLeave);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Data Extraction
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Извлекает данные из элемента с поддержкой различных паттернов.
|
||||||
|
/// </summary>
|
||||||
|
public static object? ExtractData(object? element)
|
||||||
|
{
|
||||||
|
if (element == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Проверяем, реализует ли элемент специальный интерфейс
|
||||||
|
if (element is Abstractions.IDragSource dragSource)
|
||||||
|
{
|
||||||
|
if (dragSource.CanStartDrag(out var dragInfo) && dragInfo != null)
|
||||||
|
return dragInfo.Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// В реальной реализации здесь будет рефлексия для проверки свойств
|
||||||
|
// DataContext, Content и т.д.
|
||||||
|
|
||||||
|
// Возвращаем сам элемент как данные
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Проверяет, совместимы ли данные с указанными типами.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsDataCompatible(object? data, IEnumerable<Type>? acceptedTypes)
|
||||||
|
{
|
||||||
|
if (data == null || acceptedTypes == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var dataType = data.GetType();
|
||||||
|
|
||||||
|
foreach (var acceptedType in acceptedTypes)
|
||||||
|
{
|
||||||
|
if (acceptedType.IsAssignableFrom(dataType))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Helper Classes
|
||||||
|
|
||||||
|
private sealed class SimpleDragSource : Abstractions.IDragSource
|
||||||
|
{
|
||||||
|
private readonly Func<object> _dataProvider;
|
||||||
|
private readonly Func<bool>? _canDrag;
|
||||||
|
private readonly Action<Models.DragInfo, Enums.DragDropEffects>? _onCompleted;
|
||||||
|
private readonly Action<Models.DragInfo>? _onCancelled;
|
||||||
|
|
||||||
|
public SimpleDragSource(
|
||||||
|
Func<object> dataProvider,
|
||||||
|
Func<bool>? canDrag = null,
|
||||||
|
Action<Models.DragInfo, Enums.DragDropEffects>? onCompleted = null,
|
||||||
|
Action<Models.DragInfo>? onCancelled = null)
|
||||||
|
{
|
||||||
|
_dataProvider = dataProvider ?? throw new ArgumentNullException(nameof(dataProvider));
|
||||||
|
_canDrag = canDrag;
|
||||||
|
_onCompleted = onCompleted;
|
||||||
|
_onCancelled = onCancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanStartDrag(out Models.DragInfo? dragInfo)
|
||||||
|
{
|
||||||
|
dragInfo = null;
|
||||||
|
|
||||||
|
if (_canDrag?.Invoke() == false)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var data = _dataProvider();
|
||||||
|
if (data == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
dragInfo = CreateDragInfo(data, Geometry.Point.Zero, Enums.DragDropEffects.Copy | Enums.DragDropEffects.Move, this);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool StartDrag(Models.DragInfo dragInfo) => true;
|
||||||
|
|
||||||
|
public void DragCompleted(Models.DragInfo dragInfo, Enums.DragDropEffects effects)
|
||||||
|
{
|
||||||
|
_onCompleted?.Invoke(dragInfo, effects);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DragCancelled(Models.DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
_onCancelled?.Invoke(dragInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SimpleDropTarget : Abstractions.IDropTarget
|
||||||
|
{
|
||||||
|
private readonly Func<Models.DropInfo, bool>? _canAccept;
|
||||||
|
private readonly Action<Models.DropInfo>? _onDragOver;
|
||||||
|
private readonly Action<Models.DropInfo>? _onDrop;
|
||||||
|
private readonly Action? _onDragLeave;
|
||||||
|
|
||||||
|
public SimpleDropTarget(
|
||||||
|
Func<Models.DropInfo, bool>? canAccept = null,
|
||||||
|
Action<Models.DropInfo>? onDragOver = null,
|
||||||
|
Action<Models.DropInfo>? onDrop = null,
|
||||||
|
Action? onDragLeave = null)
|
||||||
|
{
|
||||||
|
_canAccept = canAccept;
|
||||||
|
_onDragOver = onDragOver;
|
||||||
|
_onDrop = onDrop;
|
||||||
|
_onDragLeave = onDragLeave;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanAcceptDrop(Models.DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
return _canAccept?.Invoke(dropInfo) ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DragOver(Models.DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
_onDragOver?.Invoke(dropInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Drop(Models.DropInfo dropInfo)
|
||||||
|
{
|
||||||
|
_onDrop?.Invoke(dropInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DragLeave()
|
||||||
|
{
|
||||||
|
_onDragLeave?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
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,17 +0,0 @@
|
|||||||
using Lattice.Core.Models;
|
|
||||||
using Lattice.Core.Models.Enums;
|
|
||||||
|
|
||||||
namespace Lattice.Core.Abstractions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Описывает сервис для рассылки уведомлений внутри системы Lattice.
|
|
||||||
/// </summary>
|
|
||||||
public interface INotificationService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Событие, возникающее при отправке нового сообщения.
|
|
||||||
/// </summary>
|
|
||||||
event EventHandler<NotificationEventArgs> NotificationReceived;
|
|
||||||
|
|
||||||
void Show(string message, NotificationSeverity severity = NotificationSeverity.Info, int durationSeconds = 5);
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<IsTrimmable Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsTrimmable>
|
|
||||||
<LangVersion>latest</LangVersion>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
|
|
||||||
<PackageReference Include="System.Text.Json" Version="10.0.1" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,37 +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>
|
|
||||||
/// Код иконки из шрифта Segoe Fluent Icons (например, "\uE102").
|
|
||||||
/// </summary>
|
|
||||||
public string IconKey { get; init; } = "\uE102";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Группа контекста, к которой привязана кнопка (например, "CodeEditor", "Common").
|
|
||||||
/// </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,8 +0,0 @@
|
|||||||
namespace Lattice.Core.Models.Enums;
|
|
||||||
|
|
||||||
public enum NotificationSeverity {
|
|
||||||
Info,
|
|
||||||
Success,
|
|
||||||
Warning,
|
|
||||||
Error,
|
|
||||||
}
|
|
||||||
@@ -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,5 +0,0 @@
|
|||||||
using Lattice.Core.Models.Enums;
|
|
||||||
|
|
||||||
namespace Lattice.Core.Models;
|
|
||||||
|
|
||||||
public record NotificationEventArgs(string Message, NotificationSeverity Severity, int DurationSeconds);
|
|
||||||
@@ -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,54 +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/` — Базовые сущности дерева (узлы, направления, ориентация).
|
|
||||||
* `Services/` - Сервисы управления интерфейсом
|
|
||||||
* `ContextService` - Сервис управления контекстом приложения.
|
|
||||||
* `LayoutService` - Сервис управления макетом.
|
|
||||||
* `NotificationService` - Сервис уведомлений.
|
|
||||||
* `Persistence/` — Логика сохранения макета в JSON.
|
|
||||||
|
|
||||||
## 🛠 Использование
|
|
||||||
|
|
||||||
### Создание базового макета
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
var layoutService = new LayoutService();
|
|
||||||
|
|
||||||
// Создаем контентные узлы
|
|
||||||
var explorer = new ContentNode(new MyToolComponent("Solution Explorer", "Explorer"));
|
|
||||||
var editor = new ContentNode(new MyDocumentComponent("Main.cs", "CodeEditor"));
|
|
||||||
|
|
||||||
// Устанавливаем редактор как корень
|
|
||||||
layoutService.SetRoot(editor);
|
|
||||||
|
|
||||||
// Прикрепляем проводник слева от редактора
|
|
||||||
layoutService.Dock(explorer, editor, DockDirection.Left);
|
|
||||||
|
|
||||||
//Переключение контекста
|
|
||||||
var contextService = new ContextService();
|
|
||||||
|
|
||||||
// Вызывается при активации вкладки в UI
|
|
||||||
contextService.SetContext("CodeEditor");
|
|
||||||
|
|
||||||
// Проверка видимости команд в текущем контексте
|
|
||||||
bool isDebugVisible = contextService.IsCommandVisible("btnDebug", "CodeEditor");
|
|
||||||
```
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
using Lattice.Core.Abstractions;
|
|
||||||
|
|
||||||
namespace Lattice.Core.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Реализация сервиса управления контекстом приложения.
|
|
||||||
/// </summary>
|
|
||||||
public class ContextService : 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.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Реализация сервиса управления макетом.
|
|
||||||
/// </summary>
|
|
||||||
public class LayoutService : ILayoutService
|
|
||||||
{
|
|
||||||
private readonly ILogger? _logger;
|
|
||||||
private LayoutNode? _root;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public LayoutNode? Root => _root;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event EventHandler? LayoutUpdated;
|
|
||||||
|
|
||||||
public LayoutService(ILogger<LayoutService>? 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,19 +0,0 @@
|
|||||||
using Lattice.Core.Abstractions;
|
|
||||||
using Lattice.Core.Models;
|
|
||||||
using Lattice.Core.Models.Enums;
|
|
||||||
|
|
||||||
namespace Lattice.Core.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Простая реализация сервиса уведомлений.
|
|
||||||
/// Хранит только событие и вызывает его при Show().
|
|
||||||
/// </summary>
|
|
||||||
public sealed class NotificationService : INotificationService
|
|
||||||
{
|
|
||||||
public event EventHandler<NotificationEventArgs>? NotificationReceived;
|
|
||||||
|
|
||||||
public void Show(string message, NotificationSeverity severity = NotificationSeverity.Info, int durationSeconds = 5)
|
|
||||||
{
|
|
||||||
NotificationReceived?.Invoke(this, new NotificationEventArgs(message, severity, durationSeconds));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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>
|
||||||
28
Lattice.IDE/Controls/EditorView.xaml.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||||
|
using Microsoft.UI.Xaml.Data;
|
||||||
|
using Microsoft.UI.Xaml.Input;
|
||||||
|
using Microsoft.UI.Xaml.Media;
|
||||||
|
using Microsoft.UI.Xaml.Navigation;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices.WindowsRuntime;
|
||||||
|
using Windows.Foundation;
|
||||||
|
using Windows.Foundation.Collections;
|
||||||
|
|
||||||
|
// 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>
|
||||||
28
Lattice.IDE/Controls/SolutionExplorerView.xaml.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||||
|
using Microsoft.UI.Xaml.Data;
|
||||||
|
using Microsoft.UI.Xaml.Input;
|
||||||
|
using Microsoft.UI.Xaml.Media;
|
||||||
|
using Microsoft.UI.Xaml.Navigation;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices.WindowsRuntime;
|
||||||
|
using Windows.Foundation;
|
||||||
|
using Windows.Foundation.Collections;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||