Skip to content

Commit 86a5047

Browse files
committed
(GH-3227) ThemeManager: enable dynamic accents (and themes) to change on the fly
1 parent 4442aec commit 86a5047

File tree

3 files changed

+153
-13
lines changed

3 files changed

+153
-13
lines changed

src/MahApps.Metro/ThemeManager/ThemeManager.cs

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ public static AppTheme GetAppTheme(ResourceDictionary resources)
176176
{
177177
if (resources == null) throw new ArgumentNullException(nameof(resources));
178178

179-
return AppThemes.FirstOrDefault(x => AreResourceDictionarySourcesEqual(x.Resources.Source, resources.Source));
179+
return AppThemes.FirstOrDefault(x => AreResourceDictionarySourcesEqual(x.Resources, resources));
180180
}
181181

182182
/// <summary>
@@ -237,7 +237,7 @@ public static Accent GetAccent(ResourceDictionary resources)
237237
{
238238
if (resources == null) throw new ArgumentNullException(nameof(resources));
239239

240-
var builtInAccent = Accents.FirstOrDefault(x => AreResourceDictionarySourcesEqual(x.Resources.Source, resources.Source));
240+
var builtInAccent = Accents.FirstOrDefault(x => AreResourceDictionarySourcesEqual(x.Resources, resources));
241241
if (builtInAccent != null)
242242
{
243243
return builtInAccent;
@@ -413,30 +413,28 @@ private static void ChangeAppStyle(ResourceDictionary resources, Tuple<AppTheme,
413413
var oldAccent = oldThemeInfo.Item2;
414414
if (oldAccent != null && oldAccent.Name != newAccent.Name)
415415
{
416-
resources.MergedDictionaries.Add(newAccent.Resources);
417-
418-
var key = oldAccent.Resources.Source.ToString().ToLower();
419-
var oldAccentResource = resources.MergedDictionaries.Where(x => x.Source != null).FirstOrDefault(d => d.Source.ToString().ToLower() == key);
416+
var oldAccentResource = resources.MergedDictionaries.FirstOrDefault(d => AreResourceDictionarySourcesEqual(d, oldAccent.Resources));
420417
if (oldAccentResource != null)
421418
{
422419
resources.MergedDictionaries.Remove(oldAccentResource);
423420
}
421+
422+
resources.MergedDictionaries.Add(newAccent.Resources);
424423

425424
themeChanged = true;
426425
}
427426

428427
var oldTheme = oldThemeInfo.Item1;
429428
if (oldTheme != null && oldTheme != newTheme)
430429
{
431-
resources.MergedDictionaries.Add(newTheme.Resources);
432-
433-
var key = oldTheme.Resources.Source.ToString().ToLower();
434-
var oldThemeResource = resources.MergedDictionaries.Where(x => x.Source != null).FirstOrDefault(d => d.Source.ToString().ToLower() == key);
430+
var oldThemeResource = resources.MergedDictionaries.FirstOrDefault(d => AreResourceDictionarySourcesEqual(d, oldTheme.Resources));
435431
if (oldThemeResource != null)
436432
{
437433
resources.MergedDictionaries.Remove(oldThemeResource);
438434
}
439435

436+
resources.MergedDictionaries.Add(newTheme.Resources);
437+
440438
themeChanged = true;
441439
}
442440
}
@@ -644,10 +642,36 @@ private static void OnThemeChanged(Accent newAccent, AppTheme newTheme)
644642
IsThemeChanged?.Invoke(Application.Current, new OnThemeChangedEventArgs(newTheme, newAccent));
645643
}
646644

647-
private static bool AreResourceDictionarySourcesEqual(Uri first, Uri second)
645+
private static bool AreResourceDictionarySourcesEqual(ResourceDictionary first, ResourceDictionary second)
648646
{
649-
return Uri.Compare(first, second,
650-
UriComponents.Host | UriComponents.Path, UriFormat.SafeUnescaped, StringComparison.OrdinalIgnoreCase) == 0;
647+
if (first == null || second == null)
648+
{
649+
return false;
650+
}
651+
652+
if (first.Source == null || second.Source == null)
653+
{
654+
try
655+
{
656+
foreach (var key in first.Keys)
657+
{
658+
var isTheSame = second.Contains(key) && Equals(first[key], second[key]);
659+
if (!isTheSame)
660+
{
661+
return false;
662+
}
663+
}
664+
}
665+
catch (Exception exception)
666+
{
667+
Trace.TraceError($"Could not compare resource dictionaries: {exception} {Environment.NewLine} {exception.StackTrace}");
668+
return false;
669+
}
670+
671+
return true;
672+
}
673+
674+
return Uri.Compare(first.Source, second.Source, UriComponents.Host | UriComponents.Path, UriFormat.SafeUnescaped, StringComparison.OrdinalIgnoreCase) == 0;
651675
}
652676
}
653677

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System;
2+
using System.Windows;
3+
using System.Windows.Media;
4+
5+
namespace MahApps.Metro.Tests.TestHelpers
6+
{
7+
public static class AccentHelper
8+
{
9+
public static void ApplyColor(Color color, string accentName = null)
10+
{
11+
// create a runtime accent resource dictionary
12+
13+
var resDictName = accentName ?? $"ApplicationAccent_{color.ToString().Replace("#", string.Empty)}.xaml";
14+
15+
var resourceDictionary = new ResourceDictionary();
16+
17+
resourceDictionary.Add("HighlightColor", color);
18+
resourceDictionary.Add("AccentBaseColor", color);
19+
resourceDictionary.Add("AccentColor", Color.FromArgb((byte)(204), color.R, color.G, color.B));
20+
resourceDictionary.Add("AccentColor2", Color.FromArgb((byte)(153), color.R, color.G, color.B));
21+
resourceDictionary.Add("AccentColor3", Color.FromArgb((byte)(102), color.R, color.G, color.B));
22+
resourceDictionary.Add("AccentColor4", Color.FromArgb((byte)(51), color.R, color.G, color.B));
23+
24+
resourceDictionary.Add("HighlightBrush", GetSolidColorBrush((Color)resourceDictionary["HighlightColor"]));
25+
resourceDictionary.Add("AccentBaseColorBrush", GetSolidColorBrush((Color)resourceDictionary["AccentBaseColor"]));
26+
resourceDictionary.Add("AccentColorBrush", GetSolidColorBrush((Color)resourceDictionary["AccentColor"]));
27+
resourceDictionary.Add("AccentColorBrush2", GetSolidColorBrush((Color)resourceDictionary["AccentColor2"]));
28+
resourceDictionary.Add("AccentColorBrush3", GetSolidColorBrush((Color)resourceDictionary["AccentColor3"]));
29+
resourceDictionary.Add("AccentColorBrush4", GetSolidColorBrush((Color)resourceDictionary["AccentColor4"]));
30+
31+
resourceDictionary.Add("WindowTitleColorBrush", GetSolidColorBrush((Color)resourceDictionary["AccentColor"]));
32+
33+
resourceDictionary.Add("ProgressBrush", new LinearGradientBrush(
34+
new GradientStopCollection(new[]
35+
{
36+
new GradientStop((Color)resourceDictionary["HighlightColor"], 0),
37+
new GradientStop((Color)resourceDictionary["AccentColor3"], 1)
38+
}),
39+
// StartPoint="1.002,0.5" EndPoint="0.001,0.5"
40+
startPoint: new Point(1.002, 0.5), endPoint: new Point(0.001, 0.5)));
41+
42+
resourceDictionary.Add("CheckmarkFill", GetSolidColorBrush((Color)resourceDictionary["AccentColor"]));
43+
resourceDictionary.Add("RightArrowFill", GetSolidColorBrush((Color)resourceDictionary["AccentColor"]));
44+
45+
resourceDictionary.Add("IdealForegroundColor", IdealTextColor(color));
46+
resourceDictionary.Add("IdealForegroundColorBrush", GetSolidColorBrush((Color)resourceDictionary["IdealForegroundColor"]));
47+
resourceDictionary.Add("IdealForegroundDisabledBrush", GetSolidColorBrush((Color)resourceDictionary["IdealForegroundColor"], 0.4));
48+
resourceDictionary.Add("AccentSelectedColorBrush", GetSolidColorBrush((Color)resourceDictionary["IdealForegroundColor"]));
49+
50+
resourceDictionary.Add("MetroDataGrid.HighlightBrush", GetSolidColorBrush((Color)resourceDictionary["AccentColor"]));
51+
resourceDictionary.Add("MetroDataGrid.HighlightTextBrush", GetSolidColorBrush((Color)resourceDictionary["IdealForegroundColor"]));
52+
resourceDictionary.Add("MetroDataGrid.MouseOverHighlightBrush", GetSolidColorBrush((Color)resourceDictionary["AccentColor3"]));
53+
resourceDictionary.Add("MetroDataGrid.FocusBorderBrush", GetSolidColorBrush((Color)resourceDictionary["AccentColor"]));
54+
resourceDictionary.Add("MetroDataGrid.InactiveSelectionHighlightBrush", GetSolidColorBrush((Color)resourceDictionary["AccentColor2"]));
55+
resourceDictionary.Add("MetroDataGrid.InactiveSelectionHighlightTextBrush", GetSolidColorBrush((Color)resourceDictionary["IdealForegroundColor"]));
56+
57+
resourceDictionary.Add("MahApps.Metro.Brushes.ToggleSwitchButton.OnSwitchBrush.Win10", GetSolidColorBrush((Color)resourceDictionary["AccentColor"]));
58+
resourceDictionary.Add("MahApps.Metro.Brushes.ToggleSwitchButton.OnSwitchMouseOverBrush.Win10", GetSolidColorBrush((Color)resourceDictionary["AccentColor2"]));
59+
resourceDictionary.Add("MahApps.Metro.Brushes.ToggleSwitchButton.ThumbIndicatorCheckedBrush.Win10", GetSolidColorBrush((Color)resourceDictionary["IdealForegroundColor"]));
60+
61+
// applying theme to MahApps
62+
ThemeManager.AddAccent(resDictName, resourceDictionary);
63+
var newAccent = ThemeManager.GetAccent(resDictName);
64+
// detect current application theme
65+
Tuple<AppTheme, Accent> applicationTheme = ThemeManager.DetectAppStyle(Application.Current);
66+
ThemeManager.ChangeAppStyle(Application.Current, newAccent, applicationTheme.Item1);
67+
}
68+
69+
/// <summary>
70+
/// Determining Ideal Text Color Based on Specified Background Color
71+
/// http://www.codeproject.com/KB/GDI-plus/IdealTextColor.aspx
72+
/// </summary>
73+
/// <param name = "color">The bg.</param>
74+
/// <returns></returns>
75+
private static Color IdealTextColor(Color color)
76+
{
77+
const int nThreshold = 105;
78+
var bgDelta = System.Convert.ToInt32((color.R * 0.299) + (color.G * 0.587) + (color.B * 0.114));
79+
var foreColor = (255 - bgDelta < nThreshold) ? Colors.Black : Colors.White;
80+
return foreColor;
81+
}
82+
83+
private static SolidColorBrush GetSolidColorBrush(Color color, double opacity = 1d)
84+
{
85+
var brush = new SolidColorBrush(color) { Opacity = opacity };
86+
brush.Freeze();
87+
return brush;
88+
}
89+
}
90+
}

src/Mahapps.Metro.Tests/Tests/ThemeManagerTest.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Linq;
33
using System.Threading.Tasks;
44
using System.Windows;
5+
using System.Windows.Media;
56
using MahApps.Metro.Tests.TestHelpers;
67
using MahApps.Metro.Controls;
78
using Xunit;
@@ -162,5 +163,30 @@ public async Task GetAccentWithUriIsCaseInsensitive()
162163
Assert.NotNull(detected);
163164
Assert.Equal("Blue", detected.Name);
164165
}
166+
167+
[Fact]
168+
[DisplayTestMethodName]
169+
public async Task CreateDynamicAccentWithColor()
170+
{
171+
await TestHost.SwitchToAppThread();
172+
173+
var applicationTheme = ThemeManager.DetectAppStyle(Application.Current);
174+
175+
var ex = Record.Exception(() => AccentHelper.ApplyColor(Colors.Red, "CustomAccentRed"));
176+
Assert.Null(ex);
177+
178+
var detected = ThemeManager.DetectAppStyle(Application.Current);
179+
Assert.NotNull(detected);
180+
Assert.Equal("CustomAccentRed", detected.Item2.Name);
181+
182+
ex = Record.Exception(() => AccentHelper.ApplyColor(Colors.Green, "CustomAccentGreen"));
183+
Assert.Null(ex);
184+
185+
detected = ThemeManager.DetectAppStyle(Application.Current);
186+
Assert.NotNull(detected);
187+
Assert.Equal("CustomAccentGreen", detected.Item2.Name);
188+
189+
ThemeManager.ChangeAppStyle(Application.Current, applicationTheme.Item2, applicationTheme.Item1);
190+
}
165191
}
166192
}

0 commit comments

Comments
 (0)