Доработан 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

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

@@ -10,23 +10,6 @@ using System.Runtime.CompilerServices;
namespace Lattice.UI;
/// <summary>
/// Визуальный контрол для отображения группы разделения (сплиттера) в системе докинга.
/// Реализует интерфейс <see cref="IDockGroupControl"/> для интеграции с системой докинга
/// и обеспечивает отображение двух дочерних элементов с разделителем между ними.
/// </summary>
/// <remarks>
/// <para>
/// Контрол <see cref="LatticeDockGroup"/> отвечает за визуальное представление узла
/// дерева компоновки, который разделяет доступное пространство между двумя дочерними
/// элементами. Поддерживает горизонтальное и вертикальное разделение с возможностью
/// изменения соотношения сторон через перетаскивание разделителя.
/// </para>
/// <para>
/// Контрол автоматически обновляет свое представление при изменении свойств модели
/// и обеспечивает двустороннюю привязку данных с объектом <see cref="DockGroup"/>.
/// </para>
/// </remarks>
public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
{
private readonly PropertyChangedEventHandler _modelPropertyChangedHandler;
@@ -42,14 +25,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
private double _splitRatio = 0.5;
private double _splitterSize = 4.0;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="LatticeDockGroup"/>.
/// </summary>
/// <remarks>
/// Конструктор устанавливает ключ стиля по умолчанию, инициализирует обработчик
/// изменений модели и подписывается на событие изменения контекста данных.
/// Созданный контрол готов к использованию после применения шаблона.
/// </remarks>
public LatticeDockGroup()
{
this.DefaultStyleKey = typeof(LatticeDockGroup);
@@ -57,18 +32,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
this.DataContextChanged += OnDataContextChanged;
}
/// <summary>
/// Получает или задает модель данных, связанную с этим контролом.
/// </summary>
/// <value>
/// Экземпляр <see cref="DockGroup"/>, представляющий узел разделения в дереве компоновки.
/// Может быть null, если контрол не связан с моделью.
/// </value>
/// <remarks>
/// При установке новой модели контрол автоматически подписывается на события
/// изменения свойств модели и обновляет свое визуальное представление.
/// При удалении модели происходит отписка от событий и очистка ресурсов.
/// </remarks>
public IDockElement? Model
{
get => _model;
@@ -82,18 +45,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <summary>
/// Получает или задает менеджер макета, к которому принадлежит этот контрол.
/// </summary>
/// <value>
/// Экземпляр <see cref="LayoutManager"/>, управляющий структурой док-системы.
/// Может быть null, если контрол не связан с менеджером макета.
/// </value>
/// <remarks>
/// Менеджер макета используется для выполнения операций с деревом компоновки,
/// таких как перемещение элементов, создание плавающих окон и управление
/// автоскрываемыми панелями.
/// </remarks>
public LayoutManager? LayoutManager
{
get => _layoutManager;
@@ -105,17 +56,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <summary>
/// Получает или задает контекстный менеджер для этого контрола.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDockContextManager"/> или null, если менеджер не установлен.
/// </value>
/// <remarks>
/// Контекстный менеджер используется для отображения контекстных меню при щелчке
/// правой кнопкой мыши по контролу. Меню содержит команды, доступные для данного
/// элемента в текущем контексте.
/// </remarks>
public IDockContextManager? ContextManager
{
get => _contextManager;
@@ -127,18 +67,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <summary>
/// Получает или задает признак того, что контрол выбран.
/// </summary>
/// <value>
/// true, если контрол выбран; в противном случае false.
/// Значение по умолчанию: false.
/// </value>
/// <remarks>
/// Выделенный контрол обычно визуально отличается от других (например, имеет
/// выделенную границу или фон). В каждый момент времени может быть выделен
/// только один контрол в пределах контейнера.
/// </remarks>
public bool IsSelected
{
get => _isSelected;
@@ -150,17 +78,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <summary>
/// Получает или задает признак того, что контрол активен.
/// </summary>
/// <value>
/// true, если контрол активен; в противном случае false.
/// Значение по умолчанию: false.
/// </value>
/// <remarks>
/// Активный контрол получает фокус ввода и может обрабатывать команды клавиатуры.
/// Обычно соответствует последнему взаимодействию пользователя с элементом.
/// </remarks>
public bool IsActive
{
get => _isActive;
@@ -172,20 +89,9 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <summary>
/// Получает или задает ориентацию разделения группы.
/// </summary>
/// <value>
/// Направление разделения (горизонтальное или вертикальное).
/// </value>
/// <remarks>
/// Ориентация определяет, как расположены дочерние элементы относительно друг друга:
/// <list type="bullet">
/// <item><see cref="SplitDirection.Horizontal"/> - элементы расположены слева и справа</item>
/// <item><see cref="SplitDirection.Vertical"/> - элементы расположены сверху и снизу</item>
/// </list>
/// Изменение ориентации приводит к перестройке внутреннего макета контрола.
/// </remarks>
public bool CanDrag => true;
public bool CanDrop => true;
public SplitDirection Orientation
{
get => _model?.Orientation ?? SplitDirection.Horizontal;
@@ -199,18 +105,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <summary>
/// Получает или задает соотношение разделения между первым и вторым элементами.
/// </summary>
/// <value>
/// Значение от 0.0 до 1.0, где 0.5 означает равное разделение пространства.
/// Значение 0.0 отдает все пространство второму элементу, 1.0 - первому элементу.
/// </value>
/// <remarks>
/// Соотношение разделения определяет пропорции, в которых доступное пространство
/// распределяется между дочерними элементами. Изменение этого свойства приводит
/// к перестройке внутреннего макета и генерации события <see cref="SplitRatioChanged"/>.
/// </remarks>
public double SplitRatio
{
get => _splitRatio;
@@ -221,24 +115,12 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
_splitRatio = value;
UpdateLayoutDefinitions();
OnPropertyChanged(nameof(SplitRatio));
SplitRatioChanged?.Invoke(this,
new SplitRatioChangedEventArgs(value, SplitRatioChangeSource.Programmatic));
}
}
}
/// <summary>
/// Получает или задает размер разделителя в пикселях.
/// </summary>
/// <value>
/// Ширина разделителя в пикселях. Значение по умолчанию: 4.0.
/// </value>
/// <remarks>
/// Размер разделителя определяет область, доступную для перетаскивания пользователем
/// для изменения соотношения разделения. Увеличение размера облегчает взаимодействие,
/// но уменьшает полезное пространство для содержимого.
/// </remarks>
public double SplitterSize
{
get => _splitterSize;
@@ -252,57 +134,12 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <summary>
/// Получает контрол для первого дочернего элемента.
/// </summary>
/// <value>
/// Контрол, отображающий первый дочерний элемент, или null, если элемент не установлен.
/// </value>
/// <remarks>
/// Первый дочерний элемент занимает левую область при горизонтальной ориентации
/// или верхнюю область при вертикальной ориентации.
/// </remarks>
public IDockControl? FirstChild => _firstChildControl?.Content as IDockControl;
/// <summary>
/// Получает контрол для второго дочернего элемента.
/// </summary>
/// <value>
/// Контрол, отображающий второй дочерний элемент, или null, если элемент не установлен.
/// </value>
/// <remarks>
/// Второй дочерний элемент занимает правую область при горизонтальной ориентации
/// или нижнюю область при вертикальной ориентации.
/// </remarks>
public IDockControl? SecondChild => _secondChildControl?.Content as IDockControl;
/// <summary>
/// Происходит при изменении соотношения разделения между дочерними элементами.
/// </summary>
/// <remarks>
/// Событие генерируется при изменении свойства <see cref="SplitRatio"/>,
/// независимо от источника изменения (пользователь, программа или восстановление состояния).
/// Содержит информацию о новом соотношении и источнике изменения.
/// </remarks>
public event EventHandler<SplitRatioChangedEventArgs>? SplitRatioChanged;
/// <summary>
/// Происходит при изменении значения свойства.
/// </summary>
/// <remarks>
/// Событие реализует интерфейс <see cref="INotifyPropertyChanged"/> и используется
/// для уведомления системы привязки данных об изменениях свойств контрола.
/// </remarks>
public event PropertyChangedEventHandler? PropertyChanged;
/// <summary>
/// Вызывается при применении шаблона контрола.
/// </summary>
/// <remarks>
/// Метод получает ссылки на именованные части шаблона и инициализирует
/// внутренние структуры контрола. Вызывает обновление макета для корректного
/// отображения дочерних элементов.
/// </remarks>
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
@@ -314,48 +151,22 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
UpdateLayoutDefinitions();
}
/// <summary>
/// Обрабатывает изменение контекста данных контрола.
/// </summary>
/// <param name="sender">Источник события (контрол).</param>
/// <param name="args">Данные о изменении контекста.</param>
/// <remarks>
/// Метод автоматически устанавливает модель контрола на основе нового контекста данных,
/// если он является экземпляром <see cref="DockGroup"/>. Это позволяет использовать
/// привязку данных XAML для установки модели контрола.
/// </remarks>
private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
Model = args.NewValue as DockGroup;
}
/// <summary>
/// Присоединяет модель к контролу.
/// </summary>
/// <remarks>
/// Подписывается на события изменения свойств модели, устанавливает контекст данных
/// и инициализирует свойства контрола значениями из модели. Вызывает обновление макета.
/// </remarks>
private void AttachModel()
{
if (_model != null)
{
_model.PropertyChanged += _modelPropertyChangedHandler;
this.DataContext = _model;
// Инициализируем свойства из модели
_splitRatio = _model.SplitRatio;
UpdateLayoutDefinitions();
}
}
/// <summary>
/// Отсоединяет модель от контрола.
/// </summary>
/// <remarks>
/// Отписывается от событий изменения свойств модели, очищает контекст данных
/// и освобождает ресурсы, связанные с предыдущей моделью.
/// </remarks>
private void DetachModel()
{
if (_model != null)
@@ -365,16 +176,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <summary>
/// Обрабатывает изменения свойств модели.
/// </summary>
/// <param name="sender">Источник события (модель).</param>
/// <param name="e">Данные об изменении свойства.</param>
/// <remarks>
/// Реагирует на изменения ключевых свойств модели (Orientation, SplitRatio)
/// и обновляет соответствующие свойства и визуальное представление контрола.
/// Также уведомляет систему привязки данных об изменении свойств контрола.
/// </remarks>
private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
@@ -395,14 +196,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <summary>
/// Обновляет определения макета сетки на основе текущей ориентации и соотношения разделения.
/// </summary>
/// <remarks>
/// Метод перестраивает структуру строк и столбцов сетки в зависимости от ориентации
/// разделения и текущего соотношения между дочерними элементами. Обеспечивает
/// корректное позиционирование разделителя и дочерних контролов.
/// </remarks>
private void UpdateLayoutDefinitions()
{
if (_rootGrid == null || _model == null) return;
@@ -412,7 +205,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
if (_model.Orientation == SplitDirection.Horizontal)
{
// Горизонтальное разделение
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition
{ Width = new GridLength(_model.SplitRatio, GridUnitType.Star) });
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition
@@ -420,7 +212,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition
{ Width = new GridLength(1 - _model.SplitRatio, GridUnitType.Star) });
// Устанавливаем позиции элементов
if (_firstChildControl != null)
{
Grid.SetColumn(_firstChildControl, 0);
@@ -435,7 +226,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
else
{
// Вертикальное разделение
_rootGrid.RowDefinitions.Add(new RowDefinition
{ Height = new GridLength(_model.SplitRatio, GridUnitType.Star) });
_rootGrid.RowDefinitions.Add(new RowDefinition
@@ -443,7 +233,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
_rootGrid.RowDefinitions.Add(new RowDefinition
{ Height = new GridLength(1 - _model.SplitRatio, GridUnitType.Star) });
// Устанавливаем позиции элементов
if (_firstChildControl != null)
{
Grid.SetRow(_firstChildControl, 0);
@@ -458,15 +247,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <summary>
/// Устанавливает дочерние контролы для отображения.
/// </summary>
/// <param name="firstChild">Контрол для первого элемента.</param>
/// <param name="secondChild">Контрол для второго элемента.</param>
/// <remarks>
/// Метод назначает контролы для визуального представления дочерних элементов группы.
/// После установки контролов обновляет макет для корректного отображения.
/// </remarks>
public void SetChildren(IDockControl? firstChild, IDockControl? secondChild)
{
if (_firstChildControl != null)
@@ -478,44 +258,27 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
UpdateLayoutDefinitions();
}
/// <summary>
/// Обновляет внешний вид контрола в соответствии с текущим состоянием модели.
/// </summary>
/// <remarks>
/// Вызывает перестройку макета сетки для синхронизации визуального представления
/// с текущими значениями свойств модели (ориентация, соотношение разделения).
/// </remarks>
public object? PrepareDragData()
{
return Model;
}
public bool HandleDrop(object data, DockPosition position)
{
// TODO: Реализовать обработку сброса
return false;
}
public void Refresh()
{
UpdateLayoutDefinitions();
}
/// <summary>
/// Применяет указанную тему к контролу.
/// </summary>
/// <param name="theme">Тема для применения.</param>
/// <remarks>
/// Обновляет стили и параметры отображения контрола в соответствии с заданной темой.
/// В текущей реализации метод является заглушкой и должен быть расширен для
/// поддержки динамического изменения тем оформления.
/// </remarks>
public void ApplyTheme(IDockTheme theme)
{
// Применение темы к контролу
if (theme != null)
{
// TODO: Реализовать применение темы к стилям контрола
}
// TODO: Реализовать применение темы
}
/// <summary>
/// Вызывается при изменении состояния модели для обновления UI.
/// </summary>
/// <param name="propertyName">Имя изменившегося свойства модели.</param>
/// <remarks>
/// Перенаправляет вызов в обработчик изменений модели, обеспечивая уведомление
/// контрола о конкретных изменениях в связанной модели данных.
/// </remarks>
public void OnModelPropertyChanged(string propertyName)
{
if (_model != null)
@@ -524,27 +287,11 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
}
}
/// <summary>
/// Вызывает событие изменения свойства.
/// </summary>
/// <param name="propertyName">Имя изменившегося свойства.</param>
/// <remarks>
/// Используется для уведомления системы привязки данных об изменениях свойств
/// контрола. Если имя свойства не указано, автоматически определяется по имени
/// вызывающего члена.
/// </remarks>
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <summary>
/// Освобождает ресурсы, используемые этим экземпляром контрола.
/// </summary>
/// <remarks>
/// Выполняет отписку от событий модели, очистку ссылок и освобождение ресурсов.
/// После вызова этого метода контрол не должен использоваться.
/// </remarks>
public void Dispose()
{
if (!_disposed)

View File

@@ -1,10 +1,10 @@
using Lattice.Core.Docking.Abstractions;
using Lattice.Core.Docking.Engine;
using Lattice.Core.Docking.Models;
using Lattice.UI.Docking;
using Lattice.UI.Docking.Abstractions;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -13,25 +13,6 @@ using System.Runtime.CompilerServices;
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
{
private readonly PropertyChangedEventHandler _modelPropertyChangedHandler;
@@ -50,13 +31,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
private bool _showMenu = true;
private ContentControl? _rootContainer;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="LatticeDockHost"/>.
/// </summary>
/// <remarks>
/// Конструктор устанавливает ключ стиля по умолчанию, инициализирует обработчик изменений модели
/// и подписывается на событие изменения контекста данных.
/// </remarks>
public LatticeDockHost()
{
this.DefaultStyleKey = typeof(LatticeDockHost);
@@ -64,17 +38,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
this.DataContextChanged += OnDataContextChanged;
}
/// <summary>
/// Получает или задает модель данных, связанную с этим контролом.
/// </summary>
/// <value>
/// Экземпляр, реализующий <see cref="IDockElement"/>, представляющий корневой элемент
/// дерева компоновки. Может быть null.
/// </value>
/// <remarks>
/// Этот элемент является корнем всего макета док-системы. При изменении этого свойства
/// происходит перестройка всего пользовательского интерфейса.
/// </remarks>
public IDockElement? 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
{
get => _layoutManager;
@@ -109,16 +62,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
}
}
/// <summary>
/// Получает или задает контекстный менеджер для этого контрола.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDockContextManager"/>, управляющий контекстными меню и действиями.
/// </value>
/// <remarks>
/// Контекстный менеджер используется для отображения меню, связанных с этим элементом,
/// и выполнения команд, доступных в текущем контексте.
/// </remarks>
public IDockContextManager? ContextManager
{
get => _contextManager;
@@ -130,16 +73,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
}
}
/// <summary>
/// Получает или задает признак того, что контрол выбран.
/// </summary>
/// <value>
/// true, если контрол выбран; в противном случае — false.
/// </value>
/// <remarks>
/// Выделение контрола обычно визуально выделяет его границы или фон,
/// чтобы указать пользователю на активный элемент.
/// </remarks>
public bool IsSelected
{
get => _isSelected;
@@ -151,15 +84,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
}
}
/// <summary>
/// Получает или задает признак того, что контрол активен.
/// </summary>
/// <value>
/// true, если контрол активен; в противном случае — false.
/// </value>
/// <remarks>
/// Активный контрол обычно получает фокус ввода и может обрабатывать команды клавиатуры.
/// </remarks>
public bool IsActive
{
get => _isActive;
@@ -171,16 +95,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
}
}
/// <summary>
/// Получает или задает признак того, что контрол можно перетаскивать.
/// </summary>
/// <value>
/// true, если контрол можно перетаскивать; в противном случае — false.
/// </value>
/// <remarks>
/// Этот флаг влияет на возможность инициирования операции перетаскивания
/// при взаимодействии пользователя с этим контролом.
/// </remarks>
public bool CanDrag
{
get => _canDrag;
@@ -192,16 +106,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
}
}
/// <summary>
/// Получает или задает признак того, что контрол может принимать сброс.
/// </summary>
/// <value>
/// true, если контрол может принимать сброс; в противном случае — false.
/// </value>
/// <remarks>
/// Этот флаг влияет на возможность завершения операции перетаскивания
/// сбросом данных на этот контрол.
/// </remarks>
public bool CanDrop
{
get => _canDrop;
@@ -213,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;
/// <summary>
/// Получает коллекцию контролов автоскрываемых панелей, прикрепленных к краям окна.
/// </summary>
/// <value>
/// Коллекция объектов, реализующих <see cref="IAutoHidePanelControl"/>,
/// представляющих автоскрываемые панели на разных сторонах окна.
/// </value>
/// <remarks>
/// Коллекция является наблюдаемой (ObservableCollection), что позволяет автоматически
/// обновлять пользовательский интерфейс при добавлении или удалении панелей.
/// </remarks>
public IEnumerable<IAutoHidePanelControl> AutoHidePanels => _autoHidePanels;
/// <summary>
/// Получает или задает значение, указывающее, отображается ли панель инструментов (Toolbox).
/// </summary>
/// <value>
/// true, если панель инструментов видима; в противном случае — false.
/// </value>
/// <remarks>
/// Панель инструментов обычно содержит элементы для быстрого доступа к командам
/// или создания новых компонентов в приложении.
/// </remarks>
public bool ShowToolbox
{
get => _showToolbox;
@@ -260,16 +131,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
}
}
/// <summary>
/// Получает или задает значение, указывающее, отображается ли строка состояния.
/// </summary>
/// <value>
/// true, если строка состояния видима; в противном случае — false.
/// </value>
/// <remarks>
/// Строка состояния обычно отображает текущий статус приложения,
/// информацию о выбранном элементе или прогресс выполнения операций.
/// </remarks>
public bool ShowStatusBar
{
get => _showStatusBar;
@@ -281,15 +142,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
}
}
/// <summary>
/// Получает или задает значение, указывающее, отображается ли главное меню приложения.
/// </summary>
/// <value>
/// true, если главное меню видимо; в противном случае — false.
/// </value>
/// <remarks>
/// Главное меню содержит основные команды приложения, организованные в иерархическую структуру.
/// </remarks>
public bool ShowMenu
{
get => _showMenu;
@@ -301,41 +153,43 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
}
}
/// <summary>
/// Событие, возникающее при изменении структуры макета док-системы.
/// </summary>
/// <remarks>
/// Может вызываться при добавлении/удалении элементов, изменении размеров,
/// создании/закрытии плавающих окон и других операциях, влияющих на компоновку.
/// </remarks>
public event EventHandler? LayoutChanged;
/// <summary>
/// Событие, возникающее при создании нового плавающего окна.
/// </summary>
public event EventHandler<FloatingWindowCreatedEventArgs>? FloatingWindowCreated;
/// <summary>
/// Событие, возникающее при закрытии плавающего окна.
/// </summary>
public event EventHandler<FloatingWindowClosedEventArgs>? FloatingWindowClosed;
/// <summary>
/// Событие, возникающее при изменении значения свойства.
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;
/// <summary>
/// Вызывается при применении шаблона контрола.
/// </summary>
/// <remarks>
/// Метод получает ссылки на именованные части шаблона и обновляет отображение
/// корневого содержимого в соответствии с текущим состоянием модели.
/// </remarks>
/// <inheritdoc/>
public FrameworkElement? DragDropElement => this;
/// <inheritdoc/>
public void SetupDragDropHandlers()
{
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()
{
base.OnApplyTemplate();
_rootContainer = GetTemplateChild("PART_RootContainer") as ContentControl;
UpdateRootContent();
}
@@ -349,11 +203,8 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
{
if (_model != null && _layoutManager != null)
{
// Подписываемся на события менеджера макета
_layoutManager.LayoutUpdated += OnLayoutUpdated;
_layoutManager.AutoHidePanelsChanged += OnAutoHidePanelsChanged;
// Устанавливаем DataContext
this.DataContext = _model;
UpdateRootContent();
}
@@ -363,17 +214,14 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
{
if (_model != null && _layoutManager != null)
{
// Отписываемся от событий
_layoutManager.LayoutUpdated -= OnLayoutUpdated;
_layoutManager.AutoHidePanelsChanged -= OnAutoHidePanelsChanged;
this.DataContext = null;
}
}
private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
// Обработка изменений модели
OnPropertyChanged(e.PropertyName);
}
@@ -385,7 +233,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
private void OnAutoHidePanelsChanged(object? sender, EventArgs e)
{
// Обновление автоскрываемых панелей
OnPropertyChanged(nameof(AutoHidePanels));
}
@@ -393,8 +240,7 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
{
if (_rootContainer != null && _model != null && _layoutManager != null)
{
// Создаем дерево контролов через фабрику
var factory = LatticeUIFramework.ControlFactory;
var factory = Lattice.UI.Docking.LatticeUIFramework.ControlFactory;
if (factory != null)
{
var control = factory.CreateControlForElement(_model);
@@ -403,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)
{
if (element == null) throw new ArgumentNullException(nameof(element));
// TODO: Реализовать создание плавающего окна через фабрику
throw new NotImplementedException();
throw new NotImplementedException("Floating windows not implemented yet");
}
/// <summary>
/// Закрывает указанное плавающее окно и возвращает его содержимое в основной макет.
/// </summary>
/// <param name="window">
/// Плавающее окно, которое необходимо закрыть.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="window"/> равен null.
/// </exception>
/// <remarks>
/// При закрытии плавающего окна его содержимое обычно возвращается в то место
/// в основном макете, откуда оно было извлечено, или в ближайшую допустимую позицию.
/// </remarks>
public void CloseFloatingWindow(IFloatingWindowControl window)
{
if (window == null) throw new ArgumentNullException(nameof(window));
if (_floatingWindows.Remove(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)
{
if (content == null) throw new ArgumentNullException(nameof(content));
if (_layoutManager != null)
{
var panel = _layoutManager.AddAutoHidePanel(content, side);
// TODO: Создать UI-контрол для автоскрываемой панели через фабрику
throw new NotImplementedException();
throw new NotImplementedException("Auto-hide panels not implemented yet");
}
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)
{
if (panel == null) throw new ArgumentNullException(nameof(panel));
// TODO: Реализовать удаление автоскрываемой панели
throw new NotImplementedException();
throw new NotImplementedException("Auto-hide panels not implemented yet");
}
/// <summary>
/// Обновляет внешний вид контрола в соответствии с текущим состоянием модели.
/// </summary>
/// <remarks>
/// Вызывает обновление корневого содержимого и всех дочерних элементов.
/// </remarks>
public void Refresh()
{
UpdateRootContent();
}
public object? PrepareDragData() => Model;
public bool HandleDrop(object data, DockPosition position) => false;
public void Refresh() => UpdateRootContent();
/// <summary>
/// Применяет указанную тему к контролу.
/// </summary>
/// <param name="theme">Тема для применения.</param>
/// <remarks>
/// В текущей реализации метод является заглушкой и должен быть расширен
/// для поддержки динамического изменения тем.
/// </remarks>
public void ApplyTheme(IDockTheme theme)
{
// Применение темы к контролу
if (theme != null)
{
// TODO: Реализовать применение темы к стилям контрола
}
// TODO: Реализовать применение темы
}
/// <summary>
/// Вызывается при изменении состояния модели для обновления UI.
/// </summary>
/// <param name="propertyName">Имя изменившегося свойства модели.</param>
/// <remarks>
/// Перенаправляет вызов в обработчик изменений модели.
/// </remarks>
public void OnModelPropertyChanged(string propertyName)
{
if (_model != null)
@@ -567,24 +300,84 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <summary>
/// Освобождает ресурсы, используемые этим экземпляром контрола.
/// </summary>
/// <remarks>
/// Выполняет отписку от событий модели, очистку коллекций и освобождение ресурсов.
/// </remarks>
public void Dispose()
{
if (!_disposed)
{
DetachModel();
// Очищаем коллекции
_floatingWindows.Clear();
_autoHidePanels.Clear();
_disposed = true;
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.Media;
using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
namespace Lattice.UI;
/// <summary>
/// Визуальное представление контейнера вкладок с поддержкой нижнего расположения.
/// </summary>
public class LatticeDockLeaf : Control
public sealed class LatticeDockLeaf : Control, IDockLeafControl, IDisposable
{
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()
{
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()
{
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();
}
/// <summary>
/// Настраивает внутреннюю структуру TabView для отображения вкладок снизу.
/// </summary>
private void UpdateTabPlacement()
public void ApplyTheme(IDockTheme theme)
{
var tabView = GetTemplateChild("PART_TabView") as TabView;
if (tabView == null || DataContext is not DockLeaf leaf) return;
// TODO: Реализовать применение темы
}
// Вместо сложной манипуляции с визуальным деревом, используем встроенные свойства TabView
if (leaf.TabPlacement == TabPlacement.Bottom)
public void OnModelPropertyChanged(string propertyName)
{
if (_model != null)
{
// К сожалению, TabView в WinUI не поддерживает TabStripPlacement
// Это ограничение платформы, нужно либо использовать другой контрол,
// либо реализовать кастомный TabControl с поддержкой нижнего расположения
// Временно оставляем как есть с заглушкой
System.Diagnostics.Debug.WriteLine("TabPlacement.Bottom is not fully supported in WinUI TabView");
OnModelPropertyChanged(_model, new PropertyChangedEventArgs(propertyName));
}
}
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.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
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()
{
this.DefaultStyleKey = typeof(LatticeSplitter);
@@ -17,17 +30,116 @@ public class LatticeSplitter : Control
this.PointerEntered += (s, e) =>
this.ProtectedCursor = InputSystemCursor.Create(InputSystemCursorShape.SizeWestEast);
this.PointerExited += (s, e) =>
this.ProtectedCursor = null;
this.ProtectedCursor = InputSystemCursor.Create(InputSystemCursorShape.Arrow);
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)
{
// 1. Находим модель DockGroup через DataContext
if (this.DataContext is not DockGroup group) return;
// 2. Находим родительский Grid, чтобы знать общие размеры
if (VisualTreeHelper.GetParent(this) is not Grid parentGrid ||
parentGrid.ActualWidth <= 0 || parentGrid.ActualHeight <= 0)
return;
@@ -38,14 +150,35 @@ public class LatticeSplitter : Control
if (totalSize <= 0) return;
// 3. Вычисляем изменение Ratio (от -1.0 до 1.0)
double delta = group.Orientation == SplitDirection.Horizontal
? e.Delta.Translation.X
: e.Delta.Translation.Y;
double ratioChange = delta / totalSize;
// 4. Обновляем модель (с ограничением от 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

@@ -13,21 +13,6 @@ using System.Runtime.CompilerServices;
namespace Lattice.UI;
/// <summary>
/// Представляет кастомный контрол вкладок с поддержкой всех позиций размещения панели вкладок.
/// Реализует интерфейс <see cref="IDockLeafControl"/> для интеграции с системой докинга.
/// </summary>
/// <remarks>
/// <para>
/// Контрол обеспечивает отображение коллекции вкладок с возможностью навигации между ними,
/// закрытия вкладок и изменения порядка. Поддерживает все четыре позиции размещения панели
/// вкладок: сверху, снизу, слева и справа.
/// </para>
/// <para>
/// Контрол автоматически синхронизирует свое состояние с моделью данных <see cref="DockLeaf"/>
/// и обеспечивает двустороннюю привязку данных через механизм INotifyPropertyChanged.
/// </para>
/// </remarks>
public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
{
private readonly PropertyChangedEventHandler _modelPropertyChangedHandler;
@@ -44,9 +29,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
private bool _showCloseButtons = true;
private bool _canReorderTabs = true;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="LatticeTabControl"/>.
/// </summary>
public LatticeTabControl()
{
this.DefaultStyleKey = typeof(LatticeTabControl);
@@ -54,7 +36,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
this.DataContextChanged += OnDataContextChanged;
}
/// <inheritdoc/>
public IDockElement? Model
{
get => _model;
@@ -68,7 +49,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <inheritdoc/>
public LayoutManager? LayoutManager
{
get => _layoutManager;
@@ -80,7 +60,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <inheritdoc/>
public IDockContextManager? ContextManager
{
get => _contextManager;
@@ -92,7 +71,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <inheritdoc/>
public bool IsSelected
{
get => _isSelected;
@@ -104,7 +82,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <inheritdoc/>
public bool IsActive
{
get => _isActive;
@@ -116,7 +93,9 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <inheritdoc/>
public bool CanDrag => true;
public bool CanDrop => true;
public TabPlacement TabPlacement
{
get => _tabPlacement;
@@ -131,7 +110,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <inheritdoc/>
public bool ShowCloseButtons
{
get => _showCloseButtons;
@@ -146,7 +124,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <inheritdoc/>
public bool CanReorderTabs
{
get => _canReorderTabs;
@@ -160,7 +137,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <inheritdoc/>
public IDockContent? ActiveContent
{
get => _model?.ActiveContent;
@@ -173,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;
/// <inheritdoc/>
public event EventHandler<ContentClosingEventArgs>? ContentClosing;
/// <inheritdoc/>
public event EventHandler<TabsReorderedEventArgs>? TabsReordered;
/// <inheritdoc/>
public event PropertyChangedEventHandler? PropertyChanged;
/// <inheritdoc/>
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
@@ -203,17 +174,11 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
UpdateTabHeaders();
}
/// <summary>
/// Обрабатывает изменение контекста данных контрола.
/// </summary>
private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
Model = args.NewValue as DockLeaf;
}
/// <summary>
/// Присоединяет модель данных к контролу.
/// </summary>
private void AttachModel()
{
if (_model != null)
@@ -232,9 +197,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <summary>
/// Отсоединяет модель данных от контрола.
/// </summary>
private void DetachModel()
{
if (_model != null)
@@ -250,9 +212,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <summary>
/// Обрабатывает изменения свойств модели данных.
/// </summary>
private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
@@ -274,17 +233,11 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <summary>
/// Обрабатывает изменения коллекции вкладок.
/// </summary>
private void OnChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
UpdateTabHeaders();
}
/// <summary>
/// Обновляет положение панели вкладок.
/// </summary>
private void UpdateTabPlacement()
{
if (_rootGrid == null || _model == null) return;
@@ -322,10 +275,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
UpdateElementPositions();
}
/// <summary>
/// Обновляет ориентацию списка заголовков вкладок.
/// </summary>
/// <param name="orientation">Новая ориентация списка.</param>
private void UpdateHeaderListOrientation(Orientation orientation)
{
if (_tabHeaderList?.ItemsPanelRoot is StackPanel stackPanel)
@@ -334,9 +283,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <summary>
/// Обновляет позиции элементов в сетке.
/// </summary>
private void UpdateElementPositions()
{
if (_rootGrid == null || _tabHeaderList == null || _contentControl == null) return;
@@ -373,9 +319,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <summary>
/// Обновляет заголовки вкладок.
/// </summary>
private void UpdateTabHeaders()
{
if (_tabHeaderList == null || _model == null) return;
@@ -391,11 +334,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
UpdateSelectedTab();
}
/// <summary>
/// Создает элемент заголовка вкладки.
/// </summary>
/// <param name="content">Содержимое вкладки.</param>
/// <returns>Созданный элемент заголовка.</returns>
private ListBoxItem CreateTabHeaderItem(IDockContent content)
{
var item = new ListBoxItem
@@ -411,18 +349,12 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
if (e.GetCurrentPoint(item).Properties.IsLeftButtonPressed)
{
ActiveContent = content;
e.Handled = true;
}
};
return item;
}
/// <summary>
/// Создает содержимое заголовка вкладки.
/// </summary>
/// <param name="content">Содержимое вкладки.</param>
/// <returns>Созданное содержимое заголовка.</returns>
private UIElement CreateTabHeaderContent(IDockContent content)
{
var grid = new Grid();
@@ -455,7 +387,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
closeButton.Click += (sender, e) =>
{
CloseContent(content);
e.Handled = true;
};
Grid.SetColumn(closeButton, 1);
@@ -465,9 +396,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
return grid;
}
/// <summary>
/// Обновляет выбранную вкладку.
/// </summary>
private void UpdateSelectedTab()
{
if (_tabHeaderList == null || _model == null) return;
@@ -486,9 +414,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <summary>
/// Обрабатывает изменение выбора вкладки.
/// </summary>
private void OnTabSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_tabHeaderList?.SelectedItem is ListBoxItem selectedItem &&
@@ -505,7 +430,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <inheritdoc/>
public void AddContent(IDockContent content)
{
if (_model != null && !_model.Children.Contains(content))
@@ -515,7 +439,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <inheritdoc/>
public void RemoveContent(IDockContent content)
{
if (_model != null && _model.Children.Contains(content))
@@ -525,7 +448,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <inheritdoc/>
public bool CloseContent(IDockContent content)
{
var args = new ContentClosingEventArgs(content);
@@ -540,7 +462,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
return false;
}
/// <inheritdoc/>
public void CloseAllExcept(IDockContent exceptContent)
{
if (_model == null) return;
@@ -555,7 +476,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <inheritdoc/>
public void CloseAll()
{
if (_model == null) return;
@@ -567,23 +487,17 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <inheritdoc/>
public void Refresh()
{
UpdateTabHeaders();
UpdateTabPlacement();
}
/// <inheritdoc/>
public void ApplyTheme(IDockTheme theme)
{
if (theme != null)
{
// TODO: Реализовать применение темы к стилям контрола
}
// TODO: Реализовать применение темы
}
/// <inheritdoc/>
public void OnModelPropertyChanged(string propertyName)
{
if (_model != null)
@@ -592,15 +506,11 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
}
}
/// <summary>
/// Вызывает событие изменения свойства.
/// </summary>
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <inheritdoc/>
public void Dispose()
{
if (!_disposed)