Files
Lattice/Lattice.Themes.Core/ThemeManager.cs
2026-02-01 09:26:13 +03:00

334 lines
13 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Lattice.Themes.Core;
using Microsoft.UI.Xaml;
namespace Lattice.Themes;
/// <summary>
/// Менеджер тем для Lattice Framework. Управляет регистрацией, применением и переключением тем оформления.
/// Предоставляет доступ к токенам темы и поддерживает динамическое обновление UI при смене темы.
/// </summary>
public sealed class ThemeManager
{
private static readonly ThemeManager _instance = new();
/// <summary>
/// Получает текущий экземпляр менеджера тем (синглтон).
/// </summary>
public static ThemeManager Current => _instance;
private ThemePack? _currentTheme;
private readonly Dictionary<string, ThemePack> _registeredThemes = new();
/// <summary>
/// Получает текущую активную тему.
/// </summary>
public ThemePack? CurrentTheme => _currentTheme;
/// <summary>
/// Происходит при изменении текущей темы.
/// </summary>
public event EventHandler<ThemeChangedEventArgs>? ThemeChanged;
private ThemeManager() { }
/// <summary>
/// Регистрирует тему в менеджере.
/// </summary>
/// <param name="theme">Тема для регистрации.</param>
/// <exception cref="ArgumentNullException">Выбрасывается, если <paramref name="theme"/> равен null.</exception>
public void RegisterTheme(ThemePack theme)
{
if (theme == null)
throw new ArgumentNullException(nameof(theme));
_registeredThemes[theme.Name] = theme;
}
/// <summary>
/// Получает зарегистрированную тему по имени.
/// </summary>
/// <param name="name">Имя темы.</param>
/// <returns>Зарегистрированная тема или null, если тема не найдена.</returns>
public ThemePack? GetTheme(string name)
{
_registeredThemes.TryGetValue(name, out var theme);
return theme;
}
/// <summary>
/// Получает список всех зарегистрированных тем.
/// </summary>
/// <returns>Неизменяемая коллекция зарегистрированных тем.</returns>
public IReadOnlyCollection<ThemePack> GetRegisteredThemes()
{
return _registeredThemes.Values.ToList();
}
/// <summary>
/// Получает информацию о зарегистрированной теме.
/// </summary>
/// <param name="themeName">Имя темы.</param>
/// <returns>Информация о теме или null, если тема не зарегистрирована.</returns>
public ThemeInfo? GetThemeInfo(string themeName)
{
if (!_registeredThemes.TryGetValue(themeName, out var theme))
return null;
return new ThemeInfo
{
Name = theme.Name,
Description = theme.Description,
Version = theme.Version,
IsDark = theme.IsDark,
TokenCount = CountTokensInTheme(theme)
};
}
/// <summary>
/// Применяет тему по имени.
/// </summary>
/// <param name="themeName">Имя темы для применения.</param>
/// <exception cref="ArgumentException">Выбрасывается, если тема с указанным именем не зарегистрирована.</exception>
/// <exception cref="InvalidOperationException">Выбрасывается, если не удалось применить тему.</exception>
public void ApplyTheme(string themeName)
{
if (!_registeredThemes.TryGetValue(themeName, out var theme))
{
throw new ArgumentException($"Theme '{themeName}' is not registered.", nameof(themeName));
}
ApplyTheme(theme);
}
/// <summary>
/// Применяет указанную тему.
/// </summary>
/// <param name="theme">Тема для применения.</param>
/// <exception cref="ArgumentNullException">Выбрасывается, если <paramref name="theme"/> равен null.</exception>
/// <exception cref="InvalidOperationException">Выбрасывается, если не удалось применить тему.</exception>
public void ApplyTheme(ThemePack theme)
{
if (theme == null)
throw new ArgumentNullException(nameof(theme));
if (_currentTheme == theme)
return;
var oldTheme = _currentTheme;
_currentTheme = theme;
try
{
ReplaceApplicationResources(theme);
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs(oldTheme!, theme));
}
catch (Exception ex)
{
// Восстанавливаем предыдущую тему при ошибке
_currentTheme = oldTheme;
if (oldTheme != null)
{
ReplaceApplicationResources(oldTheme);
}
throw new InvalidOperationException($"Failed to apply theme '{theme.Name}'.", ex);
}
}
/// <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)
{
if (targetDictionary == null)
throw new ArgumentNullException(nameof(targetDictionary));
if (theme == null)
throw new ArgumentNullException(nameof(theme));
// Удаляем все ThemeDictionary из словаря
for (int i = targetDictionary.MergedDictionaries.Count - 1; i >= 0; i--)
{
if (targetDictionary.MergedDictionaries[i] is ThemeDictionary)
targetDictionary.MergedDictionaries.RemoveAt(i);
}
// Добавляем словари темы
foreach (var uri in theme.GetResourceUris())
{
try
{
var themeDict = new ThemeDictionary { Source = uri };
targetDictionary.MergedDictionaries.Add(themeDict);
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to load theme resource from '{uri}'.", ex);
}
}
}
/// <summary>
/// Подсчитывает количество токенов в теме.
/// </summary>
/// <param name="theme">Тема для подсчета токенов.</param>
/// <returns>Количество токенов в теме. Возвращает 0 при возникновении ошибки.</returns>
private int CountTokensInTheme(ThemePack theme)
{
try
{
var dict = new ResourceDictionary();
LoadThemeIntoDictionary(dict, theme);
return dict.Count;
}
catch
{
return 0;
}
}
/// <summary>
/// Заменяет ресурсы приложения на ресурсы указанной темы.
/// </summary>
/// <param name="theme">Тема, ресурсы которой нужно применить.</param>
private void ReplaceApplicationResources(ThemePack theme)
{
var app = Application.Current;
if (app == null)
return;
var root = app.Resources;
LoadThemeIntoDictionary(root, theme);
ForceUpdateUI();
}
/// <summary>
/// Принудительно обновляет пользовательский интерфейс после смены темы.
/// Использует легковесный подход без рекурсивного обхода дерева элементов.
/// </summary>
private void ForceUpdateUI()
{
foreach (var window in WindowTracker.Windows)
{
if (window.Content is FrameworkElement root)
{
// Перезагружаем ресурсы корневого элемента
var resources = root.Resources;
var currentTheme = _currentTheme;
if (currentTheme != null)
{
LoadThemeIntoDictionary(resources, currentTheme);
}
// Принудительное обновление стилей через перезагрузку ResourceDictionary
var mergedDictionaries = resources.MergedDictionaries;
if (mergedDictionaries.Count > 0)
{
var temp = mergedDictionaries[mergedDictionaries.Count - 1];
mergedDictionaries.RemoveAt(mergedDictionaries.Count - 1);
mergedDictionaries.Add(temp);
}
}
}
}
/// <summary>
/// Проверяет, что все необходимые токены определены в текущей теме.
/// </summary>
/// <returns>true, если все токены присутствуют; иначе false.</returns>
public bool ValidateThemeTokens()
{
if (_currentTheme == null)
return false;
var app = Application.Current;
if (app == null)
return false;
var requiredTokens = LatticeTokens.GetAllTokens().Values;
var missingTokens = new List<string>();
foreach (var token in requiredTokens)
{
if (!app.Resources.ContainsKey(token))
{
missingTokens.Add(token);
}
}
if (missingTokens.Any())
{
System.Diagnostics.Debug.WriteLine($"Missing theme tokens: {string.Join(", ", missingTokens)}");
return false;
}
return true;
}
/// <summary>
/// Получает значение токена из текущей темы.
/// </summary>
/// <param name="tokenKey">Ключ токена.</param>
/// <returns>Значение токена или null, если токен не найден или приложение не инициализировано.</returns>
public object? GetTokenValue(string tokenKey)
{
var app = Application.Current;
if (app == null)
return null;
if (app.Resources.TryGetValue(tokenKey, out var value))
{
return value;
}
return null;
}
/// <summary>
/// Получает значение токена с приведением к указанному типу.
/// </summary>
/// <typeparam name="T">Тип, к которому приводится значение токена.</typeparam>
/// <param name="tokenKey">Ключ токена.</param>
/// <returns>Значение токена или значение по умолчанию для типа T, если токен не найден.</returns>
public T? GetTokenValue<T>(string tokenKey)
{
object? value = GetTokenValue(tokenKey);
if (value is T typedValue)
return typedValue;
return default;
}
}
/// <summary>
/// Предоставляет информацию о теме оформления.
/// </summary>
public class ThemeInfo
{
/// <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>
public bool IsDark { get; set; }
/// <summary>
/// Получает или задает количество токенов в теме.
/// </summary>
public int TokenCount { get; set; }
}