Files
Lattice/Lattice.Themes.Core/ThemeManager.cs
2026-01-18 16:33:35 +03:00

293 lines
8.2 KiB
C#
Raw 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.Tokens;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
namespace Lattice.Themes;
/// <summary>
/// Менеджер тем для Lattice Framework.
/// </summary>
public sealed class ThemeManager
{
public static ThemeManager Current { get; } = new();
private ThemePack? _currentTheme;
private readonly Dictionary<string, ThemePack> _registeredThemes = new();
public ThemePack? CurrentTheme => _currentTheme;
public event EventHandler<ThemeChangedEventArgs>? ThemeChanged;
private ThemeManager() { }
/// <summary>
/// Регистрирует тему в менеджере.
/// </summary>
public void RegisterTheme(ThemePack theme)
{
if (theme == null)
throw new ArgumentNullException(nameof(theme));
_registeredThemes[theme.Name] = theme;
}
/// <summary>
/// Получает зарегистрированную тему по имени.
/// </summary>
public ThemePack? GetTheme(string name)
{
_registeredThemes.TryGetValue(name, out var theme);
return theme;
}
/// <summary>
/// Получает список всех зарегистрированных тем.
/// </summary>
public IReadOnlyCollection<ThemePack> GetRegisteredThemes()
{
return _registeredThemes.Values.ToList();
}
/// <summary>
/// Получение информации о теме.
/// </summary>
/// <param name="themeName"></param>
/// <returns></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>
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>
public void ApplyTheme(ThemePack theme)
{
if (theme == null)
throw new ArgumentNullException(nameof(theme));
if (_currentTheme == theme)
return;
var old = _currentTheme;
_currentTheme = theme;
try
{
ReplaceApplicationResources(theme);
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs(old!, theme));
}
catch (Exception ex)
{
// В случае ошибки возвращаемся к старой теме
_currentTheme = old;
if (old != null)
{
ReplaceApplicationResources(old);
}
throw new InvalidOperationException($"Failed to apply theme '{theme.Name}'.", ex);
}
}
/// <summary>
/// Загружает ресурсы темы в указанный словарь ресурсов.
/// </summary>
public void LoadThemeIntoDictionary(ResourceDictionary targetDictionary, ThemePack theme)
{
if (targetDictionary == null)
throw new ArgumentNullException(nameof(targetDictionary));
if (theme == null)
throw new ArgumentNullException(nameof(theme));
// Очищаем старые словари Lattice
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);
}
}
}
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)
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;
control.Template = template;
}
else if (current is ContentPresenter contentPresenter)
{
// Обновляем ContentPresenter
var content = contentPresenter.Content;
contentPresenter.Content = null;
contentPresenter.Content = content;
}
// Добавляем детей в стек
int count = VisualTreeHelper.GetChildrenCount(current);
for (int i = 0; i < count; i++)
{
if (VisualTreeHelper.GetChild(current, i) is FrameworkElement child)
stack.Push(child);
}
}
}
/// <summary>
/// Проверяет, что все необходимые токены определены в текущей теме.
/// </summary>
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>
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>
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; }
public string Description { get; set; }
public string Version { get; set; }
public bool IsDark { get; set; }
public int TokenCount { get; set; }
}