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
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