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