using Lattice.Themes.Core;
using Microsoft.UI.Xaml;
namespace Lattice.Themes;
///
/// Менеджер тем для Lattice Framework. Управляет регистрацией, применением и переключением тем оформления.
/// Предоставляет доступ к токенам темы и поддерживает динамическое обновление UI при смене темы.
///
public sealed class ThemeManager
{
private static readonly ThemeManager _instance = new();
///
/// Получает текущий экземпляр менеджера тем (синглтон).
///
public static ThemeManager Current => _instance;
private ThemePack? _currentTheme;
private readonly Dictionary _registeredThemes = new();
///
/// Получает текущую активную тему.
///
public ThemePack? CurrentTheme => _currentTheme;
///
/// Происходит при изменении текущей темы.
///
public event EventHandler? ThemeChanged;
private ThemeManager() { }
///
/// Регистрирует тему в менеджере.
///
/// Тема для регистрации.
/// Выбрасывается, если равен null.
public void RegisterTheme(ThemePack theme)
{
if (theme == null)
throw new ArgumentNullException(nameof(theme));
_registeredThemes[theme.Name] = theme;
}
///
/// Получает зарегистрированную тему по имени.
///
/// Имя темы.
/// Зарегистрированная тема или null, если тема не найдена.
public ThemePack? GetTheme(string name)
{
_registeredThemes.TryGetValue(name, out var theme);
return theme;
}
///
/// Получает список всех зарегистрированных тем.
///
/// Неизменяемая коллекция зарегистрированных тем.
public IReadOnlyCollection GetRegisteredThemes()
{
return _registeredThemes.Values.ToList();
}
///
/// Получает информацию о зарегистрированной теме.
///
/// Имя темы.
/// Информация о теме или null, если тема не зарегистрирована.
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)
};
}
///
/// Применяет тему по имени.
///
/// Имя темы для применения.
/// Выбрасывается, если тема с указанным именем не зарегистрирована.
/// Выбрасывается, если не удалось применить тему.
public void ApplyTheme(string themeName)
{
if (!_registeredThemes.TryGetValue(themeName, out var theme))
{
throw new ArgumentException($"Theme '{themeName}' is not registered.", nameof(themeName));
}
ApplyTheme(theme);
}
///
/// Применяет указанную тему.
///
/// Тема для применения.
/// Выбрасывается, если равен null.
/// Выбрасывается, если не удалось применить тему.
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);
}
}
///
/// Загружает ресурсы темы в указанный словарь ресурсов.
///
/// Целевой словарь ресурсов.
/// Тема, ресурсы которой нужно загрузить.
/// Выбрасывается, если или равны null.
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);
}
}
}
///
/// Подсчитывает количество токенов в теме.
///
/// Тема для подсчета токенов.
/// Количество токенов в теме. Возвращает 0 при возникновении ошибки.
private int CountTokensInTheme(ThemePack theme)
{
try
{
var dict = new ResourceDictionary();
LoadThemeIntoDictionary(dict, theme);
return dict.Count;
}
catch
{
return 0;
}
}
///
/// Заменяет ресурсы приложения на ресурсы указанной темы.
///
/// Тема, ресурсы которой нужно применить.
private void ReplaceApplicationResources(ThemePack theme)
{
var app = Application.Current;
if (app == null)
return;
var root = app.Resources;
LoadThemeIntoDictionary(root, theme);
ForceUpdateUI();
}
///
/// Принудительно обновляет пользовательский интерфейс после смены темы.
/// Использует легковесный подход без рекурсивного обхода дерева элементов.
///
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);
}
}
}
}
///
/// Проверяет, что все необходимые токены определены в текущей теме.
///
/// true, если все токены присутствуют; иначе false.
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();
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;
}
///
/// Получает значение токена из текущей темы.
///
/// Ключ токена.
/// Значение токена или null, если токен не найден или приложение не инициализировано.
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;
}
///
/// Получает значение токена с приведением к указанному типу.
///
/// Тип, к которому приводится значение токена.
/// Ключ токена.
/// Значение токена или значение по умолчанию для типа T, если токен не найден.
public T? GetTokenValue(string tokenKey)
{
object? value = GetTokenValue(tokenKey);
if (value is T typedValue)
return typedValue;
return default;
}
}
///
/// Предоставляет информацию о теме оформления.
///
public class ThemeInfo
{
///
/// Получает или задает название темы.
///
public string Name { get; set; } = string.Empty;
///
/// Получает или задает описание темы.
///
public string Description { get; set; } = string.Empty;
///
/// Получает или задает версию темы.
///
public string Version { get; set; } = string.Empty;
///
/// Получает или задает значение, указывающее, является ли тема темной.
///
public bool IsDark { get; set; }
///
/// Получает или задает количество токенов в теме.
///
public int TokenCount { get; set; }
}