DragAndDrop core

This commit is contained in:
FrigaT
2026-01-18 16:33:35 +03:00
parent 9ea82af329
commit 79bdd8bc62
229 changed files with 21214 additions and 2494 deletions

View File

@@ -0,0 +1,368 @@
using Lattice.Core.Docking.Abstractions;
using Lattice.Core.Docking.Engine;
using Lattice.Core.Docking.Models;
using Lattice.UI.Docking.Abstractions;
using Lattice.UI.Docking.Services;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Lattice.UI;
/// <summary>
/// Визуальный контрол для отображения группы разделения (сплиттера).
/// Реализует интерфейс <see cref="IDockGroupControl"/> для интеграции с системой докинга.
/// </summary>
public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
{
private readonly PropertyChangedEventHandler _modelPropertyChangedHandler;
private bool _disposed;
private DockGroup? _model;
private Grid? _rootGrid;
private ContentControl? _firstChildControl;
private ContentControl? _secondChildControl;
private LayoutManager? _layoutManager;
private DockDragDropService? _dragDropService;
private IDockContextManager? _contextManager;
private bool _isSelected;
private bool _isActive;
private bool _canDrag = true;
private bool _canDrop = true;
private double _splitRatio = 0.5;
private double _splitterSize = 4.0;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="LatticeDockGroup"/>.
/// </summary>
public LatticeDockGroup()
{
this.DefaultStyleKey = typeof(LatticeDockGroup);
_modelPropertyChangedHandler = OnModelPropertyChanged;
this.DataContextChanged += OnDataContextChanged;
}
/// <inheritdoc/>
public IDockElement? Model
{
get => _model;
set
{
if (_model == value) return;
DetachModel();
_model = value as DockGroup;
AttachModel();
OnPropertyChanged(nameof(Model));
}
}
/// <inheritdoc/>
public LayoutManager? LayoutManager
{
get => _layoutManager;
set
{
if (_layoutManager == value) return;
_layoutManager = value;
OnPropertyChanged(nameof(LayoutManager));
}
}
/// <inheritdoc/>
public IDockDragDropService? DragDropService
{
get => _dragDropService;
set
{
if (_dragDropService == value) return;
_dragDropService = value;
OnPropertyChanged(nameof(DragDropService));
}
}
/// <inheritdoc/>
public IDockContextManager? ContextManager
{
get => _contextManager;
set
{
if (_contextManager == value) return;
_contextManager = value;
OnPropertyChanged(nameof(ContextManager));
}
}
/// <inheritdoc/>
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected == value) return;
_isSelected = value;
OnPropertyChanged(nameof(IsSelected));
}
}
/// <inheritdoc/>
public bool IsActive
{
get => _isActive;
set
{
if (_isActive == value) return;
_isActive = value;
OnPropertyChanged(nameof(IsActive));
}
}
/// <inheritdoc/>
public bool CanDrag
{
get => _canDrag;
set
{
if (_canDrag == value) return;
_canDrag = value;
OnPropertyChanged(nameof(CanDrag));
}
}
/// <inheritdoc/>
public bool CanDrop
{
get => _canDrop;
set
{
if (_canDrop == value) return;
_canDrop = value;
OnPropertyChanged(nameof(CanDrop));
}
}
/// <inheritdoc/>
public SplitDirection Orientation
{
get => _model?.Orientation ?? SplitDirection.Horizontal;
set
{
if (_model != null && _model.Orientation != value)
{
_model.Orientation = value;
UpdateLayoutDefinitions();
}
}
}
/// <inheritdoc/>
public double SplitRatio
{
get => _splitRatio;
set
{
if (Math.Abs(_splitRatio - value) > 0.001)
{
_splitRatio = value;
UpdateLayoutDefinitions();
OnPropertyChanged(nameof(SplitRatio));
SplitRatioChanged?.Invoke(this,
new SplitRatioChangedEventArgs(value, SplitRatioChangeSource.Programmatic));
}
}
}
/// <inheritdoc/>
public double SplitterSize
{
get => _splitterSize;
set
{
if (Math.Abs(_splitterSize - value) > 0.001)
{
_splitterSize = value;
OnPropertyChanged(nameof(SplitterSize));
}
}
}
/// <inheritdoc/>
public IDockControl? FirstChild => _firstChildControl?.Content as IDockControl;
/// <inheritdoc/>
public IDockControl? SecondChild => _secondChildControl?.Content as IDockControl;
/// <inheritdoc/>
public event EventHandler<SplitRatioChangedEventArgs>? SplitRatioChanged;
/// <inheritdoc/>
public event PropertyChangedEventHandler? PropertyChanged;
/// <inheritdoc/>
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_rootGrid = GetTemplateChild("PART_Grid") as Grid;
_firstChildControl = GetTemplateChild("PART_First") as ContentControl;
_secondChildControl = GetTemplateChild("PART_Second") as ContentControl;
UpdateLayoutDefinitions();
}
private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
Model = args.NewValue as DockGroup;
}
private void AttachModel()
{
if (_model != null)
{
_model.PropertyChanged += _modelPropertyChangedHandler;
this.DataContext = _model;
// Инициализируем свойства из модели
_splitRatio = _model.SplitRatio;
UpdateLayoutDefinitions();
}
}
private void DetachModel()
{
if (_model != null)
{
_model.PropertyChanged -= _modelPropertyChangedHandler;
this.DataContext = null;
}
}
private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(DockGroup.Orientation):
OnPropertyChanged(nameof(Orientation));
UpdateLayoutDefinitions();
break;
case nameof(DockGroup.SplitRatio):
if (_model != null)
{
_splitRatio = _model.SplitRatio;
OnPropertyChanged(nameof(SplitRatio));
UpdateLayoutDefinitions();
}
break;
}
}
private void UpdateLayoutDefinitions()
{
if (_rootGrid == null || _model == null) return;
_rootGrid.ColumnDefinitions.Clear();
_rootGrid.RowDefinitions.Clear();
if (_model.Orientation == SplitDirection.Horizontal)
{
// Горизонтальное разделение
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition
{ Width = new GridLength(_model.SplitRatio, GridUnitType.Star) });
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition
{ Width = GridLength.Auto });
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition
{ Width = new GridLength(1 - _model.SplitRatio, GridUnitType.Star) });
// Устанавливаем позиции элементов
if (_firstChildControl != null)
{
Grid.SetColumn(_firstChildControl, 0);
Grid.SetRow(_firstChildControl, 0);
}
if (_secondChildControl != null)
{
Grid.SetColumn(_secondChildControl, 2);
Grid.SetRow(_secondChildControl, 0);
}
}
else
{
// Вертикальное разделение
_rootGrid.RowDefinitions.Add(new RowDefinition
{ Height = new GridLength(_model.SplitRatio, GridUnitType.Star) });
_rootGrid.RowDefinitions.Add(new RowDefinition
{ Height = GridLength.Auto });
_rootGrid.RowDefinitions.Add(new RowDefinition
{ Height = new GridLength(1 - _model.SplitRatio, GridUnitType.Star) });
// Устанавливаем позиции элементов
if (_firstChildControl != null)
{
Grid.SetRow(_firstChildControl, 0);
Grid.SetColumn(_firstChildControl, 0);
}
if (_secondChildControl != null)
{
Grid.SetRow(_secondChildControl, 2);
Grid.SetColumn(_secondChildControl, 0);
}
}
}
/// <inheritdoc/>
public void SetChildren(IDockControl? firstChild, IDockControl? secondChild)
{
if (_firstChildControl != null)
_firstChildControl.Content = firstChild;
if (_secondChildControl != null)
_secondChildControl.Content = secondChild;
UpdateLayoutDefinitions();
}
/// <inheritdoc/>
public void Refresh()
{
UpdateLayoutDefinitions();
}
/// <inheritdoc/>
public void ApplyTheme(IDockTheme theme)
{
// Применение темы к контролу
if (theme != null)
{
// TODO: Реализовать применение темы к стилям контрола
}
}
/// <inheritdoc/>
public void OnModelPropertyChanged(string propertyName)
{
if (_model != null)
{
OnModelPropertyChanged(_model, new PropertyChangedEventArgs(propertyName));
}
}
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <inheritdoc/>
public void Dispose()
{
if (!_disposed)
{
DetachModel();
_disposed = true;
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,611 @@
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 System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
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;
private readonly ObservableCollection<IFloatingWindowControl> _floatingWindows = new();
private readonly ObservableCollection<IAutoHidePanelControl> _autoHidePanels = new();
private bool _disposed;
private IDockElement? _model;
private LayoutManager? _layoutManager;
private IDockDragDropService? _dragDropService;
private IDockContextManager? _contextManager;
private bool _isSelected;
private bool _isActive;
private bool _canDrag = true;
private bool _canDrop = true;
private bool _showToolbox = true;
private bool _showStatusBar = true;
private bool _showMenu = true;
private ContentControl? _rootContainer;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="LatticeDockHost"/>.
/// </summary>
/// <remarks>
/// Конструктор устанавливает ключ стиля по умолчанию, инициализирует обработчик изменений модели
/// и подписывается на событие изменения контекста данных.
/// </remarks>
public LatticeDockHost()
{
this.DefaultStyleKey = typeof(LatticeDockHost);
_modelPropertyChangedHandler = OnModelPropertyChanged;
this.DataContextChanged += OnDataContextChanged;
}
/// <summary>
/// Получает или задает модель данных, связанную с этим контролом.
/// </summary>
/// <value>
/// Экземпляр, реализующий <see cref="IDockElement"/>, представляющий корневой элемент
/// дерева компоновки. Может быть null.
/// </value>
/// <remarks>
/// Этот элемент является корнем всего макета док-системы. При изменении этого свойства
/// происходит перестройка всего пользовательского интерфейса.
/// </remarks>
public IDockElement? Model
{
get => _model;
set
{
if (_model == value) return;
DetachModel();
_model = value;
AttachModel();
OnPropertyChanged(nameof(Model));
}
}
/// <summary>
/// Получает или задает менеджер макета, к которому принадлежит этот контрол.
/// </summary>
/// <value>
/// Экземпляр <see cref="LayoutManager"/>, управляющий структурой док-системы.
/// </value>
/// <remarks>
/// Менеджер макета используется для выполнения операций с деревом компоновки
/// и координации изменений между различными элементами системы.
/// </remarks>
public LayoutManager? LayoutManager
{
get => _layoutManager;
set
{
if (_layoutManager == value) return;
_layoutManager = value;
OnPropertyChanged(nameof(LayoutManager));
}
}
/// <summary>
/// Получает или задает сервис перетаскивания, используемый этим контролом.
/// </summary>
/// <value>
/// Реализация <see cref="IDockDragDropService"/> для обработки операций перетаскивания.
/// </value>
/// <remarks>
/// Сервис перетаскивания обеспечивает взаимодействие с системой drag-and-drop,
/// включая визуальную обратную связь и обработку событий.
/// </remarks>
public IDockDragDropService? DragDropService
{
get => _dragDropService;
set
{
if (_dragDropService == value) return;
_dragDropService = value;
OnPropertyChanged(nameof(DragDropService));
}
}
/// <summary>
/// Получает или задает контекстный менеджер для этого контрола.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDockContextManager"/>, управляющий контекстными меню и действиями.
/// </value>
/// <remarks>
/// Контекстный менеджер используется для отображения меню, связанных с этим элементом,
/// и выполнения команд, доступных в текущем контексте.
/// </remarks>
public IDockContextManager? ContextManager
{
get => _contextManager;
set
{
if (_contextManager == value) return;
_contextManager = value;
OnPropertyChanged(nameof(ContextManager));
}
}
/// <summary>
/// Получает или задает признак того, что контрол выбран.
/// </summary>
/// <value>
/// true, если контрол выбран; в противном случае — false.
/// </value>
/// <remarks>
/// Выделение контрола обычно визуально выделяет его границы или фон,
/// чтобы указать пользователю на активный элемент.
/// </remarks>
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected == value) return;
_isSelected = value;
OnPropertyChanged(nameof(IsSelected));
}
}
/// <summary>
/// Получает или задает признак того, что контрол активен.
/// </summary>
/// <value>
/// true, если контрол активен; в противном случае — false.
/// </value>
/// <remarks>
/// Активный контрол обычно получает фокус ввода и может обрабатывать команды клавиатуры.
/// </remarks>
public bool IsActive
{
get => _isActive;
set
{
if (_isActive == value) return;
_isActive = value;
OnPropertyChanged(nameof(IsActive));
}
}
/// <summary>
/// Получает или задает признак того, что контрол можно перетаскивать.
/// </summary>
/// <value>
/// true, если контрол можно перетаскивать; в противном случае — false.
/// </value>
/// <remarks>
/// Этот флаг влияет на возможность инициирования операции перетаскивания
/// при взаимодействии пользователя с этим контролом.
/// </remarks>
public bool CanDrag
{
get => _canDrag;
set
{
if (_canDrag == value) return;
_canDrag = value;
OnPropertyChanged(nameof(CanDrag));
}
}
/// <summary>
/// Получает или задает признак того, что контрол может принимать сброс.
/// </summary>
/// <value>
/// true, если контрол может принимать сброс; в противном случае — false.
/// </value>
/// <remarks>
/// Этот флаг влияет на возможность завершения операции перетаскивания
/// сбросом данных на этот контрол.
/// </remarks>
public bool CanDrop
{
get => _canDrop;
set
{
if (_canDrop == value) return;
_canDrop = value;
OnPropertyChanged(nameof(CanDrop));
}
}
/// <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;
set
{
if (_showToolbox == value) return;
_showToolbox = value;
OnPropertyChanged(nameof(ShowToolbox));
}
}
/// <summary>
/// Получает или задает значение, указывающее, отображается ли строка состояния.
/// </summary>
/// <value>
/// true, если строка состояния видима; в противном случае — false.
/// </value>
/// <remarks>
/// Строка состояния обычно отображает текущий статус приложения,
/// информацию о выбранном элементе или прогресс выполнения операций.
/// </remarks>
public bool ShowStatusBar
{
get => _showStatusBar;
set
{
if (_showStatusBar == value) return;
_showStatusBar = value;
OnPropertyChanged(nameof(ShowStatusBar));
}
}
/// <summary>
/// Получает или задает значение, указывающее, отображается ли главное меню приложения.
/// </summary>
/// <value>
/// true, если главное меню видимо; в противном случае — false.
/// </value>
/// <remarks>
/// Главное меню содержит основные команды приложения, организованные в иерархическую структуру.
/// </remarks>
public bool ShowMenu
{
get => _showMenu;
set
{
if (_showMenu == value) return;
_showMenu = value;
OnPropertyChanged(nameof(ShowMenu));
}
}
/// <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>
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_rootContainer = GetTemplateChild("PART_RootContainer") as ContentControl;
UpdateRootContent();
}
private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
Model = args.NewValue as IDockElement;
}
private void AttachModel()
{
if (_model != null && _layoutManager != null)
{
// Подписываемся на события менеджера макета
_layoutManager.LayoutUpdated += OnLayoutUpdated;
_layoutManager.AutoHidePanelsChanged += OnAutoHidePanelsChanged;
// Устанавливаем DataContext
this.DataContext = _model;
UpdateRootContent();
}
}
private void DetachModel()
{
if (_model != null && _layoutManager != null)
{
// Отписываемся от событий
_layoutManager.LayoutUpdated -= OnLayoutUpdated;
_layoutManager.AutoHidePanelsChanged -= OnAutoHidePanelsChanged;
this.DataContext = null;
}
}
private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
// Обработка изменений модели
OnPropertyChanged(e.PropertyName);
}
private void OnLayoutUpdated()
{
UpdateRootContent();
LayoutChanged?.Invoke(this, EventArgs.Empty);
}
private void OnAutoHidePanelsChanged(object? sender, EventArgs e)
{
// Обновление автоскрываемых панелей
OnPropertyChanged(nameof(AutoHidePanels));
}
private void UpdateRootContent()
{
if (_rootContainer != null && _model != null && _layoutManager != null)
{
// Создаем дерево контролов через фабрику
var factory = LatticeUIFramework.ControlFactory;
if (factory != null)
{
var control = factory.CreateControlForElement(_model);
_rootContainer.Content = control;
}
}
}
/// <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();
}
/// <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 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();
}
/// <summary>
/// Обновляет внешний вид контрола в соответствии с текущим состоянием модели.
/// </summary>
/// <remarks>
/// Вызывает обновление корневого содержимого и всех дочерних элементов.
/// </remarks>
public void Refresh()
{
UpdateRootContent();
}
/// <summary>
/// Применяет указанную тему к контролу.
/// </summary>
/// <param name="theme">Тема для применения.</param>
/// <remarks>
/// В текущей реализации метод является заглушкой и должен быть расширен
/// для поддержки динамического изменения тем.
/// </remarks>
public void ApplyTheme(IDockTheme theme)
{
// Применение темы к контролу
if (theme != null)
{
// TODO: Реализовать применение темы к стилям контрола
}
}
/// <summary>
/// Вызывается при изменении состояния модели для обновления UI.
/// </summary>
/// <param name="propertyName">Имя изменившегося свойства модели.</param>
/// <remarks>
/// Перенаправляет вызов в обработчик изменений модели.
/// </remarks>
public void OnModelPropertyChanged(string propertyName)
{
if (_model != null)
{
OnModelPropertyChanged(_model, new PropertyChangedEventArgs(propertyName));
}
}
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
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);
}
}
}

View File

@@ -0,0 +1,40 @@
using Lattice.Core.Docking.Models;
using Microsoft.UI.Xaml.Controls;
namespace Lattice.UI;
/// <summary>
/// Визуальное представление контейнера вкладок с поддержкой нижнего расположения.
/// </summary>
public class LatticeDockLeaf : Control
{
public LatticeDockLeaf()
{
this.DefaultStyleKey = typeof(LatticeDockLeaf);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
UpdateTabPlacement();
}
/// <summary>
/// Настраивает внутреннюю структуру TabView для отображения вкладок снизу.
/// </summary>
private void UpdateTabPlacement()
{
var tabView = GetTemplateChild("PART_TabView") as TabView;
if (tabView == null || DataContext is not DockLeaf leaf) return;
// Вместо сложной манипуляции с визуальным деревом, используем встроенные свойства TabView
if (leaf.TabPlacement == TabPlacement.Bottom)
{
// К сожалению, TabView в WinUI не поддерживает TabStripPlacement
// Это ограничение платформы, нужно либо использовать другой контрол,
// либо реализовать кастомный TabControl с поддержкой нижнего расположения
// Временно оставляем как есть с заглушкой
System.Diagnostics.Debug.WriteLine("TabPlacement.Bottom is not fully supported in WinUI TabView");
}
}
}

View File

@@ -0,0 +1,51 @@
using Lattice.Core.Docking.Models;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using System;
namespace Lattice.UI;
public class LatticeSplitter : Control
{
public LatticeSplitter()
{
this.DefaultStyleKey = typeof(LatticeSplitter);
this.ManipulationMode = ManipulationModes.TranslateX | ManipulationModes.TranslateY;
this.PointerEntered += (s, e) =>
this.ProtectedCursor = InputSystemCursor.Create(InputSystemCursorShape.SizeWestEast);
this.PointerExited += (s, e) =>
this.ProtectedCursor = null;
this.ManipulationDelta += OnManipulationDelta;
}
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;
double totalSize = group.Orientation == SplitDirection.Horizontal
? parentGrid.ActualWidth
: parentGrid.ActualHeight;
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);
}
}

View File

@@ -0,0 +1,688 @@
using Lattice.Core.Docking.Abstractions;
using Lattice.Core.Docking.Engine;
using Lattice.Core.Docking.Models;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
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>
/// Кастомный контрол вкладок с поддержкой всех позиций размещения панели вкладок.
/// Реализует интерфейс <see cref="IDockLeafControl"/> для интеграции с системой докинга.
/// </summary>
public sealed class LatticeTabControl : 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 IDockDragDropService? _dragDropService;
private IDockContextManager? _contextManager;
private bool _isSelected;
private bool _isActive;
private bool _canDrag = true;
private bool _canDrop = true;
private bool _showCloseButtons = true;
private bool _canReorderTabs = true;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="LatticeTabControl"/>.
/// </summary>
public LatticeTabControl()
{
this.DefaultStyleKey = typeof(LatticeTabControl);
_modelPropertyChangedHandler = OnModelPropertyChanged;
this.DataContextChanged += OnDataContextChanged;
// Подписываемся на события
this.PointerPressed += OnPointerPressed;
this.PointerMoved += OnPointerMoved;
this.PointerReleased += OnPointerReleased;
}
/// <inheritdoc/>
public IDockElement? Model
{
get => _model;
set
{
if (_model == value) return;
DetachModel();
_model = value as DockLeaf;
AttachModel();
OnPropertyChanged(nameof(Model));
}
}
/// <inheritdoc/>
public LayoutManager? LayoutManager
{
get => _layoutManager;
set
{
if (_layoutManager == value) return;
_layoutManager = value;
OnPropertyChanged(nameof(LayoutManager));
}
}
/// <inheritdoc/>
public IDockDragDropService? DragDropService
{
get => _dragDropService;
set
{
if (_dragDropService == value) return;
_dragDropService = value;
OnPropertyChanged(nameof(DragDropService));
}
}
/// <inheritdoc/>
public IDockContextManager? ContextManager
{
get => _contextManager;
set
{
if (_contextManager == value) return;
_contextManager = value;
OnPropertyChanged(nameof(ContextManager));
}
}
/// <inheritdoc/>
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected == value) return;
_isSelected = value;
OnPropertyChanged(nameof(IsSelected));
}
}
/// <inheritdoc/>
public bool IsActive
{
get => _isActive;
set
{
if (_isActive == value) return;
_isActive = value;
OnPropertyChanged(nameof(IsActive));
}
}
/// <inheritdoc/>
public bool CanDrag
{
get => _canDrag;
set
{
if (_canDrag == value) return;
_canDrag = value;
OnPropertyChanged(nameof(CanDrag));
}
}
/// <inheritdoc/>
public bool CanDrop
{
get => _canDrop;
set
{
if (_canDrop == value) return;
_canDrop = value;
OnPropertyChanged(nameof(CanDrop));
}
}
/// <inheritdoc/>
public TabPlacement TabPlacement
{
get => _model?.TabPlacement ?? TabPlacement.Top;
set
{
if (_model != null && _model.TabPlacement != value)
{
_model.TabPlacement = value;
UpdateTabPlacement();
}
}
}
/// <inheritdoc/>
public bool ShowCloseButtons
{
get => _showCloseButtons;
set
{
if (_showCloseButtons == value) return;
_showCloseButtons = value;
OnPropertyChanged(nameof(ShowCloseButtons));
UpdateTabHeaders();
}
}
/// <inheritdoc/>
public bool CanReorderTabs
{
get => _canReorderTabs;
set
{
if (_canReorderTabs == value) return;
_canReorderTabs = value;
OnPropertyChanged(nameof(CanReorderTabs));
}
}
/// <inheritdoc/>
public IDockContent? ActiveContent
{
get => _model?.ActiveContent;
set
{
if (_model != null)
{
_model.ActiveContent = value;
}
}
}
/// <inheritdoc/>
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();
_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;
}
// Устанавливаем DataContext для привязки в XAML
this.DataContext = _model;
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):
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();
// Настраиваем Grid в зависимости от позиции вкладок
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 != null && _tabHeaderList.ItemsPanelRoot is ItemsPanelTemplate panelTemplate)
{
if (panelTemplate.LoadContent() 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;
e.Handled = true;
}
};
// Обработка клика на кнопке закрытия
if (_showCloseButtons && content.CanClose)
{
// Добавляем контекстное меню
item.ContextRequested += (sender, e) =>
{
ShowTabContextMenu(item, content, e);
};
}
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);
e.Handled = true;
};
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));
}
}
}
// Drag-and-Drop для переупорядочивания вкладок
private ListBoxItem? _draggedItem;
private Point _dragStartPoint;
private void OnPointerPressed(object sender, PointerRoutedEventArgs e)
{
if (!_canReorderTabs) return;
var pointerPoint = e.GetCurrentPoint(this);
_dragStartPoint = new Point(pointerPoint.Position.X, pointerPoint.Position.Y);
// Находим элемент под курсором
var element = VisualTreeHelper.FindElementsInHostCoordinates(
pointerPoint.Position, this).FirstOrDefault();
if (element is ListBoxItem listBoxItem)
{
_draggedItem = listBoxItem;
}
}
private void OnPointerMoved(object sender, PointerRoutedEventArgs e)
{
if (_draggedItem == null || !_canReorderTabs) return;
var pointerPoint = e.GetCurrentPoint(this);
var currentPoint = new Point(pointerPoint.Position.X, pointerPoint.Position.Y);
// Проверяем, достаточно ли переместили для начала перетаскивания
var distance = Math.Sqrt(
Math.Pow(currentPoint.X - _dragStartPoint.X, 2) +
Math.Pow(currentPoint.Y - _dragStartPoint.Y, 2));
if (distance > 10 && _draggedItem.Tag is IDockContent content)
{
// Начинаем операцию перетаскивания
StartTabDrag(_draggedItem, content);
_draggedItem = null;
}
}
private void OnPointerReleased(object sender, PointerRoutedEventArgs e)
{
_draggedItem = null;
}
private void StartTabDrag(ListBoxItem item, IDockContent content)
{
// TODO: Реализовать перетаскивание вкладок
// Для этого нужно использовать IDockDragDropService
}
private void ShowTabContextMenu(ListBoxItem item, IDockContent content, ContextRequestedEventArgs e)
{
if (_contextManager == null) return;
var position = e.GetPosition(this);
_contextManager.ShowContextMenu(this, position.X, position.Y);
}
/// <inheritdoc/>
public void AddContent(IDockContent content)
{
if (_model != null && !_model.Children.Contains(content))
{
_model.AddContent(content);
UpdateTabHeaders();
}
}
/// <inheritdoc/>
public void RemoveContent(IDockContent content)
{
if (_model != null && _model.Children.Contains(content))
{
_model.RemoveContent(content);
UpdateTabHeaders();
}
}
/// <inheritdoc/>
public bool CloseContent(IDockContent content)
{
var args = new ContentClosingEventArgs(content);
ContentClosing?.Invoke(this, args);
if (!args.Cancel)
{
RemoveContent(content);
return true;
}
return false;
}
/// <inheritdoc/>
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);
}
}
/// <inheritdoc/>
public void CloseAll()
{
if (_model == null) return;
var itemsToClose = _model.Children.ToList();
foreach (var content in itemsToClose)
{
CloseContent(content);
}
}
/// <inheritdoc/>
public void Refresh()
{
UpdateTabHeaders();
UpdateTabPlacement();
}
/// <inheritdoc/>
public void ApplyTheme(IDockTheme theme)
{
// Применение темы к элементу
if (theme != null)
{
// TODO: Применить тему к стилям контрола
}
}
/// <inheritdoc/>
public void OnModelPropertyChanged(string propertyName)
{
if (_model != null)
{
OnModelPropertyChanged(_model, new PropertyChangedEventArgs(propertyName));
}
}
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <inheritdoc/>
public void Dispose()
{
if (!_disposed)
{
DetachModel();
if (_tabHeaderList != null)
{
_tabHeaderList.SelectionChanged -= OnTabSelectionChanged;
}
// Отписываемся от событий указателя
this.PointerPressed -= OnPointerPressed;
this.PointerMoved -= OnPointerMoved;
this.PointerReleased -= OnPointerReleased;
_disposed = true;
GC.SuppressFinalize(this);
}
}
}