581 lines
23 KiB
C#
581 lines
23 KiB
C#
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 |