Compare commits
3 Commits
a902474345
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e8b4cb9881 | |||
| 584df249f6 | |||
| 33abd94f6e |
@@ -1,8 +1,23 @@
|
|||||||
namespace Lattice.Core.Docking.Abstractions;
|
namespace Lattice.Core.Docking.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет контракт для команды в системе докинга.
|
||||||
|
/// Команды представляют действия, которые могут быть выполнены над элементами док-системы.
|
||||||
|
/// </summary>
|
||||||
public interface IDockCommand : System.Windows.Input.ICommand
|
public interface IDockCommand : System.Windows.Input.ICommand
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получает отображаемое имя команды.
|
||||||
|
/// </summary>
|
||||||
string Name { get; }
|
string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает идентификатор ресурса для иконки команды.
|
||||||
|
/// </summary>
|
||||||
string Icon { get; }
|
string Icon { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает текстовое представление жеста (горячей клавиши) для команды.
|
||||||
|
/// </summary>
|
||||||
string GestureText { get; }
|
string GestureText { get; }
|
||||||
}
|
}
|
||||||
@@ -3,22 +3,36 @@
|
|||||||
namespace Lattice.Core.Docking.Abstractions;
|
namespace Lattice.Core.Docking.Abstractions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Интерфейс для элементов (листьев дерева), которые физически содержат внутри себя коллекцию вкладок.
|
/// Определяет контракт для контейнеров, содержащих коллекцию вкладок.
|
||||||
|
/// Контейнеры являются листьями дерева компоновки и непосредственно отображают содержимое.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IDockContainer : IDockElement
|
public interface IDockContainer : IDockElement
|
||||||
{
|
{
|
||||||
/// <summary> Список вкладок, находящихся в данном контейнере. </summary>
|
/// <summary>
|
||||||
|
/// Получает список вкладок, находящихся в данном контейнере.
|
||||||
|
/// </summary>
|
||||||
IList<IDockContent> Children { get; }
|
IList<IDockContent> Children { get; }
|
||||||
|
|
||||||
/// <summary> Ссылка на текущую выбранную и отображаемую вкладку. </summary>
|
/// <summary>
|
||||||
|
/// Получает или задает текущую активную (выбранную) вкладку.
|
||||||
|
/// </summary>
|
||||||
IDockContent? ActiveContent { get; set; }
|
IDockContent? ActiveContent { get; set; }
|
||||||
|
|
||||||
/// <summary> Добавляет контент в контейнер и делает его активным. </summary>
|
/// <summary>
|
||||||
|
/// Добавляет контент в контейнер и делает его активным.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">Контент для добавления.</param>
|
||||||
void AddContent(IDockContent content);
|
void AddContent(IDockContent content);
|
||||||
|
|
||||||
/// <summary> Удаляет контент. Если Children становится пустым, контейнер может быть удален из дерева макета. </summary>
|
/// <summary>
|
||||||
|
/// Удаляет контент из контейнера. Если коллекция становится пустой,
|
||||||
|
/// контейнер может быть удален из дерева макета.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">Контент для удаления.</param>
|
||||||
void RemoveContent(IDockContent content);
|
void RemoveContent(IDockContent content);
|
||||||
|
|
||||||
/// <summary> Положение вкладок в интерфейсе. </summary>
|
/// <summary>
|
||||||
|
/// Получает или задает положение панели вкладок в интерфейсе.
|
||||||
|
/// </summary>
|
||||||
TabPlacement TabPlacement { get; set; }
|
TabPlacement TabPlacement { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,43 @@
|
|||||||
namespace Lattice.Core.Docking.Abstractions;
|
namespace Lattice.Core.Docking.Abstractions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Описывает объект содержимого (вкладку), который может быть размещен внутри IDockContainer.
|
/// Определяет контракт для содержимого (вкладки), которое может быть размещено внутри контейнера.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IDockContent
|
public interface IDockContent
|
||||||
{
|
{
|
||||||
/// <summary> Уникальный идентификатор контента (например, путь к файлу или ID инструмента). </summary>
|
/// <summary>
|
||||||
|
/// Получает уникальный идентификатор контента.
|
||||||
|
/// Используется для идентификации вкладки в системе.
|
||||||
|
/// </summary>
|
||||||
string Id { get; }
|
string Id { get; }
|
||||||
|
|
||||||
/// <summary> Заголовок, отображаемый пользователю в интерфейсе (на вкладке). </summary>
|
/// <summary>
|
||||||
|
/// Устанавливает идентификатор контента.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">Новый идентификатор.</param>
|
||||||
|
void SetId(string id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает заголовок, отображаемый пользователю на вкладке.
|
||||||
|
/// </summary>
|
||||||
string Title { get; }
|
string Title { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Сам визуальный элемент (например, Microsoft.UI.Xaml.UIElement).
|
/// Получает или задает визуальный элемент для отображения в теле вкладки.
|
||||||
/// Lattice просто отображает этот объект в теле вкладки.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
object View { get; set; }
|
object View { get; set; }
|
||||||
|
|
||||||
/// <summary> Флаг, определяющий доступность кнопки закрытия для пользователя. </summary>
|
/// <summary>
|
||||||
|
/// Получает значение, указывающее, можно ли закрыть вкладку.
|
||||||
|
/// </summary>
|
||||||
bool CanClose { get; }
|
bool CanClose { get; }
|
||||||
|
|
||||||
/// <summary> Вызывается системой при попытке закрытия контента. Возвращает true, если закрытие разрешено. </summary>
|
/// <summary>
|
||||||
|
/// Вызывается системой при попытке закрытия контента.
|
||||||
|
/// Позволяет выполнить дополнительные проверки или сохранить состояние.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// true, если закрытие разрешено; в противном случае false.
|
||||||
|
/// </returns>
|
||||||
bool OnClosing();
|
bool OnClosing();
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,91 @@
|
|||||||
namespace Lattice.Core.Docking.Abstractions;
|
namespace Lattice.Core.Docking.Abstractions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Базовый интерфейс для любого элемента, который может быть частью дерева компоновки Lattice.
|
/// Базовый интерфейс для любого элемента, являющегося частью дерева компоновки.
|
||||||
|
/// Определяет общие свойства и методы для всех элементов док-системы.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Элементы док-системы образуют древовидную структуру, где каждый элемент может иметь
|
||||||
|
/// родителя и дочерние элементы. Эта иерархия используется для организации пространства
|
||||||
|
/// главного окна и плавающих окон в IDE-подобных приложениях.
|
||||||
|
/// </remarks>
|
||||||
public interface IDockElement
|
public interface IDockElement
|
||||||
{
|
{
|
||||||
/// <summary> Уникальный идентификатор элемента. </summary>
|
/// <summary>
|
||||||
|
/// Получает уникальный идентификатор элемента.
|
||||||
|
/// Используется для поиска элементов, сериализации состояния и отслеживания изменений.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Строковый идентификатор, гарантированно уникальный в пределах дерева компоновки.
|
||||||
|
/// Обычно представляет собой GUID в строковом формате.
|
||||||
|
/// </value>
|
||||||
string Id { get; }
|
string Id { get; }
|
||||||
|
|
||||||
/// <summary> Родительский элемент в иерархии. Если null — элемент является корневым. </summary>
|
/// <summary>
|
||||||
|
/// Получает или задает родительский элемент в иерархии дерева компоновки.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Родительский элемент или null, если элемент является корневым.
|
||||||
|
/// Это свойство управляется системой компоновки при добавлении или удалении элементов.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Изменение этого свойства вручную может привести к нарушению целостности дерева.
|
||||||
|
/// Для манипуляции структурой дерева следует использовать методы <see cref="DockOperations"/>.
|
||||||
|
/// </remarks>
|
||||||
IDockElement? Parent { get; set; }
|
IDockElement? Parent { get; set; }
|
||||||
|
|
||||||
/// <summary> Желаемая ширина элемента в относительных или абсолютных единицах. </summary>
|
/// <summary>
|
||||||
|
/// Получает или задает желаемую ширину элемента.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Ширина элемента в пикселях или относительных единицах.
|
||||||
|
/// Может быть выражена как абсолютное значение (в пикселях) или как пропорция
|
||||||
|
/// (например, 0.5 для 50% доступного пространства).
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Фактическая ширина элемента определяется родительским контейнером с учетом
|
||||||
|
/// минимальных размеров и соотношений разделения.
|
||||||
|
/// </remarks>
|
||||||
double Width { get; set; }
|
double Width { get; set; }
|
||||||
|
|
||||||
/// <summary> Желаемая высота элемента в относительных или абсолютных единицах. </summary>
|
/// <summary>
|
||||||
|
/// Получает или задает желаемую высоту элемента.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Высота элемента в пикселях или относительных единицах.
|
||||||
|
/// Может быть выражена как абсолютное значение (в пикселях) или как пропорция.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Фактическая высота элемента определяется родительским контейнером с учетом
|
||||||
|
/// минимальных размеров и соотношений разделения.
|
||||||
|
/// </remarks>
|
||||||
double Height { get; set; }
|
double Height { get; set; }
|
||||||
|
|
||||||
/// <summary> Минимально допустимая ширина, при которой элемент сохраняет функциональность. </summary>
|
/// <summary>
|
||||||
|
/// Получает минимально допустимую ширину элемента.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Минимальная ширина элемента в пикселях, при которой элемент сохраняет
|
||||||
|
/// базовую функциональность и читаемость содержимого.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Система компоновки не позволит уменьшить элемент ниже этого значения.
|
||||||
|
/// Для групп разделения минимальная ширина вычисляется рекурсивно на основе
|
||||||
|
/// минимальных размеров дочерних элементов.
|
||||||
|
/// </remarks>
|
||||||
double MinWidth { get; }
|
double MinWidth { get; }
|
||||||
|
|
||||||
/// <summary> Минимально допустимая высота, при которой элемент сохраняет функциональность. </summary>
|
/// <summary>
|
||||||
|
/// Получает минимально допустимую высоту элемента.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Минимальная высота элемента в пикселях, при которой элемент сохраняет
|
||||||
|
/// базовую функциональность и читаемость содержимого.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Система компоновки не позволит уменьшить элемент ниже этого значения.
|
||||||
|
/// Для групп разделения минимальная высота вычисляется рекурсивно на основе
|
||||||
|
/// минимальных размеров дочерних элементов.
|
||||||
|
/// </remarks>
|
||||||
double MinHeight { get; }
|
double MinHeight { get; }
|
||||||
}
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
using Lattice.Core.DragDrop.Abstractions;
|
|
||||||
|
|
||||||
namespace Lattice.Core.Docking.Abstractions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Расширяет интерфейс элемента док-системы для поддержки операций перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public interface IDockElementDragSource : IDockElement, IDragSource
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или устанавливает признак того, что элемент можно перетаскивать.
|
|
||||||
/// </summary>
|
|
||||||
bool CanDrag { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает тип данных для перетаскивания этого элемента.
|
|
||||||
/// </summary>
|
|
||||||
string DragDataType { get; }
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
using Lattice.Core.DragDrop.Abstractions;
|
|
||||||
|
|
||||||
namespace Lattice.Core.Docking.Abstractions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Расширяет интерфейс элемента док-системы для возможности быть целью сброса.
|
|
||||||
/// </summary>
|
|
||||||
public interface IDockElementDropTarget : IDockElement, IDropTarget
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или устанавливает признак того, что элемент может принимать сброс.
|
|
||||||
/// </summary>
|
|
||||||
bool CanDrop { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает типы данных, которые может принимать элемент.
|
|
||||||
/// </summary>
|
|
||||||
IEnumerable<string> AcceptableDropTypes { get; }
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
using Lattice.Core.Docking.Models;
|
|
||||||
using Lattice.Core.Geometry;
|
|
||||||
|
|
||||||
namespace Lattice.Core.Docking.Abstractions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Предоставляет абстракцию для операции перетаскивания в док-системе.
|
|
||||||
/// Эта абстракция позволяет отделить логику перетаскивания от конкретной UI-платформы.
|
|
||||||
/// </summary>
|
|
||||||
public interface IDragService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Начинает операцию перетаскивания указанного элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="element">Элемент для перетаскивания.</param>
|
|
||||||
/// <param name="visualFeedback">Визуальная обратная связь (зависит от платформы).</param>
|
|
||||||
void StartDrag(IDockElement element, object? visualFeedback = null);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Обновляет позицию перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">Координата X.</param>
|
|
||||||
/// <param name="y">Координата Y.</param>
|
|
||||||
void UpdateDrag(double x, double y);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Завершает операцию перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">Координата X завершения.</param>
|
|
||||||
/// <param name="y">Координата Y завершения.</param>
|
|
||||||
void EndDrag(double x, double y);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Отменяет операцию перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
void CancelDrag();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Представляет область для сброса при операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public class DropArea
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Целевой элемент для сброса.
|
|
||||||
/// </summary>
|
|
||||||
public IDockElement Target { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Позиция сброса относительно цели.
|
|
||||||
/// </summary>
|
|
||||||
public DockPosition Position { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Границы области в экранных координатах.
|
|
||||||
/// </summary>
|
|
||||||
public Rect Bounds { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Видимость области (для анимации).
|
|
||||||
/// </summary>
|
|
||||||
public double Visibility { get; set; } = 0.0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр области сброса.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="target">Целевой элемент.</param>
|
|
||||||
/// <param name="position">Позиция сброса.</param>
|
|
||||||
/// <param name="bounds">Границы области.</param>
|
|
||||||
public DropArea(IDockElement target, DockPosition position, Rect bounds)
|
|
||||||
{
|
|
||||||
Target = target;
|
|
||||||
Position = position;
|
|
||||||
Bounds = bounds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,20 +4,30 @@ using Lattice.Core.Docking.Models;
|
|||||||
namespace Lattice.Core.Docking.Engine;
|
namespace Lattice.Core.Docking.Engine;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Статический движок для манипуляции иерархией дерева компоновки.
|
/// Предоставляет статические методы для манипуляции иерархией дерева компоновки.
|
||||||
/// Содержит чистые алгоритмы трансформации графа.
|
/// Содержит чистые алгоритмы трансформации графа без зависимости от UI.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class DockOperations
|
public static class DockOperations
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Извлекает элемент из дерева. Если родительская группа остается с одним ребенком,
|
/// Извлекает элемент из дерева компоновки.
|
||||||
/// она удаляется, а ребенок занимает её место.
|
/// Если родительская группа остается с одним ребенком, она удаляется,
|
||||||
|
/// а оставшийся ребенок занимает её место в иерархии.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="element">Элемент для удаления.</param>
|
/// <param name="element">Элемент для удаления из дерева.</param>
|
||||||
/// <param name="root">Текущий корень дерева.</param>
|
/// <param name="root">Текущий корневой элемент дерева.</param>
|
||||||
/// <returns>Новый корень дерева после оптимизации.</returns>
|
/// <returns>
|
||||||
|
/// Новый корневой элемент дерева после удаления и оптимизации структуры.
|
||||||
|
/// Возвращает null, если дерево становится пустым.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, когда <paramref name="root"/> равен null.
|
||||||
|
/// </exception>
|
||||||
public static IDockElement? Remove(IDockElement element, IDockElement root)
|
public static IDockElement? Remove(IDockElement element, IDockElement root)
|
||||||
{
|
{
|
||||||
|
if (element == null) throw new ArgumentNullException(nameof(element));
|
||||||
|
if (root == null) throw new ArgumentNullException(nameof(root));
|
||||||
|
|
||||||
if (element == root) return null;
|
if (element == root) return null;
|
||||||
|
|
||||||
var parent = element.Parent as DockGroup;
|
var parent = element.Parent as DockGroup;
|
||||||
@@ -43,15 +53,36 @@ public static class DockOperations
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Вставляет элемент в дерево, создавая новую группу разделения или объединяя контент.
|
/// Вставляет элемент в дерево компоновки относительно целевого элемента.
|
||||||
|
/// Создает новую группу разделения или объединяет контент в зависимости от позиции.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IDockElement Insert(IDockElement target, IDockElement source, DockPosition pos, IDockElement root)
|
/// <param name="target">Целевой элемент, относительно которого выполняется вставка.</param>
|
||||||
|
/// <param name="source">Вставляемый элемент.</param>
|
||||||
|
/// <param name="pos">Позиция вставки относительно целевого элемента.</param>
|
||||||
|
/// <param name="root">Текущий корневой элемент дерева.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// Новый корневой элемент дерева после вставки и оптимизации структуры.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, когда <paramref name="target"/>, <paramref name="source"/>
|
||||||
|
/// или <paramref name="root"/> равны null.
|
||||||
|
/// </exception>
|
||||||
|
/// <exception cref="ArgumentException">
|
||||||
|
/// Выбрасывается, когда <paramref name="pos"/> имеет недопустимое значение.
|
||||||
|
/// </exception>
|
||||||
|
public static IDockElement Insert(IDockElement target, IDockElement source,
|
||||||
|
DockPosition pos, IDockElement root)
|
||||||
{
|
{
|
||||||
|
if (target == null) throw new ArgumentNullException(nameof(target));
|
||||||
|
if (source == null) throw new ArgumentNullException(nameof(source));
|
||||||
|
if (root == null) throw new ArgumentNullException(nameof(root));
|
||||||
|
|
||||||
// Случай 1: Объединение вкладок в центре
|
// Случай 1: Объединение вкладок в центре
|
||||||
if (pos == DockPosition.Center)
|
if (pos == DockPosition.Center)
|
||||||
{
|
{
|
||||||
if (target is IDockContainer targetContainer && source is IDockContainer sourceContainer)
|
if (target is IDockContainer targetContainer && source is IDockContainer sourceContainer)
|
||||||
{
|
{
|
||||||
|
// Переносим все вкладки из источника в целевой контейнер
|
||||||
var items = new List<IDockContent>(sourceContainer.Children);
|
var items = new List<IDockContent>(sourceContainer.Children);
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
@@ -83,7 +114,16 @@ public static class DockOperations
|
|||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Если target был корнем, новая группа становится новым корнем
|
||||||
|
if (target == root)
|
||||||
|
{
|
||||||
|
newGroup.Parent = null;
|
||||||
|
return newGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Эта точка недостижима при правильном использовании,
|
||||||
|
// но добавляем для безопасности
|
||||||
newGroup.Parent = null;
|
newGroup.Parent = null;
|
||||||
return newGroup; // Новая группа стала корнем
|
return newGroup;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,61 +1,69 @@
|
|||||||
using Lattice.Core.Docking.Abstractions;
|
using Lattice.Core.Docking.Abstractions;
|
||||||
using Lattice.Core.Docking.Models;
|
using Lattice.Core.Docking.Models;
|
||||||
|
using Lattice.Core.Docking.Services;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("Lattice.Serialization.Docking")]
|
[assembly: InternalsVisibleTo("Lattice.UI.Docking.WinUI")]
|
||||||
|
|
||||||
namespace Lattice.Core.Docking.Engine;
|
namespace Lattice.Core.Docking.Engine;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Расширенный менеджер макета, поддерживающий автоскрываемые панели, группы документов
|
/// Центральный менеджер макета, управляющий всей структурой док-системы.
|
||||||
/// и расширенные операции управления макетом.
|
/// Координирует дерево компоновки, плавающие окна, автоскрываемые панели
|
||||||
|
/// и предоставляет API для манипуляции макетом. Использует кэширование
|
||||||
|
/// для оптимизации поиска элементов по идентификатору.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
|
||||||
/// Этот класс является центральным координатором всей док-системы, управляя деревом компоновки,
|
|
||||||
/// плавающими окнами, автоскрываемыми панелями и предоставляя API для манипуляции макетом.
|
|
||||||
/// </remarks>
|
|
||||||
public class LayoutManager
|
public class LayoutManager
|
||||||
{
|
{
|
||||||
private readonly ObservableCollection<AutoHidePanel> _autoHidePanels = new();
|
private readonly ObservableCollection<AutoHidePanel> _autoHidePanels = new();
|
||||||
|
private readonly Dictionary<string, IDockElement> _elementCache = new();
|
||||||
|
private IDockElement? _root;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Корневой элемент главного окна IDE.
|
/// Получает или задает корневой элемент дерева компоновки главного окна.
|
||||||
|
/// При изменении значения генерируется событие <see cref="LayoutUpdated"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IDockElement? Root { get; internal set; }
|
public IDockElement? Root
|
||||||
|
{
|
||||||
|
get => _root;
|
||||||
|
internal set
|
||||||
|
{
|
||||||
|
if (_root != value)
|
||||||
|
{
|
||||||
|
_root = value;
|
||||||
|
LayoutUpdated?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Список активных плавающих окон.
|
/// Получает список активных плавающих окон.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<DockWindow> FloatingWindows { get; } = new();
|
public List<DockWindow> FloatingWindows { get; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Коллекция автоскрываемых панелей.
|
/// Получает коллекцию автоскрываемых панелей.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ReadOnlyObservableCollection<AutoHidePanel> AutoHidePanels { get; }
|
public ReadOnlyObservableCollection<AutoHidePanel> AutoHidePanels { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Реестр типов контента (опционально).
|
/// Получает или задает реестр типов контента.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Services.ContentRegistry? ContentRegistry { get; set; }
|
public ContentRegistry? ContentRegistry { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Уведомляет UI, что структура дерева изменилась.
|
/// Происходит при изменении структуры дерева компоновки.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action? LayoutUpdated;
|
public event Action? LayoutUpdated;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Уведомляет об изменении в коллекции автоскрываемых панелей.
|
/// Происходит при изменении коллекции автоскрываемых панелей.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event EventHandler? AutoHidePanelsChanged;
|
public event EventHandler? AutoHidePanelsChanged;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Событие, возникающее при операции перетаскивания элемента.
|
/// Инициализирует новый экземпляр класса <see cref="LayoutManager"/>.
|
||||||
/// </summary>
|
|
||||||
public event EventHandler<DragDropEventArgs>? DragDropOperation;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр менеджера макета.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public LayoutManager()
|
public LayoutManager()
|
||||||
{
|
{
|
||||||
@@ -63,13 +71,16 @@ public class LayoutManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Добавляет автоскрываемую панель.
|
/// Добавляет автоскрываемую панель с указанным содержимым к заданной стороне окна.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="content">Содержимое панели.</param>
|
/// <param name="content">Содержимое панели.</param>
|
||||||
/// <param name="side">Сторона для прикрепления.</param>
|
/// <param name="side">Сторона окна для прикрепления панели.</param>
|
||||||
/// <returns>Созданная автоскрываемая панель.</returns>
|
/// <returns>Созданная автоскрываемая панель.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">Выбрасывается, когда <paramref name="content"/> равен null.</exception>
|
||||||
public AutoHidePanel AddAutoHidePanel(IDockContent content, DockSide side)
|
public AutoHidePanel AddAutoHidePanel(IDockContent content, DockSide side)
|
||||||
{
|
{
|
||||||
|
if (content == null) throw new ArgumentNullException(nameof(content));
|
||||||
|
|
||||||
var panel = new AutoHidePanel(content, side);
|
var panel = new AutoHidePanel(content, side);
|
||||||
_autoHidePanels.Add(panel);
|
_autoHidePanels.Add(panel);
|
||||||
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
|
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
|
||||||
@@ -77,23 +88,29 @@ public class LayoutManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Удаляет автоскрываемую панель.
|
/// Удаляет автоскрываемую панель из коллекции.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="panel">Панель для удаления.</param>
|
/// <param name="panel">Панель для удаления.</param>
|
||||||
public void RemoveAutoHidePanel(AutoHidePanel panel)
|
/// <returns>true, если панель была успешно удалена; в противном случае false.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">Выбрасывается, когда <paramref name="panel"/> равен null.</exception>
|
||||||
|
public bool RemoveAutoHidePanel(AutoHidePanel panel)
|
||||||
{
|
{
|
||||||
|
if (panel == null) throw new ArgumentNullException(nameof(panel));
|
||||||
|
|
||||||
if (_autoHidePanels.Remove(panel))
|
if (_autoHidePanels.Remove(panel))
|
||||||
{
|
{
|
||||||
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
|
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Создает документ из зарегистрированного типа контента.
|
/// Создает документ указанного типа контента с заданным идентификатором.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="contentTypeId">Идентификатор типа контента.</param>
|
/// <param name="contentTypeId">Идентификатор типа контента.</param>
|
||||||
/// <param name="id">Уникальный идентификатор документа.</param>
|
/// <param name="id">Уникальный идентификатор документа.</param>
|
||||||
/// <returns>Созданный контент или null, если ContentRegistry не установлен.</returns>
|
/// <returns>Созданный контент или null, если ContentRegistry не установлен или тип контента не зарегистрирован.</returns>
|
||||||
public IDockContent? CreateDocument(string contentTypeId, string id)
|
public IDockContent? CreateDocument(string contentTypeId, string id)
|
||||||
{
|
{
|
||||||
if (ContentRegistry == null || !ContentRegistry.IsRegistered(contentTypeId))
|
if (ContentRegistry == null || !ContentRegistry.IsRegistered(contentTypeId))
|
||||||
@@ -103,16 +120,17 @@ public class LayoutManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Основной метод перемещения элементов в макете.
|
/// Выполняет перемещение элемента в макете относительно целевого элемента.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="source">Что перетаскиваем.</param>
|
/// <param name="source">Перемещаемый элемент.</param>
|
||||||
/// <param name="target">Куда приземляем.</param>
|
/// <param name="target">Целевой элемент, относительно которого выполняется перемещение.</param>
|
||||||
/// <param name="position">Позиция относительно цели.</param>
|
/// <param name="position">Позиция перемещения относительно цели.</param>
|
||||||
/// <param name="asDocument">
|
/// <param name="asDocument">Если true, контент будет добавлен как документ в центральную область.</param>
|
||||||
/// Если true, контент будет добавлен как документ в центральную область.
|
/// <exception cref="ArgumentNullException">Выбрасывается, когда <paramref name="source"/> равен null.</exception>
|
||||||
/// </param>
|
public void Move(IDockElement source, IDockElement? target,
|
||||||
public void Move(IDockElement source, IDockElement? target, DockPosition position, bool asDocument = false)
|
DockPosition position, bool asDocument = false)
|
||||||
{
|
{
|
||||||
|
if (source == null) throw new ArgumentNullException(nameof(source));
|
||||||
if (source == target) return;
|
if (source == target) return;
|
||||||
|
|
||||||
// 1. Удаляем источник из текущего местоположения
|
// 1. Удаляем источник из текущего местоположения
|
||||||
@@ -142,16 +160,20 @@ public class LayoutManager
|
|||||||
|
|
||||||
if (!sourceRemoved) return;
|
if (!sourceRemoved) return;
|
||||||
|
|
||||||
// 2. Вставляем в цель
|
// Обновляем кэш - удаляем перемещенный элемент
|
||||||
|
_elementCache.Remove(source.Id);
|
||||||
|
|
||||||
|
// 2. Вставляем в новое место
|
||||||
if (target == null)
|
if (target == null)
|
||||||
{
|
{
|
||||||
FloatingWindows.Add(new DockWindow { Root = source as IDockElement });
|
// Создаем новое плавающее окно
|
||||||
|
FloatingWindows.Add(new DockWindow { Root = source });
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (IsDescendantOf(target, Root))
|
if (Root != null && IsDescendantOf(target, Root))
|
||||||
{
|
{
|
||||||
Root = DockOperations.Insert(target, source, position, Root!);
|
Root = DockOperations.Insert(target, source, position, Root);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -159,9 +181,17 @@ public class LayoutManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обновляем кэш для вставленного элемента
|
||||||
|
_elementCache[source.Id] = source;
|
||||||
|
|
||||||
LayoutUpdated?.Invoke();
|
LayoutUpdated?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Удаляет элемент из всех плавающих окон.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="element">Элемент для удаления.</param>
|
||||||
|
/// <returns>true, если элемент был найден и удален; в противном случае false.</returns>
|
||||||
private bool RemoveFromFloatingWindows(IDockElement element)
|
private bool RemoveFromFloatingWindows(IDockElement element)
|
||||||
{
|
{
|
||||||
foreach (var win in FloatingWindows.ToArray())
|
foreach (var win in FloatingWindows.ToArray())
|
||||||
@@ -177,7 +207,14 @@ public class LayoutManager
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InsertIntoFloatingWindow(IDockElement target, IDockElement source, DockPosition position)
|
/// <summary>
|
||||||
|
/// Вставляет элемент в плавающее окно, содержащее целевой элемент.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="target">Целевой элемент в плавающем окне.</param>
|
||||||
|
/// <param name="source">Вставляемый элемент.</param>
|
||||||
|
/// <param name="position">Позиция вставки.</param>
|
||||||
|
private void InsertIntoFloatingWindow(IDockElement target, IDockElement source,
|
||||||
|
DockPosition position)
|
||||||
{
|
{
|
||||||
foreach (var win in FloatingWindows)
|
foreach (var win in FloatingWindows)
|
||||||
{
|
{
|
||||||
@@ -189,128 +226,96 @@ public class LayoutManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет, является ли элемент потомком указанного предка.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="element">Проверяемый элемент.</param>
|
||||||
|
/// <param name="ancestor">Предполагаемый предок.</param>
|
||||||
|
/// <returns>true, если элемент является потомком предка; в противном случае false.</returns>
|
||||||
private bool IsDescendantOf(IDockElement element, IDockElement ancestor)
|
private bool IsDescendantOf(IDockElement element, IDockElement ancestor)
|
||||||
{
|
{
|
||||||
if (element == ancestor) return true;
|
var current = element.Parent;
|
||||||
if (ancestor is DockGroup group)
|
while (current != null)
|
||||||
return IsDescendantOf(element, group.First) || IsDescendantOf(element, group.Second);
|
{
|
||||||
|
if (current == ancestor)
|
||||||
|
return true;
|
||||||
|
current = current.Parent;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Поиск элемента по ID во всех окнах. </summary>
|
/// <summary>
|
||||||
|
/// Находит элемент по его идентификатору во всех окнах (главном и плавающих).
|
||||||
|
/// Использует кэширование для оптимизации повторных поисков.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">Идентификатор элемента для поиска.</param>
|
||||||
|
/// <returns>Найденный элемент или null, если элемент с таким идентификатором не найден.</returns>
|
||||||
public IDockElement? FindById(string id)
|
public IDockElement? FindById(string id)
|
||||||
{
|
{
|
||||||
var found = FindRecursive(Root, id);
|
if (string.IsNullOrEmpty(id)) return null;
|
||||||
if (found != null) return found;
|
|
||||||
|
|
||||||
|
// Проверка кэша
|
||||||
|
if (_elementCache.TryGetValue(id, out var cached))
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
// Поиск в основном дереве
|
||||||
|
var found = FindRecursive(Root, id);
|
||||||
|
if (found != null)
|
||||||
|
{
|
||||||
|
_elementCache[id] = found;
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Поиск в плавающих окнах
|
||||||
foreach (var win in FloatingWindows)
|
foreach (var win in FloatingWindows)
|
||||||
{
|
{
|
||||||
found = FindRecursive(win.Root, id);
|
found = FindRecursive(win.Root, id);
|
||||||
if (found != null) return found;
|
if (found != null)
|
||||||
|
{
|
||||||
|
_elementCache[id] = found;
|
||||||
|
return found;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Рекурсивно ищет элемент по идентификатору в поддереве.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="node">Корневой узел поддерева для поиска.</param>
|
||||||
|
/// <param name="id">Идентификатор элемента для поиска.</param>
|
||||||
|
/// <returns>Найденный элемент или null, если элемент не найден.</returns>
|
||||||
private IDockElement? FindRecursive(IDockElement? node, string id)
|
private IDockElement? FindRecursive(IDockElement? node, string id)
|
||||||
{
|
{
|
||||||
if (node == null || node.Id == id) return node;
|
if (node == null) return null;
|
||||||
if (node is DockGroup g) return FindRecursive(g.First, id) ?? FindRecursive(g.Second, id);
|
if (node.Id == id) return node;
|
||||||
|
|
||||||
|
if (node is DockGroup g)
|
||||||
|
return FindRecursive(g.First, id) ?? FindRecursive(g.Second, id);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Сбрасывает макет к состоянию по умолчанию.
|
/// Сбрасывает макет к состоянию по умолчанию.
|
||||||
|
/// Очищает корневой элемент, плавающие окна, автоскрываемые панели и кэш.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
Root = null;
|
Root = null;
|
||||||
FloatingWindows.Clear();
|
FloatingWindows.Clear();
|
||||||
_autoHidePanels.Clear();
|
_autoHidePanels.Clear();
|
||||||
|
_elementCache.Clear();
|
||||||
LayoutUpdated?.Invoke();
|
LayoutUpdated?.Invoke();
|
||||||
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
|
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Обрабатывает операцию перетаскивания между элементами.
|
/// Находит элемент по идентификатору в дереве компоновки.
|
||||||
/// </summary>
|
|
||||||
/// <param name="source">Источник перетаскивания.</param>
|
|
||||||
/// <param name="target">Цель сброса.</param>
|
|
||||||
/// <param name="position">Позиция сброса относительно цели.</param>
|
|
||||||
/// <param name="data">Данные перетаскивания.</param>
|
|
||||||
/// <returns>true, если операция успешно выполнена; иначе false.</returns>
|
|
||||||
public bool HandleDragDrop(IDockElement source, IDockElement target,
|
|
||||||
DockPosition position, object data)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (source == target)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Определяем тип операции на основе данных
|
|
||||||
if (data is ContentDragData contentData)
|
|
||||||
{
|
|
||||||
return HandleContentDragDrop(contentData, target, position);
|
|
||||||
}
|
|
||||||
else if (data is DockElementDragData elementData)
|
|
||||||
{
|
|
||||||
return HandleElementDragDrop(elementData, target, position);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
DragDropOperation?.Invoke(this, new DragDropEventArgs(
|
|
||||||
source, target, position, false, ex.Message));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool HandleContentDragDrop(ContentDragData data, IDockElement target, DockPosition position)
|
|
||||||
{
|
|
||||||
// Находим исходный контейнер с контентом
|
|
||||||
var sourceContainer = FindElementById(data.ElementId) as IDockContainer;
|
|
||||||
if (sourceContainer == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Находим контент
|
|
||||||
var content = sourceContainer.Children.FirstOrDefault(c => c.Id == data.ContentId);
|
|
||||||
if (content == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (target is IDockContainer targetContainer && position == DockPosition.Center)
|
|
||||||
{
|
|
||||||
// Объединение вкладок
|
|
||||||
sourceContainer.RemoveContent(content);
|
|
||||||
targetContainer.AddContent(content);
|
|
||||||
|
|
||||||
DragDropOperation?.Invoke(this, new DragDropEventArgs(
|
|
||||||
sourceContainer as IDockElement ?? sourceContainer as IDockElement,
|
|
||||||
target, position, true, "Content merged"));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool HandleElementDragDrop(DockElementDragData data, IDockElement target, DockPosition position)
|
|
||||||
{
|
|
||||||
// Находим перетаскиваемый элемент
|
|
||||||
var sourceElement = FindElementById(data.ElementId);
|
|
||||||
if (sourceElement == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Выполняем перемещение
|
|
||||||
Move(sourceElement, target, position);
|
|
||||||
|
|
||||||
DragDropOperation?.Invoke(this, new DragDropEventArgs(
|
|
||||||
sourceElement, target, position, true, "Element moved"));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Находит элемент по идентификатору.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="id">Идентификатор элемента для поиска.</param>
|
||||||
|
/// <returns>Найденный элемент или null, если элемент с таким идентификатором не найден.</returns>
|
||||||
public IDockElement? FindElementById(string id)
|
public IDockElement? FindElementById(string id)
|
||||||
{
|
{
|
||||||
return FindElementByIdRecursive(Root, id) ??
|
return FindElementByIdRecursive(Root, id) ??
|
||||||
@@ -318,6 +323,12 @@ public class LayoutManager
|
|||||||
.FirstOrDefault(result => result != null);
|
.FirstOrDefault(result => result != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Рекурсивно ищет элемент по идентификатору в поддереве.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="element">Корневой элемент поддерева для поиска.</param>
|
||||||
|
/// <param name="id">Идентификатор элемента для поиска.</param>
|
||||||
|
/// <returns>Найденный элемент или null, если элемент не найден.</returns>
|
||||||
private IDockElement? FindElementByIdRecursive(IDockElement? element, string id)
|
private IDockElement? FindElementByIdRecursive(IDockElement? element, string id)
|
||||||
{
|
{
|
||||||
if (element == null) return null;
|
if (element == null) return null;
|
||||||
@@ -331,39 +342,4 @@ public class LayoutManager
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Аргументы события операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public class DragDropEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
/// <summary> Источник перетаскивания. </summary>
|
|
||||||
public IDockElement Source { get; }
|
|
||||||
|
|
||||||
/// <summary> Цель сброса. </summary>
|
|
||||||
public IDockElement Target { get; }
|
|
||||||
|
|
||||||
/// <summary> Позиция сброса. </summary>
|
|
||||||
public DockPosition Position { get; }
|
|
||||||
|
|
||||||
/// <summary> Показывает, была ли операция успешной. </summary>
|
|
||||||
public bool Success { get; }
|
|
||||||
|
|
||||||
/// <summary> Сообщение о результате операции. </summary>
|
|
||||||
public string Message { get; }
|
|
||||||
|
|
||||||
/// <summary> Время выполнения операции. </summary>
|
|
||||||
public DateTime Timestamp { get; }
|
|
||||||
|
|
||||||
public DragDropEventArgs(IDockElement source, IDockElement target,
|
|
||||||
DockPosition position, bool success, string message)
|
|
||||||
{
|
|
||||||
Source = source;
|
|
||||||
Target = target;
|
|
||||||
Position = position;
|
|
||||||
Success = success;
|
|
||||||
Message = message;
|
|
||||||
Timestamp = DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,5 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Lattice.Core.Geometry\Lattice.Core.Geometry.csproj" />
|
<ProjectReference Include="..\Lattice.Core.Geometry\Lattice.Core.Geometry.csproj" />
|
||||||
<ProjectReference Include="..\Lattice.Core.DragDrop\Lattice.Core.DragDrop.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,93 +1,62 @@
|
|||||||
using System.ComponentModel;
|
using Lattice.Core.Docking.Abstractions;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
namespace Lattice.Core.Docking.Models;
|
namespace Lattice.Core.Docking.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Представляет автоскрываемую панель, которая может быть прикреплена к одной из сторон окна.
|
/// Представляет автоскрываемую панель, которая может быть прикреплена к одной из сторон окна.
|
||||||
/// Автоскрываемые панели скрываются, оставляя только заголовок, и появляются при наведении курсора.
|
/// Автоскрываемые панели скрываются, оставляя видимой только заголовок, и разворачиваются при наведении курсора или клике.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
public class AutoHidePanel
|
||||||
/// Автоскрываемые панели являются ключевым элементом интерфейса современных IDE,
|
|
||||||
/// позволяя экономить пространство экрана при сохранении быстрого доступа к инструментам.
|
|
||||||
/// </remarks>
|
|
||||||
public class AutoHidePanel : INotifyPropertyChanged
|
|
||||||
{
|
{
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
|
||||||
protected void OnPropertyChanged([CallerMemberName] string? name = null) =>
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
|
||||||
|
|
||||||
private bool _isVisible = false;
|
|
||||||
private double _slideOffset = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Уникальный идентификатор автоскрываемой панели.
|
/// Инициализирует новый экземпляр класса <see cref="AutoHidePanel"/>.
|
||||||
/// </summary>
|
|
||||||
public string Id { get; } = Guid.NewGuid().ToString();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Содержимое панели.
|
|
||||||
/// </summary>
|
|
||||||
public Abstractions.IDockContent Content { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Сторона окна, к которой прикреплена панель.
|
|
||||||
/// </summary>
|
|
||||||
public DockSide Side { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ширина панели (для левой/правой сторон) или высота (для верхней/нижней сторон).
|
|
||||||
/// </summary>
|
|
||||||
public double Size { get; set; } = 300;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Признак видимости панели.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsVisible
|
|
||||||
{
|
|
||||||
get => _isVisible;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_isVisible != value)
|
|
||||||
{
|
|
||||||
_isVisible = value;
|
|
||||||
OnPropertyChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Смещение для анимации выезда/заезда панели (0-1).
|
|
||||||
/// </summary>
|
|
||||||
public double SlideOffset
|
|
||||||
{
|
|
||||||
get => _slideOffset;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (Math.Abs(_slideOffset - value) > 0.001)
|
|
||||||
{
|
|
||||||
_slideOffset = value;
|
|
||||||
OnPropertyChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Заголовок панели (обычно берется из содержимого).
|
|
||||||
/// </summary>
|
|
||||||
public string Title => Content?.Title ?? "Auto-hide Panel";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр автоскрываемой панели.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="content">Содержимое панели.</param>
|
/// <param name="content">Содержимое панели.</param>
|
||||||
/// <param name="side">Сторона окна для прикрепления.</param>
|
/// <param name="side">Сторона окна для прикрепления панели.</param>
|
||||||
public AutoHidePanel(Abstractions.IDockContent content, DockSide side)
|
/// <exception cref="ArgumentNullException">Выбрасывается, когда <paramref name="content"/> равен null.</exception>
|
||||||
|
public AutoHidePanel(IDockContent content, DockSide side)
|
||||||
{
|
{
|
||||||
Content = content ?? throw new ArgumentNullException(nameof(content));
|
Content = content ?? throw new ArgumentNullException(nameof(content));
|
||||||
Side = side;
|
Side = side;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает уникальный идентификатор панели.
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; } = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает содержимое панели.
|
||||||
|
/// </summary>
|
||||||
|
public IDockContent Content { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает сторону окна, к которой прикреплена панель.
|
||||||
|
/// </summary>
|
||||||
|
public DockSide Side { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает размер панели в пикселях.
|
||||||
|
/// Для левой/правой сторон - ширина, для верхней/нижней - высота.
|
||||||
|
/// </summary>
|
||||||
|
public double Size { get; set; } = 300;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает значение, указывающее, видима ли панель.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsVisible { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает смещение для анимации выезда/заезда панели.
|
||||||
|
/// Значение от 0.0 (полностью скрыта) до 1.0 (полностью развернута).
|
||||||
|
/// </summary>
|
||||||
|
public double SlideOffset { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает заголовок панели, взятый из содержимого.
|
||||||
|
/// </summary>
|
||||||
|
public string Title => Content?.Title ?? "Auto-hide Panel";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Переключает видимость панели.
|
/// Переключает видимость панели.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -111,4 +80,4 @@ public class AutoHidePanel : INotifyPropertyChanged
|
|||||||
{
|
{
|
||||||
IsVisible = false;
|
IsVisible = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,146 +1,90 @@
|
|||||||
using Lattice.Core.Docking.Abstractions;
|
using Lattice.Core.Docking.Abstractions;
|
||||||
using Lattice.Core.DragDrop.Abstractions;
|
|
||||||
using Lattice.Core.DragDrop.Enums;
|
|
||||||
using Lattice.Core.DragDrop.Models;
|
|
||||||
using Lattice.Core.Geometry;
|
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Lattice.Core.Docking.Models;
|
namespace Lattice.Core.Docking.Models;
|
||||||
|
|
||||||
/// <summary>
|
public class DockGroup : IDockElement, INotifyPropertyChanged
|
||||||
/// Представляет узел дерева компоновки, который разделяет доступную область
|
|
||||||
/// между двумя дочерними элементами. Этот класс является основным структурным
|
|
||||||
/// элементом для создания сложных макетов с разделителями.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// <see cref="DockGroup"/> реализует как <see cref="IDragSource"/> (для
|
|
||||||
/// возможности перетаскивания всей группы), так и <see cref="IDropTarget"/>
|
|
||||||
/// (для возможности сброса на группу), что делает его полностью интегрированным
|
|
||||||
/// в систему перетаскивания док-системы.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Каждая группа содержит два дочерних элемента (<see cref="First"/> и
|
|
||||||
/// <see cref="Second"/>), которые могут быть либо другими группами (для
|
|
||||||
/// создания вложенной структуры), либо листами (<see cref="DockLeaf"/>)
|
|
||||||
/// с контентом. Направление разделения определяется свойством
|
|
||||||
/// <see cref="Orientation"/>.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public class DockGroup : IDockElement, IDragSource, IDropTarget, INotifyPropertyChanged
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
private IDockElement _first;
|
||||||
/// Событие, возникающее при изменении значения свойства.
|
private IDockElement _second;
|
||||||
/// </summary>
|
private SplitDirection _orientation;
|
||||||
|
private double _splitRatio = 0.5;
|
||||||
|
private IDockElement? _parent;
|
||||||
|
private double _width;
|
||||||
|
private double _height;
|
||||||
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
private double _splitRatio = 0.5;
|
public DockGroup(IDockElement first, IDockElement second, SplitDirection orientation)
|
||||||
private string _id;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает уникальный идентификатор группы.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Строковый идентификатор, уникальный в пределах дерева компоновки.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Идентификатор используется для сериализации/десериализации макета,
|
|
||||||
/// поиска элементов и отслеживания изменений в дереве.
|
|
||||||
/// </remarks>
|
|
||||||
public string Id
|
|
||||||
{
|
{
|
||||||
get => _id;
|
First = first ?? throw new ArgumentNullException(nameof(first));
|
||||||
internal set
|
Second = second ?? throw new ArgumentNullException(nameof(second));
|
||||||
|
Orientation = orientation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Id { get; } = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
public IDockElement? Parent
|
||||||
|
{
|
||||||
|
get => _parent;
|
||||||
|
set
|
||||||
{
|
{
|
||||||
if (_id != value)
|
if (_parent != value)
|
||||||
{
|
{
|
||||||
_id = value;
|
_parent = value;
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public IDockElement First
|
||||||
/// Получает или задает родительский элемент в иерархии дерева компоновки.
|
{
|
||||||
/// </summary>
|
get => _first;
|
||||||
/// <value>
|
set
|
||||||
/// Родительский элемент или null, если эта группа является корневой.
|
{
|
||||||
/// </value>
|
if (_first != value)
|
||||||
/// <remarks>
|
{
|
||||||
/// Это свойство управляется системой компоновки при добавлении или
|
_first = value ?? throw new ArgumentNullException(nameof(value));
|
||||||
/// удалении элементов из дерева.
|
_first.Parent = this;
|
||||||
/// </remarks>
|
OnPropertyChanged();
|
||||||
public IDockElement? Parent { get; set; }
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
public IDockElement Second
|
||||||
/// Получает или задает первый дочерний элемент (левую или верхнюю область).
|
{
|
||||||
/// </summary>
|
get => _second;
|
||||||
/// <value>
|
set
|
||||||
/// Элемент, занимающий первую часть разделенной области.
|
{
|
||||||
/// </value>
|
if (_second != value)
|
||||||
/// <exception cref="ArgumentNullException">
|
{
|
||||||
/// Выбрасывается при попытке установить значение null.
|
_second = value ?? throw new ArgumentNullException(nameof(value));
|
||||||
/// </exception>
|
_second.Parent = this;
|
||||||
/// <remarks>
|
OnPropertyChanged();
|
||||||
/// При установке нового значения автоматически обновляется свойство
|
}
|
||||||
/// <see cref="Parent"/> у дочернего элемента.
|
}
|
||||||
/// </remarks>
|
}
|
||||||
public IDockElement First { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
public SplitDirection Orientation
|
||||||
/// Получает или задает второй дочерний элемент (правую или нижнюю область).
|
{
|
||||||
/// </summary>
|
get => _orientation;
|
||||||
/// <value>
|
set
|
||||||
/// Элемент, занимающий вторую часть разделенной области.
|
{
|
||||||
/// </value>
|
if (_orientation != value)
|
||||||
/// <exception cref="ArgumentNullException">
|
{
|
||||||
/// Выбрасывается при попытке установить значение null.
|
_orientation = value;
|
||||||
/// </exception>
|
OnPropertyChanged();
|
||||||
/// <remarks>
|
}
|
||||||
/// При установке нового значения автоматически обновляется свойство
|
}
|
||||||
/// <see cref="Parent"/> у дочернего элемента.
|
}
|
||||||
/// </remarks>
|
|
||||||
public IDockElement Second { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает направление разделения данной группы.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Значение перечисления <see cref="SplitDirection"/>, указывающее,
|
|
||||||
/// как разделена область: горизонтально или вертикально.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// <see cref="SplitDirection.Horizontal"/> создает левую и правую области.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// <see cref="SplitDirection.Vertical"/> создает верхнюю и нижнюю области.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public SplitDirection Orientation { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает соотношение разделения между первым и вторым элементами.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Значение от 0.0 до 1.0, где:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>0.0 - вся область принадлежит второму элементу</item>
|
|
||||||
/// <item>0.5 - область разделена поровну</item>
|
|
||||||
/// <item>1.0 - вся область принадлежит первому элементу</item>
|
|
||||||
/// </list>
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Изменение этого свойства вызывает событие <see cref="PropertyChanged"/>
|
|
||||||
/// и может привести к перерисовке пользовательского интерфейса.
|
|
||||||
/// </remarks>
|
|
||||||
public double SplitRatio
|
public double SplitRatio
|
||||||
{
|
{
|
||||||
get => _splitRatio;
|
get => _splitRatio;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (Math.Abs(_splitRatio - value) > double.Epsilon)
|
if (Math.Abs(_splitRatio - value) > 0.001)
|
||||||
{
|
{
|
||||||
_splitRatio = value;
|
_splitRatio = value;
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
@@ -148,297 +92,42 @@ public class DockGroup : IDockElement, IDragSource, IDropTarget, INotifyProperty
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public double Width
|
||||||
/// Получает или задает желаемую ширину элемента.
|
{
|
||||||
/// </summary>
|
get => _width;
|
||||||
/// <value>
|
set
|
||||||
/// Ширина в пикселях или относительных единицах.
|
{
|
||||||
/// </value>
|
if (Math.Abs(_width - value) > 0.001)
|
||||||
public double Width { get; set; }
|
{
|
||||||
|
_width = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
public double Height
|
||||||
/// Получает или задает желаемую высоту элемента.
|
{
|
||||||
/// </summary>
|
get => _height;
|
||||||
/// <value>
|
set
|
||||||
/// Высота в пикселях или относительных единицах.
|
{
|
||||||
/// </value>
|
if (Math.Abs(_height - value) > 0.001)
|
||||||
public double Height { get; set; }
|
{
|
||||||
|
_height = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает минимально допустимую ширину элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Минимальная ширина в пикселях, при которой элемент сохраняет функциональность.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Для группы минимальная ширина вычисляется как сумма минимальных ширин
|
|
||||||
/// дочерних элементов при горизонтальной ориентации или максимум минимальных
|
|
||||||
/// ширин при вертикальной ориентации.
|
|
||||||
/// </remarks>
|
|
||||||
public double MinWidth => Orientation == SplitDirection.Horizontal
|
public double MinWidth => Orientation == SplitDirection.Horizontal
|
||||||
? First.MinWidth + Second.MinWidth
|
? First.MinWidth + Second.MinWidth
|
||||||
: Math.Max(First.MinWidth, Second.MinWidth);
|
: Math.Max(First.MinWidth, Second.MinWidth);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает минимально допустимую высоту элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Минимальная высота в пикселях, при которой элемент сохраняет функциональность.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Для группы минимальная высота вычисляется как сумма минимальных высот
|
|
||||||
/// дочерних элементов при вертикальной ориентации или максимум минимальных
|
|
||||||
/// высот при горизонтальной ориентации.
|
|
||||||
/// </remarks>
|
|
||||||
public double MinHeight => Orientation == SplitDirection.Vertical
|
public double MinHeight => Orientation == SplitDirection.Vertical
|
||||||
? First.MinHeight + Second.MinHeight
|
? First.MinHeight + Second.MinHeight
|
||||||
: Math.Max(First.MinHeight, Second.MinHeight);
|
: Math.Max(First.MinHeight, Second.MinHeight);
|
||||||
|
|
||||||
/// <summary>
|
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DockGroup"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="first">
|
|
||||||
/// Первый дочерний элемент (левая или верхняя область).
|
|
||||||
/// </param>
|
|
||||||
/// <param name="second">
|
|
||||||
/// Второй дочерний элемент (правая или нижняя область).
|
|
||||||
/// </param>
|
|
||||||
/// <param name="orientation">
|
|
||||||
/// Направление разделения между дочерними элементами.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="id">
|
|
||||||
/// Уникальный идентификатор группы. Если не указан, генерируется новый GUID.
|
|
||||||
/// </param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, когда <paramref name="first"/> или <paramref name="second"/>
|
|
||||||
/// равны null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Конструктор автоматически устанавливает свойство <see cref="Parent"/>
|
|
||||||
/// у дочерних элементов на текущую группу и генерирует уникальный идентификатор,
|
|
||||||
/// если он не был предоставлен.
|
|
||||||
/// </remarks>
|
|
||||||
public DockGroup(IDockElement first, IDockElement second, SplitDirection orientation, string? id = null)
|
|
||||||
{
|
{
|
||||||
First = first ?? throw new ArgumentNullException(nameof(first));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
Second = second ?? throw new ArgumentNullException(nameof(second));
|
|
||||||
Orientation = orientation;
|
|
||||||
Id = id ?? Guid.NewGuid().ToString();
|
|
||||||
|
|
||||||
First.Parent = this;
|
|
||||||
Second.Parent = this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывает событие <see cref="PropertyChanged"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="name">
|
|
||||||
/// Имя изменившегося свойства. Если не указано, определяется автоматически.
|
|
||||||
/// </param>
|
|
||||||
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
|
||||||
{
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Реализация IDragSource
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Определяет, может ли группа начать операцию перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragInfo">
|
|
||||||
/// При успешном возврате содержит информацию о перетаскивании;
|
|
||||||
/// в противном случае — null.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// true, если группа может начать перетаскивание; в противном случае — false.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Группа может быть перетащена только если она не является корневым
|
|
||||||
/// элементом дерева (имеет родителя).
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// При успешной проверке метод заполняет <paramref name="dragInfo"/>
|
|
||||||
/// данными типа <see cref="DockElementDragData"/>.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public bool CanStartDrag(out DragInfo? dragInfo)
|
|
||||||
{
|
|
||||||
dragInfo = null;
|
|
||||||
|
|
||||||
// DockGroup можно перетаскивать только если он не является корневым элементом
|
|
||||||
if (Parent == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Создаем данные для перетаскивания
|
|
||||||
var data = new DockElementDragData
|
|
||||||
{
|
|
||||||
ElementId = Id,
|
|
||||||
ElementType = GetType().Name,
|
|
||||||
IsGroup = true
|
|
||||||
};
|
|
||||||
|
|
||||||
dragInfo = new DragInfo(data, DragDropEffects.Move, Point.Zero, this);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Начинает операцию перетаскивания для группы.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragInfo">
|
|
||||||
/// Информация о перетаскивании, полученная из <see cref="CanStartDrag"/>.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Всегда возвращает true, так как группа не требует специальной подготовки
|
|
||||||
/// для начала перетаскивания.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Для <see cref="DockGroup"/> этот метод не выполняет дополнительных действий,
|
|
||||||
/// так как все необходимые данные уже содержатся в <paramref name="dragInfo"/>.
|
|
||||||
/// </remarks>
|
|
||||||
public bool StartDrag(DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
// DockGroup не требует дополнительной подготовки для перетаскивания
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывается при завершении операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragInfo">
|
|
||||||
/// Исходная информация о перетаскивании.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="effects">
|
|
||||||
/// Эффекты, которые были применены при сбросе.
|
|
||||||
/// </param>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод вызывается после того, как операция перетаскивания была
|
|
||||||
/// завершена (успешно или неуспешно).
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Для <see cref="DockGroup"/> этот метод не выполняет действий, так как
|
|
||||||
/// все изменения в структуре дерева уже обработаны <see cref="LayoutManager"/>.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public void DragCompleted(DragInfo dragInfo, DragDropEffects effects)
|
|
||||||
{
|
|
||||||
// Если группа была перемещена, ничего не делаем - LayoutManager уже обработал изменение
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывается при отмене операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragInfo">
|
|
||||||
/// Исходная информация о перетаскивании.
|
|
||||||
/// </param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Для <see cref="DockGroup"/> отмена перетаскивания не требует специальных
|
|
||||||
/// действий, так как структура дерева не была изменена.
|
|
||||||
/// </remarks>
|
|
||||||
public void DragCancelled(DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
// Отмена перетаскивания не требует действий
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Реализация IDropTarget
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Определяет, может ли группа принять сбрасываемые данные.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dropInfo">
|
|
||||||
/// Информация о потенциальном сбросе.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// true, если группа может принять данные; в противном случае — false.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Группа может принимать только данные типа <see cref="DockElementDragData"/>
|
|
||||||
/// для элементов док-системы (<see cref="DockGroup"/> или <see cref="DockLeaf"/>).
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Группа не может принять сброс самой себя (проверяется по идентификатору).
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public bool CanAcceptDrop(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
if (dropInfo.Data is not DockElementDragData dragData)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Нельзя сбросить элемент на самого себя
|
|
||||||
if (dragData.ElementId == Id)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Можно принимать только элементы док-системы
|
|
||||||
return dragData.ElementType == nameof(DockGroup) || dragData.ElementType == nameof(DockLeaf);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывается, когда перетаскиваемый объект находится над группой.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dropInfo">
|
|
||||||
/// Информация о текущем положении перетаскивания.
|
|
||||||
/// </param>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод вызывается постоянно, пока пользователь перемещает объект
|
|
||||||
/// над целью. Для группы он устанавливает предлагаемые эффекты в
|
|
||||||
/// <see cref="DropInfo.SuggestedEffects"/>.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Если группа может принять сброс, предлагается эффект перемещения;
|
|
||||||
/// в противном случае эффекты не предлагаются.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public void DragOver(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
if (CanAcceptDrop(dropInfo))
|
|
||||||
{
|
|
||||||
dropInfo.SuggestedEffects = DragDropEffects.Move;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
dropInfo.SuggestedEffects = DragDropEffects.None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывается, когда пользователь сбрасывает данные на группу.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dropInfo">
|
|
||||||
/// Информация о сбросе.
|
|
||||||
/// </param>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Для <see cref="DockGroup"/> обработка сброса делегируется
|
|
||||||
/// <see cref="LayoutManager"/>, поэтому метод просто помечает операцию
|
|
||||||
/// как обработанную.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Фактическое изменение структуры дерева выполняется менеджером макета
|
|
||||||
/// на основе данных из <paramref name="dropInfo"/>.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public void Drop(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
// Обработка сброса делегируется LayoutManager
|
|
||||||
dropInfo.MarkAsHandled();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывается, когда перетаскиваемый объект покидает область группы.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Для группы этот метод не выполняет действий, так как очистка визуальной
|
|
||||||
/// обратной связи выполняется в UI-слое.
|
|
||||||
/// </remarks>
|
|
||||||
public void DragLeave()
|
|
||||||
{
|
|
||||||
// Очистка визуальной обратной связи (будет выполнена в UI слое)
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
@@ -1,107 +1,48 @@
|
|||||||
using Lattice.Core.Docking.Abstractions;
|
using Lattice.Core.Docking.Abstractions;
|
||||||
using Lattice.Core.DragDrop.Abstractions;
|
|
||||||
using Lattice.Core.DragDrop.Enums;
|
|
||||||
using Lattice.Core.DragDrop.Models;
|
|
||||||
using Lattice.Core.Geometry;
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Lattice.Core.Docking.Models;
|
namespace Lattice.Core.Docking.Models;
|
||||||
|
|
||||||
/// <summary>
|
public class DockLeaf : IDockContainer, INotifyPropertyChanged
|
||||||
/// Представляет конечный узел (лист) дерева компоновки, который непосредственно
|
|
||||||
/// содержит коллекцию вкладок с контентом. Этот класс является контейнером для
|
|
||||||
/// отображаемого пользователю содержимого.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// <see cref="DockLeaf"/> реализует интерфейсы <see cref="IDockContainer"/>,
|
|
||||||
/// <see cref="IDragSource"/> и <see cref="IDropTarget"/>, что позволяет ему:
|
|
||||||
/// </para>
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Управлять коллекцией вкладок</item>
|
|
||||||
/// <item>Быть источником перетаскивания (как всего листа, так и отдельных вкладок)</item>
|
|
||||||
/// <item>Принимать сброс других элементов или вкладок</item>
|
|
||||||
/// </list>
|
|
||||||
/// <para>
|
|
||||||
/// Лист является основным элементом, с которым взаимодействует пользователь
|
|
||||||
/// при работе с документами или инструментальными панелями в IDE-подобных
|
|
||||||
/// приложениях.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public class DockLeaf : IDockContainer, INotifyPropertyChanged, IDragSource, IDropTarget
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Событие, возникающее при изменении значения свойства.
|
|
||||||
/// </summary>
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
|
||||||
|
|
||||||
private readonly ObservableCollection<IDockContent> _items = new();
|
private readonly ObservableCollection<IDockContent> _items = new();
|
||||||
private IDockContent? _activeContent;
|
private IDockContent? _activeContent;
|
||||||
private string _id;
|
private IDockElement? _parent;
|
||||||
|
private double _width;
|
||||||
|
private double _height;
|
||||||
|
private TabPlacement _tabPlacement = TabPlacement.Top;
|
||||||
|
|
||||||
/// <summary>
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
/// Получает уникальный идентификатор листа.
|
|
||||||
/// </summary>
|
public DockLeaf()
|
||||||
/// <value>
|
|
||||||
/// Строковый идентификатор, уникальный в пределах дерева компоновки.
|
|
||||||
/// </value>
|
|
||||||
public string Id
|
|
||||||
{
|
{
|
||||||
get => _id;
|
_items.CollectionChanged += (s, e) => OnPropertyChanged(nameof(Children));
|
||||||
internal set
|
}
|
||||||
|
|
||||||
|
public string Id { get; } = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
public IDockElement? Parent
|
||||||
|
{
|
||||||
|
get => _parent;
|
||||||
|
set
|
||||||
{
|
{
|
||||||
if (_id != value)
|
if (_parent != value)
|
||||||
{
|
{
|
||||||
_id = value;
|
_parent = value;
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает родительский элемент в иерархии дерева компоновки.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Родительский элемент или null, если этот лист является корневым.
|
|
||||||
/// </value>
|
|
||||||
public IDockElement? Parent { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает список вкладок, содержащихся в данном контейнере.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Коллекция объектов, реализующих <see cref="IDockContent"/>.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Эта коллекция является наблюдаемой (ObservableCollection), что позволяет
|
|
||||||
/// автоматически обновлять пользовательский интерфейс при добавлении или
|
|
||||||
/// удалении вкладок.
|
|
||||||
/// </remarks>
|
|
||||||
public IList<IDockContent> Children => _items;
|
public IList<IDockContent> Children => _items;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает активную (выбранную) вкладку в контейнере.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Активная вкладка или null, если в контейнере нет вкладок.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// При установке нового значения проверяется, что вкладка действительно
|
|
||||||
/// содержится в коллекции <see cref="Children"/>.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Изменение этого свойства вызывает событие <see cref="PropertyChanged"/>.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public IDockContent? ActiveContent
|
public IDockContent? ActiveContent
|
||||||
{
|
{
|
||||||
get => _activeContent;
|
get => _activeContent;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (value != null && !_items.Contains(value)) return;
|
|
||||||
if (_activeContent != value)
|
if (_activeContent != value)
|
||||||
{
|
{
|
||||||
_activeContent = value;
|
_activeContent = value;
|
||||||
@@ -110,94 +51,53 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged, IDragSource, IDr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public double Width
|
||||||
/// Получает или задает желаемую ширину элемента.
|
{
|
||||||
/// </summary>
|
get => _width;
|
||||||
/// <value>
|
set
|
||||||
/// Ширина в пикселях или относительных единицах.
|
{
|
||||||
/// </value>
|
if (Math.Abs(_width - value) > 0.001)
|
||||||
public double Width { get; set; }
|
{
|
||||||
|
_width = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
public double Height
|
||||||
/// Получает или задает желаемую высоту элемента.
|
{
|
||||||
/// </summary>
|
get => _height;
|
||||||
/// <value>
|
set
|
||||||
/// Высота в пикселях или относительных единицах.
|
{
|
||||||
/// </value>
|
if (Math.Abs(_height - value) > 0.001)
|
||||||
public double Height { get; set; }
|
{
|
||||||
|
_height = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает минимально допустимую ширину элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Минимальная ширина в пикселях. Значение по умолчанию: 100.
|
|
||||||
/// </value>
|
|
||||||
public double MinWidth { get; set; } = 100;
|
public double MinWidth { get; set; } = 100;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает минимально допустимую высоту элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Минимальная высота в пикселях. Значение по умолчанию: 100.
|
|
||||||
/// </value>
|
|
||||||
public double MinHeight { get; set; } = 100;
|
public double MinHeight { get; set; } = 100;
|
||||||
|
|
||||||
/// <summary>
|
public TabPlacement TabPlacement
|
||||||
/// Получает или задает положение полосы вкладок в контейнере.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Значение перечисления <see cref="TabPlacement"/>, определяющее,
|
|
||||||
/// где располагаются вкладки относительно содержимого.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Поддерживаются все четыре стороны: верх, низ, лево, право.
|
|
||||||
/// </remarks>
|
|
||||||
public TabPlacement TabPlacement { get; set; } = TabPlacement.Bottom;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DockLeaf"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">
|
|
||||||
/// Уникальный идентификатор листа. Если не указан, генерируется новый GUID.
|
|
||||||
/// </param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Создает пустой лист с коллекцией вкладок и генерирует уникальный
|
|
||||||
/// идентификатор, если он не был предоставлен.
|
|
||||||
/// </remarks>
|
|
||||||
public DockLeaf(string? id = null)
|
|
||||||
{
|
{
|
||||||
_id = id ?? Guid.NewGuid().ToString();
|
get => _tabPlacement;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_tabPlacement != value)
|
||||||
|
{
|
||||||
|
_tabPlacement = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывает событие <see cref="PropertyChanged"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="name">
|
|
||||||
/// Имя изменившегося свойства. Если не указано, определяется автоматически.
|
|
||||||
/// </param>
|
|
||||||
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
|
||||||
{
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Добавляет контент в контейнер и делает его активным.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="content">
|
|
||||||
/// Контент для добавления.
|
|
||||||
/// </param>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Если контент уже содержится в коллекции, он не добавляется повторно,
|
|
||||||
/// но становится активным.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод обновляет свойство <see cref="ActiveContent"/> и вызывает
|
|
||||||
/// соответствующее событие изменения свойства.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public void AddContent(IDockContent content)
|
public void AddContent(IDockContent content)
|
||||||
{
|
{
|
||||||
|
if (content == null)
|
||||||
|
throw new ArgumentNullException(nameof(content));
|
||||||
|
|
||||||
if (!_items.Contains(content))
|
if (!_items.Contains(content))
|
||||||
{
|
{
|
||||||
_items.Add(content);
|
_items.Add(content);
|
||||||
@@ -205,25 +105,11 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged, IDragSource, IDr
|
|||||||
ActiveContent = content;
|
ActiveContent = content;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Удаляет контент из контейнера.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="content">
|
|
||||||
/// Контент для удаления.
|
|
||||||
/// </param>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Если удаляемый контент является активным, автоматически выбирается
|
|
||||||
/// новая активная вкладка (следующая в списке или предыдущая, если удалена
|
|
||||||
/// последняя).
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Если после удаления контейнер становится пустым, он может быть удален
|
|
||||||
/// из дерева макета системой компоновки.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public void RemoveContent(IDockContent content)
|
public void RemoveContent(IDockContent content)
|
||||||
{
|
{
|
||||||
|
if (content == null)
|
||||||
|
throw new ArgumentNullException(nameof(content));
|
||||||
|
|
||||||
int index = _items.IndexOf(content);
|
int index = _items.IndexOf(content);
|
||||||
if (index == -1) return;
|
if (index == -1) return;
|
||||||
|
|
||||||
@@ -238,343 +124,8 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged, IDragSource, IDr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Реализация IDragSource
|
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Определяет, может ли лист начать операцию перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragInfo">
|
|
||||||
/// При успешном возврате содержит информацию о перетаскивании;
|
|
||||||
/// в противном случае — null.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// true, если лист может начать перетаскивание; в противном случае — false.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Лист может быть перетащен, если:
|
|
||||||
/// </para>
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Он имеет родителя (не является корневым)</item>
|
|
||||||
/// <item>Или имеет хотя бы одну вкладку (не пустой)</item>
|
|
||||||
/// </list>
|
|
||||||
/// <para>
|
|
||||||
/// В зависимости от наличия активного контента создаются разные данные:
|
|
||||||
/// </para>
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>
|
|
||||||
/// Если есть активный контент - создается <see cref="ContentDragData"/>
|
|
||||||
/// для перетаскивания конкретной вкладки
|
|
||||||
/// </item>
|
|
||||||
/// <item>
|
|
||||||
/// Если нет активного контента - создается <see cref="DockElementDragData"/>
|
|
||||||
/// для перетаскивания всего листа
|
|
||||||
/// </item>
|
|
||||||
/// </list>
|
|
||||||
/// </remarks>
|
|
||||||
public bool CanStartDrag(out DragInfo? dragInfo)
|
|
||||||
{
|
{
|
||||||
dragInfo = null;
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
|
||||||
// DockLeaf можно перетаскивать
|
|
||||||
if (Parent == null && Children.Count == 0)
|
|
||||||
return false; // Не перетаскиваем пустые корневые листья
|
|
||||||
|
|
||||||
object data;
|
|
||||||
|
|
||||||
// Если есть активный контент, перетаскиваем контент, иначе перетаскиваем весь лист
|
|
||||||
if (ActiveContent != null)
|
|
||||||
{
|
|
||||||
data = new ContentDragData
|
|
||||||
{
|
|
||||||
ElementId = Id,
|
|
||||||
ContentId = ActiveContent.Id,
|
|
||||||
ContentTitle = ActiveContent.Title,
|
|
||||||
ContentType = ActiveContent.GetType().Name
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
data = new DockElementDragData
|
|
||||||
{
|
|
||||||
ElementId = Id,
|
|
||||||
ElementType = GetType().Name,
|
|
||||||
IsGroup = false,
|
|
||||||
Width = Width,
|
|
||||||
Height = Height
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
dragInfo = new DragInfo(data, DragDropEffects.Move | DragDropEffects.Copy, Point.Zero, this);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Начинает операцию перетаскивания для листа.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragInfo">
|
|
||||||
/// Информация о перетаскивании.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Всегда возвращает true.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Для <see cref="DockLeaf"/> этот метод не выполняет дополнительных действий.
|
|
||||||
/// </remarks>
|
|
||||||
public bool StartDrag(DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
// DockLeaf не требует дополнительной подготовки
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывается при завершении операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragInfo">
|
|
||||||
/// Исходная информация о перетаскивании.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="effects">
|
|
||||||
/// Эффекты, которые были применены при сбросе.
|
|
||||||
/// </param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Для <see cref="DockLeaf"/> этот метод не выполняет действий.
|
|
||||||
/// </remarks>
|
|
||||||
public void DragCompleted(DragInfo dragInfo, DragDropEffects effects)
|
|
||||||
{
|
|
||||||
// Если лист был перемещен или скопирован, LayoutManager уже обработал это
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывается при отмене операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragInfo">
|
|
||||||
/// Исходная информация о перетаскивании.
|
|
||||||
/// </param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Для <see cref="DockLeaf"/> отмена перетаскивания не требует действий.
|
|
||||||
/// </remarks>
|
|
||||||
public void DragCancelled(DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
// Отмена не требует действий
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Реализация IDropTarget
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Определяет, может ли лист принять сбрасываемые данные.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dropInfo">
|
|
||||||
/// Информация о потенциальном сбросе.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// true, если лист может принять данные; в противном случае — false.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Лист может принимать:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>
|
|
||||||
/// <see cref="DockElementDragData"/> для других листов и групп
|
|
||||||
/// (для объединения или разделения)
|
|
||||||
/// </item>
|
|
||||||
/// <item>
|
|
||||||
/// <see cref="ContentDragData"/> для вкладок (для объединения вкладок)
|
|
||||||
/// </item>
|
|
||||||
/// </list>
|
|
||||||
/// </remarks>
|
|
||||||
public bool CanAcceptDrop(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
if (dropInfo.Data is DockElementDragData elementData)
|
|
||||||
{
|
|
||||||
// Можно принимать другие листы и группы
|
|
||||||
return elementData.ElementType == nameof(DockLeaf) ||
|
|
||||||
elementData.ElementType == nameof(DockGroup);
|
|
||||||
}
|
|
||||||
else if (dropInfo.Data is ContentDragData contentData)
|
|
||||||
{
|
|
||||||
// Можно принимать контент для объединения вкладок
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывается, когда перетаскиваемый объект находится над листом.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dropInfo">
|
|
||||||
/// Информация о текущем положении перетаскивания.
|
|
||||||
/// </param>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// В зависимости от типа данных устанавливаются разные предлагаемые эффекты:
|
|
||||||
/// </para>
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>
|
|
||||||
/// Для <see cref="ContentDragData"/> - эффект копирования (объединение вкладок)
|
|
||||||
/// </item>
|
|
||||||
/// <item>
|
|
||||||
/// Для <see cref="DockElementDragData"/> - эффект перемещения
|
|
||||||
/// </item>
|
|
||||||
/// </list>
|
|
||||||
/// </remarks>
|
|
||||||
public void DragOver(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
if (CanAcceptDrop(dropInfo))
|
|
||||||
{
|
|
||||||
if (dropInfo.Data is ContentDragData)
|
|
||||||
{
|
|
||||||
// Для контента предлагаем копирование (объединение вкладок)
|
|
||||||
dropInfo.SuggestedEffects = DragDropEffects.Copy;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Для элементов предлагаем перемещение
|
|
||||||
dropInfo.SuggestedEffects = DragDropEffects.Move;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
dropInfo.SuggestedEffects = DragDropEffects.None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывается, когда пользователь сбрасывает данные на лист.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dropInfo">
|
|
||||||
/// Информация о сбросе.
|
|
||||||
/// </param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Обработка сброса делегируется <see cref="LayoutManager"/>.
|
|
||||||
/// </remarks>
|
|
||||||
public void Drop(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
// Обработка делегируется LayoutManager
|
|
||||||
dropInfo.MarkAsHandled();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывается, когда перетаскиваемый объект покидает область листа.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Очистка визуальной обратной связи выполняется в UI-слое.
|
|
||||||
/// </remarks>
|
|
||||||
public void DragLeave()
|
|
||||||
{
|
|
||||||
// Очистка визуальной обратной связи
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Представляет данные для перетаскивания элементов док-системы (групп или листов).
|
|
||||||
/// Используется при перетаскивании целых структурных элементов дерева компоновки.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот класс сериализуется и передается между компонентами системы перетаскивания
|
|
||||||
/// для идентификации перетаскиваемого элемента и его свойств.
|
|
||||||
/// </remarks>
|
|
||||||
public class DockElementDragData
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает уникальный идентификатор элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Идентификатор элемента, соответствующий свойству <see cref="IDockElement.Id"/>.
|
|
||||||
/// </value>
|
|
||||||
public string ElementId { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает тип элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Имя типа элемента (обычно "DockGroup" или "DockLeaf").
|
|
||||||
/// </value>
|
|
||||||
public string ElementType { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает значение, указывающее, является ли элемент группой.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// true, если элемент является <see cref="DockGroup"/>; false, если <see cref="DockLeaf"/>.
|
|
||||||
/// </value>
|
|
||||||
public bool IsGroup { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает идентификатор родительского элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Идентификатор родительского элемента или null, если элемент корневой.
|
|
||||||
/// </value>
|
|
||||||
public string? ParentId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает ширину элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Текущая ширина элемента в пикселях.
|
|
||||||
/// </value>
|
|
||||||
public double Width { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает высоту элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Текущая высота элемента в пикселях.
|
|
||||||
/// </value>
|
|
||||||
public double Height { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Представляет данные для перетаскивания контента (вкладок).
|
|
||||||
/// Используется при перетаскивании отдельных вкладок между контейнерами.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот класс позволяет идентифицировать конкретную вкладку для операций
|
|
||||||
/// объединения или перемещения между контейнерами.
|
|
||||||
/// </remarks>
|
|
||||||
public class ContentDragData
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает идентификатор контейнера (листа), содержащего контент.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Идентификатор <see cref="DockLeaf"/>, в котором находится перетаскиваемая вкладка.
|
|
||||||
/// </value>
|
|
||||||
public string ElementId { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает уникальный идентификатор контента.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Идентификатор контента, соответствующий свойству <see cref="IDockContent.Id"/>.
|
|
||||||
/// </value>
|
|
||||||
public string ContentId { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает заголовок контента.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Текст, отображаемый на вкладке.
|
|
||||||
/// </value>
|
|
||||||
public string ContentTitle { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает тип контента.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Имя типа контента (например, "TextEditor", "Toolbox", и т.д.).
|
|
||||||
/// </value>
|
|
||||||
public string ContentType { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает значение, указывающее, можно ли закрыть контент.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// true, если контент можно закрыть; в противном случае — false.
|
|
||||||
/// </value>
|
|
||||||
public bool CanClose { get; set; } = true;
|
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,33 @@
|
|||||||
namespace Lattice.Core.Docking.Models;
|
namespace Lattice.Core.Docking.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Определяет позицию вставки при операции Drag-and-Drop.
|
/// Определяет позицию вставки элемента относительно целевого элемента.
|
||||||
|
/// Используется при операциях перемещения и вставки элементов в дерево компоновки.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum DockPosition
|
public enum DockPosition
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Слева от целевого элемента.
|
||||||
|
/// </summary>
|
||||||
Left,
|
Left,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Справа от целевого элемента.
|
||||||
|
/// </summary>
|
||||||
Right,
|
Right,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сверху от целевого элемента.
|
||||||
|
/// </summary>
|
||||||
Top,
|
Top,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Снизу от целевого элемента.
|
||||||
|
/// </summary>
|
||||||
Bottom,
|
Bottom,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// В центре целевого элемента (для объединения вкладок).
|
||||||
|
/// </summary>
|
||||||
Center,
|
Center,
|
||||||
}
|
}
|
||||||
@@ -5,15 +5,23 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public enum DockSide
|
public enum DockSide
|
||||||
{
|
{
|
||||||
/// <summary> Левая сторона окна. </summary>
|
/// <summary>
|
||||||
|
/// Левая сторона окна.
|
||||||
|
/// </summary>
|
||||||
Left,
|
Left,
|
||||||
|
|
||||||
/// <summary> Правая сторона окна. </summary>
|
/// <summary>
|
||||||
|
/// Правая сторона окна.
|
||||||
|
/// </summary>
|
||||||
Right,
|
Right,
|
||||||
|
|
||||||
/// <summary> Верхняя сторона окна. </summary>
|
/// <summary>
|
||||||
|
/// Верхняя сторона окна.
|
||||||
|
/// </summary>
|
||||||
Top,
|
Top,
|
||||||
|
|
||||||
/// <summary> Нижняя сторона окна. </summary>
|
/// <summary>
|
||||||
|
/// Нижняя сторона окна.
|
||||||
|
/// </summary>
|
||||||
Bottom
|
Bottom
|
||||||
}
|
}
|
||||||
@@ -3,21 +3,66 @@
|
|||||||
namespace Lattice.Core.Docking.Models;
|
namespace Lattice.Core.Docking.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Описывает состояние плавающего окна в системе Lattice.
|
/// Представляет плавающее окно в системе докинга.
|
||||||
|
/// Плавающие окна могут перемещаться по экрану независимо от главного окна.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DockWindow
|
public class DockWindow
|
||||||
{
|
{
|
||||||
/// <summary> Уникальный ID окна для сохранения его позиции в конфиге. </summary>
|
/// <summary>
|
||||||
|
/// Получает уникальный идентификатор окна.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Строковый идентификатор, сгенерированный с помощью GUID.
|
||||||
|
/// Используется для сохранения позиции и размера окна в конфигурации.
|
||||||
|
/// </value>
|
||||||
public string Id { get; } = Guid.NewGuid().ToString();
|
public string Id { get; } = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
/// <summary> Корневой элемент макета внутри данного окна. </summary>
|
/// <summary>
|
||||||
|
/// Получает или задает корневой элемент макета внутри данного окна.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Корневой элемент дерева компоновки плавающего окна.
|
||||||
|
/// </value>
|
||||||
public IDockElement? Root { get; set; }
|
public IDockElement? Root { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает позицию X окна на экране.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Координата X левого верхнего угла окна в пикселях.
|
||||||
|
/// </value>
|
||||||
public double X { get; set; }
|
public double X { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает позицию Y окна на экране.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Координата Y левого верхнего угла окна в пикселях.
|
||||||
|
/// </value>
|
||||||
public double Y { get; set; }
|
public double Y { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает ширину окна.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Ширина окна в пикселях. Значение по умолчанию: 800.
|
||||||
|
/// </value>
|
||||||
public double Width { get; set; } = 800;
|
public double Width { get; set; } = 800;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает высоту окна.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Высота окна в пикселях. Значение по умолчанию: 600.
|
||||||
|
/// </value>
|
||||||
public double Height { get; set; } = 600;
|
public double Height { get; set; } = 600;
|
||||||
|
|
||||||
/// <summary> Заголовок окна (обычно берется из активного контента). </summary>
|
/// <summary>
|
||||||
|
/// Получает или задает заголовок окна.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Текст заголовка окна. Обычно берется из активного контента.
|
||||||
|
/// Значение по умолчанию: "Lattice Tool Window".
|
||||||
|
/// </value>
|
||||||
public string Title { get; set; } = "Lattice Tool Window";
|
public string Title { get; set; } = "Lattice Tool Window";
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
namespace Lattice.Core.Docking.Models;
|
namespace Lattice.Core.Docking.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Перечисление направлений разделения пространства внутри группы.
|
/// Определяет направление разделения пространства внутри группы.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum SplitDirection
|
public enum SplitDirection
|
||||||
{
|
{
|
||||||
/// <summary> Разделение по горизонтали (создает левую и правую области). </summary>
|
/// <summary>
|
||||||
|
/// Разделение по горизонтали (создает левую и правую области).
|
||||||
|
/// </summary>
|
||||||
Horizontal,
|
Horizontal,
|
||||||
/// <summary> Разделение по вертикали (создает верхнюю и нижнюю области). </summary>
|
|
||||||
|
/// <summary>
|
||||||
|
/// Разделение по вертикали (создает верхнюю и нижнюю области).
|
||||||
|
/// </summary>
|
||||||
Vertical
|
Vertical
|
||||||
}
|
}
|
||||||
@@ -5,8 +5,23 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public enum TabPlacement
|
public enum TabPlacement
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Вкладки располагаются сверху.
|
||||||
|
/// </summary>
|
||||||
Top,
|
Top,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вкладки располагаются снизу.
|
||||||
|
/// </summary>
|
||||||
Bottom,
|
Bottom,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вкладки располагаются слева.
|
||||||
|
/// </summary>
|
||||||
Left,
|
Left,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вкладки располагаются справа.
|
||||||
|
/// </summary>
|
||||||
Right,
|
Right,
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
namespace Lattice.Core.Docking.Serialization;
|
namespace Lattice.Core.Docking.Serialization;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Абстракция для сериализации и десериализации состояния макета док-системы.
|
/// Определяет контракт для сериализации и десериализации состояния макета док-системы.
|
||||||
/// Позволяет сохранять и восстанавливать расположение панелей, окон и их состояние.
|
/// Позволяет сохранять и восстанавливать расположение панелей, окон и их состояние.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
@@ -14,7 +14,12 @@ public interface ILayoutSerializer
|
|||||||
/// Сериализует состояние менеджера макета в строку.
|
/// Сериализует состояние менеджера макета в строку.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="manager">Менеджер макета для сериализации.</param>
|
/// <param name="manager">Менеджер макета для сериализации.</param>
|
||||||
/// <returns>Строковое представление состояния макета.</returns>
|
/// <returns>
|
||||||
|
/// Строковое представление состояния макета.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, когда <paramref name="manager"/> равен null.
|
||||||
|
/// </exception>
|
||||||
string Serialize(Engine.LayoutManager manager);
|
string Serialize(Engine.LayoutManager manager);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -26,6 +31,10 @@ public interface ILayoutSerializer
|
|||||||
/// Функция разрешения контента по идентификатору, используемая для восстановления
|
/// Функция разрешения контента по идентификатору, используемая для восстановления
|
||||||
/// ссылок на контент в десериализованном состоянии.
|
/// ссылок на контент в десериализованном состоянии.
|
||||||
/// </param>
|
/// </param>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, когда <paramref name="manager"/> или <paramref name="serializedLayout"/>
|
||||||
|
/// равны null.
|
||||||
|
/// </exception>
|
||||||
void Deserialize(Engine.LayoutManager manager, string serializedLayout,
|
void Deserialize(Engine.LayoutManager manager, string serializedLayout,
|
||||||
Func<string, Abstractions.IDockContent?> contentResolver);
|
Func<string, Abstractions.IDockContent?> contentResolver);
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,24 @@
|
|||||||
namespace Lattice.Core.Docking.Serialization;
|
namespace Lattice.Core.Docking.Serialization;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Контракт для объектов, которые могут предоставлять состояние для сериализации.
|
/// Определяет контракт для объектов, которые могут предоставлять состояние для сериализации.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ISerializableLayout
|
public interface ISerializableLayout
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает состояние для сериализации.
|
/// Получает состояние объекта для сериализации.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Объект состояния, готовый к сериализации.</returns>
|
/// <returns>
|
||||||
|
/// Объект состояния, готовый к сериализации.
|
||||||
|
/// </returns>
|
||||||
object GetSerializableState();
|
object GetSerializableState();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Восстанавливает состояние из десериализованного объекта.
|
/// Восстанавливает состояние объекта из десериализованного объекта.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="state">Десериализованное состояние.</param>
|
/// <param name="state">Десериализованное состояние.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, когда <paramref name="state"/> равен null.
|
||||||
|
/// </exception>
|
||||||
void RestoreFromState(object state);
|
void RestoreFromState(object state);
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Реестр типов содержимого, который позволяет создавать экземпляры контента по типу.
|
/// Реестр типов содержимого, который позволяет создавать экземпляры контента по типу.
|
||||||
/// Этот сервис является центральным для динамического создания панелей инструментов и документов в IDE.
|
/// Этот сервис является центральным для динамического создания панелей инструментов и документов.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Реализует шаблон "Фабрика" для создания экземпляров <see cref="Abstractions.IDockContent"/>.
|
/// Реализует шаблон "Фабрика" для создания экземпляров <see cref="Abstractions.IDockContent"/>.
|
||||||
@@ -16,12 +16,19 @@ public class ContentRegistry
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Регистрирует фабричный метод для создания контента указанного типа.
|
/// Регистрирует фабричный метод для создания контента указанного типа.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">Тип контента, реализующий <see cref="Abstractions.IDockContent"/>.</typeparam>
|
/// <typeparam name="T">
|
||||||
|
/// Тип контента, реализующий <see cref="Abstractions.IDockContent"/>.
|
||||||
|
/// </typeparam>
|
||||||
/// <param name="contentTypeId">Уникальный идентификатор типа контента.</param>
|
/// <param name="contentTypeId">Уникальный идентификатор типа контента.</param>
|
||||||
/// <param name="factory">Фабричный метод для создания экземпляров контента.</param>
|
/// <param name="factory">Фабричный метод для создания экземпляров контента.</param>
|
||||||
/// <param name="metadata">Метаданные типа контента (опционально).</param>
|
/// <param name="metadata">Метаданные типа контента (опционально).</param>
|
||||||
/// <exception cref="ArgumentNullException">Выбрасывается, если contentTypeId или factory равны null.</exception>
|
/// <exception cref="ArgumentNullException">
|
||||||
/// <exception cref="ArgumentException">Выбрасывается, если contentTypeId уже зарегистрирован.</exception>
|
/// Выбрасывается, если <paramref name="contentTypeId"/> или <paramref name="factory"/>
|
||||||
|
/// равны null.
|
||||||
|
/// </exception>
|
||||||
|
/// <exception cref="ArgumentException">
|
||||||
|
/// Выбрасывается, если <paramref name="contentTypeId"/> уже зарегистрирован.
|
||||||
|
/// </exception>
|
||||||
public void Register<T>(string contentTypeId, Func<T> factory, ContentMetadata? metadata = null)
|
public void Register<T>(string contentTypeId, Func<T> factory, ContentMetadata? metadata = null)
|
||||||
where T : Abstractions.IDockContent
|
where T : Abstractions.IDockContent
|
||||||
{
|
{
|
||||||
@@ -30,8 +37,12 @@ public class ContentRegistry
|
|||||||
if (factory == null)
|
if (factory == null)
|
||||||
throw new ArgumentNullException(nameof(factory));
|
throw new ArgumentNullException(nameof(factory));
|
||||||
|
|
||||||
|
// Дополнительная проверка на пустую строку
|
||||||
|
if (string.IsNullOrEmpty(contentTypeId.Trim()))
|
||||||
|
throw new ArgumentException("Идентификатор типа контента не может быть пустой строкой.", nameof(contentTypeId));
|
||||||
|
|
||||||
if (_contentTypes.ContainsKey(contentTypeId))
|
if (_contentTypes.ContainsKey(contentTypeId))
|
||||||
throw new ArgumentException($"Content type '{contentTypeId}' is already registered.");
|
throw new ArgumentException($"Тип контента '{contentTypeId}' уже зарегистрирован.");
|
||||||
|
|
||||||
_contentTypes[contentTypeId] = new ContentDescriptor(
|
_contentTypes[contentTypeId] = new ContentDescriptor(
|
||||||
typeof(T),
|
typeof(T),
|
||||||
@@ -45,20 +56,25 @@ public class ContentRegistry
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="contentTypeId">Идентификатор типа контента.</param>
|
/// <param name="contentTypeId">Идентификатор типа контента.</param>
|
||||||
/// <param name="id">Уникальный идентификатор для создаваемого экземпляра контента.</param>
|
/// <param name="id">Уникальный идентификатор для создаваемого экземпляра контента.</param>
|
||||||
/// <returns>Новый экземпляр контента.</returns>
|
/// <returns>
|
||||||
/// <exception cref="KeyNotFoundException">Выбрасывается, если тип контента не зарегистрирован.</exception>
|
/// Новый экземпляр контента.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, если <paramref name="contentTypeId"/> равен null или пустой строке.
|
||||||
|
/// </exception>
|
||||||
|
/// <exception cref="KeyNotFoundException">
|
||||||
|
/// Выбрасывается, если тип контента не зарегистрирован.
|
||||||
|
/// </exception>
|
||||||
public Abstractions.IDockContent CreateContent(string contentTypeId, string id)
|
public Abstractions.IDockContent CreateContent(string contentTypeId, string id)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(contentTypeId))
|
||||||
|
throw new ArgumentNullException(nameof(contentTypeId));
|
||||||
|
|
||||||
if (!_contentTypes.TryGetValue(contentTypeId, out var descriptor))
|
if (!_contentTypes.TryGetValue(contentTypeId, out var descriptor))
|
||||||
throw new KeyNotFoundException($"Content type '{contentTypeId}' is not registered.");
|
throw new KeyNotFoundException($"Тип контента '{contentTypeId}' не зарегистрирован.");
|
||||||
|
|
||||||
var content = descriptor.Factory();
|
var content = descriptor.Factory();
|
||||||
// Устанавливаем ID через рефлексию, если есть свойство Id
|
content.SetId(id);
|
||||||
var property = content.GetType().GetProperty("Id");
|
|
||||||
if (property != null && property.CanWrite)
|
|
||||||
{
|
|
||||||
property.SetValue(content, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
@@ -67,9 +83,14 @@ public class ContentRegistry
|
|||||||
/// Получает метаданные для указанного типа контента.
|
/// Получает метаданные для указанного типа контента.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="contentTypeId">Идентификатор типа контента.</param>
|
/// <param name="contentTypeId">Идентификатор типа контента.</param>
|
||||||
/// <returns>Метаданные типа контента или null, если тип не найден.</returns>
|
/// <returns>
|
||||||
|
/// Метаданные типа контента или null, если тип не найден.
|
||||||
|
/// </returns>
|
||||||
public ContentMetadata? GetMetadata(string contentTypeId)
|
public ContentMetadata? GetMetadata(string contentTypeId)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(contentTypeId))
|
||||||
|
return null;
|
||||||
|
|
||||||
return _contentTypes.TryGetValue(contentTypeId, out var descriptor)
|
return _contentTypes.TryGetValue(contentTypeId, out var descriptor)
|
||||||
? descriptor.Metadata
|
? descriptor.Metadata
|
||||||
: null;
|
: null;
|
||||||
@@ -78,24 +99,54 @@ public class ContentRegistry
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает все зарегистрированные типы контента.
|
/// Получает все зарегистрированные типы контента.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Коллекция идентификаторов зарегистрированных типов контента.</returns>
|
/// <returns>
|
||||||
|
/// Коллекция идентификаторов зарегистрированных типов контента.
|
||||||
|
/// </returns>
|
||||||
public IEnumerable<string> GetRegisteredTypes() => _contentTypes.Keys;
|
public IEnumerable<string> GetRegisteredTypes() => _contentTypes.Keys;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Проверяет, зарегистрирован ли указанный тип контента.
|
/// Проверяет, зарегистрирован ли указанный тип контента.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsRegistered(string contentTypeId) => _contentTypes.ContainsKey(contentTypeId);
|
/// <param name="contentTypeId">Идентификатор типа контента.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// true, если тип контента зарегистрирован; в противном случае false.
|
||||||
|
/// </returns>
|
||||||
|
public bool IsRegistered(string contentTypeId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(contentTypeId))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return _contentTypes.ContainsKey(contentTypeId);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Дескриптор типа контента, содержащий информацию о фабричном методе и метаданных.
|
/// Представляет дескриптор типа контента, содержащий информацию о фабричном методе и метаданных.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private class ContentDescriptor
|
private class ContentDescriptor
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получает тип контента.
|
||||||
|
/// </summary>
|
||||||
public Type ContentType { get; }
|
public Type ContentType { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает фабричный метод для создания экземпляров контента.
|
||||||
|
/// </summary>
|
||||||
public Func<Abstractions.IDockContent> Factory { get; }
|
public Func<Abstractions.IDockContent> Factory { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает метаданные типа контента.
|
||||||
|
/// </summary>
|
||||||
public ContentMetadata Metadata { get; }
|
public ContentMetadata Metadata { get; }
|
||||||
|
|
||||||
public ContentDescriptor(Type contentType, Func<Abstractions.IDockContent> factory, ContentMetadata metadata)
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="ContentDescriptor"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="contentType">Тип контента.</param>
|
||||||
|
/// <param name="factory">Фабричный метод.</param>
|
||||||
|
/// <param name="metadata">Метаданные.</param>
|
||||||
|
public ContentDescriptor(Type contentType, Func<Abstractions.IDockContent> factory,
|
||||||
|
ContentMetadata metadata)
|
||||||
{
|
{
|
||||||
ContentType = contentType;
|
ContentType = contentType;
|
||||||
Factory = factory;
|
Factory = factory;
|
||||||
@@ -105,54 +156,80 @@ public class ContentRegistry
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Метаданные типа контента, предоставляющие дополнительную информацию для отображения в UI.
|
/// Представляет метаданные типа контента, предоставляющие дополнительную информацию для отображения в UI.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ContentMetadata
|
public class ContentMetadata
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Идентификатор типа контента.
|
/// Получает идентификатор типа контента.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Уникальный строковый идентификатор типа контента.
|
||||||
|
/// </value>
|
||||||
public string ContentTypeId { get; }
|
public string ContentTypeId { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Отображаемое имя типа контента.
|
/// Получает или задает отображаемое имя типа контента.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Имя типа контента, отображаемое пользователю.
|
||||||
|
/// </value>
|
||||||
public string DisplayName { get; set; }
|
public string DisplayName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Описание типа контента.
|
/// Получает или задает описание типа контента.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Текстовое описание функциональности контента.
|
||||||
|
/// </value>
|
||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Имя ресурса для иконки (опционально).
|
/// Получает или задает имя ресурса для иконки типа контента.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Имя ресурса иконки или null, если иконка не определена.
|
||||||
|
/// </value>
|
||||||
public string? IconResource { get; set; }
|
public string? IconResource { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Признак того, что контент является документом (а не инструментальной панелью).
|
/// Получает или задает значение, указывающее, является ли контент документом
|
||||||
|
/// (а не инструментальной панелью).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// true, если контент является документом; в противном случае false.
|
||||||
|
/// </value>
|
||||||
public bool IsDocument { get; set; }
|
public bool IsDocument { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Минимальная ширина контента в пикселях.
|
/// Получает или задает ширину контента по умолчанию.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Ширина контента в пикселях. Значение по умолчанию: 300.
|
||||||
|
/// </value>
|
||||||
public double DefaultWidth { get; set; } = 300;
|
public double DefaultWidth { get; set; } = 300;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Минимальная высота контента в пикселях.
|
/// Получает или задает высоту контента по умолчанию.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Высота контента в пикселях. Значение по умолчанию: 200.
|
||||||
|
/// </value>
|
||||||
public double DefaultHeight { get; set; } = 200;
|
public double DefaultHeight { get; set; } = 200;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Инициализирует новый экземпляр метаданных контента.
|
/// Инициализирует новый экземпляр класса <see cref="ContentMetadata"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="contentTypeId">Идентификатор типа контента.</param>
|
/// <param name="contentTypeId">Идентификатор типа контента.</param>
|
||||||
/// <param name="displayName">Отображаемое имя.</param>
|
/// <param name="displayName">Отображаемое имя типа контента.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, если <paramref name="contentTypeId"/> или <paramref name="displayName"/>
|
||||||
|
/// равны null.
|
||||||
|
/// </exception>
|
||||||
public ContentMetadata(string contentTypeId, string displayName)
|
public ContentMetadata(string contentTypeId, string displayName)
|
||||||
{
|
{
|
||||||
ContentTypeId = contentTypeId;
|
ContentTypeId = contentTypeId ?? throw new ArgumentNullException(nameof(contentTypeId));
|
||||||
DisplayName = displayName;
|
DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName));
|
||||||
Description = string.Empty;
|
Description = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
using Lattice.Core.DragDrop.Abstractions;
|
|
||||||
using Lattice.Core.DragDrop.Enums;
|
|
||||||
using Lattice.Core.DragDrop.Models;
|
|
||||||
using Lattice.Core.DragDrop.Services;
|
|
||||||
using Lattice.Core.Geometry;
|
|
||||||
using Moq;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Lattice.Core.DragDrop.Tests;
|
|
||||||
|
|
||||||
public class DragDropServiceTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void StartDrag_WithValidSource_StartsDragOperation()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var service = new DragDropService();
|
|
||||||
var mockSource = new Mock<IDragSource>();
|
|
||||||
var dragInfo = new DragInfo("test", DragDropEffects.Copy, new Point(0, 0));
|
|
||||||
|
|
||||||
mockSource.Setup(s => s.CanStartDrag(out dragInfo)).Returns(true);
|
|
||||||
mockSource.Setup(s => s.StartDrag(It.IsAny<DragInfo>())).Returns(true);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = service.StartDrag(mockSource.Object, new Point(0, 0));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(result);
|
|
||||||
Assert.True(service.IsDragActive);
|
|
||||||
Assert.NotNull(service.CurrentDragInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void RegisterDropTarget_ReturnsValidId()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var service = new DragDropService();
|
|
||||||
var mockTarget = new Mock<IDropTarget>();
|
|
||||||
var bounds = new Rect(0, 0, 100, 100);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var id = service.RegisterDropTarget(mockTarget.Object, bounds);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(id);
|
|
||||||
Assert.NotEmpty(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void UpdateDrag_WithValidDropTarget_CallsDragOver()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var service = new DragDropService();
|
|
||||||
var mockSource = new Mock<IDragSource>();
|
|
||||||
var mockTarget = new Mock<IDropTarget>();
|
|
||||||
|
|
||||||
var dragInfo = new DragInfo("test", DragDropEffects.Copy, new Point(0, 0));
|
|
||||||
mockSource.Setup(s => s.CanStartDrag(out dragInfo)).Returns(true);
|
|
||||||
mockSource.Setup(s => s.StartDrag(It.IsAny<DragInfo>())).Returns(true);
|
|
||||||
|
|
||||||
var targetId = service.RegisterDropTarget(mockTarget.Object, new Rect(0, 0, 100, 100));
|
|
||||||
service.StartDrag(mockSource.Object, new Point(0, 0));
|
|
||||||
|
|
||||||
// Act
|
|
||||||
service.UpdateDrag(new Point(50, 50));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
mockTarget.Verify(t => t.DragOver(It.IsAny<DropInfo>()), Times.AtLeastOnce());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void EndDrag_WithValidDrop_CallsDrop()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var service = new DragDropService();
|
|
||||||
var mockSource = new Mock<IDragSource>();
|
|
||||||
var mockTarget = new Mock<IDropTarget>();
|
|
||||||
|
|
||||||
var dragInfo = new DragInfo("test", DragDropEffects.Copy, new Point(0, 0));
|
|
||||||
mockSource.Setup(s => s.CanStartDrag(out dragInfo)).Returns(true);
|
|
||||||
mockSource.Setup(s => s.StartDrag(It.IsAny<DragInfo>())).Returns(true);
|
|
||||||
|
|
||||||
service.RegisterDropTarget(mockTarget.Object, new Rect(0, 0, 100, 100));
|
|
||||||
service.StartDrag(mockSource.Object, new Point(0, 0));
|
|
||||||
service.UpdateDrag(new Point(50, 50));
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var effects = service.EndDrag(new Point(50, 50));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
mockTarget.Verify(t => t.Drop(It.IsAny<DropInfo>()), Times.Once());
|
|
||||||
Assert.False(service.IsDragActive);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CancelDrag_WithActiveDrag_CallsDragCancelled()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var service = new DragDropService();
|
|
||||||
var mockSource = new Mock<IDragSource>();
|
|
||||||
var dragInfo = new DragInfo("test", DragDropEffects.Copy, new Point(0, 0));
|
|
||||||
|
|
||||||
mockSource.Setup(s => s.CanStartDrag(out dragInfo)).Returns(true);
|
|
||||||
mockSource.Setup(s => s.StartDrag(It.IsAny<DragInfo>())).Returns(true);
|
|
||||||
|
|
||||||
service.StartDrag(mockSource.Object, new Point(0, 0));
|
|
||||||
|
|
||||||
// Act
|
|
||||||
service.CancelDrag();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
mockSource.Verify(s => s.DragCancelled(It.IsAny<DragInfo>()), Times.Once());
|
|
||||||
Assert.False(service.IsDragActive);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
|
||||||
<PackageReference Include="Moq" Version="4.20.70" />
|
|
||||||
<PackageReference Include="xunit" Version="2.6.3" />
|
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Lattice.Core.DragDrop\Lattice.Core.DragDrop.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
namespace Lattice.Core.DragDrop.Abstractions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Определяет контракт для объектов, которые могут быть источником данных
|
|
||||||
/// в операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Объекты, реализующие этот интерфейс, могут инициировать операции перетаскивания
|
|
||||||
/// и предоставлять данные для передачи другим элементам через механизм drag-and-drop.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Интерфейс полностью асинхронный и поддерживает отмену операций через CancellationToken.
|
|
||||||
/// Все методы должны быть потокобезопасными и поддерживать вызов из любого потока.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public interface IDragSource
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Пытается начать операцию перетаскивания из указанной позиции.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="startPosition">Начальная позиция операции в координатах экрана.</param>
|
|
||||||
/// <param name="cancellationToken">Токен отмены операции.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// Информация о перетаскивании, если операция может быть начата; в противном случае — null.
|
|
||||||
/// Возвращаемый объект <see cref="Models.DragInfo"/> должен быть полностью инициализирован,
|
|
||||||
/// включая данные, разрешенные эффекты и ссылку на источник.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод вызывается сервисом перетаскивания при попытке начать операцию
|
|
||||||
/// (обычно при нажатии и перемещении мыши). Метод должен проверить, может ли
|
|
||||||
/// источник начать перетаскивание в текущем контексте.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Реализация должна быть быстрой и не выполнять длительных операций.
|
|
||||||
/// Если подготовка данных требует времени, ее следует выполнить асинхронно
|
|
||||||
/// после подтверждения возможности начала.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
Task<Models.DragInfo?> TryStartDragAsync(Geometry.Point startPosition, CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Уведомляет источник о завершении операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragInfo">Информация о перетаскивании, полученная при начале операции.</param>
|
|
||||||
/// <param name="effects">Эффекты, которые были применены при сбросе.</param>
|
|
||||||
/// <param name="cancellationToken">Токен отмены операции.</param>
|
|
||||||
/// <returns>Задача, представляющая асинхронную операцию.</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод вызывается после завершения операции перетаскивания
|
|
||||||
/// (успешного или неуспешного). Реализация может:
|
|
||||||
/// </para>
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Выполнить очистку ресурсов, связанных с операцией</item>
|
|
||||||
/// <item>Обновить состояние на основе результата (например, удалить данные при перемещении)</item>
|
|
||||||
/// <item>Отобразить визуальную обратную связь о результате</item>
|
|
||||||
/// </list>
|
|
||||||
/// <para>
|
|
||||||
/// Если операция завершилась с эффектом <see cref="Enums.DragDropEffects.Move"/>,
|
|
||||||
/// источник обычно должен удалить или обновить исходные данные.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
Task OnDragCompletedAsync(Models.DragInfo dragInfo, Enums.DragDropEffects effects, CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Уведомляет источник об отмене операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragInfo">Информация о перетаскивании, полученная при начале операции.</param>
|
|
||||||
/// <param name="cancellationToken">Токен отмены операции.</param>
|
|
||||||
/// <returns>Задача, представляющая асинхронную операцию.</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод вызывается, когда операция перетаскивания была отменена
|
|
||||||
/// пользователем (например, нажатием клавиши Escape) или системой.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Реализация должна выполнить очистку и восстановить исходное состояние.
|
|
||||||
/// Обычно это включает освобождение ресурсов и сброс визуальных индикаторов.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
Task OnDragCancelledAsync(Models.DragInfo dragInfo, CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
namespace Lattice.Core.DragDrop.Abstractions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Определяет контракт для объектов, которые могут принимать сбрасываемые данные
|
|
||||||
/// в операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Объекты, реализующие этот интерфейс, могут обрабатывать данные, сброшенные
|
|
||||||
/// пользователем, и предоставлять визуальную обратную связь во время перетаскивания.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Интерфейс поддерживает асинхронные операции и отмену через CancellationToken.
|
|
||||||
/// Все методы должны быть потокобезопасными и идемпотентными (многократный вызов
|
|
||||||
/// с одинаковыми параметрами должен давать одинаковый результат).
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public interface IDropTarget
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Определяет, может ли объект принять сбрасываемые данные.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dropInfo">Информация о потенциальном сбросе.</param>
|
|
||||||
/// <param name="cancellationToken">Токен отмены операции.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// true, если объект может принять данные; в противном случае — false.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод вызывается, когда перетаскиваемый объект находится над целью.
|
|
||||||
/// Реализация должна проверить, совместимы ли данные с целью, и установить
|
|
||||||
/// предлагаемые эффекты в свойстве <see cref="Models.DropInfo.SuggestedEffects"/>.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Метод может вызываться многократно при перемещении курсора над целью.
|
|
||||||
/// Реализация должна быть эффективной и избегать длительных операций.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Если метод возвращает false, система не будет вызывать другие методы
|
|
||||||
/// для этой цели до тех пор, пока курсор не покинет ее область.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
Task<bool> CanAcceptDropAsync(Models.DropInfo dropInfo, CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывается, когда перетаскиваемый объект перемещается над целью.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dropInfo">Информация о текущем положении перетаскивания.</param>
|
|
||||||
/// <param name="cancellationToken">Токен отмены операции.</param>
|
|
||||||
/// <returns>Задача, представляющая асинхронную операцию.</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод вызывается постоянно, пока пользователь перемещает объект над целью.
|
|
||||||
/// Реализация может:
|
|
||||||
/// </para>
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Обновить визуальную обратную связь (подсветка, изменение курсора)</item>
|
|
||||||
/// <item>Вычислить точную позицию сброса (например, между элементами списка)</item>
|
|
||||||
/// <item>Уточнить предлагаемые эффекты на основе текущей позиции</item>
|
|
||||||
/// <item>Прокрутить содержимое, если цель поддерживает прокрутку</item>
|
|
||||||
/// </list>
|
|
||||||
/// <para>
|
|
||||||
/// Метод должен быть оптимизирован для частого вызова. Длительные операции
|
|
||||||
/// должны выполняться асинхронно без блокировки потока.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
Task OnDragOverAsync(Models.DropInfo dropInfo, CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывается, когда пользователь сбрасывает данные на цель.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dropInfo">Информация о сбросе, включая данные и позицию.</param>
|
|
||||||
/// <param name="cancellationToken">Токен отмены операции.</param>
|
|
||||||
/// <returns>Задача, представляющая асинхронную операцию.</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод вызывается, когда пользователь отпускает кнопку мыши над целью.
|
|
||||||
/// Реализация должна обработать принятие данных и выполнить соответствующее действие:
|
|
||||||
/// </para>
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Добавить данные в коллекцию (для копирования)</item>
|
|
||||||
/// <item>Переместить данные (при поддержке перемещения)</item>
|
|
||||||
/// <item>Создать ссылку на данные</item>
|
|
||||||
/// <item>Выполнить пользовательскую логику обработки</item>
|
|
||||||
/// </list>
|
|
||||||
/// <para>
|
|
||||||
/// После успешной обработки данных следует вызвать <see cref="Models.DropInfo.MarkAsHandled()"/>,
|
|
||||||
/// чтобы указать системе, что операция обработана и дополнительная обработка не требуется.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Если операция завершилась успешно, система уведомит источник через
|
|
||||||
/// <see cref="IDragSource.OnDragCompletedAsync"/> с соответствующими эффектами.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
Task OnDropAsync(Models.DropInfo dropInfo, CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывается, когда перетаскиваемый объект покидает область цели.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="cancellationToken">Токен отмены операции.</param>
|
|
||||||
/// <returns>Задача, представляющая асинхронную операцию.</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод вызывается, когда пользователь перемещает объект за пределы цели.
|
|
||||||
/// Реализация должна:
|
|
||||||
/// </para>
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Очистить любую визуальную обратную связь, установленную ранее</item>
|
|
||||||
/// <item>Сбросить временное состояние, связанное с операцией</item>
|
|
||||||
/// <item>Освободить ресурсы, выделенные для предварительного просмотра</item>
|
|
||||||
/// </list>
|
|
||||||
/// <para>
|
|
||||||
/// Метод гарантированно вызывается после любого успешного или неуспешного
|
|
||||||
/// вызова <see cref="OnDragOverAsync"/>, если курсор покидает область цели.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
Task OnDragLeaveAsync(CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
namespace Lattice.Core.DragDrop.Enums;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Определяет эффекты, которые могут быть применены при операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот перечисление используется для указания допустимых операций перетаскивания
|
|
||||||
/// и передачи информации о результате операции между источником и целью.
|
|
||||||
/// </remarks>
|
|
||||||
[Flags]
|
|
||||||
public enum DragDropEffects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Операция перетаскивания не разрешена.
|
|
||||||
/// </summary>
|
|
||||||
None = 0,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Данные копируются из источника в цель.
|
|
||||||
/// </summary>
|
|
||||||
Copy = 1 << 0,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Данные перемещаются из источника в цель.
|
|
||||||
/// </summary>
|
|
||||||
Move = 1 << 1,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создается ссылка на исходные данные.
|
|
||||||
/// </summary>
|
|
||||||
Link = 1 << 2,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Целевой элемент может прокручиваться во время перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
Scroll = 1 << 3,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Комбинированный эффект копирования и перемещения.
|
|
||||||
/// </summary>
|
|
||||||
CopyOrMove = Copy | Move,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Все эффекты разрешены.
|
|
||||||
/// </summary>
|
|
||||||
All = Copy | Move | Link | Scroll
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Расширения для работы с DragDropEffects.
|
|
||||||
/// </summary>
|
|
||||||
public static class DragDropEffectsExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Проверяет, содержит ли эффекты указанный эффект.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="effects">Эффекты для проверки.</param>
|
|
||||||
/// <param name="effect">Эффект для поиска.</param>
|
|
||||||
/// <returns>true, если эффект присутствует; в противном случае — false.</returns>
|
|
||||||
public static bool HasEffect(this DragDropEffects effects, DragDropEffects effect)
|
|
||||||
{
|
|
||||||
return (effects & effect) == effect;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Проверяет, содержат ли эффекты копирование.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="effects">Эффекты для проверки.</param>
|
|
||||||
/// <returns>true, если разрешено копирование; в противном случае — false.</returns>
|
|
||||||
public static bool CanCopy(this DragDropEffects effects)
|
|
||||||
{
|
|
||||||
return effects.HasEffect(DragDropEffects.Copy);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Проверяет, содержат ли эффекты перемещение.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="effects">Эффекты для проверки.</param>
|
|
||||||
/// <returns>true, если разрешено перемещение; в противном случае — false.</returns>
|
|
||||||
public static bool CanMove(this DragDropEffects effects)
|
|
||||||
{
|
|
||||||
return effects.HasEffect(DragDropEffects.Move);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Проверяет, содержат ли эффекты ссылку.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="effects">Эффекты для проверки.</param>
|
|
||||||
/// <returns>true, если разрешена ссылка; в противном случае — false.</returns>
|
|
||||||
public static bool CanLink(this DragDropEffects effects)
|
|
||||||
{
|
|
||||||
return effects.HasEffect(DragDropEffects.Link);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает наиболее подходящий эффект на основе модификаторов клавиатуры.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="controlKey">Нажата ли клавиша Control.</param>
|
|
||||||
/// <param name="shiftKey">Нажата ли клавиша Shift.</param>
|
|
||||||
/// <param name="altKey">Нажата ли клавиша Alt.</param>
|
|
||||||
/// <returns>Наиболее подходящий эффект перетаскивания.</returns>
|
|
||||||
public static DragDropEffects GetEffectFromKeys(bool controlKey, bool shiftKey, bool altKey)
|
|
||||||
{
|
|
||||||
if (controlKey && shiftKey)
|
|
||||||
return DragDropEffects.Link;
|
|
||||||
if (controlKey)
|
|
||||||
return DragDropEffects.Copy;
|
|
||||||
if (shiftKey)
|
|
||||||
return DragDropEffects.Move;
|
|
||||||
if (altKey)
|
|
||||||
return DragDropEffects.Link;
|
|
||||||
|
|
||||||
return DragDropEffects.Move; // По умолчанию
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
namespace Lattice.Core.DragDrop.Enums;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Позиция сброса относительно цели.
|
|
||||||
/// </summary>
|
|
||||||
public enum DropPosition
|
|
||||||
{
|
|
||||||
Inside,
|
|
||||||
Top,
|
|
||||||
Bottom,
|
|
||||||
Left,
|
|
||||||
Right,
|
|
||||||
Center
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
namespace Lattice.Core.DragDrop.Exceptions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Исключение, возникающее при ошибках в системе перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public class DragDropException : Exception
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Код ошибки.
|
|
||||||
/// </summary>
|
|
||||||
public string ErrorCode { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DragDropException"/>.
|
|
||||||
/// </summary>
|
|
||||||
public DragDropException()
|
|
||||||
: base("Drag & Drop operation failed.")
|
|
||||||
{
|
|
||||||
ErrorCode = "DRAGDROP_0001";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DragDropException"/> с указанным сообщением.
|
|
||||||
/// </summary>
|
|
||||||
public DragDropException(string message)
|
|
||||||
: base(message)
|
|
||||||
{
|
|
||||||
ErrorCode = "DRAGDROP_0002";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DragDropException"/> с кодом ошибки.
|
|
||||||
/// </summary>
|
|
||||||
public DragDropException(string errorCode, string message)
|
|
||||||
: base(message)
|
|
||||||
{
|
|
||||||
ErrorCode = errorCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DragDropException"/>
|
|
||||||
/// с указанным сообщением и внутренним исключением.
|
|
||||||
/// </summary>
|
|
||||||
public DragDropException(string message, Exception innerException)
|
|
||||||
: base(message, innerException)
|
|
||||||
{
|
|
||||||
ErrorCode = "DRAGDROP_0003";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DragDropException"/>
|
|
||||||
/// с кодом ошибки, сообщением и внутренним исключением.
|
|
||||||
/// </summary>
|
|
||||||
public DragDropException(string errorCode, string message, Exception innerException)
|
|
||||||
: base(message, innerException)
|
|
||||||
{
|
|
||||||
ErrorCode = errorCode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Коды ошибок Drag and Drop системы.
|
|
||||||
/// </summary>
|
|
||||||
public static class DragDropErrorCodes
|
|
||||||
{
|
|
||||||
// Общие ошибки
|
|
||||||
public const string OperationAlreadyActive = "DRAGDROP_1001";
|
|
||||||
public const string OperationNotActive = "DRAGDROP_1002";
|
|
||||||
public const string InvalidData = "DRAGDROP_1003";
|
|
||||||
public const string Timeout = "DRAGDROP_1004";
|
|
||||||
|
|
||||||
// Ошибки источников
|
|
||||||
public const string SourceCannotDrag = "DRAGDROP_2001";
|
|
||||||
public const string SourceStartFailed = "DRAGDROP_2002";
|
|
||||||
|
|
||||||
// Ошибки целей
|
|
||||||
public const string TargetNotFound = "DRAGDROP_3001";
|
|
||||||
public const string TargetCannotAccept = "DRAGDROP_3002";
|
|
||||||
public const string TargetDropFailed = "DRAGDROP_3003";
|
|
||||||
|
|
||||||
// Ошибки системы
|
|
||||||
public const string SystemNotInitialized = "DRAGDROP_4001";
|
|
||||||
public const string SystemDisposed = "DRAGDROP_4002";
|
|
||||||
public const string MemoryAllocationFailed = "DRAGDROP_4003";
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
using Lattice.Core.DragDrop.Enums;
|
|
||||||
using Lattice.Core.Geometry;
|
|
||||||
|
|
||||||
namespace Lattice.Core.DragDrop.Extensions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Методы расширения для DropInfo.
|
|
||||||
/// </summary>
|
|
||||||
public static class DropInfoExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Проверяет, могут ли данные быть приведены к указанному типу.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">Тип данных для проверки.</typeparam>
|
|
||||||
/// <param name="dropInfo">Информация о сбросе.</param>
|
|
||||||
/// <returns>true, если данные могут быть приведены к типу T; в противном случае — false.</returns>
|
|
||||||
public static bool CanAccept<T>(this Models.DropInfo dropInfo)
|
|
||||||
where T : class
|
|
||||||
{
|
|
||||||
return dropInfo.Data is T;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Пытается получить данные как указанный тип.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">Тип, к которому нужно привести данные.</typeparam>
|
|
||||||
/// <param name="dropInfo">Информация о сбросе.</param>
|
|
||||||
/// <returns>Данные как тип T или null.</returns>
|
|
||||||
public static T? GetDataAs<T>(this Models.DropInfo dropInfo)
|
|
||||||
where T : class
|
|
||||||
{
|
|
||||||
return dropInfo.Data as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает данные как указанный тип или выбрасывает исключение.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">Тип, к которому нужно привести данные.</typeparam>
|
|
||||||
/// <param name="dropInfo">Информация о сбросе.</param>
|
|
||||||
/// <returns>Данные как тип T.</returns>
|
|
||||||
/// <exception cref="InvalidCastException">Выбрасывается, если данные не могут быть приведены к типу T.</exception>
|
|
||||||
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>
|
|
||||||
/// <param name="dropInfo">Информация о сбросе.</param>
|
|
||||||
/// <param name="bounds">Границы для проверки.</param>
|
|
||||||
/// <returns>true, если позиция находится в границах; в противном случае — false.</returns>
|
|
||||||
public static bool IsInBounds(this Models.DropInfo dropInfo, Rect bounds)
|
|
||||||
{
|
|
||||||
return bounds.Contains(dropInfo.Position);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Проверяет можно ли добавить эффект перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dropInfo"></param>
|
|
||||||
/// <param name="effect"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static bool CanAcceptEffect(this Models.DropInfo dropInfo, DragDropEffects effect)
|
|
||||||
{
|
|
||||||
return dropInfo.AllowedEffects.HasEffect(effect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,581 +0,0 @@
|
|||||||
using Lattice.Core.DragDrop.Abstractions;
|
|
||||||
using Lattice.Core.DragDrop.Enums;
|
|
||||||
using Lattice.Core.DragDrop.Models;
|
|
||||||
using Lattice.Core.DragDrop.Services;
|
|
||||||
using Lattice.Core.Geometry;
|
|
||||||
|
|
||||||
namespace Lattice.Core.DragDrop.Factories;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Фабрика для создания компонентов системы перетаскивания.
|
|
||||||
/// Предоставляет методы для создания сервисов, источников и целей перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Эта фабрика позволяет создавать компоненты системы перетаскивания без использования
|
|
||||||
/// Dependency Injection, предоставляя простой и понятный API для наиболее распространенных сценариев.
|
|
||||||
/// </remarks>
|
|
||||||
public static class DragDropFactory
|
|
||||||
{
|
|
||||||
#region Сервисы перетаскивания
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает новый экземпляр сервиса перетаскивания с настройками по умолчанию.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>
|
|
||||||
/// Экземпляр <see cref="IDragDropService"/> с настройками по умолчанию.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Созданный сервис имеет следующие настройки по умолчанию:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Порог начала перетаскивания: 3.0 пикселей</item>
|
|
||||||
/// <item>Таймаут асинхронных операций: 5000 миллисекунд</item>
|
|
||||||
/// <item>Асинхронные операции: включены</item>
|
|
||||||
/// </list>
|
|
||||||
/// </remarks>
|
|
||||||
public static IDragDropService CreateDragDropService()
|
|
||||||
{
|
|
||||||
return new DragDropService();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает новый экземпляр сервиса перетаскивания с пользовательскими настройками.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="configure">
|
|
||||||
/// Делегат для настройки опций сервиса. Передает экземпляр <see cref="DragDropServiceOptions"/>
|
|
||||||
/// для настройки параметров.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Настроенный экземпляр <see cref="IDragDropService"/>.
|
|
||||||
/// </returns>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// var service = DragDropFactory.CreateDragDropService(options =>
|
|
||||||
/// {
|
|
||||||
/// options.DragStartThreshold = 5.0;
|
|
||||||
/// options.AsyncOperationTimeout = 3000;
|
|
||||||
/// options.EnableAsyncOperations = true;
|
|
||||||
/// });
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
public static IDragDropService CreateDragDropService(Action<DragDropServiceOptions> configure)
|
|
||||||
{
|
|
||||||
var options = new DragDropServiceOptions();
|
|
||||||
configure(options);
|
|
||||||
|
|
||||||
return new DragDropService
|
|
||||||
{
|
|
||||||
DragStartThreshold = options.DragStartThreshold,
|
|
||||||
AsyncOperationTimeout = options.AsyncOperationTimeout,
|
|
||||||
EnableAsyncOperations = options.EnableAsyncOperations
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает сервис перетаскивания, оптимизированный для сенсорных устройств.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>
|
|
||||||
/// Экземпляр <see cref="IDragDropService"/> с увеличенным порогом перетаскивания.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот метод создает сервис с увеличенным порогом начала перетаскивания (10.0 пикселей),
|
|
||||||
/// что уменьшает вероятность случайного начала перетаскивания при использовании сенсорного экрана.
|
|
||||||
/// </remarks>
|
|
||||||
public static IDragDropService CreateTouchOptimizedService()
|
|
||||||
{
|
|
||||||
return new DragDropService
|
|
||||||
{
|
|
||||||
DragStartThreshold = 10.0,
|
|
||||||
AsyncOperationTimeout = 3000,
|
|
||||||
EnableAsyncOperations = true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает сервис перетаскивания для точных операций (графические редакторы, карты).
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>
|
|
||||||
/// Экземпляр <see cref="IDragDropService"/> с уменьшенным порогом перетаскивания.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот метод создает сервис с минимальным порогом начала перетаскивания (1.0 пиксель),
|
|
||||||
/// что позволяет начинать перетаскивание с максимальной точностью.
|
|
||||||
/// </remarks>
|
|
||||||
public static IDragDropService CreatePrecisionDragService()
|
|
||||||
{
|
|
||||||
return new DragDropService
|
|
||||||
{
|
|
||||||
DragStartThreshold = 1.0,
|
|
||||||
AsyncOperationTimeout = 10000, // Больше времени для сложных операций
|
|
||||||
EnableAsyncOperations = true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Источники перетаскивания
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает простой источник перетаскивания с фиксированными данными.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">
|
|
||||||
/// Данные, которые будут перетаскиваться. Не может быть null.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="allowedEffects">
|
|
||||||
/// Разрешенные эффекты перетаскивания. По умолчанию разрешены копирование и перемещение.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Экземпляр <see cref="IDragSource"/>, который всегда предоставляет указанные данные.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, когда <paramref name="data"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот источник подходит для случаев, когда данные для перетаскивания известны заранее
|
|
||||||
/// и не изменяются в процессе операции.
|
|
||||||
/// </remarks>
|
|
||||||
public static IDragSource CreateSimpleSource(object data, DragDropEffects allowedEffects = DragDropEffects.Copy | DragDropEffects.Move)
|
|
||||||
{
|
|
||||||
if (data == null)
|
|
||||||
throw new ArgumentNullException(nameof(data));
|
|
||||||
|
|
||||||
return new SimpleDragSource(data, allowedEffects);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает источник перетаскивания с отложенной загрузкой данных.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dataFactory">
|
|
||||||
/// Фабрика данных, которая будет вызвана при начале перетаскивания.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="canDragChecker">
|
|
||||||
/// Функция проверки возможности начала перетаскивания.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="allowedEffects">
|
|
||||||
/// Разрешенные эффекты перетаскивания.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Экземпляр <see cref="IDragSource"/> с отложенной загрузкой данных.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот источник полезен, когда данные для перетаскивания дорого создавать заранее
|
|
||||||
/// или зависят от контекста в момент начала операции.
|
|
||||||
/// </remarks>
|
|
||||||
public static IDragSource CreateLazySource(
|
|
||||||
Func<object> dataFactory,
|
|
||||||
Func<bool> canDragChecker = null,
|
|
||||||
DragDropEffects allowedEffects = DragDropEffects.Copy | DragDropEffects.Move)
|
|
||||||
{
|
|
||||||
if (dataFactory == null)
|
|
||||||
throw new ArgumentNullException(nameof(dataFactory));
|
|
||||||
|
|
||||||
return new LazyDragSource(dataFactory, canDragChecker, allowedEffects);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает источник перетаскивания для коллекции элементов.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">
|
|
||||||
/// Тип элементов в коллекции.
|
|
||||||
/// </typeparam>
|
|
||||||
/// <param name="items">
|
|
||||||
/// Коллекция элементов для перетаскивания.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="itemSelector">
|
|
||||||
/// Функция выбора конкретного элемента для перетаскивания из коллекции.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="allowedEffects">
|
|
||||||
/// Разрешенные эффекты перетаскивания.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Экземпляр <see cref="IDragSource"/> для коллекции элементов.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот источник позволяет перетаскивать элементы из коллекции. Функция <paramref name="itemSelector"/>
|
|
||||||
/// определяет, какой именно элемент из коллекции будет перетаскиваться в текущем контексте.
|
|
||||||
/// </remarks>
|
|
||||||
public static IDragSource CreateCollectionSource<T>(
|
|
||||||
IEnumerable<T> items,
|
|
||||||
Func<IEnumerable<T>, T> itemSelector,
|
|
||||||
DragDropEffects allowedEffects = DragDropEffects.Copy | DragDropEffects.Move)
|
|
||||||
{
|
|
||||||
if (items == null)
|
|
||||||
throw new ArgumentNullException(nameof(items));
|
|
||||||
if (itemSelector == null)
|
|
||||||
throw new ArgumentNullException(nameof(itemSelector));
|
|
||||||
|
|
||||||
return new CollectionDragSource<T>(items, itemSelector, allowedEffects);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Цели сброса
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает простую цель сброса, которая принимает данные любого типа.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="onDrop">
|
|
||||||
/// Обработчик, вызываемый при сбросе данных.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Экземпляр <see cref="IDropTarget"/>, который принимает любые данные.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Эта цель подходит для простых сценариев, когда не требуется валидация типа данных.
|
|
||||||
/// </remarks>
|
|
||||||
public static IDropTarget CreateSimpleTarget(Action<DropInfo> onDrop)
|
|
||||||
{
|
|
||||||
if (onDrop == null)
|
|
||||||
throw new ArgumentNullException(nameof(onDrop));
|
|
||||||
|
|
||||||
return new SimpleDropTarget(onDrop);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает цель сброса с фильтрацией по типу данных.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">
|
|
||||||
/// Тип данных, которые может принимать цель.
|
|
||||||
/// </typeparam>
|
|
||||||
/// <param name="onDrop">
|
|
||||||
/// Обработчик, вызываемый при сбросе данных.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Экземпляр <see cref="IDropTarget"/>, который принимает только данные типа <typeparamref name="T"/>.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Эта цель автоматически проверяет тип сбрасываемых данных и вызывает обработчик только
|
|
||||||
/// если данные могут быть приведены к указанному типу.
|
|
||||||
/// </remarks>
|
|
||||||
public static IDropTarget CreateTypedTarget<T>(Action<T, DropInfo> onDrop) where T : class
|
|
||||||
{
|
|
||||||
if (onDrop == null)
|
|
||||||
throw new ArgumentNullException(nameof(onDrop));
|
|
||||||
|
|
||||||
return new TypedDropTarget<T>(onDrop);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает цель сброса с пользовательской логикой валидации.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="canAccept">
|
|
||||||
/// Функция проверки возможности приема данных.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="onDrop">
|
|
||||||
/// Обработчик, вызываемый при сбросе данных.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Экземпляр <see cref="IDropTarget"/> с пользовательской логикой валидации.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Эта цель позволяет реализовать сложную логику валидации, выходящую за рамки простой проверки типа.
|
|
||||||
/// </remarks>
|
|
||||||
public static IDropTarget CreateConditionalTarget(Func<DropInfo, bool> canAccept, Action<DropInfo> onDrop)
|
|
||||||
{
|
|
||||||
if (canAccept == null)
|
|
||||||
throw new ArgumentNullException(nameof(canAccept));
|
|
||||||
if (onDrop == null)
|
|
||||||
throw new ArgumentNullException(nameof(onDrop));
|
|
||||||
|
|
||||||
return new ConditionalDropTarget(canAccept, onDrop);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Вспомогательные методы
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает стандартные эффекты перетаскивания на основе модификаторов клавиатуры.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="controlKey">
|
|
||||||
/// Нажата ли клавиша Control.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="shiftKey">
|
|
||||||
/// Нажата ли клавиша Shift.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="altKey">
|
|
||||||
/// Нажата ли клавиша Alt.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// <see cref="DragDropEffects"/>, соответствующие комбинации клавиш.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Стандартная логика:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Control + Shift: Link</item>
|
|
||||||
/// <item>Control: Copy</item>
|
|
||||||
/// <item>Shift: Move</item>
|
|
||||||
/// <item>Alt: Link</item>
|
|
||||||
/// <item>Без модификаторов: Move</item>
|
|
||||||
/// </list>
|
|
||||||
/// </remarks>
|
|
||||||
public static DragDropEffects GetEffectsFromKeys(bool controlKey, bool shiftKey, bool altKey)
|
|
||||||
{
|
|
||||||
return DragDropEffectsExtensions.GetEffectFromKeys(controlKey, shiftKey, altKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Опции для настройки сервиса перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public class DragDropServiceOptions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает порог начала перетаскивания в пикселях.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Минимальное расстояние, которое должен пройти курсор, чтобы началась операция перетаскивания.
|
|
||||||
/// Значение по умолчанию: 3.0.
|
|
||||||
/// </value>
|
|
||||||
public double DragStartThreshold { get; set; } = Constants.DragDropConstants.DefaultDragThreshold;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает максимальное время ожидания асинхронных операций в миллисекундах.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Время в миллисекундах, после которого асинхронная операция будет прервана.
|
|
||||||
/// Значение по умолчанию: 5000.
|
|
||||||
/// </value>
|
|
||||||
public int AsyncOperationTimeout { get; set; } = Constants.DragDropConstants.DefaultAsyncTimeout;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает значение, указывающее, включены ли асинхронные операции.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// true, если асинхронные операции включены; в противном случае — false.
|
|
||||||
/// Значение по умолчанию: true.
|
|
||||||
/// </value>
|
|
||||||
public bool EnableAsyncOperations { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает интервал автоматической очистки неиспользуемых целей в минутах.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Интервал в минутах, через который будут удаляться цели сброса, к которым не было обращений.
|
|
||||||
/// Значение по умолчанию: 10.
|
|
||||||
/// </value>
|
|
||||||
public int CleanupInterval { get; set; } = Constants.DragDropConstants.TargetLifetimeMinutes;
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Внутренние реализации
|
|
||||||
|
|
||||||
internal class SimpleDragSource : IDragSource
|
|
||||||
{
|
|
||||||
private readonly object _data;
|
|
||||||
private readonly DragDropEffects _allowedEffects;
|
|
||||||
|
|
||||||
public SimpleDragSource(object data, DragDropEffects allowedEffects)
|
|
||||||
{
|
|
||||||
_data = data ?? throw new ArgumentNullException(nameof(data));
|
|
||||||
_allowedEffects = allowedEffects;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<DragInfo?> TryStartDragAsync(Point startPosition, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var dragInfo = new DragInfo(_data, _allowedEffects, startPosition, this);
|
|
||||||
return Task.FromResult<DragInfo?>(dragInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task OnDragCompletedAsync(DragInfo dragInfo, DragDropEffects effects, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task OnDragCancelledAsync(DragInfo dragInfo, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class LazyDragSource : IDragSource
|
|
||||||
{
|
|
||||||
private readonly Func<object> _dataFactory;
|
|
||||||
private readonly Func<bool> _canDragChecker;
|
|
||||||
private readonly DragDropEffects _allowedEffects;
|
|
||||||
|
|
||||||
public LazyDragSource(Func<object> dataFactory, Func<bool> canDragChecker, DragDropEffects allowedEffects)
|
|
||||||
{
|
|
||||||
_dataFactory = dataFactory ?? throw new ArgumentNullException(nameof(dataFactory));
|
|
||||||
_canDragChecker = canDragChecker ?? (() => true);
|
|
||||||
_allowedEffects = allowedEffects;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<DragInfo?> TryStartDragAsync(Point startPosition, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (!_canDragChecker())
|
|
||||||
return Task.FromResult<DragInfo?>(null);
|
|
||||||
|
|
||||||
var data = _dataFactory();
|
|
||||||
if (data == null)
|
|
||||||
return Task.FromResult<DragInfo?>(null);
|
|
||||||
|
|
||||||
var dragInfo = new DragInfo(data, _allowedEffects, startPosition, this);
|
|
||||||
return Task.FromResult<DragInfo?>(dragInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task OnDragCompletedAsync(DragInfo dragInfo, DragDropEffects effects, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task OnDragCancelledAsync(DragInfo dragInfo, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class CollectionDragSource<T> : IDragSource
|
|
||||||
{
|
|
||||||
private readonly IEnumerable<T> _items;
|
|
||||||
private readonly Func<IEnumerable<T>, T> _itemSelector;
|
|
||||||
private readonly DragDropEffects _allowedEffects;
|
|
||||||
|
|
||||||
public CollectionDragSource(IEnumerable<T> items, Func<IEnumerable<T>, T> itemSelector, DragDropEffects allowedEffects)
|
|
||||||
{
|
|
||||||
_items = items ?? throw new ArgumentNullException(nameof(items));
|
|
||||||
_itemSelector = itemSelector ?? throw new ArgumentNullException(nameof(itemSelector));
|
|
||||||
_allowedEffects = allowedEffects;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<DragInfo?> TryStartDragAsync(Point startPosition, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var selectedItem = _itemSelector(_items);
|
|
||||||
if (selectedItem == null)
|
|
||||||
return Task.FromResult<DragInfo?>(null);
|
|
||||||
|
|
||||||
var dragInfo = new DragInfo(selectedItem, _allowedEffects, startPosition, this);
|
|
||||||
return Task.FromResult<DragInfo?>(dragInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task OnDragCompletedAsync(DragInfo dragInfo, DragDropEffects effects, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task OnDragCancelledAsync(DragInfo dragInfo, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class SimpleDropTarget : IDropTarget
|
|
||||||
{
|
|
||||||
private readonly Action<DropInfo> _onDrop;
|
|
||||||
|
|
||||||
public SimpleDropTarget(Action<DropInfo> onDrop)
|
|
||||||
{
|
|
||||||
_onDrop = onDrop ?? throw new ArgumentNullException(nameof(onDrop));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<bool> CanAcceptDropAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return Task.FromResult(dropInfo.Data != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task OnDragOverAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (dropInfo.Data != null)
|
|
||||||
{
|
|
||||||
dropInfo.SuggestedEffects = DragDropEffects.Move;
|
|
||||||
}
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task OnDropAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (dropInfo.Data != null)
|
|
||||||
{
|
|
||||||
_onDrop(dropInfo);
|
|
||||||
dropInfo.MarkAsHandled();
|
|
||||||
}
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task OnDragLeaveAsync(CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class TypedDropTarget<T> : IDropTarget where T : class
|
|
||||||
{
|
|
||||||
private readonly Action<T, DropInfo> _onDrop;
|
|
||||||
|
|
||||||
public TypedDropTarget(Action<T, DropInfo> onDrop)
|
|
||||||
{
|
|
||||||
_onDrop = onDrop ?? throw new ArgumentNullException(nameof(onDrop));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<bool> CanAcceptDropAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return Task.FromResult(dropInfo.Data is T);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task OnDragOverAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (dropInfo.Data is T)
|
|
||||||
{
|
|
||||||
dropInfo.SuggestedEffects = DragDropEffects.Move;
|
|
||||||
}
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task OnDropAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (dropInfo.Data is T data)
|
|
||||||
{
|
|
||||||
_onDrop(data, dropInfo);
|
|
||||||
dropInfo.MarkAsHandled();
|
|
||||||
}
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task OnDragLeaveAsync(CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class ConditionalDropTarget : IDropTarget
|
|
||||||
{
|
|
||||||
private readonly Func<DropInfo, bool> _canAccept;
|
|
||||||
private readonly Action<DropInfo> _onDrop;
|
|
||||||
|
|
||||||
public ConditionalDropTarget(Func<DropInfo, bool> canAccept, Action<DropInfo> onDrop)
|
|
||||||
{
|
|
||||||
_canAccept = canAccept ?? throw new ArgumentNullException(nameof(canAccept));
|
|
||||||
_onDrop = onDrop ?? throw new ArgumentNullException(nameof(onDrop));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<bool> CanAcceptDropAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return Task.FromResult(_canAccept(dropInfo));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task OnDragOverAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (_canAccept(dropInfo))
|
|
||||||
{
|
|
||||||
dropInfo.SuggestedEffects = DragDropEffects.Move;
|
|
||||||
}
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task OnDropAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (_canAccept(dropInfo))
|
|
||||||
{
|
|
||||||
_onDrop(dropInfo);
|
|
||||||
dropInfo.MarkAsHandled();
|
|
||||||
}
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task OnDragLeaveAsync(CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<LangVersion>latest</LangVersion>
|
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
|
||||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
|
||||||
<PackageId>Lattice.Core.DragDrop</PackageId>
|
|
||||||
<Version>1.0.0</Version>
|
|
||||||
<Authors>FrigaT</Authors>
|
|
||||||
<Description>Professional drag-and-drop system for Lattice UI Framework</Description>
|
|
||||||
<PackageTags>ui;framework;drag;drop;docking;toolbox</PackageTags>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Lattice.Core.Geometry\Lattice.Core.Geometry.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
using Lattice.Core.Geometry;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
|
|
||||||
namespace Lattice.Core.DragDrop.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Содержит информацию о начале операции перетаскивания.
|
|
||||||
/// Этот класс передается от источника перетаскивания к системе перетаскивания
|
|
||||||
/// для инициализации и управления операцией.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// <see cref="DragInfo"/> является ключевым компонентом системы перетаскивания,
|
|
||||||
/// инкапсулирующим все необходимые данные для начала операции. Он содержит:
|
|
||||||
/// </para>
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Данные для передачи</item>
|
|
||||||
/// <item>Разрешенные эффекты перетаскивания</item>
|
|
||||||
/// <item>Начальную позицию операции</item>
|
|
||||||
/// <item>Ссылку на источник перетаскивания</item>
|
|
||||||
/// <item>Дополнительные параметры операции</item>
|
|
||||||
/// </list>
|
|
||||||
/// <para>
|
|
||||||
/// Этот класс используется как внутренний механизм передачи данных между
|
|
||||||
/// <see cref="Abstractions.IDragSource"/> и системой управления перетаскиванием.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public class DragInfo : IDisposable, ICloneable
|
|
||||||
{
|
|
||||||
private readonly ConcurrentDictionary<string, object> _parameters = new();
|
|
||||||
private bool _disposed;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает данные, которые передаются в операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Объект, содержащий данные для передачи. Может быть любого типа,
|
|
||||||
/// поддерживаемого системой перетаскивания.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Эти данные будут доступны цели сброса через <see cref="DropInfo.Data"/>.
|
|
||||||
/// Важно, чтобы данные были сериализуемыми, если операция перетаскивания
|
|
||||||
/// может выходить за пределы процесса приложения.
|
|
||||||
/// </remarks>
|
|
||||||
public object Data { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает разрешенные эффекты для этой операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Комбинация флагов <see cref="Enums.DragDropEffects"/>, определяющая,
|
|
||||||
/// какие операции разрешены для этого перетаскивания.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот параметр используется системой для фильтрации допустимых операций
|
|
||||||
/// и предоставления соответствующей визуальной обратной связи пользователю.
|
|
||||||
/// </remarks>
|
|
||||||
public Enums.DragDropEffects AllowedEffects { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает начальную позицию операции перетаскивания в координатах экрана.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Точка в экранных координатах, где была начата операция перетаскивания.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Эта позиция используется для вычисления смещения при создании визуального
|
|
||||||
/// представления перетаскивания и для определения порога начала операции.
|
|
||||||
/// </remarks>
|
|
||||||
public Point StartPosition { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает источник перетаскивания, который инициировал операцию.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Объект, реализующий <see cref="Abstractions.IDragSource"/>, или null,
|
|
||||||
/// если источник не доступен или не требуется.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Эта ссылка может использоваться для уведомления источника о результате
|
|
||||||
/// операции перетаскивания (завершении или отмене).
|
|
||||||
/// </remarks>
|
|
||||||
public object? Source { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает дополнительные параметры, специфичные для конкретной
|
|
||||||
/// реализации перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Словарь, содержащий пары ключ-значение с дополнительными параметрами.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Используется для передачи контекстной информации, которая не входит
|
|
||||||
/// в стандартный набор свойств, но может быть полезной для обработки
|
|
||||||
/// операции перетаскивания.
|
|
||||||
/// </remarks>
|
|
||||||
public IReadOnlyDictionary<string, object> Parameters => _parameters;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DragInfo"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">
|
|
||||||
/// Данные, которые передаются в операции перетаскивания.
|
|
||||||
/// Не может быть null.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="allowedEffects">
|
|
||||||
/// Разрешенные эффекты для этой операции перетаскивания.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="startPosition">
|
|
||||||
/// Начальная позиция операции перетаскивания в координатах экрана.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="source">
|
|
||||||
/// Источник перетаскивания, который инициировал операцию. Может быть null.
|
|
||||||
/// </param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, когда <paramref name="data"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Конструктор создает экземпляр <see cref="DragInfo"/> с указанными
|
|
||||||
/// параметрами и инициализирует коллекцию параметров пустым словарем.
|
|
||||||
/// </remarks>
|
|
||||||
public DragInfo(object data, Enums.DragDropEffects allowedEffects, Point startPosition, object? source = null)
|
|
||||||
{
|
|
||||||
Data = data ?? throw new ArgumentNullException(nameof(data));
|
|
||||||
|
|
||||||
// Проверка допустимых значений перечисления
|
|
||||||
if (!Enum.IsDefined(typeof(Enums.DragDropEffects), allowedEffects))
|
|
||||||
{
|
|
||||||
throw new ArgumentException(
|
|
||||||
$"Недопустимое значение для {nameof(allowedEffects)}: {allowedEffects}",
|
|
||||||
nameof(allowedEffects));
|
|
||||||
}
|
|
||||||
|
|
||||||
AllowedEffects = allowedEffects;
|
|
||||||
StartPosition = startPosition;
|
|
||||||
Source = source;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает новый экземпляр <see cref="DragInfo"/> с теми же данными,
|
|
||||||
/// но новой позицией.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="newPosition">
|
|
||||||
/// Новая позиция для информации о перетаскивании.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Новый экземпляр <see cref="DragInfo"/> с обновленной позицией.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот метод используется для обновления информации о перетаскивании
|
|
||||||
/// при перемещении курсора, сохраняя исходные данные и параметры.
|
|
||||||
/// </remarks>
|
|
||||||
public DragInfo CloneWithPosition(Point newPosition)
|
|
||||||
{
|
|
||||||
ThrowIfDisposed();
|
|
||||||
|
|
||||||
var clone = new DragInfo(Data, AllowedEffects, newPosition, Source)
|
|
||||||
{
|
|
||||||
_disposed = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var kvp in _parameters)
|
|
||||||
{
|
|
||||||
clone._parameters[kvp.Key] = kvp.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return clone;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает новый экземпляр <see cref="DragInfo"/> с теми же данными.
|
|
||||||
/// </summary>
|
|
||||||
public DragInfo Clone() => new DragInfo(Data, AllowedEffects, StartPosition, Source);
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
object ICloneable.Clone() => this.Clone();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или дополнительные параметры, специфичные для конкретной
|
|
||||||
/// реализации перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public T? GetParameter<T>(string key, T? defaultValue = default)
|
|
||||||
{
|
|
||||||
if (Parameters.TryGetValue(key, out var value) && value is T typedValue)
|
|
||||||
{
|
|
||||||
return typedValue;
|
|
||||||
}
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или дополнительные параметры, специфичные для конкретной
|
|
||||||
/// реализации перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public bool TryGetParameter<T>(string key, out T? value)
|
|
||||||
{
|
|
||||||
value = default;
|
|
||||||
|
|
||||||
if (_parameters.TryGetValue(key, out var objValue) && objValue is T typedValue)
|
|
||||||
{
|
|
||||||
value = typedValue;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Задает дополнительные параметры, специфичные для конкретной
|
|
||||||
/// реализации перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public void SetParameter<T>(string key, T value)
|
|
||||||
{
|
|
||||||
_parameters[key] = value!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Освобождает ресурсы.
|
|
||||||
/// </summary>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (_disposed) return;
|
|
||||||
|
|
||||||
foreach (var value in _parameters.Values)
|
|
||||||
{
|
|
||||||
if (value is IDisposable disposable)
|
|
||||||
{
|
|
||||||
disposable.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_parameters.Clear();
|
|
||||||
_disposed = true;
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ThrowIfDisposed()
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
throw new ObjectDisposedException(nameof(DragInfo));
|
|
||||||
}
|
|
||||||
|
|
||||||
~DragInfo()
|
|
||||||
{
|
|
||||||
Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,281 +0,0 @@
|
|||||||
using Lattice.Core.DragDrop.Enums;
|
|
||||||
using Lattice.Core.Geometry;
|
|
||||||
|
|
||||||
namespace Lattice.Core.DragDrop.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Содержит информацию о потенциальном или фактическом сбросе в операции перетаскивания.
|
|
||||||
/// Этот класс используется для передачи данных между системой перетаскивания
|
|
||||||
/// и целью сброса (<see cref="Abstractions.IDropTarget"/>).
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// <see cref="DropInfo"/> предоставляет цель сброса всей необходимой информацией
|
|
||||||
/// для принятия решения о возможности сброса и выполнения соответствующей операции.
|
|
||||||
/// Ключевые аспекты включают:
|
|
||||||
/// </para>
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Предлагаемые для сброса данные</item>
|
|
||||||
/// <item>Текущую позицию курсора</item>
|
|
||||||
/// <item>Разрешенные эффекты от источника</item>
|
|
||||||
/// <item>Предлагаемые эффекты для сброса</item>
|
|
||||||
/// <item>Ссылку на цель сброса</item>
|
|
||||||
/// <item>Флаг обработки операции</item>
|
|
||||||
/// </list>
|
|
||||||
/// <para>
|
|
||||||
/// Этот класс является изменяемым, позволяя цели сброса обновлять предлагаемые
|
|
||||||
/// эффекты и помечать операцию как обработанную.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public class DropInfo
|
|
||||||
{
|
|
||||||
private DragDropEffects _effects = DragDropEffects.None;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает позицию сброса относительно цели.
|
|
||||||
/// </summary>
|
|
||||||
public DropPosition DropPosition { get; set; } = DropPosition.Inside;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает значение, указывающее, нужно ли показывать визуальную обратную связь.
|
|
||||||
/// </summary>
|
|
||||||
public bool ShowVisualFeedback { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает данные для визуальной обратной связи.
|
|
||||||
/// </summary>
|
|
||||||
public object? VisualFeedbackData { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает данные, которые предлагаются для сброса.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Данные, переданные от источника перетаскивания, или null, если данные
|
|
||||||
/// не доступны или операция была отменена.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Эти данные соответствуют свойству <see cref="DragInfo.Data"/> из
|
|
||||||
/// исходной информации о перетаскивании.
|
|
||||||
/// </remarks>
|
|
||||||
public object? Data { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает текущую позицию курсора в координатах экрана.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Точка в экранных координатах, представляющая текущее положение курсора
|
|
||||||
/// мыши во время операции перетаскивания.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Эта позиция используется для определения точного места сброса и может
|
|
||||||
/// влиять на предлагаемые эффекты (например, различные операции для
|
|
||||||
/// разных областей цели сброса).
|
|
||||||
/// </remarks>
|
|
||||||
public Point Position { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает разрешенные эффекты от источника перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Комбинация флагов <see cref="Enums.DragDropEffects"/>, определяющая,
|
|
||||||
/// какие операции разрешил источник.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Цель сброса должна уважать эти ограничения и не предлагать эффекты,
|
|
||||||
/// которые не разрешены источником.
|
|
||||||
/// </remarks>
|
|
||||||
public Enums.DragDropEffects AllowedEffects { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает предлагаемые эффекты для операции сброса.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Комбинация флагов <see cref="Enums.DragDropEffects"/>, предлагаемая
|
|
||||||
/// целью сброса. По умолчанию равно <see cref="Enums.DragDropEffects.None"/>.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Цель сброса должна установить это свойство в методе <see cref="Abstractions.IDropTarget.DragOver"/>
|
|
||||||
/// на основе анализа предоставленных данных и текущего контекста.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Если цель не устанавливает это свойство, система перетаскивания
|
|
||||||
/// будет использовать эффекты по умолчанию.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public Enums.DragDropEffects SuggestedEffects
|
|
||||||
{
|
|
||||||
get => _effects;
|
|
||||||
set => _effects = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает цель сброса, которая обрабатывает эту информацию.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Объект, реализующий <see cref="Abstractions.IDropTarget"/>, или null,
|
|
||||||
/// если цель не определена.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Эта ссылка позволяет системе идентифицировать, какая цель обрабатывает
|
|
||||||
/// информацию о сбросе, и используется для отслеживания изменений цели
|
|
||||||
/// во время операции перетаскивания.
|
|
||||||
/// </remarks>
|
|
||||||
public object? Target { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает дополнительные параметры, специфичные для конкретной
|
|
||||||
/// реализации перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Словарь, содержащий пары ключ-значение с дополнительными параметрами.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Может использоваться для передачи контекстной информации между
|
|
||||||
/// различными компонентами системы перетаскивания или для хранения
|
|
||||||
/// временных данных во время обработки операции.
|
|
||||||
/// </remarks>
|
|
||||||
public Dictionary<string, object> Parameters { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает значение, указывающее, был ли сброс уже обработан.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// true, если операция сброса была помечена как обработанная;
|
|
||||||
/// в противном случае — false.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Это свойство используется для предотвращения множественной обработки
|
|
||||||
/// одной и той же операции сброса. После вызова метода <see cref="MarkAsHandled"/>,
|
|
||||||
/// свойство становится true.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Система перетаскивания может проверять это свойство, чтобы определить,
|
|
||||||
/// нужно ли выполнять дополнительную обработку по умолчанию.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public bool Handled { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает дополнительные параметры, специфичные для конкретной
|
|
||||||
/// реализации перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public T? GetParameter<T>(string key, T? defaultValue = default)
|
|
||||||
{
|
|
||||||
if (Parameters.TryGetValue(key, out var value) && value is T typedValue)
|
|
||||||
{
|
|
||||||
return typedValue;
|
|
||||||
}
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает дополнительные параметры, специфичные для конкретной
|
|
||||||
/// реализации перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public bool TryGetParameter<T>(string key, out T? value)
|
|
||||||
{
|
|
||||||
value = default;
|
|
||||||
|
|
||||||
if (Parameters.TryGetValue(key, out var objValue) && objValue is T typedValue)
|
|
||||||
{
|
|
||||||
value = typedValue;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Задает дополнительные параметры, специфичные для конкретной
|
|
||||||
/// реализации перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public void SetParameter<T>(string key, T value)
|
|
||||||
{
|
|
||||||
Parameters[key] = value!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DropInfo"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">
|
|
||||||
/// Данные, которые предлагаются для сброса. Может быть null.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="position">
|
|
||||||
/// Текущая позиция курсора в координатах экрана.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="allowedEffects">
|
|
||||||
/// Разрешенные эффекты от источника перетаскивания.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="target">
|
|
||||||
/// Цель сброса, которая обрабатывает эту информацию. Может быть null.
|
|
||||||
/// </param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Конструктор создает экземпляр <see cref="DropInfo"/> с указанными
|
|
||||||
/// параметрами, инициализирует коллекцию параметров пустым словарем
|
|
||||||
/// и устанавливает флаг <see cref="Handled"/> в false.
|
|
||||||
/// </remarks>
|
|
||||||
public DropInfo(object? data, Point position, Enums.DragDropEffects allowedEffects, object? target = null)
|
|
||||||
{
|
|
||||||
Data = data;
|
|
||||||
Position = position;
|
|
||||||
AllowedEffects = allowedEffects;
|
|
||||||
Target = target;
|
|
||||||
Parameters = new Dictionary<string, object>();
|
|
||||||
Handled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Помечает сброс как обработанный.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод должен вызываться целью сброса в методе <see cref="Abstractions.IDropTarget.Drop"/>,
|
|
||||||
/// если она успешно обработала операцию сброса.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// После вызова этого метода свойство <see cref="Handled"/> становится true,
|
|
||||||
/// что сигнализирует системе перетаскивания о том, что дополнительная
|
|
||||||
/// обработка не требуется.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public void MarkAsHandled()
|
|
||||||
{
|
|
||||||
Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает новый экземпляр <see cref="DropInfo"/> с теми же данными,
|
|
||||||
/// но новой позицией.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="newPosition">
|
|
||||||
/// Новая позиция для информации о сбросе.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Новый экземпляр <see cref="DropInfo"/> с обновленной позицией.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот метод используется для обновления информации о сбросе при
|
|
||||||
/// перемещении курсора, сохраняя исходные данные и параметры.
|
|
||||||
/// </remarks>
|
|
||||||
public DropInfo WithPosition(Point newPosition)
|
|
||||||
{
|
|
||||||
return new DropInfo(Data, newPosition, AllowedEffects, Target)
|
|
||||||
{
|
|
||||||
Parameters = new Dictionary<string, object>(Parameters),
|
|
||||||
SuggestedEffects = _effects,
|
|
||||||
DropPosition = DropPosition,
|
|
||||||
ShowVisualFeedback = ShowVisualFeedback,
|
|
||||||
VisualFeedbackData = VisualFeedbackData
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Проверка установки эффекта перетаскивания в разрешенные эффекты.
|
|
||||||
/// </summary>
|
|
||||||
public bool CanAcceptEffect(Enums.DragDropEffects effect)
|
|
||||||
{
|
|
||||||
return (AllowedEffects & effect) != Enums.DragDropEffects.None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,829 +0,0 @@
|
|||||||
# Lattice.Core.DragDrop
|
|
||||||
|
|
||||||
Профессиональная, асинхронная система перетаскивания для .NET приложений. Полностью потокобезопасная, расширяемая архитектура с поддержкой кросс-платформенности.
|
|
||||||
|
|
||||||
## 📋 Особенности
|
|
||||||
|
|
||||||
- ✅ **Полная асинхронная поддержка** - async/await для всех операций
|
|
||||||
- ✅ **Потокобезопасность** - `ReaderWriterLockSlim` для эффективной синхронизации
|
|
||||||
- ✅ **Производительность** - Оптимизированные алгоритмы, кэширование, минимальные аллокации
|
|
||||||
- ✅ **Расширяемость** - Легкая интеграция с любыми UI фреймворками
|
|
||||||
- ✅ **Надежность** - Таймауты, обработка ошибок, корректное освобождение ресурсов
|
|
||||||
- ✅ **Статистика** - Встроенный мониторинг производительности
|
|
||||||
|
|
||||||
## 🏗️ Архитектура
|
|
||||||
|
|
||||||
### Основные компоненты
|
|
||||||
|
|
||||||
```
|
|
||||||
Lattice.Core.DragDrop/
|
|
||||||
├── Abstractions/ # Интерфейсы
|
|
||||||
│ ├── IDragSource.cs # Источник перетаскивания (синхронный)
|
|
||||||
│ ├── IAsyncDragSource.cs # Асинхронный источник
|
|
||||||
│ ├── IDropTarget.cs # Цель сброса (синхронная)
|
|
||||||
│ └── IAsyncDropTarget.cs # Асинхронная цель
|
|
||||||
├── Enums/ # Перечисления
|
|
||||||
├── Exceptions/ # Исключения с кодами ошибок
|
|
||||||
├── Extensions/ # Расширения для DI
|
|
||||||
├── Models/ # Модели данных
|
|
||||||
│ ├── DragInfo.cs # Информация о перетаскивании
|
|
||||||
│ └── DropInfo.cs # Информация о сбросе
|
|
||||||
└── Services/ # Сервисы
|
|
||||||
├── IDragDropService.cs # Основной интерфейс
|
|
||||||
├── DragDropService.cs # Реализация сервиса
|
|
||||||
└── EventArgs/ # Аргументы событий
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 Быстрый старт
|
|
||||||
|
|
||||||
### 1. Установка
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Добавьте проект Lattice.Core.DragDrop в ваше решение
|
|
||||||
// или создайте NuGet пакет
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Базовое использование
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
using Lattice.Core.DragDrop;
|
|
||||||
using Lattice.Core.DragDrop.Abstractions;
|
|
||||||
using Lattice.Core.DragDrop.Services;
|
|
||||||
using Lattice.Core.Geometry;
|
|
||||||
|
|
||||||
// Создаем сервис
|
|
||||||
var dragDropService = new DragDropService();
|
|
||||||
|
|
||||||
// Создаем простой источник перетаскивания
|
|
||||||
var dragSource = DragDropUtilities.CreateSimpleDragSource(
|
|
||||||
dataProvider: () => "Example Data",
|
|
||||||
canDrag: () => true,
|
|
||||||
onCompleted: (dragInfo, effects) =>
|
|
||||||
Console.WriteLine($"Drag completed with effects: {effects}"),
|
|
||||||
onCancelled: dragInfo =>
|
|
||||||
Console.WriteLine("Drag cancelled")
|
|
||||||
);
|
|
||||||
|
|
||||||
// Создаем простую цель сброса
|
|
||||||
var dropTarget = DragDropUtilities.CreateSimpleDropTarget(
|
|
||||||
canAccept: dropInfo => dropInfo.Data is string,
|
|
||||||
onDragOver: dropInfo =>
|
|
||||||
dropInfo.SuggestedEffects = DragDropEffects.Copy,
|
|
||||||
onDrop: dropInfo =>
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Dropped: {dropInfo.Data}");
|
|
||||||
dropInfo.MarkAsHandled();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Регистрируем цель
|
|
||||||
string targetId = dragDropService.RegisterDropTarget(
|
|
||||||
dropTarget,
|
|
||||||
new Rect(100, 100, 300, 200)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Начинаем перетаскивание
|
|
||||||
bool started = dragDropService.StartDrag(
|
|
||||||
dragSource,
|
|
||||||
new Point(50, 50)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (started)
|
|
||||||
{
|
|
||||||
// Обновляем позицию
|
|
||||||
dragDropService.UpdateDrag(new Point(150, 150));
|
|
||||||
|
|
||||||
// Завершаем
|
|
||||||
var effects = dragDropService.EndDrag(new Point(200, 200));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📖 Подробное руководство
|
|
||||||
|
|
||||||
### Сервис перетаскивания
|
|
||||||
|
|
||||||
Основной класс системы - `DragDropService`, реализующий `IDragDropService`.
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Создание с кастомными настройками
|
|
||||||
var service = new DragDropService(options =>
|
|
||||||
{
|
|
||||||
options.DragStartThreshold = 5.0;
|
|
||||||
options.EnableAsyncOperations = true;
|
|
||||||
options.AsyncOperationTimeout = 3000;
|
|
||||||
options.EnableAutoCleanup = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Свойства
|
|
||||||
bool isActive = service.IsDragActive; // Активна ли операция
|
|
||||||
DragInfo? currentDrag = service.CurrentDragInfo; // Текущая информация
|
|
||||||
double threshold = service.DragStartThreshold; // Порог начала
|
|
||||||
|
|
||||||
// События
|
|
||||||
service.DragStarted += OnDragStarted;
|
|
||||||
service.DragUpdated += OnDragUpdated;
|
|
||||||
service.DragCompleted += OnDragCompleted;
|
|
||||||
service.DragCancelled += OnDragCancelled;
|
|
||||||
service.ErrorOccurred += OnErrorOccurred;
|
|
||||||
|
|
||||||
// Регистрация целей
|
|
||||||
string id = service.RegisterDropTarget(
|
|
||||||
target, // IDropTarget
|
|
||||||
bounds, // Rect
|
|
||||||
priority: 1, // Приоритет (выше = выше приоритет)
|
|
||||||
group: "main" // Группа для группового удаления
|
|
||||||
);
|
|
||||||
|
|
||||||
// Обновление границ
|
|
||||||
service.UpdateDropTargetBounds(id, newBounds);
|
|
||||||
|
|
||||||
// Удаление
|
|
||||||
service.UnregisterDropTarget(id);
|
|
||||||
service.UnregisterDropTargetsInGroup("main");
|
|
||||||
```
|
|
||||||
|
|
||||||
### Асинхронное использование
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Асинхронные методы
|
|
||||||
bool started = await service.StartDragAsync(source, startPosition);
|
|
||||||
await service.UpdateDragAsync(currentPosition);
|
|
||||||
DragDropEffects effects = await service.EndDragAsync(dropPosition);
|
|
||||||
await service.CancelDragAsync();
|
|
||||||
|
|
||||||
// Статистика
|
|
||||||
var stats = service.GetStats();
|
|
||||||
Console.WriteLine($"Operations: {stats.TotalDragOperations}");
|
|
||||||
Console.WriteLine($"Success rate: {stats.SuccessfulDrops}/{stats.TotalDragOperations}");
|
|
||||||
Console.WriteLine($"Avg time: {stats.AverageOperationTime.TotalMilliseconds}ms");
|
|
||||||
```
|
|
||||||
|
|
||||||
### Создание кастомных источников и целей
|
|
||||||
|
|
||||||
#### Синхронная реализация
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class FileDragSource : IDragSource
|
|
||||||
{
|
|
||||||
private readonly FileInfo _file;
|
|
||||||
|
|
||||||
public FileDragSource(FileInfo file) => _file = file;
|
|
||||||
|
|
||||||
public bool CanStartDrag(out DragInfo? dragInfo)
|
|
||||||
{
|
|
||||||
// Проверяем условия
|
|
||||||
if (!_file.Exists || _file.Length > 100 * 1024 * 1024) // 100 MB limit
|
|
||||||
{
|
|
||||||
dragInfo = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создаем DragInfo
|
|
||||||
dragInfo = new DragInfo(
|
|
||||||
data: _file,
|
|
||||||
allowedEffects: DragDropEffects.Copy | DragDropEffects.Move,
|
|
||||||
startPosition: Point.Zero,
|
|
||||||
source: this
|
|
||||||
);
|
|
||||||
|
|
||||||
// Добавляем дополнительные параметры
|
|
||||||
dragInfo.SetParameter("FileSize", _file.Length);
|
|
||||||
dragInfo.SetParameter("MimeType", GetMimeType(_file));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool StartDrag(DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
// Подготовка к перетаскиванию
|
|
||||||
// Можно создать визуальное представление и т.д.
|
|
||||||
Console.WriteLine($"Starting drag of {_file.Name}");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DragCompleted(DragInfo dragInfo, DragDropEffects effects)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"File drag completed with {effects}");
|
|
||||||
|
|
||||||
if (effects == DragDropEffects.Move)
|
|
||||||
{
|
|
||||||
// Файл был перемещен - возможно, удалить оригинал
|
|
||||||
// _file.Delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DragCancelled(DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
Console.WriteLine("File drag cancelled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Асинхронная реализация
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class DatabaseItemDragSource : IAsyncDragSource
|
|
||||||
{
|
|
||||||
private readonly DatabaseService _db;
|
|
||||||
private readonly int _itemId;
|
|
||||||
|
|
||||||
public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Асинхронные проверки
|
|
||||||
var canDrag = await _db.CanDragItemAsync(_itemId);
|
|
||||||
if (!canDrag) return (false, null);
|
|
||||||
|
|
||||||
// Асинхронная загрузка данных
|
|
||||||
var data = await _db.GetItemForDragAsync(_itemId);
|
|
||||||
if (data == null) return (false, null);
|
|
||||||
|
|
||||||
var dragInfo = new DragInfo(
|
|
||||||
data: data,
|
|
||||||
allowedEffects: DragDropEffects.Copy | DragDropEffects.Move,
|
|
||||||
startPosition: Point.Zero,
|
|
||||||
source: this
|
|
||||||
);
|
|
||||||
|
|
||||||
return (true, dragInfo);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Логирование ошибки
|
|
||||||
return (false, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<bool> StartDragAsync(DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
// Асинхронная подготовка
|
|
||||||
return Task.FromResult(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects)
|
|
||||||
{
|
|
||||||
// Асинхронная обработка завершения
|
|
||||||
await _db.LogDragOperationAsync(_itemId, effects);
|
|
||||||
|
|
||||||
if (effects == DragDropEffects.Move)
|
|
||||||
{
|
|
||||||
await _db.MarkItemAsMovedAsync(_itemId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task DragCancelledAsync(DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Синхронные методы для совместимости
|
|
||||||
public bool CanStartDrag(out DragInfo? dragInfo)
|
|
||||||
{
|
|
||||||
var result = Task.Run(() => CanStartDragAsync()).GetAwaiter().GetResult();
|
|
||||||
dragInfo = result.DragInfo;
|
|
||||||
return result.CanStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... остальные синхронные методы
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Работа с моделями данных
|
|
||||||
|
|
||||||
#### DragInfo
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Создание
|
|
||||||
var dragInfo = new DragInfo(
|
|
||||||
data: myObject,
|
|
||||||
allowedEffects: DragDropEffects.Copy | DragDropEffects.Move,
|
|
||||||
startPosition: new Point(x, y),
|
|
||||||
source: this
|
|
||||||
);
|
|
||||||
|
|
||||||
// Параметры
|
|
||||||
dragInfo.SetParameter("Timestamp", DateTime.UtcNow);
|
|
||||||
dragInfo.SetParameter("UserId", currentUser.Id);
|
|
||||||
|
|
||||||
// Получение параметров
|
|
||||||
if (dragInfo.TryGetParameter<string>("Category", out var category))
|
|
||||||
{
|
|
||||||
// Используем категорию
|
|
||||||
}
|
|
||||||
|
|
||||||
// Клонирование с новой позицией
|
|
||||||
var updatedDragInfo = dragInfo.CloneWithPosition(newPosition);
|
|
||||||
|
|
||||||
// Очистка ресурсов
|
|
||||||
dragInfo.Dispose();
|
|
||||||
```
|
|
||||||
|
|
||||||
#### DropInfo
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Создается сервисом автоматически
|
|
||||||
// Работа с DropInfo в методах цели:
|
|
||||||
|
|
||||||
public void DragOver(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
// Проверяем данные
|
|
||||||
if (dropInfo.Data is MyDataType myData)
|
|
||||||
{
|
|
||||||
// Определяем позицию относительно цели
|
|
||||||
dropInfo.DropPosition = CalculateDropPosition(dropInfo.Position);
|
|
||||||
|
|
||||||
// Предлагаем эффекты
|
|
||||||
if (CanAcceptData(myData))
|
|
||||||
{
|
|
||||||
dropInfo.SuggestedEffects = DragDropEffects.Move;
|
|
||||||
dropInfo.ShowVisualFeedback = true;
|
|
||||||
dropInfo.VisualFeedbackData = CreatePreview(myData);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
dropInfo.SuggestedEffects = DragDropEffects.None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Drop(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
if (dropInfo.Data is MyDataType myData)
|
|
||||||
{
|
|
||||||
// Обработка сброса
|
|
||||||
ProcessDrop(myData, dropInfo.DropPosition);
|
|
||||||
|
|
||||||
// Помечаем как обработанное
|
|
||||||
dropInfo.MarkAsHandled();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Утилиты и фабрики
|
|
||||||
|
|
||||||
#### Синхронные утилиты
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Простые реализации
|
|
||||||
var simpleSource = DragDropUtilities.CreateSimpleDragSource(
|
|
||||||
() => data,
|
|
||||||
() => true,
|
|
||||||
(dragInfo, effects) => Console.WriteLine("Completed"),
|
|
||||||
dragInfo => Console.WriteLine("Cancelled")
|
|
||||||
);
|
|
||||||
|
|
||||||
var simpleTarget = DragDropUtilities.CreateSimpleDropTarget(
|
|
||||||
dropInfo => dropInfo.Data != null,
|
|
||||||
dropInfo => dropInfo.SuggestedEffects = DragDropEffects.Copy,
|
|
||||||
dropInfo => Console.WriteLine($"Dropped: {dropInfo.Data}"),
|
|
||||||
() => Console.WriteLine("Drag left")
|
|
||||||
);
|
|
||||||
|
|
||||||
// Геометрия
|
|
||||||
double distance = DragDropUtilities.CalculateDistance(p1, p2);
|
|
||||||
bool exceeded = DragDropUtilities.HasExceededDragThreshold(start, current, threshold);
|
|
||||||
DropPosition position = DragDropUtilities.GetDropPosition(point, bounds, edgeThreshold);
|
|
||||||
|
|
||||||
// Проверка совместимости
|
|
||||||
bool compatible = DragDropUtilities.AreEffectsCompatible(sourceEffects, targetEffects);
|
|
||||||
bool typeMatch = DragDropUtilities.IsDataCompatible(data, new[] { typeof(string), typeof(int) });
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Асинхронные утилиты
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Асинхронные реализации
|
|
||||||
var asyncSource = AsyncDragDropUtilities.CreateAsyncDragSource(
|
|
||||||
async () => await LoadDataAsync(),
|
|
||||||
async () => await CanDragAsync(),
|
|
||||||
async (dragInfo, effects) => await OnCompletedAsync(dragInfo, effects),
|
|
||||||
async dragInfo => await OnCancelledAsync(dragInfo)
|
|
||||||
);
|
|
||||||
|
|
||||||
var asyncTarget = AsyncDragDropUtilities.CreateAsyncDropTarget(
|
|
||||||
async dropInfo => await CanAcceptAsync(dropInfo.Data),
|
|
||||||
async dropInfo => await OnDragOverAsync(dropInfo),
|
|
||||||
async dropInfo => await OnDropAsync(dropInfo),
|
|
||||||
async () => await OnDragLeaveAsync()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Адаптеры для синхронных интерфейсов
|
|
||||||
IAsyncDragSource asyncFromSync = AsyncDragDropUtilities.CreateAsyncAdapter(syncSource);
|
|
||||||
IAsyncDropTarget asyncTargetFromSync = AsyncDragDropUtilities.CreateAsyncAdapter(syncTarget);
|
|
||||||
|
|
||||||
// Комбинированные реализации (fallback стратегия)
|
|
||||||
var combined = AsyncDragDropUtilities.Combine(
|
|
||||||
syncSource,
|
|
||||||
asyncSource,
|
|
||||||
preferAsync: true // При ошибке в async использует sync
|
|
||||||
);
|
|
||||||
|
|
||||||
// Таймауты
|
|
||||||
var result = await AsyncDragDropUtilities.ExecuteWithTimeoutAsync(
|
|
||||||
task: LongOperationAsync(),
|
|
||||||
timeout: TimeSpan.FromSeconds(5),
|
|
||||||
defaultValue: fallbackValue
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Обработка ошибок
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Подписка на ошибки
|
|
||||||
service.ErrorOccurred += (sender, e) =>
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Error in {e.Operation}: {e.Exception.Message}");
|
|
||||||
|
|
||||||
// Коды ошибок определены в DragDropErrorCodes
|
|
||||||
switch (e.ErrorCode)
|
|
||||||
{
|
|
||||||
case DragDropErrorCodes.Timeout:
|
|
||||||
Console.WriteLine("Operation timed out");
|
|
||||||
break;
|
|
||||||
case DragDropErrorCodes.SourceCannotDrag:
|
|
||||||
Console.WriteLine("Source cannot drag");
|
|
||||||
break;
|
|
||||||
case DragDropErrorCodes.TargetCannotAccept:
|
|
||||||
Console.WriteLine("Target cannot accept");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Использование в коде
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await service.StartDragAsync(source, position);
|
|
||||||
}
|
|
||||||
catch (DragDropException ex)
|
|
||||||
{
|
|
||||||
// Обработка специфичных для DragDrop ошибок
|
|
||||||
Console.WriteLine($"DragDrop error {ex.ErrorCode}: {ex.Message}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Обработка других ошибок
|
|
||||||
Console.WriteLine($"General error: {ex.Message}");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Интеграция с UI фреймворками
|
|
||||||
|
|
||||||
### Базовый адаптер для WinUI/WPF
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class UIElementDragSource : IAsyncDragSource
|
|
||||||
{
|
|
||||||
private readonly FrameworkElement _element;
|
|
||||||
private readonly Func<object> _dataProvider;
|
|
||||||
|
|
||||||
public UIElementDragSource(FrameworkElement element, Func<object> dataProvider)
|
|
||||||
{
|
|
||||||
_element = element;
|
|
||||||
_dataProvider = dataProvider;
|
|
||||||
|
|
||||||
// Подписка на события
|
|
||||||
_element.PointerPressed += OnPointerPressed;
|
|
||||||
_element.PointerMoved += OnPointerMoved;
|
|
||||||
_element.PointerReleased += OnPointerReleased;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Point _dragStartPosition;
|
|
||||||
private bool _isDragging;
|
|
||||||
|
|
||||||
private void OnPointerPressed(object sender, PointerRoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var point = e.GetCurrentPoint(_element);
|
|
||||||
_dragStartPosition = new Point(point.Position.X, point.Position.Y);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnPointerMoved(object sender, PointerRoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_isDragging) return;
|
|
||||||
|
|
||||||
var point = e.GetCurrentPoint(_element);
|
|
||||||
var current = new Point(point.Position.X, point.Position.Y);
|
|
||||||
|
|
||||||
var distance = Math.Sqrt(
|
|
||||||
Math.Pow(current.X - _dragStartPosition.X, 2) +
|
|
||||||
Math.Pow(current.Y - _dragStartPosition.Y, 2));
|
|
||||||
|
|
||||||
if (distance > 3.0) // Порог
|
|
||||||
{
|
|
||||||
_isDragging = true;
|
|
||||||
|
|
||||||
// Начинаем перетаскивание через сервис
|
|
||||||
var service = GetDragDropService();
|
|
||||||
await service.StartDragAsync(this, ConvertToScreen(_dragStartPosition));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
|
|
||||||
{
|
|
||||||
var data = _dataProvider();
|
|
||||||
if (data == null) return (false, null);
|
|
||||||
|
|
||||||
var dragInfo = new DragInfo(
|
|
||||||
data,
|
|
||||||
DragDropEffects.Copy | DragDropEffects.Move,
|
|
||||||
Point.Zero,
|
|
||||||
this
|
|
||||||
);
|
|
||||||
|
|
||||||
return (true, dragInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... остальная реализация
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🧪 Тестирование
|
|
||||||
|
|
||||||
### Примеры модульных тестов
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
[TestClass]
|
|
||||||
public class DragDropServiceTests
|
|
||||||
{
|
|
||||||
private DragDropService _service;
|
|
||||||
private Mock<IAsyncDragSource> _mockSource;
|
|
||||||
private Mock<IAsyncDropTarget> _mockTarget;
|
|
||||||
|
|
||||||
[TestInitialize]
|
|
||||||
public void Setup()
|
|
||||||
{
|
|
||||||
_service = new DragDropService();
|
|
||||||
_mockSource = new Mock<IAsyncDragSource>();
|
|
||||||
_mockTarget = new Mock<IAsyncDropTarget>();
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
public async Task StartDrag_ValidSource_ReturnsTrue()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var dragInfo = new DragInfo("test", DragDropEffects.Copy, Point.Zero);
|
|
||||||
_mockSource.Setup(s => s.CanStartDragAsync())
|
|
||||||
.ReturnsAsync((true, dragInfo));
|
|
||||||
_mockSource.Setup(s => s.StartDragAsync(It.IsAny<DragInfo>()))
|
|
||||||
.ReturnsAsync(true);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _service.StartDragAsync(_mockSource.Object, Point.Zero);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.IsTrue(result);
|
|
||||||
Assert.IsTrue(_service.IsDragActive);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
public async Task UpdateDrag_FindsTarget_CallsDragOver()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var targetId = _service.RegisterDropTarget(
|
|
||||||
_mockTarget.Object,
|
|
||||||
new Rect(0, 0, 100, 100)
|
|
||||||
);
|
|
||||||
|
|
||||||
await StartTestDrag();
|
|
||||||
|
|
||||||
_mockTarget.Setup(t => t.CanAcceptDropAsync(It.IsAny<DropInfo>()))
|
|
||||||
.ReturnsAsync(true);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await _service.UpdateDragAsync(new Point(50, 50));
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
_mockTarget.Verify(t => t.DragOverAsync(It.IsAny<DropInfo>()), Times.Once);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task StartTestDrag()
|
|
||||||
{
|
|
||||||
var dragInfo = new DragInfo("test", DragDropEffects.Copy, Point.Zero);
|
|
||||||
_mockSource.Setup(s => s.CanStartDragAsync())
|
|
||||||
.ReturnsAsync((true, dragInfo));
|
|
||||||
_mockSource.Setup(s => s.StartDragAsync(It.IsAny<DragInfo>()))
|
|
||||||
.ReturnsAsync(true);
|
|
||||||
|
|
||||||
await _service.StartDragAsync(_mockSource.Object, Point.Zero);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 Мониторинг и производительность
|
|
||||||
|
|
||||||
### Сбор статистики
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Получение статистики
|
|
||||||
var stats = service.GetStats();
|
|
||||||
|
|
||||||
Console.WriteLine($"Total operations: {stats.TotalDragOperations}");
|
|
||||||
Console.WriteLine($"Successful: {stats.SuccessfulDrops}");
|
|
||||||
Console.WriteLine($"Cancelled: {stats.CancelledOperations}");
|
|
||||||
Console.WriteLine($"Errors: {stats.ErrorCount}");
|
|
||||||
Console.WriteLine($"Avg time: {stats.AverageOperationTime.TotalMilliseconds}ms");
|
|
||||||
|
|
||||||
// Мониторинг в реальном времени
|
|
||||||
private Stopwatch _operationTimer;
|
|
||||||
|
|
||||||
service.DragStarted += (s, e) =>
|
|
||||||
{
|
|
||||||
_operationTimer = Stopwatch.StartNew();
|
|
||||||
Console.WriteLine($"Drag started from {e.DragInfo.Source}");
|
|
||||||
};
|
|
||||||
|
|
||||||
service.DragCompleted += (s, e) =>
|
|
||||||
{
|
|
||||||
_operationTimer.Stop();
|
|
||||||
Console.WriteLine($"Drag completed in {_operationTimer.ElapsedMilliseconds}ms");
|
|
||||||
|
|
||||||
if (service.EnableAsyncOperations)
|
|
||||||
{
|
|
||||||
var stats = service.GetStats();
|
|
||||||
Console.WriteLine($"Success rate: {(double)stats.SuccessfulDrops / stats.TotalDragOperations:P}");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Оптимизация производительности
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// 1. Настройка параметров
|
|
||||||
var service = new DragDropService(options =>
|
|
||||||
{
|
|
||||||
options.DragStartThreshold = 4.0; // Увеличить порог для предотвращения случайных перетаскиваний
|
|
||||||
options.AsyncOperationTimeout = 2000; // Уменьшить таймаут для отзывчивости
|
|
||||||
options.EnableAutoCleanup = true; // Автоочистка неиспользуемых целей
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. Группировка целей
|
|
||||||
_service.RegisterDropTarget(target1, bounds1, group: "toolbox");
|
|
||||||
_service.RegisterDropTarget(target2, bounds2, group: "toolbox");
|
|
||||||
// Быстрое удаление всех целей группы
|
|
||||||
_service.UnregisterDropTargetsInGroup("toolbox");
|
|
||||||
|
|
||||||
// 3. Приоритеты для оптимизации поиска
|
|
||||||
_service.RegisterDropTarget(importantTarget, bounds, priority: 100); // Высокий приоритет
|
|
||||||
_service.RegisterDropTarget(defaultTarget, bounds, priority: 0); // Низкий приоритет
|
|
||||||
|
|
||||||
// 4. Периодическая очистка
|
|
||||||
service.ClearAllDropTargets(); // При смене контекста
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 Продвинутые сценарии
|
|
||||||
|
|
||||||
### Переупорядочивание элементов
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class ReorderableListDropTarget : IAsyncDropTarget
|
|
||||||
{
|
|
||||||
private readonly IList<object> _items;
|
|
||||||
|
|
||||||
public async Task<bool> CanAcceptDropAsync(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
return dropInfo.Data is object && _items.Contains(dropInfo.Data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DropAsync(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
var item = dropInfo.Data;
|
|
||||||
var insertIndex = CalculateInsertIndex(dropInfo);
|
|
||||||
|
|
||||||
// Удаляем из старой позиции
|
|
||||||
_items.Remove(item);
|
|
||||||
|
|
||||||
// Вставляем в новую позицию
|
|
||||||
if (insertIndex < _items.Count)
|
|
||||||
_items.Insert(insertIndex, item);
|
|
||||||
else
|
|
||||||
_items.Add(item);
|
|
||||||
|
|
||||||
dropInfo.MarkAsHandled();
|
|
||||||
}
|
|
||||||
|
|
||||||
private int CalculateInsertIndex(DropInfo dropInfo)
|
|
||||||
{
|
|
||||||
// Логика определения позиции вставки на основе dropInfo.Position
|
|
||||||
// и визуального расположения элементов
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Мультиселект и групповое перетаскивание
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class MultiSelectionDragSource : IAsyncDragSource
|
|
||||||
{
|
|
||||||
private readonly IEnumerable<object> _selectedItems;
|
|
||||||
|
|
||||||
public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
|
|
||||||
{
|
|
||||||
if (!_selectedItems.Any()) return (false, null);
|
|
||||||
|
|
||||||
// Создаем коллекцию для перетаскивания
|
|
||||||
var dragData = new DragItemCollection(_selectedItems);
|
|
||||||
|
|
||||||
var dragInfo = new DragInfo(
|
|
||||||
dragData,
|
|
||||||
DragDropEffects.Copy | DragDropEffects.Move,
|
|
||||||
Point.Zero,
|
|
||||||
this
|
|
||||||
);
|
|
||||||
|
|
||||||
dragInfo.SetParameter("ItemCount", _selectedItems.Count());
|
|
||||||
dragInfo.SetParameter("IsMultiSelect", true);
|
|
||||||
|
|
||||||
return (true, dragInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📚 API Reference
|
|
||||||
|
|
||||||
### Основные интерфейсы
|
|
||||||
|
|
||||||
#### IDragDropService
|
|
||||||
```csharp
|
|
||||||
bool IsDragActive { get; }
|
|
||||||
DragInfo? CurrentDragInfo { get; }
|
|
||||||
IDropTarget? CurrentDropTarget { get; }
|
|
||||||
double DragStartThreshold { get; set; }
|
|
||||||
bool EnableAsyncOperations { get; set; }
|
|
||||||
|
|
||||||
// Регистрация целей
|
|
||||||
string RegisterDropTarget(IDropTarget target, Rect bounds, int priority = 0, string? group = null);
|
|
||||||
bool UpdateDropTargetBounds(string id, Rect bounds);
|
|
||||||
bool UnregisterDropTarget(string id);
|
|
||||||
void UnregisterDropTargetsInGroup(string group);
|
|
||||||
|
|
||||||
// Асинхронные операции
|
|
||||||
Task<bool> StartDragAsync(IDragSource source, Point startPosition);
|
|
||||||
Task UpdateDragAsync(Point position);
|
|
||||||
Task<DragDropEffects> EndDragAsync(Point position);
|
|
||||||
Task CancelDragAsync();
|
|
||||||
|
|
||||||
// Синхронные операции
|
|
||||||
bool StartDrag(IDragSource source, Point startPosition);
|
|
||||||
void UpdateDrag(Point position);
|
|
||||||
DragDropEffects EndDrag(Point position);
|
|
||||||
void CancelDrag();
|
|
||||||
|
|
||||||
// Утилиты
|
|
||||||
void ClearAllDropTargets();
|
|
||||||
DragDropStats GetStats();
|
|
||||||
```
|
|
||||||
|
|
||||||
#### IAsyncDragSource
|
|
||||||
```csharp
|
|
||||||
Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync();
|
|
||||||
Task<bool> StartDragAsync(DragInfo dragInfo);
|
|
||||||
Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects);
|
|
||||||
Task DragCancelledAsync(DragInfo dragInfo);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### IAsyncDropTarget
|
|
||||||
```csharp
|
|
||||||
Task<bool> CanAcceptDropAsync(DropInfo dropInfo);
|
|
||||||
Task DragOverAsync(DropInfo dropInfo);
|
|
||||||
Task DropAsync(DropInfo dropInfo);
|
|
||||||
Task DragLeaveAsync();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Перечисления
|
|
||||||
|
|
||||||
#### DragDropEffects
|
|
||||||
```csharp
|
|
||||||
[Flags]
|
|
||||||
None = 0
|
|
||||||
Copy = 1 << 0 // Копирование данных
|
|
||||||
Move = 1 << 1 // Перемещение данных
|
|
||||||
Link = 1 << 2 // Ссылка на данные
|
|
||||||
CopyOrMove = Copy | Move
|
|
||||||
All = Copy | Move | Link
|
|
||||||
|
|
||||||
// Методы расширения:
|
|
||||||
bool CanCopy(this DragDropEffects effects)
|
|
||||||
bool CanMove(this DragDropEffects effects)
|
|
||||||
bool CanLink(this DragDropEffects effects)
|
|
||||||
DragDropEffects GetEffectFromKeys(bool controlKey, bool shiftKey, bool altKey)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### DropPosition
|
|
||||||
```csharp
|
|
||||||
Inside // Внутри элемента
|
|
||||||
Top // Сверху
|
|
||||||
Bottom // Снизу
|
|
||||||
Left // Слева
|
|
||||||
Right // Справа
|
|
||||||
Center // По центру
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔮 Планы развития
|
|
||||||
|
|
||||||
1. **Интеграция с популярными UI фреймворками** (WinUI, Uno Platform, Avalonia)
|
|
||||||
2. **Поддержка жестов** (тач, мультитач)
|
|
||||||
3. **Виртуализация** для работы с большими наборами данных
|
|
||||||
4. **Продвинутые визуальные эффекты** (анимации, превью)
|
|
||||||
5. **Source Generators** для автоматической генерации кода
|
|
||||||
6. **Инструменты разработчика** (дебаггер, профилировщик)
|
|
||||||
@@ -1,846 +0,0 @@
|
|||||||
using Lattice.Core.DragDrop.Abstractions;
|
|
||||||
using Lattice.Core.DragDrop.Constants;
|
|
||||||
using Lattice.Core.DragDrop.Enums;
|
|
||||||
using Lattice.Core.DragDrop.Models;
|
|
||||||
using Lattice.Core.Geometry;
|
|
||||||
|
|
||||||
namespace Lattice.Core.DragDrop.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Центральный сервис управления операциями перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// <see cref="DragDropService"/> является основным компонентом системы drag-and-drop,
|
|
||||||
/// который координирует взаимодействие между источниками данных (<see cref="Abstractions.IDragSource"/>)
|
|
||||||
/// и целями сброса (<see cref="Abstractions.IDropTarget"/>).
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Основные функции сервиса:
|
|
||||||
/// </para>
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Регистрация и управление целями сброса</item>
|
|
||||||
/// <item>Оркестрация жизненного цикла операций перетаскивания</item>
|
|
||||||
/// <item>Обработка событий мыши и клавиатуры</item>
|
|
||||||
/// <item>Распространение информации между компонентами</item>
|
|
||||||
/// <item>Обеспечение потокобезопасности операций</item>
|
|
||||||
/// </list>
|
|
||||||
/// <para>
|
|
||||||
/// Сервис поддерживает полностью асинхронную модель работы, уведомления через события
|
|
||||||
/// и статистику использования. Все операции защищены от параллельного доступа
|
|
||||||
/// и обеспечивают корректную очистку ресурсов.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Для использования сервиса необходимо:
|
|
||||||
/// <list type="number">
|
|
||||||
/// <item>Зарегистрировать цели сброса с помощью <see cref="RegisterDropTarget"/></item>
|
|
||||||
/// <item>Вызывать методы <see cref="StartDragAsync"/>, <see cref="UpdateDragAsync"/>,
|
|
||||||
/// <see cref="EndDragAsync"/> в ответ на действия пользователя</item>
|
|
||||||
/// <item>Подписаться на события для отслеживания состояния операций</item>
|
|
||||||
/// </list>
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public sealed class DragDropService : IDragDropService
|
|
||||||
{
|
|
||||||
#region Nested Types
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Информация о зарегистрированной цели сброса.
|
|
||||||
/// </summary>
|
|
||||||
private sealed class DropTargetInfo : IDisposable
|
|
||||||
{
|
|
||||||
public required Abstractions.IDropTarget Target { get; init; }
|
|
||||||
public required Geometry.Rect Bounds { get; set; }
|
|
||||||
public required int Priority { get; init; }
|
|
||||||
public required string? Group { get; init; }
|
|
||||||
public required string Id { get; init; }
|
|
||||||
public DateTime LastAccessTime { get; set; } = DateTime.UtcNow;
|
|
||||||
public int UsageCount { get; set; }
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (Target is IDisposable disposable)
|
|
||||||
disposable.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Контекст текущей операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
private sealed class DragOperationContext : IDisposable
|
|
||||||
{
|
|
||||||
public Abstractions.IDragSource? Source { get; set; }
|
|
||||||
public Models.DragInfo? DragInfo { get; set; }
|
|
||||||
public Abstractions.IDropTarget? CurrentDropTarget { get; set; }
|
|
||||||
public CancellationTokenSource? CancellationTokenSource { get; set; }
|
|
||||||
public DateTime StartTime { get; set; } = DateTime.UtcNow;
|
|
||||||
public bool ThresholdExceeded { get; set; }
|
|
||||||
public Point LastPosition { get; set; }
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
DragInfo?.Dispose();
|
|
||||||
CancellationTokenSource?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Fields
|
|
||||||
|
|
||||||
private readonly Dictionary<string, DropTargetInfo> _dropTargets = new();
|
|
||||||
private readonly ReaderWriterLockSlim _dropTargetsLock = new(LockRecursionPolicy.NoRecursion);
|
|
||||||
private readonly object _dragOperationLock = new();
|
|
||||||
|
|
||||||
private DragOperationContext? _currentDragOperation;
|
|
||||||
private Timer? _cleanupTimer;
|
|
||||||
private bool _disposed;
|
|
||||||
|
|
||||||
private int _totalDragOperations;
|
|
||||||
private int _successfulDrops;
|
|
||||||
private int _cancelledOperations;
|
|
||||||
private int _errorCount;
|
|
||||||
private long _totalOperationTicks;
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Events
|
|
||||||
|
|
||||||
private event EventHandler<DragStartedEventArgs>? _dragStarted;
|
|
||||||
private event EventHandler<DragUpdatedEventArgs>? _dragUpdated;
|
|
||||||
private event EventHandler<DropTargetChangedEventArgs>? _dropTargetChanged;
|
|
||||||
private event EventHandler<DragCompletedEventArgs>? _dragCompleted;
|
|
||||||
private event EventHandler<DragCancelledEventArgs>? _dragCancelled;
|
|
||||||
private event EventHandler<DragDropErrorEventArgs>? _errorOccurred;
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public bool IsDragActive => Volatile.Read(ref _currentDragOperation) != null;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public Models.DragInfo? CurrentDragInfo => _currentDragOperation?.DragInfo;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public Abstractions.IDropTarget? CurrentDropTarget => _currentDragOperation?.CurrentDropTarget;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public double DragStartThreshold { get; set; } = DragDropConstants.DefaultDragThreshold;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public bool EnableAsyncOperations { get; set; } = true;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public int AsyncOperationTimeout { get; set; } = DragDropConstants.DefaultAsyncTimeout;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event EventHandler<DragStartedEventArgs> DragStarted
|
|
||||||
{
|
|
||||||
add => _dragStarted += value;
|
|
||||||
remove => _dragStarted -= value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event EventHandler<DragUpdatedEventArgs> DragUpdated
|
|
||||||
{
|
|
||||||
add => _dragUpdated += value;
|
|
||||||
remove => _dragUpdated -= value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event EventHandler<DropTargetChangedEventArgs> DropTargetChanged
|
|
||||||
{
|
|
||||||
add => _dropTargetChanged += value;
|
|
||||||
remove => _dropTargetChanged -= value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event EventHandler<DragCompletedEventArgs> DragCompleted
|
|
||||||
{
|
|
||||||
add => _dragCompleted += value;
|
|
||||||
remove => _dragCompleted -= value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event EventHandler<DragCancelledEventArgs> DragCancelled
|
|
||||||
{
|
|
||||||
add => _dragCancelled += value;
|
|
||||||
remove => _dragCancelled -= value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event EventHandler<DragDropErrorEventArgs> ErrorOccurred
|
|
||||||
{
|
|
||||||
add => _errorOccurred += value;
|
|
||||||
remove => _errorOccurred -= value;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Constructor
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DragDropService"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Создает сервис с настройками по умолчанию:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Порог начала перетаскивания: <see cref="Constants.DragDropConstants.DefaultDragThreshold"/> пикселей</item>
|
|
||||||
/// <item>Таймаут асинхронных операций: <see cref="Constants.DragDropConstants.DefaultAsyncTimeout"/> миллисекунд</item>
|
|
||||||
/// <item>Включены асинхронные операции: true</item>
|
|
||||||
/// </list>
|
|
||||||
/// </remarks>
|
|
||||||
public DragDropService()
|
|
||||||
{
|
|
||||||
// Инициализация таймера очистки (каждые 5 минут)
|
|
||||||
_cleanupTimer = new Timer(CleanupExpiredTargets, null,
|
|
||||||
TimeSpan.FromMinutes(Constants.DragDropConstants.TargetLifetimeMinutes / 2),
|
|
||||||
TimeSpan.FromMinutes(Constants.DragDropConstants.TargetLifetimeMinutes / 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Registration Methods
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public string RegisterDropTarget(Abstractions.IDropTarget target, Geometry.Rect bounds, int priority = 0, string? group = null)
|
|
||||||
{
|
|
||||||
ThrowIfDisposed();
|
|
||||||
if (target == null) throw new ArgumentNullException(nameof(target));
|
|
||||||
|
|
||||||
var id = Guid.NewGuid().ToString("N");
|
|
||||||
var info = new DropTargetInfo
|
|
||||||
{
|
|
||||||
Target = target,
|
|
||||||
Bounds = bounds,
|
|
||||||
Priority = priority,
|
|
||||||
Group = group,
|
|
||||||
Id = id
|
|
||||||
};
|
|
||||||
|
|
||||||
_dropTargetsLock.EnterWriteLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_dropTargets[id] = info;
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_dropTargetsLock.ExitWriteLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public bool UpdateDropTargetBounds(string id, Geometry.Rect bounds)
|
|
||||||
{
|
|
||||||
ThrowIfDisposed();
|
|
||||||
|
|
||||||
_dropTargetsLock.EnterUpgradeableReadLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!_dropTargets.TryGetValue(id, out var info))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
_dropTargetsLock.EnterWriteLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
info.Bounds = bounds;
|
|
||||||
info.LastAccessTime = DateTime.UtcNow;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_dropTargetsLock.ExitWriteLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_dropTargetsLock.ExitUpgradeableReadLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public bool UnregisterDropTarget(string id)
|
|
||||||
{
|
|
||||||
ThrowIfDisposed();
|
|
||||||
|
|
||||||
_dropTargetsLock.EnterWriteLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_dropTargets.Remove(id, out var info))
|
|
||||||
{
|
|
||||||
info.Dispose();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_dropTargetsLock.ExitWriteLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void UnregisterDropTargetsInGroup(string group)
|
|
||||||
{
|
|
||||||
ThrowIfDisposed();
|
|
||||||
if (string.IsNullOrEmpty(group)) return;
|
|
||||||
|
|
||||||
_dropTargetsLock.EnterWriteLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var idsToRemove = new List<string>();
|
|
||||||
|
|
||||||
foreach (var kvp in _dropTargets)
|
|
||||||
{
|
|
||||||
if (kvp.Value.Group == group)
|
|
||||||
{
|
|
||||||
idsToRemove.Add(kvp.Key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var id in idsToRemove)
|
|
||||||
{
|
|
||||||
if (_dropTargets.Remove(id, out var info))
|
|
||||||
{
|
|
||||||
info.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_dropTargetsLock.ExitWriteLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Async Operations
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public async Task<bool> StartDragAsync(IDragSource source, Point startPosition)
|
|
||||||
{
|
|
||||||
ThrowIfDisposed();
|
|
||||||
if (source == null) throw new ArgumentNullException(nameof(source));
|
|
||||||
|
|
||||||
lock (_dragOperationLock)
|
|
||||||
{
|
|
||||||
if (_currentDragOperation != null)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Interlocked.Increment(ref _totalDragOperations);
|
|
||||||
|
|
||||||
DragInfo? dragInfo;
|
|
||||||
|
|
||||||
// Пытаемся начать перетаскивание
|
|
||||||
if (EnableAsyncOperations)
|
|
||||||
{
|
|
||||||
dragInfo = await ExecuteWithTimeoutAsync(
|
|
||||||
source.TryStartDragAsync(startPosition),
|
|
||||||
"TryStartDragAsync",
|
|
||||||
source);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
dragInfo = await source.TryStartDragAsync(startPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dragInfo == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var updatedDragInfo = dragInfo.CloneWithPosition(startPosition);
|
|
||||||
|
|
||||||
lock (_dragOperationLock)
|
|
||||||
{
|
|
||||||
_currentDragOperation = new DragOperationContext
|
|
||||||
{
|
|
||||||
Source = source,
|
|
||||||
DragInfo = updatedDragInfo,
|
|
||||||
CancellationTokenSource = new CancellationTokenSource(),
|
|
||||||
LastPosition = startPosition
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Вызов события
|
|
||||||
_dragStarted?.Invoke(this, new DragStartedEventArgs(updatedDragInfo, startPosition));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Interlocked.Increment(ref _errorCount);
|
|
||||||
HandleError(ex, "StartDragAsync", source);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public async Task UpdateDragAsync(Point position)
|
|
||||||
{
|
|
||||||
ThrowIfDisposed();
|
|
||||||
|
|
||||||
DragOperationContext? context;
|
|
||||||
lock (_dragOperationLock)
|
|
||||||
{
|
|
||||||
context = _currentDragOperation;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context?.DragInfo == null || context.Source == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Проверка порога начала
|
|
||||||
if (!context.ThresholdExceeded && DragStartThreshold > 0)
|
|
||||||
{
|
|
||||||
var distance = CalculateDistance(context.DragInfo.StartPosition, position);
|
|
||||||
if (distance < DragStartThreshold)
|
|
||||||
return;
|
|
||||||
|
|
||||||
context.ThresholdExceeded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var updatedDragInfo = context.DragInfo.CloneWithPosition(position);
|
|
||||||
context.DragInfo.Dispose();
|
|
||||||
context.DragInfo = updatedDragInfo;
|
|
||||||
context.LastPosition = position;
|
|
||||||
|
|
||||||
// Поиск новой цели сброса
|
|
||||||
var newDropTarget = await FindDropTargetAsync(position, updatedDragInfo);
|
|
||||||
|
|
||||||
// Обработка смены цели
|
|
||||||
if (context.CurrentDropTarget != newDropTarget?.Target)
|
|
||||||
{
|
|
||||||
if (context.CurrentDropTarget != null)
|
|
||||||
{
|
|
||||||
await ExecuteTargetOperationAsync(
|
|
||||||
context.CurrentDropTarget,
|
|
||||||
t => t.OnDragLeaveAsync(),
|
|
||||||
"OnDragLeave");
|
|
||||||
}
|
|
||||||
|
|
||||||
context.CurrentDropTarget = newDropTarget?.Target;
|
|
||||||
|
|
||||||
if (newDropTarget != null)
|
|
||||||
{
|
|
||||||
newDropTarget.UsageCount++;
|
|
||||||
_dropTargetChanged?.Invoke(this, new DropTargetChangedEventArgs(
|
|
||||||
updatedDragInfo, position, newDropTarget.Target, newDropTarget.Bounds));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Уведомление текущей цели
|
|
||||||
if (context.CurrentDropTarget != null)
|
|
||||||
{
|
|
||||||
var dropInfo = new DropInfo(
|
|
||||||
updatedDragInfo.Data,
|
|
||||||
position,
|
|
||||||
updatedDragInfo.AllowedEffects,
|
|
||||||
context.CurrentDropTarget);
|
|
||||||
|
|
||||||
await ExecuteTargetOperationAsync(
|
|
||||||
context.CurrentDropTarget,
|
|
||||||
t => t.OnDragOverAsync(dropInfo),
|
|
||||||
"OnDragOver");
|
|
||||||
}
|
|
||||||
|
|
||||||
_dragUpdated?.Invoke(this, new DragUpdatedEventArgs(updatedDragInfo, position));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Interlocked.Increment(ref _errorCount);
|
|
||||||
HandleError(ex, "UpdateDragAsync", context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public async Task<DragDropEffects> EndDragAsync(Point position)
|
|
||||||
{
|
|
||||||
ThrowIfDisposed();
|
|
||||||
|
|
||||||
DragOperationContext? context;
|
|
||||||
lock (_dragOperationLock)
|
|
||||||
{
|
|
||||||
context = _currentDragOperation;
|
|
||||||
_currentDragOperation = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context == null || context.DragInfo == null || context.Source == null)
|
|
||||||
{
|
|
||||||
Reset();
|
|
||||||
return DragDropEffects.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var effects = DragDropEffects.None;
|
|
||||||
var operationTime = DateTime.UtcNow - context.StartTime;
|
|
||||||
Interlocked.Add(ref _totalOperationTicks, operationTime.Ticks);
|
|
||||||
|
|
||||||
// Выполнение сброса
|
|
||||||
if (context.CurrentDropTarget != null)
|
|
||||||
{
|
|
||||||
var dropInfo = new DropInfo(
|
|
||||||
context.DragInfo.Data,
|
|
||||||
position,
|
|
||||||
context.DragInfo.AllowedEffects,
|
|
||||||
context.CurrentDropTarget);
|
|
||||||
|
|
||||||
await ExecuteTargetOperationAsync(
|
|
||||||
context.CurrentDropTarget,
|
|
||||||
t => t.OnDropAsync(dropInfo),
|
|
||||||
"OnDrop");
|
|
||||||
|
|
||||||
if (dropInfo.Handled)
|
|
||||||
{
|
|
||||||
effects = dropInfo.SuggestedEffects;
|
|
||||||
Interlocked.Increment(ref _successfulDrops);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Уведомление источника
|
|
||||||
await ExecuteSourceOperationAsync(
|
|
||||||
context.Source,
|
|
||||||
s => s.OnDragCompletedAsync(context.DragInfo, effects),
|
|
||||||
"OnDragCompleted");
|
|
||||||
|
|
||||||
// Событие завершения
|
|
||||||
_dragCompleted?.Invoke(this, new DragCompletedEventArgs(
|
|
||||||
context.DragInfo, position, effects));
|
|
||||||
|
|
||||||
return effects;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Interlocked.Increment(ref _errorCount);
|
|
||||||
HandleError(ex, "EndDragAsync", context);
|
|
||||||
return DragDropEffects.None;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
context.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public async Task CancelDragAsync()
|
|
||||||
{
|
|
||||||
ThrowIfDisposed();
|
|
||||||
|
|
||||||
DragOperationContext? context;
|
|
||||||
lock (_dragOperationLock)
|
|
||||||
{
|
|
||||||
context = _currentDragOperation;
|
|
||||||
_currentDragOperation = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context == null || context.DragInfo == null || context.Source == null)
|
|
||||||
{
|
|
||||||
Reset();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
context.CancellationTokenSource?.Cancel();
|
|
||||||
Interlocked.Increment(ref _cancelledOperations);
|
|
||||||
|
|
||||||
await ExecuteSourceOperationAsync(
|
|
||||||
context.Source,
|
|
||||||
s => s.OnDragCancelledAsync(context.DragInfo),
|
|
||||||
"OnDragCancelled");
|
|
||||||
|
|
||||||
_dragCancelled?.Invoke(this, new DragCancelledEventArgs(context.DragInfo, context.LastPosition));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Interlocked.Increment(ref _errorCount);
|
|
||||||
HandleError(ex, "CancelDragAsync", context);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
context.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Utility Methods
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void ClearAllDropTargets()
|
|
||||||
{
|
|
||||||
ThrowIfDisposed();
|
|
||||||
|
|
||||||
_dropTargetsLock.EnterWriteLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
foreach (var info in _dropTargets.Values)
|
|
||||||
{
|
|
||||||
info.Dispose();
|
|
||||||
}
|
|
||||||
_dropTargets.Clear();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_dropTargetsLock.ExitWriteLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public DragDropStats GetStats()
|
|
||||||
{
|
|
||||||
return new DragDropStats
|
|
||||||
{
|
|
||||||
TotalDragOperations = _totalDragOperations,
|
|
||||||
SuccessfulDrops = _successfulDrops,
|
|
||||||
CancelledOperations = _cancelledOperations,
|
|
||||||
ErrorCount = _errorCount,
|
|
||||||
RegisteredTargets = _dropTargets.Count,
|
|
||||||
AverageOperationTime = _totalDragOperations > 0
|
|
||||||
? TimeSpan.FromTicks(_totalOperationTicks / _totalDragOperations)
|
|
||||||
: TimeSpan.Zero
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Private Helper Methods
|
|
||||||
|
|
||||||
private async Task<DropTargetInfo?> FindDropTargetAsync(Geometry.Point position, Models.DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
DropTargetInfo? bestTarget = null;
|
|
||||||
|
|
||||||
_dropTargetsLock.EnterReadLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Фильтруем цели по границам и сортируем по приоритету
|
|
||||||
var candidates = _dropTargets.Values
|
|
||||||
.Where(info => info.Bounds.Contains(position))
|
|
||||||
.OrderByDescending(info => info.Priority)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
foreach (var info in candidates)
|
|
||||||
{
|
|
||||||
var dropInfo = new Models.DropInfo(
|
|
||||||
dragInfo.Data,
|
|
||||||
position,
|
|
||||||
dragInfo.AllowedEffects,
|
|
||||||
info.Target);
|
|
||||||
|
|
||||||
bool canAccept = await ExecuteWithTimeoutAsync(
|
|
||||||
info.Target.CanAcceptDropAsync(dropInfo),
|
|
||||||
"CanAcceptDropAsync",
|
|
||||||
info.Target);
|
|
||||||
|
|
||||||
if (canAccept)
|
|
||||||
{
|
|
||||||
info.LastAccessTime = DateTime.UtcNow;
|
|
||||||
bestTarget = info;
|
|
||||||
break; // Берем первую подходящую с наивысшим приоритетом
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_dropTargetsLock.ExitReadLock();
|
|
||||||
}
|
|
||||||
|
|
||||||
return bestTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ExecuteTargetOperationAsync(
|
|
||||||
IDropTarget target,
|
|
||||||
Func<IDropTarget, Task> operation,
|
|
||||||
string operationName)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (EnableAsyncOperations)
|
|
||||||
{
|
|
||||||
await ExecuteWithTimeoutAsync(
|
|
||||||
operation(target),
|
|
||||||
$"{operationName}Async",
|
|
||||||
target);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await operation(target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Interlocked.Increment(ref _errorCount);
|
|
||||||
HandleError(ex, operationName, target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ExecuteSourceOperationAsync(
|
|
||||||
IDragSource source,
|
|
||||||
Func<IDragSource, Task> operation,
|
|
||||||
string operationName)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (EnableAsyncOperations)
|
|
||||||
{
|
|
||||||
await ExecuteWithTimeoutAsync(
|
|
||||||
operation(source),
|
|
||||||
$"{operationName}Async",
|
|
||||||
source);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await operation(source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Interlocked.Increment(ref _errorCount);
|
|
||||||
HandleError(ex, operationName, source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<T> ExecuteWithTimeoutAsync<T>(Task<T> task, string operationName, object? context = null)
|
|
||||||
{
|
|
||||||
if (AsyncOperationTimeout <= 0)
|
|
||||||
return await task;
|
|
||||||
|
|
||||||
var timeoutTask = Task.Delay(AsyncOperationTimeout);
|
|
||||||
var completedTask = await Task.WhenAny(task, timeoutTask);
|
|
||||||
|
|
||||||
if (completedTask == timeoutTask)
|
|
||||||
{
|
|
||||||
throw new TimeoutException($"{operationName} timed out after {AsyncOperationTimeout}ms");
|
|
||||||
}
|
|
||||||
|
|
||||||
return await task;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ExecuteWithTimeoutAsync(Task task, string operationName, object? context = null)
|
|
||||||
{
|
|
||||||
if (AsyncOperationTimeout <= 0)
|
|
||||||
{
|
|
||||||
await task;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var timeoutTask = Task.Delay(AsyncOperationTimeout);
|
|
||||||
var completedTask = await Task.WhenAny(task, timeoutTask);
|
|
||||||
|
|
||||||
if (completedTask == timeoutTask)
|
|
||||||
{
|
|
||||||
throw new TimeoutException($"{operationName} timed out after {AsyncOperationTimeout}ms");
|
|
||||||
}
|
|
||||||
|
|
||||||
await task;
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Reset()
|
|
||||||
{
|
|
||||||
lock (_dragOperationLock)
|
|
||||||
{
|
|
||||||
_currentDragOperation?.Dispose();
|
|
||||||
_currentDragOperation = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CleanupExpiredTargets(object? state)
|
|
||||||
{
|
|
||||||
var expirationTime = DateTime.UtcNow.AddMinutes(-Constants.DragDropConstants.TargetLifetimeMinutes);
|
|
||||||
|
|
||||||
_dropTargetsLock.EnterWriteLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var idsToRemove = new List<string>();
|
|
||||||
var currentTarget = _currentDragOperation?.CurrentDropTarget;
|
|
||||||
|
|
||||||
foreach (var kvp in _dropTargets)
|
|
||||||
{
|
|
||||||
if (kvp.Value.LastAccessTime < expirationTime && !ReferenceEquals(kvp.Value.Target, currentTarget))
|
|
||||||
{
|
|
||||||
idsToRemove.Add(kvp.Key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var id in idsToRemove)
|
|
||||||
{
|
|
||||||
if (_dropTargets.Remove(id, out var info))
|
|
||||||
{
|
|
||||||
info.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_dropTargetsLock.ExitWriteLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleError(Exception exception, string operation, object? context = null)
|
|
||||||
{
|
|
||||||
_errorOccurred?.Invoke(this, new DragDropErrorEventArgs(exception, operation, context));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ThrowIfDisposed()
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
throw new ObjectDisposedException(GetType().Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region IDisposable Implementation
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (_disposed) return;
|
|
||||||
|
|
||||||
lock (_dragOperationLock)
|
|
||||||
{
|
|
||||||
if (_disposed) return;
|
|
||||||
|
|
||||||
var timer = Interlocked.Exchange(ref _cleanupTimer, null);
|
|
||||||
timer?.Dispose();
|
|
||||||
|
|
||||||
_cleanupTimer?.Dispose();
|
|
||||||
_cleanupTimer = null;
|
|
||||||
|
|
||||||
if (_currentDragOperation != null)
|
|
||||||
{
|
|
||||||
_currentDragOperation.CancellationTokenSource?.Cancel();
|
|
||||||
_currentDragOperation.Dispose();
|
|
||||||
_currentDragOperation = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
ClearAllDropTargets();
|
|
||||||
|
|
||||||
_dropTargetsLock.Dispose();
|
|
||||||
|
|
||||||
// Очистка событий
|
|
||||||
_dragStarted = null;
|
|
||||||
_dragUpdated = null;
|
|
||||||
_dropTargetChanged = null;
|
|
||||||
_dragCompleted = null;
|
|
||||||
_dragCancelled = null;
|
|
||||||
_errorOccurred = null;
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
using Lattice.Core.DragDrop.Models;
|
|
||||||
using Lattice.Core.Geometry;
|
|
||||||
|
|
||||||
namespace Lattice.Core.DragDrop.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Предоставляет базовые данные для событий перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот класс содержит общие свойства, которые используются в большинстве событий
|
|
||||||
/// системы перетаскивания. Является базовым классом для специализированных событий.
|
|
||||||
/// </remarks>
|
|
||||||
public abstract class DragEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Получает информацию о текущей операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Объект <see cref="DragInfo"/>, содержащий данные, эффекты и метаданные операции.
|
|
||||||
/// Всегда возвращает актуальную информацию на момент возникновения события.
|
|
||||||
/// </value>
|
|
||||||
public DragInfo DragInfo { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает текущую позицию курсора в координатах экрана.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Точка, представляющая положение курсора мыши в момент события.
|
|
||||||
/// Используется для точного позиционирования и визуальной обратной связи.
|
|
||||||
/// </value>
|
|
||||||
public Point Position { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DragEventArgs"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragInfo">Информация о перетаскивании.</param>
|
|
||||||
/// <param name="position">Текущая позиция курсора.</param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, когда <paramref name="dragInfo"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
protected DragEventArgs(DragInfo dragInfo, Point position)
|
|
||||||
{
|
|
||||||
DragInfo = dragInfo ?? throw new ArgumentNullException(nameof(dragInfo));
|
|
||||||
Position = position;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Предоставляет данные для события начала перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Возникает, когда пользователь начинает операцию перетаскивания.
|
|
||||||
/// Это первое событие в жизненном цикле операции.
|
|
||||||
/// </remarks>
|
|
||||||
public sealed class DragStartedEventArgs : DragEventArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DragStartedEventArgs"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragInfo">Информация о перетаскивании.</param>
|
|
||||||
/// <param name="position">Начальная позиция перетаскивания.</param>
|
|
||||||
public DragStartedEventArgs(DragInfo dragInfo, Point position)
|
|
||||||
: base(dragInfo, position)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Предоставляет данные для события обновления позиции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Возникает при каждом перемещении курсора во время операции перетаскивания.
|
|
||||||
/// Может вызываться многократно с высокой частотой, поэтому обработчики
|
|
||||||
/// должны быть оптимизированы для производительности.
|
|
||||||
/// </remarks>
|
|
||||||
public sealed class DragUpdatedEventArgs : DragEventArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DragUpdatedEventArgs"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragInfo">Информация о перетаскивании.</param>
|
|
||||||
/// <param name="position">Текущая позиция курсора.</param>
|
|
||||||
public DragUpdatedEventArgs(DragInfo dragInfo, Point position)
|
|
||||||
: base(dragInfo, position)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Предоставляет данные для события завершения перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Возникает, когда пользователь завершает операцию перетаскивания
|
|
||||||
/// (отпускает кнопку мыши над целью или вне области сброса).
|
|
||||||
/// Содержит информацию о примененных эффектах и результатах операции.
|
|
||||||
/// </remarks>
|
|
||||||
public sealed class DragCompletedEventArgs : DragEventArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Получает эффекты, примененные при завершении операции.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Комбинация флагов <see cref="Enums.DragDropEffects"/>, указывающая,
|
|
||||||
/// как были обработаны данные (копирование, перемещение и т.д.).
|
|
||||||
/// </value>
|
|
||||||
public Enums.DragDropEffects Effects { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DragCompletedEventArgs"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragInfo">Информация о перетаскивании.</param>
|
|
||||||
/// <param name="position">Позиция завершения операции.</param>
|
|
||||||
/// <param name="effects">Примененные эффекты перетаскивания.</param>
|
|
||||||
public DragCompletedEventArgs(DragInfo dragInfo, Point position, Enums.DragDropEffects effects)
|
|
||||||
: base(dragInfo, position)
|
|
||||||
{
|
|
||||||
Effects = effects;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Предоставляет данные для события отмены перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Возникает, когда операция перетаскивания была отменена пользователем
|
|
||||||
/// (например, нажатием клавиши Escape) или системой (например, при ошибке).
|
|
||||||
/// После этого события система возвращается в исходное состояние.
|
|
||||||
/// </remarks>
|
|
||||||
public sealed class DragCancelledEventArgs : DragEventArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DragCancelledEventArgs"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragInfo">Информация о перетаскивании.</param>
|
|
||||||
/// <param name="position">Позиция в момент отмены.</param>
|
|
||||||
public DragCancelledEventArgs(DragInfo dragInfo, Point position)
|
|
||||||
: base(dragInfo, position)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Предоставляет данные для события изменения цели сброса.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Возникает, когда курсор перемещается с одной цели сброса на другую
|
|
||||||
/// или покидает область всех целей. Позволяет обновлять визуальную
|
|
||||||
/// обратную связь при изменении контекста сброса.
|
|
||||||
/// </remarks>
|
|
||||||
public sealed class DropTargetChangedEventArgs : DragEventArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Получает новую цель сброса, над которой находится курсор.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Объект <see cref="Abstractions.IDropTarget"/>, готовый принять данные,
|
|
||||||
/// или null, если курсор покинул область всех целей.
|
|
||||||
/// </value>
|
|
||||||
public Abstractions.IDropTarget? Target { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает границы новой цели сброса.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Прямоугольник, определяющий область цели в координатах экрана.
|
|
||||||
/// Может использоваться для точного позиционирования визуальной обратной связи.
|
|
||||||
/// </value>
|
|
||||||
public Rect TargetBounds { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DropTargetChangedEventArgs"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragInfo">Информация о перетаскивании.</param>
|
|
||||||
/// <param name="position">Текущая позиция курсора.</param>
|
|
||||||
/// <param name="target">Новая цель сброса.</param>
|
|
||||||
/// <param name="targetBounds">Границы цели сброса.</param>
|
|
||||||
public DropTargetChangedEventArgs(DragInfo dragInfo, Point position, Abstractions.IDropTarget? target, Rect targetBounds)
|
|
||||||
: base(dragInfo, position)
|
|
||||||
{
|
|
||||||
Target = target;
|
|
||||||
TargetBounds = targetBounds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Предоставляет данные для события ошибки в операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Возникает при возникновении исключения в любом из компонентов
|
|
||||||
/// системы перетаскивания. Позволяет централизованно обрабатывать ошибки
|
|
||||||
/// и предоставлять пользователю информацию о проблемах.
|
|
||||||
/// </remarks>
|
|
||||||
public sealed class DragDropErrorEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Получает исключение, вызвавшее ошибку.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Объект <see cref="Exception"/>, содержащий информацию об ошибке.
|
|
||||||
/// Может быть любого типа, в зависимости от источника ошибки.
|
|
||||||
/// </value>
|
|
||||||
public Exception Exception { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает название операции, во время которой произошла ошибка.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Строка, идентифицирующая операцию (например, "StartDragAsync",
|
|
||||||
/// "OnDropAsync", "UpdateDropTargetBounds").
|
|
||||||
/// </value>
|
|
||||||
public string Operation { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает контекст, в котором произошла ошибка.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Объект, содержащий дополнительную информацию о контексте ошибки,
|
|
||||||
/// или null, если контекст недоступен.
|
|
||||||
/// </value>
|
|
||||||
public object? Context { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DragDropErrorEventArgs"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="exception">Исключение, вызвавшее ошибку.</param>
|
|
||||||
/// <param name="operation">Название операции.</param>
|
|
||||||
/// <param name="context">Контекст ошибки.</param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, когда <paramref name="exception"/> или <paramref name="operation"/> равны null.
|
|
||||||
/// </exception>
|
|
||||||
public DragDropErrorEventArgs(Exception exception, string operation, object? context = null)
|
|
||||||
{
|
|
||||||
Exception = exception ?? throw new ArgumentNullException(nameof(exception));
|
|
||||||
Operation = operation ?? throw new ArgumentNullException(nameof(operation));
|
|
||||||
Context = context;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
namespace Lattice.Core.DragDrop.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Предоставляет централизованный сервис для управления операциями перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public interface IDragDropService : IDisposable
|
|
||||||
{
|
|
||||||
#region Свойства
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает значение, указывающее, активна ли операция перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>true, если операция перетаскивания активна; в противном случае — false.</value>
|
|
||||||
bool IsDragActive { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает информацию о текущей операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Объект <see cref="Models.DragInfo"/>, содержащий данные текущей операции,
|
|
||||||
/// или null, если операция не активна.
|
|
||||||
/// </value>
|
|
||||||
Models.DragInfo? CurrentDragInfo { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает текущую цель сброса.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Объект <see cref="Abstractions.IDropTarget"/>, над которым находится курсор,
|
|
||||||
/// или null, если курсор не над зарегистрированной целью.
|
|
||||||
/// </value>
|
|
||||||
Abstractions.IDropTarget? CurrentDropTarget { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает порог начала перетаскивания в пикселях.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Минимальное расстояние, которое должен пройти курсор мыши, чтобы начать операцию перетаскивания.
|
|
||||||
/// Значение по умолчанию: <see cref="Constants.DragDropConstants.DefaultDragThreshold"/>.
|
|
||||||
/// </value>
|
|
||||||
double DragStartThreshold { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает значение, указывающее, включены ли асинхронные операции.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>true, если асинхронные операции включены; в противном случае — false.</value>
|
|
||||||
bool EnableAsyncOperations { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает максимальное время ожидания асинхронной операции в миллисекундах.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Время ожидания в миллисекундах. Значение 0 или меньше означает отсутствие таймаута.
|
|
||||||
/// Значение по умолчанию: <see cref="Constants.DragDropConstants.DefaultAsyncTimeout"/>.
|
|
||||||
/// </value>
|
|
||||||
int AsyncOperationTimeout { get; set; }
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region События
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Происходит при начале операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
event EventHandler<DragStartedEventArgs> DragStarted;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Происходит при обновлении позиции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
event EventHandler<DragUpdatedEventArgs> DragUpdated;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Происходит при изменении цели сброса.
|
|
||||||
/// </summary>
|
|
||||||
event EventHandler<DropTargetChangedEventArgs> DropTargetChanged;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Происходит при завершении операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
event EventHandler<DragCompletedEventArgs> DragCompleted;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Происходит при отмене операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
event EventHandler<DragCancelledEventArgs> DragCancelled;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Происходит при возникновении ошибки в операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
event EventHandler<DragDropErrorEventArgs> ErrorOccurred;
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Регистрация целей сброса
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Регистрирует цель сброса в системе.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="target">Цель сброса для регистрации.</param>
|
|
||||||
/// <param name="bounds">Границы области цели в координатах экрана.</param>
|
|
||||||
/// <param name="priority">Приоритет цели (высшие значения обрабатываются первыми).</param>
|
|
||||||
/// <param name="group">Имя группы для групповой отмены регистрации.</param>
|
|
||||||
/// <returns>Уникальный идентификатор зарегистрированной цели.</returns>
|
|
||||||
/// <exception cref="ArgumentNullException">Выбрасывается, когда <paramref name="target"/> равен null.</exception>
|
|
||||||
/// <exception cref="ObjectDisposedException">Выбрасывается, если сервис был удален.</exception>
|
|
||||||
string RegisterDropTarget(Abstractions.IDropTarget target, Geometry.Rect bounds, int priority = 0, string? group = null);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Обновляет границы цели сброса.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">Идентификатор цели сброса.</param>
|
|
||||||
/// <param name="bounds">Новые границы области цели.</param>
|
|
||||||
/// <returns>true, если границы успешно обновлены; в противном случае — false.</returns>
|
|
||||||
/// <exception cref="ObjectDisposedException">Выбрасывается, если сервис был удален.</exception>
|
|
||||||
bool UpdateDropTargetBounds(string id, Geometry.Rect bounds);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Отменяет регистрацию цели сброса.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">Идентификатор цели сброса.</param>
|
|
||||||
/// <returns>true, если цель успешно удалена; в противном случае — false.</returns>
|
|
||||||
/// <exception cref="ObjectDisposedException">Выбрасывается, если сервис был удален.</exception>
|
|
||||||
bool UnregisterDropTarget(string id);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Отменяет регистрацию всех целей сброса в указанной группе.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="group">Имя группы для удаления.</param>
|
|
||||||
/// <exception cref="ObjectDisposedException">Выбрасывается, если сервис был удален.</exception>
|
|
||||||
void UnregisterDropTargetsInGroup(string group);
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Асинхронные операции
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Начинает операцию перетаскивания из указанной позиции.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="source">Источник данных для перетаскивания.</param>
|
|
||||||
/// <param name="startPosition">Начальная позиция операции в координатах экрана.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// Задача, представляющая асинхронную операцию. Результат содержит true, если операция успешно начата;
|
|
||||||
/// в противном случае — false.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">Выбрасывается, когда <paramref name="source"/> равен null.</exception>
|
|
||||||
/// <exception cref="ObjectDisposedException">Выбрасывается, если сервис был удален.</exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот метод следует вызывать в ответ на событие нажатия кнопки мыши или начала жеста перетаскивания.
|
|
||||||
/// </remarks>
|
|
||||||
Task<bool> StartDragAsync(Abstractions.IDragSource source, Geometry.Point startPosition);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Обновляет позицию текущей операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="position">Новая позиция курсора в координатах экрана.</param>
|
|
||||||
/// <returns>Задача, представляющая асинхронную операцию.</returns>
|
|
||||||
/// <exception cref="ObjectDisposedException">Выбрасывается, если сервис был удален.</exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот метод следует вызывать при каждом перемещении мыши во время операции перетаскивания.
|
|
||||||
/// </remarks>
|
|
||||||
Task UpdateDragAsync(Geometry.Point position);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Завершает текущую операцию перетаскивания в указанной позиции.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="position">Позиция завершения операции в координатах экрана.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// Задача, представляющая асинхронную операцию. Результат содержит эффекты, примененные при завершении операции.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ObjectDisposedException">Выбрасывается, если сервис был удален.</exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот метод следует вызывать при отпускании кнопки мыши или завершении жеста перетаскивания.
|
|
||||||
/// </remarks>
|
|
||||||
Task<Enums.DragDropEffects> EndDragAsync(Geometry.Point position);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Отменяет текущую операцию перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Задача, представляющая асинхронную операцию.</returns>
|
|
||||||
/// <exception cref="ObjectDisposedException">Выбрасывается, если сервис был удален.</exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот метод следует вызывать при отмене операции пользователем (например, нажатием клавиши Escape)
|
|
||||||
/// или при возникновении ошибки.
|
|
||||||
/// </remarks>
|
|
||||||
Task CancelDragAsync();
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Утилиты
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Очищает все зарегистрированные цели сброса.
|
|
||||||
/// </summary>
|
|
||||||
/// <exception cref="ObjectDisposedException">Выбрасывается, если сервис был удален.</exception>
|
|
||||||
void ClearAllDropTargets();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает статистику использования системы перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Объект <see cref="DragDropStats"/> со статистикой использования.</returns>
|
|
||||||
DragDropStats GetStats();
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Содержит статистику использования системы перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public class DragDropStats
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает общее количество операций перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public int TotalDragOperations { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает количество успешных сбросов.
|
|
||||||
/// </summary>
|
|
||||||
public int SuccessfulDrops { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает количество отмененных операций.
|
|
||||||
/// </summary>
|
|
||||||
public int CancelledOperations { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает количество ошибок.
|
|
||||||
/// </summary>
|
|
||||||
public int ErrorCount { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает количество зарегистрированных целей сброса.
|
|
||||||
/// </summary>
|
|
||||||
public int RegisteredTargets { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает среднее время операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan AverageOperationTime { get; set; }
|
|
||||||
}
|
|
||||||
15
Lattice.Example.DragDrop/App.xaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<Application
|
||||||
|
x:Class="Lattice.Example.DragDrop.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="using:Lattice.Example.DragDrop">
|
||||||
|
|
||||||
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<!-- Fluent Theme Resources -->
|
||||||
|
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
33
Lattice.Example.DragDrop/App.xaml.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using Lattice.Themes;
|
||||||
|
using Lattice.Themes.Fluent;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
|
||||||
|
namespace Lattice.Example.DragDrop;
|
||||||
|
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
private Window? _window;
|
||||||
|
|
||||||
|
public App()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||||
|
{
|
||||||
|
// Регистрируем Fluent тему
|
||||||
|
var themeManager = ThemeManager.Current;
|
||||||
|
themeManager.RegisterTheme(new FluentThemePack(false)); // Light тема
|
||||||
|
themeManager.RegisterTheme(new FluentThemePack(true)); // Dark тема
|
||||||
|
|
||||||
|
// Применяем тему по умолчанию
|
||||||
|
themeManager.ApplyTheme("Fluent Dark");
|
||||||
|
|
||||||
|
// Создаем главное окно
|
||||||
|
_window = new MainWindow();
|
||||||
|
_window.Activate();
|
||||||
|
|
||||||
|
// Регистрируем окно в трекере
|
||||||
|
WindowTracker.Register(_window);
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Lattice.Example.DragDrop/Assets/LockScreenLogo.scale-200.png
Normal file
|
After Width: | Height: | Size: 432 B |
BIN
Lattice.Example.DragDrop/Assets/SplashScreen.scale-200.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
Lattice.Example.DragDrop/Assets/Square150x150Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
Lattice.Example.DragDrop/Assets/Square44x44Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 637 B |
|
After Width: | Height: | Size: 283 B |
BIN
Lattice.Example.DragDrop/Assets/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 456 B |
BIN
Lattice.Example.DragDrop/Assets/Wide310x150Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
64
Lattice.Example.DragDrop/Lattice.Example.DragDrop.csproj
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||||
|
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||||
|
<RootNamespace>Lattice.Example.DragDrop</RootNamespace>
|
||||||
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
<Platforms>x86;x64;ARM64</Platforms>
|
||||||
|
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
|
||||||
|
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
|
||||||
|
<UseWinUI>true</UseWinUI>
|
||||||
|
<WinUISDKReferences>false</WinUISDKReferences>
|
||||||
|
<EnableMsixTooling>true</EnableMsixTooling>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="Assets\SplashScreen.scale-200.png" />
|
||||||
|
<Content Include="Assets\LockScreenLogo.scale-200.png" />
|
||||||
|
<Content Include="Assets\Square150x150Logo.scale-200.png" />
|
||||||
|
<Content Include="Assets\Square44x44Logo.scale-200.png" />
|
||||||
|
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
|
||||||
|
<Content Include="Assets\StoreLogo.png" />
|
||||||
|
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Manifest Include="$(ApplicationManifest)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
|
||||||
|
Tools extension to be activated for this project even if the Windows App SDK Nuget
|
||||||
|
package has not yet been restored.
|
||||||
|
-->
|
||||||
|
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||||
|
<ProjectCapability Include="Msix" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
|
||||||
|
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260101001" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Lattice.Themes.Core\Lattice.Themes.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\Lattice.Themes.Fluent\Lattice.Themes.Fluent.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
|
||||||
|
Explorer "Package and Publish" context menu entry to be enabled for this project even if
|
||||||
|
the Windows App SDK Nuget package has not yet been restored.
|
||||||
|
-->
|
||||||
|
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||||
|
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- Publish Properties -->
|
||||||
|
<PropertyGroup>
|
||||||
|
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
|
||||||
|
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
|
||||||
|
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
|
||||||
|
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
141
Lattice.Example.DragDrop/MainWindow.xaml
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<Window
|
||||||
|
x:Class="Lattice.Example.DragDrop.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:lattice="using:Lattice.UI.DragDrop.WinUI"
|
||||||
|
Title="Drag Drop Demo"
|
||||||
|
>
|
||||||
|
|
||||||
|
<Grid Background="#F0F2F5">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Инструкция -->
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
Text="Просто перетащите элементы справа влево!"
|
||||||
|
FontSize="14" Margin="20" HorizontalAlignment="Center"
|
||||||
|
FontWeight="SemiBold"/>
|
||||||
|
|
||||||
|
<!-- Основное содержимое -->
|
||||||
|
<Grid Grid.Row="1" Margin="20">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- ЦЕЛЕВАЯ ЗОНА (куда бросаем) -->
|
||||||
|
<Border Grid.Column="0"
|
||||||
|
Background="White"
|
||||||
|
CornerRadius="10"
|
||||||
|
BorderThickness="2"
|
||||||
|
BorderBrush="#4CAF50"
|
||||||
|
Padding="20"
|
||||||
|
Margin="0,0,10,0"
|
||||||
|
lattice:DragDropProperties.IsDropTarget="True">
|
||||||
|
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="🟢 ЦЕЛЕВАЯ ЗОНА"
|
||||||
|
FontSize="16" FontWeight="Bold"
|
||||||
|
Foreground="#4CAF50"
|
||||||
|
Margin="0,0,0,15"/>
|
||||||
|
|
||||||
|
<TextBlock x:Name="DropInfoText"
|
||||||
|
Text="Бросьте сюда элементы"
|
||||||
|
FontSize="14"
|
||||||
|
Foreground="#666"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- ЗОНА С ЭЛЕМЕНТАМИ (откуда тянем) -->
|
||||||
|
<StackPanel Grid.Column="1"
|
||||||
|
Background="White"
|
||||||
|
CornerRadius="10"
|
||||||
|
Padding="20"
|
||||||
|
Margin="10,0,0,0">
|
||||||
|
|
||||||
|
<TextBlock Text="📦 ЭЛЕМЕНТЫ ДЛЯ ПЕРЕТАСКИВАНИЯ"
|
||||||
|
FontSize="16" FontWeight="Bold"
|
||||||
|
Foreground="#2196F3"
|
||||||
|
Margin="0,0,0,15"/>
|
||||||
|
|
||||||
|
<!-- 1. TextBlock элемент -->
|
||||||
|
<Border Padding="15"
|
||||||
|
Background="#E3F2FD"
|
||||||
|
CornerRadius="8"
|
||||||
|
BorderThickness="1"
|
||||||
|
BorderBrush="#90CAF9"
|
||||||
|
Margin="0,0,0,10"
|
||||||
|
lattice:DragDropProperties.IsDragSource="True"
|
||||||
|
lattice:DragDropProperties.DragData="TextBlock Element">
|
||||||
|
|
||||||
|
<TextBlock Text="📝 Это TextBlock"
|
||||||
|
FontSize="14"
|
||||||
|
Foreground="#1565C0"
|
||||||
|
FontWeight="SemiBold"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 2. Border элемент -->
|
||||||
|
<Border Padding="15"
|
||||||
|
Background="#E8F5E9"
|
||||||
|
CornerRadius="8"
|
||||||
|
BorderThickness="1"
|
||||||
|
BorderBrush="#A5D6A7"
|
||||||
|
Margin="0,0,0,10"
|
||||||
|
lattice:DragDropProperties.IsDragSource="True"
|
||||||
|
lattice:DragDropProperties.DragData="Border Element">
|
||||||
|
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="🟩 Это Border"
|
||||||
|
FontSize="14"
|
||||||
|
Foreground="#2E7D32"
|
||||||
|
FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="С рамкой и заливкой"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="#666"
|
||||||
|
Margin="0,5,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 3. Grid элемент -->
|
||||||
|
<Border Padding="15"
|
||||||
|
Background="#FFF3E0"
|
||||||
|
CornerRadius="8"
|
||||||
|
BorderThickness="1"
|
||||||
|
BorderBrush="#FFCC80"
|
||||||
|
lattice:DragDropProperties.IsDragSource="True"
|
||||||
|
lattice:DragDropProperties.DragData="Grid Element">
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Иконка -->
|
||||||
|
<TextBlock Grid.Column="0"
|
||||||
|
Text="🔲"
|
||||||
|
FontSize="18"
|
||||||
|
Margin="0,0,10,0"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
|
||||||
|
<!-- Контент -->
|
||||||
|
<StackPanel Grid.Column="1">
|
||||||
|
<TextBlock Text="Это Grid"
|
||||||
|
FontSize="14"
|
||||||
|
Foreground="#EF6C00"
|
||||||
|
FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="С несколькими колонками"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="#666"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
17
Lattice.Example.DragDrop/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using Lattice.UI.DragDrop.WinUI;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
|
||||||
|
namespace Lattice.Example.DragDrop;
|
||||||
|
|
||||||
|
public sealed partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
private bool _isInitialized = false;
|
||||||
|
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
XamlInitializer.Initialize(this);
|
||||||
|
Title = "✅ Drag & Drop работает! Перетащите элементы →";
|
||||||
|
}
|
||||||
|
}
|
||||||
51
Lattice.Example.DragDrop/Package.appxmanifest
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<Package
|
||||||
|
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||||
|
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
|
||||||
|
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||||
|
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||||
|
IgnorableNamespaces="uap rescap">
|
||||||
|
|
||||||
|
<Identity
|
||||||
|
Name="dcfd6640-86d9-4ce7-bc17-24685f01b577"
|
||||||
|
Publisher="CN=frost"
|
||||||
|
Version="1.0.0.0" />
|
||||||
|
|
||||||
|
<mp:PhoneIdentity PhoneProductId="dcfd6640-86d9-4ce7-bc17-24685f01b577" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||||
|
|
||||||
|
<Properties>
|
||||||
|
<DisplayName>Lattice.Example.DragDrop</DisplayName>
|
||||||
|
<PublisherDisplayName>frost</PublisherDisplayName>
|
||||||
|
<Logo>Assets\StoreLogo.png</Logo>
|
||||||
|
</Properties>
|
||||||
|
|
||||||
|
<Dependencies>
|
||||||
|
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||||
|
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||||
|
</Dependencies>
|
||||||
|
|
||||||
|
<Resources>
|
||||||
|
<Resource Language="x-generate"/>
|
||||||
|
</Resources>
|
||||||
|
|
||||||
|
<Applications>
|
||||||
|
<Application Id="App"
|
||||||
|
Executable="$targetnametoken$.exe"
|
||||||
|
EntryPoint="$targetentrypoint$">
|
||||||
|
<uap:VisualElements
|
||||||
|
DisplayName="Lattice.Example.DragDrop"
|
||||||
|
Description="Lattice.Example.DragDrop"
|
||||||
|
BackgroundColor="transparent"
|
||||||
|
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||||
|
Square44x44Logo="Assets\Square44x44Logo.png">
|
||||||
|
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
|
||||||
|
<uap:SplashScreen Image="Assets\SplashScreen.png" />
|
||||||
|
</uap:VisualElements>
|
||||||
|
</Application>
|
||||||
|
</Applications>
|
||||||
|
|
||||||
|
<Capabilities>
|
||||||
|
<rescap:Capability Name="runFullTrust" />
|
||||||
|
</Capabilities>
|
||||||
|
</Package>
|
||||||
10
Lattice.Example.DragDrop/Properties/launchSettings.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"Lattice.Example.DragDrop (Package)": {
|
||||||
|
"commandName": "MsixPackage"
|
||||||
|
},
|
||||||
|
"Lattice.Example.DragDrop (Unpackaged)": {
|
||||||
|
"commandName": "Project"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
Lattice.Example.DragDrop/app.manifest
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="Lattice.Example.DragDrop.app"/>
|
||||||
|
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
|
||||||
|
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
|
||||||
|
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
</assembly>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Lattice.Themes.Core.Tokens;
|
namespace Lattice.Themes.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Статические ключи для ресурсов Lattice Framework.
|
/// Статические ключи для ресурсов Lattice Framework.
|
||||||
|
|||||||
@@ -1,22 +1,32 @@
|
|||||||
using Lattice.Themes.Core.Tokens;
|
using Lattice.Themes.Core;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
|
||||||
using Microsoft.UI.Xaml.Media;
|
|
||||||
|
|
||||||
namespace Lattice.Themes;
|
namespace Lattice.Themes;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Менеджер тем для Lattice Framework.
|
/// Менеджер тем для Lattice Framework. Управляет регистрацией, применением и переключением тем оформления.
|
||||||
|
/// Предоставляет доступ к токенам темы и поддерживает динамическое обновление UI при смене темы.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ThemeManager
|
public sealed class ThemeManager
|
||||||
{
|
{
|
||||||
public static ThemeManager Current { get; } = new();
|
private static readonly ThemeManager _instance = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает текущий экземпляр менеджера тем (синглтон).
|
||||||
|
/// </summary>
|
||||||
|
public static ThemeManager Current => _instance;
|
||||||
|
|
||||||
private ThemePack? _currentTheme;
|
private ThemePack? _currentTheme;
|
||||||
private readonly Dictionary<string, ThemePack> _registeredThemes = new();
|
private readonly Dictionary<string, ThemePack> _registeredThemes = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает текущую активную тему.
|
||||||
|
/// </summary>
|
||||||
public ThemePack? CurrentTheme => _currentTheme;
|
public ThemePack? CurrentTheme => _currentTheme;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Происходит при изменении текущей темы.
|
||||||
|
/// </summary>
|
||||||
public event EventHandler<ThemeChangedEventArgs>? ThemeChanged;
|
public event EventHandler<ThemeChangedEventArgs>? ThemeChanged;
|
||||||
|
|
||||||
private ThemeManager() { }
|
private ThemeManager() { }
|
||||||
@@ -24,6 +34,8 @@ public sealed class ThemeManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Регистрирует тему в менеджере.
|
/// Регистрирует тему в менеджере.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="theme">Тема для регистрации.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">Выбрасывается, если <paramref name="theme"/> равен null.</exception>
|
||||||
public void RegisterTheme(ThemePack theme)
|
public void RegisterTheme(ThemePack theme)
|
||||||
{
|
{
|
||||||
if (theme == null)
|
if (theme == null)
|
||||||
@@ -35,6 +47,8 @@ public sealed class ThemeManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает зарегистрированную тему по имени.
|
/// Получает зарегистрированную тему по имени.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="name">Имя темы.</param>
|
||||||
|
/// <returns>Зарегистрированная тема или null, если тема не найдена.</returns>
|
||||||
public ThemePack? GetTheme(string name)
|
public ThemePack? GetTheme(string name)
|
||||||
{
|
{
|
||||||
_registeredThemes.TryGetValue(name, out var theme);
|
_registeredThemes.TryGetValue(name, out var theme);
|
||||||
@@ -44,17 +58,18 @@ public sealed class ThemeManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает список всех зарегистрированных тем.
|
/// Получает список всех зарегистрированных тем.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <returns>Неизменяемая коллекция зарегистрированных тем.</returns>
|
||||||
public IReadOnlyCollection<ThemePack> GetRegisteredThemes()
|
public IReadOnlyCollection<ThemePack> GetRegisteredThemes()
|
||||||
{
|
{
|
||||||
return _registeredThemes.Values.ToList();
|
return _registeredThemes.Values.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получение информации о теме.
|
/// Получает информацию о зарегистрированной теме.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="themeName"></param>
|
/// <param name="themeName">Имя темы.</param>
|
||||||
/// <returns></returns>
|
/// <returns>Информация о теме или null, если тема не зарегистрирована.</returns>
|
||||||
public ThemeInfo GetThemeInfo(string themeName)
|
public ThemeInfo? GetThemeInfo(string themeName)
|
||||||
{
|
{
|
||||||
if (!_registeredThemes.TryGetValue(themeName, out var theme))
|
if (!_registeredThemes.TryGetValue(themeName, out var theme))
|
||||||
return null;
|
return null;
|
||||||
@@ -72,6 +87,9 @@ public sealed class ThemeManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Применяет тему по имени.
|
/// Применяет тему по имени.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="themeName">Имя темы для применения.</param>
|
||||||
|
/// <exception cref="ArgumentException">Выбрасывается, если тема с указанным именем не зарегистрирована.</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">Выбрасывается, если не удалось применить тему.</exception>
|
||||||
public void ApplyTheme(string themeName)
|
public void ApplyTheme(string themeName)
|
||||||
{
|
{
|
||||||
if (!_registeredThemes.TryGetValue(themeName, out var theme))
|
if (!_registeredThemes.TryGetValue(themeName, out var theme))
|
||||||
@@ -85,6 +103,9 @@ public sealed class ThemeManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Применяет указанную тему.
|
/// Применяет указанную тему.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="theme">Тема для применения.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">Выбрасывается, если <paramref name="theme"/> равен null.</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">Выбрасывается, если не удалось применить тему.</exception>
|
||||||
public void ApplyTheme(ThemePack theme)
|
public void ApplyTheme(ThemePack theme)
|
||||||
{
|
{
|
||||||
if (theme == null)
|
if (theme == null)
|
||||||
@@ -93,21 +114,21 @@ public sealed class ThemeManager
|
|||||||
if (_currentTheme == theme)
|
if (_currentTheme == theme)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var old = _currentTheme;
|
var oldTheme = _currentTheme;
|
||||||
_currentTheme = theme;
|
_currentTheme = theme;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ReplaceApplicationResources(theme);
|
ReplaceApplicationResources(theme);
|
||||||
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs(old!, theme));
|
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs(oldTheme!, theme));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// В случае ошибки возвращаемся к старой теме
|
// Восстанавливаем предыдущую тему при ошибке
|
||||||
_currentTheme = old;
|
_currentTheme = oldTheme;
|
||||||
if (old != null)
|
if (oldTheme != null)
|
||||||
{
|
{
|
||||||
ReplaceApplicationResources(old);
|
ReplaceApplicationResources(oldTheme);
|
||||||
}
|
}
|
||||||
throw new InvalidOperationException($"Failed to apply theme '{theme.Name}'.", ex);
|
throw new InvalidOperationException($"Failed to apply theme '{theme.Name}'.", ex);
|
||||||
}
|
}
|
||||||
@@ -116,22 +137,24 @@ public sealed class ThemeManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Загружает ресурсы темы в указанный словарь ресурсов.
|
/// Загружает ресурсы темы в указанный словарь ресурсов.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="targetDictionary">Целевой словарь ресурсов.</param>
|
||||||
|
/// <param name="theme">Тема, ресурсы которой нужно загрузить.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">Выбрасывается, если <paramref name="targetDictionary"/> или <paramref name="theme"/> равны null.</exception>
|
||||||
public void LoadThemeIntoDictionary(ResourceDictionary targetDictionary, ThemePack theme)
|
public void LoadThemeIntoDictionary(ResourceDictionary targetDictionary, ThemePack theme)
|
||||||
{
|
{
|
||||||
if (targetDictionary == null)
|
if (targetDictionary == null)
|
||||||
throw new ArgumentNullException(nameof(targetDictionary));
|
throw new ArgumentNullException(nameof(targetDictionary));
|
||||||
|
|
||||||
if (theme == null)
|
if (theme == null)
|
||||||
throw new ArgumentNullException(nameof(theme));
|
throw new ArgumentNullException(nameof(theme));
|
||||||
|
|
||||||
// Очищаем старые словари Lattice
|
// Удаляем все ThemeDictionary из словаря
|
||||||
for (int i = targetDictionary.MergedDictionaries.Count - 1; i >= 0; i--)
|
for (int i = targetDictionary.MergedDictionaries.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
if (targetDictionary.MergedDictionaries[i] is ThemeDictionary)
|
if (targetDictionary.MergedDictionaries[i] is ThemeDictionary)
|
||||||
targetDictionary.MergedDictionaries.RemoveAt(i);
|
targetDictionary.MergedDictionaries.RemoveAt(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем новые словари темы
|
// Добавляем словари темы
|
||||||
foreach (var uri in theme.GetResourceUris())
|
foreach (var uri in theme.GetResourceUris())
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -146,6 +169,11 @@ public sealed class ThemeManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Подсчитывает количество токенов в теме.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="theme">Тема для подсчета токенов.</param>
|
||||||
|
/// <returns>Количество токенов в теме. Возвращает 0 при возникновении ошибки.</returns>
|
||||||
private int CountTokensInTheme(ThemePack theme)
|
private int CountTokensInTheme(ThemePack theme)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -160,6 +188,10 @@ public sealed class ThemeManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Заменяет ресурсы приложения на ресурсы указанной темы.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="theme">Тема, ресурсы которой нужно применить.</param>
|
||||||
private void ReplaceApplicationResources(ThemePack theme)
|
private void ReplaceApplicationResources(ThemePack theme)
|
||||||
{
|
{
|
||||||
var app = Application.Current;
|
var app = Application.Current;
|
||||||
@@ -171,45 +203,32 @@ public sealed class ThemeManager
|
|||||||
ForceUpdateUI();
|
ForceUpdateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Принудительно обновляет пользовательский интерфейс после смены темы.
|
||||||
|
/// Использует легковесный подход без рекурсивного обхода дерева элементов.
|
||||||
|
/// </summary>
|
||||||
private void ForceUpdateUI()
|
private void ForceUpdateUI()
|
||||||
{
|
{
|
||||||
foreach (var window in WindowTracker.Windows)
|
foreach (var window in WindowTracker.Windows)
|
||||||
{
|
{
|
||||||
if (window.Content is FrameworkElement root)
|
if (window.Content is FrameworkElement root)
|
||||||
RefreshElement(root);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RefreshElement(FrameworkElement element)
|
|
||||||
{
|
|
||||||
var stack = new Stack<FrameworkElement>();
|
|
||||||
stack.Push(element);
|
|
||||||
|
|
||||||
while (stack.Count > 0)
|
|
||||||
{
|
|
||||||
var current = stack.Pop();
|
|
||||||
|
|
||||||
// Пересоздаём Template только у Control
|
|
||||||
if (current is Control control)
|
|
||||||
{
|
{
|
||||||
var template = control.Template;
|
// Перезагружаем ресурсы корневого элемента
|
||||||
control.Template = null;
|
var resources = root.Resources;
|
||||||
control.Template = template;
|
var currentTheme = _currentTheme;
|
||||||
}
|
if (currentTheme != null)
|
||||||
else if (current is ContentPresenter contentPresenter)
|
{
|
||||||
{
|
LoadThemeIntoDictionary(resources, currentTheme);
|
||||||
// Обновляем ContentPresenter
|
}
|
||||||
var content = contentPresenter.Content;
|
|
||||||
contentPresenter.Content = null;
|
|
||||||
contentPresenter.Content = content;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Добавляем детей в стек
|
// Принудительное обновление стилей через перезагрузку ResourceDictionary
|
||||||
int count = VisualTreeHelper.GetChildrenCount(current);
|
var mergedDictionaries = resources.MergedDictionaries;
|
||||||
for (int i = 0; i < count; i++)
|
if (mergedDictionaries.Count > 0)
|
||||||
{
|
{
|
||||||
if (VisualTreeHelper.GetChild(current, i) is FrameworkElement child)
|
var temp = mergedDictionaries[mergedDictionaries.Count - 1];
|
||||||
stack.Push(child);
|
mergedDictionaries.RemoveAt(mergedDictionaries.Count - 1);
|
||||||
|
mergedDictionaries.Add(temp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,6 +236,7 @@ public sealed class ThemeManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Проверяет, что все необходимые токены определены в текущей теме.
|
/// Проверяет, что все необходимые токены определены в текущей теме.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <returns>true, если все токены присутствуют; иначе false.</returns>
|
||||||
public bool ValidateThemeTokens()
|
public bool ValidateThemeTokens()
|
||||||
{
|
{
|
||||||
if (_currentTheme == null)
|
if (_currentTheme == null)
|
||||||
@@ -249,6 +269,8 @@ public sealed class ThemeManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает значение токена из текущей темы.
|
/// Получает значение токена из текущей темы.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="tokenKey">Ключ токена.</param>
|
||||||
|
/// <returns>Значение токена или null, если токен не найден или приложение не инициализировано.</returns>
|
||||||
public object? GetTokenValue(string tokenKey)
|
public object? GetTokenValue(string tokenKey)
|
||||||
{
|
{
|
||||||
var app = Application.Current;
|
var app = Application.Current;
|
||||||
@@ -266,6 +288,9 @@ public sealed class ThemeManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает значение токена с приведением к указанному типу.
|
/// Получает значение токена с приведением к указанному типу.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Тип, к которому приводится значение токена.</typeparam>
|
||||||
|
/// <param name="tokenKey">Ключ токена.</param>
|
||||||
|
/// <returns>Значение токена или значение по умолчанию для типа T, если токен не найден.</returns>
|
||||||
public T? GetTokenValue<T>(string tokenKey)
|
public T? GetTokenValue<T>(string tokenKey)
|
||||||
{
|
{
|
||||||
object? value = GetTokenValue(tokenKey);
|
object? value = GetTokenValue(tokenKey);
|
||||||
@@ -278,16 +303,32 @@ public sealed class ThemeManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Информация о теме.
|
/// Предоставляет информацию о теме оформления.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ThemeInfo
|
public class ThemeInfo
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Название темы.
|
/// Получает или задает название темы.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает описание темы.
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает версию темы.
|
||||||
|
/// </summary>
|
||||||
|
public string Version { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает значение, указывающее, является ли тема темной.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Name { get; set; }
|
|
||||||
public string Description { get; set; }
|
|
||||||
public string Version { get; set; }
|
|
||||||
public bool IsDark { get; set; }
|
public bool IsDark { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает количество токенов в теме.
|
||||||
|
/// </summary>
|
||||||
public int TokenCount { get; set; }
|
public int TokenCount { get; set; }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using Lattice.UI.Docking.Abstractions;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
|
||||||
|
namespace Lattice.UI.Docking.WinUI.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Интерфейс для элементов, поддерживающих WinUI Drag & Drop.
|
||||||
|
/// Наследуется от IDockControl и добавляет WinUI-специфичные возможности.
|
||||||
|
/// </summary>
|
||||||
|
public interface IWinUIDragDropControl : IDockControl
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получает UI-элемент для операций Drag & Drop.
|
||||||
|
/// </summary>
|
||||||
|
FrameworkElement? DragDropElement { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настраивает обработчики Drag & Drop.
|
||||||
|
/// </summary>
|
||||||
|
void SetupDragDropHandlers();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Начинает операцию перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
void StartDrag();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Завершает операцию перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
void EndDrag();
|
||||||
|
}
|
||||||
189
Lattice.UI.Docking.WinUI/Controls/AdvancedTabControl.cs
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
using Lattice.Core.Docking.Models;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace Lattice.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Представляет расширенный контрол вкладок с поддержкой всех позиций размещения панели вкладок.
|
||||||
|
/// Обеспечивает отображение коллекции вкладок с возможностью навигации, закрытия и изменения порядка.
|
||||||
|
/// Поддерживает четыре позиции размещения: сверху, снизу, слева и справа.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AdvancedTabControl : Control
|
||||||
|
{
|
||||||
|
private Grid? _rootGrid;
|
||||||
|
private TabView? _tabView;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="AdvancedTabControl"/>.
|
||||||
|
/// </summary>
|
||||||
|
public AdvancedTabControl()
|
||||||
|
{
|
||||||
|
DefaultStyleKey = typeof(AdvancedTabControl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Идентифицирует свойство зависимостей <see cref="ItemsSource"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly DependencyProperty ItemsSourceProperty =
|
||||||
|
DependencyProperty.Register(nameof(ItemsSource), typeof(ObservableCollection<object>),
|
||||||
|
typeof(AdvancedTabControl), new PropertyMetadata(null));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Идентифицирует свойство зависимостей <see cref="SelectedItem"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly DependencyProperty SelectedItemProperty =
|
||||||
|
DependencyProperty.Register(nameof(SelectedItem), typeof(object),
|
||||||
|
typeof(AdvancedTabControl), new PropertyMetadata(null));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Идентифицирует свойство зависимостей <see cref="TabPlacement"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly DependencyProperty TabPlacementProperty =
|
||||||
|
DependencyProperty.Register(nameof(TabPlacement), typeof(TabPlacement),
|
||||||
|
typeof(AdvancedTabControl), new PropertyMetadata(TabPlacement.Top, OnTabPlacementChanged));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает источник данных для вкладок.
|
||||||
|
/// </summary>
|
||||||
|
public ObservableCollection<object> ItemsSource
|
||||||
|
{
|
||||||
|
get => (ObservableCollection<object>)GetValue(ItemsSourceProperty);
|
||||||
|
set => SetValue(ItemsSourceProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает выбранный элемент вкладки.
|
||||||
|
/// </summary>
|
||||||
|
public object SelectedItem
|
||||||
|
{
|
||||||
|
get => GetValue(SelectedItemProperty);
|
||||||
|
set => SetValue(SelectedItemProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает положение панели вкладок.
|
||||||
|
/// </summary>
|
||||||
|
public TabPlacement TabPlacement
|
||||||
|
{
|
||||||
|
get => (TabPlacement)GetValue(TabPlacementProperty);
|
||||||
|
set => SetValue(TabPlacementProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается при применении шаблона контрола.
|
||||||
|
/// </summary>
|
||||||
|
protected override void OnApplyTemplate()
|
||||||
|
{
|
||||||
|
base.OnApplyTemplate();
|
||||||
|
|
||||||
|
_rootGrid = GetTemplateChild("PART_RootGrid") as Grid;
|
||||||
|
_tabView = GetTemplateChild("PART_TabView") as TabView;
|
||||||
|
|
||||||
|
UpdateTabPlacement();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Обновляет положение панели вкладок в соответствии с текущим значением свойства <see cref="TabPlacement"/>.
|
||||||
|
/// </summary>
|
||||||
|
private void UpdateTabPlacement()
|
||||||
|
{
|
||||||
|
if (_rootGrid == null) return;
|
||||||
|
|
||||||
|
// Очищаем определения строк и столбцов
|
||||||
|
_rootGrid.RowDefinitions.Clear();
|
||||||
|
_rootGrid.ColumnDefinitions.Clear();
|
||||||
|
|
||||||
|
switch (TabPlacement)
|
||||||
|
{
|
||||||
|
case TabPlacement.Top:
|
||||||
|
SetupTopPlacement();
|
||||||
|
break;
|
||||||
|
case TabPlacement.Bottom:
|
||||||
|
SetupBottomPlacement();
|
||||||
|
break;
|
||||||
|
case TabPlacement.Left:
|
||||||
|
SetupLeftPlacement();
|
||||||
|
break;
|
||||||
|
case TabPlacement.Right:
|
||||||
|
SetupRightPlacement();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настраивает размещение панели вкладок вверху.
|
||||||
|
/// </summary>
|
||||||
|
private void SetupTopPlacement()
|
||||||
|
{
|
||||||
|
_rootGrid!.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
||||||
|
_rootGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
|
||||||
|
|
||||||
|
if (_tabView != null)
|
||||||
|
{
|
||||||
|
Grid.SetRow(_tabView, 0);
|
||||||
|
Grid.SetColumn(_tabView, 0);
|
||||||
|
Grid.SetRowSpan(_tabView, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настраивает размещение панели вкладок внизу.
|
||||||
|
/// </summary>
|
||||||
|
private void SetupBottomPlacement()
|
||||||
|
{
|
||||||
|
_rootGrid!.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
|
||||||
|
_rootGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
||||||
|
|
||||||
|
if (_tabView != null)
|
||||||
|
{
|
||||||
|
Grid.SetRow(_tabView, 1);
|
||||||
|
Grid.SetColumn(_tabView, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настраивает размещение панели вкладок слева.
|
||||||
|
/// </summary>
|
||||||
|
private void SetupLeftPlacement()
|
||||||
|
{
|
||||||
|
_rootGrid!.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||||
|
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||||
|
|
||||||
|
if (_tabView != null)
|
||||||
|
{
|
||||||
|
// Для вертикального размещения требуется специальный стиль
|
||||||
|
_tabView.Style = Application.Current.Resources["VerticalTabViewStyle"] as Style;
|
||||||
|
Grid.SetRow(_tabView, 0);
|
||||||
|
Grid.SetColumn(_tabView, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настраивает размещение панели вкладок справа.
|
||||||
|
/// </summary>
|
||||||
|
private void SetupRightPlacement()
|
||||||
|
{
|
||||||
|
_rootGrid!.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||||
|
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||||
|
|
||||||
|
if (_tabView != null)
|
||||||
|
{
|
||||||
|
_tabView.Style = Application.Current.Resources["VerticalTabViewStyle"] as Style;
|
||||||
|
Grid.SetRow(_tabView, 0);
|
||||||
|
Grid.SetColumn(_tabView, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Обрабатывает изменение значения свойства <see cref="TabPlacement"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="d">Объект зависимости, значение которого изменилось.</param>
|
||||||
|
/// <param name="e">Данные о изменении свойства.</param>
|
||||||
|
private static void OnTabPlacementChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (d is AdvancedTabControl control)
|
||||||
|
control.UpdateTabPlacement();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
using Lattice.Core.Docking.Engine;
|
using Lattice.Core.Docking.Engine;
|
||||||
using Lattice.Core.Docking.Models;
|
using Lattice.Core.Docking.Models;
|
||||||
using Lattice.UI.Docking.Abstractions;
|
using Lattice.UI.Docking.Abstractions;
|
||||||
using Lattice.UI.Docking.Services;
|
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using System;
|
using System;
|
||||||
@@ -11,10 +10,6 @@ using System.Runtime.CompilerServices;
|
|||||||
|
|
||||||
namespace Lattice.UI;
|
namespace Lattice.UI;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Визуальный контрол для отображения группы разделения (сплиттера).
|
|
||||||
/// Реализует интерфейс <see cref="IDockGroupControl"/> для интеграции с системой докинга.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
||||||
{
|
{
|
||||||
private readonly PropertyChangedEventHandler _modelPropertyChangedHandler;
|
private readonly PropertyChangedEventHandler _modelPropertyChangedHandler;
|
||||||
@@ -24,18 +19,12 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
private ContentControl? _firstChildControl;
|
private ContentControl? _firstChildControl;
|
||||||
private ContentControl? _secondChildControl;
|
private ContentControl? _secondChildControl;
|
||||||
private LayoutManager? _layoutManager;
|
private LayoutManager? _layoutManager;
|
||||||
private DockDragDropService? _dragDropService;
|
|
||||||
private IDockContextManager? _contextManager;
|
private IDockContextManager? _contextManager;
|
||||||
private bool _isSelected;
|
private bool _isSelected;
|
||||||
private bool _isActive;
|
private bool _isActive;
|
||||||
private bool _canDrag = true;
|
|
||||||
private bool _canDrop = true;
|
|
||||||
private double _splitRatio = 0.5;
|
private double _splitRatio = 0.5;
|
||||||
private double _splitterSize = 4.0;
|
private double _splitterSize = 4.0;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="LatticeDockGroup"/>.
|
|
||||||
/// </summary>
|
|
||||||
public LatticeDockGroup()
|
public LatticeDockGroup()
|
||||||
{
|
{
|
||||||
this.DefaultStyleKey = typeof(LatticeDockGroup);
|
this.DefaultStyleKey = typeof(LatticeDockGroup);
|
||||||
@@ -43,7 +32,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
this.DataContextChanged += OnDataContextChanged;
|
this.DataContextChanged += OnDataContextChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public IDockElement? Model
|
public IDockElement? Model
|
||||||
{
|
{
|
||||||
get => _model;
|
get => _model;
|
||||||
@@ -57,7 +45,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public LayoutManager? LayoutManager
|
public LayoutManager? LayoutManager
|
||||||
{
|
{
|
||||||
get => _layoutManager;
|
get => _layoutManager;
|
||||||
@@ -69,19 +56,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public IDockDragDropService? DragDropService
|
|
||||||
{
|
|
||||||
get => _dragDropService;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_dragDropService == value) return;
|
|
||||||
_dragDropService = value;
|
|
||||||
OnPropertyChanged(nameof(DragDropService));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public IDockContextManager? ContextManager
|
public IDockContextManager? ContextManager
|
||||||
{
|
{
|
||||||
get => _contextManager;
|
get => _contextManager;
|
||||||
@@ -93,7 +67,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public bool IsSelected
|
public bool IsSelected
|
||||||
{
|
{
|
||||||
get => _isSelected;
|
get => _isSelected;
|
||||||
@@ -105,7 +78,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public bool IsActive
|
public bool IsActive
|
||||||
{
|
{
|
||||||
get => _isActive;
|
get => _isActive;
|
||||||
@@ -117,31 +89,9 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
public bool CanDrag => true;
|
||||||
public bool CanDrag
|
public bool CanDrop => true;
|
||||||
{
|
|
||||||
get => _canDrag;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_canDrag == value) return;
|
|
||||||
_canDrag = value;
|
|
||||||
OnPropertyChanged(nameof(CanDrag));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public bool CanDrop
|
|
||||||
{
|
|
||||||
get => _canDrop;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_canDrop == value) return;
|
|
||||||
_canDrop = value;
|
|
||||||
OnPropertyChanged(nameof(CanDrop));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public SplitDirection Orientation
|
public SplitDirection Orientation
|
||||||
{
|
{
|
||||||
get => _model?.Orientation ?? SplitDirection.Horizontal;
|
get => _model?.Orientation ?? SplitDirection.Horizontal;
|
||||||
@@ -155,7 +105,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public double SplitRatio
|
public double SplitRatio
|
||||||
{
|
{
|
||||||
get => _splitRatio;
|
get => _splitRatio;
|
||||||
@@ -166,14 +115,12 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
_splitRatio = value;
|
_splitRatio = value;
|
||||||
UpdateLayoutDefinitions();
|
UpdateLayoutDefinitions();
|
||||||
OnPropertyChanged(nameof(SplitRatio));
|
OnPropertyChanged(nameof(SplitRatio));
|
||||||
|
|
||||||
SplitRatioChanged?.Invoke(this,
|
SplitRatioChanged?.Invoke(this,
|
||||||
new SplitRatioChangedEventArgs(value, SplitRatioChangeSource.Programmatic));
|
new SplitRatioChangedEventArgs(value, SplitRatioChangeSource.Programmatic));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public double SplitterSize
|
public double SplitterSize
|
||||||
{
|
{
|
||||||
get => _splitterSize;
|
get => _splitterSize;
|
||||||
@@ -187,19 +134,12 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public IDockControl? FirstChild => _firstChildControl?.Content as IDockControl;
|
public IDockControl? FirstChild => _firstChildControl?.Content as IDockControl;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public IDockControl? SecondChild => _secondChildControl?.Content as IDockControl;
|
public IDockControl? SecondChild => _secondChildControl?.Content as IDockControl;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event EventHandler<SplitRatioChangedEventArgs>? SplitRatioChanged;
|
public event EventHandler<SplitRatioChangedEventArgs>? SplitRatioChanged;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
protected override void OnApplyTemplate()
|
protected override void OnApplyTemplate()
|
||||||
{
|
{
|
||||||
base.OnApplyTemplate();
|
base.OnApplyTemplate();
|
||||||
@@ -222,8 +162,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
{
|
{
|
||||||
_model.PropertyChanged += _modelPropertyChangedHandler;
|
_model.PropertyChanged += _modelPropertyChangedHandler;
|
||||||
this.DataContext = _model;
|
this.DataContext = _model;
|
||||||
|
|
||||||
// Инициализируем свойства из модели
|
|
||||||
_splitRatio = _model.SplitRatio;
|
_splitRatio = _model.SplitRatio;
|
||||||
UpdateLayoutDefinitions();
|
UpdateLayoutDefinitions();
|
||||||
}
|
}
|
||||||
@@ -267,7 +205,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
|
|
||||||
if (_model.Orientation == SplitDirection.Horizontal)
|
if (_model.Orientation == SplitDirection.Horizontal)
|
||||||
{
|
{
|
||||||
// Горизонтальное разделение
|
|
||||||
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition
|
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition
|
||||||
{ Width = new GridLength(_model.SplitRatio, GridUnitType.Star) });
|
{ Width = new GridLength(_model.SplitRatio, GridUnitType.Star) });
|
||||||
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition
|
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition
|
||||||
@@ -275,7 +212,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition
|
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition
|
||||||
{ Width = new GridLength(1 - _model.SplitRatio, GridUnitType.Star) });
|
{ Width = new GridLength(1 - _model.SplitRatio, GridUnitType.Star) });
|
||||||
|
|
||||||
// Устанавливаем позиции элементов
|
|
||||||
if (_firstChildControl != null)
|
if (_firstChildControl != null)
|
||||||
{
|
{
|
||||||
Grid.SetColumn(_firstChildControl, 0);
|
Grid.SetColumn(_firstChildControl, 0);
|
||||||
@@ -290,7 +226,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Вертикальное разделение
|
|
||||||
_rootGrid.RowDefinitions.Add(new RowDefinition
|
_rootGrid.RowDefinitions.Add(new RowDefinition
|
||||||
{ Height = new GridLength(_model.SplitRatio, GridUnitType.Star) });
|
{ Height = new GridLength(_model.SplitRatio, GridUnitType.Star) });
|
||||||
_rootGrid.RowDefinitions.Add(new RowDefinition
|
_rootGrid.RowDefinitions.Add(new RowDefinition
|
||||||
@@ -298,7 +233,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
_rootGrid.RowDefinitions.Add(new RowDefinition
|
_rootGrid.RowDefinitions.Add(new RowDefinition
|
||||||
{ Height = new GridLength(1 - _model.SplitRatio, GridUnitType.Star) });
|
{ Height = new GridLength(1 - _model.SplitRatio, GridUnitType.Star) });
|
||||||
|
|
||||||
// Устанавливаем позиции элементов
|
|
||||||
if (_firstChildControl != null)
|
if (_firstChildControl != null)
|
||||||
{
|
{
|
||||||
Grid.SetRow(_firstChildControl, 0);
|
Grid.SetRow(_firstChildControl, 0);
|
||||||
@@ -313,7 +247,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void SetChildren(IDockControl? firstChild, IDockControl? secondChild)
|
public void SetChildren(IDockControl? firstChild, IDockControl? secondChild)
|
||||||
{
|
{
|
||||||
if (_firstChildControl != null)
|
if (_firstChildControl != null)
|
||||||
@@ -325,23 +258,27 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
UpdateLayoutDefinitions();
|
UpdateLayoutDefinitions();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
public object? PrepareDragData()
|
||||||
|
{
|
||||||
|
return Model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HandleDrop(object data, DockPosition position)
|
||||||
|
{
|
||||||
|
// TODO: Реализовать обработку сброса
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public void Refresh()
|
public void Refresh()
|
||||||
{
|
{
|
||||||
UpdateLayoutDefinitions();
|
UpdateLayoutDefinitions();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void ApplyTheme(IDockTheme theme)
|
public void ApplyTheme(IDockTheme theme)
|
||||||
{
|
{
|
||||||
// Применение темы к контролу
|
// TODO: Реализовать применение темы
|
||||||
if (theme != null)
|
|
||||||
{
|
|
||||||
// TODO: Реализовать применение темы к стилям контрола
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void OnModelPropertyChanged(string propertyName)
|
public void OnModelPropertyChanged(string propertyName)
|
||||||
{
|
{
|
||||||
if (_model != null)
|
if (_model != null)
|
||||||
@@ -355,7 +292,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (!_disposed)
|
if (!_disposed)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Lattice.Core.Docking.Models;
|
|||||||
using Lattice.UI.Docking.Abstractions;
|
using Lattice.UI.Docking.Abstractions;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.UI.Xaml.Media;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
@@ -12,25 +13,6 @@ using System.Runtime.CompilerServices;
|
|||||||
|
|
||||||
namespace Lattice.UI;
|
namespace Lattice.UI;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Представляет главный контейнер док-системы для WinUI, который служит корневым элементом
|
|
||||||
/// пользовательского интерфейса для размещения всех компонентов системы докинга.
|
|
||||||
/// Этот контрол управляет всем макетом приложения, включая основное дерево компоновки,
|
|
||||||
/// плавающие окна и автоскрываемые панели.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// <see cref="LatticeDockHost"/> является центральным координатором UI-слоя док-системы,
|
|
||||||
/// интегрирующим функциональность менеджера макета, системы перетаскивания и контекстных меню.
|
|
||||||
/// Он обеспечивает согласованное отображение всех элементов и обрабатывает пользовательские
|
|
||||||
/// взаимодействия на верхнем уровне.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Контрол реализует интерфейс <see cref="IDockHost"/> и предоставляет полный набор методов
|
|
||||||
/// для управления структурой док-системы, включая создание/закрытие плавающих окон и
|
|
||||||
/// добавление/удаление автоскрываемых панелей.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
||||||
{
|
{
|
||||||
private readonly PropertyChangedEventHandler _modelPropertyChangedHandler;
|
private readonly PropertyChangedEventHandler _modelPropertyChangedHandler;
|
||||||
@@ -39,7 +21,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
private IDockElement? _model;
|
private IDockElement? _model;
|
||||||
private LayoutManager? _layoutManager;
|
private LayoutManager? _layoutManager;
|
||||||
private IDockDragDropService? _dragDropService;
|
|
||||||
private IDockContextManager? _contextManager;
|
private IDockContextManager? _contextManager;
|
||||||
private bool _isSelected;
|
private bool _isSelected;
|
||||||
private bool _isActive;
|
private bool _isActive;
|
||||||
@@ -50,13 +31,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
private bool _showMenu = true;
|
private bool _showMenu = true;
|
||||||
private ContentControl? _rootContainer;
|
private ContentControl? _rootContainer;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="LatticeDockHost"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Конструктор устанавливает ключ стиля по умолчанию, инициализирует обработчик изменений модели
|
|
||||||
/// и подписывается на событие изменения контекста данных.
|
|
||||||
/// </remarks>
|
|
||||||
public LatticeDockHost()
|
public LatticeDockHost()
|
||||||
{
|
{
|
||||||
this.DefaultStyleKey = typeof(LatticeDockHost);
|
this.DefaultStyleKey = typeof(LatticeDockHost);
|
||||||
@@ -64,17 +38,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
this.DataContextChanged += OnDataContextChanged;
|
this.DataContextChanged += OnDataContextChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает модель данных, связанную с этим контролом.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Экземпляр, реализующий <see cref="IDockElement"/>, представляющий корневой элемент
|
|
||||||
/// дерева компоновки. Может быть null.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот элемент является корнем всего макета док-системы. При изменении этого свойства
|
|
||||||
/// происходит перестройка всего пользовательского интерфейса.
|
|
||||||
/// </remarks>
|
|
||||||
public IDockElement? Model
|
public IDockElement? Model
|
||||||
{
|
{
|
||||||
get => _model;
|
get => _model;
|
||||||
@@ -88,16 +51,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает менеджер макета, к которому принадлежит этот контрол.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="LayoutManager"/>, управляющий структурой док-системы.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Менеджер макета используется для выполнения операций с деревом компоновки
|
|
||||||
/// и координации изменений между различными элементами системы.
|
|
||||||
/// </remarks>
|
|
||||||
public LayoutManager? LayoutManager
|
public LayoutManager? LayoutManager
|
||||||
{
|
{
|
||||||
get => _layoutManager;
|
get => _layoutManager;
|
||||||
@@ -109,37 +62,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает сервис перетаскивания, используемый этим контролом.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Реализация <see cref="IDockDragDropService"/> для обработки операций перетаскивания.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Сервис перетаскивания обеспечивает взаимодействие с системой drag-and-drop,
|
|
||||||
/// включая визуальную обратную связь и обработку событий.
|
|
||||||
/// </remarks>
|
|
||||||
public IDockDragDropService? DragDropService
|
|
||||||
{
|
|
||||||
get => _dragDropService;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_dragDropService == value) return;
|
|
||||||
_dragDropService = value;
|
|
||||||
OnPropertyChanged(nameof(DragDropService));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает контекстный менеджер для этого контрола.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="IDockContextManager"/>, управляющий контекстными меню и действиями.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Контекстный менеджер используется для отображения меню, связанных с этим элементом,
|
|
||||||
/// и выполнения команд, доступных в текущем контексте.
|
|
||||||
/// </remarks>
|
|
||||||
public IDockContextManager? ContextManager
|
public IDockContextManager? ContextManager
|
||||||
{
|
{
|
||||||
get => _contextManager;
|
get => _contextManager;
|
||||||
@@ -151,16 +73,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает признак того, что контрол выбран.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// true, если контрол выбран; в противном случае — false.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Выделение контрола обычно визуально выделяет его границы или фон,
|
|
||||||
/// чтобы указать пользователю на активный элемент.
|
|
||||||
/// </remarks>
|
|
||||||
public bool IsSelected
|
public bool IsSelected
|
||||||
{
|
{
|
||||||
get => _isSelected;
|
get => _isSelected;
|
||||||
@@ -172,15 +84,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает признак того, что контрол активен.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// true, если контрол активен; в противном случае — false.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Активный контрол обычно получает фокус ввода и может обрабатывать команды клавиатуры.
|
|
||||||
/// </remarks>
|
|
||||||
public bool IsActive
|
public bool IsActive
|
||||||
{
|
{
|
||||||
get => _isActive;
|
get => _isActive;
|
||||||
@@ -192,16 +95,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает признак того, что контрол можно перетаскивать.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// true, если контрол можно перетаскивать; в противном случае — false.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот флаг влияет на возможность инициирования операции перетаскивания
|
|
||||||
/// при взаимодействии пользователя с этим контролом.
|
|
||||||
/// </remarks>
|
|
||||||
public bool CanDrag
|
public bool CanDrag
|
||||||
{
|
{
|
||||||
get => _canDrag;
|
get => _canDrag;
|
||||||
@@ -213,16 +106,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает признак того, что контрол может принимать сброс.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// true, если контрол может принимать сброс; в противном случае — false.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот флаг влияет на возможность завершения операции перетаскивания
|
|
||||||
/// сбросом данных на этот контрол.
|
|
||||||
/// </remarks>
|
|
||||||
public bool CanDrop
|
public bool CanDrop
|
||||||
{
|
{
|
||||||
get => _canDrop;
|
get => _canDrop;
|
||||||
@@ -234,42 +117,9 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает коллекцию контролов плавающих окон, связанных с этим хостом.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Коллекция объектов, реализующих <see cref="IFloatingWindowControl"/>,
|
|
||||||
/// представляющих все активные плавающие окна в системе.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Коллекция является наблюдаемой (ObservableCollection), что позволяет автоматически
|
|
||||||
/// обновлять пользовательский интерфейс при добавлении или удалении окон.
|
|
||||||
/// </remarks>
|
|
||||||
public IEnumerable<IFloatingWindowControl> FloatingWindows => _floatingWindows;
|
public IEnumerable<IFloatingWindowControl> FloatingWindows => _floatingWindows;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает коллекцию контролов автоскрываемых панелей, прикрепленных к краям окна.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Коллекция объектов, реализующих <see cref="IAutoHidePanelControl"/>,
|
|
||||||
/// представляющих автоскрываемые панели на разных сторонах окна.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Коллекция является наблюдаемой (ObservableCollection), что позволяет автоматически
|
|
||||||
/// обновлять пользовательский интерфейс при добавлении или удалении панелей.
|
|
||||||
/// </remarks>
|
|
||||||
public IEnumerable<IAutoHidePanelControl> AutoHidePanels => _autoHidePanels;
|
public IEnumerable<IAutoHidePanelControl> AutoHidePanels => _autoHidePanels;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает значение, указывающее, отображается ли панель инструментов (Toolbox).
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// true, если панель инструментов видима; в противном случае — false.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Панель инструментов обычно содержит элементы для быстрого доступа к командам
|
|
||||||
/// или создания новых компонентов в приложении.
|
|
||||||
/// </remarks>
|
|
||||||
public bool ShowToolbox
|
public bool ShowToolbox
|
||||||
{
|
{
|
||||||
get => _showToolbox;
|
get => _showToolbox;
|
||||||
@@ -281,16 +131,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает значение, указывающее, отображается ли строка состояния.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// true, если строка состояния видима; в противном случае — false.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Строка состояния обычно отображает текущий статус приложения,
|
|
||||||
/// информацию о выбранном элементе или прогресс выполнения операций.
|
|
||||||
/// </remarks>
|
|
||||||
public bool ShowStatusBar
|
public bool ShowStatusBar
|
||||||
{
|
{
|
||||||
get => _showStatusBar;
|
get => _showStatusBar;
|
||||||
@@ -302,15 +142,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает значение, указывающее, отображается ли главное меню приложения.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// true, если главное меню видимо; в противном случае — false.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Главное меню содержит основные команды приложения, организованные в иерархическую структуру.
|
|
||||||
/// </remarks>
|
|
||||||
public bool ShowMenu
|
public bool ShowMenu
|
||||||
{
|
{
|
||||||
get => _showMenu;
|
get => _showMenu;
|
||||||
@@ -322,41 +153,43 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Событие, возникающее при изменении структуры макета док-системы.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Может вызываться при добавлении/удалении элементов, изменении размеров,
|
|
||||||
/// создании/закрытии плавающих окон и других операциях, влияющих на компоновку.
|
|
||||||
/// </remarks>
|
|
||||||
public event EventHandler? LayoutChanged;
|
public event EventHandler? LayoutChanged;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Событие, возникающее при создании нового плавающего окна.
|
|
||||||
/// </summary>
|
|
||||||
public event EventHandler<FloatingWindowCreatedEventArgs>? FloatingWindowCreated;
|
public event EventHandler<FloatingWindowCreatedEventArgs>? FloatingWindowCreated;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Событие, возникающее при закрытии плавающего окна.
|
|
||||||
/// </summary>
|
|
||||||
public event EventHandler<FloatingWindowClosedEventArgs>? FloatingWindowClosed;
|
public event EventHandler<FloatingWindowClosedEventArgs>? FloatingWindowClosed;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Событие, возникающее при изменении значения свойства.
|
|
||||||
/// </summary>
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Вызывается при применении шаблона контрола.
|
public FrameworkElement? DragDropElement => this;
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
/// <inheritdoc/>
|
||||||
/// Метод получает ссылки на именованные части шаблона и обновляет отображение
|
public void SetupDragDropHandlers()
|
||||||
/// корневого содержимого в соответствии с текущим состоянием модели.
|
{
|
||||||
/// </remarks>
|
this.AllowDrop = true;
|
||||||
|
this.CanDrag = true;
|
||||||
|
|
||||||
|
// Настройка обработчиков для хоста
|
||||||
|
this.Drop += OnHostDrop;
|
||||||
|
this.DragOver += OnHostDragOver;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void StartDrag() { /* Реализация */ }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void EndDrag() { /* Реализация */ }
|
||||||
|
|
||||||
|
private void OnHostDragOver(object sender, DragEventArgs args)
|
||||||
|
{
|
||||||
|
args.AcceptedOperation = CanDrop ?
|
||||||
|
Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move :
|
||||||
|
Windows.ApplicationModel.DataTransfer.DataPackageOperation.None;
|
||||||
|
args.DragUIOverride.IsGlyphVisible = true;
|
||||||
|
args.DragUIOverride.Caption = "Закрепить здесь";
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnApplyTemplate()
|
protected override void OnApplyTemplate()
|
||||||
{
|
{
|
||||||
base.OnApplyTemplate();
|
base.OnApplyTemplate();
|
||||||
|
|
||||||
_rootContainer = GetTemplateChild("PART_RootContainer") as ContentControl;
|
_rootContainer = GetTemplateChild("PART_RootContainer") as ContentControl;
|
||||||
UpdateRootContent();
|
UpdateRootContent();
|
||||||
}
|
}
|
||||||
@@ -370,11 +203,8 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
{
|
{
|
||||||
if (_model != null && _layoutManager != null)
|
if (_model != null && _layoutManager != null)
|
||||||
{
|
{
|
||||||
// Подписываемся на события менеджера макета
|
|
||||||
_layoutManager.LayoutUpdated += OnLayoutUpdated;
|
_layoutManager.LayoutUpdated += OnLayoutUpdated;
|
||||||
_layoutManager.AutoHidePanelsChanged += OnAutoHidePanelsChanged;
|
_layoutManager.AutoHidePanelsChanged += OnAutoHidePanelsChanged;
|
||||||
|
|
||||||
// Устанавливаем DataContext
|
|
||||||
this.DataContext = _model;
|
this.DataContext = _model;
|
||||||
UpdateRootContent();
|
UpdateRootContent();
|
||||||
}
|
}
|
||||||
@@ -384,17 +214,14 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
{
|
{
|
||||||
if (_model != null && _layoutManager != null)
|
if (_model != null && _layoutManager != null)
|
||||||
{
|
{
|
||||||
// Отписываемся от событий
|
|
||||||
_layoutManager.LayoutUpdated -= OnLayoutUpdated;
|
_layoutManager.LayoutUpdated -= OnLayoutUpdated;
|
||||||
_layoutManager.AutoHidePanelsChanged -= OnAutoHidePanelsChanged;
|
_layoutManager.AutoHidePanelsChanged -= OnAutoHidePanelsChanged;
|
||||||
|
|
||||||
this.DataContext = null;
|
this.DataContext = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
// Обработка изменений модели
|
|
||||||
OnPropertyChanged(e.PropertyName);
|
OnPropertyChanged(e.PropertyName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,7 +233,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
|
|
||||||
private void OnAutoHidePanelsChanged(object? sender, EventArgs e)
|
private void OnAutoHidePanelsChanged(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
// Обновление автоскрываемых панелей
|
|
||||||
OnPropertyChanged(nameof(AutoHidePanels));
|
OnPropertyChanged(nameof(AutoHidePanels));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,8 +240,7 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
{
|
{
|
||||||
if (_rootContainer != null && _model != null && _layoutManager != null)
|
if (_rootContainer != null && _model != null && _layoutManager != null)
|
||||||
{
|
{
|
||||||
// Создаем дерево контролов через фабрику
|
var factory = Lattice.UI.Docking.LatticeUIFramework.ControlFactory;
|
||||||
var factory = LatticeUIFramework.ControlFactory;
|
|
||||||
if (factory != null)
|
if (factory != null)
|
||||||
{
|
{
|
||||||
var control = factory.CreateControlForElement(_model);
|
var control = factory.CreateControlForElement(_model);
|
||||||
@@ -424,157 +249,44 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает новое плавающее окно для размещения указанного элемента док-системы.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="element">
|
|
||||||
/// Элемент док-системы (группа или лист), который будет размещен в плавающем окне.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="title">Заголовок создаваемого окна.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// Экземпляр <see cref="IFloatingWindowControl"/>, представляющий созданное плавающее окно.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="element"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <exception cref="NotImplementedException">
|
|
||||||
/// Выбрасывается, так как метод еще не реализован.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Созданное окно может быть перемещено пользователем в любое место экрана,
|
|
||||||
/// изменено в размерах и обычно содержит стандартные элементы управления окном
|
|
||||||
/// (заголовок, кнопки закрытия/сворачивания).
|
|
||||||
/// </remarks>
|
|
||||||
public IFloatingWindowControl CreateFloatingWindow(IDockElement element, string title)
|
public IFloatingWindowControl CreateFloatingWindow(IDockElement element, string title)
|
||||||
{
|
{
|
||||||
if (element == null) throw new ArgumentNullException(nameof(element));
|
throw new NotImplementedException("Floating windows not implemented yet");
|
||||||
|
|
||||||
// TODO: Реализовать создание плавающего окна через фабрику
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Закрывает указанное плавающее окно и возвращает его содержимое в основной макет.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="window">
|
|
||||||
/// Плавающее окно, которое необходимо закрыть.
|
|
||||||
/// </param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="window"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// При закрытии плавающего окна его содержимое обычно возвращается в то место
|
|
||||||
/// в основном макете, откуда оно было извлечено, или в ближайшую допустимую позицию.
|
|
||||||
/// </remarks>
|
|
||||||
public void CloseFloatingWindow(IFloatingWindowControl window)
|
public void CloseFloatingWindow(IFloatingWindowControl window)
|
||||||
{
|
{
|
||||||
if (window == null) throw new ArgumentNullException(nameof(window));
|
|
||||||
|
|
||||||
if (_floatingWindows.Remove(window))
|
if (_floatingWindows.Remove(window))
|
||||||
{
|
{
|
||||||
FloatingWindowClosed?.Invoke(this, new FloatingWindowClosedEventArgs(window));
|
FloatingWindowClosed?.Invoke(this, new FloatingWindowClosedEventArgs(window));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Добавляет автоскрываемую панель с указанным содержимым к заданной стороне окна.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="content">
|
|
||||||
/// Контент, который будет отображаться в автоскрываемой панели.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="side">
|
|
||||||
/// Сторона окна, к которой будет прикреплена панель.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Экземпляр <see cref="IAutoHidePanelControl"/>, представляющий созданную панель.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="content"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <exception cref="InvalidOperationException">
|
|
||||||
/// Выбрасывается, если свойство <see cref="LayoutManager"/> не установлено.
|
|
||||||
/// </exception>
|
|
||||||
/// <exception cref="NotImplementedException">
|
|
||||||
/// Выбрасывается, так как метод еще не реализован.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Автоскрываемые панели полезны для инструментов, к которым нужен частый,
|
|
||||||
/// но не постоянный доступ, так как они экономят пространство экрана.
|
|
||||||
/// </remarks>
|
|
||||||
public IAutoHidePanelControl AddAutoHidePanel(Core.Docking.Abstractions.IDockContent content, DockSide side)
|
public IAutoHidePanelControl AddAutoHidePanel(Core.Docking.Abstractions.IDockContent content, DockSide side)
|
||||||
{
|
{
|
||||||
if (content == null) throw new ArgumentNullException(nameof(content));
|
|
||||||
|
|
||||||
if (_layoutManager != null)
|
if (_layoutManager != null)
|
||||||
{
|
{
|
||||||
var panel = _layoutManager.AddAutoHidePanel(content, side);
|
var panel = _layoutManager.AddAutoHidePanel(content, side);
|
||||||
|
throw new NotImplementedException("Auto-hide panels not implemented yet");
|
||||||
// TODO: Создать UI-контрол для автоскрываемой панели через фабрику
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new InvalidOperationException("LayoutManager is not set");
|
throw new InvalidOperationException("LayoutManager is not set");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Удаляет автоскрываемую панель из интерфейса.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="panel">
|
|
||||||
/// Автоскрываемая панель, которую необходимо удалить.
|
|
||||||
/// </param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="panel"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <exception cref="NotImplementedException">
|
|
||||||
/// Выбрасывается, так как метод еще не реализован.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// После удаления панели её содержимое обычно либо закрывается полностью,
|
|
||||||
/// либо преобразуется в обычную закрепленную панель, в зависимости от настроек.
|
|
||||||
/// </remarks>
|
|
||||||
public void RemoveAutoHidePanel(IAutoHidePanelControl panel)
|
public void RemoveAutoHidePanel(IAutoHidePanelControl panel)
|
||||||
{
|
{
|
||||||
if (panel == null) throw new ArgumentNullException(nameof(panel));
|
throw new NotImplementedException("Auto-hide panels not implemented yet");
|
||||||
|
|
||||||
// TODO: Реализовать удаление автоскрываемой панели
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public object? PrepareDragData() => Model;
|
||||||
/// Обновляет внешний вид контрола в соответствии с текущим состоянием модели.
|
public bool HandleDrop(object data, DockPosition position) => false;
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
public void Refresh() => UpdateRootContent();
|
||||||
/// Вызывает обновление корневого содержимого и всех дочерних элементов.
|
|
||||||
/// </remarks>
|
|
||||||
public void Refresh()
|
|
||||||
{
|
|
||||||
UpdateRootContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Применяет указанную тему к контролу.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="theme">Тема для применения.</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// В текущей реализации метод является заглушкой и должен быть расширен
|
|
||||||
/// для поддержки динамического изменения тем.
|
|
||||||
/// </remarks>
|
|
||||||
public void ApplyTheme(IDockTheme theme)
|
public void ApplyTheme(IDockTheme theme)
|
||||||
{
|
{
|
||||||
// Применение темы к контролу
|
// TODO: Реализовать применение темы
|
||||||
if (theme != null)
|
|
||||||
{
|
|
||||||
// TODO: Реализовать применение темы к стилям контрола
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывается при изменении состояния модели для обновления UI.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="propertyName">Имя изменившегося свойства модели.</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Перенаправляет вызов в обработчик изменений модели.
|
|
||||||
/// </remarks>
|
|
||||||
public void OnModelPropertyChanged(string propertyName)
|
public void OnModelPropertyChanged(string propertyName)
|
||||||
{
|
{
|
||||||
if (_model != null)
|
if (_model != null)
|
||||||
@@ -588,24 +300,84 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Освобождает ресурсы, используемые этим экземпляром контрола.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Выполняет отписку от событий модели, очистку коллекций и освобождение ресурсов.
|
|
||||||
/// </remarks>
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (!_disposed)
|
if (!_disposed)
|
||||||
{
|
{
|
||||||
DetachModel();
|
DetachModel();
|
||||||
|
|
||||||
// Очищаем коллекции
|
|
||||||
_floatingWindows.Clear();
|
_floatingWindows.Clear();
|
||||||
_autoHidePanels.Clear();
|
_autoHidePanels.Clear();
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DockPosition GetDropPosition(Windows.Foundation.Point point)
|
||||||
|
{
|
||||||
|
if (ActualWidth <= 0 || ActualHeight <= 0)
|
||||||
|
return DockPosition.Center;
|
||||||
|
|
||||||
|
var relativeX = point.X / ActualWidth;
|
||||||
|
var relativeY = point.Y / ActualHeight;
|
||||||
|
|
||||||
|
// Определяем регионы для докирования
|
||||||
|
const double edgeThreshold = 0.2; // 20% от краев
|
||||||
|
const double centerThreshold = 0.4; // Центральная область
|
||||||
|
|
||||||
|
// Проверяем края
|
||||||
|
if (relativeX < edgeThreshold) return DockPosition.Left;
|
||||||
|
if (relativeX > (1 - edgeThreshold)) return DockPosition.Right;
|
||||||
|
if (relativeY < edgeThreshold) return DockPosition.Top;
|
||||||
|
if (relativeY > (1 - edgeThreshold)) return DockPosition.Bottom;
|
||||||
|
|
||||||
|
// Если в центральной области
|
||||||
|
if (relativeX > centerThreshold && relativeX < (1 - centerThreshold) &&
|
||||||
|
relativeY > centerThreshold && relativeY < (1 - centerThreshold))
|
||||||
|
{
|
||||||
|
return DockPosition.Center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// По умолчанию - центр
|
||||||
|
return DockPosition.Center;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHostDrop(object sender, DragEventArgs args)
|
||||||
|
{
|
||||||
|
if (CanDrop && args.DataView.Properties.TryGetValue("LatticeDockElement", out var data))
|
||||||
|
{
|
||||||
|
// Получаем позицию сброса
|
||||||
|
var position = GetDropPosition(args.GetPosition(this));
|
||||||
|
|
||||||
|
// Определяем целевой элемент
|
||||||
|
IDockElement? target = null;
|
||||||
|
if (args.OriginalSource is FrameworkElement element)
|
||||||
|
{
|
||||||
|
// Находим соответствующий контрол докинга
|
||||||
|
var dockControl = FindDockControl(element);
|
||||||
|
target = dockControl?.Model;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если цель не найдена, используем корневой элемент
|
||||||
|
target ??= LayoutManager?.Root;
|
||||||
|
|
||||||
|
if (data is IDockElement source && target != null)
|
||||||
|
{
|
||||||
|
LayoutManager?.Move(source, target, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IDockControl? FindDockControl(FrameworkElement element)
|
||||||
|
{
|
||||||
|
// Поднимаемся по дереву элементов, чтобы найти контрол докинга
|
||||||
|
var current = element;
|
||||||
|
while (current != null)
|
||||||
|
{
|
||||||
|
if (current is IDockControl dockControl)
|
||||||
|
return dockControl;
|
||||||
|
|
||||||
|
current = VisualTreeHelper.GetParent(current) as FrameworkElement;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,40 +1,529 @@
|
|||||||
using Lattice.Core.Docking.Models;
|
using Lattice.Core.Docking.Abstractions;
|
||||||
|
using Lattice.Core.Docking.Engine;
|
||||||
|
using Lattice.Core.Docking.Models;
|
||||||
|
using Lattice.UI.Docking.Abstractions;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.UI.Xaml.Media;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Lattice.UI;
|
namespace Lattice.UI;
|
||||||
|
|
||||||
/// <summary>
|
public sealed class LatticeDockLeaf : Control, IDockLeafControl, IDisposable
|
||||||
/// Визуальное представление контейнера вкладок с поддержкой нижнего расположения.
|
|
||||||
/// </summary>
|
|
||||||
public class LatticeDockLeaf : Control
|
|
||||||
{
|
{
|
||||||
|
private readonly PropertyChangedEventHandler _modelPropertyChangedHandler;
|
||||||
|
private bool _disposed;
|
||||||
|
private DockLeaf? _model;
|
||||||
|
private Grid? _rootGrid;
|
||||||
|
private ListBox? _tabHeaderList;
|
||||||
|
private ContentControl? _contentControl;
|
||||||
|
private LayoutManager? _layoutManager;
|
||||||
|
private IDockContextManager? _contextManager;
|
||||||
|
private bool _isSelected;
|
||||||
|
private bool _isActive;
|
||||||
|
private TabPlacement _tabPlacement = TabPlacement.Top;
|
||||||
|
private bool _showCloseButtons = true;
|
||||||
|
private bool _canReorderTabs = true;
|
||||||
|
|
||||||
public LatticeDockLeaf()
|
public LatticeDockLeaf()
|
||||||
{
|
{
|
||||||
this.DefaultStyleKey = typeof(LatticeDockLeaf);
|
this.DefaultStyleKey = typeof(LatticeDockLeaf);
|
||||||
|
_modelPropertyChangedHandler = OnModelPropertyChanged;
|
||||||
|
this.DataContextChanged += OnDataContextChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IDockElement? Model
|
||||||
|
{
|
||||||
|
get => _model;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_model == value) return;
|
||||||
|
DetachModel();
|
||||||
|
_model = value as DockLeaf;
|
||||||
|
AttachModel();
|
||||||
|
OnPropertyChanged(nameof(Model));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public LayoutManager? LayoutManager
|
||||||
|
{
|
||||||
|
get => _layoutManager;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_layoutManager == value) return;
|
||||||
|
_layoutManager = value;
|
||||||
|
OnPropertyChanged(nameof(LayoutManager));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDockContextManager? ContextManager
|
||||||
|
{
|
||||||
|
get => _contextManager;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_contextManager == value) return;
|
||||||
|
_contextManager = value;
|
||||||
|
OnPropertyChanged(nameof(ContextManager));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSelected
|
||||||
|
{
|
||||||
|
get => _isSelected;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_isSelected == value) return;
|
||||||
|
_isSelected = value;
|
||||||
|
OnPropertyChanged(nameof(IsSelected));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsActive
|
||||||
|
{
|
||||||
|
get => _isActive;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_isActive == value) return;
|
||||||
|
_isActive = value;
|
||||||
|
OnPropertyChanged(nameof(IsActive));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanDrag => true;
|
||||||
|
public bool CanDrop => true;
|
||||||
|
|
||||||
|
public TabPlacement TabPlacement
|
||||||
|
{
|
||||||
|
get => _tabPlacement;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_tabPlacement != value)
|
||||||
|
{
|
||||||
|
_tabPlacement = value;
|
||||||
|
UpdateTabPlacement();
|
||||||
|
OnPropertyChanged(nameof(TabPlacement));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShowCloseButtons
|
||||||
|
{
|
||||||
|
get => _showCloseButtons;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_showCloseButtons != value)
|
||||||
|
{
|
||||||
|
_showCloseButtons = value;
|
||||||
|
OnPropertyChanged(nameof(ShowCloseButtons));
|
||||||
|
UpdateTabHeaders();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanReorderTabs
|
||||||
|
{
|
||||||
|
get => _canReorderTabs;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_canReorderTabs != value)
|
||||||
|
{
|
||||||
|
_canReorderTabs = value;
|
||||||
|
OnPropertyChanged(nameof(CanReorderTabs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDockContent? ActiveContent
|
||||||
|
{
|
||||||
|
get => _model?.ActiveContent;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_model != null)
|
||||||
|
{
|
||||||
|
_model.ActiveContent = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? PrepareDragData() => Model;
|
||||||
|
public bool HandleDrop(object data, DockPosition position) => false;
|
||||||
|
|
||||||
|
public event EventHandler<ActiveContentChangedEventArgs>? ActiveContentChanged;
|
||||||
|
public event EventHandler<ContentClosingEventArgs>? ContentClosing;
|
||||||
|
public event EventHandler<TabsReorderedEventArgs>? TabsReordered;
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
protected override void OnApplyTemplate()
|
protected override void OnApplyTemplate()
|
||||||
{
|
{
|
||||||
base.OnApplyTemplate();
|
base.OnApplyTemplate();
|
||||||
|
|
||||||
|
_rootGrid = GetTemplateChild("PART_RootGrid") as Grid;
|
||||||
|
_tabHeaderList = GetTemplateChild("PART_TabHeaderList") as ListBox;
|
||||||
|
_contentControl = GetTemplateChild("PART_ContentControl") as ContentControl;
|
||||||
|
|
||||||
|
if (_tabHeaderList != null)
|
||||||
|
{
|
||||||
|
_tabHeaderList.SelectionChanged += OnTabSelectionChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateTabPlacement();
|
||||||
|
UpdateTabHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
|
||||||
|
{
|
||||||
|
Model = args.NewValue as DockLeaf;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AttachModel()
|
||||||
|
{
|
||||||
|
if (_model != null)
|
||||||
|
{
|
||||||
|
_model.PropertyChanged += _modelPropertyChangedHandler;
|
||||||
|
|
||||||
|
if (_model.Children is INotifyCollectionChanged notifyCollection)
|
||||||
|
{
|
||||||
|
notifyCollection.CollectionChanged += OnChildrenCollectionChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.DataContext = _model;
|
||||||
|
_tabPlacement = _model.TabPlacement;
|
||||||
|
UpdateTabHeaders();
|
||||||
|
UpdateTabPlacement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DetachModel()
|
||||||
|
{
|
||||||
|
if (_model != null)
|
||||||
|
{
|
||||||
|
_model.PropertyChanged -= _modelPropertyChangedHandler;
|
||||||
|
|
||||||
|
if (_model.Children is INotifyCollectionChanged notifyCollection)
|
||||||
|
{
|
||||||
|
notifyCollection.CollectionChanged -= OnChildrenCollectionChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.DataContext = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
switch (e.PropertyName)
|
||||||
|
{
|
||||||
|
case nameof(DockLeaf.TabPlacement):
|
||||||
|
_tabPlacement = _model?.TabPlacement ?? TabPlacement.Top;
|
||||||
|
OnPropertyChanged(nameof(TabPlacement));
|
||||||
|
UpdateTabPlacement();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case nameof(DockLeaf.ActiveContent):
|
||||||
|
OnPropertyChanged(nameof(ActiveContent));
|
||||||
|
UpdateSelectedTab();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case nameof(DockLeaf.Children):
|
||||||
|
UpdateTabHeaders();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
UpdateTabHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTabPlacement()
|
||||||
|
{
|
||||||
|
if (_rootGrid == null || _model == null) return;
|
||||||
|
|
||||||
|
_rootGrid.RowDefinitions.Clear();
|
||||||
|
_rootGrid.ColumnDefinitions.Clear();
|
||||||
|
|
||||||
|
switch (_model.TabPlacement)
|
||||||
|
{
|
||||||
|
case TabPlacement.Top:
|
||||||
|
_rootGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
||||||
|
_rootGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
|
||||||
|
UpdateHeaderListOrientation(Orientation.Horizontal);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TabPlacement.Bottom:
|
||||||
|
_rootGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
|
||||||
|
_rootGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
||||||
|
UpdateHeaderListOrientation(Orientation.Horizontal);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TabPlacement.Left:
|
||||||
|
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||||
|
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||||
|
UpdateHeaderListOrientation(Orientation.Vertical);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TabPlacement.Right:
|
||||||
|
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||||
|
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||||
|
UpdateHeaderListOrientation(Orientation.Vertical);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateElementPositions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateHeaderListOrientation(Orientation orientation)
|
||||||
|
{
|
||||||
|
if (_tabHeaderList?.ItemsPanelRoot is StackPanel stackPanel)
|
||||||
|
{
|
||||||
|
stackPanel.Orientation = orientation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateElementPositions()
|
||||||
|
{
|
||||||
|
if (_rootGrid == null || _tabHeaderList == null || _contentControl == null) return;
|
||||||
|
|
||||||
|
switch (_model?.TabPlacement)
|
||||||
|
{
|
||||||
|
case TabPlacement.Top:
|
||||||
|
Grid.SetRow(_tabHeaderList, 0);
|
||||||
|
Grid.SetRow(_contentControl, 1);
|
||||||
|
Grid.SetColumn(_tabHeaderList, 0);
|
||||||
|
Grid.SetColumn(_contentControl, 0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TabPlacement.Bottom:
|
||||||
|
Grid.SetRow(_contentControl, 0);
|
||||||
|
Grid.SetRow(_tabHeaderList, 1);
|
||||||
|
Grid.SetColumn(_contentControl, 0);
|
||||||
|
Grid.SetColumn(_tabHeaderList, 0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TabPlacement.Left:
|
||||||
|
Grid.SetColumn(_tabHeaderList, 0);
|
||||||
|
Grid.SetColumn(_contentControl, 1);
|
||||||
|
Grid.SetRow(_tabHeaderList, 0);
|
||||||
|
Grid.SetRow(_contentControl, 0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TabPlacement.Right:
|
||||||
|
Grid.SetColumn(_contentControl, 0);
|
||||||
|
Grid.SetColumn(_tabHeaderList, 1);
|
||||||
|
Grid.SetRow(_contentControl, 0);
|
||||||
|
Grid.SetRow(_tabHeaderList, 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTabHeaders()
|
||||||
|
{
|
||||||
|
if (_tabHeaderList == null || _model == null) return;
|
||||||
|
|
||||||
|
_tabHeaderList.Items.Clear();
|
||||||
|
|
||||||
|
foreach (var content in _model.Children)
|
||||||
|
{
|
||||||
|
var item = CreateTabHeaderItem(content);
|
||||||
|
_tabHeaderList.Items.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateSelectedTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListBoxItem CreateTabHeaderItem(IDockContent content)
|
||||||
|
{
|
||||||
|
var item = new ListBoxItem
|
||||||
|
{
|
||||||
|
Content = CreateTabHeaderContent(content),
|
||||||
|
Tag = content,
|
||||||
|
HorizontalContentAlignment = HorizontalAlignment.Stretch,
|
||||||
|
VerticalContentAlignment = VerticalAlignment.Stretch
|
||||||
|
};
|
||||||
|
|
||||||
|
item.PointerPressed += (sender, e) =>
|
||||||
|
{
|
||||||
|
if (e.GetCurrentPoint(item).Properties.IsLeftButtonPressed)
|
||||||
|
{
|
||||||
|
ActiveContent = content;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UIElement CreateTabHeaderContent(IDockContent content)
|
||||||
|
{
|
||||||
|
var grid = new Grid();
|
||||||
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||||
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||||
|
|
||||||
|
var textBlock = new TextBlock
|
||||||
|
{
|
||||||
|
Text = content.Title,
|
||||||
|
Margin = new Thickness(8, 4, 8, 4),
|
||||||
|
VerticalAlignment = VerticalAlignment.Center
|
||||||
|
};
|
||||||
|
Grid.SetColumn(textBlock, 0);
|
||||||
|
grid.Children.Add(textBlock);
|
||||||
|
|
||||||
|
if (_showCloseButtons && content.CanClose)
|
||||||
|
{
|
||||||
|
var closeButton = new Button
|
||||||
|
{
|
||||||
|
Content = "×",
|
||||||
|
FontSize = 16,
|
||||||
|
Width = 24,
|
||||||
|
Height = 24,
|
||||||
|
Margin = new Thickness(2),
|
||||||
|
Padding = new Thickness(0),
|
||||||
|
Background = new SolidColorBrush(Microsoft.UI.Colors.Transparent),
|
||||||
|
BorderThickness = new Thickness(0)
|
||||||
|
};
|
||||||
|
|
||||||
|
closeButton.Click += (sender, e) =>
|
||||||
|
{
|
||||||
|
CloseContent(content);
|
||||||
|
};
|
||||||
|
|
||||||
|
Grid.SetColumn(closeButton, 1);
|
||||||
|
grid.Children.Add(closeButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSelectedTab()
|
||||||
|
{
|
||||||
|
if (_tabHeaderList == null || _model == null) return;
|
||||||
|
|
||||||
|
foreach (var item in _tabHeaderList.Items)
|
||||||
|
{
|
||||||
|
if (item is ListBoxItem listBoxItem && listBoxItem.Tag is IDockContent content)
|
||||||
|
{
|
||||||
|
listBoxItem.IsSelected = content == _model.ActiveContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_contentControl != null)
|
||||||
|
{
|
||||||
|
_contentControl.Content = _model.ActiveContent?.View;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTabSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_tabHeaderList?.SelectedItem is ListBoxItem selectedItem &&
|
||||||
|
selectedItem.Tag is IDockContent content)
|
||||||
|
{
|
||||||
|
var oldContent = ActiveContent;
|
||||||
|
ActiveContent = content;
|
||||||
|
|
||||||
|
if (oldContent != content)
|
||||||
|
{
|
||||||
|
ActiveContentChanged?.Invoke(this,
|
||||||
|
new ActiveContentChangedEventArgs(oldContent, content));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddContent(IDockContent content)
|
||||||
|
{
|
||||||
|
if (_model != null && !_model.Children.Contains(content))
|
||||||
|
{
|
||||||
|
_model.AddContent(content);
|
||||||
|
UpdateTabHeaders();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveContent(IDockContent content)
|
||||||
|
{
|
||||||
|
if (_model != null && _model.Children.Contains(content))
|
||||||
|
{
|
||||||
|
_model.RemoveContent(content);
|
||||||
|
UpdateTabHeaders();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CloseContent(IDockContent content)
|
||||||
|
{
|
||||||
|
var args = new ContentClosingEventArgs(content);
|
||||||
|
ContentClosing?.Invoke(this, args);
|
||||||
|
|
||||||
|
if (!args.Cancel)
|
||||||
|
{
|
||||||
|
RemoveContent(content);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CloseAllExcept(IDockContent exceptContent)
|
||||||
|
{
|
||||||
|
if (_model == null) return;
|
||||||
|
|
||||||
|
var itemsToClose = _model.Children
|
||||||
|
.Where(c => c != exceptContent)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var content in itemsToClose)
|
||||||
|
{
|
||||||
|
CloseContent(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CloseAll()
|
||||||
|
{
|
||||||
|
if (_model == null) return;
|
||||||
|
|
||||||
|
var itemsToClose = _model.Children.ToList();
|
||||||
|
foreach (var content in itemsToClose)
|
||||||
|
{
|
||||||
|
CloseContent(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Refresh()
|
||||||
|
{
|
||||||
|
UpdateTabHeaders();
|
||||||
UpdateTabPlacement();
|
UpdateTabPlacement();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public void ApplyTheme(IDockTheme theme)
|
||||||
/// Настраивает внутреннюю структуру TabView для отображения вкладок снизу.
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateTabPlacement()
|
|
||||||
{
|
{
|
||||||
var tabView = GetTemplateChild("PART_TabView") as TabView;
|
// TODO: Реализовать применение темы
|
||||||
if (tabView == null || DataContext is not DockLeaf leaf) return;
|
}
|
||||||
|
|
||||||
// Вместо сложной манипуляции с визуальным деревом, используем встроенные свойства TabView
|
public void OnModelPropertyChanged(string propertyName)
|
||||||
if (leaf.TabPlacement == TabPlacement.Bottom)
|
{
|
||||||
|
if (_model != null)
|
||||||
{
|
{
|
||||||
// К сожалению, TabView в WinUI не поддерживает TabStripPlacement
|
OnModelPropertyChanged(_model, new PropertyChangedEventArgs(propertyName));
|
||||||
// Это ограничение платформы, нужно либо использовать другой контрол,
|
}
|
||||||
// либо реализовать кастомный TabControl с поддержкой нижнего расположения
|
}
|
||||||
// Временно оставляем как есть с заглушкой
|
|
||||||
System.Diagnostics.Debug.WriteLine("TabPlacement.Bottom is not fully supported in WinUI TabView");
|
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
DetachModel();
|
||||||
|
|
||||||
|
if (_tabHeaderList != null)
|
||||||
|
{
|
||||||
|
_tabHeaderList.SelectionChanged -= OnTabSelectionChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,27 @@
|
|||||||
using Lattice.Core.Docking.Models;
|
using Lattice.Core.Docking.Abstractions;
|
||||||
|
using Lattice.Core.Docking.Engine;
|
||||||
|
using Lattice.Core.Docking.Models;
|
||||||
|
using Lattice.UI.Docking.Abstractions;
|
||||||
using Microsoft.UI.Input;
|
using Microsoft.UI.Input;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Microsoft.UI.Xaml.Input;
|
using Microsoft.UI.Xaml.Input;
|
||||||
using Microsoft.UI.Xaml.Media;
|
using Microsoft.UI.Xaml.Media;
|
||||||
using System;
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Lattice.UI;
|
namespace Lattice.UI;
|
||||||
|
|
||||||
public class LatticeSplitter : Control
|
public sealed class LatticeSplitter : Control, IDockSplitterControl, IDisposable
|
||||||
{
|
{
|
||||||
|
private bool _disposed;
|
||||||
|
private IDockElement? _model;
|
||||||
|
private LayoutManager? _layoutManager;
|
||||||
|
private IDockContextManager? _contextManager;
|
||||||
|
private bool _isSelected;
|
||||||
|
private bool _isActive;
|
||||||
|
private bool _isDragging;
|
||||||
|
|
||||||
public LatticeSplitter()
|
public LatticeSplitter()
|
||||||
{
|
{
|
||||||
this.DefaultStyleKey = typeof(LatticeSplitter);
|
this.DefaultStyleKey = typeof(LatticeSplitter);
|
||||||
@@ -17,17 +30,116 @@ public class LatticeSplitter : Control
|
|||||||
this.PointerEntered += (s, e) =>
|
this.PointerEntered += (s, e) =>
|
||||||
this.ProtectedCursor = InputSystemCursor.Create(InputSystemCursorShape.SizeWestEast);
|
this.ProtectedCursor = InputSystemCursor.Create(InputSystemCursorShape.SizeWestEast);
|
||||||
this.PointerExited += (s, e) =>
|
this.PointerExited += (s, e) =>
|
||||||
this.ProtectedCursor = null;
|
this.ProtectedCursor = InputSystemCursor.Create(InputSystemCursorShape.Arrow);
|
||||||
|
|
||||||
this.ManipulationDelta += OnManipulationDelta;
|
this.ManipulationDelta += OnManipulationDelta;
|
||||||
|
this.ManipulationStarted += (s, e) =>
|
||||||
|
{
|
||||||
|
IsDragging = true;
|
||||||
|
DragStarted?.Invoke(this, EventArgs.Empty);
|
||||||
|
};
|
||||||
|
this.ManipulationCompleted += (s, e) =>
|
||||||
|
{
|
||||||
|
IsDragging = false;
|
||||||
|
DragCompleted?.Invoke(this, EventArgs.Empty);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IDockElement? Model
|
||||||
|
{
|
||||||
|
get => _model;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_model != value)
|
||||||
|
{
|
||||||
|
_model = value;
|
||||||
|
OnPropertyChanged(nameof(Model));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public LayoutManager? LayoutManager
|
||||||
|
{
|
||||||
|
get => _layoutManager;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_layoutManager != value)
|
||||||
|
{
|
||||||
|
_layoutManager = value;
|
||||||
|
OnPropertyChanged(nameof(LayoutManager));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDockContextManager? ContextManager
|
||||||
|
{
|
||||||
|
get => _contextManager;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_contextManager != value)
|
||||||
|
{
|
||||||
|
_contextManager = value;
|
||||||
|
OnPropertyChanged(nameof(ContextManager));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSelected
|
||||||
|
{
|
||||||
|
get => _isSelected;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_isSelected != value)
|
||||||
|
{
|
||||||
|
_isSelected = value;
|
||||||
|
OnPropertyChanged(nameof(IsSelected));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsActive
|
||||||
|
{
|
||||||
|
get => _isActive;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_isActive != value)
|
||||||
|
{
|
||||||
|
_isActive = value;
|
||||||
|
OnPropertyChanged(nameof(IsActive));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanDrag => false;
|
||||||
|
public bool CanDrop => false;
|
||||||
|
|
||||||
|
public object? PrepareDragData() => null;
|
||||||
|
public bool HandleDrop(object data, DockPosition position) => false;
|
||||||
|
|
||||||
|
public Core.Docking.Models.SplitDirection Orientation { get; set; }
|
||||||
|
|
||||||
|
public bool IsDragging
|
||||||
|
{
|
||||||
|
get => _isDragging;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_isDragging != value)
|
||||||
|
{
|
||||||
|
_isDragging = value;
|
||||||
|
OnPropertyChanged(nameof(IsDragging));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler? DragStarted;
|
||||||
|
public event EventHandler<SplitterDraggedEventArgs>? DragDelta;
|
||||||
|
public event EventHandler? DragCompleted;
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
private void OnManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
|
private void OnManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
|
||||||
{
|
{
|
||||||
// 1. Находим модель DockGroup через DataContext
|
|
||||||
if (this.DataContext is not DockGroup group) return;
|
if (this.DataContext is not DockGroup group) return;
|
||||||
|
|
||||||
// 2. Находим родительский Grid, чтобы знать общие размеры
|
|
||||||
if (VisualTreeHelper.GetParent(this) is not Grid parentGrid ||
|
if (VisualTreeHelper.GetParent(this) is not Grid parentGrid ||
|
||||||
parentGrid.ActualWidth <= 0 || parentGrid.ActualHeight <= 0)
|
parentGrid.ActualWidth <= 0 || parentGrid.ActualHeight <= 0)
|
||||||
return;
|
return;
|
||||||
@@ -38,14 +150,35 @@ public class LatticeSplitter : Control
|
|||||||
|
|
||||||
if (totalSize <= 0) return;
|
if (totalSize <= 0) return;
|
||||||
|
|
||||||
// 3. Вычисляем изменение Ratio (от -1.0 до 1.0)
|
|
||||||
double delta = group.Orientation == SplitDirection.Horizontal
|
double delta = group.Orientation == SplitDirection.Horizontal
|
||||||
? e.Delta.Translation.X
|
? e.Delta.Translation.X
|
||||||
: e.Delta.Translation.Y;
|
: e.Delta.Translation.Y;
|
||||||
|
|
||||||
double ratioChange = delta / totalSize;
|
double ratioChange = delta / totalSize;
|
||||||
|
|
||||||
// 4. Обновляем модель (с ограничением от 0.05 до 0.95)
|
|
||||||
group.SplitRatio = Math.Clamp(group.SplitRatio + ratioChange, 0.05, 0.95);
|
group.SplitRatio = Math.Clamp(group.SplitRatio + ratioChange, 0.05, 0.95);
|
||||||
|
|
||||||
|
DragDelta?.Invoke(this, new SplitterDraggedEventArgs(
|
||||||
|
group.Orientation == SplitDirection.Horizontal ? delta : 0,
|
||||||
|
group.Orientation == SplitDirection.Vertical ? delta : 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Refresh() { }
|
||||||
|
|
||||||
|
public void ApplyTheme(IDockTheme theme) { }
|
||||||
|
|
||||||
|
public void OnModelPropertyChanged(string propertyName) { }
|
||||||
|
|
||||||
|
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
_disposed = true;
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
using Lattice.Core.Docking.Abstractions;
|
using Lattice.Core.Docking.Abstractions;
|
||||||
using Lattice.Core.Docking.Engine;
|
using Lattice.Core.Docking.Engine;
|
||||||
using Lattice.Core.Docking.Models;
|
using Lattice.Core.Docking.Models;
|
||||||
|
using Lattice.UI.Docking.Abstractions;
|
||||||
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.Media;
|
using Microsoft.UI.Xaml.Media;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
@@ -13,10 +13,6 @@ using System.Runtime.CompilerServices;
|
|||||||
|
|
||||||
namespace Lattice.UI;
|
namespace Lattice.UI;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Кастомный контрол вкладок с поддержкой всех позиций размещения панели вкладок.
|
|
||||||
/// Реализует интерфейс <see cref="IDockLeafControl"/> для интеграции с системой докинга.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
||||||
{
|
{
|
||||||
private readonly PropertyChangedEventHandler _modelPropertyChangedHandler;
|
private readonly PropertyChangedEventHandler _modelPropertyChangedHandler;
|
||||||
@@ -26,31 +22,20 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
private ListBox? _tabHeaderList;
|
private ListBox? _tabHeaderList;
|
||||||
private ContentControl? _contentControl;
|
private ContentControl? _contentControl;
|
||||||
private LayoutManager? _layoutManager;
|
private LayoutManager? _layoutManager;
|
||||||
private IDockDragDropService? _dragDropService;
|
|
||||||
private IDockContextManager? _contextManager;
|
private IDockContextManager? _contextManager;
|
||||||
private bool _isSelected;
|
private bool _isSelected;
|
||||||
private bool _isActive;
|
private bool _isActive;
|
||||||
private bool _canDrag = true;
|
private TabPlacement _tabPlacement = TabPlacement.Top;
|
||||||
private bool _canDrop = true;
|
|
||||||
private bool _showCloseButtons = true;
|
private bool _showCloseButtons = true;
|
||||||
private bool _canReorderTabs = true;
|
private bool _canReorderTabs = true;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="LatticeTabControl"/>.
|
|
||||||
/// </summary>
|
|
||||||
public LatticeTabControl()
|
public LatticeTabControl()
|
||||||
{
|
{
|
||||||
this.DefaultStyleKey = typeof(LatticeTabControl);
|
this.DefaultStyleKey = typeof(LatticeTabControl);
|
||||||
_modelPropertyChangedHandler = OnModelPropertyChanged;
|
_modelPropertyChangedHandler = OnModelPropertyChanged;
|
||||||
this.DataContextChanged += OnDataContextChanged;
|
this.DataContextChanged += OnDataContextChanged;
|
||||||
|
|
||||||
// Подписываемся на события
|
|
||||||
this.PointerPressed += OnPointerPressed;
|
|
||||||
this.PointerMoved += OnPointerMoved;
|
|
||||||
this.PointerReleased += OnPointerReleased;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public IDockElement? Model
|
public IDockElement? Model
|
||||||
{
|
{
|
||||||
get => _model;
|
get => _model;
|
||||||
@@ -64,7 +49,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public LayoutManager? LayoutManager
|
public LayoutManager? LayoutManager
|
||||||
{
|
{
|
||||||
get => _layoutManager;
|
get => _layoutManager;
|
||||||
@@ -76,19 +60,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public IDockDragDropService? DragDropService
|
|
||||||
{
|
|
||||||
get => _dragDropService;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_dragDropService == value) return;
|
|
||||||
_dragDropService = value;
|
|
||||||
OnPropertyChanged(nameof(DragDropService));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public IDockContextManager? ContextManager
|
public IDockContextManager? ContextManager
|
||||||
{
|
{
|
||||||
get => _contextManager;
|
get => _contextManager;
|
||||||
@@ -100,7 +71,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public bool IsSelected
|
public bool IsSelected
|
||||||
{
|
{
|
||||||
get => _isSelected;
|
get => _isSelected;
|
||||||
@@ -112,7 +82,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public bool IsActive
|
public bool IsActive
|
||||||
{
|
{
|
||||||
get => _isActive;
|
get => _isActive;
|
||||||
@@ -124,70 +93,50 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
public bool CanDrag => true;
|
||||||
public bool CanDrag
|
public bool CanDrop => true;
|
||||||
{
|
|
||||||
get => _canDrag;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_canDrag == value) return;
|
|
||||||
_canDrag = value;
|
|
||||||
OnPropertyChanged(nameof(CanDrag));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public bool CanDrop
|
|
||||||
{
|
|
||||||
get => _canDrop;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_canDrop == value) return;
|
|
||||||
_canDrop = value;
|
|
||||||
OnPropertyChanged(nameof(CanDrop));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public TabPlacement TabPlacement
|
public TabPlacement TabPlacement
|
||||||
{
|
{
|
||||||
get => _model?.TabPlacement ?? TabPlacement.Top;
|
get => _tabPlacement;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (_model != null && _model.TabPlacement != value)
|
if (_tabPlacement != value)
|
||||||
{
|
{
|
||||||
_model.TabPlacement = value;
|
_tabPlacement = value;
|
||||||
UpdateTabPlacement();
|
UpdateTabPlacement();
|
||||||
|
OnPropertyChanged(nameof(TabPlacement));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public bool ShowCloseButtons
|
public bool ShowCloseButtons
|
||||||
{
|
{
|
||||||
get => _showCloseButtons;
|
get => _showCloseButtons;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (_showCloseButtons == value) return;
|
if (_showCloseButtons != value)
|
||||||
_showCloseButtons = value;
|
{
|
||||||
OnPropertyChanged(nameof(ShowCloseButtons));
|
_showCloseButtons = value;
|
||||||
UpdateTabHeaders();
|
OnPropertyChanged(nameof(ShowCloseButtons));
|
||||||
|
UpdateTabHeaders();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public bool CanReorderTabs
|
public bool CanReorderTabs
|
||||||
{
|
{
|
||||||
get => _canReorderTabs;
|
get => _canReorderTabs;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (_canReorderTabs == value) return;
|
if (_canReorderTabs != value)
|
||||||
_canReorderTabs = value;
|
{
|
||||||
OnPropertyChanged(nameof(CanReorderTabs));
|
_canReorderTabs = value;
|
||||||
|
OnPropertyChanged(nameof(CanReorderTabs));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public IDockContent? ActiveContent
|
public IDockContent? ActiveContent
|
||||||
{
|
{
|
||||||
get => _model?.ActiveContent;
|
get => _model?.ActiveContent;
|
||||||
@@ -200,19 +149,14 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
public object? PrepareDragData() => Model;
|
||||||
|
public bool HandleDrop(object data, DockPosition position) => false;
|
||||||
|
|
||||||
public event EventHandler<ActiveContentChangedEventArgs>? ActiveContentChanged;
|
public event EventHandler<ActiveContentChangedEventArgs>? ActiveContentChanged;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event EventHandler<ContentClosingEventArgs>? ContentClosing;
|
public event EventHandler<ContentClosingEventArgs>? ContentClosing;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event EventHandler<TabsReorderedEventArgs>? TabsReordered;
|
public event EventHandler<TabsReorderedEventArgs>? TabsReordered;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
protected override void OnApplyTemplate()
|
protected override void OnApplyTemplate()
|
||||||
{
|
{
|
||||||
base.OnApplyTemplate();
|
base.OnApplyTemplate();
|
||||||
@@ -241,14 +185,13 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
{
|
{
|
||||||
_model.PropertyChanged += _modelPropertyChangedHandler;
|
_model.PropertyChanged += _modelPropertyChangedHandler;
|
||||||
|
|
||||||
// Подписываемся на изменения коллекции
|
|
||||||
if (_model.Children is INotifyCollectionChanged notifyCollection)
|
if (_model.Children is INotifyCollectionChanged notifyCollection)
|
||||||
{
|
{
|
||||||
notifyCollection.CollectionChanged += OnChildrenCollectionChanged;
|
notifyCollection.CollectionChanged += OnChildrenCollectionChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Устанавливаем DataContext для привязки в XAML
|
|
||||||
this.DataContext = _model;
|
this.DataContext = _model;
|
||||||
|
_tabPlacement = _model.TabPlacement;
|
||||||
UpdateTabHeaders();
|
UpdateTabHeaders();
|
||||||
UpdateTabPlacement();
|
UpdateTabPlacement();
|
||||||
}
|
}
|
||||||
@@ -274,6 +217,7 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
switch (e.PropertyName)
|
switch (e.PropertyName)
|
||||||
{
|
{
|
||||||
case nameof(DockLeaf.TabPlacement):
|
case nameof(DockLeaf.TabPlacement):
|
||||||
|
_tabPlacement = _model?.TabPlacement ?? TabPlacement.Top;
|
||||||
OnPropertyChanged(nameof(TabPlacement));
|
OnPropertyChanged(nameof(TabPlacement));
|
||||||
UpdateTabPlacement();
|
UpdateTabPlacement();
|
||||||
break;
|
break;
|
||||||
@@ -298,11 +242,9 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
{
|
{
|
||||||
if (_rootGrid == null || _model == null) return;
|
if (_rootGrid == null || _model == null) return;
|
||||||
|
|
||||||
// Очищаем все определения
|
|
||||||
_rootGrid.RowDefinitions.Clear();
|
_rootGrid.RowDefinitions.Clear();
|
||||||
_rootGrid.ColumnDefinitions.Clear();
|
_rootGrid.ColumnDefinitions.Clear();
|
||||||
|
|
||||||
// Настраиваем Grid в зависимости от позиции вкладок
|
|
||||||
switch (_model.TabPlacement)
|
switch (_model.TabPlacement)
|
||||||
{
|
{
|
||||||
case TabPlacement.Top:
|
case TabPlacement.Top:
|
||||||
@@ -330,18 +272,14 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем позиции элементов
|
|
||||||
UpdateElementPositions();
|
UpdateElementPositions();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateHeaderListOrientation(Orientation orientation)
|
private void UpdateHeaderListOrientation(Orientation orientation)
|
||||||
{
|
{
|
||||||
if (_tabHeaderList != null && _tabHeaderList.ItemsPanelRoot is ItemsPanelTemplate panelTemplate)
|
if (_tabHeaderList?.ItemsPanelRoot is StackPanel stackPanel)
|
||||||
{
|
{
|
||||||
if (panelTemplate.LoadContent() is StackPanel stackPanel)
|
stackPanel.Orientation = orientation;
|
||||||
{
|
|
||||||
stackPanel.Orientation = orientation;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,10 +323,8 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
{
|
{
|
||||||
if (_tabHeaderList == null || _model == null) return;
|
if (_tabHeaderList == null || _model == null) return;
|
||||||
|
|
||||||
// Очищаем текущие элементы
|
|
||||||
_tabHeaderList.Items.Clear();
|
_tabHeaderList.Items.Clear();
|
||||||
|
|
||||||
// Добавляем новые элементы
|
|
||||||
foreach (var content in _model.Children)
|
foreach (var content in _model.Children)
|
||||||
{
|
{
|
||||||
var item = CreateTabHeaderItem(content);
|
var item = CreateTabHeaderItem(content);
|
||||||
@@ -408,26 +344,14 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
VerticalContentAlignment = VerticalAlignment.Stretch
|
VerticalContentAlignment = VerticalAlignment.Stretch
|
||||||
};
|
};
|
||||||
|
|
||||||
// Обработка клика для выбора вкладки
|
|
||||||
item.PointerPressed += (sender, e) =>
|
item.PointerPressed += (sender, e) =>
|
||||||
{
|
{
|
||||||
if (e.GetCurrentPoint(item).Properties.IsLeftButtonPressed)
|
if (e.GetCurrentPoint(item).Properties.IsLeftButtonPressed)
|
||||||
{
|
{
|
||||||
ActiveContent = content;
|
ActiveContent = content;
|
||||||
e.Handled = true;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Обработка клика на кнопке закрытия
|
|
||||||
if (_showCloseButtons && content.CanClose)
|
|
||||||
{
|
|
||||||
// Добавляем контекстное меню
|
|
||||||
item.ContextRequested += (sender, e) =>
|
|
||||||
{
|
|
||||||
ShowTabContextMenu(item, content, e);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,7 +361,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||||
|
|
||||||
// Заголовок вкладки
|
|
||||||
var textBlock = new TextBlock
|
var textBlock = new TextBlock
|
||||||
{
|
{
|
||||||
Text = content.Title,
|
Text = content.Title,
|
||||||
@@ -447,7 +370,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
Grid.SetColumn(textBlock, 0);
|
Grid.SetColumn(textBlock, 0);
|
||||||
grid.Children.Add(textBlock);
|
grid.Children.Add(textBlock);
|
||||||
|
|
||||||
// Кнопка закрытия (если разрешено)
|
|
||||||
if (_showCloseButtons && content.CanClose)
|
if (_showCloseButtons && content.CanClose)
|
||||||
{
|
{
|
||||||
var closeButton = new Button
|
var closeButton = new Button
|
||||||
@@ -465,7 +387,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
closeButton.Click += (sender, e) =>
|
closeButton.Click += (sender, e) =>
|
||||||
{
|
{
|
||||||
CloseContent(content);
|
CloseContent(content);
|
||||||
e.Handled = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Grid.SetColumn(closeButton, 1);
|
Grid.SetColumn(closeButton, 1);
|
||||||
@@ -479,7 +400,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
{
|
{
|
||||||
if (_tabHeaderList == null || _model == null) return;
|
if (_tabHeaderList == null || _model == null) return;
|
||||||
|
|
||||||
// Находим элемент, соответствующий активному контенту
|
|
||||||
foreach (var item in _tabHeaderList.Items)
|
foreach (var item in _tabHeaderList.Items)
|
||||||
{
|
{
|
||||||
if (item is ListBoxItem listBoxItem && listBoxItem.Tag is IDockContent content)
|
if (item is ListBoxItem listBoxItem && listBoxItem.Tag is IDockContent content)
|
||||||
@@ -488,7 +408,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем контент
|
|
||||||
if (_contentControl != null)
|
if (_contentControl != null)
|
||||||
{
|
{
|
||||||
_contentControl.Content = _model.ActiveContent?.View;
|
_contentControl.Content = _model.ActiveContent?.View;
|
||||||
@@ -511,67 +430,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drag-and-Drop для переупорядочивания вкладок
|
|
||||||
private ListBoxItem? _draggedItem;
|
|
||||||
private Point _dragStartPoint;
|
|
||||||
|
|
||||||
private void OnPointerPressed(object sender, PointerRoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (!_canReorderTabs) return;
|
|
||||||
|
|
||||||
var pointerPoint = e.GetCurrentPoint(this);
|
|
||||||
_dragStartPoint = new Point(pointerPoint.Position.X, pointerPoint.Position.Y);
|
|
||||||
|
|
||||||
// Находим элемент под курсором
|
|
||||||
var element = VisualTreeHelper.FindElementsInHostCoordinates(
|
|
||||||
pointerPoint.Position, this).FirstOrDefault();
|
|
||||||
|
|
||||||
if (element is ListBoxItem listBoxItem)
|
|
||||||
{
|
|
||||||
_draggedItem = listBoxItem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPointerMoved(object sender, PointerRoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_draggedItem == null || !_canReorderTabs) return;
|
|
||||||
|
|
||||||
var pointerPoint = e.GetCurrentPoint(this);
|
|
||||||
var currentPoint = new Point(pointerPoint.Position.X, pointerPoint.Position.Y);
|
|
||||||
|
|
||||||
// Проверяем, достаточно ли переместили для начала перетаскивания
|
|
||||||
var distance = Math.Sqrt(
|
|
||||||
Math.Pow(currentPoint.X - _dragStartPoint.X, 2) +
|
|
||||||
Math.Pow(currentPoint.Y - _dragStartPoint.Y, 2));
|
|
||||||
|
|
||||||
if (distance > 10 && _draggedItem.Tag is IDockContent content)
|
|
||||||
{
|
|
||||||
// Начинаем операцию перетаскивания
|
|
||||||
StartTabDrag(_draggedItem, content);
|
|
||||||
_draggedItem = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPointerReleased(object sender, PointerRoutedEventArgs e)
|
|
||||||
{
|
|
||||||
_draggedItem = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StartTabDrag(ListBoxItem item, IDockContent content)
|
|
||||||
{
|
|
||||||
// TODO: Реализовать перетаскивание вкладок
|
|
||||||
// Для этого нужно использовать IDockDragDropService
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowTabContextMenu(ListBoxItem item, IDockContent content, ContextRequestedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_contextManager == null) return;
|
|
||||||
|
|
||||||
var position = e.GetPosition(this);
|
|
||||||
_contextManager.ShowContextMenu(this, position.X, position.Y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void AddContent(IDockContent content)
|
public void AddContent(IDockContent content)
|
||||||
{
|
{
|
||||||
if (_model != null && !_model.Children.Contains(content))
|
if (_model != null && !_model.Children.Contains(content))
|
||||||
@@ -581,7 +439,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void RemoveContent(IDockContent content)
|
public void RemoveContent(IDockContent content)
|
||||||
{
|
{
|
||||||
if (_model != null && _model.Children.Contains(content))
|
if (_model != null && _model.Children.Contains(content))
|
||||||
@@ -591,7 +448,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public bool CloseContent(IDockContent content)
|
public bool CloseContent(IDockContent content)
|
||||||
{
|
{
|
||||||
var args = new ContentClosingEventArgs(content);
|
var args = new ContentClosingEventArgs(content);
|
||||||
@@ -606,7 +462,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void CloseAllExcept(IDockContent exceptContent)
|
public void CloseAllExcept(IDockContent exceptContent)
|
||||||
{
|
{
|
||||||
if (_model == null) return;
|
if (_model == null) return;
|
||||||
@@ -621,7 +476,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void CloseAll()
|
public void CloseAll()
|
||||||
{
|
{
|
||||||
if (_model == null) return;
|
if (_model == null) return;
|
||||||
@@ -633,24 +487,17 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void Refresh()
|
public void Refresh()
|
||||||
{
|
{
|
||||||
UpdateTabHeaders();
|
UpdateTabHeaders();
|
||||||
UpdateTabPlacement();
|
UpdateTabPlacement();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void ApplyTheme(IDockTheme theme)
|
public void ApplyTheme(IDockTheme theme)
|
||||||
{
|
{
|
||||||
// Применение темы к элементу
|
// TODO: Реализовать применение темы
|
||||||
if (theme != null)
|
|
||||||
{
|
|
||||||
// TODO: Применить тему к стилям контрола
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void OnModelPropertyChanged(string propertyName)
|
public void OnModelPropertyChanged(string propertyName)
|
||||||
{
|
{
|
||||||
if (_model != null)
|
if (_model != null)
|
||||||
@@ -664,7 +511,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (!_disposed)
|
if (!_disposed)
|
||||||
@@ -676,11 +522,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
_tabHeaderList.SelectionChanged -= OnTabSelectionChanged;
|
_tabHeaderList.SelectionChanged -= OnTabSelectionChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отписываемся от событий указателя
|
|
||||||
this.PointerPressed -= OnPointerPressed;
|
|
||||||
this.PointerMoved -= OnPointerMoved;
|
|
||||||
this.PointerReleased -= OnPointerReleased;
|
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|||||||
189
Lattice.UI.Docking.WinUI/DockBuilder.cs
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
using Lattice.Core.Docking.Engine;
|
||||||
|
using Lattice.Core.Docking.Services;
|
||||||
|
using Lattice.UI.Docking.Abstractions;
|
||||||
|
using Lattice.UI.Docking.Factories;
|
||||||
|
using Lattice.UI.Docking.WinUI.Factories;
|
||||||
|
using Lattice.UI.Docking.WinUI.Services;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Lattice.UI.Docking;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Предоставляет упрощенный статический API для инициализации и конфигурации системы докинга Lattice.
|
||||||
|
/// </summary>
|
||||||
|
public static class LatticeDock
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Создает новый строитель конфигурации системы докинга для WinUI.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Экземпляр <see cref="DockBuilder"/> для настройки системы.</returns>
|
||||||
|
public static DockBuilder CreateWinUIBuilder()
|
||||||
|
{
|
||||||
|
return new DockBuilder()
|
||||||
|
.WithWinUIFactory()
|
||||||
|
.WithWinUIContextManager()
|
||||||
|
.WithWinUIService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настраивает строитель для использования фабрики WinUI.
|
||||||
|
/// </summary>
|
||||||
|
public static DockBuilder WithWinUIFactory(this DockBuilder builder)
|
||||||
|
{
|
||||||
|
return builder.WithControlFactory(new WinUIDockControlFactory());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настраивает строитель для использования контекстного менеджера WinUI.
|
||||||
|
/// </summary>
|
||||||
|
public static DockBuilder WithWinUIContextManager(this DockBuilder builder)
|
||||||
|
{
|
||||||
|
return builder.WithContextManager(new WinUIDockContextManager());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настраивает строитель для использования UI-сервиса WinUI.
|
||||||
|
/// </summary>
|
||||||
|
public static DockBuilder WithWinUIService(this DockBuilder builder)
|
||||||
|
{
|
||||||
|
return builder.WithUIService(new WinUIDockUIService());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Предоставляет fluent-интерфейс для конфигурации системы докинга Lattice.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DockBuilder
|
||||||
|
{
|
||||||
|
private readonly LayoutManager _layoutManager;
|
||||||
|
private readonly ContentRegistry _contentRegistry;
|
||||||
|
private IDockControlFactory? _factory;
|
||||||
|
private IDockContextManager? _contextManager;
|
||||||
|
private IDockUIService? _uiService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="DockBuilder"/>.
|
||||||
|
/// Создает менеджер макета и реестр контента по умолчанию.
|
||||||
|
/// </summary>
|
||||||
|
public DockBuilder()
|
||||||
|
{
|
||||||
|
_layoutManager = new LayoutManager();
|
||||||
|
_contentRegistry = new ContentRegistry();
|
||||||
|
_layoutManager.ContentRegistry = _contentRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Регистрирует фабрику контролов для создания UI-элементов.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="factory">Фабрика контролов.</param>
|
||||||
|
/// <returns>Текущий экземпляр <see cref="DockBuilder"/> для цепочки вызовов.</returns>
|
||||||
|
public DockBuilder WithControlFactory(IDockControlFactory factory)
|
||||||
|
{
|
||||||
|
_factory = factory;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Регистрирует менеджер контекстных меню.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="contextManager">Менеджер контекстных меню.</param>
|
||||||
|
/// <returns>Текущий экземпляр <see cref="DockBuilder"/> для цепочки вызовов.</returns>
|
||||||
|
public DockBuilder WithContextManager(IDockContextManager contextManager)
|
||||||
|
{
|
||||||
|
_contextManager = contextManager;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Регистрирует UI-сервис для выполнения платформенно-зависимых операций.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uiService">UI-сервис.</param>
|
||||||
|
/// <returns>Текущий экземпляр <see cref="DockBuilder"/> для цепочки вызовов.</returns>
|
||||||
|
public DockBuilder WithUIService(IDockUIService uiService)
|
||||||
|
{
|
||||||
|
_uiService = uiService;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Регистрирует тип контента в реестре.
|
||||||
|
/// </summary>
|
||||||
|
public DockBuilder RegisterContentType<T>(string contentTypeId, Func<T> factory, ContentMetadata? metadata = null)
|
||||||
|
where T : Core.Docking.Abstractions.IDockContent
|
||||||
|
{
|
||||||
|
_contentRegistry.Register(contentTypeId, factory, metadata);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Завершает конфигурацию системы докинга и возвращает настроенный экземпляр <see cref="IDockSystem"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Настроенная система докинга.</returns>
|
||||||
|
public IDockSystem Build()
|
||||||
|
{
|
||||||
|
// Настраиваем связи между компонентами
|
||||||
|
if (_factory is DockControlFactoryBase factoryBase && _contextManager != null)
|
||||||
|
{
|
||||||
|
factoryBase.ContextManager = _contextManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DockSystem(_layoutManager, _contentRegistry, _factory, _contextManager, _uiService);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Представляет настроенную систему докинга с доступом ко всем основным компонентам.
|
||||||
|
/// </summary>
|
||||||
|
public interface IDockSystem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получает менеджер макета.
|
||||||
|
/// </summary>
|
||||||
|
LayoutManager LayoutManager { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает реестр контента.
|
||||||
|
/// </summary>
|
||||||
|
ContentRegistry ContentRegistry { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает фабрику контролов.
|
||||||
|
/// </summary>
|
||||||
|
IDockControlFactory? ControlFactory { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает менеджер контекстных меню.
|
||||||
|
/// </summary>
|
||||||
|
IDockContextManager? ContextManager { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает UI-сервис.
|
||||||
|
/// </summary>
|
||||||
|
IDockUIService? UIService { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Реализация интерфейса <see cref="IDockSystem"/>.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class DockSystem : IDockSystem
|
||||||
|
{
|
||||||
|
public LayoutManager LayoutManager { get; }
|
||||||
|
public ContentRegistry ContentRegistry { get; }
|
||||||
|
public IDockControlFactory? ControlFactory { get; }
|
||||||
|
public IDockContextManager? ContextManager { get; }
|
||||||
|
public IDockUIService? UIService { get; }
|
||||||
|
|
||||||
|
public DockSystem(
|
||||||
|
LayoutManager layoutManager,
|
||||||
|
ContentRegistry contentRegistry,
|
||||||
|
IDockControlFactory? controlFactory,
|
||||||
|
IDockContextManager? contextManager,
|
||||||
|
IDockUIService? uiService)
|
||||||
|
{
|
||||||
|
LayoutManager = layoutManager ?? throw new ArgumentNullException(nameof(layoutManager));
|
||||||
|
ContentRegistry = contentRegistry ?? throw new ArgumentNullException(nameof(contentRegistry));
|
||||||
|
ControlFactory = controlFactory;
|
||||||
|
ContextManager = contextManager;
|
||||||
|
UIService = uiService;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,79 +4,69 @@ using Lattice.UI.Docking.Abstractions;
|
|||||||
using Lattice.UI.Docking.Factories;
|
using Lattice.UI.Docking.Factories;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Lattice.UI.Docking.WinUI.Factories;
|
namespace Lattice.UI.Docking.WinUI.Factories;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Фабрика контролов для платформы WinUI.
|
|
||||||
/// Создает UI-элементы для отображения компонентов системы докинга.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class WinUIDockControlFactory : DockControlFactoryBase, IDockControlFactory
|
public sealed class WinUIDockControlFactory : DockControlFactoryBase, IDockControlFactory
|
||||||
{
|
{
|
||||||
private readonly IDockTheme _theme;
|
private readonly Dictionary<Type, Func<object, IDockControl>> _creators;
|
||||||
|
|
||||||
/// <summary>
|
public WinUIDockControlFactory()
|
||||||
/// Инициализирует новый экземпляр фабрики WinUI.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="theme">Тема оформления.</param>
|
|
||||||
public WinUIDockControlFactory(IDockTheme theme)
|
|
||||||
{
|
{
|
||||||
_theme = theme ?? throw new ArgumentNullException(nameof(theme));
|
_creators = new Dictionary<Type, Func<object, IDockControl>>
|
||||||
|
{
|
||||||
|
[typeof(DockGroup)] = model => CreateGroupControl((DockGroup)model),
|
||||||
|
[typeof(DockLeaf)] = model => CreateLeafControl((DockLeaf)model),
|
||||||
|
[typeof(DockWindow)] = model => CreateFloatingWindowControl((DockWindow)model),
|
||||||
|
[typeof(AutoHidePanel)] = model => CreateAutoHidePanelControl((AutoHidePanel)model),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override IDockGroupControl CreateGroupControl(DockGroup group)
|
public override IDockGroupControl CreateGroupControl(DockGroup group)
|
||||||
{
|
{
|
||||||
|
if (group == null) throw new ArgumentNullException(nameof(group));
|
||||||
|
|
||||||
var control = new LatticeDockGroup();
|
var control = new LatticeDockGroup();
|
||||||
ConfigureControl(control, group);
|
ConfigureControl(control, group);
|
||||||
control.ApplyTheme(_theme);
|
|
||||||
return control;
|
return control;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override IDockLeafControl CreateLeafControl(DockLeaf leaf)
|
public override IDockLeafControl CreateLeafControl(DockLeaf leaf)
|
||||||
{
|
{
|
||||||
|
if (leaf == null) throw new ArgumentNullException(nameof(leaf));
|
||||||
|
|
||||||
var control = new LatticeTabControl();
|
var control = new LatticeTabControl();
|
||||||
ConfigureControl(control, leaf);
|
ConfigureControl(control, leaf);
|
||||||
control.ApplyTheme(_theme);
|
|
||||||
return control;
|
return control;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override IFloatingWindowControl CreateFloatingWindowControl(DockWindow window)
|
public override IFloatingWindowControl CreateFloatingWindowControl(DockWindow window)
|
||||||
{
|
{
|
||||||
// TODO: Реализовать создание плавающего окна
|
throw new NotImplementedException("Floating windows not implemented yet");
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override IAutoHidePanelControl CreateAutoHidePanelControl(AutoHidePanel panel)
|
public override IAutoHidePanelControl CreateAutoHidePanelControl(AutoHidePanel panel)
|
||||||
{
|
{
|
||||||
// TODO: Реализовать создание автоскрываемой панели
|
throw new NotImplementedException("Auto-hide panels not implemented yet");
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override IDockSplitterControl CreateSplitterControl(SplitDirection orientation)
|
public override IDockSplitterControl CreateSplitterControl(SplitDirection orientation)
|
||||||
{
|
{
|
||||||
var control = new LatticeSplitter
|
var control = new LatticeSplitter { Orientation = orientation };
|
||||||
{
|
|
||||||
Orientation = orientation
|
|
||||||
};
|
|
||||||
ConfigureControl(control);
|
ConfigureControl(control);
|
||||||
control.ApplyTheme(_theme);
|
|
||||||
return control;
|
return control;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public override IDockControl? CreateControlForElement(IDockElement element)
|
||||||
/// Создает хост для размещения системы докинга.
|
|
||||||
/// </summary>
|
|
||||||
public IDockHost CreateDockHost()
|
|
||||||
{
|
{
|
||||||
var host = new LatticeDockHost();
|
if (element == null) throw new ArgumentNullException(nameof(element));
|
||||||
ConfigureControl(host);
|
|
||||||
host.ApplyTheme(_theme);
|
var type = element.GetType();
|
||||||
return host;
|
if (_creators.TryGetValue(type, out var creator))
|
||||||
|
return creator(element);
|
||||||
|
|
||||||
|
return base.CreateControlForElement(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ConfigureControl(IDockControl control, IDockElement? model = null)
|
private void ConfigureControl(IDockControl control, IDockElement? model = null)
|
||||||
@@ -84,9 +74,8 @@ public sealed class WinUIDockControlFactory : DockControlFactoryBase, IDockContr
|
|||||||
if (control == null) return;
|
if (control == null) return;
|
||||||
|
|
||||||
control.Model = model;
|
control.Model = model;
|
||||||
control.LayoutManager = LatticeUIFramework.LayoutManager;
|
control.LayoutManager = Lattice.UI.Docking.LatticeUIFramework.LayoutManager;
|
||||||
control.DragDropService = LatticeUIFramework.DragDropService;
|
control.ContextManager = Lattice.UI.Docking.LatticeUIFramework.ContextManager;
|
||||||
control.ContextManager = LatticeUIFramework.ContextManager;
|
|
||||||
|
|
||||||
if (control is FrameworkElement frameworkElement && model != null)
|
if (control is FrameworkElement frameworkElement && model != null)
|
||||||
{
|
{
|
||||||
|
|||||||
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 Microsoft.UI.Xaml.Controls;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
namespace Lattice.UI.Docking.WinUI.Services;
|
namespace Lattice.UI.Docking.WinUI.Services;
|
||||||
|
|
||||||
@@ -21,9 +24,38 @@ public sealed class WinUIDockContextManager : DockContextManagerBase, IDisposabl
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public WinUIDockContextManager()
|
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)
|
public override void ShowContextMenu(IDockControl element, double x, double y)
|
||||||
{
|
{
|
||||||
if (element is not FrameworkElement uiElement) return;
|
if (element is not FrameworkElement uiElement) return;
|
||||||
@@ -39,13 +71,26 @@ public sealed class WinUIDockContextManager : DockContextManagerBase, IDisposabl
|
|||||||
var item = new MenuFlyoutItem
|
var item = new MenuFlyoutItem
|
||||||
{
|
{
|
||||||
Text = command.Name,
|
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))
|
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);
|
flyout.Items.Add(item);
|
||||||
@@ -68,7 +113,6 @@ public sealed class WinUIDockContextManager : DockContextManagerBase, IDisposabl
|
|||||||
OnContextMenuShown(element, x, y);
|
OnContextMenuShown(element, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override void HideContextMenu()
|
public override void HideContextMenu()
|
||||||
{
|
{
|
||||||
if (_currentFlyout != null)
|
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>
|
/// <summary>
|
||||||
/// Класс-заглушка для реализации ICommand.
|
/// Получает команду по идентификатору.
|
||||||
/// </summary>
|
/// </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 Action _execute;
|
||||||
private readonly Func<bool>? _canExecute;
|
private readonly Func<bool> _canExecute;
|
||||||
|
|
||||||
public event EventHandler? CanExecuteChanged;
|
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));
|
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||||
_canExecute = canExecute;
|
_canExecute = canExecute;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
|
public bool CanExecute(object? parameter) => _canExecute();
|
||||||
|
|
||||||
public void Execute(object? parameter) => _execute();
|
public void Execute(object? parameter) => _execute();
|
||||||
|
|
||||||
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
HideContextMenu();
|
HideContextMenu();
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ using System.Threading.Tasks;
|
|||||||
namespace Lattice.UI.Docking.WinUI.Services;
|
namespace Lattice.UI.Docking.WinUI.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Реализация UI-сервиса для WinUI.
|
/// Реализация UI-сервиса для платформы WinUI.
|
||||||
|
/// Инкапсулирует платформенно-зависимые операции, такие как создание окон,
|
||||||
|
/// показ диалогов и синхронизация с UI-потоком.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class WinUIDockUIService : DockUIServiceBase
|
public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService
|
||||||
{
|
{
|
||||||
/// <inheritdoc/>
|
|
||||||
public override object CreateMainWindow(IDockHost host)
|
public override object CreateMainWindow(IDockHost host)
|
||||||
{
|
{
|
||||||
if (host is not FrameworkElement hostElement)
|
if (host is not FrameworkElement hostElement)
|
||||||
@@ -23,12 +24,11 @@ public sealed class WinUIDockUIService : DockUIServiceBase
|
|||||||
window.AppWindow.Title = "Lattice IDE";
|
window.AppWindow.Title = "Lattice IDE";
|
||||||
|
|
||||||
// Регистрируем окно в трекере
|
// Регистрируем окно в трекере
|
||||||
Themes.WindowTracker.Register(window);
|
Lattice.Themes.WindowTracker.Register(window);
|
||||||
|
|
||||||
return window;
|
return window;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override bool? ShowDialog(string title, object content)
|
public override bool? ShowDialog(string title, object content)
|
||||||
{
|
{
|
||||||
if (content is not FrameworkElement contentElement)
|
if (content is not FrameworkElement contentElement)
|
||||||
@@ -43,7 +43,6 @@ public sealed class WinUIDockUIService : DockUIServiceBase
|
|||||||
XamlRoot = GetActiveXamlRoot()
|
XamlRoot = GetActiveXamlRoot()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Показываем диалог и возвращаем результат
|
|
||||||
var result = dialog.ShowAsync();
|
var result = dialog.ShowAsync();
|
||||||
return result.GetAwaiter().GetResult() switch
|
return result.GetAwaiter().GetResult() switch
|
||||||
{
|
{
|
||||||
@@ -53,7 +52,6 @@ public sealed class WinUIDockUIService : DockUIServiceBase
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override void ShowMessage(string message, string caption)
|
public override void ShowMessage(string message, string caption)
|
||||||
{
|
{
|
||||||
var dialog = new ContentDialog
|
var dialog = new ContentDialog
|
||||||
@@ -67,7 +65,6 @@ public sealed class WinUIDockUIService : DockUIServiceBase
|
|||||||
dialog.ShowAsync();
|
dialog.ShowAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override bool Confirm(string message, string caption)
|
public override bool Confirm(string message, string caption)
|
||||||
{
|
{
|
||||||
var dialog = new ContentDialog
|
var dialog = new ContentDialog
|
||||||
@@ -83,7 +80,6 @@ public sealed class WinUIDockUIService : DockUIServiceBase
|
|||||||
return result == ContentDialogResult.Primary;
|
return result == ContentDialogResult.Primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override string? Prompt(string prompt, string? defaultValue = null)
|
public override string? Prompt(string prompt, string? defaultValue = null)
|
||||||
{
|
{
|
||||||
var textBox = new TextBox
|
var textBox = new TextBox
|
||||||
@@ -106,7 +102,6 @@ public sealed class WinUIDockUIService : DockUIServiceBase
|
|||||||
return result == ContentDialogResult.Primary ? textBox.Text : null;
|
return result == ContentDialogResult.Primary ? textBox.Text : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override void InvokeOnUIThread(Action action)
|
public override void InvokeOnUIThread(Action action)
|
||||||
{
|
{
|
||||||
if (action == null) return;
|
if (action == null) return;
|
||||||
@@ -122,8 +117,10 @@ public sealed class WinUIDockUIService : DockUIServiceBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <summary>
|
||||||
public override async Task InvokeOnUIThreadAsync(Func<Task> action)
|
/// Выполняет указанную асинхронную функцию в UI-потоке.
|
||||||
|
/// </summary>
|
||||||
|
public async Task InvokeOnUIThreadAsync(Func<Task> action)
|
||||||
{
|
{
|
||||||
if (action == null) return;
|
if (action == null) return;
|
||||||
|
|
||||||
@@ -153,8 +150,7 @@ public sealed class WinUIDockUIService : DockUIServiceBase
|
|||||||
|
|
||||||
private XamlRoot? GetActiveXamlRoot()
|
private XamlRoot? GetActiveXamlRoot()
|
||||||
{
|
{
|
||||||
// Получаем XamlRoot из активного окна
|
foreach (var window in Lattice.Themes.WindowTracker.Windows)
|
||||||
foreach (var window in Themes.WindowTracker.Windows)
|
|
||||||
{
|
{
|
||||||
if (window.Content is FrameworkElement element)
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,8 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:controls="using:Lattice.UI"
|
xmlns:controls="using:Lattice.UI"
|
||||||
xmlns:conv="using:Lattice.UI.Docking.WinUI.Converters"
|
xmlns:conv="using:Lattice.UI.Docking.WinUI.Converters"
|
||||||
xmlns:models="using:Lattice.Core.Docking.Models">
|
xmlns:models="using:Lattice.Core.Docking.Models"
|
||||||
|
xmlns:muxc="using:Microsoft.UI.Xaml.Controls">
|
||||||
|
|
||||||
<!-- 1. Шаблоны -->
|
<!-- 1. Шаблоны -->
|
||||||
<DataTemplate x:Key="LatticeGroupTemplate">
|
<DataTemplate x:Key="LatticeGroupTemplate">
|
||||||
@@ -20,55 +21,73 @@
|
|||||||
GroupTemplate="{StaticResource LatticeGroupTemplate}"
|
GroupTemplate="{StaticResource LatticeGroupTemplate}"
|
||||||
LeafTemplate="{StaticResource LatticeLeafTemplate}" />
|
LeafTemplate="{StaticResource LatticeLeafTemplate}" />
|
||||||
|
|
||||||
<!-- 3. Стиль Сплиттера -->
|
<!-- 3. Стиль Splitter -->
|
||||||
<Style TargetType="controls:LatticeSplitter">
|
<Style TargetType="controls:LatticeSplitter">
|
||||||
<Setter Property="Background" Value="{ThemeResource Lattice.Brush.Splitter.Normal}"/>
|
<Setter Property="Background" Value="{ThemeResource Lattice.Brush.Splitter.Normal}"/>
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="controls:LatticeSplitter">
|
<ControlTemplate TargetType="controls:LatticeSplitter">
|
||||||
<Grid Background="Transparent">
|
<Grid Background="Transparent">
|
||||||
<Rectangle Fill="{TemplateBinding Background}"
|
<Rectangle x:Name="SplitterRect"
|
||||||
Width="{ThemeResource Lattice.Size.SplitterWidth}"
|
Fill="{TemplateBinding Background}"
|
||||||
|
Width="{ThemeResource Lattice.Size.Splitter.Width}"
|
||||||
HorizontalAlignment="Center"/>
|
HorizontalAlignment="Center"/>
|
||||||
|
<VisualStateManager.VisualStateGroups>
|
||||||
|
<VisualStateGroup x:Name="CommonStates">
|
||||||
|
<VisualState x:Name="Normal"/>
|
||||||
|
<VisualState x:Name="PointerOver">
|
||||||
|
<Storyboard>
|
||||||
|
<ColorAnimation Storyboard.TargetName="SplitterRect"
|
||||||
|
Storyboard.TargetProperty="(Rectangle.Fill).(SolidColorBrush.Color)"
|
||||||
|
To="{ThemeResource Lattice.Color.Accent.Action}"
|
||||||
|
Duration="0:0:0.2"/>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
</VisualStateManager.VisualStateGroups>
|
||||||
</Grid>
|
</Grid>
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
</Setter.Value>
|
</Setter.Value>
|
||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- 4. Стиль Хоста -->
|
<!-- 4. Стиль Host -->
|
||||||
<Style TargetType="controls:LatticeDockHost">
|
<Style TargetType="controls:LatticeDockHost">
|
||||||
|
<Setter Property="Background" Value="{ThemeResource Lattice.Brush.Background.Primary}"/>
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="controls:LatticeDockHost">
|
<ControlTemplate TargetType="controls:LatticeDockHost">
|
||||||
<ContentControl
|
<Border Background="{TemplateBinding Background}"
|
||||||
Content="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Manager.Root}"
|
Padding="4">
|
||||||
ContentTemplateSelector="{StaticResource GlobalDockSelector}"
|
<ContentControl x:Name="PART_RootContainer"
|
||||||
HorizontalContentAlignment="Stretch"
|
HorizontalContentAlignment="Stretch"
|
||||||
VerticalContentAlignment="Stretch" />
|
VerticalContentAlignment="Stretch" />
|
||||||
|
</Border>
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
</Setter.Value>
|
</Setter.Value>
|
||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- 5. Стиль Группы (Рекурсия) -->
|
<!-- 5. Стиль Group -->
|
||||||
<Style TargetType="controls:LatticeDockGroup">
|
<Style TargetType="controls:LatticeDockGroup">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="controls:LatticeDockGroup">
|
<ControlTemplate TargetType="controls:LatticeDockGroup">
|
||||||
<!-- Grid перестраивается в коде LatticeDockGroup.cs -->
|
<Grid x:Name="PART_Grid" Background="{TemplateBinding Background}">
|
||||||
<Grid x:Name="PART_Grid">
|
<!-- First child area -->
|
||||||
<!-- Первая область -->
|
|
||||||
<ContentControl x:Name="PART_First"
|
<ContentControl x:Name="PART_First"
|
||||||
Content="{Binding First}"
|
Content="{Binding First}"
|
||||||
ContentTemplateSelector="{StaticResource GlobalDockSelector}"
|
ContentTemplateSelector="{StaticResource GlobalDockSelector}"
|
||||||
HorizontalContentAlignment="Stretch"
|
HorizontalContentAlignment="Stretch"
|
||||||
VerticalContentAlignment="Stretch" />
|
VerticalContentAlignment="Stretch" />
|
||||||
|
|
||||||
<!-- Сплиттер (его положение в Grid.Row/Column устанавливается автоматически при перестроении Grid) -->
|
<!-- Splitter -->
|
||||||
<controls:LatticeSplitter x:Name="PART_Splitter" />
|
<controls:LatticeSplitter x:Name="PART_Splitter"
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.Row="0"/>
|
||||||
|
|
||||||
<!-- Вторая область -->
|
<!-- Second child area -->
|
||||||
<ContentControl x:Name="PART_Second"
|
<ContentControl x:Name="PART_Second"
|
||||||
Content="{Binding Second}"
|
Content="{Binding Second}"
|
||||||
ContentTemplateSelector="{StaticResource GlobalDockSelector}"
|
ContentTemplateSelector="{StaticResource GlobalDockSelector}"
|
||||||
@@ -80,58 +99,16 @@
|
|||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- 6. Стиль Листа -->
|
<!-- 6. Стиль TabControl -->
|
||||||
<Style TargetType="controls:LatticeDockLeaf">
|
<Style TargetType="controls:LatticeDockLeaf">
|
||||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
<Setter Property="Background" Value="{ThemeResource Lattice.Brush.Background.Primary}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{ThemeResource Lattice.Brush.Panel.Border}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="{ThemeResource Lattice.BorderThickness.Panel}"/>
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="controls:LatticeDockLeaf">
|
<ControlTemplate TargetType="controls:LatticeDockLeaf">
|
||||||
<!-- Grid и Border должны растягиваться -->
|
<Grid x:Name="PART_RootGrid" Background="{TemplateBinding Background}">
|
||||||
<Grid Margin="{ThemeResource Lattice.Thickness.PanelMargin}" VerticalAlignment="Stretch">
|
<!-- Tab headers -->
|
||||||
<Border Background="{ThemeResource Lattice.Brush.Background.Primary}"
|
|
||||||
BorderBrush="{ThemeResource Lattice.Brush.Panel.Border}"
|
|
||||||
BorderThickness="{ThemeResource Lattice.Thickness.PanelBorder}"
|
|
||||||
CornerRadius="{ThemeResource Lattice.Geometry.PanelCornerRadius}"
|
|
||||||
VerticalAlignment="Stretch">
|
|
||||||
|
|
||||||
<!-- Используем кастомный TabControl или оставляем стандартный -->
|
|
||||||
<TabView x:Name="PART_TabView"
|
|
||||||
TabItemsSource="{Binding Children}"
|
|
||||||
SelectedItem="{Binding ActiveContent, Mode=TwoWay}"
|
|
||||||
IsAddTabButtonVisible="False"
|
|
||||||
VerticalAlignment="Stretch"
|
|
||||||
VerticalContentAlignment="Stretch"
|
|
||||||
TabWidthMode="SizeToContent"
|
|
||||||
Padding="0">
|
|
||||||
|
|
||||||
<TabView.TabItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<TabViewItem Header="{Binding Title}" FontSize="11" Height="28" MinWidth="0" >
|
|
||||||
<!-- ContentPresenter ДОЛЖЕН иметь VerticalAlignment="Stretch" -->
|
|
||||||
<ContentPresenter Content="{Binding View}"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Stretch" />
|
|
||||||
</TabViewItem>
|
|
||||||
</DataTemplate>
|
|
||||||
</TabView.TabItemTemplate>
|
|
||||||
</TabView>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- Добавить в Generic.xaml -->
|
|
||||||
<Style TargetType="controls:LatticeTabControl">
|
|
||||||
<Setter Property="Background" Value="{ThemeResource Lattice.Brush.Background.Primary}"/>
|
|
||||||
<Setter Property="BorderBrush" Value="{ThemeResource Lattice.Brush.Panel.Border}"/>
|
|
||||||
<Setter Property="BorderThickness" Value="{ThemeResource Lattice.Thickness.PanelBorder}"/>
|
|
||||||
<Setter Property="Template">
|
|
||||||
<Setter.Value>
|
|
||||||
<ControlTemplate TargetType="controls:LatticeTabControl">
|
|
||||||
<Grid x:Name="PART_RootGrid">
|
|
||||||
<!-- Заголовки вкладок -->
|
|
||||||
<ListBox x:Name="PART_TabHeaderList"
|
<ListBox x:Name="PART_TabHeaderList"
|
||||||
Background="{TemplateBinding Background}"
|
Background="{TemplateBinding Background}"
|
||||||
BorderBrush="{TemplateBinding BorderBrush}"
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
@@ -142,18 +119,46 @@
|
|||||||
<Style TargetType="ListBoxItem">
|
<Style TargetType="ListBoxItem">
|
||||||
<Setter Property="Background" Value="Transparent"/>
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
<Setter Property="BorderBrush" Value="{ThemeResource Lattice.Brush.Accent.Action}"/>
|
<Setter Property="BorderBrush" Value="{ThemeResource Lattice.Brush.Accent.Action}"/>
|
||||||
<Setter Property="BorderThickness" Value="0,0,0,2"/>
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
<Setter Property="Margin" Value="0,0,4,0"/>
|
<Setter Property="Margin" Value="0,0,4,0"/>
|
||||||
<Setter Property="Padding" Value="0"/>
|
<Setter Property="Padding" Value="0"/>
|
||||||
<Style.Triggers>
|
<!-- Используем VisualStateManager вместо триггеров -->
|
||||||
<Trigger Property="IsSelected" Value="True">
|
<Setter Property="Template">
|
||||||
<Setter Property="BorderThickness" Value="0,0,0,2"/>
|
<Setter.Value>
|
||||||
<Setter Property="Foreground" Value="{ThemeResource Lattice.Brush.Accent.Action}"/>
|
<ControlTemplate TargetType="ListBoxItem">
|
||||||
</Trigger>
|
<Grid>
|
||||||
<Trigger Property="IsPointerOver" Value="True">
|
<VisualStateManager.VisualStateGroups>
|
||||||
<Setter Property="Background" Value="{ThemeResource SystemControlBackgroundListLowBrush}"/>
|
<VisualStateGroup x:Name="CommonStates">
|
||||||
</Trigger>
|
<VisualState x:Name="Normal"/>
|
||||||
</Style.Triggers>
|
<VisualState x:Name="Selected">
|
||||||
|
<VisualState.Setters>
|
||||||
|
<Setter Target="Border.BorderThickness" Value="0,0,0,2"/>
|
||||||
|
<Setter Target="ContentPresenter.Foreground" Value="{ThemeResource Lattice.Brush.Accent.Action}"/>
|
||||||
|
</VisualState.Setters>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="PointerOver">
|
||||||
|
<VisualState.Setters>
|
||||||
|
<Setter Target="Border.Background" Value="{ThemeResource Lattice.Brush.Background.Secondary}"/>
|
||||||
|
</VisualState.Setters>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
</VisualStateManager.VisualStateGroups>
|
||||||
|
<Border x:Name="Border"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
Margin="{TemplateBinding Margin}">
|
||||||
|
<ContentPresenter x:Name="ContentPresenter"
|
||||||
|
Content="{TemplateBinding Content}"
|
||||||
|
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||||
|
Padding="{TemplateBinding Padding}"
|
||||||
|
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||||
|
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
</ListBox.ItemContainerStyle>
|
</ListBox.ItemContainerStyle>
|
||||||
<ListBox.ItemsPanel>
|
<ListBox.ItemsPanel>
|
||||||
@@ -163,7 +168,7 @@
|
|||||||
</ListBox.ItemsPanel>
|
</ListBox.ItemsPanel>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
|
|
||||||
<!-- Контент вкладки -->
|
<!-- Tab content -->
|
||||||
<ContentControl x:Name="PART_ContentControl"
|
<ContentControl x:Name="PART_ContentControl"
|
||||||
Background="{TemplateBinding Background}"
|
Background="{TemplateBinding Background}"
|
||||||
HorizontalContentAlignment="Stretch"
|
HorizontalContentAlignment="Stretch"
|
||||||
@@ -174,21 +179,28 @@
|
|||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- 7. Ресурсы по умолчанию (если тема не загружена) -->
|
<!-- 7. Ресурсы по умолчанию -->
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<!-- Значения по умолчанию -->
|
<!-- Цвета по умолчанию -->
|
||||||
<SolidColorBrush x:Key="Lattice.Brush.Background.Primary" Color="#1E1E1E" />
|
<SolidColorBrush x:Key="Lattice.Brush.Background.Primary" Color="#1E1E1E" />
|
||||||
<SolidColorBrush x:Key="Lattice.Brush.Background.Secondary" Color="#252526" />
|
<SolidColorBrush x:Key="Lattice.Brush.Background.Secondary" Color="#252526" />
|
||||||
<SolidColorBrush x:Key="Lattice.Brush.Panel.Border" Color="#3F3F46" />
|
<SolidColorBrush x:Key="Lattice.Brush.Panel.Border" Color="#3F3F46" />
|
||||||
<SolidColorBrush x:Key="Lattice.Brush.Splitter.Normal" Color="#2D2D2D" />
|
<SolidColorBrush x:Key="Lattice.Brush.Splitter.Normal" Color="#2D2D2D" />
|
||||||
<SolidColorBrush x:Key="Lattice.Brush.Splitter.Hover" Color="#007ACC" />
|
|
||||||
<SolidColorBrush x:Key="Lattice.Brush.Accent.Action" Color="#007ACC" />
|
<SolidColorBrush x:Key="Lattice.Brush.Accent.Action" Color="#007ACC" />
|
||||||
|
<SolidColorBrush x:Key="Lattice.Brush.Accent" Color="#007ACC" />
|
||||||
|
<SolidColorBrush x:Key="Lattice.Brush.Text.Primary" Color="#FFFFFF" />
|
||||||
|
|
||||||
<CornerRadius x:Key="Lattice.Geometry.PanelCornerRadius">0</CornerRadius>
|
<!-- Геометрия -->
|
||||||
<Thickness x:Key="Lattice.Thickness.PanelMargin">0,0,1,1</Thickness>
|
<CornerRadius x:Key="Lattice.CornerRadius.Panel">4</CornerRadius>
|
||||||
<Thickness x:Key="Lattice.Thickness.PanelBorder">1</Thickness>
|
<x:Double x:Key="Lattice.Size.Splitter.Width">6</x:Double>
|
||||||
<x:Double x:Key="Lattice.Size.SplitterWidth">1</x:Double>
|
|
||||||
|
<!-- Толщины -->
|
||||||
|
<Thickness x:Key="Lattice.BorderThickness.Panel">1</Thickness>
|
||||||
|
<Thickness x:Key="Lattice.Thickness.PanelMargin">2</Thickness>
|
||||||
|
|
||||||
|
<!-- Отступы -->
|
||||||
|
<x:Double x:Key="Lattice.Spacing.Panel">8</x:Double>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
|
||||||
|
|||||||
@@ -77,21 +77,23 @@ public interface IAutoHidePanelControl : IDockControl
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Задает фиксированное состояние панели.
|
/// Задает фиксированное состояние панели.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pinned">true, чтобы зафиксировать панель; false, чтобы разрешить автоскрытие.</param>
|
/// <param name="pinned">
|
||||||
|
/// true, чтобы зафиксировать панель; false, чтобы разрешить автоскрытие.
|
||||||
|
/// </param>
|
||||||
void SetPinned(bool pinned);
|
void SetPinned(bool pinned);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Событие, возникающее при изменении видимости панели.
|
/// Происходит при изменении видимости панели.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event EventHandler VisibilityChanged;
|
event EventHandler VisibilityChanged;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Событие, возникающее при наведении курсора на панель.
|
/// Происходит при наведении курсора на панель.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event EventHandler MouseEntered;
|
event EventHandler MouseEntered;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Событие, возникающее при уходе курсора с панели.
|
/// Происходит при уходе курсора с панели.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event EventHandler MouseLeft;
|
event EventHandler MouseLeft;
|
||||||
}
|
}
|
||||||
@@ -1,40 +1,58 @@
|
|||||||
namespace Lattice.UI.Docking.Abstractions;
|
namespace Lattice.UI.Docking.Abstractions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Определяет контракт для команды док-системы.
|
/// Определяет контракт для команды в UI-слое док-системы.
|
||||||
|
/// Команды представляют действия, которые могут быть выполнены пользователем.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IDockCommand
|
public interface IDockCommand
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает идентификатор команды.
|
/// Получает уникальный идентификатор команды.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Строковый идентификатор команды.
|
||||||
|
/// </value>
|
||||||
string Id { get; }
|
string Id { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает отображаемое имя команды.
|
/// Получает отображаемое имя команды.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Имя команды, отображаемое в пользовательском интерфейсе.
|
||||||
|
/// </value>
|
||||||
string Name { get; }
|
string Name { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает описание команды.
|
/// Получает описание команды.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Текстовое описание функциональности команды.
|
||||||
|
/// </value>
|
||||||
string Description { get; }
|
string Description { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает значок команды.
|
/// Получает идентификатор ресурса для иконки команды.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Имя ресурса иконки или путь к файлу иконки.
|
||||||
|
/// </value>
|
||||||
string Icon { get; }
|
string Icon { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает комбинацию клавиш для команды.
|
/// Получает комбинацию клавиш для быстрого вызова команды.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Строковое представление горячей клавиши (например, "Ctrl+S").
|
||||||
|
/// </value>
|
||||||
string Shortcut { get; }
|
string Shortcut { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Определяет, можно ли выполнить команду.
|
/// Определяет, можно ли выполнить команду в текущем контексте.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="parameter">Параметр команды.</param>
|
/// <param name="parameter">Параметр команды.</param>
|
||||||
/// <returns>true, если команду можно выполнить; в противном случае — false.</returns>
|
/// <returns>
|
||||||
|
/// true, если команду можно выполнить; в противном случае false.
|
||||||
|
/// </returns>
|
||||||
bool CanExecute(object? parameter);
|
bool CanExecute(object? parameter);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -44,7 +62,7 @@ public interface IDockCommand
|
|||||||
void Execute(object? parameter);
|
void Execute(object? parameter);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Событие, возникающее при изменении возможности выполнения команды.
|
/// Происходит при изменении возможности выполнения команды.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event EventHandler CanExecuteChanged;
|
event EventHandler CanExecuteChanged;
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,9 @@ public interface IDockContextManager
|
|||||||
/// <param name="element">Элемент, для которого показывается меню.</param>
|
/// <param name="element">Элемент, для которого показывается меню.</param>
|
||||||
/// <param name="x">Координата X для отображения меню.</param>
|
/// <param name="x">Координата X для отображения меню.</param>
|
||||||
/// <param name="y">Координата Y для отображения меню.</param>
|
/// <param name="y">Координата Y для отображения меню.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, если <paramref name="element"/> равен null.
|
||||||
|
/// </exception>
|
||||||
void ShowContextMenu(IDockControl element, double x, double y);
|
void ShowContextMenu(IDockControl element, double x, double y);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -23,6 +26,10 @@ public interface IDockContextManager
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="commandId">Идентификатор команды.</param>
|
/// <param name="commandId">Идентификатор команды.</param>
|
||||||
/// <param name="command">Команда для регистрации.</param>
|
/// <param name="command">Команда для регистрации.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, если <paramref name="commandId"/> или <paramref name="command"/>
|
||||||
|
/// равны null.
|
||||||
|
/// </exception>
|
||||||
void RegisterCommand(string commandId, IDockCommand command);
|
void RegisterCommand(string commandId, IDockCommand command);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -32,12 +39,12 @@ public interface IDockContextManager
|
|||||||
void UnregisterCommand(string commandId);
|
void UnregisterCommand(string commandId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Событие, возникающее при показе контекстного меню.
|
/// Происходит при показе контекстного меню.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event EventHandler<ContextMenuShownEventArgs> ContextMenuShown;
|
event EventHandler<ContextMenuShownEventArgs> ContextMenuShown;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Событие, возникающее при скрытии контекстного меню.
|
/// Происходит при скрытии контекстного меню.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event EventHandler ContextMenuHidden;
|
event EventHandler ContextMenuHidden;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Lattice.Core.Docking.Abstractions;
|
using Lattice.Core.Docking.Abstractions;
|
||||||
using Lattice.Core.Docking.Engine;
|
using Lattice.Core.Docking.Engine;
|
||||||
|
using Lattice.Core.Docking.Models;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
|
||||||
namespace Lattice.UI.Docking.Abstractions;
|
namespace Lattice.UI.Docking.Abstractions;
|
||||||
@@ -7,11 +8,12 @@ namespace Lattice.UI.Docking.Abstractions;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Определяет базовый контракт для всех UI-контролов, участвующих в системе докинга.
|
/// Определяет базовый контракт для всех UI-контролов, участвующих в системе докинга.
|
||||||
/// Этот интерфейс предоставляет общие свойства и методы, необходимые для интеграции
|
/// Этот интерфейс предоставляет общие свойства и методы, необходимые для интеграции
|
||||||
/// с менеджером макета и системой перетаскивания.
|
/// с менеджером макета и UI-сервисами.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Реализации этого интерфейса должны отображать элементы док-системы (DockGroup, DockLeaf)
|
/// Реализации этого интерфейса должны отображать элементы док-системы (DockGroup, DockLeaf)
|
||||||
/// и обеспечивать взаимодействие пользователя с ними через жесты мыши, клавиатуру и сенсорный ввод.
|
/// и обеспечивать взаимодействие пользователя с ними через жесты мыши, клавиатуру и сенсорный ввод.
|
||||||
|
/// Интерфейс обеспечивает двухстороннюю связь между визуальными элементами и их моделями данных.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public interface IDockControl : INotifyPropertyChanged
|
public interface IDockControl : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
@@ -22,6 +24,10 @@ public interface IDockControl : INotifyPropertyChanged
|
|||||||
/// Экземпляр класса, реализующего <see cref="IDockElement"/>, который представляет
|
/// Экземпляр класса, реализующего <see cref="IDockElement"/>, который представляет
|
||||||
/// состояние и структуру отображаемого элемента док-системы.
|
/// состояние и структуру отображаемого элемента док-системы.
|
||||||
/// </value>
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Изменение модели должно приводить к обновлению визуального представления.
|
||||||
|
/// Свойство используется для привязки данных между UI-слоем и слоем бизнес-логики.
|
||||||
|
/// </remarks>
|
||||||
IDockElement? Model { get; set; }
|
IDockElement? Model { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -30,55 +36,68 @@ public interface IDockControl : INotifyPropertyChanged
|
|||||||
/// <value>
|
/// <value>
|
||||||
/// Экземпляр <see cref="LayoutManager"/>, управляющий структурой док-системы.
|
/// Экземпляр <see cref="LayoutManager"/>, управляющий структурой док-системы.
|
||||||
/// </value>
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Менеджер макета предоставляет доступ к дереву компоновки, плавающим окнам
|
||||||
|
/// и автоскрываемым панелям, а также методы для манипуляции структурой.
|
||||||
|
/// </remarks>
|
||||||
LayoutManager? LayoutManager { get; set; }
|
LayoutManager? LayoutManager { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает сервис перетаскивания, используемый этим контролом.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Реализация <see cref="IDragDropService"/> для обработки операций перетаскивания.
|
|
||||||
/// </value>
|
|
||||||
IDragDropService? DragDropService { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает или задает контекстный менеджер для этого контрола.
|
/// Получает или задает контекстный менеджер для этого контрола.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
/// <value>
|
||||||
/// Экземпляр <see cref="IDockContextManager"/>, управляющий контекстными меню и действиями.
|
/// Экземпляр <see cref="IDockContextManager"/>, управляющий контекстными меню и действиями.
|
||||||
/// </value>
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Контекстный менеджер используется для отображения контекстно-зависимых команд
|
||||||
|
/// при щелчке правой кнопкой мыши или других пользовательских действиях.
|
||||||
|
/// </remarks>
|
||||||
IDockContextManager? ContextManager { get; set; }
|
IDockContextManager? ContextManager { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает или задает признак того, что контрол выбран.
|
/// Получает или задает признак того, что контрол выбран.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
/// <value>
|
||||||
/// true, если контрол выбран; в противном случае — false.
|
/// true, если контрол выбран; в противном случае false.
|
||||||
/// </value>
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Выделение контрола обычно визуально выделяет его границы или фон,
|
||||||
|
/// чтобы указать пользователю на активный элемент. В каждый момент времени
|
||||||
|
/// может быть выбран только один контрол в пределах контейнера.
|
||||||
|
/// </remarks>
|
||||||
bool IsSelected { get; set; }
|
bool IsSelected { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает или задает признак того, что контрол активен.
|
/// Получает или задает признак того, что контрол активен.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
/// <value>
|
||||||
/// true, если контрол активен; в противном случае — false.
|
/// true, если контрол активен; в противном случае false.
|
||||||
/// </value>
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// Активный контрол получает фокус ввода и может обрабатывать команды клавиатуры.
|
||||||
|
/// Обычно соответствует активной вкладке в контейнере или активному окну.
|
||||||
|
/// </remarks>
|
||||||
bool IsActive { get; set; }
|
bool IsActive { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает или задает признак того, что контрол можно перетаскивать.
|
/// Получает признак того, что элемент можно перетаскивать.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
bool CanDrag { get; }
|
||||||
/// true, если контрол можно перетаскивать; в противном случае — false.
|
|
||||||
/// </value>
|
|
||||||
bool CanDrag { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает или задает признак того, что контрол может принимать сброс.
|
/// Получает признак того, что на элемент можно сбрасывать.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
bool CanDrop { get; }
|
||||||
/// true, если контрол может принимать сброс; в противном случае — false.
|
|
||||||
/// </value>
|
/// <summary>
|
||||||
bool CanDrop { get; set; }
|
/// Подготавливает данные для перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
object? PrepareDragData();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Обрабатывает сброс данных.
|
||||||
|
/// </summary>
|
||||||
|
bool HandleDrop(object data, DockPosition position);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Обновляет внешний вид контрола в соответствии с текущим состоянием модели.
|
/// Обновляет внешний вид контрола в соответствии с текущим состоянием модели.
|
||||||
@@ -86,6 +105,8 @@ public interface IDockControl : INotifyPropertyChanged
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Этот метод должен вызываться при изменении свойств модели или при необходимости
|
/// Этот метод должен вызываться при изменении свойств модели или при необходимости
|
||||||
/// принудительного обновления UI (например, после изменения темы или масштаба).
|
/// принудительного обновления UI (например, после изменения темы или масштаба).
|
||||||
|
/// Реализация должна обеспечить синхронизацию всех визуальных аспектов контрола
|
||||||
|
/// с текущими значениями свойств модели.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
void Refresh();
|
void Refresh();
|
||||||
|
|
||||||
@@ -93,11 +114,23 @@ public interface IDockControl : INotifyPropertyChanged
|
|||||||
/// Применяет указанную тему к контролу.
|
/// Применяет указанную тему к контролу.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="theme">Тема для применения.</param>
|
/// <param name="theme">Тема для применения.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, если <paramref name="theme"/> равен null.
|
||||||
|
/// </exception>
|
||||||
|
/// <remarks>
|
||||||
|
/// Метод должен обновить все стили, цвета и параметры отображения контрола
|
||||||
|
/// в соответствии с переданной темой. Изменения должны применяться немедленно.
|
||||||
|
/// </remarks>
|
||||||
void ApplyTheme(IDockTheme theme);
|
void ApplyTheme(IDockTheme theme);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Вызывается при изменении состояния модели для обновления UI.
|
/// Вызывается при изменении состояния модели для обновления UI.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="propertyName">Имя изменившегося свойства модели.</param>
|
/// <param name="propertyName">Имя изменившегося свойства модели.</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот метод предназначен для уведомления UI о конкретных изменениях в модели,
|
||||||
|
/// что позволяет выполнять точечные обновления вместо полного перестроения.
|
||||||
|
/// Должен вызываться из обработчиков событий изменения свойств модели.
|
||||||
|
/// </remarks>
|
||||||
void OnModelPropertyChanged(string propertyName);
|
void OnModelPropertyChanged(string propertyName);
|
||||||
}
|
}
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
namespace Lattice.UI.Docking.Abstractions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Предоставляет сервис для операций перетаскивания в UI-слое док-системы.
|
|
||||||
/// Абстрагирует платформенно-зависимую логику перетаскивания и обеспечивает
|
|
||||||
/// единый интерфейс для управления операциями drag-and-drop.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот интерфейс служит мостом между базовым менеджером перетаскивания из Core
|
|
||||||
/// и UI-контролами, добавляя визуальную обратную связь и обработку событий,
|
|
||||||
/// специфичных для пользовательского интерфейса.
|
|
||||||
/// </remarks>
|
|
||||||
public interface IDockDragDropService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Начинает операцию перетаскивания для указанного элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="element">UI-контрол, который инициирует перетаскивание.</param>
|
|
||||||
/// <param name="dragInfo">
|
|
||||||
/// Информация о перетаскивании, содержащая данные и параметры операции.
|
|
||||||
/// </param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="element"/> или <paramref name="dragInfo"/> равны null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот метод должен создавать визуальное представление перетаскиваемого элемента
|
|
||||||
/// и инициировать отслеживание перемещения мыши.
|
|
||||||
/// </remarks>
|
|
||||||
void StartDrag(IDockControl element, Core.DragDrop.Models.DragInfo dragInfo);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Обновляет позицию текущей операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">Новая координата X курсора в экранных координатах.</param>
|
|
||||||
/// <param name="y">Новая координата Y курсора в экранных координатах.</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Вызывается при каждом перемещении мыши во время операции перетаскивания.
|
|
||||||
/// Должен обновлять позицию визуального представления и проверять возможные цели сброса.
|
|
||||||
/// </remarks>
|
|
||||||
void UpdateDrag(double x, double y);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Завершает текущую операцию перетаскивания в указанной позиции.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">Координата X завершения перетаскивания.</param>
|
|
||||||
/// <param name="y">Координата Y завершения перетаскивания.</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Выполняет сброс данных на текущую цель (если она есть) и очищает ресурсы,
|
|
||||||
/// выделенные для операции перетаскивания.
|
|
||||||
/// </remarks>
|
|
||||||
void EndDrag(double x, double y);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Отменяет текущую операцию перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Вызывается при нажатии клавиши Escape или других действиях, приводящих к отмене.
|
|
||||||
/// Должен восстанавливать исходное состояние элементов и очищать ресурсы.
|
|
||||||
/// </remarks>
|
|
||||||
void CancelDrag();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Показывает визуальную подсказку о возможной позиции сброса.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="element">UI-контрол, для которого показывается подсказка.</param>
|
|
||||||
/// <param name="position">Предполагаемая позиция сброса.</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Используется для визуальной обратной связи, чтобы пользователь видел,
|
|
||||||
/// куда будет помещен элемент при отпускании кнопки мыши.
|
|
||||||
/// </remarks>
|
|
||||||
void ShowDropHint(IDockControl element, Models.DropPosition position);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Скрывает текущую визуальную подсказку о сбросе.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Вызывается, когда курсор покидает допустимую область сброса
|
|
||||||
/// или операция перетаскивания завершается.
|
|
||||||
/// </remarks>
|
|
||||||
void HideDropHint();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Событие, возникающее при начале операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
event EventHandler<DragStartedEventArgs> DragStarted;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Событие, возникающее при обновлении позиции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
event EventHandler<DragUpdatedEventArgs> DragUpdated;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Событие, возникающее при завершении операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
event EventHandler<DragCompletedEventArgs> DragCompleted;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Событие, возникающее при отмене операции перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
event EventHandler DragCancelled;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Предоставляет данные для события начала перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public class DragStartedEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Получает UI-контрол, который инициировал перетаскивание.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="IDockControl"/>, представляющий источник перетаскивания.
|
|
||||||
/// Может быть null, если перетаскивание инициировано не из UI-элемента.
|
|
||||||
/// </value>
|
|
||||||
public IDockControl? Source { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает информацию о перетаскивании.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="Core.DragDrop.Models.DragInfo"/> с данными перетаскивания.
|
|
||||||
/// </value>
|
|
||||||
public Core.DragDrop.Models.DragInfo DragInfo { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DragStartedEventArgs"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="source">Источник перетаскивания.</param>
|
|
||||||
/// <param name="dragInfo">Информация о перетаскивании.</param>
|
|
||||||
public DragStartedEventArgs(IDockControl? source, Core.DragDrop.Models.DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
Source = source;
|
|
||||||
DragInfo = dragInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Предоставляет данные для события обновления перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public class DragUpdatedEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Получает UI-контрол, который инициировал перетаскивание.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="IDockControl"/>, представляющий источник перетаскивания.
|
|
||||||
/// </value>
|
|
||||||
public IDockControl? Source { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает текущую координату X курсора.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Координата X в экранных координатах.
|
|
||||||
/// </value>
|
|
||||||
public double X { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает текущую координату Y курсора.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Координата Y в экранных координатах.
|
|
||||||
/// </value>
|
|
||||||
public double Y { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает информацию о перетаскивании.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="Core.DragDrop.Models.DragInfo"/> с текущими данными перетаскивания.
|
|
||||||
/// </value>
|
|
||||||
public Core.DragDrop.Models.DragInfo DragInfo { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DragUpdatedEventArgs"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="source">Источник перетаскивания.</param>
|
|
||||||
/// <param name="x">Текущая координата X.</param>
|
|
||||||
/// <param name="y">Текущая координата Y.</param>
|
|
||||||
/// <param name="dragInfo">Информация о перетаскивании.</param>
|
|
||||||
public DragUpdatedEventArgs(IDockControl? source, double x, double y, Core.DragDrop.Models.DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
Source = source;
|
|
||||||
X = x;
|
|
||||||
Y = y;
|
|
||||||
DragInfo = dragInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Предоставляет данные для события завершения перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public class DragCompletedEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Получает UI-контрол, который инициировал перетаскивание.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="IDockControl"/>, представляющий источник перетаскивания.
|
|
||||||
/// Может быть null, если операция была инициирована не из UI.
|
|
||||||
/// </value>
|
|
||||||
public IDockControl? Source { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает UI-контрол, на который был выполнен сброс.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="IDockControl"/>, представляющий цель сброса.
|
|
||||||
/// Может быть null, если сброс был выполнен вне допустимой области.
|
|
||||||
/// </value>
|
|
||||||
public IDockControl? Target { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает позицию сброса относительно целевого элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Значение перечисления <see cref="DropPosition"/>, указывающее позицию сброса.
|
|
||||||
/// </value>
|
|
||||||
public Models.DropPosition DropPosition { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает информацию о перетаскивании.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="Core.DragDrop.Models.DragInfo"/> с данными завершенной операции.
|
|
||||||
/// Может быть null, если операция была отменена.
|
|
||||||
/// </value>
|
|
||||||
public Core.DragDrop.Models.DragInfo? DragInfo { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает значение, указывающее успешность операции сброса.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// true, если данные были успешно сброшены на цель; false, если операция была отменена
|
|
||||||
/// или сброс не был выполнен.
|
|
||||||
/// </value>
|
|
||||||
public bool Success { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DragCompletedEventArgs"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="source">Источник перетаскивания.</param>
|
|
||||||
/// <param name="target">Цель сброса.</param>
|
|
||||||
/// <param name="dropPosition">Позиция сброса.</param>
|
|
||||||
/// <param name="dragInfo">Информация о перетаскивании.</param>
|
|
||||||
/// <param name="success">Признак успешности операции.</param>
|
|
||||||
public DragCompletedEventArgs(IDockControl? source, IDockControl? target,
|
|
||||||
Models.DropPosition dropPosition, Core.DragDrop.Models.DragInfo? dragInfo, bool success)
|
|
||||||
{
|
|
||||||
Source = source;
|
|
||||||
Target = target;
|
|
||||||
DropPosition = dropPosition;
|
|
||||||
DragInfo = dragInfo;
|
|
||||||
Success = success;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -60,23 +60,23 @@ public interface IDockGroupControl : IDockControl
|
|||||||
void SetChildren(IDockControl? firstChild, IDockControl? secondChild);
|
void SetChildren(IDockControl? firstChild, IDockControl? secondChild);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Событие, возникающее при изменении соотношения разделения.
|
/// Происходит при изменении соотношения разделения.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event EventHandler<SplitRatioChangedEventArgs> SplitRatioChanged;
|
event EventHandler<SplitRatioChangedEventArgs> SplitRatioChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Аргументы события изменения соотношения разделения.
|
/// Предоставляет данные для события изменения соотношения разделения.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SplitRatioChangedEventArgs : EventArgs
|
public class SplitRatioChangedEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Новое соотношение разделения.
|
/// Получает новое соотношение разделения.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double NewRatio { get; }
|
public double NewRatio { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Источник изменения (пользователь или программа).
|
/// Получает источник изменения соотношения разделения.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SplitRatioChangeSource Source { get; }
|
public SplitRatioChangeSource Source { get; }
|
||||||
|
|
||||||
@@ -93,16 +93,22 @@ public class SplitRatioChangedEventArgs : EventArgs
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Источник изменения соотношения разделения.
|
/// Определяет источник изменения соотношения разделения.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum SplitRatioChangeSource
|
public enum SplitRatioChangeSource
|
||||||
{
|
{
|
||||||
/// <summary>Изменение выполнено пользователем.</summary>
|
/// <summary>
|
||||||
|
/// Изменение выполнено пользователем.
|
||||||
|
/// </summary>
|
||||||
User,
|
User,
|
||||||
|
|
||||||
/// <summary>Изменение выполнено программой.</summary>
|
/// <summary>
|
||||||
|
/// Изменение выполнено программой.
|
||||||
|
/// </summary>
|
||||||
Programmatic,
|
Programmatic,
|
||||||
|
|
||||||
/// <summary>Изменение выполнено при восстановлении состояния.</summary>
|
/// <summary>
|
||||||
|
/// Изменение выполнено при восстановлении состояния.
|
||||||
|
/// </summary>
|
||||||
Restore
|
Restore
|
||||||
}
|
}
|
||||||
@@ -10,8 +10,7 @@ namespace Lattice.UI.Docking.Abstractions;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Реализации этого интерфейса представляют собой центральный координатор UI-слоя,
|
/// Реализации этого интерфейса представляют собой центральный координатор UI-слоя,
|
||||||
/// который интегрирует функциональность менеджера макета, системы перетаскивания
|
/// который интегрирует функциональность менеджера макета и контекстных меню в единый визуальный компонент.
|
||||||
/// и контекстных меню в единый визуальный компонент.
|
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public interface IDockHost : IDockControl
|
public interface IDockHost : IDockControl
|
||||||
{
|
{
|
||||||
@@ -23,8 +22,7 @@ public interface IDockHost : IDockControl
|
|||||||
/// представляющих все активные плавающие окна в системе.
|
/// представляющих все активные плавающие окна в системе.
|
||||||
/// </value>
|
/// </value>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Плавающие окна могут быть созданы пользователем путем перетаскивания элементов
|
/// Плавающие окна могут быть созданы пользователем или программно через методы API.
|
||||||
/// за пределы основного окна или программно через методы API.
|
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
IEnumerable<IFloatingWindowControl> FloatingWindows { get; }
|
IEnumerable<IFloatingWindowControl> FloatingWindows { get; }
|
||||||
|
|
||||||
@@ -45,7 +43,7 @@ public interface IDockHost : IDockControl
|
|||||||
/// Получает или задает значение, указывающее, отображается ли панель инструментов (Toolbox).
|
/// Получает или задает значение, указывающее, отображается ли панель инструментов (Toolbox).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
/// <value>
|
||||||
/// true, если панель инструментов видима; в противном случае — false.
|
/// true, если панель инструментов видима; в противном случае false.
|
||||||
/// Значение по умолчанию зависит от реализации.
|
/// Значение по умолчанию зависит от реализации.
|
||||||
/// </value>
|
/// </value>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
@@ -58,7 +56,7 @@ public interface IDockHost : IDockControl
|
|||||||
/// Получает или задает значение, указывающее, отображается ли строка состояния.
|
/// Получает или задает значение, указывающее, отображается ли строка состояния.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
/// <value>
|
||||||
/// true, если строка состояния видима; в противном случае — false.
|
/// true, если строка состояния видима; в противном случае false.
|
||||||
/// Значение по умолчанию зависит от реализации.
|
/// Значение по умолчанию зависит от реализации.
|
||||||
/// </value>
|
/// </value>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
@@ -71,9 +69,9 @@ public interface IDockHost : IDockControl
|
|||||||
/// Получает или задает значение, указывающее, отображается ли главное меню приложения.
|
/// Получает или задает значение, указывающее, отображается ли главное меню приложения.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
/// <value>
|
||||||
/// true, если главное меню видимо; в противном случае — false.
|
/// true, если главное меню видимо; в противном случае false.
|
||||||
/// Значение по умолчанию зависит от реализации.
|
/// Значение по умолчанию зависит от реализации.
|
||||||
/// </remarks>
|
/// </value>
|
||||||
bool ShowMenu { get; set; }
|
bool ShowMenu { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -148,7 +146,7 @@ public interface IDockHost : IDockControl
|
|||||||
void RemoveAutoHidePanel(IAutoHidePanelControl panel);
|
void RemoveAutoHidePanel(IAutoHidePanelControl panel);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Событие, возникающее при изменении структуры макета док-системы.
|
/// Происходит при изменении структуры макета док-системы.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Может вызываться при добавлении/удалении элементов, изменении размеров,
|
/// Может вызываться при добавлении/удалении элементов, изменении размеров,
|
||||||
@@ -157,12 +155,12 @@ public interface IDockHost : IDockControl
|
|||||||
event EventHandler LayoutChanged;
|
event EventHandler LayoutChanged;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Событие, возникающее при создании нового плавающего окна.
|
/// Происходит при создании нового плавающего окна.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event EventHandler<FloatingWindowCreatedEventArgs> FloatingWindowCreated;
|
event EventHandler<FloatingWindowCreatedEventArgs> FloatingWindowCreated;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Событие, возникающее при закрытии плавающего окна.
|
/// Происходит при закрытии плавающего окна.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event EventHandler<FloatingWindowClosedEventArgs> FloatingWindowClosed;
|
event EventHandler<FloatingWindowClosedEventArgs> FloatingWindowClosed;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public interface IDockLeafControl : IDockControl
|
|||||||
/// Получает или задает признак отображения кнопки закрытия на вкладках.
|
/// Получает или задает признак отображения кнопки закрытия на вкладках.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
/// <value>
|
||||||
/// true, если кнопки закрытия отображаются; в противном случае — false.
|
/// true, если кнопки закрытия отображаются; в противном случае false.
|
||||||
/// </value>
|
/// </value>
|
||||||
bool ShowCloseButtons { get; set; }
|
bool ShowCloseButtons { get; set; }
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ public interface IDockLeafControl : IDockControl
|
|||||||
/// Получает или задает признак возможности изменения порядка вкладок.
|
/// Получает или задает признак возможности изменения порядка вкладок.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
/// <value>
|
||||||
/// true, если порядок вкладок можно изменять; в противном случае — false.
|
/// true, если порядок вкладок можно изменять; в противном случае false.
|
||||||
/// </value>
|
/// </value>
|
||||||
bool CanReorderTabs { get; set; }
|
bool CanReorderTabs { get; set; }
|
||||||
|
|
||||||
@@ -50,25 +50,39 @@ public interface IDockLeafControl : IDockControl
|
|||||||
/// Добавляет вкладку в контрол.
|
/// Добавляет вкладку в контрол.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="content">Контент для добавления.</param>
|
/// <param name="content">Контент для добавления.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, если <paramref name="content"/> равен null.
|
||||||
|
/// </exception>
|
||||||
void AddContent(IDockContent content);
|
void AddContent(IDockContent content);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Удаляет вкладку из контрола.
|
/// Удаляет вкладку из контрола.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="content">Контент для удаления.</param>
|
/// <param name="content">Контент для удаления.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, если <paramref name="content"/> равен null.
|
||||||
|
/// </exception>
|
||||||
void RemoveContent(IDockContent content);
|
void RemoveContent(IDockContent content);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Закрывает указанную вкладку.
|
/// Закрывает указанную вкладку.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="content">Контент для закрытия.</param>
|
/// <param name="content">Контент для закрытия.</param>
|
||||||
/// <returns>true, если вкладка была закрыта; в противном случае — false.</returns>
|
/// <returns>
|
||||||
|
/// true, если вкладка была закрыта; в противном случае false.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, если <paramref name="content"/> равен null.
|
||||||
|
/// </exception>
|
||||||
bool CloseContent(IDockContent content);
|
bool CloseContent(IDockContent content);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Закрывает все вкладки, кроме указанной.
|
/// Закрывает все вкладки, кроме указанной.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="exceptContent">Вкладка, которую нужно оставить открытой.</param>
|
/// <param name="exceptContent">Вкладка, которую нужно оставить открытой.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, если <paramref name="exceptContent"/> равен null.
|
||||||
|
/// </exception>
|
||||||
void CloseAllExcept(IDockContent exceptContent);
|
void CloseAllExcept(IDockContent exceptContent);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -77,39 +91,41 @@ public interface IDockLeafControl : IDockControl
|
|||||||
void CloseAll();
|
void CloseAll();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Событие, возникающее при изменении активной вкладки.
|
/// Происходит при изменении активной вкладки.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event EventHandler<ActiveContentChangedEventArgs> ActiveContentChanged;
|
event EventHandler<ActiveContentChangedEventArgs> ActiveContentChanged;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Событие, возникающее при запросе закрытия вкладки.
|
/// Происходит при запросе закрытия вкладки.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event EventHandler<ContentClosingEventArgs> ContentClosing;
|
event EventHandler<ContentClosingEventArgs> ContentClosing;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Событие, возникающее при изменении порядка вкладок.
|
/// Происходит при изменении порядка вкладок.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event EventHandler<TabsReorderedEventArgs> TabsReordered;
|
event EventHandler<TabsReorderedEventArgs> TabsReordered;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Аргументы события изменения активного контента.
|
/// Предоставляет данные для события изменения активного контента.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ActiveContentChangedEventArgs : EventArgs
|
public class ActiveContentChangedEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Предыдущий активный контент.
|
/// Получает предыдущий активный контент.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IDockContent? OldContent { get; }
|
public IDockContent? OldContent { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Новый активный контент.
|
/// Получает новый активный контент.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IDockContent? NewContent { get; }
|
public IDockContent? NewContent { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Инициализирует новый экземпляр класса <see cref="ActiveContentChangedEventArgs"/>.
|
/// Инициализирует новый экземпляр класса <see cref="ActiveContentChangedEventArgs"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="oldContent">Предыдущий активный контент.</param>
|
||||||
|
/// <param name="newContent">Новый активный контент.</param>
|
||||||
public ActiveContentChangedEventArgs(IDockContent? oldContent, IDockContent? newContent)
|
public ActiveContentChangedEventArgs(IDockContent? oldContent, IDockContent? newContent)
|
||||||
{
|
{
|
||||||
OldContent = oldContent;
|
OldContent = oldContent;
|
||||||
@@ -118,63 +134,79 @@ public class ActiveContentChangedEventArgs : EventArgs
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Аргументы события закрытия контента.
|
/// Предоставляет данные для события закрытия контента.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ContentClosingEventArgs : EventArgs
|
public class ContentClosingEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Контент, который закрывается.
|
/// Получает контент, который закрывается.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IDockContent Content { get; }
|
public IDockContent Content { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Показывает, можно ли отменить закрытие.
|
/// Получает или задает значение, указывающее, можно ли отменить закрытие.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// true, если закрытие можно отменить; в противном случае false.
|
||||||
|
/// </value>
|
||||||
public bool CanCancel { get; set; }
|
public bool CanCancel { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает или задает признак отмены закрытия.
|
/// Получает или задает признак отмены закрытия.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// true, если закрытие отменено; в противном случае false.
|
||||||
|
/// </value>
|
||||||
public bool Cancel { get; set; }
|
public bool Cancel { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Инициализирует новый экземпляр класса <see cref="ContentClosingEventArgs"/>.
|
/// Инициализирует новый экземпляр класса <see cref="ContentClosingEventArgs"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="content">Контент, который закрывается.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, если <paramref name="content"/> равен null.
|
||||||
|
/// </exception>
|
||||||
public ContentClosingEventArgs(IDockContent content)
|
public ContentClosingEventArgs(IDockContent content)
|
||||||
{
|
{
|
||||||
Content = content;
|
Content = content ?? throw new ArgumentNullException(nameof(content));
|
||||||
CanCancel = true;
|
CanCancel = true;
|
||||||
Cancel = false;
|
Cancel = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Аргументы события изменения порядка вкладок.
|
/// Предоставляет данные для события изменения порядка вкладок.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TabsReorderedEventArgs : EventArgs
|
public class TabsReorderedEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Старый индекс вкладки.
|
/// Получает старый индекс вкладки.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int OldIndex { get; }
|
public int OldIndex { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Новый индекс вкладки.
|
/// Получает новый индекс вкладки.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int NewIndex { get; }
|
public int NewIndex { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Перемещаемый контент.
|
/// Получает перемещаемый контент.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IDockContent Content { get; }
|
public IDockContent Content { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Инициализирует новый экземпляр класса <see cref="TabsReorderedEventArgs"/>.
|
/// Инициализирует новый экземпляр класса <see cref="TabsReorderedEventArgs"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="oldIndex">Старый индекс вкладки.</param>
|
||||||
|
/// <param name="newIndex">Новый индекс вкладки.</param>
|
||||||
|
/// <param name="content">Перемещаемый контент.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, если <paramref name="content"/> равен null.
|
||||||
|
/// </exception>
|
||||||
public TabsReorderedEventArgs(int oldIndex, int newIndex, IDockContent content)
|
public TabsReorderedEventArgs(int oldIndex, int newIndex, IDockContent content)
|
||||||
{
|
{
|
||||||
OldIndex = oldIndex;
|
OldIndex = oldIndex;
|
||||||
NewIndex = newIndex;
|
NewIndex = newIndex;
|
||||||
Content = content;
|
Content = content ?? throw new ArgumentNullException(nameof(content));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
72
Lattice.UI.Docking/Abstractions/IDockSplitterControl.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
namespace Lattice.UI.Docking.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет контракт для контрола разделителя между элементами док-системы.
|
||||||
|
/// Разделитель позволяет пользователю изменять размер смежных элементов.
|
||||||
|
/// </summary>
|
||||||
|
public interface IDockSplitterControl : IDockControl
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает ориентацию разделителя.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Ориентация разделителя (горизонтальная или вертикальная).
|
||||||
|
/// </value>
|
||||||
|
Core.Docking.Models.SplitDirection Orientation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает признак того, что разделитель активен (перетаскивается).
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// true, если разделитель активен; в противном случае false.
|
||||||
|
/// </value>
|
||||||
|
bool IsDragging { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Происходит при начале перетаскивания разделителя.
|
||||||
|
/// </summary>
|
||||||
|
event EventHandler DragStarted;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Происходит при перетаскивании разделителя.
|
||||||
|
/// </summary>
|
||||||
|
event EventHandler<SplitterDraggedEventArgs> DragDelta;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Происходит при завершении перетаскивания разделителя.
|
||||||
|
/// </summary>
|
||||||
|
event EventHandler DragCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Предоставляет данные для события перетаскивания разделителя.
|
||||||
|
/// </summary>
|
||||||
|
public class SplitterDraggedEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получает изменение позиции по горизонтали.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Изменение по горизонтали в пикселях.
|
||||||
|
/// </value>
|
||||||
|
public double HorizontalChange { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает изменение позиции по вертикали.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Изменение по вертикали в пикселях.
|
||||||
|
/// </value>
|
||||||
|
public double VerticalChange { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="SplitterDraggedEventArgs"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="horizontalChange">Изменение по горизонтали.</param>
|
||||||
|
/// <param name="verticalChange">Изменение по вертикали.</param>
|
||||||
|
public SplitterDraggedEventArgs(double horizontalChange, double verticalChange)
|
||||||
|
{
|
||||||
|
HorizontalChange = horizontalChange;
|
||||||
|
VerticalChange = verticalChange;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,31 +77,37 @@ public interface IDockTheme
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Аргументы события показа контекстного меню.
|
/// Предоставляет данные для события показа контекстного меню.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ContextMenuShownEventArgs : EventArgs
|
public class ContextMenuShownEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Элемент, для которого показано меню.
|
/// Получает элемент, для которого показано меню.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IDockControl Target { get; }
|
public IDockControl Target { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Координата X меню.
|
/// Получает координату X меню.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double X { get; }
|
public double X { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Координата Y меню.
|
/// Получает координату Y меню.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double Y { get; }
|
public double Y { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Инициализирует новый экземпляр класса <see cref="ContextMenuShownEventArgs"/>.
|
/// Инициализирует новый экземпляр класса <see cref="ContextMenuShownEventArgs"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="target">Элемент, для которого показано меню.</param>
|
||||||
|
/// <param name="x">Координата X меню.</param>
|
||||||
|
/// <param name="y">Координата Y меню.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, если <paramref name="target"/> равен null.
|
||||||
|
/// </exception>
|
||||||
public ContextMenuShownEventArgs(IDockControl target, double x, double y)
|
public ContextMenuShownEventArgs(IDockControl target, double x, double y)
|
||||||
{
|
{
|
||||||
Target = target;
|
Target = target ?? throw new ArgumentNullException(nameof(target));
|
||||||
X = x;
|
X = x;
|
||||||
Y = y;
|
Y = y;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,12 +22,8 @@ public interface IDockUIService
|
|||||||
/// Платформенно-зависимый объект окна, который можно отобразить.
|
/// Платформенно-зависимый объект окна, который можно отобразить.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
/// <exception cref="ArgumentNullException">
|
/// <exception cref="ArgumentNullException">
|
||||||
/// Выбрасывается, если <paramref name="host"/> равен null.
|
/// Выбрасывается, когда <paramref name="host"/> равен null.
|
||||||
/// </exception>
|
/// </exception>
|
||||||
/// <remarks>
|
|
||||||
/// Реализация должна создавать окно с соответствующими стилями и поведением
|
|
||||||
/// для целевой платформы, настроенное для работы с док-системой.
|
|
||||||
/// </remarks>
|
|
||||||
object CreateMainWindow(IDockHost host);
|
object CreateMainWindow(IDockHost host);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -42,12 +38,8 @@ public interface IDockUIService
|
|||||||
/// null - диалог был закрыт без выбора.
|
/// null - диалог был закрыт без выбора.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
/// <exception cref="ArgumentNullException">
|
/// <exception cref="ArgumentNullException">
|
||||||
/// Выбрасывается, если <paramref name="title"/> или <paramref name="content"/> равны null.
|
/// Выбрасывается, когда <paramref name="title"/> или <paramref name="content"/> равны null.
|
||||||
/// </exception>
|
/// </exception>
|
||||||
/// <remarks>
|
|
||||||
/// Реализация должна блокировать взаимодействие с родительским окном
|
|
||||||
/// до закрытия диалога.
|
|
||||||
/// </remarks>
|
|
||||||
bool? ShowDialog(string title, object content);
|
bool? ShowDialog(string title, object content);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -56,12 +48,8 @@ public interface IDockUIService
|
|||||||
/// <param name="message">Текст сообщения.</param>
|
/// <param name="message">Текст сообщения.</param>
|
||||||
/// <param name="caption">Заголовок окна сообщения.</param>
|
/// <param name="caption">Заголовок окна сообщения.</param>
|
||||||
/// <exception cref="ArgumentNullException">
|
/// <exception cref="ArgumentNullException">
|
||||||
/// Выбрасывается, если <paramref name="message"/> или <paramref name="caption"/> равны null.
|
/// Выбрасывается, когда <paramref name="message"/> или <paramref name="caption"/> равны null.
|
||||||
/// </exception>
|
/// </exception>
|
||||||
/// <remarks>
|
|
||||||
/// Реализация должна использовать стандартные диалоги платформы
|
|
||||||
/// или создавать кастомные окна сообщений.
|
|
||||||
/// </remarks>
|
|
||||||
void ShowMessage(string message, string caption);
|
void ShowMessage(string message, string caption);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -73,12 +61,8 @@ public interface IDockUIService
|
|||||||
/// true, если пользователь выбрал "Yes"; false, если пользователь выбрал "No".
|
/// true, если пользователь выбрал "Yes"; false, если пользователь выбрал "No".
|
||||||
/// </returns>
|
/// </returns>
|
||||||
/// <exception cref="ArgumentNullException">
|
/// <exception cref="ArgumentNullException">
|
||||||
/// Выбрасывается, если <paramref name="message"/> или <paramref name="caption"/> равны null.
|
/// Выбрасывается, когда <paramref name="message"/> или <paramref name="caption"/> равны null.
|
||||||
/// </exception>
|
/// </exception>
|
||||||
/// <remarks>
|
|
||||||
/// Используется для получения подтверждения от пользователя перед выполнением
|
|
||||||
/// критических операций (закрытие вкладок, сброс настроек и т.д.).
|
|
||||||
/// </remarks>
|
|
||||||
bool Confirm(string message, string caption);
|
bool Confirm(string message, string caption);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -90,12 +74,8 @@ public interface IDockUIService
|
|||||||
/// Введенный пользователем текст или null, если диалог был отменен.
|
/// Введенный пользователем текст или null, если диалог был отменен.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
/// <exception cref="ArgumentNullException">
|
/// <exception cref="ArgumentNullException">
|
||||||
/// Выбрасывается, если <paramref name="prompt"/> равен null.
|
/// Выбрасывается, когда <paramref name="prompt"/> равен null.
|
||||||
/// </exception>
|
/// </exception>
|
||||||
/// <remarks>
|
|
||||||
/// Реализация должна предоставлять однострочное поле ввода текста
|
|
||||||
/// с возможностью отмены операции.
|
|
||||||
/// </remarks>
|
|
||||||
string? Prompt(string prompt, string? defaultValue = null);
|
string? Prompt(string prompt, string? defaultValue = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -103,12 +83,7 @@ public interface IDockUIService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="action">Действие для выполнения.</param>
|
/// <param name="action">Действие для выполнения.</param>
|
||||||
/// <exception cref="ArgumentNullException">
|
/// <exception cref="ArgumentNullException">
|
||||||
/// Выбрасывается, если <paramref name="action"/> равен null.
|
/// Выбрасывается, когда <paramref name="action"/> равен null.
|
||||||
/// </exception>
|
/// </exception>
|
||||||
/// <remarks>
|
|
||||||
/// Этот метод гарантирует, что действие будет выполнено в потоке,
|
|
||||||
/// связанном с пользовательским интерфейсом, что необходимо для
|
|
||||||
/// безопасного обновления UI-элементов.
|
|
||||||
/// </remarks>
|
|
||||||
void InvokeOnUIThread(Action action);
|
void InvokeOnUIThread(Action action);
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ public interface IFloatingWindowControl : IDockControl
|
|||||||
/// Получает или задает признак того, что окно можно изменять.
|
/// Получает или задает признак того, что окно можно изменять.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
/// <value>
|
||||||
/// true, если размеры окна можно изменять; в противном случае — false.
|
/// true, если размеры окна можно изменять; в противном случае false.
|
||||||
/// </value>
|
/// </value>
|
||||||
bool CanResize { get; set; }
|
bool CanResize { get; set; }
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ public interface IFloatingWindowControl : IDockControl
|
|||||||
/// Получает или задает признак того, что окно можно перемещать.
|
/// Получает или задает признак того, что окно можно перемещать.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
/// <value>
|
||||||
/// true, если окно можно перемещать; в противном случае — false.
|
/// true, если окно можно перемещать; в противном случае false.
|
||||||
/// </value>
|
/// </value>
|
||||||
bool CanMove { get; set; }
|
bool CanMove { get; set; }
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ public interface IFloatingWindowControl : IDockControl
|
|||||||
/// Получает или задает признак того, что окно всегда поверх других окон.
|
/// Получает или задает признак того, что окно всегда поверх других окон.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
/// <value>
|
||||||
/// true, если окно всегда поверх; в противном случае — false.
|
/// true, если окно всегда поверх; в противном случае false.
|
||||||
/// </value>
|
/// </value>
|
||||||
bool AlwaysOnTop { get; set; }
|
bool AlwaysOnTop { get; set; }
|
||||||
|
|
||||||
@@ -94,17 +94,17 @@ public interface IFloatingWindowControl : IDockControl
|
|||||||
void Activate();
|
void Activate();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Событие, возникающее при закрытии окна.
|
/// Происходит при закрытии окна.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event EventHandler Closing;
|
event EventHandler Closing;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Событие, возникающее при изменении положения окна.
|
/// Происходит при изменении положения окна.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event EventHandler LocationChanged;
|
event EventHandler LocationChanged;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Событие, возникающее при изменении размера окна.
|
/// Происходит при изменении размера окна.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event EventHandler SizeChanged;
|
event EventHandler SizeChanged;
|
||||||
}
|
}
|
||||||
@@ -1,33 +1,85 @@
|
|||||||
using Lattice.UI.Docking.Abstractions;
|
using Lattice.UI.Docking.Abstractions;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Lattice.UI.Docking.Commands;
|
namespace Lattice.UI.Docking.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Базовая реализация команды док-системы.
|
/// Предоставляет базовую реализацию команды док-системы.
|
||||||
|
/// Реализует интерфейс <see cref="IDockCommand"/> и <see cref="INotifyPropertyChanged"/>
|
||||||
|
/// для поддержки уведомлений об изменении свойств.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class DockCommandBase : IDockCommand
|
/// <remarks>
|
||||||
|
/// Этот класс предоставляет общую логику для команд, включая управление состоянием
|
||||||
|
/// возможности выполнения, уведомление об изменениях и реализацию интерфейса ICommand.
|
||||||
|
/// </remarks>
|
||||||
|
public abstract class DockCommandBase : IDockCommand, INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Происходит при изменении возможности выполнения команды.
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler? CanExecuteChanged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Происходит при изменении значения свойства.
|
||||||
|
/// </summary>
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
private bool _canExecute = true;
|
private bool _canExecute = true;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <summary>
|
||||||
|
/// Получает уникальный идентификатор команды.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Строковый идентификатор команды. Должен быть уникальным в пределах системы.
|
||||||
|
/// </value>
|
||||||
public abstract string Id { get; }
|
public abstract string Id { get; }
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <summary>
|
||||||
|
/// Получает отображаемое имя команды.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Имя команды, отображаемое в пользовательском интерфейсе.
|
||||||
|
/// </value>
|
||||||
public abstract string Name { get; }
|
public abstract string Name { get; }
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <summary>
|
||||||
|
/// Получает описание команды.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Текстовое описание функциональности команды.
|
||||||
|
/// Реализация по умолчанию возвращает пустую строку.
|
||||||
|
/// </value>
|
||||||
public virtual string Description => string.Empty;
|
public virtual string Description => string.Empty;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <summary>
|
||||||
|
/// Получает идентификатор ресурса для иконки команды.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Имя ресурса иконки или путь к файлу иконки.
|
||||||
|
/// Реализация по умолчанию возвращает пустую строку.
|
||||||
|
/// </value>
|
||||||
public virtual string Icon => string.Empty;
|
public virtual string Icon => string.Empty;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <summary>
|
||||||
|
/// Получает комбинацию клавиш для быстрого вызова команды.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Строковое представление горячей клавиши.
|
||||||
|
/// Реализация по умолчанию возвращает пустую строку.
|
||||||
|
/// </value>
|
||||||
public virtual string Shortcut => string.Empty;
|
public virtual string Shortcut => string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает или задает признак возможности выполнения команды.
|
/// Получает или задает значение, указывающее, можно ли выполнить команду.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool CanExecute
|
/// <value>
|
||||||
|
/// true, если команду можно выполнить; в противном случае false.
|
||||||
|
/// </value>
|
||||||
|
/// <remarks>
|
||||||
|
/// При изменении этого свойства генерируется событие <see cref="CanExecuteChanged"/>.
|
||||||
|
/// </remarks>
|
||||||
|
protected bool CanExecuteValue
|
||||||
{
|
{
|
||||||
get => _canExecute;
|
get => _canExecute;
|
||||||
set
|
set
|
||||||
@@ -36,42 +88,131 @@ public abstract class DockCommandBase : IDockCommand
|
|||||||
{
|
{
|
||||||
_canExecute = value;
|
_canExecute = value;
|
||||||
OnCanExecuteChanged();
|
OnCanExecuteChanged();
|
||||||
|
OnPropertyChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <summary>
|
||||||
public event EventHandler? CanExecuteChanged;
|
/// Определяет, можно ли выполнить команду в текущем контексте.
|
||||||
|
/// </summary>
|
||||||
/// <inheritdoc/>
|
/// <param name="parameter">Параметр команды.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// true, если команду можно выполнить; в противном случае false.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Реализация по умолчанию возвращает значение свойства <see cref="CanExecuteValue"/>.
|
||||||
|
/// </remarks>
|
||||||
public virtual bool CanExecute(object? parameter)
|
public virtual bool CanExecute(object? parameter)
|
||||||
{
|
{
|
||||||
return _canExecute;
|
return _canExecute;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <summary>
|
||||||
|
/// Выполняет команду.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parameter">Параметр команды.</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот метод должен быть реализован в производных классах.
|
||||||
|
/// </remarks>
|
||||||
public abstract void Execute(object? parameter);
|
public abstract void Execute(object? parameter);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Вызывает событие изменения возможности выполнения команды.
|
/// Вызывает событие изменения возможности выполнения команды.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот метод может быть переопределен в производных классах для добавления
|
||||||
|
/// дополнительной логики перед вызовом события.
|
||||||
|
/// </remarks>
|
||||||
protected virtual void OnCanExecuteChanged()
|
protected virtual void OnCanExecuteChanged()
|
||||||
{
|
{
|
||||||
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывает событие <see cref="PropertyChanged"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="propertyName">
|
||||||
|
/// Имя изменившегося свойства. Если не указано, определяется автоматически.
|
||||||
|
/// </param>
|
||||||
|
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Базовая команда для закрытия контента.
|
/// Предоставляет команду для закрытия контента (вкладки).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Команда закрывает активную вкладку в указанном контроле листа.
|
||||||
|
/// </remarks>
|
||||||
public class CloseContentCommand : DockCommandBase
|
public class CloseContentCommand : DockCommandBase
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получает уникальный идентификатор команды.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Идентификатор "CloseContent".
|
||||||
|
/// </value>
|
||||||
public override string Id => "CloseContent";
|
public override string Id => "CloseContent";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает отображаемое имя команды.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Имя "Close".
|
||||||
|
/// </value>
|
||||||
public override string Name => "Close";
|
public override string Name => "Close";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает описание команды.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Описание "Close the current tab".
|
||||||
|
/// </value>
|
||||||
public override string Description => "Close the current tab";
|
public override string Description => "Close the current tab";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает идентификатор ресурса для иконки команды.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Идентификатор "Close".
|
||||||
|
/// </value>
|
||||||
public override string Icon => "Close";
|
public override string Icon => "Close";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает комбинацию клавиш для быстрого вызова команды.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Горячая клавиша "Ctrl+F4".
|
||||||
|
/// </value>
|
||||||
public override string Shortcut => "Ctrl+F4";
|
public override string Shortcut => "Ctrl+F4";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет, можно ли выполнить команду в текущем контексте.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parameter">Параметр команды.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// true, если команду можно выполнить; в противном случае false.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Команда доступна только если параметр является контролом листа
|
||||||
|
/// и содержит активный контент.
|
||||||
|
/// </remarks>
|
||||||
|
public override bool CanExecute(object? parameter)
|
||||||
|
{
|
||||||
|
return parameter is Abstractions.IDockLeafControl leafControl &&
|
||||||
|
leafControl.ActiveContent != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Выполняет команду закрытия контента.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parameter">Параметр команды. Ожидается <see cref="Abstractions.IDockLeafControl"/>.</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// Команда закрывает активную вкладку в указанном контроле листа.
|
||||||
|
/// </remarks>
|
||||||
public override void Execute(object? parameter)
|
public override void Execute(object? parameter)
|
||||||
{
|
{
|
||||||
if (parameter is Abstractions.IDockLeafControl leafControl &&
|
if (parameter is Abstractions.IDockLeafControl leafControl &&
|
||||||
@@ -83,33 +224,137 @@ public class CloseContentCommand : DockCommandBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Базовая команда для создания плавающего окна.
|
/// Предоставляет команду для создания плавающего окна из элемента.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class FloatWindowCommand : DockCommandBase
|
public class FloatWindowCommand : DockCommandBase
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получает уникальный идентификатор команды.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Идентификатор "FloatWindow".
|
||||||
|
/// </value>
|
||||||
public override string Id => "FloatWindow";
|
public override string Id => "FloatWindow";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает отображаемое имя команды.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Имя "Float".
|
||||||
|
/// </value>
|
||||||
public override string Name => "Float";
|
public override string Name => "Float";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает описание команды.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Описание "Float the window as a separate window".
|
||||||
|
/// </value>
|
||||||
public override string Description => "Float the window as a separate window";
|
public override string Description => "Float the window as a separate window";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает идентификатор ресурса для иконки команды.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Идентификатор "Float".
|
||||||
|
/// </value>
|
||||||
public override string Icon => "Float";
|
public override string Icon => "Float";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет, можно ли выполнить команду в текущем контексте.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parameter">Параметр команды.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// true, если команду можно выполнить; в противном случае false.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Команда доступна только если параметр является элементом док-системы,
|
||||||
|
/// который может быть преобразован в плавающее окно.
|
||||||
|
/// </remarks>
|
||||||
|
public override bool CanExecute(object? parameter)
|
||||||
|
{
|
||||||
|
return parameter is Core.Docking.Abstractions.IDockElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Выполняет команду создания плавающего окна.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parameter">Параметр команды. Ожидается <see cref="Core.Docking.Abstractions.IDockElement"/>.</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// Реализация зависит от конкретного UI и должна быть предоставлена в производных классах.
|
||||||
|
/// Базовая реализация не выполняет никаких действий.
|
||||||
|
/// </remarks>
|
||||||
public override void Execute(object? parameter)
|
public override void Execute(object? parameter)
|
||||||
{
|
{
|
||||||
// Реализация зависит от конкретного UI
|
// Реализация зависит от конкретного UI
|
||||||
|
// В базовом классе метод не выполняет никаких действий
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Базовая команда для закрепления окна.
|
/// Предоставляет команду для закрепления плавающего окна.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DockWindowCommand : DockCommandBase
|
public class DockWindowCommand : DockCommandBase
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получает уникальный идентификатор команды.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Идентификатор "DockWindow".
|
||||||
|
/// </value>
|
||||||
public override string Id => "DockWindow";
|
public override string Id => "DockWindow";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает отображаемое имя команды.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Имя "Dock".
|
||||||
|
/// </value>
|
||||||
public override string Name => "Dock";
|
public override string Name => "Dock";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает описание команды.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Описание "Dock the window to the main window".
|
||||||
|
/// </value>
|
||||||
public override string Description => "Dock the window to the main window";
|
public override string Description => "Dock the window to the main window";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает идентификатор ресурса для иконки команды.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// Идентификатор "Dock".
|
||||||
|
/// </value>
|
||||||
public override string Icon => "Dock";
|
public override string Icon => "Dock";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет, можно ли выполнить команду в текущем контексте.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parameter">Параметр команды.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// true, если команду можно выполнить; в противном случае false.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Команда доступна только если параметр является плавающим окном,
|
||||||
|
/// которое может быть закреплено в основном окне.
|
||||||
|
/// </remarks>
|
||||||
|
public override bool CanExecute(object? parameter)
|
||||||
|
{
|
||||||
|
return parameter is Abstractions.IFloatingWindowControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Выполняет команду закрепления окна.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parameter">Параметр команды. Ожидается <see cref="Abstractions.IFloatingWindowControl"/>.</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// Реализация зависит от конкретного UI и должна быть предоставлена в производных классах.
|
||||||
|
/// Базовая реализация не выполняет никаких действий.
|
||||||
|
/// </remarks>
|
||||||
public override void Execute(object? parameter)
|
public override void Execute(object? parameter)
|
||||||
{
|
{
|
||||||
// Реализация зависит от конкретного UI
|
// Реализация зависит от конкретного UI
|
||||||
|
// В базовом классе метод не выполняет никаких действий
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,57 +5,136 @@ using Lattice.UI.Docking.Abstractions;
|
|||||||
namespace Lattice.UI.Docking.Factories;
|
namespace Lattice.UI.Docking.Factories;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Базовая фабрика для создания UI-контролов док-системы.
|
/// Предоставляет базовую реализацию фабрики для создания UI-контролов док-системы.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот класс реализует общую логику для фабрик контролов, включая настройку
|
||||||
|
/// общих свойств и создание контролов для произвольных элементов.
|
||||||
|
/// </remarks>
|
||||||
public abstract class DockControlFactoryBase : IDockControlFactory
|
public abstract class DockControlFactoryBase : IDockControlFactory
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает сервис перетаскивания для создаваемых контролов.
|
|
||||||
/// </summary>
|
|
||||||
public Services.IDockDragDropService? DragDropService { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает или задает менеджер контекста для создаваемых контролов.
|
/// Получает или задает менеджер контекста для создаваемых контролов.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Services.IDockContextManager? ContextManager { get; set; }
|
/// <value>
|
||||||
|
/// Экземпляр <see cref="IDockContextManager"/> или null, если не установлен.
|
||||||
/// <inheritdoc/>
|
/// </value>
|
||||||
public abstract IDockGroupControl CreateGroupControl(DockGroup group);
|
public IDockContextManager? ContextManager { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public abstract IDockLeafControl CreateLeafControl(DockLeaf leaf);
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public abstract IFloatingWindowControl CreateFloatingWindowControl(DockWindow window);
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public abstract IAutoHidePanelControl CreateAutoHidePanelControl(AutoHidePanel panel);
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public abstract IDockSplitterControl CreateSplitterControl(SplitDirection orientation);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Создает контрол для произвольного элемента док-системы.
|
/// Создает контрол для группы разделения.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="group">Модель группы.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// Созданный контрол группы.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, если <paramref name="group"/> равен null.
|
||||||
|
/// </exception>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот метод должен быть реализован в производных классах.
|
||||||
|
/// </remarks>
|
||||||
|
public abstract IDockGroupControl CreateGroupControl(DockGroup group);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создает контрол для контейнера вкладок.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="leaf">Модель листа.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// Созданный контрол листа.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, если <paramref name="leaf"/> равен null.
|
||||||
|
/// </exception>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот метод должен быть реализован в производных классах.
|
||||||
|
/// </remarks>
|
||||||
|
public abstract IDockLeafControl CreateLeafControl(DockLeaf leaf);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создает контрол для плавающего окна.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="window">Модель окна.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// Созданный контрол окна.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, если <paramref name="window"/> равен null.
|
||||||
|
/// </exception>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот метод должен быть реализован в производных классах.
|
||||||
|
/// </remarks>
|
||||||
|
public abstract IFloatingWindowControl CreateFloatingWindowControl(DockWindow window);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создает контрол для автоскрываемой панели.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="panel">Модель панели.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// Созданный контрол панели.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, если <paramref name="panel"/> равен null.
|
||||||
|
/// </exception>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот метод должен быть реализован в производных классах.
|
||||||
|
/// </remarks>
|
||||||
|
public abstract IAutoHidePanelControl CreateAutoHidePanelControl(AutoHidePanel panel);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создает контрол для разделителя.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="orientation">Ориентация разделителя.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// Созданный контрол разделителя.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Этот метод должен быть реализован в производных классах.
|
||||||
|
/// </remarks>
|
||||||
|
public abstract IDockSplitterControl CreateSplitterControl(SplitDirection orientation);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
public virtual IDockControl? CreateControlForElement(IDockElement element)
|
public virtual IDockControl? CreateControlForElement(IDockElement element)
|
||||||
{
|
{
|
||||||
return element switch
|
if (element == null) throw new ArgumentNullException(nameof(element));
|
||||||
|
|
||||||
|
IDockControl? control = null;
|
||||||
|
|
||||||
|
switch (element)
|
||||||
{
|
{
|
||||||
DockGroup group => CreateGroupControl(group),
|
case DockGroup group:
|
||||||
DockLeaf leaf => CreateLeafControl(leaf),
|
control = CreateGroupControl(group);
|
||||||
_ => null
|
break;
|
||||||
};
|
case DockLeaf leaf:
|
||||||
|
control = CreateLeafControl(leaf);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
control = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (control != null)
|
||||||
|
{
|
||||||
|
ConfigureControl(control);
|
||||||
|
}
|
||||||
|
|
||||||
|
return control;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Настраивает общие свойства контрола.
|
/// Настраивает общие свойства контрола.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="control">Контрол для настройки.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, если <paramref name="control"/> равен null.
|
||||||
|
/// </exception>
|
||||||
|
/// <remarks>
|
||||||
|
/// Устанавливает общие свойства, такие как контекстный менеджер,
|
||||||
|
/// для всех создаваемых контролов.
|
||||||
|
/// </remarks>
|
||||||
protected virtual void ConfigureControl(IDockControl control)
|
protected virtual void ConfigureControl(IDockControl control)
|
||||||
{
|
{
|
||||||
if (DragDropService != null)
|
if (control == null) throw new ArgumentNullException(nameof(control));
|
||||||
{
|
|
||||||
control.DragDropService = DragDropService;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ContextManager != null)
|
if (ContextManager != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Lattice.Core.Docking.Models;
|
using Lattice.Core.Docking.Abstractions;
|
||||||
|
using Lattice.Core.Docking.Models;
|
||||||
using Lattice.UI.Docking.Abstractions;
|
using Lattice.UI.Docking.Abstractions;
|
||||||
|
|
||||||
namespace Lattice.UI.Docking.Factories;
|
namespace Lattice.UI.Docking.Factories;
|
||||||
@@ -6,96 +7,82 @@ namespace Lattice.UI.Docking.Factories;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Определяет контракт для фабрики, создающей UI-контролы для элементов док-системы.
|
/// Определяет контракт для фабрики, создающей UI-контролы для элементов док-системы.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Фабрика обеспечивает абстракцию над созданием конкретных UI-контролов,
|
||||||
|
/// что позволяет легко заменять реализации для разных платформ или тем оформления.
|
||||||
|
/// </remarks>
|
||||||
public interface IDockControlFactory
|
public interface IDockControlFactory
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Создает контрол для группы разделения.
|
/// Создает контрол для группы разделения.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="group">Модель группы.</param>
|
/// <param name="group">Модель группы.</param>
|
||||||
/// <returns>Созданный контрол группы.</returns>
|
/// <returns>
|
||||||
|
/// Созданный контрол группы.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, если <paramref name="group"/> равен null.
|
||||||
|
/// </exception>
|
||||||
IDockGroupControl CreateGroupControl(DockGroup group);
|
IDockGroupControl CreateGroupControl(DockGroup group);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Создает контрол для контейнера вкладок.
|
/// Создает контрол для контейнера вкладок.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="leaf">Модель листа.</param>
|
/// <param name="leaf">Модель листа.</param>
|
||||||
/// <returns>Созданный контрол листа.</returns>
|
/// <returns>
|
||||||
|
/// Созданный контрол листа.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, если <paramref name="leaf"/> равен null.
|
||||||
|
/// </exception>
|
||||||
IDockLeafControl CreateLeafControl(DockLeaf leaf);
|
IDockLeafControl CreateLeafControl(DockLeaf leaf);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Создает контрол для плавающего окна.
|
/// Создает контрол для плавающего окна.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="window">Модель окна.</param>
|
/// <param name="window">Модель окна.</param>
|
||||||
/// <returns>Созданный контрол окна.</returns>
|
/// <returns>
|
||||||
|
/// Созданный контрол окна.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, если <paramref name="window"/> равен null.
|
||||||
|
/// </exception>
|
||||||
IFloatingWindowControl CreateFloatingWindowControl(DockWindow window);
|
IFloatingWindowControl CreateFloatingWindowControl(DockWindow window);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Создает контрол для автоскрываемой панели.
|
/// Создает контрол для автоскрываемой панели.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="panel">Модель панели.</param>
|
/// <param name="panel">Модель панели.</param>
|
||||||
/// <returns>Созданный контрол панели.</returns>
|
/// <returns>
|
||||||
|
/// Созданный контрол панели.
|
||||||
|
/// </returns>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Выбрасывается, если <paramref name="panel"/> равен null.
|
||||||
|
/// </exception>
|
||||||
IAutoHidePanelControl CreateAutoHidePanelControl(AutoHidePanel panel);
|
IAutoHidePanelControl CreateAutoHidePanelControl(AutoHidePanel panel);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Создает контрол для разделителя.
|
/// Создает контрол для разделителя.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="orientation">Ориентация разделителя.</param>
|
/// <param name="orientation">Ориентация разделителя.</param>
|
||||||
/// <returns>Созданный контрол разделителя.</returns>
|
/// <returns>
|
||||||
|
/// Созданный контрол разделителя.
|
||||||
|
/// </returns>
|
||||||
IDockSplitterControl CreateSplitterControl(SplitDirection orientation);
|
IDockSplitterControl CreateSplitterControl(SplitDirection orientation);
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Определяет контракт для контрола разделителя.
|
|
||||||
/// </summary>
|
|
||||||
public interface IDockSplitterControl : IDockControl
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает ориентацию разделителя.
|
|
||||||
/// </summary>
|
|
||||||
SplitDirection Orientation { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает или задает признак того, что разделитель активен.
|
/// Создает контрол для произвольного элемента док-системы.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool IsActive { get; set; }
|
/// <param name="element">Элемент для создания контрола.</param>
|
||||||
|
/// <returns>
|
||||||
/// <summary>
|
/// Созданный контрол или null, если тип элемента не поддерживается.
|
||||||
/// Событие, возникающее при начале перетаскивания разделителя.
|
/// </returns>
|
||||||
/// </summary>
|
/// <exception cref="ArgumentNullException">
|
||||||
event EventHandler DragStarted;
|
/// Выбрасывается, если <paramref name="element"/> равен null.
|
||||||
|
/// </exception>
|
||||||
/// <summary>
|
/// <remarks>
|
||||||
/// Событие, возникающее при перетаскивании разделителя.
|
/// Метод использует сопоставление с шаблоном для определения типа элемента
|
||||||
/// </summary>
|
/// и вызова соответствующего метода создания.
|
||||||
event EventHandler<SplitterDraggedEventArgs> DragDelta;
|
/// </remarks>
|
||||||
|
IDockControl? CreateControlForElement(IDockElement element);
|
||||||
/// <summary>
|
|
||||||
/// Событие, возникающее при завершении перетаскивания разделителя.
|
|
||||||
/// </summary>
|
|
||||||
event EventHandler DragCompleted;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Аргументы события перетаскивания разделителя.
|
|
||||||
/// </summary>
|
|
||||||
public class SplitterDraggedEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Изменение по горизонтали.
|
|
||||||
/// </summary>
|
|
||||||
public double HorizontalChange { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Изменение по вертикали.
|
|
||||||
/// </summary>
|
|
||||||
public double VerticalChange { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="SplitterDraggedEventArgs"/>.
|
|
||||||
/// </summary>
|
|
||||||
public SplitterDraggedEventArgs(double horizontalChange, double verticalChange)
|
|
||||||
{
|
|
||||||
HorizontalChange = horizontalChange;
|
|
||||||
VerticalChange = verticalChange;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -9,8 +9,4 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Lattice.Core.Docking\Lattice.Core.Docking.csproj" />
|
<ProjectReference Include="..\Lattice.Core.Docking\Lattice.Core.Docking.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Converters\" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Lattice.Core.Docking.Abstractions;
|
using Lattice.Core.Docking.Engine;
|
||||||
using Lattice.Core.Docking.Engine;
|
|
||||||
using Lattice.Core.Docking.Services;
|
using Lattice.Core.Docking.Services;
|
||||||
using Lattice.UI.Docking.Abstractions;
|
using Lattice.UI.Docking.Abstractions;
|
||||||
using Lattice.UI.Docking.Factories;
|
using Lattice.UI.Docking.Factories;
|
||||||
@@ -8,14 +7,7 @@ namespace Lattice.UI.Docking;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Предоставляет статический API для инициализации и управления UI-фреймворком Lattice.
|
/// Предоставляет статический API для инициализации и управления UI-фреймворком Lattice.
|
||||||
/// Является точкой входа для интеграции док-системы в приложение и централизованным
|
|
||||||
/// хранилищем для основных сервисов и компонентов.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
|
||||||
/// Этот класс реализует шаблон Singleton для доступа к глобальным сервисам.
|
|
||||||
/// Все компоненты инициализируются через строитель <see cref="LatticeBuilder"/>,
|
|
||||||
/// что обеспечивает гибкую конфигурацию и соблюдение принципа инверсии зависимостей.
|
|
||||||
/// </remarks>
|
|
||||||
public static class LatticeUIFramework
|
public static class LatticeUIFramework
|
||||||
{
|
{
|
||||||
private static bool _isInitialized;
|
private static bool _isInitialized;
|
||||||
@@ -24,22 +16,11 @@ public static class LatticeUIFramework
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает значение, указывающее, инициализирован ли фреймворк.
|
/// Получает значение, указывающее, инициализирован ли фреймворк.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
|
||||||
/// true, если фреймворк был инициализирован вызовом <see cref="Initialize"/>;
|
|
||||||
/// в противном случае — false.
|
|
||||||
/// </value>
|
|
||||||
public static bool IsInitialized => _isInitialized;
|
public static bool IsInitialized => _isInitialized;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает текущий строитель конфигурации фреймворка.
|
/// Получает текущий строитель конфигурации фреймворка.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="LatticeBuilder"/>, используемый для настройки фреймворка.
|
|
||||||
/// Возвращает null, если фреймворк не инициализирован.
|
|
||||||
/// </value>
|
|
||||||
/// <exception cref="InvalidOperationException">
|
|
||||||
/// Выбрасывается при попытке доступа к свойству до инициализации фреймворка.
|
|
||||||
/// </exception>
|
|
||||||
public static LatticeBuilder CurrentBuilder
|
public static LatticeBuilder CurrentBuilder
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -53,67 +34,31 @@ public static class LatticeUIFramework
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает менеджер макета из текущего строителя.
|
/// Получает менеджер макета из текущего строителя.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="LayoutManager"/>, управляющий структурой док-системы.
|
|
||||||
/// </value>
|
|
||||||
public static LayoutManager? LayoutManager => _currentBuilder?.LayoutManager;
|
public static LayoutManager? LayoutManager => _currentBuilder?.LayoutManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает реестр контента из текущего строителя.
|
/// Получает реестр контента из текущего строителя.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="ContentRegistry"/>, содержащий зарегистрированные типы контента.
|
|
||||||
/// </value>
|
|
||||||
public static ContentRegistry? ContentRegistry => _currentBuilder?.ContentRegistry;
|
public static ContentRegistry? ContentRegistry => _currentBuilder?.ContentRegistry;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает фабрику контролов из текущего строителя.
|
/// Получает фабрику контролов из текущего строителя.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="IDockControlFactory"/> для создания UI-контролов.
|
|
||||||
/// </value>
|
|
||||||
public static IDockControlFactory? ControlFactory => _currentBuilder?.ControlFactory;
|
public static IDockControlFactory? ControlFactory => _currentBuilder?.ControlFactory;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает сервис перетаскивания из текущего строителя.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="IDockDragDropService"/> для управления операциями drag-and-drop.
|
|
||||||
/// </value>
|
|
||||||
public static IDockDragDropService? DragDropService => _currentBuilder?.DragDropService;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает менеджер контекстных меню из текущего строителя.
|
/// Получает менеджер контекстных меню из текущего строителя.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="IDockContextManager"/> для управления контекстными меню.
|
|
||||||
/// </value>
|
|
||||||
public static IDockContextManager? ContextManager => _currentBuilder?.ContextManager;
|
public static IDockContextManager? ContextManager => _currentBuilder?.ContextManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает UI-сервис из текущего строителя.
|
/// Получает UI-сервис из текущего строителя.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="IDockUIService"/> для выполнения платформенно-зависимых операций.
|
|
||||||
/// </value>
|
|
||||||
public static IDockUIService? UIService => _currentBuilder?.UIService;
|
public static IDockUIService? UIService => _currentBuilder?.UIService;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Инициализирует фреймворк Lattice с указанными параметрами.
|
/// Инициализирует фреймворк Lattice с указанными параметрами.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="options">
|
|
||||||
/// Настройки инициализации. Если null, используются параметры по умолчанию.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Экземпляр <see cref="LatticeBuilder"/> для дальнейшей конфигурации фреймворка.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="InvalidOperationException">
|
|
||||||
/// Выбрасывается, если фреймворк уже инициализирован.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот метод должен вызываться один раз при запуске приложения, перед любыми
|
|
||||||
/// попытками использования компонентов док-системы.
|
|
||||||
/// </remarks>
|
|
||||||
public static LatticeBuilder Initialize(LatticeOptions? options = null)
|
public static LatticeBuilder Initialize(LatticeOptions? options = null)
|
||||||
{
|
{
|
||||||
if (_isInitialized)
|
if (_isInitialized)
|
||||||
@@ -121,11 +66,9 @@ public static class LatticeUIFramework
|
|||||||
|
|
||||||
options ??= new LatticeOptions();
|
options ??= new LatticeOptions();
|
||||||
|
|
||||||
// Создаем основные компоненты Core-слоя
|
|
||||||
var layoutManager = new LayoutManager();
|
var layoutManager = new LayoutManager();
|
||||||
var contentRegistry = new ContentRegistry();
|
var contentRegistry = new ContentRegistry();
|
||||||
|
|
||||||
// Создаем строитель с основными компонентами
|
|
||||||
_currentBuilder = new LatticeBuilder(layoutManager, contentRegistry, options);
|
_currentBuilder = new LatticeBuilder(layoutManager, contentRegistry, options);
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
|
|
||||||
@@ -135,10 +78,6 @@ public static class LatticeUIFramework
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Сбрасывает состояние фреймворка к неинициализированному.
|
/// Сбрасывает состояние фреймворка к неинициализированному.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
|
||||||
/// Используется в основном для целей тестирования. В рабочем приложении
|
|
||||||
/// фреймворк должен инициализироваться один раз на протяжении жизненного цикла.
|
|
||||||
/// </remarks>
|
|
||||||
public static void Reset()
|
public static void Reset()
|
||||||
{
|
{
|
||||||
_isInitialized = false;
|
_isInitialized = false;
|
||||||
@@ -148,54 +87,35 @@ public static class LatticeUIFramework
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Представляет настройки инициализации фреймворка Lattice.
|
/// Представляет настройки инициализации фреймворка Lattice.
|
||||||
/// Позволяет кастомизировать поведение системы при запуске.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LatticeOptions
|
public class LatticeOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает или задает значение, указывающее, следует ли автоматически
|
/// Получает или задает значение, указывающее, следует ли автоматически
|
||||||
/// регистрировать стандартные команды (закрыть, сделать плавающим и т.д.).
|
/// регистрировать стандартные команды.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
|
||||||
/// true, чтобы зарегистрировать стандартные команды; в противном случае — false.
|
|
||||||
/// Значение по умолчанию: true.
|
|
||||||
/// </value>
|
|
||||||
public bool RegisterDefaultCommands { get; set; } = true;
|
public bool RegisterDefaultCommands { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает или задает значение, указывающее, следует ли автоматически
|
/// Получает или задает значение, указывающее, следует ли автоматически
|
||||||
/// создавать сервисы при их первом запросе.
|
/// создавать сервисы при их первом запросе.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
|
||||||
/// true, чтобы автоматически создавать сервисы; в противном случае — false.
|
|
||||||
/// Значение по умолчанию: true.
|
|
||||||
/// </value>
|
|
||||||
public bool AutoCreateServices { get; set; } = true;
|
public bool AutoCreateServices { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает или задает идентификатор приложения, используемый при
|
/// Получает или задает идентификатор приложения.
|
||||||
/// сериализации макета для различения конфигураций разных приложений.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
|
||||||
/// Строковый идентификатор приложения или null, если идентификатор не задан.
|
|
||||||
/// </value>
|
|
||||||
public string? ApplicationId { get; set; }
|
public string? ApplicationId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает или задает значение, указывающее, следует ли включить
|
/// Получает или задает значение, указывающее, следует ли включить
|
||||||
/// расширенное логирование операций системы.
|
/// расширенное логирование операций системы.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
|
||||||
/// true, чтобы включить подробное логирование; в противном случае — false.
|
|
||||||
/// Значение по умолчанию: false.
|
|
||||||
/// </value>
|
|
||||||
public bool EnableVerboseLogging { get; set; } = false;
|
public bool EnableVerboseLogging { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Предоставляет fluent-интерфейс для конфигурации фреймворка Lattice.
|
/// Предоставляет fluent-интерфейс для конфигурации фреймворка Lattice.
|
||||||
/// Инкапсулирует процесс настройки всех компонентов системы и обеспечивает
|
|
||||||
/// согласованное состояние после инициализации.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class LatticeBuilder
|
public sealed class LatticeBuilder
|
||||||
{
|
{
|
||||||
@@ -203,120 +123,59 @@ public sealed class LatticeBuilder
|
|||||||
private readonly ContentRegistry _contentRegistry;
|
private readonly ContentRegistry _contentRegistry;
|
||||||
private readonly LatticeOptions _options;
|
private readonly LatticeOptions _options;
|
||||||
private IDockControlFactory? _controlFactory;
|
private IDockControlFactory? _controlFactory;
|
||||||
private IDockDragDropService? _dragDropService;
|
|
||||||
private IDockContextManager? _contextManager;
|
private IDockContextManager? _contextManager;
|
||||||
private IDockUIService? _uiService;
|
private IDockUIService? _uiService;
|
||||||
private bool _isBuilt;
|
private bool _isBuilt;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает менеджер макета, связанный с этим строителем.
|
/// Получает менеджер макета.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="LayoutManager"/> для управления структурой док-системы.
|
|
||||||
/// </value>
|
|
||||||
public LayoutManager LayoutManager => _layoutManager;
|
public LayoutManager LayoutManager => _layoutManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает реестр контента, связанный с этим строителем.
|
/// Получает реестр контента.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="ContentRegistry"/> для регистрации типов контента.
|
|
||||||
/// </value>
|
|
||||||
public ContentRegistry ContentRegistry => _contentRegistry;
|
public ContentRegistry ContentRegistry => _contentRegistry;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает фабрику контролов, связанную с этим строителем.
|
/// Получает фабрику контролов.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="IDockControlFactory"/> или null, если фабрика не задана.
|
|
||||||
/// </value>
|
|
||||||
public IDockControlFactory? ControlFactory => _controlFactory;
|
public IDockControlFactory? ControlFactory => _controlFactory;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает сервис перетаскивания, связанный с этим строителем.
|
/// Получает менеджер контекстных меню.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="IDockDragDropService"/> или null, если сервис не задан.
|
|
||||||
/// </value>
|
|
||||||
public IDockDragDropService? DragDropService => _dragDropService;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает менеджер контекстных меню, связанный с этим строителем.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="IDockContextManager"/> или null, если менеджер не задан.
|
|
||||||
/// </value>
|
|
||||||
public IDockContextManager? ContextManager => _contextManager;
|
public IDockContextManager? ContextManager => _contextManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает UI-сервис, связанный с этим строителем.
|
/// Получает UI-сервис.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="IDockUIService"/> или null, если сервис не задан.
|
|
||||||
/// </value>
|
|
||||||
public IDockUIService? UIService => _uiService;
|
public IDockUIService? UIService => _uiService;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Инициализирует новый экземпляр класса <see cref="LatticeBuilder"/>.
|
/// Инициализирует новый экземпляр класса <see cref="LatticeBuilder"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="layoutManager">Менеджер макета.</param>
|
|
||||||
/// <param name="contentRegistry">Реестр контента.</param>
|
|
||||||
/// <param name="options">Опции инициализации.</param>
|
|
||||||
internal LatticeBuilder(LayoutManager layoutManager, ContentRegistry contentRegistry, LatticeOptions options)
|
internal LatticeBuilder(LayoutManager layoutManager, ContentRegistry contentRegistry, LatticeOptions options)
|
||||||
{
|
{
|
||||||
_layoutManager = layoutManager ?? throw new ArgumentNullException(nameof(layoutManager));
|
_layoutManager = layoutManager ?? throw new ArgumentNullException(nameof(layoutManager));
|
||||||
_contentRegistry = contentRegistry ?? throw new ArgumentNullException(nameof(contentRegistry));
|
_contentRegistry = contentRegistry ?? throw new ArgumentNullException(nameof(contentRegistry));
|
||||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
|
||||||
|
_layoutManager.ContentRegistry = contentRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Регистрирует фабрику контролов для создания UI-элементов.
|
/// Регистрирует фабрику контролов для создания UI-элементов.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="factory">
|
|
||||||
/// Фабрика контролов, реализующая <see cref="IDockControlFactory"/>.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Текущий экземпляр строителя для цепочки вызовов.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="factory"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
public LatticeBuilder WithControlFactory(IDockControlFactory factory)
|
public LatticeBuilder WithControlFactory(IDockControlFactory factory)
|
||||||
{
|
{
|
||||||
_controlFactory = factory ?? throw new ArgumentNullException(nameof(factory));
|
_controlFactory = factory ?? throw new ArgumentNullException(nameof(factory));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Регистрирует сервис перетаскивания для управления операциями drag-and-drop.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="service">
|
|
||||||
/// Сервис перетаскивания, реализующий <see cref="IDockDragDropService"/>.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Текущий экземпляр строителя для цепочки вызовов.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="service"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
public LatticeBuilder WithDragDropService(IDockDragDropService service)
|
|
||||||
{
|
|
||||||
_dragDropService = service ?? throw new ArgumentNullException(nameof(service));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Регистрирует менеджер контекстных меню для управления контекстными действиями.
|
/// Регистрирует менеджер контекстных меню для управления контекстными действиями.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="manager">
|
|
||||||
/// Менеджер контекстных меню, реализующий <see cref="IDockContextManager"/>.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Текущий экземпляр строителя для цепочки вызовов.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="manager"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
public LatticeBuilder WithContextManager(IDockContextManager manager)
|
public LatticeBuilder WithContextManager(IDockContextManager manager)
|
||||||
{
|
{
|
||||||
_contextManager = manager ?? throw new ArgumentNullException(nameof(manager));
|
_contextManager = manager ?? throw new ArgumentNullException(nameof(manager));
|
||||||
@@ -326,15 +185,6 @@ public sealed class LatticeBuilder
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Регистрирует UI-сервис для выполнения платформенно-зависимых операций.
|
/// Регистрирует UI-сервис для выполнения платформенно-зависимых операций.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="service">
|
|
||||||
/// UI-сервис, реализующий <see cref="IDockUIService"/>.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Текущий экземпляр строителя для цепочки вызовов.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="service"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
public LatticeBuilder WithUIService(IDockUIService service)
|
public LatticeBuilder WithUIService(IDockUIService service)
|
||||||
{
|
{
|
||||||
_uiService = service ?? throw new ArgumentNullException(nameof(service));
|
_uiService = service ?? throw new ArgumentNullException(nameof(service));
|
||||||
@@ -342,20 +192,8 @@ public sealed class LatticeBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Регистрирует тип контента в реестре для последующего создания экземпляров.
|
/// Регистрирует тип контента в реестре.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">
|
|
||||||
/// Тип контента, реализующий <see cref="IDockContent"/>.
|
|
||||||
/// </typeparam>
|
|
||||||
/// <param name="contentTypeId">Уникальный идентификатор типа контента.</param>
|
|
||||||
/// <param name="factory">Фабричный метод для создания экземпляров контента.</param>
|
|
||||||
/// <param name="metadata">Метаданные типа контента (опционально).</param>
|
|
||||||
/// <returns>
|
|
||||||
/// Текущий экземпляр строителя для цепочки вызовов.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="contentTypeId"/> или <paramref name="factory"/> равны null.
|
|
||||||
/// </exception>
|
|
||||||
public LatticeBuilder RegisterContentType<T>(string contentTypeId, Func<T> factory, ContentMetadata? metadata = null)
|
public LatticeBuilder RegisterContentType<T>(string contentTypeId, Func<T> factory, ContentMetadata? metadata = null)
|
||||||
where T : Core.Docking.Abstractions.IDockContent
|
where T : Core.Docking.Abstractions.IDockContent
|
||||||
{
|
{
|
||||||
@@ -367,73 +205,76 @@ public sealed class LatticeBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Завершает конфигурацию и создает готовый к использованию док-хост.
|
/// Завершает конфигурацию фреймворка.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>
|
public ILatticeFramework Build()
|
||||||
/// Экземпляр <see cref="IDockHost"/>, настроенный в соответствии с текущей конфигурацией.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="InvalidOperationException">
|
|
||||||
/// Выбрасывается, если не задана фабрика контролов или метод уже был вызван.
|
|
||||||
/// </exception>
|
|
||||||
public IDockHost Build()
|
|
||||||
{
|
{
|
||||||
if (_isBuilt)
|
if (_isBuilt)
|
||||||
throw new InvalidOperationException("Builder has already been built.");
|
throw new InvalidOperationException("Framework has already been built.");
|
||||||
|
|
||||||
if (_controlFactory == null)
|
_isBuilt = true;
|
||||||
throw new InvalidOperationException("Control factory must be specified. Call WithControlFactory() first.");
|
return new LatticeFramework(this);
|
||||||
|
|
||||||
// Автоматически создаем отсутствующие сервисы, если включена опция
|
|
||||||
if (_options.AutoCreateServices)
|
|
||||||
{
|
|
||||||
_dragDropService ??= CreateDefaultDragDropService();
|
|
||||||
_contextManager ??= CreateDefaultContextManager();
|
|
||||||
_uiService ??= CreateDefaultUIService();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создаем хост через фабрику
|
|
||||||
// (предполагается, что фабрика имеет метод CreateDockHost)
|
|
||||||
if (_controlFactory is WinUI.Factories.WinUIDockControlFactory winUIFactory)
|
|
||||||
{
|
|
||||||
var host = winUIFactory.CreateDockHost();
|
|
||||||
|
|
||||||
// Настраиваем хост
|
|
||||||
host.LayoutManager = _layoutManager;
|
|
||||||
host.DragDropService = _dragDropService;
|
|
||||||
host.ContextManager = _contextManager;
|
|
||||||
|
|
||||||
_isBuilt = true;
|
|
||||||
return host;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new NotSupportedException($"Control factory of type {_controlFactory.GetType().Name} is not supported.");
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Предоставляет интерфейс для доступа к компонентам фреймворка Lattice.
|
||||||
|
/// </summary>
|
||||||
|
public interface ILatticeFramework
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получает менеджер макета.
|
||||||
|
/// </summary>
|
||||||
|
LayoutManager LayoutManager { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Создает сервис перетаскивания по умолчанию.
|
/// Получает реестр контента.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private IDockDragDropService CreateDefaultDragDropService()
|
ContentRegistry ContentRegistry { get; }
|
||||||
{
|
|
||||||
// Реализация зависит от платформы
|
|
||||||
// В реальном коде здесь должна быть проверка платформы
|
|
||||||
return new WinUI.Services.WinUIDragDropService();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Создает менеджер контекстных меню по умолчанию.
|
/// Получает фабрику контролов.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private IDockContextManager CreateDefaultContextManager()
|
IDockControlFactory? ControlFactory { get; }
|
||||||
{
|
|
||||||
// Реализация зависит от платформы
|
|
||||||
return new WinUI.Services.WinUIDockContextManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Создает UI-сервис по умолчанию.
|
/// Получает менеджер контекстных меню.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private IDockUIService CreateDefaultUIService()
|
IDockContextManager? ContextManager { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает UI-сервис.
|
||||||
|
/// </summary>
|
||||||
|
IDockUIService? UIService { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Реализация интерфейса <see cref="ILatticeFramework"/>.
|
||||||
|
/// </summary>
|
||||||
|
internal class LatticeFramework : ILatticeFramework
|
||||||
|
{
|
||||||
|
private readonly LatticeBuilder _builder;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="LatticeFramework"/>.
|
||||||
|
/// </summary>
|
||||||
|
public LatticeFramework(LatticeBuilder builder)
|
||||||
{
|
{
|
||||||
// Реализация зависит от платформы
|
_builder = builder ?? throw new ArgumentNullException(nameof(builder));
|
||||||
return new WinUI.Services.WinUIDockUIService();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public LayoutManager LayoutManager => _builder.LayoutManager;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ContentRegistry ContentRegistry => _builder.ContentRegistry;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IDockControlFactory? ControlFactory => _builder.ControlFactory;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IDockContextManager? ContextManager => _builder.ContextManager;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IDockUIService? UIService => _builder.UIService;
|
||||||
}
|
}
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
using Lattice.Core.Geometry;
|
|
||||||
using Lattice.UI.Docking.Abstractions;
|
|
||||||
|
|
||||||
namespace Lattice.UI.Docking.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Расширенная информация о перетаскивании для UI-слоя.
|
|
||||||
/// Добавляет визуальные аспекты и UI-контекст к базовой информации о перетаскивании.
|
|
||||||
/// </summary>
|
|
||||||
public class UiDragInfo
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Базовые данные перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public Core.DragDrop.Models.DragInfo BaseDragInfo { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// UI-контрол, который является источником перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public IDockControl? SourceControl { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Визуальное представление перетаскиваемого элемента.
|
|
||||||
/// </summary>
|
|
||||||
public object? DragVisual { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Смещение курсора относительно элемента при начале перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public Point VisualOffset { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Размер визуального представления.
|
|
||||||
/// </summary>
|
|
||||||
public Size VisualSize { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Прозрачность визуального представления.
|
|
||||||
/// </summary>
|
|
||||||
public double VisualOpacity { get; set; } = 0.7;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр <see cref="UiDragInfo"/>.
|
|
||||||
/// </summary>
|
|
||||||
public UiDragInfo(Core.DragDrop.Models.DragInfo baseDragInfo, IDockControl? sourceControl = null)
|
|
||||||
{
|
|
||||||
BaseDragInfo = baseDragInfo;
|
|
||||||
SourceControl = sourceControl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Расширенная информация о сбросе для UI-слоя.
|
|
||||||
/// Добавляет визуальные подсказки и UI-контекст.
|
|
||||||
/// </summary>
|
|
||||||
public class UiDropInfo
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Базовые данные сброса.
|
|
||||||
/// </summary>
|
|
||||||
public Core.DragDrop.Models.DropInfo BaseDropInfo { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// UI-контрол, который является целью сброса.
|
|
||||||
/// </summary>
|
|
||||||
public IDockControl? TargetControl { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Позиция сброса относительно элемента.
|
|
||||||
/// </summary>
|
|
||||||
public DropPosition DropPosition { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Визуальная подсказка для области сброса.
|
|
||||||
/// </summary>
|
|
||||||
public object? DropHintVisual { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Признак того, что курсор находится над допустимой областью сброса.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsOverValidTarget { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Интенсивность подсветки области сброса (0.0 - 1.0).
|
|
||||||
/// </summary>
|
|
||||||
public double HighlightIntensity { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр <see cref="UiDropInfo"/>.
|
|
||||||
/// </summary>
|
|
||||||
public UiDropInfo(Core.DragDrop.Models.DropInfo baseDropInfo, IDockControl? targetControl = null)
|
|
||||||
{
|
|
||||||
BaseDropInfo = baseDropInfo;
|
|
||||||
TargetControl = targetControl;
|
|
||||||
DropPosition = DropPosition.Center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Определяет позицию сброса относительно элемента.
|
|
||||||
/// </summary>
|
|
||||||
public enum DropPosition
|
|
||||||
{
|
|
||||||
/// <summary> Слева от элемента. </summary>
|
|
||||||
Left,
|
|
||||||
|
|
||||||
/// <summary> Справа от элемента. </summary>
|
|
||||||
Right,
|
|
||||||
|
|
||||||
/// <summary> Сверху от элемента. </summary>
|
|
||||||
Top,
|
|
||||||
|
|
||||||
/// <summary> Снизу от элемента. </summary>
|
|
||||||
Bottom,
|
|
||||||
|
|
||||||
/// <summary> В центре элемента (для объединения вкладок). </summary>
|
|
||||||
Center,
|
|
||||||
|
|
||||||
/// <summary> В виде новой вкладки. </summary>
|
|
||||||
Tab
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public class DragStartedEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
public IDockControl? Source { get; }
|
|
||||||
public Core.DragDrop.Models.DragInfo DragInfo { get; }
|
|
||||||
// ... конструктор
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DragUpdatedEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
public IDockControl? Source { get; }
|
|
||||||
public double X { get; }
|
|
||||||
public double Y { get; }
|
|
||||||
public Core.DragDrop.Models.DragInfo DragInfo { get; }
|
|
||||||
// ... конструктор
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DragCompletedEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
public IDockControl? Source { get; }
|
|
||||||
public IDockControl? Target { get; }
|
|
||||||
public Models.DropPosition DropPosition { get; }
|
|
||||||
public Core.DragDrop.Models.DragInfo? DragInfo { get; }
|
|
||||||
public bool Success { get; }
|
|
||||||
// ... конструктор
|
|
||||||
}
|
|
||||||
@@ -7,15 +7,6 @@ namespace Lattice.UI.Docking.Services;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class DockContextManagerBase : IDockContextManager
|
public abstract class DockContextManagerBase : IDockContextManager
|
||||||
{
|
{
|
||||||
private readonly Dictionary<string, IDockCommand> _commands = new();
|
|
||||||
private IDockControl? _currentContextTarget;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event EventHandler<ContextMenuShownEventArgs>? ContextMenuShown;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event EventHandler? ContextMenuHidden;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public abstract void ShowContextMenu(IDockControl element, double x, double y);
|
public abstract void ShowContextMenu(IDockControl element, double x, double y);
|
||||||
|
|
||||||
@@ -25,25 +16,22 @@ public abstract class DockContextManagerBase : IDockContextManager
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual void RegisterCommand(string commandId, IDockCommand command)
|
public virtual void RegisterCommand(string commandId, IDockCommand command)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(commandId))
|
// Базовая реализация, должна быть переопределена в производных классах
|
||||||
throw new ArgumentNullException(nameof(commandId));
|
|
||||||
|
|
||||||
_commands[commandId] = command ?? throw new ArgumentNullException(nameof(command));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual void UnregisterCommand(string commandId)
|
public virtual void UnregisterCommand(string commandId)
|
||||||
{
|
{
|
||||||
_commands.Remove(commandId);
|
// Базовая реализация, должна быть переопределена в производных классах
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает команду по идентификатору.
|
/// Получает команду по идентификатору.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IDockCommand? GetCommand(string commandId)
|
protected virtual IDockCommand? GetCommand(string commandId)
|
||||||
{
|
{
|
||||||
_commands.TryGetValue(commandId, out var command);
|
// Базовая реализация, должна быть переопределена в производных классах
|
||||||
return command;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -52,7 +40,7 @@ public abstract class DockContextManagerBase : IDockContextManager
|
|||||||
protected virtual IEnumerable<IDockCommand> GetCommandsForElement(IDockControl element)
|
protected virtual IEnumerable<IDockCommand> GetCommandsForElement(IDockControl element)
|
||||||
{
|
{
|
||||||
// Фильтрация команд по типу элемента и его состоянию
|
// Фильтрация команд по типу элемента и его состоянию
|
||||||
return _commands.Values.Where(c => CanExecuteCommand(c, element));
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -76,7 +64,6 @@ public abstract class DockContextManagerBase : IDockContextManager
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual void OnContextMenuShown(IDockControl target, double x, double y)
|
protected virtual void OnContextMenuShown(IDockControl target, double x, double y)
|
||||||
{
|
{
|
||||||
_currentContextTarget = target;
|
|
||||||
ContextMenuShown?.Invoke(this, new ContextMenuShownEventArgs(target, x, y));
|
ContextMenuShown?.Invoke(this, new ContextMenuShownEventArgs(target, x, y));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,12 +72,12 @@ public abstract class DockContextManagerBase : IDockContextManager
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual void OnContextMenuHidden()
|
protected virtual void OnContextMenuHidden()
|
||||||
{
|
{
|
||||||
_currentContextTarget = null;
|
|
||||||
ContextMenuHidden?.Invoke(this, EventArgs.Empty);
|
ContextMenuHidden?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Получает текущий целевой элемент контекстного меню.
|
public event EventHandler<ContextMenuShownEventArgs>? ContextMenuShown;
|
||||||
/// </summary>
|
|
||||||
protected IDockControl? CurrentContextTarget => _currentContextTarget;
|
/// <inheritdoc/>
|
||||||
|
public event EventHandler? ContextMenuHidden;
|
||||||
}
|
}
|
||||||
@@ -1,417 +0,0 @@
|
|||||||
using Lattice.Core.DragDrop.Abstractions;
|
|
||||||
using Lattice.Core.DragDrop.Services;
|
|
||||||
using Lattice.UI.Docking.Abstractions;
|
|
||||||
using Lattice.UI.Docking.Models;
|
|
||||||
|
|
||||||
namespace Lattice.UI.Docking.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Реализация сервиса перетаскивания для UI-слоя док-системы.
|
|
||||||
/// Координирует взаимодействие между базовым менеджером перетаскивания
|
|
||||||
/// и UI-контролами, обеспечивая визуальную обратную связь.
|
|
||||||
/// </summary>
|
|
||||||
public class DockDragDropService : IDockDragDropService
|
|
||||||
{
|
|
||||||
private readonly DragDropManagerEx _dragDropManager;
|
|
||||||
private readonly Dictionary<IDockControl, IDragSource> _registeredDragSources = new();
|
|
||||||
private readonly Dictionary<IDockControl, IDropTarget> _registeredDropTargets = new();
|
|
||||||
private UiDragInfo? _currentUiDragInfo;
|
|
||||||
private UiDropInfo? _currentUiDropInfo;
|
|
||||||
private IDropTarget? _lastDropTarget;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр <see cref="DockDragDropService"/>.
|
|
||||||
/// </summary>
|
|
||||||
public DockDragDropService()
|
|
||||||
{
|
|
||||||
_dragDropManager = new DragDropManagerEx();
|
|
||||||
HookEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр с указанным менеджером перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public DockDragDropService(DragDropManagerEx dragDropManager)
|
|
||||||
{
|
|
||||||
_dragDropManager = dragDropManager;
|
|
||||||
HookEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HookEvents()
|
|
||||||
{
|
|
||||||
_dragDropManager.DragStarted += OnDragStarted;
|
|
||||||
_dragDropManager.DragUpdated += OnDragUpdated;
|
|
||||||
_dragDropManager.DragCompleted += OnDragCompleted;
|
|
||||||
_dragDropManager.DragCancelled += OnDragCancelled;
|
|
||||||
_dragDropManager.DropTargetChanged += OnDropTargetChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void RegisterDragSource(IDockControl element, IDragSource dragSource)
|
|
||||||
{
|
|
||||||
if (element == null) throw new ArgumentNullException(nameof(element));
|
|
||||||
if (dragSource == null) throw new ArgumentNullException(nameof(dragSource));
|
|
||||||
|
|
||||||
_registeredDragSources[element] = dragSource;
|
|
||||||
|
|
||||||
// Регистрируем границы элемента в менеджере
|
|
||||||
var bounds = CalculateBounds(element);
|
|
||||||
_dragDropManager.RegisterDropTarget(dragSource as IDropTarget ?? new AdapterDropTarget(dragSource),
|
|
||||||
bounds, 0, element.GetType().Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void RegisterDropTarget(IDockControl element, IDropTarget dropTarget)
|
|
||||||
{
|
|
||||||
if (element == null) throw new ArgumentNullException(nameof(element));
|
|
||||||
if (dropTarget == null) throw new ArgumentNullException(nameof(dropTarget));
|
|
||||||
|
|
||||||
_registeredDropTargets[element] = dropTarget;
|
|
||||||
|
|
||||||
var bounds = CalculateBounds(element);
|
|
||||||
_dragDropManager.RegisterDropTarget(dropTarget, bounds, 0, element.GetType().Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void UnregisterDragSource(IDockControl element)
|
|
||||||
{
|
|
||||||
if (element == null) throw new ArgumentNullException(nameof(element));
|
|
||||||
|
|
||||||
_registeredDragSources.Remove(element);
|
|
||||||
// TODO: Реализовать отмену регистрации в менеджере
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void UnregisterDropTarget(IDockControl element)
|
|
||||||
{
|
|
||||||
if (element == null) throw new ArgumentNullException(nameof(element));
|
|
||||||
|
|
||||||
_registeredDropTargets.Remove(element);
|
|
||||||
// TODO: Реализовать отмену регистрации в менеджере
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void StartDrag(IDockControl element, Core.DragDrop.Models.DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
if (element == null) throw new ArgumentNullException(nameof(element));
|
|
||||||
if (dragInfo == null) throw new ArgumentNullException(nameof(dragInfo));
|
|
||||||
|
|
||||||
if (_registeredDragSources.TryGetValue(element, out var dragSource))
|
|
||||||
{
|
|
||||||
_currentUiDragInfo = new UiDragInfo(dragInfo, element);
|
|
||||||
_dragDropManager.StartDrag(dragSource, dragInfo.StartPosition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void UpdateDragVisual(double x, double y)
|
|
||||||
{
|
|
||||||
var position = new Core.DragDrop.Geometry.Point(x, y);
|
|
||||||
_dragDropManager.UpdateDrag(position);
|
|
||||||
|
|
||||||
if (_currentUiDragInfo != null)
|
|
||||||
{
|
|
||||||
// Обновляем позицию визуального представления
|
|
||||||
OnDragVisualUpdated?.Invoke(this, new DragVisualUpdatedEventArgs(
|
|
||||||
_currentUiDragInfo, position));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void EndDrag(double x, double y)
|
|
||||||
{
|
|
||||||
var position = new Core.DragDrop.Geometry.Point(x, y);
|
|
||||||
_dragDropManager.EndDrag(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void CancelDrag()
|
|
||||||
{
|
|
||||||
_dragDropManager.CancelDrag();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void ShowDropHint(IDockControl element, DropPosition position)
|
|
||||||
{
|
|
||||||
if (_currentUiDropInfo != null)
|
|
||||||
{
|
|
||||||
_currentUiDropInfo.DropPosition = position;
|
|
||||||
_currentUiDropInfo.IsOverValidTarget = true;
|
|
||||||
_currentUiDropInfo.HighlightIntensity = 0.8;
|
|
||||||
|
|
||||||
OnDropHintChanged?.Invoke(this, new DropHintEventArgs(
|
|
||||||
element, position, true, _currentUiDropInfo.HighlightIntensity));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void HideDropHint()
|
|
||||||
{
|
|
||||||
if (_currentUiDropInfo != null)
|
|
||||||
{
|
|
||||||
_currentUiDropInfo.IsOverValidTarget = false;
|
|
||||||
_currentUiDropInfo.HighlightIntensity = 0.0;
|
|
||||||
|
|
||||||
OnDropHintChanged?.Invoke(this, new DropHintEventArgs(
|
|
||||||
_currentUiDropInfo.TargetControl,
|
|
||||||
_currentUiDropInfo.DropPosition,
|
|
||||||
false,
|
|
||||||
0.0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event EventHandler<DragStartedEventArgs>? DragStarted;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event EventHandler<DragUpdatedEventArgs>? DragUpdated;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event EventHandler<DragCompletedEventArgs>? DragCompleted;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event EventHandler? DragCancelled;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Событие, возникающее при обновлении визуального представления перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public event EventHandler<DragVisualUpdatedEventArgs>? OnDragVisualUpdated;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Событие, возникающее при изменении визуальной подсказки сброса.
|
|
||||||
/// </summary>
|
|
||||||
public event EventHandler<DropHintEventArgs>? OnDropHintChanged;
|
|
||||||
|
|
||||||
private void OnDragStarted(object? sender, DragStartedEventArgs e)
|
|
||||||
{
|
|
||||||
// Обновляем UI-информацию
|
|
||||||
if (_currentUiDragInfo != null)
|
|
||||||
{
|
|
||||||
_currentUiDragInfo.BaseDragInfo.StartPosition = e.StartPosition;
|
|
||||||
|
|
||||||
// Создаем визуальное представление
|
|
||||||
CreateDragVisual(_currentUiDragInfo);
|
|
||||||
|
|
||||||
DragStarted?.Invoke(this, new DragStartedEventArgs(
|
|
||||||
_currentUiDragInfo.SourceControl,
|
|
||||||
_currentUiDragInfo.BaseDragInfo));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDragUpdated(object? sender, DragUpdatedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_currentUiDragInfo != null)
|
|
||||||
{
|
|
||||||
// Обновляем позицию визуального представления
|
|
||||||
UpdateDragVisualPosition(e.Position);
|
|
||||||
|
|
||||||
DragUpdated?.Invoke(this, new DragUpdatedEventArgs(
|
|
||||||
_currentUiDragInfo.SourceControl,
|
|
||||||
e.Position.X,
|
|
||||||
e.Position.Y,
|
|
||||||
_currentUiDragInfo.BaseDragInfo));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDragCompleted(object? sender, DragCompletedEventArgs e)
|
|
||||||
{
|
|
||||||
var targetControl = _currentUiDropInfo?.TargetControl;
|
|
||||||
var dropPosition = _currentUiDropInfo?.DropPosition ?? DropPosition.Center;
|
|
||||||
|
|
||||||
DragCompleted?.Invoke(this, new DragCompletedEventArgs(
|
|
||||||
_currentUiDragInfo?.SourceControl,
|
|
||||||
targetControl,
|
|
||||||
dropPosition,
|
|
||||||
_currentUiDragInfo?.BaseDragInfo,
|
|
||||||
e.Effects != Core.DragDrop.Enums.DragDropEffects.None));
|
|
||||||
|
|
||||||
// Очищаем визуальные представления
|
|
||||||
CleanupDragVisual();
|
|
||||||
CleanupDropHint();
|
|
||||||
|
|
||||||
_currentUiDragInfo = null;
|
|
||||||
_currentUiDropInfo = null;
|
|
||||||
_lastDropTarget = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDragCancelled(object? sender, DragCancelledEventArgs e)
|
|
||||||
{
|
|
||||||
DragCancelled?.Invoke(this, EventArgs.Empty);
|
|
||||||
|
|
||||||
CleanupDragVisual();
|
|
||||||
CleanupDropHint();
|
|
||||||
|
|
||||||
_currentUiDragInfo = null;
|
|
||||||
_currentUiDropInfo = null;
|
|
||||||
_lastDropTarget = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDropTargetChanged(object? sender, DropTargetChangedEventArgs e)
|
|
||||||
{
|
|
||||||
var dropTarget = e.Target;
|
|
||||||
|
|
||||||
// Обновляем UI-информацию о сбросе
|
|
||||||
if (dropTarget != null)
|
|
||||||
{
|
|
||||||
// Находим соответствующий UI-контрол
|
|
||||||
var targetControl = _registeredDropTargets
|
|
||||||
.FirstOrDefault(kv => kv.Value == dropTarget)
|
|
||||||
.Key;
|
|
||||||
|
|
||||||
_currentUiDropInfo = new UiDropInfo(
|
|
||||||
new Core.DragDrop.Models.DropInfo(
|
|
||||||
_dragDropManager.CurrentDragInfo?.Data,
|
|
||||||
e.TargetBounds.Center,
|
|
||||||
_dragDropManager.CurrentDragInfo?.AllowedEffects ?? Core.DragDrop.Enums.DragDropEffects.None,
|
|
||||||
dropTarget),
|
|
||||||
targetControl);
|
|
||||||
|
|
||||||
_currentUiDropInfo.DropPosition = CalculateDropPosition(
|
|
||||||
_currentUiDragInfo?.BaseDragInfo.StartPosition ?? Core.DragDrop.Geometry.Point.Zero,
|
|
||||||
e.TargetBounds);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_currentUiDropInfo = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Уведомляем об изменении цели сброса
|
|
||||||
if (_lastDropTarget != dropTarget)
|
|
||||||
{
|
|
||||||
if (_lastDropTarget != null)
|
|
||||||
{
|
|
||||||
HideDropHint();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dropTarget != null && _currentUiDropInfo != null)
|
|
||||||
{
|
|
||||||
ShowDropHint(_currentUiDropInfo.TargetControl, _currentUiDropInfo.DropPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
_lastDropTarget = dropTarget;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Core.DragDrop.Geometry.Rect CalculateBounds(IDockControl element)
|
|
||||||
{
|
|
||||||
// В UI-реализациях этот метод должен быть переопределен
|
|
||||||
// для вычисления реальных границ элемента на экране
|
|
||||||
return new Core.DragDrop.Geometry.Rect(0, 0, 100, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DropPosition CalculateDropPosition(Core.DragDrop.Geometry.Point cursorPos, Core.DragDrop.Geometry.Rect targetBounds)
|
|
||||||
{
|
|
||||||
// Простая логика определения позиции сброса
|
|
||||||
var center = targetBounds.Center;
|
|
||||||
var relativeX = (cursorPos.X - targetBounds.X) / targetBounds.Width;
|
|
||||||
var relativeY = (cursorPos.Y - targetBounds.Y) / targetBounds.Height;
|
|
||||||
|
|
||||||
if (relativeX < 0.25) return DropPosition.Left;
|
|
||||||
if (relativeX > 0.75) return DropPosition.Right;
|
|
||||||
if (relativeY < 0.25) return DropPosition.Top;
|
|
||||||
if (relativeY > 0.75) return DropPosition.Bottom;
|
|
||||||
|
|
||||||
return DropPosition.Center;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CreateDragVisual(UiDragInfo dragInfo)
|
|
||||||
{
|
|
||||||
// В UI-реализациях этот метод должен создавать визуальное представление
|
|
||||||
OnDragVisualUpdated?.Invoke(this, new DragVisualUpdatedEventArgs(
|
|
||||||
dragInfo, dragInfo.BaseDragInfo.StartPosition));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateDragVisualPosition(Core.DragDrop.Geometry.Point position)
|
|
||||||
{
|
|
||||||
if (_currentUiDragInfo != null)
|
|
||||||
{
|
|
||||||
OnDragVisualUpdated?.Invoke(this, new DragVisualUpdatedEventArgs(
|
|
||||||
_currentUiDragInfo, position));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CleanupDragVisual()
|
|
||||||
{
|
|
||||||
// В UI-реализациях этот метод должен очищать визуальное представление
|
|
||||||
OnDragVisualUpdated?.Invoke(this, new DragVisualUpdatedEventArgs(null, Core.DragDrop.Geometry.Point.Zero));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CleanupDropHint()
|
|
||||||
{
|
|
||||||
HideDropHint();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Адаптер для преобразования IDragSource в IDropTarget.
|
|
||||||
/// </summary>
|
|
||||||
internal class AdapterDropTarget : IDropTarget
|
|
||||||
{
|
|
||||||
private readonly IDragSource _dragSource;
|
|
||||||
|
|
||||||
public AdapterDropTarget(IDragSource dragSource)
|
|
||||||
{
|
|
||||||
_dragSource = dragSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanAcceptDrop(Core.DragDrop.Models.DropInfo dropInfo) => false;
|
|
||||||
public void DragOver(Core.DragDrop.Models.DropInfo dropInfo) { }
|
|
||||||
public void Drop(Core.DragDrop.Models.DropInfo dropInfo) { }
|
|
||||||
public void DragLeave() { }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Аргументы события обновления визуального представления перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public class DragVisualUpdatedEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Информация о перетаскивании.
|
|
||||||
/// </summary>
|
|
||||||
public UiDragInfo? DragInfo { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Текущая позиция.
|
|
||||||
/// </summary>
|
|
||||||
public Core.DragDrop.Geometry.Point Position { get; }
|
|
||||||
|
|
||||||
public DragVisualUpdatedEventArgs(UiDragInfo? dragInfo, Core.DragDrop.Geometry.Point position)
|
|
||||||
{
|
|
||||||
DragInfo = dragInfo;
|
|
||||||
Position = position;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Аргументы события изменения визуальной подсказки сброса.
|
|
||||||
/// </summary>
|
|
||||||
public class DropHintEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Целевой элемент.
|
|
||||||
/// </summary>
|
|
||||||
public IDockControl? Target { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Позиция сброса.
|
|
||||||
/// </summary>
|
|
||||||
public DropPosition Position { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Показывает, видима ли подсказка.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsVisible { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Интенсивность подсветки.
|
|
||||||
/// </summary>
|
|
||||||
public double Intensity { get; }
|
|
||||||
|
|
||||||
public DropHintEventArgs(IDockControl? target, DropPosition position, bool isVisible, double intensity)
|
|
||||||
{
|
|
||||||
Target = target;
|
|
||||||
Position = position;
|
|
||||||
IsVisible = isVisible;
|
|
||||||
Intensity = intensity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Lattice.UI.Docking.Abstractions;
|
// Lattice.UI.Docking\Services\DockUIServiceBase.cs
|
||||||
|
using Lattice.UI.Docking.Abstractions;
|
||||||
|
|
||||||
namespace Lattice.UI.Docking.Services;
|
namespace Lattice.UI.Docking.Services;
|
||||||
|
|
||||||
@@ -7,107 +8,21 @@ namespace Lattice.UI.Docking.Services;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class DockUIServiceBase : IDockUIService
|
public abstract class DockUIServiceBase : IDockUIService
|
||||||
{
|
{
|
||||||
private IDockTheme? _currentTheme;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public abstract object CreateMainWindow(IDockHost host);
|
public abstract object CreateMainWindow(IDockHost host);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual bool? ShowDialog(string title, object content)
|
public abstract bool? ShowDialog(string title, object content);
|
||||||
{
|
|
||||||
// Базовая реализация - просто возвращает null
|
|
||||||
// В производных классах должна быть реальная реализация
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual void ShowMessage(string message, string caption)
|
public abstract void ShowMessage(string message, string caption);
|
||||||
{
|
|
||||||
// Базовая реализация не делает ничего
|
|
||||||
// В производных классах должна быть реальная реализация
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual bool Confirm(string message, string caption)
|
public abstract bool Confirm(string message, string caption);
|
||||||
{
|
|
||||||
// Базовая реализация всегда возвращает true
|
|
||||||
// В производных классах должна быть реальная реализация
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual string? Prompt(string prompt, string? defaultValue = null)
|
public abstract string? Prompt(string prompt, string? defaultValue = null);
|
||||||
{
|
|
||||||
// Базовая реализация возвращает значение по умолчанию
|
|
||||||
// В производных классах должна быть реальная реализация
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual void InvokeOnUIThread(Action action)
|
public abstract void InvokeOnUIThread(Action action);
|
||||||
{
|
|
||||||
// Базовая реализация просто выполняет действие
|
|
||||||
// В производных классах должна быть синхронизация с UI-потоком
|
|
||||||
action?.Invoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public virtual IDockTheme GetCurrentTheme()
|
|
||||||
{
|
|
||||||
return _currentTheme ?? CreateDefaultTheme();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public virtual void SetTheme(IDockTheme theme)
|
|
||||||
{
|
|
||||||
_currentTheme = theme;
|
|
||||||
theme.Apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает тему по умолчанию.
|
|
||||||
/// </summary>
|
|
||||||
protected virtual IDockTheme CreateDefaultTheme()
|
|
||||||
{
|
|
||||||
return new DefaultDockTheme();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Тема оформления по умолчанию.
|
|
||||||
/// </summary>
|
|
||||||
public class DefaultDockTheme : IDockTheme
|
|
||||||
{
|
|
||||||
public string Name => "Default";
|
|
||||||
public string BackgroundColor { get; set; } = "#1E1E1E";
|
|
||||||
public string PanelBackgroundColor { get; set; } = "#252526";
|
|
||||||
public string TabBackgroundColor { get; set; } = "#2D2D2D";
|
|
||||||
public string ActiveTabBackgroundColor { get; set; } = "#3E3E3E";
|
|
||||||
public string BorderColor { get; set; } = "#3F3F46";
|
|
||||||
public string SplitterColor { get; set; } = "#2D2D2D";
|
|
||||||
public string TextColor { get; set; } = "#CCCCCC";
|
|
||||||
public string AccentColor { get; set; } = "#007ACC";
|
|
||||||
public double CornerRadius { get; set; } = 3.0;
|
|
||||||
public double BorderThickness { get; set; } = 1.0;
|
|
||||||
public double SplitterWidth { get; set; } = 4.0;
|
|
||||||
|
|
||||||
public void Apply()
|
|
||||||
{
|
|
||||||
// В UI-реализациях этот метод должен применять тему к элементам
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Reset()
|
|
||||||
{
|
|
||||||
BackgroundColor = "#1E1E1E";
|
|
||||||
PanelBackgroundColor = "#252526";
|
|
||||||
TabBackgroundColor = "#2D2D2D";
|
|
||||||
ActiveTabBackgroundColor = "#3E3E3E";
|
|
||||||
BorderColor = "#3F3F46";
|
|
||||||
SplitterColor = "#2D2D2D";
|
|
||||||
TextColor = "#CCCCCC";
|
|
||||||
AccentColor = "#007ACC";
|
|
||||||
CornerRadius = 3.0;
|
|
||||||
BorderThickness = 1.0;
|
|
||||||
SplitterWidth = 4.0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -34,21 +34,16 @@ public static class DockUtilities
|
|||||||
var control = factory.CreateControlForElement(element);
|
var control = factory.CreateControlForElement(element);
|
||||||
if (control == null) return null;
|
if (control == null) return null;
|
||||||
|
|
||||||
// Устанавливаем родительский контрол
|
|
||||||
if (parentControl != null)
|
if (parentControl != null)
|
||||||
{
|
{
|
||||||
// Здесь может быть установка дополнительных связей
|
// Здесь может быть установка дополнительных связей
|
||||||
}
|
}
|
||||||
|
|
||||||
// Рекурсивно создаем дочерние контролы
|
if (element is DockGroup group && control is IDockGroupControl groupControl)
|
||||||
if (element is DockGroup group)
|
|
||||||
{
|
{
|
||||||
if (control is IDockGroupControl groupControl)
|
var firstChild = CreateControlForElement(group.First, factory, control);
|
||||||
{
|
var secondChild = CreateControlForElement(group.Second, factory, control);
|
||||||
var firstChild = CreateControlForElement(group.First, factory, control);
|
groupControl.SetChildren(firstChild, secondChild);
|
||||||
var secondChild = CreateControlForElement(group.Second, factory, control);
|
|
||||||
groupControl.SetChildren(firstChild, secondChild);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return control;
|
return control;
|
||||||
@@ -57,10 +52,9 @@ public static class DockUtilities
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Находит контрол для указанного элемента в дереве контролов.
|
/// Находит контрол для указанного элемента в дереве контролов.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IDockControl? FindControlForElement(IDockControl root, IDockElement element)
|
public static IDockControl? FindControlForElement(IDockControl? root, IDockElement element)
|
||||||
{
|
{
|
||||||
if (root == null) throw new ArgumentNullException(nameof(root));
|
if (root == null || element == null) return null;
|
||||||
if (element == null) throw new ArgumentNullException(nameof(element));
|
|
||||||
|
|
||||||
if (root.Model?.Id == element.Id)
|
if (root.Model?.Id == element.Id)
|
||||||
return root;
|
return root;
|
||||||
@@ -88,6 +82,5 @@ public static class DockUtilities
|
|||||||
if (factory == null) throw new ArgumentNullException(nameof(factory));
|
if (factory == null) throw new ArgumentNullException(nameof(factory));
|
||||||
|
|
||||||
// TODO: Реализовать эффективное обновление дерева контролов
|
// TODO: Реализовать эффективное обновление дерева контролов
|
||||||
// вместо полной перестройки
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
using Lattice.Core.DragDrop.Models;
|
|
||||||
using Lattice.Core.DragDrop.Services;
|
|
||||||
using Lattice.Core.Geometry;
|
|
||||||
using Lattice.UI.DragDrop.Abstractions;
|
|
||||||
using Lattice.UI.DragDrop.Behaviors;
|
|
||||||
using Lattice.UI.DragDrop.WinUI.Services;
|
|
||||||
using Microsoft.UI.Xaml;
|
|
||||||
using Microsoft.UI.Xaml.Input;
|
|
||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Lattice.UI.DragDrop.WinUI.Behaviors;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Реализация поведения источника перетаскивания для элементов WinUI.
|
|
||||||
/// Наследуется от <see cref="DragSourceBehaviorBase{FrameworkElement}"/> для использования
|
|
||||||
/// общей логики управления операциями перетаскивания и интеграции с системой <see cref="Core.DragDrop"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот класс предоставляет конкретную реализацию для платформы WinUI, обрабатывая события
|
|
||||||
/// указателя (мышь, тач, перо) и преобразуя их в операции перетаскивания через центральный
|
|
||||||
/// сервис <see cref="IDragDropService"/>.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Основные функции:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Обработка событий PointerPressed, PointerMoved, PointerReleased</item>
|
|
||||||
/// <item>Автоматическое отслеживание порога начала перетаскивания</item>
|
|
||||||
/// <item>Создание информации о перетаскивании на основе данных элемента</item>
|
|
||||||
/// <item>Интеграция с визуальной обратной связью через <see cref="WinUIDragDropHost"/></item>
|
|
||||||
/// <item>Преобразование координат между локальной системой элемента и экранными координатами</item>
|
|
||||||
/// </list>
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Для использования необходимо:
|
|
||||||
/// <list type="number">
|
|
||||||
/// <item>Создать экземпляр поведения через фабрику <see cref="Factories.WinUIDragDropFactory.CreateDragSourceBehavior"/></item>
|
|
||||||
/// <item>Прикрепить к элементу с помощью метода <see cref="Attach"/></item>
|
|
||||||
/// <item>Указать данные для перетаскивания (опционально, по умолчанию используется DataContext)</item>
|
|
||||||
/// </list>
|
|
||||||
/// </para>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// // Создание поведения
|
|
||||||
/// var behavior = WinUIDragDropFactory.CreateDragSourceBehavior(dragDropService, host);
|
|
||||||
///
|
|
||||||
/// // Прикрепление к элементу
|
|
||||||
/// behavior.Attach(myElement, myData);
|
|
||||||
///
|
|
||||||
/// // Или через attached properties
|
|
||||||
/// <Border x:Name="DragElement"
|
|
||||||
/// local:DragDropProperties.IsDragSource="True"
|
|
||||||
/// local:DragDropProperties.DragData="{Binding MyData}" />
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
/// </remarks>
|
|
||||||
public sealed class WinUIDragSourceBehavior : DragSourceBehaviorBase<FrameworkElement>
|
|
||||||
{
|
|
||||||
#region Поля
|
|
||||||
|
|
||||||
private readonly IDragDropHost _host;
|
|
||||||
private object? _dragData;
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Конструктор
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="WinUIDragSourceBehavior"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragDropService">
|
|
||||||
/// Сервис управления операциями перетаскивания. Используется для координации
|
|
||||||
/// между источниками и целями перетаскивания.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="host">
|
|
||||||
/// Хост для управления визуальными элементами перетаскивания. Обеспечивает
|
|
||||||
/// отображение визуальной обратной связи во время операции.
|
|
||||||
/// </param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="dragDropService"/> или <paramref name="host"/>
|
|
||||||
/// равны null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Конструктор инициализирует базовый класс <see cref="DragSourceBehaviorBase{FrameworkElement}"/>
|
|
||||||
/// и сохраняет ссылки на необходимые сервисы для последующего использования.
|
|
||||||
/// </remarks>
|
|
||||||
public WinUIDragSourceBehavior(
|
|
||||||
Core.DragDrop.Services.IDragDropService dragDropService,
|
|
||||||
WinUIDragDropHost host)
|
|
||||||
: base(dragDropService)
|
|
||||||
{
|
|
||||||
_host = host ?? throw new ArgumentNullException(nameof(host));
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Публичные методы
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Прикрепляет поведение к указанному элементу WinUI.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="element">
|
|
||||||
/// Элемент <see cref="FrameworkElement"/>, который должен стать источником перетаскивания.
|
|
||||||
/// Не может быть null.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="dragData">
|
|
||||||
/// Данные, которые будут перетаскиваться. Может быть null.
|
|
||||||
/// Если не указано, используется <see cref="FrameworkElement.DataContext"/> или
|
|
||||||
/// <see cref="FrameworkElement.Tag"/> элемента.
|
|
||||||
/// </param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="element"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// После вызова этого метода:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Элементу автоматически подписываются обработчики событий указателя</item>
|
|
||||||
/// <item>Поведение начинает отслеживать взаимодействия с элементом</item>
|
|
||||||
/// <item>При превышении порога перетаскивания инициируется операция через <see cref="Core.DragDrop.Services.IDragDropService"/></item>
|
|
||||||
/// </list>
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Для открепления поведения используйте метод <see cref="Detach"/>.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public void Attach(FrameworkElement element, object? dragData = null)
|
|
||||||
{
|
|
||||||
if (element == null)
|
|
||||||
throw new ArgumentNullException(nameof(element));
|
|
||||||
|
|
||||||
_dragData = dragData ?? element.DataContext ?? element.Tag;
|
|
||||||
AssociatedElement = element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Открепляет поведение от текущего элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод выполняет следующие действия:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Отписывается от всех событий элемента</item>
|
|
||||||
/// <item>Сбрасывает внутреннее состояние</item>
|
|
||||||
/// <item>Освобождает ссылки на связанные объекты</item>
|
|
||||||
/// </list>
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Если в момент вызова активна операция перетаскивания, она будет автоматически отменена.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// После вызова этого метода поведение может быть повторно прикреплено к другому элементу.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public new void Detach()
|
|
||||||
{
|
|
||||||
base.Detach();
|
|
||||||
_dragData = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Реализация абстрактных методов DragSourceBehaviorBase<FrameworkElement>
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
protected override void SubscribeToEvents(FrameworkElement element)
|
|
||||||
{
|
|
||||||
if (element == null) return;
|
|
||||||
|
|
||||||
element.PointerPressed += OnPointerPressed;
|
|
||||||
element.PointerMoved += OnPointerMoved;
|
|
||||||
element.PointerReleased += OnPointerReleased;
|
|
||||||
element.PointerCanceled += OnPointerCanceled;
|
|
||||||
element.PointerCaptureLost += OnPointerCaptureLost;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
protected override void UnsubscribeFromEvents(FrameworkElement element)
|
|
||||||
{
|
|
||||||
if (element == null) return;
|
|
||||||
|
|
||||||
element.PointerPressed -= OnPointerPressed;
|
|
||||||
element.PointerMoved -= OnPointerMoved;
|
|
||||||
element.PointerReleased -= OnPointerReleased;
|
|
||||||
element.PointerCanceled -= OnPointerCanceled;
|
|
||||||
element.PointerCaptureLost -= OnPointerCaptureLost;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
protected override Point ConvertToScreenCoordinates(Point point)
|
|
||||||
{
|
|
||||||
if (AssociatedElement == null)
|
|
||||||
return point;
|
|
||||||
|
|
||||||
return WinUIWindowHelper.ConvertToScreenCoordinates(AssociatedElement, point);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Реализация интерфейса IDragSource
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override async Task<DragInfo?> TryStartDragAsync(
|
|
||||||
Point startPosition,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (AssociatedElement == null || _dragData == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Создаем информацию о перетаскивании
|
|
||||||
var dragInfo = new DragInfo(
|
|
||||||
data: _dragData,
|
|
||||||
allowedEffects: Core.DragDrop.Enums.DragDropEffects.Copy |
|
|
||||||
Core.DragDrop.Enums.DragDropEffects.Move |
|
|
||||||
Core.DragDrop.Enums.DragDropEffects.Link,
|
|
||||||
startPosition: startPosition,
|
|
||||||
source: this
|
|
||||||
);
|
|
||||||
|
|
||||||
// Добавляем дополнительные параметры
|
|
||||||
dragInfo.SetParameter("SourceElement", AssociatedElement);
|
|
||||||
dragInfo.SetParameter("SourceType", AssociatedElement.GetType().Name);
|
|
||||||
|
|
||||||
return await Task.FromResult(dragInfo);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Логирование ошибки создания информации о перетаскивании
|
|
||||||
System.Diagnostics.Debug.WriteLine(
|
|
||||||
$"Ошибка создания DragInfo: {ex.Message}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Обработчики событий WinUI
|
|
||||||
|
|
||||||
private async void OnPointerPressed(object sender, PointerRoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (AssociatedElement == null) return;
|
|
||||||
|
|
||||||
var point = e.GetCurrentPoint(AssociatedElement);
|
|
||||||
var position = new Point(point.Position.X, point.Position.Y);
|
|
||||||
|
|
||||||
await OnInteractionStarted(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnPointerMoved(object sender, PointerRoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (AssociatedElement == null) return;
|
|
||||||
|
|
||||||
var point = e.GetCurrentPoint(AssociatedElement);
|
|
||||||
var position = new Point(point.Position.X, point.Position.Y);
|
|
||||||
|
|
||||||
await OnInteractionMoved(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnPointerReleased(object sender, PointerRoutedEventArgs e)
|
|
||||||
{
|
|
||||||
await OnInteractionEnded();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnPointerCanceled(object sender, PointerRoutedEventArgs e)
|
|
||||||
{
|
|
||||||
await OnInteractionCancelled();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnPointerCaptureLost(object sender, PointerRoutedEventArgs e)
|
|
||||||
{
|
|
||||||
await OnInteractionCancelled();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Переопределение виртуальных методов
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
protected override void OnDragCompleted(DragInfo dragInfo, Core.DragDrop.Enums.DragDropEffects effects)
|
|
||||||
{
|
|
||||||
base.OnDragCompleted(dragInfo, effects);
|
|
||||||
|
|
||||||
// Дополнительная логика для WinUI может быть добавлена здесь
|
|
||||||
// Например, обновление состояния элемента после успешного перетаскивания
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
protected override void OnDragCancelled(DragInfo dragInfo)
|
|
||||||
{
|
|
||||||
base.OnDragCancelled(dragInfo);
|
|
||||||
|
|
||||||
// Дополнительная логика для WinUI может быть добавлена здесь
|
|
||||||
// Например, восстановление визуального состояния элемента после отмены
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
@@ -1,514 +0,0 @@
|
|||||||
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;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Реализация поведения цели сброса для элементов WinUI.
|
|
||||||
/// Наследуется от <see cref="DropTargetBehaviorBase{FrameworkElement}"/> для использования
|
|
||||||
/// общей логики регистрации целей и обработки операций сброса.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот класс предоставляет конкретную реализацию для платформы WinUI, обрабатывая события
|
|
||||||
/// перетаскивания WinUI и преобразуя их в вызовы методов интерфейса <see cref="Core.DragDrop.Abstractions.IDropTarget"/>.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Основные функции:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Автоматическая регистрация в <see cref="IDragDropService"/> при прикреплении к элементу</item>
|
|
||||||
/// <item>Обработка событий DragEnter, DragOver, DragLeave, Drop</item>
|
|
||||||
/// <item>Автоматическое обновление границ элемента при изменении размера или позиции</item>
|
|
||||||
/// <item>Поддержка фильтрации принимаемых типов данных</item>
|
|
||||||
/// <item>Интеграция с визуальной обратной связью</item>
|
|
||||||
/// </list>
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Для использования необходимо:
|
|
||||||
/// <list type="number">
|
|
||||||
/// <item>Создать экземпляр поведения через фабрику <see cref="Factories.WinUIDragDropFactory.CreateDropTargetBehavior"/></item>
|
|
||||||
/// <item>Прикрепить к элементу с помощью метода <see cref="Attach"/></item>
|
|
||||||
/// <item>Настроить фильтры принимаемых данных (опционально)</item>
|
|
||||||
/// </list>
|
|
||||||
/// </para>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// // Создание поведения с фильтрацией типов
|
|
||||||
/// 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" />
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
/// </remarks>
|
|
||||||
public sealed class WinUIDropTargetBehavior : DropTargetBehaviorBase<FrameworkElement>
|
|
||||||
{
|
|
||||||
#region Поля
|
|
||||||
|
|
||||||
private readonly IDragDropHost _host;
|
|
||||||
private readonly List<Type> _acceptedTypes = new();
|
|
||||||
private readonly HashSet<string> _acceptedFormats = new();
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Конструктор
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="WinUIDropTargetBehavior"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragDropService">
|
|
||||||
/// Сервис управления операциями перетаскивания. Используется для регистрации
|
|
||||||
/// цели и координации операций сброса.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="host">
|
|
||||||
/// Хост для управления визуальной обратной связью. Обеспечивает отображение
|
|
||||||
/// индикаторов возможности сброса.
|
|
||||||
/// </param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="dragDropService"/> или <paramref name="host"/>
|
|
||||||
/// равны null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Конструктор инициализирует базовый класс и сохраняет ссылки на сервисы.
|
|
||||||
/// По умолчанию цель принимает все типы данных. Для настройки фильтрации
|
|
||||||
/// используйте методы <see cref="AcceptTypes"/> и <see cref="AcceptFormats"/>.
|
|
||||||
/// </remarks>
|
|
||||||
public WinUIDropTargetBehavior(
|
|
||||||
Core.DragDrop.Services.IDragDropService dragDropService,
|
|
||||||
IDragDropHost host)
|
|
||||||
: base(dragDropService)
|
|
||||||
{
|
|
||||||
_host = host ?? throw new ArgumentNullException(nameof(host));
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Публичные методы
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Прикрепляет поведение к указанному элементу WinUI.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="element">
|
|
||||||
/// Элемент <see cref="FrameworkElement"/>, который должен стать целью сброса.
|
|
||||||
/// Не может быть null.
|
|
||||||
/// </param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="element"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// После вызова этого метода:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Элементу устанавливается свойство <see cref="UIElement.AllowDrop"/> = true</item>
|
|
||||||
/// <item>Поведение подписывается на события перетаскивания WinUI</item>
|
|
||||||
/// <item>Элемент регистрируется в системе перетаскивания с текущими границами</item>
|
|
||||||
/// <item>Начинается отслеживание изменений размера и позиции элемента</item>
|
|
||||||
/// </list>
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Для открепления поведения используйте метод <see cref="Detach"/>.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public void Attach(FrameworkElement element)
|
|
||||||
{
|
|
||||||
if (element == null)
|
|
||||||
throw new ArgumentNullException(nameof(element));
|
|
||||||
|
|
||||||
element.AllowDrop = true;
|
|
||||||
AssociatedElement = element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Настраивает поведение для приема только указанных типов данных.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="types">
|
|
||||||
/// Типы данных, которые может принимать цель. Если пусто, принимаются все типы.
|
|
||||||
/// </param>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод позволяет ограничить типы данных, которые могут быть сброшены на цель.
|
|
||||||
/// Проверка выполняется в методе <see cref="CanAcceptDropAsync"/> путем сравнения
|
|
||||||
/// типа сбрасываемых данных с указанными типами.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Если метод не вызывался или передан пустой список, цель будет принимать данные любого типа.
|
|
||||||
/// </para>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// // Принимать только строки и объекты MyModel
|
|
||||||
/// behavior.AcceptTypes(typeof(string), typeof(MyModel));
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
/// </remarks>
|
|
||||||
public void AcceptTypes(params Type[] types)
|
|
||||||
{
|
|
||||||
_acceptedTypes.Clear();
|
|
||||||
if (types != null && types.Length > 0)
|
|
||||||
{
|
|
||||||
_acceptedTypes.AddRange(types);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Настраивает поведение для приема только указанных форматов данных.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="formats">
|
|
||||||
/// Форматы данных (например, "Text", "Bitmap", "FileDrop"), которые может принимать цель.
|
|
||||||
/// Если пусто, формат не проверяется.
|
|
||||||
/// </param>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод позволяет ограничить форматы данных, которые могут быть сброшены на цель.
|
|
||||||
/// Актуально для межпроцессного перетаскивания или работы с системными форматами.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Если метод не вызывался или передан пустой список, проверка формата не выполняется.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public void AcceptFormats(params string[] formats)
|
|
||||||
{
|
|
||||||
_acceptedFormats.Clear();
|
|
||||||
if (formats != null && formats.Length > 0)
|
|
||||||
{
|
|
||||||
foreach (var format in formats)
|
|
||||||
{
|
|
||||||
_acceptedFormats.Add(format);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Открепляет поведение от текущего элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод выполняет следующие действия:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Отписывается от всех событий элемента</item>
|
|
||||||
/// <item>Отменяет регистрацию цели в системе перетаскивания</item>
|
|
||||||
/// <item>Сбрасывает свойство <see cref="UIElement.AllowDrop"/> = false</item>
|
|
||||||
/// <item>Освобождает ссылки на связанные объекты</item>
|
|
||||||
/// </list>
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// После вызова этого метода поведение может быть повторно прикреплено к другому элементу.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public new void Detach()
|
|
||||||
{
|
|
||||||
if (AssociatedElement != null)
|
|
||||||
{
|
|
||||||
AssociatedElement.AllowDrop = false;
|
|
||||||
}
|
|
||||||
base.Detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Реализация абстрактных методов DropTargetBehaviorBase<FrameworkElement>
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
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
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override async Task<bool> 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override async Task OnDragOverAsync(
|
|
||||||
DropInfo dropInfo,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
await base.OnDragOverAsync(dropInfo, cancellationToken);
|
|
||||||
|
|
||||||
// Дополнительная логика для WinUI может быть добавлена здесь
|
|
||||||
// Например, обновление визуальной обратной связи через хост
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override async Task OnDropAsync(
|
|
||||||
DropInfo dropInfo,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
// Базовая реализация вызывает CanAcceptDropAsync и помечает как обработанное
|
|
||||||
if (await CanAcceptDropAsync(dropInfo, cancellationToken))
|
|
||||||
{
|
|
||||||
dropInfo.MarkAsHandled();
|
|
||||||
|
|
||||||
// Здесь может быть добавлена логика обработки сброшенных данных
|
|
||||||
// Например, вызов события или обновление модели данных
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
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 Вспомогательные методы
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает объект <see cref="DropInfo"/> на основе события перетаскивания WinUI.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="e">Аргументы события перетаскивания WinUI.</param>
|
|
||||||
/// <param name="position">Локальная позиция курсора относительно элемента.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// Экземпляр <see cref="DropInfo"/>, содержащий информацию о потенциальном сбросе.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод извлекает данные из события перетаскивания и преобразует их
|
|
||||||
/// в формат, понятный системе <see cref="Core.DragDrop"/>.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Поддерживаются как пользовательские данные (через свойство "DragData"),
|
|
||||||
/// так и стандартные форматы данных WinUI.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Предоставляет асинхронный доступ к данным перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот класс используется для отложенной загрузки данных перетаскивания,
|
|
||||||
/// что особенно важно для больших данных или данных, требующих обработки.
|
|
||||||
/// </remarks>
|
|
||||||
internal class AsyncDataProvider
|
|
||||||
{
|
|
||||||
private readonly Func<Task<object>> _dataLoader;
|
|
||||||
|
|
||||||
public AsyncDataProvider(Func<Task<object>> dataLoader)
|
|
||||||
{
|
|
||||||
_dataLoader = dataLoader;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<object> GetDataAsync()
|
|
||||||
{
|
|
||||||
return await _dataLoader();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
using Lattice.Core.Geometry;
|
|
||||||
using Microsoft.UI.Xaml;
|
|
||||||
using Microsoft.UI.Xaml.Controls;
|
|
||||||
using Microsoft.UI.Xaml.Media;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Lattice.UI.DragDrop.WinUI.Controls;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Визуальный элемент, отображаемый во время перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот элемент отображает репрезентативное представление перетаскиваемых данных
|
|
||||||
/// и следует за курсором мыши во время операции перетаскивания.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Элемент поддерживает настройку прозрачности, смещения и угла поворота,
|
|
||||||
/// а также анимированное появление и скрытие.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public class DragAdorner : Control
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Идентификатор свойства для данных перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DependencyProperty DragDataProperty =
|
|
||||||
DependencyProperty.Register(
|
|
||||||
"DragData",
|
|
||||||
typeof(object),
|
|
||||||
typeof(DragAdorner),
|
|
||||||
new PropertyMetadata(null, OnDragDataChanged));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Идентификатор свойства для смещения относительно курсора.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DependencyProperty OffsetProperty =
|
|
||||||
DependencyProperty.Register(
|
|
||||||
"Offset",
|
|
||||||
typeof(Point),
|
|
||||||
typeof(DragAdorner),
|
|
||||||
new PropertyMetadata(new Point(0, 0)));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Идентификатор свойства для угла поворота.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DependencyProperty RotationAngleProperty =
|
|
||||||
DependencyProperty.Register(
|
|
||||||
"RotationAngle",
|
|
||||||
typeof(double),
|
|
||||||
typeof(DragAdorner),
|
|
||||||
new PropertyMetadata(0.0));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Идентификатор свойства для прозрачности.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DependencyProperty OpacityLevelProperty =
|
|
||||||
DependencyProperty.Register(
|
|
||||||
"OpacityLevel",
|
|
||||||
typeof(double),
|
|
||||||
typeof(DragAdorner),
|
|
||||||
new PropertyMetadata(0.7));
|
|
||||||
|
|
||||||
private ContentPresenter? _contentPresenter;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DragAdorner"/>.
|
|
||||||
/// </summary>
|
|
||||||
public DragAdorner()
|
|
||||||
{
|
|
||||||
DefaultStyleKey = typeof(DragAdorner);
|
|
||||||
|
|
||||||
// Устанавливаем свойства для корректного отображения поверх других элементов
|
|
||||||
IsHitTestVisible = false;
|
|
||||||
UseSystemFocusVisuals = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает данные, которые отображаются в визуальном элементе.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Объект данных для отображения. Обычно это те же данные, которые перетаскиваются.
|
|
||||||
/// </value>
|
|
||||||
public object DragData
|
|
||||||
{
|
|
||||||
get => GetValue(DragDataProperty);
|
|
||||||
set => SetValue(DragDataProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает смещение элемента относительно позиции курсора.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Смещение по осям X и Y. Используется для позиционирования элемента так,
|
|
||||||
/// чтобы он не перекрывал курсор. Значение по умолчанию вычисляется автоматически
|
|
||||||
/// на основе размера элемента.
|
|
||||||
/// </value>
|
|
||||||
public Point Offset
|
|
||||||
{
|
|
||||||
get => (Point)GetValue(OffsetProperty);
|
|
||||||
set => SetValue(OffsetProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает угол поворота.
|
|
||||||
/// </summary>
|
|
||||||
public double RotationAngle
|
|
||||||
{
|
|
||||||
get => (double)GetValue(RotationAngleProperty);
|
|
||||||
set => SetValue(RotationAngleProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает уровень прозрачности.
|
|
||||||
/// </summary>
|
|
||||||
public double OpacityLevel
|
|
||||||
{
|
|
||||||
get => (double)GetValue(OpacityLevelProperty);
|
|
||||||
set => SetValue(OpacityLevelProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
protected override void OnApplyTemplate()
|
|
||||||
{
|
|
||||||
base.OnApplyTemplate();
|
|
||||||
|
|
||||||
_contentPresenter = GetTemplateChild("PART_ContentPresenter") as ContentPresenter;
|
|
||||||
UpdateContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Обновляет позицию элемента в соответствии с позицией курсора.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="cursorPosition">Текущая позиция курсора в экранных координатах.</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Метод применяет трансформации для позиционирования элемента с учетом
|
|
||||||
/// заданного смещения и угла поворота.
|
|
||||||
/// </remarks>
|
|
||||||
public void UpdatePosition(Point cursorPosition)
|
|
||||||
{
|
|
||||||
var transform = new TranslateTransform
|
|
||||||
{
|
|
||||||
X = cursorPosition.X + Offset.X,
|
|
||||||
Y = cursorPosition.Y + Offset.Y
|
|
||||||
};
|
|
||||||
|
|
||||||
RenderTransform = new TransformGroup
|
|
||||||
{
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
transform,
|
|
||||||
new RotateTransform { Angle = RotationAngle }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Показывает элемент с анимацией.
|
|
||||||
/// </summary>
|
|
||||||
public void Show()
|
|
||||||
{
|
|
||||||
Visibility = Visibility.Visible;
|
|
||||||
|
|
||||||
// Анимация появления
|
|
||||||
var animation = new Microsoft.UI.Xaml.Media.Animation.DoubleAnimation
|
|
||||||
{
|
|
||||||
From = 0,
|
|
||||||
To = OpacityLevel,
|
|
||||||
Duration = TimeSpan.FromMilliseconds(150),
|
|
||||||
EasingFunction = new Microsoft.UI.Xaml.Media.Animation.CubicEase
|
|
||||||
{
|
|
||||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var storyboard = new Microsoft.UI.Xaml.Media.Animation.Storyboard();
|
|
||||||
storyboard.Children.Add(animation);
|
|
||||||
Microsoft.UI.Xaml.Media.Animation.Storyboard.SetTarget(animation, this);
|
|
||||||
Microsoft.UI.Xaml.Media.Animation.Storyboard.SetTargetProperty(animation, "Opacity");
|
|
||||||
|
|
||||||
storyboard.Begin();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Скрывает элемент с анимацией.
|
|
||||||
/// </summary>
|
|
||||||
public void Hide()
|
|
||||||
{
|
|
||||||
var animation = new Microsoft.UI.Xaml.Media.Animation.DoubleAnimation
|
|
||||||
{
|
|
||||||
From = Opacity,
|
|
||||||
To = 0,
|
|
||||||
Duration = TimeSpan.FromMilliseconds(100),
|
|
||||||
EasingFunction = new Microsoft.UI.Xaml.Media.Animation.CubicEase
|
|
||||||
{
|
|
||||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
animation.Completed += (s, e) =>
|
|
||||||
{
|
|
||||||
Visibility = Visibility.Collapsed;
|
|
||||||
};
|
|
||||||
|
|
||||||
var storyboard = new Microsoft.UI.Xaml.Media.Animation.Storyboard();
|
|
||||||
storyboard.Children.Add(animation);
|
|
||||||
Microsoft.UI.Xaml.Media.Animation.Storyboard.SetTarget(animation, this);
|
|
||||||
Microsoft.UI.Xaml.Media.Animation.Storyboard.SetTargetProperty(animation, "Opacity");
|
|
||||||
|
|
||||||
storyboard.Begin();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void OnDragDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (d is DragAdorner adorner)
|
|
||||||
{
|
|
||||||
adorner.UpdateContent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateContent()
|
|
||||||
{
|
|
||||||
if (_contentPresenter != null)
|
|
||||||
{
|
|
||||||
// Можно добавить DataTemplateSelector для разных типов данных
|
|
||||||
_contentPresenter.Content = DragData;
|
|
||||||
|
|
||||||
// Автоматически вычисляем смещение для приятного вида
|
|
||||||
if (DragData is FrameworkElement element)
|
|
||||||
{
|
|
||||||
Offset = new Point(-element.ActualWidth / 2, -element.ActualHeight / 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
using Microsoft.UI.Xaml;
|
|
||||||
using Microsoft.UI.Xaml.Controls;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Lattice.UI.DragDrop.WinUI.Controls;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Оверлейный слой для отображения всех визуальных элементов перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот элемент добавляется поверх всего содержимого окна и содержит:
|
|
||||||
/// - Drag-визуализации (элементы, следующие за курсором)
|
|
||||||
/// - Drop-превью (подсветка областей сброса)
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Элемент имеет <see cref="Canvas.IsHitTestVisible"/> = false, чтобы не перехватывать
|
|
||||||
/// пользовательский ввод во время операций перетаскивания.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public class DragDropOverlay : Canvas
|
|
||||||
{
|
|
||||||
private readonly List<UIElement> _dragVisuals = new();
|
|
||||||
private readonly List<DropPreviewAdorner> _dropAdorners = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DragDropOverlay"/>.
|
|
||||||
/// </summary>
|
|
||||||
public DragDropOverlay()
|
|
||||||
{
|
|
||||||
IsHitTestVisible = false;
|
|
||||||
Background = null;
|
|
||||||
|
|
||||||
// Устанавливаем высокий Z-Index, чтобы быть поверх всего
|
|
||||||
Canvas.SetZIndex(this, 10000);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Показывает визуальное представление перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragVisual">Визуальное представление.</param>
|
|
||||||
/// <param name="initialX">Начальная позиция X.</param>
|
|
||||||
/// <param name="initialY">Начальная позиция Y.</param>
|
|
||||||
public void ShowDragVisual(UIElement dragVisual, double initialX, double initialY)
|
|
||||||
{
|
|
||||||
if (!Children.Contains(dragVisual))
|
|
||||||
{
|
|
||||||
Children.Add(dragVisual);
|
|
||||||
_dragVisuals.Add(dragVisual);
|
|
||||||
}
|
|
||||||
|
|
||||||
SetLeft(dragVisual, initialX);
|
|
||||||
SetTop(dragVisual, initialY);
|
|
||||||
dragVisual.Visibility = Visibility.Visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Обновляет позицию визуального представления перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragVisual">Визуальное представление.</param>
|
|
||||||
/// <param name="x">Новая позиция X.</param>
|
|
||||||
/// <param name="y">Новая позиция Y.</param>
|
|
||||||
public void UpdateDragVisualPosition(UIElement dragVisual, double x, double y)
|
|
||||||
{
|
|
||||||
if (Children.Contains(dragVisual))
|
|
||||||
{
|
|
||||||
SetLeft(dragVisual, x);
|
|
||||||
SetTop(dragVisual, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Скрывает визуальное представление перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragVisual">Визуальное представление.</param>
|
|
||||||
public void HideDragVisual(UIElement dragVisual)
|
|
||||||
{
|
|
||||||
dragVisual.Visibility = Visibility.Collapsed;
|
|
||||||
if (Children.Contains(dragVisual))
|
|
||||||
{
|
|
||||||
Children.Remove(dragVisual);
|
|
||||||
_dragVisuals.Remove(dragVisual);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Показывает предварительный просмотр области сброса.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="bounds">Границы области.</param>
|
|
||||||
/// <returns>Созданный элемент предварительного просмотра.</returns>
|
|
||||||
public DropPreviewAdorner ShowDropPreview(Core.Geometry.Rect bounds)
|
|
||||||
{
|
|
||||||
var adorner = new DropPreviewAdorner
|
|
||||||
{
|
|
||||||
PreviewColor = Windows.UI.Color.FromArgb(100, 0, 120, 215),
|
|
||||||
PreviewThickness = 2.0
|
|
||||||
};
|
|
||||||
|
|
||||||
Children.Add(adorner);
|
|
||||||
_dropAdorners.Add(adorner);
|
|
||||||
|
|
||||||
adorner.Show(bounds);
|
|
||||||
return adorner;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Обновляет предварительный просмотр области сброса.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="adorner">Элемент предварительного просмотра.</param>
|
|
||||||
/// <param name="bounds">Новые границы.</param>
|
|
||||||
public void UpdateDropPreview(DropPreviewAdorner adorner, Core.Geometry.Rect bounds)
|
|
||||||
{
|
|
||||||
adorner.UpdatePosition(bounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Скрывает все предварительные просмотры областей сброса.
|
|
||||||
/// </summary>
|
|
||||||
public void HideAllDropPreviews()
|
|
||||||
{
|
|
||||||
foreach (var adorner in _dropAdorners.ToList())
|
|
||||||
{
|
|
||||||
adorner.Hide();
|
|
||||||
|
|
||||||
var timer = new DispatcherTimer
|
|
||||||
{
|
|
||||||
Interval = TimeSpan.FromMilliseconds(200)
|
|
||||||
};
|
|
||||||
|
|
||||||
timer.Tick += (s, e) =>
|
|
||||||
{
|
|
||||||
timer.Stop();
|
|
||||||
if (Children.Contains(adorner))
|
|
||||||
{
|
|
||||||
Children.Remove(adorner);
|
|
||||||
}
|
|
||||||
_dropAdorners.Remove(adorner);
|
|
||||||
};
|
|
||||||
|
|
||||||
timer.Start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Скрывает все визуальные элементы.
|
|
||||||
/// </summary>
|
|
||||||
public void ClearAllVisuals()
|
|
||||||
{
|
|
||||||
foreach (var visual in _dragVisuals.ToList())
|
|
||||||
{
|
|
||||||
HideDragVisual(visual);
|
|
||||||
}
|
|
||||||
|
|
||||||
HideAllDropPreviews();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает текущий элемент перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Элемент перетаскивания или null.</returns>
|
|
||||||
public UIElement? GetCurrentDragVisual()
|
|
||||||
{
|
|
||||||
return _dragVisuals.FirstOrDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
using Microsoft.UI;
|
|
||||||
using Microsoft.UI.Xaml;
|
|
||||||
using Microsoft.UI.Xaml.Controls;
|
|
||||||
using Microsoft.UI.Xaml.Media;
|
|
||||||
using System;
|
|
||||||
using Windows.UI;
|
|
||||||
|
|
||||||
namespace Lattice.UI.DragDrop.WinUI.Controls;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Визуальный элемент для подсветки области сброса.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот элемент отображается вокруг целевого элемента при наведении перетаскиваемого
|
|
||||||
/// объекта для визуального указания возможности сброса.
|
|
||||||
/// </remarks>
|
|
||||||
[TemplateVisualState(Name = "Normal", GroupName = "CommonStates")]
|
|
||||||
[TemplateVisualState(Name = "Highlighted", GroupName = "CommonStates")]
|
|
||||||
public class DropPreviewAdorner : Control
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Идентификатор свойства для цвета предварительного просмотра.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DependencyProperty PreviewColorProperty =
|
|
||||||
DependencyProperty.Register(
|
|
||||||
"PreviewColor",
|
|
||||||
typeof(Color),
|
|
||||||
typeof(DropPreviewAdorner),
|
|
||||||
new PropertyMetadata(Colors.DodgerBlue));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Идентификатор свойства для толщины границы.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DependencyProperty PreviewThicknessProperty =
|
|
||||||
DependencyProperty.Register(
|
|
||||||
"PreviewThickness",
|
|
||||||
typeof(double),
|
|
||||||
typeof(DropPreviewAdorner),
|
|
||||||
new PropertyMetadata(2.0));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Идентификатор свойства для кисти границы.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DependencyProperty PreviewBrushProperty =
|
|
||||||
DependencyProperty.Register(
|
|
||||||
"PreviewBrush",
|
|
||||||
typeof(Brush),
|
|
||||||
typeof(DropPreviewAdorner),
|
|
||||||
new PropertyMetadata(null));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DropPreviewAdorner"/>.
|
|
||||||
/// </summary>
|
|
||||||
public DropPreviewAdorner()
|
|
||||||
{
|
|
||||||
DefaultStyleKey = typeof(DropPreviewAdorner);
|
|
||||||
IsHitTestVisible = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает цвет подсветки области сброса.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Цвет границы и фона подсветки. Значение по умолчанию берется из ресурсов темы.
|
|
||||||
/// </value>
|
|
||||||
public Color PreviewColor
|
|
||||||
{
|
|
||||||
get => (Color)GetValue(PreviewColorProperty);
|
|
||||||
set => SetValue(PreviewColorProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает толщину границы.
|
|
||||||
/// </summary>
|
|
||||||
public double PreviewThickness
|
|
||||||
{
|
|
||||||
get => (double)GetValue(PreviewThicknessProperty);
|
|
||||||
set => SetValue(PreviewThicknessProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает кисть границы.
|
|
||||||
/// </summary>
|
|
||||||
public Brush PreviewBrush
|
|
||||||
{
|
|
||||||
get => (Brush)GetValue(PreviewBrushProperty);
|
|
||||||
set => SetValue(PreviewBrushProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Показывает элемент подсветки для указанной области.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="bounds">Границы области для подсветки.</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Метод позиционирует элемент по указанным границам и запускает анимацию появления.
|
|
||||||
/// </remarks>
|
|
||||||
public void Show(Core.Geometry.Rect bounds)
|
|
||||||
{
|
|
||||||
Width = bounds.Width;
|
|
||||||
Height = bounds.Height;
|
|
||||||
|
|
||||||
var translateTransform = new TranslateTransform
|
|
||||||
{
|
|
||||||
X = bounds.X,
|
|
||||||
Y = bounds.Y
|
|
||||||
};
|
|
||||||
|
|
||||||
RenderTransform = translateTransform;
|
|
||||||
Visibility = Visibility.Visible;
|
|
||||||
|
|
||||||
VisualStateManager.GoToState(this, "Highlighted", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Скрывает элемент.
|
|
||||||
/// </summary>
|
|
||||||
public void Hide()
|
|
||||||
{
|
|
||||||
VisualStateManager.GoToState(this, "Normal", true);
|
|
||||||
|
|
||||||
// Отложенное скрытие для плавной анимации
|
|
||||||
var timer = new DispatcherTimer
|
|
||||||
{
|
|
||||||
Interval = TimeSpan.FromMilliseconds(150)
|
|
||||||
};
|
|
||||||
|
|
||||||
timer.Tick += (s, e) =>
|
|
||||||
{
|
|
||||||
timer.Stop();
|
|
||||||
Visibility = Visibility.Collapsed;
|
|
||||||
};
|
|
||||||
|
|
||||||
timer.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Обновляет позицию и размер элемента подсветки.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="bounds">Новые границы области для подсветки.</param>
|
|
||||||
public void UpdatePosition(Core.Geometry.Rect bounds)
|
|
||||||
{
|
|
||||||
if (RenderTransform is TranslateTransform transform)
|
|
||||||
{
|
|
||||||
transform.X = bounds.X;
|
|
||||||
transform.Y = bounds.Y;
|
|
||||||
}
|
|
||||||
|
|
||||||
Width = bounds.Width;
|
|
||||||
Height = bounds.Height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
using Microsoft.UI.Xaml;
|
|
||||||
|
|
||||||
namespace Lattice.UI.DragDrop.WinUI;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Предоставляет attached properties для настройки drag-and-drop поведения элементов WinUI.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот класс содержит attached properties, которые позволяют включать и настраивать
|
|
||||||
/// возможности перетаскивания и сброса для любых FrameworkElement в приложении WinUI.
|
|
||||||
/// </remarks>
|
|
||||||
public static class DragDropProperties
|
|
||||||
{
|
|
||||||
#region Drag Source Properties
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Прикрепленное свойство для включения перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DependencyProperty IsDragSourceProperty =
|
|
||||||
DependencyProperty.RegisterAttached(
|
|
||||||
"IsDragSource",
|
|
||||||
typeof(bool),
|
|
||||||
typeof(DragDropProperties),
|
|
||||||
new PropertyMetadata(false, OnIsDragSourceChanged));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Прикрепленное свойство для данных перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DependencyProperty DragDataProperty =
|
|
||||||
DependencyProperty.RegisterAttached(
|
|
||||||
"DragData",
|
|
||||||
typeof(object),
|
|
||||||
typeof(DragDropProperties),
|
|
||||||
new PropertyMetadata(null));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает значение IsDragSource.
|
|
||||||
/// </summary>
|
|
||||||
public static bool GetIsDragSource(UIElement element) =>
|
|
||||||
(bool)element.GetValue(IsDragSourceProperty);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Устанавливает значение IsDragSource.
|
|
||||||
/// </summary>
|
|
||||||
public static void SetIsDragSource(UIElement element, bool value) =>
|
|
||||||
element.SetValue(IsDragSourceProperty, value);
|
|
||||||
|
|
||||||
/// <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);
|
|
||||||
|
|
||||||
private static void OnIsDragSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (d is FrameworkElement element)
|
|
||||||
{
|
|
||||||
if ((bool)e.NewValue)
|
|
||||||
{
|
|
||||||
var data = GetDragData(element);
|
|
||||||
Services.WinUIDragDropManager.Instance.MakeDragSource(element, data);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Services.WinUIDragDropManager.Instance.RemoveDragSource(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Drop Target Properties
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Прикрепленное свойство для включения цели сброса.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DependencyProperty IsDropTargetProperty =
|
|
||||||
DependencyProperty.RegisterAttached(
|
|
||||||
"IsDropTarget",
|
|
||||||
typeof(bool),
|
|
||||||
typeof(DragDropProperties),
|
|
||||||
new PropertyMetadata(false, OnIsDropTargetChanged));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает значение IsDropTarget.
|
|
||||||
/// </summary>
|
|
||||||
public static bool GetIsDropTarget(UIElement element) =>
|
|
||||||
(bool)element.GetValue(IsDropTargetProperty);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Устанавливает значение IsDropTarget.
|
|
||||||
/// </summary>
|
|
||||||
public static void SetIsDropTarget(UIElement element, bool value) =>
|
|
||||||
element.SetValue(IsDropTargetProperty, value);
|
|
||||||
|
|
||||||
private static void OnIsDropTargetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (d is FrameworkElement element)
|
|
||||||
{
|
|
||||||
if ((bool)e.NewValue)
|
|
||||||
{
|
|
||||||
Services.WinUIDragDropManager.Instance.MakeDropTarget(element);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Services.WinUIDragDropManager.Instance.RemoveDropTarget(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Helper Methods
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Включает перетаскивание для элемента с указанными данными.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="element">Элемент, для которого включается перетаскивание.</param>
|
|
||||||
/// <param name="dragData">Данные для перетаскивания. Если не указано, используются DataContext или Tag элемента.</param>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// myElement.EnableDrag(item);
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
public static void EnableDrag(this FrameworkElement element, object? dragData = null)
|
|
||||||
{
|
|
||||||
if (dragData != null)
|
|
||||||
{
|
|
||||||
SetDragData(element, dragData);
|
|
||||||
}
|
|
||||||
SetIsDragSource(element, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Отключает перетаскивание для элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="element">Элемент, для которого отключается перетаскивание.</param>
|
|
||||||
public static void DisableDrag(this FrameworkElement element)
|
|
||||||
{
|
|
||||||
SetIsDragSource(element, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Включает возможность сброса для элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="element">Элемент, для которого включается возможность сброса.</param>
|
|
||||||
public static void EnableDrop(this FrameworkElement element)
|
|
||||||
{
|
|
||||||
SetIsDropTarget(element, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Отключает возможность сброса для элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="element">Элемент, для которого отключается возможность сброса.</param>
|
|
||||||
public static void DisableDrop(this FrameworkElement element)
|
|
||||||
{
|
|
||||||
SetIsDropTarget(element, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
@@ -1,950 +0,0 @@
|
|||||||
using Lattice.Core.DragDrop.Services;
|
|
||||||
using Lattice.UI.DragDrop.WinUI.Behaviors;
|
|
||||||
using Lattice.UI.DragDrop.WinUI.Controls;
|
|
||||||
using Lattice.UI.DragDrop.WinUI.Services;
|
|
||||||
using Microsoft.UI.Xaml;
|
|
||||||
using Microsoft.UI.Xaml.Controls;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Lattice.UI.DragDrop.WinUI.Factories;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Фабрика для создания и настройки компонентов системы перетаскивания WinUI.
|
|
||||||
/// Предоставляет удобные методы для быстрой интеграции drag-and-drop функциональности
|
|
||||||
/// в приложениях WinUI с поддержкой различных сценариев использования.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// <see cref="WinUIDragDropFactory"/> служит высокоуровневым API для работы с системой
|
|
||||||
/// перетаскивания, инкапсулируя сложность инициализации и настройки компонентов.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Основные возможности фабрики:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Создание и инициализация менеджера перетаскивания</item>
|
|
||||||
/// <item>Генерация поведений для источников и целей перетаскивания</item>
|
|
||||||
/// <item>Создание визуальных элементов для обратной связи</item>
|
|
||||||
/// <item>Предварительные конфигурации для типовых сценариев</item>
|
|
||||||
/// <item>Вспомогательные методы для работы с XAML</item>
|
|
||||||
/// </list>
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Фабрика поддерживает два подхода к использованию:
|
|
||||||
/// <list type="number">
|
|
||||||
/// <item><strong>Императивный подход</strong> - создание компонентов в коде C#</item>
|
|
||||||
/// <item><strong>Декларативный подход</strong> - использование attached properties в XAML</item>
|
|
||||||
/// </list>
|
|
||||||
/// </para>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// // Императивный подход
|
|
||||||
/// var manager = WinUIDragDropFactory.CreateManager(window);
|
|
||||||
/// manager.MakeDragSource(element, data);
|
|
||||||
/// manager.MakeDropTarget(dropArea);
|
|
||||||
///
|
|
||||||
/// // Декларативный подход (в XAML)
|
|
||||||
/// <Border local:DragDropProperties.IsDragSource="True"
|
|
||||||
/// local:DragDropProperties.DragData="{Binding Item}" />
|
|
||||||
/// <Border local:DragDropProperties.IsDropTarget="True" />
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
/// </remarks>
|
|
||||||
public static class WinUIDragDropFactory
|
|
||||||
{
|
|
||||||
#region Основные компоненты
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает и инициализирует менеджер перетаскивания для указанного окна WinUI.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="window">
|
|
||||||
/// Окно WinUI, для которого создается менеджер перетаскивания.
|
|
||||||
/// Не может быть null.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Инициализированный экземпляр <see cref="WinUIDragDropManager"/>.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, когда <paramref name="window"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод является основным способом получения менеджера перетаскивания.
|
|
||||||
/// Он создает (или возвращает существующий) экземпляр менеджера и инициализирует
|
|
||||||
/// его для работы с указанным окном.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Метод следует вызывать один раз при запуске приложения, обычно в конструкторе
|
|
||||||
/// главного окна или в методе <see cref="Application.OnLaunched"/>.
|
|
||||||
/// </para>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// public partial class MainWindow : Window
|
|
||||||
/// {
|
|
||||||
/// public MainWindow()
|
|
||||||
/// {
|
|
||||||
/// InitializeComponent();
|
|
||||||
/// var manager = WinUIDragDropFactory.CreateManager(this);
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
/// </remarks>
|
|
||||||
public static WinUIDragDropManager CreateManager(Window window)
|
|
||||||
{
|
|
||||||
if (window == null)
|
|
||||||
throw new ArgumentNullException(nameof(window));
|
|
||||||
|
|
||||||
var manager = WinUIDragDropManager.Instance;
|
|
||||||
if (!manager.IsInitialized)
|
|
||||||
{
|
|
||||||
manager.Initialize(window);
|
|
||||||
}
|
|
||||||
return manager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает и инициализирует менеджер перетаскивания с пользовательскими настройками.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="window">
|
|
||||||
/// Окно WinUI, для которого создается менеджер перетаскивания.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="configure">
|
|
||||||
/// Делегат для настройки параметров менеджера перед инициализацией.
|
|
||||||
/// Передает экземпляр <see cref="WinUIDragDropManager"/> для конфигурации.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Инициализированный экземпляр <see cref="WinUIDragDropManager"/>.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, когда <paramref name="window"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод позволяет настроить параметры менеджера перед его инициализацией,
|
|
||||||
/// что полезно для тонкой настройки поведения системы перетаскивания.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Доступные для настройки параметры включают:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item><see cref="WinUIDragDropManager.DragVisualOffset"/> - смещение визуального элемента</item>
|
|
||||||
/// </list>
|
|
||||||
/// </para>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// var manager = WinUIDragDropFactory.CreateManager(window, m =>
|
|
||||||
/// {
|
|
||||||
/// m.DragVisualOffset = new Point(-15, -15); // Ближе к курсору
|
|
||||||
/// });
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
/// </remarks>
|
|
||||||
public static WinUIDragDropManager CreateManager(Window window, Action<WinUIDragDropManager> configure)
|
|
||||||
{
|
|
||||||
if (window == null)
|
|
||||||
throw new ArgumentNullException(nameof(window));
|
|
||||||
|
|
||||||
var manager = WinUIDragDropManager.Instance;
|
|
||||||
|
|
||||||
// Применяем настройки перед инициализацией
|
|
||||||
configure?.Invoke(manager);
|
|
||||||
|
|
||||||
if (!manager.IsInitialized)
|
|
||||||
{
|
|
||||||
manager.Initialize(window);
|
|
||||||
}
|
|
||||||
|
|
||||||
return manager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает хост для управления визуальными элементами перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="window">
|
|
||||||
/// Окно, к которому будет привязан хост.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Экземпляр <see cref="WinUIDragDropHost"/>, готовый к использованию.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="window"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Хост управляет отображением визуальных элементов во время операций перетаскивания,
|
|
||||||
/// включая drag-визуализации (элементы, следующие за курсором) и drop-превью
|
|
||||||
/// (подсветка областей сброса).
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// В большинстве случаев хост создается автоматически менеджером перетаскивания.
|
|
||||||
/// Этот метод полезен для продвинутых сценариев, когда требуется прямой контроль
|
|
||||||
/// над визуальной обратной связью.
|
|
||||||
/// </para>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// var host = WinUIDragDropFactory.CreateHost(window);
|
|
||||||
/// // Настройка кастомной визуализации
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
/// </remarks>
|
|
||||||
public static WinUIDragDropHost CreateHost(Window window)
|
|
||||||
{
|
|
||||||
if (window == null)
|
|
||||||
throw new ArgumentNullException(nameof(window));
|
|
||||||
|
|
||||||
var host = new WinUIDragDropHost();
|
|
||||||
host.Initialize(window);
|
|
||||||
return host;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Поведения (Behaviors)
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает поведение источника перетаскивания для элемента WinUI.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragDropService">
|
|
||||||
/// Сервис перетаскивания, который будет управлять операциями.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="host">
|
|
||||||
/// Хост для отображения визуальных элементов.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Экземпляр <see cref="WinUIDragSourceBehavior"/>, готовый к прикреплению к элементу.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если любой из параметров равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Созданное поведение необходимо прикрепить к элементу с помощью метода
|
|
||||||
/// <see cref="WinUIDragSourceBehavior.Attach"/>.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод полезен для продвинутых сценариев, когда требуется создавать поведения
|
|
||||||
/// вручную, например, при динамическом создании элементов интерфейса.
|
|
||||||
/// </para>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// // Создание поведения вручную
|
|
||||||
/// var behavior = WinUIDragDropFactory.CreateDragSourceBehavior(service, host);
|
|
||||||
/// behavior.Attach(element, data);
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
/// </remarks>
|
|
||||||
public static WinUIDragSourceBehavior CreateDragSourceBehavior(
|
|
||||||
IDragDropService dragDropService,
|
|
||||||
WinUIDragDropHost host)
|
|
||||||
{
|
|
||||||
if (dragDropService == null)
|
|
||||||
throw new ArgumentNullException(nameof(dragDropService));
|
|
||||||
if (host == null)
|
|
||||||
throw new ArgumentNullException(nameof(host));
|
|
||||||
|
|
||||||
return new WinUIDragSourceBehavior(dragDropService, host);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает поведение цели сброса для элемента WinUI.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragDropService">
|
|
||||||
/// Сервис перетаскивания, который будет управлять операциями.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="host">
|
|
||||||
/// Хост для отображения визуальных элементов.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Экземпляр <see cref="WinUIDropTargetBehavior"/>, готовый к прикреплению к элементу.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если любой из параметров равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Созданное поведение необходимо прикрепить к элементу с помощью метода
|
|
||||||
/// <see cref="WinUIDropTargetBehavior.Attach"/>.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Поведение можно дополнительно настроить с помощью методов
|
|
||||||
/// <see cref="WinUIDropTargetBehavior.AcceptTypes"/> и
|
|
||||||
/// <see cref="WinUIDropTargetBehavior.AcceptFormats"/> для фильтрации
|
|
||||||
/// принимаемых данных.
|
|
||||||
/// </para>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// // Создание поведения вручную
|
|
||||||
/// var behavior = WinUIDragDropFactory.CreateDropTargetBehavior(service, host);
|
|
||||||
/// behavior.AcceptTypes(typeof(string), typeof(MyModel));
|
|
||||||
/// behavior.Attach(dropArea);
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
/// </remarks>
|
|
||||||
public static WinUIDropTargetBehavior CreateDropTargetBehavior(
|
|
||||||
IDragDropService dragDropService,
|
|
||||||
WinUIDragDropHost host)
|
|
||||||
{
|
|
||||||
if (dragDropService == null)
|
|
||||||
throw new ArgumentNullException(nameof(dragDropService));
|
|
||||||
if (host == null)
|
|
||||||
throw new ArgumentNullException(nameof(host));
|
|
||||||
|
|
||||||
return new WinUIDropTargetBehavior(dragDropService, host);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает поведение источника перетаскивания, используя сервисы из менеджера по умолчанию.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>
|
|
||||||
/// Экземпляр <see cref="WinUIDragSourceBehavior"/>, готовый к прикреплению к элементу.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="InvalidOperationException">
|
|
||||||
/// Выбрасывается, если менеджер не инициализирован.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод использует <see cref="WinUIDragDropManager.Instance"/> для получения
|
|
||||||
/// сервиса перетаскивания и хоста, что упрощает создание поведений в контексте
|
|
||||||
/// уже инициализированной системы.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Перед использованием убедитесь, что менеджер инициализирован через метод
|
|
||||||
/// <see cref="CreateManager"/>.
|
|
||||||
/// </para>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// // Инициализация менеджера
|
|
||||||
/// WinUIDragDropFactory.CreateManager(window);
|
|
||||||
///
|
|
||||||
/// // Создание поведения с использованием менеджера
|
|
||||||
/// var behavior = WinUIDragDropFactory.CreateDragSourceBehavior();
|
|
||||||
/// behavior.Attach(element, data);
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
/// </remarks>
|
|
||||||
public static WinUIDragSourceBehavior CreateDragSourceBehavior()
|
|
||||||
{
|
|
||||||
var manager = WinUIDragDropManager.Instance;
|
|
||||||
if (!manager.IsInitialized)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"Менеджер не инициализирован. Вызовите CreateManager перед созданием поведений.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new WinUIDragSourceBehavior(manager.DragDropService, manager.Host);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает поведение цели сброса, используя сервисы из менеджера по умолчанию.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>
|
|
||||||
/// Экземпляр <see cref="WinUIDropTargetBehavior"/>, готовый к прикреплению к элементу.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="InvalidOperationException">
|
|
||||||
/// Выбрасывается, если менеджер не инициализирован.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод использует <see cref="WinUIDragDropManager.Instance"/> для получения
|
|
||||||
/// сервиса перетаскивания и хоста, что упрощает создание поведений в контексте
|
|
||||||
/// уже инициализированной системы.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Поведение можно дополнительно настроить с помощью методов
|
|
||||||
/// <see cref="WinUIDropTargetBehavior.AcceptTypes"/> и
|
|
||||||
/// <see cref="WinUIDropTargetBehavior.AcceptFormats"/> для фильтрации
|
|
||||||
/// принимаемых данных.
|
|
||||||
/// </para>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// // Инициализация менеджера
|
|
||||||
/// WinUIDragDropFactory.CreateManager(window);
|
|
||||||
///
|
|
||||||
/// // Создание поведения с использованием менеджера
|
|
||||||
/// var behavior = WinUIDragDropFactory.CreateDropTargetBehavior();
|
|
||||||
/// behavior.AcceptTypes(typeof(string));
|
|
||||||
/// behavior.Attach(dropArea);
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
/// </remarks>
|
|
||||||
public static WinUIDropTargetBehavior CreateDropTargetBehavior()
|
|
||||||
{
|
|
||||||
var manager = WinUIDragDropManager.Instance;
|
|
||||||
if (!manager.IsInitialized)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"Менеджер не инициализирован. Вызовите CreateManager перед созданием поведений.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new WinUIDropTargetBehavior(manager.DragDropService, manager.Host);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Визуальные элементы
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает визуальный элемент для отображения во время перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragData">
|
|
||||||
/// Данные, которые будут отображены в визуальном элементе.
|
|
||||||
/// Могут быть любого типа, поддерживаемого системой перетаскивания.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="opacity">
|
|
||||||
/// Прозрачность элемента в диапазоне от 0.0 (полностью прозрачный) до 1.0 (полностью непрозрачный).
|
|
||||||
/// Значение по умолчанию: 0.8.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Экземпляр <see cref="DragAdorner"/>, настроенный для отображения указанных данных.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Созданный элемент можно использовать с методом <see cref="WinUIDragDropHost.ShowDragVisual"/>
|
|
||||||
/// для отображения во время операции перетаскивания.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Элемент автоматически адаптирует отображение в зависимости от типа данных:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Для строк отображается текстовое представление</item>
|
|
||||||
/// <item>Для изображений отображается миниатюра</item>
|
|
||||||
/// <item>Для пользовательских объектов используется DataTemplate или ToString()</item>
|
|
||||||
/// </list>
|
|
||||||
/// </para>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// // Создание визуального элемента
|
|
||||||
/// var adorner = WinUIDragDropFactory.CreateDragAdorner("Текст для перетаскивания");
|
|
||||||
///
|
|
||||||
/// // Отображение во время перетаскивания
|
|
||||||
/// host.ShowDragVisual(adorner, position);
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
/// </remarks>
|
|
||||||
public static DragAdorner CreateDragAdorner(object dragData, double opacity = 0.8)
|
|
||||||
{
|
|
||||||
return new DragAdorner
|
|
||||||
{
|
|
||||||
DragData = dragData ?? throw new ArgumentNullException(nameof(dragData)),
|
|
||||||
Opacity = Math.Clamp(opacity, 0.0, 1.0)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает элемент предварительного просмотра для области сброса.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="color">
|
|
||||||
/// Цвет подсветки области. Если не указан, используется системный цвет акцента.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="thickness">
|
|
||||||
/// Толщина границы подсветки в пикселях.
|
|
||||||
/// Значение по умолчанию: 2.0.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Экземпляр <see cref="DropPreviewAdorner"/>, готовый к отображению.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот элемент используется для визуального указания области, на которую можно
|
|
||||||
/// сбросить данные. Он отображается как подсветка границ целевого элемента с
|
|
||||||
/// поддержкой анимации появления и скрытия.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Элемент автоматически адаптирует свой внешний вид в зависимости от состояния:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item><strong>Normal</strong> - стандартное состояние</item>
|
|
||||||
/// <item><strong>Highlighted</strong> - подсветка при наведении</item>
|
|
||||||
/// </list>
|
|
||||||
/// </para>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// // Создание элемента подсветки
|
|
||||||
/// var preview = WinUIDragDropFactory.CreateDropPreviewAdorner(
|
|
||||||
/// Colors.Blue, // Цвет
|
|
||||||
/// 3.0 // Толщина границы
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// // Отображение подсветки
|
|
||||||
/// preview.Show(bounds);
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
/// </remarks>
|
|
||||||
public static DropPreviewAdorner CreateDropPreviewAdorner(
|
|
||||||
Windows.UI.Color? color = null,
|
|
||||||
double thickness = 2.0)
|
|
||||||
{
|
|
||||||
var adorner = new DropPreviewAdorner
|
|
||||||
{
|
|
||||||
PreviewThickness = Math.Max(thickness, 0.0)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (color.HasValue)
|
|
||||||
{
|
|
||||||
adorner.PreviewColor = color.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return adorner;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Готовые конфигурации для типовых сценариев
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает полную систему перетаскивания для WinUI приложения.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="window">
|
|
||||||
/// Главное окно приложения.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Кортеж, содержащий менеджер перетаскивания и сервис перетаскивания.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод создает все необходимые компоненты для работы перетаскивания в приложении
|
|
||||||
/// и возвращает их для дальнейшего использования.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Возвращаемый сервис можно использовать для создания дополнительных источников и целей
|
|
||||||
/// или для низкоуровневого управления операциями перетаскивания.
|
|
||||||
/// </para>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// // Создание полной системы
|
|
||||||
/// var (manager, service) = WinUIDragDropFactory.CreateCompleteSystem(window);
|
|
||||||
///
|
|
||||||
/// // Использование менеджера для настройки элементов
|
|
||||||
/// manager.MakeDragSource(element, data);
|
|
||||||
///
|
|
||||||
/// // Использование сервиса для расширенного управления
|
|
||||||
/// var stats = service.GetStats();
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
/// </remarks>
|
|
||||||
public static (WinUIDragDropManager Manager, IDragDropService Service) CreateCompleteSystem(Window window)
|
|
||||||
{
|
|
||||||
var manager = CreateManager(window);
|
|
||||||
return (manager, manager.DragDropService);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает систему перетаскивания, оптимизированную для переупорядочивания элементов в списках.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="window">
|
|
||||||
/// Главное окно приложения.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Менеджер перетаскивания, настроенный для переупорядочивания элементов в списках.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Эта конфигурация устанавливает оптимальные параметры для перетаскивания элементов
|
|
||||||
/// внутри списков и коллекций, таких как:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Изменение порядка элементов в ListView</item>
|
|
||||||
/// <item>Перемещение элементов между ItemsControl</item>
|
|
||||||
/// <item>Сортировка элементов в коллекциях</item>
|
|
||||||
/// </list>
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Особенности конфигурации:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Уменьшенный порог начала перетаскивания для более быстрого отклика</item>
|
|
||||||
/// <item>Смещение визуального элемента для лучшего визуального выравнивания</item>
|
|
||||||
/// <item>Оптимизированная визуальная обратная связь</item>
|
|
||||||
/// </list>
|
|
||||||
/// </para>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// // Создание системы для переупорядочивания списков
|
|
||||||
/// var manager = WinUIDragDropFactory.CreateListReorderSystem(window);
|
|
||||||
///
|
|
||||||
/// // Настройка ListView для переупорядочивания
|
|
||||||
/// foreach (var item in myListView.Items)
|
|
||||||
/// {
|
|
||||||
/// if (item is FrameworkElement element)
|
|
||||||
/// {
|
|
||||||
/// manager.MakeDragSource(element, element.DataContext);
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// manager.MakeDropTarget(myListView);
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
/// </remarks>
|
|
||||||
public static WinUIDragDropManager CreateListReorderSystem(Window window)
|
|
||||||
{
|
|
||||||
var manager = CreateManager(window, m =>
|
|
||||||
{
|
|
||||||
// Уменьшенное смещение для лучшего визуального выравнивания в списках
|
|
||||||
m.DragVisualOffset = new Core.Geometry.Point(-15, -15);
|
|
||||||
});
|
|
||||||
|
|
||||||
return manager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает систему перетаскивания для работы с файлами и документами.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="window">
|
|
||||||
/// Главное окно приложения.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Менеджер перетаскивания, настроенный для работы с файлами.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Эта конфигурация оптимизирована для сценариев работы с файлами:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Перетаскивание файлов из проводника в приложение</item>
|
|
||||||
/// <item>Перемещение файлов между элементами интерфейса</item>
|
|
||||||
/// <item>Работа с большими объемами данных</item>
|
|
||||||
/// </list>
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Особенности конфигурации:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Увеличенный порог перетаскивания для предотвращения случайных операций</item>
|
|
||||||
/// <item>Специальные визуальные эффекты, характерные для файловых операций</item>
|
|
||||||
/// <item>Оптимизация для работы с внешними источниками данных</item>
|
|
||||||
/// </list>
|
|
||||||
/// </para>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// // Создание системы для работы с файлами
|
|
||||||
/// var manager = WinUIDragDropFactory.CreateFileDragDropSystem(window);
|
|
||||||
///
|
|
||||||
/// // Настройка области для приема файлов
|
|
||||||
/// manager.MakeDropTarget(fileDropArea);
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
/// </remarks>
|
|
||||||
public static WinUIDragDropManager CreateFileDragDropSystem(Window window)
|
|
||||||
{
|
|
||||||
var manager = CreateManager(window, m =>
|
|
||||||
{
|
|
||||||
// Увеличенное смещение для файлов (имитация "переноса" файла)
|
|
||||||
m.DragVisualOffset = new Core.Geometry.Point(-25, -25);
|
|
||||||
});
|
|
||||||
|
|
||||||
return manager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает систему перетаскивания для графических редакторов и инструментов дизайна.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="window">
|
|
||||||
/// Главное окно приложения.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Менеджер перетаскивания, настроенный для точного позиционирования.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Эта конфигурация оптимизирована для приложений, требующих высокой точности
|
|
||||||
/// позиционирования, таких как:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Графические редакторы (Photoshop, Figma)</item>
|
|
||||||
/// <item>Инструменты проектирования интерфейсов</item>
|
|
||||||
/// <item>CAD-системы и приложения для 3D-моделирования</item>
|
|
||||||
/// </list>
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Особенности конфигурации:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Минимальный порог начала перетаскивания для максимальной точности</item>
|
|
||||||
/// <item>Минималистичная визуализация для уменьшения визуального шума</item>
|
|
||||||
/// <item>Оптимизация для работы с высокоточными устройствами ввода (графические планшеты)</item>
|
|
||||||
/// </list>
|
|
||||||
/// </para>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// // Создание системы для графического редактора
|
|
||||||
/// var manager = WinUIDragDropFactory.CreateDesignToolSystem(window);
|
|
||||||
///
|
|
||||||
/// // Настройка инструментов для перетаскивания
|
|
||||||
/// manager.MakeDragSource(toolIcon, toolData);
|
|
||||||
/// manager.MakeDropTarget(canvas);
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
/// </remarks>
|
|
||||||
public static WinUIDragDropManager CreateDesignToolSystem(Window window)
|
|
||||||
{
|
|
||||||
var manager = CreateManager(window, m =>
|
|
||||||
{
|
|
||||||
// Минимальное смещение для точного позиционирования
|
|
||||||
m.DragVisualOffset = new Core.Geometry.Point(-10, -10);
|
|
||||||
});
|
|
||||||
|
|
||||||
return manager;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Вспомогательные методы
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Настраивает элемент как источник перетаскивания с использованием указанного менеджера.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="manager">
|
|
||||||
/// Менеджер перетаскивания, который будет управлять операцией.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="element">
|
|
||||||
/// Элемент WinUI, который должен стать источником перетаскивания.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="dragData">
|
|
||||||
/// Данные для перетаскивания. Если не указаны, будут использованы DataContext или Tag элемента.
|
|
||||||
/// </param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="manager"/> или <paramref name="element"/> равны null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод является удобной оберткой вокруг <see cref="WinUIDragDropManager.MakeDragSource"/>,
|
|
||||||
/// предоставляющей более лаконичный синтаксис.
|
|
||||||
/// </para>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// // Использование вспомогательного метода
|
|
||||||
/// WinUIDragDropFactory.SetupAsDragSource(manager, myElement, myData);
|
|
||||||
///
|
|
||||||
/// // Эквивалентно:
|
|
||||||
/// manager.MakeDragSource(myElement, myData);
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
/// </remarks>
|
|
||||||
public static void SetupAsDragSource(WinUIDragDropManager manager, FrameworkElement element, object dragData = null)
|
|
||||||
{
|
|
||||||
if (manager == null)
|
|
||||||
throw new ArgumentNullException(nameof(manager));
|
|
||||||
if (element == null)
|
|
||||||
throw new ArgumentNullException(nameof(element));
|
|
||||||
|
|
||||||
manager.MakeDragSource(element, dragData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Настраивает элемент как цель сброса с использованием указанного менеджера.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="manager">
|
|
||||||
/// Менеджер перетаскивания, который будет управлять операцией.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="element">
|
|
||||||
/// Элемент WinUI, который должен стать целью сброса.
|
|
||||||
/// </param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="manager"/> или <paramref name="element"/> равны null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод является удобной оберткой вокруг <see cref="WinUIDragDropManager.MakeDropTarget"/>,
|
|
||||||
/// предоставляющей более лаконичный синтаксис.
|
|
||||||
/// </para>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// // Использование вспомогательного метода
|
|
||||||
/// WinUIDragDropFactory.SetupAsDropTarget(manager, myDropArea);
|
|
||||||
///
|
|
||||||
/// // Эквивалентно:
|
|
||||||
/// manager.MakeDropTarget(myDropArea);
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
/// </remarks>
|
|
||||||
public static void SetupAsDropTarget(WinUIDragDropManager manager, FrameworkElement element)
|
|
||||||
{
|
|
||||||
if (manager == null)
|
|
||||||
throw new ArgumentNullException(nameof(manager));
|
|
||||||
if (element == null)
|
|
||||||
throw new ArgumentNullException(nameof(element));
|
|
||||||
|
|
||||||
manager.MakeDropTarget(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Настраивает контейнер для поддержки переупорядочивания дочерних элементов.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="manager">
|
|
||||||
/// Менеджер перетаскивания.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="container">
|
|
||||||
/// Контейнер (например, StackPanel, Grid или ItemsControl), дочерние элементы которого можно переупорядочивать.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="childSelector">
|
|
||||||
/// Функция для получения данных перетаскивания из дочернего элемента.
|
|
||||||
/// Если не указана, используются DataContext дочерних элементов.
|
|
||||||
/// </param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="manager"/> или <paramref name="container"/> равны null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод автоматически настраивает все дочерние элементы контейнера как источники перетаскивания,
|
|
||||||
/// а сам контейнер — как цель сброса, создавая функциональность переупорядочивания элементов.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Поддерживаемые типы контейнеров:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item><see cref="Panel"/> и его производные (StackPanel, Grid, Canvas)</item>
|
|
||||||
/// <item><see cref="ItemsControl"/> и его производные (ListView, ListBox)</item>
|
|
||||||
/// <item>Любые другие контейнеры с коллекцией дочерних элементов</item>
|
|
||||||
/// </list>
|
|
||||||
/// </para>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// // Настройка StackPanel для переупорядочивания дочерних элементов
|
|
||||||
/// WinUIDragDropFactory.SetupReorderContainer(manager, myStackPanel);
|
|
||||||
///
|
|
||||||
/// // С кастомным селектором данных
|
|
||||||
/// WinUIDragDropFactory.SetupReorderContainer(manager, myListView,
|
|
||||||
/// element => ((FrameworkElement)element).DataContext);
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
/// </remarks>
|
|
||||||
public static void SetupReorderContainer(
|
|
||||||
WinUIDragDropManager manager,
|
|
||||||
FrameworkElement container,
|
|
||||||
Func<FrameworkElement, object> childSelector = null)
|
|
||||||
{
|
|
||||||
if (manager == null)
|
|
||||||
throw new ArgumentNullException(nameof(manager));
|
|
||||||
if (container == null)
|
|
||||||
throw new ArgumentNullException(nameof(container));
|
|
||||||
|
|
||||||
// Настраиваем контейнер как цель сброса
|
|
||||||
manager.MakeDropTarget(container);
|
|
||||||
|
|
||||||
// Настраиваем дочерние элементы как источники перетаскивания
|
|
||||||
if (container is Panel panel)
|
|
||||||
{
|
|
||||||
SetupPanelChildren(manager, panel, childSelector);
|
|
||||||
}
|
|
||||||
else if (container is ItemsControl itemsControl)
|
|
||||||
{
|
|
||||||
SetupItemsControlChildren(manager, itemsControl, childSelector);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Настраивает дочерние элементы Panel как источники перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
private static void SetupPanelChildren(
|
|
||||||
WinUIDragDropManager manager,
|
|
||||||
Panel panel,
|
|
||||||
Func<FrameworkElement, object> childSelector)
|
|
||||||
{
|
|
||||||
foreach (var child in panel.Children)
|
|
||||||
{
|
|
||||||
if (child is FrameworkElement element)
|
|
||||||
{
|
|
||||||
var data = childSelector?.Invoke(element) ?? element.DataContext ?? element.Tag;
|
|
||||||
manager.MakeDragSource(element, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Настраивает элементы ItemsControl как источники перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
private static void SetupItemsControlChildren(
|
|
||||||
WinUIDragDropManager manager,
|
|
||||||
ItemsControl itemsControl,
|
|
||||||
Func<FrameworkElement, object> childSelector)
|
|
||||||
{
|
|
||||||
// Для ItemsControl нам нужно работать с ItemContainerGenerator
|
|
||||||
// В реальной реализации здесь должна быть более сложная логика
|
|
||||||
// для обработки виртуализации и динамических элементов
|
|
||||||
|
|
||||||
foreach (var item in itemsControl.Items)
|
|
||||||
{
|
|
||||||
if (item is FrameworkElement element)
|
|
||||||
{
|
|
||||||
var data = childSelector?.Invoke(element) ?? element.DataContext ?? element.Tag;
|
|
||||||
manager.MakeDragSource(element, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Методы для работы с XAML
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Настраивает attached properties для элемента источника перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="element">
|
|
||||||
/// Элемент, который должен стать источником перетаскивания.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="dragData">
|
|
||||||
/// Данные для перетаскивания.
|
|
||||||
/// </param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="element"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод устанавливает attached properties, которые активируют поведение перетаскивания
|
|
||||||
/// при использовании в XAML. Эквивалентно установке свойств IsDragSource и DragData в XAML.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Метод полезен для динамической настройки элементов в коде C# при сохранении
|
|
||||||
/// декларативного стиля программирования.
|
|
||||||
/// </para>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// // Настройка элемента в коде C#
|
|
||||||
/// WinUIDragDropFactory.SetupDragSourceInXaml(myElement, myData);
|
|
||||||
///
|
|
||||||
/// // Эквивалентно в XAML:
|
|
||||||
/// <Border local:DragDropProperties.IsDragSource="True"
|
|
||||||
/// local:DragDropProperties.DragData="{Binding MyData}" />
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
/// </remarks>
|
|
||||||
public static void SetupDragSourceInXaml(FrameworkElement element, object dragData)
|
|
||||||
{
|
|
||||||
if (element == null)
|
|
||||||
throw new ArgumentNullException(nameof(element));
|
|
||||||
|
|
||||||
// Устанавливаем attached properties
|
|
||||||
element.SetValue(DragDropProperties.IsDragSourceProperty, true);
|
|
||||||
if (dragData != null)
|
|
||||||
{
|
|
||||||
element.SetValue(DragDropProperties.DragDataProperty, dragData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Настраивает attached properties для элемента цели сброса.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="element">
|
|
||||||
/// Элемент, который должен стать целью сброса.
|
|
||||||
/// </param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="element"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Этот метод устанавливает attached properties, которые активируют поведение цели сброса
|
|
||||||
/// при использовании в XAML. Эквивалентно установке свойства IsDropTarget в XAML.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Метод полезен для динамической настройки элементов в коде C# при сохранении
|
|
||||||
/// декларативного стиля программирования.
|
|
||||||
/// </para>
|
|
||||||
/// <example>
|
|
||||||
/// <code>
|
|
||||||
/// // Настройка элемента в коде C#
|
|
||||||
/// WinUIDragDropFactory.SetupDropTargetInXaml(myDropArea);
|
|
||||||
///
|
|
||||||
/// // Эквивалентно в XAML:
|
|
||||||
/// <Border local:DragDropProperties.IsDropTarget="True" />
|
|
||||||
/// </code>
|
|
||||||
/// </example>
|
|
||||||
/// </remarks>
|
|
||||||
public static void SetupDropTargetInXaml(FrameworkElement element)
|
|
||||||
{
|
|
||||||
if (element == null)
|
|
||||||
throw new ArgumentNullException(nameof(element));
|
|
||||||
|
|
||||||
element.SetValue(DragDropProperties.IsDropTargetProperty, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
using Microsoft.UI.Xaml;
|
|
||||||
using Microsoft.UI.Xaml.Media;
|
|
||||||
using Windows.UI;
|
|
||||||
|
|
||||||
namespace Lattice.UI.DragDrop.WinUI.Helpers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вспомогательный класс для работы с ресурсами.
|
|
||||||
/// </summary>
|
|
||||||
public static class ResourceHelper
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует ресурсы перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
public static void InitializeDragDropResources()
|
|
||||||
{
|
|
||||||
// Загружаем Generic.xaml, если он еще не загружен
|
|
||||||
var resourceDictionary = new ResourceDictionary();
|
|
||||||
|
|
||||||
// В реальном приложении нужно загружать из правильного пути
|
|
||||||
try
|
|
||||||
{
|
|
||||||
resourceDictionary.Source = new System.Uri("ms-appx:///Lattice.UI.DragDrop.WinUI/Themes/Generic.xaml");
|
|
||||||
Application.Current.Resources.MergedDictionaries.Add(resourceDictionary);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Если не удалось загрузить из файла, создаем базовые ресурсы
|
|
||||||
CreateFallbackResources();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void CreateFallbackResources()
|
|
||||||
{
|
|
||||||
var resources = Application.Current.Resources;
|
|
||||||
|
|
||||||
// Базовые кисти для визуальной обратной связи
|
|
||||||
if (!resources.ContainsKey("DragOverBackgroundBrush"))
|
|
||||||
{
|
|
||||||
resources["DragOverBackgroundBrush"] = new SolidColorBrush(
|
|
||||||
Color.FromArgb(76, 0, 120, 215)); // 30% opacity
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!resources.ContainsKey("DragOverBorderBrush"))
|
|
||||||
{
|
|
||||||
resources["DragOverBorderBrush"] = new SolidColorBrush(
|
|
||||||
Color.FromArgb(255, 0, 120, 215));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!resources.ContainsKey("DropValidBrush"))
|
|
||||||
{
|
|
||||||
resources["DropValidBrush"] = new SolidColorBrush(
|
|
||||||
Color.FromArgb(255, 0, 204, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!resources.ContainsKey("DropInvalidBrush"))
|
|
||||||
{
|
|
||||||
resources["DropInvalidBrush"] = new SolidColorBrush(
|
|
||||||
Color.FromArgb(255, 255, 0, 0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает стиль из ресурсов.
|
|
||||||
/// </summary>
|
|
||||||
public static Style? GetStyle(string styleKey)
|
|
||||||
{
|
|
||||||
if (Application.Current.Resources.TryGetValue(styleKey, out var style) && style is Style)
|
|
||||||
{
|
|
||||||
return style as Style;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает кисть из ресурсов.
|
|
||||||
/// </summary>
|
|
||||||
public static Brush? GetBrush(string brushKey)
|
|
||||||
{
|
|
||||||
if (Application.Current.Resources.TryGetValue(brushKey, out var brush) && brush is Brush)
|
|
||||||
{
|
|
||||||
return brush as Brush;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Добавляет или обновляет ресурс.
|
|
||||||
/// </summary>
|
|
||||||
public static void SetResource(string key, object value)
|
|
||||||
{
|
|
||||||
Application.Current.Resources[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Проверяет существование ресурса.
|
|
||||||
/// </summary>
|
|
||||||
public static bool HasResource(string key)
|
|
||||||
{
|
|
||||||
return Application.Current.Resources.ContainsKey(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
using Lattice.Core.Geometry;
|
|
||||||
using Microsoft.UI.Xaml;
|
|
||||||
using System;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вспомогательный класс для получения экранных координат в WinUI 3.
|
|
||||||
/// Использует P/Invoke для доступа к нативным API Windows.
|
|
||||||
/// </summary>
|
|
||||||
internal static class WinUIWindowHelper
|
|
||||||
{
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
public struct POINT
|
|
||||||
{
|
|
||||||
public int X;
|
|
||||||
public int Y;
|
|
||||||
|
|
||||||
public POINT(int x, int y)
|
|
||||||
{
|
|
||||||
X = x;
|
|
||||||
Y = y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
public static extern bool ClientToScreen(IntPtr hWnd, ref POINT lpPoint);
|
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
|
||||||
public static extern IntPtr WindowFromPoint(POINT point);
|
|
||||||
|
|
||||||
[DllImport("user32.dll", SetLastError = true)]
|
|
||||||
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Преобразует координаты элемента в экранные координаты.
|
|
||||||
/// </summary>
|
|
||||||
public static Point ConvertToScreenCoordinates(FrameworkElement element, Point localPoint)
|
|
||||||
{
|
|
||||||
if (element == null || !element.IsLoaded)
|
|
||||||
return localPoint;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var window = Window.Current;
|
|
||||||
if (window == null)
|
|
||||||
return localPoint;
|
|
||||||
|
|
||||||
// Получаем хэндл окна
|
|
||||||
var hwnd = GetWindowHandle(window);
|
|
||||||
if (hwnd == IntPtr.Zero)
|
|
||||||
return localPoint;
|
|
||||||
|
|
||||||
// Преобразуем координаты элемента в координаты окна
|
|
||||||
var transform = element.TransformToVisual(window.Content);
|
|
||||||
var windowPoint = transform.TransformPoint(
|
|
||||||
new Windows.Foundation.Point(localPoint.X, localPoint.Y));
|
|
||||||
|
|
||||||
// Преобразуем в POINT для P/Invoke
|
|
||||||
var point = new POINT(
|
|
||||||
(int)Math.Round(windowPoint.X),
|
|
||||||
(int)Math.Round(windowPoint.Y));
|
|
||||||
|
|
||||||
// Преобразуем клиентские координаты в экранные
|
|
||||||
if (ClientToScreen(hwnd, ref point))
|
|
||||||
{
|
|
||||||
return new Point(point.X, point.Y);
|
|
||||||
}
|
|
||||||
|
|
||||||
return localPoint;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
System.Diagnostics.Debug.WriteLine($"Ошибка преобразования координат: {ex.Message}");
|
|
||||||
return localPoint;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает хэндл окна WinUI.
|
|
||||||
/// </summary>
|
|
||||||
private static IntPtr GetWindowHandle(Window window)
|
|
||||||
{
|
|
||||||
// В WinUI 3 можно использовать WinRT API для получения хэндла
|
|
||||||
// или альтернативные методы в зависимости от контекста
|
|
||||||
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(window);
|
|
||||||
return hwnd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||