Убраны синхронные методы
This commit is contained in:
@@ -1,28 +0,0 @@
|
|||||||
namespace Lattice.Core.DragDrop.Abstractions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Определяет контракт для объектов, которые могут быть источником данных
|
|
||||||
/// в операции перетаскивания с поддержкой асинхронных операций.
|
|
||||||
/// </summary>
|
|
||||||
public interface IAsyncDragSource : IDragSource
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Определяет, может ли объект начать операцию перетаскивания (асинхронно).
|
|
||||||
/// </summary>
|
|
||||||
Task<(bool CanStart, Models.DragInfo? DragInfo)> CanStartDragAsync();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Начинает операцию перетаскивания (асинхронно).
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> StartDragAsync(Models.DragInfo dragInfo);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывается при завершении операции перетаскивания (асинхронно).
|
|
||||||
/// </summary>
|
|
||||||
Task DragCompletedAsync(Models.DragInfo dragInfo, Enums.DragDropEffects effects);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывается при отмене операции перетаскивания (асинхронно).
|
|
||||||
/// </summary>
|
|
||||||
Task DragCancelledAsync(Models.DragInfo dragInfo);
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
namespace Lattice.Core.DragDrop.Abstractions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Определяет контракт для объектов, которые могут принимать сбрасываемые данные
|
|
||||||
/// в операции перетаскивания с поддержкой асинхронных операций.
|
|
||||||
/// </summary>
|
|
||||||
public interface IAsyncDropTarget : IDropTarget
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Определяет, может ли объект принять сбрасываемые данные (асинхронно).
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> CanAcceptDropAsync(Models.DropInfo dropInfo);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывается, когда перетаскиваемый объект находится над целью (асинхронно).
|
|
||||||
/// </summary>
|
|
||||||
Task DragOverAsync(Models.DropInfo dropInfo);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывается, когда пользователь сбрасывает данные на цель (асинхронно).
|
|
||||||
/// </summary>
|
|
||||||
Task DropAsync(Models.DropInfo dropInfo);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывается, когда перетаскиваемый объект покидает область цели (асинхронно).
|
|
||||||
/// </summary>
|
|
||||||
Task DragLeaveAsync();
|
|
||||||
}
|
|
||||||
@@ -13,18 +13,15 @@ public interface IDragSource
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Определяет, может ли объект начать операцию перетаскивания.
|
/// Определяет, может ли объект начать операцию перетаскивания.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dragInfo">
|
|
||||||
/// Информация о перетаскивании, которая будет заполнена данными, если операция разрешена.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
/// <returns>
|
||||||
/// true, если объект может начать перетаскивание; в противном случае — false.
|
/// Кортеж, содержащий флаг возможности начала перетаскивания и информацию о перетаскивании.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Этот метод вызывается системой перетаскивания для проверки возможности
|
/// Этот метод вызывается системой перетаскивания для проверки возможности
|
||||||
/// начала операции. Если метод возвращает true, он должен заполнить
|
/// начала операции. Если метод возвращает true, он должен заполнить
|
||||||
/// <paramref name="dragInfo"/> необходимыми данными.
|
/// DragInfo необходимыми данными.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
bool CanStartDrag(out Models.DragInfo? dragInfo);
|
Task<(bool CanStart, Models.DragInfo? DragInfo)> CanStartDragAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Начинает операцию перетаскивания.
|
/// Начинает операцию перетаскивания.
|
||||||
@@ -38,7 +35,7 @@ public interface IDragSource
|
|||||||
/// Реализация должна подготовить данные для перетаскивания и, возможно,
|
/// Реализация должна подготовить данные для перетаскивания и, возможно,
|
||||||
/// создать визуальное представление перетаскиваемого объекта.
|
/// создать визуальное представление перетаскиваемого объекта.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
bool StartDrag(Models.DragInfo dragInfo);
|
Task<bool> StartDragAsync(Models.DragInfo dragInfo, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Вызывается при завершении операции перетаскивания.
|
/// Вызывается при завершении операции перетаскивания.
|
||||||
@@ -50,7 +47,7 @@ public interface IDragSource
|
|||||||
/// (успешного или неуспешного). Реализация может выполнить очистку
|
/// (успешного или неуспешного). Реализация может выполнить очистку
|
||||||
/// или обновить состояние на основе результата операции.
|
/// или обновить состояние на основе результата операции.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
void DragCompleted(Models.DragInfo dragInfo, Enums.DragDropEffects effects);
|
Task DragCompletedAsync(Models.DragInfo dragInfo, Enums.DragDropEffects effects, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Вызывается при отмене операции перетаскивания.
|
/// Вызывается при отмене операции перетаскивания.
|
||||||
@@ -60,5 +57,5 @@ public interface IDragSource
|
|||||||
/// Этот метод вызывается, когда операция перетаскивания была отменена
|
/// Этот метод вызывается, когда операция перетаскивания была отменена
|
||||||
/// пользователем (например, нажатием клавиши Escape).
|
/// пользователем (например, нажатием клавиши Escape).
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
void DragCancelled(Models.DragInfo dragInfo);
|
Task DragCancelledAsync(Models.DragInfo dragInfo, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@ public interface IDropTarget
|
|||||||
/// Реализация должна проверить, совместимы ли данные с целью, и установить
|
/// Реализация должна проверить, совместимы ли данные с целью, и установить
|
||||||
/// предлагаемые эффекты в <paramref name="dropInfo"/>.
|
/// предлагаемые эффекты в <paramref name="dropInfo"/>.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
bool CanAcceptDrop(Models.DropInfo dropInfo);
|
Task<bool> CanAcceptDropAsync(Models.DropInfo dropInfo, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Вызывается, когда перетаскиваемый объект находится над целью.
|
/// Вызывается, когда перетаскиваемый объект находится над целью.
|
||||||
@@ -32,7 +32,7 @@ public interface IDropTarget
|
|||||||
/// Этот метод вызывается постоянно, пока пользователь перемещает объект над целью.
|
/// Этот метод вызывается постоянно, пока пользователь перемещает объект над целью.
|
||||||
/// Реализация может обновить визуальную обратную связь или изменить предлагаемые эффекты.
|
/// Реализация может обновить визуальную обратную связь или изменить предлагаемые эффекты.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
void DragOver(Models.DropInfo dropInfo);
|
Task DragOverAsync(Models.DropInfo dropInfo, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Вызывается, когда пользователь сбрасывает данные на цель.
|
/// Вызывается, когда пользователь сбрасывает данные на цель.
|
||||||
@@ -42,7 +42,7 @@ public interface IDropTarget
|
|||||||
/// Этот метод вызывается, когда пользователь отпускает кнопку мыши над целью.
|
/// Этот метод вызывается, когда пользователь отпускает кнопку мыши над целью.
|
||||||
/// Реализация должна обработать принятие данных и выполнить соответствующее действие.
|
/// Реализация должна обработать принятие данных и выполнить соответствующее действие.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
void Drop(Models.DropInfo dropInfo);
|
Task DropAsync(Models.DropInfo dropInfo, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Вызывается, когда перетаскиваемый объект покидает область цели.
|
/// Вызывается, когда перетаскиваемый объект покидает область цели.
|
||||||
@@ -51,5 +51,5 @@ public interface IDropTarget
|
|||||||
/// Этот метод вызывается, когда пользователь перемещает объект за пределы цели.
|
/// Этот метод вызывается, когда пользователь перемещает объект за пределы цели.
|
||||||
/// Реализация должна очистить любую визуальную обратную связь, установленную ранее.
|
/// Реализация должна очистить любую визуальную обратную связь, установленную ранее.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
void DragLeave();
|
Task DragLeaveAsync(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
27
Lattice.Core.DragDrop/Constants/DragDropConstants.cs
Normal file
27
Lattice.Core.DragDrop/Constants/DragDropConstants.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -88,7 +88,7 @@ public static class DragDropEffectsExtensions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static DragDropEffects GetEffectFromKeys(bool controlKey, bool shiftKey, bool altKey)
|
public static DragDropEffects GetEffectFromKeys(bool controlKey, bool shiftKey, bool altKey)
|
||||||
{
|
{
|
||||||
if (controlKey && altKey)
|
if (controlKey && shiftKey)
|
||||||
return DragDropEffects.Link;
|
return DragDropEffects.Link;
|
||||||
if (controlKey)
|
if (controlKey)
|
||||||
return DragDropEffects.Copy;
|
return DragDropEffects.Copy;
|
||||||
|
|||||||
47
Lattice.Core.DragDrop/Extensions/DropInfoExtensions.cs
Normal file
47
Lattice.Core.DragDrop/Extensions/DropInfoExtensions.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
namespace Lattice.Core.DragDrop.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Методы расширения для DropInfo.
|
||||||
|
/// </summary>
|
||||||
|
public static class DropInfoExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Проверяет, могут ли данные быть приведены к указанному типу.
|
||||||
|
/// </summary>
|
||||||
|
public static bool CanAccept<T>(this Models.DropInfo dropInfo)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
return dropInfo.Data is T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Пытается получить данные как указанный тип.
|
||||||
|
/// </summary>
|
||||||
|
public static T? GetDataAs<T>(this Models.DropInfo dropInfo)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
return dropInfo.Data as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает данные как указанный тип или выбрасывает исключение.
|
||||||
|
/// </summary>
|
||||||
|
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>
|
||||||
|
public static bool IsInBounds(this Models.DropInfo dropInfo, Geometry.Rect bounds)
|
||||||
|
{
|
||||||
|
return bounds.Contains(dropInfo.Position);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -122,6 +122,15 @@ public class DragInfo : IDisposable, ICloneable
|
|||||||
public DragInfo(object data, Enums.DragDropEffects allowedEffects, Point startPosition, object? source = null)
|
public DragInfo(object data, Enums.DragDropEffects allowedEffects, Point startPosition, object? source = null)
|
||||||
{
|
{
|
||||||
Data = data ?? throw new ArgumentNullException(nameof(data));
|
Data = data ?? throw new ArgumentNullException(nameof(data));
|
||||||
|
|
||||||
|
// Проверка допустимых значений перечисления
|
||||||
|
if (!Enum.IsDefined(typeof(Enums.DragDropEffects), allowedEffects))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
$"Недопустимое значение для {nameof(allowedEffects)}: {allowedEffects}",
|
||||||
|
nameof(allowedEffects));
|
||||||
|
}
|
||||||
|
|
||||||
AllowedEffects = allowedEffects;
|
AllowedEffects = allowedEffects;
|
||||||
StartPosition = startPosition;
|
StartPosition = startPosition;
|
||||||
Source = source;
|
Source = source;
|
||||||
@@ -145,7 +154,10 @@ public class DragInfo : IDisposable, ICloneable
|
|||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
|
|
||||||
var clone = new DragInfo(Data, AllowedEffects, newPosition, Source);
|
var clone = new DragInfo(Data, AllowedEffects, newPosition, Source)
|
||||||
|
{
|
||||||
|
_disposed = false,
|
||||||
|
};
|
||||||
|
|
||||||
foreach (var kvp in _parameters)
|
foreach (var kvp in _parameters)
|
||||||
{
|
{
|
||||||
@@ -209,6 +221,14 @@ public class DragInfo : IDisposable, ICloneable
|
|||||||
{
|
{
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
|
|
||||||
|
foreach (var value in _parameters.Values)
|
||||||
|
{
|
||||||
|
if (value is IDisposable disposable)
|
||||||
|
{
|
||||||
|
disposable.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_parameters.Clear();
|
_parameters.Clear();
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
|
|||||||
@@ -130,7 +130,8 @@ public sealed class DragDropService : IDragDropService
|
|||||||
{
|
{
|
||||||
// Инициализация таймера очистки (каждые 5 минут)
|
// Инициализация таймера очистки (каждые 5 минут)
|
||||||
_cleanupTimer = new Timer(CleanupExpiredTargets, null,
|
_cleanupTimer = new Timer(CleanupExpiredTargets, null,
|
||||||
TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
|
TimeSpan.FromMinutes(Constants.DragDropConstants.TargetLifetimeMinutes / 2),
|
||||||
|
TimeSpan.FromMinutes(Constants.DragDropConstants.TargetLifetimeMinutes / 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -266,7 +267,7 @@ public sealed class DragDropService : IDragDropService
|
|||||||
Models.DragInfo? dragInfo = null;
|
Models.DragInfo? dragInfo = null;
|
||||||
|
|
||||||
// Проверка возможности начала перетаскивания
|
// Проверка возможности начала перетаскивания
|
||||||
if (EnableAsyncOperations && source is Abstractions.IAsyncDragSource asyncSource)
|
if (EnableAsyncOperations && source is Abstractions.IDragSource asyncSource)
|
||||||
{
|
{
|
||||||
var result = await ExecuteWithTimeoutAsync(
|
var result = await ExecuteWithTimeoutAsync(
|
||||||
asyncSource.CanStartDragAsync(),
|
asyncSource.CanStartDragAsync(),
|
||||||
@@ -280,8 +281,11 @@ public sealed class DragDropService : IDragDropService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!source.CanStartDrag(out dragInfo) || dragInfo == null)
|
var startDragResult = await source.CanStartDragAsync();
|
||||||
|
if (!startDragResult.CanStart || startDragResult.DragInfo == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
dragInfo = startDragResult.DragInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatedDragInfo = dragInfo.CloneWithPosition(startPosition);
|
var updatedDragInfo = dragInfo.CloneWithPosition(startPosition);
|
||||||
@@ -289,7 +293,7 @@ public sealed class DragDropService : IDragDropService
|
|||||||
// Начало перетаскивания
|
// Начало перетаскивания
|
||||||
bool started;
|
bool started;
|
||||||
|
|
||||||
if (EnableAsyncOperations && source is Abstractions.IAsyncDragSource asyncSource2)
|
if (EnableAsyncOperations && source is Abstractions.IDragSource asyncSource2)
|
||||||
{
|
{
|
||||||
started = await ExecuteWithTimeoutAsync(
|
started = await ExecuteWithTimeoutAsync(
|
||||||
asyncSource2.StartDragAsync(updatedDragInfo),
|
asyncSource2.StartDragAsync(updatedDragInfo),
|
||||||
@@ -298,7 +302,7 @@ public sealed class DragDropService : IDragDropService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
started = source.StartDrag(updatedDragInfo);
|
started = await source.StartDragAsync(updatedDragInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!started)
|
if (!started)
|
||||||
@@ -355,8 +359,8 @@ public sealed class DragDropService : IDragDropService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var updatedDragInfo = context.DragInfo.CloneWithPosition(position);
|
var updatedDragInfo = context.DragInfo.CloneWithPosition(position);
|
||||||
context.DragInfo.Dispose();
|
|
||||||
context.DragInfo = updatedDragInfo;
|
context.DragInfo = updatedDragInfo;
|
||||||
|
context.DragInfo.Dispose();
|
||||||
|
|
||||||
// Поиск новой цели сброса
|
// Поиск новой цели сброса
|
||||||
var newDropTarget = await FindDropTargetAsync(position, updatedDragInfo);
|
var newDropTarget = await FindDropTargetAsync(position, updatedDragInfo);
|
||||||
@@ -369,7 +373,7 @@ public sealed class DragDropService : IDragDropService
|
|||||||
await ExecuteTargetOperationAsync(
|
await ExecuteTargetOperationAsync(
|
||||||
context.CurrentDropTarget,
|
context.CurrentDropTarget,
|
||||||
t => t.DragLeaveAsync(),
|
t => t.DragLeaveAsync(),
|
||||||
t => t.DragLeave(),
|
t => t.DragLeaveAsync(),
|
||||||
"DragLeave");
|
"DragLeave");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,7 +399,7 @@ public sealed class DragDropService : IDragDropService
|
|||||||
await ExecuteTargetOperationAsync(
|
await ExecuteTargetOperationAsync(
|
||||||
context.CurrentDropTarget,
|
context.CurrentDropTarget,
|
||||||
t => t.DragOverAsync(dropInfo),
|
t => t.DragOverAsync(dropInfo),
|
||||||
t => t.DragOver(dropInfo),
|
t => t.DragOverAsync(dropInfo),
|
||||||
"DragOver");
|
"DragOver");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,7 +447,7 @@ public sealed class DragDropService : IDragDropService
|
|||||||
await ExecuteTargetOperationAsync(
|
await ExecuteTargetOperationAsync(
|
||||||
context.CurrentDropTarget,
|
context.CurrentDropTarget,
|
||||||
t => t.DropAsync(dropInfo),
|
t => t.DropAsync(dropInfo),
|
||||||
t => t.Drop(dropInfo),
|
t => t.DropAsync(dropInfo),
|
||||||
"Drop");
|
"Drop");
|
||||||
|
|
||||||
if (dropInfo.Handled)
|
if (dropInfo.Handled)
|
||||||
@@ -457,7 +461,7 @@ public sealed class DragDropService : IDragDropService
|
|||||||
await ExecuteSourceOperationAsync(
|
await ExecuteSourceOperationAsync(
|
||||||
context.Source,
|
context.Source,
|
||||||
s => s.DragCompletedAsync(context.DragInfo, effects),
|
s => s.DragCompletedAsync(context.DragInfo, effects),
|
||||||
s => s.DragCompleted(context.DragInfo, effects),
|
s => s.DragCompletedAsync(context.DragInfo, effects),
|
||||||
"DragCompleted",
|
"DragCompleted",
|
||||||
effects);
|
effects);
|
||||||
|
|
||||||
@@ -504,7 +508,7 @@ public sealed class DragDropService : IDragDropService
|
|||||||
await ExecuteSourceOperationAsync(
|
await ExecuteSourceOperationAsync(
|
||||||
context.Source,
|
context.Source,
|
||||||
s => s.DragCancelledAsync(context.DragInfo),
|
s => s.DragCancelledAsync(context.DragInfo),
|
||||||
s => s.DragCancelled(context.DragInfo),
|
s => s.DragCancelledAsync(context.DragInfo),
|
||||||
"DragCancelled");
|
"DragCancelled");
|
||||||
|
|
||||||
_dragCancelled?.Invoke(this, new DragCancelledEventArgs(context.DragInfo));
|
_dragCancelled?.Invoke(this, new DragCancelledEventArgs(context.DragInfo));
|
||||||
@@ -522,30 +526,6 @@ public sealed class DragDropService : IDragDropService
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Synchronous Operations (for compatibility)
|
|
||||||
|
|
||||||
public bool StartDrag(Abstractions.IDragSource source, Geometry.Point startPosition)
|
|
||||||
{
|
|
||||||
return Task.Run(() => StartDragAsync(source, startPosition)).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateDrag(Geometry.Point position)
|
|
||||||
{
|
|
||||||
Task.Run(() => UpdateDragAsync(position)).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Enums.DragDropEffects EndDrag(Geometry.Point position)
|
|
||||||
{
|
|
||||||
return Task.Run(() => EndDragAsync(position)).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CancelDrag()
|
|
||||||
{
|
|
||||||
Task.Run(CancelDragAsync).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Utility Methods
|
#region Utility Methods
|
||||||
|
|
||||||
public void ClearAllDropTargets()
|
public void ClearAllDropTargets()
|
||||||
@@ -593,39 +573,30 @@ public sealed class DragDropService : IDragDropService
|
|||||||
_dropTargetsLock.EnterReadLock();
|
_dropTargetsLock.EnterReadLock();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (var info in _dropTargets.Values)
|
// Фильтруем цели по границам и сортируем по приоритету
|
||||||
{
|
var candidates = _dropTargets.Values
|
||||||
if (!info.Bounds.Contains(position))
|
.Where(info => info.Bounds.Contains(position))
|
||||||
continue;
|
.OrderByDescending(info => info.Priority)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var info in candidates)
|
||||||
|
{
|
||||||
var dropInfo = new Models.DropInfo(
|
var dropInfo = new Models.DropInfo(
|
||||||
dragInfo.Data,
|
dragInfo.Data,
|
||||||
position,
|
position,
|
||||||
dragInfo.AllowedEffects,
|
dragInfo.AllowedEffects,
|
||||||
info.Target);
|
info.Target);
|
||||||
|
|
||||||
bool canAccept;
|
bool canAccept = await ExecuteWithTimeoutAsync(
|
||||||
|
info.Target.CanAcceptDropAsync(dropInfo),
|
||||||
if (EnableAsyncOperations && info.Target is Abstractions.IAsyncDropTarget asyncTarget)
|
|
||||||
{
|
|
||||||
canAccept = await ExecuteWithTimeoutAsync(
|
|
||||||
asyncTarget.CanAcceptDropAsync(dropInfo),
|
|
||||||
"CanAcceptDropAsync",
|
"CanAcceptDropAsync",
|
||||||
info.Target);
|
info.Target);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
canAccept = info.Target.CanAcceptDrop(dropInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (canAccept)
|
if (canAccept)
|
||||||
{
|
{
|
||||||
info.LastAccessTime = DateTime.UtcNow;
|
info.LastAccessTime = DateTime.UtcNow;
|
||||||
|
|
||||||
if (bestTarget == null || info.Priority > bestTarget.Priority)
|
|
||||||
{
|
|
||||||
bestTarget = info;
|
bestTarget = info;
|
||||||
}
|
break; // Берем первую подходящую с наивысшим приоритетом
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -639,13 +610,13 @@ public sealed class DragDropService : IDragDropService
|
|||||||
|
|
||||||
private async Task ExecuteTargetOperationAsync(
|
private async Task ExecuteTargetOperationAsync(
|
||||||
Abstractions.IDropTarget target,
|
Abstractions.IDropTarget target,
|
||||||
Func<Abstractions.IAsyncDropTarget, Task> asyncOperation,
|
Func<Abstractions.IDropTarget, Task> asyncOperation,
|
||||||
Action<Abstractions.IDropTarget> syncOperation,
|
Action<Abstractions.IDropTarget> syncOperation,
|
||||||
string operationName)
|
string operationName)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (EnableAsyncOperations && target is Abstractions.IAsyncDropTarget asyncTarget)
|
if (EnableAsyncOperations && target is Abstractions.IDropTarget asyncTarget)
|
||||||
{
|
{
|
||||||
await ExecuteWithTimeoutAsync(
|
await ExecuteWithTimeoutAsync(
|
||||||
asyncOperation(asyncTarget),
|
asyncOperation(asyncTarget),
|
||||||
@@ -666,14 +637,14 @@ public sealed class DragDropService : IDragDropService
|
|||||||
|
|
||||||
private async Task ExecuteSourceOperationAsync(
|
private async Task ExecuteSourceOperationAsync(
|
||||||
Abstractions.IDragSource source,
|
Abstractions.IDragSource source,
|
||||||
Func<Abstractions.IAsyncDragSource, Task> asyncOperation,
|
Func<Abstractions.IDragSource, Task> asyncOperation,
|
||||||
Action<Abstractions.IDragSource> syncOperation,
|
Action<Abstractions.IDragSource> syncOperation,
|
||||||
string operationName,
|
string operationName,
|
||||||
Enums.DragDropEffects effects = Enums.DragDropEffects.None)
|
Enums.DragDropEffects effects = Enums.DragDropEffects.None)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (EnableAsyncOperations && source is Abstractions.IAsyncDragSource asyncSource)
|
if (EnableAsyncOperations && source is Abstractions.IDragSource asyncSource)
|
||||||
{
|
{
|
||||||
await ExecuteWithTimeoutAsync(
|
await ExecuteWithTimeoutAsync(
|
||||||
asyncOperation(asyncSource),
|
asyncOperation(asyncSource),
|
||||||
@@ -745,16 +716,17 @@ public sealed class DragDropService : IDragDropService
|
|||||||
|
|
||||||
private void CleanupExpiredTargets(object? state)
|
private void CleanupExpiredTargets(object? state)
|
||||||
{
|
{
|
||||||
var expirationTime = DateTime.UtcNow.AddMinutes(-10); // Цели старше 10 минут
|
var expirationTime = DateTime.UtcNow.AddMinutes(-Constants.DragDropConstants.TargetLifetimeMinutes);
|
||||||
|
|
||||||
_dropTargetsLock.EnterWriteLock();
|
_dropTargetsLock.EnterWriteLock();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var idsToRemove = new List<string>();
|
var idsToRemove = new List<string>();
|
||||||
|
var currentTarget = _currentDragOperation?.CurrentDropTarget;
|
||||||
|
|
||||||
foreach (var kvp in _dropTargets)
|
foreach (var kvp in _dropTargets)
|
||||||
{
|
{
|
||||||
if (kvp.Value.LastAccessTime < expirationTime)
|
if (kvp.Value.LastAccessTime < expirationTime && !ReferenceEquals(kvp.Value.Target, currentTarget))
|
||||||
{
|
{
|
||||||
idsToRemove.Add(kvp.Key);
|
idsToRemove.Add(kvp.Key);
|
||||||
}
|
}
|
||||||
@@ -797,6 +769,9 @@ public sealed class DragDropService : IDragDropService
|
|||||||
{
|
{
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
|
|
||||||
|
var timer = Interlocked.Exchange(ref _cleanupTimer, null);
|
||||||
|
timer?.Dispose();
|
||||||
|
|
||||||
_cleanupTimer?.Dispose();
|
_cleanupTimer?.Dispose();
|
||||||
_cleanupTimer = null;
|
_cleanupTimer = null;
|
||||||
|
|
||||||
|
|||||||
@@ -121,30 +121,6 @@ public interface IDragDropService : IDisposable
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Синхронные операции (для обратной совместимости)
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Начинает операцию перетаскивания (синхронно).
|
|
||||||
/// </summary>
|
|
||||||
bool StartDrag(Abstractions.IDragSource source, Geometry.Point startPosition);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Обновляет позицию перетаскивания (синхронно).
|
|
||||||
/// </summary>
|
|
||||||
void UpdateDrag(Geometry.Point position);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Завершает операцию перетаскивания (синхронно).
|
|
||||||
/// </summary>
|
|
||||||
Enums.DragDropEffects EndDrag(Geometry.Point position);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Отменяет операцию перетаскивания (синхронно).
|
|
||||||
/// </summary>
|
|
||||||
void CancelDrag();
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Утилиты
|
#region Утилиты
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public static class AsyncDragDropUtilities
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Создает асинхронную реализацию источника перетаскивания.
|
/// Создает асинхронную реализацию источника перетаскивания.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IAsyncDragSource CreateAsyncDragSource(
|
public static IDragSource CreateAsyncDragSource(
|
||||||
Func<Task<object>> dataProviderAsync,
|
Func<Task<object>> dataProviderAsync,
|
||||||
Func<Task<bool>>? canDragAsync = null,
|
Func<Task<bool>>? canDragAsync = null,
|
||||||
Func<DragInfo, DragDropEffects, Task>? onCompletedAsync = null,
|
Func<DragInfo, DragDropEffects, Task>? onCompletedAsync = null,
|
||||||
@@ -24,7 +24,7 @@ public static class AsyncDragDropUtilities
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Создает асинхронную реализацию цели сброса.
|
/// Создает асинхронную реализацию цели сброса.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IAsyncDropTarget CreateAsyncDropTarget(
|
public static IDropTarget CreateAsyncDropTarget(
|
||||||
Func<DropInfo, Task<bool>>? canAcceptAsync = null,
|
Func<DropInfo, Task<bool>>? canAcceptAsync = null,
|
||||||
Func<DropInfo, Task>? onDragOverAsync = null,
|
Func<DropInfo, Task>? onDragOverAsync = null,
|
||||||
Func<DropInfo, Task>? onDropAsync = null,
|
Func<DropInfo, Task>? onDropAsync = null,
|
||||||
@@ -33,28 +33,39 @@ public static class AsyncDragDropUtilities
|
|||||||
return new AsyncDropTargetWrapper(canAcceptAsync, onDragOverAsync, onDropAsync, onDragLeaveAsync);
|
return new AsyncDropTargetWrapper(canAcceptAsync, onDragOverAsync, onDropAsync, onDragLeaveAsync);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает адаптер для преобразования синхронного источника в асинхронный.
|
#region Factory Methods
|
||||||
/// </summary>
|
|
||||||
public static IAsyncDragSource CreateAsyncAdapter(IDragSource syncSource)
|
|
||||||
{
|
|
||||||
return new SyncToAsyncDragSourceAdapter(syncSource);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Создает адаптер для преобразования синхронной цели в асинхронную.
|
/// Создает информацию о перетаскивании.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IAsyncDropTarget CreateAsyncAdapter(IDropTarget syncTarget)
|
public static Models.DragInfo CreateDragInfo(
|
||||||
|
object data,
|
||||||
|
Geometry.Point startPosition,
|
||||||
|
Enums.DragDropEffects allowedEffects = Enums.DragDropEffects.Copy | Enums.DragDropEffects.Move,
|
||||||
|
object? source = null,
|
||||||
|
Dictionary<string, object>? parameters = null)
|
||||||
{
|
{
|
||||||
return new SyncToAsyncDropTargetAdapter(syncTarget);
|
var dragInfo = new Models.DragInfo(data, allowedEffects, startPosition, source);
|
||||||
|
|
||||||
|
if (parameters != null)
|
||||||
|
{
|
||||||
|
foreach (var param in parameters)
|
||||||
|
{
|
||||||
|
dragInfo.SetParameter(param.Key, param.Value);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dragInfo;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Обертки-реализации
|
#region Обертки-реализации
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Обертка для создания асинхронного источника перетаскивания.
|
/// Обертка для создания асинхронного источника перетаскивания.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private sealed class AsyncDragSourceWrapper : IAsyncDragSource
|
private sealed class AsyncDragSourceWrapper : IDragSource
|
||||||
{
|
{
|
||||||
private readonly Func<Task<object>> _dataProviderAsync;
|
private readonly Func<Task<object>> _dataProviderAsync;
|
||||||
private readonly Func<Task<bool>>? _canDragAsync;
|
private readonly Func<Task<bool>>? _canDragAsync;
|
||||||
@@ -73,17 +84,14 @@ public static class AsyncDragDropUtilities
|
|||||||
_onCancelledAsync = onCancelledAsync;
|
_onCancelledAsync = onCancelledAsync;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
|
public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Проверяем, может ли начаться перетаскивание
|
// Проверяем, может ли начаться перетаскивание
|
||||||
if (_canDragAsync != null)
|
var canDrag = _canDragAsync != null ? await _canDragAsync().ConfigureAwait(false) : true;
|
||||||
{
|
|
||||||
var canDrag = await _canDragAsync().ConfigureAwait(false);
|
|
||||||
if (!canDrag)
|
if (!canDrag)
|
||||||
return (false, null);
|
return (false, null);
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем данные
|
// Получаем данные
|
||||||
var data = await _dataProviderAsync().ConfigureAwait(false);
|
var data = await _dataProviderAsync().ConfigureAwait(false);
|
||||||
@@ -91,7 +99,7 @@ public static class AsyncDragDropUtilities
|
|||||||
return (false, null);
|
return (false, null);
|
||||||
|
|
||||||
// Создаем информацию о перетаскивании
|
// Создаем информацию о перетаскивании
|
||||||
var dragInfo = DragDropUtilities.CreateDragInfo(
|
var dragInfo = CreateDragInfo(
|
||||||
data,
|
data,
|
||||||
Geometry.Point.Zero,
|
Geometry.Point.Zero,
|
||||||
DragDropEffects.Copy | DragDropEffects.Move,
|
DragDropEffects.Copy | DragDropEffects.Move,
|
||||||
@@ -105,13 +113,13 @@ public static class AsyncDragDropUtilities
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<bool> StartDragAsync(DragInfo dragInfo)
|
public Task<bool> StartDragAsync(DragInfo dragInfo, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
// Базовая реализация всегда разрешает начало
|
// Базовая реализация всегда разрешает начало
|
||||||
return Task.FromResult(true);
|
return Task.FromResult(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects)
|
public async Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (_onCompletedAsync != null)
|
if (_onCompletedAsync != null)
|
||||||
{
|
{
|
||||||
@@ -119,46 +127,19 @@ public static class AsyncDragDropUtilities
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DragCancelledAsync(DragInfo dragInfo)
|
public async Task DragCancelledAsync(DragInfo dragInfo, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (_onCancelledAsync != null)
|
if (_onCancelledAsync != null)
|
||||||
{
|
{
|
||||||
await _onCancelledAsync(dragInfo).ConfigureAwait(false);
|
await _onCancelledAsync(dragInfo).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Синхронная реализация (для IDragSource)
|
|
||||||
|
|
||||||
public bool CanStartDrag(out DragInfo? dragInfo)
|
|
||||||
{
|
|
||||||
// Для синхронного вызова используем Task.Result
|
|
||||||
var result = Task.Run(() => CanStartDragAsync()).GetAwaiter().GetResult();
|
|
||||||
dragInfo = result.DragInfo;
|
|
||||||
return result.CanStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool StartDrag(DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
return Task.Run(() => StartDragAsync(dragInfo)).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DragCompleted(DragInfo dragInfo, DragDropEffects effects)
|
|
||||||
{
|
|
||||||
Task.Run(() => DragCompletedAsync(dragInfo, effects)).Wait();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DragCancelled(DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
Task.Run(() => DragCancelledAsync(dragInfo)).Wait();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Обертка для создания асинхронной цели сброса.
|
/// Обертка для создания асинхронной цели сброса.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private sealed class AsyncDropTargetWrapper : IAsyncDropTarget
|
private sealed class AsyncDropTargetWrapper : IDropTarget
|
||||||
{
|
{
|
||||||
private readonly Func<DropInfo, Task<bool>>? _canAcceptAsync;
|
private readonly Func<DropInfo, Task<bool>>? _canAcceptAsync;
|
||||||
private readonly Func<DropInfo, Task>? _onDragOverAsync;
|
private readonly Func<DropInfo, Task>? _onDragOverAsync;
|
||||||
@@ -177,7 +158,7 @@ public static class AsyncDragDropUtilities
|
|||||||
_onDragLeaveAsync = onDragLeaveAsync;
|
_onDragLeaveAsync = onDragLeaveAsync;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> CanAcceptDropAsync(DropInfo dropInfo)
|
public async Task<bool> CanAcceptDropAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -193,7 +174,7 @@ public static class AsyncDragDropUtilities
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DragOverAsync(DropInfo dropInfo)
|
public async Task DragOverAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -208,7 +189,7 @@ public static class AsyncDragDropUtilities
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DropAsync(DropInfo dropInfo)
|
public async Task DropAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -223,7 +204,7 @@ public static class AsyncDragDropUtilities
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DragLeaveAsync()
|
public async Task DragLeaveAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -237,477 +218,8 @@ public static class AsyncDragDropUtilities
|
|||||||
// Игнорируем ошибки в обработчике
|
// Игнорируем ошибки в обработчике
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Синхронная реализация (для IDropTarget)
|
|
||||||
|
|
||||||
public bool CanAcceptDrop(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
return Task.Run(() => CanAcceptDropAsync(dropInfo)).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DragOver(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
Task.Run(() => DragOverAsync(dropInfo)).Wait();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Drop(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
Task.Run(() => DropAsync(dropInfo)).Wait();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DragLeave()
|
|
||||||
{
|
|
||||||
Task.Run(DragLeaveAsync).Wait();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Адаптер для преобразования синхронного источника в асинхронный.
|
|
||||||
/// </summary>
|
|
||||||
private sealed class SyncToAsyncDragSourceAdapter : IAsyncDragSource
|
|
||||||
{
|
|
||||||
private readonly IDragSource _syncSource;
|
|
||||||
|
|
||||||
public SyncToAsyncDragSourceAdapter(IDragSource syncSource)
|
|
||||||
{
|
|
||||||
_syncSource = syncSource ?? throw new ArgumentNullException(nameof(syncSource));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
|
|
||||||
{
|
|
||||||
return await Task.Run(() =>
|
|
||||||
{
|
|
||||||
var canStart = _syncSource.CanStartDrag(out var dragInfo);
|
|
||||||
return (canStart, dragInfo);
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> StartDragAsync(DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
return await Task.Run(() => _syncSource.StartDrag(dragInfo)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects)
|
|
||||||
{
|
|
||||||
await Task.Run(() => _syncSource.DragCompleted(dragInfo, effects)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DragCancelledAsync(DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
await Task.Run(() => _syncSource.DragCancelled(dragInfo)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Синхронная реализация (делегируем синхронному источнику)
|
|
||||||
|
|
||||||
public bool CanStartDrag(out DragInfo? dragInfo)
|
|
||||||
{
|
|
||||||
return _syncSource.CanStartDrag(out dragInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool StartDrag(DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
return _syncSource.StartDrag(dragInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DragCompleted(DragInfo dragInfo, DragDropEffects effects)
|
|
||||||
{
|
|
||||||
_syncSource.DragCompleted(dragInfo, effects);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DragCancelled(DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
_syncSource.DragCancelled(dragInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Адаптер для преобразования синхронной цели в асинхронную.
|
|
||||||
/// </summary>
|
|
||||||
private sealed class SyncToAsyncDropTargetAdapter : IAsyncDropTarget
|
|
||||||
{
|
|
||||||
private readonly IDropTarget _syncTarget;
|
|
||||||
|
|
||||||
public SyncToAsyncDropTargetAdapter(IDropTarget syncTarget)
|
|
||||||
{
|
|
||||||
_syncTarget = syncTarget ?? throw new ArgumentNullException(nameof(syncTarget));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> CanAcceptDropAsync(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
return await Task.Run(() => _syncTarget.CanAcceptDrop(dropInfo)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DragOverAsync(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
await Task.Run(() => _syncTarget.DragOver(dropInfo)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DropAsync(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
await Task.Run(() => _syncTarget.Drop(dropInfo)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DragLeaveAsync()
|
|
||||||
{
|
|
||||||
await Task.Run(() => _syncTarget.DragLeave()).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Синхронная реализация (делегируем синхронной цели)
|
|
||||||
|
|
||||||
public bool CanAcceptDrop(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
return _syncTarget.CanAcceptDrop(dropInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DragOver(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
_syncTarget.DragOver(dropInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Drop(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
_syncTarget.Drop(dropInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DragLeave()
|
|
||||||
{
|
|
||||||
_syncTarget.DragLeave();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Утилитарные методы
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Выполняет асинхронную операцию с таймаутом.
|
|
||||||
/// </summary>
|
|
||||||
public static async Task<T> ExecuteWithTimeoutAsync<T>(
|
|
||||||
Task<T> task,
|
|
||||||
TimeSpan timeout,
|
|
||||||
T defaultValue = default!)
|
|
||||||
{
|
|
||||||
if (timeout <= TimeSpan.Zero)
|
|
||||||
return await task.ConfigureAwait(false);
|
|
||||||
|
|
||||||
var delayTask = Task.Delay(timeout);
|
|
||||||
var completedTask = await Task.WhenAny(task, delayTask).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (completedTask == delayTask)
|
|
||||||
{
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await task.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Выполняет асинхронную операцию с таймаутом.
|
|
||||||
/// </summary>
|
|
||||||
public static async Task<bool> ExecuteWithTimeoutAsync(
|
|
||||||
Task task,
|
|
||||||
TimeSpan timeout)
|
|
||||||
{
|
|
||||||
if (timeout <= TimeSpan.Zero)
|
|
||||||
{
|
|
||||||
await task.ConfigureAwait(false);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var delayTask = Task.Delay(timeout);
|
|
||||||
var completedTask = await Task.WhenAny(task, delayTask).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return completedTask == task;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Выполняет асинхронную операцию с таймаутом и обработкой ошибок.
|
|
||||||
/// </summary>
|
|
||||||
public static async Task<T?> ExecuteSafeWithTimeoutAsync<T>(
|
|
||||||
Task<T> task,
|
|
||||||
TimeSpan timeout,
|
|
||||||
Func<Exception, T?> errorHandler = null) where T : class
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (timeout <= TimeSpan.Zero)
|
|
||||||
return await task.ConfigureAwait(false);
|
|
||||||
|
|
||||||
var delayTask = Task.Delay(timeout);
|
|
||||||
var completedTask = await Task.WhenAny(task, delayTask).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (completedTask == delayTask)
|
|
||||||
{
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await task.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return errorHandler?.Invoke(ex) ?? default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает комбинированный источник из синхронного и асинхронного.
|
|
||||||
/// </summary>
|
|
||||||
public static IAsyncDragSource Combine(
|
|
||||||
IDragSource syncSource,
|
|
||||||
IAsyncDragSource asyncSource,
|
|
||||||
bool preferAsync = true)
|
|
||||||
{
|
|
||||||
return new CombinedDragSource(syncSource, asyncSource, preferAsync);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает комбинированную цель из синхронной и асинхронной.
|
|
||||||
/// </summary>
|
|
||||||
public static IAsyncDropTarget Combine(
|
|
||||||
IDropTarget syncTarget,
|
|
||||||
IAsyncDropTarget asyncTarget,
|
|
||||||
bool preferAsync = true)
|
|
||||||
{
|
|
||||||
return new CombinedDropTarget(syncTarget, asyncTarget, preferAsync);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Комбинированные реализации
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Комбинированный источник, поддерживающий как синхронный, так и асинхронный API.
|
|
||||||
/// </summary>
|
|
||||||
private sealed class CombinedDragSource : IAsyncDragSource
|
|
||||||
{
|
|
||||||
private readonly IDragSource _syncSource;
|
|
||||||
private readonly IAsyncDragSource _asyncSource;
|
|
||||||
private readonly bool _preferAsync;
|
|
||||||
|
|
||||||
public CombinedDragSource(IDragSource syncSource, IAsyncDragSource asyncSource, bool preferAsync)
|
|
||||||
{
|
|
||||||
_syncSource = syncSource ?? throw new ArgumentNullException(nameof(syncSource));
|
|
||||||
_asyncSource = asyncSource ?? throw new ArgumentNullException(nameof(asyncSource));
|
|
||||||
_preferAsync = preferAsync;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
|
|
||||||
{
|
|
||||||
if (_preferAsync)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await _asyncSource.CanStartDragAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// В случае ошибки пробуем синхронную версию
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Используем синхронную версию в отдельной задаче
|
|
||||||
return await Task.Run(() =>
|
|
||||||
{
|
|
||||||
var canStart = _syncSource.CanStartDrag(out var dragInfo);
|
|
||||||
return (canStart, dragInfo);
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> StartDragAsync(DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
if (_preferAsync)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await _asyncSource.StartDragAsync(dragInfo).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// В случае ошибки пробуем синхронную версию
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await Task.Run(() => _syncSource.StartDrag(dragInfo)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects)
|
|
||||||
{
|
|
||||||
if (_preferAsync)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _asyncSource.DragCompletedAsync(dragInfo, effects).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// В случае ошибки пробуем синхронную версию
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Run(() => _syncSource.DragCompleted(dragInfo, effects)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DragCancelledAsync(DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
if (_preferAsync)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _asyncSource.DragCancelledAsync(dragInfo).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// В случае ошибки пробуем синхронную версию
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Run(() => _syncSource.DragCancelled(dragInfo)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Синхронная реализация
|
|
||||||
|
|
||||||
public bool CanStartDrag(out DragInfo? dragInfo)
|
|
||||||
{
|
|
||||||
return _syncSource.CanStartDrag(out dragInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool StartDrag(DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
return _syncSource.StartDrag(dragInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DragCompleted(DragInfo dragInfo, DragDropEffects effects)
|
|
||||||
{
|
|
||||||
_syncSource.DragCompleted(dragInfo, effects);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DragCancelled(DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
_syncSource.DragCancelled(dragInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Комбинированная цель, поддерживающая как синхронный, так и асинхронный API.
|
|
||||||
/// </summary>
|
|
||||||
private sealed class CombinedDropTarget : IAsyncDropTarget
|
|
||||||
{
|
|
||||||
private readonly IDropTarget _syncTarget;
|
|
||||||
private readonly IAsyncDropTarget _asyncTarget;
|
|
||||||
private readonly bool _preferAsync;
|
|
||||||
|
|
||||||
public CombinedDropTarget(IDropTarget syncTarget, IAsyncDropTarget asyncTarget, bool preferAsync)
|
|
||||||
{
|
|
||||||
_syncTarget = syncTarget ?? throw new ArgumentNullException(nameof(syncTarget));
|
|
||||||
_asyncTarget = asyncTarget ?? throw new ArgumentNullException(nameof(asyncTarget));
|
|
||||||
_preferAsync = preferAsync;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> CanAcceptDropAsync(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
if (_preferAsync)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await _asyncTarget.CanAcceptDropAsync(dropInfo).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// В случае ошибки пробуем синхронную версию
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await Task.Run(() => _syncTarget.CanAcceptDrop(dropInfo)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DragOverAsync(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
if (_preferAsync)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _asyncTarget.DragOverAsync(dropInfo).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// В случае ошибки пробуем синхронную версию
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Run(() => _syncTarget.DragOver(dropInfo)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DropAsync(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
if (_preferAsync)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _asyncTarget.DropAsync(dropInfo).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// В случае ошибки пробуем синхронную версию
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Run(() => _syncTarget.Drop(dropInfo)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DragLeaveAsync()
|
|
||||||
{
|
|
||||||
if (_preferAsync)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _asyncTarget.DragLeaveAsync().ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// В случае ошибки пробуем синхронную версию
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Run(() => _syncTarget.DragLeave()).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Синхронная реализация
|
|
||||||
|
|
||||||
public bool CanAcceptDrop(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
return _syncTarget.CanAcceptDrop(dropInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DragOver(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
_syncTarget.DragOver(dropInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Drop(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
_syncTarget.Drop(dropInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DragLeave()
|
|
||||||
{
|
|
||||||
_syncTarget.DragLeave();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
namespace Lattice.Core.DragDrop.Utilities;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Утилиты для работы с системой перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static class DragDropUtilities
|
|
||||||
{
|
|
||||||
#region Effect Utilities
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Проверяет, совместимы ли эффекты источника и цели.
|
|
||||||
/// </summary>
|
|
||||||
public static bool AreEffectsCompatible(Enums.DragDropEffects sourceEffects, Enums.DragDropEffects targetEffects)
|
|
||||||
{
|
|
||||||
if (sourceEffects == Enums.DragDropEffects.None || targetEffects == Enums.DragDropEffects.None)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return (sourceEffects & targetEffects) != Enums.DragDropEffects.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает наиболее подходящий эффект на основе доступных.
|
|
||||||
/// </summary>
|
|
||||||
public static Enums.DragDropEffects GetBestEffect(Enums.DragDropEffects available, Enums.DragDropEffects preferred)
|
|
||||||
{
|
|
||||||
if ((available & preferred) != Enums.DragDropEffects.None)
|
|
||||||
return available & preferred;
|
|
||||||
|
|
||||||
if ((available & Enums.DragDropEffects.Move) != Enums.DragDropEffects.None)
|
|
||||||
return Enums.DragDropEffects.Move;
|
|
||||||
|
|
||||||
if ((available & Enums.DragDropEffects.Copy) != Enums.DragDropEffects.None)
|
|
||||||
return Enums.DragDropEffects.Copy;
|
|
||||||
|
|
||||||
if ((available & Enums.DragDropEffects.Link) != Enums.DragDropEffects.None)
|
|
||||||
return Enums.DragDropEffects.Link;
|
|
||||||
|
|
||||||
return Enums.DragDropEffects.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Geometry Utilities
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вычисляет расстояние между двумя точками.
|
|
||||||
/// </summary>
|
|
||||||
public static double CalculateDistance(Geometry.Point p1, Geometry.Point p2)
|
|
||||||
{
|
|
||||||
var dx = p2.X - p1.X;
|
|
||||||
var dy = p2.Y - p1.Y;
|
|
||||||
return Math.Sqrt(dx * dx + dy * dy);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Проверяет, превысило ли перемещение пороговое значение.
|
|
||||||
/// </summary>
|
|
||||||
public static bool HasExceededDragThreshold(Geometry.Point startPoint, Geometry.Point currentPoint, double threshold)
|
|
||||||
{
|
|
||||||
var distance = CalculateDistance(startPoint, currentPoint);
|
|
||||||
return distance >= threshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Определяет позицию сброса относительно прямоугольника.
|
|
||||||
/// </summary>
|
|
||||||
public static Enums.DropPosition GetDropPosition(Geometry.Point point, Geometry.Rect bounds, double edgeThreshold = 20.0)
|
|
||||||
{
|
|
||||||
if (!bounds.Contains(new Geometry.Point(point.X, point.Y)))
|
|
||||||
return Enums.DropPosition.Inside;
|
|
||||||
|
|
||||||
var relativeX = (point.X - bounds.X) / bounds.Width;
|
|
||||||
var relativeY = (point.Y - bounds.Y) / bounds.Height;
|
|
||||||
|
|
||||||
if (relativeX < edgeThreshold / bounds.Width)
|
|
||||||
return Enums.DropPosition.Left;
|
|
||||||
if (relativeX > 1 - edgeThreshold / bounds.Width)
|
|
||||||
return Enums.DropPosition.Right;
|
|
||||||
if (relativeY < edgeThreshold / bounds.Height)
|
|
||||||
return Enums.DropPosition.Top;
|
|
||||||
if (relativeY > 1 - edgeThreshold / bounds.Height)
|
|
||||||
return Enums.DropPosition.Bottom;
|
|
||||||
|
|
||||||
return Enums.DropPosition.Center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Factory Methods
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает информацию о перетаскивании.
|
|
||||||
/// </summary>
|
|
||||||
public static Models.DragInfo CreateDragInfo(
|
|
||||||
object data,
|
|
||||||
Geometry.Point startPosition,
|
|
||||||
Enums.DragDropEffects allowedEffects = Enums.DragDropEffects.Copy | Enums.DragDropEffects.Move,
|
|
||||||
object? source = null,
|
|
||||||
Dictionary<string, object>? parameters = null)
|
|
||||||
{
|
|
||||||
var dragInfo = new Models.DragInfo(data, allowedEffects, startPosition, source);
|
|
||||||
|
|
||||||
if (parameters != null)
|
|
||||||
{
|
|
||||||
foreach (var param in parameters)
|
|
||||||
{
|
|
||||||
dragInfo.SetParameter(param.Key, param.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dragInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает простую реализацию источника перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static Abstractions.IDragSource CreateSimpleDragSource(
|
|
||||||
Func<object> dataProvider,
|
|
||||||
Func<bool>? canDrag = null,
|
|
||||||
Action<Models.DragInfo, Enums.DragDropEffects>? onCompleted = null,
|
|
||||||
Action<Models.DragInfo>? onCancelled = null)
|
|
||||||
{
|
|
||||||
return new SimpleDragSource(dataProvider, canDrag, onCompleted, onCancelled);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает простую реализацию цели сброса.
|
|
||||||
/// </summary>
|
|
||||||
public static Abstractions.IDropTarget CreateSimpleDropTarget(
|
|
||||||
Func<Models.DropInfo, bool>? canAccept = null,
|
|
||||||
Action<Models.DropInfo>? onDragOver = null,
|
|
||||||
Action<Models.DropInfo>? onDrop = null,
|
|
||||||
Action? onDragLeave = null)
|
|
||||||
{
|
|
||||||
return new SimpleDropTarget(canAccept, onDragOver, onDrop, onDragLeave);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Data Extraction
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Извлекает данные из элемента с поддержкой различных паттернов.
|
|
||||||
/// </summary>
|
|
||||||
public static object? ExtractData(object? element)
|
|
||||||
{
|
|
||||||
if (element == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
// Проверяем, реализует ли элемент специальный интерфейс
|
|
||||||
if (element is Abstractions.IDragSource dragSource)
|
|
||||||
{
|
|
||||||
if (dragSource.CanStartDrag(out var dragInfo) && dragInfo != null)
|
|
||||||
return dragInfo.Data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// В реальной реализации здесь будет рефлексия для проверки свойств
|
|
||||||
// DataContext, Content и т.д.
|
|
||||||
|
|
||||||
// Возвращаем сам элемент как данные
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Проверяет, совместимы ли данные с указанными типами.
|
|
||||||
/// </summary>
|
|
||||||
public static bool IsDataCompatible(object? data, IEnumerable<Type>? acceptedTypes)
|
|
||||||
{
|
|
||||||
if (data == null || acceptedTypes == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var dataType = data.GetType();
|
|
||||||
|
|
||||||
foreach (var acceptedType in acceptedTypes)
|
|
||||||
{
|
|
||||||
if (acceptedType.IsAssignableFrom(dataType))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Helper Classes
|
|
||||||
|
|
||||||
private sealed class SimpleDragSource : Abstractions.IDragSource
|
|
||||||
{
|
|
||||||
private readonly Func<object> _dataProvider;
|
|
||||||
private readonly Func<bool>? _canDrag;
|
|
||||||
private readonly Action<Models.DragInfo, Enums.DragDropEffects>? _onCompleted;
|
|
||||||
private readonly Action<Models.DragInfo>? _onCancelled;
|
|
||||||
|
|
||||||
public SimpleDragSource(
|
|
||||||
Func<object> dataProvider,
|
|
||||||
Func<bool>? canDrag = null,
|
|
||||||
Action<Models.DragInfo, Enums.DragDropEffects>? onCompleted = null,
|
|
||||||
Action<Models.DragInfo>? onCancelled = null)
|
|
||||||
{
|
|
||||||
_dataProvider = dataProvider ?? throw new ArgumentNullException(nameof(dataProvider));
|
|
||||||
_canDrag = canDrag;
|
|
||||||
_onCompleted = onCompleted;
|
|
||||||
_onCancelled = onCancelled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanStartDrag(out Models.DragInfo? dragInfo)
|
|
||||||
{
|
|
||||||
dragInfo = null;
|
|
||||||
|
|
||||||
if (_canDrag?.Invoke() == false)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var data = _dataProvider();
|
|
||||||
if (data == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
dragInfo = CreateDragInfo(data, Geometry.Point.Zero, Enums.DragDropEffects.Copy | Enums.DragDropEffects.Move, this);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool StartDrag(Models.DragInfo dragInfo) => true;
|
|
||||||
|
|
||||||
public void DragCompleted(Models.DragInfo dragInfo, Enums.DragDropEffects effects)
|
|
||||||
{
|
|
||||||
_onCompleted?.Invoke(dragInfo, effects);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DragCancelled(Models.DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
_onCancelled?.Invoke(dragInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class SimpleDropTarget : Abstractions.IDropTarget
|
|
||||||
{
|
|
||||||
private readonly Func<Models.DropInfo, bool>? _canAccept;
|
|
||||||
private readonly Action<Models.DropInfo>? _onDragOver;
|
|
||||||
private readonly Action<Models.DropInfo>? _onDrop;
|
|
||||||
private readonly Action? _onDragLeave;
|
|
||||||
|
|
||||||
public SimpleDropTarget(
|
|
||||||
Func<Models.DropInfo, bool>? canAccept = null,
|
|
||||||
Action<Models.DropInfo>? onDragOver = null,
|
|
||||||
Action<Models.DropInfo>? onDrop = null,
|
|
||||||
Action? onDragLeave = null)
|
|
||||||
{
|
|
||||||
_canAccept = canAccept;
|
|
||||||
_onDragOver = onDragOver;
|
|
||||||
_onDrop = onDrop;
|
|
||||||
_onDragLeave = onDragLeave;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanAcceptDrop(Models.DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
return _canAccept?.Invoke(dropInfo) ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DragOver(Models.DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
_onDragOver?.Invoke(dropInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Drop(Models.DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
_onDrop?.Invoke(dropInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DragLeave()
|
|
||||||
{
|
|
||||||
_onDragLeave?.Invoke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
49
Lattice.UI.DragDrop.WinUI/Behaviors/DragSource.cs
Normal file
49
Lattice.UI.DragDrop.WinUI/Behaviors/DragSource.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
|
||||||
|
namespace Lattice.UI.DragDrop.WinUI.Behaviors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attached properties для DragSource.
|
||||||
|
/// </summary>
|
||||||
|
public static class DragSource
|
||||||
|
{
|
||||||
|
public static readonly DependencyProperty IsEnabledProperty =
|
||||||
|
DependencyProperty.RegisterAttached(
|
||||||
|
"IsEnabled",
|
||||||
|
typeof(bool),
|
||||||
|
typeof(DragSource),
|
||||||
|
new PropertyMetadata(false, OnIsEnabledChanged));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty DragDataProperty =
|
||||||
|
DependencyProperty.RegisterAttached(
|
||||||
|
"DragData",
|
||||||
|
typeof(object),
|
||||||
|
typeof(DragSource),
|
||||||
|
new PropertyMetadata(null));
|
||||||
|
|
||||||
|
public static bool GetIsEnabled(UIElement element) =>
|
||||||
|
(bool)element.GetValue(IsEnabledProperty);
|
||||||
|
|
||||||
|
public static void SetIsEnabled(UIElement element, bool value) =>
|
||||||
|
element.SetValue(IsEnabledProperty, value);
|
||||||
|
|
||||||
|
public static object GetDragData(UIElement element) =>
|
||||||
|
element.GetValue(DragDataProperty);
|
||||||
|
|
||||||
|
public static void SetDragData(UIElement element, object value) =>
|
||||||
|
element.SetValue(DragDataProperty, value);
|
||||||
|
|
||||||
|
private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (d is not UIElement element) return;
|
||||||
|
|
||||||
|
// TODO: Здесь нужно создать экземпляр WinUIDragSourceBehavior
|
||||||
|
// и прикрепить его к элементу через DI
|
||||||
|
// Пока что устанавливаем данные в Tag
|
||||||
|
if ((bool)e.NewValue)
|
||||||
|
{
|
||||||
|
var data = GetDragData(element);
|
||||||
|
element.Tag = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,58 +1,31 @@
|
|||||||
using Lattice.Core.Geometry;
|
using Lattice.Core.DragDrop.Models;
|
||||||
|
using Lattice.Core.Geometry;
|
||||||
|
using Lattice.UI.DragDrop.Behaviors;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Microsoft.UI.Xaml.Input;
|
using Microsoft.UI.Xaml.Input;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Lattice.UI.DragDrop.WinUI.Behaviors;
|
namespace Lattice.UI.DragDrop.WinUI.Behaviors;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Поведение источника перетаскивания для WinUI UIElement.
|
/// Поведение источника перетаскивания для WinUI FrameworkElement.
|
||||||
/// Исправленная версия с правильной очисткой ресурсов.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class WinUIDragSourceBehavior
|
public class WinUIDragSourceBehavior : DragSourceBehaviorBase<FrameworkElement>
|
||||||
{
|
{
|
||||||
#region Вложенные типы
|
|
||||||
|
|
||||||
private sealed class DragSourceContext : IDisposable
|
|
||||||
{
|
|
||||||
public Point DragStartPosition { get; set; }
|
|
||||||
public bool IsDragging { get; set; }
|
|
||||||
public UIElement? CurrentDragElement { get; set; }
|
|
||||||
public object? DragData { get; set; }
|
|
||||||
public Action<UIElement>? DragStartedHandler { get; set; }
|
|
||||||
public Action<UIElement, Core.DragDrop.Enums.DragDropEffects>? DragCompletedHandler { get; set; }
|
|
||||||
public Core.DragDrop.Enums.DragDropEffects AllowedEffects { get; set; }
|
|
||||||
= Core.DragDrop.Enums.DragDropEffects.Copy | Core.DragDrop.Enums.DragDropEffects.Move;
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
DragStartedHandler = null;
|
|
||||||
DragCompletedHandler = null;
|
|
||||||
DragData = null;
|
|
||||||
CurrentDragElement = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Прикрепленные свойства
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Идентификатор свойства для привязки данных перетаскивания.
|
/// Прикрепленное свойство для данных перетаскивания.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly DependencyProperty DragDataProperty =
|
public static readonly DependencyProperty DragDataProperty =
|
||||||
DependencyProperty.RegisterAttached(
|
DependencyProperty.RegisterAttached(
|
||||||
"DragData",
|
"DragData",
|
||||||
typeof(object),
|
typeof(object),
|
||||||
typeof(WinUIDragSourceBehavior),
|
typeof(WinUIDragSourceBehavior),
|
||||||
new PropertyMetadata(null, OnDragDataChanged));
|
new PropertyMetadata(null));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Идентификатор свойства для включения перетаскивания.
|
/// Прикрепленное свойство для включения перетаскивания.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly DependencyProperty IsEnabledProperty =
|
public static readonly DependencyProperty IsEnabledProperty =
|
||||||
DependencyProperty.RegisterAttached(
|
DependencyProperty.RegisterAttached(
|
||||||
@@ -62,368 +35,256 @@ public static class WinUIDragSourceBehavior
|
|||||||
new PropertyMetadata(false, OnIsEnabledChanged));
|
new PropertyMetadata(false, OnIsEnabledChanged));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Идентификатор свойства для разрешенных эффектов перетаскивания.
|
/// Получает значение DragData.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly DependencyProperty AllowedEffectsProperty =
|
public static object GetDragData(FrameworkElement element)
|
||||||
DependencyProperty.RegisterAttached(
|
{
|
||||||
"AllowedEffects",
|
return element.GetValue(DragDataProperty);
|
||||||
typeof(Core.DragDrop.Enums.DragDropEffects),
|
}
|
||||||
typeof(WinUIDragSourceBehavior),
|
|
||||||
new PropertyMetadata(Core.DragDrop.Enums.DragDropEffects.Copy | Core.DragDrop.Enums.DragDropEffects.Move));
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Идентификатор свойства для обработчика начала перетаскивания.
|
/// Устанавливает значение DragData.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly DependencyProperty DragStartedHandlerProperty =
|
public static void SetDragData(FrameworkElement element, object value)
|
||||||
DependencyProperty.RegisterAttached(
|
{
|
||||||
"DragStartedHandler",
|
|
||||||
typeof(Action<UIElement>),
|
|
||||||
typeof(WinUIDragSourceBehavior),
|
|
||||||
new PropertyMetadata(null));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Идентификатор свойства для обработчика завершения перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DependencyProperty DragCompletedHandlerProperty =
|
|
||||||
DependencyProperty.RegisterAttached(
|
|
||||||
"DragCompletedHandler",
|
|
||||||
typeof(Action<UIElement, Core.DragDrop.Enums.DragDropEffects>),
|
|
||||||
typeof(WinUIDragSourceBehavior),
|
|
||||||
new PropertyMetadata(null));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Идентификатор свойства для обработчика создания данных перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DependencyProperty DragDataFactoryProperty =
|
|
||||||
DependencyProperty.RegisterAttached(
|
|
||||||
"DragDataFactory",
|
|
||||||
typeof(Func<UIElement, Task<object>>),
|
|
||||||
typeof(WinUIDragSourceBehavior),
|
|
||||||
new PropertyMetadata(null));
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Статические поля
|
|
||||||
|
|
||||||
private static readonly ConditionalWeakTable<UIElement, DragSourceContext> _contexts = new();
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Методы доступа к свойствам
|
|
||||||
|
|
||||||
/// <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);
|
element.SetValue(DragDataProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает значение свойства IsEnabled для указанного элемента.
|
/// Получает значение IsEnabled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool GetIsEnabled(UIElement element) =>
|
public static bool GetIsEnabled(FrameworkElement element)
|
||||||
(bool)element.GetValue(IsEnabledProperty);
|
{
|
||||||
|
return (bool)element.GetValue(IsEnabledProperty);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Устанавливает значение свойства IsEnabled для указанного элемента.
|
/// Устанавливает значение IsEnabled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void SetIsEnabled(UIElement element, bool value) =>
|
public static void SetIsEnabled(FrameworkElement element, bool value)
|
||||||
|
{
|
||||||
element.SetValue(IsEnabledProperty, value);
|
element.SetValue(IsEnabledProperty, value);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает значение свойства AllowedEffects для указанного элемента.
|
|
||||||
/// </summary>
|
|
||||||
public static Core.DragDrop.Enums.DragDropEffects GetAllowedEffects(UIElement element) =>
|
|
||||||
(Core.DragDrop.Enums.DragDropEffects)element.GetValue(AllowedEffectsProperty);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Устанавливает значение свойства AllowedEffects для указанного элемента.
|
|
||||||
/// </summary>
|
|
||||||
public static void SetAllowedEffects(UIElement element, Core.DragDrop.Enums.DragDropEffects value) =>
|
|
||||||
element.SetValue(AllowedEffectsProperty, value);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает обработчик начала перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static Action<UIElement> GetDragStartedHandler(UIElement element) =>
|
|
||||||
(Action<UIElement>)element.GetValue(DragStartedHandlerProperty);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Устанавливает обработчик начала перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static void SetDragStartedHandler(UIElement element, Action<UIElement> value) =>
|
|
||||||
element.SetValue(DragStartedHandlerProperty, value);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает обработчик завершения перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static Action<UIElement, Core.DragDrop.Enums.DragDropEffects> GetDragCompletedHandler(UIElement element) =>
|
|
||||||
(Action<UIElement, Core.DragDrop.Enums.DragDropEffects>)element.GetValue(DragCompletedHandlerProperty);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Устанавливает обработчик завершения перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static void SetDragCompletedHandler(UIElement element, Action<UIElement, Core.DragDrop.Enums.DragDropEffects> value) =>
|
|
||||||
element.SetValue(DragCompletedHandlerProperty, value);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает фабрику данных перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static Func<UIElement, Task<object>> GetDragDataFactory(UIElement element) =>
|
|
||||||
(Func<UIElement, Task<object>>)element.GetValue(DragDataFactoryProperty);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Устанавливает фабрику данных перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static void SetDragDataFactory(UIElement element, Func<UIElement, Task<object>> value) =>
|
|
||||||
element.SetValue(DragDataFactoryProperty, value);
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Обработчики изменений свойств
|
|
||||||
|
|
||||||
private static void OnDragDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (d is UIElement element)
|
|
||||||
{
|
|
||||||
if (_contexts.TryGetValue(element, out var context))
|
|
||||||
{
|
|
||||||
context.DragData = e.NewValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (d is UIElement element)
|
if (d is FrameworkElement element)
|
||||||
{
|
{
|
||||||
|
// Получаем или создаем экземпляр поведения через Attached Property
|
||||||
|
var behavior = GetBehavior(element);
|
||||||
|
|
||||||
if ((bool)e.NewValue)
|
if ((bool)e.NewValue)
|
||||||
{
|
{
|
||||||
EnableDrag(element);
|
if (behavior == null)
|
||||||
|
{
|
||||||
|
behavior = new WinUIDragSourceBehavior();
|
||||||
|
SetBehavior(element, behavior);
|
||||||
|
}
|
||||||
|
|
||||||
|
behavior.AssociatedElement = element;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
DisableDrag(element);
|
if (behavior != null)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Включение/выключение перетаскивания
|
|
||||||
|
|
||||||
private static void EnableDrag(UIElement element)
|
|
||||||
{
|
{
|
||||||
if (_contexts.TryGetValue(element, out _))
|
behavior.Detach();
|
||||||
return;
|
SetBehavior(element, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var context = new DragSourceContext
|
public WinUIDragSourceBehavior()
|
||||||
|
: base(ServiceProviderHelper.GetServiceProvider())
|
||||||
{
|
{
|
||||||
DragData = GetDragData(element),
|
}
|
||||||
DragStartedHandler = GetDragStartedHandler(element),
|
|
||||||
DragCompletedHandler = GetDragCompletedHandler(element),
|
|
||||||
AllowedEffects = GetAllowedEffects(element)
|
|
||||||
};
|
|
||||||
|
|
||||||
_contexts.Add(element, context);
|
|
||||||
|
|
||||||
|
protected override void SubscribeToEvents(FrameworkElement element)
|
||||||
|
{
|
||||||
element.PointerPressed += OnPointerPressed;
|
element.PointerPressed += OnPointerPressed;
|
||||||
element.PointerMoved += OnPointerMoved;
|
element.PointerMoved += OnPointerMoved;
|
||||||
element.PointerReleased += OnPointerReleased;
|
element.PointerReleased += OnPointerReleased;
|
||||||
element.PointerCanceled += OnPointerCanceled;
|
element.PointerCanceled += OnPointerCanceled;
|
||||||
element.PointerCaptureLost += OnPointerCaptureLost;
|
element.PointerCaptureLost += OnPointerCaptureLost;
|
||||||
element.Unloaded += OnElementUnloaded;
|
element.LostFocus += OnLostFocus;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DisableDrag(UIElement element)
|
protected override void UnsubscribeFromEvents(FrameworkElement element)
|
||||||
{
|
{
|
||||||
element.PointerPressed -= OnPointerPressed;
|
element.PointerPressed -= OnPointerPressed;
|
||||||
element.PointerMoved -= OnPointerMoved;
|
element.PointerMoved -= OnPointerMoved;
|
||||||
element.PointerReleased -= OnPointerReleased;
|
element.PointerReleased -= OnPointerReleased;
|
||||||
element.PointerCanceled -= OnPointerCanceled;
|
element.PointerCanceled -= OnPointerCanceled;
|
||||||
element.PointerCaptureLost -= OnPointerCaptureLost;
|
element.PointerCaptureLost -= OnPointerCaptureLost;
|
||||||
element.Unloaded -= OnElementUnloaded;
|
element.LostFocus -= OnLostFocus;
|
||||||
|
|
||||||
if (_contexts.TryGetValue(element, out var context))
|
|
||||||
{
|
|
||||||
context.Dispose();
|
|
||||||
_contexts.Remove(element);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
private void OnPointerPressed(object sender, PointerRoutedEventArgs e)
|
||||||
|
|
||||||
#region Обработчики событий
|
|
||||||
|
|
||||||
private static void OnPointerPressed(object sender, PointerRoutedEventArgs e)
|
|
||||||
{
|
{
|
||||||
if (sender is UIElement element && _contexts.TryGetValue(element, out var context))
|
if (AssociatedElement == null) return;
|
||||||
{
|
|
||||||
var point = e.GetCurrentPoint(element);
|
var point = e.GetCurrentPoint(AssociatedElement);
|
||||||
context.DragStartPosition = new Point(point.Position.X, point.Position.Y);
|
OnInteractionStarted(new Point(point.Position.X, point.Position.Y));
|
||||||
context.CurrentDragElement = element;
|
|
||||||
element.CapturePointer(e.Pointer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async void OnPointerMoved(object sender, PointerRoutedEventArgs e)
|
private void OnPointerMoved(object sender, PointerRoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is UIElement element && _contexts.TryGetValue(element, out var context))
|
if (AssociatedElement == null) return;
|
||||||
{
|
|
||||||
if (context.IsDragging || context.CurrentDragElement == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var currentPoint = e.GetCurrentPoint(context.CurrentDragElement);
|
var point = e.GetCurrentPoint(AssociatedElement);
|
||||||
var currentPosition = new Point(currentPoint.Position.X, currentPoint.Position.Y);
|
OnInteractionMoved(new Point(point.Position.X, point.Position.Y));
|
||||||
|
|
||||||
var distance = CalculateDistance(context.DragStartPosition, currentPosition);
|
|
||||||
if (distance > 3.0) // Порог перетаскивания
|
|
||||||
{
|
|
||||||
context.IsDragging = true;
|
|
||||||
await StartDragAsync(context.CurrentDragElement, currentPosition, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task StartDragAsync(UIElement element, Point currentPosition, DragSourceContext context)
|
private void OnPointerReleased(object sender, PointerRoutedEventArgs e)
|
||||||
{
|
{
|
||||||
try
|
OnInteractionEnded();
|
||||||
{
|
|
||||||
object? data = context.DragData;
|
|
||||||
|
|
||||||
// Если есть фабрика данных, используем ее
|
|
||||||
var factory = GetDragDataFactory(element);
|
|
||||||
if (factory != null)
|
|
||||||
{
|
|
||||||
data = await factory(element);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data != null)
|
private void OnPointerCanceled(object sender, PointerRoutedEventArgs e)
|
||||||
{
|
{
|
||||||
context.DragData = data;
|
OnInteractionCancelled();
|
||||||
|
|
||||||
// Вызываем обработчик начала перетаскивания
|
|
||||||
context.DragStartedHandler?.Invoke(element);
|
|
||||||
|
|
||||||
// Устанавливаем визуальный эффект
|
|
||||||
SetDragVisualState(element, true);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch (Exception ex)
|
private void OnPointerCaptureLost(object sender, PointerRoutedEventArgs e)
|
||||||
{
|
{
|
||||||
System.Diagnostics.Debug.WriteLine($"Error starting drag: {ex.Message}");
|
OnInteractionCancelled();
|
||||||
context.IsDragging = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnPointerReleased(object sender, PointerRoutedEventArgs e)
|
private void OnLostFocus(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
CompleteDrag(sender as UIElement, Core.DragDrop.Enums.DragDropEffects.Copy);
|
OnInteractionCancelled();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnPointerCanceled(object sender, PointerRoutedEventArgs e)
|
protected override Point ConvertToScreenCoordinates(Point point)
|
||||||
{
|
{
|
||||||
CompleteDrag(sender as UIElement, Core.DragDrop.Enums.DragDropEffects.None);
|
if (AssociatedElement == null)
|
||||||
|
return point;
|
||||||
|
|
||||||
|
var transform = AssociatedElement.TransformToVisual(null);
|
||||||
|
var screenPoint = transform.TransformPoint(new Windows.Foundation.Point(point.X, point.Y));
|
||||||
|
return new Point(screenPoint.X, screenPoint.Y);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnPointerCaptureLost(object sender, PointerRoutedEventArgs e)
|
/// <inheritdoc/>
|
||||||
|
public override async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
|
||||||
{
|
{
|
||||||
CompleteDrag(sender as UIElement, Core.DragDrop.Enums.DragDropEffects.None);
|
if (AssociatedElement == null)
|
||||||
|
{
|
||||||
|
return (false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnElementUnloaded(object sender, RoutedEventArgs e)
|
var data = GetDragData(AssociatedElement);
|
||||||
|
if (data == null)
|
||||||
{
|
{
|
||||||
if (sender is UIElement element)
|
// Пробуем получить данные из Tag или других источников
|
||||||
{
|
data = AssociatedElement.Tag ?? AssociatedElement.DataContext;
|
||||||
DisableDrag(element);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
if (data == null)
|
||||||
|
|
||||||
#region Вспомогательные методы
|
|
||||||
|
|
||||||
private static void CompleteDrag(UIElement? element, Core.DragDrop.Enums.DragDropEffects effects)
|
|
||||||
{
|
{
|
||||||
if (element != null && _contexts.TryGetValue(element, out var context))
|
return (false, null);
|
||||||
{
|
|
||||||
// Вызываем обработчик завершения перетаскивания
|
|
||||||
context.DragCompletedHandler?.Invoke(element, effects);
|
|
||||||
|
|
||||||
// Сбрасываем визуальный эффект
|
|
||||||
SetDragVisualState(element, false);
|
|
||||||
|
|
||||||
element.ReleasePointerCaptures();
|
|
||||||
|
|
||||||
ResetDragState(context);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SetDragVisualState(UIElement element, bool isDragging)
|
// Получаем начальную позицию в экранных координатах
|
||||||
|
var startPosition = ConvertToScreenCoordinates(_dragStartPosition);
|
||||||
|
|
||||||
|
// Создаем DragInfo с учетом вашего конструктора
|
||||||
|
var dragInfo = new DragInfo(
|
||||||
|
data: data,
|
||||||
|
allowedEffects: Core.DragDrop.Enums.DragDropEffects.Move |
|
||||||
|
Core.DragDrop.Enums.DragDropEffects.Copy,
|
||||||
|
startPosition: startPosition,
|
||||||
|
source: this
|
||||||
|
);
|
||||||
|
|
||||||
|
return (true, dragInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDragCompleted(DragInfo dragInfo, Core.DragDrop.Enums.DragDropEffects effects)
|
||||||
|
{
|
||||||
|
base.OnDragCompleted(dragInfo, effects);
|
||||||
|
|
||||||
|
// Визуальная обратная связь при завершении
|
||||||
|
SetVisualState(AssociatedElement, "Normal");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDragCancelled(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
base.OnDragCancelled(dragInfo);
|
||||||
|
|
||||||
|
// Визуальная обратная связь при отмене
|
||||||
|
SetVisualState(AssociatedElement, "Normal");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetVisualState(FrameworkElement? element, string stateName)
|
||||||
{
|
{
|
||||||
// Для элементов, которые являются Control, используем VisualStateManager
|
|
||||||
if (element is Control control)
|
if (element is Control control)
|
||||||
{
|
{
|
||||||
// Пытаемся перейти к состоянию "Dragging" или "Normal"
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
VisualStateManager.GoToState(control, isDragging ? "Dragging" : "Normal", true);
|
VisualStateManager.GoToState(control, stateName, true);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// Если состояния не определены, меняем свойства напрямую
|
// Fallback
|
||||||
control.Opacity = isDragging ? 0.7 : 1.0;
|
control.Opacity = 1.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (element != null)
|
||||||
|
{
|
||||||
|
// Альтернативная визуальная обратная связь для не-Control элементов
|
||||||
|
element.Opacity = 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attached property для хранения экземпляра поведения
|
||||||
|
private static readonly DependencyProperty BehaviorProperty =
|
||||||
|
DependencyProperty.RegisterAttached(
|
||||||
|
"Behavior",
|
||||||
|
typeof(WinUIDragSourceBehavior),
|
||||||
|
typeof(WinUIDragSourceBehavior),
|
||||||
|
new PropertyMetadata(null));
|
||||||
|
|
||||||
|
private static WinUIDragSourceBehavior GetBehavior(FrameworkElement element)
|
||||||
|
{
|
||||||
|
return (WinUIDragSourceBehavior)element.GetValue(BehaviorProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetBehavior(FrameworkElement element, WinUIDragSourceBehavior? value)
|
||||||
|
{
|
||||||
|
element.SetValue(BehaviorProperty, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вспомогательный класс для получения IServiceProvider.
|
||||||
|
/// </summary>
|
||||||
|
internal static class ServiceProviderHelper
|
||||||
|
{
|
||||||
|
private static IServiceProvider? _serviceProvider;
|
||||||
|
|
||||||
|
public static IServiceProvider GetServiceProvider()
|
||||||
|
{
|
||||||
|
if (_serviceProvider == null)
|
||||||
|
{
|
||||||
|
// Ищем IServiceProvider в Application.Current.Resources
|
||||||
|
if (Application.Current.Resources.TryGetValue("ServiceProvider", out var provider) &&
|
||||||
|
provider is IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Для обычных UIElement меняем свойства напрямую
|
throw new InvalidOperationException(
|
||||||
element.Opacity = isDragging ? 0.7 : 1.0;
|
"IServiceProvider не найден. Убедитесь, что ServiceProvider зарегистрирован в ресурсах приложения.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double CalculateDistance(Point p1, Point p2)
|
return _serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetServiceProvider(IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
var dx = p2.X - p1.X;
|
_serviceProvider = serviceProvider;
|
||||||
var dy = p2.Y - p1.Y;
|
|
||||||
return Math.Sqrt(dx * dx + dy * dy);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ResetDragState(DragSourceContext context)
|
|
||||||
{
|
|
||||||
context.IsDragging = false;
|
|
||||||
context.CurrentDragElement = null;
|
|
||||||
context.DragStartPosition = default;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Методы очистки
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Очищает все ресурсы, связанные с поведением перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static void Cleanup()
|
|
||||||
{
|
|
||||||
var elements = new List<UIElement>();
|
|
||||||
|
|
||||||
foreach (var kvp in _contexts)
|
|
||||||
{
|
|
||||||
elements.Add(kvp.Key);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var element in elements)
|
|
||||||
{
|
|
||||||
DisableDrag(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
_contexts.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
@@ -1,402 +1,345 @@
|
|||||||
using Microsoft.UI.Xaml;
|
using Lattice.Core.DragDrop.Models;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Lattice.Core.Geometry;
|
||||||
|
using Lattice.UI.DragDrop.Behaviors;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Lattice.UI.DragDrop.WinUI.Behaviors;
|
namespace Lattice.UI.DragDrop.WinUI.Behaviors
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Поведение цели сброса для WinUI UIElement.
|
|
||||||
/// </summary>
|
|
||||||
public static class WinUIDropTargetBehavior
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Идентификатор свойства для включения цели сброса.
|
/// Поведение цели сброса для элементов WinUI.
|
||||||
|
/// Позволяет элементам принимать данные при операции перетаскивания.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly DependencyProperty IsEnabledProperty =
|
/// <remarks>
|
||||||
DependencyProperty.RegisterAttached(
|
/// <para>
|
||||||
"IsEnabled",
|
/// Это поведение должно быть прикреплено к <see cref="FrameworkElement"/>, который должен выступать в качестве цели сброса.
|
||||||
typeof(bool),
|
/// Поведение автоматически регистрирует элемент в системе перетаскивания и обрабатывает все аспекты операции сброса.
|
||||||
typeof(WinUIDropTargetBehavior),
|
/// </para>
|
||||||
new PropertyMetadata(false, OnIsEnabledChanged));
|
/// <para>
|
||||||
|
/// Для использования необходимо:
|
||||||
/// <summary>
|
/// 1. Создать экземпляр поведения с помощью <see cref="Attach"/> или через DI.
|
||||||
/// Идентификатор свойства для фильтрации принимаемых данных.
|
/// 2. Переопределить методы <see cref="CanAcceptDrop"/> и <see cref="Drop"/> для реализации логики принятия данных.
|
||||||
/// </summary>
|
/// 3. При необходимости переопределить <see cref="DragOver"/> для настройки визуальной обратной связи.
|
||||||
public static readonly DependencyProperty AcceptsDataTypesProperty =
|
/// </para>
|
||||||
DependencyProperty.RegisterAttached(
|
/// </remarks>
|
||||||
"AcceptsDataTypes",
|
public class WinUIDropTargetBehavior : DropTargetBehaviorBase<FrameworkElement>
|
||||||
typeof(Type[]),
|
|
||||||
typeof(WinUIDropTargetBehavior),
|
|
||||||
new PropertyMetadata(null));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Идентификатор свойства для обработчика сброса.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DependencyProperty DropHandlerProperty =
|
|
||||||
DependencyProperty.RegisterAttached(
|
|
||||||
"DropHandler",
|
|
||||||
typeof(Core.DragDrop.Abstractions.IDropTarget),
|
|
||||||
typeof(WinUIDropTargetBehavior),
|
|
||||||
new PropertyMetadata(null));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Идентификатор свойства для отображения визуальной обратной связи.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DependencyProperty ShowVisualFeedbackProperty =
|
|
||||||
DependencyProperty.RegisterAttached(
|
|
||||||
"ShowVisualFeedback",
|
|
||||||
typeof(bool),
|
|
||||||
typeof(WinUIDropTargetBehavior),
|
|
||||||
new PropertyMetadata(true));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Идентификатор свойства для стиля визуальной обратной связи.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DependencyProperty FeedbackStyleProperty =
|
|
||||||
DependencyProperty.RegisterAttached(
|
|
||||||
"FeedbackStyle",
|
|
||||||
typeof(Style),
|
|
||||||
typeof(WinUIDropTargetBehavior),
|
|
||||||
new PropertyMetadata(null));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Идентификатор свойства для обработчика входа перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DependencyProperty DragEnterHandlerProperty =
|
|
||||||
DependencyProperty.RegisterAttached(
|
|
||||||
"DragEnterHandler",
|
|
||||||
typeof(Action<UIElement>),
|
|
||||||
typeof(WinUIDropTargetBehavior),
|
|
||||||
new PropertyMetadata(null));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Идентификатор свойства для обработчика выхода перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DependencyProperty DragLeaveHandlerProperty =
|
|
||||||
DependencyProperty.RegisterAttached(
|
|
||||||
"DragLeaveHandler",
|
|
||||||
typeof(Action<UIElement>),
|
|
||||||
typeof(WinUIDropTargetBehavior),
|
|
||||||
new PropertyMetadata(null));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Идентификатор свойства для обработчика сброса.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DependencyProperty DropHandlerActionProperty =
|
|
||||||
DependencyProperty.RegisterAttached(
|
|
||||||
"DropHandlerAction",
|
|
||||||
typeof(Action<UIElement, object>),
|
|
||||||
typeof(WinUIDropTargetBehavior),
|
|
||||||
new PropertyMetadata(null));
|
|
||||||
|
|
||||||
// Словарь для отслеживания текущих перетаскиваемых элементов
|
|
||||||
private static readonly Dictionary<UIElement, bool> _dragOverElements = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает значение свойства IsEnabled для указанного элемента.
|
|
||||||
/// </summary>
|
|
||||||
public static bool GetIsEnabled(UIElement element) =>
|
|
||||||
(bool)element.GetValue(IsEnabledProperty);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Устанавливает значение свойства IsEnabled для указанного элемента.
|
|
||||||
/// </summary>
|
|
||||||
public static void SetIsEnabled(UIElement element, bool value) =>
|
|
||||||
element.SetValue(IsEnabledProperty, value);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает значение свойства AcceptsDataTypes для указанного элемента.
|
|
||||||
/// </summary>
|
|
||||||
public static Type[] GetAcceptsDataTypes(UIElement element) =>
|
|
||||||
(Type[])element.GetValue(AcceptsDataTypesProperty);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Устанавливает значение свойства AcceptsDataTypes для указанного элемента.
|
|
||||||
/// </summary>
|
|
||||||
public static void SetAcceptsDataTypes(UIElement element, Type[] value) =>
|
|
||||||
element.SetValue(AcceptsDataTypesProperty, value);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает значение свойства DropHandler для указанного элемента.
|
|
||||||
/// </summary>
|
|
||||||
public static Core.DragDrop.Abstractions.IDropTarget GetDropHandler(UIElement element) =>
|
|
||||||
(Core.DragDrop.Abstractions.IDropTarget)element.GetValue(DropHandlerProperty);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Устанавливает значение свойства DropHandler для указанного элемента.
|
|
||||||
/// </summary>
|
|
||||||
public static void SetDropHandler(UIElement element, Core.DragDrop.Abstractions.IDropTarget value) =>
|
|
||||||
element.SetValue(DropHandlerProperty, value);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает значение свойства ShowVisualFeedback для указанного элемента.
|
|
||||||
/// </summary>
|
|
||||||
public static bool GetShowVisualFeedback(UIElement element) =>
|
|
||||||
(bool)element.GetValue(ShowVisualFeedbackProperty);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Устанавливает значение свойства ShowVisualFeedback для указанного элемента.
|
|
||||||
/// </summary>
|
|
||||||
public static void SetShowVisualFeedback(UIElement element, bool value) =>
|
|
||||||
element.SetValue(ShowVisualFeedbackProperty, value);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает значение свойства FeedbackStyle для указанного элемента.
|
|
||||||
/// </summary>
|
|
||||||
public static Style GetFeedbackStyle(UIElement element) =>
|
|
||||||
(Style)element.GetValue(FeedbackStyleProperty);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Устанавливает значение свойства FeedbackStyle для указанного элемента.
|
|
||||||
/// </summary>
|
|
||||||
public static void SetFeedbackStyle(UIElement element, Style value) =>
|
|
||||||
element.SetValue(FeedbackStyleProperty, value);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает обработчик входа перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static Action<UIElement> GetDragEnterHandler(UIElement element) =>
|
|
||||||
(Action<UIElement>)element.GetValue(DragEnterHandlerProperty);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Устанавливает обработчик входа перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static void SetDragEnterHandler(UIElement element, Action<UIElement> value) =>
|
|
||||||
element.SetValue(DragEnterHandlerProperty, value);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает обработчик выхода перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static Action<UIElement> GetDragLeaveHandler(UIElement element) =>
|
|
||||||
(Action<UIElement>)element.GetValue(DragLeaveHandlerProperty);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Устанавливает обработчик выхода перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static void SetDragLeaveHandler(UIElement element, Action<UIElement> value) =>
|
|
||||||
element.SetValue(DragLeaveHandlerProperty, value);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает обработчик сброса.
|
|
||||||
/// </summary>
|
|
||||||
public static Action<UIElement, object> GetDropHandlerAction(UIElement element) =>
|
|
||||||
(Action<UIElement, object>)element.GetValue(DropHandlerActionProperty);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Устанавливает обработчик сброса.
|
|
||||||
/// </summary>
|
|
||||||
public static void SetDropHandlerAction(UIElement element, Action<UIElement, object> value) =>
|
|
||||||
element.SetValue(DropHandlerActionProperty, value);
|
|
||||||
|
|
||||||
private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
||||||
{
|
{
|
||||||
if (d is UIElement element)
|
private static readonly ConcurrentDictionary<FrameworkElement, WinUIDropTargetBehavior> _attachedBehaviors = new();
|
||||||
|
private readonly WeakReference<FrameworkElement>? _weakElement;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="WinUIDropTargetBehavior"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serviceProvider">Провайдер сервисов.</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// Конструктор создает экземпляр поведения, но не прикрепляет его к элементу.
|
||||||
|
/// Для прикрепления используйте метод <see cref="Attach(FrameworkElement, IServiceProvider)"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public WinUIDropTargetBehavior(IServiceProvider serviceProvider)
|
||||||
|
: base(serviceProvider)
|
||||||
{
|
{
|
||||||
if ((bool)e.NewValue)
|
|
||||||
{
|
|
||||||
EnableDrop(element);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
DisableDrop(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void EnableDrop(UIElement element)
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="WinUIDropTargetBehavior"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serviceProvider">Провайдер сервисов.</param>
|
||||||
|
/// <param name="element">Элемент, к которому прикрепляется поведение.</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// Конструктор создает экземпляр поведения и сразу прикрепляет его к указанному элементу.
|
||||||
|
/// </remarks>
|
||||||
|
public WinUIDropTargetBehavior(IServiceProvider serviceProvider, FrameworkElement element)
|
||||||
|
: base(serviceProvider)
|
||||||
{
|
{
|
||||||
element.AllowDrop = true;
|
AssociatedElement = element ?? throw new ArgumentNullException(nameof(element));
|
||||||
element.DragEnter += OnDragEnter;
|
|
||||||
element.DragOver += OnDragOver;
|
|
||||||
element.DragLeave += OnDragLeave;
|
|
||||||
element.Drop += OnDrop;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DisableDrop(UIElement element)
|
/// <summary>
|
||||||
|
/// Прикрепляет поведение к указанному элементу.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="element">Элемент, к которому прикрепляется поведение.</param>
|
||||||
|
/// <param name="serviceProvider">Провайдер сервисов.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// Экземпляр поведения, прикрепленного к элементу. Если к элементу уже прикреплено поведение,
|
||||||
|
/// возвращает существующий экземпляр.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, когда <paramref name="element"/> или <paramref name="serviceProvider"/> равны null.
|
||||||
|
/// </exception>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Этот метод обеспечивает, что к каждому элементу прикреплен только один экземпляр поведения.
|
||||||
|
/// Если метод вызывается повторно для того же элемента, возвращается существующий экземпляр.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Прикрепленное поведение автоматически отслеживает изменения макета элемента и обновляет
|
||||||
|
/// его границы в системе перетаскивания.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public static WinUIDropTargetBehavior Attach(FrameworkElement element, IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
element.AllowDrop = false;
|
if (element == null) throw new ArgumentNullException(nameof(element));
|
||||||
element.DragEnter -= OnDragEnter;
|
if (serviceProvider == null) throw new ArgumentNullException(nameof(serviceProvider));
|
||||||
element.DragOver -= OnDragOver;
|
|
||||||
element.DragLeave -= OnDragLeave;
|
return _attachedBehaviors.GetOrAdd(element, key =>
|
||||||
element.Drop -= OnDrop;
|
{
|
||||||
|
var behavior = new WinUIDropTargetBehavior(serviceProvider, key);
|
||||||
|
|
||||||
|
// Подписка на события жизненного цикла элемента
|
||||||
|
key.Unloaded += OnElementUnloaded;
|
||||||
|
return behavior;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnDragEnter(object sender, DragEventArgs e)
|
/// <summary>
|
||||||
|
/// Открепляет поведение от указанного элемента.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="element">Элемент, от которого открепляется поведение.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// true, если поведение было успешно откреплено; false, если поведение не было прикреплено к элементу.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот метод освобождает все ресурсы, связанные с поведением, и отписывается от событий элемента.
|
||||||
|
/// После вызова этого метода элемент перестает быть целью сброса.
|
||||||
|
/// </remarks>
|
||||||
|
public static bool Detach(FrameworkElement element)
|
||||||
{
|
{
|
||||||
var element = sender as UIElement;
|
if (element == null) return false;
|
||||||
if (element == null) return;
|
|
||||||
|
|
||||||
// Проверяем, можно ли принять данные
|
if (_attachedBehaviors.TryRemove(element, out var behavior))
|
||||||
if (CanAcceptData(element, e.DataView))
|
|
||||||
{
|
{
|
||||||
e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Copy;
|
element.Unloaded -= OnElementUnloaded;
|
||||||
|
behavior.Detach();
|
||||||
// Визуальная обратная связь
|
|
||||||
ShowVisualFeedback(element, true);
|
|
||||||
|
|
||||||
// Вызываем пользовательский обработчик
|
|
||||||
GetDragEnterHandler(element)?.Invoke(element);
|
|
||||||
|
|
||||||
// Добавляем в словарь
|
|
||||||
_dragOverElements[element] = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void OnDragOver(object sender, DragEventArgs e)
|
|
||||||
{
|
|
||||||
var element = sender as UIElement;
|
|
||||||
if (element == null) return;
|
|
||||||
|
|
||||||
if (CanAcceptData(element, e.DataView))
|
|
||||||
{
|
|
||||||
e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Copy;
|
|
||||||
|
|
||||||
// Обновляем визуальную обратную связь на основе позиции
|
|
||||||
var position = e.GetPosition(element);
|
|
||||||
UpdateVisualFeedback(element, position);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void OnDragLeave(object sender, DragEventArgs e)
|
|
||||||
{
|
|
||||||
var element = sender as UIElement;
|
|
||||||
if (element == null) return;
|
|
||||||
|
|
||||||
// Скрываем визуальную обратную связь
|
|
||||||
ShowVisualFeedback(element, false);
|
|
||||||
|
|
||||||
// Вызываем пользовательский обработчик
|
|
||||||
GetDragLeaveHandler(element)?.Invoke(element);
|
|
||||||
|
|
||||||
// Удаляем из словаря
|
|
||||||
_dragOverElements.Remove(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void OnDrop(object sender, DragEventArgs e)
|
|
||||||
{
|
|
||||||
var element = sender as UIElement;
|
|
||||||
if (element == null) return;
|
|
||||||
|
|
||||||
// Скрываем визуальную обратную связь
|
|
||||||
ShowVisualFeedback(element, false);
|
|
||||||
|
|
||||||
// Получаем данные
|
|
||||||
var data = ExtractData(e.DataView);
|
|
||||||
|
|
||||||
// Получаем обработчик или используем встроенный
|
|
||||||
var handler = GetDropHandler(element);
|
|
||||||
if (handler != null)
|
|
||||||
{
|
|
||||||
// Создаем DropInfo для обработчика
|
|
||||||
var dropInfo = CreateDropInfo(element, e, data);
|
|
||||||
handler.Drop(dropInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Вызываем пользовательский обработчик
|
|
||||||
var actionHandler = GetDropHandlerAction(element);
|
|
||||||
if (actionHandler != null && data != null)
|
|
||||||
{
|
|
||||||
actionHandler.Invoke(element, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Удаляем из словаря
|
|
||||||
_dragOverElements.Remove(element);
|
|
||||||
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool CanAcceptData(UIElement element, Windows.ApplicationModel.DataTransfer.DataPackageView dataView)
|
|
||||||
{
|
|
||||||
// Проверяем фильтр типов данных
|
|
||||||
var acceptedTypes = GetAcceptsDataTypes(element);
|
|
||||||
if (acceptedTypes != null && acceptedTypes.Length > 0)
|
|
||||||
{
|
|
||||||
// В реальной реализации нужно проверять доступные форматы данных
|
|
||||||
// Здесь упрощенная проверка
|
|
||||||
return dataView.AvailableFormats.Count > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если нет фильтра, принимаем все
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object? ExtractData(Windows.ApplicationModel.DataTransfer.DataPackageView dataView)
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает поведение, прикрепленное к указанному элементу.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="element">Элемент, для которого требуется получить поведение.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// Экземпляр поведения, прикрепленного к элементу, или null, если поведение не прикреплено.
|
||||||
|
/// </returns>
|
||||||
|
public static WinUIDropTargetBehavior? GetAttachedBehavior(FrameworkElement element)
|
||||||
{
|
{
|
||||||
// Упрощенная реализация извлечения данных
|
_attachedBehaviors.TryGetValue(element, out var behavior);
|
||||||
// В реальном приложении нужно обрабатывать разные форматы данных
|
return behavior;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Подписывается на события элемента.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="element">Элемент, к которому прикрепляется поведение.</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Этот метод подписывается на следующие события:
|
||||||
|
/// </para>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><see cref="FrameworkElement.LayoutUpdated"/> - для отслеживания изменений макета</item>
|
||||||
|
/// <item><see cref="FrameworkElement.SizeChanged"/> - для отслеживания изменений размера</item>
|
||||||
|
/// <item><see cref="FrameworkElement.Loaded"/> - для инициализации при загрузке элемента</item>
|
||||||
|
/// </list>
|
||||||
|
/// <para>
|
||||||
|
/// Переопределите этот метод, чтобы добавить подписку на дополнительные события.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
protected override void SubscribeToEvents(FrameworkElement element)
|
||||||
|
{
|
||||||
|
if (element == null) return;
|
||||||
|
|
||||||
|
element.LayoutUpdated += OnLayoutUpdated;
|
||||||
|
element.SizeChanged += OnSizeChanged;
|
||||||
|
element.Loaded += OnLoaded;
|
||||||
|
|
||||||
|
// Если элемент уже загружен, сразу обновляем границы
|
||||||
|
if (element.IsLoaded)
|
||||||
|
{
|
||||||
|
UpdateBounds();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отписывается от событий элемента.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="element">Элемент, от которого отписывается поведение.</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот метод отписывается от всех событий, на которые подписался <see cref="SubscribeToEvents"/>.
|
||||||
|
/// </remarks>
|
||||||
|
protected override void UnsubscribeFromEvents(FrameworkElement element)
|
||||||
|
{
|
||||||
|
if (element == null) return;
|
||||||
|
|
||||||
|
element.LayoutUpdated -= OnLayoutUpdated;
|
||||||
|
element.SizeChanged -= OnSizeChanged;
|
||||||
|
element.Loaded -= OnLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает границы элемента в экранных координатах.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="element">Элемент, границы которого нужно получить.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// Прямоугольник, описывающий границы элемента в экранных координатах.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Метод использует преобразование координат через <see cref="UIElement.TransformToVisual"/>
|
||||||
|
/// для получения глобальных координат элемента.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Если элемент не прикреплен к визуальному дереву или его границы не могут быть вычислены,
|
||||||
|
/// возвращается пустой прямоугольник.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
protected override Rect GetScreenBounds(FrameworkElement element)
|
||||||
|
{
|
||||||
|
if (element == null || !element.IsLoaded)
|
||||||
|
return Rect.Empty;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (dataView.Contains(Windows.ApplicationModel.DataTransfer.StandardDataFormats.Text))
|
// Получаем корневой элемент окна
|
||||||
{
|
var rootVisual = element.XamlRoot?.Content as UIElement;
|
||||||
// В WinUI 3 нужно использовать async/await, но это упрощенный пример
|
if (rootVisual == null)
|
||||||
// В реальном коде нужно использовать async методы
|
return Rect.Empty;
|
||||||
return "Text data from drag";
|
|
||||||
}
|
// Преобразуем границы элемента в координаты корневого элемента
|
||||||
|
var transform = element.TransformToVisual(rootVisual);
|
||||||
|
var position = transform.TransformPoint(new Windows.Foundation.Point(0, 0));
|
||||||
|
|
||||||
|
return new Rect(
|
||||||
|
position.X,
|
||||||
|
position.Y,
|
||||||
|
element.ActualWidth,
|
||||||
|
element.ActualHeight);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// Игнорируем ошибки извлечения данных
|
// В случае ошибки возвращаем пустой прямоугольник
|
||||||
}
|
return Rect.Empty;
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ShowVisualFeedback(UIElement element, bool show)
|
|
||||||
{
|
|
||||||
if (!GetShowVisualFeedback(element)) return;
|
|
||||||
|
|
||||||
// Для элементов, которые являются Control, используем VisualStateManager
|
|
||||||
if (element is Control control)
|
|
||||||
{
|
|
||||||
// Пытаемся перейти к состоянию "DragOver" или "Normal"
|
|
||||||
try
|
|
||||||
{
|
|
||||||
VisualStateManager.GoToState(control, show ? "DragOver" : "Normal", true);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Если состояния не определены, меняем свойства напрямую
|
|
||||||
control.Background = show ?
|
|
||||||
new Microsoft.UI.Xaml.Media.SolidColorBrush(Windows.UI.Color.FromArgb(30, 0, 120, 215)) :
|
|
||||||
null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Для обычных UIElement меняем свойства напрямую
|
|
||||||
element.Opacity = show ? 0.8 : 1.0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UpdateVisualFeedback(UIElement element, Windows.Foundation.Point position)
|
/// <summary>
|
||||||
|
/// Определяет, может ли элемент принять сбрасываемые данные.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dropInfo">Информация о сбросе.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// true, если элемент может принять данные; в противном случае — false.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Этот метод является абстрактным и должен быть переопределен в производных классах
|
||||||
|
/// для реализации логики принятия данных.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Базовая реализация всегда возвращает false. Переопределите этот метод, чтобы определить,
|
||||||
|
/// какие типы данных может принимать ваш элемент и при каких условиях.
|
||||||
|
/// </para>
|
||||||
|
/// <example>
|
||||||
|
/// Пример реализации:
|
||||||
|
/// <code>
|
||||||
|
/// public override bool CanAcceptDrop(DropInfo dropInfo)
|
||||||
|
/// {
|
||||||
|
/// // Принимаем только строковые данные
|
||||||
|
/// return dropInfo.Data is string;
|
||||||
|
/// }
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
/// </remarks>
|
||||||
|
public override async Task<bool> CanAcceptDropAsync(DropInfo dropInfo)
|
||||||
{
|
{
|
||||||
// Можно добавить логику для различных зон сброса
|
// Базовая реализация - не принимает никакие данные.
|
||||||
// Например, подсветка разных частей элемента
|
// Переопределите этот метод в производных классах.
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Core.DragDrop.Models.DropInfo CreateDropInfo(UIElement element, DragEventArgs e, object? data)
|
/// <summary>
|
||||||
|
/// Обрабатывает сброс данных на элемент.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dropInfo">Информация о сбросе.</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Этот метод вызывается, когда пользователь отпускает кнопку мыши над элементом,
|
||||||
|
/// и данные должны быть приняты.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Базовая реализация ничего не делает. Переопределите этот метод, чтобы реализовать
|
||||||
|
/// логику обработки принятых данных.
|
||||||
|
/// </para>
|
||||||
|
/// <example>
|
||||||
|
/// Пример реализации:
|
||||||
|
/// <code>
|
||||||
|
/// public override void Drop(DropInfo dropInfo)
|
||||||
|
/// {
|
||||||
|
/// if (dropInfo.Data is string text)
|
||||||
|
/// {
|
||||||
|
/// // Обработка текстовых данных
|
||||||
|
/// AssociatedElement.SetValue(TextBlock.TextProperty, text);
|
||||||
|
/// dropInfo.MarkAsHandled();
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
/// </remarks>
|
||||||
|
public override async Task DropAsync(DropInfo dropInfo)
|
||||||
{
|
{
|
||||||
var position = e.GetPosition(element);
|
// Базовая реализация ничего не делает.
|
||||||
var screenPosition = element.TransformToVisual(null).TransformPoint(position);
|
// Переопределите этот метод в производных классах.
|
||||||
|
}
|
||||||
|
|
||||||
return new Core.DragDrop.Models.DropInfo(
|
/// <summary>
|
||||||
data: data,
|
/// Освобождает ресурсы, связанные с поведением.
|
||||||
position: new Core.Geometry.Point(screenPosition.X, screenPosition.Y),
|
/// </summary>
|
||||||
allowedEffects: Core.DragDrop.Enums.DragDropEffects.Copy | Core.DragDrop.Enums.DragDropEffects.Move,
|
/// <remarks>
|
||||||
target: GetDropHandler(element)
|
/// <para>
|
||||||
);
|
/// Этот метод отписывается от всех событий, отменяет регистрацию в сервисе перетаскивания
|
||||||
|
/// и очищает все ресурсы.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// После вызова этого метода поведение больше не может быть использовано.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public override void Detach()
|
||||||
|
{
|
||||||
|
if (AssociatedElement != null && _attachedBehaviors.TryGetValue(AssociatedElement, out _))
|
||||||
|
{
|
||||||
|
_attachedBehaviors.TryRemove(AssociatedElement, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
base.Detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Event Handlers
|
||||||
|
|
||||||
|
private void OnLayoutUpdated(object? sender, object e)
|
||||||
|
{
|
||||||
|
OnElementLayoutChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||||
|
{
|
||||||
|
OnElementLayoutChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
UpdateBounds();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnElementUnloaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is FrameworkElement element)
|
||||||
|
{
|
||||||
|
Detach(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,7 @@ public static class DragDropExtensions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Делает элемент источником перетаскивания с указанными данными.
|
/// Делает элемент источником перетаскивания с указанными данными.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void MakeDragSource(this UIElement element, object dragData)
|
public static void MakeDragSource(this FrameworkElement element, object dragData)
|
||||||
{
|
{
|
||||||
Behaviors.WinUIDragSourceBehavior.SetDragData(element, dragData);
|
Behaviors.WinUIDragSourceBehavior.SetDragData(element, dragData);
|
||||||
Behaviors.WinUIDragSourceBehavior.SetIsEnabled(element, true);
|
Behaviors.WinUIDragSourceBehavior.SetIsEnabled(element, true);
|
||||||
@@ -25,53 +25,33 @@ public static class DragDropExtensions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Делает элемент источником перетаскивания с фабрикой данных.
|
/// Делает элемент источником перетаскивания с фабрикой данных.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void MakeDragSource(this UIElement element, Func<object> dataFactory)
|
public static void MakeDragSource(this FrameworkElement element, Func<object> dataFactory)
|
||||||
{
|
{
|
||||||
element.MakeDragSource(dataFactory());
|
element.MakeDragSource(dataFactory());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Делает элемент источником перетаскивания с настраиваемыми параметрами.
|
|
||||||
/// </summary>
|
|
||||||
public static void MakeDragSource(this UIElement element,
|
|
||||||
object dragData,
|
|
||||||
Core.DragDrop.Enums.DragDropEffects allowedEffects,
|
|
||||||
Func<bool>? canDrag = null)
|
|
||||||
{
|
|
||||||
element.MakeDragSource(dragData);
|
|
||||||
element.SetAllowedEffects(allowedEffects);
|
|
||||||
|
|
||||||
if (canDrag != null)
|
|
||||||
{
|
|
||||||
// Можно добавить кастомную логику проверки
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Удаляет возможность перетаскивания с элемента.
|
/// Удаляет возможность перетаскивания с элемента.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void RemoveDragSource(this UIElement element)
|
public static void RemoveDragSource(this FrameworkElement element)
|
||||||
{
|
{
|
||||||
Behaviors.WinUIDragSourceBehavior.SetIsEnabled(element, false);
|
Behaviors.WinUIDragSourceBehavior.SetIsEnabled(element, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Устанавливает разрешенные эффекты перетаскивания для элемента.
|
/// Проверяет, является ли элемент источником перетаскивания.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void SetAllowedEffects(this UIElement element, Core.DragDrop.Enums.DragDropEffects effects)
|
public static bool IsDragSource(this FrameworkElement element)
|
||||||
{
|
{
|
||||||
Behaviors.WinUIDragSourceBehavior.SetAllowedEffects(element, effects);
|
return Behaviors.WinUIDragSourceBehavior.GetIsEnabled(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Устанавливает смещение для визуального элемента перетаскивания.
|
/// Получает данные перетаскивания из элемента.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void SetDragVisualOffset(this UIElement element, double offsetX, double offsetY)
|
public static object? GetDragData(this FrameworkElement element)
|
||||||
{
|
{
|
||||||
if (element is FrameworkElement frameworkElement)
|
return Behaviors.WinUIDragSourceBehavior.GetDragData(element);
|
||||||
{
|
|
||||||
frameworkElement.Tag = new Windows.Foundation.Point(offsetX, offsetY);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -81,80 +61,65 @@ public static class DragDropExtensions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Делает элемент целью сброса.
|
/// Делает элемент целью сброса.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void MakeDropTarget(this UIElement element)
|
public static void MakeDropTarget(this FrameworkElement element)
|
||||||
{
|
{
|
||||||
Behaviors.WinUIDropTargetBehavior.SetIsEnabled(element, true);
|
// Включаем AllowDrop для WinUI
|
||||||
|
element.AllowDrop = true;
|
||||||
|
element.SetValue(IsDropTargetProperty, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Делает элемент целью сброса с фильтром типов данных.
|
/// Делает элемент целью сброса с фильтром типов данных.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void MakeDropTarget(this UIElement element, params Type[] acceptedTypes)
|
public static void MakeDropTarget(this FrameworkElement element, params Type[] acceptedTypes)
|
||||||
{
|
{
|
||||||
Behaviors.WinUIDropTargetBehavior.SetAcceptsDataTypes(element, acceptedTypes);
|
element.SetValue(AcceptsDataTypesProperty, acceptedTypes);
|
||||||
Behaviors.WinUIDropTargetBehavior.SetIsEnabled(element, true);
|
element.MakeDropTarget();
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Делает элемент целью сброса с обработчиком.
|
|
||||||
/// </summary>
|
|
||||||
public static void MakeDropTarget(this UIElement element, Core.DragDrop.Abstractions.IDropTarget handler)
|
|
||||||
{
|
|
||||||
Behaviors.WinUIDropTargetBehavior.SetDropHandler(element, handler);
|
|
||||||
Behaviors.WinUIDropTargetBehavior.SetIsEnabled(element, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Делает элемент целью сброса с полной настройкой.
|
|
||||||
/// </summary>
|
|
||||||
public static void MakeDropTarget(this UIElement element,
|
|
||||||
Type[] acceptedTypes,
|
|
||||||
Core.DragDrop.Abstractions.IDropTarget? handler = null,
|
|
||||||
bool showVisualFeedback = true,
|
|
||||||
Style? feedbackStyle = null)
|
|
||||||
{
|
|
||||||
if (acceptedTypes != null && acceptedTypes.Length > 0)
|
|
||||||
{
|
|
||||||
Behaviors.WinUIDropTargetBehavior.SetAcceptsDataTypes(element, acceptedTypes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (handler != null)
|
|
||||||
{
|
|
||||||
Behaviors.WinUIDropTargetBehavior.SetDropHandler(element, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
Behaviors.WinUIDropTargetBehavior.SetShowVisualFeedback(element, showVisualFeedback);
|
|
||||||
|
|
||||||
if (feedbackStyle != null)
|
|
||||||
{
|
|
||||||
Behaviors.WinUIDropTargetBehavior.SetFeedbackStyle(element, feedbackStyle);
|
|
||||||
}
|
|
||||||
|
|
||||||
Behaviors.WinUIDropTargetBehavior.SetIsEnabled(element, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Удаляет возможность сброса с элемента.
|
/// Удаляет возможность сброса с элемента.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void RemoveDropTarget(this UIElement element)
|
public static void RemoveDropTarget(this FrameworkElement element)
|
||||||
{
|
{
|
||||||
Behaviors.WinUIDropTargetBehavior.SetIsEnabled(element, false);
|
element.AllowDrop = false;
|
||||||
|
element.SetValue(IsDropTargetProperty, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Устанавливает стиль визуальной обратной связи для цели сброса.
|
/// Проверяет, является ли элемент целью сброса.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void SetDropFeedbackStyle(this UIElement element, Style style)
|
public static bool IsDropTarget(this FrameworkElement element)
|
||||||
{
|
{
|
||||||
Behaviors.WinUIDropTargetBehavior.SetFeedbackStyle(element, style);
|
return (bool)element.GetValue(IsDropTargetProperty);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Включает или выключает визуальную обратную связь при сбросе.
|
/// Attached property для отметки цели сброса.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void SetDropVisualFeedback(this UIElement element, bool enabled)
|
public static readonly DependencyProperty IsDropTargetProperty =
|
||||||
|
DependencyProperty.RegisterAttached(
|
||||||
|
"IsDropTarget",
|
||||||
|
typeof(bool),
|
||||||
|
typeof(DragDropExtensions),
|
||||||
|
new PropertyMetadata(false));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attached property для фильтра типов данных.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly DependencyProperty AcceptsDataTypesProperty =
|
||||||
|
DependencyProperty.RegisterAttached(
|
||||||
|
"AcceptsDataTypes",
|
||||||
|
typeof(Type[]),
|
||||||
|
typeof(DragDropExtensions),
|
||||||
|
new PropertyMetadata(null));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает фильтр типов данных.
|
||||||
|
/// </summary>
|
||||||
|
public static Type[]? GetAcceptsDataTypes(this FrameworkElement element)
|
||||||
{
|
{
|
||||||
Behaviors.WinUIDropTargetBehavior.SetShowVisualFeedback(element, enabled);
|
return (Type[]?)element.GetValue(AcceptsDataTypesProperty);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -171,30 +136,15 @@ public static class DragDropExtensions
|
|||||||
{
|
{
|
||||||
control.Style = style;
|
control.Style = style;
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Применяет стиль цели сброса к элементу.
|
|
||||||
/// </summary>
|
|
||||||
public static void ApplyDropTargetStyle(this Control control)
|
|
||||||
{
|
{
|
||||||
var style = Application.Current.Resources["DropTargetStyle"] as Style;
|
// Fallback стиль
|
||||||
if (style != null)
|
var brush = Application.Current.Resources["SystemControlBackgroundAccentBrush"] as SolidColorBrush;
|
||||||
|
if (brush != null)
|
||||||
{
|
{
|
||||||
control.Style = style;
|
control.Background = brush;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Включает визуальную обратную связь для элемента при перетаскивании.
|
|
||||||
/// </summary>
|
|
||||||
public static void EnableDragVisualFeedback(this Control control)
|
|
||||||
{
|
|
||||||
control.Loaded += (sender, e) =>
|
|
||||||
{
|
|
||||||
// Убеждаемся, что у элемента есть визуальные состояния
|
|
||||||
EnsureVisualStates(control);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -208,22 +158,18 @@ public static class DragDropExtensions
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// Если состояние не найдено, используем альтернативные методы
|
// Fallback для элементов без визуальных состояний
|
||||||
switch (stateName)
|
switch (stateName)
|
||||||
{
|
{
|
||||||
case "Dragging":
|
case "Dragging":
|
||||||
control.Opacity = 0.7;
|
control.Opacity = 0.7;
|
||||||
control.RenderTransform = new ScaleTransform { ScaleX = 0.95, ScaleY = 0.95 };
|
|
||||||
break;
|
break;
|
||||||
case "DragOver":
|
case "DragOver":
|
||||||
control.Background = Application.Current.Resources["DragOverBackgroundBrush"] as Brush;
|
control.Background = new SolidColorBrush(Windows.UI.Color.FromArgb(50, 0, 120, 215));
|
||||||
control.BorderBrush = Application.Current.Resources["DragOverBorderBrush"] as Brush;
|
|
||||||
break;
|
break;
|
||||||
case "Normal":
|
case "Normal":
|
||||||
control.ClearValue(Control.OpacityProperty);
|
control.ClearValue(Control.OpacityProperty);
|
||||||
control.ClearValue(Control.RenderTransformProperty);
|
|
||||||
control.ClearValue(Control.BackgroundProperty);
|
control.ClearValue(Control.BackgroundProperty);
|
||||||
control.ClearValue(Control.BorderBrushProperty);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,17 +179,6 @@ public static class DragDropExtensions
|
|||||||
|
|
||||||
#region Advanced Configuration
|
#region Advanced Configuration
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Настраивает элемент для работы с сервисом перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static void ConfigureForDragDropService(this UIElement element,
|
|
||||||
Core.DragDrop.Services.IDragDropService service,
|
|
||||||
string? dropTargetGroup = null)
|
|
||||||
{
|
|
||||||
// Этот метод позволяет интегрировать элемент с централизованным сервисом
|
|
||||||
// В реальной реализации нужно регистрировать элемент в сервисе
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Создает контейнер с поддержкой перетаскивания для элементов.
|
/// Создает контейнер с поддержкой перетаскивания для элементов.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -260,8 +195,7 @@ public static class DragDropExtensions
|
|||||||
|
|
||||||
if (enableReordering)
|
if (enableReordering)
|
||||||
{
|
{
|
||||||
// Настраиваем контейнер для переупорядочивания элементов
|
container.MakeDropTarget(typeof(FrameworkElement));
|
||||||
container.MakeDropTarget(typeof(UIElement));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
@@ -270,9 +204,9 @@ public static class DragDropExtensions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Делает все дочерние элементы перетаскиваемыми.
|
/// Делает все дочерние элементы перетаскиваемыми.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void MakeChildrenDraggable(this Panel container, Func<UIElement, object> dataSelector)
|
public static void MakeChildrenDraggable(this Panel container, Func<FrameworkElement, object> dataSelector)
|
||||||
{
|
{
|
||||||
foreach (var child in container.Children.OfType<UIElement>())
|
foreach (var child in container.Children.OfType<FrameworkElement>())
|
||||||
{
|
{
|
||||||
var data = dataSelector(child);
|
var data = dataSelector(child);
|
||||||
child.MakeDragSource(data);
|
child.MakeDragSource(data);
|
||||||
@@ -280,48 +214,4 @@ public static class DragDropExtensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Utility Methods
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Проверяет, является ли элемент источником перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static bool IsDragSource(this UIElement element)
|
|
||||||
{
|
|
||||||
return Behaviors.WinUIDragSourceBehavior.GetIsEnabled(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Проверяет, является ли элемент целью сброса.
|
|
||||||
/// </summary>
|
|
||||||
public static bool IsDropTarget(this UIElement element)
|
|
||||||
{
|
|
||||||
return Behaviors.WinUIDropTargetBehavior.GetIsEnabled(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает данные перетаскивания из элемента.
|
|
||||||
/// </summary>
|
|
||||||
public static object? GetDragData(this UIElement element)
|
|
||||||
{
|
|
||||||
return Behaviors.WinUIDragSourceBehavior.GetDragData(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Helper Methods
|
|
||||||
|
|
||||||
private static void EnsureVisualStates(Control control)
|
|
||||||
{
|
|
||||||
// В WinUI 3 визуальные состояния должны быть определены в стиле элемента
|
|
||||||
// Этот метод проверяет, что у элемента есть базовые визуальные состояния
|
|
||||||
|
|
||||||
if (control.Template == null && control.Style == null)
|
|
||||||
{
|
|
||||||
// Применяем стандартный стиль, если у элемента нет своего
|
|
||||||
control.ApplyDragStyle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
|
||||||
|
namespace Lattice.UI.DragDrop.WinUI.Helpers;
|
||||||
|
|
||||||
|
public static class FrameworkElementExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получает фактические размеры FrameworkElement.
|
||||||
|
/// </summary>
|
||||||
|
public static Windows.Foundation.Size GetActualSize(this FrameworkElement element)
|
||||||
|
{
|
||||||
|
return new Windows.Foundation.Size(element.ActualWidth, element.ActualHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает границы элемента в экранных координатах.
|
||||||
|
/// </summary>
|
||||||
|
public static Windows.Foundation.Rect GetScreenBounds(this FrameworkElement element)
|
||||||
|
{
|
||||||
|
var transform = element.TransformToVisual(null);
|
||||||
|
var topLeft = transform.TransformPoint(new Windows.Foundation.Point(0, 0));
|
||||||
|
var bottomRight = transform.TransformPoint(new Windows.Foundation.Point(element.ActualWidth, element.ActualHeight));
|
||||||
|
|
||||||
|
return new Windows.Foundation.Rect(topLeft, bottomRight);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ using Lattice.Core.DragDrop.Models;
|
|||||||
using Lattice.Core.DragDrop.Services;
|
using Lattice.Core.DragDrop.Services;
|
||||||
using Lattice.Core.Geometry;
|
using Lattice.Core.Geometry;
|
||||||
using Lattice.UI.DragDrop.Abstractions;
|
using Lattice.UI.DragDrop.Abstractions;
|
||||||
|
using Lattice.UI.DragDrop.WinUI.Behaviors;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Input;
|
using Microsoft.UI.Xaml.Input;
|
||||||
using System;
|
using System;
|
||||||
@@ -14,25 +15,23 @@ namespace Lattice.UI.DragDrop.WinUI.Integration;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Сервис интеграции Drag & Drop с WinUI приложением.
|
/// Сервис интеграции Drag & Drop с WinUI приложением.
|
||||||
/// Исправленная версия с правильной обработкой событий и очисткой ресурсов.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class WinUIDragDropIntegrationService : IDisposable
|
public sealed class WinUIDragDropIntegrationService : IDisposable
|
||||||
{
|
{
|
||||||
#region Вложенные типы
|
#region Вложенные типы
|
||||||
|
|
||||||
private sealed class DragSourceAdapter : IDisposable
|
private sealed class DragSourceAdapter : IDragSource
|
||||||
{
|
{
|
||||||
private readonly UIElement _element;
|
private readonly FrameworkElement _element;
|
||||||
private readonly Func<Task<DragInfo>> _dragInfoFactory;
|
private readonly Func<DragInfo> _dragInfoFactory;
|
||||||
private readonly IDragDropService _dragDropService;
|
private readonly IDragDropService _dragDropService;
|
||||||
private Point _dragStartPosition;
|
private Point _dragStartPosition;
|
||||||
private bool _isDragging;
|
private bool _isDragging;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
private object? _dragData;
|
|
||||||
|
|
||||||
public DragSourceAdapter(
|
public DragSourceAdapter(
|
||||||
UIElement element,
|
FrameworkElement element,
|
||||||
Func<Task<DragInfo>> dragInfoFactory,
|
Func<DragInfo> dragInfoFactory,
|
||||||
IDragDropService dragDropService)
|
IDragDropService dragDropService)
|
||||||
{
|
{
|
||||||
_element = element ?? throw new ArgumentNullException(nameof(element));
|
_element = element ?? throw new ArgumentNullException(nameof(element));
|
||||||
@@ -42,24 +41,6 @@ public sealed class WinUIDragDropIntegrationService : IDisposable
|
|||||||
SubscribeToEvents();
|
SubscribeToEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
public DragSourceAdapter(
|
|
||||||
UIElement element,
|
|
||||||
object dragData,
|
|
||||||
IDragDropService dragDropService)
|
|
||||||
{
|
|
||||||
_element = element ?? throw new ArgumentNullException(nameof(element));
|
|
||||||
_dragData = dragData ?? throw new ArgumentNullException(nameof(dragData));
|
|
||||||
_dragDropService = dragDropService ?? throw new ArgumentNullException(nameof(dragDropService));
|
|
||||||
|
|
||||||
_dragInfoFactory = async () => new DragInfo(
|
|
||||||
_dragData,
|
|
||||||
Core.DragDrop.Enums.DragDropEffects.Copy | Core.DragDrop.Enums.DragDropEffects.Move,
|
|
||||||
Point.Zero,
|
|
||||||
_element);
|
|
||||||
|
|
||||||
SubscribeToEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SubscribeToEvents()
|
private void SubscribeToEvents()
|
||||||
{
|
{
|
||||||
_element.PointerPressed += OnPointerPressed;
|
_element.PointerPressed += OnPointerPressed;
|
||||||
@@ -86,7 +67,7 @@ public sealed class WinUIDragDropIntegrationService : IDisposable
|
|||||||
_dragStartPosition = new Point(point.Position.X, point.Position.Y);
|
_dragStartPosition = new Point(point.Position.X, point.Position.Y);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnPointerMoved(object sender, PointerRoutedEventArgs e)
|
private void OnPointerMoved(object sender, PointerRoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_isDragging || _disposed) return;
|
if (_isDragging || _disposed) return;
|
||||||
|
|
||||||
@@ -96,16 +77,24 @@ public sealed class WinUIDragDropIntegrationService : IDisposable
|
|||||||
var distance = CalculateDistance(_dragStartPosition, currentPosition);
|
var distance = CalculateDistance(_dragStartPosition, currentPosition);
|
||||||
if (distance > _dragDropService.DragStartThreshold)
|
if (distance > _dragDropService.DragStartThreshold)
|
||||||
{
|
{
|
||||||
await StartDragAsync(currentPosition);
|
StartDragOperation(currentPosition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task StartDragAsync(Point position)
|
private async Task StartDragOperation(Point position)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var dragInfo = await _dragInfoFactory();
|
var dragInfo = _dragInfoFactory();
|
||||||
|
|
||||||
|
// Обновляем позицию в dragInfo
|
||||||
var screenPosition = GetScreenPosition(position);
|
var screenPosition = GetScreenPosition(position);
|
||||||
|
dragInfo = new DragInfo(
|
||||||
|
dragInfo.Data,
|
||||||
|
dragInfo.AllowedEffects,
|
||||||
|
screenPosition,
|
||||||
|
dragInfo.Source
|
||||||
|
);
|
||||||
|
|
||||||
_isDragging = await _dragDropService.StartDragAsync(this, screenPosition);
|
_isDragging = await _dragDropService.StartDragAsync(this, screenPosition);
|
||||||
}
|
}
|
||||||
@@ -134,131 +123,71 @@ public sealed class WinUIDragDropIntegrationService : IDisposable
|
|||||||
{
|
{
|
||||||
if (!_isDragging || _disposed) return;
|
if (!_isDragging || _disposed) return;
|
||||||
|
|
||||||
var point = e.GetCurrentPoint(_element);
|
await _dragDropService.EndDragAsync();
|
||||||
var position = GetScreenPosition(new Point(point.Position.X, point.Position.Y));
|
|
||||||
await _dragDropService.EndDragAsync(position);
|
|
||||||
_isDragging = false;
|
_isDragging = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnPointerCanceled(object sender, PointerRoutedEventArgs e)
|
private void OnPointerCanceled(object sender, PointerRoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (!_isDragging || _disposed) return;
|
if (!_isDragging || _disposed) return;
|
||||||
|
|
||||||
await _dragDropService.CancelDragAsync();
|
_dragDropService.CancelDragAsync();
|
||||||
_isDragging = false;
|
_isDragging = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnPointerCaptureLost(object sender, PointerRoutedEventArgs e)
|
private void OnPointerCaptureLost(object sender, PointerRoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (!_isDragging || _disposed) return;
|
if (!_isDragging || _disposed) return;
|
||||||
|
|
||||||
await _dragDropService.CancelDragAsync();
|
_dragDropService.CancelDragAsync();
|
||||||
_isDragging = false;
|
_isDragging = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region IDragSource Implementation
|
||||||
|
|
||||||
|
public async Task<(bool, DragInfo? dragInfo)> CancelDragAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
dragInfo = _dragInfoFactory();
|
||||||
|
return dragInfo != null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
dragInfo = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> StartDragAsync(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DragCompletedAsync(DragInfo dragInfo, Core.DragDrop.Enums.DragDropEffects effects)
|
||||||
|
{
|
||||||
|
_isDragging = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DragCancelledAsync(DragInfo dragInfo)
|
||||||
|
{
|
||||||
|
_isDragging = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
|
|
||||||
UnsubscribeFromEvents();
|
UnsubscribeFromEvents();
|
||||||
|
|
||||||
if (_isDragging)
|
|
||||||
{
|
|
||||||
Task.Run(async () => await _dragDropService.CancelDragAsync()).Wait(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class DropTargetAdapter : IDisposable
|
public Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
|
||||||
{
|
{
|
||||||
private readonly UIElement _element;
|
throw new NotImplementedException();
|
||||||
private readonly IDropTarget _dropTarget;
|
|
||||||
private readonly IDragDropService _dragDropService;
|
|
||||||
private string? _registrationId;
|
|
||||||
private bool _disposed;
|
|
||||||
|
|
||||||
public DropTargetAdapter(
|
|
||||||
UIElement element,
|
|
||||||
IDropTarget dropTarget,
|
|
||||||
IDragDropService dragDropService)
|
|
||||||
{
|
|
||||||
_element = element ?? throw new ArgumentNullException(nameof(element));
|
|
||||||
_dropTarget = dropTarget ?? throw new ArgumentNullException(nameof(dropTarget));
|
|
||||||
_dragDropService = dragDropService ?? throw new ArgumentNullException(nameof(dragDropService));
|
|
||||||
|
|
||||||
SubscribeToEvents();
|
|
||||||
UpdateRegistration();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SubscribeToEvents()
|
|
||||||
{
|
|
||||||
_element.LayoutUpdated += OnLayoutUpdated;
|
|
||||||
_element.Unloaded += OnElementUnloaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UnsubscribeFromEvents()
|
|
||||||
{
|
|
||||||
_element.LayoutUpdated -= OnLayoutUpdated;
|
|
||||||
_element.Unloaded -= OnElementUnloaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateRegistration()
|
|
||||||
{
|
|
||||||
var bounds = GetElementBounds();
|
|
||||||
if (_registrationId is null)
|
|
||||||
{
|
|
||||||
_registrationId = _dragDropService.RegisterDropTarget(_dropTarget, bounds);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_dragDropService.UpdateDropTargetBounds(_registrationId, bounds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Rect GetElementBounds()
|
|
||||||
{
|
|
||||||
var transform = _element.TransformToVisual(null);
|
|
||||||
var topLeft = transform.TransformPoint(new Windows.Foundation.Point(0, 0));
|
|
||||||
var bottomRight = transform.TransformPoint(new Windows.Foundation.Point(_element.ActualWidth, _element.ActualHeight));
|
|
||||||
|
|
||||||
return new Rect(
|
|
||||||
topLeft.X,
|
|
||||||
topLeft.Y,
|
|
||||||
bottomRight.X - topLeft.X,
|
|
||||||
bottomRight.Y - topLeft.Y);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnLayoutUpdated(object? sender, object e)
|
|
||||||
{
|
|
||||||
if (_disposed) return;
|
|
||||||
UpdateRegistration();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnElementUnloaded(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (_disposed) return;
|
|
||||||
|
|
||||||
UnsubscribeFromEvents();
|
|
||||||
|
|
||||||
if (_registrationId is not null)
|
|
||||||
{
|
|
||||||
_dragDropService.UnregisterDropTarget(_registrationId);
|
|
||||||
_registrationId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_dropTarget is IDisposable disposable)
|
|
||||||
disposable.Dispose();
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,30 +198,22 @@ public sealed class WinUIDragDropIntegrationService : IDisposable
|
|||||||
private readonly IDragDropService _dragDropService;
|
private readonly IDragDropService _dragDropService;
|
||||||
private readonly IDragVisualProvider _dragVisualProvider;
|
private readonly IDragVisualProvider _dragVisualProvider;
|
||||||
private readonly IDragDropHost _dragDropHost;
|
private readonly IDragDropHost _dragDropHost;
|
||||||
private readonly Dictionary<UIElement, DragSourceAdapter> _dragSources = new();
|
private readonly Dictionary<FrameworkElement, DragSourceAdapter> _dragSources = new();
|
||||||
private readonly Dictionary<UIElement, DropTargetAdapter> _dropTargets = new();
|
private readonly Dictionary<FrameworkElement, WinUIDropTargetBehavior> _dropTargets = new();
|
||||||
private readonly Window _window;
|
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Конструктор
|
#region Конструктор
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр.
|
|
||||||
/// </summary>
|
|
||||||
public WinUIDragDropIntegrationService(
|
public WinUIDragDropIntegrationService(
|
||||||
Window window,
|
|
||||||
IDragDropService dragDropService,
|
IDragDropService dragDropService,
|
||||||
IDragVisualProvider dragVisualProvider,
|
IDragVisualProvider dragVisualProvider,
|
||||||
IDragDropHost dragDropHost)
|
IDragDropHost dragDropHost)
|
||||||
{
|
{
|
||||||
_window = window ?? throw new ArgumentNullException(nameof(window));
|
|
||||||
_dragDropService = dragDropService ?? throw new ArgumentNullException(nameof(dragDropService));
|
_dragDropService = dragDropService ?? throw new ArgumentNullException(nameof(dragDropService));
|
||||||
_dragVisualProvider = dragVisualProvider ?? throw new ArgumentNullException(nameof(dragVisualProvider));
|
_dragVisualProvider = dragVisualProvider ?? throw new ArgumentNullException(nameof(dragVisualProvider));
|
||||||
_dragDropHost = dragDropHost ?? throw new ArgumentNullException(nameof(dragDropHost));
|
_dragDropHost = dragDropHost ?? throw new ArgumentNullException(nameof(dragDropHost));
|
||||||
|
|
||||||
InitializeEvents();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -302,9 +223,9 @@ public sealed class WinUIDragDropIntegrationService : IDisposable
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Регистрирует элемент как источник перетаскивания.
|
/// Регистрирует элемент как источник перетаскивания.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void RegisterDragSource(UIElement element, Func<Task<DragInfo>> dragInfoFactory)
|
public void RegisterDragSource(FrameworkElement element, Func<DragInfo> dragInfoFactory)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
if (_disposed) throw new ObjectDisposedException(nameof(WinUIDragDropIntegrationService));
|
||||||
ArgumentNullException.ThrowIfNull(element);
|
ArgumentNullException.ThrowIfNull(element);
|
||||||
ArgumentNullException.ThrowIfNull(dragInfoFactory);
|
ArgumentNullException.ThrowIfNull(dragInfoFactory);
|
||||||
|
|
||||||
@@ -317,39 +238,130 @@ public sealed class WinUIDragDropIntegrationService : IDisposable
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Регистрирует элемент как источник перетаскивания с данными.
|
/// Регистрирует элемент как источник перетаскивания с данными.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void RegisterDragSource(UIElement element, object dragData)
|
public void RegisterDragSource(FrameworkElement element, object dragData)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
RegisterDragSource(element, () =>
|
||||||
ArgumentNullException.ThrowIfNull(element);
|
{
|
||||||
ArgumentNullException.ThrowIfNull(dragData);
|
var position = GetScreenPosition(element, new Point(0, 0));
|
||||||
|
return new DragInfo(
|
||||||
if (_dragSources.ContainsKey(element)) return;
|
dragData,
|
||||||
|
Core.DragDrop.Enums.DragDropEffects.Copy | Core.DragDrop.Enums.DragDropEffects.Move,
|
||||||
var adapter = new DragSourceAdapter(element, dragData, _dragDropService);
|
position,
|
||||||
_dragSources[element] = adapter;
|
element
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Регистрирует элемент как цель сброса.
|
/// Регистрирует элемент как цель сброса.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void RegisterDropTarget(UIElement element, IDropTarget dropTarget)
|
public void RegisterDropTarget(FrameworkElement element)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
if (_disposed) throw new ObjectDisposedException(nameof(WinUIDragDropIntegrationService));
|
||||||
ArgumentNullException.ThrowIfNull(element);
|
ArgumentNullException.ThrowIfNull(element);
|
||||||
ArgumentNullException.ThrowIfNull(dropTarget);
|
|
||||||
|
|
||||||
if (_dropTargets.ContainsKey(element)) return;
|
if (_dropTargets.ContainsKey(element)) return;
|
||||||
|
|
||||||
var adapter = new DropTargetAdapter(element, dropTarget, _dragDropService);
|
var behavior = new WinUIDropTargetBehavior(ServiceProviderHelper.GetServiceProvider())
|
||||||
_dropTargets[element] = adapter;
|
{
|
||||||
|
AssociatedElement = element
|
||||||
|
};
|
||||||
|
|
||||||
|
_dropTargets[element] = behavior;
|
||||||
|
|
||||||
|
// Настраиваем события WinUI
|
||||||
|
element.AllowDrop = true;
|
||||||
|
element.DragEnter += OnDragEnter;
|
||||||
|
element.DragOver += OnDragOver;
|
||||||
|
element.DragLeave += OnDragLeave;
|
||||||
|
element.Drop += OnDrop;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Point GetScreenPosition(FrameworkElement element, Point relativePoint)
|
||||||
|
{
|
||||||
|
var transform = element.TransformToVisual(null);
|
||||||
|
var point = transform.TransformPoint(new Windows.Foundation.Point(relativePoint.X, relativePoint.Y));
|
||||||
|
return new Point(point.X, point.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDragEnter(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is FrameworkElement element && _dropTargets.TryGetValue(element, out var behavior))
|
||||||
|
{
|
||||||
|
var position = GetScreenPosition(element, e.GetPosition(element).ToCorePoint());
|
||||||
|
var dropInfo = CreateDropInfo(e, position, element);
|
||||||
|
|
||||||
|
if (behavior.CanAcceptDrop(dropInfo))
|
||||||
|
{
|
||||||
|
e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Copy;
|
||||||
|
behavior.DragOver(dropInfo);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDragOver(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is FrameworkElement element && _dropTargets.TryGetValue(element, out var behavior))
|
||||||
|
{
|
||||||
|
var position = GetScreenPosition(element, e.GetPosition(element).ToCorePoint());
|
||||||
|
var dropInfo = CreateDropInfo(e, position, element);
|
||||||
|
|
||||||
|
if (behavior.CanAcceptDrop(dropInfo))
|
||||||
|
{
|
||||||
|
e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Copy;
|
||||||
|
behavior.DragOver(dropInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDragLeave(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is FrameworkElement element && _dropTargets.TryGetValue(element, out var behavior))
|
||||||
|
{
|
||||||
|
behavior.DragLeave();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDrop(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is FrameworkElement element && _dropTargets.TryGetValue(element, out var behavior))
|
||||||
|
{
|
||||||
|
var position = GetScreenPosition(element, e.GetPosition(element).ToCorePoint());
|
||||||
|
var dropInfo = CreateDropInfo(e, position, element);
|
||||||
|
behavior.Drop(dropInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DropInfo CreateDropInfo(DragEventArgs e, Point position, FrameworkElement target)
|
||||||
|
{
|
||||||
|
// Извлекаем данные из DragEventArgs
|
||||||
|
object? data = null;
|
||||||
|
|
||||||
|
// В реальной реализации нужно извлечь данные из e.DataView
|
||||||
|
// Это упрощенная версия
|
||||||
|
if (e.DataView.Properties.TryGetValue("DragData", out var dragData))
|
||||||
|
{
|
||||||
|
data = dragData;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DropInfo(
|
||||||
|
data: data,
|
||||||
|
position: position,
|
||||||
|
allowedEffects: Core.DragDrop.Enums.DragDropEffects.Copy | Core.DragDrop.Enums.DragDropEffects.Move,
|
||||||
|
target: target
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Удаляет регистрацию элемента как источника.
|
/// Удаляет регистрацию элемента как источника.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void UnregisterDragSource(UIElement element)
|
public void UnregisterDragSource(FrameworkElement element)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
if (_disposed) throw new ObjectDisposedException(nameof(WinUIDragDropIntegrationService));
|
||||||
|
|
||||||
if (_dragSources.Remove(element, out var adapter))
|
if (_dragSources.Remove(element, out var adapter))
|
||||||
{
|
{
|
||||||
@@ -360,122 +372,38 @@ public sealed class WinUIDragDropIntegrationService : IDisposable
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Удаляет регистрацию элемента как цели.
|
/// Удаляет регистрацию элемента как цели.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void UnregisterDropTarget(UIElement element)
|
public void UnregisterDropTarget(FrameworkElement element)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
if (_disposed) throw new ObjectDisposedException(nameof(WinUIDragDropIntegrationService));
|
||||||
|
|
||||||
if (_dropTargets.Remove(element, out var adapter))
|
if (_dropTargets.Remove(element, out var behavior))
|
||||||
{
|
{
|
||||||
adapter.Dispose();
|
behavior.Detach();
|
||||||
|
element.AllowDrop = false;
|
||||||
|
element.DragEnter -= OnDragEnter;
|
||||||
|
element.DragOver -= OnDragOver;
|
||||||
|
element.DragLeave -= OnDragLeave;
|
||||||
|
element.Drop -= OnDrop;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Обработка событий
|
|
||||||
|
|
||||||
private void InitializeEvents()
|
|
||||||
{
|
|
||||||
_dragDropService.DragStarted += OnDragStarted;
|
|
||||||
_dragDropService.DragUpdated += OnDragUpdated;
|
|
||||||
_dragDropService.DragCompleted += OnDragCompleted;
|
|
||||||
_dragDropService.DragCancelled += OnDragCancelled;
|
|
||||||
_dragDropService.ErrorOccurred += OnErrorOccurred;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UnsubscribeFromEvents()
|
|
||||||
{
|
|
||||||
_dragDropService.DragStarted -= OnDragStarted;
|
|
||||||
_dragDropService.DragUpdated -= OnDragUpdated;
|
|
||||||
_dragDropService.DragCompleted -= OnDragCompleted;
|
|
||||||
_dragDropService.DragCancelled -= OnDragCancelled;
|
|
||||||
_dragDropService.ErrorOccurred -= OnErrorOccurred;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDragStarted(object? sender, DragStartedEventArgs e)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var visual = _dragVisualProvider.CreateDragVisual(e.DragInfo, e.StartPosition);
|
|
||||||
_dragDropHost.ShowDragVisual(visual, e.StartPosition);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.WriteLine($"Error in OnDragStarted: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDragUpdated(object? sender, DragUpdatedEventArgs e)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Обновление визуала будет через хост
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.WriteLine($"Error in OnDragUpdated: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDragCompleted(object? sender, DragCompletedEventArgs e)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_dragDropHost.HideDragVisual(null!); // В реальности нужно передать конкретный визуал
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.WriteLine($"Error in OnDragCompleted: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDragCancelled(object? sender, DragCancelledEventArgs e)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_dragDropHost.HideDragVisual(null!);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.WriteLine($"Error in OnDragCancelled: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnErrorOccurred(object? sender, DragDropErrorEventArgs e)
|
|
||||||
{
|
|
||||||
Debug.WriteLine($"DragDrop error in {e.Operation}: {e.Exception.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Вспомогательные методы
|
|
||||||
|
|
||||||
private void ThrowIfDisposed()
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
throw new ObjectDisposedException(nameof(WinUIDragDropIntegrationService));
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region IDisposable
|
#region IDisposable
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
|
|
||||||
UnsubscribeFromEvents();
|
|
||||||
|
|
||||||
foreach (var adapter in _dragSources.Values)
|
foreach (var adapter in _dragSources.Values)
|
||||||
{
|
{
|
||||||
adapter.Dispose();
|
adapter.Dispose();
|
||||||
}
|
}
|
||||||
_dragSources.Clear();
|
_dragSources.Clear();
|
||||||
|
|
||||||
foreach (var adapter in _dropTargets.Values)
|
foreach (var behavior in _dropTargets.Values)
|
||||||
{
|
{
|
||||||
adapter.Dispose();
|
behavior.Detach();
|
||||||
}
|
}
|
||||||
_dropTargets.Clear();
|
_dropTargets.Clear();
|
||||||
|
|
||||||
@@ -485,3 +413,14 @@ public sealed class WinUIDragDropIntegrationService : IDisposable
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Методы расширения для преобразования координат.
|
||||||
|
/// </summary>
|
||||||
|
internal static class PointExtensions
|
||||||
|
{
|
||||||
|
public static Point ToCorePoint(this Windows.Foundation.Point point)
|
||||||
|
{
|
||||||
|
return new Point(point.X, point.Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
Lattice.UI.DragDrop.WinUI/Services/WinUIDragDropHost.cs
Normal file
72
Lattice.UI.DragDrop.WinUI/Services/WinUIDragDropHost.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
public class WinUIDragDropHost : IDragDropHost
|
||||||
|
{
|
||||||
|
private readonly DragDropOverlay _overlay;
|
||||||
|
private readonly Window _window;
|
||||||
|
|
||||||
|
public WinUIDragDropHost(Window window)
|
||||||
|
{
|
||||||
|
_window = window ?? throw new ArgumentNullException(nameof(window));
|
||||||
|
_overlay = new DragDropOverlay();
|
||||||
|
|
||||||
|
// Добавляем оверлей в окно
|
||||||
|
if (_window.Content is Panel panel)
|
||||||
|
{
|
||||||
|
panel.Children.Add(_overlay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowDragVisual(object dragVisual, Point position)
|
||||||
|
{
|
||||||
|
if (dragVisual is UIElement element)
|
||||||
|
{
|
||||||
|
_overlay.ShowDragVisual(element, position.X, position.Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateDragVisualPosition(object dragVisual, Point position)
|
||||||
|
{
|
||||||
|
if (dragVisual is UIElement element)
|
||||||
|
{
|
||||||
|
_overlay.UpdateDragVisualPosition(element, position.X, position.Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HideDragVisual(object dragVisual)
|
||||||
|
{
|
||||||
|
if (dragVisual is UIElement element)
|
||||||
|
{
|
||||||
|
_overlay.HideDragVisual(element);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Скрываем все, если передан null
|
||||||
|
var current = _overlay.GetCurrentDragVisual();
|
||||||
|
if (current != null)
|
||||||
|
{
|
||||||
|
_overlay.HideDragVisual(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowDropAdorner(IDropVisualAdorner adorner)
|
||||||
|
{
|
||||||
|
if (adorner is DropPreviewAdorner dropAdorner)
|
||||||
|
{
|
||||||
|
// TODO: Показываем превью сброса
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HideDropAdorner(IDropVisualAdorner adorner)
|
||||||
|
{
|
||||||
|
_overlay.HideAllDropPreviews();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,45 +2,22 @@
|
|||||||
using Lattice.Core.Geometry;
|
using Lattice.Core.Geometry;
|
||||||
using Lattice.UI.DragDrop.Abstractions;
|
using Lattice.UI.DragDrop.Abstractions;
|
||||||
using Lattice.UI.DragDrop.WinUI.Controls;
|
using Lattice.UI.DragDrop.WinUI.Controls;
|
||||||
using Microsoft.UI.Xaml;
|
|
||||||
using Microsoft.UI.Xaml.Controls;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Lattice.UI.DragDrop.WinUI.Services;
|
namespace Lattice.UI.DragDrop.WinUI.Services;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Поставщик визуального представления для WinUI.
|
|
||||||
/// </summary>
|
|
||||||
public class WinUIDragVisualProvider : IDragVisualProvider
|
public class WinUIDragVisualProvider : IDragVisualProvider
|
||||||
{
|
{
|
||||||
private readonly ResourceDictionary _resources;
|
|
||||||
private DragAdorner? _currentAdorner;
|
private DragAdorner? _currentAdorner;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="WinUIDragVisualProvider"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="resources">Ресурсы для стилей.</param>
|
|
||||||
public WinUIDragVisualProvider(ResourceDictionary resources)
|
|
||||||
{
|
|
||||||
_resources = resources ?? throw new ArgumentNullException(nameof(resources));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public object CreateDragVisual(DragInfo dragInfo, Point initialPosition)
|
public object CreateDragVisual(DragInfo dragInfo, Point initialPosition)
|
||||||
{
|
{
|
||||||
// Создаем новый DragAdorner
|
// Создаем DragAdorner на основе данных
|
||||||
_currentAdorner = new DragAdorner
|
_currentAdorner = new DragAdorner
|
||||||
{
|
{
|
||||||
DragData = dragInfo.Data,
|
DragData = dragInfo.Data,
|
||||||
OpacityLevel = 0.8
|
OpacityLevel = 0.8
|
||||||
};
|
};
|
||||||
|
|
||||||
// Применяем стиль из ресурсов, если есть
|
|
||||||
if (_resources.ContainsKey("DragAdornerStyle"))
|
|
||||||
{
|
|
||||||
_currentAdorner.Style = _resources["DragAdornerStyle"] as Style;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Настраиваем начальную позицию
|
// Настраиваем начальную позицию
|
||||||
_currentAdorner.UpdatePosition(initialPosition);
|
_currentAdorner.UpdatePosition(initialPosition);
|
||||||
_currentAdorner.Show();
|
_currentAdorner.Show();
|
||||||
@@ -48,7 +25,6 @@ public class WinUIDragVisualProvider : IDragVisualProvider
|
|||||||
return _currentAdorner;
|
return _currentAdorner;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void UpdateDragVisualPosition(object dragVisual, Point position)
|
public void UpdateDragVisualPosition(object dragVisual, Point position)
|
||||||
{
|
{
|
||||||
if (dragVisual is DragAdorner adorner)
|
if (dragVisual is DragAdorner adorner)
|
||||||
@@ -57,31 +33,12 @@ public class WinUIDragVisualProvider : IDragVisualProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void ReleaseDragVisual(object dragVisual)
|
public void ReleaseDragVisual(object dragVisual)
|
||||||
{
|
{
|
||||||
if (dragVisual is DragAdorner adorner)
|
if (dragVisual is DragAdorner adorner)
|
||||||
{
|
{
|
||||||
adorner.Hide();
|
adorner.Hide();
|
||||||
|
|
||||||
// Отложенное удаление после анимации
|
|
||||||
var timer = new DispatcherTimer
|
|
||||||
{
|
|
||||||
Interval = TimeSpan.FromMilliseconds(150)
|
|
||||||
};
|
|
||||||
|
|
||||||
timer.Tick += (s, e) =>
|
|
||||||
{
|
|
||||||
timer.Stop();
|
|
||||||
if (adorner.Parent is Panel panel)
|
|
||||||
{
|
|
||||||
panel.Children.Remove(adorner);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
timer.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentAdorner = null;
|
_currentAdorner = null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,8 @@ using Lattice.Core.DragDrop.Services;
|
|||||||
using Lattice.Core.Geometry;
|
using Lattice.Core.Geometry;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Lattice.UI.DragDrop.Behaviors;
|
namespace Lattice.UI.DragDrop.Behaviors;
|
||||||
|
|
||||||
@@ -103,7 +105,7 @@ public abstract class DragSourceBehaviorBase<TElement> : IDragSource
|
|||||||
/// Обрабатывает начало взаимодействия (например, нажатие мыши).
|
/// Обрабатывает начало взаимодействия (например, нажатие мыши).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="position">Позиция в координатах элемента.</param>
|
/// <param name="position">Позиция в координатах элемента.</param>
|
||||||
protected virtual void OnInteractionStarted(Point position)
|
protected virtual async Task OnInteractionStarted(Point position)
|
||||||
{
|
{
|
||||||
if (_isDragging)
|
if (_isDragging)
|
||||||
return;
|
return;
|
||||||
@@ -115,7 +117,7 @@ public abstract class DragSourceBehaviorBase<TElement> : IDragSource
|
|||||||
/// Обрабатывает перемещение во время взаимодействия.
|
/// Обрабатывает перемещение во время взаимодействия.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="position">Позиция в координатах элемента.</param>
|
/// <param name="position">Позиция в координатах элемента.</param>
|
||||||
protected virtual void OnInteractionMoved(Point position)
|
protected virtual async Task OnInteractionMoved(Point position)
|
||||||
{
|
{
|
||||||
if (_isDragging)
|
if (_isDragging)
|
||||||
return;
|
return;
|
||||||
@@ -123,14 +125,14 @@ public abstract class DragSourceBehaviorBase<TElement> : IDragSource
|
|||||||
var distance = CalculateDistance(_dragStartPosition, position);
|
var distance = CalculateDistance(_dragStartPosition, position);
|
||||||
if (distance > DragDropService.DragStartThreshold)
|
if (distance > DragDropService.DragStartThreshold)
|
||||||
{
|
{
|
||||||
StartDragOperation();
|
await StartDragOperation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Обрабатывает завершение взаимодействия.
|
/// Обрабатывает завершение взаимодействия.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual void OnInteractionEnded()
|
protected virtual async Task OnInteractionEnded()
|
||||||
{
|
{
|
||||||
// Сброс состояния, если перетаскивание не началось
|
// Сброс состояния, если перетаскивание не началось
|
||||||
if (!_isDragging)
|
if (!_isDragging)
|
||||||
@@ -142,11 +144,11 @@ public abstract class DragSourceBehaviorBase<TElement> : IDragSource
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Обрабатывает отмену взаимодействия.
|
/// Обрабатывает отмену взаимодействия.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual void OnInteractionCancelled()
|
protected virtual async Task OnInteractionCancelled()
|
||||||
{
|
{
|
||||||
if (_isDragging)
|
if (_isDragging)
|
||||||
{
|
{
|
||||||
DragDropService.CancelDrag();
|
await DragDropService.CancelDragAsync();
|
||||||
}
|
}
|
||||||
Reset();
|
Reset();
|
||||||
}
|
}
|
||||||
@@ -154,7 +156,7 @@ public abstract class DragSourceBehaviorBase<TElement> : IDragSource
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Начинает операцию перетаскивания.
|
/// Начинает операцию перетаскивания.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual void StartDragOperation()
|
protected virtual async Task StartDragOperation()
|
||||||
{
|
{
|
||||||
if (_isDragging || AssociatedElement == null)
|
if (_isDragging || AssociatedElement == null)
|
||||||
return;
|
return;
|
||||||
@@ -163,7 +165,7 @@ public abstract class DragSourceBehaviorBase<TElement> : IDragSource
|
|||||||
var screenPosition = ConvertToScreenCoordinates(_dragStartPosition);
|
var screenPosition = ConvertToScreenCoordinates(_dragStartPosition);
|
||||||
|
|
||||||
// Начинаем перетаскивание
|
// Начинаем перетаскивание
|
||||||
_isDragging = DragDropService.StartDrag(this, screenPosition);
|
_isDragging = await DragDropService.StartDragAsync(this, screenPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -195,30 +197,25 @@ public abstract class DragSourceBehaviorBase<TElement> : IDragSource
|
|||||||
#region IDragSource Implementation
|
#region IDragSource Implementation
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public abstract bool CanStartDrag(out DragInfo? dragInfo);
|
public abstract Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync(CancellationToken ct = default);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual bool StartDrag(DragInfo dragInfo)
|
public virtual async Task<bool> StartDragAsync(DragInfo dragInfo, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// Базовая реализация всегда разрешает начало перетаскивания
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual void DragCompleted(DragInfo dragInfo, Core.DragDrop.Enums.DragDropEffects effects)
|
public virtual async Task DragCompletedAsync(DragInfo dragInfo, Core.DragDrop.Enums.DragDropEffects effects, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
_isDragging = false;
|
_isDragging = false;
|
||||||
|
|
||||||
// Оповещаем о завершении перетаскивания
|
|
||||||
OnDragCompleted(dragInfo, effects);
|
OnDragCompleted(dragInfo, effects);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual void DragCancelled(DragInfo dragInfo)
|
public virtual async Task DragCancelledAsync(DragInfo dragInfo, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
_isDragging = false;
|
_isDragging = false;
|
||||||
|
|
||||||
// Оповещаем об отмене перетаскивания
|
|
||||||
OnDragCancelled(dragInfo);
|
OnDragCancelled(dragInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ using Lattice.Core.DragDrop.Services;
|
|||||||
using Lattice.Core.Geometry;
|
using Lattice.Core.Geometry;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Lattice.UI.DragDrop.Behaviors;
|
namespace Lattice.UI.DragDrop.Behaviors;
|
||||||
|
|
||||||
@@ -176,16 +178,28 @@ public abstract class DropTargetBehaviorBase<TElement> : IDropTarget
|
|||||||
#region IDropTarget Implementation
|
#region IDropTarget Implementation
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public abstract bool CanAcceptDrop(DropInfo dropInfo);
|
public abstract Task<bool> CanAcceptDropAsync(DropInfo dropInfo, CancellationToken ct = default);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual void DragOver(DropInfo dropInfo)
|
public virtual async Task DragOverAsync(DropInfo dropInfo, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// Базовая реализация устанавливает эффект по умолчанию
|
// Базовая реализация устанавливает эффект по умолчанию
|
||||||
if (CanAcceptDrop(dropInfo))
|
if (await CanAcceptDropAsync(dropInfo))
|
||||||
|
{
|
||||||
|
// Установить эффект по умолчанию, если он разрешен
|
||||||
|
if (dropInfo.CanAcceptEffect(Core.DragDrop.Enums.DragDropEffects.Move))
|
||||||
{
|
{
|
||||||
dropInfo.SuggestedEffects = Core.DragDrop.Enums.DragDropEffects.Move;
|
dropInfo.SuggestedEffects = Core.DragDrop.Enums.DragDropEffects.Move;
|
||||||
}
|
}
|
||||||
|
else if (dropInfo.CanAcceptEffect(Core.DragDrop.Enums.DragDropEffects.Copy))
|
||||||
|
{
|
||||||
|
dropInfo.SuggestedEffects = Core.DragDrop.Enums.DragDropEffects.Copy;
|
||||||
|
}
|
||||||
|
else if (dropInfo.CanAcceptEffect(Core.DragDrop.Enums.DragDropEffects.Link))
|
||||||
|
{
|
||||||
|
dropInfo.SuggestedEffects = Core.DragDrop.Enums.DragDropEffects.Link;
|
||||||
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
dropInfo.SuggestedEffects = Core.DragDrop.Enums.DragDropEffects.None;
|
dropInfo.SuggestedEffects = Core.DragDrop.Enums.DragDropEffects.None;
|
||||||
@@ -193,10 +207,10 @@ public abstract class DropTargetBehaviorBase<TElement> : IDropTarget
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public abstract void Drop(DropInfo dropInfo);
|
public abstract Task DropAsync(DropInfo dropInfo, CancellationToken ct = default);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual void DragLeave()
|
public virtual async Task DragLeaveAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// Базовая реализация не делает ничего
|
// Базовая реализация не делает ничего
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user