Доработан winui
This commit is contained in:
53
Lattice.UI.Docking.WinUI/Services/DragDropService.cs
Normal file
53
Lattice.UI.Docking.WinUI/Services/DragDropService.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using System;
|
||||
|
||||
namespace Lattice.UI.Docking.WinUI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Сервис для управления операциями Drag & Drop в WinUI.
|
||||
/// </summary>
|
||||
public static class DragDropService
|
||||
{
|
||||
/// <summary>
|
||||
/// Настраивает элемент для поддержки перетаскивания.
|
||||
/// </summary>
|
||||
public static void SetupDragElement(UIElement element, Func<object?> getDataCallback)
|
||||
{
|
||||
element.CanDrag = true;
|
||||
element.DragStarting += (sender, args) =>
|
||||
{
|
||||
var data = getDataCallback();
|
||||
if (data != null)
|
||||
{
|
||||
args.Data.Properties.Add("LatticeDockElement", data);
|
||||
args.Data.SetData("LatticeDockElement", data);
|
||||
args.AllowedOperations = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Настраивает элемент для приема сброса.
|
||||
/// </summary>
|
||||
public static void SetupDropElement(UIElement element, Func<object, bool> dropCallback)
|
||||
{
|
||||
element.AllowDrop = true;
|
||||
element.Drop += (sender, args) =>
|
||||
{
|
||||
if (args.DataView.Properties.TryGetValue("LatticeDockElement", out var data))
|
||||
{
|
||||
if (dropCallback(data))
|
||||
{
|
||||
args.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
element.DragOver += (sender, args) =>
|
||||
{
|
||||
args.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move;
|
||||
args.DragUIOverride.IsGlyphVisible = true;
|
||||
args.DragUIOverride.Caption = "Переместить";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,9 @@ using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Lattice.UI.Docking.WinUI.Services;
|
||||
|
||||
@@ -21,9 +24,38 @@ public sealed class WinUIDockContextManager : DockContextManagerBase, IDisposabl
|
||||
/// </summary>
|
||||
public WinUIDockContextManager()
|
||||
{
|
||||
// Регистрируем стандартные команды
|
||||
RegisterDefaultCommands();
|
||||
}
|
||||
|
||||
private void RegisterDefaultCommands()
|
||||
{
|
||||
// Пример регистрации стандартных команд
|
||||
RegisterCommand("Close", new DockCommand("Close", "Close", "Close the selected content", () => "", () => true, OnCloseCommand));
|
||||
RegisterCommand("Float", new DockCommand("Float", "Float", "Float the window", () => "", () => true, OnFloatCommand));
|
||||
RegisterCommand("Dock", new DockCommand("Dock", "Dock", "Dock the window", () => "", () => true, OnDockCommand));
|
||||
}
|
||||
|
||||
private void OnCloseCommand()
|
||||
{
|
||||
if (_currentContextTarget is Lattice.UI.LatticeDockLeaf leafControl && leafControl.ActiveContent != null)
|
||||
{
|
||||
leafControl.CloseContent(leafControl.ActiveContent);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFloatCommand()
|
||||
{
|
||||
// TODO: Реализовать плавающее окно
|
||||
System.Diagnostics.Debug.WriteLine("Float command triggered");
|
||||
}
|
||||
|
||||
private void OnDockCommand()
|
||||
{
|
||||
// TODO: Реализовать закрепление окна
|
||||
System.Diagnostics.Debug.WriteLine("Dock command triggered");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ShowContextMenu(IDockControl element, double x, double y)
|
||||
{
|
||||
if (element is not FrameworkElement uiElement) return;
|
||||
@@ -39,13 +71,26 @@ public sealed class WinUIDockContextManager : DockContextManagerBase, IDisposabl
|
||||
var item = new MenuFlyoutItem
|
||||
{
|
||||
Text = command.Name,
|
||||
Command = new RelayCommand(() => ExecuteCommand(command, element))
|
||||
Tag = command,
|
||||
Command = new RelayCommand(() => ExecuteCommand(command, element),
|
||||
() => command.CanExecute(element))
|
||||
};
|
||||
|
||||
// Добавляем иконку, если есть
|
||||
// Устанавливаем иконку, если есть
|
||||
if (!string.IsNullOrEmpty(command.Icon))
|
||||
{
|
||||
// TODO: Добавить иконку команды
|
||||
var icon = new FontIcon
|
||||
{
|
||||
Glyph = command.Icon,
|
||||
FontSize = 12
|
||||
};
|
||||
item.Icon = icon;
|
||||
}
|
||||
|
||||
// Добавляем подсказку, если есть описание
|
||||
if (!string.IsNullOrEmpty(command.Description))
|
||||
{
|
||||
ToolTipService.SetToolTip(item, command.Description);
|
||||
}
|
||||
|
||||
flyout.Items.Add(item);
|
||||
@@ -68,7 +113,6 @@ public sealed class WinUIDockContextManager : DockContextManagerBase, IDisposabl
|
||||
OnContextMenuShown(element, x, y);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void HideContextMenu()
|
||||
{
|
||||
if (_currentFlyout != null)
|
||||
@@ -84,30 +128,93 @@ public sealed class WinUIDockContextManager : DockContextManagerBase, IDisposabl
|
||||
}
|
||||
}
|
||||
|
||||
public override void RegisterCommand(string commandId, IDockCommand command)
|
||||
{
|
||||
if (string.IsNullOrEmpty(commandId))
|
||||
throw new ArgumentNullException(nameof(commandId));
|
||||
|
||||
_commands[commandId] = command ?? throw new ArgumentNullException(nameof(command));
|
||||
}
|
||||
|
||||
public override void UnregisterCommand(string commandId)
|
||||
{
|
||||
_commands.TryRemove(commandId, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Класс-заглушка для реализации ICommand.
|
||||
/// Получает команду по идентификатору.
|
||||
/// </summary>
|
||||
private sealed class RelayCommand : System.Windows.Input.ICommand
|
||||
protected override IDockCommand? GetCommand(string commandId)
|
||||
{
|
||||
_commands.TryGetValue(commandId, out var command);
|
||||
return command;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает все доступные команды для указанного элемента.
|
||||
/// </summary>
|
||||
protected override IEnumerable<IDockCommand> GetCommandsForElement(IDockControl element)
|
||||
{
|
||||
return _commands.Values.Where(c => CanExecuteCommand(c, element));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Класс для реализации ICommand.
|
||||
/// </summary>
|
||||
private sealed class RelayCommand : ICommand
|
||||
{
|
||||
private readonly Action _execute;
|
||||
private readonly Func<bool>? _canExecute;
|
||||
private readonly Func<bool> _canExecute;
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public RelayCommand(Action execute, Func<bool>? canExecute = null)
|
||||
public RelayCommand(Action execute, Func<bool> canExecute)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
|
||||
public bool CanExecute(object? parameter) => _canExecute();
|
||||
|
||||
public void Execute(object? parameter) => _execute();
|
||||
|
||||
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <summary>
|
||||
/// Базовая реализация команды докинга.
|
||||
/// </summary>
|
||||
private class DockCommand : IDockCommand
|
||||
{
|
||||
public string Id { get; }
|
||||
public string Name { get; }
|
||||
public string Description { get; }
|
||||
private readonly Func<string> _getIcon;
|
||||
private readonly Func<bool> _canExecute;
|
||||
private readonly Action _execute;
|
||||
|
||||
public DockCommand(string id, string name, string description, Func<string> getIcon, Func<bool> canExecute, Action execute)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
Description = description;
|
||||
_getIcon = getIcon;
|
||||
_canExecute = canExecute;
|
||||
_execute = execute;
|
||||
}
|
||||
|
||||
public string Icon => _getIcon();
|
||||
public string Shortcut => "";
|
||||
|
||||
public bool CanExecute(object? parameter) => _canExecute();
|
||||
|
||||
public void Execute(object? parameter) => _execute();
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
HideContextMenu();
|
||||
|
||||
@@ -12,39 +12,8 @@ namespace Lattice.UI.Docking.WinUI.Services;
|
||||
/// Инкапсулирует платформенно-зависимые операции, такие как создание окон,
|
||||
/// показ диалогов и синхронизация с UI-потоком.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="WinUIDockUIService"/> предоставляет конкретные реализации методов
|
||||
/// <see cref="IDockUIService"/> для платформы WinUI. Это позволяет основной
|
||||
/// бизнес-логике док-системы оставаться независимой от конкретной UI-платформы.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Сервис использует API WinUI для создания окон, показа ContentDialog и
|
||||
/// управления диспетчером потока пользовательского интерфейса.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService
|
||||
{
|
||||
/// <summary>
|
||||
/// Создает главное окно приложения для размещения док-хоста.
|
||||
/// </summary>
|
||||
/// <param name="host">
|
||||
/// Экземпляр <see cref="IDockHost"/>, который будет содержаться в окне.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Объект окна WinUI, который можно отобразить и управлять им.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, если <paramref name="host"/> равен null.
|
||||
/// </exception>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Выбрасывается, если <paramref name="host"/> не является элементом WinUI.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// Создает окно WinUI с заголовком "Lattice IDE", устанавливает указанный хост
|
||||
/// в качестве содержимого и регистрирует окно в системе отслеживания окон.
|
||||
/// Окно создается с настройками по умолчанию для IDE-подобных приложений.
|
||||
/// </remarks>
|
||||
public override object CreateMainWindow(IDockHost host)
|
||||
{
|
||||
if (host is not FrameworkElement hostElement)
|
||||
@@ -55,30 +24,11 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService
|
||||
window.AppWindow.Title = "Lattice IDE";
|
||||
|
||||
// Регистрируем окно в трекере
|
||||
Themes.WindowTracker.Register(window);
|
||||
Lattice.Themes.WindowTracker.Register(window);
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отображает модальное диалоговое окно с указанным содержимым.
|
||||
/// </summary>
|
||||
/// <param name="title">Заголовок диалогового окна.</param>
|
||||
/// <param name="content">Содержимое диалогового окна.</param>
|
||||
/// <returns>
|
||||
/// Nullable boolean значение, указывающее результат диалога:
|
||||
/// true - пользователь подтвердил действие,
|
||||
/// false - пользователь отменил действие,
|
||||
/// null - диалог был закрыт без выбора.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, если <paramref name="title"/> или <paramref name="content"/> равны null.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// Создает и показывает ContentDialog с кнопками OK и Cancel.
|
||||
/// Блокирует взаимодействие с родительским окном до закрытия диалога.
|
||||
/// Использует XamlRoot активного окна для корректного отображения.
|
||||
/// </remarks>
|
||||
public override bool? ShowDialog(string title, object content)
|
||||
{
|
||||
if (content is not FrameworkElement contentElement)
|
||||
@@ -93,7 +43,6 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService
|
||||
XamlRoot = GetActiveXamlRoot()
|
||||
};
|
||||
|
||||
// Показываем диалог и возвращаем результат
|
||||
var result = dialog.ShowAsync();
|
||||
return result.GetAwaiter().GetResult() switch
|
||||
{
|
||||
@@ -103,19 +52,6 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отображает информационное сообщение с кнопкой OK.
|
||||
/// </summary>
|
||||
/// <param name="message">Текст сообщения.</param>
|
||||
/// <param name="caption">Заголовок окна сообщения.</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, если <paramref name="message"/> или <paramref name="caption"/> равны null.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// Создает ContentDialog с текстом сообщения и одной кнопкой OK.
|
||||
/// Используется для информирования пользователя о результате операции
|
||||
/// или отображения некритичных ошибок.
|
||||
/// </remarks>
|
||||
public override void ShowMessage(string message, string caption)
|
||||
{
|
||||
var dialog = new ContentDialog
|
||||
@@ -129,22 +65,6 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService
|
||||
dialog.ShowAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отображает диалог подтверждения с кнопками Yes/No.
|
||||
/// </summary>
|
||||
/// <param name="message">Текст вопроса.</param>
|
||||
/// <param name="caption">Заголовок окна подтверждения.</param>
|
||||
/// <returns>
|
||||
/// true, если пользователь выбрал "Yes"; false, если пользователь выбрал "No".
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, если <paramref name="message"/> или <paramref name="caption"/> равны null.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// Создает ContentDialog с кнопками Yes и No. Используется для получения
|
||||
/// подтверждения пользователя перед выполнением критических операций,
|
||||
/// таких как закрытие вкладок с несохраненными данными или сброс настроек.
|
||||
/// </remarks>
|
||||
public override bool Confirm(string message, string caption)
|
||||
{
|
||||
var dialog = new ContentDialog
|
||||
@@ -160,22 +80,6 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService
|
||||
return result == ContentDialogResult.Primary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отображает диалог ввода текста.
|
||||
/// </summary>
|
||||
/// <param name="prompt">Текст подсказки для пользователя.</param>
|
||||
/// <param name="defaultValue">Значение по умолчанию для поля ввода.</param>
|
||||
/// <returns>
|
||||
/// Введенный пользователем текст или null, если диалог был отменен.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, если <paramref name="prompt"/> равен null.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// Создает ContentDialog с однострочным полем ввода TextBox.
|
||||
/// Используется для получения текстового ввода от пользователя, такого как
|
||||
/// имена файлов, названия документов или параметры конфигурации.
|
||||
/// </remarks>
|
||||
public override string? Prompt(string prompt, string? defaultValue = null)
|
||||
{
|
||||
var textBox = new TextBox
|
||||
@@ -198,19 +102,6 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService
|
||||
return result == ContentDialogResult.Primary ? textBox.Text : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Выполняет указанное действие в UI-потоке.
|
||||
/// </summary>
|
||||
/// <param name="action">Действие для выполнения.</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, если <paramref name="action"/> равен null.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// Гарантирует, что действие будет выполнено в потоке, связанном с
|
||||
/// пользовательским интерфейсом. Если текущий поток уже является UI-потоком,
|
||||
/// действие выполняется немедленно. В противном случае действие ставится
|
||||
/// в очередь диспетчера WinUI.
|
||||
/// </remarks>
|
||||
public override void InvokeOnUIThread(Action action)
|
||||
{
|
||||
if (action == null) return;
|
||||
@@ -229,19 +120,7 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService
|
||||
/// <summary>
|
||||
/// Выполняет указанную асинхронную функцию в UI-потоке.
|
||||
/// </summary>
|
||||
/// <param name="action">Асинхронная функция для выполнения.</param>
|
||||
/// <returns>
|
||||
/// Задача, представляющая асинхронную операцию.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, если <paramref name="action"/> равен null.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// Гарантирует, что асинхронная функция будет выполнена в UI-потоке.
|
||||
/// Используется для операций, которые требуют доступа к UI-элементам
|
||||
/// или выполняют асинхронные вызовы с обновлением интерфейса.
|
||||
/// </remarks>
|
||||
public override async Task InvokeOnUIThreadAsync(Func<Task> action)
|
||||
public async Task InvokeOnUIThreadAsync(Func<Task> action)
|
||||
{
|
||||
if (action == null) return;
|
||||
|
||||
@@ -269,21 +148,9 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает XamlRoot активного окна приложения.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// XamlRoot активного окна или null, если нет активных окон.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// Используется для корректного отображения диалоговых окон в контексте
|
||||
/// текущего окна приложения. Перебирает все зарегистрированные окна
|
||||
/// и возвращает XamlRoot первого найденного.
|
||||
/// </remarks>
|
||||
private XamlRoot? GetActiveXamlRoot()
|
||||
{
|
||||
// Получаем XamlRoot из активного окна
|
||||
foreach (var window in Themes.WindowTracker.Windows)
|
||||
foreach (var window in Lattice.Themes.WindowTracker.Windows)
|
||||
{
|
||||
if (window.Content is FrameworkElement element)
|
||||
{
|
||||
|
||||
@@ -1,533 +0,0 @@
|
||||
using Lattice.Core.Geometry;
|
||||
using Lattice.UI.Docking.Abstractions;
|
||||
using Lattice.UI.Docking.Models;
|
||||
using Lattice.UI.Docking.Services;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Lattice.UI.Docking.WinUI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Предоставляет реализацию сервиса перетаскивания для платформы WinUI с расширенной
|
||||
/// поддержкой визуальных эффектов и интеграцией с системой докинга Lattice.
|
||||
/// Координирует взаимодействие между базовым менеджером перетаскивания и UI-контролами,
|
||||
/// обеспечивая богатую визуальную обратную связь во время операций drag-and-drop.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="WinUIDragDropService"/> расширяет базовый функционал <see cref="DockDragDropService"/>
|
||||
/// платформенно-зависимыми визуальными эффектами, включая:
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item>Прозрачное визуальное представление перетаскиваемого элемента</item>
|
||||
/// <item>Интерактивные подсказки областей сброса</item>
|
||||
/// <item>Анимации при начале и завершении перетаскивания</item>
|
||||
/// <item>Подсветку допустимых целей сброса</item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// Сервис поддерживает регистрацию UI-элементов и автоматически вычисляет их границы
|
||||
/// для точного определения целей сброса.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class WinUIDragDropService : DockDragDropService, IDisposable
|
||||
{
|
||||
private readonly ConcurrentDictionary<IDockControl, FrameworkElement> _controlToElement = new();
|
||||
private readonly DragDropManagerEx _dragDropManager;
|
||||
private Popup? _dragVisualPopup;
|
||||
private Border? _dragVisual;
|
||||
private DropHintOverlay? _dropHintOverlay;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый экземпляр сервиса перетаскивания WinUI.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Создает внутренний менеджер перетаскивания, инициализирует визуальные элементы
|
||||
/// и подписывается на события менеджера для обработки операций перетаскивания.
|
||||
/// </remarks>
|
||||
public WinUIDragDropService()
|
||||
{
|
||||
_dragDropManager = new DragDropManagerEx();
|
||||
HookEvents();
|
||||
InitializeDragVisual();
|
||||
InitializeDropHintOverlay();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый экземпляр с указанным менеджером перетаскивания.
|
||||
/// </summary>
|
||||
/// <param name="dragDropManager">
|
||||
/// Предварительно настроенный менеджер перетаскивания.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, если <paramref name="dragDropManager"/> равен null.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// Позволяет использовать кастомную конфигурацию менеджера перетаскивания
|
||||
/// при сохранении всех визуальных эффектов WinUI.
|
||||
/// </remarks>
|
||||
public WinUIDragDropService(DragDropManagerEx dragDropManager)
|
||||
{
|
||||
_dragDropManager = dragDropManager ?? throw new ArgumentNullException(nameof(dragDropManager));
|
||||
HookEvents();
|
||||
InitializeDragVisual();
|
||||
InitializeDropHintOverlay();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Подписывается на события менеджера перетаскивания.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Обрабатывает следующие события:
|
||||
/// <list type="bullet">
|
||||
/// <item>Начало перетаскивания</item>
|
||||
/// <item>Обновление позиции перетаскивания</item>
|
||||
/// <item>Завершение перетаскивания</item>
|
||||
/// <item>Отмена перетаскивания</item>
|
||||
/// <item>Изменение цели сброса</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
private void HookEvents()
|
||||
{
|
||||
_dragDropManager.DragStarted += OnDragStarted;
|
||||
_dragDropManager.DragUpdated += OnDragUpdated;
|
||||
_dragDropManager.DragCompleted += OnDragCompleted;
|
||||
_dragDropManager.DragCancelled += OnDragCancelled;
|
||||
_dragDropManager.DropTargetChanged += OnDropTargetChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует визуальное представление перетаскиваемого элемента.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Создает Popup с Border для отображения полупрозрачной копии
|
||||
/// перетаскиваемого элемента во время операции drag-and-drop.
|
||||
/// </remarks>
|
||||
private void InitializeDragVisual()
|
||||
{
|
||||
// Создаем Popup для отображения визуального представления перетаскивания
|
||||
_dragVisualPopup = new Popup
|
||||
{
|
||||
IsHitTestVisible = false,
|
||||
IsLightDismissEnabled = false,
|
||||
Child = null
|
||||
};
|
||||
|
||||
// Создаем визуальный элемент для перетаскивания
|
||||
_dragVisual = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Microsoft.UI.Colors.Transparent),
|
||||
BorderBrush = new SolidColorBrush(Microsoft.UI.Colors.DodgerBlue),
|
||||
BorderThickness = new Thickness(2),
|
||||
CornerRadius = new CornerRadius(4),
|
||||
Opacity = 0.7
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует оверлей для отображения подсказок при сбросе.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Добавляет оверлей в корневой контейнер приложения для отображения
|
||||
/// визуальных подсказок о возможных позициях сброса.
|
||||
/// </remarks>
|
||||
private void InitializeDropHintOverlay()
|
||||
{
|
||||
// Создаем оверлей для подсказок при сбросе
|
||||
_dropHintOverlay = new DropHintOverlay();
|
||||
|
||||
// Добавляем оверлей в корневой контейнер приложения
|
||||
if (Window.Current?.Content is Panel rootPanel)
|
||||
{
|
||||
rootPanel.Children.Add(_dropHintOverlay);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Регистрирует связь между абстрактным контролом док-системы и конкретным UI-элементом WinUI.
|
||||
/// </summary>
|
||||
/// <param name="control">Абстрактный контрол док-системы.</param>
|
||||
/// <param name="element">Конкретный UI-элемент WinUI.</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, если <paramref name="control"/> или <paramref name="element"/> равны null.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// Эта связь необходима для:
|
||||
/// <list type="bullet">
|
||||
/// <item>Вычисления границ элемента на экране</item>
|
||||
/// <item>Создания визуального представления перетаскивания</item>
|
||||
/// <item>Определения позиции сброса относительно элемента</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public void RegisterControl(IDockControl control, FrameworkElement element)
|
||||
{
|
||||
if (control == null) throw new ArgumentNullException(nameof(control));
|
||||
if (element == null) throw new ArgumentNullException(nameof(element));
|
||||
|
||||
_controlToElement[control] = element;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отменяет регистрацию связи между абстрактным контролом док-системы и UI-элементом WinUI.
|
||||
/// </summary>
|
||||
/// <param name="control">Абстрактный контрол док-системы.</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, если <paramref name="control"/> равен null.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// Удаляет элемент из внутреннего словаря, освобождая связанные с ним ресурсы.
|
||||
/// </remarks>
|
||||
public void UnregisterControl(IDockControl control)
|
||||
{
|
||||
if (control == null) throw new ArgumentNullException(nameof(control));
|
||||
|
||||
_controlToElement.TryRemove(control, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Вычисляет границы элемента на экране.
|
||||
/// </summary>
|
||||
/// <param name="element">Элемент, для которого вычисляются границы.</param>
|
||||
/// <returns>
|
||||
/// Прямоугольник в экранных координатах, представляющий границы элемента.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Метод выполняет преобразование координат элемента в экранные координаты
|
||||
/// с использованием трансформации визуального дерева.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// В случае ошибки вычисления возвращает прямоугольник размером 100x100 пикселей
|
||||
/// в точке (0, 0).
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
protected override Rect CalculateBounds(IDockControl element)
|
||||
{
|
||||
if (_controlToElement.TryGetValue(element, out var uiElement))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Получаем преобразование координат в экранные
|
||||
var transform = uiElement.TransformToVisual(Window.Current.Content);
|
||||
var point = transform.TransformPoint(new Windows.Foundation.Point(0, 0));
|
||||
|
||||
return new Rect(
|
||||
point.X, point.Y,
|
||||
uiElement.ActualWidth, uiElement.ActualHeight);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Failed to calculate bounds: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Возвращаем значения по умолчанию, если не удалось вычислить
|
||||
return new Rect(0, 0, 100, 100);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создает визуальное представление перетаскиваемого элемента.
|
||||
/// </summary>
|
||||
/// <param name="dragInfo">Информация о перетаскивании.</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// На основе источника перетаскивания создает полупрозрачную копию элемента,
|
||||
/// которая следует за курсором мыши во время операции перетаскивания.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Визуальное представление включает:
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item>Тень для создания эффекта глубины</item>
|
||||
/// <item>Прозрачность для видимости фонового содержимого</item>
|
||||
/// <item>Синюю границу для визуального выделения</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
protected override void CreateDragVisual(UiDragInfo dragInfo)
|
||||
{
|
||||
if (_dragVisual == null || _dragVisualPopup == null || dragInfo.SourceControl == null)
|
||||
return;
|
||||
|
||||
// Настраиваем визуальное представление на основе источника
|
||||
if (_controlToElement.TryGetValue(dragInfo.SourceControl, out var sourceElement))
|
||||
{
|
||||
// Устанавливаем размеры визуального представления
|
||||
_dragVisual.Width = sourceElement.ActualWidth;
|
||||
_dragVisual.Height = sourceElement.ActualHeight;
|
||||
|
||||
// Создаем эффект прозрачности и тени
|
||||
_dragVisual.Opacity = 0.7;
|
||||
|
||||
// Устанавливаем позицию Popup
|
||||
_dragVisualPopup.HorizontalOffset = dragInfo.BaseDragInfo.StartPosition.X;
|
||||
_dragVisualPopup.VerticalOffset = dragInfo.BaseDragInfo.StartPosition.Y;
|
||||
_dragVisualPopup.Child = _dragVisual;
|
||||
_dragVisualPopup.IsOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Обновляет позицию визуального представления перетаскивания.
|
||||
/// </summary>
|
||||
/// <param name="position">Новая позиция курсора.</param>
|
||||
/// <remarks>
|
||||
/// Перемещает Popup с визуальным представлением в указанную позицию,
|
||||
/// обеспечивая плавное следование за курсором мыши.
|
||||
/// </remarks>
|
||||
protected override void UpdateDragVisualPosition(Point position)
|
||||
{
|
||||
if (_dragVisualPopup != null)
|
||||
{
|
||||
_dragVisualPopup.HorizontalOffset = position.X;
|
||||
_dragVisualPopup.VerticalOffset = position.Y;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Очищает визуальное представление перетаскивания.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Скрывает и освобождает ресурсы Popup, используемого для отображения
|
||||
/// визуального представления перетаскиваемого элемента.
|
||||
/// </remarks>
|
||||
protected override void CleanupDragVisual()
|
||||
{
|
||||
if (_dragVisualPopup != null)
|
||||
{
|
||||
_dragVisualPopup.IsOpen = false;
|
||||
_dragVisualPopup.Child = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Показывает визуальную подсказку о возможной позиции сброса.
|
||||
/// </summary>
|
||||
/// <param name="element">Элемент, для которого показывается подсказка.</param>
|
||||
/// <param name="position">Предполагаемая позиция сброса.</param>
|
||||
/// <remarks>
|
||||
/// Отображает полупрозрачный прямоугольник в указанной позиции относительно элемента,
|
||||
/// давая пользователю визуальную обратную связь о том, куда будет помещен элемент.
|
||||
/// </remarks>
|
||||
protected override void ShowDropHint(IDockControl element, DropPosition position)
|
||||
{
|
||||
_dropHintOverlay?.ShowHint(element, position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Скрывает текущую визуальную подсказку о сбросе.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Убирает все отображаемые подсказки сброса, очищая оверлей.
|
||||
/// </remarks>
|
||||
protected override void HideDropHint()
|
||||
{
|
||||
_dropHintOverlay?.HideHint();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Освобождает ресурсы, используемые сервисом перетаскивания.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Выполняет следующие действия:
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item>Отписывается от всех событий менеджера перетаскивания</item>
|
||||
/// <item>Удаляет оверлей подсказок из корневого контейнера</item>
|
||||
/// <item>Очищает словарь зарегистрированных контролов</item>
|
||||
/// <item>Освобождает визуальные элементы</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (_dragDropManager != null)
|
||||
{
|
||||
_dragDropManager.DragStarted -= OnDragStarted;
|
||||
_dragDropManager.DragUpdated -= OnDragUpdated;
|
||||
_dragDropManager.DragCompleted -= OnDragCompleted;
|
||||
_dragDropManager.DragCancelled -= OnDragCancelled;
|
||||
_dragDropManager.DropTargetChanged -= OnDropTargetChanged;
|
||||
}
|
||||
|
||||
if (_dropHintOverlay != null && Window.Current?.Content is Panel rootPanel)
|
||||
{
|
||||
rootPanel.Children.Remove(_dropHintOverlay);
|
||||
_dropHintOverlay = null;
|
||||
}
|
||||
|
||||
_controlToElement.Clear();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Представляет оверлей для отображения визуальных подсказок при сбросе в операции перетаскивания.
|
||||
/// Этот элемент отображает полупрозрачные прямоугольники в местах возможного сброса,
|
||||
/// давая пользователю визуальную обратную связь о допустимых позициях.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="DropHintOverlay"/> является внутренним вспомогательным классом,
|
||||
/// который отображается поверх всего пользовательского интерфейса во время операции
|
||||
/// перетаскивания для показа визуальных подсказок.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Оверлей поддерживает все позиции сброса, определенные в <see cref="DropPosition"/>,
|
||||
/// и автоматически вычисляет размеры и положение подсказок на основе целевого элемента.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed class DropHintOverlay : Grid
|
||||
{
|
||||
private readonly Dictionary<DropPosition, Border> _hintRectangles = new();
|
||||
private readonly SolidColorBrush _hintBrush;
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый экземпляр класса <see cref="DropHintOverlay"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Создает прозрачный оверлей, который не участвует в тестировании попаданий,
|
||||
/// и инициализирует прямоугольники для всех возможных позиций сброса.
|
||||
/// </remarks>
|
||||
public DropHintOverlay()
|
||||
{
|
||||
this.IsHitTestVisible = false;
|
||||
this.Background = new SolidColorBrush(Microsoft.UI.Colors.Transparent);
|
||||
|
||||
// Используем акцентный цвет для подсказок
|
||||
_hintBrush = new SolidColorBrush(Microsoft.UI.Colors.DodgerBlue);
|
||||
|
||||
InitializeHintRectangles();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует прямоугольники для всех позиций сброса.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Создает отдельный Border для каждой позиции сброса и добавляет их в дочернюю коллекцию.
|
||||
/// Все прямоугольники изначально скрыты и отображаются только при необходимости.
|
||||
/// </remarks>
|
||||
private void InitializeHintRectangles()
|
||||
{
|
||||
// Создаем прямоугольники для каждой позиции сброса
|
||||
var positions = new[]
|
||||
{
|
||||
DropPosition.Left, DropPosition.Right,
|
||||
DropPosition.Top, DropPosition.Bottom,
|
||||
DropPosition.Center, DropPosition.Tab
|
||||
};
|
||||
|
||||
foreach (var position in positions)
|
||||
{
|
||||
var rect = new Border
|
||||
{
|
||||
Background = _hintBrush,
|
||||
Opacity = 0.3,
|
||||
BorderBrush = new SolidColorBrush(Microsoft.UI.Colors.DodgerBlue),
|
||||
BorderThickness = new Thickness(2),
|
||||
Visibility = Visibility.Collapsed,
|
||||
CornerRadius = new CornerRadius(4)
|
||||
};
|
||||
|
||||
_hintRectangles[position] = rect;
|
||||
this.Children.Add(rect);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Показывает визуальную подсказку для указанного элемента и позиции сброса.
|
||||
/// </summary>
|
||||
/// <param name="element">Элемент, для которого показывается подсказка.</param>
|
||||
/// <param name="position">Позиция сброса относительно элемента.</param>
|
||||
/// <remarks>
|
||||
/// Вычисляет положение и размер подсказки на основе границ элемента и позиции сброса,
|
||||
/// затем делает соответствующий прямоугольник видимым.
|
||||
/// </remarks>
|
||||
public void ShowHint(IDockControl element, DropPosition position)
|
||||
{
|
||||
if (element is not FrameworkElement uiElement) return;
|
||||
if (!_hintRectangles.TryGetValue(position, out var rect)) return;
|
||||
|
||||
// Вычисляем позицию и размер подсказки
|
||||
var bounds = CalculateHintBounds(uiElement, position);
|
||||
|
||||
Canvas.SetLeft(rect, bounds.X);
|
||||
Canvas.SetTop(rect, bounds.Y);
|
||||
rect.Width = bounds.Width;
|
||||
rect.Height = bounds.Height;
|
||||
rect.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Вычисляет границы подсказки для указанного элемента и позиции сброса.
|
||||
/// </summary>
|
||||
/// <param name="element">Целевой элемент.</param>
|
||||
/// <param name="position">Позиция сброса.</param>
|
||||
/// <returns>Прямоугольник с координатами и размерами подсказки.</returns>
|
||||
/// <remarks>
|
||||
/// Размеры подсказок зависят от позиции:
|
||||
/// <list type="bullet">
|
||||
/// <item>Слева/справа: ширина 50px, высота равна высоте элемента</item>
|
||||
/// <item>Сверху/снизу: высота 50px, ширина равна ширине элемента</item>
|
||||
/// <item>В центре: размеры равны размерам элемента</item>
|
||||
/// <item>Вкладка: высота 30px, ширина равна ширине элемента, позиция сверху</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
private Rect CalculateHintBounds(FrameworkElement element, DropPosition position)
|
||||
{
|
||||
// Получаем позицию элемента относительно оверлея
|
||||
var transform = element.TransformToVisual(this);
|
||||
var point = transform.TransformPoint(new Windows.Foundation.Point(0, 0));
|
||||
|
||||
// Вычисляем размеры подсказки в зависимости от позиции
|
||||
return position switch
|
||||
{
|
||||
DropPosition.Left => new Rect(
|
||||
point.X - 50, point.Y,
|
||||
50, element.ActualHeight),
|
||||
|
||||
DropPosition.Right => new Rect(
|
||||
point.X + element.ActualWidth, point.Y,
|
||||
50, element.ActualHeight),
|
||||
|
||||
DropPosition.Top => new Rect(
|
||||
point.X, point.Y - 50,
|
||||
element.ActualWidth, 50),
|
||||
|
||||
DropPosition.Bottom => new Rect(
|
||||
point.X, point.Y + element.ActualHeight,
|
||||
element.ActualWidth, 50),
|
||||
|
||||
DropPosition.Center => new Rect(
|
||||
point.X, point.Y,
|
||||
element.ActualWidth, element.ActualHeight),
|
||||
|
||||
DropPosition.Tab => new Rect(
|
||||
point.X, point.Y - 30,
|
||||
element.ActualWidth, 30),
|
||||
|
||||
_ => new Rect(point.X, point.Y, 100, 100)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Скрывает все визуальные подсказки.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Делает все прямоугольники подсказок невидимыми, очищая оверлей.
|
||||
/// </remarks>
|
||||
public void HideHint()
|
||||
{
|
||||
foreach (var rect in _hintRectangles.Values)
|
||||
{
|
||||
rect.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user