Compare commits

...

7 Commits

122 changed files with 3490 additions and 12241 deletions

View File

@@ -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; }
} }

View File

@@ -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; }
} }

View File

@@ -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();
} }

View File

@@ -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; }
} }

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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;
}
}

View File

@@ -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;
} }
} }

View File

@@ -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;
}
} }

View File

@@ -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>

View File

@@ -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;
} }
} }

View File

@@ -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
} }

View File

@@ -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;
} }

View File

@@ -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,
} }

View File

@@ -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
} }

View File

@@ -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";
} }

View File

@@ -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
} }

View File

@@ -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,
} }

View File

@@ -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);
} }

View File

@@ -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);
} }

View File

@@ -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;
} }
} }

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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; // По умолчанию
}
}

View File

@@ -1,14 +0,0 @@
namespace Lattice.Core.DragDrop.Enums;
/// <summary>
/// Позиция сброса относительно цели.
/// </summary>
public enum DropPosition
{
Inside,
Top,
Bottom,
Left,
Right,
Center
}

View File

@@ -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 & 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";
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -1,832 +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/ # Аргументы событий
└── Utilities/ # Утилиты и фабрики
├── DragDropUtilities.cs # Синхронные утилиты
└── AsyncDragDropUtilities.cs # Асинхронные утилиты
```
## 🚀 Быстрый старт
### 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. **Инструменты разработчика** (дебаггер, профилировщик)

View File

@@ -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
}

View File

@@ -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;
}
}

View File

@@ -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; }
}

View 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>

View 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);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View 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>

View 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>

View 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 работает! Перетащите элементы →";
}
}

View 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>

View File

@@ -0,0 +1,10 @@
{
"profiles": {
"Lattice.Example.DragDrop (Package)": {
"commandName": "MsixPackage"
},
"Lattice.Example.DragDrop (Unpackaged)": {
"commandName": "Project"
}
}
}

View 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>

View File

@@ -1,17 +1,4 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
// To learn more about WinUI, the WinUI project structure, // To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info. // and more about our project templates, see: http://aka.ms/winui-project-info.

View File

@@ -1,17 +1,4 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
// To learn more about WinUI, the WinUI project structure, // To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info. // and more about our project templates, see: http://aka.ms/winui-project-info.

View File

@@ -1,4 +1,4 @@
namespace Lattice.Themes.Core.Tokens; namespace Lattice.Themes.Core;
/// <summary> /// <summary>
/// Статические ключи для ресурсов Lattice Framework. /// Статические ключи для ресурсов Lattice Framework.

View File

@@ -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; }
} }

View File

@@ -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();
}

View 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();
}
}

View File

@@ -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)

View File

@@ -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;
}
} }

View File

@@ -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);
} }
} }
} }

View File

@@ -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);
}
} }
} }

View File

@@ -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);
} }

View 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;
}
}

View File

@@ -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)
{ {

View 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 = "Переместить";
};
}
}

View File

@@ -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();

View File

@@ -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)
{ {

View File

@@ -1,534 +0,0 @@
using Lattice.Core.DragDrop.Services;
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;
}
}
}

View File

@@ -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>

View File

@@ -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;
} }

View File

@@ -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;
} }

View File

@@ -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;
} }

View File

@@ -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);
} }

View File

@@ -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;
}
}

View File

@@ -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
} }

View File

@@ -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;
} }

View File

@@ -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));
} }
} }

View 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;
}
}

View File

@@ -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;
} }

View File

@@ -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);
} }

View File

@@ -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;
} }

View File

@@ -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
// В базовом классе метод не выполняет никаких действий
} }
} }

View File

@@ -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)
{ {

View File

@@ -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;
}
} }

View File

@@ -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>

View File

@@ -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;
} }

View File

@@ -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; }
// ... конструктор
}

View File

@@ -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;
} }

View File

@@ -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;
}
}

View File

@@ -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;
}
} }

View File

@@ -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: Реализовать эффективное обновление дерева контролов
// вместо полной перестройки
} }
} }

View File

@@ -1,49 +0,0 @@
using Microsoft.UI.Xaml;
namespace Lattice.UI.DragDrop.WinUI.Behaviors;
/// <summary>
/// Attached properties для DragSource.
/// </summary>
public static class DragSource
{
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached(
"IsEnabled",
typeof(bool),
typeof(DragSource),
new PropertyMetadata(false, OnIsEnabledChanged));
public static readonly DependencyProperty DragDataProperty =
DependencyProperty.RegisterAttached(
"DragData",
typeof(object),
typeof(DragSource),
new PropertyMetadata(null));
public static bool GetIsEnabled(UIElement element) =>
(bool)element.GetValue(IsEnabledProperty);
public static void SetIsEnabled(UIElement element, bool value) =>
element.SetValue(IsEnabledProperty, value);
public static object GetDragData(UIElement element) =>
element.GetValue(DragDataProperty);
public static void SetDragData(UIElement element, object value) =>
element.SetValue(DragDataProperty, value);
private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not UIElement element) return;
// TODO: Здесь нужно создать экземпляр WinUIDragSourceBehavior
// и прикрепить его к элементу через DI
// Пока что устанавливаем данные в Tag
if ((bool)e.NewValue)
{
var data = GetDragData(element);
element.Tag = data;
}
}
}

View File

@@ -1,290 +0,0 @@
using Lattice.Core.DragDrop.Models;
using Lattice.Core.Geometry;
using Lattice.UI.DragDrop.Behaviors;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using System;
using System.Threading.Tasks;
namespace Lattice.UI.DragDrop.WinUI.Behaviors;
/// <summary>
/// Поведение источника перетаскивания для WinUI FrameworkElement.
/// </summary>
public class WinUIDragSourceBehavior : DragSourceBehaviorBase<FrameworkElement>
{
/// <summary>
/// Прикрепленное свойство для данных перетаскивания.
/// </summary>
public static readonly DependencyProperty DragDataProperty =
DependencyProperty.RegisterAttached(
"DragData",
typeof(object),
typeof(WinUIDragSourceBehavior),
new PropertyMetadata(null));
/// <summary>
/// Прикрепленное свойство для включения перетаскивания.
/// </summary>
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached(
"IsEnabled",
typeof(bool),
typeof(WinUIDragSourceBehavior),
new PropertyMetadata(false, OnIsEnabledChanged));
/// <summary>
/// Получает значение DragData.
/// </summary>
public static object GetDragData(FrameworkElement element)
{
return element.GetValue(DragDataProperty);
}
/// <summary>
/// Устанавливает значение DragData.
/// </summary>
public static void SetDragData(FrameworkElement element, object value)
{
element.SetValue(DragDataProperty, value);
}
/// <summary>
/// Получает значение IsEnabled.
/// </summary>
public static bool GetIsEnabled(FrameworkElement element)
{
return (bool)element.GetValue(IsEnabledProperty);
}
/// <summary>
/// Устанавливает значение IsEnabled.
/// </summary>
public static void SetIsEnabled(FrameworkElement element, bool value)
{
element.SetValue(IsEnabledProperty, value);
}
private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is FrameworkElement element)
{
// Получаем или создаем экземпляр поведения через Attached Property
var behavior = GetBehavior(element);
if ((bool)e.NewValue)
{
if (behavior == null)
{
behavior = new WinUIDragSourceBehavior();
SetBehavior(element, behavior);
}
behavior.AssociatedElement = element;
}
else
{
if (behavior != null)
{
behavior.Detach();
SetBehavior(element, null);
}
}
}
}
public WinUIDragSourceBehavior()
: base(ServiceProviderHelper.GetServiceProvider())
{
}
protected override void SubscribeToEvents(FrameworkElement element)
{
element.PointerPressed += OnPointerPressed;
element.PointerMoved += OnPointerMoved;
element.PointerReleased += OnPointerReleased;
element.PointerCanceled += OnPointerCanceled;
element.PointerCaptureLost += OnPointerCaptureLost;
element.LostFocus += OnLostFocus;
}
protected override void UnsubscribeFromEvents(FrameworkElement element)
{
element.PointerPressed -= OnPointerPressed;
element.PointerMoved -= OnPointerMoved;
element.PointerReleased -= OnPointerReleased;
element.PointerCanceled -= OnPointerCanceled;
element.PointerCaptureLost -= OnPointerCaptureLost;
element.LostFocus -= OnLostFocus;
}
private void OnPointerPressed(object sender, PointerRoutedEventArgs e)
{
if (AssociatedElement == null) return;
var point = e.GetCurrentPoint(AssociatedElement);
OnInteractionStarted(new Point(point.Position.X, point.Position.Y));
}
private void OnPointerMoved(object sender, PointerRoutedEventArgs e)
{
if (AssociatedElement == null) return;
var point = e.GetCurrentPoint(AssociatedElement);
OnInteractionMoved(new Point(point.Position.X, point.Position.Y));
}
private void OnPointerReleased(object sender, PointerRoutedEventArgs e)
{
OnInteractionEnded();
}
private void OnPointerCanceled(object sender, PointerRoutedEventArgs e)
{
OnInteractionCancelled();
}
private void OnPointerCaptureLost(object sender, PointerRoutedEventArgs e)
{
OnInteractionCancelled();
}
private void OnLostFocus(object sender, RoutedEventArgs e)
{
OnInteractionCancelled();
}
protected override Point ConvertToScreenCoordinates(Point point)
{
if (AssociatedElement == null)
return point;
var transform = AssociatedElement.TransformToVisual(null);
var screenPoint = transform.TransformPoint(new Windows.Foundation.Point(point.X, point.Y));
return new Point(screenPoint.X, screenPoint.Y);
}
/// <inheritdoc/>
public override async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
{
if (AssociatedElement == null)
{
return (false, null);
}
var data = GetDragData(AssociatedElement);
if (data == null)
{
// Пробуем получить данные из Tag или других источников
data = AssociatedElement.Tag ?? AssociatedElement.DataContext;
}
if (data == null)
{
return (false, null);
}
// Получаем начальную позицию в экранных координатах
var startPosition = ConvertToScreenCoordinates(_dragStartPosition);
// Создаем DragInfo с учетом вашего конструктора
var dragInfo = new DragInfo(
data: data,
allowedEffects: Core.DragDrop.Enums.DragDropEffects.Move |
Core.DragDrop.Enums.DragDropEffects.Copy,
startPosition: startPosition,
source: this
);
return (true, dragInfo);
}
protected override void OnDragCompleted(DragInfo dragInfo, Core.DragDrop.Enums.DragDropEffects effects)
{
base.OnDragCompleted(dragInfo, effects);
// Визуальная обратная связь при завершении
SetVisualState(AssociatedElement, "Normal");
}
protected override void OnDragCancelled(DragInfo dragInfo)
{
base.OnDragCancelled(dragInfo);
// Визуальная обратная связь при отмене
SetVisualState(AssociatedElement, "Normal");
}
private void SetVisualState(FrameworkElement? element, string stateName)
{
if (element is Control control)
{
try
{
VisualStateManager.GoToState(control, stateName, true);
}
catch
{
// Fallback
control.Opacity = 1.0;
}
}
else if (element != null)
{
// Альтернативная визуальная обратная связь для не-Control элементов
element.Opacity = 1.0;
}
}
// Attached property для хранения экземпляра поведения
private static readonly DependencyProperty BehaviorProperty =
DependencyProperty.RegisterAttached(
"Behavior",
typeof(WinUIDragSourceBehavior),
typeof(WinUIDragSourceBehavior),
new PropertyMetadata(null));
private static WinUIDragSourceBehavior GetBehavior(FrameworkElement element)
{
return (WinUIDragSourceBehavior)element.GetValue(BehaviorProperty);
}
private static void SetBehavior(FrameworkElement element, WinUIDragSourceBehavior? value)
{
element.SetValue(BehaviorProperty, value);
}
}
/// <summary>
/// Вспомогательный класс для получения IServiceProvider.
/// </summary>
internal static class ServiceProviderHelper
{
private static IServiceProvider? _serviceProvider;
public static IServiceProvider GetServiceProvider()
{
if (_serviceProvider == null)
{
// Ищем IServiceProvider в Application.Current.Resources
if (Application.Current.Resources.TryGetValue("ServiceProvider", out var provider) &&
provider is IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
else
{
throw new InvalidOperationException(
"IServiceProvider не найден. Убедитесь, что ServiceProvider зарегистрирован в ресурсах приложения.");
}
}
return _serviceProvider;
}
public static void SetServiceProvider(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
}

View File

@@ -1,345 +0,0 @@
using Lattice.Core.DragDrop.Models;
using Lattice.Core.Geometry;
using Lattice.UI.DragDrop.Behaviors;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
namespace Lattice.UI.DragDrop.WinUI.Behaviors
{
/// <summary>
/// Поведение цели сброса для элементов WinUI.
/// Позволяет элементам принимать данные при операции перетаскивания.
/// </summary>
/// <remarks>
/// <para>
/// Это поведение должно быть прикреплено к <see cref="FrameworkElement"/>, который должен выступать в качестве цели сброса.
/// Поведение автоматически регистрирует элемент в системе перетаскивания и обрабатывает все аспекты операции сброса.
/// </para>
/// <para>
/// Для использования необходимо:
/// 1. Создать экземпляр поведения с помощью <see cref="Attach"/> или через DI.
/// 2. Переопределить методы <see cref="CanAcceptDrop"/> и <see cref="Drop"/> для реализации логики принятия данных.
/// 3. При необходимости переопределить <see cref="DragOver"/> для настройки визуальной обратной связи.
/// </para>
/// </remarks>
public class WinUIDropTargetBehavior : DropTargetBehaviorBase<FrameworkElement>
{
private static readonly ConcurrentDictionary<FrameworkElement, WinUIDropTargetBehavior> _attachedBehaviors = new();
private readonly WeakReference<FrameworkElement>? _weakElement;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="WinUIDropTargetBehavior"/>.
/// </summary>
/// <param name="serviceProvider">Провайдер сервисов.</param>
/// <remarks>
/// Конструктор создает экземпляр поведения, но не прикрепляет его к элементу.
/// Для прикрепления используйте метод <see cref="Attach(FrameworkElement, IServiceProvider)"/>.
/// </remarks>
public WinUIDropTargetBehavior(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="WinUIDropTargetBehavior"/>.
/// </summary>
/// <param name="serviceProvider">Провайдер сервисов.</param>
/// <param name="element">Элемент, к которому прикрепляется поведение.</param>
/// <remarks>
/// Конструктор создает экземпляр поведения и сразу прикрепляет его к указанному элементу.
/// </remarks>
public WinUIDropTargetBehavior(IServiceProvider serviceProvider, FrameworkElement element)
: base(serviceProvider)
{
AssociatedElement = element ?? throw new ArgumentNullException(nameof(element));
}
/// <summary>
/// Прикрепляет поведение к указанному элементу.
/// </summary>
/// <param name="element">Элемент, к которому прикрепляется поведение.</param>
/// <param name="serviceProvider">Провайдер сервисов.</param>
/// <returns>
/// Экземпляр поведения, прикрепленного к элементу. Если к элементу уже прикреплено поведение,
/// возвращает существующий экземпляр.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="element"/> или <paramref name="serviceProvider"/> равны null.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод обеспечивает, что к каждому элементу прикреплен только один экземпляр поведения.
/// Если метод вызывается повторно для того же элемента, возвращается существующий экземпляр.
/// </para>
/// <para>
/// Прикрепленное поведение автоматически отслеживает изменения макета элемента и обновляет
/// его границы в системе перетаскивания.
/// </para>
/// </remarks>
public static WinUIDropTargetBehavior Attach(FrameworkElement element, IServiceProvider serviceProvider)
{
if (element == null) throw new ArgumentNullException(nameof(element));
if (serviceProvider == null) throw new ArgumentNullException(nameof(serviceProvider));
return _attachedBehaviors.GetOrAdd(element, key =>
{
var behavior = new WinUIDropTargetBehavior(serviceProvider, key);
// Подписка на события жизненного цикла элемента
key.Unloaded += OnElementUnloaded;
return behavior;
});
}
/// <summary>
/// Открепляет поведение от указанного элемента.
/// </summary>
/// <param name="element">Элемент, от которого открепляется поведение.</param>
/// <returns>
/// true, если поведение было успешно откреплено; false, если поведение не было прикреплено к элементу.
/// </returns>
/// <remarks>
/// Этот метод освобождает все ресурсы, связанные с поведением, и отписывается от событий элемента.
/// После вызова этого метода элемент перестает быть целью сброса.
/// </remarks>
public static bool Detach(FrameworkElement element)
{
if (element == null) return false;
if (_attachedBehaviors.TryRemove(element, out var behavior))
{
element.Unloaded -= OnElementUnloaded;
behavior.Detach();
return true;
}
return false;
}
/// <summary>
/// Получает поведение, прикрепленное к указанному элементу.
/// </summary>
/// <param name="element">Элемент, для которого требуется получить поведение.</param>
/// <returns>
/// Экземпляр поведения, прикрепленного к элементу, или null, если поведение не прикреплено.
/// </returns>
public static WinUIDropTargetBehavior? GetAttachedBehavior(FrameworkElement element)
{
_attachedBehaviors.TryGetValue(element, out var behavior);
return behavior;
}
/// <summary>
/// Подписывается на события элемента.
/// </summary>
/// <param name="element">Элемент, к которому прикрепляется поведение.</param>
/// <remarks>
/// <para>
/// Этот метод подписывается на следующие события:
/// </para>
/// <list type="bullet">
/// <item><see cref="FrameworkElement.LayoutUpdated"/> - для отслеживания изменений макета</item>
/// <item><see cref="FrameworkElement.SizeChanged"/> - для отслеживания изменений размера</item>
/// <item><see cref="FrameworkElement.Loaded"/> - для инициализации при загрузке элемента</item>
/// </list>
/// <para>
/// Переопределите этот метод, чтобы добавить подписку на дополнительные события.
/// </para>
/// </remarks>
protected override void SubscribeToEvents(FrameworkElement element)
{
if (element == null) return;
element.LayoutUpdated += OnLayoutUpdated;
element.SizeChanged += OnSizeChanged;
element.Loaded += OnLoaded;
// Если элемент уже загружен, сразу обновляем границы
if (element.IsLoaded)
{
UpdateBounds();
}
}
/// <summary>
/// Отписывается от событий элемента.
/// </summary>
/// <param name="element">Элемент, от которого отписывается поведение.</param>
/// <remarks>
/// Этот метод отписывается от всех событий, на которые подписался <see cref="SubscribeToEvents"/>.
/// </remarks>
protected override void UnsubscribeFromEvents(FrameworkElement element)
{
if (element == null) return;
element.LayoutUpdated -= OnLayoutUpdated;
element.SizeChanged -= OnSizeChanged;
element.Loaded -= OnLoaded;
}
/// <summary>
/// Получает границы элемента в экранных координатах.
/// </summary>
/// <param name="element">Элемент, границы которого нужно получить.</param>
/// <returns>
/// Прямоугольник, описывающий границы элемента в экранных координатах.
/// </returns>
/// <remarks>
/// <para>
/// Метод использует преобразование координат через <see cref="UIElement.TransformToVisual"/>
/// для получения глобальных координат элемента.
/// </para>
/// <para>
/// Если элемент не прикреплен к визуальному дереву или его границы не могут быть вычислены,
/// возвращается пустой прямоугольник.
/// </para>
/// </remarks>
protected override Rect GetScreenBounds(FrameworkElement element)
{
if (element == null || !element.IsLoaded)
return Rect.Empty;
try
{
// Получаем корневой элемент окна
var rootVisual = element.XamlRoot?.Content as UIElement;
if (rootVisual == null)
return Rect.Empty;
// Преобразуем границы элемента в координаты корневого элемента
var transform = element.TransformToVisual(rootVisual);
var position = transform.TransformPoint(new Windows.Foundation.Point(0, 0));
return new Rect(
position.X,
position.Y,
element.ActualWidth,
element.ActualHeight);
}
catch
{
// В случае ошибки возвращаем пустой прямоугольник
return Rect.Empty;
}
}
/// <summary>
/// Определяет, может ли элемент принять сбрасываемые данные.
/// </summary>
/// <param name="dropInfo">Информация о сбросе.</param>
/// <returns>
/// true, если элемент может принять данные; в противном случае — false.
/// </returns>
/// <remarks>
/// <para>
/// Этот метод является абстрактным и должен быть переопределен в производных классах
/// для реализации логики принятия данных.
/// </para>
/// <para>
/// Базовая реализация всегда возвращает false. Переопределите этот метод, чтобы определить,
/// какие типы данных может принимать ваш элемент и при каких условиях.
/// </para>
/// <example>
/// Пример реализации:
/// <code>
/// public override bool CanAcceptDrop(DropInfo dropInfo)
/// {
/// // Принимаем только строковые данные
/// return dropInfo.Data is string;
/// }
/// </code>
/// </example>
/// </remarks>
public override async Task<bool> CanAcceptDropAsync(DropInfo dropInfo)
{
// Базовая реализация - не принимает никакие данные.
// Переопределите этот метод в производных классах.
return false;
}
/// <summary>
/// Обрабатывает сброс данных на элемент.
/// </summary>
/// <param name="dropInfo">Информация о сбросе.</param>
/// <remarks>
/// <para>
/// Этот метод вызывается, когда пользователь отпускает кнопку мыши над элементом,
/// и данные должны быть приняты.
/// </para>
/// <para>
/// Базовая реализация ничего не делает. Переопределите этот метод, чтобы реализовать
/// логику обработки принятых данных.
/// </para>
/// <example>
/// Пример реализации:
/// <code>
/// public override void Drop(DropInfo dropInfo)
/// {
/// if (dropInfo.Data is string text)
/// {
/// // Обработка текстовых данных
/// AssociatedElement.SetValue(TextBlock.TextProperty, text);
/// dropInfo.MarkAsHandled();
/// }
/// }
/// </code>
/// </example>
/// </remarks>
public override async Task DropAsync(DropInfo dropInfo)
{
// Базовая реализация ничего не делает.
// Переопределите этот метод в производных классах.
}
/// <summary>
/// Освобождает ресурсы, связанные с поведением.
/// </summary>
/// <remarks>
/// <para>
/// Этот метод отписывается от всех событий, отменяет регистрацию в сервисе перетаскивания
/// и очищает все ресурсы.
/// </para>
/// <para>
/// После вызова этого метода поведение больше не может быть использовано.
/// </para>
/// </remarks>
public override void Detach()
{
if (AssociatedElement != null && _attachedBehaviors.TryGetValue(AssociatedElement, out _))
{
_attachedBehaviors.TryRemove(AssociatedElement, out _);
}
base.Detach();
}
#region Event Handlers
private void OnLayoutUpdated(object? sender, object e)
{
OnElementLayoutChanged();
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
OnElementLayoutChanged();
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
UpdateBounds();
}
private static void OnElementUnloaded(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement element)
{
Detach(element);
}
}
#endregion
}
}

View File

@@ -1,213 +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>
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>
public object DragData
{
get => GetValue(DragDataProperty);
set => SetValue(DragDataProperty, value);
}
/// <summary>
/// Получает или задает смещение относительно курсора.
/// </summary>
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>
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);
}
}
}
}

View File

@@ -1,156 +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>
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();
}
}

View File

@@ -1,141 +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>
[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>
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>
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;
}
}

View File

@@ -1,217 +0,0 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using System;
using System.Linq;
namespace Lattice.UI.DragDrop.WinUI.Extensions;
/// <summary>
/// Методы расширения для настройки перетаскивания в WinUI.
/// </summary>
public static class DragDropExtensions
{
#region Drag Source Extensions
/// <summary>
/// Делает элемент источником перетаскивания с указанными данными.
/// </summary>
public static void MakeDragSource(this FrameworkElement element, object dragData)
{
Behaviors.WinUIDragSourceBehavior.SetDragData(element, dragData);
Behaviors.WinUIDragSourceBehavior.SetIsEnabled(element, true);
}
/// <summary>
/// Делает элемент источником перетаскивания с фабрикой данных.
/// </summary>
public static void MakeDragSource(this FrameworkElement element, Func<object> dataFactory)
{
element.MakeDragSource(dataFactory());
}
/// <summary>
/// Удаляет возможность перетаскивания с элемента.
/// </summary>
public static void RemoveDragSource(this FrameworkElement element)
{
Behaviors.WinUIDragSourceBehavior.SetIsEnabled(element, false);
}
/// <summary>
/// Проверяет, является ли элемент источником перетаскивания.
/// </summary>
public static bool IsDragSource(this FrameworkElement element)
{
return Behaviors.WinUIDragSourceBehavior.GetIsEnabled(element);
}
/// <summary>
/// Получает данные перетаскивания из элемента.
/// </summary>
public static object? GetDragData(this FrameworkElement element)
{
return Behaviors.WinUIDragSourceBehavior.GetDragData(element);
}
#endregion
#region Drop Target Extensions
/// <summary>
/// Делает элемент целью сброса.
/// </summary>
public static void MakeDropTarget(this FrameworkElement element)
{
// Включаем AllowDrop для WinUI
element.AllowDrop = true;
element.SetValue(IsDropTargetProperty, true);
}
/// <summary>
/// Делает элемент целью сброса с фильтром типов данных.
/// </summary>
public static void MakeDropTarget(this FrameworkElement element, params Type[] acceptedTypes)
{
element.SetValue(AcceptsDataTypesProperty, acceptedTypes);
element.MakeDropTarget();
}
/// <summary>
/// Удаляет возможность сброса с элемента.
/// </summary>
public static void RemoveDropTarget(this FrameworkElement element)
{
element.AllowDrop = false;
element.SetValue(IsDropTargetProperty, false);
}
/// <summary>
/// Проверяет, является ли элемент целью сброса.
/// </summary>
public static bool IsDropTarget(this FrameworkElement element)
{
return (bool)element.GetValue(IsDropTargetProperty);
}
/// <summary>
/// Attached property для отметки цели сброса.
/// </summary>
public static readonly DependencyProperty IsDropTargetProperty =
DependencyProperty.RegisterAttached(
"IsDropTarget",
typeof(bool),
typeof(DragDropExtensions),
new PropertyMetadata(false));
/// <summary>
/// Attached property для фильтра типов данных.
/// </summary>
public static readonly DependencyProperty AcceptsDataTypesProperty =
DependencyProperty.RegisterAttached(
"AcceptsDataTypes",
typeof(Type[]),
typeof(DragDropExtensions),
new PropertyMetadata(null));
/// <summary>
/// Получает фильтр типов данных.
/// </summary>
public static Type[]? GetAcceptsDataTypes(this FrameworkElement element)
{
return (Type[]?)element.GetValue(AcceptsDataTypesProperty);
}
#endregion
#region Style Extensions
/// <summary>
/// Применяет стиль перетаскивания к элементу.
/// </summary>
public static void ApplyDragStyle(this Control control)
{
var style = Application.Current.Resources["DragEnabledStyle"] as Style;
if (style != null)
{
control.Style = style;
}
else
{
// Fallback стиль
var brush = Application.Current.Resources["SystemControlBackgroundAccentBrush"] as SolidColorBrush;
if (brush != null)
{
control.Background = brush;
}
}
}
/// <summary>
/// Переключает визуальное состояние элемента для перетаскивания.
/// </summary>
public static void SetDragVisualState(this Control control, string stateName, bool useTransitions = true)
{
try
{
VisualStateManager.GoToState(control, stateName, useTransitions);
}
catch
{
// Fallback для элементов без визуальных состояний
switch (stateName)
{
case "Dragging":
control.Opacity = 0.7;
break;
case "DragOver":
control.Background = new SolidColorBrush(Windows.UI.Color.FromArgb(50, 0, 120, 215));
break;
case "Normal":
control.ClearValue(Control.OpacityProperty);
control.ClearValue(Control.BackgroundProperty);
break;
}
}
}
#endregion
#region Advanced Configuration
/// <summary>
/// Создает контейнер с поддержкой перетаскивания для элементов.
/// </summary>
public static Panel CreateDragDropContainer(
Orientation orientation = Orientation.Vertical,
double spacing = 8,
bool enableReordering = true)
{
var container = new StackPanel
{
Orientation = orientation,
Spacing = spacing
};
if (enableReordering)
{
container.MakeDropTarget(typeof(FrameworkElement));
}
return container;
}
/// <summary>
/// Делает все дочерние элементы перетаскиваемыми.
/// </summary>
public static void MakeChildrenDraggable(this Panel container, Func<FrameworkElement, object> dataSelector)
{
foreach (var child in container.Children.OfType<FrameworkElement>())
{
var data = dataSelector(child);
child.MakeDragSource(data);
}
}
#endregion
}

View File

@@ -1,26 +0,0 @@
using Microsoft.UI.Xaml;
namespace Lattice.UI.DragDrop.WinUI.Helpers;
public static class FrameworkElementExtensions
{
/// <summary>
/// Получает фактические размеры FrameworkElement.
/// </summary>
public static Windows.Foundation.Size GetActualSize(this FrameworkElement element)
{
return new Windows.Foundation.Size(element.ActualWidth, element.ActualHeight);
}
/// <summary>
/// Получает границы элемента в экранных координатах.
/// </summary>
public static Windows.Foundation.Rect GetScreenBounds(this FrameworkElement element)
{
var transform = element.TransformToVisual(null);
var topLeft = transform.TransformPoint(new Windows.Foundation.Point(0, 0));
var bottomRight = transform.TransformPoint(new Windows.Foundation.Point(element.ActualWidth, element.ActualHeight));
return new Windows.Foundation.Rect(topLeft, bottomRight);
}
}

Some files were not shown because too many files have changed in this diff Show More