From e8b4cb98817aaa610d79d0edc5cd4b84dd04aa17 Mon Sep 17 00:00:00 2001 From: FrigaT Date: Sun, 1 Feb 2026 09:26:13 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=20winui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Abstractions/IDockContent.cs | 6 + Lattice.Core.Docking/Engine/DockOperations.cs | 11 +- Lattice.Core.Docking/Engine/LayoutManager.cs | 153 ++--- Lattice.Core.Docking/Models/AutoHidePanel.cs | 181 ++---- Lattice.Core.Docking/Models/DockGroup.cs | 241 ++------ Lattice.Core.Docking/Models/DockLeaf.cs | 200 ++----- .../Services/ContentRegistry.cs | 12 +- Lattice.Themes.Core/LatticeTokens.cs | 2 +- Lattice.Themes.Core/ThemeManager.cs | 151 +++-- .../Abstractions/IWinUIDragDropControl.cs | 31 + .../Controls/AdvancedTabControl.cs | 189 +++++++ .../Controls/LatticeDockGroup.cs | 283 +--------- .../Controls/LatticeDockHost.cs | 423 ++++---------- .../Controls/LatticeDockLeaf.cs | 525 ++++++++++++++++- .../Controls/LatticeSplitter.cs | 149 ++++- .../Controls/LatticeTabControl.cs | 104 +--- Lattice.UI.Docking.WinUI/DockBuilder.cs | 189 +++++++ .../Factories/WinUIDockControlFactory.cs | 164 +----- .../Services/DragDropService.cs | 53 ++ .../Services/WinUIDockContextManager.cs | 129 ++++- .../Services/WinUIDockUIService.cs | 139 +---- .../Services/WinUIDragDropService.cs | 533 ------------------ Lattice.UI.Docking.WinUI/Themes/Generic.xaml | 178 +++--- .../Abstractions/IDockControl.cs | 21 + .../Implementations/DockControlBase.cs | 113 ---- .../Services/DockContextManagerBase.cs | 35 +- 26 files changed, 1842 insertions(+), 2373 deletions(-) create mode 100644 Lattice.UI.Docking.WinUI/Abstractions/IWinUIDragDropControl.cs create mode 100644 Lattice.UI.Docking.WinUI/Controls/AdvancedTabControl.cs create mode 100644 Lattice.UI.Docking.WinUI/DockBuilder.cs create mode 100644 Lattice.UI.Docking.WinUI/Services/DragDropService.cs delete mode 100644 Lattice.UI.Docking.WinUI/Services/WinUIDragDropService.cs delete mode 100644 Lattice.UI.Docking/Implementations/DockControlBase.cs diff --git a/Lattice.Core.Docking/Abstractions/IDockContent.cs b/Lattice.Core.Docking/Abstractions/IDockContent.cs index 35a2446..49c5b9c 100644 --- a/Lattice.Core.Docking/Abstractions/IDockContent.cs +++ b/Lattice.Core.Docking/Abstractions/IDockContent.cs @@ -11,6 +11,12 @@ public interface IDockContent /// string Id { get; } + /// + /// Устанавливает идентификатор контента. + /// + /// Новый идентификатор. + void SetId(string id); + /// /// Получает заголовок, отображаемый пользователю на вкладке. /// diff --git a/Lattice.Core.Docking/Engine/DockOperations.cs b/Lattice.Core.Docking/Engine/DockOperations.cs index b3c5bf6..59ef2e2 100644 --- a/Lattice.Core.Docking/Engine/DockOperations.cs +++ b/Lattice.Core.Docking/Engine/DockOperations.cs @@ -114,7 +114,16 @@ public static class DockOperations return root; } + // Если target был корнем, новая группа становится новым корнем + if (target == root) + { + newGroup.Parent = null; + return newGroup; + } + + // Эта точка недостижима при правильном использовании, + // но добавляем для безопасности newGroup.Parent = null; - return newGroup; // Новая группа стала корнем + return newGroup; } } \ No newline at end of file diff --git a/Lattice.Core.Docking/Engine/LayoutManager.cs b/Lattice.Core.Docking/Engine/LayoutManager.cs index 64c33d7..ab11fc3 100644 --- a/Lattice.Core.Docking/Engine/LayoutManager.cs +++ b/Lattice.Core.Docking/Engine/LayoutManager.cs @@ -1,41 +1,29 @@ using Lattice.Core.Docking.Abstractions; using Lattice.Core.Docking.Models; +using Lattice.Core.Docking.Services; using System.Collections.ObjectModel; using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Lattice.Serialization.Docking")] +[assembly: InternalsVisibleTo("Lattice.UI.Docking.WinUI")] namespace Lattice.Core.Docking.Engine; /// /// Центральный менеджер макета, управляющий всей структурой док-системы. /// Координирует дерево компоновки, плавающие окна, автоскрываемые панели -/// и предоставляет API для манипуляции макетом. +/// и предоставляет API для манипуляции макетом. Использует кэширование +/// для оптимизации поиска элементов по идентификатору. /// -/// -/// Этот класс является основным координатором док-системы. Он управляет: -/// -/// Деревом компоновки главного окна -/// Коллекцией плавающих окон -/// Коллекцией автоскрываемых панелей -/// Реестром типов контента -/// -/// Все изменения в структуре макета должны выполняться через методы этого класса. -/// public class LayoutManager { private readonly ObservableCollection _autoHidePanels = new(); + private readonly Dictionary _elementCache = new(); private IDockElement? _root; /// /// Получает или задает корневой элемент дерева компоновки главного окна. + /// При изменении значения генерируется событие . /// - /// - /// Корневой элемент или null, если макет пуст. - /// - /// - /// При изменении этого свойства генерируется событие . - /// public IDockElement? Root { get => _root; @@ -52,34 +40,21 @@ public class LayoutManager /// /// Получает список активных плавающих окон. /// - /// - /// Коллекция объектов , представляющих плавающие окна. - /// public List FloatingWindows { get; } = new(); /// /// Получает коллекцию автоскрываемых панелей. /// - /// - /// Доступная только для чтения коллекция объектов . - /// public ReadOnlyObservableCollection AutoHidePanels { get; } /// /// Получает или задает реестр типов контента. /// - /// - /// Реестр типов контента или null, если реестр не установлен. - /// - public Services.ContentRegistry? ContentRegistry { get; set; } + public ContentRegistry? ContentRegistry { get; set; } /// /// Происходит при изменении структуры дерева компоновки. /// - /// - /// Событие генерируется при любых изменениях в дереве компоновки, - /// включая добавление, удаление или перемещение элементов. - /// public event Action? LayoutUpdated; /// @@ -100,12 +75,8 @@ public class LayoutManager /// /// Содержимое панели. /// Сторона окна для прикрепления панели. - /// - /// Созданная автоскрываемая панель. - /// - /// - /// Выбрасывается, когда равен null. - /// + /// Созданная автоскрываемая панель. + /// Выбрасывается, когда равен null. public AutoHidePanel AddAutoHidePanel(IDockContent content, DockSide side) { if (content == null) throw new ArgumentNullException(nameof(content)); @@ -120,12 +91,8 @@ public class LayoutManager /// Удаляет автоскрываемую панель из коллекции. /// /// Панель для удаления. - /// - /// true, если панель была успешно удалена; в противном случае false. - /// - /// - /// Выбрасывается, когда равен null. - /// + /// true, если панель была успешно удалена; в противном случае false. + /// Выбрасывается, когда равен null. public bool RemoveAutoHidePanel(AutoHidePanel panel) { if (panel == null) throw new ArgumentNullException(nameof(panel)); @@ -143,10 +110,7 @@ public class LayoutManager /// /// Идентификатор типа контента. /// Уникальный идентификатор документа. - /// - /// Созданный контент или null, если ContentRegistry не установлен - /// или тип контента не зарегистрирован. - /// + /// Созданный контент или null, если ContentRegistry не установлен или тип контента не зарегистрирован. public IDockContent? CreateDocument(string contentTypeId, string id) { if (ContentRegistry == null || !ContentRegistry.IsRegistered(contentTypeId)) @@ -161,22 +125,8 @@ public class LayoutManager /// Перемещаемый элемент. /// Целевой элемент, относительно которого выполняется перемещение. /// Позиция перемещения относительно цели. - /// - /// Если true, контент будет добавлен как документ в центральную область. - /// В текущей реализации этот параметр не используется. - /// - /// - /// Выбрасывается, когда равен null. - /// - /// - /// Метод выполняет следующие шаги: - /// - /// Удаляет источник из текущего местоположения - /// Вставляет источник в новое местоположение относительно цели - /// Обновляет структуру дерева компоновки - /// - /// Если равен null, элемент помещается в новое плавающее окно. - /// + /// Если true, контент будет добавлен как документ в центральную область. + /// Выбрасывается, когда равен null. public void Move(IDockElement source, IDockElement? target, DockPosition position, bool asDocument = false) { @@ -210,7 +160,10 @@ public class LayoutManager if (!sourceRemoved) return; - // 2. Вставляем в цель + // Обновляем кэш - удаляем перемещенный элемент + _elementCache.Remove(source.Id); + + // 2. Вставляем в новое место if (target == null) { // Создаем новое плавающее окно @@ -228,6 +181,9 @@ public class LayoutManager } } + // Обновляем кэш для вставленного элемента + _elementCache[source.Id] = source; + LayoutUpdated?.Invoke(); } @@ -235,9 +191,7 @@ public class LayoutManager /// Удаляет элемент из всех плавающих окон. /// /// Элемент для удаления. - /// - /// true, если элемент был найден и удален; в противном случае false. - /// + /// true, если элемент был найден и удален; в противном случае false. private bool RemoveFromFloatingWindows(IDockElement element) { foreach (var win in FloatingWindows.ToArray()) @@ -277,36 +231,52 @@ public class LayoutManager /// /// Проверяемый элемент. /// Предполагаемый предок. - /// - /// true, если элемент является потомком предка; в противном случае false. - /// + /// true, если элемент является потомком предка; в противном случае false. private bool IsDescendantOf(IDockElement element, IDockElement ancestor) { - if (element == ancestor) return true; - if (ancestor is DockGroup group) - return IsDescendantOf(element, group.First) || IsDescendantOf(element, group.Second); + var current = element.Parent; + while (current != null) + { + if (current == ancestor) + return true; + current = current.Parent; + } return false; } /// /// Находит элемент по его идентификатору во всех окнах (главном и плавающих). + /// Использует кэширование для оптимизации повторных поисков. /// /// Идентификатор элемента для поиска. - /// - /// Найденный элемент или null, если элемент с таким идентификатором не найден. - /// + /// Найденный элемент или null, если элемент с таким идентификатором не найден. public IDockElement? FindById(string id) { if (string.IsNullOrEmpty(id)) return null; - var found = FindRecursive(Root, id); - if (found != null) return found; + // Проверка кэша + if (_elementCache.TryGetValue(id, out var cached)) + return cached; + // Поиск в основном дереве + var found = FindRecursive(Root, id); + if (found != null) + { + _elementCache[id] = found; + return found; + } + + // Поиск в плавающих окнах foreach (var win in FloatingWindows) { found = FindRecursive(win.Root, id); - if (found != null) return found; + if (found != null) + { + _elementCache[id] = found; + return found; + } } + return null; } @@ -315,9 +285,7 @@ public class LayoutManager /// /// Корневой узел поддерева для поиска. /// Идентификатор элемента для поиска. - /// - /// Найденный элемент или null, если элемент не найден. - /// + /// Найденный элемент или null, если элемент не найден. private IDockElement? FindRecursive(IDockElement? node, string id) { if (node == null) return null; @@ -331,21 +299,14 @@ public class LayoutManager /// /// Сбрасывает макет к состоянию по умолчанию. + /// Очищает корневой элемент, плавающие окна, автоскрываемые панели и кэш. /// - /// - /// Метод выполняет следующие действия: - /// - /// Очищает корневой элемент - /// Закрывает все плавающие окна - /// Удаляет все автоскрываемые панели - /// Генерирует соответствующие события - /// - /// public void Reset() { Root = null; FloatingWindows.Clear(); _autoHidePanels.Clear(); + _elementCache.Clear(); LayoutUpdated?.Invoke(); AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty); } @@ -354,9 +315,7 @@ public class LayoutManager /// Находит элемент по идентификатору в дереве компоновки. /// /// Идентификатор элемента для поиска. - /// - /// Найденный элемент или null, если элемент с таким идентификатором не найден. - /// + /// Найденный элемент или null, если элемент с таким идентификатором не найден. public IDockElement? FindElementById(string id) { return FindElementByIdRecursive(Root, id) ?? @@ -369,9 +328,7 @@ public class LayoutManager /// /// Корневой элемент поддерева для поиска. /// Идентификатор элемента для поиска. - /// - /// Найденный элемент или null, если элемент не найден. - /// + /// Найденный элемент или null, если элемент не найден. private IDockElement? FindElementByIdRecursive(IDockElement? element, string id) { if (element == null) return null; diff --git a/Lattice.Core.Docking/Models/AutoHidePanel.cs b/Lattice.Core.Docking/Models/AutoHidePanel.cs index fe2a0d3..9e45fcf 100644 --- a/Lattice.Core.Docking/Models/AutoHidePanel.cs +++ b/Lattice.Core.Docking/Models/AutoHidePanel.cs @@ -1,149 +1,65 @@ -using System.ComponentModel; -using System.Runtime.CompilerServices; +using Lattice.Core.Docking.Abstractions; namespace Lattice.Core.Docking.Models; /// /// Представляет автоскрываемую панель, которая может быть прикреплена к одной из сторон окна. -/// Автоскрываемые панели скрываются, оставляя видимой только полоску-заголовок, -/// и разворачиваются при наведении курсора или клике. +/// Автоскрываемые панели скрываются, оставляя видимой только заголовок, и разворачиваются при наведении курсора или клике. /// -/// -/// Автоскрываемые панели являются важным элементом современных IDE-подобных приложений, -/// позволяя экономить пространство экрана при сохранении быстрого доступа к инструментам. -/// -public class AutoHidePanel : INotifyPropertyChanged +public class AutoHidePanel { - /// - /// Происходит при изменении значения свойства. - /// - public event PropertyChangedEventHandler? PropertyChanged; - - private bool _isVisible = false; - private double _slideOffset = 0; - - /// - /// Получает уникальный идентификатор автоскрываемой панели. - /// - /// - /// Строковый идентификатор, сгенерированный с помощью GUID. - /// - public string Id { get; } = Guid.NewGuid().ToString(); - - /// - /// Получает или задает содержимое панели. - /// - /// - /// Объект, реализующий . - /// - /// - /// Выбрасывается при попытке установить значение null. - /// - public Abstractions.IDockContent Content - { - get => _content; - set - { - if (_content != value) - { - _content = value ?? throw new ArgumentNullException(nameof(value)); - OnPropertyChanged(); - OnPropertyChanged(nameof(Title)); - } - } - } - private Abstractions.IDockContent _content; - - /// - /// Получает или задает сторону окна, к которой прикреплена панель. - /// - /// - /// Значение перечисления , указывающее сторону прикрепления. - /// - public DockSide Side { get; set; } - - /// - /// Получает или задает ширину панели (для левой/правой сторон) - /// или высоту (для верхней/нижней сторон). - /// - /// - /// Размер панели в пикселях. Значение по умолчанию: 300. - /// - public double Size { get; set; } = 300; - - /// - /// Получает или задает признак видимости панели. - /// - /// - /// true, если панель развернута и видима; в противном случае false. - /// - /// - /// При изменении этого свойства генерируется событие . - /// - public bool IsVisible - { - get => _isVisible; - set - { - if (_isVisible != value) - { - _isVisible = value; - OnPropertyChanged(); - } - } - } - - /// - /// Получает или задает смещение для анимации выезда/заезда панели. - /// - /// - /// Значение от 0.0 до 1.0, где 0.0 - полностью скрыта, 1.0 - полностью развернута. - /// - /// - /// Используется для плавной анимации отображения/скрытия панели. - /// - public double SlideOffset - { - get => _slideOffset; - set - { - if (Math.Abs(_slideOffset - value) > 0.001) - { - _slideOffset = value; - OnPropertyChanged(); - } - } - } - - /// - /// Получает заголовок панели. - /// - /// - /// Заголовок, взятый из содержимого панели. - /// Если содержимое не установлено, возвращает "Auto-hide Panel". - /// - public string Title => Content?.Title ?? "Auto-hide Panel"; - /// /// Инициализирует новый экземпляр класса . /// /// Содержимое панели. - /// Сторона окна для прикрепления. - /// - /// Выбрасывается, когда равен null. - /// - public AutoHidePanel(Abstractions.IDockContent content, DockSide side) + /// Сторона окна для прикрепления панели. + /// Выбрасывается, когда равен null. + public AutoHidePanel(IDockContent content, DockSide side) { Content = content ?? throw new ArgumentNullException(nameof(content)); Side = side; } + /// + /// Получает уникальный идентификатор панели. + /// + public string Id { get; } = Guid.NewGuid().ToString(); + + /// + /// Получает содержимое панели. + /// + public IDockContent Content { get; } + + /// + /// Получает или задает сторону окна, к которой прикреплена панель. + /// + public DockSide Side { get; set; } + + /// + /// Получает или задает размер панели в пикселях. + /// Для левой/правой сторон - ширина, для верхней/нижней - высота. + /// + public double Size { get; set; } = 300; + + /// + /// Получает или задает значение, указывающее, видима ли панель. + /// + public bool IsVisible { get; set; } + + /// + /// Получает или задает смещение для анимации выезда/заезда панели. + /// Значение от 0.0 (полностью скрыта) до 1.0 (полностью развернута). + /// + public double SlideOffset { get; set; } + + /// + /// Получает заголовок панели, взятый из содержимого. + /// + public string Title => Content?.Title ?? "Auto-hide Panel"; + /// /// Переключает видимость панели. /// - /// - /// Если панель была видимой, становится скрытой, и наоборот. - /// public void Toggle() { IsVisible = !IsVisible; @@ -164,15 +80,4 @@ public class AutoHidePanel : INotifyPropertyChanged { IsVisible = false; } - - /// - /// Вызывает событие . - /// - /// - /// Имя изменившегося свойства. Если не указано, определяется автоматически. - /// - protected void OnPropertyChanged([CallerMemberName] string? name = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); - } } \ No newline at end of file diff --git a/Lattice.Core.Docking/Models/DockGroup.cs b/Lattice.Core.Docking/Models/DockGroup.cs index c6745cd..a7b8af5 100644 --- a/Lattice.Core.Docking/Models/DockGroup.cs +++ b/Lattice.Core.Docking/Models/DockGroup.cs @@ -4,77 +4,40 @@ using System.Runtime.CompilerServices; namespace Lattice.Core.Docking.Models; -/// -/// Представляет узел дерева компоновки, который разделяет доступную область -/// между двумя дочерними элементами. Этот класс является основным структурным -/// элементом для создания сложных макетов с разделителями. -/// -/// -/// Каждая группа содержит два дочерних элемента ( и ), -/// которые могут быть либо другими группами (для создания вложенной структуры), -/// либо листами () с контентом. -/// Направление разделения определяется свойством . -/// public class DockGroup : IDockElement, INotifyPropertyChanged { - /// - /// Происходит при изменении значения свойства. - /// - public event PropertyChangedEventHandler? PropertyChanged; - - private double _splitRatio = 0.5; - private string _id; private IDockElement _first; private IDockElement _second; + private SplitDirection _orientation; + private double _splitRatio = 0.5; + private IDockElement? _parent; + private double _width; + private double _height; - /// - /// Получает или задает уникальный идентификатор группы. - /// - /// - /// Строковый идентификатор, уникальный в пределах дерева компоновки. - /// - /// - /// Идентификатор используется для сериализации/десериализации макета, - /// поиска элементов и отслеживания изменений в дереве. - /// - public string Id + public event PropertyChangedEventHandler? PropertyChanged; + + public DockGroup(IDockElement first, IDockElement second, SplitDirection orientation) { - get => _id; - internal set + First = first ?? throw new ArgumentNullException(nameof(first)); + Second = second ?? throw new ArgumentNullException(nameof(second)); + Orientation = orientation; + } + + public string Id { get; } = Guid.NewGuid().ToString(); + + public IDockElement? Parent + { + get => _parent; + set { - if (_id != value) + if (_parent != value) { - _id = value; + _parent = value; OnPropertyChanged(); } } } - /// - /// Получает или задает родительский элемент в иерархии дерева компоновки. - /// - /// - /// Родительский элемент или null, если эта группа является корневой. - /// - /// - /// Это свойство управляется системой компоновки при добавлении или - /// удалении элементов из дерева. - /// - public IDockElement? Parent { get; set; } - - /// - /// Получает или задает первый дочерний элемент (левую или верхнюю область). - /// - /// - /// Элемент, занимающий первую часть разделенной области. - /// - /// - /// Выбрасывается при попытке установить значение null. - /// - /// - /// При установке нового значения автоматически обновляется свойство - /// у дочернего элемента. - /// public IDockElement First { get => _first; @@ -89,19 +52,6 @@ public class DockGroup : IDockElement, INotifyPropertyChanged } } - /// - /// Получает или задает второй дочерний элемент (правую или нижнюю область). - /// - /// - /// Элемент, занимающий вторую часть разделенной области. - /// - /// - /// Выбрасывается при попытке установить значение null. - /// - /// - /// При установке нового значения автоматически обновляется свойство - /// у дочернего элемента. - /// public IDockElement Second { get => _second; @@ -116,42 +66,25 @@ public class DockGroup : IDockElement, INotifyPropertyChanged } } - /// - /// Получает или задает направление разделения данной группы. - /// - /// - /// Значение перечисления , указывающее, - /// как разделена область: горизонтально или вертикально. - /// - /// - /// - /// создает левую и правую области - /// создает верхнюю и нижнюю области - /// - /// - public SplitDirection Orientation { get; set; } + public SplitDirection Orientation + { + get => _orientation; + set + { + if (_orientation != value) + { + _orientation = value; + OnPropertyChanged(); + } + } + } - /// - /// Получает или задает соотношение разделения между первым и вторым элементами. - /// - /// - /// Значение от 0.0 до 1.0, где: - /// - /// 0.0 - вся область принадлежит второму элементу - /// 0.5 - область разделена поровну - /// 1.0 - вся область принадлежит первому элементу - /// - /// - /// - /// Изменение этого свойства вызывает событие - /// и может привести к перерисовке пользовательского интерфейса. - /// public double SplitRatio { get => _splitRatio; set { - if (Math.Abs(_splitRatio - value) > double.Epsilon) + if (Math.Abs(_splitRatio - value) > 0.001) { _splitRatio = value; OnPropertyChanged(); @@ -159,96 +92,42 @@ public class DockGroup : IDockElement, INotifyPropertyChanged } } - /// - /// Получает или задает желаемую ширину элемента. - /// - /// - /// Ширина в пикселях или относительных единицах. - /// - public double Width { get; set; } + public double Width + { + get => _width; + set + { + if (Math.Abs(_width - value) > 0.001) + { + _width = value; + OnPropertyChanged(); + } + } + } - /// - /// Получает или задает желаемую высоту элемента. - /// - /// - /// Высота в пикселях или относительных единицах. - /// - public double Height { get; set; } + public double Height + { + get => _height; + set + { + if (Math.Abs(_height - value) > 0.001) + { + _height = value; + OnPropertyChanged(); + } + } + } - /// - /// Получает минимально допустимую ширину элемента. - /// - /// - /// Минимальная ширина в пикселях, при которой элемент сохраняет функциональность. - /// - /// - /// Для группы минимальная ширина вычисляется как сумма минимальных ширин - /// дочерних элементов при горизонтальной ориентации или максимум минимальных - /// ширин при вертикальной ориентации. - /// public double MinWidth => Orientation == SplitDirection.Horizontal ? First.MinWidth + Second.MinWidth : Math.Max(First.MinWidth, Second.MinWidth); - /// - /// Получает минимально допустимую высоту элемента. - /// - /// - /// Минимальная высота в пикселях, при которой элемент сохраняет функциональность. - /// - /// - /// Для группы минимальная высота вычисляется как сумма минимальных высот - /// дочерних элементов при вертикальной ориентации или максимум минимальных - /// высот при горизонтальной ориентации. - /// public double MinHeight => Orientation == SplitDirection.Vertical ? First.MinHeight + Second.MinHeight : Math.Max(First.MinHeight, Second.MinHeight); - /// - /// Инициализирует новый экземпляр класса . - /// - /// - /// Первый дочерний элемент (левая или верхняя область). - /// - /// - /// Второй дочерний элемент (правая или нижняя область). - /// - /// - /// Направление разделения между дочерними элементами. - /// - /// - /// Уникальный идентификатор группы. Если не указан, генерируется новый GUID. - /// - /// - /// Выбрасывается, когда или - /// равны null. - /// - /// - /// Конструктор автоматически устанавливает свойство - /// у дочерних элементов на текущую группу и генерирует уникальный идентификатор, - /// если он не был предоставлен. - /// - public DockGroup(IDockElement first, IDockElement second, - SplitDirection orientation, string? id = null) + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) { - First = first ?? throw new ArgumentNullException(nameof(first)); - Second = second ?? throw new ArgumentNullException(nameof(second)); - Orientation = orientation; - Id = id ?? Guid.NewGuid().ToString(); - - First.Parent = this; - Second.Parent = this; - } - - /// - /// Вызывает событие . - /// - /// - /// Имя изменившегося свойства. Если не указано, определяется автоматически. - /// - protected void OnPropertyChanged([CallerMemberName] string? name = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } \ No newline at end of file diff --git a/Lattice.Core.Docking/Models/DockLeaf.cs b/Lattice.Core.Docking/Models/DockLeaf.cs index 793c79e..28f2732 100644 --- a/Lattice.Core.Docking/Models/DockLeaf.cs +++ b/Lattice.Core.Docking/Models/DockLeaf.cs @@ -5,85 +5,44 @@ using System.Runtime.CompilerServices; namespace Lattice.Core.Docking.Models; -/// -/// Представляет конечный узел (лист) дерева компоновки, который непосредственно -/// содержит коллекцию вкладок с контентом. Этот класс является контейнером для -/// отображаемого пользователю содержимого. -/// -/// -/// Лист является основным элементом, с которым взаимодействует пользователь -/// при работе с документами или инструментальными панелями в IDE-подобных -/// приложениях. -/// public class DockLeaf : IDockContainer, INotifyPropertyChanged { - /// - /// Происходит при изменении значения свойства. - /// - public event PropertyChangedEventHandler? PropertyChanged; - private readonly ObservableCollection _items = new(); private IDockContent? _activeContent; - private string _id; - private TabPlacement _tabPlacement = TabPlacement.Bottom; + private IDockElement? _parent; + private double _width; + private double _height; + private TabPlacement _tabPlacement = TabPlacement.Top; - /// - /// Получает или задает уникальный идентификатор листа. - /// - /// - /// Строковый идентификатор, уникальный в пределах дерева компоновки. - /// - public string Id + public event PropertyChangedEventHandler? PropertyChanged; + + public DockLeaf() { - get => _id; - internal set + _items.CollectionChanged += (s, e) => OnPropertyChanged(nameof(Children)); + } + + public string Id { get; } = Guid.NewGuid().ToString(); + + public IDockElement? Parent + { + get => _parent; + set { - if (_id != value) + if (_parent != value) { - _id = value; + _parent = value; OnPropertyChanged(); } } } - /// - /// Получает или задает родительский элемент в иерархии дерева компоновки. - /// - /// - /// Родительский элемент или null, если этот лист является корневым. - /// - public IDockElement? Parent { get; set; } - - /// - /// Получает список вкладок, содержащихся в данном контейнере. - /// - /// - /// Коллекция объектов, реализующих . - /// - /// - /// Эта коллекция является наблюдаемой (ObservableCollection), что позволяет - /// автоматически обновлять пользовательский интерфейс при добавлении или - /// удалении вкладок. - /// public IList Children => _items; - /// - /// Получает или задает активную (выбранную) вкладку в контейнере. - /// - /// - /// Активная вкладка или null, если в контейнере нет вкладок. - /// - /// - /// При установке нового значения проверяется, что вкладка действительно - /// содержится в коллекции . - /// Изменение этого свойства вызывает событие . - /// public IDockContent? ActiveContent { get => _activeContent; set { - if (value != null && !_items.Contains(value)) return; if (_activeContent != value) { _activeContent = value; @@ -92,48 +51,35 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged } } - /// - /// Получает или задает желаемую ширину элемента. - /// - /// - /// Ширина в пикселях или относительных единицах. - /// - public double Width { get; set; } + public double Width + { + get => _width; + set + { + if (Math.Abs(_width - value) > 0.001) + { + _width = value; + OnPropertyChanged(); + } + } + } - /// - /// Получает или задает желаемую высоту элемента. - /// - /// - /// Высота в пикселях или относительных единицах. - /// - public double Height { get; set; } + public double Height + { + get => _height; + set + { + if (Math.Abs(_height - value) > 0.001) + { + _height = value; + OnPropertyChanged(); + } + } + } - /// - /// Получает или задает минимально допустимую ширину элемента. - /// - /// - /// Минимальная ширина в пикселях. Значение по умолчанию: 100. - /// public double MinWidth { get; set; } = 100; - - /// - /// Получает или задает минимально допустимую высоту элемента. - /// - /// - /// Минимальная высота в пикселях. Значение по умолчанию: 100. - /// public double MinHeight { get; set; } = 100; - /// - /// Получает или задает положение полосы вкладок в контейнере. - /// - /// - /// Значение перечисления , определяющее, - /// где располагаются вкладки относительно содержимого. - /// - /// - /// Поддерживаются все четыре стороны: верх, низ, лево, право. - /// public TabPlacement TabPlacement { get => _tabPlacement; @@ -147,47 +93,10 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged } } - /// - /// Инициализирует новый экземпляр класса . - /// - /// - /// Уникальный идентификатор листа. Если не указан, генерируется новый GUID. - /// - /// - /// Создает пустой лист с коллекцией вкладок и генерирует уникальный - /// идентификатор, если он не был предоставлен. - /// - public DockLeaf(string? id = null) - { - _id = id ?? Guid.NewGuid().ToString(); - } - - /// - /// Вызывает событие . - /// - /// - /// Имя изменившегося свойства. Если не указано, определяется автоматически. - /// - protected void OnPropertyChanged([CallerMemberName] string? name = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); - } - - /// - /// Добавляет контент в контейнер и делает его активным. - /// - /// - /// Контент для добавления. - /// - /// - /// Если контент уже содержится в коллекции, он не добавляется повторно, - /// но становится активным. - /// Этот метод обновляет свойство и вызывает - /// соответствующее событие изменения свойства. - /// public void AddContent(IDockContent content) { - if (content == null) return; + if (content == null) + throw new ArgumentNullException(nameof(content)); if (!_items.Contains(content)) { @@ -196,22 +105,10 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged ActiveContent = content; } - /// - /// Удаляет контент из контейнера. - /// - /// - /// Контент для удаления. - /// - /// - /// Если удаляемый контент является активным, автоматически выбирается - /// новая активная вкладка (следующая в списке или предыдущая, если удалена - /// последняя). - /// Если после удаления контейнер становится пустым, он может быть удален - /// из дерева макета системой компоновки. - /// public void RemoveContent(IDockContent content) { - if (content == null) return; + if (content == null) + throw new ArgumentNullException(nameof(content)); int index = _items.IndexOf(content); if (index == -1) return; @@ -226,4 +123,9 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged ActiveContent = null; } } + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } } \ No newline at end of file diff --git a/Lattice.Core.Docking/Services/ContentRegistry.cs b/Lattice.Core.Docking/Services/ContentRegistry.cs index 0b49376..6fac68a 100644 --- a/Lattice.Core.Docking/Services/ContentRegistry.cs +++ b/Lattice.Core.Docking/Services/ContentRegistry.cs @@ -37,6 +37,10 @@ public class ContentRegistry if (factory == null) throw new ArgumentNullException(nameof(factory)); + // Дополнительная проверка на пустую строку + if (string.IsNullOrEmpty(contentTypeId.Trim())) + throw new ArgumentException("Идентификатор типа контента не может быть пустой строкой.", nameof(contentTypeId)); + if (_contentTypes.ContainsKey(contentTypeId)) throw new ArgumentException($"Тип контента '{contentTypeId}' уже зарегистрирован."); @@ -70,13 +74,7 @@ public class ContentRegistry throw new KeyNotFoundException($"Тип контента '{contentTypeId}' не зарегистрирован."); var content = descriptor.Factory(); - - // Устанавливаем ID через рефлексию, если есть свойство Id - var property = content.GetType().GetProperty("Id"); - if (property != null && property.CanWrite) - { - property.SetValue(content, id); - } + content.SetId(id); return content; } diff --git a/Lattice.Themes.Core/LatticeTokens.cs b/Lattice.Themes.Core/LatticeTokens.cs index b8bf665..1ce0bc9 100644 --- a/Lattice.Themes.Core/LatticeTokens.cs +++ b/Lattice.Themes.Core/LatticeTokens.cs @@ -1,4 +1,4 @@ -namespace Lattice.Themes.Core.Tokens; +namespace Lattice.Themes.Core; /// /// Статические ключи для ресурсов Lattice Framework. diff --git a/Lattice.Themes.Core/ThemeManager.cs b/Lattice.Themes.Core/ThemeManager.cs index f6b246d..e7273f2 100644 --- a/Lattice.Themes.Core/ThemeManager.cs +++ b/Lattice.Themes.Core/ThemeManager.cs @@ -1,22 +1,32 @@ -using Lattice.Themes.Core.Tokens; +using Lattice.Themes.Core; using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Media; namespace Lattice.Themes; /// -/// Менеджер тем для Lattice Framework. +/// Менеджер тем для Lattice Framework. Управляет регистрацией, применением и переключением тем оформления. +/// Предоставляет доступ к токенам темы и поддерживает динамическое обновление UI при смене темы. /// public sealed class ThemeManager { - public static ThemeManager Current { get; } = new(); + private static readonly ThemeManager _instance = new(); + + /// + /// Получает текущий экземпляр менеджера тем (синглтон). + /// + public static ThemeManager Current => _instance; private ThemePack? _currentTheme; private readonly Dictionary _registeredThemes = new(); + /// + /// Получает текущую активную тему. + /// public ThemePack? CurrentTheme => _currentTheme; + /// + /// Происходит при изменении текущей темы. + /// public event EventHandler? ThemeChanged; private ThemeManager() { } @@ -24,6 +34,8 @@ public sealed class ThemeManager /// /// Регистрирует тему в менеджере. /// + /// Тема для регистрации. + /// Выбрасывается, если равен null. public void RegisterTheme(ThemePack theme) { if (theme == null) @@ -35,6 +47,8 @@ public sealed class ThemeManager /// /// Получает зарегистрированную тему по имени. /// + /// Имя темы. + /// Зарегистрированная тема или null, если тема не найдена. public ThemePack? GetTheme(string name) { _registeredThemes.TryGetValue(name, out var theme); @@ -44,17 +58,18 @@ public sealed class ThemeManager /// /// Получает список всех зарегистрированных тем. /// + /// Неизменяемая коллекция зарегистрированных тем. public IReadOnlyCollection GetRegisteredThemes() { return _registeredThemes.Values.ToList(); } /// - /// Получение информации о теме. + /// Получает информацию о зарегистрированной теме. /// - /// - /// - public ThemeInfo GetThemeInfo(string themeName) + /// Имя темы. + /// Информация о теме или null, если тема не зарегистрирована. + public ThemeInfo? GetThemeInfo(string themeName) { if (!_registeredThemes.TryGetValue(themeName, out var theme)) return null; @@ -72,6 +87,9 @@ public sealed class ThemeManager /// /// Применяет тему по имени. /// + /// Имя темы для применения. + /// Выбрасывается, если тема с указанным именем не зарегистрирована. + /// Выбрасывается, если не удалось применить тему. public void ApplyTheme(string themeName) { if (!_registeredThemes.TryGetValue(themeName, out var theme)) @@ -85,6 +103,9 @@ public sealed class ThemeManager /// /// Применяет указанную тему. /// + /// Тема для применения. + /// Выбрасывается, если равен null. + /// Выбрасывается, если не удалось применить тему. public void ApplyTheme(ThemePack theme) { if (theme == null) @@ -93,21 +114,21 @@ public sealed class ThemeManager if (_currentTheme == theme) return; - var old = _currentTheme; + var oldTheme = _currentTheme; _currentTheme = theme; try { ReplaceApplicationResources(theme); - ThemeChanged?.Invoke(this, new ThemeChangedEventArgs(old!, theme)); + ThemeChanged?.Invoke(this, new ThemeChangedEventArgs(oldTheme!, theme)); } catch (Exception ex) { - // В случае ошибки возвращаемся к старой теме - _currentTheme = old; - if (old != null) + // Восстанавливаем предыдущую тему при ошибке + _currentTheme = oldTheme; + if (oldTheme != null) { - ReplaceApplicationResources(old); + ReplaceApplicationResources(oldTheme); } throw new InvalidOperationException($"Failed to apply theme '{theme.Name}'.", ex); } @@ -116,22 +137,24 @@ public sealed class ThemeManager /// /// Загружает ресурсы темы в указанный словарь ресурсов. /// + /// Целевой словарь ресурсов. + /// Тема, ресурсы которой нужно загрузить. + /// Выбрасывается, если или равны null. public void LoadThemeIntoDictionary(ResourceDictionary targetDictionary, ThemePack theme) { if (targetDictionary == null) throw new ArgumentNullException(nameof(targetDictionary)); - if (theme == null) throw new ArgumentNullException(nameof(theme)); - // Очищаем старые словари Lattice + // Удаляем все ThemeDictionary из словаря for (int i = targetDictionary.MergedDictionaries.Count - 1; i >= 0; i--) { if (targetDictionary.MergedDictionaries[i] is ThemeDictionary) targetDictionary.MergedDictionaries.RemoveAt(i); } - // Добавляем новые словари темы + // Добавляем словари темы foreach (var uri in theme.GetResourceUris()) { try @@ -146,6 +169,11 @@ public sealed class ThemeManager } } + /// + /// Подсчитывает количество токенов в теме. + /// + /// Тема для подсчета токенов. + /// Количество токенов в теме. Возвращает 0 при возникновении ошибки. private int CountTokensInTheme(ThemePack theme) { try @@ -160,6 +188,10 @@ public sealed class ThemeManager } } + /// + /// Заменяет ресурсы приложения на ресурсы указанной темы. + /// + /// Тема, ресурсы которой нужно применить. private void ReplaceApplicationResources(ThemePack theme) { var app = Application.Current; @@ -171,45 +203,32 @@ public sealed class ThemeManager ForceUpdateUI(); } + /// + /// Принудительно обновляет пользовательский интерфейс после смены темы. + /// Использует легковесный подход без рекурсивного обхода дерева элементов. + /// private void ForceUpdateUI() { foreach (var window in WindowTracker.Windows) { if (window.Content is FrameworkElement root) - RefreshElement(root); - } - } - - private void RefreshElement(FrameworkElement element) - { - var stack = new Stack(); - stack.Push(element); - - while (stack.Count > 0) - { - var current = stack.Pop(); - - // Пересоздаём Template только у Control - if (current is Control control) { - var template = control.Template; - control.Template = null; - control.Template = template; - } - else if (current is ContentPresenter contentPresenter) - { - // Обновляем ContentPresenter - var content = contentPresenter.Content; - contentPresenter.Content = null; - contentPresenter.Content = content; - } + // Перезагружаем ресурсы корневого элемента + var resources = root.Resources; + var currentTheme = _currentTheme; + if (currentTheme != null) + { + LoadThemeIntoDictionary(resources, currentTheme); + } - // Добавляем детей в стек - int count = VisualTreeHelper.GetChildrenCount(current); - for (int i = 0; i < count; i++) - { - if (VisualTreeHelper.GetChild(current, i) is FrameworkElement child) - stack.Push(child); + // Принудительное обновление стилей через перезагрузку ResourceDictionary + var mergedDictionaries = resources.MergedDictionaries; + if (mergedDictionaries.Count > 0) + { + var temp = mergedDictionaries[mergedDictionaries.Count - 1]; + mergedDictionaries.RemoveAt(mergedDictionaries.Count - 1); + mergedDictionaries.Add(temp); + } } } } @@ -217,6 +236,7 @@ public sealed class ThemeManager /// /// Проверяет, что все необходимые токены определены в текущей теме. /// + /// true, если все токены присутствуют; иначе false. public bool ValidateThemeTokens() { if (_currentTheme == null) @@ -249,6 +269,8 @@ public sealed class ThemeManager /// /// Получает значение токена из текущей темы. /// + /// Ключ токена. + /// Значение токена или null, если токен не найден или приложение не инициализировано. public object? GetTokenValue(string tokenKey) { var app = Application.Current; @@ -266,6 +288,9 @@ public sealed class ThemeManager /// /// Получает значение токена с приведением к указанному типу. /// + /// Тип, к которому приводится значение токена. + /// Ключ токена. + /// Значение токена или значение по умолчанию для типа T, если токен не найден. public T? GetTokenValue(string tokenKey) { object? value = GetTokenValue(tokenKey); @@ -278,16 +303,32 @@ public sealed class ThemeManager } /// -/// Информация о теме. +/// Предоставляет информацию о теме оформления. /// public class ThemeInfo { /// - /// Название темы. + /// Получает или задает название темы. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Получает или задает описание темы. + /// + public string Description { get; set; } = string.Empty; + + /// + /// Получает или задает версию темы. + /// + public string Version { get; set; } = string.Empty; + + /// + /// Получает или задает значение, указывающее, является ли тема темной. /// - public string Name { get; set; } - public string Description { get; set; } - public string Version { get; set; } public bool IsDark { get; set; } + + /// + /// Получает или задает количество токенов в теме. + /// public int TokenCount { get; set; } } \ No newline at end of file diff --git a/Lattice.UI.Docking.WinUI/Abstractions/IWinUIDragDropControl.cs b/Lattice.UI.Docking.WinUI/Abstractions/IWinUIDragDropControl.cs new file mode 100644 index 0000000..c334000 --- /dev/null +++ b/Lattice.UI.Docking.WinUI/Abstractions/IWinUIDragDropControl.cs @@ -0,0 +1,31 @@ +using Lattice.UI.Docking.Abstractions; +using Microsoft.UI.Xaml; + +namespace Lattice.UI.Docking.WinUI.Abstractions; + +/// +/// Интерфейс для элементов, поддерживающих WinUI Drag & Drop. +/// Наследуется от IDockControl и добавляет WinUI-специфичные возможности. +/// +public interface IWinUIDragDropControl : IDockControl +{ + /// + /// Получает UI-элемент для операций Drag & Drop. + /// + FrameworkElement? DragDropElement { get; } + + /// + /// Настраивает обработчики Drag & Drop. + /// + void SetupDragDropHandlers(); + + /// + /// Начинает операцию перетаскивания. + /// + void StartDrag(); + + /// + /// Завершает операцию перетаскивания. + /// + void EndDrag(); +} \ No newline at end of file diff --git a/Lattice.UI.Docking.WinUI/Controls/AdvancedTabControl.cs b/Lattice.UI.Docking.WinUI/Controls/AdvancedTabControl.cs new file mode 100644 index 0000000..3be407c --- /dev/null +++ b/Lattice.UI.Docking.WinUI/Controls/AdvancedTabControl.cs @@ -0,0 +1,189 @@ +using Lattice.Core.Docking.Models; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using System.Collections.ObjectModel; + +namespace Lattice.UI; + +/// +/// Представляет расширенный контрол вкладок с поддержкой всех позиций размещения панели вкладок. +/// Обеспечивает отображение коллекции вкладок с возможностью навигации, закрытия и изменения порядка. +/// Поддерживает четыре позиции размещения: сверху, снизу, слева и справа. +/// +public sealed class AdvancedTabControl : Control +{ + private Grid? _rootGrid; + private TabView? _tabView; + + /// + /// Инициализирует новый экземпляр класса . + /// + public AdvancedTabControl() + { + DefaultStyleKey = typeof(AdvancedTabControl); + } + + /// + /// Идентифицирует свойство зависимостей . + /// + public static readonly DependencyProperty ItemsSourceProperty = + DependencyProperty.Register(nameof(ItemsSource), typeof(ObservableCollection), + typeof(AdvancedTabControl), new PropertyMetadata(null)); + + /// + /// Идентифицирует свойство зависимостей . + /// + public static readonly DependencyProperty SelectedItemProperty = + DependencyProperty.Register(nameof(SelectedItem), typeof(object), + typeof(AdvancedTabControl), new PropertyMetadata(null)); + + /// + /// Идентифицирует свойство зависимостей . + /// + public static readonly DependencyProperty TabPlacementProperty = + DependencyProperty.Register(nameof(TabPlacement), typeof(TabPlacement), + typeof(AdvancedTabControl), new PropertyMetadata(TabPlacement.Top, OnTabPlacementChanged)); + + /// + /// Получает или задает источник данных для вкладок. + /// + public ObservableCollection ItemsSource + { + get => (ObservableCollection)GetValue(ItemsSourceProperty); + set => SetValue(ItemsSourceProperty, value); + } + + /// + /// Получает или задает выбранный элемент вкладки. + /// + public object SelectedItem + { + get => GetValue(SelectedItemProperty); + set => SetValue(SelectedItemProperty, value); + } + + /// + /// Получает или задает положение панели вкладок. + /// + public TabPlacement TabPlacement + { + get => (TabPlacement)GetValue(TabPlacementProperty); + set => SetValue(TabPlacementProperty, value); + } + + /// + /// Вызывается при применении шаблона контрола. + /// + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + _rootGrid = GetTemplateChild("PART_RootGrid") as Grid; + _tabView = GetTemplateChild("PART_TabView") as TabView; + + UpdateTabPlacement(); + } + + /// + /// Обновляет положение панели вкладок в соответствии с текущим значением свойства . + /// + private void UpdateTabPlacement() + { + if (_rootGrid == null) return; + + // Очищаем определения строк и столбцов + _rootGrid.RowDefinitions.Clear(); + _rootGrid.ColumnDefinitions.Clear(); + + switch (TabPlacement) + { + case TabPlacement.Top: + SetupTopPlacement(); + break; + case TabPlacement.Bottom: + SetupBottomPlacement(); + break; + case TabPlacement.Left: + SetupLeftPlacement(); + break; + case TabPlacement.Right: + SetupRightPlacement(); + break; + } + } + + /// + /// Настраивает размещение панели вкладок вверху. + /// + private void SetupTopPlacement() + { + _rootGrid!.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + _rootGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); + + if (_tabView != null) + { + Grid.SetRow(_tabView, 0); + Grid.SetColumn(_tabView, 0); + Grid.SetRowSpan(_tabView, 1); + } + } + + /// + /// Настраивает размещение панели вкладок внизу. + /// + private void SetupBottomPlacement() + { + _rootGrid!.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); + _rootGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + + if (_tabView != null) + { + Grid.SetRow(_tabView, 1); + Grid.SetColumn(_tabView, 0); + } + } + + /// + /// Настраивает размещение панели вкладок слева. + /// + private void SetupLeftPlacement() + { + _rootGrid!.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + _rootGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + + if (_tabView != null) + { + // Для вертикального размещения требуется специальный стиль + _tabView.Style = Application.Current.Resources["VerticalTabViewStyle"] as Style; + Grid.SetRow(_tabView, 0); + Grid.SetColumn(_tabView, 0); + } + } + + /// + /// Настраивает размещение панели вкладок справа. + /// + private void SetupRightPlacement() + { + _rootGrid!.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + _rootGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + if (_tabView != null) + { + _tabView.Style = Application.Current.Resources["VerticalTabViewStyle"] as Style; + Grid.SetRow(_tabView, 0); + Grid.SetColumn(_tabView, 1); + } + } + + /// + /// Обрабатывает изменение значения свойства . + /// + /// Объект зависимости, значение которого изменилось. + /// Данные о изменении свойства. + private static void OnTabPlacementChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is AdvancedTabControl control) + control.UpdateTabPlacement(); + } +} \ No newline at end of file diff --git a/Lattice.UI.Docking.WinUI/Controls/LatticeDockGroup.cs b/Lattice.UI.Docking.WinUI/Controls/LatticeDockGroup.cs index ad9b45b..58e54e1 100644 --- a/Lattice.UI.Docking.WinUI/Controls/LatticeDockGroup.cs +++ b/Lattice.UI.Docking.WinUI/Controls/LatticeDockGroup.cs @@ -10,23 +10,6 @@ using System.Runtime.CompilerServices; namespace Lattice.UI; -/// -/// Визуальный контрол для отображения группы разделения (сплиттера) в системе докинга. -/// Реализует интерфейс для интеграции с системой докинга -/// и обеспечивает отображение двух дочерних элементов с разделителем между ними. -/// -/// -/// -/// Контрол отвечает за визуальное представление узла -/// дерева компоновки, который разделяет доступное пространство между двумя дочерними -/// элементами. Поддерживает горизонтальное и вертикальное разделение с возможностью -/// изменения соотношения сторон через перетаскивание разделителя. -/// -/// -/// Контрол автоматически обновляет свое представление при изменении свойств модели -/// и обеспечивает двустороннюю привязку данных с объектом . -/// -/// public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable { private readonly PropertyChangedEventHandler _modelPropertyChangedHandler; @@ -42,14 +25,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable private double _splitRatio = 0.5; private double _splitterSize = 4.0; - /// - /// Инициализирует новый экземпляр класса . - /// - /// - /// Конструктор устанавливает ключ стиля по умолчанию, инициализирует обработчик - /// изменений модели и подписывается на событие изменения контекста данных. - /// Созданный контрол готов к использованию после применения шаблона. - /// public LatticeDockGroup() { this.DefaultStyleKey = typeof(LatticeDockGroup); @@ -57,18 +32,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable this.DataContextChanged += OnDataContextChanged; } - /// - /// Получает или задает модель данных, связанную с этим контролом. - /// - /// - /// Экземпляр , представляющий узел разделения в дереве компоновки. - /// Может быть null, если контрол не связан с моделью. - /// - /// - /// При установке новой модели контрол автоматически подписывается на события - /// изменения свойств модели и обновляет свое визуальное представление. - /// При удалении модели происходит отписка от событий и очистка ресурсов. - /// public IDockElement? Model { get => _model; @@ -82,18 +45,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable } } - /// - /// Получает или задает менеджер макета, к которому принадлежит этот контрол. - /// - /// - /// Экземпляр , управляющий структурой док-системы. - /// Может быть null, если контрол не связан с менеджером макета. - /// - /// - /// Менеджер макета используется для выполнения операций с деревом компоновки, - /// таких как перемещение элементов, создание плавающих окон и управление - /// автоскрываемыми панелями. - /// public LayoutManager? LayoutManager { get => _layoutManager; @@ -105,17 +56,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable } } - /// - /// Получает или задает контекстный менеджер для этого контрола. - /// - /// - /// Экземпляр или null, если менеджер не установлен. - /// - /// - /// Контекстный менеджер используется для отображения контекстных меню при щелчке - /// правой кнопкой мыши по контролу. Меню содержит команды, доступные для данного - /// элемента в текущем контексте. - /// public IDockContextManager? ContextManager { get => _contextManager; @@ -127,18 +67,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable } } - /// - /// Получает или задает признак того, что контрол выбран. - /// - /// - /// true, если контрол выбран; в противном случае false. - /// Значение по умолчанию: false. - /// - /// - /// Выделенный контрол обычно визуально отличается от других (например, имеет - /// выделенную границу или фон). В каждый момент времени может быть выделен - /// только один контрол в пределах контейнера. - /// public bool IsSelected { get => _isSelected; @@ -150,17 +78,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable } } - /// - /// Получает или задает признак того, что контрол активен. - /// - /// - /// true, если контрол активен; в противном случае false. - /// Значение по умолчанию: false. - /// - /// - /// Активный контрол получает фокус ввода и может обрабатывать команды клавиатуры. - /// Обычно соответствует последнему взаимодействию пользователя с элементом. - /// public bool IsActive { get => _isActive; @@ -172,20 +89,9 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable } } - /// - /// Получает или задает ориентацию разделения группы. - /// - /// - /// Направление разделения (горизонтальное или вертикальное). - /// - /// - /// Ориентация определяет, как расположены дочерние элементы относительно друг друга: - /// - /// - элементы расположены слева и справа - /// - элементы расположены сверху и снизу - /// - /// Изменение ориентации приводит к перестройке внутреннего макета контрола. - /// + public bool CanDrag => true; + public bool CanDrop => true; + public SplitDirection Orientation { get => _model?.Orientation ?? SplitDirection.Horizontal; @@ -199,18 +105,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable } } - /// - /// Получает или задает соотношение разделения между первым и вторым элементами. - /// - /// - /// Значение от 0.0 до 1.0, где 0.5 означает равное разделение пространства. - /// Значение 0.0 отдает все пространство второму элементу, 1.0 - первому элементу. - /// - /// - /// Соотношение разделения определяет пропорции, в которых доступное пространство - /// распределяется между дочерними элементами. Изменение этого свойства приводит - /// к перестройке внутреннего макета и генерации события . - /// public double SplitRatio { get => _splitRatio; @@ -221,24 +115,12 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable _splitRatio = value; UpdateLayoutDefinitions(); OnPropertyChanged(nameof(SplitRatio)); - SplitRatioChanged?.Invoke(this, new SplitRatioChangedEventArgs(value, SplitRatioChangeSource.Programmatic)); } } } - /// - /// Получает или задает размер разделителя в пикселях. - /// - /// - /// Ширина разделителя в пикселях. Значение по умолчанию: 4.0. - /// - /// - /// Размер разделителя определяет область, доступную для перетаскивания пользователем - /// для изменения соотношения разделения. Увеличение размера облегчает взаимодействие, - /// но уменьшает полезное пространство для содержимого. - /// public double SplitterSize { get => _splitterSize; @@ -252,57 +134,12 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable } } - /// - /// Получает контрол для первого дочернего элемента. - /// - /// - /// Контрол, отображающий первый дочерний элемент, или null, если элемент не установлен. - /// - /// - /// Первый дочерний элемент занимает левую область при горизонтальной ориентации - /// или верхнюю область при вертикальной ориентации. - /// public IDockControl? FirstChild => _firstChildControl?.Content as IDockControl; - - /// - /// Получает контрол для второго дочернего элемента. - /// - /// - /// Контрол, отображающий второй дочерний элемент, или null, если элемент не установлен. - /// - /// - /// Второй дочерний элемент занимает правую область при горизонтальной ориентации - /// или нижнюю область при вертикальной ориентации. - /// public IDockControl? SecondChild => _secondChildControl?.Content as IDockControl; - /// - /// Происходит при изменении соотношения разделения между дочерними элементами. - /// - /// - /// Событие генерируется при изменении свойства , - /// независимо от источника изменения (пользователь, программа или восстановление состояния). - /// Содержит информацию о новом соотношении и источнике изменения. - /// public event EventHandler? SplitRatioChanged; - - /// - /// Происходит при изменении значения свойства. - /// - /// - /// Событие реализует интерфейс и используется - /// для уведомления системы привязки данных об изменениях свойств контрола. - /// public event PropertyChangedEventHandler? PropertyChanged; - /// - /// Вызывается при применении шаблона контрола. - /// - /// - /// Метод получает ссылки на именованные части шаблона и инициализирует - /// внутренние структуры контрола. Вызывает обновление макета для корректного - /// отображения дочерних элементов. - /// protected override void OnApplyTemplate() { base.OnApplyTemplate(); @@ -314,48 +151,22 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable UpdateLayoutDefinitions(); } - /// - /// Обрабатывает изменение контекста данных контрола. - /// - /// Источник события (контрол). - /// Данные о изменении контекста. - /// - /// Метод автоматически устанавливает модель контрола на основе нового контекста данных, - /// если он является экземпляром . Это позволяет использовать - /// привязку данных XAML для установки модели контрола. - /// 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) @@ -365,16 +176,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable } } - /// - /// Обрабатывает изменения свойств модели. - /// - /// Источник события (модель). - /// Данные об изменении свойства. - /// - /// Реагирует на изменения ключевых свойств модели (Orientation, SplitRatio) - /// и обновляет соответствующие свойства и визуальное представление контрола. - /// Также уведомляет систему привязки данных об изменении свойств контрола. - /// private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e) { switch (e.PropertyName) @@ -395,14 +196,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable } } - /// - /// Обновляет определения макета сетки на основе текущей ориентации и соотношения разделения. - /// - /// - /// Метод перестраивает структуру строк и столбцов сетки в зависимости от ориентации - /// разделения и текущего соотношения между дочерними элементами. Обеспечивает - /// корректное позиционирование разделителя и дочерних контролов. - /// private void UpdateLayoutDefinitions() { if (_rootGrid == null || _model == null) return; @@ -412,7 +205,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable if (_model.Orientation == SplitDirection.Horizontal) { - // Горизонтальное разделение _rootGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(_model.SplitRatio, GridUnitType.Star) }); _rootGrid.ColumnDefinitions.Add(new ColumnDefinition @@ -420,7 +212,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable _rootGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1 - _model.SplitRatio, GridUnitType.Star) }); - // Устанавливаем позиции элементов if (_firstChildControl != null) { Grid.SetColumn(_firstChildControl, 0); @@ -435,7 +226,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable } else { - // Вертикальное разделение _rootGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(_model.SplitRatio, GridUnitType.Star) }); _rootGrid.RowDefinitions.Add(new RowDefinition @@ -443,7 +233,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable _rootGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1 - _model.SplitRatio, GridUnitType.Star) }); - // Устанавливаем позиции элементов if (_firstChildControl != null) { Grid.SetRow(_firstChildControl, 0); @@ -458,15 +247,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable } } - /// - /// Устанавливает дочерние контролы для отображения. - /// - /// Контрол для первого элемента. - /// Контрол для второго элемента. - /// - /// Метод назначает контролы для визуального представления дочерних элементов группы. - /// После установки контролов обновляет макет для корректного отображения. - /// public void SetChildren(IDockControl? firstChild, IDockControl? secondChild) { if (_firstChildControl != null) @@ -478,44 +258,27 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable UpdateLayoutDefinitions(); } - /// - /// Обновляет внешний вид контрола в соответствии с текущим состоянием модели. - /// - /// - /// Вызывает перестройку макета сетки для синхронизации визуального представления - /// с текущими значениями свойств модели (ориентация, соотношение разделения). - /// + public object? PrepareDragData() + { + return Model; + } + + public bool HandleDrop(object data, DockPosition position) + { + // TODO: Реализовать обработку сброса + return false; + } + public void Refresh() { UpdateLayoutDefinitions(); } - /// - /// Применяет указанную тему к контролу. - /// - /// Тема для применения. - /// - /// Обновляет стили и параметры отображения контрола в соответствии с заданной темой. - /// В текущей реализации метод является заглушкой и должен быть расширен для - /// поддержки динамического изменения тем оформления. - /// public void ApplyTheme(IDockTheme theme) { - // Применение темы к контролу - if (theme != null) - { - // TODO: Реализовать применение темы к стилям контрола - } + // TODO: Реализовать применение темы } - /// - /// Вызывается при изменении состояния модели для обновления UI. - /// - /// Имя изменившегося свойства модели. - /// - /// Перенаправляет вызов в обработчик изменений модели, обеспечивая уведомление - /// контрола о конкретных изменениях в связанной модели данных. - /// public void OnModelPropertyChanged(string propertyName) { if (_model != null) @@ -524,27 +287,11 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable } } - /// - /// Вызывает событие изменения свойства. - /// - /// Имя изменившегося свойства. - /// - /// Используется для уведомления системы привязки данных об изменениях свойств - /// контрола. Если имя свойства не указано, автоматически определяется по имени - /// вызывающего члена. - /// private void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } - /// - /// Освобождает ресурсы, используемые этим экземпляром контрола. - /// - /// - /// Выполняет отписку от событий модели, очистку ссылок и освобождение ресурсов. - /// После вызова этого метода контрол не должен использоваться. - /// public void Dispose() { if (!_disposed) diff --git a/Lattice.UI.Docking.WinUI/Controls/LatticeDockHost.cs b/Lattice.UI.Docking.WinUI/Controls/LatticeDockHost.cs index 179220a..6905252 100644 --- a/Lattice.UI.Docking.WinUI/Controls/LatticeDockHost.cs +++ b/Lattice.UI.Docking.WinUI/Controls/LatticeDockHost.cs @@ -1,10 +1,10 @@ using Lattice.Core.Docking.Abstractions; using Lattice.Core.Docking.Engine; using Lattice.Core.Docking.Models; -using Lattice.UI.Docking; using Lattice.UI.Docking.Abstractions; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -13,25 +13,6 @@ using System.Runtime.CompilerServices; namespace Lattice.UI; -/// -/// Представляет главный контейнер док-системы для WinUI, который служит корневым элементом -/// пользовательского интерфейса для размещения всех компонентов системы докинга. -/// Этот контрол управляет всем макетом приложения, включая основное дерево компоновки, -/// плавающие окна и автоскрываемые панели. -/// -/// -/// -/// является центральным координатором UI-слоя док-системы, -/// интегрирующим функциональность менеджера макета, системы перетаскивания и контекстных меню. -/// Он обеспечивает согласованное отображение всех элементов и обрабатывает пользовательские -/// взаимодействия на верхнем уровне. -/// -/// -/// Контрол реализует интерфейс и предоставляет полный набор методов -/// для управления структурой док-системы, включая создание/закрытие плавающих окон и -/// добавление/удаление автоскрываемых панелей. -/// -/// public sealed class LatticeDockHost : Control, IDockHost, IDisposable { private readonly PropertyChangedEventHandler _modelPropertyChangedHandler; @@ -50,13 +31,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable private bool _showMenu = true; private ContentControl? _rootContainer; - /// - /// Инициализирует новый экземпляр класса . - /// - /// - /// Конструктор устанавливает ключ стиля по умолчанию, инициализирует обработчик изменений модели - /// и подписывается на событие изменения контекста данных. - /// public LatticeDockHost() { this.DefaultStyleKey = typeof(LatticeDockHost); @@ -64,17 +38,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable this.DataContextChanged += OnDataContextChanged; } - /// - /// Получает или задает модель данных, связанную с этим контролом. - /// - /// - /// Экземпляр, реализующий , представляющий корневой элемент - /// дерева компоновки. Может быть null. - /// - /// - /// Этот элемент является корнем всего макета док-системы. При изменении этого свойства - /// происходит перестройка всего пользовательского интерфейса. - /// public IDockElement? Model { get => _model; @@ -88,16 +51,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable } } - /// - /// Получает или задает менеджер макета, к которому принадлежит этот контрол. - /// - /// - /// Экземпляр , управляющий структурой док-системы. - /// - /// - /// Менеджер макета используется для выполнения операций с деревом компоновки - /// и координации изменений между различными элементами системы. - /// public LayoutManager? LayoutManager { get => _layoutManager; @@ -109,16 +62,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable } } - /// - /// Получает или задает контекстный менеджер для этого контрола. - /// - /// - /// Экземпляр , управляющий контекстными меню и действиями. - /// - /// - /// Контекстный менеджер используется для отображения меню, связанных с этим элементом, - /// и выполнения команд, доступных в текущем контексте. - /// public IDockContextManager? ContextManager { get => _contextManager; @@ -130,16 +73,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable } } - /// - /// Получает или задает признак того, что контрол выбран. - /// - /// - /// true, если контрол выбран; в противном случае — false. - /// - /// - /// Выделение контрола обычно визуально выделяет его границы или фон, - /// чтобы указать пользователю на активный элемент. - /// public bool IsSelected { get => _isSelected; @@ -151,15 +84,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable } } - /// - /// Получает или задает признак того, что контрол активен. - /// - /// - /// true, если контрол активен; в противном случае — false. - /// - /// - /// Активный контрол обычно получает фокус ввода и может обрабатывать команды клавиатуры. - /// public bool IsActive { get => _isActive; @@ -171,16 +95,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable } } - /// - /// Получает или задает признак того, что контрол можно перетаскивать. - /// - /// - /// true, если контрол можно перетаскивать; в противном случае — false. - /// - /// - /// Этот флаг влияет на возможность инициирования операции перетаскивания - /// при взаимодействии пользователя с этим контролом. - /// public bool CanDrag { get => _canDrag; @@ -192,16 +106,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable } } - /// - /// Получает или задает признак того, что контрол может принимать сброс. - /// - /// - /// true, если контрол может принимать сброс; в противном случае — false. - /// - /// - /// Этот флаг влияет на возможность завершения операции перетаскивания - /// сбросом данных на этот контрол. - /// public bool CanDrop { get => _canDrop; @@ -213,42 +117,9 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable } } - /// - /// Получает коллекцию контролов плавающих окон, связанных с этим хостом. - /// - /// - /// Коллекция объектов, реализующих , - /// представляющих все активные плавающие окна в системе. - /// - /// - /// Коллекция является наблюдаемой (ObservableCollection), что позволяет автоматически - /// обновлять пользовательский интерфейс при добавлении или удалении окон. - /// public IEnumerable FloatingWindows => _floatingWindows; - - /// - /// Получает коллекцию контролов автоскрываемых панелей, прикрепленных к краям окна. - /// - /// - /// Коллекция объектов, реализующих , - /// представляющих автоскрываемые панели на разных сторонах окна. - /// - /// - /// Коллекция является наблюдаемой (ObservableCollection), что позволяет автоматически - /// обновлять пользовательский интерфейс при добавлении или удалении панелей. - /// public IEnumerable AutoHidePanels => _autoHidePanels; - /// - /// Получает или задает значение, указывающее, отображается ли панель инструментов (Toolbox). - /// - /// - /// true, если панель инструментов видима; в противном случае — false. - /// - /// - /// Панель инструментов обычно содержит элементы для быстрого доступа к командам - /// или создания новых компонентов в приложении. - /// public bool ShowToolbox { get => _showToolbox; @@ -260,16 +131,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable } } - /// - /// Получает или задает значение, указывающее, отображается ли строка состояния. - /// - /// - /// true, если строка состояния видима; в противном случае — false. - /// - /// - /// Строка состояния обычно отображает текущий статус приложения, - /// информацию о выбранном элементе или прогресс выполнения операций. - /// public bool ShowStatusBar { get => _showStatusBar; @@ -281,15 +142,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable } } - /// - /// Получает или задает значение, указывающее, отображается ли главное меню приложения. - /// - /// - /// true, если главное меню видимо; в противном случае — false. - /// - /// - /// Главное меню содержит основные команды приложения, организованные в иерархическую структуру. - /// public bool ShowMenu { get => _showMenu; @@ -301,41 +153,43 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable } } - /// - /// Событие, возникающее при изменении структуры макета док-системы. - /// - /// - /// Может вызываться при добавлении/удалении элементов, изменении размеров, - /// создании/закрытии плавающих окон и других операциях, влияющих на компоновку. - /// public event EventHandler? LayoutChanged; - - /// - /// Событие, возникающее при создании нового плавающего окна. - /// public event EventHandler? FloatingWindowCreated; - - /// - /// Событие, возникающее при закрытии плавающего окна. - /// public event EventHandler? FloatingWindowClosed; - - /// - /// Событие, возникающее при изменении значения свойства. - /// public event PropertyChangedEventHandler? PropertyChanged; - /// - /// Вызывается при применении шаблона контрола. - /// - /// - /// Метод получает ссылки на именованные части шаблона и обновляет отображение - /// корневого содержимого в соответствии с текущим состоянием модели. - /// + /// + public FrameworkElement? DragDropElement => this; + + /// + public void SetupDragDropHandlers() + { + this.AllowDrop = true; + this.CanDrag = true; + + // Настройка обработчиков для хоста + this.Drop += OnHostDrop; + this.DragOver += OnHostDragOver; + } + + /// + public void StartDrag() { /* Реализация */ } + + /// + public void EndDrag() { /* Реализация */ } + + private void OnHostDragOver(object sender, DragEventArgs args) + { + args.AcceptedOperation = CanDrop ? + Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move : + Windows.ApplicationModel.DataTransfer.DataPackageOperation.None; + args.DragUIOverride.IsGlyphVisible = true; + args.DragUIOverride.Caption = "Закрепить здесь"; + } + protected override void OnApplyTemplate() { base.OnApplyTemplate(); - _rootContainer = GetTemplateChild("PART_RootContainer") as ContentControl; UpdateRootContent(); } @@ -349,11 +203,8 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable { if (_model != null && _layoutManager != null) { - // Подписываемся на события менеджера макета _layoutManager.LayoutUpdated += OnLayoutUpdated; _layoutManager.AutoHidePanelsChanged += OnAutoHidePanelsChanged; - - // Устанавливаем DataContext this.DataContext = _model; UpdateRootContent(); } @@ -363,17 +214,14 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable { if (_model != null && _layoutManager != null) { - // Отписываемся от событий _layoutManager.LayoutUpdated -= OnLayoutUpdated; _layoutManager.AutoHidePanelsChanged -= OnAutoHidePanelsChanged; - this.DataContext = null; } } private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e) { - // Обработка изменений модели OnPropertyChanged(e.PropertyName); } @@ -385,7 +233,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable private void OnAutoHidePanelsChanged(object? sender, EventArgs e) { - // Обновление автоскрываемых панелей OnPropertyChanged(nameof(AutoHidePanels)); } @@ -393,8 +240,7 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable { if (_rootContainer != null && _model != null && _layoutManager != null) { - // Создаем дерево контролов через фабрику - var factory = LatticeUIFramework.ControlFactory; + var factory = Lattice.UI.Docking.LatticeUIFramework.ControlFactory; if (factory != null) { var control = factory.CreateControlForElement(_model); @@ -403,157 +249,44 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable } } - /// - /// Создает новое плавающее окно для размещения указанного элемента док-системы. - /// - /// - /// Элемент док-системы (группа или лист), который будет размещен в плавающем окне. - /// - /// Заголовок создаваемого окна. - /// - /// Экземпляр , представляющий созданное плавающее окно. - /// - /// - /// Выбрасывается, если равен null. - /// - /// - /// Выбрасывается, так как метод еще не реализован. - /// - /// - /// Созданное окно может быть перемещено пользователем в любое место экрана, - /// изменено в размерах и обычно содержит стандартные элементы управления окном - /// (заголовок, кнопки закрытия/сворачивания). - /// public IFloatingWindowControl CreateFloatingWindow(IDockElement element, string title) { - if (element == null) throw new ArgumentNullException(nameof(element)); - - // TODO: Реализовать создание плавающего окна через фабрику - throw new NotImplementedException(); + throw new NotImplementedException("Floating windows not implemented yet"); } - /// - /// Закрывает указанное плавающее окно и возвращает его содержимое в основной макет. - /// - /// - /// Плавающее окно, которое необходимо закрыть. - /// - /// - /// Выбрасывается, если равен null. - /// - /// - /// При закрытии плавающего окна его содержимое обычно возвращается в то место - /// в основном макете, откуда оно было извлечено, или в ближайшую допустимую позицию. - /// public void CloseFloatingWindow(IFloatingWindowControl window) { - if (window == null) throw new ArgumentNullException(nameof(window)); - if (_floatingWindows.Remove(window)) { FloatingWindowClosed?.Invoke(this, new FloatingWindowClosedEventArgs(window)); } } - /// - /// Добавляет автоскрываемую панель с указанным содержимым к заданной стороне окна. - /// - /// - /// Контент, который будет отображаться в автоскрываемой панели. - /// - /// - /// Сторона окна, к которой будет прикреплена панель. - /// - /// - /// Экземпляр , представляющий созданную панель. - /// - /// - /// Выбрасывается, если равен null. - /// - /// - /// Выбрасывается, если свойство не установлено. - /// - /// - /// Выбрасывается, так как метод еще не реализован. - /// - /// - /// Автоскрываемые панели полезны для инструментов, к которым нужен частый, - /// но не постоянный доступ, так как они экономят пространство экрана. - /// public IAutoHidePanelControl AddAutoHidePanel(Core.Docking.Abstractions.IDockContent content, DockSide side) { - if (content == null) throw new ArgumentNullException(nameof(content)); - if (_layoutManager != null) { var panel = _layoutManager.AddAutoHidePanel(content, side); - - // TODO: Создать UI-контрол для автоскрываемой панели через фабрику - throw new NotImplementedException(); + throw new NotImplementedException("Auto-hide panels not implemented yet"); } - throw new InvalidOperationException("LayoutManager is not set"); } - /// - /// Удаляет автоскрываемую панель из интерфейса. - /// - /// - /// Автоскрываемая панель, которую необходимо удалить. - /// - /// - /// Выбрасывается, если равен null. - /// - /// - /// Выбрасывается, так как метод еще не реализован. - /// - /// - /// После удаления панели её содержимое обычно либо закрывается полностью, - /// либо преобразуется в обычную закрепленную панель, в зависимости от настроек. - /// public void RemoveAutoHidePanel(IAutoHidePanelControl panel) { - if (panel == null) throw new ArgumentNullException(nameof(panel)); - - // TODO: Реализовать удаление автоскрываемой панели - throw new NotImplementedException(); + throw new NotImplementedException("Auto-hide panels not implemented yet"); } - /// - /// Обновляет внешний вид контрола в соответствии с текущим состоянием модели. - /// - /// - /// Вызывает обновление корневого содержимого и всех дочерних элементов. - /// - public void Refresh() - { - UpdateRootContent(); - } + public object? PrepareDragData() => Model; + public bool HandleDrop(object data, DockPosition position) => false; + + public void Refresh() => UpdateRootContent(); - /// - /// Применяет указанную тему к контролу. - /// - /// Тема для применения. - /// - /// В текущей реализации метод является заглушкой и должен быть расширен - /// для поддержки динамического изменения тем. - /// public void ApplyTheme(IDockTheme theme) { - // Применение темы к контролу - if (theme != null) - { - // TODO: Реализовать применение темы к стилям контрола - } + // TODO: Реализовать применение темы } - /// - /// Вызывается при изменении состояния модели для обновления UI. - /// - /// Имя изменившегося свойства модели. - /// - /// Перенаправляет вызов в обработчик изменений модели. - /// public void OnModelPropertyChanged(string propertyName) { if (_model != null) @@ -567,24 +300,84 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } - /// - /// Освобождает ресурсы, используемые этим экземпляром контрола. - /// - /// - /// Выполняет отписку от событий модели, очистку коллекций и освобождение ресурсов. - /// public void Dispose() { if (!_disposed) { DetachModel(); - - // Очищаем коллекции _floatingWindows.Clear(); _autoHidePanels.Clear(); - _disposed = true; GC.SuppressFinalize(this); } } + + private DockPosition GetDropPosition(Windows.Foundation.Point point) + { + if (ActualWidth <= 0 || ActualHeight <= 0) + return DockPosition.Center; + + var relativeX = point.X / ActualWidth; + var relativeY = point.Y / ActualHeight; + + // Определяем регионы для докирования + const double edgeThreshold = 0.2; // 20% от краев + const double centerThreshold = 0.4; // Центральная область + + // Проверяем края + if (relativeX < edgeThreshold) return DockPosition.Left; + if (relativeX > (1 - edgeThreshold)) return DockPosition.Right; + if (relativeY < edgeThreshold) return DockPosition.Top; + if (relativeY > (1 - edgeThreshold)) return DockPosition.Bottom; + + // Если в центральной области + if (relativeX > centerThreshold && relativeX < (1 - centerThreshold) && + relativeY > centerThreshold && relativeY < (1 - centerThreshold)) + { + return DockPosition.Center; + } + + // По умолчанию - центр + return DockPosition.Center; + } + + private void OnHostDrop(object sender, DragEventArgs args) + { + if (CanDrop && args.DataView.Properties.TryGetValue("LatticeDockElement", out var data)) + { + // Получаем позицию сброса + var position = GetDropPosition(args.GetPosition(this)); + + // Определяем целевой элемент + IDockElement? target = null; + if (args.OriginalSource is FrameworkElement element) + { + // Находим соответствующий контрол докинга + var dockControl = FindDockControl(element); + target = dockControl?.Model; + } + + // Если цель не найдена, используем корневой элемент + target ??= LayoutManager?.Root; + + if (data is IDockElement source && target != null) + { + LayoutManager?.Move(source, target, position); + } + } + } + + private IDockControl? FindDockControl(FrameworkElement element) + { + // Поднимаемся по дереву элементов, чтобы найти контрол докинга + var current = element; + while (current != null) + { + if (current is IDockControl dockControl) + return dockControl; + + current = VisualTreeHelper.GetParent(current) as FrameworkElement; + } + return null; + } } \ No newline at end of file diff --git a/Lattice.UI.Docking.WinUI/Controls/LatticeDockLeaf.cs b/Lattice.UI.Docking.WinUI/Controls/LatticeDockLeaf.cs index 7485588..eb598ad 100644 --- a/Lattice.UI.Docking.WinUI/Controls/LatticeDockLeaf.cs +++ b/Lattice.UI.Docking.WinUI/Controls/LatticeDockLeaf.cs @@ -1,40 +1,529 @@ -using Lattice.Core.Docking.Models; +using Lattice.Core.Docking.Abstractions; +using Lattice.Core.Docking.Engine; +using Lattice.Core.Docking.Models; +using Lattice.UI.Docking.Abstractions; +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using System; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; namespace Lattice.UI; -/// -/// Визуальное представление контейнера вкладок с поддержкой нижнего расположения. -/// -public class LatticeDockLeaf : Control +public sealed class LatticeDockLeaf : Control, IDockLeafControl, IDisposable { + private readonly PropertyChangedEventHandler _modelPropertyChangedHandler; + private bool _disposed; + private DockLeaf? _model; + private Grid? _rootGrid; + private ListBox? _tabHeaderList; + private ContentControl? _contentControl; + private LayoutManager? _layoutManager; + private IDockContextManager? _contextManager; + private bool _isSelected; + private bool _isActive; + private TabPlacement _tabPlacement = TabPlacement.Top; + private bool _showCloseButtons = true; + private bool _canReorderTabs = true; + public LatticeDockLeaf() { this.DefaultStyleKey = typeof(LatticeDockLeaf); + _modelPropertyChangedHandler = OnModelPropertyChanged; + this.DataContextChanged += OnDataContextChanged; } + public IDockElement? Model + { + get => _model; + set + { + if (_model == value) return; + DetachModel(); + _model = value as DockLeaf; + AttachModel(); + OnPropertyChanged(nameof(Model)); + } + } + + public LayoutManager? LayoutManager + { + get => _layoutManager; + set + { + if (_layoutManager == value) return; + _layoutManager = value; + OnPropertyChanged(nameof(LayoutManager)); + } + } + + public IDockContextManager? ContextManager + { + get => _contextManager; + set + { + if (_contextManager == value) return; + _contextManager = value; + OnPropertyChanged(nameof(ContextManager)); + } + } + + public bool IsSelected + { + get => _isSelected; + set + { + if (_isSelected == value) return; + _isSelected = value; + OnPropertyChanged(nameof(IsSelected)); + } + } + + public bool IsActive + { + get => _isActive; + set + { + if (_isActive == value) return; + _isActive = value; + OnPropertyChanged(nameof(IsActive)); + } + } + + public bool CanDrag => true; + public bool CanDrop => true; + + public TabPlacement TabPlacement + { + get => _tabPlacement; + set + { + if (_tabPlacement != value) + { + _tabPlacement = value; + UpdateTabPlacement(); + OnPropertyChanged(nameof(TabPlacement)); + } + } + } + + public bool ShowCloseButtons + { + get => _showCloseButtons; + set + { + if (_showCloseButtons != value) + { + _showCloseButtons = value; + OnPropertyChanged(nameof(ShowCloseButtons)); + UpdateTabHeaders(); + } + } + } + + public bool CanReorderTabs + { + get => _canReorderTabs; + set + { + if (_canReorderTabs != value) + { + _canReorderTabs = value; + OnPropertyChanged(nameof(CanReorderTabs)); + } + } + } + + public IDockContent? ActiveContent + { + get => _model?.ActiveContent; + set + { + if (_model != null) + { + _model.ActiveContent = value; + } + } + } + + public object? PrepareDragData() => Model; + public bool HandleDrop(object data, DockPosition position) => false; + + public event EventHandler? 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; + } + + this.DataContext = _model; + _tabPlacement = _model.TabPlacement; + UpdateTabHeaders(); + UpdateTabPlacement(); + } + } + + private void DetachModel() + { + if (_model != null) + { + _model.PropertyChanged -= _modelPropertyChangedHandler; + + if (_model.Children is INotifyCollectionChanged notifyCollection) + { + notifyCollection.CollectionChanged -= OnChildrenCollectionChanged; + } + + this.DataContext = null; + } + } + + private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case nameof(DockLeaf.TabPlacement): + _tabPlacement = _model?.TabPlacement ?? TabPlacement.Top; + OnPropertyChanged(nameof(TabPlacement)); + UpdateTabPlacement(); + break; + + case nameof(DockLeaf.ActiveContent): + OnPropertyChanged(nameof(ActiveContent)); + UpdateSelectedTab(); + break; + + case nameof(DockLeaf.Children): + UpdateTabHeaders(); + break; + } + } + + private void OnChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + UpdateTabHeaders(); + } + + private void UpdateTabPlacement() + { + if (_rootGrid == null || _model == null) return; + + _rootGrid.RowDefinitions.Clear(); + _rootGrid.ColumnDefinitions.Clear(); + + switch (_model.TabPlacement) + { + case TabPlacement.Top: + _rootGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + _rootGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); + UpdateHeaderListOrientation(Orientation.Horizontal); + break; + + case TabPlacement.Bottom: + _rootGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); + _rootGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + UpdateHeaderListOrientation(Orientation.Horizontal); + break; + + case TabPlacement.Left: + _rootGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + _rootGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + UpdateHeaderListOrientation(Orientation.Vertical); + break; + + case TabPlacement.Right: + _rootGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + _rootGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + UpdateHeaderListOrientation(Orientation.Vertical); + break; + } + + UpdateElementPositions(); + } + + private void UpdateHeaderListOrientation(Orientation orientation) + { + if (_tabHeaderList?.ItemsPanelRoot is StackPanel stackPanel) + { + stackPanel.Orientation = orientation; + } + } + + private void UpdateElementPositions() + { + if (_rootGrid == null || _tabHeaderList == null || _contentControl == null) return; + + switch (_model?.TabPlacement) + { + case TabPlacement.Top: + Grid.SetRow(_tabHeaderList, 0); + Grid.SetRow(_contentControl, 1); + Grid.SetColumn(_tabHeaderList, 0); + Grid.SetColumn(_contentControl, 0); + break; + + case TabPlacement.Bottom: + Grid.SetRow(_contentControl, 0); + Grid.SetRow(_tabHeaderList, 1); + Grid.SetColumn(_contentControl, 0); + Grid.SetColumn(_tabHeaderList, 0); + break; + + case TabPlacement.Left: + Grid.SetColumn(_tabHeaderList, 0); + Grid.SetColumn(_contentControl, 1); + Grid.SetRow(_tabHeaderList, 0); + Grid.SetRow(_contentControl, 0); + break; + + case TabPlacement.Right: + Grid.SetColumn(_contentControl, 0); + Grid.SetColumn(_tabHeaderList, 1); + Grid.SetRow(_contentControl, 0); + Grid.SetRow(_tabHeaderList, 0); + break; + } + } + + private void UpdateTabHeaders() + { + if (_tabHeaderList == null || _model == null) return; + + _tabHeaderList.Items.Clear(); + + foreach (var content in _model.Children) + { + var item = CreateTabHeaderItem(content); + _tabHeaderList.Items.Add(item); + } + + UpdateSelectedTab(); + } + + private ListBoxItem CreateTabHeaderItem(IDockContent content) + { + var item = new ListBoxItem + { + Content = CreateTabHeaderContent(content), + Tag = content, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + + item.PointerPressed += (sender, e) => + { + if (e.GetCurrentPoint(item).Properties.IsLeftButtonPressed) + { + ActiveContent = content; + } + }; + + return item; + } + + private UIElement CreateTabHeaderContent(IDockContent content) + { + var grid = new Grid(); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + var textBlock = new TextBlock + { + Text = content.Title, + Margin = new Thickness(8, 4, 8, 4), + VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(textBlock, 0); + grid.Children.Add(textBlock); + + if (_showCloseButtons && content.CanClose) + { + var closeButton = new Button + { + Content = "×", + FontSize = 16, + Width = 24, + Height = 24, + Margin = new Thickness(2), + Padding = new Thickness(0), + Background = new SolidColorBrush(Microsoft.UI.Colors.Transparent), + BorderThickness = new Thickness(0) + }; + + closeButton.Click += (sender, e) => + { + CloseContent(content); + }; + + Grid.SetColumn(closeButton, 1); + grid.Children.Add(closeButton); + } + + return grid; + } + + private void UpdateSelectedTab() + { + if (_tabHeaderList == null || _model == null) return; + + foreach (var item in _tabHeaderList.Items) + { + if (item is ListBoxItem listBoxItem && listBoxItem.Tag is IDockContent content) + { + listBoxItem.IsSelected = content == _model.ActiveContent; + } + } + + if (_contentControl != null) + { + _contentControl.Content = _model.ActiveContent?.View; + } + } + + private void OnTabSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (_tabHeaderList?.SelectedItem is ListBoxItem selectedItem && + selectedItem.Tag is IDockContent content) + { + var oldContent = ActiveContent; + ActiveContent = content; + + if (oldContent != content) + { + ActiveContentChanged?.Invoke(this, + new ActiveContentChangedEventArgs(oldContent, content)); + } + } + } + + public void AddContent(IDockContent content) + { + if (_model != null && !_model.Children.Contains(content)) + { + _model.AddContent(content); + UpdateTabHeaders(); + } + } + + public void RemoveContent(IDockContent content) + { + if (_model != null && _model.Children.Contains(content)) + { + _model.RemoveContent(content); + UpdateTabHeaders(); + } + } + + public bool CloseContent(IDockContent content) + { + var args = new ContentClosingEventArgs(content); + ContentClosing?.Invoke(this, args); + + if (!args.Cancel) + { + RemoveContent(content); + return true; + } + + return false; + } + + public void CloseAllExcept(IDockContent exceptContent) + { + if (_model == null) return; + + var itemsToClose = _model.Children + .Where(c => c != exceptContent) + .ToList(); + + foreach (var content in itemsToClose) + { + CloseContent(content); + } + } + + public void CloseAll() + { + if (_model == null) return; + + var itemsToClose = _model.Children.ToList(); + foreach (var content in itemsToClose) + { + CloseContent(content); + } + } + + public void Refresh() + { + UpdateTabHeaders(); UpdateTabPlacement(); } - /// - /// Настраивает внутреннюю структуру TabView для отображения вкладок снизу. - /// - private void UpdateTabPlacement() + public void ApplyTheme(IDockTheme theme) { - var tabView = GetTemplateChild("PART_TabView") as TabView; - if (tabView == null || DataContext is not DockLeaf leaf) return; + // TODO: Реализовать применение темы + } - // Вместо сложной манипуляции с визуальным деревом, используем встроенные свойства TabView - if (leaf.TabPlacement == TabPlacement.Bottom) + public void OnModelPropertyChanged(string propertyName) + { + if (_model != null) { - // К сожалению, TabView в WinUI не поддерживает TabStripPlacement - // Это ограничение платформы, нужно либо использовать другой контрол, - // либо реализовать кастомный TabControl с поддержкой нижнего расположения - // Временно оставляем как есть с заглушкой - System.Diagnostics.Debug.WriteLine("TabPlacement.Bottom is not fully supported in WinUI TabView"); + OnModelPropertyChanged(_model, new PropertyChangedEventArgs(propertyName)); + } + } + + private void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public void Dispose() + { + if (!_disposed) + { + DetachModel(); + + if (_tabHeaderList != null) + { + _tabHeaderList.SelectionChanged -= OnTabSelectionChanged; + } + + _disposed = true; + GC.SuppressFinalize(this); } } } \ No newline at end of file diff --git a/Lattice.UI.Docking.WinUI/Controls/LatticeSplitter.cs b/Lattice.UI.Docking.WinUI/Controls/LatticeSplitter.cs index 2f0a7b7..057f2a4 100644 --- a/Lattice.UI.Docking.WinUI/Controls/LatticeSplitter.cs +++ b/Lattice.UI.Docking.WinUI/Controls/LatticeSplitter.cs @@ -1,14 +1,27 @@ -using Lattice.Core.Docking.Models; +using Lattice.Core.Docking.Abstractions; +using Lattice.Core.Docking.Engine; +using Lattice.Core.Docking.Models; +using Lattice.UI.Docking.Abstractions; using Microsoft.UI.Input; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media; using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; namespace Lattice.UI; -public class LatticeSplitter : Control +public sealed class LatticeSplitter : Control, IDockSplitterControl, IDisposable { + private bool _disposed; + private IDockElement? _model; + private LayoutManager? _layoutManager; + private IDockContextManager? _contextManager; + private bool _isSelected; + private bool _isActive; + private bool _isDragging; + public LatticeSplitter() { this.DefaultStyleKey = typeof(LatticeSplitter); @@ -17,17 +30,116 @@ public class LatticeSplitter : Control this.PointerEntered += (s, e) => this.ProtectedCursor = InputSystemCursor.Create(InputSystemCursorShape.SizeWestEast); this.PointerExited += (s, e) => - this.ProtectedCursor = null; + this.ProtectedCursor = InputSystemCursor.Create(InputSystemCursorShape.Arrow); this.ManipulationDelta += OnManipulationDelta; + this.ManipulationStarted += (s, e) => + { + IsDragging = true; + DragStarted?.Invoke(this, EventArgs.Empty); + }; + this.ManipulationCompleted += (s, e) => + { + IsDragging = false; + DragCompleted?.Invoke(this, EventArgs.Empty); + }; } + public IDockElement? Model + { + get => _model; + set + { + if (_model != value) + { + _model = value; + OnPropertyChanged(nameof(Model)); + } + } + } + + public LayoutManager? LayoutManager + { + get => _layoutManager; + set + { + if (_layoutManager != value) + { + _layoutManager = value; + OnPropertyChanged(nameof(LayoutManager)); + } + } + } + + public IDockContextManager? ContextManager + { + get => _contextManager; + set + { + if (_contextManager != value) + { + _contextManager = value; + OnPropertyChanged(nameof(ContextManager)); + } + } + } + + public bool IsSelected + { + get => _isSelected; + set + { + if (_isSelected != value) + { + _isSelected = value; + OnPropertyChanged(nameof(IsSelected)); + } + } + } + + public bool IsActive + { + get => _isActive; + set + { + if (_isActive != value) + { + _isActive = value; + OnPropertyChanged(nameof(IsActive)); + } + } + } + + public bool CanDrag => false; + public bool CanDrop => false; + + public object? PrepareDragData() => null; + public bool HandleDrop(object data, DockPosition position) => false; + + public Core.Docking.Models.SplitDirection Orientation { get; set; } + + public bool IsDragging + { + get => _isDragging; + set + { + if (_isDragging != value) + { + _isDragging = value; + OnPropertyChanged(nameof(IsDragging)); + } + } + } + + public event EventHandler? DragStarted; + public event EventHandler? DragDelta; + public event EventHandler? DragCompleted; + public event PropertyChangedEventHandler? PropertyChanged; + private void OnManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e) { - // 1. Находим модель DockGroup через DataContext if (this.DataContext is not DockGroup group) return; - // 2. Находим родительский Grid, чтобы знать общие размеры if (VisualTreeHelper.GetParent(this) is not Grid parentGrid || parentGrid.ActualWidth <= 0 || parentGrid.ActualHeight <= 0) return; @@ -38,14 +150,35 @@ public class LatticeSplitter : Control if (totalSize <= 0) return; - // 3. Вычисляем изменение Ratio (от -1.0 до 1.0) double delta = group.Orientation == SplitDirection.Horizontal ? e.Delta.Translation.X : e.Delta.Translation.Y; double ratioChange = delta / totalSize; - - // 4. Обновляем модель (с ограничением от 0.05 до 0.95) group.SplitRatio = Math.Clamp(group.SplitRatio + ratioChange, 0.05, 0.95); + + DragDelta?.Invoke(this, new SplitterDraggedEventArgs( + group.Orientation == SplitDirection.Horizontal ? delta : 0, + group.Orientation == SplitDirection.Vertical ? delta : 0)); + } + + public void Refresh() { } + + public void ApplyTheme(IDockTheme theme) { } + + public void OnModelPropertyChanged(string propertyName) { } + + private void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + GC.SuppressFinalize(this); + } } } \ No newline at end of file diff --git a/Lattice.UI.Docking.WinUI/Controls/LatticeTabControl.cs b/Lattice.UI.Docking.WinUI/Controls/LatticeTabControl.cs index 8b7c2fa..8803974 100644 --- a/Lattice.UI.Docking.WinUI/Controls/LatticeTabControl.cs +++ b/Lattice.UI.Docking.WinUI/Controls/LatticeTabControl.cs @@ -13,21 +13,6 @@ using System.Runtime.CompilerServices; namespace Lattice.UI; -/// -/// Представляет кастомный контрол вкладок с поддержкой всех позиций размещения панели вкладок. -/// Реализует интерфейс для интеграции с системой докинга. -/// -/// -/// -/// Контрол обеспечивает отображение коллекции вкладок с возможностью навигации между ними, -/// закрытия вкладок и изменения порядка. Поддерживает все четыре позиции размещения панели -/// вкладок: сверху, снизу, слева и справа. -/// -/// -/// Контрол автоматически синхронизирует свое состояние с моделью данных -/// и обеспечивает двустороннюю привязку данных через механизм INotifyPropertyChanged. -/// -/// public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable { private readonly PropertyChangedEventHandler _modelPropertyChangedHandler; @@ -44,9 +29,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable private bool _showCloseButtons = true; private bool _canReorderTabs = true; - /// - /// Инициализирует новый экземпляр класса . - /// public LatticeTabControl() { this.DefaultStyleKey = typeof(LatticeTabControl); @@ -54,7 +36,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable this.DataContextChanged += OnDataContextChanged; } - /// public IDockElement? Model { get => _model; @@ -68,7 +49,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable } } - /// public LayoutManager? LayoutManager { get => _layoutManager; @@ -80,7 +60,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable } } - /// public IDockContextManager? ContextManager { get => _contextManager; @@ -92,7 +71,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable } } - /// public bool IsSelected { get => _isSelected; @@ -104,7 +82,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable } } - /// public bool IsActive { get => _isActive; @@ -116,7 +93,9 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable } } - /// + public bool CanDrag => true; + public bool CanDrop => true; + public TabPlacement TabPlacement { get => _tabPlacement; @@ -131,7 +110,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable } } - /// public bool ShowCloseButtons { get => _showCloseButtons; @@ -146,7 +124,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable } } - /// public bool CanReorderTabs { get => _canReorderTabs; @@ -160,7 +137,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable } } - /// public IDockContent? ActiveContent { get => _model?.ActiveContent; @@ -173,19 +149,14 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable } } - /// + public object? PrepareDragData() => Model; + public bool HandleDrop(object data, DockPosition position) => false; + public event EventHandler? ActiveContentChanged; - - /// public event EventHandler? ContentClosing; - - /// public event EventHandler? TabsReordered; - - /// public event PropertyChangedEventHandler? PropertyChanged; - /// protected override void OnApplyTemplate() { base.OnApplyTemplate(); @@ -203,17 +174,11 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable UpdateTabHeaders(); } - /// - /// Обрабатывает изменение контекста данных контрола. - /// private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args) { Model = args.NewValue as DockLeaf; } - /// - /// Присоединяет модель данных к контролу. - /// private void AttachModel() { if (_model != null) @@ -232,9 +197,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable } } - /// - /// Отсоединяет модель данных от контрола. - /// private void DetachModel() { if (_model != null) @@ -250,9 +212,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable } } - /// - /// Обрабатывает изменения свойств модели данных. - /// private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e) { switch (e.PropertyName) @@ -274,17 +233,11 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable } } - /// - /// Обрабатывает изменения коллекции вкладок. - /// private void OnChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { UpdateTabHeaders(); } - /// - /// Обновляет положение панели вкладок. - /// private void UpdateTabPlacement() { if (_rootGrid == null || _model == null) return; @@ -322,10 +275,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable UpdateElementPositions(); } - /// - /// Обновляет ориентацию списка заголовков вкладок. - /// - /// Новая ориентация списка. private void UpdateHeaderListOrientation(Orientation orientation) { if (_tabHeaderList?.ItemsPanelRoot is StackPanel stackPanel) @@ -334,9 +283,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable } } - /// - /// Обновляет позиции элементов в сетке. - /// private void UpdateElementPositions() { if (_rootGrid == null || _tabHeaderList == null || _contentControl == null) return; @@ -373,9 +319,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable } } - /// - /// Обновляет заголовки вкладок. - /// private void UpdateTabHeaders() { if (_tabHeaderList == null || _model == null) return; @@ -391,11 +334,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable UpdateSelectedTab(); } - /// - /// Создает элемент заголовка вкладки. - /// - /// Содержимое вкладки. - /// Созданный элемент заголовка. private ListBoxItem CreateTabHeaderItem(IDockContent content) { var item = new ListBoxItem @@ -411,18 +349,12 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable if (e.GetCurrentPoint(item).Properties.IsLeftButtonPressed) { ActiveContent = content; - e.Handled = true; } }; return item; } - /// - /// Создает содержимое заголовка вкладки. - /// - /// Содержимое вкладки. - /// Созданное содержимое заголовка. private UIElement CreateTabHeaderContent(IDockContent content) { var grid = new Grid(); @@ -455,7 +387,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable closeButton.Click += (sender, e) => { CloseContent(content); - e.Handled = true; }; Grid.SetColumn(closeButton, 1); @@ -465,9 +396,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable return grid; } - /// - /// Обновляет выбранную вкладку. - /// private void UpdateSelectedTab() { if (_tabHeaderList == null || _model == null) return; @@ -486,9 +414,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable } } - /// - /// Обрабатывает изменение выбора вкладки. - /// private void OnTabSelectionChanged(object sender, SelectionChangedEventArgs e) { if (_tabHeaderList?.SelectedItem is ListBoxItem selectedItem && @@ -505,7 +430,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable } } - /// public void AddContent(IDockContent content) { if (_model != null && !_model.Children.Contains(content)) @@ -515,7 +439,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable } } - /// public void RemoveContent(IDockContent content) { if (_model != null && _model.Children.Contains(content)) @@ -525,7 +448,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable } } - /// public bool CloseContent(IDockContent content) { var args = new ContentClosingEventArgs(content); @@ -540,7 +462,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable return false; } - /// public void CloseAllExcept(IDockContent exceptContent) { if (_model == null) return; @@ -555,7 +476,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable } } - /// public void CloseAll() { if (_model == null) return; @@ -567,23 +487,17 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable } } - /// public void Refresh() { UpdateTabHeaders(); UpdateTabPlacement(); } - /// public void ApplyTheme(IDockTheme theme) { - if (theme != null) - { - // TODO: Реализовать применение темы к стилям контрола - } + // TODO: Реализовать применение темы } - /// public void OnModelPropertyChanged(string propertyName) { if (_model != null) @@ -592,15 +506,11 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable } } - /// - /// Вызывает событие изменения свойства. - /// private void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } - /// public void Dispose() { if (!_disposed) diff --git a/Lattice.UI.Docking.WinUI/DockBuilder.cs b/Lattice.UI.Docking.WinUI/DockBuilder.cs new file mode 100644 index 0000000..507f8be --- /dev/null +++ b/Lattice.UI.Docking.WinUI/DockBuilder.cs @@ -0,0 +1,189 @@ +using Lattice.Core.Docking.Engine; +using Lattice.Core.Docking.Services; +using Lattice.UI.Docking.Abstractions; +using Lattice.UI.Docking.Factories; +using Lattice.UI.Docking.WinUI.Factories; +using Lattice.UI.Docking.WinUI.Services; +using System; + +namespace Lattice.UI.Docking; + +/// +/// Предоставляет упрощенный статический API для инициализации и конфигурации системы докинга Lattice. +/// +public static class LatticeDock +{ + /// + /// Создает новый строитель конфигурации системы докинга для WinUI. + /// + /// Экземпляр для настройки системы. + public static DockBuilder CreateWinUIBuilder() + { + return new DockBuilder() + .WithWinUIFactory() + .WithWinUIContextManager() + .WithWinUIService(); + } + + /// + /// Настраивает строитель для использования фабрики WinUI. + /// + public static DockBuilder WithWinUIFactory(this DockBuilder builder) + { + return builder.WithControlFactory(new WinUIDockControlFactory()); + } + + /// + /// Настраивает строитель для использования контекстного менеджера WinUI. + /// + public static DockBuilder WithWinUIContextManager(this DockBuilder builder) + { + return builder.WithContextManager(new WinUIDockContextManager()); + } + + /// + /// Настраивает строитель для использования UI-сервиса WinUI. + /// + public static DockBuilder WithWinUIService(this DockBuilder builder) + { + return builder.WithUIService(new WinUIDockUIService()); + } +} + +/// +/// Предоставляет fluent-интерфейс для конфигурации системы докинга Lattice. +/// +public sealed class DockBuilder +{ + private readonly LayoutManager _layoutManager; + private readonly ContentRegistry _contentRegistry; + private IDockControlFactory? _factory; + private IDockContextManager? _contextManager; + private IDockUIService? _uiService; + + /// + /// Инициализирует новый экземпляр класса . + /// Создает менеджер макета и реестр контента по умолчанию. + /// + public DockBuilder() + { + _layoutManager = new LayoutManager(); + _contentRegistry = new ContentRegistry(); + _layoutManager.ContentRegistry = _contentRegistry; + } + + /// + /// Регистрирует фабрику контролов для создания UI-элементов. + /// + /// Фабрика контролов. + /// Текущий экземпляр для цепочки вызовов. + public DockBuilder WithControlFactory(IDockControlFactory factory) + { + _factory = factory; + return this; + } + + /// + /// Регистрирует менеджер контекстных меню. + /// + /// Менеджер контекстных меню. + /// Текущий экземпляр для цепочки вызовов. + public DockBuilder WithContextManager(IDockContextManager contextManager) + { + _contextManager = contextManager; + return this; + } + + /// + /// Регистрирует UI-сервис для выполнения платформенно-зависимых операций. + /// + /// UI-сервис. + /// Текущий экземпляр для цепочки вызовов. + public DockBuilder WithUIService(IDockUIService uiService) + { + _uiService = uiService; + return this; + } + + /// + /// Регистрирует тип контента в реестре. + /// + public DockBuilder RegisterContentType(string contentTypeId, Func factory, ContentMetadata? metadata = null) + where T : Core.Docking.Abstractions.IDockContent + { + _contentRegistry.Register(contentTypeId, factory, metadata); + return this; + } + + /// + /// Завершает конфигурацию системы докинга и возвращает настроенный экземпляр . + /// + /// Настроенная система докинга. + public IDockSystem Build() + { + // Настраиваем связи между компонентами + if (_factory is DockControlFactoryBase factoryBase && _contextManager != null) + { + factoryBase.ContextManager = _contextManager; + } + + return new DockSystem(_layoutManager, _contentRegistry, _factory, _contextManager, _uiService); + } +} + +/// +/// Представляет настроенную систему докинга с доступом ко всем основным компонентам. +/// +public interface IDockSystem +{ + /// + /// Получает менеджер макета. + /// + LayoutManager LayoutManager { get; } + + /// + /// Получает реестр контента. + /// + ContentRegistry ContentRegistry { get; } + + /// + /// Получает фабрику контролов. + /// + IDockControlFactory? ControlFactory { get; } + + /// + /// Получает менеджер контекстных меню. + /// + IDockContextManager? ContextManager { get; } + + /// + /// Получает UI-сервис. + /// + IDockUIService? UIService { get; } +} + +/// +/// Реализация интерфейса . +/// +internal sealed class DockSystem : IDockSystem +{ + public LayoutManager LayoutManager { get; } + public ContentRegistry ContentRegistry { get; } + public IDockControlFactory? ControlFactory { get; } + public IDockContextManager? ContextManager { get; } + public IDockUIService? UIService { get; } + + public DockSystem( + LayoutManager layoutManager, + ContentRegistry contentRegistry, + IDockControlFactory? controlFactory, + IDockContextManager? contextManager, + IDockUIService? uiService) + { + LayoutManager = layoutManager ?? throw new ArgumentNullException(nameof(layoutManager)); + ContentRegistry = contentRegistry ?? throw new ArgumentNullException(nameof(contentRegistry)); + ControlFactory = controlFactory; + ContextManager = contextManager; + UIService = uiService; + } +} \ No newline at end of file diff --git a/Lattice.UI.Docking.WinUI/Factories/WinUIDockControlFactory.cs b/Lattice.UI.Docking.WinUI/Factories/WinUIDockControlFactory.cs index ece13a2..e3033b5 100644 --- a/Lattice.UI.Docking.WinUI/Factories/WinUIDockControlFactory.cs +++ b/Lattice.UI.Docking.WinUI/Factories/WinUIDockControlFactory.cs @@ -4,188 +4,78 @@ using Lattice.UI.Docking.Abstractions; using Lattice.UI.Docking.Factories; using Microsoft.UI.Xaml; using System; +using System.Collections.Generic; namespace Lattice.UI.Docking.WinUI.Factories; -/// -/// Фабрика контролов для платформы WinUI. -/// Создает UI-элементы для отображения компонентов системы докинга. -/// -/// -/// -/// Фабрика реализует паттерн "Абстрактная фабрика", предоставляя единый интерфейс -/// для создания всех типов контролов док-системы. Это позволяет абстрагировать -/// конкретную UI-платформу (WinUI) от бизнес-логики системы. -/// -/// -/// Все создаваемые контролы автоматически настраиваются: устанавливаются связи -/// с менеджером макета, контекстным менеджером и применяется текущая тема оформления. -/// -/// public sealed class WinUIDockControlFactory : DockControlFactoryBase, IDockControlFactory { - private readonly IDockTheme _theme; + private readonly Dictionary> _creators; - /// - /// Инициализирует новый экземпляр фабрики WinUI. - /// - /// Тема оформления для применения к создаваемым контролам. - /// - /// Выбрасывается, если равен null. - /// - /// - /// Конструктор создает фабрику с заданной темой оформления. Все контролы, - /// созданные этой фабрикой, будут автоматически применять указанную тему. - /// - public WinUIDockControlFactory(IDockTheme theme) + public WinUIDockControlFactory() { - _theme = theme ?? throw new ArgumentNullException(nameof(theme)); + _creators = new Dictionary> + { + [typeof(DockGroup)] = model => CreateGroupControl((DockGroup)model), + [typeof(DockLeaf)] = model => CreateLeafControl((DockLeaf)model), + [typeof(DockWindow)] = model => CreateFloatingWindowControl((DockWindow)model), + [typeof(AutoHidePanel)] = model => CreateAutoHidePanelControl((AutoHidePanel)model), + }; } - /// - /// Создает контрол для группы разделения. - /// - /// Модель группы разделения. - /// - /// Созданный контрол группы. - /// - /// - /// Выбрасывается, если равен null. - /// - /// - /// Создает экземпляр , настраивает его связи - /// с моделью и другими сервисами, применяет текущую тему оформления. - /// public override IDockGroupControl CreateGroupControl(DockGroup group) { + if (group == null) throw new ArgumentNullException(nameof(group)); + var control = new LatticeDockGroup(); ConfigureControl(control, group); - control.ApplyTheme(_theme); return control; } - /// - /// Создает контрол для контейнера вкладок. - /// - /// Модель контейнера вкладок. - /// - /// Созданный контрол листа. - /// - /// - /// Выбрасывается, если равен null. - /// - /// - /// Создает экземпляр , настраивает его связи - /// с моделью и другими сервисами, применяет текущую тему оформления. - /// Контрол поддерживает все положения панели вкладок и операции с вкладками. - /// public override IDockLeafControl CreateLeafControl(DockLeaf leaf) { + if (leaf == null) throw new ArgumentNullException(nameof(leaf)); + var control = new LatticeTabControl(); ConfigureControl(control, leaf); - control.ApplyTheme(_theme); return control; } - /// - /// Создает контрол для плавающего окна. - /// - /// Модель плавающего окна. - /// - /// Созданный контрол окна. - /// - /// - /// Выбрасывается, если равен null. - /// - /// - /// В текущей реализации метод не реализован. Плавающие окна требуют - /// дополнительной интеграции с оконной системой платформы. - /// public override IFloatingWindowControl CreateFloatingWindowControl(DockWindow window) { - // TODO: Реализовать создание плавающего окна - throw new NotImplementedException(); + throw new NotImplementedException("Floating windows not implemented yet"); } - /// - /// Создает контрол для автоскрываемой панели. - /// - /// Модель автоскрываемой панели. - /// - /// Созданный контрол панели. - /// - /// - /// Выбрасывается, если равен null. - /// - /// - /// В текущей реализации метод не реализован. Автоскрываемые панели требуют - /// сложной логики анимации и взаимодействия с краями окна. - /// public override IAutoHidePanelControl CreateAutoHidePanelControl(AutoHidePanel panel) { - // TODO: Реализовать создание автоскрываемой панели - throw new NotImplementedException(); + throw new NotImplementedException("Auto-hide panels not implemented yet"); } - /// - /// Создает контрол для разделителя. - /// - /// Ориентация разделителя. - /// - /// Созданный контрол разделителя. - /// - /// - /// Создает экземпляр , настраивает его ориентацию - /// и применяет текущую тему оформления. Разделитель поддерживает перетаскивание - /// для изменения соотношения размеров между соседними областями. - /// public override IDockSplitterControl CreateSplitterControl(SplitDirection orientation) { - var control = new LatticeSplitter - { - Orientation = orientation - }; + var control = new LatticeSplitter { Orientation = orientation }; ConfigureControl(control); - control.ApplyTheme(_theme); return control; } - /// - /// Создает хост для размещения системы докинга. - /// - /// - /// Созданный док-хост. - /// - /// - /// Создает корневой контейнер для всей системы докинга - экземпляр . - /// Хост управляет всем макетом приложения, включая основное дерево компоновки, - /// плавающие окна и автоскрываемые панели. - /// - public IDockHost CreateDockHost() + public override IDockControl? CreateControlForElement(IDockElement element) { - var host = new LatticeDockHost(); - ConfigureControl(host); - host.ApplyTheme(_theme); - return host; + if (element == null) throw new ArgumentNullException(nameof(element)); + + var type = element.GetType(); + if (_creators.TryGetValue(type, out var creator)) + return creator(element); + + return base.CreateControlForElement(element); } - /// - /// Настраивает созданный контрол. - /// - /// Контрол для настройки. - /// Модель данных для контрола (опционально). - /// - /// Устанавливает основные связи контрола: модель данных, менеджер макета, - /// контекстный менеджер. Также настраивает привязку данных через DataContext. - /// Этот метод вызывается для всех создаваемых контролов. - /// private void ConfigureControl(IDockControl control, IDockElement? model = null) { if (control == null) return; control.Model = model; - control.LayoutManager = LatticeUIFramework.LayoutManager; - control.ContextManager = LatticeUIFramework.ContextManager; + control.LayoutManager = Lattice.UI.Docking.LatticeUIFramework.LayoutManager; + control.ContextManager = Lattice.UI.Docking.LatticeUIFramework.ContextManager; if (control is FrameworkElement frameworkElement && model != null) { diff --git a/Lattice.UI.Docking.WinUI/Services/DragDropService.cs b/Lattice.UI.Docking.WinUI/Services/DragDropService.cs new file mode 100644 index 0000000..fa919ca --- /dev/null +++ b/Lattice.UI.Docking.WinUI/Services/DragDropService.cs @@ -0,0 +1,53 @@ +using Microsoft.UI.Xaml; +using System; + +namespace Lattice.UI.Docking.WinUI.Services; + +/// +/// Сервис для управления операциями Drag & Drop в WinUI. +/// +public static class DragDropService +{ + /// + /// Настраивает элемент для поддержки перетаскивания. + /// + public static void SetupDragElement(UIElement element, Func getDataCallback) + { + element.CanDrag = true; + element.DragStarting += (sender, args) => + { + var data = getDataCallback(); + if (data != null) + { + args.Data.Properties.Add("LatticeDockElement", data); + args.Data.SetData("LatticeDockElement", data); + args.AllowedOperations = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move; + } + }; + } + + /// + /// Настраивает элемент для приема сброса. + /// + public static void SetupDropElement(UIElement element, Func dropCallback) + { + element.AllowDrop = true; + element.Drop += (sender, args) => + { + if (args.DataView.Properties.TryGetValue("LatticeDockElement", out var data)) + { + if (dropCallback(data)) + { + args.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move; + } + } + }; + + element.DragOver += (sender, args) => + { + args.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move; + args.DragUIOverride.IsGlyphVisible = true; + args.DragUIOverride.Caption = "Переместить"; + }; + } +} \ No newline at end of file diff --git a/Lattice.UI.Docking.WinUI/Services/WinUIDockContextManager.cs b/Lattice.UI.Docking.WinUI/Services/WinUIDockContextManager.cs index 8204e5b..62f23a9 100644 --- a/Lattice.UI.Docking.WinUI/Services/WinUIDockContextManager.cs +++ b/Lattice.UI.Docking.WinUI/Services/WinUIDockContextManager.cs @@ -4,6 +4,9 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using System; using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Input; namespace Lattice.UI.Docking.WinUI.Services; @@ -21,9 +24,38 @@ public sealed class WinUIDockContextManager : DockContextManagerBase, IDisposabl /// public WinUIDockContextManager() { + // Регистрируем стандартные команды + RegisterDefaultCommands(); + } + + private void RegisterDefaultCommands() + { + // Пример регистрации стандартных команд + RegisterCommand("Close", new DockCommand("Close", "Close", "Close the selected content", () => "", () => true, OnCloseCommand)); + RegisterCommand("Float", new DockCommand("Float", "Float", "Float the window", () => "", () => true, OnFloatCommand)); + RegisterCommand("Dock", new DockCommand("Dock", "Dock", "Dock the window", () => "", () => true, OnDockCommand)); + } + + private void OnCloseCommand() + { + if (_currentContextTarget is Lattice.UI.LatticeDockLeaf leafControl && leafControl.ActiveContent != null) + { + leafControl.CloseContent(leafControl.ActiveContent); + } + } + + private void OnFloatCommand() + { + // TODO: Реализовать плавающее окно + System.Diagnostics.Debug.WriteLine("Float command triggered"); + } + + private void OnDockCommand() + { + // TODO: Реализовать закрепление окна + System.Diagnostics.Debug.WriteLine("Dock command triggered"); } - /// public override void ShowContextMenu(IDockControl element, double x, double y) { if (element is not FrameworkElement uiElement) return; @@ -39,13 +71,26 @@ public sealed class WinUIDockContextManager : DockContextManagerBase, IDisposabl var item = new MenuFlyoutItem { Text = command.Name, - Command = new RelayCommand(() => ExecuteCommand(command, element)) + Tag = command, + Command = new RelayCommand(() => ExecuteCommand(command, element), + () => command.CanExecute(element)) }; - // Добавляем иконку, если есть + // Устанавливаем иконку, если есть if (!string.IsNullOrEmpty(command.Icon)) { - // TODO: Добавить иконку команды + var icon = new FontIcon + { + Glyph = command.Icon, + FontSize = 12 + }; + item.Icon = icon; + } + + // Добавляем подсказку, если есть описание + if (!string.IsNullOrEmpty(command.Description)) + { + ToolTipService.SetToolTip(item, command.Description); } flyout.Items.Add(item); @@ -68,7 +113,6 @@ public sealed class WinUIDockContextManager : DockContextManagerBase, IDisposabl OnContextMenuShown(element, x, y); } - /// public override void HideContextMenu() { if (_currentFlyout != null) @@ -84,30 +128,93 @@ public sealed class WinUIDockContextManager : DockContextManagerBase, IDisposabl } } + public override void RegisterCommand(string commandId, IDockCommand command) + { + if (string.IsNullOrEmpty(commandId)) + throw new ArgumentNullException(nameof(commandId)); + + _commands[commandId] = command ?? throw new ArgumentNullException(nameof(command)); + } + + public override void UnregisterCommand(string commandId) + { + _commands.TryRemove(commandId, out _); + } + /// - /// Класс-заглушка для реализации ICommand. + /// Получает команду по идентификатору. /// - private sealed class RelayCommand : System.Windows.Input.ICommand + protected override IDockCommand? GetCommand(string commandId) + { + _commands.TryGetValue(commandId, out var command); + return command; + } + + /// + /// Получает все доступные команды для указанного элемента. + /// + protected override IEnumerable GetCommandsForElement(IDockControl element) + { + return _commands.Values.Where(c => CanExecuteCommand(c, element)); + } + + /// + /// Класс для реализации ICommand. + /// + private sealed class RelayCommand : ICommand { private readonly Action _execute; - private readonly Func? _canExecute; + private readonly Func _canExecute; public event EventHandler? CanExecuteChanged; - public RelayCommand(Action execute, Func? canExecute = null) + public RelayCommand(Action execute, Func canExecute) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute = canExecute; } - public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true; + public bool CanExecute(object? parameter) => _canExecute(); public void Execute(object? parameter) => _execute(); public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); } - /// + /// + /// Базовая реализация команды докинга. + /// + private class DockCommand : IDockCommand + { + public string Id { get; } + public string Name { get; } + public string Description { get; } + private readonly Func _getIcon; + private readonly Func _canExecute; + private readonly Action _execute; + + public DockCommand(string id, string name, string description, Func getIcon, Func canExecute, Action execute) + { + Id = id; + Name = name; + Description = description; + _getIcon = getIcon; + _canExecute = canExecute; + _execute = execute; + } + + public string Icon => _getIcon(); + public string Shortcut => ""; + + public bool CanExecute(object? parameter) => _canExecute(); + + public void Execute(object? parameter) => _execute(); + + public event EventHandler? CanExecuteChanged; + + public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); + } + public void Dispose() { HideContextMenu(); diff --git a/Lattice.UI.Docking.WinUI/Services/WinUIDockUIService.cs b/Lattice.UI.Docking.WinUI/Services/WinUIDockUIService.cs index 4d4b3ac..ff6ddc3 100644 --- a/Lattice.UI.Docking.WinUI/Services/WinUIDockUIService.cs +++ b/Lattice.UI.Docking.WinUI/Services/WinUIDockUIService.cs @@ -12,39 +12,8 @@ namespace Lattice.UI.Docking.WinUI.Services; /// Инкапсулирует платформенно-зависимые операции, такие как создание окон, /// показ диалогов и синхронизация с UI-потоком. /// -/// -/// -/// предоставляет конкретные реализации методов -/// для платформы WinUI. Это позволяет основной -/// бизнес-логике док-системы оставаться независимой от конкретной UI-платформы. -/// -/// -/// Сервис использует API WinUI для создания окон, показа ContentDialog и -/// управления диспетчером потока пользовательского интерфейса. -/// -/// public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService { - /// - /// Создает главное окно приложения для размещения док-хоста. - /// - /// - /// Экземпляр , который будет содержаться в окне. - /// - /// - /// Объект окна WinUI, который можно отобразить и управлять им. - /// - /// - /// Выбрасывается, если равен null. - /// - /// - /// Выбрасывается, если не является элементом WinUI. - /// - /// - /// Создает окно WinUI с заголовком "Lattice IDE", устанавливает указанный хост - /// в качестве содержимого и регистрирует окно в системе отслеживания окон. - /// Окно создается с настройками по умолчанию для IDE-подобных приложений. - /// public override object CreateMainWindow(IDockHost host) { if (host is not FrameworkElement hostElement) @@ -55,30 +24,11 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService window.AppWindow.Title = "Lattice IDE"; // Регистрируем окно в трекере - Themes.WindowTracker.Register(window); + Lattice.Themes.WindowTracker.Register(window); return window; } - /// - /// Отображает модальное диалоговое окно с указанным содержимым. - /// - /// Заголовок диалогового окна. - /// Содержимое диалогового окна. - /// - /// Nullable boolean значение, указывающее результат диалога: - /// true - пользователь подтвердил действие, - /// false - пользователь отменил действие, - /// null - диалог был закрыт без выбора. - /// - /// - /// Выбрасывается, если или равны null. - /// - /// - /// Создает и показывает ContentDialog с кнопками OK и Cancel. - /// Блокирует взаимодействие с родительским окном до закрытия диалога. - /// Использует XamlRoot активного окна для корректного отображения. - /// public override bool? ShowDialog(string title, object content) { if (content is not FrameworkElement contentElement) @@ -93,7 +43,6 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService XamlRoot = GetActiveXamlRoot() }; - // Показываем диалог и возвращаем результат var result = dialog.ShowAsync(); return result.GetAwaiter().GetResult() switch { @@ -103,19 +52,6 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService }; } - /// - /// Отображает информационное сообщение с кнопкой OK. - /// - /// Текст сообщения. - /// Заголовок окна сообщения. - /// - /// Выбрасывается, если или равны null. - /// - /// - /// Создает ContentDialog с текстом сообщения и одной кнопкой OK. - /// Используется для информирования пользователя о результате операции - /// или отображения некритичных ошибок. - /// public override void ShowMessage(string message, string caption) { var dialog = new ContentDialog @@ -129,22 +65,6 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService dialog.ShowAsync(); } - /// - /// Отображает диалог подтверждения с кнопками Yes/No. - /// - /// Текст вопроса. - /// Заголовок окна подтверждения. - /// - /// true, если пользователь выбрал "Yes"; false, если пользователь выбрал "No". - /// - /// - /// Выбрасывается, если или равны null. - /// - /// - /// Создает ContentDialog с кнопками Yes и No. Используется для получения - /// подтверждения пользователя перед выполнением критических операций, - /// таких как закрытие вкладок с несохраненными данными или сброс настроек. - /// public override bool Confirm(string message, string caption) { var dialog = new ContentDialog @@ -160,22 +80,6 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService return result == ContentDialogResult.Primary; } - /// - /// Отображает диалог ввода текста. - /// - /// Текст подсказки для пользователя. - /// Значение по умолчанию для поля ввода. - /// - /// Введенный пользователем текст или null, если диалог был отменен. - /// - /// - /// Выбрасывается, если равен null. - /// - /// - /// Создает ContentDialog с однострочным полем ввода TextBox. - /// Используется для получения текстового ввода от пользователя, такого как - /// имена файлов, названия документов или параметры конфигурации. - /// public override string? Prompt(string prompt, string? defaultValue = null) { var textBox = new TextBox @@ -198,19 +102,6 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService return result == ContentDialogResult.Primary ? textBox.Text : null; } - /// - /// Выполняет указанное действие в UI-потоке. - /// - /// Действие для выполнения. - /// - /// Выбрасывается, если равен null. - /// - /// - /// Гарантирует, что действие будет выполнено в потоке, связанном с - /// пользовательским интерфейсом. Если текущий поток уже является UI-потоком, - /// действие выполняется немедленно. В противном случае действие ставится - /// в очередь диспетчера WinUI. - /// public override void InvokeOnUIThread(Action action) { if (action == null) return; @@ -229,19 +120,7 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService /// /// Выполняет указанную асинхронную функцию в UI-потоке. /// - /// Асинхронная функция для выполнения. - /// - /// Задача, представляющая асинхронную операцию. - /// - /// - /// Выбрасывается, если равен null. - /// - /// - /// Гарантирует, что асинхронная функция будет выполнена в UI-потоке. - /// Используется для операций, которые требуют доступа к UI-элементам - /// или выполняют асинхронные вызовы с обновлением интерфейса. - /// - public override async Task InvokeOnUIThreadAsync(Func action) + public async Task InvokeOnUIThreadAsync(Func action) { if (action == null) return; @@ -269,21 +148,9 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService } } - /// - /// Получает XamlRoot активного окна приложения. - /// - /// - /// XamlRoot активного окна или null, если нет активных окон. - /// - /// - /// Используется для корректного отображения диалоговых окон в контексте - /// текущего окна приложения. Перебирает все зарегистрированные окна - /// и возвращает XamlRoot первого найденного. - /// private XamlRoot? GetActiveXamlRoot() { - // Получаем XamlRoot из активного окна - foreach (var window in Themes.WindowTracker.Windows) + foreach (var window in Lattice.Themes.WindowTracker.Windows) { if (window.Content is FrameworkElement element) { diff --git a/Lattice.UI.Docking.WinUI/Services/WinUIDragDropService.cs b/Lattice.UI.Docking.WinUI/Services/WinUIDragDropService.cs deleted file mode 100644 index 3315100..0000000 --- a/Lattice.UI.Docking.WinUI/Services/WinUIDragDropService.cs +++ /dev/null @@ -1,533 +0,0 @@ -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; - -/// -/// Предоставляет реализацию сервиса перетаскивания для платформы WinUI с расширенной -/// поддержкой визуальных эффектов и интеграцией с системой докинга Lattice. -/// Координирует взаимодействие между базовым менеджером перетаскивания и UI-контролами, -/// обеспечивая богатую визуальную обратную связь во время операций drag-and-drop. -/// -/// -/// -/// расширяет базовый функционал -/// платформенно-зависимыми визуальными эффектами, включая: -/// -/// -/// Прозрачное визуальное представление перетаскиваемого элемента -/// Интерактивные подсказки областей сброса -/// Анимации при начале и завершении перетаскивания -/// Подсветку допустимых целей сброса -/// -/// -/// Сервис поддерживает регистрацию UI-элементов и автоматически вычисляет их границы -/// для точного определения целей сброса. -/// -/// -public sealed class WinUIDragDropService : DockDragDropService, IDisposable -{ - private readonly ConcurrentDictionary _controlToElement = new(); - private readonly DragDropManagerEx _dragDropManager; - private Popup? _dragVisualPopup; - private Border? _dragVisual; - private DropHintOverlay? _dropHintOverlay; - private bool _disposed; - - /// - /// Инициализирует новый экземпляр сервиса перетаскивания WinUI. - /// - /// - /// Создает внутренний менеджер перетаскивания, инициализирует визуальные элементы - /// и подписывается на события менеджера для обработки операций перетаскивания. - /// - public WinUIDragDropService() - { - _dragDropManager = new DragDropManagerEx(); - HookEvents(); - InitializeDragVisual(); - InitializeDropHintOverlay(); - } - - /// - /// Инициализирует новый экземпляр с указанным менеджером перетаскивания. - /// - /// - /// Предварительно настроенный менеджер перетаскивания. - /// - /// - /// Выбрасывается, если равен null. - /// - /// - /// Позволяет использовать кастомную конфигурацию менеджера перетаскивания - /// при сохранении всех визуальных эффектов WinUI. - /// - public WinUIDragDropService(DragDropManagerEx dragDropManager) - { - _dragDropManager = dragDropManager ?? throw new ArgumentNullException(nameof(dragDropManager)); - HookEvents(); - InitializeDragVisual(); - InitializeDropHintOverlay(); - } - - /// - /// Подписывается на события менеджера перетаскивания. - /// - /// - /// Обрабатывает следующие события: - /// - /// Начало перетаскивания - /// Обновление позиции перетаскивания - /// Завершение перетаскивания - /// Отмена перетаскивания - /// Изменение цели сброса - /// - /// - private void HookEvents() - { - _dragDropManager.DragStarted += OnDragStarted; - _dragDropManager.DragUpdated += OnDragUpdated; - _dragDropManager.DragCompleted += OnDragCompleted; - _dragDropManager.DragCancelled += OnDragCancelled; - _dragDropManager.DropTargetChanged += OnDropTargetChanged; - } - - /// - /// Инициализирует визуальное представление перетаскиваемого элемента. - /// - /// - /// Создает Popup с Border для отображения полупрозрачной копии - /// перетаскиваемого элемента во время операции drag-and-drop. - /// - 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 - }; - } - - /// - /// Инициализирует оверлей для отображения подсказок при сбросе. - /// - /// - /// Добавляет оверлей в корневой контейнер приложения для отображения - /// визуальных подсказок о возможных позициях сброса. - /// - private void InitializeDropHintOverlay() - { - // Создаем оверлей для подсказок при сбросе - _dropHintOverlay = new DropHintOverlay(); - - // Добавляем оверлей в корневой контейнер приложения - if (Window.Current?.Content is Panel rootPanel) - { - rootPanel.Children.Add(_dropHintOverlay); - } - } - - /// - /// Регистрирует связь между абстрактным контролом док-системы и конкретным UI-элементом WinUI. - /// - /// Абстрактный контрол док-системы. - /// Конкретный UI-элемент WinUI. - /// - /// Выбрасывается, если или равны null. - /// - /// - /// Эта связь необходима для: - /// - /// Вычисления границ элемента на экране - /// Создания визуального представления перетаскивания - /// Определения позиции сброса относительно элемента - /// - /// - 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; - } - - /// - /// Отменяет регистрацию связи между абстрактным контролом док-системы и UI-элементом WinUI. - /// - /// Абстрактный контрол док-системы. - /// - /// Выбрасывается, если равен null. - /// - /// - /// Удаляет элемент из внутреннего словаря, освобождая связанные с ним ресурсы. - /// - public void UnregisterControl(IDockControl control) - { - if (control == null) throw new ArgumentNullException(nameof(control)); - - _controlToElement.TryRemove(control, out _); - } - - /// - /// Вычисляет границы элемента на экране. - /// - /// Элемент, для которого вычисляются границы. - /// - /// Прямоугольник в экранных координатах, представляющий границы элемента. - /// - /// - /// - /// Метод выполняет преобразование координат элемента в экранные координаты - /// с использованием трансформации визуального дерева. - /// - /// - /// В случае ошибки вычисления возвращает прямоугольник размером 100x100 пикселей - /// в точке (0, 0). - /// - /// - 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); - } - - /// - /// Создает визуальное представление перетаскиваемого элемента. - /// - /// Информация о перетаскивании. - /// - /// - /// На основе источника перетаскивания создает полупрозрачную копию элемента, - /// которая следует за курсором мыши во время операции перетаскивания. - /// - /// - /// Визуальное представление включает: - /// - /// - /// Тень для создания эффекта глубины - /// Прозрачность для видимости фонового содержимого - /// Синюю границу для визуального выделения - /// - /// - 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; - } - } - - /// - /// Обновляет позицию визуального представления перетаскивания. - /// - /// Новая позиция курсора. - /// - /// Перемещает Popup с визуальным представлением в указанную позицию, - /// обеспечивая плавное следование за курсором мыши. - /// - protected override void UpdateDragVisualPosition(Point position) - { - if (_dragVisualPopup != null) - { - _dragVisualPopup.HorizontalOffset = position.X; - _dragVisualPopup.VerticalOffset = position.Y; - } - } - - /// - /// Очищает визуальное представление перетаскивания. - /// - /// - /// Скрывает и освобождает ресурсы Popup, используемого для отображения - /// визуального представления перетаскиваемого элемента. - /// - protected override void CleanupDragVisual() - { - if (_dragVisualPopup != null) - { - _dragVisualPopup.IsOpen = false; - _dragVisualPopup.Child = null; - } - } - - /// - /// Показывает визуальную подсказку о возможной позиции сброса. - /// - /// Элемент, для которого показывается подсказка. - /// Предполагаемая позиция сброса. - /// - /// Отображает полупрозрачный прямоугольник в указанной позиции относительно элемента, - /// давая пользователю визуальную обратную связь о том, куда будет помещен элемент. - /// - protected override void ShowDropHint(IDockControl element, DropPosition position) - { - _dropHintOverlay?.ShowHint(element, position); - } - - /// - /// Скрывает текущую визуальную подсказку о сбросе. - /// - /// - /// Убирает все отображаемые подсказки сброса, очищая оверлей. - /// - protected override void HideDropHint() - { - _dropHintOverlay?.HideHint(); - } - - /// - /// Освобождает ресурсы, используемые сервисом перетаскивания. - /// - /// - /// - /// Выполняет следующие действия: - /// - /// - /// Отписывается от всех событий менеджера перетаскивания - /// Удаляет оверлей подсказок из корневого контейнера - /// Очищает словарь зарегистрированных контролов - /// Освобождает визуальные элементы - /// - /// - 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; - } - } -} - -/// -/// Представляет оверлей для отображения визуальных подсказок при сбросе в операции перетаскивания. -/// Этот элемент отображает полупрозрачные прямоугольники в местах возможного сброса, -/// давая пользователю визуальную обратную связь о допустимых позициях. -/// -/// -/// -/// является внутренним вспомогательным классом, -/// который отображается поверх всего пользовательского интерфейса во время операции -/// перетаскивания для показа визуальных подсказок. -/// -/// -/// Оверлей поддерживает все позиции сброса, определенные в , -/// и автоматически вычисляет размеры и положение подсказок на основе целевого элемента. -/// -/// -internal sealed class DropHintOverlay : Grid -{ - private readonly Dictionary _hintRectangles = new(); - private readonly SolidColorBrush _hintBrush; - - /// - /// Инициализирует новый экземпляр класса . - /// - /// - /// Создает прозрачный оверлей, который не участвует в тестировании попаданий, - /// и инициализирует прямоугольники для всех возможных позиций сброса. - /// - public DropHintOverlay() - { - this.IsHitTestVisible = false; - this.Background = new SolidColorBrush(Microsoft.UI.Colors.Transparent); - - // Используем акцентный цвет для подсказок - _hintBrush = new SolidColorBrush(Microsoft.UI.Colors.DodgerBlue); - - InitializeHintRectangles(); - } - - /// - /// Инициализирует прямоугольники для всех позиций сброса. - /// - /// - /// Создает отдельный Border для каждой позиции сброса и добавляет их в дочернюю коллекцию. - /// Все прямоугольники изначально скрыты и отображаются только при необходимости. - /// - 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); - } - } - - /// - /// Показывает визуальную подсказку для указанного элемента и позиции сброса. - /// - /// Элемент, для которого показывается подсказка. - /// Позиция сброса относительно элемента. - /// - /// Вычисляет положение и размер подсказки на основе границ элемента и позиции сброса, - /// затем делает соответствующий прямоугольник видимым. - /// - 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; - } - - /// - /// Вычисляет границы подсказки для указанного элемента и позиции сброса. - /// - /// Целевой элемент. - /// Позиция сброса. - /// Прямоугольник с координатами и размерами подсказки. - /// - /// Размеры подсказок зависят от позиции: - /// - /// Слева/справа: ширина 50px, высота равна высоте элемента - /// Сверху/снизу: высота 50px, ширина равна ширине элемента - /// В центре: размеры равны размерам элемента - /// Вкладка: высота 30px, ширина равна ширине элемента, позиция сверху - /// - /// - 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) - }; - } - - /// - /// Скрывает все визуальные подсказки. - /// - /// - /// Делает все прямоугольники подсказок невидимыми, очищая оверлей. - /// - public void HideHint() - { - foreach (var rect in _hintRectangles.Values) - { - rect.Visibility = Visibility.Collapsed; - } - } -} \ No newline at end of file diff --git a/Lattice.UI.Docking.WinUI/Themes/Generic.xaml b/Lattice.UI.Docking.WinUI/Themes/Generic.xaml index 19a666d..68c6296 100644 --- a/Lattice.UI.Docking.WinUI/Themes/Generic.xaml +++ b/Lattice.UI.Docking.WinUI/Themes/Generic.xaml @@ -4,7 +4,8 @@ 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"> + xmlns:models="using:Lattice.Core.Docking.Models" + xmlns:muxc="using:Microsoft.UI.Xaml.Controls"> @@ -20,55 +21,73 @@ GroupTemplate="{StaticResource LatticeGroupTemplate}" LeafTemplate="{StaticResource LatticeLeafTemplate}" /> - + - + - + - + - - - @@ -163,7 +168,7 @@ - + - + - + - + + - 0 - 0,0,1,1 - 1 - 1 + + 4 + 6 + + + 1 + 2 + + + 8 diff --git a/Lattice.UI.Docking/Abstractions/IDockControl.cs b/Lattice.UI.Docking/Abstractions/IDockControl.cs index fb4351d..25a3b47 100644 --- a/Lattice.UI.Docking/Abstractions/IDockControl.cs +++ b/Lattice.UI.Docking/Abstractions/IDockControl.cs @@ -1,5 +1,6 @@ using Lattice.Core.Docking.Abstractions; using Lattice.Core.Docking.Engine; +using Lattice.Core.Docking.Models; using System.ComponentModel; namespace Lattice.UI.Docking.Abstractions; @@ -78,6 +79,26 @@ public interface IDockControl : INotifyPropertyChanged /// bool IsActive { get; set; } + /// + /// Получает признак того, что элемент можно перетаскивать. + /// + bool CanDrag { get; } + + /// + /// Получает признак того, что на элемент можно сбрасывать. + /// + bool CanDrop { get; } + + /// + /// Подготавливает данные для перетаскивания. + /// + object? PrepareDragData(); + + /// + /// Обрабатывает сброс данных. + /// + bool HandleDrop(object data, DockPosition position); + /// /// Обновляет внешний вид контрола в соответствии с текущим состоянием модели. /// diff --git a/Lattice.UI.Docking/Implementations/DockControlBase.cs b/Lattice.UI.Docking/Implementations/DockControlBase.cs deleted file mode 100644 index 7d4061b..0000000 --- a/Lattice.UI.Docking/Implementations/DockControlBase.cs +++ /dev/null @@ -1,113 +0,0 @@ -using Lattice.Core.Docking.Abstractions; -using Lattice.Core.Docking.Engine; -using Lattice.UI.Docking.Abstractions; -using System.ComponentModel; - -namespace Lattice.UI.Docking.Implementations; - -/// -/// Базовая реализация контрола док-системы. -/// -public abstract class DockControlBase : IDockControl, INotifyPropertyChanged -{ - /// - public event PropertyChangedEventHandler? PropertyChanged; - - private IDockElement? _model; - private LayoutManager? _layoutManager; - private IDockContextManager? _contextManager; - private bool _isSelected; - private bool _isActive; - - /// - public IDockElement? Model - { - get => _model; - set - { - if (_model != value) - { - _model = value; - OnPropertyChanged(nameof(Model)); - } - } - } - - /// - public LayoutManager? LayoutManager - { - get => _layoutManager; - set - { - if (_layoutManager != value) - { - _layoutManager = value; - OnPropertyChanged(nameof(LayoutManager)); - } - } - } - - /// - public IDockContextManager? ContextManager - { - get => _contextManager; - set - { - if (_contextManager != value) - { - _contextManager = value; - OnPropertyChanged(nameof(ContextManager)); - } - } - } - - /// - public bool IsSelected - { - get => _isSelected; - set - { - if (_isSelected != value) - { - _isSelected = value; - OnPropertyChanged(nameof(IsSelected)); - } - } - } - - /// - public bool IsActive - { - get => _isActive; - set - { - if (_isActive != value) - { - _isActive = value; - OnPropertyChanged(nameof(IsActive)); - } - } - } - - /// - public abstract void Refresh(); - - /// - public abstract void ApplyTheme(IDockTheme theme); - - /// - public virtual void OnModelPropertyChanged(string propertyName) - { - // Базовая реализация просто обновляет весь контрол - Refresh(); - } - - /// - /// Вызывает событие . - /// - /// Имя измененного свойства. - protected virtual void OnPropertyChanged(string propertyName) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } -} \ No newline at end of file diff --git a/Lattice.UI.Docking/Services/DockContextManagerBase.cs b/Lattice.UI.Docking/Services/DockContextManagerBase.cs index 50a431d..aa957e6 100644 --- a/Lattice.UI.Docking/Services/DockContextManagerBase.cs +++ b/Lattice.UI.Docking/Services/DockContextManagerBase.cs @@ -7,15 +7,6 @@ namespace Lattice.UI.Docking.Services; /// public abstract class DockContextManagerBase : IDockContextManager { - private readonly Dictionary _commands = new(); - private IDockControl? _currentContextTarget; - - /// - public event EventHandler? ContextMenuShown; - - /// - public event EventHandler? ContextMenuHidden; - /// public abstract void ShowContextMenu(IDockControl element, double x, double y); @@ -25,25 +16,22 @@ public abstract class DockContextManagerBase : IDockContextManager /// public virtual void RegisterCommand(string commandId, IDockCommand command) { - if (string.IsNullOrEmpty(commandId)) - throw new ArgumentNullException(nameof(commandId)); - - _commands[commandId] = command ?? throw new ArgumentNullException(nameof(command)); + // Базовая реализация, должна быть переопределена в производных классах } /// public virtual void UnregisterCommand(string commandId) { - _commands.Remove(commandId); + // Базовая реализация, должна быть переопределена в производных классах } /// /// Получает команду по идентификатору. /// - public IDockCommand? GetCommand(string commandId) + protected virtual IDockCommand? GetCommand(string commandId) { - _commands.TryGetValue(commandId, out var command); - return command; + // Базовая реализация, должна быть переопределена в производных классах + return null; } /// @@ -52,7 +40,7 @@ public abstract class DockContextManagerBase : IDockContextManager protected virtual IEnumerable GetCommandsForElement(IDockControl element) { // Фильтрация команд по типу элемента и его состоянию - return _commands.Values.Where(c => CanExecuteCommand(c, element)); + yield break; } /// @@ -76,7 +64,6 @@ public abstract class DockContextManagerBase : IDockContextManager /// protected virtual void OnContextMenuShown(IDockControl target, double x, double y) { - _currentContextTarget = target; ContextMenuShown?.Invoke(this, new ContextMenuShownEventArgs(target, x, y)); } @@ -85,12 +72,12 @@ public abstract class DockContextManagerBase : IDockContextManager /// protected virtual void OnContextMenuHidden() { - _currentContextTarget = null; ContextMenuHidden?.Invoke(this, EventArgs.Empty); } - /// - /// Получает текущий целевой элемент контекстного меню. - /// - protected IDockControl? CurrentContextTarget => _currentContextTarget; + /// + public event EventHandler? ContextMenuShown; + + /// + public event EventHandler? ContextMenuHidden; } \ No newline at end of file