334 lines
13 KiB
C#
334 lines
13 KiB
C#
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; }
|
||
} |