444 lines
20 KiB
C#
444 lines
20 KiB
C#
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.Runtime.CompilerServices;
|
||
|
||
namespace Lattice.Core.Docking.Models;
|
||
|
||
/// <summary>
|
||
/// Представляет узел дерева компоновки, который разделяет доступную область
|
||
/// между двумя дочерними элементами. Этот класс является основным структурным
|
||
/// элементом для создания сложных макетов с разделителями.
|
||
/// </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>
|
||
/// Событие, возникающее при изменении значения свойства.
|
||
/// </summary>
|
||
public event PropertyChangedEventHandler? PropertyChanged;
|
||
|
||
private double _splitRatio = 0.5;
|
||
private string _id;
|
||
|
||
/// <summary>
|
||
/// Получает уникальный идентификатор группы.
|
||
/// </summary>
|
||
/// <value>
|
||
/// Строковый идентификатор, уникальный в пределах дерева компоновки.
|
||
/// </value>
|
||
/// <remarks>
|
||
/// Идентификатор используется для сериализации/десериализации макета,
|
||
/// поиска элементов и отслеживания изменений в дереве.
|
||
/// </remarks>
|
||
public string Id
|
||
{
|
||
get => _id;
|
||
internal set
|
||
{
|
||
if (_id != value)
|
||
{
|
||
_id = value;
|
||
OnPropertyChanged();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получает или задает родительский элемент в иерархии дерева компоновки.
|
||
/// </summary>
|
||
/// <value>
|
||
/// Родительский элемент или null, если эта группа является корневой.
|
||
/// </value>
|
||
/// <remarks>
|
||
/// Это свойство управляется системой компоновки при добавлении или
|
||
/// удалении элементов из дерева.
|
||
/// </remarks>
|
||
public IDockElement? Parent { get; set; }
|
||
|
||
/// <summary>
|
||
/// Получает или задает первый дочерний элемент (левую или верхнюю область).
|
||
/// </summary>
|
||
/// <value>
|
||
/// Элемент, занимающий первую часть разделенной области.
|
||
/// </value>
|
||
/// <exception cref="ArgumentNullException">
|
||
/// Выбрасывается при попытке установить значение null.
|
||
/// </exception>
|
||
/// <remarks>
|
||
/// При установке нового значения автоматически обновляется свойство
|
||
/// <see cref="Parent"/> у дочернего элемента.
|
||
/// </remarks>
|
||
public IDockElement First { get; set; }
|
||
|
||
/// <summary>
|
||
/// Получает или задает второй дочерний элемент (правую или нижнюю область).
|
||
/// </summary>
|
||
/// <value>
|
||
/// Элемент, занимающий вторую часть разделенной области.
|
||
/// </value>
|
||
/// <exception cref="ArgumentNullException">
|
||
/// Выбрасывается при попытке установить значение null.
|
||
/// </exception>
|
||
/// <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
|
||
{
|
||
get => _splitRatio;
|
||
set
|
||
{
|
||
if (Math.Abs(_splitRatio - value) > double.Epsilon)
|
||
{
|
||
_splitRatio = value;
|
||
OnPropertyChanged();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получает или задает желаемую ширину элемента.
|
||
/// </summary>
|
||
/// <value>
|
||
/// Ширина в пикселях или относительных единицах.
|
||
/// </value>
|
||
public double Width { get; set; }
|
||
|
||
/// <summary>
|
||
/// Получает или задает желаемую высоту элемента.
|
||
/// </summary>
|
||
/// <value>
|
||
/// Высота в пикселях или относительных единицах.
|
||
/// </value>
|
||
public double Height { get; set; }
|
||
|
||
/// <summary>
|
||
/// Получает минимально допустимую ширину элемента.
|
||
/// </summary>
|
||
/// <value>
|
||
/// Минимальная ширина в пикселях, при которой элемент сохраняет функциональность.
|
||
/// </value>
|
||
/// <remarks>
|
||
/// Для группы минимальная ширина вычисляется как сумма минимальных ширин
|
||
/// дочерних элементов при горизонтальной ориентации или максимум минимальных
|
||
/// ширин при вертикальной ориентации.
|
||
/// </remarks>
|
||
public double MinWidth => Orientation == SplitDirection.Horizontal
|
||
? First.MinWidth + Second.MinWidth
|
||
: Math.Max(First.MinWidth, Second.MinWidth);
|
||
|
||
/// <summary>
|
||
/// Получает минимально допустимую высоту элемента.
|
||
/// </summary>
|
||
/// <value>
|
||
/// Минимальная высота в пикселях, при которой элемент сохраняет функциональность.
|
||
/// </value>
|
||
/// <remarks>
|
||
/// Для группы минимальная высота вычисляется как сумма минимальных высот
|
||
/// дочерних элементов при вертикальной ориентации или максимум минимальных
|
||
/// высот при горизонтальной ориентации.
|
||
/// </remarks>
|
||
public double MinHeight => Orientation == SplitDirection.Vertical
|
||
? First.MinHeight + Second.MinHeight
|
||
: Math.Max(First.MinHeight, Second.MinHeight);
|
||
|
||
/// <summary>
|
||
/// Инициализирует новый экземпляр класса <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));
|
||
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
|
||
} |