366 lines
17 KiB
C#
366 lines
17 KiB
C#
using Lattice.Core.DragDrop.Abstractions;
|
||
using Lattice.Core.DragDrop.Models;
|
||
using Lattice.Core.DragDrop.Services;
|
||
using Lattice.Core.Geometry;
|
||
using System;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
|
||
namespace Lattice.UI.DragDrop.Behaviors;
|
||
|
||
/// <summary>
|
||
/// Базовый класс поведения источника перетаскивания для UI элементов.
|
||
/// </summary>
|
||
/// <typeparam name="TElement">Тип UI элемента, к которому прикрепляется поведение.</typeparam>
|
||
/// <remarks>
|
||
/// <para>
|
||
/// Этот класс предоставляет базовую реализацию поведения перетаскивания для UI элементов.
|
||
/// Он обрабатывает события мыши/тач, управляет порогом начала перетаскивания и
|
||
/// интегрируется с сервисом <see cref="IDragDropService"/> из ядра.
|
||
/// </para>
|
||
/// <para>
|
||
/// Производные классы должны реализовать абстрактные методы для конкретной
|
||
/// UI-платформы и предоставить логику создания информации о перетаскивании.
|
||
/// </para>
|
||
/// </remarks>
|
||
public abstract class DragSourceBehaviorBase<TElement> : IDragSource
|
||
where TElement : class
|
||
{
|
||
private IDragDropService? _dragDropService;
|
||
private Point _dragStartPosition;
|
||
private bool _isDragging;
|
||
private TElement? _associatedElement;
|
||
private CancellationTokenSource? _dragCancellationTokenSource;
|
||
|
||
/// <summary>
|
||
/// Получает или задает связанный UI элемент.
|
||
/// </summary>
|
||
/// <value>
|
||
/// Элемент UI, к которому прикреплено поведение перетаскивания.
|
||
/// При изменении значения автоматически выполняется переподключение событий.
|
||
/// </value>
|
||
protected TElement? AssociatedElement
|
||
{
|
||
get => _associatedElement;
|
||
set
|
||
{
|
||
if (_associatedElement != value)
|
||
{
|
||
DetachFromElement();
|
||
_associatedElement = value;
|
||
AttachToElement();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получает сервис перетаскивания из контейнера зависимостей.
|
||
/// </summary>
|
||
/// <value>
|
||
/// Экземпляр <see cref="IDragDropService"/>, используемый для управления операциями перетаскивания.
|
||
/// </value>
|
||
protected IDragDropService DragDropService { get; }
|
||
|
||
/// <summary>
|
||
/// Получает значение, указывающее, выполняется ли в данный момент операция перетаскивания.
|
||
/// </summary>
|
||
protected bool IsDragging => _isDragging;
|
||
|
||
/// <summary>
|
||
/// Инициализирует новый экземпляр класса <see cref="DragSourceBehaviorBase{TElement}"/>.
|
||
/// </summary>
|
||
/// <param name="dragDropService">Сервис перетаскивания.</param>
|
||
/// <exception cref="ArgumentNullException">
|
||
/// Выбрасывается, когда <paramref name="dragDropService"/> равен null.
|
||
/// </exception>
|
||
protected DragSourceBehaviorBase(IDragDropService dragDropService)
|
||
{
|
||
DragDropService = dragDropService ?? throw new ArgumentNullException(nameof(dragDropService));
|
||
}
|
||
|
||
/// <summary>
|
||
/// Вызывается при прикреплении поведения к элементу.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// Реализация по умолчанию подписывается на события элемента через <see cref="SubscribeToEvents"/>.
|
||
/// Производные классы могут переопределить этот метод для дополнительной инициализации.
|
||
/// </remarks>
|
||
protected virtual void AttachToElement()
|
||
{
|
||
if (_associatedElement != null)
|
||
{
|
||
SubscribeToEvents(_associatedElement);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Вызывается при откреплении поведения от элемента.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// Реализация по умолчанию отписывается от событий элемента через <see cref="UnsubscribeFromEvents"/>.
|
||
/// Производные классы могут переопределить этот метод для дополнительной очистки.
|
||
/// </remarks>
|
||
protected virtual void DetachFromElement()
|
||
{
|
||
if (_associatedElement != null)
|
||
{
|
||
UnsubscribeFromEvents(_associatedElement);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Подписывается на события элемента, необходимые для отслеживания начала перетаскивания.
|
||
/// </summary>
|
||
/// <param name="element">Элемент, к событиям которого нужно подписаться.</param>
|
||
/// <remarks>
|
||
/// Производные классы должны реализовать этот метод для подписки на события конкретной
|
||
/// UI-платформы (например, MouseDown для WPF, PointerPressed для Avalonia).
|
||
/// </remarks>
|
||
protected abstract void SubscribeToEvents(TElement element);
|
||
|
||
/// <summary>
|
||
/// Отписывается от событий элемента.
|
||
/// </summary>
|
||
/// <param name="element">Элемент, от событий которого нужно отписаться.</param>
|
||
/// <remarks>
|
||
/// Производные классы должны реализовать этот метод для корректной отписки
|
||
/// от событий, на которые была выполнена подписка в <see cref="SubscribeToEvents"/>.
|
||
/// </remarks>
|
||
protected abstract void UnsubscribeFromEvents(TElement element);
|
||
|
||
/// <summary>
|
||
/// Обрабатывает начало взаимодействия с элементом (например, нажатие кнопки мыши).
|
||
/// </summary>
|
||
/// <param name="position">Позиция взаимодействия в координатах элемента.</param>
|
||
/// <returns>Задача, представляющая асинхронную операцию.</returns>
|
||
/// <remarks>
|
||
/// <para>
|
||
/// Этот метод вызывается из обработчиков событий UI-платформы при начале
|
||
/// взаимодействия, которое может привести к перетаскиванию.
|
||
/// </para>
|
||
/// <para>
|
||
/// Реализация по умолчанию сохраняет начальную позицию для последующей
|
||
/// проверки порога перетаскивания.
|
||
/// </para>
|
||
/// </remarks>
|
||
protected virtual Task OnInteractionStarted(Point position)
|
||
{
|
||
if (_isDragging)
|
||
return Task.CompletedTask;
|
||
|
||
_dragStartPosition = position;
|
||
_dragCancellationTokenSource = new CancellationTokenSource();
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Обрабатывает перемещение во время взаимодействия с элементом.
|
||
/// </summary>
|
||
/// <param name="position">Текущая позиция взаимодействия в координатах элемента.</param>
|
||
/// <returns>Задача, представляющая асинхронную операцию.</returns>
|
||
/// <remarks>
|
||
/// <para>
|
||
/// Этот метод вызывается при перемещении курсора/тач-точки во время удержания
|
||
/// взаимодействия (например, перемещение мыши с нажатой кнопкой).
|
||
/// </para>
|
||
/// <para>
|
||
/// Реализация по умолчанию проверяет, превышено ли расстояние от начальной
|
||
/// точки порога перетаскивания, и если да - начинает операцию перетаскивания.
|
||
/// </para>
|
||
/// </remarks>
|
||
protected virtual async Task OnInteractionMoved(Point position)
|
||
{
|
||
if (_isDragging || AssociatedElement == null)
|
||
return;
|
||
|
||
var distance = CalculateDistance(_dragStartPosition, position);
|
||
if (distance > DragDropService.DragStartThreshold)
|
||
{
|
||
await StartDragOperation();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Обрабатывает завершение взаимодействия с элементом.
|
||
/// </summary>
|
||
/// <returns>Задача, представляющая асинхронную операцию.</returns>
|
||
/// <remarks>
|
||
/// <para>
|
||
/// Этот метод вызывается при завершении взаимодействия (например, отпускании кнопки мыши).
|
||
/// </para>
|
||
/// <para>
|
||
/// Реализация по умолчанию сбрасывает состояние поведения, если перетаскивание не было начато.
|
||
/// </para>
|
||
/// </remarks>
|
||
protected virtual Task OnInteractionEnded()
|
||
{
|
||
// Сброс состояния, если перетаскивание не началось
|
||
if (!_isDragging)
|
||
{
|
||
Reset();
|
||
}
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Обрабатывает отмену взаимодействия с элементом.
|
||
/// </summary>
|
||
/// <returns>Задача, представляющая асинхронную операцию.</returns>
|
||
/// <remarks>
|
||
/// <para>
|
||
/// Этот метод вызывается при отмене взаимодействия (например, нажатии клавиши Escape
|
||
/// или выходе за пределы допустимой области).
|
||
/// </para>
|
||
/// <para>
|
||
/// Реализация по умолчанию отменяет текущую операцию перетаскивания, если она активна,
|
||
/// и сбрасывает состояние поведения.
|
||
/// </para>
|
||
/// </remarks>
|
||
protected virtual async Task OnInteractionCancelled()
|
||
{
|
||
if (_isDragging)
|
||
{
|
||
await DragDropService.CancelDragAsync();
|
||
}
|
||
Reset();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Начинает операцию перетаскивания.
|
||
/// </summary>
|
||
/// <returns>Задача, представляющая асинхронную операцию.</returns>
|
||
/// <remarks>
|
||
/// <para>
|
||
/// Этот метод преобразует начальную позицию в экранные координаты и вызывает
|
||
/// сервис перетаскивания для начала операции.
|
||
/// </para>
|
||
/// <para>
|
||
/// Операция начинается только если поведение прикреплено к элементу и
|
||
/// не выполняется другая операция перетаскивания.
|
||
/// </para>
|
||
/// </remarks>
|
||
protected virtual async Task StartDragOperation()
|
||
{
|
||
if (_isDragging || AssociatedElement == null || _dragCancellationTokenSource == null)
|
||
return;
|
||
|
||
// Получаем начальную позицию в экранных координатах
|
||
var screenPosition = ConvertToScreenCoordinates(_dragStartPosition);
|
||
|
||
// Начинаем перетаскивание
|
||
try
|
||
{
|
||
_isDragging = await DragDropService.StartDragAsync(this, screenPosition);
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
// Операция была отменена
|
||
Reset();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Преобразует координаты элемента в экранные координаты.
|
||
/// </summary>
|
||
/// <param name="point">Точка в координатах элемента.</param>
|
||
/// <returns>Точка в экранных координатах.</returns>
|
||
/// <remarks>
|
||
/// Производные классы должны реализовать этот метод для преобразования
|
||
/// координат в соответствии с конкретной UI-платформой.
|
||
/// </remarks>
|
||
protected abstract Point ConvertToScreenCoordinates(Point point);
|
||
|
||
/// <summary>
|
||
/// Вычисляет расстояние между двумя точками.
|
||
/// </summary>
|
||
/// <param name="p1">Первая точка.</param>
|
||
/// <param name="p2">Вторая точка.</param>
|
||
/// <returns>Расстояние между точками.</returns>
|
||
protected virtual double CalculateDistance(Point p1, Point p2)
|
||
{
|
||
var dx = p2.X - p1.X;
|
||
var dy = p2.Y - p1.Y;
|
||
return Math.Sqrt(dx * dx + dy * dy);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Сбрасывает состояние поведения.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// Этот метод очищает все временные данные и отменяет токены отмены,
|
||
/// связанные с текущей операцией перетаскивания.
|
||
/// </remarks>
|
||
protected virtual void Reset()
|
||
{
|
||
_isDragging = false;
|
||
_dragStartPosition = default;
|
||
|
||
_dragCancellationTokenSource?.Dispose();
|
||
_dragCancellationTokenSource = null;
|
||
}
|
||
|
||
#region IDragSource Implementation
|
||
|
||
/// <inheritdoc/>
|
||
public abstract Task<DragInfo?> TryStartDragAsync(Point startPosition, CancellationToken cancellationToken = default);
|
||
|
||
/// <inheritdoc/>
|
||
public async Task OnDragCompletedAsync(DragInfo dragInfo, Lattice.Core.DragDrop.Enums.DragDropEffects effects, CancellationToken cancellationToken = default)
|
||
{
|
||
_isDragging = false;
|
||
OnDragCompleted(dragInfo, effects);
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public async Task OnDragCancelledAsync(DragInfo dragInfo, CancellationToken cancellationToken = default)
|
||
{
|
||
_isDragging = false;
|
||
OnDragCancelled(dragInfo);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Virtual Methods for Derived Classes
|
||
|
||
/// <summary>
|
||
/// Вызывается при успешном завершении операции перетаскивания.
|
||
/// </summary>
|
||
/// <param name="dragInfo">Информация о перетаскивании, использованная в операции.</param>
|
||
/// <param name="effects">Эффекты, примененные при завершении операции.</param>
|
||
/// <remarks>
|
||
/// Производные классы могут переопределить этот метод для выполнения
|
||
/// дополнительных действий после успешного завершения перетаскивания,
|
||
/// например, удаления исходного элемента при перемещении.
|
||
/// </remarks>
|
||
protected virtual void OnDragCompleted(DragInfo dragInfo, Lattice.Core.DragDrop.Enums.DragDropEffects effects)
|
||
{
|
||
}
|
||
|
||
/// <summary>
|
||
/// Вызывается при отмене операции перетаскивания.
|
||
/// </summary>
|
||
/// <param name="dragInfo">Информация о перетаскивании, использованная в операции.</param>
|
||
/// <remarks>
|
||
/// Производные классы могут переопределить этот метод для выполнения
|
||
/// действий по восстановлению состояния после отмены перетаскивания.
|
||
/// </remarks>
|
||
protected virtual void OnDragCancelled(DragInfo dragInfo)
|
||
{
|
||
}
|
||
|
||
#endregion
|
||
|
||
/// <summary>
|
||
/// Открепляет поведение от элемента и освобождает ресурсы.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// После вызова этого метода поведение больше не будет обрабатывать события
|
||
/// элемента и может быть безопасно удалено.
|
||
/// </remarks>
|
||
public virtual void Detach()
|
||
{
|
||
DetachFromElement();
|
||
_associatedElement = null;
|
||
Reset();
|
||
}
|
||
} |