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; /// /// Кастомный контрол вкладок с поддержкой всех позиций размещения панели вкладок. /// Реализует интерфейс для интеграции с системой докинга. /// 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; /// /// Инициализирует новый экземпляр класса . /// public LatticeTabControl() { this.DefaultStyleKey = typeof(LatticeTabControl); _modelPropertyChangedHandler = OnModelPropertyChanged; this.DataContextChanged += OnDataContextChanged; // Подписываемся на события this.PointerPressed += OnPointerPressed; this.PointerMoved += OnPointerMoved; this.PointerReleased += OnPointerReleased; } /// public IDockElement? Model { get => _model; set { if (_model == value) return; DetachModel(); _model = value as DockLeaf; AttachModel(); OnPropertyChanged(nameof(Model)); } } /// public LayoutManager? LayoutManager { get => _layoutManager; set { if (_layoutManager == value) return; _layoutManager = value; OnPropertyChanged(nameof(LayoutManager)); } } /// public IDockDragDropService? DragDropService { get => _dragDropService; set { if (_dragDropService == value) return; _dragDropService = value; OnPropertyChanged(nameof(DragDropService)); } } /// public IDockContextManager? ContextManager { get => _contextManager; set { if (_contextManager == value) return; _contextManager = value; OnPropertyChanged(nameof(ContextManager)); } } /// public bool IsSelected { get => _isSelected; set { if (_isSelected == value) return; _isSelected = value; OnPropertyChanged(nameof(IsSelected)); } } /// public bool IsActive { get => _isActive; set { if (_isActive == value) return; _isActive = value; OnPropertyChanged(nameof(IsActive)); } } /// public bool CanDrag { get => _canDrag; set { if (_canDrag == value) return; _canDrag = value; OnPropertyChanged(nameof(CanDrag)); } } /// public bool CanDrop { get => _canDrop; set { if (_canDrop == value) return; _canDrop = value; OnPropertyChanged(nameof(CanDrop)); } } /// public TabPlacement TabPlacement { get => _model?.TabPlacement ?? TabPlacement.Top; set { if (_model != null && _model.TabPlacement != value) { _model.TabPlacement = value; UpdateTabPlacement(); } } } /// public bool ShowCloseButtons { get => _showCloseButtons; set { if (_showCloseButtons == value) return; _showCloseButtons = value; OnPropertyChanged(nameof(ShowCloseButtons)); UpdateTabHeaders(); } } /// public bool CanReorderTabs { get => _canReorderTabs; set { if (_canReorderTabs == value) return; _canReorderTabs = value; OnPropertyChanged(nameof(CanReorderTabs)); } } /// public IDockContent? ActiveContent { get => _model?.ActiveContent; set { if (_model != null) { _model.ActiveContent = value; } } } /// public event EventHandler? ActiveContentChanged; /// public event EventHandler? ContentClosing; /// public event EventHandler? TabsReordered; /// public event PropertyChangedEventHandler? PropertyChanged; /// protected override void OnApplyTemplate() { base.OnApplyTemplate(); _rootGrid = GetTemplateChild("PART_RootGrid") as Grid; _tabHeaderList = GetTemplateChild("PART_TabHeaderList") as ListBox; _contentControl = GetTemplateChild("PART_ContentControl") as ContentControl; if (_tabHeaderList != null) { _tabHeaderList.SelectionChanged += OnTabSelectionChanged; } UpdateTabPlacement(); UpdateTabHeaders(); } private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args) { Model = args.NewValue as DockLeaf; } private void AttachModel() { if (_model != null) { _model.PropertyChanged += _modelPropertyChangedHandler; // Подписываемся на изменения коллекции if (_model.Children is INotifyCollectionChanged notifyCollection) { notifyCollection.CollectionChanged += OnChildrenCollectionChanged; } // Устанавливаем 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); } /// public void AddContent(IDockContent content) { if (_model != null && !_model.Children.Contains(content)) { _model.AddContent(content); UpdateTabHeaders(); } } /// public void RemoveContent(IDockContent content) { if (_model != null && _model.Children.Contains(content)) { _model.RemoveContent(content); UpdateTabHeaders(); } } /// public bool CloseContent(IDockContent content) { var args = new ContentClosingEventArgs(content); ContentClosing?.Invoke(this, args); if (!args.Cancel) { RemoveContent(content); return true; } return false; } /// public void CloseAllExcept(IDockContent exceptContent) { if (_model == null) return; var itemsToClose = _model.Children .Where(c => c != exceptContent) .ToList(); foreach (var content in itemsToClose) { CloseContent(content); } } /// public void CloseAll() { if (_model == null) return; var itemsToClose = _model.Children.ToList(); foreach (var content in itemsToClose) { CloseContent(content); } } /// public void Refresh() { UpdateTabHeaders(); UpdateTabPlacement(); } /// public void ApplyTheme(IDockTheme theme) { // Применение темы к элементу if (theme != null) { // TODO: Применить тему к стилям контрола } } /// 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)); } /// 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); } } }