Compare commits

...

2 Commits

Author SHA1 Message Date
584df249f6 Доработан Docking 2026-01-27 06:07:15 +03:00
33abd94f6e Добавлен example 2026-01-25 07:56:35 +03:00
106 changed files with 2500 additions and 11971 deletions

View File

@@ -1,8 +1,23 @@
namespace Lattice.Core.Docking.Abstractions;
/// <summary>
/// Определяет контракт для команды в системе докинга.
/// Команды представляют действия, которые могут быть выполнены над элементами док-системы.
/// </summary>
public interface IDockCommand : System.Windows.Input.ICommand
{
/// <summary>
/// Получает отображаемое имя команды.
/// </summary>
string Name { get; }
/// <summary>
/// Получает идентификатор ресурса для иконки команды.
/// </summary>
string Icon { get; }
/// <summary>
/// Получает текстовое представление жеста (горячей клавиши) для команды.
/// </summary>
string GestureText { get; }
}
}

View File

@@ -3,22 +3,36 @@
namespace Lattice.Core.Docking.Abstractions;
/// <summary>
/// Интерфейс для элементов (листьев дерева), которые физически содержат внутри себя коллекцию вкладок.
/// Определяет контракт для контейнеров, содержащих коллекцию вкладок.
/// Контейнеры являются листьями дерева компоновки и непосредственно отображают содержимое.
/// </summary>
public interface IDockContainer : IDockElement
{
/// <summary> Список вкладок, находящихся в данном контейнере. </summary>
/// <summary>
/// Получает список вкладок, находящихся в данном контейнере.
/// </summary>
IList<IDockContent> Children { get; }
/// <summary> Ссылка на текущую выбранную и отображаемую вкладку. </summary>
/// <summary>
/// Получает или задает текущую активную (выбранную) вкладку.
/// </summary>
IDockContent? ActiveContent { get; set; }
/// <summary> Добавляет контент в контейнер и делает его активным. </summary>
/// <summary>
/// Добавляет контент в контейнер и делает его активным.
/// </summary>
/// <param name="content">Контент для добавления.</param>
void AddContent(IDockContent content);
/// <summary> Удаляет контент. Если Children становится пустым, контейнер может быть удален из дерева макета. </summary>
/// <summary>
/// Удаляет контент из контейнера. Если коллекция становится пустой,
/// контейнер может быть удален из дерева макета.
/// </summary>
/// <param name="content">Контент для удаления.</param>
void RemoveContent(IDockContent content);
/// <summary> Положение вкладок в интерфейсе. </summary>
/// <summary>
/// Получает или задает положение панели вкладок в интерфейсе.
/// </summary>
TabPlacement TabPlacement { get; set; }
}
}

View File

@@ -1,25 +1,37 @@
namespace Lattice.Core.Docking.Abstractions;
/// <summary>
/// Описывает объект содержимого (вкладку), который может быть размещен внутри IDockContainer.
/// Определяет контракт для содержимого (вкладки), которое может быть размещено внутри контейнера.
/// </summary>
public interface IDockContent
{
/// <summary> Уникальный идентификатор контента (например, путь к файлу или ID инструмента). </summary>
/// <summary>
/// Получает уникальный идентификатор контента.
/// Используется для идентификации вкладки в системе.
/// </summary>
string Id { get; }
/// <summary> Заголовок, отображаемый пользователю в интерфейсе (на вкладке). </summary>
/// <summary>
/// Получает заголовок, отображаемый пользователю на вкладке.
/// </summary>
string Title { get; }
/// <summary>
/// Сам визуальный элемент (например, Microsoft.UI.Xaml.UIElement).
/// Lattice просто отображает этот объект в теле вкладки.
/// <summary>
/// Получает или задает визуальный элемент для отображения в теле вкладки.
/// </summary>
object View { get; set; }
/// <summary> Флаг, определяющий доступность кнопки закрытия для пользователя. </summary>
/// <summary>
/// Получает значение, указывающее, можно ли закрыть вкладку.
/// </summary>
bool CanClose { get; }
/// <summary> Вызывается системой при попытке закрытия контента. Возвращает true, если закрытие разрешено. </summary>
/// <summary>
/// Вызывается системой при попытке закрытия контента.
/// Позволяет выполнить дополнительные проверки или сохранить состояние.
/// </summary>
/// <returns>
/// true, если закрытие разрешено; в противном случае false.
/// </returns>
bool OnClosing();
}
}

View File

@@ -1,25 +1,91 @@
namespace Lattice.Core.Docking.Abstractions;
/// <summary>
/// Базовый интерфейс для любого элемента, который может быть частью дерева компоновки Lattice.
/// Базовый интерфейс для любого элемента, являющегося частью дерева компоновки.
/// Определяет общие свойства и методы для всех элементов док-системы.
/// </summary>
/// <remarks>
/// Элементы док-системы образуют древовидную структуру, где каждый элемент может иметь
/// родителя и дочерние элементы. Эта иерархия используется для организации пространства
/// главного окна и плавающих окон в IDE-подобных приложениях.
/// </remarks>
public interface IDockElement
{
/// <summary> Уникальный идентификатор элемента. </summary>
/// <summary>
/// Получает уникальный идентификатор элемента.
/// Используется для поиска элементов, сериализации состояния и отслеживания изменений.
/// </summary>
/// <value>
/// Строковый идентификатор, гарантированно уникальный в пределах дерева компоновки.
/// Обычно представляет собой GUID в строковом формате.
/// </value>
string Id { get; }
/// <summary> Родительский элемент в иерархии. Если null — элемент является корневым. </summary>
/// <summary>
/// Получает или задает родительский элемент в иерархии дерева компоновки.
/// </summary>
/// <value>
/// Родительский элемент или null, если элемент является корневым.
/// Это свойство управляется системой компоновки при добавлении или удалении элементов.
/// </value>
/// <remarks>
/// Изменение этого свойства вручную может привести к нарушению целостности дерева.
/// Для манипуляции структурой дерева следует использовать методы <see cref="DockOperations"/>.
/// </remarks>
IDockElement? Parent { get; set; }
/// <summary> Желаемая ширина элемента в относительных или абсолютных единицах. </summary>
/// <summary>
/// Получает или задает желаемую ширину элемента.
/// </summary>
/// <value>
/// Ширина элемента в пикселях или относительных единицах.
/// Может быть выражена как абсолютное значение (в пикселях) или как пропорция
/// (например, 0.5 для 50% доступного пространства).
/// </value>
/// <remarks>
/// Фактическая ширина элемента определяется родительским контейнером с учетом
/// минимальных размеров и соотношений разделения.
/// </remarks>
double Width { get; set; }
/// <summary> Желаемая высота элемента в относительных или абсолютных единицах. </summary>
/// <summary>
/// Получает или задает желаемую высоту элемента.
/// </summary>
/// <value>
/// Высота элемента в пикселях или относительных единицах.
/// Может быть выражена как абсолютное значение (в пикселях) или как пропорция.
/// </value>
/// <remarks>
/// Фактическая высота элемента определяется родительским контейнером с учетом
/// минимальных размеров и соотношений разделения.
/// </remarks>
double Height { get; set; }
/// <summary> Минимально допустимая ширина, при которой элемент сохраняет функциональность. </summary>
/// <summary>
/// Получает минимально допустимую ширину элемента.
/// </summary>
/// <value>
/// Минимальная ширина элемента в пикселях, при которой элемент сохраняет
/// базовую функциональность и читаемость содержимого.
/// </value>
/// <remarks>
/// Система компоновки не позволит уменьшить элемент ниже этого значения.
/// Для групп разделения минимальная ширина вычисляется рекурсивно на основе
/// минимальных размеров дочерних элементов.
/// </remarks>
double MinWidth { get; }
/// <summary> Минимально допустимая высота, при которой элемент сохраняет функциональность. </summary>
/// <summary>
/// Получает минимально допустимую высоту элемента.
/// </summary>
/// <value>
/// Минимальная высота элемента в пикселях, при которой элемент сохраняет
/// базовую функциональность и читаемость содержимого.
/// </value>
/// <remarks>
/// Система компоновки не позволит уменьшить элемент ниже этого значения.
/// Для групп разделения минимальная высота вычисляется рекурсивно на основе
/// минимальных размеров дочерних элементов.
/// </remarks>
double MinHeight { get; }
}
}

View File

@@ -1,19 +0,0 @@
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; }
}

View File

@@ -1,19 +0,0 @@
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; }
}

View File

@@ -1,76 +0,0 @@
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;
}
}

View File

@@ -4,20 +4,30 @@ using Lattice.Core.Docking.Models;
namespace Lattice.Core.Docking.Engine;
/// <summary>
/// Статический движок для манипуляции иерархией дерева компоновки.
/// Содержит чистые алгоритмы трансформации графа.
/// Предоставляет статические методы для манипуляции иерархией дерева компоновки.
/// Содержит чистые алгоритмы трансформации графа без зависимости от UI.
/// </summary>
public static class DockOperations
{
/// <summary>
/// Извлекает элемент из дерева. Если родительская группа остается с одним ребенком,
/// она удаляется, а ребенок занимает её место.
/// Извлекает элемент из дерева компоновки.
/// Если родительская группа остается с одним ребенком, она удаляется,
/// а оставшийся ребенок занимает её место в иерархии.
/// </summary>
/// <param name="element">Элемент для удаления.</param>
/// <param name="root">Текущий корень дерева.</param>
/// <returns>Новый корень дерева после оптимизации.</returns>
/// <param name="element">Элемент для удаления из дерева.</param>
/// <param name="root">Текущий корневой элемент дерева.</param>
/// <returns>
/// Новый корневой элемент дерева после удаления и оптимизации структуры.
/// Возвращает null, если дерево становится пустым.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="root"/> равен null.
/// </exception>
public static IDockElement? Remove(IDockElement element, IDockElement root)
{
if (element == null) throw new ArgumentNullException(nameof(element));
if (root == null) throw new ArgumentNullException(nameof(root));
if (element == root) return null;
var parent = element.Parent as DockGroup;
@@ -43,15 +53,36 @@ public static class DockOperations
}
/// <summary>
/// Вставляет элемент в дерево, создавая новую группу разделения или объединяя контент.
/// Вставляет элемент в дерево компоновки относительно целевого элемента.
/// Создает новую группу разделения или объединяет контент в зависимости от позиции.
/// </summary>
public static IDockElement Insert(IDockElement target, IDockElement source, DockPosition pos, IDockElement root)
/// <param name="target">Целевой элемент, относительно которого выполняется вставка.</param>
/// <param name="source">Вставляемый элемент.</param>
/// <param name="pos">Позиция вставки относительно целевого элемента.</param>
/// <param name="root">Текущий корневой элемент дерева.</param>
/// <returns>
/// Новый корневой элемент дерева после вставки и оптимизации структуры.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="target"/>, <paramref name="source"/>
/// или <paramref name="root"/> равны null.
/// </exception>
/// <exception cref="ArgumentException">
/// Выбрасывается, когда <paramref name="pos"/> имеет недопустимое значение.
/// </exception>
public static IDockElement Insert(IDockElement target, IDockElement source,
DockPosition pos, IDockElement root)
{
if (target == null) throw new ArgumentNullException(nameof(target));
if (source == null) throw new ArgumentNullException(nameof(source));
if (root == null) throw new ArgumentNullException(nameof(root));
// Случай 1: Объединение вкладок в центре
if (pos == DockPosition.Center)
{
if (target is IDockContainer targetContainer && source is IDockContainer sourceContainer)
{
// Переносим все вкладки из источника в целевой контейнер
var items = new List<IDockContent>(sourceContainer.Children);
foreach (var item in items)
{

View File

@@ -8,54 +8,87 @@ using System.Runtime.CompilerServices;
namespace Lattice.Core.Docking.Engine;
/// <summary>
/// Расширенный менеджер макета, поддерживающий автоскрываемые панели, группы документов
/// и расширенные операции управления макетом.
/// Центральный менеджер макета, управляющий всей структурой док-системы.
/// Координирует дерево компоновки, плавающие окна, автоскрываемые панели
/// и предоставляет API для манипуляции макетом.
/// </summary>
/// <remarks>
/// Этот класс является центральным координатором всей док-системы, управляя деревом компоновки,
/// плавающими окнами, автоскрываемыми панелями и предоставляя API для манипуляции макетом.
/// Этот класс является основным координатором док-системы. Он управляет:
/// <list type="bullet">
/// <item>Деревом компоновки главного окна</item>
/// <item>Коллекцией плавающих окон</item>
/// <item>Коллекцией автоскрываемых панелей</item>
/// <item>Реестром типов контента</item>
/// </list>
/// Все изменения в структуре макета должны выполняться через методы этого класса.
/// </remarks>
public class LayoutManager
{
private readonly ObservableCollection<AutoHidePanel> _autoHidePanels = new();
private IDockElement? _root;
/// <summary>
/// Корневой элемент главного окна IDE.
/// Получает или задает корневой элемент дерева компоновки главного окна.
/// </summary>
public IDockElement? Root { get; internal set; }
/// <value>
/// Корневой элемент или null, если макет пуст.
/// </value>
/// <remarks>
/// При изменении этого свойства генерируется событие <see cref="LayoutUpdated"/>.
/// </remarks>
public IDockElement? Root
{
get => _root;
internal set
{
if (_root != value)
{
_root = value;
LayoutUpdated?.Invoke();
}
}
}
/// <summary>
/// Список активных плавающих окон.
/// Получает список активных плавающих окон.
/// </summary>
/// <value>
/// Коллекция объектов <see cref="DockWindow"/>, представляющих плавающие окна.
/// </value>
public List<DockWindow> FloatingWindows { get; } = new();
/// <summary>
/// Коллекция автоскрываемых панелей.
/// Получает коллекцию автоскрываемых панелей.
/// </summary>
/// <value>
/// Доступная только для чтения коллекция объектов <see cref="AutoHidePanel"/>.
/// </value>
public ReadOnlyObservableCollection<AutoHidePanel> AutoHidePanels { get; }
/// <summary>
/// Реестр типов контента (опционально).
/// Получает или задает реестр типов контента.
/// </summary>
/// <value>
/// Реестр типов контента или null, если реестр не установлен.
/// </value>
public Services.ContentRegistry? ContentRegistry { get; set; }
/// <summary>
/// Уведомляет UI, что структура дерева изменилась.
/// Происходит при изменении структуры дерева компоновки.
/// </summary>
/// <remarks>
/// Событие генерируется при любых изменениях в дереве компоновки,
/// включая добавление, удаление или перемещение элементов.
/// </remarks>
public event Action? LayoutUpdated;
/// <summary>
/// Уведомляет об изменении в коллекции автоскрываемых панелей.
/// Происходит при изменении коллекции автоскрываемых панелей.
/// </summary>
public event EventHandler? AutoHidePanelsChanged;
/// <summary>
/// Событие, возникающее при операции перетаскивания элемента.
/// </summary>
public event EventHandler<DragDropEventArgs>? DragDropOperation;
/// <summary>
/// Инициализирует новый экземпляр менеджера макета.
/// Инициализирует новый экземпляр класса <see cref="LayoutManager"/>.
/// </summary>
public LayoutManager()
{
@@ -63,13 +96,20 @@ public class LayoutManager
}
/// <summary>
/// Добавляет автоскрываемую панель.
/// Добавляет автоскрываемую панель с указанным содержимым к заданной стороне окна.
/// </summary>
/// <param name="content">Содержимое панели.</param>
/// <param name="side">Сторона для прикрепления.</param>
/// <returns>Созданная автоскрываемая панель.</returns>
/// <param name="side">Сторона окна для прикрепления панели.</param>
/// <returns>
/// Созданная автоскрываемая панель.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="content"/> равен null.
/// </exception>
public AutoHidePanel AddAutoHidePanel(IDockContent content, DockSide side)
{
if (content == null) throw new ArgumentNullException(nameof(content));
var panel = new AutoHidePanel(content, side);
_autoHidePanels.Add(panel);
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
@@ -77,23 +117,36 @@ public class LayoutManager
}
/// <summary>
/// Удаляет автоскрываемую панель.
/// Удаляет автоскрываемую панель из коллекции.
/// </summary>
/// <param name="panel">Панель для удаления.</param>
public void RemoveAutoHidePanel(AutoHidePanel panel)
/// <returns>
/// true, если панель была успешно удалена; в противном случае false.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="panel"/> равен null.
/// </exception>
public bool RemoveAutoHidePanel(AutoHidePanel panel)
{
if (panel == null) throw new ArgumentNullException(nameof(panel));
if (_autoHidePanels.Remove(panel))
{
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
return true;
}
return false;
}
/// <summary>
/// Создает документ из зарегистрированного типа контента.
/// Создает документ указанного типа контента с заданным идентификатором.
/// </summary>
/// <param name="contentTypeId">Идентификатор типа контента.</param>
/// <param name="id">Уникальный идентификатор документа.</param>
/// <returns>Созданный контент или null, если ContentRegistry не установлен.</returns>
/// <returns>
/// Созданный контент или null, если ContentRegistry не установлен
/// или тип контента не зарегистрирован.
/// </returns>
public IDockContent? CreateDocument(string contentTypeId, string id)
{
if (ContentRegistry == null || !ContentRegistry.IsRegistered(contentTypeId))
@@ -103,16 +156,31 @@ public class LayoutManager
}
/// <summary>
/// Основной метод перемещения элементов в макете.
/// Выполняет перемещение элемента в макете относительно целевого элемента.
/// </summary>
/// <param name="source">Что перетаскиваем.</param>
/// <param name="target">Куда приземляем.</param>
/// <param name="position">Позиция относительно цели.</param>
/// <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)
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="source"/> равен null.
/// </exception>
/// <remarks>
/// Метод выполняет следующие шаги:
/// <list type="number">
/// <item>Удаляет источник из текущего местоположения</item>
/// <item>Вставляет источник в новое местоположение относительно цели</item>
/// <item>Обновляет структуру дерева компоновки</item>
/// </list>
/// Если <paramref name="target"/> равен null, элемент помещается в новое плавающее окно.
/// </remarks>
public void Move(IDockElement source, IDockElement? target,
DockPosition position, bool asDocument = false)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (source == target) return;
// 1. Удаляем источник из текущего местоположения
@@ -145,13 +213,14 @@ public class LayoutManager
// 2. Вставляем в цель
if (target == null)
{
FloatingWindows.Add(new DockWindow { Root = source as IDockElement });
// Создаем новое плавающее окно
FloatingWindows.Add(new DockWindow { Root = source });
}
else
{
if (IsDescendantOf(target, Root))
if (Root != null && IsDescendantOf(target, Root))
{
Root = DockOperations.Insert(target, source, position, Root!);
Root = DockOperations.Insert(target, source, position, Root);
}
else
{
@@ -162,6 +231,13 @@ public class LayoutManager
LayoutUpdated?.Invoke();
}
/// <summary>
/// Удаляет элемент из всех плавающих окон.
/// </summary>
/// <param name="element">Элемент для удаления.</param>
/// <returns>
/// true, если элемент был найден и удален; в противном случае false.
/// </returns>
private bool RemoveFromFloatingWindows(IDockElement element)
{
foreach (var win in FloatingWindows.ToArray())
@@ -177,7 +253,14 @@ public class LayoutManager
return false;
}
private void InsertIntoFloatingWindow(IDockElement target, IDockElement source, DockPosition position)
/// <summary>
/// Вставляет элемент в плавающее окно, содержащее целевой элемент.
/// </summary>
/// <param name="target">Целевой элемент в плавающем окне.</param>
/// <param name="source">Вставляемый элемент.</param>
/// <param name="position">Позиция вставки.</param>
private void InsertIntoFloatingWindow(IDockElement target, IDockElement source,
DockPosition position)
{
foreach (var win in FloatingWindows)
{
@@ -189,6 +272,14 @@ public class LayoutManager
}
}
/// <summary>
/// Определяет, является ли элемент потомком указанного предка.
/// </summary>
/// <param name="element">Проверяемый элемент.</param>
/// <param name="ancestor">Предполагаемый предок.</param>
/// <returns>
/// true, если элемент является потомком предка; в противном случае false.
/// </returns>
private bool IsDescendantOf(IDockElement element, IDockElement ancestor)
{
if (element == ancestor) return true;
@@ -197,9 +288,17 @@ public class LayoutManager
return false;
}
/// <summary> Поиск элемента по ID во всех окнах. </summary>
/// <summary>
/// Находит элемент по его идентификатору во всех окнах (главном и плавающих).
/// </summary>
/// <param name="id">Идентификатор элемента для поиска.</param>
/// <returns>
/// Найденный элемент или null, если элемент с таким идентификатором не найден.
/// </returns>
public IDockElement? FindById(string id)
{
if (string.IsNullOrEmpty(id)) return null;
var found = FindRecursive(Root, id);
if (found != null) return found;
@@ -211,16 +310,37 @@ public class LayoutManager
return null;
}
/// <summary>
/// Рекурсивно ищет элемент по идентификатору в поддереве.
/// </summary>
/// <param name="node">Корневой узел поддерева для поиска.</param>
/// <param name="id">Идентификатор элемента для поиска.</param>
/// <returns>
/// Найденный элемент или null, если элемент не найден.
/// </returns>
private IDockElement? FindRecursive(IDockElement? node, string id)
{
if (node == null || node.Id == id) return node;
if (node is DockGroup g) return FindRecursive(g.First, id) ?? FindRecursive(g.Second, id);
if (node == null) return null;
if (node.Id == id) return node;
if (node is DockGroup g)
return FindRecursive(g.First, id) ?? FindRecursive(g.Second, id);
return null;
}
/// <summary>
/// Сбрасывает макет к состоянию по умолчанию.
/// </summary>
/// <remarks>
/// Метод выполняет следующие действия:
/// <list type="bullet">
/// <item>Очищает корневой элемент</item>
/// <item>Закрывает все плавающие окна</item>
/// <item>Удаляет все автоскрываемые панели</item>
/// <item>Генерирует соответствующие события</item>
/// </list>
/// </remarks>
public void Reset()
{
Root = null;
@@ -231,86 +351,12 @@ public class LayoutManager
}
/// <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>
/// <param name="id">Идентификатор элемента для поиска.</param>
/// <returns>
/// Найденный элемент или null, если элемент с таким идентификатором не найден.
/// </returns>
public IDockElement? FindElementById(string id)
{
return FindElementByIdRecursive(Root, id) ??
@@ -318,6 +364,14 @@ public class LayoutManager
.FirstOrDefault(result => result != null);
}
/// <summary>
/// Рекурсивно ищет элемент по идентификатору в поддереве.
/// </summary>
/// <param name="element">Корневой элемент поддерева для поиска.</param>
/// <param name="id">Идентификатор элемента для поиска.</param>
/// <returns>
/// Найденный элемент или null, если элемент не найден.
/// </returns>
private IDockElement? FindElementByIdRecursive(IDockElement? element, string id)
{
if (element == null) return null;
@@ -331,39 +385,4 @@ public class LayoutManager
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;
}
}

View File

@@ -8,6 +8,5 @@
<ItemGroup>
<ProjectReference Include="..\Lattice.Core.Geometry\Lattice.Core.Geometry.csproj" />
<ProjectReference Include="..\Lattice.Core.DragDrop\Lattice.Core.DragDrop.csproj" />
</ItemGroup>
</Project>

View File

@@ -5,44 +5,81 @@ namespace Lattice.Core.Docking.Models;
/// <summary>
/// Представляет автоскрываемую панель, которая может быть прикреплена к одной из сторон окна.
/// Автоскрываемые панели скрываются, оставляя только заголовок, и появляются при наведении курсора.
/// Автоскрываемые панели скрываются, оставляя видимой только полоску-заголовок,
/// и разворачиваются при наведении курсора или клике.
/// </summary>
/// <remarks>
/// Автоскрываемые панели являются ключевым элементом интерфейса современных IDE,
/// Автоскрываемые панели являются важным элементом современных IDE-подобных приложений,
/// позволяя экономить пространство экрана при сохранении быстрого доступа к инструментам.
/// </remarks>
public class AutoHidePanel : INotifyPropertyChanged
{
/// <summary>
/// Происходит при изменении значения свойства.
/// </summary>
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>
/// <value>
/// Строковый идентификатор, сгенерированный с помощью GUID.
/// </value>
public string Id { get; } = Guid.NewGuid().ToString();
/// <summary>
/// Содержимое панели.
/// Получает или задает содержимое панели.
/// </summary>
public Abstractions.IDockContent Content { get; set; }
/// <value>
/// Объект, реализующий <see cref="Abstractions.IDockContent"/>.
/// </value>
/// <exception cref="ArgumentNullException">
/// Выбрасывается при попытке установить значение null.
/// </exception>
public Abstractions.IDockContent Content
{
get => _content;
set
{
if (_content != value)
{
_content = value ?? throw new ArgumentNullException(nameof(value));
OnPropertyChanged();
OnPropertyChanged(nameof(Title));
}
}
}
private Abstractions.IDockContent _content;
/// <summary>
/// Сторона окна, к которой прикреплена панель.
/// Получает или задает сторону окна, к которой прикреплена панель.
/// </summary>
/// <value>
/// Значение перечисления <see cref="DockSide"/>, указывающее сторону прикрепления.
/// </value>
public DockSide Side { get; set; }
/// <summary>
/// Ширина панели (для левой/правой сторон) или высота (для верхней/нижней сторон).
/// Получает или задает ширину панели (для левой/правой сторон)
/// или высоту (для верхней/нижней сторон).
/// </summary>
/// <value>
/// Размер панели в пикселях. Значение по умолчанию: 300.
/// </value>
public double Size { get; set; } = 300;
/// <summary>
/// Признак видимости панели.
/// Получает или задает признак видимости панели.
/// </summary>
/// <value>
/// true, если панель развернута и видима; в противном случае false.
/// </value>
/// <remarks>
/// При изменении этого свойства генерируется событие <see cref="PropertyChanged"/>.
/// </remarks>
public bool IsVisible
{
get => _isVisible;
@@ -57,8 +94,14 @@ public class AutoHidePanel : INotifyPropertyChanged
}
/// <summary>
/// Смещение для анимации выезда/заезда панели (0-1).
/// Получает или задает смещение для анимации выезда/заезда панели.
/// </summary>
/// <value>
/// Значение от 0.0 до 1.0, где 0.0 - полностью скрыта, 1.0 - полностью развернута.
/// </value>
/// <remarks>
/// Используется для плавной анимации отображения/скрытия панели.
/// </remarks>
public double SlideOffset
{
get => _slideOffset;
@@ -73,15 +116,22 @@ public class AutoHidePanel : INotifyPropertyChanged
}
/// <summary>
/// Заголовок панели (обычно берется из содержимого).
/// Получает заголовок панели.
/// </summary>
/// <value>
/// Заголовок, взятый из содержимого панели.
/// Если содержимое не установлено, возвращает "Auto-hide Panel".
/// </value>
public string Title => Content?.Title ?? "Auto-hide Panel";
/// <summary>
/// Инициализирует новый экземпляр автоскрываемой панели.
/// Инициализирует новый экземпляр класса <see cref="AutoHidePanel"/>.
/// </summary>
/// <param name="content">Содержимое панели.</param>
/// <param name="side">Сторона окна для прикрепления.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="content"/> равен null.
/// </exception>
public AutoHidePanel(Abstractions.IDockContent content, DockSide side)
{
Content = content ?? throw new ArgumentNullException(nameof(content));
@@ -91,6 +141,9 @@ public class AutoHidePanel : INotifyPropertyChanged
/// <summary>
/// Переключает видимость панели.
/// </summary>
/// <remarks>
/// Если панель была видимой, становится скрытой, и наоборот.
/// </remarks>
public void Toggle()
{
IsVisible = !IsVisible;
@@ -111,4 +164,15 @@ public class AutoHidePanel : INotifyPropertyChanged
{
IsVisible = false;
}
}
/// <summary>
/// Вызывает событие <see cref="PropertyChanged"/>.
/// </summary>
/// <param name="name">
/// Имя изменившегося свойства. Если не указано, определяется автоматически.
/// </param>
protected void OnPropertyChanged([CallerMemberName] string? name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}

View File

@@ -1,8 +1,4 @@
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;
@@ -14,32 +10,25 @@ namespace Lattice.Core.Docking.Models;
/// элементом для создания сложных макетов с разделителями.
/// </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>
/// Каждая группа содержит два дочерних элемента (<see cref="First"/> и <see cref="Second"/>),
/// которые могут быть либо другими группами (для создания вложенной структуры),
/// либо листами (<see cref="DockLeaf"/>) с контентом.
/// Направление разделения определяется свойством <see cref="Orientation"/>.
/// </remarks>
public class DockGroup : IDockElement, IDragSource, IDropTarget, INotifyPropertyChanged
public class DockGroup : IDockElement, INotifyPropertyChanged
{
/// <summary>
/// Событие, возникающее при изменении значения свойства.
/// Происходит при изменении значения свойства.
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;
private double _splitRatio = 0.5;
private string _id;
private IDockElement _first;
private IDockElement _second;
/// <summary>
/// Получает уникальный идентификатор группы.
/// Получает или задает уникальный идентификатор группы.
/// </summary>
/// <value>
/// Строковый идентификатор, уникальный в пределах дерева компоновки.
@@ -86,7 +75,19 @@ public class DockGroup : IDockElement, IDragSource, IDropTarget, INotifyProperty
/// При установке нового значения автоматически обновляется свойство
/// <see cref="Parent"/> у дочернего элемента.
/// </remarks>
public IDockElement First { get; set; }
public IDockElement First
{
get => _first;
set
{
if (_first != value)
{
_first = value ?? throw new ArgumentNullException(nameof(value));
_first.Parent = this;
OnPropertyChanged();
}
}
}
/// <summary>
/// Получает или задает второй дочерний элемент (правую или нижнюю область).
@@ -101,7 +102,19 @@ public class DockGroup : IDockElement, IDragSource, IDropTarget, INotifyProperty
/// При установке нового значения автоматически обновляется свойство
/// <see cref="Parent"/> у дочернего элемента.
/// </remarks>
public IDockElement Second { get; set; }
public IDockElement Second
{
get => _second;
set
{
if (_second != value)
{
_second = value ?? throw new ArgumentNullException(nameof(value));
_second.Parent = this;
OnPropertyChanged();
}
}
}
/// <summary>
/// Получает или задает направление разделения данной группы.
@@ -111,12 +124,10 @@ public class DockGroup : IDockElement, IDragSource, IDropTarget, INotifyProperty
/// как разделена область: горизонтально или вертикально.
/// </value>
/// <remarks>
/// <para>
/// <see cref="SplitDirection.Horizontal"/> создает левую и правую области.
/// </para>
/// <para>
/// <see cref="SplitDirection.Vertical"/> создает верхнюю и нижнюю области.
/// </para>
/// <list type="bullet">
/// <item><see cref="SplitDirection.Horizontal"/> создает левую и правую области</item>
/// <item><see cref="SplitDirection.Vertical"/> создает верхнюю и нижнюю области</item>
/// </list>
/// </remarks>
public SplitDirection Orientation { get; set; }
@@ -218,7 +229,8 @@ public class DockGroup : IDockElement, IDragSource, IDropTarget, INotifyProperty
/// у дочерних элементов на текущую группу и генерирует уникальный идентификатор,
/// если он не был предоставлен.
/// </remarks>
public DockGroup(IDockElement first, IDockElement second, SplitDirection orientation, string? id = null)
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));
@@ -239,206 +251,4 @@ public class DockGroup : IDockElement, IDragSource, IDropTarget, INotifyProperty
{
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
}

View File

@@ -1,8 +1,4 @@
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;
@@ -15,34 +11,24 @@ namespace Lattice.Core.Docking.Models;
/// отображаемого пользователю содержимого.
/// </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
public class DockLeaf : IDockContainer, INotifyPropertyChanged
{
/// <summary>
/// Событие, возникающее при изменении значения свойства.
/// Происходит при изменении значения свойства.
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;
private readonly ObservableCollection<IDockContent> _items = new();
private IDockContent? _activeContent;
private string _id;
private TabPlacement _tabPlacement = TabPlacement.Bottom;
/// <summary>
/// Получает уникальный идентификатор листа.
/// Получает или задает уникальный идентификатор листа.
/// </summary>
/// <value>
/// Строковый идентификатор, уникальный в пределах дерева компоновки.
@@ -88,13 +74,9 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged, IDragSource, IDr
/// Активная вкладка или null, если в контейнере нет вкладок.
/// </value>
/// <remarks>
/// <para>
/// При установке нового значения проверяется, что вкладка действительно
/// содержится в коллекции <see cref="Children"/>.
/// </para>
/// <para>
/// Изменение этого свойства вызывает событие <see cref="PropertyChanged"/>.
/// </para>
/// </remarks>
public IDockContent? ActiveContent
{
@@ -152,7 +134,18 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged, IDragSource, IDr
/// <remarks>
/// Поддерживаются все четыре стороны: верх, низ, лево, право.
/// </remarks>
public TabPlacement TabPlacement { get; set; } = TabPlacement.Bottom;
public TabPlacement TabPlacement
{
get => _tabPlacement;
set
{
if (_tabPlacement != value)
{
_tabPlacement = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DockLeaf"/>.
@@ -187,17 +180,15 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged, IDragSource, IDr
/// Контент для добавления.
/// </param>
/// <remarks>
/// <para>
/// Если контент уже содержится в коллекции, он не добавляется повторно,
/// но становится активным.
/// </para>
/// <para>
/// Этот метод обновляет свойство <see cref="ActiveContent"/> и вызывает
/// соответствующее событие изменения свойства.
/// </para>
/// </remarks>
public void AddContent(IDockContent content)
{
if (content == null) return;
if (!_items.Contains(content))
{
_items.Add(content);
@@ -212,18 +203,16 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged, IDragSource, IDr
/// Контент для удаления.
/// </param>
/// <remarks>
/// <para>
/// Если удаляемый контент является активным, автоматически выбирается
/// новая активная вкладка (следующая в списке или предыдущая, если удалена
/// последняя).
/// </para>
/// <para>
/// Если после удаления контейнер становится пустым, он может быть удален
/// из дерева макета системой компоновки.
/// </para>
/// </remarks>
public void RemoveContent(IDockContent content)
{
if (content == null) return;
int index = _items.IndexOf(content);
if (index == -1) return;
@@ -237,344 +226,4 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged, IDragSource, IDr
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;
}

View File

@@ -1,13 +1,33 @@
namespace Lattice.Core.Docking.Models;
/// <summary>
/// Определяет позицию вставки при операции Drag-and-Drop.
/// Определяет позицию вставки элемента относительно целевого элемента.
/// Используется при операциях перемещения и вставки элементов в дерево компоновки.
/// </summary>
public enum DockPosition
{
/// <summary>
/// Слева от целевого элемента.
/// </summary>
Left,
/// <summary>
/// Справа от целевого элемента.
/// </summary>
Right,
/// <summary>
/// Сверху от целевого элемента.
/// </summary>
Top,
/// <summary>
/// Снизу от целевого элемента.
/// </summary>
Bottom,
/// <summary>
/// В центре целевого элемента (для объединения вкладок).
/// </summary>
Center,
}
}

View File

@@ -5,15 +5,23 @@
/// </summary>
public enum DockSide
{
/// <summary> Левая сторона окна. </summary>
/// <summary>
/// Левая сторона окна.
/// </summary>
Left,
/// <summary> Правая сторона окна. </summary>
/// <summary>
/// Правая сторона окна.
/// </summary>
Right,
/// <summary> Верхняя сторона окна. </summary>
/// <summary>
/// Верхняя сторона окна.
/// </summary>
Top,
/// <summary> Нижняя сторона окна. </summary>
/// <summary>
/// Нижняя сторона окна.
/// </summary>
Bottom
}

View File

@@ -3,21 +3,66 @@
namespace Lattice.Core.Docking.Models;
/// <summary>
/// Описывает состояние плавающего окна в системе Lattice.
/// Представляет плавающее окно в системе докинга.
/// Плавающие окна могут перемещаться по экрану независимо от главного окна.
/// </summary>
public class DockWindow
{
/// <summary> Уникальный ID окна для сохранения его позиции в конфиге. </summary>
/// <summary>
/// Получает уникальный идентификатор окна.
/// </summary>
/// <value>
/// Строковый идентификатор, сгенерированный с помощью GUID.
/// Используется для сохранения позиции и размера окна в конфигурации.
/// </value>
public string Id { get; } = Guid.NewGuid().ToString();
/// <summary> Корневой элемент макета внутри данного окна. </summary>
/// <summary>
/// Получает или задает корневой элемент макета внутри данного окна.
/// </summary>
/// <value>
/// Корневой элемент дерева компоновки плавающего окна.
/// </value>
public IDockElement? Root { get; set; }
/// <summary>
/// Получает или задает позицию X окна на экране.
/// </summary>
/// <value>
/// Координата X левого верхнего угла окна в пикселях.
/// </value>
public double X { get; set; }
/// <summary>
/// Получает или задает позицию Y окна на экране.
/// </summary>
/// <value>
/// Координата Y левого верхнего угла окна в пикселях.
/// </value>
public double Y { get; set; }
/// <summary>
/// Получает или задает ширину окна.
/// </summary>
/// <value>
/// Ширина окна в пикселях. Значение по умолчанию: 800.
/// </value>
public double Width { get; set; } = 800;
/// <summary>
/// Получает или задает высоту окна.
/// </summary>
/// <value>
/// Высота окна в пикселях. Значение по умолчанию: 600.
/// </value>
public double Height { get; set; } = 600;
/// <summary> Заголовок окна (обычно берется из активного контента). </summary>
/// <summary>
/// Получает или задает заголовок окна.
/// </summary>
/// <value>
/// Текст заголовка окна. Обычно берется из активного контента.
/// Значение по умолчанию: "Lattice Tool Window".
/// </value>
public string Title { get; set; } = "Lattice Tool Window";
}
}

View File

@@ -1,12 +1,17 @@
namespace Lattice.Core.Docking.Models;
/// <summary>
/// Перечисление направлений разделения пространства внутри группы.
/// Определяет направление разделения пространства внутри группы.
/// </summary>
public enum SplitDirection
{
/// <summary> Разделение по горизонтали (создает левую и правую области). </summary>
/// <summary>
/// Разделение по горизонтали (создает левую и правую области).
/// </summary>
Horizontal,
/// <summary> Разделение по вертикали (создает верхнюю и нижнюю области). </summary>
/// <summary>
/// Разделение по вертикали (создает верхнюю и нижнюю области).
/// </summary>
Vertical
}
}

View File

@@ -5,8 +5,23 @@
/// </summary>
public enum TabPlacement
{
/// <summary>
/// Вкладки располагаются сверху.
/// </summary>
Top,
/// <summary>
/// Вкладки располагаются снизу.
/// </summary>
Bottom,
/// <summary>
/// Вкладки располагаются слева.
/// </summary>
Left,
/// <summary>
/// Вкладки располагаются справа.
/// </summary>
Right,
}

View File

@@ -1,7 +1,7 @@
namespace Lattice.Core.Docking.Serialization;
/// <summary>
/// Абстракция для сериализации и десериализации состояния макета док-системы.
/// Определяет контракт для сериализации и десериализации состояния макета док-системы.
/// Позволяет сохранять и восстанавливать расположение панелей, окон и их состояние.
/// </summary>
/// <remarks>
@@ -14,7 +14,12 @@ public interface ILayoutSerializer
/// Сериализует состояние менеджера макета в строку.
/// </summary>
/// <param name="manager">Менеджер макета для сериализации.</param>
/// <returns>Строковое представление состояния макета.</returns>
/// <returns>
/// Строковое представление состояния макета.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="manager"/> равен null.
/// </exception>
string Serialize(Engine.LayoutManager manager);
/// <summary>
@@ -26,6 +31,10 @@ public interface ILayoutSerializer
/// Функция разрешения контента по идентификатору, используемая для восстановления
/// ссылок на контент в десериализованном состоянии.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="manager"/> или <paramref name="serializedLayout"/>
/// равны null.
/// </exception>
void Deserialize(Engine.LayoutManager manager, string serializedLayout,
Func<string, Abstractions.IDockContent?> contentResolver);
}
}

View File

@@ -1,19 +1,24 @@
namespace Lattice.Core.Docking.Serialization;
/// <summary>
/// Контракт для объектов, которые могут предоставлять состояние для сериализации.
/// Определяет контракт для объектов, которые могут предоставлять состояние для сериализации.
/// </summary>
public interface ISerializableLayout
{
/// <summary>
/// Получает состояние для сериализации.
/// Получает состояние объекта для сериализации.
/// </summary>
/// <returns>Объект состояния, готовый к сериализации.</returns>
/// <returns>
/// Объект состояния, готовый к сериализации.
/// </returns>
object GetSerializableState();
/// <summary>
/// Восстанавливает состояние из десериализованного объекта.
/// Восстанавливает состояние объекта из десериализованного объекта.
/// </summary>
/// <param name="state">Десериализованное состояние.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="state"/> равен null.
/// </exception>
void RestoreFromState(object state);
}

View File

@@ -2,7 +2,7 @@
/// <summary>
/// Реестр типов содержимого, который позволяет создавать экземпляры контента по типу.
/// Этот сервис является центральным для динамического создания панелей инструментов и документов в IDE.
/// Этот сервис является центральным для динамического создания панелей инструментов и документов.
/// </summary>
/// <remarks>
/// Реализует шаблон "Фабрика" для создания экземпляров <see cref="Abstractions.IDockContent"/>.
@@ -16,12 +16,19 @@ public class ContentRegistry
/// <summary>
/// Регистрирует фабричный метод для создания контента указанного типа.
/// </summary>
/// <typeparam name="T">Тип контента, реализующий <see cref="Abstractions.IDockContent"/>.</typeparam>
/// <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>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="contentTypeId"/> или <paramref name="factory"/>
/// равны null.
/// </exception>
/// <exception cref="ArgumentException">
/// Выбрасывается, если <paramref name="contentTypeId"/> уже зарегистрирован.
/// </exception>
public void Register<T>(string contentTypeId, Func<T> factory, ContentMetadata? metadata = null)
where T : Abstractions.IDockContent
{
@@ -31,7 +38,7 @@ public class ContentRegistry
throw new ArgumentNullException(nameof(factory));
if (_contentTypes.ContainsKey(contentTypeId))
throw new ArgumentException($"Content type '{contentTypeId}' is already registered.");
throw new ArgumentException($"Тип контента '{contentTypeId}' уже зарегистрирован.");
_contentTypes[contentTypeId] = new ContentDescriptor(
typeof(T),
@@ -45,14 +52,25 @@ public class ContentRegistry
/// </summary>
/// <param name="contentTypeId">Идентификатор типа контента.</param>
/// <param name="id">Уникальный идентификатор для создаваемого экземпляра контента.</param>
/// <returns>Новый экземпляр контента.</returns>
/// <exception cref="KeyNotFoundException">Выбрасывается, если тип контента не зарегистрирован.</exception>
/// <returns>
/// Новый экземпляр контента.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="contentTypeId"/> равен null или пустой строке.
/// </exception>
/// <exception cref="KeyNotFoundException">
/// Выбрасывается, если тип контента не зарегистрирован.
/// </exception>
public Abstractions.IDockContent CreateContent(string contentTypeId, string id)
{
if (string.IsNullOrWhiteSpace(contentTypeId))
throw new ArgumentNullException(nameof(contentTypeId));
if (!_contentTypes.TryGetValue(contentTypeId, out var descriptor))
throw new KeyNotFoundException($"Content type '{contentTypeId}' is not registered.");
throw new KeyNotFoundException($"Тип контента '{contentTypeId}' не зарегистрирован.");
var content = descriptor.Factory();
// Устанавливаем ID через рефлексию, если есть свойство Id
var property = content.GetType().GetProperty("Id");
if (property != null && property.CanWrite)
@@ -67,9 +85,14 @@ public class ContentRegistry
/// Получает метаданные для указанного типа контента.
/// </summary>
/// <param name="contentTypeId">Идентификатор типа контента.</param>
/// <returns>Метаданные типа контента или null, если тип не найден.</returns>
/// <returns>
/// Метаданные типа контента или null, если тип не найден.
/// </returns>
public ContentMetadata? GetMetadata(string contentTypeId)
{
if (string.IsNullOrWhiteSpace(contentTypeId))
return null;
return _contentTypes.TryGetValue(contentTypeId, out var descriptor)
? descriptor.Metadata
: null;
@@ -78,24 +101,54 @@ public class ContentRegistry
/// <summary>
/// Получает все зарегистрированные типы контента.
/// </summary>
/// <returns>Коллекция идентификаторов зарегистрированных типов контента.</returns>
/// <returns>
/// Коллекция идентификаторов зарегистрированных типов контента.
/// </returns>
public IEnumerable<string> GetRegisteredTypes() => _contentTypes.Keys;
/// <summary>
/// Проверяет, зарегистрирован ли указанный тип контента.
/// </summary>
public bool IsRegistered(string contentTypeId) => _contentTypes.ContainsKey(contentTypeId);
/// <param name="contentTypeId">Идентификатор типа контента.</param>
/// <returns>
/// true, если тип контента зарегистрирован; в противном случае false.
/// </returns>
public bool IsRegistered(string contentTypeId)
{
if (string.IsNullOrWhiteSpace(contentTypeId))
return false;
return _contentTypes.ContainsKey(contentTypeId);
}
/// <summary>
/// Дескриптор типа контента, содержащий информацию о фабричном методе и метаданных.
/// Представляет дескриптор типа контента, содержащий информацию о фабричном методе и метаданных.
/// </summary>
private class ContentDescriptor
{
/// <summary>
/// Получает тип контента.
/// </summary>
public Type ContentType { get; }
/// <summary>
/// Получает фабричный метод для создания экземпляров контента.
/// </summary>
public Func<Abstractions.IDockContent> Factory { get; }
/// <summary>
/// Получает метаданные типа контента.
/// </summary>
public ContentMetadata Metadata { get; }
public ContentDescriptor(Type contentType, Func<Abstractions.IDockContent> factory, ContentMetadata metadata)
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="ContentDescriptor"/>.
/// </summary>
/// <param name="contentType">Тип контента.</param>
/// <param name="factory">Фабричный метод.</param>
/// <param name="metadata">Метаданные.</param>
public ContentDescriptor(Type contentType, Func<Abstractions.IDockContent> factory,
ContentMetadata metadata)
{
ContentType = contentType;
Factory = factory;
@@ -105,54 +158,80 @@ public class ContentRegistry
}
/// <summary>
/// Метаданные типа контента, предоставляющие дополнительную информацию для отображения в UI.
/// Представляет метаданные типа контента, предоставляющие дополнительную информацию для отображения в UI.
/// </summary>
public class ContentMetadata
{
/// <summary>
/// Идентификатор типа контента.
/// Получает идентификатор типа контента.
/// </summary>
/// <value>
/// Уникальный строковый идентификатор типа контента.
/// </value>
public string ContentTypeId { get; }
/// <summary>
/// Отображаемое имя типа контента.
/// Получает или задает отображаемое имя типа контента.
/// </summary>
/// <value>
/// Имя типа контента, отображаемое пользователю.
/// </value>
public string DisplayName { get; set; }
/// <summary>
/// Описание типа контента.
/// Получает или задает описание типа контента.
/// </summary>
/// <value>
/// Текстовое описание функциональности контента.
/// </value>
public string Description { get; set; }
/// <summary>
/// Имя ресурса для иконки (опционально).
/// Получает или задает имя ресурса для иконки типа контента.
/// </summary>
/// <value>
/// Имя ресурса иконки или null, если иконка не определена.
/// </value>
public string? IconResource { get; set; }
/// <summary>
/// Признак того, что контент является документом (а не инструментальной панелью).
/// Получает или задает значение, указывающее, является ли контент документом
/// (а не инструментальной панелью).
/// </summary>
/// <value>
/// true, если контент является документом; в противном случае false.
/// </value>
public bool IsDocument { get; set; }
/// <summary>
/// Минимальная ширина контента в пикселях.
/// Получает или задает ширину контента по умолчанию.
/// </summary>
/// <value>
/// Ширина контента в пикселях. Значение по умолчанию: 300.
/// </value>
public double DefaultWidth { get; set; } = 300;
/// <summary>
/// Минимальная высота контента в пикселях.
/// Получает или задает высоту контента по умолчанию.
/// </summary>
/// <value>
/// Высота контента в пикселях. Значение по умолчанию: 200.
/// </value>
public double DefaultHeight { get; set; } = 200;
/// <summary>
/// Инициализирует новый экземпляр метаданных контента.
/// Инициализирует новый экземпляр класса <see cref="ContentMetadata"/>.
/// </summary>
/// <param name="contentTypeId">Идентификатор типа контента.</param>
/// <param name="displayName">Отображаемое имя.</param>
/// <param name="displayName">Отображаемое имя типа контента.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="contentTypeId"/> или <paramref name="displayName"/>
/// равны null.
/// </exception>
public ContentMetadata(string contentTypeId, string displayName)
{
ContentTypeId = contentTypeId;
DisplayName = displayName;
ContentTypeId = contentTypeId ?? throw new ArgumentNullException(nameof(contentTypeId));
DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName));
Description = string.Empty;
}
}

View File

@@ -1,115 +0,0 @@
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);
}
}

View File

@@ -1,27 +0,0 @@
<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>

View File

@@ -1,84 +0,0 @@
namespace Lattice.Core.DragDrop.Abstractions;
/// <summary>
/// Определяет контракт для объектов, которые могут быть источником данных
/// в операции перетаскивания.
/// </summary>
/// <remarks>
/// <para>
/// Объекты, реализующие этот интерфейс, могут инициировать операции перетаскивания
/// и предоставлять данные для передачи другим элементам через механизм drag-and-drop.
/// </para>
/// <para>
/// Интерфейс полностью асинхронный и поддерживает отмену операций через CancellationToken.
/// Все методы должны быть потокобезопасными и поддерживать вызов из любого потока.
/// </para>
/// </remarks>
public interface IDragSource
{
/// <summary>
/// Пытается начать операцию перетаскивания из указанной позиции.
/// </summary>
/// <param name="startPosition">Начальная позиция операции в координатах экрана.</param>
/// <param name="cancellationToken">Токен отмены операции.</param>
/// <returns>
/// Информация о перетаскивании, если операция может быть начата; в противном случае — null.
/// Возвращаемый объект <see cref="Models.DragInfo"/> должен быть полностью инициализирован,
/// включая данные, разрешенные эффекты и ссылку на источник.
/// </returns>
/// <remarks>
/// <para>
/// Этот метод вызывается сервисом перетаскивания при попытке начать операцию
/// (обычно при нажатии и перемещении мыши). Метод должен проверить, может ли
/// источник начать перетаскивание в текущем контексте.
/// </para>
/// <para>
/// Реализация должна быть быстрой и не выполнять длительных операций.
/// Если подготовка данных требует времени, ее следует выполнить асинхронно
/// после подтверждения возможности начала.
/// </para>
/// </remarks>
Task<Models.DragInfo?> TryStartDragAsync(Geometry.Point startPosition, CancellationToken cancellationToken = default);
/// <summary>
/// Уведомляет источник о завершении операции перетаскивания.
/// </summary>
/// <param name="dragInfo">Информация о перетаскивании, полученная при начале операции.</param>
/// <param name="effects">Эффекты, которые были применены при сбросе.</param>
/// <param name="cancellationToken">Токен отмены операции.</param>
/// <returns>Задача, представляющая асинхронную операцию.</returns>
/// <remarks>
/// <para>
/// Этот метод вызывается после завершения операции перетаскивания
/// (успешного или неуспешного). Реализация может:
/// </para>
/// <list type="bullet">
/// <item>Выполнить очистку ресурсов, связанных с операцией</item>
/// <item>Обновить состояние на основе результата (например, удалить данные при перемещении)</item>
/// <item>Отобразить визуальную обратную связь о результате</item>
/// </list>
/// <para>
/// Если операция завершилась с эффектом <see cref="Enums.DragDropEffects.Move"/>,
/// источник обычно должен удалить или обновить исходные данные.
/// </para>
/// </remarks>
Task OnDragCompletedAsync(Models.DragInfo dragInfo, Enums.DragDropEffects effects, CancellationToken cancellationToken = default);
/// <summary>
/// Уведомляет источник об отмене операции перетаскивания.
/// </summary>
/// <param name="dragInfo">Информация о перетаскивании, полученная при начале операции.</param>
/// <param name="cancellationToken">Токен отмены операции.</param>
/// <returns>Задача, представляющая асинхронную операцию.</returns>
/// <remarks>
/// <para>
/// Этот метод вызывается, когда операция перетаскивания была отменена
/// пользователем (например, нажатием клавиши Escape) или системой.
/// </para>
/// <para>
/// Реализация должна выполнить очистку и восстановить исходное состояние.
/// Обычно это включает освобождение ресурсов и сброс визуальных индикаторов.
/// </para>
/// </remarks>
Task OnDragCancelledAsync(Models.DragInfo dragInfo, CancellationToken cancellationToken = default);
}

View File

@@ -1,118 +0,0 @@
namespace Lattice.Core.DragDrop.Abstractions;
/// <summary>
/// Определяет контракт для объектов, которые могут принимать сбрасываемые данные
/// в операции перетаскивания.
/// </summary>
/// <remarks>
/// <para>
/// Объекты, реализующие этот интерфейс, могут обрабатывать данные, сброшенные
/// пользователем, и предоставлять визуальную обратную связь во время перетаскивания.
/// </para>
/// <para>
/// Интерфейс поддерживает асинхронные операции и отмену через CancellationToken.
/// Все методы должны быть потокобезопасными и идемпотентными (многократный вызов
/// с одинаковыми параметрами должен давать одинаковый результат).
/// </para>
/// </remarks>
public interface IDropTarget
{
/// <summary>
/// Определяет, может ли объект принять сбрасываемые данные.
/// </summary>
/// <param name="dropInfo">Информация о потенциальном сбросе.</param>
/// <param name="cancellationToken">Токен отмены операции.</param>
/// <returns>
/// true, если объект может принять данные; в противном случае — false.
/// </returns>
/// <remarks>
/// <para>
/// Этот метод вызывается, когда перетаскиваемый объект находится над целью.
/// Реализация должна проверить, совместимы ли данные с целью, и установить
/// предлагаемые эффекты в свойстве <see cref="Models.DropInfo.SuggestedEffects"/>.
/// </para>
/// <para>
/// Метод может вызываться многократно при перемещении курсора над целью.
/// Реализация должна быть эффективной и избегать длительных операций.
/// </para>
/// <para>
/// Если метод возвращает false, система не будет вызывать другие методы
/// для этой цели до тех пор, пока курсор не покинет ее область.
/// </para>
/// </remarks>
Task<bool> CanAcceptDropAsync(Models.DropInfo dropInfo, CancellationToken cancellationToken = default);
/// <summary>
/// Вызывается, когда перетаскиваемый объект перемещается над целью.
/// </summary>
/// <param name="dropInfo">Информация о текущем положении перетаскивания.</param>
/// <param name="cancellationToken">Токен отмены операции.</param>
/// <returns>Задача, представляющая асинхронную операцию.</returns>
/// <remarks>
/// <para>
/// Этот метод вызывается постоянно, пока пользователь перемещает объект над целью.
/// Реализация может:
/// </para>
/// <list type="bullet">
/// <item>Обновить визуальную обратную связь (подсветка, изменение курсора)</item>
/// <item>Вычислить точную позицию сброса (например, между элементами списка)</item>
/// <item>Уточнить предлагаемые эффекты на основе текущей позиции</item>
/// <item>Прокрутить содержимое, если цель поддерживает прокрутку</item>
/// </list>
/// <para>
/// Метод должен быть оптимизирован для частого вызова. Длительные операции
/// должны выполняться асинхронно без блокировки потока.
/// </para>
/// </remarks>
Task OnDragOverAsync(Models.DropInfo dropInfo, CancellationToken cancellationToken = default);
/// <summary>
/// Вызывается, когда пользователь сбрасывает данные на цель.
/// </summary>
/// <param name="dropInfo">Информация о сбросе, включая данные и позицию.</param>
/// <param name="cancellationToken">Токен отмены операции.</param>
/// <returns>Задача, представляющая асинхронную операцию.</returns>
/// <remarks>
/// <para>
/// Этот метод вызывается, когда пользователь отпускает кнопку мыши над целью.
/// Реализация должна обработать принятие данных и выполнить соответствующее действие:
/// </para>
/// <list type="bullet">
/// <item>Добавить данные в коллекцию (для копирования)</item>
/// <item>Переместить данные (при поддержке перемещения)</item>
/// <item>Создать ссылку на данные</item>
/// <item>Выполнить пользовательскую логику обработки</item>
/// </list>
/// <para>
/// После успешной обработки данных следует вызвать <see cref="Models.DropInfo.MarkAsHandled()"/>,
/// чтобы указать системе, что операция обработана и дополнительная обработка не требуется.
/// </para>
/// <para>
/// Если операция завершилась успешно, система уведомит источник через
/// <see cref="IDragSource.OnDragCompletedAsync"/> с соответствующими эффектами.
/// </para>
/// </remarks>
Task OnDropAsync(Models.DropInfo dropInfo, CancellationToken cancellationToken = default);
/// <summary>
/// Вызывается, когда перетаскиваемый объект покидает область цели.
/// </summary>
/// <param name="cancellationToken">Токен отмены операции.</param>
/// <returns>Задача, представляющая асинхронную операцию.</returns>
/// <remarks>
/// <para>
/// Этот метод вызывается, когда пользователь перемещает объект за пределы цели.
/// Реализация должна:
/// </para>
/// <list type="bullet">
/// <item>Очистить любую визуальную обратную связь, установленную ранее</item>
/// <item>Сбросить временное состояние, связанное с операцией</item>
/// <item>Освободить ресурсы, выделенные для предварительного просмотра</item>
/// </list>
/// <para>
/// Метод гарантированно вызывается после любого успешного или неуспешного
/// вызова <see cref="OnDragOverAsync"/>, если курсор покидает область цели.
/// </para>
/// </remarks>
Task OnDragLeaveAsync(CancellationToken cancellationToken = default);
}

View File

@@ -1,27 +0,0 @@
namespace Lattice.Core.DragDrop.Constants;
/// <summary>
/// Константы для системы перетаскивания.
/// </summary>
public static class DragDropConstants
{
/// <summary>
/// Порог начала перетаскивания по умолчанию (пикселей).
/// </summary>
public const double DefaultDragThreshold = 3.0;
/// <summary>
/// Интервал очистки по умолчанию (миллисекунды).
/// </summary>
public const int DefaultCleanupInterval = 60000;
/// <summary>
/// Таймаут асинхронных операций по умолчанию (миллисекунды).
/// </summary>
public const int DefaultAsyncTimeout = 5000;
/// <summary>
/// Время жизни неиспользуемых целей (минуты).
/// </summary>
public const int TargetLifetimeMinutes = 10;
}

View File

@@ -1,115 +0,0 @@
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>
/// <param name="effects">Эффекты для проверки.</param>
/// <param name="effect">Эффект для поиска.</param>
/// <returns>true, если эффект присутствует; в противном случае — false.</returns>
public static bool HasEffect(this DragDropEffects effects, DragDropEffects effect)
{
return (effects & effect) == effect;
}
/// <summary>
/// Проверяет, содержат ли эффекты копирование.
/// </summary>
/// <param name="effects">Эффекты для проверки.</param>
/// <returns>true, если разрешено копирование; в противном случае — false.</returns>
public static bool CanCopy(this DragDropEffects effects)
{
return effects.HasEffect(DragDropEffects.Copy);
}
/// <summary>
/// Проверяет, содержат ли эффекты перемещение.
/// </summary>
/// <param name="effects">Эффекты для проверки.</param>
/// <returns>true, если разрешено перемещение; в противном случае — false.</returns>
public static bool CanMove(this DragDropEffects effects)
{
return effects.HasEffect(DragDropEffects.Move);
}
/// <summary>
/// Проверяет, содержат ли эффекты ссылку.
/// </summary>
/// <param name="effects">Эффекты для проверки.</param>
/// <returns>true, если разрешена ссылка; в противном случае — false.</returns>
public static bool CanLink(this DragDropEffects effects)
{
return effects.HasEffect(DragDropEffects.Link);
}
/// <summary>
/// Получает наиболее подходящий эффект на основе модификаторов клавиатуры.
/// </summary>
/// <param name="controlKey">Нажата ли клавиша Control.</param>
/// <param name="shiftKey">Нажата ли клавиша Shift.</param>
/// <param name="altKey">Нажата ли клавиша Alt.</param>
/// <returns>Наиболее подходящий эффект перетаскивания.</returns>
public static DragDropEffects GetEffectFromKeys(bool controlKey, bool shiftKey, bool altKey)
{
if (controlKey && shiftKey)
return DragDropEffects.Link;
if (controlKey)
return DragDropEffects.Copy;
if (shiftKey)
return DragDropEffects.Move;
if (altKey)
return DragDropEffects.Link;
return DragDropEffects.Move; // По умолчанию
}
}

View File

@@ -1,14 +0,0 @@
namespace Lattice.Core.DragDrop.Enums;
/// <summary>
/// Позиция сброса относительно цели.
/// </summary>
public enum DropPosition
{
Inside,
Top,
Bottom,
Left,
Right,
Center
}

View File

@@ -1,85 +0,0 @@
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 and 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";
}

View File

@@ -1,74 +0,0 @@
using Lattice.Core.DragDrop.Enums;
using Lattice.Core.Geometry;
namespace Lattice.Core.DragDrop.Extensions;
/// <summary>
/// Методы расширения для DropInfo.
/// </summary>
public static class DropInfoExtensions
{
/// <summary>
/// Проверяет, могут ли данные быть приведены к указанному типу.
/// </summary>
/// <typeparam name="T">Тип данных для проверки.</typeparam>
/// <param name="dropInfo">Информация о сбросе.</param>
/// <returns>true, если данные могут быть приведены к типу T; в противном случае — false.</returns>
public static bool CanAccept<T>(this Models.DropInfo dropInfo)
where T : class
{
return dropInfo.Data is T;
}
/// <summary>
/// Пытается получить данные как указанный тип.
/// </summary>
/// <typeparam name="T">Тип, к которому нужно привести данные.</typeparam>
/// <param name="dropInfo">Информация о сбросе.</param>
/// <returns>Данные как тип T или null.</returns>
public static T? GetDataAs<T>(this Models.DropInfo dropInfo)
where T : class
{
return dropInfo.Data as T;
}
/// <summary>
/// Получает данные как указанный тип или выбрасывает исключение.
/// </summary>
/// <typeparam name="T">Тип, к которому нужно привести данные.</typeparam>
/// <param name="dropInfo">Информация о сбросе.</param>
/// <returns>Данные как тип T.</returns>
/// <exception cref="InvalidCastException">Выбрасывается, если данные не могут быть приведены к типу T.</exception>
public static T GetRequiredDataAs<T>(this Models.DropInfo dropInfo)
where T : class
{
if (dropInfo.Data is not T data)
{
throw new InvalidCastException(
$"Ожидался тип {typeof(T).Name}, но получен {dropInfo.Data?.GetType().Name ?? "null"}");
}
return data;
}
/// <summary>
/// Проверяет, содержится ли позиция в указанных границах.
/// </summary>
/// <param name="dropInfo">Информация о сбросе.</param>
/// <param name="bounds">Границы для проверки.</param>
/// <returns>true, если позиция находится в границах; в противном случае — false.</returns>
public static bool IsInBounds(this Models.DropInfo dropInfo, Rect bounds)
{
return bounds.Contains(dropInfo.Position);
}
/// <summary>
/// Проверяет можно ли добавить эффект перетаскивания.
/// </summary>
/// <param name="dropInfo"></param>
/// <param name="effect"></param>
/// <returns></returns>
public static bool CanAcceptEffect(this Models.DropInfo dropInfo, DragDropEffects effect)
{
return dropInfo.AllowedEffects.HasEffect(effect);
}
}

View File

@@ -1,581 +0,0 @@
using Lattice.Core.DragDrop.Abstractions;
using Lattice.Core.DragDrop.Enums;
using Lattice.Core.DragDrop.Models;
using Lattice.Core.DragDrop.Services;
using Lattice.Core.Geometry;
namespace Lattice.Core.DragDrop.Factories;
/// <summary>
/// Фабрика для создания компонентов системы перетаскивания.
/// Предоставляет методы для создания сервисов, источников и целей перетаскивания.
/// </summary>
/// <remarks>
/// Эта фабрика позволяет создавать компоненты системы перетаскивания без использования
/// Dependency Injection, предоставляя простой и понятный API для наиболее распространенных сценариев.
/// </remarks>
public static class DragDropFactory
{
#region Сервисы перетаскивания
/// <summary>
/// Создает новый экземпляр сервиса перетаскивания с настройками по умолчанию.
/// </summary>
/// <returns>
/// Экземпляр <see cref="IDragDropService"/> с настройками по умолчанию.
/// </returns>
/// <remarks>
/// Созданный сервис имеет следующие настройки по умолчанию:
/// <list type="bullet">
/// <item>Порог начала перетаскивания: 3.0 пикселей</item>
/// <item>Таймаут асинхронных операций: 5000 миллисекунд</item>
/// <item>Асинхронные операции: включены</item>
/// </list>
/// </remarks>
public static IDragDropService CreateDragDropService()
{
return new DragDropService();
}
/// <summary>
/// Создает новый экземпляр сервиса перетаскивания с пользовательскими настройками.
/// </summary>
/// <param name="configure">
/// Делегат для настройки опций сервиса. Передает экземпляр <see cref="DragDropServiceOptions"/>
/// для настройки параметров.
/// </param>
/// <returns>
/// Настроенный экземпляр <see cref="IDragDropService"/>.
/// </returns>
/// <example>
/// <code>
/// var service = DragDropFactory.CreateDragDropService(options =>
/// {
/// options.DragStartThreshold = 5.0;
/// options.AsyncOperationTimeout = 3000;
/// options.EnableAsyncOperations = true;
/// });
/// </code>
/// </example>
public static IDragDropService CreateDragDropService(Action<DragDropServiceOptions> configure)
{
var options = new DragDropServiceOptions();
configure(options);
return new DragDropService
{
DragStartThreshold = options.DragStartThreshold,
AsyncOperationTimeout = options.AsyncOperationTimeout,
EnableAsyncOperations = options.EnableAsyncOperations
};
}
/// <summary>
/// Создает сервис перетаскивания, оптимизированный для сенсорных устройств.
/// </summary>
/// <returns>
/// Экземпляр <see cref="IDragDropService"/> с увеличенным порогом перетаскивания.
/// </returns>
/// <remarks>
/// Этот метод создает сервис с увеличенным порогом начала перетаскивания (10.0 пикселей),
/// что уменьшает вероятность случайного начала перетаскивания при использовании сенсорного экрана.
/// </remarks>
public static IDragDropService CreateTouchOptimizedService()
{
return new DragDropService
{
DragStartThreshold = 10.0,
AsyncOperationTimeout = 3000,
EnableAsyncOperations = true
};
}
/// <summary>
/// Создает сервис перетаскивания для точных операций (графические редакторы, карты).
/// </summary>
/// <returns>
/// Экземпляр <see cref="IDragDropService"/> с уменьшенным порогом перетаскивания.
/// </returns>
/// <remarks>
/// Этот метод создает сервис с минимальным порогом начала перетаскивания (1.0 пиксель),
/// что позволяет начинать перетаскивание с максимальной точностью.
/// </remarks>
public static IDragDropService CreatePrecisionDragService()
{
return new DragDropService
{
DragStartThreshold = 1.0,
AsyncOperationTimeout = 10000, // Больше времени для сложных операций
EnableAsyncOperations = true
};
}
#endregion
#region Источники перетаскивания
/// <summary>
/// Создает простой источник перетаскивания с фиксированными данными.
/// </summary>
/// <param name="data">
/// Данные, которые будут перетаскиваться. Не может быть null.
/// </param>
/// <param name="allowedEffects">
/// Разрешенные эффекты перетаскивания. По умолчанию разрешены копирование и перемещение.
/// </param>
/// <returns>
/// Экземпляр <see cref="IDragSource"/>, который всегда предоставляет указанные данные.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="data"/> равен null.
/// </exception>
/// <remarks>
/// Этот источник подходит для случаев, когда данные для перетаскивания известны заранее
/// и не изменяются в процессе операции.
/// </remarks>
public static IDragSource CreateSimpleSource(object data, DragDropEffects allowedEffects = DragDropEffects.Copy | DragDropEffects.Move)
{
if (data == null)
throw new ArgumentNullException(nameof(data));
return new SimpleDragSource(data, allowedEffects);
}
/// <summary>
/// Создает источник перетаскивания с отложенной загрузкой данных.
/// </summary>
/// <param name="dataFactory">
/// Фабрика данных, которая будет вызвана при начале перетаскивания.
/// </param>
/// <param name="canDragChecker">
/// Функция проверки возможности начала перетаскивания.
/// </param>
/// <param name="allowedEffects">
/// Разрешенные эффекты перетаскивания.
/// </param>
/// <returns>
/// Экземпляр <see cref="IDragSource"/> с отложенной загрузкой данных.
/// </returns>
/// <remarks>
/// Этот источник полезен, когда данные для перетаскивания дорого создавать заранее
/// или зависят от контекста в момент начала операции.
/// </remarks>
public static IDragSource CreateLazySource(
Func<object> dataFactory,
Func<bool> canDragChecker = null,
DragDropEffects allowedEffects = DragDropEffects.Copy | DragDropEffects.Move)
{
if (dataFactory == null)
throw new ArgumentNullException(nameof(dataFactory));
return new LazyDragSource(dataFactory, canDragChecker, allowedEffects);
}
/// <summary>
/// Создает источник перетаскивания для коллекции элементов.
/// </summary>
/// <typeparam name="T">
/// Тип элементов в коллекции.
/// </typeparam>
/// <param name="items">
/// Коллекция элементов для перетаскивания.
/// </param>
/// <param name="itemSelector">
/// Функция выбора конкретного элемента для перетаскивания из коллекции.
/// </param>
/// <param name="allowedEffects">
/// Разрешенные эффекты перетаскивания.
/// </param>
/// <returns>
/// Экземпляр <see cref="IDragSource"/> для коллекции элементов.
/// </returns>
/// <remarks>
/// Этот источник позволяет перетаскивать элементы из коллекции. Функция <paramref name="itemSelector"/>
/// определяет, какой именно элемент из коллекции будет перетаскиваться в текущем контексте.
/// </remarks>
public static IDragSource CreateCollectionSource<T>(
IEnumerable<T> items,
Func<IEnumerable<T>, T> itemSelector,
DragDropEffects allowedEffects = DragDropEffects.Copy | DragDropEffects.Move)
{
if (items == null)
throw new ArgumentNullException(nameof(items));
if (itemSelector == null)
throw new ArgumentNullException(nameof(itemSelector));
return new CollectionDragSource<T>(items, itemSelector, allowedEffects);
}
#endregion
#region Цели сброса
/// <summary>
/// Создает простую цель сброса, которая принимает данные любого типа.
/// </summary>
/// <param name="onDrop">
/// Обработчик, вызываемый при сбросе данных.
/// </param>
/// <returns>
/// Экземпляр <see cref="IDropTarget"/>, который принимает любые данные.
/// </returns>
/// <remarks>
/// Эта цель подходит для простых сценариев, когда не требуется валидация типа данных.
/// </remarks>
public static IDropTarget CreateSimpleTarget(Action<DropInfo> onDrop)
{
if (onDrop == null)
throw new ArgumentNullException(nameof(onDrop));
return new SimpleDropTarget(onDrop);
}
/// <summary>
/// Создает цель сброса с фильтрацией по типу данных.
/// </summary>
/// <typeparam name="T">
/// Тип данных, которые может принимать цель.
/// </typeparam>
/// <param name="onDrop">
/// Обработчик, вызываемый при сбросе данных.
/// </param>
/// <returns>
/// Экземпляр <see cref="IDropTarget"/>, который принимает только данные типа <typeparamref name="T"/>.
/// </returns>
/// <remarks>
/// Эта цель автоматически проверяет тип сбрасываемых данных и вызывает обработчик только
/// если данные могут быть приведены к указанному типу.
/// </remarks>
public static IDropTarget CreateTypedTarget<T>(Action<T, DropInfo> onDrop) where T : class
{
if (onDrop == null)
throw new ArgumentNullException(nameof(onDrop));
return new TypedDropTarget<T>(onDrop);
}
/// <summary>
/// Создает цель сброса с пользовательской логикой валидации.
/// </summary>
/// <param name="canAccept">
/// Функция проверки возможности приема данных.
/// </param>
/// <param name="onDrop">
/// Обработчик, вызываемый при сбросе данных.
/// </param>
/// <returns>
/// Экземпляр <see cref="IDropTarget"/> с пользовательской логикой валидации.
/// </returns>
/// <remarks>
/// Эта цель позволяет реализовать сложную логику валидации, выходящую за рамки простой проверки типа.
/// </remarks>
public static IDropTarget CreateConditionalTarget(Func<DropInfo, bool> canAccept, Action<DropInfo> onDrop)
{
if (canAccept == null)
throw new ArgumentNullException(nameof(canAccept));
if (onDrop == null)
throw new ArgumentNullException(nameof(onDrop));
return new ConditionalDropTarget(canAccept, onDrop);
}
#endregion
#region Вспомогательные методы
/// <summary>
/// Создает стандартные эффекты перетаскивания на основе модификаторов клавиатуры.
/// </summary>
/// <param name="controlKey">
/// Нажата ли клавиша Control.
/// </param>
/// <param name="shiftKey">
/// Нажата ли клавиша Shift.
/// </param>
/// <param name="altKey">
/// Нажата ли клавиша Alt.
/// </param>
/// <returns>
/// <see cref="DragDropEffects"/>, соответствующие комбинации клавиш.
/// </returns>
/// <remarks>
/// Стандартная логика:
/// <list type="bullet">
/// <item>Control + Shift: Link</item>
/// <item>Control: Copy</item>
/// <item>Shift: Move</item>
/// <item>Alt: Link</item>
/// <item>Без модификаторов: Move</item>
/// </list>
/// </remarks>
public static DragDropEffects GetEffectsFromKeys(bool controlKey, bool shiftKey, bool altKey)
{
return DragDropEffectsExtensions.GetEffectFromKeys(controlKey, shiftKey, altKey);
}
#endregion
}
/// <summary>
/// Опции для настройки сервиса перетаскивания.
/// </summary>
public class DragDropServiceOptions
{
/// <summary>
/// Получает или задает порог начала перетаскивания в пикселях.
/// </summary>
/// <value>
/// Минимальное расстояние, которое должен пройти курсор, чтобы началась операция перетаскивания.
/// Значение по умолчанию: 3.0.
/// </value>
public double DragStartThreshold { get; set; } = Constants.DragDropConstants.DefaultDragThreshold;
/// <summary>
/// Получает или задает максимальное время ожидания асинхронных операций в миллисекундах.
/// </summary>
/// <value>
/// Время в миллисекундах, после которого асинхронная операция будет прервана.
/// Значение по умолчанию: 5000.
/// </value>
public int AsyncOperationTimeout { get; set; } = Constants.DragDropConstants.DefaultAsyncTimeout;
/// <summary>
/// Получает или задает значение, указывающее, включены ли асинхронные операции.
/// </summary>
/// <value>
/// true, если асинхронные операции включены; в противном случае — false.
/// Значение по умолчанию: true.
/// </value>
public bool EnableAsyncOperations { get; set; } = true;
/// <summary>
/// Получает или задает интервал автоматической очистки неиспользуемых целей в минутах.
/// </summary>
/// <value>
/// Интервал в минутах, через который будут удаляться цели сброса, к которым не было обращений.
/// Значение по умолчанию: 10.
/// </value>
public int CleanupInterval { get; set; } = Constants.DragDropConstants.TargetLifetimeMinutes;
}
#region Внутренние реализации
internal class SimpleDragSource : IDragSource
{
private readonly object _data;
private readonly DragDropEffects _allowedEffects;
public SimpleDragSource(object data, DragDropEffects allowedEffects)
{
_data = data ?? throw new ArgumentNullException(nameof(data));
_allowedEffects = allowedEffects;
}
public Task<DragInfo?> TryStartDragAsync(Point startPosition, CancellationToken cancellationToken = default)
{
var dragInfo = new DragInfo(_data, _allowedEffects, startPosition, this);
return Task.FromResult<DragInfo?>(dragInfo);
}
public Task OnDragCompletedAsync(DragInfo dragInfo, DragDropEffects effects, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public Task OnDragCancelledAsync(DragInfo dragInfo, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
}
internal class LazyDragSource : IDragSource
{
private readonly Func<object> _dataFactory;
private readonly Func<bool> _canDragChecker;
private readonly DragDropEffects _allowedEffects;
public LazyDragSource(Func<object> dataFactory, Func<bool> canDragChecker, DragDropEffects allowedEffects)
{
_dataFactory = dataFactory ?? throw new ArgumentNullException(nameof(dataFactory));
_canDragChecker = canDragChecker ?? (() => true);
_allowedEffects = allowedEffects;
}
public Task<DragInfo?> TryStartDragAsync(Point startPosition, CancellationToken cancellationToken = default)
{
if (!_canDragChecker())
return Task.FromResult<DragInfo?>(null);
var data = _dataFactory();
if (data == null)
return Task.FromResult<DragInfo?>(null);
var dragInfo = new DragInfo(data, _allowedEffects, startPosition, this);
return Task.FromResult<DragInfo?>(dragInfo);
}
public Task OnDragCompletedAsync(DragInfo dragInfo, DragDropEffects effects, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public Task OnDragCancelledAsync(DragInfo dragInfo, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
}
internal class CollectionDragSource<T> : IDragSource
{
private readonly IEnumerable<T> _items;
private readonly Func<IEnumerable<T>, T> _itemSelector;
private readonly DragDropEffects _allowedEffects;
public CollectionDragSource(IEnumerable<T> items, Func<IEnumerable<T>, T> itemSelector, DragDropEffects allowedEffects)
{
_items = items ?? throw new ArgumentNullException(nameof(items));
_itemSelector = itemSelector ?? throw new ArgumentNullException(nameof(itemSelector));
_allowedEffects = allowedEffects;
}
public Task<DragInfo?> TryStartDragAsync(Point startPosition, CancellationToken cancellationToken = default)
{
var selectedItem = _itemSelector(_items);
if (selectedItem == null)
return Task.FromResult<DragInfo?>(null);
var dragInfo = new DragInfo(selectedItem, _allowedEffects, startPosition, this);
return Task.FromResult<DragInfo?>(dragInfo);
}
public Task OnDragCompletedAsync(DragInfo dragInfo, DragDropEffects effects, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public Task OnDragCancelledAsync(DragInfo dragInfo, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
}
internal class SimpleDropTarget : IDropTarget
{
private readonly Action<DropInfo> _onDrop;
public SimpleDropTarget(Action<DropInfo> onDrop)
{
_onDrop = onDrop ?? throw new ArgumentNullException(nameof(onDrop));
}
public Task<bool> CanAcceptDropAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
{
return Task.FromResult(dropInfo.Data != null);
}
public Task OnDragOverAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
{
if (dropInfo.Data != null)
{
dropInfo.SuggestedEffects = DragDropEffects.Move;
}
return Task.CompletedTask;
}
public Task OnDropAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
{
if (dropInfo.Data != null)
{
_onDrop(dropInfo);
dropInfo.MarkAsHandled();
}
return Task.CompletedTask;
}
public Task OnDragLeaveAsync(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
}
internal class TypedDropTarget<T> : IDropTarget where T : class
{
private readonly Action<T, DropInfo> _onDrop;
public TypedDropTarget(Action<T, DropInfo> onDrop)
{
_onDrop = onDrop ?? throw new ArgumentNullException(nameof(onDrop));
}
public Task<bool> CanAcceptDropAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
{
return Task.FromResult(dropInfo.Data is T);
}
public Task OnDragOverAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
{
if (dropInfo.Data is T)
{
dropInfo.SuggestedEffects = DragDropEffects.Move;
}
return Task.CompletedTask;
}
public Task OnDropAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
{
if (dropInfo.Data is T data)
{
_onDrop(data, dropInfo);
dropInfo.MarkAsHandled();
}
return Task.CompletedTask;
}
public Task OnDragLeaveAsync(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
}
internal class ConditionalDropTarget : IDropTarget
{
private readonly Func<DropInfo, bool> _canAccept;
private readonly Action<DropInfo> _onDrop;
public ConditionalDropTarget(Func<DropInfo, bool> canAccept, Action<DropInfo> onDrop)
{
_canAccept = canAccept ?? throw new ArgumentNullException(nameof(canAccept));
_onDrop = onDrop ?? throw new ArgumentNullException(nameof(onDrop));
}
public Task<bool> CanAcceptDropAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
{
return Task.FromResult(_canAccept(dropInfo));
}
public Task OnDragOverAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
{
if (_canAccept(dropInfo))
{
dropInfo.SuggestedEffects = DragDropEffects.Move;
}
return Task.CompletedTask;
}
public Task OnDropAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
{
if (_canAccept(dropInfo))
{
_onDrop(dropInfo);
dropInfo.MarkAsHandled();
}
return Task.CompletedTask;
}
public Task OnDragLeaveAsync(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
}
#endregion

View File

@@ -1,20 +0,0 @@
<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>

View File

@@ -1,247 +0,0 @@
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));
// Проверка допустимых значений перечисления
if (!Enum.IsDefined(typeof(Enums.DragDropEffects), allowedEffects))
{
throw new ArgumentException(
$"Недопустимое значение для {nameof(allowedEffects)}: {allowedEffects}",
nameof(allowedEffects));
}
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)
{
_disposed = false,
};
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;
foreach (var value in _parameters.Values)
{
if (value is IDisposable disposable)
{
disposable.Dispose();
}
}
_parameters.Clear();
_disposed = true;
GC.SuppressFinalize(this);
}
private void ThrowIfDisposed()
{
if (_disposed)
throw new ObjectDisposedException(nameof(DragInfo));
}
~DragInfo()
{
Dispose();
}
}

View File

@@ -1,281 +0,0 @@
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;
/// <summary>
/// Получает или задает позицию сброса относительно цели.
/// </summary>
public DropPosition DropPosition { get; set; } = DropPosition.Inside;
/// <summary>
/// Получает или задает значение, указывающее, нужно ли показывать визуальную обратную связь.
/// </summary>
public bool ShowVisualFeedback { get; set; } = true;
/// <summary>
/// Получает или задает данные для визуальной обратной связи.
/// </summary>
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;
}
}

View File

@@ -1,829 +0,0 @@
# 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/ # Аргументы событий
```
## 🚀 Быстрый старт
### 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. **Инструменты разработчика** (дебаггер, профилировщик)

View File

@@ -1,846 +0,0 @@
using Lattice.Core.DragDrop.Abstractions;
using Lattice.Core.DragDrop.Constants;
using Lattice.Core.DragDrop.Enums;
using Lattice.Core.DragDrop.Models;
using Lattice.Core.Geometry;
namespace Lattice.Core.DragDrop.Services;
/// <summary>
/// Центральный сервис управления операциями перетаскивания.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="DragDropService"/> является основным компонентом системы drag-and-drop,
/// который координирует взаимодействие между источниками данных (<see cref="Abstractions.IDragSource"/>)
/// и целями сброса (<see cref="Abstractions.IDropTarget"/>).
/// </para>
/// <para>
/// Основные функции сервиса:
/// </para>
/// <list type="bullet">
/// <item>Регистрация и управление целями сброса</item>
/// <item>Оркестрация жизненного цикла операций перетаскивания</item>
/// <item>Обработка событий мыши и клавиатуры</item>
/// <item>Распространение информации между компонентами</item>
/// <item>Обеспечение потокобезопасности операций</item>
/// </list>
/// <para>
/// Сервис поддерживает полностью асинхронную модель работы, уведомления через события
/// и статистику использования. Все операции защищены от параллельного доступа
/// и обеспечивают корректную очистку ресурсов.
/// </para>
/// <para>
/// Для использования сервиса необходимо:
/// <list type="number">
/// <item>Зарегистрировать цели сброса с помощью <see cref="RegisterDropTarget"/></item>
/// <item>Вызывать методы <see cref="StartDragAsync"/>, <see cref="UpdateDragAsync"/>,
/// <see cref="EndDragAsync"/> в ответ на действия пользователя</item>
/// <item>Подписаться на события для отслеживания состояния операций</item>
/// </list>
/// </para>
/// </remarks>
public sealed class DragDropService : IDragDropService
{
#region Nested Types
/// <summary>
/// Информация о зарегистрированной цели сброса.
/// </summary>
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();
}
}
/// <summary>
/// Контекст текущей операции перетаскивания.
/// </summary>
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 Point LastPosition { 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
/// <inheritdoc/>
public bool IsDragActive => Volatile.Read(ref _currentDragOperation) != null;
/// <inheritdoc/>
public Models.DragInfo? CurrentDragInfo => _currentDragOperation?.DragInfo;
/// <inheritdoc/>
public Abstractions.IDropTarget? CurrentDropTarget => _currentDragOperation?.CurrentDropTarget;
/// <inheritdoc/>
public double DragStartThreshold { get; set; } = DragDropConstants.DefaultDragThreshold;
/// <inheritdoc/>
public bool EnableAsyncOperations { get; set; } = true;
/// <inheritdoc/>
public int AsyncOperationTimeout { get; set; } = DragDropConstants.DefaultAsyncTimeout;
/// <inheritdoc/>
public event EventHandler<DragStartedEventArgs> DragStarted
{
add => _dragStarted += value;
remove => _dragStarted -= value;
}
/// <inheritdoc/>
public event EventHandler<DragUpdatedEventArgs> DragUpdated
{
add => _dragUpdated += value;
remove => _dragUpdated -= value;
}
/// <inheritdoc/>
public event EventHandler<DropTargetChangedEventArgs> DropTargetChanged
{
add => _dropTargetChanged += value;
remove => _dropTargetChanged -= value;
}
/// <inheritdoc/>
public event EventHandler<DragCompletedEventArgs> DragCompleted
{
add => _dragCompleted += value;
remove => _dragCompleted -= value;
}
/// <inheritdoc/>
public event EventHandler<DragCancelledEventArgs> DragCancelled
{
add => _dragCancelled += value;
remove => _dragCancelled -= value;
}
/// <inheritdoc/>
public event EventHandler<DragDropErrorEventArgs> ErrorOccurred
{
add => _errorOccurred += value;
remove => _errorOccurred -= value;
}
#endregion
#region Constructor
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragDropService"/>.
/// </summary>
/// <remarks>
/// Создает сервис с настройками по умолчанию:
/// <list type="bullet">
/// <item>Порог начала перетаскивания: <see cref="Constants.DragDropConstants.DefaultDragThreshold"/> пикселей</item>
/// <item>Таймаут асинхронных операций: <see cref="Constants.DragDropConstants.DefaultAsyncTimeout"/> миллисекунд</item>
/// <item>Включены асинхронные операции: true</item>
/// </list>
/// </remarks>
public DragDropService()
{
// Инициализация таймера очистки (каждые 5 минут)
_cleanupTimer = new Timer(CleanupExpiredTargets, null,
TimeSpan.FromMinutes(Constants.DragDropConstants.TargetLifetimeMinutes / 2),
TimeSpan.FromMinutes(Constants.DragDropConstants.TargetLifetimeMinutes / 2));
}
#endregion
#region Registration Methods
/// <inheritdoc/>
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();
}
}
/// <inheritdoc/>
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();
}
}
/// <inheritdoc/>
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();
}
}
/// <inheritdoc/>
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
/// <inheritdoc/>
public async Task<bool> StartDragAsync(IDragSource source, Point startPosition)
{
ThrowIfDisposed();
if (source == null) throw new ArgumentNullException(nameof(source));
lock (_dragOperationLock)
{
if (_currentDragOperation != null)
return false;
}
try
{
Interlocked.Increment(ref _totalDragOperations);
DragInfo? dragInfo;
// Пытаемся начать перетаскивание
if (EnableAsyncOperations)
{
dragInfo = await ExecuteWithTimeoutAsync(
source.TryStartDragAsync(startPosition),
"TryStartDragAsync",
source);
}
else
{
dragInfo = await source.TryStartDragAsync(startPosition);
}
if (dragInfo == null)
return false;
var updatedDragInfo = dragInfo.CloneWithPosition(startPosition);
lock (_dragOperationLock)
{
_currentDragOperation = new DragOperationContext
{
Source = source,
DragInfo = updatedDragInfo,
CancellationTokenSource = new CancellationTokenSource(),
LastPosition = startPosition
};
}
// Вызов события
_dragStarted?.Invoke(this, new DragStartedEventArgs(updatedDragInfo, startPosition));
return true;
}
catch (Exception ex)
{
Interlocked.Increment(ref _errorCount);
HandleError(ex, "StartDragAsync", source);
return false;
}
}
/// <inheritdoc/>
public async Task UpdateDragAsync(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;
context.LastPosition = position;
// Поиск новой цели сброса
var newDropTarget = await FindDropTargetAsync(position, updatedDragInfo);
// Обработка смены цели
if (context.CurrentDropTarget != newDropTarget?.Target)
{
if (context.CurrentDropTarget != null)
{
await ExecuteTargetOperationAsync(
context.CurrentDropTarget,
t => t.OnDragLeaveAsync(),
"OnDragLeave");
}
context.CurrentDropTarget = newDropTarget?.Target;
if (newDropTarget != null)
{
newDropTarget.UsageCount++;
_dropTargetChanged?.Invoke(this, new DropTargetChangedEventArgs(
updatedDragInfo, position, newDropTarget.Target, newDropTarget.Bounds));
}
}
// Уведомление текущей цели
if (context.CurrentDropTarget != null)
{
var dropInfo = new DropInfo(
updatedDragInfo.Data,
position,
updatedDragInfo.AllowedEffects,
context.CurrentDropTarget);
await ExecuteTargetOperationAsync(
context.CurrentDropTarget,
t => t.OnDragOverAsync(dropInfo),
"OnDragOver");
}
_dragUpdated?.Invoke(this, new DragUpdatedEventArgs(updatedDragInfo, position));
}
catch (Exception ex)
{
Interlocked.Increment(ref _errorCount);
HandleError(ex, "UpdateDragAsync", context);
}
}
/// <inheritdoc/>
public async Task<DragDropEffects> EndDragAsync(Point position)
{
ThrowIfDisposed();
DragOperationContext? context;
lock (_dragOperationLock)
{
context = _currentDragOperation;
_currentDragOperation = null;
}
if (context == null || context.DragInfo == null || context.Source == null)
{
Reset();
return DragDropEffects.None;
}
try
{
var effects = DragDropEffects.None;
var operationTime = DateTime.UtcNow - context.StartTime;
Interlocked.Add(ref _totalOperationTicks, operationTime.Ticks);
// Выполнение сброса
if (context.CurrentDropTarget != null)
{
var dropInfo = new DropInfo(
context.DragInfo.Data,
position,
context.DragInfo.AllowedEffects,
context.CurrentDropTarget);
await ExecuteTargetOperationAsync(
context.CurrentDropTarget,
t => t.OnDropAsync(dropInfo),
"OnDrop");
if (dropInfo.Handled)
{
effects = dropInfo.SuggestedEffects;
Interlocked.Increment(ref _successfulDrops);
}
}
// Уведомление источника
await ExecuteSourceOperationAsync(
context.Source,
s => s.OnDragCompletedAsync(context.DragInfo, effects),
"OnDragCompleted");
// Событие завершения
_dragCompleted?.Invoke(this, new DragCompletedEventArgs(
context.DragInfo, position, effects));
return effects;
}
catch (Exception ex)
{
Interlocked.Increment(ref _errorCount);
HandleError(ex, "EndDragAsync", context);
return DragDropEffects.None;
}
finally
{
context.Dispose();
}
}
/// <inheritdoc/>
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.OnDragCancelledAsync(context.DragInfo),
"OnDragCancelled");
_dragCancelled?.Invoke(this, new DragCancelledEventArgs(context.DragInfo, context.LastPosition));
}
catch (Exception ex)
{
Interlocked.Increment(ref _errorCount);
HandleError(ex, "CancelDragAsync", context);
}
finally
{
context.Dispose();
}
}
#endregion
#region Utility Methods
/// <inheritdoc/>
public void ClearAllDropTargets()
{
ThrowIfDisposed();
_dropTargetsLock.EnterWriteLock();
try
{
foreach (var info in _dropTargets.Values)
{
info.Dispose();
}
_dropTargets.Clear();
}
finally
{
_dropTargetsLock.ExitWriteLock();
}
}
/// <inheritdoc/>
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
{
// Фильтруем цели по границам и сортируем по приоритету
var candidates = _dropTargets.Values
.Where(info => info.Bounds.Contains(position))
.OrderByDescending(info => info.Priority)
.ToList();
foreach (var info in candidates)
{
var dropInfo = new Models.DropInfo(
dragInfo.Data,
position,
dragInfo.AllowedEffects,
info.Target);
bool canAccept = await ExecuteWithTimeoutAsync(
info.Target.CanAcceptDropAsync(dropInfo),
"CanAcceptDropAsync",
info.Target);
if (canAccept)
{
info.LastAccessTime = DateTime.UtcNow;
bestTarget = info;
break; // Берем первую подходящую с наивысшим приоритетом
}
}
}
finally
{
_dropTargetsLock.ExitReadLock();
}
return bestTarget;
}
private async Task ExecuteTargetOperationAsync(
IDropTarget target,
Func<IDropTarget, Task> operation,
string operationName)
{
try
{
if (EnableAsyncOperations)
{
await ExecuteWithTimeoutAsync(
operation(target),
$"{operationName}Async",
target);
}
else
{
await operation(target);
}
}
catch (Exception ex)
{
Interlocked.Increment(ref _errorCount);
HandleError(ex, operationName, target);
}
}
private async Task ExecuteSourceOperationAsync(
IDragSource source,
Func<IDragSource, Task> operation,
string operationName)
{
try
{
if (EnableAsyncOperations)
{
await ExecuteWithTimeoutAsync(
operation(source),
$"{operationName}Async",
source);
}
else
{
await operation(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(-Constants.DragDropConstants.TargetLifetimeMinutes);
_dropTargetsLock.EnterWriteLock();
try
{
var idsToRemove = new List<string>();
var currentTarget = _currentDragOperation?.CurrentDropTarget;
foreach (var kvp in _dropTargets)
{
if (kvp.Value.LastAccessTime < expirationTime && !ReferenceEquals(kvp.Value.Target, currentTarget))
{
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;
var timer = Interlocked.Exchange(ref _cleanupTimer, null);
timer?.Dispose();
_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
}

View File

@@ -1,237 +0,0 @@
using Lattice.Core.DragDrop.Models;
using Lattice.Core.Geometry;
namespace Lattice.Core.DragDrop.Services;
/// <summary>
/// Предоставляет базовые данные для событий перетаскивания.
/// </summary>
/// <remarks>
/// Этот класс содержит общие свойства, которые используются в большинстве событий
/// системы перетаскивания. Является базовым классом для специализированных событий.
/// </remarks>
public abstract class DragEventArgs : EventArgs
{
/// <summary>
/// Получает информацию о текущей операции перетаскивания.
/// </summary>
/// <value>
/// Объект <see cref="DragInfo"/>, содержащий данные, эффекты и метаданные операции.
/// Всегда возвращает актуальную информацию на момент возникновения события.
/// </value>
public DragInfo DragInfo { get; }
/// <summary>
/// Получает текущую позицию курсора в координатах экрана.
/// </summary>
/// <value>
/// Точка, представляющая положение курсора мыши в момент события.
/// Используется для точного позиционирования и визуальной обратной связи.
/// </value>
public Point Position { get; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragEventArgs"/>.
/// </summary>
/// <param name="dragInfo">Информация о перетаскивании.</param>
/// <param name="position">Текущая позиция курсора.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="dragInfo"/> равен null.
/// </exception>
protected DragEventArgs(DragInfo dragInfo, Point position)
{
DragInfo = dragInfo ?? throw new ArgumentNullException(nameof(dragInfo));
Position = position;
}
}
/// <summary>
/// Предоставляет данные для события начала перетаскивания.
/// </summary>
/// <remarks>
/// Возникает, когда пользователь начинает операцию перетаскивания.
/// Это первое событие в жизненном цикле операции.
/// </remarks>
public sealed class DragStartedEventArgs : DragEventArgs
{
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragStartedEventArgs"/>.
/// </summary>
/// <param name="dragInfo">Информация о перетаскивании.</param>
/// <param name="position">Начальная позиция перетаскивания.</param>
public DragStartedEventArgs(DragInfo dragInfo, Point position)
: base(dragInfo, position)
{
}
}
/// <summary>
/// Предоставляет данные для события обновления позиции перетаскивания.
/// </summary>
/// <remarks>
/// Возникает при каждом перемещении курсора во время операции перетаскивания.
/// Может вызываться многократно с высокой частотой, поэтому обработчики
/// должны быть оптимизированы для производительности.
/// </remarks>
public sealed class DragUpdatedEventArgs : DragEventArgs
{
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragUpdatedEventArgs"/>.
/// </summary>
/// <param name="dragInfo">Информация о перетаскивании.</param>
/// <param name="position">Текущая позиция курсора.</param>
public DragUpdatedEventArgs(DragInfo dragInfo, Point position)
: base(dragInfo, position)
{
}
}
/// <summary>
/// Предоставляет данные для события завершения перетаскивания.
/// </summary>
/// <remarks>
/// Возникает, когда пользователь завершает операцию перетаскивания
/// (отпускает кнопку мыши над целью или вне области сброса).
/// Содержит информацию о примененных эффектах и результатах операции.
/// </remarks>
public sealed class DragCompletedEventArgs : DragEventArgs
{
/// <summary>
/// Получает эффекты, примененные при завершении операции.
/// </summary>
/// <value>
/// Комбинация флагов <see cref="Enums.DragDropEffects"/>, указывающая,
/// как были обработаны данные (копирование, перемещение и т.д.).
/// </value>
public Enums.DragDropEffects Effects { get; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragCompletedEventArgs"/>.
/// </summary>
/// <param name="dragInfo">Информация о перетаскивании.</param>
/// <param name="position">Позиция завершения операции.</param>
/// <param name="effects">Примененные эффекты перетаскивания.</param>
public DragCompletedEventArgs(DragInfo dragInfo, Point position, Enums.DragDropEffects effects)
: base(dragInfo, position)
{
Effects = effects;
}
}
/// <summary>
/// Предоставляет данные для события отмены перетаскивания.
/// </summary>
/// <remarks>
/// Возникает, когда операция перетаскивания была отменена пользователем
/// (например, нажатием клавиши Escape) или системой (например, при ошибке).
/// После этого события система возвращается в исходное состояние.
/// </remarks>
public sealed class DragCancelledEventArgs : DragEventArgs
{
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragCancelledEventArgs"/>.
/// </summary>
/// <param name="dragInfo">Информация о перетаскивании.</param>
/// <param name="position">Позиция в момент отмены.</param>
public DragCancelledEventArgs(DragInfo dragInfo, Point position)
: base(dragInfo, position)
{
}
}
/// <summary>
/// Предоставляет данные для события изменения цели сброса.
/// </summary>
/// <remarks>
/// Возникает, когда курсор перемещается с одной цели сброса на другую
/// или покидает область всех целей. Позволяет обновлять визуальную
/// обратную связь при изменении контекста сброса.
/// </remarks>
public sealed class DropTargetChangedEventArgs : DragEventArgs
{
/// <summary>
/// Получает новую цель сброса, над которой находится курсор.
/// </summary>
/// <value>
/// Объект <see cref="Abstractions.IDropTarget"/>, готовый принять данные,
/// или null, если курсор покинул область всех целей.
/// </value>
public Abstractions.IDropTarget? Target { get; }
/// <summary>
/// Получает границы новой цели сброса.
/// </summary>
/// <value>
/// Прямоугольник, определяющий область цели в координатах экрана.
/// Может использоваться для точного позиционирования визуальной обратной связи.
/// </value>
public Rect TargetBounds { get; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DropTargetChangedEventArgs"/>.
/// </summary>
/// <param name="dragInfo">Информация о перетаскивании.</param>
/// <param name="position">Текущая позиция курсора.</param>
/// <param name="target">Новая цель сброса.</param>
/// <param name="targetBounds">Границы цели сброса.</param>
public DropTargetChangedEventArgs(DragInfo dragInfo, Point position, Abstractions.IDropTarget? target, Rect targetBounds)
: base(dragInfo, position)
{
Target = target;
TargetBounds = targetBounds;
}
}
/// <summary>
/// Предоставляет данные для события ошибки в операции перетаскивания.
/// </summary>
/// <remarks>
/// Возникает при возникновении исключения в любом из компонентов
/// системы перетаскивания. Позволяет централизованно обрабатывать ошибки
/// и предоставлять пользователю информацию о проблемах.
/// </remarks>
public sealed class DragDropErrorEventArgs : EventArgs
{
/// <summary>
/// Получает исключение, вызвавшее ошибку.
/// </summary>
/// <value>
/// Объект <see cref="Exception"/>, содержащий информацию об ошибке.
/// Может быть любого типа, в зависимости от источника ошибки.
/// </value>
public Exception Exception { get; }
/// <summary>
/// Получает название операции, во время которой произошла ошибка.
/// </summary>
/// <value>
/// Строка, идентифицирующая операцию (например, "StartDragAsync",
/// "OnDropAsync", "UpdateDropTargetBounds").
/// </value>
public string Operation { get; }
/// <summary>
/// Получает контекст, в котором произошла ошибка.
/// </summary>
/// <value>
/// Объект, содержащий дополнительную информацию о контексте ошибки,
/// или null, если контекст недоступен.
/// </value>
public object? Context { get; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragDropErrorEventArgs"/>.
/// </summary>
/// <param name="exception">Исключение, вызвавшее ошибку.</param>
/// <param name="operation">Название операции.</param>
/// <param name="context">Контекст ошибки.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="exception"/> или <paramref name="operation"/> равны null.
/// </exception>
public DragDropErrorEventArgs(Exception exception, string operation, object? context = null)
{
Exception = exception ?? throw new ArgumentNullException(nameof(exception));
Operation = operation ?? throw new ArgumentNullException(nameof(operation));
Context = context;
}
}

View File

@@ -1,240 +0,0 @@
namespace Lattice.Core.DragDrop.Services;
/// <summary>
/// Предоставляет централизованный сервис для управления операциями перетаскивания.
/// </summary>
public interface IDragDropService : IDisposable
{
#region Свойства
/// <summary>
/// Получает значение, указывающее, активна ли операция перетаскивания.
/// </summary>
/// <value>true, если операция перетаскивания активна; в противном случае — false.</value>
bool IsDragActive { get; }
/// <summary>
/// Получает информацию о текущей операции перетаскивания.
/// </summary>
/// <value>
/// Объект <see cref="Models.DragInfo"/>, содержащий данные текущей операции,
/// или null, если операция не активна.
/// </value>
Models.DragInfo? CurrentDragInfo { get; }
/// <summary>
/// Получает текущую цель сброса.
/// </summary>
/// <value>
/// Объект <see cref="Abstractions.IDropTarget"/>, над которым находится курсор,
/// или null, если курсор не над зарегистрированной целью.
/// </value>
Abstractions.IDropTarget? CurrentDropTarget { get; }
/// <summary>
/// Получает или задает порог начала перетаскивания в пикселях.
/// </summary>
/// <value>
/// Минимальное расстояние, которое должен пройти курсор мыши, чтобы начать операцию перетаскивания.
/// Значение по умолчанию: <see cref="Constants.DragDropConstants.DefaultDragThreshold"/>.
/// </value>
double DragStartThreshold { get; set; }
/// <summary>
/// Получает или задает значение, указывающее, включены ли асинхронные операции.
/// </summary>
/// <value>true, если асинхронные операции включены; в противном случае — false.</value>
bool EnableAsyncOperations { get; set; }
/// <summary>
/// Получает или задает максимальное время ожидания асинхронной операции в миллисекундах.
/// </summary>
/// <value>
/// Время ожидания в миллисекундах. Значение 0 или меньше означает отсутствие таймаута.
/// Значение по умолчанию: <see cref="Constants.DragDropConstants.DefaultAsyncTimeout"/>.
/// </value>
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>
/// <param name="target">Цель сброса для регистрации.</param>
/// <param name="bounds">Границы области цели в координатах экрана.</param>
/// <param name="priority">Приоритет цели (высшие значения обрабатываются первыми).</param>
/// <param name="group">Имя группы для групповой отмены регистрации.</param>
/// <returns>Уникальный идентификатор зарегистрированной цели.</returns>
/// <exception cref="ArgumentNullException">Выбрасывается, когда <paramref name="target"/> равен null.</exception>
/// <exception cref="ObjectDisposedException">Выбрасывается, если сервис был удален.</exception>
string RegisterDropTarget(Abstractions.IDropTarget target, Geometry.Rect bounds, int priority = 0, string? group = null);
/// <summary>
/// Обновляет границы цели сброса.
/// </summary>
/// <param name="id">Идентификатор цели сброса.</param>
/// <param name="bounds">Новые границы области цели.</param>
/// <returns>true, если границы успешно обновлены; в противном случае — false.</returns>
/// <exception cref="ObjectDisposedException">Выбрасывается, если сервис был удален.</exception>
bool UpdateDropTargetBounds(string id, Geometry.Rect bounds);
/// <summary>
/// Отменяет регистрацию цели сброса.
/// </summary>
/// <param name="id">Идентификатор цели сброса.</param>
/// <returns>true, если цель успешно удалена; в противном случае — false.</returns>
/// <exception cref="ObjectDisposedException">Выбрасывается, если сервис был удален.</exception>
bool UnregisterDropTarget(string id);
/// <summary>
/// Отменяет регистрацию всех целей сброса в указанной группе.
/// </summary>
/// <param name="group">Имя группы для удаления.</param>
/// <exception cref="ObjectDisposedException">Выбрасывается, если сервис был удален.</exception>
void UnregisterDropTargetsInGroup(string group);
#endregion
#region Асинхронные операции
/// <summary>
/// Начинает операцию перетаскивания из указанной позиции.
/// </summary>
/// <param name="source">Источник данных для перетаскивания.</param>
/// <param name="startPosition">Начальная позиция операции в координатах экрана.</param>
/// <returns>
/// Задача, представляющая асинхронную операцию. Результат содержит true, если операция успешно начата;
/// в противном случае — false.
/// </returns>
/// <exception cref="ArgumentNullException">Выбрасывается, когда <paramref name="source"/> равен null.</exception>
/// <exception cref="ObjectDisposedException">Выбрасывается, если сервис был удален.</exception>
/// <remarks>
/// Этот метод следует вызывать в ответ на событие нажатия кнопки мыши или начала жеста перетаскивания.
/// </remarks>
Task<bool> StartDragAsync(Abstractions.IDragSource source, Geometry.Point startPosition);
/// <summary>
/// Обновляет позицию текущей операции перетаскивания.
/// </summary>
/// <param name="position">Новая позиция курсора в координатах экрана.</param>
/// <returns>Задача, представляющая асинхронную операцию.</returns>
/// <exception cref="ObjectDisposedException">Выбрасывается, если сервис был удален.</exception>
/// <remarks>
/// Этот метод следует вызывать при каждом перемещении мыши во время операции перетаскивания.
/// </remarks>
Task UpdateDragAsync(Geometry.Point position);
/// <summary>
/// Завершает текущую операцию перетаскивания в указанной позиции.
/// </summary>
/// <param name="position">Позиция завершения операции в координатах экрана.</param>
/// <returns>
/// Задача, представляющая асинхронную операцию. Результат содержит эффекты, примененные при завершении операции.
/// </returns>
/// <exception cref="ObjectDisposedException">Выбрасывается, если сервис был удален.</exception>
/// <remarks>
/// Этот метод следует вызывать при отпускании кнопки мыши или завершении жеста перетаскивания.
/// </remarks>
Task<Enums.DragDropEffects> EndDragAsync(Geometry.Point position);
/// <summary>
/// Отменяет текущую операцию перетаскивания.
/// </summary>
/// <returns>Задача, представляющая асинхронную операцию.</returns>
/// <exception cref="ObjectDisposedException">Выбрасывается, если сервис был удален.</exception>
/// <remarks>
/// Этот метод следует вызывать при отмене операции пользователем (например, нажатием клавиши Escape)
/// или при возникновении ошибки.
/// </remarks>
Task CancelDragAsync();
#endregion
#region Утилиты
/// <summary>
/// Очищает все зарегистрированные цели сброса.
/// </summary>
/// <exception cref="ObjectDisposedException">Выбрасывается, если сервис был удален.</exception>
void ClearAllDropTargets();
/// <summary>
/// Получает статистику использования системы перетаскивания.
/// </summary>
/// <returns>Объект <see cref="DragDropStats"/> со статистикой использования.</returns>
DragDropStats GetStats();
#endregion
}
/// <summary>
/// Содержит статистику использования системы перетаскивания.
/// </summary>
public class DragDropStats
{
/// <summary>
/// Получает или задает общее количество операций перетаскивания.
/// </summary>
public int TotalDragOperations { get; set; }
/// <summary>
/// Получает или задает количество успешных сбросов.
/// </summary>
public int SuccessfulDrops { get; set; }
/// <summary>
/// Получает или задает количество отмененных операций.
/// </summary>
public int CancelledOperations { get; set; }
/// <summary>
/// Получает или задает количество ошибок.
/// </summary>
public int ErrorCount { get; set; }
/// <summary>
/// Получает или задает количество зарегистрированных целей сброса.
/// </summary>
public int RegisteredTargets { get; set; }
/// <summary>
/// Получает или задает среднее время операции перетаскивания.
/// </summary>
public TimeSpan AverageOperationTime { get; set; }
}

View File

@@ -0,0 +1,15 @@
<Application
x:Class="Lattice.Example.DragDrop.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Lattice.Example.DragDrop">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- Fluent Theme Resources -->
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,33 @@
using Lattice.Themes;
using Lattice.Themes.Fluent;
using Microsoft.UI.Xaml;
namespace Lattice.Example.DragDrop;
public partial class App : Application
{
private Window? _window;
public App()
{
InitializeComponent();
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
// Регистрируем Fluent тему
var themeManager = ThemeManager.Current;
themeManager.RegisterTheme(new FluentThemePack(false)); // Light тема
themeManager.RegisterTheme(new FluentThemePack(true)); // Dark тема
// Применяем тему по умолчанию
themeManager.ApplyTheme("Fluent Dark");
// Создаем главное окно
_window = new MainWindow();
_window.Activate();
// Регистрируем окно в трекере
WindowTracker.Register(_window);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,64 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>Lattice.Example.DragDrop</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
<UseWinUI>true</UseWinUI>
<WinUISDKReferences>false</WinUISDKReferences>
<EnableMsixTooling>true</EnableMsixTooling>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Content Include="Assets\SplashScreen.scale-200.png" />
<Content Include="Assets\LockScreenLogo.scale-200.png" />
<Content Include="Assets\Square150x150Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Assets\StoreLogo.png" />
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
</ItemGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<!--
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
Tools extension to be activated for this project even if the Windows App SDK Nuget
package has not yet been restored.
-->
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260101001" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Lattice.Themes.Core\Lattice.Themes.Core.csproj" />
<ProjectReference Include="..\Lattice.Themes.Fluent\Lattice.Themes.Fluent.csproj" />
</ItemGroup>
<!--
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
Explorer "Package and Publish" context menu entry to be enabled for this project even if
the Windows App SDK Nuget package has not yet been restored.
-->
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
</PropertyGroup>
<!-- Publish Properties -->
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,141 @@
<Window
x:Class="Lattice.Example.DragDrop.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:lattice="using:Lattice.UI.DragDrop.WinUI"
Title="Drag Drop Demo"
>
<Grid Background="#F0F2F5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Инструкция -->
<TextBlock Grid.Row="0"
Text="Просто перетащите элементы справа влево!"
FontSize="14" Margin="20" HorizontalAlignment="Center"
FontWeight="SemiBold"/>
<!-- Основное содержимое -->
<Grid Grid.Row="1" Margin="20">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- ЦЕЛЕВАЯ ЗОНА (куда бросаем) -->
<Border Grid.Column="0"
Background="White"
CornerRadius="10"
BorderThickness="2"
BorderBrush="#4CAF50"
Padding="20"
Margin="0,0,10,0"
lattice:DragDropProperties.IsDropTarget="True">
<StackPanel>
<TextBlock Text="🟢 ЦЕЛЕВАЯ ЗОНА"
FontSize="16" FontWeight="Bold"
Foreground="#4CAF50"
Margin="0,0,0,15"/>
<TextBlock x:Name="DropInfoText"
Text="Бросьте сюда элементы"
FontSize="14"
Foreground="#666"
TextWrapping="Wrap"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</StackPanel>
</Border>
<!-- ЗОНА С ЭЛЕМЕНТАМИ (откуда тянем) -->
<StackPanel Grid.Column="1"
Background="White"
CornerRadius="10"
Padding="20"
Margin="10,0,0,0">
<TextBlock Text="📦 ЭЛЕМЕНТЫ ДЛЯ ПЕРЕТАСКИВАНИЯ"
FontSize="16" FontWeight="Bold"
Foreground="#2196F3"
Margin="0,0,0,15"/>
<!-- 1. TextBlock элемент -->
<Border Padding="15"
Background="#E3F2FD"
CornerRadius="8"
BorderThickness="1"
BorderBrush="#90CAF9"
Margin="0,0,0,10"
lattice:DragDropProperties.IsDragSource="True"
lattice:DragDropProperties.DragData="TextBlock Element">
<TextBlock Text="📝 Это TextBlock"
FontSize="14"
Foreground="#1565C0"
FontWeight="SemiBold"/>
</Border>
<!-- 2. Border элемент -->
<Border Padding="15"
Background="#E8F5E9"
CornerRadius="8"
BorderThickness="1"
BorderBrush="#A5D6A7"
Margin="0,0,0,10"
lattice:DragDropProperties.IsDragSource="True"
lattice:DragDropProperties.DragData="Border Element">
<StackPanel>
<TextBlock Text="🟩 Это Border"
FontSize="14"
Foreground="#2E7D32"
FontWeight="SemiBold"/>
<TextBlock Text="С рамкой и заливкой"
FontSize="12"
Foreground="#666"
Margin="0,5,0,0"/>
</StackPanel>
</Border>
<!-- 3. Grid элемент -->
<Border Padding="15"
Background="#FFF3E0"
CornerRadius="8"
BorderThickness="1"
BorderBrush="#FFCC80"
lattice:DragDropProperties.IsDragSource="True"
lattice:DragDropProperties.DragData="Grid Element">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Иконка -->
<TextBlock Grid.Column="0"
Text="🔲"
FontSize="18"
Margin="0,0,10,0"
VerticalAlignment="Center"/>
<!-- Контент -->
<StackPanel Grid.Column="1">
<TextBlock Text="Это Grid"
FontSize="14"
Foreground="#EF6C00"
FontWeight="SemiBold"/>
<TextBlock Text="С несколькими колонками"
FontSize="12"
Foreground="#666"/>
</StackPanel>
</Grid>
</Border>
</StackPanel>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,17 @@
using Lattice.UI.DragDrop.WinUI;
using Microsoft.UI.Xaml;
namespace Lattice.Example.DragDrop;
public sealed partial class MainWindow : Window
{
private bool _isInitialized = false;
public MainWindow()
{
InitializeComponent();
XamlInitializer.Initialize(this);
Title = "✅ Drag & Drop работает! Перетащите элементы →";
}
}

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity
Name="dcfd6640-86d9-4ce7-bc17-24685f01b577"
Publisher="CN=frost"
Version="1.0.0.0" />
<mp:PhoneIdentity PhoneProductId="dcfd6640-86d9-4ce7-bc17-24685f01b577" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>Lattice.Example.DragDrop</DisplayName>
<PublisherDisplayName>frost</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="Lattice.Example.DragDrop"
Description="Lattice.Example.DragDrop"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -0,0 +1,10 @@
{
"profiles": {
"Lattice.Example.DragDrop (Package)": {
"commandName": "MsixPackage"
},
"Lattice.Example.DragDrop (Unpackaged)": {
"commandName": "Project"
}
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="Lattice.Example.DragDrop.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View File

@@ -2,7 +2,6 @@
using Lattice.Core.Docking.Engine;
using Lattice.Core.Docking.Models;
using Lattice.UI.Docking.Abstractions;
using Lattice.UI.Docking.Services;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
@@ -12,9 +11,22 @@ using System.Runtime.CompilerServices;
namespace Lattice.UI;
/// <summary>
/// Визуальный контрол для отображения группы разделения (сплиттера).
/// Реализует интерфейс <see cref="IDockGroupControl"/> для интеграции с системой докинга.
/// Визуальный контрол для отображения группы разделения (сплиттера) в системе докинга.
/// Реализует интерфейс <see cref="IDockGroupControl"/> для интеграции с системой докинга
/// и обеспечивает отображение двух дочерних элементов с разделителем между ними.
/// </summary>
/// <remarks>
/// <para>
/// Контрол <see cref="LatticeDockGroup"/> отвечает за визуальное представление узла
/// дерева компоновки, который разделяет доступное пространство между двумя дочерними
/// элементами. Поддерживает горизонтальное и вертикальное разделение с возможностью
/// изменения соотношения сторон через перетаскивание разделителя.
/// </para>
/// <para>
/// Контрол автоматически обновляет свое представление при изменении свойств модели
/// и обеспечивает двустороннюю привязку данных с объектом <see cref="DockGroup"/>.
/// </para>
/// </remarks>
public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
{
private readonly PropertyChangedEventHandler _modelPropertyChangedHandler;
@@ -24,18 +36,20 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
private ContentControl? _firstChildControl;
private ContentControl? _secondChildControl;
private LayoutManager? _layoutManager;
private DockDragDropService? _dragDropService;
private IDockContextManager? _contextManager;
private bool _isSelected;
private bool _isActive;
private bool _canDrag = true;
private bool _canDrop = true;
private double _splitRatio = 0.5;
private double _splitterSize = 4.0;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="LatticeDockGroup"/>.
/// </summary>
/// <remarks>
/// Конструктор устанавливает ключ стиля по умолчанию, инициализирует обработчик
/// изменений модели и подписывается на событие изменения контекста данных.
/// Созданный контрол готов к использованию после применения шаблона.
/// </remarks>
public LatticeDockGroup()
{
this.DefaultStyleKey = typeof(LatticeDockGroup);
@@ -43,7 +57,18 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
this.DataContextChanged += OnDataContextChanged;
}
/// <inheritdoc/>
/// <summary>
/// Получает или задает модель данных, связанную с этим контролом.
/// </summary>
/// <value>
/// Экземпляр <see cref="DockGroup"/>, представляющий узел разделения в дереве компоновки.
/// Может быть null, если контрол не связан с моделью.
/// </value>
/// <remarks>
/// При установке новой модели контрол автоматически подписывается на события
/// изменения свойств модели и обновляет свое визуальное представление.
/// При удалении модели происходит отписка от событий и очистка ресурсов.
/// </remarks>
public IDockElement? Model
{
get => _model;
@@ -57,7 +82,18 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <inheritdoc/>
/// <summary>
/// Получает или задает менеджер макета, к которому принадлежит этот контрол.
/// </summary>
/// <value>
/// Экземпляр <see cref="LayoutManager"/>, управляющий структурой док-системы.
/// Может быть null, если контрол не связан с менеджером макета.
/// </value>
/// <remarks>
/// Менеджер макета используется для выполнения операций с деревом компоновки,
/// таких как перемещение элементов, создание плавающих окон и управление
/// автоскрываемыми панелями.
/// </remarks>
public LayoutManager? LayoutManager
{
get => _layoutManager;
@@ -69,19 +105,17 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <inheritdoc/>
public IDockDragDropService? DragDropService
{
get => _dragDropService;
set
{
if (_dragDropService == value) return;
_dragDropService = value;
OnPropertyChanged(nameof(DragDropService));
}
}
/// <inheritdoc/>
/// <summary>
/// Получает или задает контекстный менеджер для этого контрола.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDockContextManager"/> или null, если менеджер не установлен.
/// </value>
/// <remarks>
/// Контекстный менеджер используется для отображения контекстных меню при щелчке
/// правой кнопкой мыши по контролу. Меню содержит команды, доступные для данного
/// элемента в текущем контексте.
/// </remarks>
public IDockContextManager? ContextManager
{
get => _contextManager;
@@ -93,7 +127,18 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <inheritdoc/>
/// <summary>
/// Получает или задает признак того, что контрол выбран.
/// </summary>
/// <value>
/// true, если контрол выбран; в противном случае false.
/// Значение по умолчанию: false.
/// </value>
/// <remarks>
/// Выделенный контрол обычно визуально отличается от других (например, имеет
/// выделенную границу или фон). В каждый момент времени может быть выделен
/// только один контрол в пределах контейнера.
/// </remarks>
public bool IsSelected
{
get => _isSelected;
@@ -105,7 +150,17 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <inheritdoc/>
/// <summary>
/// Получает или задает признак того, что контрол активен.
/// </summary>
/// <value>
/// true, если контрол активен; в противном случае false.
/// Значение по умолчанию: false.
/// </value>
/// <remarks>
/// Активный контрол получает фокус ввода и может обрабатывать команды клавиатуры.
/// Обычно соответствует последнему взаимодействию пользователя с элементом.
/// </remarks>
public bool IsActive
{
get => _isActive;
@@ -117,31 +172,20 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <inheritdoc/>
public bool CanDrag
{
get => _canDrag;
set
{
if (_canDrag == value) return;
_canDrag = value;
OnPropertyChanged(nameof(CanDrag));
}
}
/// <inheritdoc/>
public bool CanDrop
{
get => _canDrop;
set
{
if (_canDrop == value) return;
_canDrop = value;
OnPropertyChanged(nameof(CanDrop));
}
}
/// <inheritdoc/>
/// <summary>
/// Получает или задает ориентацию разделения группы.
/// </summary>
/// <value>
/// Направление разделения (горизонтальное или вертикальное).
/// </value>
/// <remarks>
/// Ориентация определяет, как расположены дочерние элементы относительно друг друга:
/// <list type="bullet">
/// <item><see cref="SplitDirection.Horizontal"/> - элементы расположены слева и справа</item>
/// <item><see cref="SplitDirection.Vertical"/> - элементы расположены сверху и снизу</item>
/// </list>
/// Изменение ориентации приводит к перестройке внутреннего макета контрола.
/// </remarks>
public SplitDirection Orientation
{
get => _model?.Orientation ?? SplitDirection.Horizontal;
@@ -155,7 +199,18 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <inheritdoc/>
/// <summary>
/// Получает или задает соотношение разделения между первым и вторым элементами.
/// </summary>
/// <value>
/// Значение от 0.0 до 1.0, где 0.5 означает равное разделение пространства.
/// Значение 0.0 отдает все пространство второму элементу, 1.0 - первому элементу.
/// </value>
/// <remarks>
/// Соотношение разделения определяет пропорции, в которых доступное пространство
/// распределяется между дочерними элементами. Изменение этого свойства приводит
/// к перестройке внутреннего макета и генерации события <see cref="SplitRatioChanged"/>.
/// </remarks>
public double SplitRatio
{
get => _splitRatio;
@@ -173,7 +228,17 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <inheritdoc/>
/// <summary>
/// Получает или задает размер разделителя в пикселях.
/// </summary>
/// <value>
/// Ширина разделителя в пикселях. Значение по умолчанию: 4.0.
/// </value>
/// <remarks>
/// Размер разделителя определяет область, доступную для перетаскивания пользователем
/// для изменения соотношения разделения. Увеличение размера облегчает взаимодействие,
/// но уменьшает полезное пространство для содержимого.
/// </remarks>
public double SplitterSize
{
get => _splitterSize;
@@ -187,19 +252,57 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <inheritdoc/>
/// <summary>
/// Получает контрол для первого дочернего элемента.
/// </summary>
/// <value>
/// Контрол, отображающий первый дочерний элемент, или null, если элемент не установлен.
/// </value>
/// <remarks>
/// Первый дочерний элемент занимает левую область при горизонтальной ориентации
/// или верхнюю область при вертикальной ориентации.
/// </remarks>
public IDockControl? FirstChild => _firstChildControl?.Content as IDockControl;
/// <inheritdoc/>
/// <summary>
/// Получает контрол для второго дочернего элемента.
/// </summary>
/// <value>
/// Контрол, отображающий второй дочерний элемент, или null, если элемент не установлен.
/// </value>
/// <remarks>
/// Второй дочерний элемент занимает правую область при горизонтальной ориентации
/// или нижнюю область при вертикальной ориентации.
/// </remarks>
public IDockControl? SecondChild => _secondChildControl?.Content as IDockControl;
/// <inheritdoc/>
/// <summary>
/// Происходит при изменении соотношения разделения между дочерними элементами.
/// </summary>
/// <remarks>
/// Событие генерируется при изменении свойства <see cref="SplitRatio"/>,
/// независимо от источника изменения (пользователь, программа или восстановление состояния).
/// Содержит информацию о новом соотношении и источнике изменения.
/// </remarks>
public event EventHandler<SplitRatioChangedEventArgs>? SplitRatioChanged;
/// <inheritdoc/>
/// <summary>
/// Происходит при изменении значения свойства.
/// </summary>
/// <remarks>
/// Событие реализует интерфейс <see cref="INotifyPropertyChanged"/> и используется
/// для уведомления системы привязки данных об изменениях свойств контрола.
/// </remarks>
public event PropertyChangedEventHandler? PropertyChanged;
/// <inheritdoc/>
/// <summary>
/// Вызывается при применении шаблона контрола.
/// </summary>
/// <remarks>
/// Метод получает ссылки на именованные части шаблона и инициализирует
/// внутренние структуры контрола. Вызывает обновление макета для корректного
/// отображения дочерних элементов.
/// </remarks>
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
@@ -211,11 +314,28 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
UpdateLayoutDefinitions();
}
/// <summary>
/// Обрабатывает изменение контекста данных контрола.
/// </summary>
/// <param name="sender">Источник события (контрол).</param>
/// <param name="args">Данные о изменении контекста.</param>
/// <remarks>
/// Метод автоматически устанавливает модель контрола на основе нового контекста данных,
/// если он является экземпляром <see cref="DockGroup"/>. Это позволяет использовать
/// привязку данных XAML для установки модели контрола.
/// </remarks>
private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
Model = args.NewValue as DockGroup;
}
/// <summary>
/// Присоединяет модель к контролу.
/// </summary>
/// <remarks>
/// Подписывается на события изменения свойств модели, устанавливает контекст данных
/// и инициализирует свойства контрола значениями из модели. Вызывает обновление макета.
/// </remarks>
private void AttachModel()
{
if (_model != null)
@@ -229,6 +349,13 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <summary>
/// Отсоединяет модель от контрола.
/// </summary>
/// <remarks>
/// Отписывается от событий изменения свойств модели, очищает контекст данных
/// и освобождает ресурсы, связанные с предыдущей моделью.
/// </remarks>
private void DetachModel()
{
if (_model != null)
@@ -238,6 +365,16 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <summary>
/// Обрабатывает изменения свойств модели.
/// </summary>
/// <param name="sender">Источник события (модель).</param>
/// <param name="e">Данные об изменении свойства.</param>
/// <remarks>
/// Реагирует на изменения ключевых свойств модели (Orientation, SplitRatio)
/// и обновляет соответствующие свойства и визуальное представление контрола.
/// Также уведомляет систему привязки данных об изменении свойств контрола.
/// </remarks>
private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
@@ -258,6 +395,14 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <summary>
/// Обновляет определения макета сетки на основе текущей ориентации и соотношения разделения.
/// </summary>
/// <remarks>
/// Метод перестраивает структуру строк и столбцов сетки в зависимости от ориентации
/// разделения и текущего соотношения между дочерними элементами. Обеспечивает
/// корректное позиционирование разделителя и дочерних контролов.
/// </remarks>
private void UpdateLayoutDefinitions()
{
if (_rootGrid == null || _model == null) return;
@@ -313,7 +458,15 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <inheritdoc/>
/// <summary>
/// Устанавливает дочерние контролы для отображения.
/// </summary>
/// <param name="firstChild">Контрол для первого элемента.</param>
/// <param name="secondChild">Контрол для второго элемента.</param>
/// <remarks>
/// Метод назначает контролы для визуального представления дочерних элементов группы.
/// После установки контролов обновляет макет для корректного отображения.
/// </remarks>
public void SetChildren(IDockControl? firstChild, IDockControl? secondChild)
{
if (_firstChildControl != null)
@@ -325,13 +478,27 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
UpdateLayoutDefinitions();
}
/// <inheritdoc/>
/// <summary>
/// Обновляет внешний вид контрола в соответствии с текущим состоянием модели.
/// </summary>
/// <remarks>
/// Вызывает перестройку макета сетки для синхронизации визуального представления
/// с текущими значениями свойств модели (ориентация, соотношение разделения).
/// </remarks>
public void Refresh()
{
UpdateLayoutDefinitions();
}
/// <inheritdoc/>
/// <summary>
/// Применяет указанную тему к контролу.
/// </summary>
/// <param name="theme">Тема для применения.</param>
/// <remarks>
/// Обновляет стили и параметры отображения контрола в соответствии с заданной темой.
/// В текущей реализации метод является заглушкой и должен быть расширен для
/// поддержки динамического изменения тем оформления.
/// </remarks>
public void ApplyTheme(IDockTheme theme)
{
// Применение темы к контролу
@@ -341,7 +508,14 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <inheritdoc/>
/// <summary>
/// Вызывается при изменении состояния модели для обновления UI.
/// </summary>
/// <param name="propertyName">Имя изменившегося свойства модели.</param>
/// <remarks>
/// Перенаправляет вызов в обработчик изменений модели, обеспечивая уведомление
/// контрола о конкретных изменениях в связанной модели данных.
/// </remarks>
public void OnModelPropertyChanged(string propertyName)
{
if (_model != null)
@@ -350,12 +524,27 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <summary>
/// Вызывает событие изменения свойства.
/// </summary>
/// <param name="propertyName">Имя изменившегося свойства.</param>
/// <remarks>
/// Используется для уведомления системы привязки данных об изменениях свойств
/// контрола. Если имя свойства не указано, автоматически определяется по имени
/// вызывающего члена.
/// </remarks>
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <inheritdoc/>
/// <summary>
/// Освобождает ресурсы, используемые этим экземпляром контрола.
/// </summary>
/// <remarks>
/// Выполняет отписку от событий модели, очистку ссылок и освобождение ресурсов.
/// После вызова этого метода контрол не должен использоваться.
/// </remarks>
public void Dispose()
{
if (!_disposed)

View File

@@ -1,6 +1,7 @@
using Lattice.Core.Docking.Abstractions;
using Lattice.Core.Docking.Engine;
using Lattice.Core.Docking.Models;
using Lattice.UI.Docking;
using Lattice.UI.Docking.Abstractions;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -39,7 +40,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
private bool _disposed;
private IDockElement? _model;
private LayoutManager? _layoutManager;
private IDockDragDropService? _dragDropService;
private IDockContextManager? _contextManager;
private bool _isSelected;
private bool _isActive;
@@ -109,27 +109,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
}
}
/// <summary>
/// Получает или задает сервис перетаскивания, используемый этим контролом.
/// </summary>
/// <value>
/// Реализация <see cref="IDockDragDropService"/> для обработки операций перетаскивания.
/// </value>
/// <remarks>
/// Сервис перетаскивания обеспечивает взаимодействие с системой drag-and-drop,
/// включая визуальную обратную связь и обработку событий.
/// </remarks>
public IDockDragDropService? DragDropService
{
get => _dragDropService;
set
{
if (_dragDropService == value) return;
_dragDropService = value;
OnPropertyChanged(nameof(DragDropService));
}
}
/// <summary>
/// Получает или задает контекстный менеджер для этого контрола.
/// </summary>

View File

@@ -1,9 +1,9 @@
using Lattice.Core.Docking.Abstractions;
using Lattice.Core.Docking.Engine;
using Lattice.Core.Docking.Models;
using Lattice.UI.Docking.Abstractions;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using System;
using System.Collections.Specialized;
@@ -14,9 +14,20 @@ using System.Runtime.CompilerServices;
namespace Lattice.UI;
/// <summary>
/// Кастомный контрол вкладок с поддержкой всех позиций размещения панели вкладок.
/// Представляет кастомный контрол вкладок с поддержкой всех позиций размещения панели вкладок.
/// Реализует интерфейс <see cref="IDockLeafControl"/> для интеграции с системой докинга.
/// </summary>
/// <remarks>
/// <para>
/// Контрол обеспечивает отображение коллекции вкладок с возможностью навигации между ними,
/// закрытия вкладок и изменения порядка. Поддерживает все четыре позиции размещения панели
/// вкладок: сверху, снизу, слева и справа.
/// </para>
/// <para>
/// Контрол автоматически синхронизирует свое состояние с моделью данных <see cref="DockLeaf"/>
/// и обеспечивает двустороннюю привязку данных через механизм INotifyPropertyChanged.
/// </para>
/// </remarks>
public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
{
private readonly PropertyChangedEventHandler _modelPropertyChangedHandler;
@@ -26,12 +37,10 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
private ListBox? _tabHeaderList;
private ContentControl? _contentControl;
private LayoutManager? _layoutManager;
private IDockDragDropService? _dragDropService;
private IDockContextManager? _contextManager;
private bool _isSelected;
private bool _isActive;
private bool _canDrag = true;
private bool _canDrop = true;
private TabPlacement _tabPlacement = TabPlacement.Top;
private bool _showCloseButtons = true;
private bool _canReorderTabs = true;
@@ -43,11 +52,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
this.DefaultStyleKey = typeof(LatticeTabControl);
_modelPropertyChangedHandler = OnModelPropertyChanged;
this.DataContextChanged += OnDataContextChanged;
// Подписываемся на события
this.PointerPressed += OnPointerPressed;
this.PointerMoved += OnPointerMoved;
this.PointerReleased += OnPointerReleased;
}
/// <inheritdoc/>
@@ -76,18 +80,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <inheritdoc/>
public IDockDragDropService? DragDropService
{
get => _dragDropService;
set
{
if (_dragDropService == value) return;
_dragDropService = value;
OnPropertyChanged(nameof(DragDropService));
}
}
/// <inheritdoc/>
public IDockContextManager? ContextManager
{
@@ -124,40 +116,17 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <inheritdoc/>
public bool CanDrag
{
get => _canDrag;
set
{
if (_canDrag == value) return;
_canDrag = value;
OnPropertyChanged(nameof(CanDrag));
}
}
/// <inheritdoc/>
public bool CanDrop
{
get => _canDrop;
set
{
if (_canDrop == value) return;
_canDrop = value;
OnPropertyChanged(nameof(CanDrop));
}
}
/// <inheritdoc/>
public TabPlacement TabPlacement
{
get => _model?.TabPlacement ?? TabPlacement.Top;
get => _tabPlacement;
set
{
if (_model != null && _model.TabPlacement != value)
if (_tabPlacement != value)
{
_model.TabPlacement = value;
_tabPlacement = value;
UpdateTabPlacement();
OnPropertyChanged(nameof(TabPlacement));
}
}
}
@@ -168,10 +137,12 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
get => _showCloseButtons;
set
{
if (_showCloseButtons == value) return;
_showCloseButtons = value;
OnPropertyChanged(nameof(ShowCloseButtons));
UpdateTabHeaders();
if (_showCloseButtons != value)
{
_showCloseButtons = value;
OnPropertyChanged(nameof(ShowCloseButtons));
UpdateTabHeaders();
}
}
}
@@ -181,9 +152,11 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
get => _canReorderTabs;
set
{
if (_canReorderTabs == value) return;
_canReorderTabs = value;
OnPropertyChanged(nameof(CanReorderTabs));
if (_canReorderTabs != value)
{
_canReorderTabs = value;
OnPropertyChanged(nameof(CanReorderTabs));
}
}
}
@@ -230,30 +203,38 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
UpdateTabHeaders();
}
/// <summary>
/// Обрабатывает изменение контекста данных контрола.
/// </summary>
private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
Model = args.NewValue as DockLeaf;
}
/// <summary>
/// Присоединяет модель данных к контролу.
/// </summary>
private void AttachModel()
{
if (_model != null)
{
_model.PropertyChanged += _modelPropertyChangedHandler;
// Подписываемся на изменения коллекции
if (_model.Children is INotifyCollectionChanged notifyCollection)
{
notifyCollection.CollectionChanged += OnChildrenCollectionChanged;
}
// Устанавливаем DataContext для привязки в XAML
this.DataContext = _model;
_tabPlacement = _model.TabPlacement;
UpdateTabHeaders();
UpdateTabPlacement();
}
}
/// <summary>
/// Отсоединяет модель данных от контрола.
/// </summary>
private void DetachModel()
{
if (_model != null)
@@ -269,11 +250,15 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <summary>
/// Обрабатывает изменения свойств модели данных.
/// </summary>
private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(DockLeaf.TabPlacement):
_tabPlacement = _model?.TabPlacement ?? TabPlacement.Top;
OnPropertyChanged(nameof(TabPlacement));
UpdateTabPlacement();
break;
@@ -289,20 +274,24 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <summary>
/// Обрабатывает изменения коллекции вкладок.
/// </summary>
private void OnChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
UpdateTabHeaders();
}
/// <summary>
/// Обновляет положение панели вкладок.
/// </summary>
private void UpdateTabPlacement()
{
if (_rootGrid == null || _model == null) return;
// Очищаем все определения
_rootGrid.RowDefinitions.Clear();
_rootGrid.ColumnDefinitions.Clear();
// Настраиваем Grid в зависимости от позиции вкладок
switch (_model.TabPlacement)
{
case TabPlacement.Top:
@@ -330,21 +319,24 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
break;
}
// Обновляем позиции элементов
UpdateElementPositions();
}
/// <summary>
/// Обновляет ориентацию списка заголовков вкладок.
/// </summary>
/// <param name="orientation">Новая ориентация списка.</param>
private void UpdateHeaderListOrientation(Orientation orientation)
{
if (_tabHeaderList != null && _tabHeaderList.ItemsPanelRoot is ItemsPanelTemplate panelTemplate)
if (_tabHeaderList?.ItemsPanelRoot is StackPanel stackPanel)
{
if (panelTemplate.LoadContent() is StackPanel stackPanel)
{
stackPanel.Orientation = orientation;
}
stackPanel.Orientation = orientation;
}
}
/// <summary>
/// Обновляет позиции элементов в сетке.
/// </summary>
private void UpdateElementPositions()
{
if (_rootGrid == null || _tabHeaderList == null || _contentControl == null) return;
@@ -381,14 +373,15 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <summary>
/// Обновляет заголовки вкладок.
/// </summary>
private void UpdateTabHeaders()
{
if (_tabHeaderList == null || _model == null) return;
// Очищаем текущие элементы
_tabHeaderList.Items.Clear();
// Добавляем новые элементы
foreach (var content in _model.Children)
{
var item = CreateTabHeaderItem(content);
@@ -398,6 +391,11 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
UpdateSelectedTab();
}
/// <summary>
/// Создает элемент заголовка вкладки.
/// </summary>
/// <param name="content">Содержимое вкладки.</param>
/// <returns>Созданный элемент заголовка.</returns>
private ListBoxItem CreateTabHeaderItem(IDockContent content)
{
var item = new ListBoxItem
@@ -408,7 +406,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
VerticalContentAlignment = VerticalAlignment.Stretch
};
// Обработка клика для выбора вкладки
item.PointerPressed += (sender, e) =>
{
if (e.GetCurrentPoint(item).Properties.IsLeftButtonPressed)
@@ -418,26 +415,20 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
};
// Обработка клика на кнопке закрытия
if (_showCloseButtons && content.CanClose)
{
// Добавляем контекстное меню
item.ContextRequested += (sender, e) =>
{
ShowTabContextMenu(item, content, e);
};
}
return item;
}
/// <summary>
/// Создает содержимое заголовка вкладки.
/// </summary>
/// <param name="content">Содержимое вкладки.</param>
/// <returns>Созданное содержимое заголовка.</returns>
private UIElement CreateTabHeaderContent(IDockContent content)
{
var grid = new Grid();
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
// Заголовок вкладки
var textBlock = new TextBlock
{
Text = content.Title,
@@ -447,7 +438,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
Grid.SetColumn(textBlock, 0);
grid.Children.Add(textBlock);
// Кнопка закрытия (если разрешено)
if (_showCloseButtons && content.CanClose)
{
var closeButton = new Button
@@ -475,11 +465,13 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
return grid;
}
/// <summary>
/// Обновляет выбранную вкладку.
/// </summary>
private void UpdateSelectedTab()
{
if (_tabHeaderList == null || _model == null) return;
// Находим элемент, соответствующий активному контенту
foreach (var item in _tabHeaderList.Items)
{
if (item is ListBoxItem listBoxItem && listBoxItem.Tag is IDockContent content)
@@ -488,13 +480,15 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
// Обновляем контент
if (_contentControl != null)
{
_contentControl.Content = _model.ActiveContent?.View;
}
}
/// <summary>
/// Обрабатывает изменение выбора вкладки.
/// </summary>
private void OnTabSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_tabHeaderList?.SelectedItem is ListBoxItem selectedItem &&
@@ -511,66 +505,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
// Drag-and-Drop для переупорядочивания вкладок
private ListBoxItem? _draggedItem;
private Point _dragStartPoint;
private void OnPointerPressed(object sender, PointerRoutedEventArgs e)
{
if (!_canReorderTabs) return;
var pointerPoint = e.GetCurrentPoint(this);
_dragStartPoint = new Point(pointerPoint.Position.X, pointerPoint.Position.Y);
// Находим элемент под курсором
var element = VisualTreeHelper.FindElementsInHostCoordinates(
pointerPoint.Position, this).FirstOrDefault();
if (element is ListBoxItem listBoxItem)
{
_draggedItem = listBoxItem;
}
}
private void OnPointerMoved(object sender, PointerRoutedEventArgs e)
{
if (_draggedItem == null || !_canReorderTabs) return;
var pointerPoint = e.GetCurrentPoint(this);
var currentPoint = new Point(pointerPoint.Position.X, pointerPoint.Position.Y);
// Проверяем, достаточно ли переместили для начала перетаскивания
var distance = Math.Sqrt(
Math.Pow(currentPoint.X - _dragStartPoint.X, 2) +
Math.Pow(currentPoint.Y - _dragStartPoint.Y, 2));
if (distance > 10 && _draggedItem.Tag is IDockContent content)
{
// Начинаем операцию перетаскивания
StartTabDrag(_draggedItem, content);
_draggedItem = null;
}
}
private void OnPointerReleased(object sender, PointerRoutedEventArgs e)
{
_draggedItem = null;
}
private void StartTabDrag(ListBoxItem item, IDockContent content)
{
// TODO: Реализовать перетаскивание вкладок
// Для этого нужно использовать IDockDragDropService
}
private void ShowTabContextMenu(ListBoxItem item, IDockContent content, ContextRequestedEventArgs e)
{
if (_contextManager == null) return;
var position = e.GetPosition(this);
_contextManager.ShowContextMenu(this, position.X, position.Y);
}
/// <inheritdoc/>
public void AddContent(IDockContent content)
{
@@ -643,10 +577,9 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
/// <inheritdoc/>
public void ApplyTheme(IDockTheme theme)
{
// Применение темы к элементу
if (theme != null)
{
// TODO: Применить тему к стилям контрола
// TODO: Реализовать применение темы к стилям контрола
}
}
@@ -659,6 +592,9 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <summary>
/// Вызывает событие изменения свойства.
/// </summary>
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
@@ -676,11 +612,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
_tabHeaderList.SelectionChanged -= OnTabSelectionChanged;
}
// Отписываемся от событий указателя
this.PointerPressed -= OnPointerPressed;
this.PointerMoved -= OnPointerMoved;
this.PointerReleased -= OnPointerReleased;
_disposed = true;
GC.SuppressFinalize(this);
}

View File

@@ -11,6 +11,17 @@ namespace Lattice.UI.Docking.WinUI.Factories;
/// Фабрика контролов для платформы WinUI.
/// Создает UI-элементы для отображения компонентов системы докинга.
/// </summary>
/// <remarks>
/// <para>
/// Фабрика реализует паттерн "Абстрактная фабрика", предоставляя единый интерфейс
/// для создания всех типов контролов док-системы. Это позволяет абстрагировать
/// конкретную UI-платформу (WinUI) от бизнес-логики системы.
/// </para>
/// <para>
/// Все создаваемые контролы автоматически настраиваются: устанавливаются связи
/// с менеджером макета, контекстным менеджером и применяется текущая тема оформления.
/// </para>
/// </remarks>
public sealed class WinUIDockControlFactory : DockControlFactoryBase, IDockControlFactory
{
private readonly IDockTheme _theme;
@@ -18,13 +29,33 @@ public sealed class WinUIDockControlFactory : DockControlFactoryBase, IDockContr
/// <summary>
/// Инициализирует новый экземпляр фабрики WinUI.
/// </summary>
/// <param name="theme">Тема оформления.</param>
/// <param name="theme">Тема оформления для применения к создаваемым контролам.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="theme"/> равен null.
/// </exception>
/// <remarks>
/// Конструктор создает фабрику с заданной темой оформления. Все контролы,
/// созданные этой фабрикой, будут автоматически применять указанную тему.
/// </remarks>
public WinUIDockControlFactory(IDockTheme theme)
{
_theme = theme ?? throw new ArgumentNullException(nameof(theme));
}
/// <inheritdoc/>
/// <summary>
/// Создает контрол для группы разделения.
/// </summary>
/// <param name="group">Модель группы разделения.</param>
/// <returns>
/// Созданный контрол группы.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="group"/> равен null.
/// </exception>
/// <remarks>
/// Создает экземпляр <see cref="LatticeDockGroup"/>, настраивает его связи
/// с моделью и другими сервисами, применяет текущую тему оформления.
/// </remarks>
public override IDockGroupControl CreateGroupControl(DockGroup group)
{
var control = new LatticeDockGroup();
@@ -33,7 +64,21 @@ public sealed class WinUIDockControlFactory : DockControlFactoryBase, IDockContr
return control;
}
/// <inheritdoc/>
/// <summary>
/// Создает контрол для контейнера вкладок.
/// </summary>
/// <param name="leaf">Модель контейнера вкладок.</param>
/// <returns>
/// Созданный контрол листа.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="leaf"/> равен null.
/// </exception>
/// <remarks>
/// Создает экземпляр <see cref="LatticeTabControl"/>, настраивает его связи
/// с моделью и другими сервисами, применяет текущую тему оформления.
/// Контрол поддерживает все положения панели вкладок и операции с вкладками.
/// </remarks>
public override IDockLeafControl CreateLeafControl(DockLeaf leaf)
{
var control = new LatticeTabControl();
@@ -42,21 +87,58 @@ public sealed class WinUIDockControlFactory : DockControlFactoryBase, IDockContr
return control;
}
/// <inheritdoc/>
/// <summary>
/// Создает контрол для плавающего окна.
/// </summary>
/// <param name="window">Модель плавающего окна.</param>
/// <returns>
/// Созданный контрол окна.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="window"/> равен null.
/// </exception>
/// <remarks>
/// В текущей реализации метод не реализован. Плавающие окна требуют
/// дополнительной интеграции с оконной системой платформы.
/// </remarks>
public override IFloatingWindowControl CreateFloatingWindowControl(DockWindow window)
{
// TODO: Реализовать создание плавающего окна
throw new NotImplementedException();
}
/// <inheritdoc/>
/// <summary>
/// Создает контрол для автоскрываемой панели.
/// </summary>
/// <param name="panel">Модель автоскрываемой панели.</param>
/// <returns>
/// Созданный контрол панели.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="panel"/> равен null.
/// </exception>
/// <remarks>
/// В текущей реализации метод не реализован. Автоскрываемые панели требуют
/// сложной логики анимации и взаимодействия с краями окна.
/// </remarks>
public override IAutoHidePanelControl CreateAutoHidePanelControl(AutoHidePanel panel)
{
// TODO: Реализовать создание автоскрываемой панели
throw new NotImplementedException();
}
/// <inheritdoc/>
/// <summary>
/// Создает контрол для разделителя.
/// </summary>
/// <param name="orientation">Ориентация разделителя.</param>
/// <returns>
/// Созданный контрол разделителя.
/// </returns>
/// <remarks>
/// Создает экземпляр <see cref="LatticeSplitter"/>, настраивает его ориентацию
/// и применяет текущую тему оформления. Разделитель поддерживает перетаскивание
/// для изменения соотношения размеров между соседними областями.
/// </remarks>
public override IDockSplitterControl CreateSplitterControl(SplitDirection orientation)
{
var control = new LatticeSplitter
@@ -71,6 +153,14 @@ public sealed class WinUIDockControlFactory : DockControlFactoryBase, IDockContr
/// <summary>
/// Создает хост для размещения системы докинга.
/// </summary>
/// <returns>
/// Созданный док-хост.
/// </returns>
/// <remarks>
/// Создает корневой контейнер для всей системы докинга - экземпляр <see cref="LatticeDockHost"/>.
/// Хост управляет всем макетом приложения, включая основное дерево компоновки,
/// плавающие окна и автоскрываемые панели.
/// </remarks>
public IDockHost CreateDockHost()
{
var host = new LatticeDockHost();
@@ -79,13 +169,22 @@ public sealed class WinUIDockControlFactory : DockControlFactoryBase, IDockContr
return host;
}
/// <summary>
/// Настраивает созданный контрол.
/// </summary>
/// <param name="control">Контрол для настройки.</param>
/// <param name="model">Модель данных для контрола (опционально).</param>
/// <remarks>
/// Устанавливает основные связи контрола: модель данных, менеджер макета,
/// контекстный менеджер. Также настраивает привязку данных через DataContext.
/// Этот метод вызывается для всех создаваемых контролов.
/// </remarks>
private void ConfigureControl(IDockControl control, IDockElement? model = null)
{
if (control == null) return;
control.Model = model;
control.LayoutManager = LatticeUIFramework.LayoutManager;
control.DragDropService = LatticeUIFramework.DragDropService;
control.ContextManager = LatticeUIFramework.ContextManager;
if (control is FrameworkElement frameworkElement && model != null)

View File

@@ -8,11 +8,43 @@ using System.Threading.Tasks;
namespace Lattice.UI.Docking.WinUI.Services;
/// <summary>
/// Реализация UI-сервиса для WinUI.
/// Реализация UI-сервиса для платформы WinUI.
/// Инкапсулирует платформенно-зависимые операции, такие как создание окон,
/// показ диалогов и синхронизация с UI-потоком.
/// </summary>
public sealed class WinUIDockUIService : DockUIServiceBase
/// <remarks>
/// <para>
/// <see cref="WinUIDockUIService"/> предоставляет конкретные реализации методов
/// <see cref="IDockUIService"/> для платформы WinUI. Это позволяет основной
/// бизнес-логике док-системы оставаться независимой от конкретной UI-платформы.
/// </para>
/// <para>
/// Сервис использует API WinUI для создания окон, показа ContentDialog и
/// управления диспетчером потока пользовательского интерфейса.
/// </para>
/// </remarks>
public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService
{
/// <inheritdoc/>
/// <summary>
/// Создает главное окно приложения для размещения док-хоста.
/// </summary>
/// <param name="host">
/// Экземпляр <see cref="IDockHost"/>, который будет содержаться в окне.
/// </param>
/// <returns>
/// Объект окна WinUI, который можно отобразить и управлять им.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="host"/> равен null.
/// </exception>
/// <exception cref="ArgumentException">
/// Выбрасывается, если <paramref name="host"/> не является элементом WinUI.
/// </exception>
/// <remarks>
/// Создает окно WinUI с заголовком "Lattice IDE", устанавливает указанный хост
/// в качестве содержимого и регистрирует окно в системе отслеживания окон.
/// Окно создается с настройками по умолчанию для IDE-подобных приложений.
/// </remarks>
public override object CreateMainWindow(IDockHost host)
{
if (host is not FrameworkElement hostElement)
@@ -28,7 +60,25 @@ public sealed class WinUIDockUIService : DockUIServiceBase
return window;
}
/// <inheritdoc/>
/// <summary>
/// Отображает модальное диалоговое окно с указанным содержимым.
/// </summary>
/// <param name="title">Заголовок диалогового окна.</param>
/// <param name="content">Содержимое диалогового окна.</param>
/// <returns>
/// Nullable boolean значение, указывающее результат диалога:
/// true - пользователь подтвердил действие,
/// false - пользователь отменил действие,
/// null - диалог был закрыт без выбора.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="title"/> или <paramref name="content"/> равны null.
/// </exception>
/// <remarks>
/// Создает и показывает ContentDialog с кнопками OK и Cancel.
/// Блокирует взаимодействие с родительским окном до закрытия диалога.
/// Использует XamlRoot активного окна для корректного отображения.
/// </remarks>
public override bool? ShowDialog(string title, object content)
{
if (content is not FrameworkElement contentElement)
@@ -53,7 +103,19 @@ public sealed class WinUIDockUIService : DockUIServiceBase
};
}
/// <inheritdoc/>
/// <summary>
/// Отображает информационное сообщение с кнопкой OK.
/// </summary>
/// <param name="message">Текст сообщения.</param>
/// <param name="caption">Заголовок окна сообщения.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="message"/> или <paramref name="caption"/> равны null.
/// </exception>
/// <remarks>
/// Создает ContentDialog с текстом сообщения и одной кнопкой OK.
/// Используется для информирования пользователя о результате операции
/// или отображения некритичных ошибок.
/// </remarks>
public override void ShowMessage(string message, string caption)
{
var dialog = new ContentDialog
@@ -67,7 +129,22 @@ public sealed class WinUIDockUIService : DockUIServiceBase
dialog.ShowAsync();
}
/// <inheritdoc/>
/// <summary>
/// Отображает диалог подтверждения с кнопками Yes/No.
/// </summary>
/// <param name="message">Текст вопроса.</param>
/// <param name="caption">Заголовок окна подтверждения.</param>
/// <returns>
/// true, если пользователь выбрал "Yes"; false, если пользователь выбрал "No".
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="message"/> или <paramref name="caption"/> равны null.
/// </exception>
/// <remarks>
/// Создает ContentDialog с кнопками Yes и No. Используется для получения
/// подтверждения пользователя перед выполнением критических операций,
/// таких как закрытие вкладок с несохраненными данными или сброс настроек.
/// </remarks>
public override bool Confirm(string message, string caption)
{
var dialog = new ContentDialog
@@ -83,7 +160,22 @@ public sealed class WinUIDockUIService : DockUIServiceBase
return result == ContentDialogResult.Primary;
}
/// <inheritdoc/>
/// <summary>
/// Отображает диалог ввода текста.
/// </summary>
/// <param name="prompt">Текст подсказки для пользователя.</param>
/// <param name="defaultValue">Значение по умолчанию для поля ввода.</param>
/// <returns>
/// Введенный пользователем текст или null, если диалог был отменен.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="prompt"/> равен null.
/// </exception>
/// <remarks>
/// Создает ContentDialog с однострочным полем ввода TextBox.
/// Используется для получения текстового ввода от пользователя, такого как
/// имена файлов, названия документов или параметры конфигурации.
/// </remarks>
public override string? Prompt(string prompt, string? defaultValue = null)
{
var textBox = new TextBox
@@ -106,7 +198,19 @@ public sealed class WinUIDockUIService : DockUIServiceBase
return result == ContentDialogResult.Primary ? textBox.Text : null;
}
/// <inheritdoc/>
/// <summary>
/// Выполняет указанное действие в UI-потоке.
/// </summary>
/// <param name="action">Действие для выполнения.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="action"/> равен null.
/// </exception>
/// <remarks>
/// Гарантирует, что действие будет выполнено в потоке, связанном с
/// пользовательским интерфейсом. Если текущий поток уже является UI-потоком,
/// действие выполняется немедленно. В противном случае действие ставится
/// в очередь диспетчера WinUI.
/// </remarks>
public override void InvokeOnUIThread(Action action)
{
if (action == null) return;
@@ -122,7 +226,21 @@ public sealed class WinUIDockUIService : DockUIServiceBase
}
}
/// <inheritdoc/>
/// <summary>
/// Выполняет указанную асинхронную функцию в UI-потоке.
/// </summary>
/// <param name="action">Асинхронная функция для выполнения.</param>
/// <returns>
/// Задача, представляющая асинхронную операцию.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="action"/> равен null.
/// </exception>
/// <remarks>
/// Гарантирует, что асинхронная функция будет выполнена в UI-потоке.
/// Используется для операций, которые требуют доступа к UI-элементам
/// или выполняют асинхронные вызовы с обновлением интерфейса.
/// </remarks>
public override async Task InvokeOnUIThreadAsync(Func<Task> action)
{
if (action == null) return;
@@ -151,6 +269,17 @@ public sealed class WinUIDockUIService : DockUIServiceBase
}
}
/// <summary>
/// Получает XamlRoot активного окна приложения.
/// </summary>
/// <returns>
/// XamlRoot активного окна или null, если нет активных окон.
/// </returns>
/// <remarks>
/// Используется для корректного отображения диалоговых окон в контексте
/// текущего окна приложения. Перебирает все зарегистрированные окна
/// и возвращает XamlRoot первого найденного.
/// </remarks>
private XamlRoot? GetActiveXamlRoot()
{
// Получаем XamlRoot из активного окна

View File

@@ -77,21 +77,23 @@ public interface IAutoHidePanelControl : IDockControl
/// <summary>
/// Задает фиксированное состояние панели.
/// </summary>
/// <param name="pinned">true, чтобы зафиксировать панель; false, чтобы разрешить автоскрытие.</param>
/// <param name="pinned">
/// true, чтобы зафиксировать панель; false, чтобы разрешить автоскрытие.
/// </param>
void SetPinned(bool pinned);
/// <summary>
/// Событие, возникающее при изменении видимости панели.
/// Происходит при изменении видимости панели.
/// </summary>
event EventHandler VisibilityChanged;
/// <summary>
/// Событие, возникающее при наведении курсора на панель.
/// Происходит при наведении курсора на панель.
/// </summary>
event EventHandler MouseEntered;
/// <summary>
/// Событие, возникающее при уходе курсора с панели.
/// Происходит при уходе курсора с панели.
/// </summary>
event EventHandler MouseLeft;
}

View File

@@ -1,40 +1,58 @@
namespace Lattice.UI.Docking.Abstractions;
/// <summary>
/// Определяет контракт для команды док-системы.
/// Определяет контракт для команды в UI-слое док-системы.
/// Команды представляют действия, которые могут быть выполнены пользователем.
/// </summary>
public interface IDockCommand
{
/// <summary>
/// Получает идентификатор команды.
/// Получает уникальный идентификатор команды.
/// </summary>
/// <value>
/// Строковый идентификатор команды.
/// </value>
string Id { get; }
/// <summary>
/// Получает отображаемое имя команды.
/// </summary>
/// <value>
/// Имя команды, отображаемое в пользовательском интерфейсе.
/// </value>
string Name { get; }
/// <summary>
/// Получает описание команды.
/// </summary>
/// <value>
/// Текстовое описание функциональности команды.
/// </value>
string Description { get; }
/// <summary>
/// Получает значок команды.
/// Получает идентификатор ресурса для иконки команды.
/// </summary>
/// <value>
/// Имя ресурса иконки или путь к файлу иконки.
/// </value>
string Icon { get; }
/// <summary>
/// Получает комбинацию клавиш для команды.
/// Получает комбинацию клавиш для быстрого вызова команды.
/// </summary>
/// <value>
/// Строковое представление горячей клавиши (например, "Ctrl+S").
/// </value>
string Shortcut { get; }
/// <summary>
/// Определяет, можно ли выполнить команду.
/// Определяет, можно ли выполнить команду в текущем контексте.
/// </summary>
/// <param name="parameter">Параметр команды.</param>
/// <returns>true, если команду можно выполнить; в противном случае — false.</returns>
/// <returns>
/// true, если команду можно выполнить; в противном случае false.
/// </returns>
bool CanExecute(object? parameter);
/// <summary>
@@ -44,7 +62,7 @@ public interface IDockCommand
void Execute(object? parameter);
/// <summary>
/// Событие, возникающее при изменении возможности выполнения команды.
/// Происходит при изменении возможности выполнения команды.
/// </summary>
event EventHandler CanExecuteChanged;
}
}

View File

@@ -11,6 +11,9 @@ public interface IDockContextManager
/// <param name="element">Элемент, для которого показывается меню.</param>
/// <param name="x">Координата X для отображения меню.</param>
/// <param name="y">Координата Y для отображения меню.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="element"/> равен null.
/// </exception>
void ShowContextMenu(IDockControl element, double x, double y);
/// <summary>
@@ -23,6 +26,10 @@ public interface IDockContextManager
/// </summary>
/// <param name="commandId">Идентификатор команды.</param>
/// <param name="command">Команда для регистрации.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="commandId"/> или <paramref name="command"/>
/// равны null.
/// </exception>
void RegisterCommand(string commandId, IDockCommand command);
/// <summary>
@@ -32,12 +39,12 @@ public interface IDockContextManager
void UnregisterCommand(string commandId);
/// <summary>
/// Событие, возникающее при показе контекстного меню.
/// Происходит при показе контекстного меню.
/// </summary>
event EventHandler<ContextMenuShownEventArgs> ContextMenuShown;
/// <summary>
/// Событие, возникающее при скрытии контекстного меню.
/// Происходит при скрытии контекстного меню.
/// </summary>
event EventHandler ContextMenuHidden;
}
}

View File

@@ -7,11 +7,12 @@ namespace Lattice.UI.Docking.Abstractions;
/// <summary>
/// Определяет базовый контракт для всех UI-контролов, участвующих в системе докинга.
/// Этот интерфейс предоставляет общие свойства и методы, необходимые для интеграции
/// с менеджером макета и системой перетаскивания.
/// с менеджером макета и UI-сервисами.
/// </summary>
/// <remarks>
/// Реализации этого интерфейса должны отображать элементы док-системы (DockGroup, DockLeaf)
/// и обеспечивать взаимодействие пользователя с ними через жесты мыши, клавиатуру и сенсорный ввод.
/// Интерфейс обеспечивает двухстороннюю связь между визуальными элементами и их моделями данных.
/// </remarks>
public interface IDockControl : INotifyPropertyChanged
{
@@ -22,6 +23,10 @@ public interface IDockControl : INotifyPropertyChanged
/// Экземпляр класса, реализующего <see cref="IDockElement"/>, который представляет
/// состояние и структуру отображаемого элемента док-системы.
/// </value>
/// <remarks>
/// Изменение модели должно приводить к обновлению визуального представления.
/// Свойство используется для привязки данных между UI-слоем и слоем бизнес-логики.
/// </remarks>
IDockElement? Model { get; set; }
/// <summary>
@@ -30,62 +35,57 @@ public interface IDockControl : INotifyPropertyChanged
/// <value>
/// Экземпляр <see cref="LayoutManager"/>, управляющий структурой док-системы.
/// </value>
/// <remarks>
/// Менеджер макета предоставляет доступ к дереву компоновки, плавающим окнам
/// и автоскрываемым панелям, а также методы для манипуляции структурой.
/// </remarks>
LayoutManager? LayoutManager { get; set; }
/// <summary>
/// Получает или задает сервис перетаскивания, используемый этим контролом.
/// </summary>
/// <value>
/// Реализация <see cref="IDragDropService"/> для обработки операций перетаскивания.
/// </value>
IDragDropService? DragDropService { get; set; }
/// <summary>
/// Получает или задает контекстный менеджер для этого контрола.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDockContextManager"/>, управляющий контекстными меню и действиями.
/// </value>
/// <remarks>
/// Контекстный менеджер используется для отображения контекстно-зависимых команд
/// при щелчке правой кнопкой мыши или других пользовательских действиях.
/// </remarks>
IDockContextManager? ContextManager { get; set; }
/// <summary>
/// Получает или задает признак того, что контрол выбран.
/// </summary>
/// <value>
/// true, если контрол выбран; в противном случае false.
/// true, если контрол выбран; в противном случае false.
/// </value>
/// <remarks>
/// Выделение контрола обычно визуально выделяет его границы или фон,
/// чтобы указать пользователю на активный элемент. В каждый момент времени
/// может быть выбран только один контрол в пределах контейнера.
/// </remarks>
bool IsSelected { get; set; }
/// <summary>
/// Получает или задает признак того, что контрол активен.
/// </summary>
/// <value>
/// true, если контрол активен; в противном случае false.
/// true, если контрол активен; в противном случае false.
/// </value>
/// <remarks>
/// Активный контрол получает фокус ввода и может обрабатывать команды клавиатуры.
/// Обычно соответствует активной вкладке в контейнере или активному окну.
/// </remarks>
bool IsActive { get; set; }
/// <summary>
/// Получает или задает признак того, что контрол можно перетаскивать.
/// </summary>
/// <value>
/// true, если контрол можно перетаскивать; в противном случае — false.
/// </value>
bool CanDrag { get; set; }
/// <summary>
/// Получает или задает признак того, что контрол может принимать сброс.
/// </summary>
/// <value>
/// true, если контрол может принимать сброс; в противном случае — false.
/// </value>
bool CanDrop { get; set; }
/// <summary>
/// Обновляет внешний вид контрола в соответствии с текущим состоянием модели.
/// </summary>
/// <remarks>
/// Этот метод должен вызываться при изменении свойств модели или при необходимости
/// принудительного обновления UI (например, после изменения темы или масштаба).
/// Реализация должна обеспечить синхронизацию всех визуальных аспектов контрола
/// с текущими значениями свойств модели.
/// </remarks>
void Refresh();
@@ -93,11 +93,23 @@ public interface IDockControl : INotifyPropertyChanged
/// Применяет указанную тему к контролу.
/// </summary>
/// <param name="theme">Тема для применения.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="theme"/> равен null.
/// </exception>
/// <remarks>
/// Метод должен обновить все стили, цвета и параметры отображения контрола
/// в соответствии с переданной темой. Изменения должны применяться немедленно.
/// </remarks>
void ApplyTheme(IDockTheme theme);
/// <summary>
/// Вызывается при изменении состояния модели для обновления UI.
/// </summary>
/// <param name="propertyName">Имя изменившегося свойства модели.</param>
/// <remarks>
/// Этот метод предназначен для уведомления UI о конкретных изменениях в модели,
/// что позволяет выполнять точечные обновления вместо полного перестроения.
/// Должен вызываться из обработчиков событий изменения свойств модели.
/// </remarks>
void OnModelPropertyChanged(string propertyName);
}

View File

@@ -1,256 +0,0 @@
namespace Lattice.UI.Docking.Abstractions;
/// <summary>
/// Предоставляет сервис для операций перетаскивания в UI-слое док-системы.
/// Абстрагирует платформенно-зависимую логику перетаскивания и обеспечивает
/// единый интерфейс для управления операциями drag-and-drop.
/// </summary>
/// <remarks>
/// Этот интерфейс служит мостом между базовым менеджером перетаскивания из Core
/// и UI-контролами, добавляя визуальную обратную связь и обработку событий,
/// специфичных для пользовательского интерфейса.
/// </remarks>
public interface IDockDragDropService
{
/// <summary>
/// Начинает операцию перетаскивания для указанного элемента.
/// </summary>
/// <param name="element">UI-контрол, который инициирует перетаскивание.</param>
/// <param name="dragInfo">
/// Информация о перетаскивании, содержащая данные и параметры операции.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="element"/> или <paramref name="dragInfo"/> равны null.
/// </exception>
/// <remarks>
/// Этот метод должен создавать визуальное представление перетаскиваемого элемента
/// и инициировать отслеживание перемещения мыши.
/// </remarks>
void StartDrag(IDockControl element, Core.DragDrop.Models.DragInfo dragInfo);
/// <summary>
/// Обновляет позицию текущей операции перетаскивания.
/// </summary>
/// <param name="x">Новая координата X курсора в экранных координатах.</param>
/// <param name="y">Новая координата Y курсора в экранных координатах.</param>
/// <remarks>
/// Вызывается при каждом перемещении мыши во время операции перетаскивания.
/// Должен обновлять позицию визуального представления и проверять возможные цели сброса.
/// </remarks>
void UpdateDrag(double x, double y);
/// <summary>
/// Завершает текущую операцию перетаскивания в указанной позиции.
/// </summary>
/// <param name="x">Координата X завершения перетаскивания.</param>
/// <param name="y">Координата Y завершения перетаскивания.</param>
/// <remarks>
/// Выполняет сброс данных на текущую цель (если она есть) и очищает ресурсы,
/// выделенные для операции перетаскивания.
/// </remarks>
void EndDrag(double x, double y);
/// <summary>
/// Отменяет текущую операцию перетаскивания.
/// </summary>
/// <remarks>
/// Вызывается при нажатии клавиши Escape или других действиях, приводящих к отмене.
/// Должен восстанавливать исходное состояние элементов и очищать ресурсы.
/// </remarks>
void CancelDrag();
/// <summary>
/// Показывает визуальную подсказку о возможной позиции сброса.
/// </summary>
/// <param name="element">UI-контрол, для которого показывается подсказка.</param>
/// <param name="position">Предполагаемая позиция сброса.</param>
/// <remarks>
/// Используется для визуальной обратной связи, чтобы пользователь видел,
/// куда будет помещен элемент при отпускании кнопки мыши.
/// </remarks>
void ShowDropHint(IDockControl element, Models.DropPosition position);
/// <summary>
/// Скрывает текущую визуальную подсказку о сбросе.
/// </summary>
/// <remarks>
/// Вызывается, когда курсор покидает допустимую область сброса
/// или операция перетаскивания завершается.
/// </remarks>
void HideDropHint();
/// <summary>
/// Событие, возникающее при начале операции перетаскивания.
/// </summary>
event EventHandler<DragStartedEventArgs> DragStarted;
/// <summary>
/// Событие, возникающее при обновлении позиции перетаскивания.
/// </summary>
event EventHandler<DragUpdatedEventArgs> DragUpdated;
/// <summary>
/// Событие, возникающее при завершении операции перетаскивания.
/// </summary>
event EventHandler<DragCompletedEventArgs> DragCompleted;
/// <summary>
/// Событие, возникающее при отмене операции перетаскивания.
/// </summary>
event EventHandler DragCancelled;
}
/// <summary>
/// Предоставляет данные для события начала перетаскивания.
/// </summary>
public class DragStartedEventArgs : EventArgs
{
/// <summary>
/// Получает UI-контрол, который инициировал перетаскивание.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDockControl"/>, представляющий источник перетаскивания.
/// Может быть null, если перетаскивание инициировано не из UI-элемента.
/// </value>
public IDockControl? Source { get; }
/// <summary>
/// Получает информацию о перетаскивании.
/// </summary>
/// <value>
/// Экземпляр <see cref="Core.DragDrop.Models.DragInfo"/> с данными перетаскивания.
/// </value>
public Core.DragDrop.Models.DragInfo DragInfo { get; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragStartedEventArgs"/>.
/// </summary>
/// <param name="source">Источник перетаскивания.</param>
/// <param name="dragInfo">Информация о перетаскивании.</param>
public DragStartedEventArgs(IDockControl? source, Core.DragDrop.Models.DragInfo dragInfo)
{
Source = source;
DragInfo = dragInfo;
}
}
/// <summary>
/// Предоставляет данные для события обновления перетаскивания.
/// </summary>
public class DragUpdatedEventArgs : EventArgs
{
/// <summary>
/// Получает UI-контрол, который инициировал перетаскивание.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDockControl"/>, представляющий источник перетаскивания.
/// </value>
public IDockControl? Source { get; }
/// <summary>
/// Получает текущую координату X курсора.
/// </summary>
/// <value>
/// Координата X в экранных координатах.
/// </value>
public double X { get; }
/// <summary>
/// Получает текущую координату Y курсора.
/// </summary>
/// <value>
/// Координата Y в экранных координатах.
/// </value>
public double Y { get; }
/// <summary>
/// Получает информацию о перетаскивании.
/// </summary>
/// <value>
/// Экземпляр <see cref="Core.DragDrop.Models.DragInfo"/> с текущими данными перетаскивания.
/// </value>
public Core.DragDrop.Models.DragInfo DragInfo { get; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragUpdatedEventArgs"/>.
/// </summary>
/// <param name="source">Источник перетаскивания.</param>
/// <param name="x">Текущая координата X.</param>
/// <param name="y">Текущая координата Y.</param>
/// <param name="dragInfo">Информация о перетаскивании.</param>
public DragUpdatedEventArgs(IDockControl? source, double x, double y, Core.DragDrop.Models.DragInfo dragInfo)
{
Source = source;
X = x;
Y = y;
DragInfo = dragInfo;
}
}
/// <summary>
/// Предоставляет данные для события завершения перетаскивания.
/// </summary>
public class DragCompletedEventArgs : EventArgs
{
/// <summary>
/// Получает UI-контрол, который инициировал перетаскивание.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDockControl"/>, представляющий источник перетаскивания.
/// Может быть null, если операция была инициирована не из UI.
/// </value>
public IDockControl? Source { get; }
/// <summary>
/// Получает UI-контрол, на который был выполнен сброс.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDockControl"/>, представляющий цель сброса.
/// Может быть null, если сброс был выполнен вне допустимой области.
/// </value>
public IDockControl? Target { get; }
/// <summary>
/// Получает позицию сброса относительно целевого элемента.
/// </summary>
/// <value>
/// Значение перечисления <see cref="DropPosition"/>, указывающее позицию сброса.
/// </value>
public Models.DropPosition DropPosition { get; }
/// <summary>
/// Получает информацию о перетаскивании.
/// </summary>
/// <value>
/// Экземпляр <see cref="Core.DragDrop.Models.DragInfo"/> с данными завершенной операции.
/// Может быть null, если операция была отменена.
/// </value>
public Core.DragDrop.Models.DragInfo? DragInfo { get; }
/// <summary>
/// Получает значение, указывающее успешность операции сброса.
/// </summary>
/// <value>
/// true, если данные были успешно сброшены на цель; false, если операция была отменена
/// или сброс не был выполнен.
/// </value>
public bool Success { get; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragCompletedEventArgs"/>.
/// </summary>
/// <param name="source">Источник перетаскивания.</param>
/// <param name="target">Цель сброса.</param>
/// <param name="dropPosition">Позиция сброса.</param>
/// <param name="dragInfo">Информация о перетаскивании.</param>
/// <param name="success">Признак успешности операции.</param>
public DragCompletedEventArgs(IDockControl? source, IDockControl? target,
Models.DropPosition dropPosition, Core.DragDrop.Models.DragInfo? dragInfo, bool success)
{
Source = source;
Target = target;
DropPosition = dropPosition;
DragInfo = dragInfo;
Success = success;
}
}

View File

@@ -60,23 +60,23 @@ public interface IDockGroupControl : IDockControl
void SetChildren(IDockControl? firstChild, IDockControl? secondChild);
/// <summary>
/// Событие, возникающее при изменении соотношения разделения.
/// Происходит при изменении соотношения разделения.
/// </summary>
event EventHandler<SplitRatioChangedEventArgs> SplitRatioChanged;
}
/// <summary>
/// Аргументы события изменения соотношения разделения.
/// Предоставляет данные для события изменения соотношения разделения.
/// </summary>
public class SplitRatioChangedEventArgs : EventArgs
{
/// <summary>
/// Новое соотношение разделения.
/// Получает новое соотношение разделения.
/// </summary>
public double NewRatio { get; }
/// <summary>
/// Источник изменения (пользователь или программа).
/// Получает источник изменения соотношения разделения.
/// </summary>
public SplitRatioChangeSource Source { get; }
@@ -93,16 +93,22 @@ public class SplitRatioChangedEventArgs : EventArgs
}
/// <summary>
/// Источник изменения соотношения разделения.
/// Определяет источник изменения соотношения разделения.
/// </summary>
public enum SplitRatioChangeSource
{
/// <summary>Изменение выполнено пользователем.</summary>
/// <summary>
/// Изменение выполнено пользователем.
/// </summary>
User,
/// <summary>Изменение выполнено программой.</summary>
/// <summary>
/// Изменение выполнено программой.
/// </summary>
Programmatic,
/// <summary>Изменение выполнено при восстановлении состояния.</summary>
/// <summary>
/// Изменение выполнено при восстановлении состояния.
/// </summary>
Restore
}

View File

@@ -10,8 +10,7 @@ namespace Lattice.UI.Docking.Abstractions;
/// </summary>
/// <remarks>
/// Реализации этого интерфейса представляют собой центральный координатор UI-слоя,
/// который интегрирует функциональность менеджера макета, системы перетаскивания
/// и контекстных меню в единый визуальный компонент.
/// который интегрирует функциональность менеджера макета и контекстных меню в единый визуальный компонент.
/// </remarks>
public interface IDockHost : IDockControl
{
@@ -23,8 +22,7 @@ public interface IDockHost : IDockControl
/// представляющих все активные плавающие окна в системе.
/// </value>
/// <remarks>
/// Плавающие окна могут быть созданы пользователем путем перетаскивания элементов
/// за пределы основного окна или программно через методы API.
/// Плавающие окна могут быть созданы пользователем или программно через методы API.
/// </remarks>
IEnumerable<IFloatingWindowControl> FloatingWindows { get; }
@@ -45,7 +43,7 @@ public interface IDockHost : IDockControl
/// Получает или задает значение, указывающее, отображается ли панель инструментов (Toolbox).
/// </summary>
/// <value>
/// true, если панель инструментов видима; в противном случае false.
/// true, если панель инструментов видима; в противном случае false.
/// Значение по умолчанию зависит от реализации.
/// </value>
/// <remarks>
@@ -58,7 +56,7 @@ public interface IDockHost : IDockControl
/// Получает или задает значение, указывающее, отображается ли строка состояния.
/// </summary>
/// <value>
/// true, если строка состояния видима; в противном случае false.
/// true, если строка состояния видима; в противном случае false.
/// Значение по умолчанию зависит от реализации.
/// </value>
/// <remarks>
@@ -71,9 +69,9 @@ public interface IDockHost : IDockControl
/// Получает или задает значение, указывающее, отображается ли главное меню приложения.
/// </summary>
/// <value>
/// true, если главное меню видимо; в противном случае false.
/// true, если главное меню видимо; в противном случае false.
/// Значение по умолчанию зависит от реализации.
/// </remarks>
/// </value>
bool ShowMenu { get; set; }
/// <summary>
@@ -148,7 +146,7 @@ public interface IDockHost : IDockControl
void RemoveAutoHidePanel(IAutoHidePanelControl panel);
/// <summary>
/// Событие, возникающее при изменении структуры макета док-системы.
/// Происходит при изменении структуры макета док-системы.
/// </summary>
/// <remarks>
/// Может вызываться при добавлении/удалении элементов, изменении размеров,
@@ -157,12 +155,12 @@ public interface IDockHost : IDockControl
event EventHandler LayoutChanged;
/// <summary>
/// Событие, возникающее при создании нового плавающего окна.
/// Происходит при создании нового плавающего окна.
/// </summary>
event EventHandler<FloatingWindowCreatedEventArgs> FloatingWindowCreated;
/// <summary>
/// Событие, возникающее при закрытии плавающего окна.
/// Происходит при закрытии плавающего окна.
/// </summary>
event EventHandler<FloatingWindowClosedEventArgs> FloatingWindowClosed;
}

View File

@@ -26,7 +26,7 @@ public interface IDockLeafControl : IDockControl
/// Получает или задает признак отображения кнопки закрытия на вкладках.
/// </summary>
/// <value>
/// true, если кнопки закрытия отображаются; в противном случае false.
/// true, если кнопки закрытия отображаются; в противном случае false.
/// </value>
bool ShowCloseButtons { get; set; }
@@ -34,7 +34,7 @@ public interface IDockLeafControl : IDockControl
/// Получает или задает признак возможности изменения порядка вкладок.
/// </summary>
/// <value>
/// true, если порядок вкладок можно изменять; в противном случае false.
/// true, если порядок вкладок можно изменять; в противном случае false.
/// </value>
bool CanReorderTabs { get; set; }
@@ -50,25 +50,39 @@ public interface IDockLeafControl : IDockControl
/// Добавляет вкладку в контрол.
/// </summary>
/// <param name="content">Контент для добавления.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="content"/> равен null.
/// </exception>
void AddContent(IDockContent content);
/// <summary>
/// Удаляет вкладку из контрола.
/// </summary>
/// <param name="content">Контент для удаления.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="content"/> равен null.
/// </exception>
void RemoveContent(IDockContent content);
/// <summary>
/// Закрывает указанную вкладку.
/// </summary>
/// <param name="content">Контент для закрытия.</param>
/// <returns>true, если вкладка была закрыта; в противном случае — false.</returns>
/// <returns>
/// true, если вкладка была закрыта; в противном случае false.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="content"/> равен null.
/// </exception>
bool CloseContent(IDockContent content);
/// <summary>
/// Закрывает все вкладки, кроме указанной.
/// </summary>
/// <param name="exceptContent">Вкладка, которую нужно оставить открытой.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="exceptContent"/> равен null.
/// </exception>
void CloseAllExcept(IDockContent exceptContent);
/// <summary>
@@ -77,39 +91,41 @@ public interface IDockLeafControl : IDockControl
void CloseAll();
/// <summary>
/// Событие, возникающее при изменении активной вкладки.
/// Происходит при изменении активной вкладки.
/// </summary>
event EventHandler<ActiveContentChangedEventArgs> ActiveContentChanged;
/// <summary>
/// Событие, возникающее при запросе закрытия вкладки.
/// Происходит при запросе закрытия вкладки.
/// </summary>
event EventHandler<ContentClosingEventArgs> ContentClosing;
/// <summary>
/// Событие, возникающее при изменении порядка вкладок.
/// Происходит при изменении порядка вкладок.
/// </summary>
event EventHandler<TabsReorderedEventArgs> TabsReordered;
}
/// <summary>
/// Аргументы события изменения активного контента.
/// Предоставляет данные для события изменения активного контента.
/// </summary>
public class ActiveContentChangedEventArgs : EventArgs
{
/// <summary>
/// Предыдущий активный контент.
/// Получает предыдущий активный контент.
/// </summary>
public IDockContent? OldContent { get; }
/// <summary>
/// Новый активный контент.
/// Получает новый активный контент.
/// </summary>
public IDockContent? NewContent { get; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="ActiveContentChangedEventArgs"/>.
/// </summary>
/// <param name="oldContent">Предыдущий активный контент.</param>
/// <param name="newContent">Новый активный контент.</param>
public ActiveContentChangedEventArgs(IDockContent? oldContent, IDockContent? newContent)
{
OldContent = oldContent;
@@ -118,63 +134,79 @@ public class ActiveContentChangedEventArgs : EventArgs
}
/// <summary>
/// Аргументы события закрытия контента.
/// Предоставляет данные для события закрытия контента.
/// </summary>
public class ContentClosingEventArgs : EventArgs
{
/// <summary>
/// Контент, который закрывается.
/// Получает контент, который закрывается.
/// </summary>
public IDockContent Content { get; }
/// <summary>
/// Показывает, можно ли отменить закрытие.
/// Получает или задает значение, указывающее, можно ли отменить закрытие.
/// </summary>
/// <value>
/// true, если закрытие можно отменить; в противном случае false.
/// </value>
public bool CanCancel { get; set; }
/// <summary>
/// Получает или задает признак отмены закрытия.
/// </summary>
/// <value>
/// true, если закрытие отменено; в противном случае false.
/// </value>
public bool Cancel { get; set; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="ContentClosingEventArgs"/>.
/// </summary>
/// <param name="content">Контент, который закрывается.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="content"/> равен null.
/// </exception>
public ContentClosingEventArgs(IDockContent content)
{
Content = content;
Content = content ?? throw new ArgumentNullException(nameof(content));
CanCancel = true;
Cancel = false;
}
}
/// <summary>
/// Аргументы события изменения порядка вкладок.
/// Предоставляет данные для события изменения порядка вкладок.
/// </summary>
public class TabsReorderedEventArgs : EventArgs
{
/// <summary>
/// Старый индекс вкладки.
/// Получает старый индекс вкладки.
/// </summary>
public int OldIndex { get; }
/// <summary>
/// Новый индекс вкладки.
/// Получает новый индекс вкладки.
/// </summary>
public int NewIndex { get; }
/// <summary>
/// Перемещаемый контент.
/// Получает перемещаемый контент.
/// </summary>
public IDockContent Content { get; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="TabsReorderedEventArgs"/>.
/// </summary>
/// <param name="oldIndex">Старый индекс вкладки.</param>
/// <param name="newIndex">Новый индекс вкладки.</param>
/// <param name="content">Перемещаемый контент.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="content"/> равен null.
/// </exception>
public TabsReorderedEventArgs(int oldIndex, int newIndex, IDockContent content)
{
OldIndex = oldIndex;
NewIndex = newIndex;
Content = content;
Content = content ?? throw new ArgumentNullException(nameof(content));
}
}

View File

@@ -0,0 +1,72 @@
namespace Lattice.UI.Docking.Abstractions;
/// <summary>
/// Определяет контракт для контрола разделителя между элементами док-системы.
/// Разделитель позволяет пользователю изменять размер смежных элементов.
/// </summary>
public interface IDockSplitterControl : IDockControl
{
/// <summary>
/// Получает или задает ориентацию разделителя.
/// </summary>
/// <value>
/// Ориентация разделителя (горизонтальная или вертикальная).
/// </value>
Core.Docking.Models.SplitDirection Orientation { get; set; }
/// <summary>
/// Получает или задает признак того, что разделитель активен (перетаскивается).
/// </summary>
/// <value>
/// true, если разделитель активен; в противном случае false.
/// </value>
bool IsDragging { get; set; }
/// <summary>
/// Происходит при начале перетаскивания разделителя.
/// </summary>
event EventHandler DragStarted;
/// <summary>
/// Происходит при перетаскивании разделителя.
/// </summary>
event EventHandler<SplitterDraggedEventArgs> DragDelta;
/// <summary>
/// Происходит при завершении перетаскивания разделителя.
/// </summary>
event EventHandler DragCompleted;
}
/// <summary>
/// Предоставляет данные для события перетаскивания разделителя.
/// </summary>
public class SplitterDraggedEventArgs : EventArgs
{
/// <summary>
/// Получает изменение позиции по горизонтали.
/// </summary>
/// <value>
/// Изменение по горизонтали в пикселях.
/// </value>
public double HorizontalChange { get; }
/// <summary>
/// Получает изменение позиции по вертикали.
/// </summary>
/// <value>
/// Изменение по вертикали в пикселях.
/// </value>
public double VerticalChange { get; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="SplitterDraggedEventArgs"/>.
/// </summary>
/// <param name="horizontalChange">Изменение по горизонтали.</param>
/// <param name="verticalChange">Изменение по вертикали.</param>
public SplitterDraggedEventArgs(double horizontalChange, double verticalChange)
{
HorizontalChange = horizontalChange;
VerticalChange = verticalChange;
}
}

View File

@@ -77,31 +77,37 @@ public interface IDockTheme
}
/// <summary>
/// Аргументы события показа контекстного меню.
/// Предоставляет данные для события показа контекстного меню.
/// </summary>
public class ContextMenuShownEventArgs : EventArgs
{
/// <summary>
/// Элемент, для которого показано меню.
/// Получает элемент, для которого показано меню.
/// </summary>
public IDockControl Target { get; }
/// <summary>
/// Координата X меню.
/// Получает координату X меню.
/// </summary>
public double X { get; }
/// <summary>
/// Координата Y меню.
/// Получает координату Y меню.
/// </summary>
public double Y { get; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="ContextMenuShownEventArgs"/>.
/// </summary>
/// <param name="target">Элемент, для которого показано меню.</param>
/// <param name="x">Координата X меню.</param>
/// <param name="y">Координата Y меню.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="target"/> равен null.
/// </exception>
public ContextMenuShownEventArgs(IDockControl target, double x, double y)
{
Target = target;
Target = target ?? throw new ArgumentNullException(nameof(target));
X = x;
Y = y;
}

View File

@@ -22,12 +22,8 @@ public interface IDockUIService
/// Платформенно-зависимый объект окна, который можно отобразить.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="host"/> равен null.
/// Выбрасывается, когда <paramref name="host"/> равен null.
/// </exception>
/// <remarks>
/// Реализация должна создавать окно с соответствующими стилями и поведением
/// для целевой платформы, настроенное для работы с док-системой.
/// </remarks>
object CreateMainWindow(IDockHost host);
/// <summary>
@@ -42,12 +38,8 @@ public interface IDockUIService
/// null - диалог был закрыт без выбора.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="title"/> или <paramref name="content"/> равны null.
/// Выбрасывается, когда <paramref name="title"/> или <paramref name="content"/> равны null.
/// </exception>
/// <remarks>
/// Реализация должна блокировать взаимодействие с родительским окном
/// до закрытия диалога.
/// </remarks>
bool? ShowDialog(string title, object content);
/// <summary>
@@ -56,12 +48,8 @@ public interface IDockUIService
/// <param name="message">Текст сообщения.</param>
/// <param name="caption">Заголовок окна сообщения.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="message"/> или <paramref name="caption"/> равны null.
/// Выбрасывается, когда <paramref name="message"/> или <paramref name="caption"/> равны null.
/// </exception>
/// <remarks>
/// Реализация должна использовать стандартные диалоги платформы
/// или создавать кастомные окна сообщений.
/// </remarks>
void ShowMessage(string message, string caption);
/// <summary>
@@ -73,12 +61,8 @@ public interface IDockUIService
/// true, если пользователь выбрал "Yes"; false, если пользователь выбрал "No".
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="message"/> или <paramref name="caption"/> равны null.
/// Выбрасывается, когда <paramref name="message"/> или <paramref name="caption"/> равны null.
/// </exception>
/// <remarks>
/// Используется для получения подтверждения от пользователя перед выполнением
/// критических операций (закрытие вкладок, сброс настроек и т.д.).
/// </remarks>
bool Confirm(string message, string caption);
/// <summary>
@@ -90,12 +74,8 @@ public interface IDockUIService
/// Введенный пользователем текст или null, если диалог был отменен.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="prompt"/> равен null.
/// Выбрасывается, когда <paramref name="prompt"/> равен null.
/// </exception>
/// <remarks>
/// Реализация должна предоставлять однострочное поле ввода текста
/// с возможностью отмены операции.
/// </remarks>
string? Prompt(string prompt, string? defaultValue = null);
/// <summary>
@@ -103,12 +83,7 @@ public interface IDockUIService
/// </summary>
/// <param name="action">Действие для выполнения.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="action"/> равен null.
/// Выбрасывается, когда <paramref name="action"/> равен null.
/// </exception>
/// <remarks>
/// Этот метод гарантирует, что действие будет выполнено в потоке,
/// связанном с пользовательским интерфейсом, что необходимо для
/// безопасного обновления UI-элементов.
/// </remarks>
void InvokeOnUIThread(Action action);
}

View File

@@ -53,7 +53,7 @@ public interface IFloatingWindowControl : IDockControl
/// Получает или задает признак того, что окно можно изменять.
/// </summary>
/// <value>
/// true, если размеры окна можно изменять; в противном случае false.
/// true, если размеры окна можно изменять; в противном случае false.
/// </value>
bool CanResize { get; set; }
@@ -61,7 +61,7 @@ public interface IFloatingWindowControl : IDockControl
/// Получает или задает признак того, что окно можно перемещать.
/// </summary>
/// <value>
/// true, если окно можно перемещать; в противном случае false.
/// true, если окно можно перемещать; в противном случае false.
/// </value>
bool CanMove { get; set; }
@@ -69,7 +69,7 @@ public interface IFloatingWindowControl : IDockControl
/// Получает или задает признак того, что окно всегда поверх других окон.
/// </summary>
/// <value>
/// true, если окно всегда поверх; в противном случае false.
/// true, если окно всегда поверх; в противном случае false.
/// </value>
bool AlwaysOnTop { get; set; }
@@ -94,17 +94,17 @@ public interface IFloatingWindowControl : IDockControl
void Activate();
/// <summary>
/// Событие, возникающее при закрытии окна.
/// Происходит при закрытии окна.
/// </summary>
event EventHandler Closing;
/// <summary>
/// Событие, возникающее при изменении положения окна.
/// Происходит при изменении положения окна.
/// </summary>
event EventHandler LocationChanged;
/// <summary>
/// Событие, возникающее при изменении размера окна.
/// Происходит при изменении размера окна.
/// </summary>
event EventHandler SizeChanged;
}

View File

@@ -1,33 +1,85 @@
using Lattice.UI.Docking.Abstractions;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Lattice.UI.Docking.Commands;
/// <summary>
/// Базовая реализация команды док-системы.
/// Предоставляет базовую реализацию команды док-системы.
/// Реализует интерфейс <see cref="IDockCommand"/> и <see cref="INotifyPropertyChanged"/>
/// для поддержки уведомлений об изменении свойств.
/// </summary>
public abstract class DockCommandBase : IDockCommand
/// <remarks>
/// Этот класс предоставляет общую логику для команд, включая управление состоянием
/// возможности выполнения, уведомление об изменениях и реализацию интерфейса ICommand.
/// </remarks>
public abstract class DockCommandBase : IDockCommand, INotifyPropertyChanged
{
/// <summary>
/// Происходит при изменении возможности выполнения команды.
/// </summary>
public event EventHandler? CanExecuteChanged;
/// <summary>
/// Происходит при изменении значения свойства.
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;
private bool _canExecute = true;
/// <inheritdoc/>
/// <summary>
/// Получает уникальный идентификатор команды.
/// </summary>
/// <value>
/// Строковый идентификатор команды. Должен быть уникальным в пределах системы.
/// </value>
public abstract string Id { get; }
/// <inheritdoc/>
/// <summary>
/// Получает отображаемое имя команды.
/// </summary>
/// <value>
/// Имя команды, отображаемое в пользовательском интерфейсе.
/// </value>
public abstract string Name { get; }
/// <inheritdoc/>
/// <summary>
/// Получает описание команды.
/// </summary>
/// <value>
/// Текстовое описание функциональности команды.
/// Реализация по умолчанию возвращает пустую строку.
/// </value>
public virtual string Description => string.Empty;
/// <inheritdoc/>
/// <summary>
/// Получает идентификатор ресурса для иконки команды.
/// </summary>
/// <value>
/// Имя ресурса иконки или путь к файлу иконки.
/// Реализация по умолчанию возвращает пустую строку.
/// </value>
public virtual string Icon => string.Empty;
/// <inheritdoc/>
/// <summary>
/// Получает комбинацию клавиш для быстрого вызова команды.
/// </summary>
/// <value>
/// Строковое представление горячей клавиши.
/// Реализация по умолчанию возвращает пустую строку.
/// </value>
public virtual string Shortcut => string.Empty;
/// <summary>
/// Получает или задает признак возможности выполнения команды.
/// Получает или задает значение, указывающее, можно ли выполнить команду.
/// </summary>
public bool CanExecute
/// <value>
/// true, если команду можно выполнить; в противном случае false.
/// </value>
/// <remarks>
/// При изменении этого свойства генерируется событие <see cref="CanExecuteChanged"/>.
/// </remarks>
protected bool CanExecuteValue
{
get => _canExecute;
set
@@ -36,42 +88,131 @@ public abstract class DockCommandBase : IDockCommand
{
_canExecute = value;
OnCanExecuteChanged();
OnPropertyChanged();
}
}
}
/// <inheritdoc/>
public event EventHandler? CanExecuteChanged;
/// <inheritdoc/>
/// <summary>
/// Определяет, можно ли выполнить команду в текущем контексте.
/// </summary>
/// <param name="parameter">Параметр команды.</param>
/// <returns>
/// true, если команду можно выполнить; в противном случае false.
/// </returns>
/// <remarks>
/// Реализация по умолчанию возвращает значение свойства <see cref="CanExecuteValue"/>.
/// </remarks>
public virtual bool CanExecute(object? parameter)
{
return _canExecute;
}
/// <inheritdoc/>
/// <summary>
/// Выполняет команду.
/// </summary>
/// <param name="parameter">Параметр команды.</param>
/// <remarks>
/// Этот метод должен быть реализован в производных классах.
/// </remarks>
public abstract void Execute(object? parameter);
/// <summary>
/// Вызывает событие изменения возможности выполнения команды.
/// </summary>
/// <remarks>
/// Этот метод может быть переопределен в производных классах для добавления
/// дополнительной логики перед вызовом события.
/// </remarks>
protected virtual void OnCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Вызывает событие <see cref="PropertyChanged"/>.
/// </summary>
/// <param name="propertyName">
/// Имя изменившегося свойства. Если не указано, определяется автоматически.
/// </param>
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
/// <summary>
/// Базовая команда для закрытия контента.
/// Предоставляет команду для закрытия контента (вкладки).
/// </summary>
/// <remarks>
/// Команда закрывает активную вкладку в указанном контроле листа.
/// </remarks>
public class CloseContentCommand : DockCommandBase
{
/// <summary>
/// Получает уникальный идентификатор команды.
/// </summary>
/// <value>
/// Идентификатор "CloseContent".
/// </value>
public override string Id => "CloseContent";
/// <summary>
/// Получает отображаемое имя команды.
/// </summary>
/// <value>
/// Имя "Close".
/// </value>
public override string Name => "Close";
/// <summary>
/// Получает описание команды.
/// </summary>
/// <value>
/// Описание "Close the current tab".
/// </value>
public override string Description => "Close the current tab";
/// <summary>
/// Получает идентификатор ресурса для иконки команды.
/// </summary>
/// <value>
/// Идентификатор "Close".
/// </value>
public override string Icon => "Close";
/// <summary>
/// Получает комбинацию клавиш для быстрого вызова команды.
/// </summary>
/// <value>
/// Горячая клавиша "Ctrl+F4".
/// </value>
public override string Shortcut => "Ctrl+F4";
/// <summary>
/// Определяет, можно ли выполнить команду в текущем контексте.
/// </summary>
/// <param name="parameter">Параметр команды.</param>
/// <returns>
/// true, если команду можно выполнить; в противном случае false.
/// </returns>
/// <remarks>
/// Команда доступна только если параметр является контролом листа
/// и содержит активный контент.
/// </remarks>
public override bool CanExecute(object? parameter)
{
return parameter is Abstractions.IDockLeafControl leafControl &&
leafControl.ActiveContent != null;
}
/// <summary>
/// Выполняет команду закрытия контента.
/// </summary>
/// <param name="parameter">Параметр команды. Ожидается <see cref="Abstractions.IDockLeafControl"/>.</param>
/// <remarks>
/// Команда закрывает активную вкладку в указанном контроле листа.
/// </remarks>
public override void Execute(object? parameter)
{
if (parameter is Abstractions.IDockLeafControl leafControl &&
@@ -83,33 +224,137 @@ public class CloseContentCommand : DockCommandBase
}
/// <summary>
/// Базовая команда для создания плавающего окна.
/// Предоставляет команду для создания плавающего окна из элемента.
/// </summary>
public class FloatWindowCommand : DockCommandBase
{
/// <summary>
/// Получает уникальный идентификатор команды.
/// </summary>
/// <value>
/// Идентификатор "FloatWindow".
/// </value>
public override string Id => "FloatWindow";
/// <summary>
/// Получает отображаемое имя команды.
/// </summary>
/// <value>
/// Имя "Float".
/// </value>
public override string Name => "Float";
/// <summary>
/// Получает описание команды.
/// </summary>
/// <value>
/// Описание "Float the window as a separate window".
/// </value>
public override string Description => "Float the window as a separate window";
/// <summary>
/// Получает идентификатор ресурса для иконки команды.
/// </summary>
/// <value>
/// Идентификатор "Float".
/// </value>
public override string Icon => "Float";
/// <summary>
/// Определяет, можно ли выполнить команду в текущем контексте.
/// </summary>
/// <param name="parameter">Параметр команды.</param>
/// <returns>
/// true, если команду можно выполнить; в противном случае false.
/// </returns>
/// <remarks>
/// Команда доступна только если параметр является элементом док-системы,
/// который может быть преобразован в плавающее окно.
/// </remarks>
public override bool CanExecute(object? parameter)
{
return parameter is Core.Docking.Abstractions.IDockElement;
}
/// <summary>
/// Выполняет команду создания плавающего окна.
/// </summary>
/// <param name="parameter">Параметр команды. Ожидается <see cref="Core.Docking.Abstractions.IDockElement"/>.</param>
/// <remarks>
/// Реализация зависит от конкретного UI и должна быть предоставлена в производных классах.
/// Базовая реализация не выполняет никаких действий.
/// </remarks>
public override void Execute(object? parameter)
{
// Реализация зависит от конкретного UI
// В базовом классе метод не выполняет никаких действий
}
}
/// <summary>
/// Базовая команда для закрепления окна.
/// Предоставляет команду для закрепления плавающего окна.
/// </summary>
public class DockWindowCommand : DockCommandBase
{
/// <summary>
/// Получает уникальный идентификатор команды.
/// </summary>
/// <value>
/// Идентификатор "DockWindow".
/// </value>
public override string Id => "DockWindow";
/// <summary>
/// Получает отображаемое имя команды.
/// </summary>
/// <value>
/// Имя "Dock".
/// </value>
public override string Name => "Dock";
/// <summary>
/// Получает описание команды.
/// </summary>
/// <value>
/// Описание "Dock the window to the main window".
/// </value>
public override string Description => "Dock the window to the main window";
/// <summary>
/// Получает идентификатор ресурса для иконки команды.
/// </summary>
/// <value>
/// Идентификатор "Dock".
/// </value>
public override string Icon => "Dock";
/// <summary>
/// Определяет, можно ли выполнить команду в текущем контексте.
/// </summary>
/// <param name="parameter">Параметр команды.</param>
/// <returns>
/// true, если команду можно выполнить; в противном случае false.
/// </returns>
/// <remarks>
/// Команда доступна только если параметр является плавающим окном,
/// которое может быть закреплено в основном окне.
/// </remarks>
public override bool CanExecute(object? parameter)
{
return parameter is Abstractions.IFloatingWindowControl;
}
/// <summary>
/// Выполняет команду закрепления окна.
/// </summary>
/// <param name="parameter">Параметр команды. Ожидается <see cref="Abstractions.IFloatingWindowControl"/>.</param>
/// <remarks>
/// Реализация зависит от конкретного UI и должна быть предоставлена в производных классах.
/// Базовая реализация не выполняет никаких действий.
/// </remarks>
public override void Execute(object? parameter)
{
// Реализация зависит от конкретного UI
// В базовом классе метод не выполняет никаких действий
}
}

View File

@@ -5,57 +5,136 @@ using Lattice.UI.Docking.Abstractions;
namespace Lattice.UI.Docking.Factories;
/// <summary>
/// Базовая фабрика для создания UI-контролов док-системы.
/// Предоставляет базовую реализацию фабрики для создания UI-контролов док-системы.
/// </summary>
/// <remarks>
/// Этот класс реализует общую логику для фабрик контролов, включая настройку
/// общих свойств и создание контролов для произвольных элементов.
/// </remarks>
public abstract class DockControlFactoryBase : IDockControlFactory
{
/// <summary>
/// Получает или задает сервис перетаскивания для создаваемых контролов.
/// </summary>
public Services.IDockDragDropService? DragDropService { get; set; }
/// <summary>
/// Получает или задает менеджер контекста для создаваемых контролов.
/// </summary>
public Services.IDockContextManager? ContextManager { get; set; }
/// <inheritdoc/>
public abstract IDockGroupControl CreateGroupControl(DockGroup group);
/// <inheritdoc/>
public abstract IDockLeafControl CreateLeafControl(DockLeaf leaf);
/// <inheritdoc/>
public abstract IFloatingWindowControl CreateFloatingWindowControl(DockWindow window);
/// <inheritdoc/>
public abstract IAutoHidePanelControl CreateAutoHidePanelControl(AutoHidePanel panel);
/// <inheritdoc/>
public abstract IDockSplitterControl CreateSplitterControl(SplitDirection orientation);
/// <value>
/// Экземпляр <see cref="IDockContextManager"/> или null, если не установлен.
/// </value>
public IDockContextManager? ContextManager { get; set; }
/// <summary>
/// Создает контрол для произвольного элемента док-системы.
/// Создает контрол для группы разделения.
/// </summary>
/// <param name="group">Модель группы.</param>
/// <returns>
/// Созданный контрол группы.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="group"/> равен null.
/// </exception>
/// <remarks>
/// Этот метод должен быть реализован в производных классах.
/// </remarks>
public abstract IDockGroupControl CreateGroupControl(DockGroup group);
/// <summary>
/// Создает контрол для контейнера вкладок.
/// </summary>
/// <param name="leaf">Модель листа.</param>
/// <returns>
/// Созданный контрол листа.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="leaf"/> равен null.
/// </exception>
/// <remarks>
/// Этот метод должен быть реализован в производных классах.
/// </remarks>
public abstract IDockLeafControl CreateLeafControl(DockLeaf leaf);
/// <summary>
/// Создает контрол для плавающего окна.
/// </summary>
/// <param name="window">Модель окна.</param>
/// <returns>
/// Созданный контрол окна.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="window"/> равен null.
/// </exception>
/// <remarks>
/// Этот метод должен быть реализован в производных классах.
/// </remarks>
public abstract IFloatingWindowControl CreateFloatingWindowControl(DockWindow window);
/// <summary>
/// Создает контрол для автоскрываемой панели.
/// </summary>
/// <param name="panel">Модель панели.</param>
/// <returns>
/// Созданный контрол панели.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="panel"/> равен null.
/// </exception>
/// <remarks>
/// Этот метод должен быть реализован в производных классах.
/// </remarks>
public abstract IAutoHidePanelControl CreateAutoHidePanelControl(AutoHidePanel panel);
/// <summary>
/// Создает контрол для разделителя.
/// </summary>
/// <param name="orientation">Ориентация разделителя.</param>
/// <returns>
/// Созданный контрол разделителя.
/// </returns>
/// <remarks>
/// Этот метод должен быть реализован в производных классах.
/// </remarks>
public abstract IDockSplitterControl CreateSplitterControl(SplitDirection orientation);
/// <inheritdoc/>
public virtual IDockControl? CreateControlForElement(IDockElement element)
{
return element switch
if (element == null) throw new ArgumentNullException(nameof(element));
IDockControl? control = null;
switch (element)
{
DockGroup group => CreateGroupControl(group),
DockLeaf leaf => CreateLeafControl(leaf),
_ => null
};
case DockGroup group:
control = CreateGroupControl(group);
break;
case DockLeaf leaf:
control = CreateLeafControl(leaf);
break;
default:
control = null;
break;
}
if (control != null)
{
ConfigureControl(control);
}
return control;
}
/// <summary>
/// Настраивает общие свойства контрола.
/// </summary>
/// <param name="control">Контрол для настройки.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="control"/> равен null.
/// </exception>
/// <remarks>
/// Устанавливает общие свойства, такие как контекстный менеджер,
/// для всех создаваемых контролов.
/// </remarks>
protected virtual void ConfigureControl(IDockControl control)
{
if (DragDropService != null)
{
control.DragDropService = DragDropService;
}
if (control == null) throw new ArgumentNullException(nameof(control));
if (ContextManager != null)
{

View File

@@ -1,4 +1,5 @@
using Lattice.Core.Docking.Models;
using Lattice.Core.Docking.Abstractions;
using Lattice.Core.Docking.Models;
using Lattice.UI.Docking.Abstractions;
namespace Lattice.UI.Docking.Factories;
@@ -6,96 +7,82 @@ namespace Lattice.UI.Docking.Factories;
/// <summary>
/// Определяет контракт для фабрики, создающей UI-контролы для элементов док-системы.
/// </summary>
/// <remarks>
/// Фабрика обеспечивает абстракцию над созданием конкретных UI-контролов,
/// что позволяет легко заменять реализации для разных платформ или тем оформления.
/// </remarks>
public interface IDockControlFactory
{
/// <summary>
/// Создает контрол для группы разделения.
/// </summary>
/// <param name="group">Модель группы.</param>
/// <returns>Созданный контрол группы.</returns>
/// <returns>
/// Созданный контрол группы.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="group"/> равен null.
/// </exception>
IDockGroupControl CreateGroupControl(DockGroup group);
/// <summary>
/// Создает контрол для контейнера вкладок.
/// </summary>
/// <param name="leaf">Модель листа.</param>
/// <returns>Созданный контрол листа.</returns>
/// <returns>
/// Созданный контрол листа.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="leaf"/> равен null.
/// </exception>
IDockLeafControl CreateLeafControl(DockLeaf leaf);
/// <summary>
/// Создает контрол для плавающего окна.
/// </summary>
/// <param name="window">Модель окна.</param>
/// <returns>Созданный контрол окна.</returns>
/// <returns>
/// Созданный контрол окна.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="window"/> равен null.
/// </exception>
IFloatingWindowControl CreateFloatingWindowControl(DockWindow window);
/// <summary>
/// Создает контрол для автоскрываемой панели.
/// </summary>
/// <param name="panel">Модель панели.</param>
/// <returns>Созданный контрол панели.</returns>
/// <returns>
/// Созданный контрол панели.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="panel"/> равен null.
/// </exception>
IAutoHidePanelControl CreateAutoHidePanelControl(AutoHidePanel panel);
/// <summary>
/// Создает контрол для разделителя.
/// </summary>
/// <param name="orientation">Ориентация разделителя.</param>
/// <returns>Созданный контрол разделителя.</returns>
/// <returns>
/// Созданный контрол разделителя.
/// </returns>
IDockSplitterControl CreateSplitterControl(SplitDirection orientation);
}
/// <summary>
/// Определяет контракт для контрола разделителя.
/// </summary>
public interface IDockSplitterControl : IDockControl
{
/// <summary>
/// Получает или задает ориентацию разделителя.
/// </summary>
SplitDirection Orientation { get; set; }
/// <summary>
/// Получает или задает признак того, что разделитель активен.
/// Создает контрол для произвольного элемента док-системы.
/// </summary>
bool IsActive { get; set; }
/// <summary>
/// Событие, возникающее при начале перетаскивания разделителя.
/// </summary>
event EventHandler DragStarted;
/// <summary>
/// Событие, возникающее при перетаскивании разделителя.
/// </summary>
event EventHandler<SplitterDraggedEventArgs> DragDelta;
/// <summary>
/// Событие, возникающее при завершении перетаскивания разделителя.
/// </summary>
event EventHandler DragCompleted;
}
/// <summary>
/// Аргументы события перетаскивания разделителя.
/// </summary>
public class SplitterDraggedEventArgs : EventArgs
{
/// <summary>
/// Изменение по горизонтали.
/// </summary>
public double HorizontalChange { get; }
/// <summary>
/// Изменение по вертикали.
/// </summary>
public double VerticalChange { get; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="SplitterDraggedEventArgs"/>.
/// </summary>
public SplitterDraggedEventArgs(double horizontalChange, double verticalChange)
{
HorizontalChange = horizontalChange;
VerticalChange = verticalChange;
}
/// <param name="element">Элемент для создания контрола.</param>
/// <returns>
/// Созданный контрол или null, если тип элемента не поддерживается.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="element"/> равен null.
/// </exception>
/// <remarks>
/// Метод использует сопоставление с шаблоном для определения типа элемента
/// и вызова соответствующего метода создания.
/// </remarks>
IDockControl? CreateControlForElement(IDockElement element);
}

View File

@@ -0,0 +1,113 @@
using Lattice.Core.Docking.Abstractions;
using Lattice.Core.Docking.Engine;
using Lattice.UI.Docking.Abstractions;
using System.ComponentModel;
namespace Lattice.UI.Docking.Implementations;
/// <summary>
/// Базовая реализация контрола док-системы.
/// </summary>
public abstract class DockControlBase : IDockControl, INotifyPropertyChanged
{
/// <inheritdoc/>
public event PropertyChangedEventHandler? PropertyChanged;
private IDockElement? _model;
private LayoutManager? _layoutManager;
private IDockContextManager? _contextManager;
private bool _isSelected;
private bool _isActive;
/// <inheritdoc/>
public IDockElement? Model
{
get => _model;
set
{
if (_model != value)
{
_model = value;
OnPropertyChanged(nameof(Model));
}
}
}
/// <inheritdoc/>
public LayoutManager? LayoutManager
{
get => _layoutManager;
set
{
if (_layoutManager != value)
{
_layoutManager = value;
OnPropertyChanged(nameof(LayoutManager));
}
}
}
/// <inheritdoc/>
public IDockContextManager? ContextManager
{
get => _contextManager;
set
{
if (_contextManager != value)
{
_contextManager = value;
OnPropertyChanged(nameof(ContextManager));
}
}
}
/// <inheritdoc/>
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected != value)
{
_isSelected = value;
OnPropertyChanged(nameof(IsSelected));
}
}
}
/// <inheritdoc/>
public bool IsActive
{
get => _isActive;
set
{
if (_isActive != value)
{
_isActive = value;
OnPropertyChanged(nameof(IsActive));
}
}
}
/// <inheritdoc/>
public abstract void Refresh();
/// <inheritdoc/>
public abstract void ApplyTheme(IDockTheme theme);
/// <inheritdoc/>
public virtual void OnModelPropertyChanged(string propertyName)
{
// Базовая реализация просто обновляет весь контрол
Refresh();
}
/// <summary>
/// Вызывает событие <see cref="PropertyChanged"/>.
/// </summary>
/// <param name="propertyName">Имя измененного свойства.</param>
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

View File

@@ -9,8 +9,4 @@
<ItemGroup>
<ProjectReference Include="..\Lattice.Core.Docking\Lattice.Core.Docking.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Converters\" />
</ItemGroup>
</Project>

View File

@@ -1,5 +1,4 @@
using Lattice.Core.Docking.Abstractions;
using Lattice.Core.Docking.Engine;
using Lattice.Core.Docking.Engine;
using Lattice.Core.Docking.Services;
using Lattice.UI.Docking.Abstractions;
using Lattice.UI.Docking.Factories;
@@ -8,14 +7,7 @@ namespace Lattice.UI.Docking;
/// <summary>
/// Предоставляет статический API для инициализации и управления UI-фреймворком Lattice.
/// Является точкой входа для интеграции док-системы в приложение и централизованным
/// хранилищем для основных сервисов и компонентов.
/// </summary>
/// <remarks>
/// Этот класс реализует шаблон Singleton для доступа к глобальным сервисам.
/// Все компоненты инициализируются через строитель <see cref="LatticeBuilder"/>,
/// что обеспечивает гибкую конфигурацию и соблюдение принципа инверсии зависимостей.
/// </remarks>
public static class LatticeUIFramework
{
private static bool _isInitialized;
@@ -24,22 +16,11 @@ public static class LatticeUIFramework
/// <summary>
/// Получает значение, указывающее, инициализирован ли фреймворк.
/// </summary>
/// <value>
/// true, если фреймворк был инициализирован вызовом <see cref="Initialize"/>;
/// в противном случае — false.
/// </value>
public static bool IsInitialized => _isInitialized;
/// <summary>
/// Получает текущий строитель конфигурации фреймворка.
/// </summary>
/// <value>
/// Экземпляр <see cref="LatticeBuilder"/>, используемый для настройки фреймворка.
/// Возвращает null, если фреймворк не инициализирован.
/// </value>
/// <exception cref="InvalidOperationException">
/// Выбрасывается при попытке доступа к свойству до инициализации фреймворка.
/// </exception>
public static LatticeBuilder CurrentBuilder
{
get
@@ -53,67 +34,31 @@ public static class LatticeUIFramework
/// <summary>
/// Получает менеджер макета из текущего строителя.
/// </summary>
/// <value>
/// Экземпляр <see cref="LayoutManager"/>, управляющий структурой док-системы.
/// </value>
public static LayoutManager? LayoutManager => _currentBuilder?.LayoutManager;
/// <summary>
/// Получает реестр контента из текущего строителя.
/// </summary>
/// <value>
/// Экземпляр <see cref="ContentRegistry"/>, содержащий зарегистрированные типы контента.
/// </value>
public static ContentRegistry? ContentRegistry => _currentBuilder?.ContentRegistry;
/// <summary>
/// Получает фабрику контролов из текущего строителя.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDockControlFactory"/> для создания UI-контролов.
/// </value>
public static IDockControlFactory? ControlFactory => _currentBuilder?.ControlFactory;
/// <summary>
/// Получает сервис перетаскивания из текущего строителя.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDockDragDropService"/> для управления операциями drag-and-drop.
/// </value>
public static IDockDragDropService? DragDropService => _currentBuilder?.DragDropService;
/// <summary>
/// Получает менеджер контекстных меню из текущего строителя.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDockContextManager"/> для управления контекстными меню.
/// </value>
public static IDockContextManager? ContextManager => _currentBuilder?.ContextManager;
/// <summary>
/// Получает UI-сервис из текущего строителя.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDockUIService"/> для выполнения платформенно-зависимых операций.
/// </value>
public static IDockUIService? UIService => _currentBuilder?.UIService;
/// <summary>
/// Инициализирует фреймворк Lattice с указанными параметрами.
/// </summary>
/// <param name="options">
/// Настройки инициализации. Если null, используются параметры по умолчанию.
/// </param>
/// <returns>
/// Экземпляр <see cref="LatticeBuilder"/> для дальнейшей конфигурации фреймворка.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Выбрасывается, если фреймворк уже инициализирован.
/// </exception>
/// <remarks>
/// Этот метод должен вызываться один раз при запуске приложения, перед любыми
/// попытками использования компонентов док-системы.
/// </remarks>
public static LatticeBuilder Initialize(LatticeOptions? options = null)
{
if (_isInitialized)
@@ -121,11 +66,9 @@ public static class LatticeUIFramework
options ??= new LatticeOptions();
// Создаем основные компоненты Core-слоя
var layoutManager = new LayoutManager();
var contentRegistry = new ContentRegistry();
// Создаем строитель с основными компонентами
_currentBuilder = new LatticeBuilder(layoutManager, contentRegistry, options);
_isInitialized = true;
@@ -135,10 +78,6 @@ public static class LatticeUIFramework
/// <summary>
/// Сбрасывает состояние фреймворка к неинициализированному.
/// </summary>
/// <remarks>
/// Используется в основном для целей тестирования. В рабочем приложении
/// фреймворк должен инициализироваться один раз на протяжении жизненного цикла.
/// </remarks>
public static void Reset()
{
_isInitialized = false;
@@ -148,54 +87,35 @@ public static class LatticeUIFramework
/// <summary>
/// Представляет настройки инициализации фреймворка Lattice.
/// Позволяет кастомизировать поведение системы при запуске.
/// </summary>
public class LatticeOptions
{
/// <summary>
/// Получает или задает значение, указывающее, следует ли автоматически
/// регистрировать стандартные команды (закрыть, сделать плавающим и т.д.).
/// регистрировать стандартные команды.
/// </summary>
/// <value>
/// true, чтобы зарегистрировать стандартные команды; в противном случае — false.
/// Значение по умолчанию: true.
/// </value>
public bool RegisterDefaultCommands { get; set; } = true;
/// <summary>
/// Получает или задает значение, указывающее, следует ли автоматически
/// создавать сервисы при их первом запросе.
/// </summary>
/// <value>
/// true, чтобы автоматически создавать сервисы; в противном случае — false.
/// Значение по умолчанию: true.
/// </value>
public bool AutoCreateServices { get; set; } = true;
/// <summary>
/// Получает или задает идентификатор приложения, используемый при
/// сериализации макета для различения конфигураций разных приложений.
/// Получает или задает идентификатор приложения.
/// </summary>
/// <value>
/// Строковый идентификатор приложения или null, если идентификатор не задан.
/// </value>
public string? ApplicationId { get; set; }
/// <summary>
/// Получает или задает значение, указывающее, следует ли включить
/// расширенное логирование операций системы.
/// </summary>
/// <value>
/// true, чтобы включить подробное логирование; в противном случае — false.
/// Значение по умолчанию: false.
/// </value>
public bool EnableVerboseLogging { get; set; } = false;
}
/// <summary>
/// Предоставляет fluent-интерфейс для конфигурации фреймворка Lattice.
/// Инкапсулирует процесс настройки всех компонентов системы и обеспечивает
/// согласованное состояние после инициализации.
/// </summary>
public sealed class LatticeBuilder
{
@@ -203,120 +123,59 @@ public sealed class LatticeBuilder
private readonly ContentRegistry _contentRegistry;
private readonly LatticeOptions _options;
private IDockControlFactory? _controlFactory;
private IDockDragDropService? _dragDropService;
private IDockContextManager? _contextManager;
private IDockUIService? _uiService;
private bool _isBuilt;
/// <summary>
/// Получает менеджер макета, связанный с этим строителем.
/// Получает менеджер макета.
/// </summary>
/// <value>
/// Экземпляр <see cref="LayoutManager"/> для управления структурой док-системы.
/// </value>
public LayoutManager LayoutManager => _layoutManager;
/// <summary>
/// Получает реестр контента, связанный с этим строителем.
/// Получает реестр контента.
/// </summary>
/// <value>
/// Экземпляр <see cref="ContentRegistry"/> для регистрации типов контента.
/// </value>
public ContentRegistry ContentRegistry => _contentRegistry;
/// <summary>
/// Получает фабрику контролов, связанную с этим строителем.
/// Получает фабрику контролов.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDockControlFactory"/> или null, если фабрика не задана.
/// </value>
public IDockControlFactory? ControlFactory => _controlFactory;
/// <summary>
/// Получает сервис перетаскивания, связанный с этим строителем.
/// Получает менеджер контекстных меню.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDockDragDropService"/> или null, если сервис не задан.
/// </value>
public IDockDragDropService? DragDropService => _dragDropService;
/// <summary>
/// Получает менеджер контекстных меню, связанный с этим строителем.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDockContextManager"/> или null, если менеджер не задан.
/// </value>
public IDockContextManager? ContextManager => _contextManager;
/// <summary>
/// Получает UI-сервис, связанный с этим строителем.
/// Получает UI-сервис.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDockUIService"/> или null, если сервис не задан.
/// </value>
public IDockUIService? UIService => _uiService;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="LatticeBuilder"/>.
/// </summary>
/// <param name="layoutManager">Менеджер макета.</param>
/// <param name="contentRegistry">Реестр контента.</param>
/// <param name="options">Опции инициализации.</param>
internal LatticeBuilder(LayoutManager layoutManager, ContentRegistry contentRegistry, LatticeOptions options)
{
_layoutManager = layoutManager ?? throw new ArgumentNullException(nameof(layoutManager));
_contentRegistry = contentRegistry ?? throw new ArgumentNullException(nameof(contentRegistry));
_options = options ?? throw new ArgumentNullException(nameof(options));
_layoutManager.ContentRegistry = contentRegistry;
}
/// <summary>
/// Регистрирует фабрику контролов для создания UI-элементов.
/// </summary>
/// <param name="factory">
/// Фабрика контролов, реализующая <see cref="IDockControlFactory"/>.
/// </param>
/// <returns>
/// Текущий экземпляр строителя для цепочки вызовов.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="factory"/> равен null.
/// </exception>
public LatticeBuilder WithControlFactory(IDockControlFactory factory)
{
_controlFactory = factory ?? throw new ArgumentNullException(nameof(factory));
return this;
}
/// <summary>
/// Регистрирует сервис перетаскивания для управления операциями drag-and-drop.
/// </summary>
/// <param name="service">
/// Сервис перетаскивания, реализующий <see cref="IDockDragDropService"/>.
/// </param>
/// <returns>
/// Текущий экземпляр строителя для цепочки вызовов.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="service"/> равен null.
/// </exception>
public LatticeBuilder WithDragDropService(IDockDragDropService service)
{
_dragDropService = service ?? throw new ArgumentNullException(nameof(service));
return this;
}
/// <summary>
/// Регистрирует менеджер контекстных меню для управления контекстными действиями.
/// </summary>
/// <param name="manager">
/// Менеджер контекстных меню, реализующий <see cref="IDockContextManager"/>.
/// </param>
/// <returns>
/// Текущий экземпляр строителя для цепочки вызовов.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="manager"/> равен null.
/// </exception>
public LatticeBuilder WithContextManager(IDockContextManager manager)
{
_contextManager = manager ?? throw new ArgumentNullException(nameof(manager));
@@ -326,15 +185,6 @@ public sealed class LatticeBuilder
/// <summary>
/// Регистрирует UI-сервис для выполнения платформенно-зависимых операций.
/// </summary>
/// <param name="service">
/// UI-сервис, реализующий <see cref="IDockUIService"/>.
/// </param>
/// <returns>
/// Текущий экземпляр строителя для цепочки вызовов.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="service"/> равен null.
/// </exception>
public LatticeBuilder WithUIService(IDockUIService service)
{
_uiService = service ?? throw new ArgumentNullException(nameof(service));
@@ -342,20 +192,8 @@ public sealed class LatticeBuilder
}
/// <summary>
/// Регистрирует тип контента в реестре для последующего создания экземпляров.
/// Регистрирует тип контента в реестре.
/// </summary>
/// <typeparam name="T">
/// Тип контента, реализующий <see cref="IDockContent"/>.
/// </typeparam>
/// <param name="contentTypeId">Уникальный идентификатор типа контента.</param>
/// <param name="factory">Фабричный метод для создания экземпляров контента.</param>
/// <param name="metadata">Метаданные типа контента (опционально).</param>
/// <returns>
/// Текущий экземпляр строителя для цепочки вызовов.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="contentTypeId"/> или <paramref name="factory"/> равны null.
/// </exception>
public LatticeBuilder RegisterContentType<T>(string contentTypeId, Func<T> factory, ContentMetadata? metadata = null)
where T : Core.Docking.Abstractions.IDockContent
{
@@ -367,73 +205,76 @@ public sealed class LatticeBuilder
}
/// <summary>
/// Завершает конфигурацию и создает готовый к использованию док-хост.
/// Завершает конфигурацию фреймворка.
/// </summary>
/// <returns>
/// Экземпляр <see cref="IDockHost"/>, настроенный в соответствии с текущей конфигурацией.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Выбрасывается, если не задана фабрика контролов или метод уже был вызван.
/// </exception>
public IDockHost Build()
public ILatticeFramework Build()
{
if (_isBuilt)
throw new InvalidOperationException("Builder has already been built.");
throw new InvalidOperationException("Framework has already been built.");
if (_controlFactory == null)
throw new InvalidOperationException("Control factory must be specified. Call WithControlFactory() first.");
// Автоматически создаем отсутствующие сервисы, если включена опция
if (_options.AutoCreateServices)
{
_dragDropService ??= CreateDefaultDragDropService();
_contextManager ??= CreateDefaultContextManager();
_uiService ??= CreateDefaultUIService();
}
// Создаем хост через фабрику
// (предполагается, что фабрика имеет метод CreateDockHost)
if (_controlFactory is WinUI.Factories.WinUIDockControlFactory winUIFactory)
{
var host = winUIFactory.CreateDockHost();
// Настраиваем хост
host.LayoutManager = _layoutManager;
host.DragDropService = _dragDropService;
host.ContextManager = _contextManager;
_isBuilt = true;
return host;
}
throw new NotSupportedException($"Control factory of type {_controlFactory.GetType().Name} is not supported.");
_isBuilt = true;
return new LatticeFramework(this);
}
}
/// <summary>
/// Предоставляет интерфейс для доступа к компонентам фреймворка Lattice.
/// </summary>
public interface ILatticeFramework
{
/// <summary>
/// Получает менеджер макета.
/// </summary>
LayoutManager LayoutManager { get; }
/// <summary>
/// Создает сервис перетаскивания по умолчанию.
/// Получает реестр контента.
/// </summary>
private IDockDragDropService CreateDefaultDragDropService()
{
// Реализация зависит от платформы
// В реальном коде здесь должна быть проверка платформы
return new WinUI.Services.WinUIDragDropService();
}
ContentRegistry ContentRegistry { get; }
/// <summary>
/// Создает менеджер контекстных меню по умолчанию.
/// Получает фабрику контролов.
/// </summary>
private IDockContextManager CreateDefaultContextManager()
{
// Реализация зависит от платформы
return new WinUI.Services.WinUIDockContextManager();
}
IDockControlFactory? ControlFactory { get; }
/// <summary>
/// Создает UI-сервис по умолчанию.
/// Получает менеджер контекстных меню.
/// </summary>
private IDockUIService CreateDefaultUIService()
IDockContextManager? ContextManager { get; }
/// <summary>
/// Получает UI-сервис.
/// </summary>
IDockUIService? UIService { get; }
}
/// <summary>
/// Реализация интерфейса <see cref="ILatticeFramework"/>.
/// </summary>
internal class LatticeFramework : ILatticeFramework
{
private readonly LatticeBuilder _builder;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="LatticeFramework"/>.
/// </summary>
public LatticeFramework(LatticeBuilder builder)
{
// Реализация зависит от платформы
return new WinUI.Services.WinUIDockUIService();
_builder = builder ?? throw new ArgumentNullException(nameof(builder));
}
/// <inheritdoc/>
public LayoutManager LayoutManager => _builder.LayoutManager;
/// <inheritdoc/>
public ContentRegistry ContentRegistry => _builder.ContentRegistry;
/// <inheritdoc/>
public IDockControlFactory? ControlFactory => _builder.ControlFactory;
/// <inheritdoc/>
public IDockContextManager? ContextManager => _builder.ContextManager;
/// <inheritdoc/>
public IDockUIService? UIService => _builder.UIService;
}

View File

@@ -1,148 +0,0 @@
using Lattice.Core.Geometry;
using Lattice.UI.Docking.Abstractions;
namespace Lattice.UI.Docking.Models;
/// <summary>
/// Расширенная информация о перетаскивании для UI-слоя.
/// Добавляет визуальные аспекты и UI-контекст к базовой информации о перетаскивании.
/// </summary>
public class UiDragInfo
{
/// <summary>
/// Базовые данные перетаскивания.
/// </summary>
public Core.DragDrop.Models.DragInfo BaseDragInfo { get; }
/// <summary>
/// UI-контрол, который является источником перетаскивания.
/// </summary>
public IDockControl? SourceControl { get; }
/// <summary>
/// Визуальное представление перетаскиваемого элемента.
/// </summary>
public object? DragVisual { get; set; }
/// <summary>
/// Смещение курсора относительно элемента при начале перетаскивания.
/// </summary>
public Point VisualOffset { get; set; }
/// <summary>
/// Размер визуального представления.
/// </summary>
public Size VisualSize { get; set; }
/// <summary>
/// Прозрачность визуального представления.
/// </summary>
public double VisualOpacity { get; set; } = 0.7;
/// <summary>
/// Инициализирует новый экземпляр <see cref="UiDragInfo"/>.
/// </summary>
public UiDragInfo(Core.DragDrop.Models.DragInfo baseDragInfo, IDockControl? sourceControl = null)
{
BaseDragInfo = baseDragInfo;
SourceControl = sourceControl;
}
}
/// <summary>
/// Расширенная информация о сбросе для UI-слоя.
/// Добавляет визуальные подсказки и UI-контекст.
/// </summary>
public class UiDropInfo
{
/// <summary>
/// Базовые данные сброса.
/// </summary>
public Core.DragDrop.Models.DropInfo BaseDropInfo { get; }
/// <summary>
/// UI-контрол, который является целью сброса.
/// </summary>
public IDockControl? TargetControl { get; }
/// <summary>
/// Позиция сброса относительно элемента.
/// </summary>
public DropPosition DropPosition { get; set; }
/// <summary>
/// Визуальная подсказка для области сброса.
/// </summary>
public object? DropHintVisual { get; set; }
/// <summary>
/// Признак того, что курсор находится над допустимой областью сброса.
/// </summary>
public bool IsOverValidTarget { get; set; }
/// <summary>
/// Интенсивность подсветки области сброса (0.0 - 1.0).
/// </summary>
public double HighlightIntensity { get; set; }
/// <summary>
/// Инициализирует новый экземпляр <see cref="UiDropInfo"/>.
/// </summary>
public UiDropInfo(Core.DragDrop.Models.DropInfo baseDropInfo, IDockControl? targetControl = null)
{
BaseDropInfo = baseDropInfo;
TargetControl = targetControl;
DropPosition = DropPosition.Center;
}
}
/// <summary>
/// Определяет позицию сброса относительно элемента.
/// </summary>
public enum DropPosition
{
/// <summary> Слева от элемента. </summary>
Left,
/// <summary> Справа от элемента. </summary>
Right,
/// <summary> Сверху от элемента. </summary>
Top,
/// <summary> Снизу от элемента. </summary>
Bottom,
/// <summary> В центре элемента (для объединения вкладок). </summary>
Center,
/// <summary> В виде новой вкладки. </summary>
Tab
}
public class DragStartedEventArgs : EventArgs
{
public IDockControl? Source { get; }
public Core.DragDrop.Models.DragInfo DragInfo { get; }
// ... конструктор
}
public class DragUpdatedEventArgs : EventArgs
{
public IDockControl? Source { get; }
public double X { get; }
public double Y { get; }
public Core.DragDrop.Models.DragInfo DragInfo { get; }
// ... конструктор
}
public class DragCompletedEventArgs : EventArgs
{
public IDockControl? Source { get; }
public IDockControl? Target { get; }
public Models.DropPosition DropPosition { get; }
public Core.DragDrop.Models.DragInfo? DragInfo { get; }
public bool Success { get; }
// ... конструктор
}

View File

@@ -1,417 +0,0 @@
using Lattice.Core.DragDrop.Abstractions;
using Lattice.Core.DragDrop.Services;
using Lattice.UI.Docking.Abstractions;
using Lattice.UI.Docking.Models;
namespace Lattice.UI.Docking.Services;
/// <summary>
/// Реализация сервиса перетаскивания для UI-слоя док-системы.
/// Координирует взаимодействие между базовым менеджером перетаскивания
/// и UI-контролами, обеспечивая визуальную обратную связь.
/// </summary>
public class DockDragDropService : IDockDragDropService
{
private readonly DragDropManagerEx _dragDropManager;
private readonly Dictionary<IDockControl, IDragSource> _registeredDragSources = new();
private readonly Dictionary<IDockControl, IDropTarget> _registeredDropTargets = new();
private UiDragInfo? _currentUiDragInfo;
private UiDropInfo? _currentUiDropInfo;
private IDropTarget? _lastDropTarget;
/// <summary>
/// Инициализирует новый экземпляр <see cref="DockDragDropService"/>.
/// </summary>
public DockDragDropService()
{
_dragDropManager = new DragDropManagerEx();
HookEvents();
}
/// <summary>
/// Инициализирует новый экземпляр с указанным менеджером перетаскивания.
/// </summary>
public DockDragDropService(DragDropManagerEx dragDropManager)
{
_dragDropManager = dragDropManager;
HookEvents();
}
private void HookEvents()
{
_dragDropManager.DragStarted += OnDragStarted;
_dragDropManager.DragUpdated += OnDragUpdated;
_dragDropManager.DragCompleted += OnDragCompleted;
_dragDropManager.DragCancelled += OnDragCancelled;
_dragDropManager.DropTargetChanged += OnDropTargetChanged;
}
/// <inheritdoc/>
public void RegisterDragSource(IDockControl element, IDragSource dragSource)
{
if (element == null) throw new ArgumentNullException(nameof(element));
if (dragSource == null) throw new ArgumentNullException(nameof(dragSource));
_registeredDragSources[element] = dragSource;
// Регистрируем границы элемента в менеджере
var bounds = CalculateBounds(element);
_dragDropManager.RegisterDropTarget(dragSource as IDropTarget ?? new AdapterDropTarget(dragSource),
bounds, 0, element.GetType().Name);
}
/// <inheritdoc/>
public void RegisterDropTarget(IDockControl element, IDropTarget dropTarget)
{
if (element == null) throw new ArgumentNullException(nameof(element));
if (dropTarget == null) throw new ArgumentNullException(nameof(dropTarget));
_registeredDropTargets[element] = dropTarget;
var bounds = CalculateBounds(element);
_dragDropManager.RegisterDropTarget(dropTarget, bounds, 0, element.GetType().Name);
}
/// <inheritdoc/>
public void UnregisterDragSource(IDockControl element)
{
if (element == null) throw new ArgumentNullException(nameof(element));
_registeredDragSources.Remove(element);
// TODO: Реализовать отмену регистрации в менеджере
}
/// <inheritdoc/>
public void UnregisterDropTarget(IDockControl element)
{
if (element == null) throw new ArgumentNullException(nameof(element));
_registeredDropTargets.Remove(element);
// TODO: Реализовать отмену регистрации в менеджере
}
/// <inheritdoc/>
public void StartDrag(IDockControl element, Core.DragDrop.Models.DragInfo dragInfo)
{
if (element == null) throw new ArgumentNullException(nameof(element));
if (dragInfo == null) throw new ArgumentNullException(nameof(dragInfo));
if (_registeredDragSources.TryGetValue(element, out var dragSource))
{
_currentUiDragInfo = new UiDragInfo(dragInfo, element);
_dragDropManager.StartDrag(dragSource, dragInfo.StartPosition);
}
}
/// <inheritdoc/>
public void UpdateDragVisual(double x, double y)
{
var position = new Core.DragDrop.Geometry.Point(x, y);
_dragDropManager.UpdateDrag(position);
if (_currentUiDragInfo != null)
{
// Обновляем позицию визуального представления
OnDragVisualUpdated?.Invoke(this, new DragVisualUpdatedEventArgs(
_currentUiDragInfo, position));
}
}
/// <inheritdoc/>
public void EndDrag(double x, double y)
{
var position = new Core.DragDrop.Geometry.Point(x, y);
_dragDropManager.EndDrag(position);
}
/// <inheritdoc/>
public void CancelDrag()
{
_dragDropManager.CancelDrag();
}
/// <inheritdoc/>
public void ShowDropHint(IDockControl element, DropPosition position)
{
if (_currentUiDropInfo != null)
{
_currentUiDropInfo.DropPosition = position;
_currentUiDropInfo.IsOverValidTarget = true;
_currentUiDropInfo.HighlightIntensity = 0.8;
OnDropHintChanged?.Invoke(this, new DropHintEventArgs(
element, position, true, _currentUiDropInfo.HighlightIntensity));
}
}
/// <inheritdoc/>
public void HideDropHint()
{
if (_currentUiDropInfo != null)
{
_currentUiDropInfo.IsOverValidTarget = false;
_currentUiDropInfo.HighlightIntensity = 0.0;
OnDropHintChanged?.Invoke(this, new DropHintEventArgs(
_currentUiDropInfo.TargetControl,
_currentUiDropInfo.DropPosition,
false,
0.0));
}
}
/// <inheritdoc/>
public event EventHandler<DragStartedEventArgs>? DragStarted;
/// <inheritdoc/>
public event EventHandler<DragUpdatedEventArgs>? DragUpdated;
/// <inheritdoc/>
public event EventHandler<DragCompletedEventArgs>? DragCompleted;
/// <inheritdoc/>
public event EventHandler? DragCancelled;
/// <summary>
/// Событие, возникающее при обновлении визуального представления перетаскивания.
/// </summary>
public event EventHandler<DragVisualUpdatedEventArgs>? OnDragVisualUpdated;
/// <summary>
/// Событие, возникающее при изменении визуальной подсказки сброса.
/// </summary>
public event EventHandler<DropHintEventArgs>? OnDropHintChanged;
private void OnDragStarted(object? sender, DragStartedEventArgs e)
{
// Обновляем UI-информацию
if (_currentUiDragInfo != null)
{
_currentUiDragInfo.BaseDragInfo.StartPosition = e.StartPosition;
// Создаем визуальное представление
CreateDragVisual(_currentUiDragInfo);
DragStarted?.Invoke(this, new DragStartedEventArgs(
_currentUiDragInfo.SourceControl,
_currentUiDragInfo.BaseDragInfo));
}
}
private void OnDragUpdated(object? sender, DragUpdatedEventArgs e)
{
if (_currentUiDragInfo != null)
{
// Обновляем позицию визуального представления
UpdateDragVisualPosition(e.Position);
DragUpdated?.Invoke(this, new DragUpdatedEventArgs(
_currentUiDragInfo.SourceControl,
e.Position.X,
e.Position.Y,
_currentUiDragInfo.BaseDragInfo));
}
}
private void OnDragCompleted(object? sender, DragCompletedEventArgs e)
{
var targetControl = _currentUiDropInfo?.TargetControl;
var dropPosition = _currentUiDropInfo?.DropPosition ?? DropPosition.Center;
DragCompleted?.Invoke(this, new DragCompletedEventArgs(
_currentUiDragInfo?.SourceControl,
targetControl,
dropPosition,
_currentUiDragInfo?.BaseDragInfo,
e.Effects != Core.DragDrop.Enums.DragDropEffects.None));
// Очищаем визуальные представления
CleanupDragVisual();
CleanupDropHint();
_currentUiDragInfo = null;
_currentUiDropInfo = null;
_lastDropTarget = null;
}
private void OnDragCancelled(object? sender, DragCancelledEventArgs e)
{
DragCancelled?.Invoke(this, EventArgs.Empty);
CleanupDragVisual();
CleanupDropHint();
_currentUiDragInfo = null;
_currentUiDropInfo = null;
_lastDropTarget = null;
}
private void OnDropTargetChanged(object? sender, DropTargetChangedEventArgs e)
{
var dropTarget = e.Target;
// Обновляем UI-информацию о сбросе
if (dropTarget != null)
{
// Находим соответствующий UI-контрол
var targetControl = _registeredDropTargets
.FirstOrDefault(kv => kv.Value == dropTarget)
.Key;
_currentUiDropInfo = new UiDropInfo(
new Core.DragDrop.Models.DropInfo(
_dragDropManager.CurrentDragInfo?.Data,
e.TargetBounds.Center,
_dragDropManager.CurrentDragInfo?.AllowedEffects ?? Core.DragDrop.Enums.DragDropEffects.None,
dropTarget),
targetControl);
_currentUiDropInfo.DropPosition = CalculateDropPosition(
_currentUiDragInfo?.BaseDragInfo.StartPosition ?? Core.DragDrop.Geometry.Point.Zero,
e.TargetBounds);
}
else
{
_currentUiDropInfo = null;
}
// Уведомляем об изменении цели сброса
if (_lastDropTarget != dropTarget)
{
if (_lastDropTarget != null)
{
HideDropHint();
}
if (dropTarget != null && _currentUiDropInfo != null)
{
ShowDropHint(_currentUiDropInfo.TargetControl, _currentUiDropInfo.DropPosition);
}
_lastDropTarget = dropTarget;
}
}
private Core.DragDrop.Geometry.Rect CalculateBounds(IDockControl element)
{
// В UI-реализациях этот метод должен быть переопределен
// для вычисления реальных границ элемента на экране
return new Core.DragDrop.Geometry.Rect(0, 0, 100, 100);
}
private DropPosition CalculateDropPosition(Core.DragDrop.Geometry.Point cursorPos, Core.DragDrop.Geometry.Rect targetBounds)
{
// Простая логика определения позиции сброса
var center = targetBounds.Center;
var relativeX = (cursorPos.X - targetBounds.X) / targetBounds.Width;
var relativeY = (cursorPos.Y - targetBounds.Y) / targetBounds.Height;
if (relativeX < 0.25) return DropPosition.Left;
if (relativeX > 0.75) return DropPosition.Right;
if (relativeY < 0.25) return DropPosition.Top;
if (relativeY > 0.75) return DropPosition.Bottom;
return DropPosition.Center;
}
private void CreateDragVisual(UiDragInfo dragInfo)
{
// В UI-реализациях этот метод должен создавать визуальное представление
OnDragVisualUpdated?.Invoke(this, new DragVisualUpdatedEventArgs(
dragInfo, dragInfo.BaseDragInfo.StartPosition));
}
private void UpdateDragVisualPosition(Core.DragDrop.Geometry.Point position)
{
if (_currentUiDragInfo != null)
{
OnDragVisualUpdated?.Invoke(this, new DragVisualUpdatedEventArgs(
_currentUiDragInfo, position));
}
}
private void CleanupDragVisual()
{
// В UI-реализациях этот метод должен очищать визуальное представление
OnDragVisualUpdated?.Invoke(this, new DragVisualUpdatedEventArgs(null, Core.DragDrop.Geometry.Point.Zero));
}
private void CleanupDropHint()
{
HideDropHint();
}
}
/// <summary>
/// Адаптер для преобразования IDragSource в IDropTarget.
/// </summary>
internal class AdapterDropTarget : IDropTarget
{
private readonly IDragSource _dragSource;
public AdapterDropTarget(IDragSource dragSource)
{
_dragSource = dragSource;
}
public bool CanAcceptDrop(Core.DragDrop.Models.DropInfo dropInfo) => false;
public void DragOver(Core.DragDrop.Models.DropInfo dropInfo) { }
public void Drop(Core.DragDrop.Models.DropInfo dropInfo) { }
public void DragLeave() { }
}
/// <summary>
/// Аргументы события обновления визуального представления перетаскивания.
/// </summary>
public class DragVisualUpdatedEventArgs : EventArgs
{
/// <summary>
/// Информация о перетаскивании.
/// </summary>
public UiDragInfo? DragInfo { get; }
/// <summary>
/// Текущая позиция.
/// </summary>
public Core.DragDrop.Geometry.Point Position { get; }
public DragVisualUpdatedEventArgs(UiDragInfo? dragInfo, Core.DragDrop.Geometry.Point position)
{
DragInfo = dragInfo;
Position = position;
}
}
/// <summary>
/// Аргументы события изменения визуальной подсказки сброса.
/// </summary>
public class DropHintEventArgs : EventArgs
{
/// <summary>
/// Целевой элемент.
/// </summary>
public IDockControl? Target { get; }
/// <summary>
/// Позиция сброса.
/// </summary>
public DropPosition Position { get; }
/// <summary>
/// Показывает, видима ли подсказка.
/// </summary>
public bool IsVisible { get; }
/// <summary>
/// Интенсивность подсветки.
/// </summary>
public double Intensity { get; }
public DropHintEventArgs(IDockControl? target, DropPosition position, bool isVisible, double intensity)
{
Target = target;
Position = position;
IsVisible = isVisible;
Intensity = intensity;
}
}

View File

@@ -1,4 +1,5 @@
using Lattice.UI.Docking.Abstractions;
// Lattice.UI.Docking\Services\DockUIServiceBase.cs
using Lattice.UI.Docking.Abstractions;
namespace Lattice.UI.Docking.Services;
@@ -7,107 +8,21 @@ namespace Lattice.UI.Docking.Services;
/// </summary>
public abstract class DockUIServiceBase : IDockUIService
{
private IDockTheme? _currentTheme;
/// <inheritdoc/>
public abstract object CreateMainWindow(IDockHost host);
/// <inheritdoc/>
public virtual bool? ShowDialog(string title, object content)
{
// Базовая реализация - просто возвращает null
// В производных классах должна быть реальная реализация
return null;
}
public abstract bool? ShowDialog(string title, object content);
/// <inheritdoc/>
public virtual void ShowMessage(string message, string caption)
{
// Базовая реализация не делает ничего
// В производных классах должна быть реальная реализация
}
public abstract void ShowMessage(string message, string caption);
/// <inheritdoc/>
public virtual bool Confirm(string message, string caption)
{
// Базовая реализация всегда возвращает true
// В производных классах должна быть реальная реализация
return true;
}
public abstract bool Confirm(string message, string caption);
/// <inheritdoc/>
public virtual string? Prompt(string prompt, string? defaultValue = null)
{
// Базовая реализация возвращает значение по умолчанию
// В производных классах должна быть реальная реализация
return defaultValue;
}
public abstract string? Prompt(string prompt, string? defaultValue = null);
/// <inheritdoc/>
public virtual void InvokeOnUIThread(Action action)
{
// Базовая реализация просто выполняет действие
// В производных классах должна быть синхронизация с UI-потоком
action?.Invoke();
}
/// <inheritdoc/>
public virtual IDockTheme GetCurrentTheme()
{
return _currentTheme ?? CreateDefaultTheme();
}
/// <inheritdoc/>
public virtual void SetTheme(IDockTheme theme)
{
_currentTheme = theme;
theme.Apply();
}
/// <summary>
/// Создает тему по умолчанию.
/// </summary>
protected virtual IDockTheme CreateDefaultTheme()
{
return new DefaultDockTheme();
}
}
/// <summary>
/// Тема оформления по умолчанию.
/// </summary>
public class DefaultDockTheme : IDockTheme
{
public string Name => "Default";
public string BackgroundColor { get; set; } = "#1E1E1E";
public string PanelBackgroundColor { get; set; } = "#252526";
public string TabBackgroundColor { get; set; } = "#2D2D2D";
public string ActiveTabBackgroundColor { get; set; } = "#3E3E3E";
public string BorderColor { get; set; } = "#3F3F46";
public string SplitterColor { get; set; } = "#2D2D2D";
public string TextColor { get; set; } = "#CCCCCC";
public string AccentColor { get; set; } = "#007ACC";
public double CornerRadius { get; set; } = 3.0;
public double BorderThickness { get; set; } = 1.0;
public double SplitterWidth { get; set; } = 4.0;
public void Apply()
{
// В UI-реализациях этот метод должен применять тему к элементам
}
public void Reset()
{
BackgroundColor = "#1E1E1E";
PanelBackgroundColor = "#252526";
TabBackgroundColor = "#2D2D2D";
ActiveTabBackgroundColor = "#3E3E3E";
BorderColor = "#3F3F46";
SplitterColor = "#2D2D2D";
TextColor = "#CCCCCC";
AccentColor = "#007ACC";
CornerRadius = 3.0;
BorderThickness = 1.0;
SplitterWidth = 4.0;
}
public abstract void InvokeOnUIThread(Action action);
}

View File

@@ -34,21 +34,16 @@ public static class DockUtilities
var control = factory.CreateControlForElement(element);
if (control == null) return null;
// Устанавливаем родительский контрол
if (parentControl != null)
{
// Здесь может быть установка дополнительных связей
}
// Рекурсивно создаем дочерние контролы
if (element is DockGroup group)
if (element is DockGroup group && control is IDockGroupControl groupControl)
{
if (control is IDockGroupControl groupControl)
{
var firstChild = CreateControlForElement(group.First, factory, control);
var secondChild = CreateControlForElement(group.Second, factory, control);
groupControl.SetChildren(firstChild, secondChild);
}
var firstChild = CreateControlForElement(group.First, factory, control);
var secondChild = CreateControlForElement(group.Second, factory, control);
groupControl.SetChildren(firstChild, secondChild);
}
return control;
@@ -57,10 +52,9 @@ public static class DockUtilities
/// <summary>
/// Находит контрол для указанного элемента в дереве контролов.
/// </summary>
public static IDockControl? FindControlForElement(IDockControl root, IDockElement element)
public static IDockControl? FindControlForElement(IDockControl? root, IDockElement element)
{
if (root == null) throw new ArgumentNullException(nameof(root));
if (element == null) throw new ArgumentNullException(nameof(element));
if (root == null || element == null) return null;
if (root.Model?.Id == element.Id)
return root;
@@ -88,6 +82,5 @@ public static class DockUtilities
if (factory == null) throw new ArgumentNullException(nameof(factory));
// TODO: Реализовать эффективное обновление дерева контролов
// вместо полной перестройки
}
}

View File

@@ -1,301 +0,0 @@
using Lattice.Core.DragDrop.Models;
using Lattice.Core.DragDrop.Services;
using Lattice.Core.Geometry;
using Lattice.UI.DragDrop.Abstractions;
using Lattice.UI.DragDrop.Behaviors;
using Lattice.UI.DragDrop.WinUI.Services;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Input;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Lattice.UI.DragDrop.WinUI.Behaviors;
/// <summary>
/// Реализация поведения источника перетаскивания для элементов WinUI.
/// Наследуется от <see cref="DragSourceBehaviorBase{FrameworkElement}"/> для использования
/// общей логики управления операциями перетаскивания и интеграции с системой <see cref="Core.DragDrop"/>.
/// </summary>
/// <remarks>
/// <para>
/// Этот класс предоставляет конкретную реализацию для платформы WinUI, обрабатывая события
/// указателя (мышь, тач, перо) и преобразуя их в операции перетаскивания через центральный
/// сервис <see cref="IDragDropService"/>.
/// </para>
/// <para>
/// Основные функции:
/// <list type="bullet">
/// <item>Обработка событий PointerPressed, PointerMoved, PointerReleased</item>
/// <item>Автоматическое отслеживание порога начала перетаскивания</item>
/// <item>Создание информации о перетаскивании на основе данных элемента</item>
/// <item>Интеграция с визуальной обратной связью через <see cref="WinUIDragDropHost"/></item>
/// <item>Преобразование координат между локальной системой элемента и экранными координатами</item>
/// </list>
/// </para>
/// <para>
/// Для использования необходимо:
/// <list type="number">
/// <item>Создать экземпляр поведения через фабрику <see cref="Factories.WinUIDragDropFactory.CreateDragSourceBehavior"/></item>
/// <item>Прикрепить к элементу с помощью метода <see cref="Attach"/></item>
/// <item>Указать данные для перетаскивания (опционально, по умолчанию используется DataContext)</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Создание поведения
/// var behavior = WinUIDragDropFactory.CreateDragSourceBehavior(dragDropService, host);
///
/// // Прикрепление к элементу
/// behavior.Attach(myElement, myData);
///
/// // Или через attached properties
/// &lt;Border x:Name="DragElement"
/// local:DragDropProperties.IsDragSource="True"
/// local:DragDropProperties.DragData="{Binding MyData}" /&gt;
/// </code>
/// </example>
/// </remarks>
public sealed class WinUIDragSourceBehavior : DragSourceBehaviorBase<FrameworkElement>
{
#region Поля
private readonly IDragDropHost _host;
private object? _dragData;
#endregion
#region Конструктор
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="WinUIDragSourceBehavior"/>.
/// </summary>
/// <param name="dragDropService">
/// Сервис управления операциями перетаскивания. Используется для координации
/// между источниками и целями перетаскивания.
/// </param>
/// <param name="host">
/// Хост для управления визуальными элементами перетаскивания. Обеспечивает
/// отображение визуальной обратной связи во время операции.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="dragDropService"/> или <paramref name="host"/>
/// равны null.
/// </exception>
/// <remarks>
/// Конструктор инициализирует базовый класс <see cref="DragSourceBehaviorBase{FrameworkElement}"/>
/// и сохраняет ссылки на необходимые сервисы для последующего использования.
/// </remarks>
public WinUIDragSourceBehavior(
Core.DragDrop.Services.IDragDropService dragDropService,
WinUIDragDropHost host)
: base(dragDropService)
{
_host = host ?? throw new ArgumentNullException(nameof(host));
}
#endregion
#region Публичные методы
/// <summary>
/// Прикрепляет поведение к указанному элементу WinUI.
/// </summary>
/// <param name="element">
/// Элемент <see cref="FrameworkElement"/>, который должен стать источником перетаскивания.
/// Не может быть null.
/// </param>
/// <param name="dragData">
/// Данные, которые будут перетаскиваться. Может быть null.
/// Если не указано, используется <see cref="FrameworkElement.DataContext"/> или
/// <see cref="FrameworkElement.Tag"/> элемента.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="element"/> равен null.
/// </exception>
/// <remarks>
/// <para>
/// После вызова этого метода:
/// <list type="bullet">
/// <item>Элементу автоматически подписываются обработчики событий указателя</item>
/// <item>Поведение начинает отслеживать взаимодействия с элементом</item>
/// <item>При превышении порога перетаскивания инициируется операция через <see cref="Core.DragDrop.Services.IDragDropService"/></item>
/// </list>
/// </para>
/// <para>
/// Для открепления поведения используйте метод <see cref="Detach"/>.
/// </para>
/// </remarks>
public void Attach(FrameworkElement element, object? dragData = null)
{
if (element == null)
throw new ArgumentNullException(nameof(element));
_dragData = dragData ?? element.DataContext ?? element.Tag;
AssociatedElement = element;
}
/// <summary>
/// Открепляет поведение от текущего элемента.
/// </summary>
/// <remarks>
/// <para>
/// Этот метод выполняет следующие действия:
/// <list type="bullet">
/// <item>Отписывается от всех событий элемента</item>
/// <item>Сбрасывает внутреннее состояние</item>
/// <item>Освобождает ссылки на связанные объекты</item>
/// </list>
/// </para>
/// <para>
/// Если в момент вызова активна операция перетаскивания, она будет автоматически отменена.
/// </para>
/// <para>
/// После вызова этого метода поведение может быть повторно прикреплено к другому элементу.
/// </para>
/// </remarks>
public new void Detach()
{
base.Detach();
_dragData = null;
}
#endregion
#region Реализация абстрактных методов DragSourceBehaviorBase<FrameworkElement>
/// <inheritdoc/>
protected override void SubscribeToEvents(FrameworkElement element)
{
if (element == null) return;
element.PointerPressed += OnPointerPressed;
element.PointerMoved += OnPointerMoved;
element.PointerReleased += OnPointerReleased;
element.PointerCanceled += OnPointerCanceled;
element.PointerCaptureLost += OnPointerCaptureLost;
}
/// <inheritdoc/>
protected override void UnsubscribeFromEvents(FrameworkElement element)
{
if (element == null) return;
element.PointerPressed -= OnPointerPressed;
element.PointerMoved -= OnPointerMoved;
element.PointerReleased -= OnPointerReleased;
element.PointerCanceled -= OnPointerCanceled;
element.PointerCaptureLost -= OnPointerCaptureLost;
}
/// <inheritdoc/>
protected override Point ConvertToScreenCoordinates(Point point)
{
if (AssociatedElement == null)
return point;
return WinUIWindowHelper.ConvertToScreenCoordinates(AssociatedElement, point);
}
#endregion
#region Реализация интерфейса IDragSource
/// <inheritdoc/>
public override async Task<DragInfo?> TryStartDragAsync(
Point startPosition,
CancellationToken cancellationToken = default)
{
if (AssociatedElement == null || _dragData == null)
return null;
try
{
// Создаем информацию о перетаскивании
var dragInfo = new DragInfo(
data: _dragData,
allowedEffects: Core.DragDrop.Enums.DragDropEffects.Copy |
Core.DragDrop.Enums.DragDropEffects.Move |
Core.DragDrop.Enums.DragDropEffects.Link,
startPosition: startPosition,
source: this
);
// Добавляем дополнительные параметры
dragInfo.SetParameter("SourceElement", AssociatedElement);
dragInfo.SetParameter("SourceType", AssociatedElement.GetType().Name);
return await Task.FromResult(dragInfo);
}
catch (Exception ex)
{
// Логирование ошибки создания информации о перетаскивании
System.Diagnostics.Debug.WriteLine(
$"Ошибка создания DragInfo: {ex.Message}");
return null;
}
}
#endregion
#region Обработчики событий WinUI
private async void OnPointerPressed(object sender, PointerRoutedEventArgs e)
{
if (AssociatedElement == null) return;
var point = e.GetCurrentPoint(AssociatedElement);
var position = new Point(point.Position.X, point.Position.Y);
await OnInteractionStarted(position);
}
private async void OnPointerMoved(object sender, PointerRoutedEventArgs e)
{
if (AssociatedElement == null) return;
var point = e.GetCurrentPoint(AssociatedElement);
var position = new Point(point.Position.X, point.Position.Y);
await OnInteractionMoved(position);
}
private async void OnPointerReleased(object sender, PointerRoutedEventArgs e)
{
await OnInteractionEnded();
}
private async void OnPointerCanceled(object sender, PointerRoutedEventArgs e)
{
await OnInteractionCancelled();
}
private async void OnPointerCaptureLost(object sender, PointerRoutedEventArgs e)
{
await OnInteractionCancelled();
}
#endregion
#region Переопределение виртуальных методов
/// <inheritdoc/>
protected override void OnDragCompleted(DragInfo dragInfo, Core.DragDrop.Enums.DragDropEffects effects)
{
base.OnDragCompleted(dragInfo, effects);
// Дополнительная логика для WinUI может быть добавлена здесь
// Например, обновление состояния элемента после успешного перетаскивания
}
/// <inheritdoc/>
protected override void OnDragCancelled(DragInfo dragInfo)
{
base.OnDragCancelled(dragInfo);
// Дополнительная логика для WinUI может быть добавлена здесь
// Например, восстановление визуального состояния элемента после отмены
}
#endregion
}

View File

@@ -1,514 +0,0 @@
using Lattice.Core.DragDrop.Models;
using Lattice.Core.Geometry;
using Lattice.UI.DragDrop.Abstractions;
using Lattice.UI.DragDrop.Behaviors;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Lattice.UI.DragDrop.WinUI.Behaviors;
/// <summary>
/// Реализация поведения цели сброса для элементов WinUI.
/// Наследуется от <see cref="DropTargetBehaviorBase{FrameworkElement}"/> для использования
/// общей логики регистрации целей и обработки операций сброса.
/// </summary>
/// <remarks>
/// <para>
/// Этот класс предоставляет конкретную реализацию для платформы WinUI, обрабатывая события
/// перетаскивания WinUI и преобразуя их в вызовы методов интерфейса <see cref="Core.DragDrop.Abstractions.IDropTarget"/>.
/// </para>
/// <para>
/// Основные функции:
/// <list type="bullet">
/// <item>Автоматическая регистрация в <see cref="IDragDropService"/> при прикреплении к элементу</item>
/// <item>Обработка событий DragEnter, DragOver, DragLeave, Drop</item>
/// <item>Автоматическое обновление границ элемента при изменении размера или позиции</item>
/// <item>Поддержка фильтрации принимаемых типов данных</item>
/// <item>Интеграция с визуальной обратной связью</item>
/// </list>
/// </para>
/// <para>
/// Для использования необходимо:
/// <list type="number">
/// <item>Создать экземпляр поведения через фабрику <see cref="Factories.WinUIDragDropFactory.CreateDropTargetBehavior"/></item>
/// <item>Прикрепить к элементу с помощью метода <see cref="Attach"/></item>
/// <item>Настроить фильтры принимаемых данных (опционально)</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Создание поведения с фильтрацией типов
/// var behavior = WinUIDragDropFactory.CreateDropTargetBehavior(dragDropService, host);
/// behavior.AcceptTypes(typeof(MyDataModel), typeof(string));
/// behavior.Attach(myDropArea);
///
/// // Или через attached properties
/// &lt;Border x:Name="DropArea"
/// local:DragDropProperties.IsDropTarget="True" /&gt;
/// </code>
/// </example>
/// </remarks>
public sealed class WinUIDropTargetBehavior : DropTargetBehaviorBase<FrameworkElement>
{
#region Поля
private readonly IDragDropHost _host;
private readonly List<Type> _acceptedTypes = new();
private readonly HashSet<string> _acceptedFormats = new();
#endregion
#region Конструктор
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="WinUIDropTargetBehavior"/>.
/// </summary>
/// <param name="dragDropService">
/// Сервис управления операциями перетаскивания. Используется для регистрации
/// цели и координации операций сброса.
/// </param>
/// <param name="host">
/// Хост для управления визуальной обратной связью. Обеспечивает отображение
/// индикаторов возможности сброса.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="dragDropService"/> или <paramref name="host"/>
/// равны null.
/// </exception>
/// <remarks>
/// Конструктор инициализирует базовый класс и сохраняет ссылки на сервисы.
/// По умолчанию цель принимает все типы данных. Для настройки фильтрации
/// используйте методы <see cref="AcceptTypes"/> и <see cref="AcceptFormats"/>.
/// </remarks>
public WinUIDropTargetBehavior(
Core.DragDrop.Services.IDragDropService dragDropService,
IDragDropHost host)
: base(dragDropService)
{
_host = host ?? throw new ArgumentNullException(nameof(host));
}
#endregion
#region Публичные методы
/// <summary>
/// Прикрепляет поведение к указанному элементу WinUI.
/// </summary>
/// <param name="element">
/// Элемент <see cref="FrameworkElement"/>, который должен стать целью сброса.
/// Не может быть null.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="element"/> равен null.
/// </exception>
/// <remarks>
/// <para>
/// После вызова этого метода:
/// <list type="bullet">
/// <item>Элементу устанавливается свойство <see cref="UIElement.AllowDrop"/> = true</item>
/// <item>Поведение подписывается на события перетаскивания WinUI</item>
/// <item>Элемент регистрируется в системе перетаскивания с текущими границами</item>
/// <item>Начинается отслеживание изменений размера и позиции элемента</item>
/// </list>
/// </para>
/// <para>
/// Для открепления поведения используйте метод <see cref="Detach"/>.
/// </para>
/// </remarks>
public void Attach(FrameworkElement element)
{
if (element == null)
throw new ArgumentNullException(nameof(element));
element.AllowDrop = true;
AssociatedElement = element;
}
/// <summary>
/// Настраивает поведение для приема только указанных типов данных.
/// </summary>
/// <param name="types">
/// Типы данных, которые может принимать цель. Если пусто, принимаются все типы.
/// </param>
/// <remarks>
/// <para>
/// Этот метод позволяет ограничить типы данных, которые могут быть сброшены на цель.
/// Проверка выполняется в методе <see cref="CanAcceptDropAsync"/> путем сравнения
/// типа сбрасываемых данных с указанными типами.
/// </para>
/// <para>
/// Если метод не вызывался или передан пустой список, цель будет принимать данные любого типа.
/// </para>
/// <example>
/// <code>
/// // Принимать только строки и объекты MyModel
/// behavior.AcceptTypes(typeof(string), typeof(MyModel));
/// </code>
/// </example>
/// </remarks>
public void AcceptTypes(params Type[] types)
{
_acceptedTypes.Clear();
if (types != null && types.Length > 0)
{
_acceptedTypes.AddRange(types);
}
}
/// <summary>
/// Настраивает поведение для приема только указанных форматов данных.
/// </summary>
/// <param name="formats">
/// Форматы данных (например, "Text", "Bitmap", "FileDrop"), которые может принимать цель.
/// Если пусто, формат не проверяется.
/// </param>
/// <remarks>
/// <para>
/// Этот метод позволяет ограничить форматы данных, которые могут быть сброшены на цель.
/// Актуально для межпроцессного перетаскивания или работы с системными форматами.
/// </para>
/// <para>
/// Если метод не вызывался или передан пустой список, проверка формата не выполняется.
/// </para>
/// </remarks>
public void AcceptFormats(params string[] formats)
{
_acceptedFormats.Clear();
if (formats != null && formats.Length > 0)
{
foreach (var format in formats)
{
_acceptedFormats.Add(format);
}
}
}
/// <summary>
/// Открепляет поведение от текущего элемента.
/// </summary>
/// <remarks>
/// <para>
/// Этот метод выполняет следующие действия:
/// <list type="bullet">
/// <item>Отписывается от всех событий элемента</item>
/// <item>Отменяет регистрацию цели в системе перетаскивания</item>
/// <item>Сбрасывает свойство <see cref="UIElement.AllowDrop"/> = false</item>
/// <item>Освобождает ссылки на связанные объекты</item>
/// </list>
/// </para>
/// <para>
/// После вызова этого метода поведение может быть повторно прикреплено к другому элементу.
/// </para>
/// </remarks>
public new void Detach()
{
if (AssociatedElement != null)
{
AssociatedElement.AllowDrop = false;
}
base.Detach();
}
#endregion
#region Реализация абстрактных методов DropTargetBehaviorBase<FrameworkElement>
/// <inheritdoc/>
protected override void SubscribeToEvents(FrameworkElement element)
{
if (element == null) return;
element.DragEnter += OnDragEnter;
element.DragOver += OnDragOver;
element.DragLeave += OnDragLeave;
element.Drop += OnDrop;
element.SizeChanged += OnSizeChanged;
element.LayoutUpdated += OnLayoutUpdated;
}
/// <inheritdoc/>
protected override void UnsubscribeFromEvents(FrameworkElement element)
{
if (element == null) return;
element.DragEnter -= OnDragEnter;
element.DragOver -= OnDragOver;
element.DragLeave -= OnDragLeave;
element.Drop -= OnDrop;
element.SizeChanged -= OnSizeChanged;
element.LayoutUpdated -= OnLayoutUpdated;
}
/// <inheritdoc/>
protected override Rect GetScreenBounds(FrameworkElement element)
{
if (element == null || !element.IsLoaded)
return Rect.Empty;
try
{
var window = Window.Current;
if (window?.Content == null)
return Rect.Empty;
// Преобразуем локальные координаты элемента в координаты окна
var transform = element.TransformToVisual(window.Content);
var position = transform.TransformPoint(new Windows.Foundation.Point(0, 0));
return new Rect(
position.X,
position.Y,
element.ActualWidth,
element.ActualHeight
);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(
$"Ошибка получения границ элемента: {ex.Message}");
return Rect.Empty;
}
}
#endregion
#region Реализация интерфейса IDropTarget
/// <inheritdoc/>
public override async Task<bool> CanAcceptDropAsync(
DropInfo dropInfo,
CancellationToken cancellationToken = default)
{
// Проверяем, есть ли данные для сброса
if (dropInfo.Data == null)
return false;
// Проверяем фильтр по типам
if (_acceptedTypes.Count > 0)
{
var dataType = dropInfo.Data.GetType();
if (!_acceptedTypes.Any(t => t.IsAssignableFrom(dataType)))
{
return false;
}
}
// Проверяем фильтр по форматам (если данные предоставляют информацию о формате)
if (_acceptedFormats.Count > 0 && dropInfo.Data is Windows.ApplicationModel.DataTransfer.DataPackageView dataView)
{
var availableFormats = dataView.AvailableFormats;
if (!_acceptedFormats.Any(f => availableFormats.Contains(f)))
{
return false;
}
}
// Дополнительная проверка может быть добавлена в производных классах
return await Task.FromResult(true);
}
/// <inheritdoc/>
public override async Task OnDragOverAsync(
DropInfo dropInfo,
CancellationToken cancellationToken = default)
{
await base.OnDragOverAsync(dropInfo, cancellationToken);
// Дополнительная логика для WinUI может быть добавлена здесь
// Например, обновление визуальной обратной связи через хост
}
/// <inheritdoc/>
public override async Task OnDropAsync(
DropInfo dropInfo,
CancellationToken cancellationToken = default)
{
// Базовая реализация вызывает CanAcceptDropAsync и помечает как обработанное
if (await CanAcceptDropAsync(dropInfo, cancellationToken))
{
dropInfo.MarkAsHandled();
// Здесь может быть добавлена логика обработки сброшенных данных
// Например, вызов события или обновление модели данных
}
}
/// <inheritdoc/>
public override async Task OnDragLeaveAsync(CancellationToken cancellationToken = default)
{
await base.OnDragLeaveAsync(cancellationToken);
// Дополнительная логика для WinUI может быть добавлена здесь
// Например, скрытие визуальной обратной связи
}
#endregion
#region Обработчики событий WinUI
private async void OnDragEnter(object sender, DragEventArgs e)
{
if (AssociatedElement == null) return;
try
{
var position = e.GetPosition(AssociatedElement);
var dropInfo = CreateDropInfo(e, new Point(position.X, position.Y));
if (await CanAcceptDropAsync(dropInfo))
{
e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Copy;
e.Handled = true;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Ошибка в OnDragEnter: {ex.Message}");
}
}
private async void OnDragOver(object sender, DragEventArgs e)
{
if (AssociatedElement == null) return;
try
{
var position = e.GetPosition(AssociatedElement);
var dropInfo = CreateDropInfo(e, new Point(position.X, position.Y));
if (await CanAcceptDropAsync(dropInfo))
{
e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Copy;
await OnDragOverAsync(dropInfo);
e.Handled = true;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Ошибка в OnDragOver: {ex.Message}");
}
}
private async void OnDragLeave(object sender, DragEventArgs e)
{
await OnDragLeaveAsync();
}
private async void OnDrop(object sender, DragEventArgs e)
{
if (AssociatedElement == null) return;
try
{
var position = e.GetPosition(AssociatedElement);
var dropInfo = CreateDropInfo(e, new Point(position.X, position.Y));
if (await CanAcceptDropAsync(dropInfo))
{
await OnDropAsync(dropInfo);
e.Handled = true;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Ошибка в OnDrop: {ex.Message}");
}
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
OnElementLayoutChanged();
}
private void OnLayoutUpdated(object sender, object e)
{
OnElementLayoutChanged();
}
#endregion
#region Вспомогательные методы
/// <summary>
/// Создает объект <see cref="DropInfo"/> на основе события перетаскивания WinUI.
/// </summary>
/// <param name="e">Аргументы события перетаскивания WinUI.</param>
/// <param name="position">Локальная позиция курсора относительно элемента.</param>
/// <returns>
/// Экземпляр <see cref="DropInfo"/>, содержащий информацию о потенциальном сбросе.
/// </returns>
/// <remarks>
/// <para>
/// Этот метод извлекает данные из события перетаскивания и преобразует их
/// в формат, понятный системе <see cref="Core.DragDrop"/>.
/// </para>
/// <para>
/// Поддерживаются как пользовательские данные (через свойство "DragData"),
/// так и стандартные форматы данных WinUI.
/// </para>
/// </remarks>
private DropInfo CreateDropInfo(DragEventArgs e, Point position)
{
object? data = null;
// Пытаемся получить пользовательские данные
if (e.DataView.Properties.TryGetValue("DragData", out var dragData))
{
data = dragData;
}
// Или получаем данные из DataPackage
else if (e.DataView.Contains(Windows.ApplicationModel.DataTransfer.StandardDataFormats.Text))
{
// Для текстовых данных можем установить асинхронную загрузку
data = new AsyncDataProvider(async () =>
{
return await e.DataView.GetTextAsync();
});
}
// Определяем разрешенные эффекты на основе модификаторов клавиатуры
var allowedEffects = Core.DragDrop.Enums.DragDropEffects.None;
if (e.AllowedOperations.HasFlag(Windows.ApplicationModel.DataTransfer.DataPackageOperation.Copy))
allowedEffects |= Core.DragDrop.Enums.DragDropEffects.Copy;
if (e.AllowedOperations.HasFlag(Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move))
allowedEffects |= Core.DragDrop.Enums.DragDropEffects.Move;
if (e.AllowedOperations.HasFlag(Windows.ApplicationModel.DataTransfer.DataPackageOperation.Link))
allowedEffects |= Core.DragDrop.Enums.DragDropEffects.Link;
return new DropInfo(
data: data,
position: position,
allowedEffects: allowedEffects,
target: this
);
}
#endregion
}
/// <summary>
/// Предоставляет асинхронный доступ к данным перетаскивания.
/// </summary>
/// <remarks>
/// Этот класс используется для отложенной загрузки данных перетаскивания,
/// что особенно важно для больших данных или данных, требующих обработки.
/// </remarks>
internal class AsyncDataProvider
{
private readonly Func<Task<object>> _dataLoader;
public AsyncDataProvider(Func<Task<object>> dataLoader)
{
_dataLoader = dataLoader;
}
public async Task<object> GetDataAsync()
{
return await _dataLoader();
}
}

View File

@@ -1,235 +0,0 @@
using Lattice.Core.Geometry;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using System;
namespace Lattice.UI.DragDrop.WinUI.Controls;
/// <summary>
/// Визуальный элемент, отображаемый во время перетаскивания.
/// </summary>
/// <remarks>
/// <para>
/// Этот элемент отображает репрезентативное представление перетаскиваемых данных
/// и следует за курсором мыши во время операции перетаскивания.
/// </para>
/// <para>
/// Элемент поддерживает настройку прозрачности, смещения и угла поворота,
/// а также анимированное появление и скрытие.
/// </para>
/// </remarks>
public class DragAdorner : Control
{
/// <summary>
/// Идентификатор свойства для данных перетаскивания.
/// </summary>
public static readonly DependencyProperty DragDataProperty =
DependencyProperty.Register(
"DragData",
typeof(object),
typeof(DragAdorner),
new PropertyMetadata(null, OnDragDataChanged));
/// <summary>
/// Идентификатор свойства для смещения относительно курсора.
/// </summary>
public static readonly DependencyProperty OffsetProperty =
DependencyProperty.Register(
"Offset",
typeof(Point),
typeof(DragAdorner),
new PropertyMetadata(new Point(0, 0)));
/// <summary>
/// Идентификатор свойства для угла поворота.
/// </summary>
public static readonly DependencyProperty RotationAngleProperty =
DependencyProperty.Register(
"RotationAngle",
typeof(double),
typeof(DragAdorner),
new PropertyMetadata(0.0));
/// <summary>
/// Идентификатор свойства для прозрачности.
/// </summary>
public static readonly DependencyProperty OpacityLevelProperty =
DependencyProperty.Register(
"OpacityLevel",
typeof(double),
typeof(DragAdorner),
new PropertyMetadata(0.7));
private ContentPresenter? _contentPresenter;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragAdorner"/>.
/// </summary>
public DragAdorner()
{
DefaultStyleKey = typeof(DragAdorner);
// Устанавливаем свойства для корректного отображения поверх других элементов
IsHitTestVisible = false;
UseSystemFocusVisuals = false;
}
/// <summary>
/// Получает или задает данные, которые отображаются в визуальном элементе.
/// </summary>
/// <value>
/// Объект данных для отображения. Обычно это те же данные, которые перетаскиваются.
/// </value>
public object DragData
{
get => GetValue(DragDataProperty);
set => SetValue(DragDataProperty, value);
}
/// <summary>
/// Получает или задает смещение элемента относительно позиции курсора.
/// </summary>
/// <value>
/// Смещение по осям X и Y. Используется для позиционирования элемента так,
/// чтобы он не перекрывал курсор. Значение по умолчанию вычисляется автоматически
/// на основе размера элемента.
/// </value>
public Point Offset
{
get => (Point)GetValue(OffsetProperty);
set => SetValue(OffsetProperty, value);
}
/// <summary>
/// Получает или задает угол поворота.
/// </summary>
public double RotationAngle
{
get => (double)GetValue(RotationAngleProperty);
set => SetValue(RotationAngleProperty, value);
}
/// <summary>
/// Получает или задает уровень прозрачности.
/// </summary>
public double OpacityLevel
{
get => (double)GetValue(OpacityLevelProperty);
set => SetValue(OpacityLevelProperty, value);
}
/// <inheritdoc/>
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_contentPresenter = GetTemplateChild("PART_ContentPresenter") as ContentPresenter;
UpdateContent();
}
/// <summary>
/// Обновляет позицию элемента в соответствии с позицией курсора.
/// </summary>
/// <param name="cursorPosition">Текущая позиция курсора в экранных координатах.</param>
/// <remarks>
/// Метод применяет трансформации для позиционирования элемента с учетом
/// заданного смещения и угла поворота.
/// </remarks>
public void UpdatePosition(Point cursorPosition)
{
var transform = new TranslateTransform
{
X = cursorPosition.X + Offset.X,
Y = cursorPosition.Y + Offset.Y
};
RenderTransform = new TransformGroup
{
Children =
{
transform,
new RotateTransform { Angle = RotationAngle }
}
};
}
/// <summary>
/// Показывает элемент с анимацией.
/// </summary>
public void Show()
{
Visibility = Visibility.Visible;
// Анимация появления
var animation = new Microsoft.UI.Xaml.Media.Animation.DoubleAnimation
{
From = 0,
To = OpacityLevel,
Duration = TimeSpan.FromMilliseconds(150),
EasingFunction = new Microsoft.UI.Xaml.Media.Animation.CubicEase
{
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut
}
};
var storyboard = new Microsoft.UI.Xaml.Media.Animation.Storyboard();
storyboard.Children.Add(animation);
Microsoft.UI.Xaml.Media.Animation.Storyboard.SetTarget(animation, this);
Microsoft.UI.Xaml.Media.Animation.Storyboard.SetTargetProperty(animation, "Opacity");
storyboard.Begin();
}
/// <summary>
/// Скрывает элемент с анимацией.
/// </summary>
public void Hide()
{
var animation = new Microsoft.UI.Xaml.Media.Animation.DoubleAnimation
{
From = Opacity,
To = 0,
Duration = TimeSpan.FromMilliseconds(100),
EasingFunction = new Microsoft.UI.Xaml.Media.Animation.CubicEase
{
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn
}
};
animation.Completed += (s, e) =>
{
Visibility = Visibility.Collapsed;
};
var storyboard = new Microsoft.UI.Xaml.Media.Animation.Storyboard();
storyboard.Children.Add(animation);
Microsoft.UI.Xaml.Media.Animation.Storyboard.SetTarget(animation, this);
Microsoft.UI.Xaml.Media.Animation.Storyboard.SetTargetProperty(animation, "Opacity");
storyboard.Begin();
}
private static void OnDragDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DragAdorner adorner)
{
adorner.UpdateContent();
}
}
private void UpdateContent()
{
if (_contentPresenter != null)
{
// Можно добавить DataTemplateSelector для разных типов данных
_contentPresenter.Content = DragData;
// Автоматически вычисляем смещение для приятного вида
if (DragData is FrameworkElement element)
{
Offset = new Point(-element.ActualWidth / 2, -element.ActualHeight / 2);
}
}
}
}

View File

@@ -1,167 +0,0 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Lattice.UI.DragDrop.WinUI.Controls;
/// <summary>
/// Оверлейный слой для отображения всех визуальных элементов перетаскивания.
/// </summary>
/// <remarks>
/// <para>
/// Этот элемент добавляется поверх всего содержимого окна и содержит:
/// - Drag-визуализации (элементы, следующие за курсором)
/// - Drop-превью (подсветка областей сброса)
/// </para>
/// <para>
/// Элемент имеет <see cref="Canvas.IsHitTestVisible"/> = false, чтобы не перехватывать
/// пользовательский ввод во время операций перетаскивания.
/// </para>
/// </remarks>
public class DragDropOverlay : Canvas
{
private readonly List<UIElement> _dragVisuals = new();
private readonly List<DropPreviewAdorner> _dropAdorners = new();
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragDropOverlay"/>.
/// </summary>
public DragDropOverlay()
{
IsHitTestVisible = false;
Background = null;
// Устанавливаем высокий Z-Index, чтобы быть поверх всего
Canvas.SetZIndex(this, 10000);
}
/// <summary>
/// Показывает визуальное представление перетаскивания.
/// </summary>
/// <param name="dragVisual">Визуальное представление.</param>
/// <param name="initialX">Начальная позиция X.</param>
/// <param name="initialY">Начальная позиция Y.</param>
public void ShowDragVisual(UIElement dragVisual, double initialX, double initialY)
{
if (!Children.Contains(dragVisual))
{
Children.Add(dragVisual);
_dragVisuals.Add(dragVisual);
}
SetLeft(dragVisual, initialX);
SetTop(dragVisual, initialY);
dragVisual.Visibility = Visibility.Visible;
}
/// <summary>
/// Обновляет позицию визуального представления перетаскивания.
/// </summary>
/// <param name="dragVisual">Визуальное представление.</param>
/// <param name="x">Новая позиция X.</param>
/// <param name="y">Новая позиция Y.</param>
public void UpdateDragVisualPosition(UIElement dragVisual, double x, double y)
{
if (Children.Contains(dragVisual))
{
SetLeft(dragVisual, x);
SetTop(dragVisual, y);
}
}
/// <summary>
/// Скрывает визуальное представление перетаскивания.
/// </summary>
/// <param name="dragVisual">Визуальное представление.</param>
public void HideDragVisual(UIElement dragVisual)
{
dragVisual.Visibility = Visibility.Collapsed;
if (Children.Contains(dragVisual))
{
Children.Remove(dragVisual);
_dragVisuals.Remove(dragVisual);
}
}
/// <summary>
/// Показывает предварительный просмотр области сброса.
/// </summary>
/// <param name="bounds">Границы области.</param>
/// <returns>Созданный элемент предварительного просмотра.</returns>
public DropPreviewAdorner ShowDropPreview(Core.Geometry.Rect bounds)
{
var adorner = new DropPreviewAdorner
{
PreviewColor = Windows.UI.Color.FromArgb(100, 0, 120, 215),
PreviewThickness = 2.0
};
Children.Add(adorner);
_dropAdorners.Add(adorner);
adorner.Show(bounds);
return adorner;
}
/// <summary>
/// Обновляет предварительный просмотр области сброса.
/// </summary>
/// <param name="adorner">Элемент предварительного просмотра.</param>
/// <param name="bounds">Новые границы.</param>
public void UpdateDropPreview(DropPreviewAdorner adorner, Core.Geometry.Rect bounds)
{
adorner.UpdatePosition(bounds);
}
/// <summary>
/// Скрывает все предварительные просмотры областей сброса.
/// </summary>
public void HideAllDropPreviews()
{
foreach (var adorner in _dropAdorners.ToList())
{
adorner.Hide();
var timer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(200)
};
timer.Tick += (s, e) =>
{
timer.Stop();
if (Children.Contains(adorner))
{
Children.Remove(adorner);
}
_dropAdorners.Remove(adorner);
};
timer.Start();
}
}
/// <summary>
/// Скрывает все визуальные элементы.
/// </summary>
public void ClearAllVisuals()
{
foreach (var visual in _dragVisuals.ToList())
{
HideDragVisual(visual);
}
HideAllDropPreviews();
}
/// <summary>
/// Получает текущий элемент перетаскивания.
/// </summary>
/// <returns>Элемент перетаскивания или null.</returns>
public UIElement? GetCurrentDragVisual()
{
return _dragVisuals.FirstOrDefault();
}
}

View File

@@ -1,151 +0,0 @@
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using System;
using Windows.UI;
namespace Lattice.UI.DragDrop.WinUI.Controls;
/// <summary>
/// Визуальный элемент для подсветки области сброса.
/// </summary>
/// <remarks>
/// Этот элемент отображается вокруг целевого элемента при наведении перетаскиваемого
/// объекта для визуального указания возможности сброса.
/// </remarks>
[TemplateVisualState(Name = "Normal", GroupName = "CommonStates")]
[TemplateVisualState(Name = "Highlighted", GroupName = "CommonStates")]
public class DropPreviewAdorner : Control
{
/// <summary>
/// Идентификатор свойства для цвета предварительного просмотра.
/// </summary>
public static readonly DependencyProperty PreviewColorProperty =
DependencyProperty.Register(
"PreviewColor",
typeof(Color),
typeof(DropPreviewAdorner),
new PropertyMetadata(Colors.DodgerBlue));
/// <summary>
/// Идентификатор свойства для толщины границы.
/// </summary>
public static readonly DependencyProperty PreviewThicknessProperty =
DependencyProperty.Register(
"PreviewThickness",
typeof(double),
typeof(DropPreviewAdorner),
new PropertyMetadata(2.0));
/// <summary>
/// Идентификатор свойства для кисти границы.
/// </summary>
public static readonly DependencyProperty PreviewBrushProperty =
DependencyProperty.Register(
"PreviewBrush",
typeof(Brush),
typeof(DropPreviewAdorner),
new PropertyMetadata(null));
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DropPreviewAdorner"/>.
/// </summary>
public DropPreviewAdorner()
{
DefaultStyleKey = typeof(DropPreviewAdorner);
IsHitTestVisible = false;
}
/// <summary>
/// Получает или задает цвет подсветки области сброса.
/// </summary>
/// <value>
/// Цвет границы и фона подсветки. Значение по умолчанию берется из ресурсов темы.
/// </value>
public Color PreviewColor
{
get => (Color)GetValue(PreviewColorProperty);
set => SetValue(PreviewColorProperty, value);
}
/// <summary>
/// Получает или задает толщину границы.
/// </summary>
public double PreviewThickness
{
get => (double)GetValue(PreviewThicknessProperty);
set => SetValue(PreviewThicknessProperty, value);
}
/// <summary>
/// Получает или задает кисть границы.
/// </summary>
public Brush PreviewBrush
{
get => (Brush)GetValue(PreviewBrushProperty);
set => SetValue(PreviewBrushProperty, value);
}
/// <summary>
/// Показывает элемент подсветки для указанной области.
/// </summary>
/// <param name="bounds">Границы области для подсветки.</param>
/// <remarks>
/// Метод позиционирует элемент по указанным границам и запускает анимацию появления.
/// </remarks>
public void Show(Core.Geometry.Rect bounds)
{
Width = bounds.Width;
Height = bounds.Height;
var translateTransform = new TranslateTransform
{
X = bounds.X,
Y = bounds.Y
};
RenderTransform = translateTransform;
Visibility = Visibility.Visible;
VisualStateManager.GoToState(this, "Highlighted", true);
}
/// <summary>
/// Скрывает элемент.
/// </summary>
public void Hide()
{
VisualStateManager.GoToState(this, "Normal", true);
// Отложенное скрытие для плавной анимации
var timer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(150)
};
timer.Tick += (s, e) =>
{
timer.Stop();
Visibility = Visibility.Collapsed;
};
timer.Start();
}
/// <summary>
/// Обновляет позицию и размер элемента подсветки.
/// </summary>
/// <param name="bounds">Новые границы области для подсветки.</param>
public void UpdatePosition(Core.Geometry.Rect bounds)
{
if (RenderTransform is TranslateTransform transform)
{
transform.X = bounds.X;
transform.Y = bounds.Y;
}
Width = bounds.Width;
Height = bounds.Height;
}
}

View File

@@ -1,168 +0,0 @@
using Microsoft.UI.Xaml;
namespace Lattice.UI.DragDrop.WinUI;
/// <summary>
/// Предоставляет attached properties для настройки drag-and-drop поведения элементов WinUI.
/// </summary>
/// <remarks>
/// Этот класс содержит attached properties, которые позволяют включать и настраивать
/// возможности перетаскивания и сброса для любых FrameworkElement в приложении WinUI.
/// </remarks>
public static class DragDropProperties
{
#region Drag Source Properties
/// <summary>
/// Прикрепленное свойство для включения перетаскивания.
/// </summary>
public static readonly DependencyProperty IsDragSourceProperty =
DependencyProperty.RegisterAttached(
"IsDragSource",
typeof(bool),
typeof(DragDropProperties),
new PropertyMetadata(false, OnIsDragSourceChanged));
/// <summary>
/// Прикрепленное свойство для данных перетаскивания.
/// </summary>
public static readonly DependencyProperty DragDataProperty =
DependencyProperty.RegisterAttached(
"DragData",
typeof(object),
typeof(DragDropProperties),
new PropertyMetadata(null));
/// <summary>
/// Получает значение IsDragSource.
/// </summary>
public static bool GetIsDragSource(UIElement element) =>
(bool)element.GetValue(IsDragSourceProperty);
/// <summary>
/// Устанавливает значение IsDragSource.
/// </summary>
public static void SetIsDragSource(UIElement element, bool value) =>
element.SetValue(IsDragSourceProperty, value);
/// <summary>
/// Получает значение DragData.
/// </summary>
public static object? GetDragData(UIElement element) =>
element.GetValue(DragDataProperty);
/// <summary>
/// Устанавливает значение DragData.
/// </summary>
public static void SetDragData(UIElement element, object? value) =>
element.SetValue(DragDataProperty, value);
private static void OnIsDragSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is FrameworkElement element)
{
if ((bool)e.NewValue)
{
var data = GetDragData(element);
Services.WinUIDragDropManager.Instance.MakeDragSource(element, data);
}
else
{
Services.WinUIDragDropManager.Instance.RemoveDragSource(element);
}
}
}
#endregion
#region Drop Target Properties
/// <summary>
/// Прикрепленное свойство для включения цели сброса.
/// </summary>
public static readonly DependencyProperty IsDropTargetProperty =
DependencyProperty.RegisterAttached(
"IsDropTarget",
typeof(bool),
typeof(DragDropProperties),
new PropertyMetadata(false, OnIsDropTargetChanged));
/// <summary>
/// Получает значение IsDropTarget.
/// </summary>
public static bool GetIsDropTarget(UIElement element) =>
(bool)element.GetValue(IsDropTargetProperty);
/// <summary>
/// Устанавливает значение IsDropTarget.
/// </summary>
public static void SetIsDropTarget(UIElement element, bool value) =>
element.SetValue(IsDropTargetProperty, value);
private static void OnIsDropTargetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is FrameworkElement element)
{
if ((bool)e.NewValue)
{
Services.WinUIDragDropManager.Instance.MakeDropTarget(element);
}
else
{
Services.WinUIDragDropManager.Instance.RemoveDropTarget(element);
}
}
}
#endregion
#region Helper Methods
/// <summary>
/// Включает перетаскивание для элемента с указанными данными.
/// </summary>
/// <param name="element">Элемент, для которого включается перетаскивание.</param>
/// <param name="dragData">Данные для перетаскивания. Если не указано, используются DataContext или Tag элемента.</param>
/// <example>
/// <code>
/// myElement.EnableDrag(item);
/// </code>
/// </example>
public static void EnableDrag(this FrameworkElement element, object? dragData = null)
{
if (dragData != null)
{
SetDragData(element, dragData);
}
SetIsDragSource(element, true);
}
/// <summary>
/// Отключает перетаскивание для элемента.
/// </summary>
/// <param name="element">Элемент, для которого отключается перетаскивание.</param>
public static void DisableDrag(this FrameworkElement element)
{
SetIsDragSource(element, false);
}
/// <summary>
/// Включает возможность сброса для элемента.
/// </summary>
/// <param name="element">Элемент, для которого включается возможность сброса.</param>
public static void EnableDrop(this FrameworkElement element)
{
SetIsDropTarget(element, true);
}
/// <summary>
/// Отключает возможность сброса для элемента.
/// </summary>
/// <param name="element">Элемент, для которого отключается возможность сброса.</param>
public static void DisableDrop(this FrameworkElement element)
{
SetIsDropTarget(element, false);
}
#endregion
}

View File

@@ -1,950 +0,0 @@
using Lattice.Core.DragDrop.Services;
using Lattice.UI.DragDrop.WinUI.Behaviors;
using Lattice.UI.DragDrop.WinUI.Controls;
using Lattice.UI.DragDrop.WinUI.Services;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
namespace Lattice.UI.DragDrop.WinUI.Factories;
/// <summary>
/// Фабрика для создания и настройки компонентов системы перетаскивания WinUI.
/// Предоставляет удобные методы для быстрой интеграции drag-and-drop функциональности
/// в приложениях WinUI с поддержкой различных сценариев использования.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="WinUIDragDropFactory"/> служит высокоуровневым API для работы с системой
/// перетаскивания, инкапсулируя сложность инициализации и настройки компонентов.
/// </para>
/// <para>
/// Основные возможности фабрики:
/// <list type="bullet">
/// <item>Создание и инициализация менеджера перетаскивания</item>
/// <item>Генерация поведений для источников и целей перетаскивания</item>
/// <item>Создание визуальных элементов для обратной связи</item>
/// <item>Предварительные конфигурации для типовых сценариев</item>
/// <item>Вспомогательные методы для работы с XAML</item>
/// </list>
/// </para>
/// <para>
/// Фабрика поддерживает два подхода к использованию:
/// <list type="number">
/// <item><strong>Императивный подход</strong> - создание компонентов в коде C#</item>
/// <item><strong>Декларативный подход</strong> - использование attached properties в XAML</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Императивный подход
/// var manager = WinUIDragDropFactory.CreateManager(window);
/// manager.MakeDragSource(element, data);
/// manager.MakeDropTarget(dropArea);
///
/// // Декларативный подход (в XAML)
/// &lt;Border local:DragDropProperties.IsDragSource="True"
/// local:DragDropProperties.DragData="{Binding Item}" /&gt;
/// &lt;Border local:DragDropProperties.IsDropTarget="True" /&gt;
/// </code>
/// </example>
/// </remarks>
public static class WinUIDragDropFactory
{
#region Основные компоненты
/// <summary>
/// Создает и инициализирует менеджер перетаскивания для указанного окна WinUI.
/// </summary>
/// <param name="window">
/// Окно WinUI, для которого создается менеджер перетаскивания.
/// Не может быть null.
/// </param>
/// <returns>
/// Инициализированный экземпляр <see cref="WinUIDragDropManager"/>.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="window"/> равен null.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод является основным способом получения менеджера перетаскивания.
/// Он создает (или возвращает существующий) экземпляр менеджера и инициализирует
/// его для работы с указанным окном.
/// </para>
/// <para>
/// Метод следует вызывать один раз при запуске приложения, обычно в конструкторе
/// главного окна или в методе <see cref="Application.OnLaunched"/>.
/// </para>
/// <example>
/// <code>
/// public partial class MainWindow : Window
/// {
/// public MainWindow()
/// {
/// InitializeComponent();
/// var manager = WinUIDragDropFactory.CreateManager(this);
/// }
/// }
/// </code>
/// </example>
/// </remarks>
public static WinUIDragDropManager CreateManager(Window window)
{
if (window == null)
throw new ArgumentNullException(nameof(window));
var manager = WinUIDragDropManager.Instance;
if (!manager.IsInitialized)
{
manager.Initialize(window);
}
return manager;
}
/// <summary>
/// Создает и инициализирует менеджер перетаскивания с пользовательскими настройками.
/// </summary>
/// <param name="window">
/// Окно WinUI, для которого создается менеджер перетаскивания.
/// </param>
/// <param name="configure">
/// Делегат для настройки параметров менеджера перед инициализацией.
/// Передает экземпляр <see cref="WinUIDragDropManager"/> для конфигурации.
/// </param>
/// <returns>
/// Инициализированный экземпляр <see cref="WinUIDragDropManager"/>.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="window"/> равен null.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод позволяет настроить параметры менеджера перед его инициализацией,
/// что полезно для тонкой настройки поведения системы перетаскивания.
/// </para>
/// <para>
/// Доступные для настройки параметры включают:
/// <list type="bullet">
/// <item><see cref="WinUIDragDropManager.DragVisualOffset"/> - смещение визуального элемента</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// var manager = WinUIDragDropFactory.CreateManager(window, m =>
/// {
/// m.DragVisualOffset = new Point(-15, -15); // Ближе к курсору
/// });
/// </code>
/// </example>
/// </remarks>
public static WinUIDragDropManager CreateManager(Window window, Action<WinUIDragDropManager> configure)
{
if (window == null)
throw new ArgumentNullException(nameof(window));
var manager = WinUIDragDropManager.Instance;
// Применяем настройки перед инициализацией
configure?.Invoke(manager);
if (!manager.IsInitialized)
{
manager.Initialize(window);
}
return manager;
}
/// <summary>
/// Создает хост для управления визуальными элементами перетаскивания.
/// </summary>
/// <param name="window">
/// Окно, к которому будет привязан хост.
/// </param>
/// <returns>
/// Экземпляр <see cref="WinUIDragDropHost"/>, готовый к использованию.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="window"/> равен null.
/// </exception>
/// <remarks>
/// <para>
/// Хост управляет отображением визуальных элементов во время операций перетаскивания,
/// включая drag-визуализации (элементы, следующие за курсором) и drop-превью
/// (подсветка областей сброса).
/// </para>
/// <para>
/// В большинстве случаев хост создается автоматически менеджером перетаскивания.
/// Этот метод полезен для продвинутых сценариев, когда требуется прямой контроль
/// над визуальной обратной связью.
/// </para>
/// <example>
/// <code>
/// var host = WinUIDragDropFactory.CreateHost(window);
/// // Настройка кастомной визуализации
/// </code>
/// </example>
/// </remarks>
public static WinUIDragDropHost CreateHost(Window window)
{
if (window == null)
throw new ArgumentNullException(nameof(window));
var host = new WinUIDragDropHost();
host.Initialize(window);
return host;
}
#endregion
#region Поведения (Behaviors)
/// <summary>
/// Создает поведение источника перетаскивания для элемента WinUI.
/// </summary>
/// <param name="dragDropService">
/// Сервис перетаскивания, который будет управлять операциями.
/// </param>
/// <param name="host">
/// Хост для отображения визуальных элементов.
/// </param>
/// <returns>
/// Экземпляр <see cref="WinUIDragSourceBehavior"/>, готовый к прикреплению к элементу.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если любой из параметров равен null.
/// </exception>
/// <remarks>
/// <para>
/// Созданное поведение необходимо прикрепить к элементу с помощью метода
/// <see cref="WinUIDragSourceBehavior.Attach"/>.
/// </para>
/// <para>
/// Этот метод полезен для продвинутых сценариев, когда требуется создавать поведения
/// вручную, например, при динамическом создании элементов интерфейса.
/// </para>
/// <example>
/// <code>
/// // Создание поведения вручную
/// var behavior = WinUIDragDropFactory.CreateDragSourceBehavior(service, host);
/// behavior.Attach(element, data);
/// </code>
/// </example>
/// </remarks>
public static WinUIDragSourceBehavior CreateDragSourceBehavior(
IDragDropService dragDropService,
WinUIDragDropHost host)
{
if (dragDropService == null)
throw new ArgumentNullException(nameof(dragDropService));
if (host == null)
throw new ArgumentNullException(nameof(host));
return new WinUIDragSourceBehavior(dragDropService, host);
}
/// <summary>
/// Создает поведение цели сброса для элемента WinUI.
/// </summary>
/// <param name="dragDropService">
/// Сервис перетаскивания, который будет управлять операциями.
/// </param>
/// <param name="host">
/// Хост для отображения визуальных элементов.
/// </param>
/// <returns>
/// Экземпляр <see cref="WinUIDropTargetBehavior"/>, готовый к прикреплению к элементу.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если любой из параметров равен null.
/// </exception>
/// <remarks>
/// <para>
/// Созданное поведение необходимо прикрепить к элементу с помощью метода
/// <see cref="WinUIDropTargetBehavior.Attach"/>.
/// </para>
/// <para>
/// Поведение можно дополнительно настроить с помощью методов
/// <see cref="WinUIDropTargetBehavior.AcceptTypes"/> и
/// <see cref="WinUIDropTargetBehavior.AcceptFormats"/> для фильтрации
/// принимаемых данных.
/// </para>
/// <example>
/// <code>
/// // Создание поведения вручную
/// var behavior = WinUIDragDropFactory.CreateDropTargetBehavior(service, host);
/// behavior.AcceptTypes(typeof(string), typeof(MyModel));
/// behavior.Attach(dropArea);
/// </code>
/// </example>
/// </remarks>
public static WinUIDropTargetBehavior CreateDropTargetBehavior(
IDragDropService dragDropService,
WinUIDragDropHost host)
{
if (dragDropService == null)
throw new ArgumentNullException(nameof(dragDropService));
if (host == null)
throw new ArgumentNullException(nameof(host));
return new WinUIDropTargetBehavior(dragDropService, host);
}
/// <summary>
/// Создает поведение источника перетаскивания, используя сервисы из менеджера по умолчанию.
/// </summary>
/// <returns>
/// Экземпляр <see cref="WinUIDragSourceBehavior"/>, готовый к прикреплению к элементу.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Выбрасывается, если менеджер не инициализирован.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод использует <see cref="WinUIDragDropManager.Instance"/> для получения
/// сервиса перетаскивания и хоста, что упрощает создание поведений в контексте
/// уже инициализированной системы.
/// </para>
/// <para>
/// Перед использованием убедитесь, что менеджер инициализирован через метод
/// <see cref="CreateManager"/>.
/// </para>
/// <example>
/// <code>
/// // Инициализация менеджера
/// WinUIDragDropFactory.CreateManager(window);
///
/// // Создание поведения с использованием менеджера
/// var behavior = WinUIDragDropFactory.CreateDragSourceBehavior();
/// behavior.Attach(element, data);
/// </code>
/// </example>
/// </remarks>
public static WinUIDragSourceBehavior CreateDragSourceBehavior()
{
var manager = WinUIDragDropManager.Instance;
if (!manager.IsInitialized)
{
throw new InvalidOperationException(
"Менеджер не инициализирован. Вызовите CreateManager перед созданием поведений.");
}
return new WinUIDragSourceBehavior(manager.DragDropService, manager.Host);
}
/// <summary>
/// Создает поведение цели сброса, используя сервисы из менеджера по умолчанию.
/// </summary>
/// <returns>
/// Экземпляр <see cref="WinUIDropTargetBehavior"/>, готовый к прикреплению к элементу.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Выбрасывается, если менеджер не инициализирован.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод использует <see cref="WinUIDragDropManager.Instance"/> для получения
/// сервиса перетаскивания и хоста, что упрощает создание поведений в контексте
/// уже инициализированной системы.
/// </para>
/// <para>
/// Поведение можно дополнительно настроить с помощью методов
/// <see cref="WinUIDropTargetBehavior.AcceptTypes"/> и
/// <see cref="WinUIDropTargetBehavior.AcceptFormats"/> для фильтрации
/// принимаемых данных.
/// </para>
/// <example>
/// <code>
/// // Инициализация менеджера
/// WinUIDragDropFactory.CreateManager(window);
///
/// // Создание поведения с использованием менеджера
/// var behavior = WinUIDragDropFactory.CreateDropTargetBehavior();
/// behavior.AcceptTypes(typeof(string));
/// behavior.Attach(dropArea);
/// </code>
/// </example>
/// </remarks>
public static WinUIDropTargetBehavior CreateDropTargetBehavior()
{
var manager = WinUIDragDropManager.Instance;
if (!manager.IsInitialized)
{
throw new InvalidOperationException(
"Менеджер не инициализирован. Вызовите CreateManager перед созданием поведений.");
}
return new WinUIDropTargetBehavior(manager.DragDropService, manager.Host);
}
#endregion
#region Визуальные элементы
/// <summary>
/// Создает визуальный элемент для отображения во время перетаскивания.
/// </summary>
/// <param name="dragData">
/// Данные, которые будут отображены в визуальном элементе.
/// Могут быть любого типа, поддерживаемого системой перетаскивания.
/// </param>
/// <param name="opacity">
/// Прозрачность элемента в диапазоне от 0.0 (полностью прозрачный) до 1.0 (полностью непрозрачный).
/// Значение по умолчанию: 0.8.
/// </param>
/// <returns>
/// Экземпляр <see cref="DragAdorner"/>, настроенный для отображения указанных данных.
/// </returns>
/// <remarks>
/// <para>
/// Созданный элемент можно использовать с методом <see cref="WinUIDragDropHost.ShowDragVisual"/>
/// для отображения во время операции перетаскивания.
/// </para>
/// <para>
/// Элемент автоматически адаптирует отображение в зависимости от типа данных:
/// <list type="bullet">
/// <item>Для строк отображается текстовое представление</item>
/// <item>Для изображений отображается миниатюра</item>
/// <item>Для пользовательских объектов используется DataTemplate или ToString()</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Создание визуального элемента
/// var adorner = WinUIDragDropFactory.CreateDragAdorner("Текст для перетаскивания");
///
/// // Отображение во время перетаскивания
/// host.ShowDragVisual(adorner, position);
/// </code>
/// </example>
/// </remarks>
public static DragAdorner CreateDragAdorner(object dragData, double opacity = 0.8)
{
return new DragAdorner
{
DragData = dragData ?? throw new ArgumentNullException(nameof(dragData)),
Opacity = Math.Clamp(opacity, 0.0, 1.0)
};
}
/// <summary>
/// Создает элемент предварительного просмотра для области сброса.
/// </summary>
/// <param name="color">
/// Цвет подсветки области. Если не указан, используется системный цвет акцента.
/// </param>
/// <param name="thickness">
/// Толщина границы подсветки в пикселях.
/// Значение по умолчанию: 2.0.
/// </param>
/// <returns>
/// Экземпляр <see cref="DropPreviewAdorner"/>, готовый к отображению.
/// </returns>
/// <remarks>
/// <para>
/// Этот элемент используется для визуального указания области, на которую можно
/// сбросить данные. Он отображается как подсветка границ целевого элемента с
/// поддержкой анимации появления и скрытия.
/// </para>
/// <para>
/// Элемент автоматически адаптирует свой внешний вид в зависимости от состояния:
/// <list type="bullet">
/// <item><strong>Normal</strong> - стандартное состояние</item>
/// <item><strong>Highlighted</strong> - подсветка при наведении</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Создание элемента подсветки
/// var preview = WinUIDragDropFactory.CreateDropPreviewAdorner(
/// Colors.Blue, // Цвет
/// 3.0 // Толщина границы
/// );
///
/// // Отображение подсветки
/// preview.Show(bounds);
/// </code>
/// </example>
/// </remarks>
public static DropPreviewAdorner CreateDropPreviewAdorner(
Windows.UI.Color? color = null,
double thickness = 2.0)
{
var adorner = new DropPreviewAdorner
{
PreviewThickness = Math.Max(thickness, 0.0)
};
if (color.HasValue)
{
adorner.PreviewColor = color.Value;
}
return adorner;
}
#endregion
#region Готовые конфигурации для типовых сценариев
/// <summary>
/// Создает полную систему перетаскивания для WinUI приложения.
/// </summary>
/// <param name="window">
/// Главное окно приложения.
/// </param>
/// <returns>
/// Кортеж, содержащий менеджер перетаскивания и сервис перетаскивания.
/// </returns>
/// <remarks>
/// <para>
/// Этот метод создает все необходимые компоненты для работы перетаскивания в приложении
/// и возвращает их для дальнейшего использования.
/// </para>
/// <para>
/// Возвращаемый сервис можно использовать для создания дополнительных источников и целей
/// или для низкоуровневого управления операциями перетаскивания.
/// </para>
/// <example>
/// <code>
/// // Создание полной системы
/// var (manager, service) = WinUIDragDropFactory.CreateCompleteSystem(window);
///
/// // Использование менеджера для настройки элементов
/// manager.MakeDragSource(element, data);
///
/// // Использование сервиса для расширенного управления
/// var stats = service.GetStats();
/// </code>
/// </example>
/// </remarks>
public static (WinUIDragDropManager Manager, IDragDropService Service) CreateCompleteSystem(Window window)
{
var manager = CreateManager(window);
return (manager, manager.DragDropService);
}
/// <summary>
/// Создает систему перетаскивания, оптимизированную для переупорядочивания элементов в списках.
/// </summary>
/// <param name="window">
/// Главное окно приложения.
/// </param>
/// <returns>
/// Менеджер перетаскивания, настроенный для переупорядочивания элементов в списках.
/// </returns>
/// <remarks>
/// <para>
/// Эта конфигурация устанавливает оптимальные параметры для перетаскивания элементов
/// внутри списков и коллекций, таких как:
/// <list type="bullet">
/// <item>Изменение порядка элементов в ListView</item>
/// <item>Перемещение элементов между ItemsControl</item>
/// <item>Сортировка элементов в коллекциях</item>
/// </list>
/// </para>
/// <para>
/// Особенности конфигурации:
/// <list type="bullet">
/// <item>Уменьшенный порог начала перетаскивания для более быстрого отклика</item>
/// <item>Смещение визуального элемента для лучшего визуального выравнивания</item>
/// <item>Оптимизированная визуальная обратная связь</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Создание системы для переупорядочивания списков
/// var manager = WinUIDragDropFactory.CreateListReorderSystem(window);
///
/// // Настройка ListView для переупорядочивания
/// foreach (var item in myListView.Items)
/// {
/// if (item is FrameworkElement element)
/// {
/// manager.MakeDragSource(element, element.DataContext);
/// }
/// }
/// manager.MakeDropTarget(myListView);
/// </code>
/// </example>
/// </remarks>
public static WinUIDragDropManager CreateListReorderSystem(Window window)
{
var manager = CreateManager(window, m =>
{
// Уменьшенное смещение для лучшего визуального выравнивания в списках
m.DragVisualOffset = new Core.Geometry.Point(-15, -15);
});
return manager;
}
/// <summary>
/// Создает систему перетаскивания для работы с файлами и документами.
/// </summary>
/// <param name="window">
/// Главное окно приложения.
/// </param>
/// <returns>
/// Менеджер перетаскивания, настроенный для работы с файлами.
/// </returns>
/// <remarks>
/// <para>
/// Эта конфигурация оптимизирована для сценариев работы с файлами:
/// <list type="bullet">
/// <item>Перетаскивание файлов из проводника в приложение</item>
/// <item>Перемещение файлов между элементами интерфейса</item>
/// <item>Работа с большими объемами данных</item>
/// </list>
/// </para>
/// <para>
/// Особенности конфигурации:
/// <list type="bullet">
/// <item>Увеличенный порог перетаскивания для предотвращения случайных операций</item>
/// <item>Специальные визуальные эффекты, характерные для файловых операций</item>
/// <item>Оптимизация для работы с внешними источниками данных</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Создание системы для работы с файлами
/// var manager = WinUIDragDropFactory.CreateFileDragDropSystem(window);
///
/// // Настройка области для приема файлов
/// manager.MakeDropTarget(fileDropArea);
/// </code>
/// </example>
/// </remarks>
public static WinUIDragDropManager CreateFileDragDropSystem(Window window)
{
var manager = CreateManager(window, m =>
{
// Увеличенное смещение для файлов (имитация "переноса" файла)
m.DragVisualOffset = new Core.Geometry.Point(-25, -25);
});
return manager;
}
/// <summary>
/// Создает систему перетаскивания для графических редакторов и инструментов дизайна.
/// </summary>
/// <param name="window">
/// Главное окно приложения.
/// </param>
/// <returns>
/// Менеджер перетаскивания, настроенный для точного позиционирования.
/// </returns>
/// <remarks>
/// <para>
/// Эта конфигурация оптимизирована для приложений, требующих высокой точности
/// позиционирования, таких как:
/// <list type="bullet">
/// <item>Графические редакторы (Photoshop, Figma)</item>
/// <item>Инструменты проектирования интерфейсов</item>
/// <item>CAD-системы и приложения для 3D-моделирования</item>
/// </list>
/// </para>
/// <para>
/// Особенности конфигурации:
/// <list type="bullet">
/// <item>Минимальный порог начала перетаскивания для максимальной точности</item>
/// <item>Минималистичная визуализация для уменьшения визуального шума</item>
/// <item>Оптимизация для работы с высокоточными устройствами ввода (графические планшеты)</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Создание системы для графического редактора
/// var manager = WinUIDragDropFactory.CreateDesignToolSystem(window);
///
/// // Настройка инструментов для перетаскивания
/// manager.MakeDragSource(toolIcon, toolData);
/// manager.MakeDropTarget(canvas);
/// </code>
/// </example>
/// </remarks>
public static WinUIDragDropManager CreateDesignToolSystem(Window window)
{
var manager = CreateManager(window, m =>
{
// Минимальное смещение для точного позиционирования
m.DragVisualOffset = new Core.Geometry.Point(-10, -10);
});
return manager;
}
#endregion
#region Вспомогательные методы
/// <summary>
/// Настраивает элемент как источник перетаскивания с использованием указанного менеджера.
/// </summary>
/// <param name="manager">
/// Менеджер перетаскивания, который будет управлять операцией.
/// </param>
/// <param name="element">
/// Элемент WinUI, который должен стать источником перетаскивания.
/// </param>
/// <param name="dragData">
/// Данные для перетаскивания. Если не указаны, будут использованы DataContext или Tag элемента.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="manager"/> или <paramref name="element"/> равны null.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод является удобной оберткой вокруг <see cref="WinUIDragDropManager.MakeDragSource"/>,
/// предоставляющей более лаконичный синтаксис.
/// </para>
/// <example>
/// <code>
/// // Использование вспомогательного метода
/// WinUIDragDropFactory.SetupAsDragSource(manager, myElement, myData);
///
/// // Эквивалентно:
/// manager.MakeDragSource(myElement, myData);
/// </code>
/// </example>
/// </remarks>
public static void SetupAsDragSource(WinUIDragDropManager manager, FrameworkElement element, object dragData = null)
{
if (manager == null)
throw new ArgumentNullException(nameof(manager));
if (element == null)
throw new ArgumentNullException(nameof(element));
manager.MakeDragSource(element, dragData);
}
/// <summary>
/// Настраивает элемент как цель сброса с использованием указанного менеджера.
/// </summary>
/// <param name="manager">
/// Менеджер перетаскивания, который будет управлять операцией.
/// </param>
/// <param name="element">
/// Элемент WinUI, который должен стать целью сброса.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="manager"/> или <paramref name="element"/> равны null.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод является удобной оберткой вокруг <see cref="WinUIDragDropManager.MakeDropTarget"/>,
/// предоставляющей более лаконичный синтаксис.
/// </para>
/// <example>
/// <code>
/// // Использование вспомогательного метода
/// WinUIDragDropFactory.SetupAsDropTarget(manager, myDropArea);
///
/// // Эквивалентно:
/// manager.MakeDropTarget(myDropArea);
/// </code>
/// </example>
/// </remarks>
public static void SetupAsDropTarget(WinUIDragDropManager manager, FrameworkElement element)
{
if (manager == null)
throw new ArgumentNullException(nameof(manager));
if (element == null)
throw new ArgumentNullException(nameof(element));
manager.MakeDropTarget(element);
}
/// <summary>
/// Настраивает контейнер для поддержки переупорядочивания дочерних элементов.
/// </summary>
/// <param name="manager">
/// Менеджер перетаскивания.
/// </param>
/// <param name="container">
/// Контейнер (например, StackPanel, Grid или ItemsControl), дочерние элементы которого можно переупорядочивать.
/// </param>
/// <param name="childSelector">
/// Функция для получения данных перетаскивания из дочернего элемента.
/// Если не указана, используются DataContext дочерних элементов.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="manager"/> или <paramref name="container"/> равны null.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод автоматически настраивает все дочерние элементы контейнера как источники перетаскивания,
/// а сам контейнер — как цель сброса, создавая функциональность переупорядочивания элементов.
/// </para>
/// <para>
/// Поддерживаемые типы контейнеров:
/// <list type="bullet">
/// <item><see cref="Panel"/> и его производные (StackPanel, Grid, Canvas)</item>
/// <item><see cref="ItemsControl"/> и его производные (ListView, ListBox)</item>
/// <item>Любые другие контейнеры с коллекцией дочерних элементов</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Настройка StackPanel для переупорядочивания дочерних элементов
/// WinUIDragDropFactory.SetupReorderContainer(manager, myStackPanel);
///
/// // С кастомным селектором данных
/// WinUIDragDropFactory.SetupReorderContainer(manager, myListView,
/// element => ((FrameworkElement)element).DataContext);
/// </code>
/// </example>
/// </remarks>
public static void SetupReorderContainer(
WinUIDragDropManager manager,
FrameworkElement container,
Func<FrameworkElement, object> childSelector = null)
{
if (manager == null)
throw new ArgumentNullException(nameof(manager));
if (container == null)
throw new ArgumentNullException(nameof(container));
// Настраиваем контейнер как цель сброса
manager.MakeDropTarget(container);
// Настраиваем дочерние элементы как источники перетаскивания
if (container is Panel panel)
{
SetupPanelChildren(manager, panel, childSelector);
}
else if (container is ItemsControl itemsControl)
{
SetupItemsControlChildren(manager, itemsControl, childSelector);
}
}
/// <summary>
/// Настраивает дочерние элементы Panel как источники перетаскивания.
/// </summary>
private static void SetupPanelChildren(
WinUIDragDropManager manager,
Panel panel,
Func<FrameworkElement, object> childSelector)
{
foreach (var child in panel.Children)
{
if (child is FrameworkElement element)
{
var data = childSelector?.Invoke(element) ?? element.DataContext ?? element.Tag;
manager.MakeDragSource(element, data);
}
}
}
/// <summary>
/// Настраивает элементы ItemsControl как источники перетаскивания.
/// </summary>
private static void SetupItemsControlChildren(
WinUIDragDropManager manager,
ItemsControl itemsControl,
Func<FrameworkElement, object> childSelector)
{
// Для ItemsControl нам нужно работать с ItemContainerGenerator
// В реальной реализации здесь должна быть более сложная логика
// для обработки виртуализации и динамических элементов
foreach (var item in itemsControl.Items)
{
if (item is FrameworkElement element)
{
var data = childSelector?.Invoke(element) ?? element.DataContext ?? element.Tag;
manager.MakeDragSource(element, data);
}
}
}
#endregion
#region Методы для работы с XAML
/// <summary>
/// Настраивает attached properties для элемента источника перетаскивания.
/// </summary>
/// <param name="element">
/// Элемент, который должен стать источником перетаскивания.
/// </param>
/// <param name="dragData">
/// Данные для перетаскивания.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="element"/> равен null.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод устанавливает attached properties, которые активируют поведение перетаскивания
/// при использовании в XAML. Эквивалентно установке свойств IsDragSource и DragData в XAML.
/// </para>
/// <para>
/// Метод полезен для динамической настройки элементов в коде C# при сохранении
/// декларативного стиля программирования.
/// </para>
/// <example>
/// <code>
/// // Настройка элемента в коде C#
/// WinUIDragDropFactory.SetupDragSourceInXaml(myElement, myData);
///
/// // Эквивалентно в XAML:
/// &lt;Border local:DragDropProperties.IsDragSource="True"
/// local:DragDropProperties.DragData="{Binding MyData}" /&gt;
/// </code>
/// </example>
/// </remarks>
public static void SetupDragSourceInXaml(FrameworkElement element, object dragData)
{
if (element == null)
throw new ArgumentNullException(nameof(element));
// Устанавливаем attached properties
element.SetValue(DragDropProperties.IsDragSourceProperty, true);
if (dragData != null)
{
element.SetValue(DragDropProperties.DragDataProperty, dragData);
}
}
/// <summary>
/// Настраивает attached properties для элемента цели сброса.
/// </summary>
/// <param name="element">
/// Элемент, который должен стать целью сброса.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="element"/> равен null.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод устанавливает attached properties, которые активируют поведение цели сброса
/// при использовании в XAML. Эквивалентно установке свойства IsDropTarget в XAML.
/// </para>
/// <para>
/// Метод полезен для динамической настройки элементов в коде C# при сохранении
/// декларативного стиля программирования.
/// </para>
/// <example>
/// <code>
/// // Настройка элемента в коде C#
/// WinUIDragDropFactory.SetupDropTargetInXaml(myDropArea);
///
/// // Эквивалентно в XAML:
/// &lt;Border local:DragDropProperties.IsDropTarget="True" /&gt;
/// </code>
/// </example>
/// </remarks>
public static void SetupDropTargetInXaml(FrameworkElement element)
{
if (element == null)
throw new ArgumentNullException(nameof(element));
element.SetValue(DragDropProperties.IsDropTargetProperty, true);
}
#endregion
}

View File

@@ -1,102 +0,0 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
namespace Lattice.UI.DragDrop.WinUI.Helpers;
/// <summary>
/// Вспомогательный класс для работы с ресурсами.
/// </summary>
public static class ResourceHelper
{
/// <summary>
/// Инициализирует ресурсы перетаскивания.
/// </summary>
public static void InitializeDragDropResources()
{
// Загружаем Generic.xaml, если он еще не загружен
var resourceDictionary = new ResourceDictionary();
// В реальном приложении нужно загружать из правильного пути
try
{
resourceDictionary.Source = new System.Uri("ms-appx:///Lattice.UI.DragDrop.WinUI/Themes/Generic.xaml");
Application.Current.Resources.MergedDictionaries.Add(resourceDictionary);
}
catch
{
// Если не удалось загрузить из файла, создаем базовые ресурсы
CreateFallbackResources();
}
}
private static void CreateFallbackResources()
{
var resources = Application.Current.Resources;
// Базовые кисти для визуальной обратной связи
if (!resources.ContainsKey("DragOverBackgroundBrush"))
{
resources["DragOverBackgroundBrush"] = new SolidColorBrush(
Color.FromArgb(76, 0, 120, 215)); // 30% opacity
}
if (!resources.ContainsKey("DragOverBorderBrush"))
{
resources["DragOverBorderBrush"] = new SolidColorBrush(
Color.FromArgb(255, 0, 120, 215));
}
if (!resources.ContainsKey("DropValidBrush"))
{
resources["DropValidBrush"] = new SolidColorBrush(
Color.FromArgb(255, 0, 204, 0));
}
if (!resources.ContainsKey("DropInvalidBrush"))
{
resources["DropInvalidBrush"] = new SolidColorBrush(
Color.FromArgb(255, 255, 0, 0));
}
}
/// <summary>
/// Получает стиль из ресурсов.
/// </summary>
public static Style? GetStyle(string styleKey)
{
if (Application.Current.Resources.TryGetValue(styleKey, out var style) && style is Style)
{
return style as Style;
}
return null;
}
/// <summary>
/// Получает кисть из ресурсов.
/// </summary>
public static Brush? GetBrush(string brushKey)
{
if (Application.Current.Resources.TryGetValue(brushKey, out var brush) && brush is Brush)
{
return brush as Brush;
}
return null;
}
/// <summary>
/// Добавляет или обновляет ресурс.
/// </summary>
public static void SetResource(string key, object value)
{
Application.Current.Resources[key] = value;
}
/// <summary>
/// Проверяет существование ресурса.
/// </summary>
public static bool HasResource(string key)
{
return Application.Current.Resources.ContainsKey(key);
}
}

View File

@@ -1,89 +0,0 @@
using Lattice.Core.Geometry;
using Microsoft.UI.Xaml;
using System;
using System.Runtime.InteropServices;
/// <summary>
/// Вспомогательный класс для получения экранных координат в WinUI 3.
/// Использует P/Invoke для доступа к нативным API Windows.
/// </summary>
internal static class WinUIWindowHelper
{
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
public POINT(int x, int y)
{
X = x;
Y = y;
}
}
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool ClientToScreen(IntPtr hWnd, ref POINT lpPoint);
[DllImport("user32.dll")]
public static extern IntPtr WindowFromPoint(POINT point);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
/// <summary>
/// Преобразует координаты элемента в экранные координаты.
/// </summary>
public static Point ConvertToScreenCoordinates(FrameworkElement element, Point localPoint)
{
if (element == null || !element.IsLoaded)
return localPoint;
try
{
var window = Window.Current;
if (window == null)
return localPoint;
// Получаем хэндл окна
var hwnd = GetWindowHandle(window);
if (hwnd == IntPtr.Zero)
return localPoint;
// Преобразуем координаты элемента в координаты окна
var transform = element.TransformToVisual(window.Content);
var windowPoint = transform.TransformPoint(
new Windows.Foundation.Point(localPoint.X, localPoint.Y));
// Преобразуем в POINT для P/Invoke
var point = new POINT(
(int)Math.Round(windowPoint.X),
(int)Math.Round(windowPoint.Y));
// Преобразуем клиентские координаты в экранные
if (ClientToScreen(hwnd, ref point))
{
return new Point(point.X, point.Y);
}
return localPoint;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Ошибка преобразования координат: {ex.Message}");
return localPoint;
}
}
/// <summary>
/// Получает хэндл окна WinUI.
/// </summary>
private static IntPtr GetWindowHandle(Window window)
{
// В WinUI 3 можно использовать WinRT API для получения хэндла
// или альтернативные методы в зависимости от контекста
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(window);
return hwnd;
}
}

View File

@@ -1,25 +0,0 @@
<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.UI.DragDrop.WinUI</RootNamespace>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<WinUISDKReferences>false</WinUISDKReferences>
<EnableMsixTooling>true</EnableMsixTooling>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Lattice.Themes.Core\Lattice.Themes.Core.csproj" />
<ProjectReference Include="..\Lattice.UI.DragDrop\Lattice.UI.DragDrop.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260101001" />
</ItemGroup>
</Project>

View File

@@ -1,313 +0,0 @@
# Lattice.Core.DragDrop
Библиотека для реализации drag-and-drop (перетаскивания) в приложениях на .NET.
## 📋 Обзор
Lattice.Core.DragDrop предоставляет полнофункциональную, асинхронную и потокобезопасную систему для реализации операций перетаскивания в пользовательских интерфейсах. Библиотека построена на принципах разделения ответственности и поддерживает сложные сценарии перетаскивания с минимальными усилиями со стороны разработчика.
### ✨ Основные возможности
-**Полностью асинхронный API** - все операции поддерживают async/await
-**Потокобезопасность** - безопасная работа в многопоточных средах
-**Расширяемая архитектура** - легко добавлять новые типы источников и целей
-**Подробные события** - полный контроль над жизненным циклом операций
-**Статистика и мониторинг** - встроенный сбор метрик использования
-**Поддержка CancellationToken** - корректная отмена длительных операций
-**Независимость от UI-фреймворков** - может использоваться с любым представлением
## 🏗️ Архитектура
### Основные компоненты
#### 1. **IDragSource**
Интерфейс для объектов, которые могут быть источником данных при перетаскивании. Определяет:
- Возможность начала перетаскивания
- Подготовку данных для передачи
- Реакцию на завершение или отмену операции
#### 2. **IDropTarget**
Интерфейс для объектов, которые могут принимать сброшенные данные. Определяет:
- Проверку совместимости данных
- Визуальную обратную связь при наведении
- Обработку сброшенных данных
#### 3. **DragDropService**
Центральный сервис, координирующий все операции. Отвечает за:
- Регистрацию и управление целями сброса
- Оркестрацию жизненного цикла операций
- Распространение событий между компонентами
- Сбор статистики и обработку ошибок
#### 4. **Модели данных**
- **DragInfo** - информация о начале перетаскивания
- **DropInfo** - информация о потенциальном сбросе
- **DragDropEffects** - перечисление возможных эффектов
## 🚀 Быстрый старт
### 1. Установка
```csharp
// Пример регистрации в DI-контейнере
services.AddSingleton<IDragDropService, DragDropService>();
```
### 2. Создание источника перетаскивания
```csharp
public class ItemDragSource : IDragSource
{
private readonly Item _item;
public ItemDragSource(Item item)
{
_item = item;
}
public async Task<DragInfo?> TryStartDragAsync(Point startPosition, CancellationToken ct)
{
// Проверяем, можно ли начать перетаскивание
if (!_item.CanBeDragged)
return null;
// Создаем информацию о перетаскивании
return new DragInfo(
data: _item,
allowedEffects: DragDropEffects.Copy | DragDropEffects.Move,
startPosition: startPosition,
source: this
);
}
public async Task OnDragCompletedAsync(DragInfo dragInfo, DragDropEffects effects, CancellationToken ct)
{
if (effects == DragDropEffects.Move)
{
// Удаляем элемент при перемещении
await _repository.DeleteAsync(_item.Id, ct);
}
}
public Task OnDragCancelledAsync(DragInfo dragInfo, CancellationToken ct)
{
// Очистка ресурсов при отмене
return Task.CompletedTask;
}
}
```
### 3. Создание цели сброса
```csharp
public class ContainerDropTarget : IDropTarget
{
private readonly ObservableCollection<Item> _items;
public async Task<bool> CanAcceptDropAsync(DropInfo dropInfo, CancellationToken ct)
{
// Проверяем тип данных
if (dropInfo.Data is not Item item)
return false;
// Проверяем бизнес-правила
return await _validator.CanAddItemAsync(item, ct);
}
public async Task OnDragOverAsync(DropInfo dropInfo, CancellationToken ct)
{
// Обновляем визуальную обратную связь
dropInfo.SuggestedEffects = DragDropEffects.Move;
dropInfo.ShowVisualFeedback = true;
}
public async Task OnDropAsync(DropInfo dropInfo, CancellationToken ct)
{
var item = (Item)dropInfo.Data;
await _items.AddAsync(item, ct);
// Помечаем как обработанное
dropInfo.MarkAsHandled();
}
public Task OnDragLeaveAsync(CancellationToken ct)
{
// Очищаем визуальную обратную связь
return Task.CompletedTask;
}
}
```
### 4. Регистрация и использование сервиса
```csharp
public class MainViewModel
{
private readonly IDragDropService _dragDropService;
public MainViewModel(IDragDropService dragDropService)
{
_dragDropService = dragDropService;
// Регистрация цели сброса
_targetId = _dragDropService.RegisterDropTarget(
target: new ContainerDropTarget(_items),
bounds: new Rect(0, 0, 400, 300),
priority: 0,
group: "main-container"
);
// Подписка на события
_dragDropService.DragStarted += OnDragStarted;
_dragDropService.DragCompleted += OnDragCompleted;
_dragDropService.ErrorOccurred += OnError;
}
// Обработка мышиных событий
public async Task OnMouseDown(Point position)
{
var source = new ItemDragSource(selectedItem);
await _dragDropService.StartDragAsync(source, position);
}
public async Task OnMouseMove(Point position)
{
await _dragDropService.UpdateDragAsync(position);
}
public async Task OnMouseUp(Point position)
{
var effects = await _dragDropService.EndDragAsync(position);
// Обработка результатов
}
}
```
## 📊 Статистика и мониторинг
```csharp
// Получение статистики использования
var stats = _dragDropService.GetStats();
Console.WriteLine($"Всего операций: {stats.TotalDragOperations}");
Console.WriteLine($"Успешных сбросов: {stats.SuccessfulDrops}");
Console.WriteLine($"Отменено операций: {stats.CancelledOperations}");
Console.WriteLine($"Среднее время операции: {stats.AverageOperationTime}");
```
## ⚙️ Конфигурация
### Параметры сервиса
```csharp
// Настройка через свойства сервиса
_dragDropService.DragStartThreshold = 5.0; // Порог в пикселях
_dragDropService.EnableAsyncOperations = true;
_dragDropService.AsyncOperationTimeout = 3000; // 3 секунды
```
### Константы по умолчанию
Все значения по умолчанию определены в классе `DragDropConstants`:
- `DefaultDragThreshold`: 3.0 пикселей
- `DefaultAsyncTimeout`: 5000 миллисекунд
- `TargetLifetimeMinutes`: 10 минут
## 🔧 Расширенные сценарии
### Группировка целей сброса
```csharp
// Регистрация группы целей
_dragDropService.RegisterDropTarget(target1, bounds1, group: "panel");
_dragDropService.RegisterDropTarget(target2, bounds2, group: "panel");
// Массовая отмена регистрации
_dragDropService.UnregisterDropTargetsInGroup("panel");
```
### Обработка ошибок
```csharp
private void OnError(object sender, DragDropErrorEventArgs e)
{
_logger.LogError(e.Exception,
"Ошибка в операции {Operation}",
e.Operation);
// Уведомление пользователя
_notificationService.ShowError("Ошибка при перетаскивании");
}
```
### Кастомизация эффектов
```csharp
// Использование расширений для работы с эффектами
if (effects.CanCopy())
{
// Логика для копирования
}
if (effects.CanMove())
{
// Логика для перемещения
}
// Определение эффекта по модификаторам клавиш
var effect = DragDropEffectsExtensions.GetEffectFromKeys(
controlKey: Keyboard.IsControlDown,
shiftKey: Keyboard.IsShiftDown,
altKey: Keyboard.IsAltDown
);
```
## 📝 Best Practices
1. **Производительность**
- Методы `CanAcceptDropAsync` и `OnDragOverAsync` вызываются часто, оптимизируйте их
- Избегайте синхронных операций в обработчиках
- Используйте кэширование при проверке типов данных
2. **Безопасность**
- Всегда проверяйте тип данных в `CanAcceptDropAsync`
- Валидируйте бизнес-правила перед обработкой сброса
- Используйте CancellationToken для отмены длительных операций
3. **Пользовательский опыт**
- Предоставляйте визуальную обратную связь через `DropInfo.ShowVisualFeedback`
- Используйте `DropInfo.VisualFeedbackData` для кастомизации отображения
- Обрабатывайте отмену операций для очистки временных данных
## 🔄 Миграция
### С версии 1.x на 2.0
1. **Интерфейсы переименованы:**
- `CanStartDragAsync``TryStartDragAsync`
- `DragCompletedAsync``OnDragCompletedAsync`
- `DragCancelledAsync``OnDragCancelledAsync`
2. **Классы EventArgs объединены:**
- Все события наследуются от `DragEventArgs`
- Упрощена иерархия классов событий
3. **Удалены устаревшие компоненты:**
- `AsyncDragDropUtilities` удален
- `ServiceCollectionExtensions` удален
## 📄 Лицензия
Библиотека распространяется под лицензией MIT. Подробности см. в файле LICENSE.
## 🤝 Вклад в разработку
Мы приветствуем вклад в развитие библиотеки. Перед отправкой pull request ознакомьтесь с руководством по контрибьютингу.
## 🐛 Отчеты об ошибках
Для сообщения об ошибках используйте Issues на GitHub. Пожалуйста, указывайте:
- Версию библиотеки
- Шаги для воспроизведения
- Ожидаемое и фактическое поведение
- Пример кода для демонстрации проблемы

View File

@@ -1,160 +0,0 @@
using Lattice.Core.Geometry;
using Lattice.UI.DragDrop.Abstractions;
using Lattice.UI.DragDrop.WinUI.Controls;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
namespace Lattice.UI.DragDrop.WinUI.Services;
/// <summary>
/// Хост для управления визуальными элементами перетаскивания в окне WinUI.
/// </summary>
/// <remarks>
/// <para>
/// Этот класс отвечает за отображение и управление визуальными элементами
/// во время операций перетаскивания, включая:
/// - Drag-визуализацию (элемент, следующий за курсором)
/// - Drop-превью (подсветка областей сброса)
/// </para>
/// <para>
/// Хост создает оверлейный слой поверх всего содержимого окна для корректного
/// отображения визуальных элементов поверх других UI-компонентов.
/// </para>
/// </remarks>
public sealed class WinUIDragDropHost : IDragDropHost, IDisposable
{
private DragDropOverlay? _overlay;
private Window? _window;
private bool _disposed;
/// <summary>
/// Инициализирует хост для работы с указанным окном.
/// </summary>
/// <param name="window">Окно, в котором будет работать перетаскивание.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="window"/> равен null.
/// </exception>
/// <exception cref="ObjectDisposedException">
/// Выбрасывается, если хост уже был удален.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод создает оверлейный слой и добавляет его в визуальное дерево окна.
/// Если содержимое окна не является <see cref="Panel"/>, создается контейнер <see cref="Grid"/>.
/// </para>
/// <para>
/// Метод должен быть вызван один раз перед использованием других методов хоста.
/// </para>
/// </remarks>
public void Initialize(Window window)
{
if (_disposed) throw new ObjectDisposedException(nameof(WinUIDragDropHost));
_window = window ?? throw new ArgumentNullException(nameof(window));
// Создаем оверлей
_overlay = new DragDropOverlay();
// Добавляем оверлей в окно
if (_window.Content is Panel panel)
{
panel.Children.Add(_overlay);
}
else
{
// Если контент не Panel, создаем Grid
var grid = new Grid();
grid.Children.Add(_window.Content as UIElement ?? new Grid());
grid.Children.Add(_overlay);
_window.Content = grid;
}
}
/// <summary>
/// Отображает визуальное представление перетаскиваемого элемента.
/// </summary>
/// <param name="dragVisual">Визуальный элемент для отображения.</param>
/// <param name="position">Позиция отображения в координатах экрана.</param>
/// <remarks>
/// Визуальный элемент будет отображен на оверлейном слое в указанной позиции
/// и будет следовать за курсором при обновлении через <see cref="UpdateDragVisualPosition"/>.
/// </remarks>
public void ShowDragVisual(object dragVisual, Point position)
{
if (_overlay == null || _disposed) return;
if (dragVisual is UIElement element)
{
_overlay.ShowDragVisual(element, position.X, position.Y);
}
}
/// <inheritdoc/>
public void UpdateDragVisualPosition(object dragVisual, Point position)
{
if (_overlay == null || _disposed) return;
if (dragVisual is UIElement element)
{
_overlay.UpdateDragVisualPosition(element, position.X, position.Y);
}
}
/// <inheritdoc/>
public void HideDragVisual(object dragVisual)
{
if (_overlay == null || _disposed) return;
if (dragVisual is UIElement element)
{
_overlay.HideDragVisual(element);
}
else
{
// Скрываем все визуальные элементы
var current = _overlay.GetCurrentDragVisual();
if (current != null)
{
_overlay.HideDragVisual(current);
}
}
}
/// <inheritdoc/>
public void ShowDropAdorner(IDropVisualAdorner adorner)
{
if (_overlay == null || _disposed) return;
if (adorner is DropPreviewAdorner dropAdorner)
{
// Для WinUI пока просто игнорируем
}
}
/// <inheritdoc/>
public void HideDropAdorner(IDropVisualAdorner adorner)
{
if (_overlay == null || _disposed) return;
_overlay.HideAllDropPreviews();
}
/// <inheritdoc/>
public void Dispose()
{
if (_disposed) return;
if (_overlay != null && _window?.Content is Panel panel)
{
panel.Children.Remove(_overlay);
_overlay.ClearAllVisuals();
}
_overlay = null;
_window = null;
_disposed = true;
GC.SuppressFinalize(this);
}
}

View File

@@ -1,625 +0,0 @@
using Lattice.Core.DragDrop.Services;
using Lattice.Core.Geometry;
using Lattice.UI.DragDrop.WinUI.Behaviors;
using Lattice.UI.DragDrop.WinUI.Controls;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
namespace Lattice.UI.DragDrop.WinUI.Services;
/// <summary>
/// Центральный менеджер для управления операциями drag-and-drop в WinUI приложении.
/// Координирует работу источников и целей перетаскивания, управляет визуальной обратной связью
/// и обеспечивает согласованное взаимодействие всех компонентов системы.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="WinUIDragDropManager"/> реализует шаблон Singleton и служит единой точкой
/// входа для настройки и управления операциями перетаскивания в WinUI-приложении.
/// </para>
/// <para>
/// Основные функции менеджера:
/// <list type="bullet">
/// <item>Инициализация системы перетаскивания для конкретного окна</item>
/// <item>Регистрация и отслеживание источников и целей перетаскивания</item>
/// <item>Управление жизненным циклом операций перетаскивания</item>
/// <item>Обеспечение визуальной обратной связи через <see cref="WinUIDragDropHost"/></item>
/// <item>Координация взаимодействия между <see cref="WinUIDragSourceBehavior"/> и <see cref="WinUIDropTargetBehavior"/></item>
/// </list>
/// </para>
/// <para>
/// Для использования менеджера необходимо:
/// <list type="number">
/// <item>Вызвать <see cref="Initialize"/> при создании главного окна приложения</item>
/// <item>Настроить элементы как источники или цели через методы <see cref="MakeDragSource"/> и <see cref="MakeDropTarget"/></item>
/// <item>Использовать attached properties для декларативной настройки в XAML</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Инициализация в коде
/// public partial class MainWindow : Window
/// {
/// private WinUIDragDropManager _manager;
///
/// public MainWindow()
/// {
/// InitializeComponent();
/// _manager = WinUIDragDropManager.Instance;
/// _manager.Initialize(this);
///
/// // Настройка элементов
/// _manager.MakeDragSource(myElement, myData);
/// _manager.MakeDropTarget(myDropArea);
/// }
/// }
///
/// // Или через attached properties в XAML
/// &lt;Border x:Name="DragElement"
/// local:DragDropProperties.IsDragSource="True"
/// local:DragDropProperties.DragData="{Binding MyData}" /&gt;
/// &lt;Border x:Name="DropArea"
/// local:DragDropProperties.IsDropTarget="True" /&gt;
/// </code>
/// </example>
/// </remarks>
public sealed class WinUIDragDropManager : IDisposable
{
#region Singleton Implementation
private static WinUIDragDropManager? _instance;
private static readonly object _lockObject = new();
/// <summary>
/// Получает единственный экземпляр <see cref="WinUIDragDropManager"/>.
/// Реализует шаблон Singleton с ленивой инициализацией и потокобезопасностью.
/// </summary>
/// <value>
/// Единственный экземпляр менеджера перетаскивания для всего приложения.
/// Если экземпляр еще не создан, он будет инициализирован при первом обращении.
/// </value>
/// <remarks>
/// Использование Singleton гарантирует, что во всем приложении существует только один
/// экземпляр менеджера, что обеспечивает согласованное управление всеми операциями
/// перетаскивания и предотвращает конфликты между разными компонентами системы.
/// </remarks>
public static WinUIDragDropManager Instance
{
get
{
if (_instance == null)
{
lock (_lockObject)
{
_instance ??= new WinUIDragDropManager();
}
}
return _instance;
}
}
#endregion
#region Fields
private readonly IDragDropService _dragDropService;
private readonly WinUIDragDropHost _host;
private readonly Dictionary<FrameworkElement, WinUIDragSourceBehavior> _dragSources = new();
private readonly Dictionary<FrameworkElement, WinUIDropTargetBehavior> _dropTargets = new();
private DragAdorner? _currentDragVisual;
private bool _disposed;
private bool _initialized;
#endregion
#region Properties
/// <summary>
/// Получает сервис перетаскивания, используемый менеджером для координации операций.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDragDropService"/>, через который менеджер взаимодействует
/// с ядром системы перетаскивания.
/// </value>
/// <remarks>
/// Этот сервис предоставляет низкоуровневый API для управления операциями перетаскивания
/// и может использоваться для расширенной настройки системы.
/// </remarks>
public IDragDropService DragDropService => _dragDropService;
/// <summary>
/// Получает хост для управления визуальными элементами перетаскивания.
/// </summary>
/// <value>
/// Экземпляр <see cref="WinUIDragDropHost"/>, отвечающий за отображение и позиционирование
/// визуальной обратной связи во время операций перетаскивания.
/// </value>
public WinUIDragDropHost Host => _host;
/// <summary>
/// Получает или задает смещение визуального элемента перетаскивания относительно курсора.
/// </summary>
/// <value>
/// Точка, определяющая смещение по осям X и Y в пикселях.
/// Значение по умолчанию: (-20, -20).
/// </value>
/// <remarks>
/// <para>
/// Отрицательные значения смещают визуальный элемент вверх и влево относительно курсора,
/// что является стандартным поведением в большинстве систем drag-and-drop.
/// </para>
/// <para>
/// Настройка смещения позволяет:
/// <list type="bullet">
/// <item>Предотвратить перекрытие курсора визуальным элементом</item>
/// <item>Обеспечить лучшую видимость области под курсором</item>
/// <item>Создать более естественное визуальное восприятие</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Настройка смещения через фабрику
/// var manager = WinUIDragDropFactory.CreateManager(window, m =>
/// {
/// m.DragVisualOffset = new Point(-15, -15); // Более близко к курсору
/// });
/// </code>
/// </example>
/// </remarks>
public Point DragVisualOffset { get; set; } = new Point(-20, -20);
/// <summary>
/// Получает значение, указывающее, инициализирован ли менеджер.
/// </summary>
/// <value>
/// true, если метод <see cref="Initialize"/> был успешно вызван;
/// в противном случае — false.
/// </value>
/// <remarks>
/// Проверка этого свойства позволяет избежать повторной инициализации менеджера
/// и гарантирует, что система перетаскивания готова к использованию.
/// </remarks>
public bool IsInitialized => _initialized;
#endregion
#region Constructor
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="WinUIDragDropManager"/>.
/// Конструктор является приватным в соответствии с шаблоном Singleton.
/// </summary>
/// <remarks>
/// <para>
/// Внутренний конструктор создает:
/// <list type="bullet">
/// <item>Экземпляр <see cref="DragDropService"/> для управления операциями перетаскивания</item>
/// <item>Экземпляр <see cref="WinUIDragDropHost"/> для визуальной обратной связи</item>
/// </list>
/// </para>
/// <para>
/// Для получения экземпляра менеджера используйте свойство <see cref="Instance"/>.
/// </para>
/// </remarks>
private WinUIDragDropManager()
{
_dragDropService = new DragDropService();
_host = new WinUIDragDropHost();
}
#endregion
#region Public Methods
/// <summary>
/// Инициализирует систему перетаскивания для указанного окна WinUI.
/// Этот метод должен быть вызван один раз при запуске приложения.
/// </summary>
/// <param name="window">
/// Главное окно приложения, для которого настраивается система перетаскивания.
/// Не может быть null.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="window"/> равен null.
/// </exception>
/// <exception cref="InvalidOperationException">
/// Выбрасывается, если менеджер уже инициализирован или был удален.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод выполняет следующие действия:
/// <list type="bullet">
/// <item>Настраивает хост визуальных элементов для работы с указанным окном</item>
/// <item>Подписывается на события сервиса перетаскивания для управления визуальной обратной связью</item>
/// <item>Помечает менеджер как инициализированный</item>
/// </list>
/// </para>
/// <para>
/// Метод должен быть вызван до использования любых других методов менеджера.
/// Рекомендуется вызывать его в конструкторе главного окна приложения.
/// </para>
/// <example>
/// <code>
/// public MainWindow()
/// {
/// InitializeComponent();
/// WinUIDragDropManager.Instance.Initialize(this);
/// }
/// </code>
/// </example>
/// </remarks>
public void Initialize(Window window)
{
if (_disposed)
throw new ObjectDisposedException(nameof(WinUIDragDropManager));
if (_initialized)
throw new InvalidOperationException("Менеджер уже инициализирован.");
if (window == null)
throw new ArgumentNullException(nameof(window));
// Инициализируем хост для работы с окном
_host.Initialize(window);
// Подписываемся на события сервиса перетаскивания
_dragDropService.DragStarted += OnDragStarted;
_dragDropService.DragUpdated += OnDragUpdated;
_dragDropService.DragCompleted += OnDragCompleted;
_dragDropService.DragCancelled += OnDragCancelled;
_initialized = true;
}
/// <summary>
/// Настраивает указанный элемент как источник перетаскивания.
/// </summary>
/// <param name="element">
/// Элемент <see cref="FrameworkElement"/ который должен стать источником перетаскивания.
/// Не может быть null.
/// </param>
/// <param name="dragData">
/// Данные, которые будут перетаскиваться. Может быть null.
/// Если не указано, используются <see cref="FrameworkElement.DataContext"/> или
/// <see cref="FrameworkElement.Tag"/> элемента.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="element"/> равен null.
/// </exception>
/// <exception cref="InvalidOperationException">
/// Выбрасывается, если менеджер не инициализирован или был удален.
/// </exception>
/// <remarks>
/// <para>
/// После вызова этого метода элемент приобретает следующие возможности:
/// <list type="bullet">
/// <item>Реагирует на жесты перетаскивания (удержание и перемещение указателя)</item>
/// <item>Предоставляет указанные данные для перетаскивания</item>
/// <item>Интегрируется с системой визуальной обратной связи</item>
/// </list>
/// </para>
/// <para>
/// Если элемент уже зарегистрирован как источник перетаскивания, метод не выполняет действий.
/// </para>
/// <para>
/// Для отмены регистрации используйте метод <see cref="RemoveDragSource"/>.
/// </para>
/// </remarks>
public void MakeDragSource(FrameworkElement element, object? dragData = null)
{
ValidateManagerState();
if (element == null)
throw new ArgumentNullException(nameof(element));
// Если элемент уже зарегистрирован, ничего не делаем
if (_dragSources.ContainsKey(element))
return;
// Создаем и настраиваем поведение
var behavior = new WinUIDragSourceBehavior(_dragDropService, _host);
behavior.Attach(element, dragData);
_dragSources[element] = behavior;
}
/// <summary>
/// Настраивает указанный элемент как цель сброса.
/// </summary>
/// <param name="element">
/// Элемент <see cref="FrameworkElement"/>, который должен стать целью сброса.
/// Не может быть null.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="element"/> равен null.
/// </exception>
/// <exception cref="InvalidOperationException">
/// Выбрасывается, если менеджер не инициализирован или был удален.
/// </exception>
/// <remarks>
/// <para>
/// После вызова этого метода элемент приобретает следующие возможности:
/// <list type="bullet">
/// <item>Принимает данные, сбрасываемые пользователем</item>
/// <item>Предоставляет визуальную обратную связь при наведении</item>
/// <item>Автоматически обновляет свои границы при изменении размера или позиции</item>
/// </list>
/// </para>
/// <para>
/// По умолчанию цель принимает данные любого типа. Для настройки фильтрации типов
/// используйте методы <see cref="WinUIDropTargetBehavior.AcceptTypes"/> и
/// <see cref="WinUIDropTargetBehavior.AcceptFormats"/>.
/// </para>
/// <para>
/// Если элемент уже зарегистрирован как цель сброса, метод не выполняет действий.
/// </para>
/// <para>
/// Для отмены регистрации используйте метод <see cref="RemoveDropTarget"/>.
/// </para>
/// </remarks>
public void MakeDropTarget(FrameworkElement element)
{
ValidateManagerState();
if (element == null)
throw new ArgumentNullException(nameof(element));
// Если элемент уже зарегистрирован, ничего не делаем
if (_dropTargets.ContainsKey(element))
return;
// Создаем и настраиваем поведение
var behavior = new WinUIDropTargetBehavior(_dragDropService, _host);
behavior.Attach(element);
_dropTargets[element] = behavior;
}
/// <summary>
/// Удаляет возможность перетаскивания у указанного элемента.
/// </summary>
/// <param name="element">
/// Элемент, у которого нужно отключить возможность перетаскивания.
/// Если элемент не зарегистрирован как источник перетаскивания, метод не выполняет действий.
/// </param>
/// <remarks>
/// <para>
/// Этот метод выполняет следующие действия:
/// <list type="bullet">
/// <item>Открепляет поведение перетаскивания от элемента</item>
/// <item>Отписывается от всех событий элемента</item>
/// <item>Удаляет элемент из внутреннего словаря источников</item>
/// <item>Освобождает ресурсы, связанные с поведением</item>
/// </list>
/// </para>
/// <para>
/// Метод безопасен для вызова даже если элемент не был зарегистрирован как источник.
/// </para>
/// </remarks>
public void RemoveDragSource(FrameworkElement element)
{
if (element == null || _disposed || !_dragSources.ContainsKey(element))
return;
if (_dragSources.Remove(element, out var behavior))
{
behavior.Detach();
}
}
/// <summary>
/// Удаляет возможность сброса у указанного элемента.
/// </summary>
/// <param name="element">
/// Элемент, у которого нужно отключить возможность сброса.
/// Если элемент не зарегистрирован как цель сброса, метод не выполняет действий.
/// </param>
/// <remarks>
/// <para>
/// Этот метод выполняет следующие действия:
/// <list type="bullet">
/// <item>Открепляет поведение цели сброса от элемента</item>
/// <item>Восстанавливает свойство <see cref="UIElement.AllowDrop"/> = false</item>
/// <item>Удаляет элемент из внутреннего словаря целей</item>
/// <item>Освобождает ресурсы, связанные с поведением</item>
/// </list>
/// </para>
/// <para>
/// Метод безопасен для вызова даже если элемент не был зарегистрирован как цель.
/// </para>
/// </remarks>
public void RemoveDropTarget(FrameworkElement element)
{
if (element == null || _disposed || !_dropTargets.ContainsKey(element))
return;
if (_dropTargets.Remove(element, out var behavior))
{
behavior.Detach();
}
}
/// <summary>
/// Очищает все регистрации источников и целей перетаскивания.
/// </summary>
/// <remarks>
/// <para>
/// Этот метод полезен в следующих сценариях:
/// <list type="bullet">
/// <item>При перезагрузке содержимого интерфейса</item>
/// <item>При смене контекста данных</item>
/// <item>При освобождении ресурсов перед удалением менеджера</item>
/// </list>
/// </para>
/// <para>
/// После вызова этого метода все элементы теряют возможность участвовать в операциях
/// перетаскивания. Для восстановления функциональности необходимо повторно
/// зарегистрировать элементы через <see cref="MakeDragSource"/> и <see cref="MakeDropTarget"/>.
/// </para>
/// </remarks>
public void Clear()
{
if (_disposed) return;
// Открепляем все источники
foreach (var behavior in _dragSources.Values)
{
behavior.Detach();
}
_dragSources.Clear();
// Открепляем все цели
foreach (var behavior in _dropTargets.Values)
{
behavior.Detach();
}
_dropTargets.Clear();
}
#endregion
#region Event Handlers
/// <summary>
/// Обрабатывает событие начала перетаскивания.
/// Создает и отображает визуальный элемент для обратной связи.
/// </summary>
private void OnDragStarted(object? sender, Core.DragDrop.Services.DragStartedEventArgs e)
{
// Создаем визуальное представление перетаскивания
_currentDragVisual = new DragAdorner
{
DragData = e.DragInfo.Data,
Opacity = 0.8
};
// Рассчитываем позицию с учетом смещения
var position = new Point(
e.Position.X + DragVisualOffset.X,
e.Position.Y + DragVisualOffset.Y
);
// Обновляем позицию и показываем элемент
_currentDragVisual.UpdatePosition(position);
_host.ShowDragVisual(_currentDragVisual, position);
}
/// <summary>
/// Обрабатывает событие обновления позиции перетаскивания.
/// Обновляет позицию визуального элемента для следования за курсором.
/// </summary>
private void OnDragUpdated(object? sender, Core.DragDrop.Services.DragUpdatedEventArgs e)
{
if (_currentDragVisual != null)
{
var position = new Point(
e.Position.X + DragVisualOffset.X,
e.Position.Y + DragVisualOffset.Y
);
_currentDragVisual.UpdatePosition(position);
}
}
/// <summary>
/// Обрабатывает событие завершения перетаскивания.
/// Очищает визуальные элементы и восстанавливает состояние.
/// </summary>
private void OnDragCompleted(object? sender, Core.DragDrop.Services.DragCompletedEventArgs e)
{
CleanupDragVisual();
}
/// <summary>
/// Обрабатывает событие отмены перетаскивания.
/// Очищает визуальные элементы и восстанавливает состояние.
/// </summary>
private void OnDragCancelled(object? sender, Core.DragDrop.Services.DragCancelledEventArgs e)
{
CleanupDragVisual();
}
/// <summary>
/// Освобождает ресурсы визуального элемента перетаскивания.
/// </summary>
private void CleanupDragVisual()
{
if (_currentDragVisual != null)
{
_currentDragVisual.Hide();
_currentDragVisual = null;
}
}
#endregion
#region Helper Methods
/// <summary>
/// Проверяет состояние менеджера перед выполнением операций.
/// </summary>
/// <exception cref="ObjectDisposedException">
/// Выбрасывается, если менеджер был удален.
/// </exception>
/// <exception cref="InvalidOperationException">
/// Выбрасывается, если менеджер не инициализирован.
/// </exception>
private void ValidateManagerState()
{
if (_disposed)
throw new ObjectDisposedException(nameof(WinUIDragDropManager));
if (!_initialized)
throw new InvalidOperationException(
"Менеджер не инициализирован. Вызовите метод Initialize перед использованием.");
}
#endregion
#region IDisposable Implementation
/// <summary>
/// Освобождает все ресурсы, используемые <see cref="WinUIDragDropManager"/>.
/// </summary>
/// <remarks>
/// <para>
/// Этот метод выполняет следующие действия:
/// <list type="bullet">
/// <item>Отписывается от всех событий сервиса перетаскивания</item>
/// <item>Очищает все зарегистрированные источники и цели</item>
/// <item>Освобождает ресурсы хоста визуальных элементов</item>
/// <item>Освобождает ресурсы сервиса перетаскивания</item>
/// </list>
/// </para>
/// <para>
/// После вызова этого метода менеджер перестает быть пригодным для использования.
/// Попытка использовать методы менеджера после удаления приведет к исключению
/// <see cref="ObjectDisposedException"/>.
/// </para>
/// <para>
/// Метод безопасен для многократного вызова.
/// </para>
/// </remarks>
public void Dispose()
{
if (_disposed) return;
// Отписываемся от событий
_dragDropService.DragStarted -= OnDragStarted;
_dragDropService.DragUpdated -= OnDragUpdated;
_dragDropService.DragCompleted -= OnDragCompleted;
_dragDropService.DragCancelled -= OnDragCancelled;
// Очищаем все регистрации
Clear();
// Освобождаем ресурсы
_dragDropService.Dispose();
_host.Dispose();
_disposed = true;
_initialized = false;
GC.SuppressFinalize(this);
}
#endregion
}

View File

@@ -1,44 +0,0 @@
using Lattice.Core.DragDrop.Models;
using Lattice.Core.Geometry;
using Lattice.UI.DragDrop.Abstractions;
using Lattice.UI.DragDrop.WinUI.Controls;
namespace Lattice.UI.DragDrop.WinUI.Services;
public class WinUIDragVisualProvider : IDragVisualProvider
{
private DragAdorner? _currentAdorner;
public object CreateDragVisual(DragInfo dragInfo, Point initialPosition)
{
// Создаем DragAdorner на основе данных
_currentAdorner = new DragAdorner
{
DragData = dragInfo.Data,
OpacityLevel = 0.8
};
// Настраиваем начальную позицию
_currentAdorner.UpdatePosition(initialPosition);
_currentAdorner.Show();
return _currentAdorner;
}
public void UpdateDragVisualPosition(object dragVisual, Point position)
{
if (dragVisual is DragAdorner adorner)
{
adorner.UpdatePosition(position);
}
}
public void ReleaseDragVisual(object dragVisual)
{
if (dragVisual is DragAdorner adorner)
{
adorner.Hide();
_currentAdorner = null;
}
}
}

View File

@@ -1,48 +0,0 @@
<!--
DragAdorner.xaml - Стили для визуального элемента перетаскивания.
Содержит шаблон элемента DragAdorner с поддержкой теней, скруглений и анимаций.
-->
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Lattice.UI.DragDrop.WinUI.Controls"
xmlns:media="using:Microsoft.UI.Xaml.Media">
<Style TargetType="controls:DragAdorner">
<Setter Property="Background" Value="{ThemeResource Lattice.Brush.Background.Secondary}" />
<Setter Property="BorderBrush" Value="{ThemeResource Lattice.Brush.Border.Primary}" />
<Setter Property="BorderThickness" Value="{ThemeResource Lattice.BorderThickness.Thin}" />
<Setter Property="Padding" Value="{ThemeResource Lattice.Spacing.Medium}" />
<Setter Property="CornerRadius" Value="{ThemeResource Lattice.CornerRadius.Medium}" />
<Setter Property="Opacity" Value="{ThemeResource Lattice.Opacity.Drag}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:DragAdorner">
<Grid>
<!-- Фон с тенью -->
<Rectangle
x:Name="Background"
Fill="{TemplateBinding Background}"
Opacity="{TemplateBinding Opacity}"
RadiusX="{TemplateBinding CornerRadius}"
RadiusY="{TemplateBinding CornerRadius}">
<Rectangle.Shadow>
<media:ThemeShadow />
</Rectangle.Shadow>
</Rectangle>
<!-- Контент -->
<ContentPresenter
x:Name="PART_ContentPresenter"
Margin="{TemplateBinding Padding}"
Content="{TemplateBinding DragData}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -1,158 +0,0 @@
<!--
DragDropStyles.xaml - Базовые стили для элементов с поддержкой перетаскивания.
Содержит стили для источников перетаскивания и целей сброса с различными
визуальными состояниями (Normal, Dragging, DragOver и т.д.).
-->
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Стиль для элементов с поддержкой перетаскивания -->
<Style x:Key="Lattice.DragDrop.DragSourceStyle" TargetType="Control">
<Setter Property="Background" Value="{ThemeResource Lattice.Brush.Background.Secondary}" />
<Setter Property="BorderBrush" Value="{ThemeResource Lattice.Brush.Border.Primary}" />
<Setter Property="BorderThickness" Value="{ThemeResource Lattice.BorderThickness.Thin}" />
<Setter Property="Padding" Value="{ThemeResource Lattice.Spacing.Medium}" />
<Setter Property="CornerRadius" Value="{ThemeResource Lattice.CornerRadius.Small}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Control">
<Border
x:Name="RootBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Padding="{TemplateBinding Padding}">
<ContentPresenter x:Name="ContentPresenter" />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="DragDropStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Dragging">
<VisualState.Setters>
<Setter Target="RootBorder.Opacity" Value="{ThemeResource Lattice.Opacity.Drag}" />
<Setter Target="RootBorder.RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX="0.95" ScaleY="0.95" />
</Setter.Value>
</Setter>
<Setter Target="RootBorder.Background">
<Setter.Value>
<SolidColorBrush Color="{ThemeResource Lattice.Color.Accent}" Opacity="0.1" />
</Setter.Value>
</Setter>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="DragOver">
<VisualState.Setters>
<Setter Target="RootBorder.Background">
<Setter.Value>
<SolidColorBrush Color="{ThemeResource Lattice.Color.Accent}" Opacity="0.2" />
</Setter.Value>
</Setter>
<Setter Target="RootBorder.BorderBrush">
<Setter.Value>
<SolidColorBrush Color="{ThemeResource Lattice.Color.Accent}" />
</Setter.Value>
</Setter>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Стиль для целей сброса -->
<Style x:Key="Lattice.DragDrop.DropTargetStyle" TargetType="Control">
<Setter Property="Background" Value="{ThemeResource Lattice.Brush.Background.Tertiary}" />
<Setter Property="BorderBrush" Value="{ThemeResource Lattice.Brush.Border.Secondary}" />
<Setter Property="BorderThickness" Value="{ThemeResource Lattice.BorderThickness.Medium}" />
<Setter Property="Padding" Value="{ThemeResource Lattice.Spacing.Large}" />
<Setter Property="CornerRadius" Value="{ThemeResource Lattice.CornerRadius.Medium}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Control">
<Border
x:Name="RootBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Padding="{TemplateBinding Padding}">
<ContentPresenter x:Name="ContentPresenter" />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="DropTargetStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="DragOver">
<VisualState.Setters>
<Setter Target="RootBorder.Background">
<Setter.Value>
<SolidColorBrush Color="{ThemeResource Lattice.Color.Accent}" Opacity="0.1" />
</Setter.Value>
</Setter>
<Setter Target="RootBorder.BorderBrush">
<Setter.Value>
<SolidColorBrush Color="{ThemeResource Lattice.Color.Accent}" />
</Setter.Value>
</Setter>
<Setter Target="RootBorder.BorderThickness" Value="3" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="DropValid">
<VisualState.Setters>
<Setter Target="RootBorder.Background">
<Setter.Value>
<SolidColorBrush Color="{ThemeResource Lattice.Color.Success}" Opacity="0.1" />
</Setter.Value>
</Setter>
<Setter Target="RootBorder.BorderBrush">
<Setter.Value>
<SolidColorBrush Color="{ThemeResource Lattice.Color.Success}" />
</Setter.Value>
</Setter>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="DropInvalid">
<VisualState.Setters>
<Setter Target="RootBorder.Background">
<Setter.Value>
<SolidColorBrush Color="{ThemeResource Lattice.Color.Error}" Opacity="0.1" />
</Setter.Value>
</Setter>
<Setter Target="RootBorder.BorderBrush">
<Setter.Value>
<SolidColorBrush Color="{ThemeResource Lattice.Color.Error}" />
</Setter.Value>
</Setter>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Бруши для визуальной обратной связи -->
<SolidColorBrush x:Key="Lattice.DragDrop.DragOverBackgroundBrush"
Color="{ThemeResource Lattice.Color.Accent}"
Opacity="0.3" />
<SolidColorBrush x:Key="Lattice.DragDrop.DragOverBorderBrush"
Color="{ThemeResource Lattice.Color.Accent}" />
<SolidColorBrush x:Key="Lattice.DragDrop.DropValidBrush"
Color="{ThemeResource Lattice.Color.Success}"
Opacity="0.5" />
<SolidColorBrush x:Key="Lattice.DragDrop.DropInvalidBrush"
Color="{ThemeResource Lattice.Color.Error}"
Opacity="0.5" />
</ResourceDictionary>

View File

@@ -1,56 +0,0 @@
<!--
DropPreviewAdorner.xaml - Ñòèëè äëÿ ýëåìåíòà ïîäñâåòêè îáëàñòè ñáðîñà.
Ñîäåðæèò øàáëîí ñ íàñòðàèâàåìûìè öâåòàìè è òîëùèíîé ãðàíèöû.
-->
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Lattice.UI.DragDrop.WinUI.Controls"
xmlns:media="using:Microsoft.UI.Xaml.Media">
<Style TargetType="controls:DropPreviewAdorner">
<Setter Property="PreviewBrush" Value="{ThemeResource Lattice.Brush.Accent}" />
<Setter Property="PreviewThickness" Value="{ThemeResource Lattice.BorderThickness.Medium}" />
<Setter Property="PreviewColor" Value="{ThemeResource Lattice.Color.Accent}" />
<Setter Property="CornerRadius" Value="{ThemeResource Lattice.CornerRadius.Medium}" />
<Setter Property="Opacity" Value="{ThemeResource Lattice.Opacity.DropPreview}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:DropPreviewAdorner">
<Border
x:Name="PreviewBorder"
BorderBrush="{TemplateBinding PreviewBrush}"
BorderThickness="{TemplateBinding PreviewThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Opacity="{TemplateBinding Opacity}">
<Border.Background>
<SolidColorBrush
Color="{TemplateBinding PreviewColor}"
Opacity="0.1" />
</Border.Background>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Highlighted">
<VisualState.Setters>
<Setter Target="PreviewBorder.Opacity" Value="0.9" />
<Setter Target="PreviewBorder.BorderThickness" Value="3" />
<Setter Target="PreviewBorder.Background">
<Setter.Value>
<SolidColorBrush
Color="{TemplateBinding PreviewColor}"
Opacity="0.15" />
</Setter.Value>
</Setter>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -1,23 +0,0 @@
<!--
Generic.xaml - Основной словарь ресурсов для компонентов drag-and-drop.
Объединяет все стили и предоставляет алиасы для обратной совместимости.
-->
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<!-- Стили компонентов DragDrop -->
<ResourceDictionary Source="DragAdorner.xaml" />
<ResourceDictionary Source="DropPreviewAdorner.xaml" />
<ResourceDictionary Source="DragDropStyles.xaml" />
</ResourceDictionary.MergedDictionaries>
<!-- Алиасы для обратной совместимости -->
<Style x:Key="DragEnabledStyle" TargetType="Control"
BasedOn="{StaticResource Lattice.DragDrop.DragSourceStyle}" />
<Style x:Key="DropTargetStyle" TargetType="Control"
BasedOn="{StaticResource Lattice.DragDrop.DropTargetStyle}" />
</ResourceDictionary>

View File

@@ -1,81 +0,0 @@
using Lattice.Core.Geometry;
namespace Lattice.UI.DragDrop.Abstractions;
/// <summary>
/// Хост для отображения визуальных элементов перетаскивания.
/// </summary>
/// <remarks>
/// <para>
/// Интерфейс предоставляет абстракцию для управления визуальными элементами
/// перетаскивания в различных UI-фреймворках (WPF, Avalonia, MAUI и т.д.).
/// </para>
/// <para>
/// Реализация должна обеспечивать корректное отображение визуальных элементов
/// поверх других элементов UI и их своевременное удаление при завершении операций.
/// </para>
/// </remarks>
public interface IDragDropHost
{
/// <summary>
/// Показывает визуальное представление перетаскиваемого элемента.
/// </summary>
/// <param name="dragVisual">Визуальное представление элемента для перетаскивания.</param>
/// <param name="position">Начальная позиция визуального элемента в экранных координатах.</param>
/// <remarks>
/// <para>
/// Метод должен отобразить переданный визуальный элемент в указанной позиции.
/// Визуальный элемент должен следовать за курсором мыши при обновлении через <see cref="UpdateDragVisualPosition"/>.
/// </para>
/// <para>
/// Визуальный элемент должен отображаться поверх всех других элементов интерфейса.
/// </para>
/// </remarks>
void ShowDragVisual(object dragVisual, Point position);
/// <summary>
/// Обновляет позицию визуального представления перетаскивания.
/// </summary>
/// <param name="dragVisual">Визуальное представление, позиция которого должна быть обновлена.</param>
/// <param name="position">Новая позиция в экранных координатах.</param>
/// <remarks>
/// Метод должен обновлять позицию уже отображаемого визуального элемента
/// с минимальной задержкой для плавного перемещения.
/// </remarks>
void UpdateDragVisualPosition(object dragVisual, Point position);
/// <summary>
/// Скрывает визуальное представление перетаскивания.
/// </summary>
/// <param name="dragVisual">Визуальное представление для скрытия.</param>
/// <remarks>
/// После вызова этого метода визуальный элемент должен быть полностью
/// удален из визуального дерева и его ресурсы освобождены.
/// </remarks>
void HideDragVisual(object dragVisual);
/// <summary>
/// Показывает визуальную обратную связь для цели сброса.
/// </summary>
/// <param name="adorner">Элемент обратной связи для отображения.</param>
/// <remarks>
/// <para>
/// Метод должен отобразить элемент обратной связи (например, подсветку, рамку или индикатор позиции)
/// для визуального указания возможности сброса на целевой элемент.
/// </para>
/// <para>
/// Элемент обратной связи должен отображаться поверх целевого элемента, но под перетаскиваемым визуальным элементом.
/// </para>
/// </remarks>
void ShowDropAdorner(IDropVisualAdorner adorner);
/// <summary>
/// Скрывает визуальную обратную связь для цели сброса.
/// </summary>
/// <param name="adorner">Элемент обратной связи для скрытия.</param>
/// <remarks>
/// После вызова этого метода элемент обратной связи должен быть
/// удален из визуального дерева и его ресурсы освобождены.
/// </remarks>
void HideDropAdorner(IDropVisualAdorner adorner);
}

View File

@@ -1,67 +0,0 @@
using Lattice.Core.DragDrop.Models;
using Lattice.Core.Geometry;
namespace Lattice.UI.DragDrop.Abstractions;
/// <summary>
/// Поставщик визуального представления для перетаскиваемого элемента.
/// </summary>
/// <remarks>
/// <para>
/// Интерфейс предоставляет абстракцию для создания и управления визуальными
/// представлениями элементов при операции перетаскивания.
/// </para>
/// <para>
/// Реализации могут предоставлять различные стили визуального представления:
/// от простого клонирования оригинального элемента до сложных анимированных представлений.
/// </para>
/// </remarks>
public interface IDragVisualProvider
{
/// <summary>
/// Создает визуальное представление для перетаскивания на основе информации о перетаскивании.
/// </summary>
/// <param name="dragInfo">Информация о перетаскивании, содержащая данные и метаданные операции.</param>
/// <param name="initialPosition">Начальная позиция в экранных координатах.</param>
/// <returns>Объект, представляющий визуальное отображение для перетаскивания.</returns>
/// <remarks>
/// <para>
/// Созданный визуальный элемент должен:
/// 1. Отображать репрезентативное представление перетаскиваемых данных
/// 2. Иметь прозрачный фон или альфа-канал для плавного отображения
/// 3. Быть легковесным для обеспечения плавной анимации
/// 4. Поддерживать возможность изменения позиции через <see cref="UpdateDragVisualPosition"/>
/// </para>
/// <para>
/// Метод может возвращать null, если визуальное представление не требуется.
/// </para>
/// </remarks>
object? CreateDragVisual(DragInfo dragInfo, Point initialPosition);
/// <summary>
/// Обновляет позицию визуального представления перетаскивания.
/// </summary>
/// <param name="dragVisual">Визуальное представление, созданное методом <see cref="CreateDragVisual"/>.</param>
/// <param name="position">Новая позиция в экранных координатах.</param>
/// <remarks>
/// Метод должен обновить позицию визуального элемента максимально эффективно,
/// так как он вызывается часто во время операции перетаскивания.
/// </remarks>
void UpdateDragVisualPosition(object dragVisual, Point position);
/// <summary>
/// Освобождает ресурсы визуального представления.
/// </summary>
/// <param name="dragVisual">Визуальное представление для освобождения.</param>
/// <remarks>
/// <para>
/// Метод должен освободить все ресурсы, связанные с визуальным представлением,
/// включая графические ресурсы, подписки на события и временные данные.
/// </para>
/// <para>
/// Этот метод гарантированно вызывается после завершения операции перетаскивания,
/// независимо от её успешности.
/// </para>
/// </remarks>
void ReleaseDragVisual(object dragVisual);
}

Some files were not shown because too many files have changed in this diff Show More