using System; using System.Collections.Generic; using System.Linq; using System.Windows; using System.IO; using System.Windows.Media; using ModAssistant.Pages; using System.Xml; using System.Windows.Markup; namespace ModAssistant { public class Themes { public static string LoadedTheme { get; private set; } public static List LoadedThemes { get => loadedThemes.Keys.ToList(); } public static string ThemeDirectory => $"{Environment.CurrentDirectory}/Themes"; private static Dictionary loadedThemes = new Dictionary(); private static List preInstalledThemes = new List { "Light", "Dark" }; /// /// Load all themes from local Themes subfolder and from embedded resources. /// This also refreshes the Themes dropdown in the Options screen. /// public static void LoadThemes() { loadedThemes.Clear(); foreach (string localTheme in preInstalledThemes) //Load local themes (Light and Dark) { ResourceDictionary theme = LoadTheme(localTheme, true); loadedThemes.Add(localTheme, theme); } if (Directory.Exists(ThemeDirectory)) //Load themes from Themes subfolder if it exists. { foreach (string file in Directory.EnumerateFiles(ThemeDirectory)) { FileInfo info = new FileInfo(file); //FileInfo includes the extension in its Name field, so we have to split the string and select only the actual name. string name = info.Name.Split('.').First(); //Ignore Themes without the xaml extension and ignore themes with the same names as others. //If requests are made I can instead make a Theme class that splits the pre-installed themes from //user-made ones so that one more user-made Light/Dark theme can be added. if (info.Extension.ToLower().Contains("xaml") && !loadedThemes.ContainsKey(name)) { ResourceDictionary theme = LoadTheme(name); if (theme != null) { loadedThemes.Add(name, theme); } } } } if (Options.Instance != null && Options.Instance.ApplicationThemeComboBox != null) //Refresh Themes dropdown in Options screen. { Options.Instance.ApplicationThemeComboBox.ItemsSource = LoadedThemes; Options.Instance.ApplicationThemeComboBox.SelectedIndex = LoadedThemes.IndexOf(LoadedTheme); } } /// /// Applies a loaded theme to ModAssistant. /// /// Name of the theme. /// Page that this is called on (Used for refreshing button icon colors). public static void ApplyTheme(string theme) { if (loadedThemes.TryGetValue(theme, out ResourceDictionary newTheme)) { Application.Current.Resources.MergedDictionaries.RemoveAt(0); LoadedTheme = theme; Application.Current.Resources.MergedDictionaries.Insert(0, newTheme); Properties.Settings.Default.SelectedTheme = theme; Properties.Settings.Default.Save(); MainWindow.Instance.MainText = $"Theme changed to {theme}."; ReloadIcons(); } else throw new ArgumentException($"{theme} does not exist."); } /// /// Writes a local theme to disk. You cannot write a theme loaded from the Themes subfolder to disk. /// /// Name of local theme. public static void WriteThemeToDisk(string themeName) { if (!Directory.Exists(ThemeDirectory)) { Directory.CreateDirectory(ThemeDirectory); } if (!File.Exists($@"{ThemeDirectory}\\{themeName}.xaml")) { //Store a local copy of the theme to prevent exceptions trying to access the saved copy while it's being written to. ResourceDictionary dictionary = LoadTheme(themeName, true); loadedThemes.Add(themeName, dictionary); Options.Instance.ApplicationThemeComboBox.ItemsSource = LoadedThemes; XmlWriterSettings settings = new XmlWriterSettings(); settings.Indent = true; XmlWriter writer = XmlWriter.Create($@"{ThemeDirectory}\\{themeName}.xaml", settings); XamlWriter.Save(dictionary, writer); MainWindow.Instance.MainText = $"Template theme \"{themeName}\" saved to Themes folder."; } else MessageBox.Show("Template theme already exists!"); } /// /// Loads a ResourceDictionary from either Embedded Resources or from a file location. /// /// ResourceDictionary file name. /// Specifies whether or not to search in Embedded Resources or in the Themes subfolder. /// private static ResourceDictionary LoadTheme(string name, bool localUri = false) { string location = $"{Environment.CurrentDirectory}/Themes/{name}.xaml"; if (!File.Exists(location) && !localUri) return null; if (localUri) location = $"Themes/{name}.xaml"; Uri uri = new Uri(location, localUri ? UriKind.Relative : UriKind.Absolute); ResourceDictionary dictionary = new ResourceDictionary(); try { dictionary.Source = uri; } catch(Exception ex) { MessageBox.Show($"Could not load {name}.\n\n{ex.Message}\n\nIgnoring..."); return null; } return dictionary; } /// /// Reload the icon colors for the About, Info, Options, and Mods buttons from the currently loaded theme. /// private static void ReloadIcons() { ResourceDictionary icons = Application.Current.Resources.MergedDictionaries.First(x => x.Source.ToString() == "Resources/Icons.xaml"); ChangeColor(icons, "AboutIconColor", "heartDrawingGroup"); ChangeColor(icons, "InfoIconColor", "info_circleDrawingGroup"); ChangeColor(icons, "OptionsIconColor", "cogDrawingGroup"); ChangeColor(icons, "ModsIconColor", "microchipDrawingGroup"); } /// /// Change the color of an image from the loaded theme. /// /// ResourceDictionary that contains the image. /// Resource name of the color to change. /// DrawingGroup name for the image. private static void ChangeColor(ResourceDictionary icons, string ResourceColorName, string DrawingGroupName) { Application.Current.Resources[ResourceColorName] = loadedThemes[LoadedTheme][ResourceColorName]; ((GeometryDrawing)((DrawingGroup)icons[DrawingGroupName]).Children[0]).Brush = (Brush)Application.Current.Resources[ResourceColorName]; } } }