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