580 lines
23 KiB
C#
580 lines
23 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.Collections.ObjectModel;
|
||
using System.ComponentModel;
|
||
using System.Runtime.CompilerServices;
|
||
|
||
namespace Lattice.Core.Docking.Models;
|
||
|
||
/// <summary>
|
||
/// Представляет конечный узел (лист) дерева компоновки, который непосредственно
|
||
/// содержит коллекцию вкладок с контентом. Этот класс является контейнером для
|
||
/// отображаемого пользователю содержимого.
|
||
/// </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 IDockContent? _activeContent;
|
||
private string _id;
|
||
|
||
/// <summary>
|
||
/// Получает уникальный идентификатор листа.
|
||
/// </summary>
|
||
/// <value>
|
||
/// Строковый идентификатор, уникальный в пределах дерева компоновки.
|
||
/// </value>
|
||
public string Id
|
||
{
|
||
get => _id;
|
||
internal set
|
||
{
|
||
if (_id != value)
|
||
{
|
||
_id = value;
|
||
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;
|
||
|
||
/// <summary>
|
||
/// Получает или задает активную (выбранную) вкладку в контейнере.
|
||
/// </summary>
|
||
/// <value>
|
||
/// Активная вкладка или null, если в контейнере нет вкладок.
|
||
/// </value>
|
||
/// <remarks>
|
||
/// <para>
|
||
/// При установке нового значения проверяется, что вкладка действительно
|
||
/// содержится в коллекции <see cref="Children"/>.
|
||
/// </para>
|
||
/// <para>
|
||
/// Изменение этого свойства вызывает событие <see cref="PropertyChanged"/>.
|
||
/// </para>
|
||
/// </remarks>
|
||
public IDockContent? ActiveContent
|
||
{
|
||
get => _activeContent;
|
||
set
|
||
{
|
||
if (value != null && !_items.Contains(value)) return;
|
||
if (_activeContent != value)
|
||
{
|
||
_activeContent = value;
|
||
OnPropertyChanged();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получает или задает желаемую ширину элемента.
|
||
/// </summary>
|
||
/// <value>
|
||
/// Ширина в пикселях или относительных единицах.
|
||
/// </value>
|
||
public double Width { get; set; }
|
||
|
||
/// <summary>
|
||
/// Получает или задает желаемую высоту элемента.
|
||
/// </summary>
|
||
/// <value>
|
||
/// Высота в пикселях или относительных единицах.
|
||
/// </value>
|
||
public double Height { get; set; }
|
||
|
||
/// <summary>
|
||
/// Получает или задает минимально допустимую ширину элемента.
|
||
/// </summary>
|
||
/// <value>
|
||
/// Минимальная ширина в пикселях. Значение по умолчанию: 100.
|
||
/// </value>
|
||
public double MinWidth { get; set; } = 100;
|
||
|
||
/// <summary>
|
||
/// Получает или задает минимально допустимую высоту элемента.
|
||
/// </summary>
|
||
/// <value>
|
||
/// Минимальная высота в пикселях. Значение по умолчанию: 100.
|
||
/// </value>
|
||
public double MinHeight { get; set; } = 100;
|
||
|
||
/// <summary>
|
||
/// Получает или задает положение полосы вкладок в контейнере.
|
||
/// </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();
|
||
}
|
||
|
||
/// <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)
|
||
{
|
||
if (!_items.Contains(content))
|
||
{
|
||
_items.Add(content);
|
||
}
|
||
ActiveContent = content;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Удаляет контент из контейнера.
|
||
/// </summary>
|
||
/// <param name="content">
|
||
/// Контент для удаления.
|
||
/// </param>
|
||
/// <remarks>
|
||
/// <para>
|
||
/// Если удаляемый контент является активным, автоматически выбирается
|
||
/// новая активная вкладка (следующая в списке или предыдущая, если удалена
|
||
/// последняя).
|
||
/// </para>
|
||
/// <para>
|
||
/// Если после удаления контейнер становится пустым, он может быть удален
|
||
/// из дерева макета системой компоновки.
|
||
/// </para>
|
||
/// </remarks>
|
||
public void RemoveContent(IDockContent content)
|
||
{
|
||
int index = _items.IndexOf(content);
|
||
if (index == -1) return;
|
||
|
||
_items.RemoveAt(index);
|
||
|
||
if (ActiveContent == content)
|
||
{
|
||
if (_items.Count > 0)
|
||
ActiveContent = _items[Math.Min(index, _items.Count - 1)];
|
||
else
|
||
ActiveContent = null;
|
||
}
|
||
}
|
||
|
||
#region Реализация IDragSource
|
||
|
||
/// <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;
|
||
|
||
// 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;
|
||
} |