Доработан winui
This commit is contained in:
@@ -11,6 +11,12 @@ public interface IDockContent
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
string Id { get; }
|
string Id { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Устанавливает идентификатор контента.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">Новый идентификатор.</param>
|
||||||
|
void SetId(string id);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает заголовок, отображаемый пользователю на вкладке.
|
/// Получает заголовок, отображаемый пользователю на вкладке.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -114,7 +114,16 @@ public static class DockOperations
|
|||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Если target был корнем, новая группа становится новым корнем
|
||||||
|
if (target == root)
|
||||||
|
{
|
||||||
|
newGroup.Parent = null;
|
||||||
|
return newGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Эта точка недостижима при правильном использовании,
|
||||||
|
// но добавляем для безопасности
|
||||||
newGroup.Parent = null;
|
newGroup.Parent = null;
|
||||||
return newGroup; // Новая группа стала корнем
|
return newGroup;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,41 +1,29 @@
|
|||||||
using Lattice.Core.Docking.Abstractions;
|
using Lattice.Core.Docking.Abstractions;
|
||||||
using Lattice.Core.Docking.Models;
|
using Lattice.Core.Docking.Models;
|
||||||
|
using Lattice.Core.Docking.Services;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("Lattice.Serialization.Docking")]
|
[assembly: InternalsVisibleTo("Lattice.UI.Docking.WinUI")]
|
||||||
|
|
||||||
namespace Lattice.Core.Docking.Engine;
|
namespace Lattice.Core.Docking.Engine;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Центральный менеджер макета, управляющий всей структурой док-системы.
|
/// Центральный менеджер макета, управляющий всей структурой док-системы.
|
||||||
/// Координирует дерево компоновки, плавающие окна, автоскрываемые панели
|
/// Координирует дерево компоновки, плавающие окна, автоскрываемые панели
|
||||||
/// и предоставляет API для манипуляции макетом.
|
/// и предоставляет API для манипуляции макетом. Использует кэширование
|
||||||
|
/// для оптимизации поиска элементов по идентификатору.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
|
||||||
/// Этот класс является основным координатором док-системы. Он управляет:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Деревом компоновки главного окна</item>
|
|
||||||
/// <item>Коллекцией плавающих окон</item>
|
|
||||||
/// <item>Коллекцией автоскрываемых панелей</item>
|
|
||||||
/// <item>Реестром типов контента</item>
|
|
||||||
/// </list>
|
|
||||||
/// Все изменения в структуре макета должны выполняться через методы этого класса.
|
|
||||||
/// </remarks>
|
|
||||||
public class LayoutManager
|
public class LayoutManager
|
||||||
{
|
{
|
||||||
private readonly ObservableCollection<AutoHidePanel> _autoHidePanels = new();
|
private readonly ObservableCollection<AutoHidePanel> _autoHidePanels = new();
|
||||||
|
private readonly Dictionary<string, IDockElement> _elementCache = new();
|
||||||
private IDockElement? _root;
|
private IDockElement? _root;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает или задает корневой элемент дерева компоновки главного окна.
|
/// Получает или задает корневой элемент дерева компоновки главного окна.
|
||||||
|
/// При изменении значения генерируется событие <see cref="LayoutUpdated"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
|
||||||
/// Корневой элемент или null, если макет пуст.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// При изменении этого свойства генерируется событие <see cref="LayoutUpdated"/>.
|
|
||||||
/// </remarks>
|
|
||||||
public IDockElement? Root
|
public IDockElement? Root
|
||||||
{
|
{
|
||||||
get => _root;
|
get => _root;
|
||||||
@@ -52,34 +40,21 @@ public class LayoutManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает список активных плавающих окон.
|
/// Получает список активных плавающих окон.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
|
||||||
/// Коллекция объектов <see cref="DockWindow"/>, представляющих плавающие окна.
|
|
||||||
/// </value>
|
|
||||||
public List<DockWindow> FloatingWindows { get; } = new();
|
public List<DockWindow> FloatingWindows { get; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает коллекцию автоскрываемых панелей.
|
/// Получает коллекцию автоскрываемых панелей.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
|
||||||
/// Доступная только для чтения коллекция объектов <see cref="AutoHidePanel"/>.
|
|
||||||
/// </value>
|
|
||||||
public ReadOnlyObservableCollection<AutoHidePanel> AutoHidePanels { get; }
|
public ReadOnlyObservableCollection<AutoHidePanel> AutoHidePanels { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает или задает реестр типов контента.
|
/// Получает или задает реестр типов контента.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>
|
public ContentRegistry? ContentRegistry { get; set; }
|
||||||
/// Реестр типов контента или null, если реестр не установлен.
|
|
||||||
/// </value>
|
|
||||||
public Services.ContentRegistry? ContentRegistry { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Происходит при изменении структуры дерева компоновки.
|
/// Происходит при изменении структуры дерева компоновки.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
|
||||||
/// Событие генерируется при любых изменениях в дереве компоновки,
|
|
||||||
/// включая добавление, удаление или перемещение элементов.
|
|
||||||
/// </remarks>
|
|
||||||
public event Action? LayoutUpdated;
|
public event Action? LayoutUpdated;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -100,12 +75,8 @@ public class LayoutManager
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="content">Содержимое панели.</param>
|
/// <param name="content">Содержимое панели.</param>
|
||||||
/// <param name="side">Сторона окна для прикрепления панели.</param>
|
/// <param name="side">Сторона окна для прикрепления панели.</param>
|
||||||
/// <returns>
|
/// <returns>Созданная автоскрываемая панель.</returns>
|
||||||
/// Созданная автоскрываемая панель.
|
/// <exception cref="ArgumentNullException">Выбрасывается, когда <paramref name="content"/> равен null.</exception>
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, когда <paramref name="content"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
public AutoHidePanel AddAutoHidePanel(IDockContent content, DockSide side)
|
public AutoHidePanel AddAutoHidePanel(IDockContent content, DockSide side)
|
||||||
{
|
{
|
||||||
if (content == null) throw new ArgumentNullException(nameof(content));
|
if (content == null) throw new ArgumentNullException(nameof(content));
|
||||||
@@ -120,12 +91,8 @@ public class LayoutManager
|
|||||||
/// Удаляет автоскрываемую панель из коллекции.
|
/// Удаляет автоскрываемую панель из коллекции.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="panel">Панель для удаления.</param>
|
/// <param name="panel">Панель для удаления.</param>
|
||||||
/// <returns>
|
/// <returns>true, если панель была успешно удалена; в противном случае false.</returns>
|
||||||
/// true, если панель была успешно удалена; в противном случае false.
|
/// <exception cref="ArgumentNullException">Выбрасывается, когда <paramref name="panel"/> равен null.</exception>
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, когда <paramref name="panel"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
public bool RemoveAutoHidePanel(AutoHidePanel panel)
|
public bool RemoveAutoHidePanel(AutoHidePanel panel)
|
||||||
{
|
{
|
||||||
if (panel == null) throw new ArgumentNullException(nameof(panel));
|
if (panel == null) throw new ArgumentNullException(nameof(panel));
|
||||||
@@ -143,10 +110,7 @@ public class LayoutManager
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="contentTypeId">Идентификатор типа контента.</param>
|
/// <param name="contentTypeId">Идентификатор типа контента.</param>
|
||||||
/// <param name="id">Уникальный идентификатор документа.</param>
|
/// <param name="id">Уникальный идентификатор документа.</param>
|
||||||
/// <returns>
|
/// <returns>Созданный контент или null, если ContentRegistry не установлен или тип контента не зарегистрирован.</returns>
|
||||||
/// Созданный контент или null, если ContentRegistry не установлен
|
|
||||||
/// или тип контента не зарегистрирован.
|
|
||||||
/// </returns>
|
|
||||||
public IDockContent? CreateDocument(string contentTypeId, string id)
|
public IDockContent? CreateDocument(string contentTypeId, string id)
|
||||||
{
|
{
|
||||||
if (ContentRegistry == null || !ContentRegistry.IsRegistered(contentTypeId))
|
if (ContentRegistry == null || !ContentRegistry.IsRegistered(contentTypeId))
|
||||||
@@ -161,22 +125,8 @@ public class LayoutManager
|
|||||||
/// <param name="source">Перемещаемый элемент.</param>
|
/// <param name="source">Перемещаемый элемент.</param>
|
||||||
/// <param name="target">Целевой элемент, относительно которого выполняется перемещение.</param>
|
/// <param name="target">Целевой элемент, относительно которого выполняется перемещение.</param>
|
||||||
/// <param name="position">Позиция перемещения относительно цели.</param>
|
/// <param name="position">Позиция перемещения относительно цели.</param>
|
||||||
/// <param name="asDocument">
|
/// <param name="asDocument">Если true, контент будет добавлен как документ в центральную область.</param>
|
||||||
/// Если true, контент будет добавлен как документ в центральную область.
|
/// <exception cref="ArgumentNullException">Выбрасывается, когда <paramref name="source"/> равен null.</exception>
|
||||||
/// В текущей реализации этот параметр не используется.
|
|
||||||
/// </param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, когда <paramref name="source"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Метод выполняет следующие шаги:
|
|
||||||
/// <list type="number">
|
|
||||||
/// <item>Удаляет источник из текущего местоположения</item>
|
|
||||||
/// <item>Вставляет источник в новое местоположение относительно цели</item>
|
|
||||||
/// <item>Обновляет структуру дерева компоновки</item>
|
|
||||||
/// </list>
|
|
||||||
/// Если <paramref name="target"/> равен null, элемент помещается в новое плавающее окно.
|
|
||||||
/// </remarks>
|
|
||||||
public void Move(IDockElement source, IDockElement? target,
|
public void Move(IDockElement source, IDockElement? target,
|
||||||
DockPosition position, bool asDocument = false)
|
DockPosition position, bool asDocument = false)
|
||||||
{
|
{
|
||||||
@@ -210,7 +160,10 @@ public class LayoutManager
|
|||||||
|
|
||||||
if (!sourceRemoved) return;
|
if (!sourceRemoved) return;
|
||||||
|
|
||||||
// 2. Вставляем в цель
|
// Обновляем кэш - удаляем перемещенный элемент
|
||||||
|
_elementCache.Remove(source.Id);
|
||||||
|
|
||||||
|
// 2. Вставляем в новое место
|
||||||
if (target == null)
|
if (target == null)
|
||||||
{
|
{
|
||||||
// Создаем новое плавающее окно
|
// Создаем новое плавающее окно
|
||||||
@@ -228,6 +181,9 @@ public class LayoutManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обновляем кэш для вставленного элемента
|
||||||
|
_elementCache[source.Id] = source;
|
||||||
|
|
||||||
LayoutUpdated?.Invoke();
|
LayoutUpdated?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,9 +191,7 @@ public class LayoutManager
|
|||||||
/// Удаляет элемент из всех плавающих окон.
|
/// Удаляет элемент из всех плавающих окон.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="element">Элемент для удаления.</param>
|
/// <param name="element">Элемент для удаления.</param>
|
||||||
/// <returns>
|
/// <returns>true, если элемент был найден и удален; в противном случае false.</returns>
|
||||||
/// true, если элемент был найден и удален; в противном случае false.
|
|
||||||
/// </returns>
|
|
||||||
private bool RemoveFromFloatingWindows(IDockElement element)
|
private bool RemoveFromFloatingWindows(IDockElement element)
|
||||||
{
|
{
|
||||||
foreach (var win in FloatingWindows.ToArray())
|
foreach (var win in FloatingWindows.ToArray())
|
||||||
@@ -277,36 +231,52 @@ public class LayoutManager
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="element">Проверяемый элемент.</param>
|
/// <param name="element">Проверяемый элемент.</param>
|
||||||
/// <param name="ancestor">Предполагаемый предок.</param>
|
/// <param name="ancestor">Предполагаемый предок.</param>
|
||||||
/// <returns>
|
/// <returns>true, если элемент является потомком предка; в противном случае false.</returns>
|
||||||
/// true, если элемент является потомком предка; в противном случае false.
|
|
||||||
/// </returns>
|
|
||||||
private bool IsDescendantOf(IDockElement element, IDockElement ancestor)
|
private bool IsDescendantOf(IDockElement element, IDockElement ancestor)
|
||||||
{
|
{
|
||||||
if (element == ancestor) return true;
|
var current = element.Parent;
|
||||||
if (ancestor is DockGroup group)
|
while (current != null)
|
||||||
return IsDescendantOf(element, group.First) || IsDescendantOf(element, group.Second);
|
{
|
||||||
|
if (current == ancestor)
|
||||||
|
return true;
|
||||||
|
current = current.Parent;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Находит элемент по его идентификатору во всех окнах (главном и плавающих).
|
/// Находит элемент по его идентификатору во всех окнах (главном и плавающих).
|
||||||
|
/// Использует кэширование для оптимизации повторных поисков.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">Идентификатор элемента для поиска.</param>
|
/// <param name="id">Идентификатор элемента для поиска.</param>
|
||||||
/// <returns>
|
/// <returns>Найденный элемент или null, если элемент с таким идентификатором не найден.</returns>
|
||||||
/// Найденный элемент или null, если элемент с таким идентификатором не найден.
|
|
||||||
/// </returns>
|
|
||||||
public IDockElement? FindById(string id)
|
public IDockElement? FindById(string id)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(id)) return null;
|
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)
|
foreach (var win in FloatingWindows)
|
||||||
{
|
{
|
||||||
found = FindRecursive(win.Root, id);
|
found = FindRecursive(win.Root, id);
|
||||||
if (found != null) return found;
|
if (found != null)
|
||||||
|
{
|
||||||
|
_elementCache[id] = found;
|
||||||
|
return found;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,9 +285,7 @@ public class LayoutManager
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="node">Корневой узел поддерева для поиска.</param>
|
/// <param name="node">Корневой узел поддерева для поиска.</param>
|
||||||
/// <param name="id">Идентификатор элемента для поиска.</param>
|
/// <param name="id">Идентификатор элемента для поиска.</param>
|
||||||
/// <returns>
|
/// <returns>Найденный элемент или null, если элемент не найден.</returns>
|
||||||
/// Найденный элемент или null, если элемент не найден.
|
|
||||||
/// </returns>
|
|
||||||
private IDockElement? FindRecursive(IDockElement? node, string id)
|
private IDockElement? FindRecursive(IDockElement? node, string id)
|
||||||
{
|
{
|
||||||
if (node == null) return null;
|
if (node == null) return null;
|
||||||
@@ -331,21 +299,14 @@ public class LayoutManager
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Сбрасывает макет к состоянию по умолчанию.
|
/// Сбрасывает макет к состоянию по умолчанию.
|
||||||
|
/// Очищает корневой элемент, плавающие окна, автоскрываемые панели и кэш.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
|
||||||
/// Метод выполняет следующие действия:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Очищает корневой элемент</item>
|
|
||||||
/// <item>Закрывает все плавающие окна</item>
|
|
||||||
/// <item>Удаляет все автоскрываемые панели</item>
|
|
||||||
/// <item>Генерирует соответствующие события</item>
|
|
||||||
/// </list>
|
|
||||||
/// </remarks>
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
Root = null;
|
Root = null;
|
||||||
FloatingWindows.Clear();
|
FloatingWindows.Clear();
|
||||||
_autoHidePanels.Clear();
|
_autoHidePanels.Clear();
|
||||||
|
_elementCache.Clear();
|
||||||
LayoutUpdated?.Invoke();
|
LayoutUpdated?.Invoke();
|
||||||
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
|
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
@@ -354,9 +315,7 @@ public class LayoutManager
|
|||||||
/// Находит элемент по идентификатору в дереве компоновки.
|
/// Находит элемент по идентификатору в дереве компоновки.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">Идентификатор элемента для поиска.</param>
|
/// <param name="id">Идентификатор элемента для поиска.</param>
|
||||||
/// <returns>
|
/// <returns>Найденный элемент или null, если элемент с таким идентификатором не найден.</returns>
|
||||||
/// Найденный элемент или null, если элемент с таким идентификатором не найден.
|
|
||||||
/// </returns>
|
|
||||||
public IDockElement? FindElementById(string id)
|
public IDockElement? FindElementById(string id)
|
||||||
{
|
{
|
||||||
return FindElementByIdRecursive(Root, id) ??
|
return FindElementByIdRecursive(Root, id) ??
|
||||||
@@ -369,9 +328,7 @@ public class LayoutManager
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="element">Корневой элемент поддерева для поиска.</param>
|
/// <param name="element">Корневой элемент поддерева для поиска.</param>
|
||||||
/// <param name="id">Идентификатор элемента для поиска.</param>
|
/// <param name="id">Идентификатор элемента для поиска.</param>
|
||||||
/// <returns>
|
/// <returns>Найденный элемент или null, если элемент не найден.</returns>
|
||||||
/// Найденный элемент или null, если элемент не найден.
|
|
||||||
/// </returns>
|
|
||||||
private IDockElement? FindElementByIdRecursive(IDockElement? element, string id)
|
private IDockElement? FindElementByIdRecursive(IDockElement? element, string id)
|
||||||
{
|
{
|
||||||
if (element == null) return null;
|
if (element == null) return null;
|
||||||
|
|||||||
@@ -1,149 +1,65 @@
|
|||||||
using System.ComponentModel;
|
using Lattice.Core.Docking.Abstractions;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
namespace Lattice.Core.Docking.Models;
|
namespace Lattice.Core.Docking.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Представляет автоскрываемую панель, которая может быть прикреплена к одной из сторон окна.
|
/// Представляет автоскрываемую панель, которая может быть прикреплена к одной из сторон окна.
|
||||||
/// Автоскрываемые панели скрываются, оставляя видимой только полоску-заголовок,
|
/// Автоскрываемые панели скрываются, оставляя видимой только заголовок, и разворачиваются при наведении курсора или клике.
|
||||||
/// и разворачиваются при наведении курсора или клике.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
public class AutoHidePanel
|
||||||
/// Автоскрываемые панели являются важным элементом современных IDE-подобных приложений,
|
|
||||||
/// позволяя экономить пространство экрана при сохранении быстрого доступа к инструментам.
|
|
||||||
/// </remarks>
|
|
||||||
public class AutoHidePanel : INotifyPropertyChanged
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Происходит при изменении значения свойства.
|
|
||||||
/// </summary>
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
|
||||||
|
|
||||||
private bool _isVisible = false;
|
|
||||||
private double _slideOffset = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает уникальный идентификатор автоскрываемой панели.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Строковый идентификатор, сгенерированный с помощью GUID.
|
|
||||||
/// </value>
|
|
||||||
public string Id { get; } = Guid.NewGuid().ToString();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает содержимое панели.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Объект, реализующий <see cref="Abstractions.IDockContent"/>.
|
|
||||||
/// </value>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается при попытке установить значение null.
|
|
||||||
/// </exception>
|
|
||||||
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;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает сторону окна, к которой прикреплена панель.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Значение перечисления <see cref="DockSide"/>, указывающее сторону прикрепления.
|
|
||||||
/// </value>
|
|
||||||
public DockSide Side { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает ширину панели (для левой/правой сторон)
|
|
||||||
/// или высоту (для верхней/нижней сторон).
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Размер панели в пикселях. Значение по умолчанию: 300.
|
|
||||||
/// </value>
|
|
||||||
public double Size { get; set; } = 300;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает признак видимости панели.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// true, если панель развернута и видима; в противном случае false.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// При изменении этого свойства генерируется событие <see cref="PropertyChanged"/>.
|
|
||||||
/// </remarks>
|
|
||||||
public bool IsVisible
|
|
||||||
{
|
|
||||||
get => _isVisible;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_isVisible != value)
|
|
||||||
{
|
|
||||||
_isVisible = value;
|
|
||||||
OnPropertyChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает смещение для анимации выезда/заезда панели.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Значение от 0.0 до 1.0, где 0.0 - полностью скрыта, 1.0 - полностью развернута.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Используется для плавной анимации отображения/скрытия панели.
|
|
||||||
/// </remarks>
|
|
||||||
public double SlideOffset
|
|
||||||
{
|
|
||||||
get => _slideOffset;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (Math.Abs(_slideOffset - value) > 0.001)
|
|
||||||
{
|
|
||||||
_slideOffset = value;
|
|
||||||
OnPropertyChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает заголовок панели.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Заголовок, взятый из содержимого панели.
|
|
||||||
/// Если содержимое не установлено, возвращает "Auto-hide Panel".
|
|
||||||
/// </value>
|
|
||||||
public string Title => Content?.Title ?? "Auto-hide Panel";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Инициализирует новый экземпляр класса <see cref="AutoHidePanel"/>.
|
/// Инициализирует новый экземпляр класса <see cref="AutoHidePanel"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="content">Содержимое панели.</param>
|
/// <param name="content">Содержимое панели.</param>
|
||||||
/// <param name="side">Сторона окна для прикрепления.</param>
|
/// <param name="side">Сторона окна для прикрепления панели.</param>
|
||||||
/// <exception cref="ArgumentNullException">
|
/// <exception cref="ArgumentNullException">Выбрасывается, когда <paramref name="content"/> равен null.</exception>
|
||||||
/// Выбрасывается, когда <paramref name="content"/> равен null.
|
public AutoHidePanel(IDockContent content, DockSide side)
|
||||||
/// </exception>
|
|
||||||
public AutoHidePanel(Abstractions.IDockContent content, DockSide side)
|
|
||||||
{
|
{
|
||||||
Content = content ?? throw new ArgumentNullException(nameof(content));
|
Content = content ?? throw new ArgumentNullException(nameof(content));
|
||||||
Side = side;
|
Side = side;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает уникальный идентификатор панели.
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; } = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает содержимое панели.
|
||||||
|
/// </summary>
|
||||||
|
public IDockContent Content { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает сторону окна, к которой прикреплена панель.
|
||||||
|
/// </summary>
|
||||||
|
public DockSide Side { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает размер панели в пикселях.
|
||||||
|
/// Для левой/правой сторон - ширина, для верхней/нижней - высота.
|
||||||
|
/// </summary>
|
||||||
|
public double Size { get; set; } = 300;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает значение, указывающее, видима ли панель.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsVisible { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает смещение для анимации выезда/заезда панели.
|
||||||
|
/// Значение от 0.0 (полностью скрыта) до 1.0 (полностью развернута).
|
||||||
|
/// </summary>
|
||||||
|
public double SlideOffset { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает заголовок панели, взятый из содержимого.
|
||||||
|
/// </summary>
|
||||||
|
public string Title => Content?.Title ?? "Auto-hide Panel";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Переключает видимость панели.
|
/// Переключает видимость панели.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
|
||||||
/// Если панель была видимой, становится скрытой, и наоборот.
|
|
||||||
/// </remarks>
|
|
||||||
public void Toggle()
|
public void Toggle()
|
||||||
{
|
{
|
||||||
IsVisible = !IsVisible;
|
IsVisible = !IsVisible;
|
||||||
@@ -164,15 +80,4 @@ public class AutoHidePanel : INotifyPropertyChanged
|
|||||||
{
|
{
|
||||||
IsVisible = false;
|
IsVisible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывает событие <see cref="PropertyChanged"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="name">
|
|
||||||
/// Имя изменившегося свойства. Если не указано, определяется автоматически.
|
|
||||||
/// </param>
|
|
||||||
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
|
||||||
{
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -4,77 +4,40 @@ using System.Runtime.CompilerServices;
|
|||||||
|
|
||||||
namespace Lattice.Core.Docking.Models;
|
namespace Lattice.Core.Docking.Models;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Представляет узел дерева компоновки, который разделяет доступную область
|
|
||||||
/// между двумя дочерними элементами. Этот класс является основным структурным
|
|
||||||
/// элементом для создания сложных макетов с разделителями.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Каждая группа содержит два дочерних элемента (<see cref="First"/> и <see cref="Second"/>),
|
|
||||||
/// которые могут быть либо другими группами (для создания вложенной структуры),
|
|
||||||
/// либо листами (<see cref="DockLeaf"/>) с контентом.
|
|
||||||
/// Направление разделения определяется свойством <see cref="Orientation"/>.
|
|
||||||
/// </remarks>
|
|
||||||
public class DockGroup : IDockElement, INotifyPropertyChanged
|
public class DockGroup : IDockElement, INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Происходит при изменении значения свойства.
|
|
||||||
/// </summary>
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
|
||||||
|
|
||||||
private double _splitRatio = 0.5;
|
|
||||||
private string _id;
|
|
||||||
private IDockElement _first;
|
private IDockElement _first;
|
||||||
private IDockElement _second;
|
private IDockElement _second;
|
||||||
|
private SplitDirection _orientation;
|
||||||
|
private double _splitRatio = 0.5;
|
||||||
|
private IDockElement? _parent;
|
||||||
|
private double _width;
|
||||||
|
private double _height;
|
||||||
|
|
||||||
/// <summary>
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
/// Получает или задает уникальный идентификатор группы.
|
|
||||||
/// </summary>
|
public DockGroup(IDockElement first, IDockElement second, SplitDirection orientation)
|
||||||
/// <value>
|
|
||||||
/// Строковый идентификатор, уникальный в пределах дерева компоновки.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Идентификатор используется для сериализации/десериализации макета,
|
|
||||||
/// поиска элементов и отслеживания изменений в дереве.
|
|
||||||
/// </remarks>
|
|
||||||
public string Id
|
|
||||||
{
|
{
|
||||||
get => _id;
|
First = first ?? throw new ArgumentNullException(nameof(first));
|
||||||
internal set
|
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();
|
OnPropertyChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает родительский элемент в иерархии дерева компоновки.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Родительский элемент или null, если эта группа является корневой.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Это свойство управляется системой компоновки при добавлении или
|
|
||||||
/// удалении элементов из дерева.
|
|
||||||
/// </remarks>
|
|
||||||
public IDockElement? Parent { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает первый дочерний элемент (левую или верхнюю область).
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Элемент, занимающий первую часть разделенной области.
|
|
||||||
/// </value>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается при попытке установить значение null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// При установке нового значения автоматически обновляется свойство
|
|
||||||
/// <see cref="Parent"/> у дочернего элемента.
|
|
||||||
/// </remarks>
|
|
||||||
public IDockElement First
|
public IDockElement First
|
||||||
{
|
{
|
||||||
get => _first;
|
get => _first;
|
||||||
@@ -89,19 +52,6 @@ public class DockGroup : IDockElement, INotifyPropertyChanged
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает второй дочерний элемент (правую или нижнюю область).
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Элемент, занимающий вторую часть разделенной области.
|
|
||||||
/// </value>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается при попытке установить значение null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// При установке нового значения автоматически обновляется свойство
|
|
||||||
/// <see cref="Parent"/> у дочернего элемента.
|
|
||||||
/// </remarks>
|
|
||||||
public IDockElement Second
|
public IDockElement Second
|
||||||
{
|
{
|
||||||
get => _second;
|
get => _second;
|
||||||
@@ -116,42 +66,25 @@ public class DockGroup : IDockElement, INotifyPropertyChanged
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public SplitDirection Orientation
|
||||||
/// Получает или задает направление разделения данной группы.
|
{
|
||||||
/// </summary>
|
get => _orientation;
|
||||||
/// <value>
|
set
|
||||||
/// Значение перечисления <see cref="SplitDirection"/>, указывающее,
|
{
|
||||||
/// как разделена область: горизонтально или вертикально.
|
if (_orientation != value)
|
||||||
/// </value>
|
{
|
||||||
/// <remarks>
|
_orientation = value;
|
||||||
/// <list type="bullet">
|
OnPropertyChanged();
|
||||||
/// <item><see cref="SplitDirection.Horizontal"/> создает левую и правую области</item>
|
}
|
||||||
/// <item><see cref="SplitDirection.Vertical"/> создает верхнюю и нижнюю области</item>
|
}
|
||||||
/// </list>
|
}
|
||||||
/// </remarks>
|
|
||||||
public SplitDirection Orientation { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает соотношение разделения между первым и вторым элементами.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Значение от 0.0 до 1.0, где:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>0.0 - вся область принадлежит второму элементу</item>
|
|
||||||
/// <item>0.5 - область разделена поровну</item>
|
|
||||||
/// <item>1.0 - вся область принадлежит первому элементу</item>
|
|
||||||
/// </list>
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Изменение этого свойства вызывает событие <see cref="PropertyChanged"/>
|
|
||||||
/// и может привести к перерисовке пользовательского интерфейса.
|
|
||||||
/// </remarks>
|
|
||||||
public double SplitRatio
|
public double SplitRatio
|
||||||
{
|
{
|
||||||
get => _splitRatio;
|
get => _splitRatio;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (Math.Abs(_splitRatio - value) > double.Epsilon)
|
if (Math.Abs(_splitRatio - value) > 0.001)
|
||||||
{
|
{
|
||||||
_splitRatio = value;
|
_splitRatio = value;
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
@@ -159,96 +92,42 @@ public class DockGroup : IDockElement, INotifyPropertyChanged
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public double Width
|
||||||
/// Получает или задает желаемую ширину элемента.
|
{
|
||||||
/// </summary>
|
get => _width;
|
||||||
/// <value>
|
set
|
||||||
/// Ширина в пикселях или относительных единицах.
|
{
|
||||||
/// </value>
|
if (Math.Abs(_width - value) > 0.001)
|
||||||
public double Width { get; set; }
|
{
|
||||||
|
_width = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
public double Height
|
||||||
/// Получает или задает желаемую высоту элемента.
|
{
|
||||||
/// </summary>
|
get => _height;
|
||||||
/// <value>
|
set
|
||||||
/// Высота в пикселях или относительных единицах.
|
{
|
||||||
/// </value>
|
if (Math.Abs(_height - value) > 0.001)
|
||||||
public double Height { get; set; }
|
{
|
||||||
|
_height = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает минимально допустимую ширину элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Минимальная ширина в пикселях, при которой элемент сохраняет функциональность.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Для группы минимальная ширина вычисляется как сумма минимальных ширин
|
|
||||||
/// дочерних элементов при горизонтальной ориентации или максимум минимальных
|
|
||||||
/// ширин при вертикальной ориентации.
|
|
||||||
/// </remarks>
|
|
||||||
public double MinWidth => Orientation == SplitDirection.Horizontal
|
public double MinWidth => Orientation == SplitDirection.Horizontal
|
||||||
? First.MinWidth + Second.MinWidth
|
? First.MinWidth + Second.MinWidth
|
||||||
: Math.Max(First.MinWidth, Second.MinWidth);
|
: Math.Max(First.MinWidth, Second.MinWidth);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает минимально допустимую высоту элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Минимальная высота в пикселях, при которой элемент сохраняет функциональность.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Для группы минимальная высота вычисляется как сумма минимальных высот
|
|
||||||
/// дочерних элементов при вертикальной ориентации или максимум минимальных
|
|
||||||
/// высот при горизонтальной ориентации.
|
|
||||||
/// </remarks>
|
|
||||||
public double MinHeight => Orientation == SplitDirection.Vertical
|
public double MinHeight => Orientation == SplitDirection.Vertical
|
||||||
? First.MinHeight + Second.MinHeight
|
? First.MinHeight + Second.MinHeight
|
||||||
: Math.Max(First.MinHeight, Second.MinHeight);
|
: Math.Max(First.MinHeight, Second.MinHeight);
|
||||||
|
|
||||||
/// <summary>
|
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DockGroup"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="first">
|
|
||||||
/// Первый дочерний элемент (левая или верхняя область).
|
|
||||||
/// </param>
|
|
||||||
/// <param name="second">
|
|
||||||
/// Второй дочерний элемент (правая или нижняя область).
|
|
||||||
/// </param>
|
|
||||||
/// <param name="orientation">
|
|
||||||
/// Направление разделения между дочерними элементами.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="id">
|
|
||||||
/// Уникальный идентификатор группы. Если не указан, генерируется новый GUID.
|
|
||||||
/// </param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, когда <paramref name="first"/> или <paramref name="second"/>
|
|
||||||
/// равны null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Конструктор автоматически устанавливает свойство <see cref="Parent"/>
|
|
||||||
/// у дочерних элементов на текущую группу и генерирует уникальный идентификатор,
|
|
||||||
/// если он не был предоставлен.
|
|
||||||
/// </remarks>
|
|
||||||
public DockGroup(IDockElement first, IDockElement second,
|
|
||||||
SplitDirection orientation, string? id = null)
|
|
||||||
{
|
{
|
||||||
First = first ?? throw new ArgumentNullException(nameof(first));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
Second = second ?? throw new ArgumentNullException(nameof(second));
|
|
||||||
Orientation = orientation;
|
|
||||||
Id = id ?? Guid.NewGuid().ToString();
|
|
||||||
|
|
||||||
First.Parent = this;
|
|
||||||
Second.Parent = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывает событие <see cref="PropertyChanged"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="name">
|
|
||||||
/// Имя изменившегося свойства. Если не указано, определяется автоматически.
|
|
||||||
/// </param>
|
|
||||||
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
|
||||||
{
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,85 +5,44 @@ using System.Runtime.CompilerServices;
|
|||||||
|
|
||||||
namespace Lattice.Core.Docking.Models;
|
namespace Lattice.Core.Docking.Models;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Представляет конечный узел (лист) дерева компоновки, который непосредственно
|
|
||||||
/// содержит коллекцию вкладок с контентом. Этот класс является контейнером для
|
|
||||||
/// отображаемого пользователю содержимого.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Лист является основным элементом, с которым взаимодействует пользователь
|
|
||||||
/// при работе с документами или инструментальными панелями в IDE-подобных
|
|
||||||
/// приложениях.
|
|
||||||
/// </remarks>
|
|
||||||
public class DockLeaf : IDockContainer, INotifyPropertyChanged
|
public class DockLeaf : IDockContainer, INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Происходит при изменении значения свойства.
|
|
||||||
/// </summary>
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
|
||||||
|
|
||||||
private readonly ObservableCollection<IDockContent> _items = new();
|
private readonly ObservableCollection<IDockContent> _items = new();
|
||||||
private IDockContent? _activeContent;
|
private IDockContent? _activeContent;
|
||||||
private string _id;
|
private IDockElement? _parent;
|
||||||
private TabPlacement _tabPlacement = TabPlacement.Bottom;
|
private double _width;
|
||||||
|
private double _height;
|
||||||
|
private TabPlacement _tabPlacement = TabPlacement.Top;
|
||||||
|
|
||||||
/// <summary>
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
/// Получает или задает уникальный идентификатор листа.
|
|
||||||
/// </summary>
|
public DockLeaf()
|
||||||
/// <value>
|
|
||||||
/// Строковый идентификатор, уникальный в пределах дерева компоновки.
|
|
||||||
/// </value>
|
|
||||||
public string Id
|
|
||||||
{
|
{
|
||||||
get => _id;
|
_items.CollectionChanged += (s, e) => OnPropertyChanged(nameof(Children));
|
||||||
internal set
|
}
|
||||||
|
|
||||||
|
public string Id { get; } = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
public IDockElement? Parent
|
||||||
|
{
|
||||||
|
get => _parent;
|
||||||
|
set
|
||||||
{
|
{
|
||||||
if (_id != value)
|
if (_parent != value)
|
||||||
{
|
{
|
||||||
_id = value;
|
_parent = value;
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает родительский элемент в иерархии дерева компоновки.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Родительский элемент или null, если этот лист является корневым.
|
|
||||||
/// </value>
|
|
||||||
public IDockElement? Parent { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает список вкладок, содержащихся в данном контейнере.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Коллекция объектов, реализующих <see cref="IDockContent"/>.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Эта коллекция является наблюдаемой (ObservableCollection), что позволяет
|
|
||||||
/// автоматически обновлять пользовательский интерфейс при добавлении или
|
|
||||||
/// удалении вкладок.
|
|
||||||
/// </remarks>
|
|
||||||
public IList<IDockContent> Children => _items;
|
public IList<IDockContent> Children => _items;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает активную (выбранную) вкладку в контейнере.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Активная вкладка или null, если в контейнере нет вкладок.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// При установке нового значения проверяется, что вкладка действительно
|
|
||||||
/// содержится в коллекции <see cref="Children"/>.
|
|
||||||
/// Изменение этого свойства вызывает событие <see cref="PropertyChanged"/>.
|
|
||||||
/// </remarks>
|
|
||||||
public IDockContent? ActiveContent
|
public IDockContent? ActiveContent
|
||||||
{
|
{
|
||||||
get => _activeContent;
|
get => _activeContent;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (value != null && !_items.Contains(value)) return;
|
|
||||||
if (_activeContent != value)
|
if (_activeContent != value)
|
||||||
{
|
{
|
||||||
_activeContent = value;
|
_activeContent = value;
|
||||||
@@ -92,48 +51,35 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public double Width
|
||||||
/// Получает или задает желаемую ширину элемента.
|
{
|
||||||
/// </summary>
|
get => _width;
|
||||||
/// <value>
|
set
|
||||||
/// Ширина в пикселях или относительных единицах.
|
{
|
||||||
/// </value>
|
if (Math.Abs(_width - value) > 0.001)
|
||||||
public double Width { get; set; }
|
{
|
||||||
|
_width = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
public double Height
|
||||||
/// Получает или задает желаемую высоту элемента.
|
{
|
||||||
/// </summary>
|
get => _height;
|
||||||
/// <value>
|
set
|
||||||
/// Высота в пикселях или относительных единицах.
|
{
|
||||||
/// </value>
|
if (Math.Abs(_height - value) > 0.001)
|
||||||
public double Height { get; set; }
|
{
|
||||||
|
_height = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает минимально допустимую ширину элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Минимальная ширина в пикселях. Значение по умолчанию: 100.
|
|
||||||
/// </value>
|
|
||||||
public double MinWidth { get; set; } = 100;
|
public double MinWidth { get; set; } = 100;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает минимально допустимую высоту элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Минимальная высота в пикселях. Значение по умолчанию: 100.
|
|
||||||
/// </value>
|
|
||||||
public double MinHeight { get; set; } = 100;
|
public double MinHeight { get; set; } = 100;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает положение полосы вкладок в контейнере.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Значение перечисления <see cref="TabPlacement"/>, определяющее,
|
|
||||||
/// где располагаются вкладки относительно содержимого.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Поддерживаются все четыре стороны: верх, низ, лево, право.
|
|
||||||
/// </remarks>
|
|
||||||
public TabPlacement TabPlacement
|
public TabPlacement TabPlacement
|
||||||
{
|
{
|
||||||
get => _tabPlacement;
|
get => _tabPlacement;
|
||||||
@@ -147,47 +93,10 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DockLeaf"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">
|
|
||||||
/// Уникальный идентификатор листа. Если не указан, генерируется новый GUID.
|
|
||||||
/// </param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Создает пустой лист с коллекцией вкладок и генерирует уникальный
|
|
||||||
/// идентификатор, если он не был предоставлен.
|
|
||||||
/// </remarks>
|
|
||||||
public DockLeaf(string? id = null)
|
|
||||||
{
|
|
||||||
_id = id ?? Guid.NewGuid().ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывает событие <see cref="PropertyChanged"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="name">
|
|
||||||
/// Имя изменившегося свойства. Если не указано, определяется автоматически.
|
|
||||||
/// </param>
|
|
||||||
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
|
||||||
{
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Добавляет контент в контейнер и делает его активным.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="content">
|
|
||||||
/// Контент для добавления.
|
|
||||||
/// </param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Если контент уже содержится в коллекции, он не добавляется повторно,
|
|
||||||
/// но становится активным.
|
|
||||||
/// Этот метод обновляет свойство <see cref="ActiveContent"/> и вызывает
|
|
||||||
/// соответствующее событие изменения свойства.
|
|
||||||
/// </remarks>
|
|
||||||
public void AddContent(IDockContent content)
|
public void AddContent(IDockContent content)
|
||||||
{
|
{
|
||||||
if (content == null) return;
|
if (content == null)
|
||||||
|
throw new ArgumentNullException(nameof(content));
|
||||||
|
|
||||||
if (!_items.Contains(content))
|
if (!_items.Contains(content))
|
||||||
{
|
{
|
||||||
@@ -196,22 +105,10 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged
|
|||||||
ActiveContent = content;
|
ActiveContent = content;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Удаляет контент из контейнера.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="content">
|
|
||||||
/// Контент для удаления.
|
|
||||||
/// </param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Если удаляемый контент является активным, автоматически выбирается
|
|
||||||
/// новая активная вкладка (следующая в списке или предыдущая, если удалена
|
|
||||||
/// последняя).
|
|
||||||
/// Если после удаления контейнер становится пустым, он может быть удален
|
|
||||||
/// из дерева макета системой компоновки.
|
|
||||||
/// </remarks>
|
|
||||||
public void RemoveContent(IDockContent content)
|
public void RemoveContent(IDockContent content)
|
||||||
{
|
{
|
||||||
if (content == null) return;
|
if (content == null)
|
||||||
|
throw new ArgumentNullException(nameof(content));
|
||||||
|
|
||||||
int index = _items.IndexOf(content);
|
int index = _items.IndexOf(content);
|
||||||
if (index == -1) return;
|
if (index == -1) return;
|
||||||
@@ -226,4 +123,9 @@ public class DockLeaf : IDockContainer, INotifyPropertyChanged
|
|||||||
ActiveContent = null;
|
ActiveContent = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -37,6 +37,10 @@ public class ContentRegistry
|
|||||||
if (factory == null)
|
if (factory == null)
|
||||||
throw new ArgumentNullException(nameof(factory));
|
throw new ArgumentNullException(nameof(factory));
|
||||||
|
|
||||||
|
// Дополнительная проверка на пустую строку
|
||||||
|
if (string.IsNullOrEmpty(contentTypeId.Trim()))
|
||||||
|
throw new ArgumentException("Идентификатор типа контента не может быть пустой строкой.", nameof(contentTypeId));
|
||||||
|
|
||||||
if (_contentTypes.ContainsKey(contentTypeId))
|
if (_contentTypes.ContainsKey(contentTypeId))
|
||||||
throw new ArgumentException($"Тип контента '{contentTypeId}' уже зарегистрирован.");
|
throw new ArgumentException($"Тип контента '{contentTypeId}' уже зарегистрирован.");
|
||||||
|
|
||||||
@@ -70,13 +74,7 @@ public class ContentRegistry
|
|||||||
throw new KeyNotFoundException($"Тип контента '{contentTypeId}' не зарегистрирован.");
|
throw new KeyNotFoundException($"Тип контента '{contentTypeId}' не зарегистрирован.");
|
||||||
|
|
||||||
var content = descriptor.Factory();
|
var content = descriptor.Factory();
|
||||||
|
content.SetId(id);
|
||||||
// Устанавливаем ID через рефлексию, если есть свойство Id
|
|
||||||
var property = content.GetType().GetProperty("Id");
|
|
||||||
if (property != null && property.CanWrite)
|
|
||||||
{
|
|
||||||
property.SetValue(content, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Lattice.Themes.Core.Tokens;
|
namespace Lattice.Themes.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Статические ключи для ресурсов Lattice Framework.
|
/// Статические ключи для ресурсов Lattice Framework.
|
||||||
|
|||||||
@@ -1,22 +1,32 @@
|
|||||||
using Lattice.Themes.Core.Tokens;
|
using Lattice.Themes.Core;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
|
||||||
using Microsoft.UI.Xaml.Media;
|
|
||||||
|
|
||||||
namespace Lattice.Themes;
|
namespace Lattice.Themes;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Менеджер тем для Lattice Framework.
|
/// Менеджер тем для Lattice Framework. Управляет регистрацией, применением и переключением тем оформления.
|
||||||
|
/// Предоставляет доступ к токенам темы и поддерживает динамическое обновление UI при смене темы.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ThemeManager
|
public sealed class ThemeManager
|
||||||
{
|
{
|
||||||
public static ThemeManager Current { get; } = new();
|
private static readonly ThemeManager _instance = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает текущий экземпляр менеджера тем (синглтон).
|
||||||
|
/// </summary>
|
||||||
|
public static ThemeManager Current => _instance;
|
||||||
|
|
||||||
private ThemePack? _currentTheme;
|
private ThemePack? _currentTheme;
|
||||||
private readonly Dictionary<string, ThemePack> _registeredThemes = new();
|
private readonly Dictionary<string, ThemePack> _registeredThemes = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает текущую активную тему.
|
||||||
|
/// </summary>
|
||||||
public ThemePack? CurrentTheme => _currentTheme;
|
public ThemePack? CurrentTheme => _currentTheme;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Происходит при изменении текущей темы.
|
||||||
|
/// </summary>
|
||||||
public event EventHandler<ThemeChangedEventArgs>? ThemeChanged;
|
public event EventHandler<ThemeChangedEventArgs>? ThemeChanged;
|
||||||
|
|
||||||
private ThemeManager() { }
|
private ThemeManager() { }
|
||||||
@@ -24,6 +34,8 @@ public sealed class ThemeManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Регистрирует тему в менеджере.
|
/// Регистрирует тему в менеджере.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="theme">Тема для регистрации.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">Выбрасывается, если <paramref name="theme"/> равен null.</exception>
|
||||||
public void RegisterTheme(ThemePack theme)
|
public void RegisterTheme(ThemePack theme)
|
||||||
{
|
{
|
||||||
if (theme == null)
|
if (theme == null)
|
||||||
@@ -35,6 +47,8 @@ public sealed class ThemeManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает зарегистрированную тему по имени.
|
/// Получает зарегистрированную тему по имени.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="name">Имя темы.</param>
|
||||||
|
/// <returns>Зарегистрированная тема или null, если тема не найдена.</returns>
|
||||||
public ThemePack? GetTheme(string name)
|
public ThemePack? GetTheme(string name)
|
||||||
{
|
{
|
||||||
_registeredThemes.TryGetValue(name, out var theme);
|
_registeredThemes.TryGetValue(name, out var theme);
|
||||||
@@ -44,17 +58,18 @@ public sealed class ThemeManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает список всех зарегистрированных тем.
|
/// Получает список всех зарегистрированных тем.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <returns>Неизменяемая коллекция зарегистрированных тем.</returns>
|
||||||
public IReadOnlyCollection<ThemePack> GetRegisteredThemes()
|
public IReadOnlyCollection<ThemePack> GetRegisteredThemes()
|
||||||
{
|
{
|
||||||
return _registeredThemes.Values.ToList();
|
return _registeredThemes.Values.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получение информации о теме.
|
/// Получает информацию о зарегистрированной теме.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="themeName"></param>
|
/// <param name="themeName">Имя темы.</param>
|
||||||
/// <returns></returns>
|
/// <returns>Информация о теме или null, если тема не зарегистрирована.</returns>
|
||||||
public ThemeInfo GetThemeInfo(string themeName)
|
public ThemeInfo? GetThemeInfo(string themeName)
|
||||||
{
|
{
|
||||||
if (!_registeredThemes.TryGetValue(themeName, out var theme))
|
if (!_registeredThemes.TryGetValue(themeName, out var theme))
|
||||||
return null;
|
return null;
|
||||||
@@ -72,6 +87,9 @@ public sealed class ThemeManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Применяет тему по имени.
|
/// Применяет тему по имени.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="themeName">Имя темы для применения.</param>
|
||||||
|
/// <exception cref="ArgumentException">Выбрасывается, если тема с указанным именем не зарегистрирована.</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">Выбрасывается, если не удалось применить тему.</exception>
|
||||||
public void ApplyTheme(string themeName)
|
public void ApplyTheme(string themeName)
|
||||||
{
|
{
|
||||||
if (!_registeredThemes.TryGetValue(themeName, out var theme))
|
if (!_registeredThemes.TryGetValue(themeName, out var theme))
|
||||||
@@ -85,6 +103,9 @@ public sealed class ThemeManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Применяет указанную тему.
|
/// Применяет указанную тему.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="theme">Тема для применения.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">Выбрасывается, если <paramref name="theme"/> равен null.</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">Выбрасывается, если не удалось применить тему.</exception>
|
||||||
public void ApplyTheme(ThemePack theme)
|
public void ApplyTheme(ThemePack theme)
|
||||||
{
|
{
|
||||||
if (theme == null)
|
if (theme == null)
|
||||||
@@ -93,21 +114,21 @@ public sealed class ThemeManager
|
|||||||
if (_currentTheme == theme)
|
if (_currentTheme == theme)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var old = _currentTheme;
|
var oldTheme = _currentTheme;
|
||||||
_currentTheme = theme;
|
_currentTheme = theme;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ReplaceApplicationResources(theme);
|
ReplaceApplicationResources(theme);
|
||||||
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs(old!, theme));
|
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs(oldTheme!, theme));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// В случае ошибки возвращаемся к старой теме
|
// Восстанавливаем предыдущую тему при ошибке
|
||||||
_currentTheme = old;
|
_currentTheme = oldTheme;
|
||||||
if (old != null)
|
if (oldTheme != null)
|
||||||
{
|
{
|
||||||
ReplaceApplicationResources(old);
|
ReplaceApplicationResources(oldTheme);
|
||||||
}
|
}
|
||||||
throw new InvalidOperationException($"Failed to apply theme '{theme.Name}'.", ex);
|
throw new InvalidOperationException($"Failed to apply theme '{theme.Name}'.", ex);
|
||||||
}
|
}
|
||||||
@@ -116,22 +137,24 @@ public sealed class ThemeManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Загружает ресурсы темы в указанный словарь ресурсов.
|
/// Загружает ресурсы темы в указанный словарь ресурсов.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="targetDictionary">Целевой словарь ресурсов.</param>
|
||||||
|
/// <param name="theme">Тема, ресурсы которой нужно загрузить.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">Выбрасывается, если <paramref name="targetDictionary"/> или <paramref name="theme"/> равны null.</exception>
|
||||||
public void LoadThemeIntoDictionary(ResourceDictionary targetDictionary, ThemePack theme)
|
public void LoadThemeIntoDictionary(ResourceDictionary targetDictionary, ThemePack theme)
|
||||||
{
|
{
|
||||||
if (targetDictionary == null)
|
if (targetDictionary == null)
|
||||||
throw new ArgumentNullException(nameof(targetDictionary));
|
throw new ArgumentNullException(nameof(targetDictionary));
|
||||||
|
|
||||||
if (theme == null)
|
if (theme == null)
|
||||||
throw new ArgumentNullException(nameof(theme));
|
throw new ArgumentNullException(nameof(theme));
|
||||||
|
|
||||||
// Очищаем старые словари Lattice
|
// Удаляем все ThemeDictionary из словаря
|
||||||
for (int i = targetDictionary.MergedDictionaries.Count - 1; i >= 0; i--)
|
for (int i = targetDictionary.MergedDictionaries.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
if (targetDictionary.MergedDictionaries[i] is ThemeDictionary)
|
if (targetDictionary.MergedDictionaries[i] is ThemeDictionary)
|
||||||
targetDictionary.MergedDictionaries.RemoveAt(i);
|
targetDictionary.MergedDictionaries.RemoveAt(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем новые словари темы
|
// Добавляем словари темы
|
||||||
foreach (var uri in theme.GetResourceUris())
|
foreach (var uri in theme.GetResourceUris())
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -146,6 +169,11 @@ public sealed class ThemeManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Подсчитывает количество токенов в теме.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="theme">Тема для подсчета токенов.</param>
|
||||||
|
/// <returns>Количество токенов в теме. Возвращает 0 при возникновении ошибки.</returns>
|
||||||
private int CountTokensInTheme(ThemePack theme)
|
private int CountTokensInTheme(ThemePack theme)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -160,6 +188,10 @@ public sealed class ThemeManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Заменяет ресурсы приложения на ресурсы указанной темы.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="theme">Тема, ресурсы которой нужно применить.</param>
|
||||||
private void ReplaceApplicationResources(ThemePack theme)
|
private void ReplaceApplicationResources(ThemePack theme)
|
||||||
{
|
{
|
||||||
var app = Application.Current;
|
var app = Application.Current;
|
||||||
@@ -171,45 +203,32 @@ public sealed class ThemeManager
|
|||||||
ForceUpdateUI();
|
ForceUpdateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Принудительно обновляет пользовательский интерфейс после смены темы.
|
||||||
|
/// Использует легковесный подход без рекурсивного обхода дерева элементов.
|
||||||
|
/// </summary>
|
||||||
private void ForceUpdateUI()
|
private void ForceUpdateUI()
|
||||||
{
|
{
|
||||||
foreach (var window in WindowTracker.Windows)
|
foreach (var window in WindowTracker.Windows)
|
||||||
{
|
{
|
||||||
if (window.Content is FrameworkElement root)
|
if (window.Content is FrameworkElement root)
|
||||||
RefreshElement(root);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RefreshElement(FrameworkElement element)
|
|
||||||
{
|
|
||||||
var stack = new Stack<FrameworkElement>();
|
|
||||||
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;
|
var resources = root.Resources;
|
||||||
control.Template = template;
|
var currentTheme = _currentTheme;
|
||||||
}
|
if (currentTheme != null)
|
||||||
else if (current is ContentPresenter contentPresenter)
|
{
|
||||||
{
|
LoadThemeIntoDictionary(resources, currentTheme);
|
||||||
// Обновляем ContentPresenter
|
}
|
||||||
var content = contentPresenter.Content;
|
|
||||||
contentPresenter.Content = null;
|
|
||||||
contentPresenter.Content = content;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Добавляем детей в стек
|
// Принудительное обновление стилей через перезагрузку ResourceDictionary
|
||||||
int count = VisualTreeHelper.GetChildrenCount(current);
|
var mergedDictionaries = resources.MergedDictionaries;
|
||||||
for (int i = 0; i < count; i++)
|
if (mergedDictionaries.Count > 0)
|
||||||
{
|
{
|
||||||
if (VisualTreeHelper.GetChild(current, i) is FrameworkElement child)
|
var temp = mergedDictionaries[mergedDictionaries.Count - 1];
|
||||||
stack.Push(child);
|
mergedDictionaries.RemoveAt(mergedDictionaries.Count - 1);
|
||||||
|
mergedDictionaries.Add(temp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,6 +236,7 @@ public sealed class ThemeManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Проверяет, что все необходимые токены определены в текущей теме.
|
/// Проверяет, что все необходимые токены определены в текущей теме.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <returns>true, если все токены присутствуют; иначе false.</returns>
|
||||||
public bool ValidateThemeTokens()
|
public bool ValidateThemeTokens()
|
||||||
{
|
{
|
||||||
if (_currentTheme == null)
|
if (_currentTheme == null)
|
||||||
@@ -249,6 +269,8 @@ public sealed class ThemeManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает значение токена из текущей темы.
|
/// Получает значение токена из текущей темы.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="tokenKey">Ключ токена.</param>
|
||||||
|
/// <returns>Значение токена или null, если токен не найден или приложение не инициализировано.</returns>
|
||||||
public object? GetTokenValue(string tokenKey)
|
public object? GetTokenValue(string tokenKey)
|
||||||
{
|
{
|
||||||
var app = Application.Current;
|
var app = Application.Current;
|
||||||
@@ -266,6 +288,9 @@ public sealed class ThemeManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает значение токена с приведением к указанному типу.
|
/// Получает значение токена с приведением к указанному типу.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Тип, к которому приводится значение токена.</typeparam>
|
||||||
|
/// <param name="tokenKey">Ключ токена.</param>
|
||||||
|
/// <returns>Значение токена или значение по умолчанию для типа T, если токен не найден.</returns>
|
||||||
public T? GetTokenValue<T>(string tokenKey)
|
public T? GetTokenValue<T>(string tokenKey)
|
||||||
{
|
{
|
||||||
object? value = GetTokenValue(tokenKey);
|
object? value = GetTokenValue(tokenKey);
|
||||||
@@ -278,16 +303,32 @@ public sealed class ThemeManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Информация о теме.
|
/// Предоставляет информацию о теме оформления.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ThemeInfo
|
public class ThemeInfo
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Название темы.
|
/// Получает или задает название темы.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает описание темы.
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает версию темы.
|
||||||
|
/// </summary>
|
||||||
|
public string Version { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает значение, указывающее, является ли тема темной.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Name { get; set; }
|
|
||||||
public string Description { get; set; }
|
|
||||||
public string Version { get; set; }
|
|
||||||
public bool IsDark { get; set; }
|
public bool IsDark { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает количество токенов в теме.
|
||||||
|
/// </summary>
|
||||||
public int TokenCount { get; set; }
|
public int TokenCount { get; set; }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using Lattice.UI.Docking.Abstractions;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
|
||||||
|
namespace Lattice.UI.Docking.WinUI.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Интерфейс для элементов, поддерживающих WinUI Drag & Drop.
|
||||||
|
/// Наследуется от IDockControl и добавляет WinUI-специфичные возможности.
|
||||||
|
/// </summary>
|
||||||
|
public interface IWinUIDragDropControl : IDockControl
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получает UI-элемент для операций Drag & Drop.
|
||||||
|
/// </summary>
|
||||||
|
FrameworkElement? DragDropElement { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настраивает обработчики Drag & Drop.
|
||||||
|
/// </summary>
|
||||||
|
void SetupDragDropHandlers();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Начинает операцию перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
void StartDrag();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Завершает операцию перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
void EndDrag();
|
||||||
|
}
|
||||||
189
Lattice.UI.Docking.WinUI/Controls/AdvancedTabControl.cs
Normal file
189
Lattice.UI.Docking.WinUI/Controls/AdvancedTabControl.cs
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Представляет расширенный контрол вкладок с поддержкой всех позиций размещения панели вкладок.
|
||||||
|
/// Обеспечивает отображение коллекции вкладок с возможностью навигации, закрытия и изменения порядка.
|
||||||
|
/// Поддерживает четыре позиции размещения: сверху, снизу, слева и справа.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AdvancedTabControl : Control
|
||||||
|
{
|
||||||
|
private Grid? _rootGrid;
|
||||||
|
private TabView? _tabView;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="AdvancedTabControl"/>.
|
||||||
|
/// </summary>
|
||||||
|
public AdvancedTabControl()
|
||||||
|
{
|
||||||
|
DefaultStyleKey = typeof(AdvancedTabControl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Идентифицирует свойство зависимостей <see cref="ItemsSource"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly DependencyProperty ItemsSourceProperty =
|
||||||
|
DependencyProperty.Register(nameof(ItemsSource), typeof(ObservableCollection<object>),
|
||||||
|
typeof(AdvancedTabControl), new PropertyMetadata(null));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Идентифицирует свойство зависимостей <see cref="SelectedItem"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly DependencyProperty SelectedItemProperty =
|
||||||
|
DependencyProperty.Register(nameof(SelectedItem), typeof(object),
|
||||||
|
typeof(AdvancedTabControl), new PropertyMetadata(null));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Идентифицирует свойство зависимостей <see cref="TabPlacement"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly DependencyProperty TabPlacementProperty =
|
||||||
|
DependencyProperty.Register(nameof(TabPlacement), typeof(TabPlacement),
|
||||||
|
typeof(AdvancedTabControl), new PropertyMetadata(TabPlacement.Top, OnTabPlacementChanged));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает источник данных для вкладок.
|
||||||
|
/// </summary>
|
||||||
|
public ObservableCollection<object> ItemsSource
|
||||||
|
{
|
||||||
|
get => (ObservableCollection<object>)GetValue(ItemsSourceProperty);
|
||||||
|
set => SetValue(ItemsSourceProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает выбранный элемент вкладки.
|
||||||
|
/// </summary>
|
||||||
|
public object SelectedItem
|
||||||
|
{
|
||||||
|
get => GetValue(SelectedItemProperty);
|
||||||
|
set => SetValue(SelectedItemProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает или задает положение панели вкладок.
|
||||||
|
/// </summary>
|
||||||
|
public TabPlacement TabPlacement
|
||||||
|
{
|
||||||
|
get => (TabPlacement)GetValue(TabPlacementProperty);
|
||||||
|
set => SetValue(TabPlacementProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вызывается при применении шаблона контрола.
|
||||||
|
/// </summary>
|
||||||
|
protected override void OnApplyTemplate()
|
||||||
|
{
|
||||||
|
base.OnApplyTemplate();
|
||||||
|
|
||||||
|
_rootGrid = GetTemplateChild("PART_RootGrid") as Grid;
|
||||||
|
_tabView = GetTemplateChild("PART_TabView") as TabView;
|
||||||
|
|
||||||
|
UpdateTabPlacement();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Обновляет положение панели вкладок в соответствии с текущим значением свойства <see cref="TabPlacement"/>.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настраивает размещение панели вкладок вверху.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настраивает размещение панели вкладок внизу.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настраивает размещение панели вкладок слева.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настраивает размещение панели вкладок справа.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Обрабатывает изменение значения свойства <see cref="TabPlacement"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="d">Объект зависимости, значение которого изменилось.</param>
|
||||||
|
/// <param name="e">Данные о изменении свойства.</param>
|
||||||
|
private static void OnTabPlacementChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (d is AdvancedTabControl control)
|
||||||
|
control.UpdateTabPlacement();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,23 +10,6 @@ using System.Runtime.CompilerServices;
|
|||||||
|
|
||||||
namespace Lattice.UI;
|
namespace Lattice.UI;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Визуальный контрол для отображения группы разделения (сплиттера) в системе докинга.
|
|
||||||
/// Реализует интерфейс <see cref="IDockGroupControl"/> для интеграции с системой докинга
|
|
||||||
/// и обеспечивает отображение двух дочерних элементов с разделителем между ними.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Контрол <see cref="LatticeDockGroup"/> отвечает за визуальное представление узла
|
|
||||||
/// дерева компоновки, который разделяет доступное пространство между двумя дочерними
|
|
||||||
/// элементами. Поддерживает горизонтальное и вертикальное разделение с возможностью
|
|
||||||
/// изменения соотношения сторон через перетаскивание разделителя.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Контрол автоматически обновляет свое представление при изменении свойств модели
|
|
||||||
/// и обеспечивает двустороннюю привязку данных с объектом <see cref="DockGroup"/>.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
||||||
{
|
{
|
||||||
private readonly PropertyChangedEventHandler _modelPropertyChangedHandler;
|
private readonly PropertyChangedEventHandler _modelPropertyChangedHandler;
|
||||||
@@ -42,14 +25,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
private double _splitRatio = 0.5;
|
private double _splitRatio = 0.5;
|
||||||
private double _splitterSize = 4.0;
|
private double _splitterSize = 4.0;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="LatticeDockGroup"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Конструктор устанавливает ключ стиля по умолчанию, инициализирует обработчик
|
|
||||||
/// изменений модели и подписывается на событие изменения контекста данных.
|
|
||||||
/// Созданный контрол готов к использованию после применения шаблона.
|
|
||||||
/// </remarks>
|
|
||||||
public LatticeDockGroup()
|
public LatticeDockGroup()
|
||||||
{
|
{
|
||||||
this.DefaultStyleKey = typeof(LatticeDockGroup);
|
this.DefaultStyleKey = typeof(LatticeDockGroup);
|
||||||
@@ -57,18 +32,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
this.DataContextChanged += OnDataContextChanged;
|
this.DataContextChanged += OnDataContextChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает модель данных, связанную с этим контролом.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="DockGroup"/>, представляющий узел разделения в дереве компоновки.
|
|
||||||
/// Может быть null, если контрол не связан с моделью.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// При установке новой модели контрол автоматически подписывается на события
|
|
||||||
/// изменения свойств модели и обновляет свое визуальное представление.
|
|
||||||
/// При удалении модели происходит отписка от событий и очистка ресурсов.
|
|
||||||
/// </remarks>
|
|
||||||
public IDockElement? Model
|
public IDockElement? Model
|
||||||
{
|
{
|
||||||
get => _model;
|
get => _model;
|
||||||
@@ -82,18 +45,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает менеджер макета, к которому принадлежит этот контрол.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="LayoutManager"/>, управляющий структурой док-системы.
|
|
||||||
/// Может быть null, если контрол не связан с менеджером макета.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Менеджер макета используется для выполнения операций с деревом компоновки,
|
|
||||||
/// таких как перемещение элементов, создание плавающих окон и управление
|
|
||||||
/// автоскрываемыми панелями.
|
|
||||||
/// </remarks>
|
|
||||||
public LayoutManager? LayoutManager
|
public LayoutManager? LayoutManager
|
||||||
{
|
{
|
||||||
get => _layoutManager;
|
get => _layoutManager;
|
||||||
@@ -105,17 +56,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает контекстный менеджер для этого контрола.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="IDockContextManager"/> или null, если менеджер не установлен.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Контекстный менеджер используется для отображения контекстных меню при щелчке
|
|
||||||
/// правой кнопкой мыши по контролу. Меню содержит команды, доступные для данного
|
|
||||||
/// элемента в текущем контексте.
|
|
||||||
/// </remarks>
|
|
||||||
public IDockContextManager? ContextManager
|
public IDockContextManager? ContextManager
|
||||||
{
|
{
|
||||||
get => _contextManager;
|
get => _contextManager;
|
||||||
@@ -127,18 +67,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает признак того, что контрол выбран.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// true, если контрол выбран; в противном случае false.
|
|
||||||
/// Значение по умолчанию: false.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Выделенный контрол обычно визуально отличается от других (например, имеет
|
|
||||||
/// выделенную границу или фон). В каждый момент времени может быть выделен
|
|
||||||
/// только один контрол в пределах контейнера.
|
|
||||||
/// </remarks>
|
|
||||||
public bool IsSelected
|
public bool IsSelected
|
||||||
{
|
{
|
||||||
get => _isSelected;
|
get => _isSelected;
|
||||||
@@ -150,17 +78,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает признак того, что контрол активен.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// true, если контрол активен; в противном случае false.
|
|
||||||
/// Значение по умолчанию: false.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Активный контрол получает фокус ввода и может обрабатывать команды клавиатуры.
|
|
||||||
/// Обычно соответствует последнему взаимодействию пользователя с элементом.
|
|
||||||
/// </remarks>
|
|
||||||
public bool IsActive
|
public bool IsActive
|
||||||
{
|
{
|
||||||
get => _isActive;
|
get => _isActive;
|
||||||
@@ -172,20 +89,9 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public bool CanDrag => true;
|
||||||
/// Получает или задает ориентацию разделения группы.
|
public bool CanDrop => true;
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Направление разделения (горизонтальное или вертикальное).
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Ориентация определяет, как расположены дочерние элементы относительно друг друга:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item><see cref="SplitDirection.Horizontal"/> - элементы расположены слева и справа</item>
|
|
||||||
/// <item><see cref="SplitDirection.Vertical"/> - элементы расположены сверху и снизу</item>
|
|
||||||
/// </list>
|
|
||||||
/// Изменение ориентации приводит к перестройке внутреннего макета контрола.
|
|
||||||
/// </remarks>
|
|
||||||
public SplitDirection Orientation
|
public SplitDirection Orientation
|
||||||
{
|
{
|
||||||
get => _model?.Orientation ?? SplitDirection.Horizontal;
|
get => _model?.Orientation ?? SplitDirection.Horizontal;
|
||||||
@@ -199,18 +105,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает соотношение разделения между первым и вторым элементами.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Значение от 0.0 до 1.0, где 0.5 означает равное разделение пространства.
|
|
||||||
/// Значение 0.0 отдает все пространство второму элементу, 1.0 - первому элементу.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Соотношение разделения определяет пропорции, в которых доступное пространство
|
|
||||||
/// распределяется между дочерними элементами. Изменение этого свойства приводит
|
|
||||||
/// к перестройке внутреннего макета и генерации события <see cref="SplitRatioChanged"/>.
|
|
||||||
/// </remarks>
|
|
||||||
public double SplitRatio
|
public double SplitRatio
|
||||||
{
|
{
|
||||||
get => _splitRatio;
|
get => _splitRatio;
|
||||||
@@ -221,24 +115,12 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
_splitRatio = value;
|
_splitRatio = value;
|
||||||
UpdateLayoutDefinitions();
|
UpdateLayoutDefinitions();
|
||||||
OnPropertyChanged(nameof(SplitRatio));
|
OnPropertyChanged(nameof(SplitRatio));
|
||||||
|
|
||||||
SplitRatioChanged?.Invoke(this,
|
SplitRatioChanged?.Invoke(this,
|
||||||
new SplitRatioChangedEventArgs(value, SplitRatioChangeSource.Programmatic));
|
new SplitRatioChangedEventArgs(value, SplitRatioChangeSource.Programmatic));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает размер разделителя в пикселях.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Ширина разделителя в пикселях. Значение по умолчанию: 4.0.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Размер разделителя определяет область, доступную для перетаскивания пользователем
|
|
||||||
/// для изменения соотношения разделения. Увеличение размера облегчает взаимодействие,
|
|
||||||
/// но уменьшает полезное пространство для содержимого.
|
|
||||||
/// </remarks>
|
|
||||||
public double SplitterSize
|
public double SplitterSize
|
||||||
{
|
{
|
||||||
get => _splitterSize;
|
get => _splitterSize;
|
||||||
@@ -252,57 +134,12 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает контрол для первого дочернего элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Контрол, отображающий первый дочерний элемент, или null, если элемент не установлен.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Первый дочерний элемент занимает левую область при горизонтальной ориентации
|
|
||||||
/// или верхнюю область при вертикальной ориентации.
|
|
||||||
/// </remarks>
|
|
||||||
public IDockControl? FirstChild => _firstChildControl?.Content as IDockControl;
|
public IDockControl? FirstChild => _firstChildControl?.Content as IDockControl;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает контрол для второго дочернего элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Контрол, отображающий второй дочерний элемент, или null, если элемент не установлен.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Второй дочерний элемент занимает правую область при горизонтальной ориентации
|
|
||||||
/// или нижнюю область при вертикальной ориентации.
|
|
||||||
/// </remarks>
|
|
||||||
public IDockControl? SecondChild => _secondChildControl?.Content as IDockControl;
|
public IDockControl? SecondChild => _secondChildControl?.Content as IDockControl;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Происходит при изменении соотношения разделения между дочерними элементами.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Событие генерируется при изменении свойства <see cref="SplitRatio"/>,
|
|
||||||
/// независимо от источника изменения (пользователь, программа или восстановление состояния).
|
|
||||||
/// Содержит информацию о новом соотношении и источнике изменения.
|
|
||||||
/// </remarks>
|
|
||||||
public event EventHandler<SplitRatioChangedEventArgs>? SplitRatioChanged;
|
public event EventHandler<SplitRatioChangedEventArgs>? SplitRatioChanged;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Происходит при изменении значения свойства.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Событие реализует интерфейс <see cref="INotifyPropertyChanged"/> и используется
|
|
||||||
/// для уведомления системы привязки данных об изменениях свойств контрола.
|
|
||||||
/// </remarks>
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывается при применении шаблона контрола.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Метод получает ссылки на именованные части шаблона и инициализирует
|
|
||||||
/// внутренние структуры контрола. Вызывает обновление макета для корректного
|
|
||||||
/// отображения дочерних элементов.
|
|
||||||
/// </remarks>
|
|
||||||
protected override void OnApplyTemplate()
|
protected override void OnApplyTemplate()
|
||||||
{
|
{
|
||||||
base.OnApplyTemplate();
|
base.OnApplyTemplate();
|
||||||
@@ -314,48 +151,22 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
UpdateLayoutDefinitions();
|
UpdateLayoutDefinitions();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Обрабатывает изменение контекста данных контрола.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sender">Источник события (контрол).</param>
|
|
||||||
/// <param name="args">Данные о изменении контекста.</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Метод автоматически устанавливает модель контрола на основе нового контекста данных,
|
|
||||||
/// если он является экземпляром <see cref="DockGroup"/>. Это позволяет использовать
|
|
||||||
/// привязку данных XAML для установки модели контрола.
|
|
||||||
/// </remarks>
|
|
||||||
private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
|
private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
|
||||||
{
|
{
|
||||||
Model = args.NewValue as DockGroup;
|
Model = args.NewValue as DockGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Присоединяет модель к контролу.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Подписывается на события изменения свойств модели, устанавливает контекст данных
|
|
||||||
/// и инициализирует свойства контрола значениями из модели. Вызывает обновление макета.
|
|
||||||
/// </remarks>
|
|
||||||
private void AttachModel()
|
private void AttachModel()
|
||||||
{
|
{
|
||||||
if (_model != null)
|
if (_model != null)
|
||||||
{
|
{
|
||||||
_model.PropertyChanged += _modelPropertyChangedHandler;
|
_model.PropertyChanged += _modelPropertyChangedHandler;
|
||||||
this.DataContext = _model;
|
this.DataContext = _model;
|
||||||
|
|
||||||
// Инициализируем свойства из модели
|
|
||||||
_splitRatio = _model.SplitRatio;
|
_splitRatio = _model.SplitRatio;
|
||||||
UpdateLayoutDefinitions();
|
UpdateLayoutDefinitions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Отсоединяет модель от контрола.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Отписывается от событий изменения свойств модели, очищает контекст данных
|
|
||||||
/// и освобождает ресурсы, связанные с предыдущей моделью.
|
|
||||||
/// </remarks>
|
|
||||||
private void DetachModel()
|
private void DetachModel()
|
||||||
{
|
{
|
||||||
if (_model != null)
|
if (_model != null)
|
||||||
@@ -365,16 +176,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Обрабатывает изменения свойств модели.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sender">Источник события (модель).</param>
|
|
||||||
/// <param name="e">Данные об изменении свойства.</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Реагирует на изменения ключевых свойств модели (Orientation, SplitRatio)
|
|
||||||
/// и обновляет соответствующие свойства и визуальное представление контрола.
|
|
||||||
/// Также уведомляет систему привязки данных об изменении свойств контрола.
|
|
||||||
/// </remarks>
|
|
||||||
private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
switch (e.PropertyName)
|
switch (e.PropertyName)
|
||||||
@@ -395,14 +196,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Обновляет определения макета сетки на основе текущей ориентации и соотношения разделения.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Метод перестраивает структуру строк и столбцов сетки в зависимости от ориентации
|
|
||||||
/// разделения и текущего соотношения между дочерними элементами. Обеспечивает
|
|
||||||
/// корректное позиционирование разделителя и дочерних контролов.
|
|
||||||
/// </remarks>
|
|
||||||
private void UpdateLayoutDefinitions()
|
private void UpdateLayoutDefinitions()
|
||||||
{
|
{
|
||||||
if (_rootGrid == null || _model == null) return;
|
if (_rootGrid == null || _model == null) return;
|
||||||
@@ -412,7 +205,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
|
|
||||||
if (_model.Orientation == SplitDirection.Horizontal)
|
if (_model.Orientation == SplitDirection.Horizontal)
|
||||||
{
|
{
|
||||||
// Горизонтальное разделение
|
|
||||||
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition
|
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition
|
||||||
{ Width = new GridLength(_model.SplitRatio, GridUnitType.Star) });
|
{ Width = new GridLength(_model.SplitRatio, GridUnitType.Star) });
|
||||||
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition
|
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition
|
||||||
@@ -420,7 +212,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition
|
_rootGrid.ColumnDefinitions.Add(new ColumnDefinition
|
||||||
{ Width = new GridLength(1 - _model.SplitRatio, GridUnitType.Star) });
|
{ Width = new GridLength(1 - _model.SplitRatio, GridUnitType.Star) });
|
||||||
|
|
||||||
// Устанавливаем позиции элементов
|
|
||||||
if (_firstChildControl != null)
|
if (_firstChildControl != null)
|
||||||
{
|
{
|
||||||
Grid.SetColumn(_firstChildControl, 0);
|
Grid.SetColumn(_firstChildControl, 0);
|
||||||
@@ -435,7 +226,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Вертикальное разделение
|
|
||||||
_rootGrid.RowDefinitions.Add(new RowDefinition
|
_rootGrid.RowDefinitions.Add(new RowDefinition
|
||||||
{ Height = new GridLength(_model.SplitRatio, GridUnitType.Star) });
|
{ Height = new GridLength(_model.SplitRatio, GridUnitType.Star) });
|
||||||
_rootGrid.RowDefinitions.Add(new RowDefinition
|
_rootGrid.RowDefinitions.Add(new RowDefinition
|
||||||
@@ -443,7 +233,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
_rootGrid.RowDefinitions.Add(new RowDefinition
|
_rootGrid.RowDefinitions.Add(new RowDefinition
|
||||||
{ Height = new GridLength(1 - _model.SplitRatio, GridUnitType.Star) });
|
{ Height = new GridLength(1 - _model.SplitRatio, GridUnitType.Star) });
|
||||||
|
|
||||||
// Устанавливаем позиции элементов
|
|
||||||
if (_firstChildControl != null)
|
if (_firstChildControl != null)
|
||||||
{
|
{
|
||||||
Grid.SetRow(_firstChildControl, 0);
|
Grid.SetRow(_firstChildControl, 0);
|
||||||
@@ -458,15 +247,6 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Устанавливает дочерние контролы для отображения.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="firstChild">Контрол для первого элемента.</param>
|
|
||||||
/// <param name="secondChild">Контрол для второго элемента.</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Метод назначает контролы для визуального представления дочерних элементов группы.
|
|
||||||
/// После установки контролов обновляет макет для корректного отображения.
|
|
||||||
/// </remarks>
|
|
||||||
public void SetChildren(IDockControl? firstChild, IDockControl? secondChild)
|
public void SetChildren(IDockControl? firstChild, IDockControl? secondChild)
|
||||||
{
|
{
|
||||||
if (_firstChildControl != null)
|
if (_firstChildControl != null)
|
||||||
@@ -478,44 +258,27 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
UpdateLayoutDefinitions();
|
UpdateLayoutDefinitions();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public object? PrepareDragData()
|
||||||
/// Обновляет внешний вид контрола в соответствии с текущим состоянием модели.
|
{
|
||||||
/// </summary>
|
return Model;
|
||||||
/// <remarks>
|
}
|
||||||
/// Вызывает перестройку макета сетки для синхронизации визуального представления
|
|
||||||
/// с текущими значениями свойств модели (ориентация, соотношение разделения).
|
public bool HandleDrop(object data, DockPosition position)
|
||||||
/// </remarks>
|
{
|
||||||
|
// TODO: Реализовать обработку сброса
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public void Refresh()
|
public void Refresh()
|
||||||
{
|
{
|
||||||
UpdateLayoutDefinitions();
|
UpdateLayoutDefinitions();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Применяет указанную тему к контролу.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="theme">Тема для применения.</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Обновляет стили и параметры отображения контрола в соответствии с заданной темой.
|
|
||||||
/// В текущей реализации метод является заглушкой и должен быть расширен для
|
|
||||||
/// поддержки динамического изменения тем оформления.
|
|
||||||
/// </remarks>
|
|
||||||
public void ApplyTheme(IDockTheme theme)
|
public void ApplyTheme(IDockTheme theme)
|
||||||
{
|
{
|
||||||
// Применение темы к контролу
|
// TODO: Реализовать применение темы
|
||||||
if (theme != null)
|
|
||||||
{
|
|
||||||
// TODO: Реализовать применение темы к стилям контрола
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывается при изменении состояния модели для обновления UI.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="propertyName">Имя изменившегося свойства модели.</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Перенаправляет вызов в обработчик изменений модели, обеспечивая уведомление
|
|
||||||
/// контрола о конкретных изменениях в связанной модели данных.
|
|
||||||
/// </remarks>
|
|
||||||
public void OnModelPropertyChanged(string propertyName)
|
public void OnModelPropertyChanged(string propertyName)
|
||||||
{
|
{
|
||||||
if (_model != null)
|
if (_model != null)
|
||||||
@@ -524,27 +287,11 @@ public sealed class LatticeDockGroup : Control, IDockGroupControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывает событие изменения свойства.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="propertyName">Имя изменившегося свойства.</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Используется для уведомления системы привязки данных об изменениях свойств
|
|
||||||
/// контрола. Если имя свойства не указано, автоматически определяется по имени
|
|
||||||
/// вызывающего члена.
|
|
||||||
/// </remarks>
|
|
||||||
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
{
|
{
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Освобождает ресурсы, используемые этим экземпляром контрола.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Выполняет отписку от событий модели, очистку ссылок и освобождение ресурсов.
|
|
||||||
/// После вызова этого метода контрол не должен использоваться.
|
|
||||||
/// </remarks>
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (!_disposed)
|
if (!_disposed)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
using Lattice.Core.Docking.Abstractions;
|
using Lattice.Core.Docking.Abstractions;
|
||||||
using Lattice.Core.Docking.Engine;
|
using Lattice.Core.Docking.Engine;
|
||||||
using Lattice.Core.Docking.Models;
|
using Lattice.Core.Docking.Models;
|
||||||
using Lattice.UI.Docking;
|
|
||||||
using Lattice.UI.Docking.Abstractions;
|
using Lattice.UI.Docking.Abstractions;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.UI.Xaml.Media;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
@@ -13,25 +13,6 @@ using System.Runtime.CompilerServices;
|
|||||||
|
|
||||||
namespace Lattice.UI;
|
namespace Lattice.UI;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Представляет главный контейнер док-системы для WinUI, который служит корневым элементом
|
|
||||||
/// пользовательского интерфейса для размещения всех компонентов системы докинга.
|
|
||||||
/// Этот контрол управляет всем макетом приложения, включая основное дерево компоновки,
|
|
||||||
/// плавающие окна и автоскрываемые панели.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// <see cref="LatticeDockHost"/> является центральным координатором UI-слоя док-системы,
|
|
||||||
/// интегрирующим функциональность менеджера макета, системы перетаскивания и контекстных меню.
|
|
||||||
/// Он обеспечивает согласованное отображение всех элементов и обрабатывает пользовательские
|
|
||||||
/// взаимодействия на верхнем уровне.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Контрол реализует интерфейс <see cref="IDockHost"/> и предоставляет полный набор методов
|
|
||||||
/// для управления структурой док-системы, включая создание/закрытие плавающих окон и
|
|
||||||
/// добавление/удаление автоскрываемых панелей.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
||||||
{
|
{
|
||||||
private readonly PropertyChangedEventHandler _modelPropertyChangedHandler;
|
private readonly PropertyChangedEventHandler _modelPropertyChangedHandler;
|
||||||
@@ -50,13 +31,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
private bool _showMenu = true;
|
private bool _showMenu = true;
|
||||||
private ContentControl? _rootContainer;
|
private ContentControl? _rootContainer;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="LatticeDockHost"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Конструктор устанавливает ключ стиля по умолчанию, инициализирует обработчик изменений модели
|
|
||||||
/// и подписывается на событие изменения контекста данных.
|
|
||||||
/// </remarks>
|
|
||||||
public LatticeDockHost()
|
public LatticeDockHost()
|
||||||
{
|
{
|
||||||
this.DefaultStyleKey = typeof(LatticeDockHost);
|
this.DefaultStyleKey = typeof(LatticeDockHost);
|
||||||
@@ -64,17 +38,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
this.DataContextChanged += OnDataContextChanged;
|
this.DataContextChanged += OnDataContextChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает модель данных, связанную с этим контролом.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Экземпляр, реализующий <see cref="IDockElement"/>, представляющий корневой элемент
|
|
||||||
/// дерева компоновки. Может быть null.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот элемент является корнем всего макета док-системы. При изменении этого свойства
|
|
||||||
/// происходит перестройка всего пользовательского интерфейса.
|
|
||||||
/// </remarks>
|
|
||||||
public IDockElement? Model
|
public IDockElement? Model
|
||||||
{
|
{
|
||||||
get => _model;
|
get => _model;
|
||||||
@@ -88,16 +51,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает менеджер макета, к которому принадлежит этот контрол.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="LayoutManager"/>, управляющий структурой док-системы.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Менеджер макета используется для выполнения операций с деревом компоновки
|
|
||||||
/// и координации изменений между различными элементами системы.
|
|
||||||
/// </remarks>
|
|
||||||
public LayoutManager? LayoutManager
|
public LayoutManager? LayoutManager
|
||||||
{
|
{
|
||||||
get => _layoutManager;
|
get => _layoutManager;
|
||||||
@@ -109,16 +62,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает контекстный менеджер для этого контрола.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Экземпляр <see cref="IDockContextManager"/>, управляющий контекстными меню и действиями.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Контекстный менеджер используется для отображения меню, связанных с этим элементом,
|
|
||||||
/// и выполнения команд, доступных в текущем контексте.
|
|
||||||
/// </remarks>
|
|
||||||
public IDockContextManager? ContextManager
|
public IDockContextManager? ContextManager
|
||||||
{
|
{
|
||||||
get => _contextManager;
|
get => _contextManager;
|
||||||
@@ -130,16 +73,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает признак того, что контрол выбран.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// true, если контрол выбран; в противном случае — false.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Выделение контрола обычно визуально выделяет его границы или фон,
|
|
||||||
/// чтобы указать пользователю на активный элемент.
|
|
||||||
/// </remarks>
|
|
||||||
public bool IsSelected
|
public bool IsSelected
|
||||||
{
|
{
|
||||||
get => _isSelected;
|
get => _isSelected;
|
||||||
@@ -151,15 +84,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает признак того, что контрол активен.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// true, если контрол активен; в противном случае — false.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Активный контрол обычно получает фокус ввода и может обрабатывать команды клавиатуры.
|
|
||||||
/// </remarks>
|
|
||||||
public bool IsActive
|
public bool IsActive
|
||||||
{
|
{
|
||||||
get => _isActive;
|
get => _isActive;
|
||||||
@@ -171,16 +95,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает признак того, что контрол можно перетаскивать.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// true, если контрол можно перетаскивать; в противном случае — false.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот флаг влияет на возможность инициирования операции перетаскивания
|
|
||||||
/// при взаимодействии пользователя с этим контролом.
|
|
||||||
/// </remarks>
|
|
||||||
public bool CanDrag
|
public bool CanDrag
|
||||||
{
|
{
|
||||||
get => _canDrag;
|
get => _canDrag;
|
||||||
@@ -192,16 +106,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает признак того, что контрол может принимать сброс.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// true, если контрол может принимать сброс; в противном случае — false.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Этот флаг влияет на возможность завершения операции перетаскивания
|
|
||||||
/// сбросом данных на этот контрол.
|
|
||||||
/// </remarks>
|
|
||||||
public bool CanDrop
|
public bool CanDrop
|
||||||
{
|
{
|
||||||
get => _canDrop;
|
get => _canDrop;
|
||||||
@@ -213,42 +117,9 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает коллекцию контролов плавающих окон, связанных с этим хостом.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Коллекция объектов, реализующих <see cref="IFloatingWindowControl"/>,
|
|
||||||
/// представляющих все активные плавающие окна в системе.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Коллекция является наблюдаемой (ObservableCollection), что позволяет автоматически
|
|
||||||
/// обновлять пользовательский интерфейс при добавлении или удалении окон.
|
|
||||||
/// </remarks>
|
|
||||||
public IEnumerable<IFloatingWindowControl> FloatingWindows => _floatingWindows;
|
public IEnumerable<IFloatingWindowControl> FloatingWindows => _floatingWindows;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает коллекцию контролов автоскрываемых панелей, прикрепленных к краям окна.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// Коллекция объектов, реализующих <see cref="IAutoHidePanelControl"/>,
|
|
||||||
/// представляющих автоскрываемые панели на разных сторонах окна.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Коллекция является наблюдаемой (ObservableCollection), что позволяет автоматически
|
|
||||||
/// обновлять пользовательский интерфейс при добавлении или удалении панелей.
|
|
||||||
/// </remarks>
|
|
||||||
public IEnumerable<IAutoHidePanelControl> AutoHidePanels => _autoHidePanels;
|
public IEnumerable<IAutoHidePanelControl> AutoHidePanels => _autoHidePanels;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает значение, указывающее, отображается ли панель инструментов (Toolbox).
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// true, если панель инструментов видима; в противном случае — false.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Панель инструментов обычно содержит элементы для быстрого доступа к командам
|
|
||||||
/// или создания новых компонентов в приложении.
|
|
||||||
/// </remarks>
|
|
||||||
public bool ShowToolbox
|
public bool ShowToolbox
|
||||||
{
|
{
|
||||||
get => _showToolbox;
|
get => _showToolbox;
|
||||||
@@ -260,16 +131,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает значение, указывающее, отображается ли строка состояния.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// true, если строка состояния видима; в противном случае — false.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Строка состояния обычно отображает текущий статус приложения,
|
|
||||||
/// информацию о выбранном элементе или прогресс выполнения операций.
|
|
||||||
/// </remarks>
|
|
||||||
public bool ShowStatusBar
|
public bool ShowStatusBar
|
||||||
{
|
{
|
||||||
get => _showStatusBar;
|
get => _showStatusBar;
|
||||||
@@ -281,15 +142,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает или задает значение, указывающее, отображается ли главное меню приложения.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// true, если главное меню видимо; в противном случае — false.
|
|
||||||
/// </value>
|
|
||||||
/// <remarks>
|
|
||||||
/// Главное меню содержит основные команды приложения, организованные в иерархическую структуру.
|
|
||||||
/// </remarks>
|
|
||||||
public bool ShowMenu
|
public bool ShowMenu
|
||||||
{
|
{
|
||||||
get => _showMenu;
|
get => _showMenu;
|
||||||
@@ -301,41 +153,43 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Событие, возникающее при изменении структуры макета док-системы.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Может вызываться при добавлении/удалении элементов, изменении размеров,
|
|
||||||
/// создании/закрытии плавающих окон и других операциях, влияющих на компоновку.
|
|
||||||
/// </remarks>
|
|
||||||
public event EventHandler? LayoutChanged;
|
public event EventHandler? LayoutChanged;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Событие, возникающее при создании нового плавающего окна.
|
|
||||||
/// </summary>
|
|
||||||
public event EventHandler<FloatingWindowCreatedEventArgs>? FloatingWindowCreated;
|
public event EventHandler<FloatingWindowCreatedEventArgs>? FloatingWindowCreated;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Событие, возникающее при закрытии плавающего окна.
|
|
||||||
/// </summary>
|
|
||||||
public event EventHandler<FloatingWindowClosedEventArgs>? FloatingWindowClosed;
|
public event EventHandler<FloatingWindowClosedEventArgs>? FloatingWindowClosed;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Событие, возникающее при изменении значения свойства.
|
|
||||||
/// </summary>
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Вызывается при применении шаблона контрола.
|
public FrameworkElement? DragDropElement => this;
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
/// <inheritdoc/>
|
||||||
/// Метод получает ссылки на именованные части шаблона и обновляет отображение
|
public void SetupDragDropHandlers()
|
||||||
/// корневого содержимого в соответствии с текущим состоянием модели.
|
{
|
||||||
/// </remarks>
|
this.AllowDrop = true;
|
||||||
|
this.CanDrag = true;
|
||||||
|
|
||||||
|
// Настройка обработчиков для хоста
|
||||||
|
this.Drop += OnHostDrop;
|
||||||
|
this.DragOver += OnHostDragOver;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void StartDrag() { /* Реализация */ }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
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()
|
protected override void OnApplyTemplate()
|
||||||
{
|
{
|
||||||
base.OnApplyTemplate();
|
base.OnApplyTemplate();
|
||||||
|
|
||||||
_rootContainer = GetTemplateChild("PART_RootContainer") as ContentControl;
|
_rootContainer = GetTemplateChild("PART_RootContainer") as ContentControl;
|
||||||
UpdateRootContent();
|
UpdateRootContent();
|
||||||
}
|
}
|
||||||
@@ -349,11 +203,8 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
{
|
{
|
||||||
if (_model != null && _layoutManager != null)
|
if (_model != null && _layoutManager != null)
|
||||||
{
|
{
|
||||||
// Подписываемся на события менеджера макета
|
|
||||||
_layoutManager.LayoutUpdated += OnLayoutUpdated;
|
_layoutManager.LayoutUpdated += OnLayoutUpdated;
|
||||||
_layoutManager.AutoHidePanelsChanged += OnAutoHidePanelsChanged;
|
_layoutManager.AutoHidePanelsChanged += OnAutoHidePanelsChanged;
|
||||||
|
|
||||||
// Устанавливаем DataContext
|
|
||||||
this.DataContext = _model;
|
this.DataContext = _model;
|
||||||
UpdateRootContent();
|
UpdateRootContent();
|
||||||
}
|
}
|
||||||
@@ -363,17 +214,14 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
{
|
{
|
||||||
if (_model != null && _layoutManager != null)
|
if (_model != null && _layoutManager != null)
|
||||||
{
|
{
|
||||||
// Отписываемся от событий
|
|
||||||
_layoutManager.LayoutUpdated -= OnLayoutUpdated;
|
_layoutManager.LayoutUpdated -= OnLayoutUpdated;
|
||||||
_layoutManager.AutoHidePanelsChanged -= OnAutoHidePanelsChanged;
|
_layoutManager.AutoHidePanelsChanged -= OnAutoHidePanelsChanged;
|
||||||
|
|
||||||
this.DataContext = null;
|
this.DataContext = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
// Обработка изменений модели
|
|
||||||
OnPropertyChanged(e.PropertyName);
|
OnPropertyChanged(e.PropertyName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,7 +233,6 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
|
|
||||||
private void OnAutoHidePanelsChanged(object? sender, EventArgs e)
|
private void OnAutoHidePanelsChanged(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
// Обновление автоскрываемых панелей
|
|
||||||
OnPropertyChanged(nameof(AutoHidePanels));
|
OnPropertyChanged(nameof(AutoHidePanels));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,8 +240,7 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
{
|
{
|
||||||
if (_rootContainer != null && _model != null && _layoutManager != null)
|
if (_rootContainer != null && _model != null && _layoutManager != null)
|
||||||
{
|
{
|
||||||
// Создаем дерево контролов через фабрику
|
var factory = Lattice.UI.Docking.LatticeUIFramework.ControlFactory;
|
||||||
var factory = LatticeUIFramework.ControlFactory;
|
|
||||||
if (factory != null)
|
if (factory != null)
|
||||||
{
|
{
|
||||||
var control = factory.CreateControlForElement(_model);
|
var control = factory.CreateControlForElement(_model);
|
||||||
@@ -403,157 +249,44 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает новое плавающее окно для размещения указанного элемента док-системы.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="element">
|
|
||||||
/// Элемент док-системы (группа или лист), который будет размещен в плавающем окне.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="title">Заголовок создаваемого окна.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// Экземпляр <see cref="IFloatingWindowControl"/>, представляющий созданное плавающее окно.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="element"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <exception cref="NotImplementedException">
|
|
||||||
/// Выбрасывается, так как метод еще не реализован.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Созданное окно может быть перемещено пользователем в любое место экрана,
|
|
||||||
/// изменено в размерах и обычно содержит стандартные элементы управления окном
|
|
||||||
/// (заголовок, кнопки закрытия/сворачивания).
|
|
||||||
/// </remarks>
|
|
||||||
public IFloatingWindowControl CreateFloatingWindow(IDockElement element, string title)
|
public IFloatingWindowControl CreateFloatingWindow(IDockElement element, string title)
|
||||||
{
|
{
|
||||||
if (element == null) throw new ArgumentNullException(nameof(element));
|
throw new NotImplementedException("Floating windows not implemented yet");
|
||||||
|
|
||||||
// TODO: Реализовать создание плавающего окна через фабрику
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Закрывает указанное плавающее окно и возвращает его содержимое в основной макет.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="window">
|
|
||||||
/// Плавающее окно, которое необходимо закрыть.
|
|
||||||
/// </param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="window"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// При закрытии плавающего окна его содержимое обычно возвращается в то место
|
|
||||||
/// в основном макете, откуда оно было извлечено, или в ближайшую допустимую позицию.
|
|
||||||
/// </remarks>
|
|
||||||
public void CloseFloatingWindow(IFloatingWindowControl window)
|
public void CloseFloatingWindow(IFloatingWindowControl window)
|
||||||
{
|
{
|
||||||
if (window == null) throw new ArgumentNullException(nameof(window));
|
|
||||||
|
|
||||||
if (_floatingWindows.Remove(window))
|
if (_floatingWindows.Remove(window))
|
||||||
{
|
{
|
||||||
FloatingWindowClosed?.Invoke(this, new FloatingWindowClosedEventArgs(window));
|
FloatingWindowClosed?.Invoke(this, new FloatingWindowClosedEventArgs(window));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Добавляет автоскрываемую панель с указанным содержимым к заданной стороне окна.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="content">
|
|
||||||
/// Контент, который будет отображаться в автоскрываемой панели.
|
|
||||||
/// </param>
|
|
||||||
/// <param name="side">
|
|
||||||
/// Сторона окна, к которой будет прикреплена панель.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Экземпляр <see cref="IAutoHidePanelControl"/>, представляющий созданную панель.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="content"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <exception cref="InvalidOperationException">
|
|
||||||
/// Выбрасывается, если свойство <see cref="LayoutManager"/> не установлено.
|
|
||||||
/// </exception>
|
|
||||||
/// <exception cref="NotImplementedException">
|
|
||||||
/// Выбрасывается, так как метод еще не реализован.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Автоскрываемые панели полезны для инструментов, к которым нужен частый,
|
|
||||||
/// но не постоянный доступ, так как они экономят пространство экрана.
|
|
||||||
/// </remarks>
|
|
||||||
public IAutoHidePanelControl AddAutoHidePanel(Core.Docking.Abstractions.IDockContent content, DockSide side)
|
public IAutoHidePanelControl AddAutoHidePanel(Core.Docking.Abstractions.IDockContent content, DockSide side)
|
||||||
{
|
{
|
||||||
if (content == null) throw new ArgumentNullException(nameof(content));
|
|
||||||
|
|
||||||
if (_layoutManager != null)
|
if (_layoutManager != null)
|
||||||
{
|
{
|
||||||
var panel = _layoutManager.AddAutoHidePanel(content, side);
|
var panel = _layoutManager.AddAutoHidePanel(content, side);
|
||||||
|
throw new NotImplementedException("Auto-hide panels not implemented yet");
|
||||||
// TODO: Создать UI-контрол для автоскрываемой панели через фабрику
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new InvalidOperationException("LayoutManager is not set");
|
throw new InvalidOperationException("LayoutManager is not set");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Удаляет автоскрываемую панель из интерфейса.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="panel">
|
|
||||||
/// Автоскрываемая панель, которую необходимо удалить.
|
|
||||||
/// </param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="panel"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <exception cref="NotImplementedException">
|
|
||||||
/// Выбрасывается, так как метод еще не реализован.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// После удаления панели её содержимое обычно либо закрывается полностью,
|
|
||||||
/// либо преобразуется в обычную закрепленную панель, в зависимости от настроек.
|
|
||||||
/// </remarks>
|
|
||||||
public void RemoveAutoHidePanel(IAutoHidePanelControl panel)
|
public void RemoveAutoHidePanel(IAutoHidePanelControl panel)
|
||||||
{
|
{
|
||||||
if (panel == null) throw new ArgumentNullException(nameof(panel));
|
throw new NotImplementedException("Auto-hide panels not implemented yet");
|
||||||
|
|
||||||
// TODO: Реализовать удаление автоскрываемой панели
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public object? PrepareDragData() => Model;
|
||||||
/// Обновляет внешний вид контрола в соответствии с текущим состоянием модели.
|
public bool HandleDrop(object data, DockPosition position) => false;
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
public void Refresh() => UpdateRootContent();
|
||||||
/// Вызывает обновление корневого содержимого и всех дочерних элементов.
|
|
||||||
/// </remarks>
|
|
||||||
public void Refresh()
|
|
||||||
{
|
|
||||||
UpdateRootContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Применяет указанную тему к контролу.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="theme">Тема для применения.</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// В текущей реализации метод является заглушкой и должен быть расширен
|
|
||||||
/// для поддержки динамического изменения тем.
|
|
||||||
/// </remarks>
|
|
||||||
public void ApplyTheme(IDockTheme theme)
|
public void ApplyTheme(IDockTheme theme)
|
||||||
{
|
{
|
||||||
// Применение темы к контролу
|
// TODO: Реализовать применение темы
|
||||||
if (theme != null)
|
|
||||||
{
|
|
||||||
// TODO: Реализовать применение темы к стилям контрола
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывается при изменении состояния модели для обновления UI.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="propertyName">Имя изменившегося свойства модели.</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Перенаправляет вызов в обработчик изменений модели.
|
|
||||||
/// </remarks>
|
|
||||||
public void OnModelPropertyChanged(string propertyName)
|
public void OnModelPropertyChanged(string propertyName)
|
||||||
{
|
{
|
||||||
if (_model != null)
|
if (_model != null)
|
||||||
@@ -567,24 +300,84 @@ public sealed class LatticeDockHost : Control, IDockHost, IDisposable
|
|||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Освобождает ресурсы, используемые этим экземпляром контрола.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Выполняет отписку от событий модели, очистку коллекций и освобождение ресурсов.
|
|
||||||
/// </remarks>
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (!_disposed)
|
if (!_disposed)
|
||||||
{
|
{
|
||||||
DetachModel();
|
DetachModel();
|
||||||
|
|
||||||
// Очищаем коллекции
|
|
||||||
_floatingWindows.Clear();
|
_floatingWindows.Clear();
|
||||||
_autoHidePanels.Clear();
|
_autoHidePanels.Clear();
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
GC.SuppressFinalize(this);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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.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;
|
namespace Lattice.UI;
|
||||||
|
|
||||||
/// <summary>
|
public sealed class LatticeDockLeaf : Control, IDockLeafControl, IDisposable
|
||||||
/// Визуальное представление контейнера вкладок с поддержкой нижнего расположения.
|
|
||||||
/// </summary>
|
|
||||||
public class LatticeDockLeaf : Control
|
|
||||||
{
|
{
|
||||||
|
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()
|
public LatticeDockLeaf()
|
||||||
{
|
{
|
||||||
this.DefaultStyleKey = typeof(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<ActiveContentChangedEventArgs>? ActiveContentChanged;
|
||||||
|
public event EventHandler<ContentClosingEventArgs>? ContentClosing;
|
||||||
|
public event EventHandler<TabsReorderedEventArgs>? TabsReordered;
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
protected override void OnApplyTemplate()
|
protected override void OnApplyTemplate()
|
||||||
{
|
{
|
||||||
base.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();
|
UpdateTabPlacement();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public void ApplyTheme(IDockTheme theme)
|
||||||
/// Настраивает внутреннюю структуру TabView для отображения вкладок снизу.
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateTabPlacement()
|
|
||||||
{
|
{
|
||||||
var tabView = GetTemplateChild("PART_TabView") as TabView;
|
// TODO: Реализовать применение темы
|
||||||
if (tabView == null || DataContext is not DockLeaf leaf) return;
|
}
|
||||||
|
|
||||||
// Вместо сложной манипуляции с визуальным деревом, используем встроенные свойства TabView
|
public void OnModelPropertyChanged(string propertyName)
|
||||||
if (leaf.TabPlacement == TabPlacement.Bottom)
|
{
|
||||||
|
if (_model != null)
|
||||||
{
|
{
|
||||||
// К сожалению, TabView в WinUI не поддерживает TabStripPlacement
|
OnModelPropertyChanged(_model, new PropertyChangedEventArgs(propertyName));
|
||||||
// Это ограничение платформы, нужно либо использовать другой контрол,
|
}
|
||||||
// либо реализовать кастомный TabControl с поддержкой нижнего расположения
|
}
|
||||||
// Временно оставляем как есть с заглушкой
|
|
||||||
System.Diagnostics.Debug.WriteLine("TabPlacement.Bottom is not fully supported in WinUI TabView");
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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.Input;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Microsoft.UI.Xaml.Input;
|
using Microsoft.UI.Xaml.Input;
|
||||||
using Microsoft.UI.Xaml.Media;
|
using Microsoft.UI.Xaml.Media;
|
||||||
using System;
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Lattice.UI;
|
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()
|
public LatticeSplitter()
|
||||||
{
|
{
|
||||||
this.DefaultStyleKey = typeof(LatticeSplitter);
|
this.DefaultStyleKey = typeof(LatticeSplitter);
|
||||||
@@ -17,17 +30,116 @@ public class LatticeSplitter : Control
|
|||||||
this.PointerEntered += (s, e) =>
|
this.PointerEntered += (s, e) =>
|
||||||
this.ProtectedCursor = InputSystemCursor.Create(InputSystemCursorShape.SizeWestEast);
|
this.ProtectedCursor = InputSystemCursor.Create(InputSystemCursorShape.SizeWestEast);
|
||||||
this.PointerExited += (s, e) =>
|
this.PointerExited += (s, e) =>
|
||||||
this.ProtectedCursor = null;
|
this.ProtectedCursor = InputSystemCursor.Create(InputSystemCursorShape.Arrow);
|
||||||
|
|
||||||
this.ManipulationDelta += OnManipulationDelta;
|
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<SplitterDraggedEventArgs>? DragDelta;
|
||||||
|
public event EventHandler? DragCompleted;
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
private void OnManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
|
private void OnManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
|
||||||
{
|
{
|
||||||
// 1. Находим модель DockGroup через DataContext
|
|
||||||
if (this.DataContext is not DockGroup group) return;
|
if (this.DataContext is not DockGroup group) return;
|
||||||
|
|
||||||
// 2. Находим родительский Grid, чтобы знать общие размеры
|
|
||||||
if (VisualTreeHelper.GetParent(this) is not Grid parentGrid ||
|
if (VisualTreeHelper.GetParent(this) is not Grid parentGrid ||
|
||||||
parentGrid.ActualWidth <= 0 || parentGrid.ActualHeight <= 0)
|
parentGrid.ActualWidth <= 0 || parentGrid.ActualHeight <= 0)
|
||||||
return;
|
return;
|
||||||
@@ -38,14 +150,35 @@ public class LatticeSplitter : Control
|
|||||||
|
|
||||||
if (totalSize <= 0) return;
|
if (totalSize <= 0) return;
|
||||||
|
|
||||||
// 3. Вычисляем изменение Ratio (от -1.0 до 1.0)
|
|
||||||
double delta = group.Orientation == SplitDirection.Horizontal
|
double delta = group.Orientation == SplitDirection.Horizontal
|
||||||
? e.Delta.Translation.X
|
? e.Delta.Translation.X
|
||||||
: e.Delta.Translation.Y;
|
: e.Delta.Translation.Y;
|
||||||
|
|
||||||
double ratioChange = delta / totalSize;
|
double ratioChange = delta / totalSize;
|
||||||
|
|
||||||
// 4. Обновляем модель (с ограничением от 0.05 до 0.95)
|
|
||||||
group.SplitRatio = Math.Clamp(group.SplitRatio + ratioChange, 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,21 +13,6 @@ using System.Runtime.CompilerServices;
|
|||||||
|
|
||||||
namespace Lattice.UI;
|
namespace Lattice.UI;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Представляет кастомный контрол вкладок с поддержкой всех позиций размещения панели вкладок.
|
|
||||||
/// Реализует интерфейс <see cref="IDockLeafControl"/> для интеграции с системой докинга.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Контрол обеспечивает отображение коллекции вкладок с возможностью навигации между ними,
|
|
||||||
/// закрытия вкладок и изменения порядка. Поддерживает все четыре позиции размещения панели
|
|
||||||
/// вкладок: сверху, снизу, слева и справа.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Контрол автоматически синхронизирует свое состояние с моделью данных <see cref="DockLeaf"/>
|
|
||||||
/// и обеспечивает двустороннюю привязку данных через механизм INotifyPropertyChanged.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
||||||
{
|
{
|
||||||
private readonly PropertyChangedEventHandler _modelPropertyChangedHandler;
|
private readonly PropertyChangedEventHandler _modelPropertyChangedHandler;
|
||||||
@@ -44,9 +29,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
private bool _showCloseButtons = true;
|
private bool _showCloseButtons = true;
|
||||||
private bool _canReorderTabs = true;
|
private bool _canReorderTabs = true;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="LatticeTabControl"/>.
|
|
||||||
/// </summary>
|
|
||||||
public LatticeTabControl()
|
public LatticeTabControl()
|
||||||
{
|
{
|
||||||
this.DefaultStyleKey = typeof(LatticeTabControl);
|
this.DefaultStyleKey = typeof(LatticeTabControl);
|
||||||
@@ -54,7 +36,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
this.DataContextChanged += OnDataContextChanged;
|
this.DataContextChanged += OnDataContextChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public IDockElement? Model
|
public IDockElement? Model
|
||||||
{
|
{
|
||||||
get => _model;
|
get => _model;
|
||||||
@@ -68,7 +49,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public LayoutManager? LayoutManager
|
public LayoutManager? LayoutManager
|
||||||
{
|
{
|
||||||
get => _layoutManager;
|
get => _layoutManager;
|
||||||
@@ -80,7 +60,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public IDockContextManager? ContextManager
|
public IDockContextManager? ContextManager
|
||||||
{
|
{
|
||||||
get => _contextManager;
|
get => _contextManager;
|
||||||
@@ -92,7 +71,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public bool IsSelected
|
public bool IsSelected
|
||||||
{
|
{
|
||||||
get => _isSelected;
|
get => _isSelected;
|
||||||
@@ -104,7 +82,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public bool IsActive
|
public bool IsActive
|
||||||
{
|
{
|
||||||
get => _isActive;
|
get => _isActive;
|
||||||
@@ -116,7 +93,9 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
public bool CanDrag => true;
|
||||||
|
public bool CanDrop => true;
|
||||||
|
|
||||||
public TabPlacement TabPlacement
|
public TabPlacement TabPlacement
|
||||||
{
|
{
|
||||||
get => _tabPlacement;
|
get => _tabPlacement;
|
||||||
@@ -131,7 +110,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public bool ShowCloseButtons
|
public bool ShowCloseButtons
|
||||||
{
|
{
|
||||||
get => _showCloseButtons;
|
get => _showCloseButtons;
|
||||||
@@ -146,7 +124,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public bool CanReorderTabs
|
public bool CanReorderTabs
|
||||||
{
|
{
|
||||||
get => _canReorderTabs;
|
get => _canReorderTabs;
|
||||||
@@ -160,7 +137,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public IDockContent? ActiveContent
|
public IDockContent? ActiveContent
|
||||||
{
|
{
|
||||||
get => _model?.ActiveContent;
|
get => _model?.ActiveContent;
|
||||||
@@ -173,19 +149,14 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
public object? PrepareDragData() => Model;
|
||||||
|
public bool HandleDrop(object data, DockPosition position) => false;
|
||||||
|
|
||||||
public event EventHandler<ActiveContentChangedEventArgs>? ActiveContentChanged;
|
public event EventHandler<ActiveContentChangedEventArgs>? ActiveContentChanged;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event EventHandler<ContentClosingEventArgs>? ContentClosing;
|
public event EventHandler<ContentClosingEventArgs>? ContentClosing;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event EventHandler<TabsReorderedEventArgs>? TabsReordered;
|
public event EventHandler<TabsReorderedEventArgs>? TabsReordered;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
protected override void OnApplyTemplate()
|
protected override void OnApplyTemplate()
|
||||||
{
|
{
|
||||||
base.OnApplyTemplate();
|
base.OnApplyTemplate();
|
||||||
@@ -203,17 +174,11 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
UpdateTabHeaders();
|
UpdateTabHeaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Обрабатывает изменение контекста данных контрола.
|
|
||||||
/// </summary>
|
|
||||||
private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
|
private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
|
||||||
{
|
{
|
||||||
Model = args.NewValue as DockLeaf;
|
Model = args.NewValue as DockLeaf;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Присоединяет модель данных к контролу.
|
|
||||||
/// </summary>
|
|
||||||
private void AttachModel()
|
private void AttachModel()
|
||||||
{
|
{
|
||||||
if (_model != null)
|
if (_model != null)
|
||||||
@@ -232,9 +197,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Отсоединяет модель данных от контрола.
|
|
||||||
/// </summary>
|
|
||||||
private void DetachModel()
|
private void DetachModel()
|
||||||
{
|
{
|
||||||
if (_model != null)
|
if (_model != null)
|
||||||
@@ -250,9 +212,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Обрабатывает изменения свойств модели данных.
|
|
||||||
/// </summary>
|
|
||||||
private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
private void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
switch (e.PropertyName)
|
switch (e.PropertyName)
|
||||||
@@ -274,17 +233,11 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Обрабатывает изменения коллекции вкладок.
|
|
||||||
/// </summary>
|
|
||||||
private void OnChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
private void OnChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
UpdateTabHeaders();
|
UpdateTabHeaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Обновляет положение панели вкладок.
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateTabPlacement()
|
private void UpdateTabPlacement()
|
||||||
{
|
{
|
||||||
if (_rootGrid == null || _model == null) return;
|
if (_rootGrid == null || _model == null) return;
|
||||||
@@ -322,10 +275,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
UpdateElementPositions();
|
UpdateElementPositions();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Обновляет ориентацию списка заголовков вкладок.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="orientation">Новая ориентация списка.</param>
|
|
||||||
private void UpdateHeaderListOrientation(Orientation orientation)
|
private void UpdateHeaderListOrientation(Orientation orientation)
|
||||||
{
|
{
|
||||||
if (_tabHeaderList?.ItemsPanelRoot is StackPanel stackPanel)
|
if (_tabHeaderList?.ItemsPanelRoot is StackPanel stackPanel)
|
||||||
@@ -334,9 +283,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Обновляет позиции элементов в сетке.
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateElementPositions()
|
private void UpdateElementPositions()
|
||||||
{
|
{
|
||||||
if (_rootGrid == null || _tabHeaderList == null || _contentControl == null) return;
|
if (_rootGrid == null || _tabHeaderList == null || _contentControl == null) return;
|
||||||
@@ -373,9 +319,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Обновляет заголовки вкладок.
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateTabHeaders()
|
private void UpdateTabHeaders()
|
||||||
{
|
{
|
||||||
if (_tabHeaderList == null || _model == null) return;
|
if (_tabHeaderList == null || _model == null) return;
|
||||||
@@ -391,11 +334,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
UpdateSelectedTab();
|
UpdateSelectedTab();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает элемент заголовка вкладки.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="content">Содержимое вкладки.</param>
|
|
||||||
/// <returns>Созданный элемент заголовка.</returns>
|
|
||||||
private ListBoxItem CreateTabHeaderItem(IDockContent content)
|
private ListBoxItem CreateTabHeaderItem(IDockContent content)
|
||||||
{
|
{
|
||||||
var item = new ListBoxItem
|
var item = new ListBoxItem
|
||||||
@@ -411,18 +349,12 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
if (e.GetCurrentPoint(item).Properties.IsLeftButtonPressed)
|
if (e.GetCurrentPoint(item).Properties.IsLeftButtonPressed)
|
||||||
{
|
{
|
||||||
ActiveContent = content;
|
ActiveContent = content;
|
||||||
e.Handled = true;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает содержимое заголовка вкладки.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="content">Содержимое вкладки.</param>
|
|
||||||
/// <returns>Созданное содержимое заголовка.</returns>
|
|
||||||
private UIElement CreateTabHeaderContent(IDockContent content)
|
private UIElement CreateTabHeaderContent(IDockContent content)
|
||||||
{
|
{
|
||||||
var grid = new Grid();
|
var grid = new Grid();
|
||||||
@@ -455,7 +387,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
closeButton.Click += (sender, e) =>
|
closeButton.Click += (sender, e) =>
|
||||||
{
|
{
|
||||||
CloseContent(content);
|
CloseContent(content);
|
||||||
e.Handled = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Grid.SetColumn(closeButton, 1);
|
Grid.SetColumn(closeButton, 1);
|
||||||
@@ -465,9 +396,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
return grid;
|
return grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Обновляет выбранную вкладку.
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateSelectedTab()
|
private void UpdateSelectedTab()
|
||||||
{
|
{
|
||||||
if (_tabHeaderList == null || _model == null) return;
|
if (_tabHeaderList == null || _model == null) return;
|
||||||
@@ -486,9 +414,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Обрабатывает изменение выбора вкладки.
|
|
||||||
/// </summary>
|
|
||||||
private void OnTabSelectionChanged(object sender, SelectionChangedEventArgs e)
|
private void OnTabSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_tabHeaderList?.SelectedItem is ListBoxItem selectedItem &&
|
if (_tabHeaderList?.SelectedItem is ListBoxItem selectedItem &&
|
||||||
@@ -505,7 +430,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void AddContent(IDockContent content)
|
public void AddContent(IDockContent content)
|
||||||
{
|
{
|
||||||
if (_model != null && !_model.Children.Contains(content))
|
if (_model != null && !_model.Children.Contains(content))
|
||||||
@@ -515,7 +439,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void RemoveContent(IDockContent content)
|
public void RemoveContent(IDockContent content)
|
||||||
{
|
{
|
||||||
if (_model != null && _model.Children.Contains(content))
|
if (_model != null && _model.Children.Contains(content))
|
||||||
@@ -525,7 +448,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public bool CloseContent(IDockContent content)
|
public bool CloseContent(IDockContent content)
|
||||||
{
|
{
|
||||||
var args = new ContentClosingEventArgs(content);
|
var args = new ContentClosingEventArgs(content);
|
||||||
@@ -540,7 +462,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void CloseAllExcept(IDockContent exceptContent)
|
public void CloseAllExcept(IDockContent exceptContent)
|
||||||
{
|
{
|
||||||
if (_model == null) return;
|
if (_model == null) return;
|
||||||
@@ -555,7 +476,6 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void CloseAll()
|
public void CloseAll()
|
||||||
{
|
{
|
||||||
if (_model == null) return;
|
if (_model == null) return;
|
||||||
@@ -567,23 +487,17 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void Refresh()
|
public void Refresh()
|
||||||
{
|
{
|
||||||
UpdateTabHeaders();
|
UpdateTabHeaders();
|
||||||
UpdateTabPlacement();
|
UpdateTabPlacement();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void ApplyTheme(IDockTheme theme)
|
public void ApplyTheme(IDockTheme theme)
|
||||||
{
|
{
|
||||||
if (theme != null)
|
// TODO: Реализовать применение темы
|
||||||
{
|
|
||||||
// TODO: Реализовать применение темы к стилям контрола
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void OnModelPropertyChanged(string propertyName)
|
public void OnModelPropertyChanged(string propertyName)
|
||||||
{
|
{
|
||||||
if (_model != null)
|
if (_model != null)
|
||||||
@@ -592,15 +506,11 @@ public sealed class LatticeTabControl : Control, IDockLeafControl, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывает событие изменения свойства.
|
|
||||||
/// </summary>
|
|
||||||
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
{
|
{
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (!_disposed)
|
if (!_disposed)
|
||||||
|
|||||||
189
Lattice.UI.Docking.WinUI/DockBuilder.cs
Normal file
189
Lattice.UI.Docking.WinUI/DockBuilder.cs
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Предоставляет упрощенный статический API для инициализации и конфигурации системы докинга Lattice.
|
||||||
|
/// </summary>
|
||||||
|
public static class LatticeDock
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Создает новый строитель конфигурации системы докинга для WinUI.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Экземпляр <see cref="DockBuilder"/> для настройки системы.</returns>
|
||||||
|
public static DockBuilder CreateWinUIBuilder()
|
||||||
|
{
|
||||||
|
return new DockBuilder()
|
||||||
|
.WithWinUIFactory()
|
||||||
|
.WithWinUIContextManager()
|
||||||
|
.WithWinUIService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настраивает строитель для использования фабрики WinUI.
|
||||||
|
/// </summary>
|
||||||
|
public static DockBuilder WithWinUIFactory(this DockBuilder builder)
|
||||||
|
{
|
||||||
|
return builder.WithControlFactory(new WinUIDockControlFactory());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настраивает строитель для использования контекстного менеджера WinUI.
|
||||||
|
/// </summary>
|
||||||
|
public static DockBuilder WithWinUIContextManager(this DockBuilder builder)
|
||||||
|
{
|
||||||
|
return builder.WithContextManager(new WinUIDockContextManager());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настраивает строитель для использования UI-сервиса WinUI.
|
||||||
|
/// </summary>
|
||||||
|
public static DockBuilder WithWinUIService(this DockBuilder builder)
|
||||||
|
{
|
||||||
|
return builder.WithUIService(new WinUIDockUIService());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Предоставляет fluent-интерфейс для конфигурации системы докинга Lattice.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DockBuilder
|
||||||
|
{
|
||||||
|
private readonly LayoutManager _layoutManager;
|
||||||
|
private readonly ContentRegistry _contentRegistry;
|
||||||
|
private IDockControlFactory? _factory;
|
||||||
|
private IDockContextManager? _contextManager;
|
||||||
|
private IDockUIService? _uiService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Инициализирует новый экземпляр класса <see cref="DockBuilder"/>.
|
||||||
|
/// Создает менеджер макета и реестр контента по умолчанию.
|
||||||
|
/// </summary>
|
||||||
|
public DockBuilder()
|
||||||
|
{
|
||||||
|
_layoutManager = new LayoutManager();
|
||||||
|
_contentRegistry = new ContentRegistry();
|
||||||
|
_layoutManager.ContentRegistry = _contentRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Регистрирует фабрику контролов для создания UI-элементов.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="factory">Фабрика контролов.</param>
|
||||||
|
/// <returns>Текущий экземпляр <see cref="DockBuilder"/> для цепочки вызовов.</returns>
|
||||||
|
public DockBuilder WithControlFactory(IDockControlFactory factory)
|
||||||
|
{
|
||||||
|
_factory = factory;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Регистрирует менеджер контекстных меню.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="contextManager">Менеджер контекстных меню.</param>
|
||||||
|
/// <returns>Текущий экземпляр <see cref="DockBuilder"/> для цепочки вызовов.</returns>
|
||||||
|
public DockBuilder WithContextManager(IDockContextManager contextManager)
|
||||||
|
{
|
||||||
|
_contextManager = contextManager;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Регистрирует UI-сервис для выполнения платформенно-зависимых операций.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uiService">UI-сервис.</param>
|
||||||
|
/// <returns>Текущий экземпляр <see cref="DockBuilder"/> для цепочки вызовов.</returns>
|
||||||
|
public DockBuilder WithUIService(IDockUIService uiService)
|
||||||
|
{
|
||||||
|
_uiService = uiService;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Регистрирует тип контента в реестре.
|
||||||
|
/// </summary>
|
||||||
|
public DockBuilder RegisterContentType<T>(string contentTypeId, Func<T> factory, ContentMetadata? metadata = null)
|
||||||
|
where T : Core.Docking.Abstractions.IDockContent
|
||||||
|
{
|
||||||
|
_contentRegistry.Register(contentTypeId, factory, metadata);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Завершает конфигурацию системы докинга и возвращает настроенный экземпляр <see cref="IDockSystem"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Настроенная система докинга.</returns>
|
||||||
|
public IDockSystem Build()
|
||||||
|
{
|
||||||
|
// Настраиваем связи между компонентами
|
||||||
|
if (_factory is DockControlFactoryBase factoryBase && _contextManager != null)
|
||||||
|
{
|
||||||
|
factoryBase.ContextManager = _contextManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DockSystem(_layoutManager, _contentRegistry, _factory, _contextManager, _uiService);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Представляет настроенную систему докинга с доступом ко всем основным компонентам.
|
||||||
|
/// </summary>
|
||||||
|
public interface IDockSystem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получает менеджер макета.
|
||||||
|
/// </summary>
|
||||||
|
LayoutManager LayoutManager { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает реестр контента.
|
||||||
|
/// </summary>
|
||||||
|
ContentRegistry ContentRegistry { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает фабрику контролов.
|
||||||
|
/// </summary>
|
||||||
|
IDockControlFactory? ControlFactory { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает менеджер контекстных меню.
|
||||||
|
/// </summary>
|
||||||
|
IDockContextManager? ContextManager { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает UI-сервис.
|
||||||
|
/// </summary>
|
||||||
|
IDockUIService? UIService { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Реализация интерфейса <see cref="IDockSystem"/>.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,188 +4,78 @@ using Lattice.UI.Docking.Abstractions;
|
|||||||
using Lattice.UI.Docking.Factories;
|
using Lattice.UI.Docking.Factories;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Lattice.UI.Docking.WinUI.Factories;
|
namespace Lattice.UI.Docking.WinUI.Factories;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Фабрика контролов для платформы WinUI.
|
|
||||||
/// Создает UI-элементы для отображения компонентов системы докинга.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Фабрика реализует паттерн "Абстрактная фабрика", предоставляя единый интерфейс
|
|
||||||
/// для создания всех типов контролов док-системы. Это позволяет абстрагировать
|
|
||||||
/// конкретную UI-платформу (WinUI) от бизнес-логики системы.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Все создаваемые контролы автоматически настраиваются: устанавливаются связи
|
|
||||||
/// с менеджером макета, контекстным менеджером и применяется текущая тема оформления.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public sealed class WinUIDockControlFactory : DockControlFactoryBase, IDockControlFactory
|
public sealed class WinUIDockControlFactory : DockControlFactoryBase, IDockControlFactory
|
||||||
{
|
{
|
||||||
private readonly IDockTheme _theme;
|
private readonly Dictionary<Type, Func<object, IDockControl>> _creators;
|
||||||
|
|
||||||
/// <summary>
|
public WinUIDockControlFactory()
|
||||||
/// Инициализирует новый экземпляр фабрики WinUI.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="theme">Тема оформления для применения к создаваемым контролам.</param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="theme"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Конструктор создает фабрику с заданной темой оформления. Все контролы,
|
|
||||||
/// созданные этой фабрикой, будут автоматически применять указанную тему.
|
|
||||||
/// </remarks>
|
|
||||||
public WinUIDockControlFactory(IDockTheme theme)
|
|
||||||
{
|
{
|
||||||
_theme = theme ?? throw new ArgumentNullException(nameof(theme));
|
_creators = new Dictionary<Type, Func<object, IDockControl>>
|
||||||
|
{
|
||||||
|
[typeof(DockGroup)] = model => CreateGroupControl((DockGroup)model),
|
||||||
|
[typeof(DockLeaf)] = model => CreateLeafControl((DockLeaf)model),
|
||||||
|
[typeof(DockWindow)] = model => CreateFloatingWindowControl((DockWindow)model),
|
||||||
|
[typeof(AutoHidePanel)] = model => CreateAutoHidePanelControl((AutoHidePanel)model),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает контрол для группы разделения.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="group">Модель группы разделения.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// Созданный контрол группы.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="group"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Создает экземпляр <see cref="LatticeDockGroup"/>, настраивает его связи
|
|
||||||
/// с моделью и другими сервисами, применяет текущую тему оформления.
|
|
||||||
/// </remarks>
|
|
||||||
public override IDockGroupControl CreateGroupControl(DockGroup group)
|
public override IDockGroupControl CreateGroupControl(DockGroup group)
|
||||||
{
|
{
|
||||||
|
if (group == null) throw new ArgumentNullException(nameof(group));
|
||||||
|
|
||||||
var control = new LatticeDockGroup();
|
var control = new LatticeDockGroup();
|
||||||
ConfigureControl(control, group);
|
ConfigureControl(control, group);
|
||||||
control.ApplyTheme(_theme);
|
|
||||||
return control;
|
return control;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает контрол для контейнера вкладок.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="leaf">Модель контейнера вкладок.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// Созданный контрол листа.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="leaf"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Создает экземпляр <see cref="LatticeTabControl"/>, настраивает его связи
|
|
||||||
/// с моделью и другими сервисами, применяет текущую тему оформления.
|
|
||||||
/// Контрол поддерживает все положения панели вкладок и операции с вкладками.
|
|
||||||
/// </remarks>
|
|
||||||
public override IDockLeafControl CreateLeafControl(DockLeaf leaf)
|
public override IDockLeafControl CreateLeafControl(DockLeaf leaf)
|
||||||
{
|
{
|
||||||
|
if (leaf == null) throw new ArgumentNullException(nameof(leaf));
|
||||||
|
|
||||||
var control = new LatticeTabControl();
|
var control = new LatticeTabControl();
|
||||||
ConfigureControl(control, leaf);
|
ConfigureControl(control, leaf);
|
||||||
control.ApplyTheme(_theme);
|
|
||||||
return control;
|
return control;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает контрол для плавающего окна.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="window">Модель плавающего окна.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// Созданный контрол окна.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="window"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// В текущей реализации метод не реализован. Плавающие окна требуют
|
|
||||||
/// дополнительной интеграции с оконной системой платформы.
|
|
||||||
/// </remarks>
|
|
||||||
public override IFloatingWindowControl CreateFloatingWindowControl(DockWindow window)
|
public override IFloatingWindowControl CreateFloatingWindowControl(DockWindow window)
|
||||||
{
|
{
|
||||||
// TODO: Реализовать создание плавающего окна
|
throw new NotImplementedException("Floating windows not implemented yet");
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает контрол для автоскрываемой панели.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="panel">Модель автоскрываемой панели.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// Созданный контрол панели.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="panel"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// В текущей реализации метод не реализован. Автоскрываемые панели требуют
|
|
||||||
/// сложной логики анимации и взаимодействия с краями окна.
|
|
||||||
/// </remarks>
|
|
||||||
public override IAutoHidePanelControl CreateAutoHidePanelControl(AutoHidePanel panel)
|
public override IAutoHidePanelControl CreateAutoHidePanelControl(AutoHidePanel panel)
|
||||||
{
|
{
|
||||||
// TODO: Реализовать создание автоскрываемой панели
|
throw new NotImplementedException("Auto-hide panels not implemented yet");
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает контрол для разделителя.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="orientation">Ориентация разделителя.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// Созданный контрол разделителя.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Создает экземпляр <see cref="LatticeSplitter"/>, настраивает его ориентацию
|
|
||||||
/// и применяет текущую тему оформления. Разделитель поддерживает перетаскивание
|
|
||||||
/// для изменения соотношения размеров между соседними областями.
|
|
||||||
/// </remarks>
|
|
||||||
public override IDockSplitterControl CreateSplitterControl(SplitDirection orientation)
|
public override IDockSplitterControl CreateSplitterControl(SplitDirection orientation)
|
||||||
{
|
{
|
||||||
var control = new LatticeSplitter
|
var control = new LatticeSplitter { Orientation = orientation };
|
||||||
{
|
|
||||||
Orientation = orientation
|
|
||||||
};
|
|
||||||
ConfigureControl(control);
|
ConfigureControl(control);
|
||||||
control.ApplyTheme(_theme);
|
|
||||||
return control;
|
return control;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public override IDockControl? CreateControlForElement(IDockElement element)
|
||||||
/// Создает хост для размещения системы докинга.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>
|
|
||||||
/// Созданный док-хост.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Создает корневой контейнер для всей системы докинга - экземпляр <see cref="LatticeDockHost"/>.
|
|
||||||
/// Хост управляет всем макетом приложения, включая основное дерево компоновки,
|
|
||||||
/// плавающие окна и автоскрываемые панели.
|
|
||||||
/// </remarks>
|
|
||||||
public IDockHost CreateDockHost()
|
|
||||||
{
|
{
|
||||||
var host = new LatticeDockHost();
|
if (element == null) throw new ArgumentNullException(nameof(element));
|
||||||
ConfigureControl(host);
|
|
||||||
host.ApplyTheme(_theme);
|
var type = element.GetType();
|
||||||
return host;
|
if (_creators.TryGetValue(type, out var creator))
|
||||||
|
return creator(element);
|
||||||
|
|
||||||
|
return base.CreateControlForElement(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Настраивает созданный контрол.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="control">Контрол для настройки.</param>
|
|
||||||
/// <param name="model">Модель данных для контрола (опционально).</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Устанавливает основные связи контрола: модель данных, менеджер макета,
|
|
||||||
/// контекстный менеджер. Также настраивает привязку данных через DataContext.
|
|
||||||
/// Этот метод вызывается для всех создаваемых контролов.
|
|
||||||
/// </remarks>
|
|
||||||
private void ConfigureControl(IDockControl control, IDockElement? model = null)
|
private void ConfigureControl(IDockControl control, IDockElement? model = null)
|
||||||
{
|
{
|
||||||
if (control == null) return;
|
if (control == null) return;
|
||||||
|
|
||||||
control.Model = model;
|
control.Model = model;
|
||||||
control.LayoutManager = LatticeUIFramework.LayoutManager;
|
control.LayoutManager = Lattice.UI.Docking.LatticeUIFramework.LayoutManager;
|
||||||
control.ContextManager = LatticeUIFramework.ContextManager;
|
control.ContextManager = Lattice.UI.Docking.LatticeUIFramework.ContextManager;
|
||||||
|
|
||||||
if (control is FrameworkElement frameworkElement && model != null)
|
if (control is FrameworkElement frameworkElement && model != null)
|
||||||
{
|
{
|
||||||
|
|||||||
53
Lattice.UI.Docking.WinUI/Services/DragDropService.cs
Normal file
53
Lattice.UI.Docking.WinUI/Services/DragDropService.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Lattice.UI.Docking.WinUI.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сервис для управления операциями Drag & Drop в WinUI.
|
||||||
|
/// </summary>
|
||||||
|
public static class DragDropService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Настраивает элемент для поддержки перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
public static void SetupDragElement(UIElement element, Func<object?> 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настраивает элемент для приема сброса.
|
||||||
|
/// </summary>
|
||||||
|
public static void SetupDropElement(UIElement element, Func<object, bool> 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 = "Переместить";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,9 @@ using Microsoft.UI.Xaml;
|
|||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
namespace Lattice.UI.Docking.WinUI.Services;
|
namespace Lattice.UI.Docking.WinUI.Services;
|
||||||
|
|
||||||
@@ -21,9 +24,38 @@ public sealed class WinUIDockContextManager : DockContextManagerBase, IDisposabl
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public WinUIDockContextManager()
|
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override void ShowContextMenu(IDockControl element, double x, double y)
|
public override void ShowContextMenu(IDockControl element, double x, double y)
|
||||||
{
|
{
|
||||||
if (element is not FrameworkElement uiElement) return;
|
if (element is not FrameworkElement uiElement) return;
|
||||||
@@ -39,13 +71,26 @@ public sealed class WinUIDockContextManager : DockContextManagerBase, IDisposabl
|
|||||||
var item = new MenuFlyoutItem
|
var item = new MenuFlyoutItem
|
||||||
{
|
{
|
||||||
Text = command.Name,
|
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))
|
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);
|
flyout.Items.Add(item);
|
||||||
@@ -68,7 +113,6 @@ public sealed class WinUIDockContextManager : DockContextManagerBase, IDisposabl
|
|||||||
OnContextMenuShown(element, x, y);
|
OnContextMenuShown(element, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override void HideContextMenu()
|
public override void HideContextMenu()
|
||||||
{
|
{
|
||||||
if (_currentFlyout != null)
|
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 _);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Класс-заглушка для реализации ICommand.
|
/// Получает команду по идентификатору.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private sealed class RelayCommand : System.Windows.Input.ICommand
|
protected override IDockCommand? GetCommand(string commandId)
|
||||||
|
{
|
||||||
|
_commands.TryGetValue(commandId, out var command);
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает все доступные команды для указанного элемента.
|
||||||
|
/// </summary>
|
||||||
|
protected override IEnumerable<IDockCommand> GetCommandsForElement(IDockControl element)
|
||||||
|
{
|
||||||
|
return _commands.Values.Where(c => CanExecuteCommand(c, element));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Класс для реализации ICommand.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class RelayCommand : ICommand
|
||||||
{
|
{
|
||||||
private readonly Action _execute;
|
private readonly Action _execute;
|
||||||
private readonly Func<bool>? _canExecute;
|
private readonly Func<bool> _canExecute;
|
||||||
|
|
||||||
public event EventHandler? CanExecuteChanged;
|
public event EventHandler? CanExecuteChanged;
|
||||||
|
|
||||||
public RelayCommand(Action execute, Func<bool>? canExecute = null)
|
public RelayCommand(Action execute, Func<bool> canExecute)
|
||||||
{
|
{
|
||||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||||
_canExecute = canExecute;
|
_canExecute = canExecute;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
|
public bool CanExecute(object? parameter) => _canExecute();
|
||||||
|
|
||||||
public void Execute(object? parameter) => _execute();
|
public void Execute(object? parameter) => _execute();
|
||||||
|
|
||||||
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <summary>
|
||||||
|
/// Базовая реализация команды докинга.
|
||||||
|
/// </summary>
|
||||||
|
private class DockCommand : IDockCommand
|
||||||
|
{
|
||||||
|
public string Id { get; }
|
||||||
|
public string Name { get; }
|
||||||
|
public string Description { get; }
|
||||||
|
private readonly Func<string> _getIcon;
|
||||||
|
private readonly Func<bool> _canExecute;
|
||||||
|
private readonly Action _execute;
|
||||||
|
|
||||||
|
public DockCommand(string id, string name, string description, Func<string> getIcon, Func<bool> 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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
HideContextMenu();
|
HideContextMenu();
|
||||||
|
|||||||
@@ -12,39 +12,8 @@ namespace Lattice.UI.Docking.WinUI.Services;
|
|||||||
/// Инкапсулирует платформенно-зависимые операции, такие как создание окон,
|
/// Инкапсулирует платформенно-зависимые операции, такие как создание окон,
|
||||||
/// показ диалогов и синхронизация с UI-потоком.
|
/// показ диалогов и синхронизация с UI-потоком.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// <see cref="WinUIDockUIService"/> предоставляет конкретные реализации методов
|
|
||||||
/// <see cref="IDockUIService"/> для платформы WinUI. Это позволяет основной
|
|
||||||
/// бизнес-логике док-системы оставаться независимой от конкретной UI-платформы.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Сервис использует API WinUI для создания окон, показа ContentDialog и
|
|
||||||
/// управления диспетчером потока пользовательского интерфейса.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService
|
public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Создает главное окно приложения для размещения док-хоста.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="host">
|
|
||||||
/// Экземпляр <see cref="IDockHost"/>, который будет содержаться в окне.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// Объект окна WinUI, который можно отобразить и управлять им.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="host"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <exception cref="ArgumentException">
|
|
||||||
/// Выбрасывается, если <paramref name="host"/> не является элементом WinUI.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Создает окно WinUI с заголовком "Lattice IDE", устанавливает указанный хост
|
|
||||||
/// в качестве содержимого и регистрирует окно в системе отслеживания окон.
|
|
||||||
/// Окно создается с настройками по умолчанию для IDE-подобных приложений.
|
|
||||||
/// </remarks>
|
|
||||||
public override object CreateMainWindow(IDockHost host)
|
public override object CreateMainWindow(IDockHost host)
|
||||||
{
|
{
|
||||||
if (host is not FrameworkElement hostElement)
|
if (host is not FrameworkElement hostElement)
|
||||||
@@ -55,30 +24,11 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService
|
|||||||
window.AppWindow.Title = "Lattice IDE";
|
window.AppWindow.Title = "Lattice IDE";
|
||||||
|
|
||||||
// Регистрируем окно в трекере
|
// Регистрируем окно в трекере
|
||||||
Themes.WindowTracker.Register(window);
|
Lattice.Themes.WindowTracker.Register(window);
|
||||||
|
|
||||||
return window;
|
return window;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Отображает модальное диалоговое окно с указанным содержимым.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="title">Заголовок диалогового окна.</param>
|
|
||||||
/// <param name="content">Содержимое диалогового окна.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// Nullable boolean значение, указывающее результат диалога:
|
|
||||||
/// true - пользователь подтвердил действие,
|
|
||||||
/// false - пользователь отменил действие,
|
|
||||||
/// null - диалог был закрыт без выбора.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="title"/> или <paramref name="content"/> равны null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Создает и показывает ContentDialog с кнопками OK и Cancel.
|
|
||||||
/// Блокирует взаимодействие с родительским окном до закрытия диалога.
|
|
||||||
/// Использует XamlRoot активного окна для корректного отображения.
|
|
||||||
/// </remarks>
|
|
||||||
public override bool? ShowDialog(string title, object content)
|
public override bool? ShowDialog(string title, object content)
|
||||||
{
|
{
|
||||||
if (content is not FrameworkElement contentElement)
|
if (content is not FrameworkElement contentElement)
|
||||||
@@ -93,7 +43,6 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService
|
|||||||
XamlRoot = GetActiveXamlRoot()
|
XamlRoot = GetActiveXamlRoot()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Показываем диалог и возвращаем результат
|
|
||||||
var result = dialog.ShowAsync();
|
var result = dialog.ShowAsync();
|
||||||
return result.GetAwaiter().GetResult() switch
|
return result.GetAwaiter().GetResult() switch
|
||||||
{
|
{
|
||||||
@@ -103,19 +52,6 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Отображает информационное сообщение с кнопкой OK.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="message">Текст сообщения.</param>
|
|
||||||
/// <param name="caption">Заголовок окна сообщения.</param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="message"/> или <paramref name="caption"/> равны null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Создает ContentDialog с текстом сообщения и одной кнопкой OK.
|
|
||||||
/// Используется для информирования пользователя о результате операции
|
|
||||||
/// или отображения некритичных ошибок.
|
|
||||||
/// </remarks>
|
|
||||||
public override void ShowMessage(string message, string caption)
|
public override void ShowMessage(string message, string caption)
|
||||||
{
|
{
|
||||||
var dialog = new ContentDialog
|
var dialog = new ContentDialog
|
||||||
@@ -129,22 +65,6 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService
|
|||||||
dialog.ShowAsync();
|
dialog.ShowAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Отображает диалог подтверждения с кнопками Yes/No.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="message">Текст вопроса.</param>
|
|
||||||
/// <param name="caption">Заголовок окна подтверждения.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// true, если пользователь выбрал "Yes"; false, если пользователь выбрал "No".
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="message"/> или <paramref name="caption"/> равны null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Создает ContentDialog с кнопками Yes и No. Используется для получения
|
|
||||||
/// подтверждения пользователя перед выполнением критических операций,
|
|
||||||
/// таких как закрытие вкладок с несохраненными данными или сброс настроек.
|
|
||||||
/// </remarks>
|
|
||||||
public override bool Confirm(string message, string caption)
|
public override bool Confirm(string message, string caption)
|
||||||
{
|
{
|
||||||
var dialog = new ContentDialog
|
var dialog = new ContentDialog
|
||||||
@@ -160,22 +80,6 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService
|
|||||||
return result == ContentDialogResult.Primary;
|
return result == ContentDialogResult.Primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Отображает диалог ввода текста.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="prompt">Текст подсказки для пользователя.</param>
|
|
||||||
/// <param name="defaultValue">Значение по умолчанию для поля ввода.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// Введенный пользователем текст или null, если диалог был отменен.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="prompt"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Создает ContentDialog с однострочным полем ввода TextBox.
|
|
||||||
/// Используется для получения текстового ввода от пользователя, такого как
|
|
||||||
/// имена файлов, названия документов или параметры конфигурации.
|
|
||||||
/// </remarks>
|
|
||||||
public override string? Prompt(string prompt, string? defaultValue = null)
|
public override string? Prompt(string prompt, string? defaultValue = null)
|
||||||
{
|
{
|
||||||
var textBox = new TextBox
|
var textBox = new TextBox
|
||||||
@@ -198,19 +102,6 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService
|
|||||||
return result == ContentDialogResult.Primary ? textBox.Text : null;
|
return result == ContentDialogResult.Primary ? textBox.Text : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Выполняет указанное действие в UI-потоке.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="action">Действие для выполнения.</param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="action"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Гарантирует, что действие будет выполнено в потоке, связанном с
|
|
||||||
/// пользовательским интерфейсом. Если текущий поток уже является UI-потоком,
|
|
||||||
/// действие выполняется немедленно. В противном случае действие ставится
|
|
||||||
/// в очередь диспетчера WinUI.
|
|
||||||
/// </remarks>
|
|
||||||
public override void InvokeOnUIThread(Action action)
|
public override void InvokeOnUIThread(Action action)
|
||||||
{
|
{
|
||||||
if (action == null) return;
|
if (action == null) return;
|
||||||
@@ -229,19 +120,7 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Выполняет указанную асинхронную функцию в UI-потоке.
|
/// Выполняет указанную асинхронную функцию в UI-потоке.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="action">Асинхронная функция для выполнения.</param>
|
public async Task InvokeOnUIThreadAsync(Func<Task> action)
|
||||||
/// <returns>
|
|
||||||
/// Задача, представляющая асинхронную операцию.
|
|
||||||
/// </returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="action"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Гарантирует, что асинхронная функция будет выполнена в UI-потоке.
|
|
||||||
/// Используется для операций, которые требуют доступа к UI-элементам
|
|
||||||
/// или выполняют асинхронные вызовы с обновлением интерфейса.
|
|
||||||
/// </remarks>
|
|
||||||
public override async Task InvokeOnUIThreadAsync(Func<Task> action)
|
|
||||||
{
|
{
|
||||||
if (action == null) return;
|
if (action == null) return;
|
||||||
|
|
||||||
@@ -269,21 +148,9 @@ public sealed class WinUIDockUIService : DockUIServiceBase, IDockUIService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Получает XamlRoot активного окна приложения.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>
|
|
||||||
/// XamlRoot активного окна или null, если нет активных окон.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Используется для корректного отображения диалоговых окон в контексте
|
|
||||||
/// текущего окна приложения. Перебирает все зарегистрированные окна
|
|
||||||
/// и возвращает XamlRoot первого найденного.
|
|
||||||
/// </remarks>
|
|
||||||
private XamlRoot? GetActiveXamlRoot()
|
private XamlRoot? GetActiveXamlRoot()
|
||||||
{
|
{
|
||||||
// Получаем XamlRoot из активного окна
|
foreach (var window in Lattice.Themes.WindowTracker.Windows)
|
||||||
foreach (var window in Themes.WindowTracker.Windows)
|
|
||||||
{
|
{
|
||||||
if (window.Content is FrameworkElement element)
|
if (window.Content is FrameworkElement element)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Предоставляет реализацию сервиса перетаскивания для платформы WinUI с расширенной
|
|
||||||
/// поддержкой визуальных эффектов и интеграцией с системой докинга Lattice.
|
|
||||||
/// Координирует взаимодействие между базовым менеджером перетаскивания и UI-контролами,
|
|
||||||
/// обеспечивая богатую визуальную обратную связь во время операций drag-and-drop.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// <see cref="WinUIDragDropService"/> расширяет базовый функционал <see cref="DockDragDropService"/>
|
|
||||||
/// платформенно-зависимыми визуальными эффектами, включая:
|
|
||||||
/// </para>
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Прозрачное визуальное представление перетаскиваемого элемента</item>
|
|
||||||
/// <item>Интерактивные подсказки областей сброса</item>
|
|
||||||
/// <item>Анимации при начале и завершении перетаскивания</item>
|
|
||||||
/// <item>Подсветку допустимых целей сброса</item>
|
|
||||||
/// </list>
|
|
||||||
/// <para>
|
|
||||||
/// Сервис поддерживает регистрацию UI-элементов и автоматически вычисляет их границы
|
|
||||||
/// для точного определения целей сброса.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public sealed class WinUIDragDropService : DockDragDropService, IDisposable
|
|
||||||
{
|
|
||||||
private readonly ConcurrentDictionary<IDockControl, FrameworkElement> _controlToElement = new();
|
|
||||||
private readonly DragDropManagerEx _dragDropManager;
|
|
||||||
private Popup? _dragVisualPopup;
|
|
||||||
private Border? _dragVisual;
|
|
||||||
private DropHintOverlay? _dropHintOverlay;
|
|
||||||
private bool _disposed;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр сервиса перетаскивания WinUI.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Создает внутренний менеджер перетаскивания, инициализирует визуальные элементы
|
|
||||||
/// и подписывается на события менеджера для обработки операций перетаскивания.
|
|
||||||
/// </remarks>
|
|
||||||
public WinUIDragDropService()
|
|
||||||
{
|
|
||||||
_dragDropManager = new DragDropManagerEx();
|
|
||||||
HookEvents();
|
|
||||||
InitializeDragVisual();
|
|
||||||
InitializeDropHintOverlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр с указанным менеджером перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragDropManager">
|
|
||||||
/// Предварительно настроенный менеджер перетаскивания.
|
|
||||||
/// </param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="dragDropManager"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Позволяет использовать кастомную конфигурацию менеджера перетаскивания
|
|
||||||
/// при сохранении всех визуальных эффектов WinUI.
|
|
||||||
/// </remarks>
|
|
||||||
public WinUIDragDropService(DragDropManagerEx dragDropManager)
|
|
||||||
{
|
|
||||||
_dragDropManager = dragDropManager ?? throw new ArgumentNullException(nameof(dragDropManager));
|
|
||||||
HookEvents();
|
|
||||||
InitializeDragVisual();
|
|
||||||
InitializeDropHintOverlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Подписывается на события менеджера перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Обрабатывает следующие события:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Начало перетаскивания</item>
|
|
||||||
/// <item>Обновление позиции перетаскивания</item>
|
|
||||||
/// <item>Завершение перетаскивания</item>
|
|
||||||
/// <item>Отмена перетаскивания</item>
|
|
||||||
/// <item>Изменение цели сброса</item>
|
|
||||||
/// </list>
|
|
||||||
/// </remarks>
|
|
||||||
private void HookEvents()
|
|
||||||
{
|
|
||||||
_dragDropManager.DragStarted += OnDragStarted;
|
|
||||||
_dragDropManager.DragUpdated += OnDragUpdated;
|
|
||||||
_dragDropManager.DragCompleted += OnDragCompleted;
|
|
||||||
_dragDropManager.DragCancelled += OnDragCancelled;
|
|
||||||
_dragDropManager.DropTargetChanged += OnDropTargetChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует визуальное представление перетаскиваемого элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Создает Popup с Border для отображения полупрозрачной копии
|
|
||||||
/// перетаскиваемого элемента во время операции drag-and-drop.
|
|
||||||
/// </remarks>
|
|
||||||
private void InitializeDragVisual()
|
|
||||||
{
|
|
||||||
// Создаем Popup для отображения визуального представления перетаскивания
|
|
||||||
_dragVisualPopup = new Popup
|
|
||||||
{
|
|
||||||
IsHitTestVisible = false,
|
|
||||||
IsLightDismissEnabled = false,
|
|
||||||
Child = null
|
|
||||||
};
|
|
||||||
|
|
||||||
// Создаем визуальный элемент для перетаскивания
|
|
||||||
_dragVisual = new Border
|
|
||||||
{
|
|
||||||
Background = new SolidColorBrush(Microsoft.UI.Colors.Transparent),
|
|
||||||
BorderBrush = new SolidColorBrush(Microsoft.UI.Colors.DodgerBlue),
|
|
||||||
BorderThickness = new Thickness(2),
|
|
||||||
CornerRadius = new CornerRadius(4),
|
|
||||||
Opacity = 0.7
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует оверлей для отображения подсказок при сбросе.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Добавляет оверлей в корневой контейнер приложения для отображения
|
|
||||||
/// визуальных подсказок о возможных позициях сброса.
|
|
||||||
/// </remarks>
|
|
||||||
private void InitializeDropHintOverlay()
|
|
||||||
{
|
|
||||||
// Создаем оверлей для подсказок при сбросе
|
|
||||||
_dropHintOverlay = new DropHintOverlay();
|
|
||||||
|
|
||||||
// Добавляем оверлей в корневой контейнер приложения
|
|
||||||
if (Window.Current?.Content is Panel rootPanel)
|
|
||||||
{
|
|
||||||
rootPanel.Children.Add(_dropHintOverlay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Регистрирует связь между абстрактным контролом док-системы и конкретным UI-элементом WinUI.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="control">Абстрактный контрол док-системы.</param>
|
|
||||||
/// <param name="element">Конкретный UI-элемент WinUI.</param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="control"/> или <paramref name="element"/> равны null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Эта связь необходима для:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Вычисления границ элемента на экране</item>
|
|
||||||
/// <item>Создания визуального представления перетаскивания</item>
|
|
||||||
/// <item>Определения позиции сброса относительно элемента</item>
|
|
||||||
/// </list>
|
|
||||||
/// </remarks>
|
|
||||||
public void RegisterControl(IDockControl control, FrameworkElement element)
|
|
||||||
{
|
|
||||||
if (control == null) throw new ArgumentNullException(nameof(control));
|
|
||||||
if (element == null) throw new ArgumentNullException(nameof(element));
|
|
||||||
|
|
||||||
_controlToElement[control] = element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Отменяет регистрацию связи между абстрактным контролом док-системы и UI-элементом WinUI.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="control">Абстрактный контрол док-системы.</param>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// Выбрасывается, если <paramref name="control"/> равен null.
|
|
||||||
/// </exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// Удаляет элемент из внутреннего словаря, освобождая связанные с ним ресурсы.
|
|
||||||
/// </remarks>
|
|
||||||
public void UnregisterControl(IDockControl control)
|
|
||||||
{
|
|
||||||
if (control == null) throw new ArgumentNullException(nameof(control));
|
|
||||||
|
|
||||||
_controlToElement.TryRemove(control, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вычисляет границы элемента на экране.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="element">Элемент, для которого вычисляются границы.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// Прямоугольник в экранных координатах, представляющий границы элемента.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Метод выполняет преобразование координат элемента в экранные координаты
|
|
||||||
/// с использованием трансформации визуального дерева.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// В случае ошибки вычисления возвращает прямоугольник размером 100x100 пикселей
|
|
||||||
/// в точке (0, 0).
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
protected override Rect CalculateBounds(IDockControl element)
|
|
||||||
{
|
|
||||||
if (_controlToElement.TryGetValue(element, out var uiElement))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Получаем преобразование координат в экранные
|
|
||||||
var transform = uiElement.TransformToVisual(Window.Current.Content);
|
|
||||||
var point = transform.TransformPoint(new Windows.Foundation.Point(0, 0));
|
|
||||||
|
|
||||||
return new Rect(
|
|
||||||
point.X, point.Y,
|
|
||||||
uiElement.ActualWidth, uiElement.ActualHeight);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
System.Diagnostics.Debug.WriteLine($"Failed to calculate bounds: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Возвращаем значения по умолчанию, если не удалось вычислить
|
|
||||||
return new Rect(0, 0, 100, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Создает визуальное представление перетаскиваемого элемента.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dragInfo">Информация о перетаскивании.</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// На основе источника перетаскивания создает полупрозрачную копию элемента,
|
|
||||||
/// которая следует за курсором мыши во время операции перетаскивания.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Визуальное представление включает:
|
|
||||||
/// </para>
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Тень для создания эффекта глубины</item>
|
|
||||||
/// <item>Прозрачность для видимости фонового содержимого</item>
|
|
||||||
/// <item>Синюю границу для визуального выделения</item>
|
|
||||||
/// </list>
|
|
||||||
/// </remarks>
|
|
||||||
protected override void CreateDragVisual(UiDragInfo dragInfo)
|
|
||||||
{
|
|
||||||
if (_dragVisual == null || _dragVisualPopup == null || dragInfo.SourceControl == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Настраиваем визуальное представление на основе источника
|
|
||||||
if (_controlToElement.TryGetValue(dragInfo.SourceControl, out var sourceElement))
|
|
||||||
{
|
|
||||||
// Устанавливаем размеры визуального представления
|
|
||||||
_dragVisual.Width = sourceElement.ActualWidth;
|
|
||||||
_dragVisual.Height = sourceElement.ActualHeight;
|
|
||||||
|
|
||||||
// Создаем эффект прозрачности и тени
|
|
||||||
_dragVisual.Opacity = 0.7;
|
|
||||||
|
|
||||||
// Устанавливаем позицию Popup
|
|
||||||
_dragVisualPopup.HorizontalOffset = dragInfo.BaseDragInfo.StartPosition.X;
|
|
||||||
_dragVisualPopup.VerticalOffset = dragInfo.BaseDragInfo.StartPosition.Y;
|
|
||||||
_dragVisualPopup.Child = _dragVisual;
|
|
||||||
_dragVisualPopup.IsOpen = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Обновляет позицию визуального представления перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="position">Новая позиция курсора.</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Перемещает Popup с визуальным представлением в указанную позицию,
|
|
||||||
/// обеспечивая плавное следование за курсором мыши.
|
|
||||||
/// </remarks>
|
|
||||||
protected override void UpdateDragVisualPosition(Point position)
|
|
||||||
{
|
|
||||||
if (_dragVisualPopup != null)
|
|
||||||
{
|
|
||||||
_dragVisualPopup.HorizontalOffset = position.X;
|
|
||||||
_dragVisualPopup.VerticalOffset = position.Y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Очищает визуальное представление перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Скрывает и освобождает ресурсы Popup, используемого для отображения
|
|
||||||
/// визуального представления перетаскиваемого элемента.
|
|
||||||
/// </remarks>
|
|
||||||
protected override void CleanupDragVisual()
|
|
||||||
{
|
|
||||||
if (_dragVisualPopup != null)
|
|
||||||
{
|
|
||||||
_dragVisualPopup.IsOpen = false;
|
|
||||||
_dragVisualPopup.Child = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Показывает визуальную подсказку о возможной позиции сброса.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="element">Элемент, для которого показывается подсказка.</param>
|
|
||||||
/// <param name="position">Предполагаемая позиция сброса.</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Отображает полупрозрачный прямоугольник в указанной позиции относительно элемента,
|
|
||||||
/// давая пользователю визуальную обратную связь о том, куда будет помещен элемент.
|
|
||||||
/// </remarks>
|
|
||||||
protected override void ShowDropHint(IDockControl element, DropPosition position)
|
|
||||||
{
|
|
||||||
_dropHintOverlay?.ShowHint(element, position);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Скрывает текущую визуальную подсказку о сбросе.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Убирает все отображаемые подсказки сброса, очищая оверлей.
|
|
||||||
/// </remarks>
|
|
||||||
protected override void HideDropHint()
|
|
||||||
{
|
|
||||||
_dropHintOverlay?.HideHint();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Освобождает ресурсы, используемые сервисом перетаскивания.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Выполняет следующие действия:
|
|
||||||
/// </para>
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Отписывается от всех событий менеджера перетаскивания</item>
|
|
||||||
/// <item>Удаляет оверлей подсказок из корневого контейнера</item>
|
|
||||||
/// <item>Очищает словарь зарегистрированных контролов</item>
|
|
||||||
/// <item>Освобождает визуальные элементы</item>
|
|
||||||
/// </list>
|
|
||||||
/// </remarks>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (!_disposed)
|
|
||||||
{
|
|
||||||
if (_dragDropManager != null)
|
|
||||||
{
|
|
||||||
_dragDropManager.DragStarted -= OnDragStarted;
|
|
||||||
_dragDropManager.DragUpdated -= OnDragUpdated;
|
|
||||||
_dragDropManager.DragCompleted -= OnDragCompleted;
|
|
||||||
_dragDropManager.DragCancelled -= OnDragCancelled;
|
|
||||||
_dragDropManager.DropTargetChanged -= OnDropTargetChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_dropHintOverlay != null && Window.Current?.Content is Panel rootPanel)
|
|
||||||
{
|
|
||||||
rootPanel.Children.Remove(_dropHintOverlay);
|
|
||||||
_dropHintOverlay = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_controlToElement.Clear();
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Представляет оверлей для отображения визуальных подсказок при сбросе в операции перетаскивания.
|
|
||||||
/// Этот элемент отображает полупрозрачные прямоугольники в местах возможного сброса,
|
|
||||||
/// давая пользователю визуальную обратную связь о допустимых позициях.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// <see cref="DropHintOverlay"/> является внутренним вспомогательным классом,
|
|
||||||
/// который отображается поверх всего пользовательского интерфейса во время операции
|
|
||||||
/// перетаскивания для показа визуальных подсказок.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Оверлей поддерживает все позиции сброса, определенные в <see cref="DropPosition"/>,
|
|
||||||
/// и автоматически вычисляет размеры и положение подсказок на основе целевого элемента.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
internal sealed class DropHintOverlay : Grid
|
|
||||||
{
|
|
||||||
private readonly Dictionary<DropPosition, Border> _hintRectangles = new();
|
|
||||||
private readonly SolidColorBrush _hintBrush;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует новый экземпляр класса <see cref="DropHintOverlay"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Создает прозрачный оверлей, который не участвует в тестировании попаданий,
|
|
||||||
/// и инициализирует прямоугольники для всех возможных позиций сброса.
|
|
||||||
/// </remarks>
|
|
||||||
public DropHintOverlay()
|
|
||||||
{
|
|
||||||
this.IsHitTestVisible = false;
|
|
||||||
this.Background = new SolidColorBrush(Microsoft.UI.Colors.Transparent);
|
|
||||||
|
|
||||||
// Используем акцентный цвет для подсказок
|
|
||||||
_hintBrush = new SolidColorBrush(Microsoft.UI.Colors.DodgerBlue);
|
|
||||||
|
|
||||||
InitializeHintRectangles();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Инициализирует прямоугольники для всех позиций сброса.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Создает отдельный Border для каждой позиции сброса и добавляет их в дочернюю коллекцию.
|
|
||||||
/// Все прямоугольники изначально скрыты и отображаются только при необходимости.
|
|
||||||
/// </remarks>
|
|
||||||
private void InitializeHintRectangles()
|
|
||||||
{
|
|
||||||
// Создаем прямоугольники для каждой позиции сброса
|
|
||||||
var positions = new[]
|
|
||||||
{
|
|
||||||
DropPosition.Left, DropPosition.Right,
|
|
||||||
DropPosition.Top, DropPosition.Bottom,
|
|
||||||
DropPosition.Center, DropPosition.Tab
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var position in positions)
|
|
||||||
{
|
|
||||||
var rect = new Border
|
|
||||||
{
|
|
||||||
Background = _hintBrush,
|
|
||||||
Opacity = 0.3,
|
|
||||||
BorderBrush = new SolidColorBrush(Microsoft.UI.Colors.DodgerBlue),
|
|
||||||
BorderThickness = new Thickness(2),
|
|
||||||
Visibility = Visibility.Collapsed,
|
|
||||||
CornerRadius = new CornerRadius(4)
|
|
||||||
};
|
|
||||||
|
|
||||||
_hintRectangles[position] = rect;
|
|
||||||
this.Children.Add(rect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Показывает визуальную подсказку для указанного элемента и позиции сброса.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="element">Элемент, для которого показывается подсказка.</param>
|
|
||||||
/// <param name="position">Позиция сброса относительно элемента.</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Вычисляет положение и размер подсказки на основе границ элемента и позиции сброса,
|
|
||||||
/// затем делает соответствующий прямоугольник видимым.
|
|
||||||
/// </remarks>
|
|
||||||
public void ShowHint(IDockControl element, DropPosition position)
|
|
||||||
{
|
|
||||||
if (element is not FrameworkElement uiElement) return;
|
|
||||||
if (!_hintRectangles.TryGetValue(position, out var rect)) return;
|
|
||||||
|
|
||||||
// Вычисляем позицию и размер подсказки
|
|
||||||
var bounds = CalculateHintBounds(uiElement, position);
|
|
||||||
|
|
||||||
Canvas.SetLeft(rect, bounds.X);
|
|
||||||
Canvas.SetTop(rect, bounds.Y);
|
|
||||||
rect.Width = bounds.Width;
|
|
||||||
rect.Height = bounds.Height;
|
|
||||||
rect.Visibility = Visibility.Visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вычисляет границы подсказки для указанного элемента и позиции сброса.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="element">Целевой элемент.</param>
|
|
||||||
/// <param name="position">Позиция сброса.</param>
|
|
||||||
/// <returns>Прямоугольник с координатами и размерами подсказки.</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Размеры подсказок зависят от позиции:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Слева/справа: ширина 50px, высота равна высоте элемента</item>
|
|
||||||
/// <item>Сверху/снизу: высота 50px, ширина равна ширине элемента</item>
|
|
||||||
/// <item>В центре: размеры равны размерам элемента</item>
|
|
||||||
/// <item>Вкладка: высота 30px, ширина равна ширине элемента, позиция сверху</item>
|
|
||||||
/// </list>
|
|
||||||
/// </remarks>
|
|
||||||
private Rect CalculateHintBounds(FrameworkElement element, DropPosition position)
|
|
||||||
{
|
|
||||||
// Получаем позицию элемента относительно оверлея
|
|
||||||
var transform = element.TransformToVisual(this);
|
|
||||||
var point = transform.TransformPoint(new Windows.Foundation.Point(0, 0));
|
|
||||||
|
|
||||||
// Вычисляем размеры подсказки в зависимости от позиции
|
|
||||||
return position switch
|
|
||||||
{
|
|
||||||
DropPosition.Left => new Rect(
|
|
||||||
point.X - 50, point.Y,
|
|
||||||
50, element.ActualHeight),
|
|
||||||
|
|
||||||
DropPosition.Right => new Rect(
|
|
||||||
point.X + element.ActualWidth, point.Y,
|
|
||||||
50, element.ActualHeight),
|
|
||||||
|
|
||||||
DropPosition.Top => new Rect(
|
|
||||||
point.X, point.Y - 50,
|
|
||||||
element.ActualWidth, 50),
|
|
||||||
|
|
||||||
DropPosition.Bottom => new Rect(
|
|
||||||
point.X, point.Y + element.ActualHeight,
|
|
||||||
element.ActualWidth, 50),
|
|
||||||
|
|
||||||
DropPosition.Center => new Rect(
|
|
||||||
point.X, point.Y,
|
|
||||||
element.ActualWidth, element.ActualHeight),
|
|
||||||
|
|
||||||
DropPosition.Tab => new Rect(
|
|
||||||
point.X, point.Y - 30,
|
|
||||||
element.ActualWidth, 30),
|
|
||||||
|
|
||||||
_ => new Rect(point.X, point.Y, 100, 100)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Скрывает все визуальные подсказки.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Делает все прямоугольники подсказок невидимыми, очищая оверлей.
|
|
||||||
/// </remarks>
|
|
||||||
public void HideHint()
|
|
||||||
{
|
|
||||||
foreach (var rect in _hintRectangles.Values)
|
|
||||||
{
|
|
||||||
rect.Visibility = Visibility.Collapsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,8 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:controls="using:Lattice.UI"
|
xmlns:controls="using:Lattice.UI"
|
||||||
xmlns:conv="using:Lattice.UI.Docking.WinUI.Converters"
|
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">
|
||||||
|
|
||||||
<!-- 1. Шаблоны -->
|
<!-- 1. Шаблоны -->
|
||||||
<DataTemplate x:Key="LatticeGroupTemplate">
|
<DataTemplate x:Key="LatticeGroupTemplate">
|
||||||
@@ -20,55 +21,73 @@
|
|||||||
GroupTemplate="{StaticResource LatticeGroupTemplate}"
|
GroupTemplate="{StaticResource LatticeGroupTemplate}"
|
||||||
LeafTemplate="{StaticResource LatticeLeafTemplate}" />
|
LeafTemplate="{StaticResource LatticeLeafTemplate}" />
|
||||||
|
|
||||||
<!-- 3. Стиль Сплиттера -->
|
<!-- 3. Стиль Splitter -->
|
||||||
<Style TargetType="controls:LatticeSplitter">
|
<Style TargetType="controls:LatticeSplitter">
|
||||||
<Setter Property="Background" Value="{ThemeResource Lattice.Brush.Splitter.Normal}"/>
|
<Setter Property="Background" Value="{ThemeResource Lattice.Brush.Splitter.Normal}"/>
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="controls:LatticeSplitter">
|
<ControlTemplate TargetType="controls:LatticeSplitter">
|
||||||
<Grid Background="Transparent">
|
<Grid Background="Transparent">
|
||||||
<Rectangle Fill="{TemplateBinding Background}"
|
<Rectangle x:Name="SplitterRect"
|
||||||
Width="{ThemeResource Lattice.Size.SplitterWidth}"
|
Fill="{TemplateBinding Background}"
|
||||||
|
Width="{ThemeResource Lattice.Size.Splitter.Width}"
|
||||||
HorizontalAlignment="Center"/>
|
HorizontalAlignment="Center"/>
|
||||||
|
<VisualStateManager.VisualStateGroups>
|
||||||
|
<VisualStateGroup x:Name="CommonStates">
|
||||||
|
<VisualState x:Name="Normal"/>
|
||||||
|
<VisualState x:Name="PointerOver">
|
||||||
|
<Storyboard>
|
||||||
|
<ColorAnimation Storyboard.TargetName="SplitterRect"
|
||||||
|
Storyboard.TargetProperty="(Rectangle.Fill).(SolidColorBrush.Color)"
|
||||||
|
To="{ThemeResource Lattice.Color.Accent.Action}"
|
||||||
|
Duration="0:0:0.2"/>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
</VisualStateManager.VisualStateGroups>
|
||||||
</Grid>
|
</Grid>
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
</Setter.Value>
|
</Setter.Value>
|
||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- 4. Стиль Хоста -->
|
<!-- 4. Стиль Host -->
|
||||||
<Style TargetType="controls:LatticeDockHost">
|
<Style TargetType="controls:LatticeDockHost">
|
||||||
|
<Setter Property="Background" Value="{ThemeResource Lattice.Brush.Background.Primary}"/>
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="controls:LatticeDockHost">
|
<ControlTemplate TargetType="controls:LatticeDockHost">
|
||||||
<ContentControl
|
<Border Background="{TemplateBinding Background}"
|
||||||
Content="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Manager.Root}"
|
Padding="4">
|
||||||
ContentTemplateSelector="{StaticResource GlobalDockSelector}"
|
<ContentControl x:Name="PART_RootContainer"
|
||||||
HorizontalContentAlignment="Stretch"
|
HorizontalContentAlignment="Stretch"
|
||||||
VerticalContentAlignment="Stretch" />
|
VerticalContentAlignment="Stretch" />
|
||||||
|
</Border>
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
</Setter.Value>
|
</Setter.Value>
|
||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- 5. Стиль Группы (Рекурсия) -->
|
<!-- 5. Стиль Group -->
|
||||||
<Style TargetType="controls:LatticeDockGroup">
|
<Style TargetType="controls:LatticeDockGroup">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="controls:LatticeDockGroup">
|
<ControlTemplate TargetType="controls:LatticeDockGroup">
|
||||||
<!-- Grid перестраивается в коде LatticeDockGroup.cs -->
|
<Grid x:Name="PART_Grid" Background="{TemplateBinding Background}">
|
||||||
<Grid x:Name="PART_Grid">
|
<!-- First child area -->
|
||||||
<!-- Первая область -->
|
|
||||||
<ContentControl x:Name="PART_First"
|
<ContentControl x:Name="PART_First"
|
||||||
Content="{Binding First}"
|
Content="{Binding First}"
|
||||||
ContentTemplateSelector="{StaticResource GlobalDockSelector}"
|
ContentTemplateSelector="{StaticResource GlobalDockSelector}"
|
||||||
HorizontalContentAlignment="Stretch"
|
HorizontalContentAlignment="Stretch"
|
||||||
VerticalContentAlignment="Stretch" />
|
VerticalContentAlignment="Stretch" />
|
||||||
|
|
||||||
<!-- Сплиттер (его положение в Grid.Row/Column устанавливается автоматически при перестроении Grid) -->
|
<!-- Splitter -->
|
||||||
<controls:LatticeSplitter x:Name="PART_Splitter" />
|
<controls:LatticeSplitter x:Name="PART_Splitter"
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.Row="0"/>
|
||||||
|
|
||||||
<!-- Вторая область -->
|
<!-- Second child area -->
|
||||||
<ContentControl x:Name="PART_Second"
|
<ContentControl x:Name="PART_Second"
|
||||||
Content="{Binding Second}"
|
Content="{Binding Second}"
|
||||||
ContentTemplateSelector="{StaticResource GlobalDockSelector}"
|
ContentTemplateSelector="{StaticResource GlobalDockSelector}"
|
||||||
@@ -80,58 +99,16 @@
|
|||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- 6. Стиль Листа -->
|
<!-- 6. Стиль TabControl -->
|
||||||
<Style TargetType="controls:LatticeDockLeaf">
|
<Style TargetType="controls:LatticeDockLeaf">
|
||||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
<Setter Property="Background" Value="{ThemeResource Lattice.Brush.Background.Primary}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{ThemeResource Lattice.Brush.Panel.Border}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="{ThemeResource Lattice.BorderThickness.Panel}"/>
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="controls:LatticeDockLeaf">
|
<ControlTemplate TargetType="controls:LatticeDockLeaf">
|
||||||
<!-- Grid и Border должны растягиваться -->
|
<Grid x:Name="PART_RootGrid" Background="{TemplateBinding Background}">
|
||||||
<Grid Margin="{ThemeResource Lattice.Thickness.PanelMargin}" VerticalAlignment="Stretch">
|
<!-- Tab headers -->
|
||||||
<Border Background="{ThemeResource Lattice.Brush.Background.Primary}"
|
|
||||||
BorderBrush="{ThemeResource Lattice.Brush.Panel.Border}"
|
|
||||||
BorderThickness="{ThemeResource Lattice.Thickness.PanelBorder}"
|
|
||||||
CornerRadius="{ThemeResource Lattice.Geometry.PanelCornerRadius}"
|
|
||||||
VerticalAlignment="Stretch">
|
|
||||||
|
|
||||||
<!-- Используем кастомный TabControl или оставляем стандартный -->
|
|
||||||
<TabView x:Name="PART_TabView"
|
|
||||||
TabItemsSource="{Binding Children}"
|
|
||||||
SelectedItem="{Binding ActiveContent, Mode=TwoWay}"
|
|
||||||
IsAddTabButtonVisible="False"
|
|
||||||
VerticalAlignment="Stretch"
|
|
||||||
VerticalContentAlignment="Stretch"
|
|
||||||
TabWidthMode="SizeToContent"
|
|
||||||
Padding="0">
|
|
||||||
|
|
||||||
<TabView.TabItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<TabViewItem Header="{Binding Title}" FontSize="11" Height="28" MinWidth="0" >
|
|
||||||
<!-- ContentPresenter ДОЛЖЕН иметь VerticalAlignment="Stretch" -->
|
|
||||||
<ContentPresenter Content="{Binding View}"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Stretch" />
|
|
||||||
</TabViewItem>
|
|
||||||
</DataTemplate>
|
|
||||||
</TabView.TabItemTemplate>
|
|
||||||
</TabView>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- Добавить в Generic.xaml -->
|
|
||||||
<Style TargetType="controls:LatticeTabControl">
|
|
||||||
<Setter Property="Background" Value="{ThemeResource Lattice.Brush.Background.Primary}"/>
|
|
||||||
<Setter Property="BorderBrush" Value="{ThemeResource Lattice.Brush.Panel.Border}"/>
|
|
||||||
<Setter Property="BorderThickness" Value="{ThemeResource Lattice.Thickness.PanelBorder}"/>
|
|
||||||
<Setter Property="Template">
|
|
||||||
<Setter.Value>
|
|
||||||
<ControlTemplate TargetType="controls:LatticeTabControl">
|
|
||||||
<Grid x:Name="PART_RootGrid">
|
|
||||||
<!-- Заголовки вкладок -->
|
|
||||||
<ListBox x:Name="PART_TabHeaderList"
|
<ListBox x:Name="PART_TabHeaderList"
|
||||||
Background="{TemplateBinding Background}"
|
Background="{TemplateBinding Background}"
|
||||||
BorderBrush="{TemplateBinding BorderBrush}"
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
@@ -142,18 +119,46 @@
|
|||||||
<Style TargetType="ListBoxItem">
|
<Style TargetType="ListBoxItem">
|
||||||
<Setter Property="Background" Value="Transparent"/>
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
<Setter Property="BorderBrush" Value="{ThemeResource Lattice.Brush.Accent.Action}"/>
|
<Setter Property="BorderBrush" Value="{ThemeResource Lattice.Brush.Accent.Action}"/>
|
||||||
<Setter Property="BorderThickness" Value="0,0,0,2"/>
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
<Setter Property="Margin" Value="0,0,4,0"/>
|
<Setter Property="Margin" Value="0,0,4,0"/>
|
||||||
<Setter Property="Padding" Value="0"/>
|
<Setter Property="Padding" Value="0"/>
|
||||||
<Style.Triggers>
|
<!-- Используем VisualStateManager вместо триггеров -->
|
||||||
<Trigger Property="IsSelected" Value="True">
|
<Setter Property="Template">
|
||||||
<Setter Property="BorderThickness" Value="0,0,0,2"/>
|
<Setter.Value>
|
||||||
<Setter Property="Foreground" Value="{ThemeResource Lattice.Brush.Accent.Action}"/>
|
<ControlTemplate TargetType="ListBoxItem">
|
||||||
</Trigger>
|
<Grid>
|
||||||
<Trigger Property="IsPointerOver" Value="True">
|
<VisualStateManager.VisualStateGroups>
|
||||||
<Setter Property="Background" Value="{ThemeResource SystemControlBackgroundListLowBrush}"/>
|
<VisualStateGroup x:Name="CommonStates">
|
||||||
</Trigger>
|
<VisualState x:Name="Normal"/>
|
||||||
</Style.Triggers>
|
<VisualState x:Name="Selected">
|
||||||
|
<VisualState.Setters>
|
||||||
|
<Setter Target="Border.BorderThickness" Value="0,0,0,2"/>
|
||||||
|
<Setter Target="ContentPresenter.Foreground" Value="{ThemeResource Lattice.Brush.Accent.Action}"/>
|
||||||
|
</VisualState.Setters>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="PointerOver">
|
||||||
|
<VisualState.Setters>
|
||||||
|
<Setter Target="Border.Background" Value="{ThemeResource Lattice.Brush.Background.Secondary}"/>
|
||||||
|
</VisualState.Setters>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
</VisualStateManager.VisualStateGroups>
|
||||||
|
<Border x:Name="Border"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
Margin="{TemplateBinding Margin}">
|
||||||
|
<ContentPresenter x:Name="ContentPresenter"
|
||||||
|
Content="{TemplateBinding Content}"
|
||||||
|
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||||
|
Padding="{TemplateBinding Padding}"
|
||||||
|
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||||
|
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
</ListBox.ItemContainerStyle>
|
</ListBox.ItemContainerStyle>
|
||||||
<ListBox.ItemsPanel>
|
<ListBox.ItemsPanel>
|
||||||
@@ -163,7 +168,7 @@
|
|||||||
</ListBox.ItemsPanel>
|
</ListBox.ItemsPanel>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
|
|
||||||
<!-- Контент вкладки -->
|
<!-- Tab content -->
|
||||||
<ContentControl x:Name="PART_ContentControl"
|
<ContentControl x:Name="PART_ContentControl"
|
||||||
Background="{TemplateBinding Background}"
|
Background="{TemplateBinding Background}"
|
||||||
HorizontalContentAlignment="Stretch"
|
HorizontalContentAlignment="Stretch"
|
||||||
@@ -174,21 +179,28 @@
|
|||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- 7. Ресурсы по умолчанию (если тема не загружена) -->
|
<!-- 7. Ресурсы по умолчанию -->
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<!-- Значения по умолчанию -->
|
<!-- Цвета по умолчанию -->
|
||||||
<SolidColorBrush x:Key="Lattice.Brush.Background.Primary" Color="#1E1E1E" />
|
<SolidColorBrush x:Key="Lattice.Brush.Background.Primary" Color="#1E1E1E" />
|
||||||
<SolidColorBrush x:Key="Lattice.Brush.Background.Secondary" Color="#252526" />
|
<SolidColorBrush x:Key="Lattice.Brush.Background.Secondary" Color="#252526" />
|
||||||
<SolidColorBrush x:Key="Lattice.Brush.Panel.Border" Color="#3F3F46" />
|
<SolidColorBrush x:Key="Lattice.Brush.Panel.Border" Color="#3F3F46" />
|
||||||
<SolidColorBrush x:Key="Lattice.Brush.Splitter.Normal" Color="#2D2D2D" />
|
<SolidColorBrush x:Key="Lattice.Brush.Splitter.Normal" Color="#2D2D2D" />
|
||||||
<SolidColorBrush x:Key="Lattice.Brush.Splitter.Hover" Color="#007ACC" />
|
|
||||||
<SolidColorBrush x:Key="Lattice.Brush.Accent.Action" Color="#007ACC" />
|
<SolidColorBrush x:Key="Lattice.Brush.Accent.Action" Color="#007ACC" />
|
||||||
|
<SolidColorBrush x:Key="Lattice.Brush.Accent" Color="#007ACC" />
|
||||||
|
<SolidColorBrush x:Key="Lattice.Brush.Text.Primary" Color="#FFFFFF" />
|
||||||
|
|
||||||
<CornerRadius x:Key="Lattice.Geometry.PanelCornerRadius">0</CornerRadius>
|
<!-- Геометрия -->
|
||||||
<Thickness x:Key="Lattice.Thickness.PanelMargin">0,0,1,1</Thickness>
|
<CornerRadius x:Key="Lattice.CornerRadius.Panel">4</CornerRadius>
|
||||||
<Thickness x:Key="Lattice.Thickness.PanelBorder">1</Thickness>
|
<x:Double x:Key="Lattice.Size.Splitter.Width">6</x:Double>
|
||||||
<x:Double x:Key="Lattice.Size.SplitterWidth">1</x:Double>
|
|
||||||
|
<!-- Толщины -->
|
||||||
|
<Thickness x:Key="Lattice.BorderThickness.Panel">1</Thickness>
|
||||||
|
<Thickness x:Key="Lattice.Thickness.PanelMargin">2</Thickness>
|
||||||
|
|
||||||
|
<!-- Отступы -->
|
||||||
|
<x:Double x:Key="Lattice.Spacing.Panel">8</x:Double>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Lattice.Core.Docking.Abstractions;
|
using Lattice.Core.Docking.Abstractions;
|
||||||
using Lattice.Core.Docking.Engine;
|
using Lattice.Core.Docking.Engine;
|
||||||
|
using Lattice.Core.Docking.Models;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
|
||||||
namespace Lattice.UI.Docking.Abstractions;
|
namespace Lattice.UI.Docking.Abstractions;
|
||||||
@@ -78,6 +79,26 @@ public interface IDockControl : INotifyPropertyChanged
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
bool IsActive { get; set; }
|
bool IsActive { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает признак того, что элемент можно перетаскивать.
|
||||||
|
/// </summary>
|
||||||
|
bool CanDrag { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получает признак того, что на элемент можно сбрасывать.
|
||||||
|
/// </summary>
|
||||||
|
bool CanDrop { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Подготавливает данные для перетаскивания.
|
||||||
|
/// </summary>
|
||||||
|
object? PrepareDragData();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Обрабатывает сброс данных.
|
||||||
|
/// </summary>
|
||||||
|
bool HandleDrop(object data, DockPosition position);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Обновляет внешний вид контрола в соответствии с текущим состоянием модели.
|
/// Обновляет внешний вид контрола в соответствии с текущим состоянием модели.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Базовая реализация контрола док-системы.
|
|
||||||
/// </summary>
|
|
||||||
public abstract class DockControlBase : IDockControl, INotifyPropertyChanged
|
|
||||||
{
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
|
||||||
|
|
||||||
private IDockElement? _model;
|
|
||||||
private LayoutManager? _layoutManager;
|
|
||||||
private IDockContextManager? _contextManager;
|
|
||||||
private bool _isSelected;
|
|
||||||
private bool _isActive;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public IDockElement? Model
|
|
||||||
{
|
|
||||||
get => _model;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_model != value)
|
|
||||||
{
|
|
||||||
_model = value;
|
|
||||||
OnPropertyChanged(nameof(Model));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public LayoutManager? LayoutManager
|
|
||||||
{
|
|
||||||
get => _layoutManager;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_layoutManager != value)
|
|
||||||
{
|
|
||||||
_layoutManager = value;
|
|
||||||
OnPropertyChanged(nameof(LayoutManager));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public IDockContextManager? ContextManager
|
|
||||||
{
|
|
||||||
get => _contextManager;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_contextManager != value)
|
|
||||||
{
|
|
||||||
_contextManager = value;
|
|
||||||
OnPropertyChanged(nameof(ContextManager));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public bool IsSelected
|
|
||||||
{
|
|
||||||
get => _isSelected;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_isSelected != value)
|
|
||||||
{
|
|
||||||
_isSelected = value;
|
|
||||||
OnPropertyChanged(nameof(IsSelected));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public bool IsActive
|
|
||||||
{
|
|
||||||
get => _isActive;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_isActive != value)
|
|
||||||
{
|
|
||||||
_isActive = value;
|
|
||||||
OnPropertyChanged(nameof(IsActive));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public abstract void Refresh();
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public abstract void ApplyTheme(IDockTheme theme);
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public virtual void OnModelPropertyChanged(string propertyName)
|
|
||||||
{
|
|
||||||
// Базовая реализация просто обновляет весь контрол
|
|
||||||
Refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Вызывает событие <see cref="PropertyChanged"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="propertyName">Имя измененного свойства.</param>
|
|
||||||
protected virtual void OnPropertyChanged(string propertyName)
|
|
||||||
{
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,15 +7,6 @@ namespace Lattice.UI.Docking.Services;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class DockContextManagerBase : IDockContextManager
|
public abstract class DockContextManagerBase : IDockContextManager
|
||||||
{
|
{
|
||||||
private readonly Dictionary<string, IDockCommand> _commands = new();
|
|
||||||
private IDockControl? _currentContextTarget;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event EventHandler<ContextMenuShownEventArgs>? ContextMenuShown;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event EventHandler? ContextMenuHidden;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public abstract void ShowContextMenu(IDockControl element, double x, double y);
|
public abstract void ShowContextMenu(IDockControl element, double x, double y);
|
||||||
|
|
||||||
@@ -25,25 +16,22 @@ public abstract class DockContextManagerBase : IDockContextManager
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual void RegisterCommand(string commandId, IDockCommand command)
|
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual void UnregisterCommand(string commandId)
|
public virtual void UnregisterCommand(string commandId)
|
||||||
{
|
{
|
||||||
_commands.Remove(commandId);
|
// Базовая реализация, должна быть переопределена в производных классах
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получает команду по идентификатору.
|
/// Получает команду по идентификатору.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IDockCommand? GetCommand(string commandId)
|
protected virtual IDockCommand? GetCommand(string commandId)
|
||||||
{
|
{
|
||||||
_commands.TryGetValue(commandId, out var command);
|
// Базовая реализация, должна быть переопределена в производных классах
|
||||||
return command;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -52,7 +40,7 @@ public abstract class DockContextManagerBase : IDockContextManager
|
|||||||
protected virtual IEnumerable<IDockCommand> GetCommandsForElement(IDockControl element)
|
protected virtual IEnumerable<IDockCommand> GetCommandsForElement(IDockControl element)
|
||||||
{
|
{
|
||||||
// Фильтрация команд по типу элемента и его состоянию
|
// Фильтрация команд по типу элемента и его состоянию
|
||||||
return _commands.Values.Where(c => CanExecuteCommand(c, element));
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -76,7 +64,6 @@ public abstract class DockContextManagerBase : IDockContextManager
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual void OnContextMenuShown(IDockControl target, double x, double y)
|
protected virtual void OnContextMenuShown(IDockControl target, double x, double y)
|
||||||
{
|
{
|
||||||
_currentContextTarget = target;
|
|
||||||
ContextMenuShown?.Invoke(this, new ContextMenuShownEventArgs(target, x, y));
|
ContextMenuShown?.Invoke(this, new ContextMenuShownEventArgs(target, x, y));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,12 +72,12 @@ public abstract class DockContextManagerBase : IDockContextManager
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual void OnContextMenuHidden()
|
protected virtual void OnContextMenuHidden()
|
||||||
{
|
{
|
||||||
_currentContextTarget = null;
|
|
||||||
ContextMenuHidden?.Invoke(this, EventArgs.Empty);
|
ContextMenuHidden?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Получает текущий целевой элемент контекстного меню.
|
public event EventHandler<ContextMenuShownEventArgs>? ContextMenuShown;
|
||||||
/// </summary>
|
|
||||||
protected IDockControl? CurrentContextTarget => _currentContextTarget;
|
/// <inheritdoc/>
|
||||||
|
public event EventHandler? ContextMenuHidden;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user