Доработан winui

This commit is contained in:
2026-02-01 09:26:13 +03:00
parent 584df249f6
commit e8b4cb9881
26 changed files with 1842 additions and 2373 deletions

View File

@@ -1,149 +1,65 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Lattice.Core.Docking.Abstractions;
namespace Lattice.Core.Docking.Models;
/// <summary>
/// Представляет автоскрываемую панель, которая может быть прикреплена к одной из сторон окна.
/// Автоскрываемые панели скрываются, оставляя видимой только полоску-заголовок,
/// и разворачиваются при наведении курсора или клике.
/// Автоскрываемые панели скрываются, оставляя видимой только заголовок, и разворачиваются при наведении курсора или клике.
/// </summary>
/// <remarks>
/// Автоскрываемые панели являются важным элементом современных IDE-подобных приложений,
/// позволяя экономить пространство экрана при сохранении быстрого доступа к инструментам.
/// </remarks>
public class AutoHidePanel : INotifyPropertyChanged
public class AutoHidePanel
{
/// <summary>
/// Происходит при изменении значения свойства.
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;
private bool _isVisible = false;
private double _slideOffset = 0;
/// <summary>
/// Получает уникальный идентификатор автоскрываемой панели.
/// </summary>
/// <value>
/// Строковый идентификатор, сгенерированный с помощью GUID.
/// </value>
public string Id { get; } = Guid.NewGuid().ToString();
/// <summary>
/// Получает или задает содержимое панели.
/// </summary>
/// <value>
/// Объект, реализующий <see cref="Abstractions.IDockContent"/>.
/// </value>
/// <exception cref="ArgumentNullException">
/// Выбрасывается при попытке установить значение null.
/// </exception>
public Abstractions.IDockContent Content
{
get => _content;
set
{
if (_content != value)
{
_content = value ?? throw new ArgumentNullException(nameof(value));
OnPropertyChanged();
OnPropertyChanged(nameof(Title));
}
}
}
private Abstractions.IDockContent _content;
/// <summary>
/// Получает или задает сторону окна, к которой прикреплена панель.
/// </summary>
/// <value>
/// Значение перечисления <see cref="DockSide"/>, указывающее сторону прикрепления.
/// </value>
public DockSide Side { get; set; }
/// <summary>
/// Получает или задает ширину панели (для левой/правой сторон)
/// или высоту (для верхней/нижней сторон).
/// </summary>
/// <value>
/// Размер панели в пикселях. Значение по умолчанию: 300.
/// </value>
public double Size { get; set; } = 300;
/// <summary>
/// Получает или задает признак видимости панели.
/// </summary>
/// <value>
/// true, если панель развернута и видима; в противном случае false.
/// </value>
/// <remarks>
/// При изменении этого свойства генерируется событие <see cref="PropertyChanged"/>.
/// </remarks>
public bool IsVisible
{
get => _isVisible;
set
{
if (_isVisible != value)
{
_isVisible = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Получает или задает смещение для анимации выезда/заезда панели.
/// </summary>
/// <value>
/// Значение от 0.0 до 1.0, где 0.0 - полностью скрыта, 1.0 - полностью развернута.
/// </value>
/// <remarks>
/// Используется для плавной анимации отображения/скрытия панели.
/// </remarks>
public double SlideOffset
{
get => _slideOffset;
set
{
if (Math.Abs(_slideOffset - value) > 0.001)
{
_slideOffset = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Получает заголовок панели.
/// </summary>
/// <value>
/// Заголовок, взятый из содержимого панели.
/// Если содержимое не установлено, возвращает "Auto-hide Panel".
/// </value>
public string Title => Content?.Title ?? "Auto-hide Panel";
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="AutoHidePanel"/>.
/// </summary>
/// <param name="content">Содержимое панели.</param>
/// <param name="side">Сторона окна для прикрепления.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="content"/> равен null.
/// </exception>
public AutoHidePanel(Abstractions.IDockContent content, DockSide side)
/// <param name="side">Сторона окна для прикрепления панели.</param>
/// <exception cref="ArgumentNullException">Выбрасывается, когда <paramref name="content"/> равен null.</exception>
public AutoHidePanel(IDockContent content, DockSide side)
{
Content = content ?? throw new ArgumentNullException(nameof(content));
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>
/// <remarks>
/// Если панель была видимой, становится скрытой, и наоборот.
/// </remarks>
public void Toggle()
{
IsVisible = !IsVisible;
@@ -164,15 +80,4 @@ public class AutoHidePanel : INotifyPropertyChanged
{
IsVisible = false;
}
/// <summary>
/// Вызывает событие <see cref="PropertyChanged"/>.
/// </summary>
/// <param name="name">
/// Имя изменившегося свойства. Если не указано, определяется автоматически.
/// </param>
protected void OnPropertyChanged([CallerMemberName] string? name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}

View File

@@ -4,77 +4,40 @@ using System.Runtime.CompilerServices;
namespace Lattice.Core.Docking.Models;
/// <summary>
/// Представляет узел дерева компоновки, который разделяет доступную область
/// между двумя дочерними элементами. Этот класс является основным структурным
/// элементом для создания сложных макетов с разделителями.
/// </summary>
/// <remarks>
/// Каждая группа содержит два дочерних элемента (<see cref="First"/> и <see cref="Second"/>),
/// которые могут быть либо другими группами (для создания вложенной структуры),
/// либо листами (<see cref="DockLeaf"/>) с контентом.
/// Направление разделения определяется свойством <see cref="Orientation"/>.
/// </remarks>
public class DockGroup : IDockElement, INotifyPropertyChanged
{
/// <summary>
/// Происходит при изменении значения свойства.
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;
private double _splitRatio = 0.5;
private string _id;
private IDockElement _first;
private IDockElement _second;
private SplitDirection _orientation;
private double _splitRatio = 0.5;
private IDockElement? _parent;
private double _width;
private double _height;
/// <summary>
/// Получает или задает уникальный идентификатор группы.
/// </summary>
/// <value>
/// Строковый идентификатор, уникальный в пределах дерева компоновки.
/// </value>
/// <remarks>
/// Идентификатор используется для сериализации/десериализации макета,
/// поиска элементов и отслеживания изменений в дереве.
/// </remarks>
public string Id
public event PropertyChangedEventHandler? PropertyChanged;
public DockGroup(IDockElement first, IDockElement second, SplitDirection orientation)
{
get => _id;
internal set
First = first ?? throw new ArgumentNullException(nameof(first));
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();
}
}
}
/// <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 => _first;
@@ -89,19 +52,6 @@ public class DockGroup : IDockElement, INotifyPropertyChanged
}
}
/// <summary>
/// Получает или задает второй дочерний элемент (правую или нижнюю область).
/// </summary>
/// <value>
/// Элемент, занимающий вторую часть разделенной области.
/// </value>
/// <exception cref="ArgumentNullException">
/// Выбрасывается при попытке установить значение null.
/// </exception>
/// <remarks>
/// При установке нового значения автоматически обновляется свойство
/// <see cref="Parent"/> у дочернего элемента.
/// </remarks>
public IDockElement Second
{
get => _second;
@@ -116,42 +66,25 @@ public class DockGroup : IDockElement, INotifyPropertyChanged
}
}
/// <summary>
/// Получает или задает направление разделения данной группы.
/// </summary>
/// <value>
/// Значение перечисления <see cref="SplitDirection"/>, указывающее,
/// как разделена область: горизонтально или вертикально.
/// </value>
/// <remarks>
/// <list type="bullet">
/// <item><see cref="SplitDirection.Horizontal"/> создает левую и правую области</item>
/// <item><see cref="SplitDirection.Vertical"/> создает верхнюю и нижнюю области</item>
/// </list>
/// </remarks>
public SplitDirection Orientation { get; set; }
public SplitDirection Orientation
{
get => _orientation;
set
{
if (_orientation != value)
{
_orientation = value;
OnPropertyChanged();
}
}
}
/// <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)
if (Math.Abs(_splitRatio - value) > 0.001)
{
_splitRatio = value;
OnPropertyChanged();
@@ -159,96 +92,42 @@ public class DockGroup : IDockElement, INotifyPropertyChanged
}
}
/// <summary>
/// Получает или задает желаемую ширину элемента.
/// </summary>
/// <value>
/// Ширина в пикселях или относительных единицах.
/// </value>
public double Width { get; set; }
public double Width
{
get => _width;
set
{
if (Math.Abs(_width - value) > 0.001)
{
_width = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Получает или задает желаемую высоту элемента.
/// </summary>
/// <value>
/// Высота в пикселях или относительных единицах.
/// </value>
public double Height { get; set; }
public double Height
{
get => _height;
set
{
if (Math.Abs(_height - value) > 0.001)
{
_height = value;
OnPropertyChanged();
}
}
}
/// <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)
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = 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));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

View File

@@ -5,85 +5,44 @@ using System.Runtime.CompilerServices;
namespace Lattice.Core.Docking.Models;
/// <summary>
/// Представляет конечный узел (лист) дерева компоновки, который непосредственно
/// содержит коллекцию вкладок с контентом. Этот класс является контейнером для
/// отображаемого пользователю содержимого.
/// </summary>
/// <remarks>
/// Лист является основным элементом, с которым взаимодействует пользователь
/// при работе с документами или инструментальными панелями в IDE-подобных
/// приложениях.
/// </remarks>
public class DockLeaf : IDockContainer, INotifyPropertyChanged
{
/// <summary>
/// Происходит при изменении значения свойства.
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;
private readonly ObservableCollection<IDockContent> _items = new();
private IDockContent? _activeContent;
private string _id;
private TabPlacement _tabPlacement = TabPlacement.Bottom;
private IDockElement? _parent;
private double _width;
private double _height;
private TabPlacement _tabPlacement = TabPlacement.Top;
/// <summary>
/// Получает или задает уникальный идентификатор листа.
/// </summary>
/// <value>
/// Строковый идентификатор, уникальный в пределах дерева компоновки.
/// </value>
public string Id
public event PropertyChangedEventHandler? PropertyChanged;
public DockLeaf()
{
get => _id;
internal set
_items.CollectionChanged += (s, e) => OnPropertyChanged(nameof(Children));
}
public string Id { get; } = Guid.NewGuid().ToString();
public IDockElement? Parent
{
get => _parent;
set
{
if (_id != value)
if (_parent != value)
{
_id = value;
_parent = 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>
/// При установке нового значения проверяется, что вкладка действительно
/// содержится в коллекции <see cref="Children"/>.
/// Изменение этого свойства вызывает событие <see cref="PropertyChanged"/>.
/// </remarks>
public IDockContent? ActiveContent
{
get => _activeContent;
set
{
if (value != null && !_items.Contains(value)) return;
if (_activeContent != value)
{
_activeContent = value;
@@ -92,48 +51,35 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged
}
}
/// <summary>
/// Получает или задает желаемую ширину элемента.
/// </summary>
/// <value>
/// Ширина в пикселях или относительных единицах.
/// </value>
public double Width { get; set; }
public double Width
{
get => _width;
set
{
if (Math.Abs(_width - value) > 0.001)
{
_width = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Получает или задает желаемую высоту элемента.
/// </summary>
/// <value>
/// Высота в пикселях или относительных единицах.
/// </value>
public double Height { get; set; }
public double Height
{
get => _height;
set
{
if (Math.Abs(_height - value) > 0.001)
{
_height = value;
OnPropertyChanged();
}
}
}
/// <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 => _tabPlacement;
@@ -147,47 +93,10 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged
}
}
/// <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>
/// Если контент уже содержится в коллекции, он не добавляется повторно,
/// но становится активным.
/// Этот метод обновляет свойство <see cref="ActiveContent"/> и вызывает
/// соответствующее событие изменения свойства.
/// </remarks>
public void AddContent(IDockContent content)
{
if (content == null) return;
if (content == null)
throw new ArgumentNullException(nameof(content));
if (!_items.Contains(content))
{
@@ -196,22 +105,10 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged
ActiveContent = content;
}
/// <summary>
/// Удаляет контент из контейнера.
/// </summary>
/// <param name="content">
/// Контент для удаления.
/// </param>
/// <remarks>
/// Если удаляемый контент является активным, автоматически выбирается
/// новая активная вкладка (следующая в списке или предыдущая, если удалена
/// последняя).
/// Если после удаления контейнер становится пустым, он может быть удален
/// из дерева макета системой компоновки.
/// </remarks>
public void RemoveContent(IDockContent content)
{
if (content == null) return;
if (content == null)
throw new ArgumentNullException(nameof(content));
int index = _items.IndexOf(content);
if (index == -1) return;
@@ -226,4 +123,9 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged
ActiveContent = null;
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}