DragAndDrop core
This commit is contained in:
368
Lattice.UI.Docking.WinUI/Controls/LatticeDockGroup.cs
Normal file
368
Lattice.UI.Docking.WinUI/Controls/LatticeDockGroup.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
611
Lattice.UI.Docking.WinUI/Controls/LatticeDockHost.cs
Normal file
611
Lattice.UI.Docking.WinUI/Controls/LatticeDockHost.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
40
Lattice.UI.Docking.WinUI/Controls/LatticeDockLeaf.cs
Normal file
40
Lattice.UI.Docking.WinUI/Controls/LatticeDockLeaf.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
51
Lattice.UI.Docking.WinUI/Controls/LatticeSplitter.cs
Normal file
51
Lattice.UI.Docking.WinUI/Controls/LatticeSplitter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
688
Lattice.UI.Docking.WinUI/Controls/LatticeTabControl.cs
Normal file
688
Lattice.UI.Docking.WinUI/Controls/LatticeTabControl.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user