Files
Lattice/Lattice.Core.DragDrop/Factories/DragDropFactory.cs

581 lines
23 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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