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);
|
||||
}
|
||||
}
|
||||
}
|
||||
27
Lattice.UI.Docking.WinUI/Converters/DockTemplateSelector.cs
Normal file
27
Lattice.UI.Docking.WinUI/Converters/DockTemplateSelector.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Lattice.Core.Docking.Models;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Lattice.UI.Docking.WinUI.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// Выбирает визуальный шаблон для узла дерева макета.
|
||||
/// </summary>
|
||||
public class DockTemplateSelector : DataTemplateSelector
|
||||
{
|
||||
/// <summary> Шаблон для узлов-разделителей (DockGroup). </summary>
|
||||
public DataTemplate? GroupTemplate { get; set; }
|
||||
|
||||
/// <summary> Шаблон для контейнеров вкладок (DockLeaf). </summary>
|
||||
public DataTemplate? LeafTemplate { get; set; }
|
||||
|
||||
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject container)
|
||||
{
|
||||
return item switch
|
||||
{
|
||||
DockGroup => GroupTemplate,
|
||||
DockLeaf => LeafTemplate,
|
||||
_ => base.SelectTemplateCore(item, container)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using Lattice.Core.Docking.Abstractions;
|
||||
using Lattice.Core.Docking.Models;
|
||||
using Lattice.UI.Docking.Abstractions;
|
||||
using Lattice.UI.Docking.Factories;
|
||||
using Microsoft.UI.Xaml;
|
||||
using System;
|
||||
|
||||
namespace Lattice.UI.Docking.WinUI.Factories;
|
||||
|
||||
/// <summary>
|
||||
/// Фабрика контролов для платформы WinUI.
|
||||
/// Создает UI-элементы для отображения компонентов системы докинга.
|
||||
/// </summary>
|
||||
public sealed class WinUIDockControlFactory : DockControlFactoryBase, IDockControlFactory
|
||||
{
|
||||
private readonly IDockTheme _theme;
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый экземпляр фабрики WinUI.
|
||||
/// </summary>
|
||||
/// <param name="theme">Тема оформления.</param>
|
||||
public WinUIDockControlFactory(IDockTheme theme)
|
||||
{
|
||||
_theme = theme ?? throw new ArgumentNullException(nameof(theme));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IDockGroupControl CreateGroupControl(DockGroup group)
|
||||
{
|
||||
var control = new LatticeDockGroup();
|
||||
ConfigureControl(control, group);
|
||||
control.ApplyTheme(_theme);
|
||||
return control;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IDockLeafControl CreateLeafControl(DockLeaf leaf)
|
||||
{
|
||||
var control = new LatticeTabControl();
|
||||
ConfigureControl(control, leaf);
|
||||
control.ApplyTheme(_theme);
|
||||
return control;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IFloatingWindowControl CreateFloatingWindowControl(DockWindow window)
|
||||
{
|
||||
// TODO: Реализовать создание плавающего окна
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IAutoHidePanelControl CreateAutoHidePanelControl(AutoHidePanel panel)
|
||||
{
|
||||
// TODO: Реализовать создание автоскрываемой панели
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IDockSplitterControl CreateSplitterControl(SplitDirection orientation)
|
||||
{
|
||||
var control = new LatticeSplitter
|
||||
{
|
||||
Orientation = orientation
|
||||
};
|
||||
ConfigureControl(control);
|
||||
control.ApplyTheme(_theme);
|
||||
return control;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создает хост для размещения системы докинга.
|
||||
/// </summary>
|
||||
public IDockHost CreateDockHost()
|
||||
{
|
||||
var host = new LatticeDockHost();
|
||||
ConfigureControl(host);
|
||||
host.ApplyTheme(_theme);
|
||||
return host;
|
||||
}
|
||||
|
||||
private void ConfigureControl(IDockControl control, IDockElement? model = null)
|
||||
{
|
||||
if (control == null) return;
|
||||
|
||||
control.Model = model;
|
||||
control.LayoutManager = LatticeUIFramework.LayoutManager;
|
||||
control.DragDropService = LatticeUIFramework.DragDropService;
|
||||
control.ContextManager = LatticeUIFramework.ContextManager;
|
||||
|
||||
if (control is FrameworkElement frameworkElement && model != null)
|
||||
{
|
||||
frameworkElement.DataContext = model;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Lattice.UI.Docking.WinUI/Lattice.UI.Docking.WinUI.csproj
Normal file
22
Lattice.UI.Docking.WinUI/Lattice.UI.Docking.WinUI.csproj
Normal file
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0-windows10.0.19041.0;net9.0-windows10.0.19041.0;net10.0-windows10.0.19041.0</TargetFrameworks>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<RootNamespace>Lattice.UI.Docking.WinUI</RootNamespace>
|
||||
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<WinUISDKReferences>false</WinUISDKReferences>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Lattice.Core.Docking\Lattice.Core.Docking.csproj" />
|
||||
<ProjectReference Include="..\Lattice.Themes.Core\Lattice.Themes.Core.csproj" />
|
||||
<ProjectReference Include="..\Lattice.UI.Docking\Lattice.UI.Docking.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
116
Lattice.UI.Docking.WinUI/Services/WinUIDockContextManager.cs
Normal file
116
Lattice.UI.Docking.WinUI/Services/WinUIDockContextManager.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using Lattice.UI.Docking.Abstractions;
|
||||
using Lattice.UI.Docking.Services;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Lattice.UI.Docking.WinUI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Реализация менеджера контекстных меню для WinUI.
|
||||
/// </summary>
|
||||
public sealed class WinUIDockContextManager : DockContextManagerBase, IDisposable
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, IDockCommand> _commands = new();
|
||||
private MenuFlyout? _currentFlyout;
|
||||
private IDockControl? _currentContextTarget;
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый экземпляр менеджера контекстных меню.
|
||||
/// </summary>
|
||||
public WinUIDockContextManager()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ShowContextMenu(IDockControl element, double x, double y)
|
||||
{
|
||||
if (element is not FrameworkElement uiElement) return;
|
||||
|
||||
// Создаем контекстное меню
|
||||
var flyout = new MenuFlyout();
|
||||
|
||||
// Получаем команды для элемента
|
||||
var commands = GetCommandsForElement(element);
|
||||
|
||||
foreach (var command in commands)
|
||||
{
|
||||
var item = new MenuFlyoutItem
|
||||
{
|
||||
Text = command.Name,
|
||||
Command = new RelayCommand(() => ExecuteCommand(command, element))
|
||||
};
|
||||
|
||||
// Добавляем иконку, если есть
|
||||
if (!string.IsNullOrEmpty(command.Icon))
|
||||
{
|
||||
// TODO: Добавить иконку команды
|
||||
}
|
||||
|
||||
flyout.Items.Add(item);
|
||||
}
|
||||
|
||||
// Если команд нет, не показываем меню
|
||||
if (flyout.Items.Count == 0) return;
|
||||
|
||||
// Закрываем предыдущее меню, если оно открыто
|
||||
HideContextMenu();
|
||||
|
||||
// Сохраняем ссылки
|
||||
_currentFlyout = flyout;
|
||||
_currentContextTarget = element;
|
||||
|
||||
// Показываем меню
|
||||
flyout.ShowAt(uiElement, new Windows.Foundation.Point(x, y));
|
||||
|
||||
// Вызываем событие
|
||||
OnContextMenuShown(element, x, y);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void HideContextMenu()
|
||||
{
|
||||
if (_currentFlyout != null)
|
||||
{
|
||||
_currentFlyout.Hide();
|
||||
_currentFlyout = null;
|
||||
}
|
||||
|
||||
if (_currentContextTarget != null)
|
||||
{
|
||||
OnContextMenuHidden();
|
||||
_currentContextTarget = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Класс-заглушка для реализации ICommand.
|
||||
/// </summary>
|
||||
private sealed class RelayCommand : System.Windows.Input.ICommand
|
||||
{
|
||||
private readonly Action _execute;
|
||||
private readonly Func<bool>? _canExecute;
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public RelayCommand(Action execute, Func<bool>? canExecute = null)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
|
||||
|
||||
public void Execute(object? parameter) => _execute();
|
||||
|
||||
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
HideContextMenu();
|
||||
_commands.Clear();
|
||||
}
|
||||
}
|
||||
167
Lattice.UI.Docking.WinUI/Services/WinUIDockUIService.cs
Normal file
167
Lattice.UI.Docking.WinUI/Services/WinUIDockUIService.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
using Lattice.UI.Docking.Abstractions;
|
||||
using Lattice.UI.Docking.Services;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Lattice.UI.Docking.WinUI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Реализация UI-сервиса для WinUI.
|
||||
/// </summary>
|
||||
public sealed class WinUIDockUIService : DockUIServiceBase
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override object CreateMainWindow(IDockHost host)
|
||||
{
|
||||
if (host is not FrameworkElement hostElement)
|
||||
throw new ArgumentException("Host must be a FrameworkElement", nameof(host));
|
||||
|
||||
var window = new Window();
|
||||
window.Content = hostElement;
|
||||
window.AppWindow.Title = "Lattice IDE";
|
||||
|
||||
// Регистрируем окно в трекере
|
||||
Themes.WindowTracker.Register(window);
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool? ShowDialog(string title, object content)
|
||||
{
|
||||
if (content is not FrameworkElement contentElement)
|
||||
return null;
|
||||
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
Title = title,
|
||||
Content = contentElement,
|
||||
PrimaryButtonText = "OK",
|
||||
CloseButtonText = "Cancel",
|
||||
XamlRoot = GetActiveXamlRoot()
|
||||
};
|
||||
|
||||
// Показываем диалог и возвращаем результат
|
||||
var result = dialog.ShowAsync();
|
||||
return result.GetAwaiter().GetResult() switch
|
||||
{
|
||||
ContentDialogResult.Primary => true,
|
||||
ContentDialogResult.Secondary => false,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ShowMessage(string message, string caption)
|
||||
{
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
Title = caption,
|
||||
Content = new TextBlock { Text = message },
|
||||
PrimaryButtonText = "OK",
|
||||
XamlRoot = GetActiveXamlRoot()
|
||||
};
|
||||
|
||||
dialog.ShowAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Confirm(string message, string caption)
|
||||
{
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
Title = caption,
|
||||
Content = new TextBlock { Text = message },
|
||||
PrimaryButtonText = "Yes",
|
||||
SecondaryButtonText = "No",
|
||||
XamlRoot = GetActiveXamlRoot()
|
||||
};
|
||||
|
||||
var result = dialog.ShowAsync().GetAwaiter().GetResult();
|
||||
return result == ContentDialogResult.Primary;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string? Prompt(string prompt, string? defaultValue = null)
|
||||
{
|
||||
var textBox = new TextBox
|
||||
{
|
||||
Text = defaultValue ?? string.Empty,
|
||||
Width = 300,
|
||||
Height = 32
|
||||
};
|
||||
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
Title = prompt,
|
||||
Content = textBox,
|
||||
PrimaryButtonText = "OK",
|
||||
SecondaryButtonText = "Cancel",
|
||||
XamlRoot = GetActiveXamlRoot()
|
||||
};
|
||||
|
||||
var result = dialog.ShowAsync().GetAwaiter().GetResult();
|
||||
return result == ContentDialogResult.Primary ? textBox.Text : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void InvokeOnUIThread(Action action)
|
||||
{
|
||||
if (action == null) return;
|
||||
|
||||
var dispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread();
|
||||
if (dispatcherQueue.HasThreadAccess)
|
||||
{
|
||||
action();
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatcherQueue.TryEnqueue(() => action());
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task InvokeOnUIThreadAsync(Func<Task> action)
|
||||
{
|
||||
if (action == null) return;
|
||||
|
||||
var dispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread();
|
||||
if (dispatcherQueue.HasThreadAccess)
|
||||
{
|
||||
await action();
|
||||
}
|
||||
else
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
dispatcherQueue.TryEnqueue(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await action();
|
||||
tcs.SetResult(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.SetException(ex);
|
||||
}
|
||||
});
|
||||
await tcs.Task;
|
||||
}
|
||||
}
|
||||
|
||||
private XamlRoot? GetActiveXamlRoot()
|
||||
{
|
||||
// Получаем XamlRoot из активного окна
|
||||
foreach (var window in Themes.WindowTracker.Windows)
|
||||
{
|
||||
if (window.Content is FrameworkElement element)
|
||||
{
|
||||
return element.XamlRoot;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
534
Lattice.UI.Docking.WinUI/Services/WinUIDragDropService.cs
Normal file
534
Lattice.UI.Docking.WinUI/Services/WinUIDragDropService.cs
Normal file
@@ -0,0 +1,534 @@
|
||||
using Lattice.Core.DragDrop.Services;
|
||||
using Lattice.Core.Geometry;
|
||||
using Lattice.UI.Docking.Abstractions;
|
||||
using Lattice.UI.Docking.Models;
|
||||
using Lattice.UI.Docking.Services;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Lattice.UI.Docking.WinUI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Предоставляет реализацию сервиса перетаскивания для платформы WinUI с расширенной
|
||||
/// поддержкой визуальных эффектов и интеграцией с системой докинга Lattice.
|
||||
/// Координирует взаимодействие между базовым менеджером перетаскивания и UI-контролами,
|
||||
/// обеспечивая богатую визуальную обратную связь во время операций drag-and-drop.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="WinUIDragDropService"/> расширяет базовый функционал <see cref="DockDragDropService"/>
|
||||
/// платформенно-зависимыми визуальными эффектами, включая:
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item>Прозрачное визуальное представление перетаскиваемого элемента</item>
|
||||
/// <item>Интерактивные подсказки областей сброса</item>
|
||||
/// <item>Анимации при начале и завершении перетаскивания</item>
|
||||
/// <item>Подсветку допустимых целей сброса</item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// Сервис поддерживает регистрацию UI-элементов и автоматически вычисляет их границы
|
||||
/// для точного определения целей сброса.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class WinUIDragDropService : DockDragDropService, IDisposable
|
||||
{
|
||||
private readonly ConcurrentDictionary<IDockControl, FrameworkElement> _controlToElement = new();
|
||||
private readonly DragDropManagerEx _dragDropManager;
|
||||
private Popup? _dragVisualPopup;
|
||||
private Border? _dragVisual;
|
||||
private DropHintOverlay? _dropHintOverlay;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый экземпляр сервиса перетаскивания WinUI.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Создает внутренний менеджер перетаскивания, инициализирует визуальные элементы
|
||||
/// и подписывается на события менеджера для обработки операций перетаскивания.
|
||||
/// </remarks>
|
||||
public WinUIDragDropService()
|
||||
{
|
||||
_dragDropManager = new DragDropManagerEx();
|
||||
HookEvents();
|
||||
InitializeDragVisual();
|
||||
InitializeDropHintOverlay();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый экземпляр с указанным менеджером перетаскивания.
|
||||
/// </summary>
|
||||
/// <param name="dragDropManager">
|
||||
/// Предварительно настроенный менеджер перетаскивания.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, если <paramref name="dragDropManager"/> равен null.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// Позволяет использовать кастомную конфигурацию менеджера перетаскивания
|
||||
/// при сохранении всех визуальных эффектов WinUI.
|
||||
/// </remarks>
|
||||
public WinUIDragDropService(DragDropManagerEx dragDropManager)
|
||||
{
|
||||
_dragDropManager = dragDropManager ?? throw new ArgumentNullException(nameof(dragDropManager));
|
||||
HookEvents();
|
||||
InitializeDragVisual();
|
||||
InitializeDropHintOverlay();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Подписывается на события менеджера перетаскивания.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Обрабатывает следующие события:
|
||||
/// <list type="bullet">
|
||||
/// <item>Начало перетаскивания</item>
|
||||
/// <item>Обновление позиции перетаскивания</item>
|
||||
/// <item>Завершение перетаскивания</item>
|
||||
/// <item>Отмена перетаскивания</item>
|
||||
/// <item>Изменение цели сброса</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
private void HookEvents()
|
||||
{
|
||||
_dragDropManager.DragStarted += OnDragStarted;
|
||||
_dragDropManager.DragUpdated += OnDragUpdated;
|
||||
_dragDropManager.DragCompleted += OnDragCompleted;
|
||||
_dragDropManager.DragCancelled += OnDragCancelled;
|
||||
_dragDropManager.DropTargetChanged += OnDropTargetChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует визуальное представление перетаскиваемого элемента.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Создает Popup с Border для отображения полупрозрачной копии
|
||||
/// перетаскиваемого элемента во время операции drag-and-drop.
|
||||
/// </remarks>
|
||||
private void InitializeDragVisual()
|
||||
{
|
||||
// Создаем Popup для отображения визуального представления перетаскивания
|
||||
_dragVisualPopup = new Popup
|
||||
{
|
||||
IsHitTestVisible = false,
|
||||
IsLightDismissEnabled = false,
|
||||
Child = null
|
||||
};
|
||||
|
||||
// Создаем визуальный элемент для перетаскивания
|
||||
_dragVisual = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Microsoft.UI.Colors.Transparent),
|
||||
BorderBrush = new SolidColorBrush(Microsoft.UI.Colors.DodgerBlue),
|
||||
BorderThickness = new Thickness(2),
|
||||
CornerRadius = new CornerRadius(4),
|
||||
Opacity = 0.7
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует оверлей для отображения подсказок при сбросе.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Добавляет оверлей в корневой контейнер приложения для отображения
|
||||
/// визуальных подсказок о возможных позициях сброса.
|
||||
/// </remarks>
|
||||
private void InitializeDropHintOverlay()
|
||||
{
|
||||
// Создаем оверлей для подсказок при сбросе
|
||||
_dropHintOverlay = new DropHintOverlay();
|
||||
|
||||
// Добавляем оверлей в корневой контейнер приложения
|
||||
if (Window.Current?.Content is Panel rootPanel)
|
||||
{
|
||||
rootPanel.Children.Add(_dropHintOverlay);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Регистрирует связь между абстрактным контролом док-системы и конкретным UI-элементом WinUI.
|
||||
/// </summary>
|
||||
/// <param name="control">Абстрактный контрол док-системы.</param>
|
||||
/// <param name="element">Конкретный UI-элемент WinUI.</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, если <paramref name="control"/> или <paramref name="element"/> равны null.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// Эта связь необходима для:
|
||||
/// <list type="bullet">
|
||||
/// <item>Вычисления границ элемента на экране</item>
|
||||
/// <item>Создания визуального представления перетаскивания</item>
|
||||
/// <item>Определения позиции сброса относительно элемента</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public void RegisterControl(IDockControl control, FrameworkElement element)
|
||||
{
|
||||
if (control == null) throw new ArgumentNullException(nameof(control));
|
||||
if (element == null) throw new ArgumentNullException(nameof(element));
|
||||
|
||||
_controlToElement[control] = element;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отменяет регистрацию связи между абстрактным контролом док-системы и UI-элементом WinUI.
|
||||
/// </summary>
|
||||
/// <param name="control">Абстрактный контрол док-системы.</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Выбрасывается, если <paramref name="control"/> равен null.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// Удаляет элемент из внутреннего словаря, освобождая связанные с ним ресурсы.
|
||||
/// </remarks>
|
||||
public void UnregisterControl(IDockControl control)
|
||||
{
|
||||
if (control == null) throw new ArgumentNullException(nameof(control));
|
||||
|
||||
_controlToElement.TryRemove(control, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Вычисляет границы элемента на экране.
|
||||
/// </summary>
|
||||
/// <param name="element">Элемент, для которого вычисляются границы.</param>
|
||||
/// <returns>
|
||||
/// Прямоугольник в экранных координатах, представляющий границы элемента.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Метод выполняет преобразование координат элемента в экранные координаты
|
||||
/// с использованием трансформации визуального дерева.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// В случае ошибки вычисления возвращает прямоугольник размером 100x100 пикселей
|
||||
/// в точке (0, 0).
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
protected override Rect CalculateBounds(IDockControl element)
|
||||
{
|
||||
if (_controlToElement.TryGetValue(element, out var uiElement))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Получаем преобразование координат в экранные
|
||||
var transform = uiElement.TransformToVisual(Window.Current.Content);
|
||||
var point = transform.TransformPoint(new Windows.Foundation.Point(0, 0));
|
||||
|
||||
return new Rect(
|
||||
point.X, point.Y,
|
||||
uiElement.ActualWidth, uiElement.ActualHeight);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Failed to calculate bounds: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Возвращаем значения по умолчанию, если не удалось вычислить
|
||||
return new Rect(0, 0, 100, 100);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создает визуальное представление перетаскиваемого элемента.
|
||||
/// </summary>
|
||||
/// <param name="dragInfo">Информация о перетаскивании.</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// На основе источника перетаскивания создает полупрозрачную копию элемента,
|
||||
/// которая следует за курсором мыши во время операции перетаскивания.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Визуальное представление включает:
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item>Тень для создания эффекта глубины</item>
|
||||
/// <item>Прозрачность для видимости фонового содержимого</item>
|
||||
/// <item>Синюю границу для визуального выделения</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
protected override void CreateDragVisual(UiDragInfo dragInfo)
|
||||
{
|
||||
if (_dragVisual == null || _dragVisualPopup == null || dragInfo.SourceControl == null)
|
||||
return;
|
||||
|
||||
// Настраиваем визуальное представление на основе источника
|
||||
if (_controlToElement.TryGetValue(dragInfo.SourceControl, out var sourceElement))
|
||||
{
|
||||
// Устанавливаем размеры визуального представления
|
||||
_dragVisual.Width = sourceElement.ActualWidth;
|
||||
_dragVisual.Height = sourceElement.ActualHeight;
|
||||
|
||||
// Создаем эффект прозрачности и тени
|
||||
_dragVisual.Opacity = 0.7;
|
||||
|
||||
// Устанавливаем позицию Popup
|
||||
_dragVisualPopup.HorizontalOffset = dragInfo.BaseDragInfo.StartPosition.X;
|
||||
_dragVisualPopup.VerticalOffset = dragInfo.BaseDragInfo.StartPosition.Y;
|
||||
_dragVisualPopup.Child = _dragVisual;
|
||||
_dragVisualPopup.IsOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Обновляет позицию визуального представления перетаскивания.
|
||||
/// </summary>
|
||||
/// <param name="position">Новая позиция курсора.</param>
|
||||
/// <remarks>
|
||||
/// Перемещает Popup с визуальным представлением в указанную позицию,
|
||||
/// обеспечивая плавное следование за курсором мыши.
|
||||
/// </remarks>
|
||||
protected override void UpdateDragVisualPosition(Point position)
|
||||
{
|
||||
if (_dragVisualPopup != null)
|
||||
{
|
||||
_dragVisualPopup.HorizontalOffset = position.X;
|
||||
_dragVisualPopup.VerticalOffset = position.Y;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Очищает визуальное представление перетаскивания.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Скрывает и освобождает ресурсы Popup, используемого для отображения
|
||||
/// визуального представления перетаскиваемого элемента.
|
||||
/// </remarks>
|
||||
protected override void CleanupDragVisual()
|
||||
{
|
||||
if (_dragVisualPopup != null)
|
||||
{
|
||||
_dragVisualPopup.IsOpen = false;
|
||||
_dragVisualPopup.Child = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Показывает визуальную подсказку о возможной позиции сброса.
|
||||
/// </summary>
|
||||
/// <param name="element">Элемент, для которого показывается подсказка.</param>
|
||||
/// <param name="position">Предполагаемая позиция сброса.</param>
|
||||
/// <remarks>
|
||||
/// Отображает полупрозрачный прямоугольник в указанной позиции относительно элемента,
|
||||
/// давая пользователю визуальную обратную связь о том, куда будет помещен элемент.
|
||||
/// </remarks>
|
||||
protected override void ShowDropHint(IDockControl element, DropPosition position)
|
||||
{
|
||||
_dropHintOverlay?.ShowHint(element, position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Скрывает текущую визуальную подсказку о сбросе.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Убирает все отображаемые подсказки сброса, очищая оверлей.
|
||||
/// </remarks>
|
||||
protected override void HideDropHint()
|
||||
{
|
||||
_dropHintOverlay?.HideHint();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Освобождает ресурсы, используемые сервисом перетаскивания.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Выполняет следующие действия:
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item>Отписывается от всех событий менеджера перетаскивания</item>
|
||||
/// <item>Удаляет оверлей подсказок из корневого контейнера</item>
|
||||
/// <item>Очищает словарь зарегистрированных контролов</item>
|
||||
/// <item>Освобождает визуальные элементы</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (_dragDropManager != null)
|
||||
{
|
||||
_dragDropManager.DragStarted -= OnDragStarted;
|
||||
_dragDropManager.DragUpdated -= OnDragUpdated;
|
||||
_dragDropManager.DragCompleted -= OnDragCompleted;
|
||||
_dragDropManager.DragCancelled -= OnDragCancelled;
|
||||
_dragDropManager.DropTargetChanged -= OnDropTargetChanged;
|
||||
}
|
||||
|
||||
if (_dropHintOverlay != null && Window.Current?.Content is Panel rootPanel)
|
||||
{
|
||||
rootPanel.Children.Remove(_dropHintOverlay);
|
||||
_dropHintOverlay = null;
|
||||
}
|
||||
|
||||
_controlToElement.Clear();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Представляет оверлей для отображения визуальных подсказок при сбросе в операции перетаскивания.
|
||||
/// Этот элемент отображает полупрозрачные прямоугольники в местах возможного сброса,
|
||||
/// давая пользователю визуальную обратную связь о допустимых позициях.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="DropHintOverlay"/> является внутренним вспомогательным классом,
|
||||
/// который отображается поверх всего пользовательского интерфейса во время операции
|
||||
/// перетаскивания для показа визуальных подсказок.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Оверлей поддерживает все позиции сброса, определенные в <see cref="DropPosition"/>,
|
||||
/// и автоматически вычисляет размеры и положение подсказок на основе целевого элемента.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed class DropHintOverlay : Grid
|
||||
{
|
||||
private readonly Dictionary<DropPosition, Border> _hintRectangles = new();
|
||||
private readonly SolidColorBrush _hintBrush;
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует новый экземпляр класса <see cref="DropHintOverlay"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Создает прозрачный оверлей, который не участвует в тестировании попаданий,
|
||||
/// и инициализирует прямоугольники для всех возможных позиций сброса.
|
||||
/// </remarks>
|
||||
public DropHintOverlay()
|
||||
{
|
||||
this.IsHitTestVisible = false;
|
||||
this.Background = new SolidColorBrush(Microsoft.UI.Colors.Transparent);
|
||||
|
||||
// Используем акцентный цвет для подсказок
|
||||
_hintBrush = new SolidColorBrush(Microsoft.UI.Colors.DodgerBlue);
|
||||
|
||||
InitializeHintRectangles();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует прямоугольники для всех позиций сброса.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Создает отдельный Border для каждой позиции сброса и добавляет их в дочернюю коллекцию.
|
||||
/// Все прямоугольники изначально скрыты и отображаются только при необходимости.
|
||||
/// </remarks>
|
||||
private void InitializeHintRectangles()
|
||||
{
|
||||
// Создаем прямоугольники для каждой позиции сброса
|
||||
var positions = new[]
|
||||
{
|
||||
DropPosition.Left, DropPosition.Right,
|
||||
DropPosition.Top, DropPosition.Bottom,
|
||||
DropPosition.Center, DropPosition.Tab
|
||||
};
|
||||
|
||||
foreach (var position in positions)
|
||||
{
|
||||
var rect = new Border
|
||||
{
|
||||
Background = _hintBrush,
|
||||
Opacity = 0.3,
|
||||
BorderBrush = new SolidColorBrush(Microsoft.UI.Colors.DodgerBlue),
|
||||
BorderThickness = new Thickness(2),
|
||||
Visibility = Visibility.Collapsed,
|
||||
CornerRadius = new CornerRadius(4)
|
||||
};
|
||||
|
||||
_hintRectangles[position] = rect;
|
||||
this.Children.Add(rect);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Показывает визуальную подсказку для указанного элемента и позиции сброса.
|
||||
/// </summary>
|
||||
/// <param name="element">Элемент, для которого показывается подсказка.</param>
|
||||
/// <param name="position">Позиция сброса относительно элемента.</param>
|
||||
/// <remarks>
|
||||
/// Вычисляет положение и размер подсказки на основе границ элемента и позиции сброса,
|
||||
/// затем делает соответствующий прямоугольник видимым.
|
||||
/// </remarks>
|
||||
public void ShowHint(IDockControl element, DropPosition position)
|
||||
{
|
||||
if (element is not FrameworkElement uiElement) return;
|
||||
if (!_hintRectangles.TryGetValue(position, out var rect)) return;
|
||||
|
||||
// Вычисляем позицию и размер подсказки
|
||||
var bounds = CalculateHintBounds(uiElement, position);
|
||||
|
||||
Canvas.SetLeft(rect, bounds.X);
|
||||
Canvas.SetTop(rect, bounds.Y);
|
||||
rect.Width = bounds.Width;
|
||||
rect.Height = bounds.Height;
|
||||
rect.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Вычисляет границы подсказки для указанного элемента и позиции сброса.
|
||||
/// </summary>
|
||||
/// <param name="element">Целевой элемент.</param>
|
||||
/// <param name="position">Позиция сброса.</param>
|
||||
/// <returns>Прямоугольник с координатами и размерами подсказки.</returns>
|
||||
/// <remarks>
|
||||
/// Размеры подсказок зависят от позиции:
|
||||
/// <list type="bullet">
|
||||
/// <item>Слева/справа: ширина 50px, высота равна высоте элемента</item>
|
||||
/// <item>Сверху/снизу: высота 50px, ширина равна ширине элемента</item>
|
||||
/// <item>В центре: размеры равны размерам элемента</item>
|
||||
/// <item>Вкладка: высота 30px, ширина равна ширине элемента, позиция сверху</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
private Rect CalculateHintBounds(FrameworkElement element, DropPosition position)
|
||||
{
|
||||
// Получаем позицию элемента относительно оверлея
|
||||
var transform = element.TransformToVisual(this);
|
||||
var point = transform.TransformPoint(new Windows.Foundation.Point(0, 0));
|
||||
|
||||
// Вычисляем размеры подсказки в зависимости от позиции
|
||||
return position switch
|
||||
{
|
||||
DropPosition.Left => new Rect(
|
||||
point.X - 50, point.Y,
|
||||
50, element.ActualHeight),
|
||||
|
||||
DropPosition.Right => new Rect(
|
||||
point.X + element.ActualWidth, point.Y,
|
||||
50, element.ActualHeight),
|
||||
|
||||
DropPosition.Top => new Rect(
|
||||
point.X, point.Y - 50,
|
||||
element.ActualWidth, 50),
|
||||
|
||||
DropPosition.Bottom => new Rect(
|
||||
point.X, point.Y + element.ActualHeight,
|
||||
element.ActualWidth, 50),
|
||||
|
||||
DropPosition.Center => new Rect(
|
||||
point.X, point.Y,
|
||||
element.ActualWidth, element.ActualHeight),
|
||||
|
||||
DropPosition.Tab => new Rect(
|
||||
point.X, point.Y - 30,
|
||||
element.ActualWidth, 30),
|
||||
|
||||
_ => new Rect(point.X, point.Y, 100, 100)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Скрывает все визуальные подсказки.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Делает все прямоугольники подсказок невидимыми, очищая оверлей.
|
||||
/// </remarks>
|
||||
public void HideHint()
|
||||
{
|
||||
foreach (var rect in _hintRectangles.Values)
|
||||
{
|
||||
rect.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
195
Lattice.UI.Docking.WinUI/Themes/Generic.xaml
Normal file
195
Lattice.UI.Docking.WinUI/Themes/Generic.xaml
Normal file
@@ -0,0 +1,195 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Lattice.UI"
|
||||
xmlns:conv="using:Lattice.UI.Docking.WinUI.Converters"
|
||||
xmlns:models="using:Lattice.Core.Docking.Models">
|
||||
|
||||
<!-- 1. Шаблоны -->
|
||||
<DataTemplate x:Key="LatticeGroupTemplate">
|
||||
<controls:LatticeDockGroup />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="LatticeLeafTemplate">
|
||||
<controls:LatticeDockLeaf />
|
||||
</DataTemplate>
|
||||
|
||||
<!-- 2. Селектор -->
|
||||
<conv:DockTemplateSelector x:Key="GlobalDockSelector"
|
||||
GroupTemplate="{StaticResource LatticeGroupTemplate}"
|
||||
LeafTemplate="{StaticResource LatticeLeafTemplate}" />
|
||||
|
||||
<!-- 3. Стиль Сплиттера -->
|
||||
<Style TargetType="controls:LatticeSplitter">
|
||||
<Setter Property="Background" Value="{ThemeResource Lattice.Brush.Splitter.Normal}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="controls:LatticeSplitter">
|
||||
<Grid Background="Transparent">
|
||||
<Rectangle Fill="{TemplateBinding Background}"
|
||||
Width="{ThemeResource Lattice.Size.SplitterWidth}"
|
||||
HorizontalAlignment="Center"/>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 4. Стиль Хоста -->
|
||||
<Style TargetType="controls:LatticeDockHost">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="controls:LatticeDockHost">
|
||||
<ContentControl
|
||||
Content="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Manager.Root}"
|
||||
ContentTemplateSelector="{StaticResource GlobalDockSelector}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch" />
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 5. Стиль Группы (Рекурсия) -->
|
||||
<Style TargetType="controls:LatticeDockGroup">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="controls:LatticeDockGroup">
|
||||
<!-- Grid перестраивается в коде LatticeDockGroup.cs -->
|
||||
<Grid x:Name="PART_Grid">
|
||||
<!-- Первая область -->
|
||||
<ContentControl x:Name="PART_First"
|
||||
Content="{Binding First}"
|
||||
ContentTemplateSelector="{StaticResource GlobalDockSelector}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch" />
|
||||
|
||||
<!-- Сплиттер (его положение в Grid.Row/Column устанавливается автоматически при перестроении Grid) -->
|
||||
<controls:LatticeSplitter x:Name="PART_Splitter" />
|
||||
|
||||
<!-- Вторая область -->
|
||||
<ContentControl x:Name="PART_Second"
|
||||
Content="{Binding Second}"
|
||||
ContentTemplateSelector="{StaticResource GlobalDockSelector}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 6. Стиль Листа -->
|
||||
<Style TargetType="controls:LatticeDockLeaf">
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="controls:LatticeDockLeaf">
|
||||
<!-- Grid и Border должны растягиваться -->
|
||||
<Grid Margin="{ThemeResource Lattice.Thickness.PanelMargin}" VerticalAlignment="Stretch">
|
||||
<Border Background="{ThemeResource Lattice.Brush.Background.Primary}"
|
||||
BorderBrush="{ThemeResource Lattice.Brush.Panel.Border}"
|
||||
BorderThickness="{ThemeResource Lattice.Thickness.PanelBorder}"
|
||||
CornerRadius="{ThemeResource Lattice.Geometry.PanelCornerRadius}"
|
||||
VerticalAlignment="Stretch">
|
||||
|
||||
<!-- Используем кастомный TabControl или оставляем стандартный -->
|
||||
<TabView x:Name="PART_TabView"
|
||||
TabItemsSource="{Binding Children}"
|
||||
SelectedItem="{Binding ActiveContent, Mode=TwoWay}"
|
||||
IsAddTabButtonVisible="False"
|
||||
VerticalAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
TabWidthMode="SizeToContent"
|
||||
Padding="0">
|
||||
|
||||
<TabView.TabItemTemplate>
|
||||
<DataTemplate>
|
||||
<TabViewItem Header="{Binding Title}" FontSize="11" Height="28" MinWidth="0" >
|
||||
<!-- ContentPresenter ДОЛЖЕН иметь VerticalAlignment="Stretch" -->
|
||||
<ContentPresenter Content="{Binding View}"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch" />
|
||||
</TabViewItem>
|
||||
</DataTemplate>
|
||||
</TabView.TabItemTemplate>
|
||||
</TabView>
|
||||
</Border>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Добавить в Generic.xaml -->
|
||||
<Style TargetType="controls:LatticeTabControl">
|
||||
<Setter Property="Background" Value="{ThemeResource Lattice.Brush.Background.Primary}"/>
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource Lattice.Brush.Panel.Border}"/>
|
||||
<Setter Property="BorderThickness" Value="{ThemeResource Lattice.Thickness.PanelBorder}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="controls:LatticeTabControl">
|
||||
<Grid x:Name="PART_RootGrid">
|
||||
<!-- Заголовки вкладок -->
|
||||
<ListBox x:Name="PART_TabHeaderList"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="0,0,0,1"
|
||||
SelectionMode="Single"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ListBox.ItemContainerStyle>
|
||||
<Style TargetType="ListBoxItem">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource Lattice.Brush.Accent.Action}"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,0,2"/>
|
||||
<Setter Property="Margin" Value="0,0,4,0"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="BorderThickness" Value="0,0,0,2"/>
|
||||
<Setter Property="Foreground" Value="{ThemeResource Lattice.Brush.Accent.Action}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPointerOver" Value="True">
|
||||
<Setter Property="Background" Value="{ThemeResource SystemControlBackgroundListLowBrush}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</ListBox.ItemContainerStyle>
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
</ListBox>
|
||||
|
||||
<!-- Контент вкладки -->
|
||||
<ContentControl x:Name="PART_ContentControl"
|
||||
Background="{TemplateBinding Background}"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"/>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 7. Ресурсы по умолчанию (если тема не загружена) -->
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary>
|
||||
<!-- Значения по умолчанию -->
|
||||
<SolidColorBrush x:Key="Lattice.Brush.Background.Primary" Color="#1E1E1E" />
|
||||
<SolidColorBrush x:Key="Lattice.Brush.Background.Secondary" Color="#252526" />
|
||||
<SolidColorBrush x:Key="Lattice.Brush.Panel.Border" Color="#3F3F46" />
|
||||
<SolidColorBrush x:Key="Lattice.Brush.Splitter.Normal" Color="#2D2D2D" />
|
||||
<SolidColorBrush x:Key="Lattice.Brush.Splitter.Hover" Color="#007ACC" />
|
||||
<SolidColorBrush x:Key="Lattice.Brush.Accent.Action" Color="#007ACC" />
|
||||
|
||||
<CornerRadius x:Key="Lattice.Geometry.PanelCornerRadius">0</CornerRadius>
|
||||
<Thickness x:Key="Lattice.Thickness.PanelMargin">0,0,1,1</Thickness>
|
||||
<Thickness x:Key="Lattice.Thickness.PanelBorder">1</Thickness>
|
||||
<x:Double x:Key="Lattice.Size.SplitterWidth">1</x:Double>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
</ResourceDictionary>
|
||||
Reference in New Issue
Block a user