Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Theme manager to preload themes from different assemblies, after initial load of sample defaults #117

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 93 additions & 32 deletions Examples/Nodify.Shared/ThemeManager.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Input;
Expand All @@ -8,12 +9,20 @@ namespace Nodify
{
public static class ThemeManager
{
private static readonly string? _assemblyName = Assembly.GetEntryAssembly()?.GetName().Name;
private static string? _assemblyName = Assembly.GetEntryAssembly()?.GetName().Name;

private static readonly Dictionary<string, List<Uri>> _themesUris = new Dictionary<string, List<Uri>>();
private static readonly Dictionary<string, List<ResourceDictionary>> _themesResources = new Dictionary<string, List<ResourceDictionary>>();

public static string? ActiveTheme { get; private set; }
private const string DEFAULT_THEME = "Nodify"; // This needs to be the one listed in App.xaml as preview, for correct switching behaviour
private static readonly string[] BUILTIN_THEMES = new string[]
{
"Dark",
"Light",
DEFAULT_THEME,
};

public static string? ActiveTheme { get; private set; } = DEFAULT_THEME;

private static readonly List<string> _availableThemes = new List<string>();
public static IReadOnlyCollection<string> AvailableThemes => _availableThemes;
Expand All @@ -22,13 +31,29 @@ public static class ThemeManager

static ThemeManager()
{
PreloadTheme("Dark");
PreloadTheme("Light");
PreloadTheme("Nodify");
PreloadThemes();

SetNextThemeCommand = new DelegateCommand(SetNextTheme);
}

public static bool PreloadThemes(string? assemblyName = null, string[]? themes = null)
{
bool success = true;
themes ??= BUILTIN_THEMES;

if (assemblyName != null)
{
_assemblyName = assemblyName;
}

foreach (var themeName in themes)
{
success &= PreloadTheme(themeName);
}

return success;
}

private static List<ResourceDictionary> FindExistingResources(List<Uri> uris)
{
var result = new List<ResourceDictionary>();
Expand All @@ -43,49 +68,74 @@ private static List<ResourceDictionary> FindExistingResources(List<Uri> uris)
return result;
}

private static void PreloadTheme(string themeName)
private static bool PreloadTheme(string themeName)
{
bool success = false;

if (_themesUris.ContainsKey(themeName))
{
// Cleanup if that one was already loaded, might be reloaded from external assembly
_themesUris.Remove(themeName);
_themesResources.Remove(themeName);
_availableThemes.Remove(themeName);
}

if (!_themesUris.TryGetValue(themeName, out var preload))
{
preload = new List<Uri>(3)
preload = new List<Uri>()
{
// These are in application merged resource list and need to be reloaded as default fallback
new Uri($"pack://application:,,,/Nodify;component/Themes/{DEFAULT_THEME}.xaml"),
new Uri($"pack://application:,,,/Nodify.Shared;component/Themes/Icons.xaml"),
new Uri($"pack://application:,,,/Nodify.Shared;component/Themes/{DEFAULT_THEME}.xaml"),
};

// Actual theme if part of Nodify library
if (themeName != DEFAULT_THEME)
{
new Uri($"pack://application:,,,/Nodify;component/Themes/{themeName}.xaml"),
new Uri($"pack://application:,,,/Nodify.Shared;component/Themes/{themeName}.xaml")
preload.Add(new Uri($"pack://application:,,,/Nodify;component/Themes/{themeName}.xaml"));
preload.Add(new Uri($"pack://application:,,,/Nodify.Shared;component/Themes/{themeName}.xaml"));
};

// Actual theme if in application (external/other assembly)
if (_assemblyName != null)
{
preload.Add(new Uri($"pack://application:,,,/{_assemblyName};component/Themes/{themeName}.xaml"));
}

_themesUris.Add(themeName, preload);
}

var resources = FindExistingResources(preload);
if (resources.Count == 0)

for (int i = 0; i < preload.Count; i++)
{
for (int i = 0; i < preload.Count; i++)
try
{
try
resources.Add(new ResourceDictionary
{
resources.Add(new ResourceDictionary
{
Source = preload[i]
});
}
catch
{

}
Source = preload[i]
});
success = true;
}
catch
{
// Debug note: Exception is OK if main application does not contain that theme
// The correct assembly can be set later through a seperate external PreloadThemes call.
}
}
else if (ActiveTheme == null)

if (success)
{
ActiveTheme = themeName;
_themesUris.Add(themeName, preload);
_themesResources.Add(themeName, resources);
_availableThemes.Add(themeName);

if (ActiveTheme == null)
{
ActiveTheme = themeName;
}
}

_themesResources.Add(themeName, resources);
_availableThemes.Add(themeName);
return success;
}

public static void SetNextTheme()
Expand Down Expand Up @@ -113,20 +163,31 @@ public static void SetTheme(string themeName)
// Load new theme if it is valid
if (_themesResources.TryGetValue(themeName, out var resources))
{
foreach (var res in resources)
{
Application.Current.Resources.MergedDictionaries.Add(res);
}

// Unload current theme
if (ActiveTheme != null)
{
foreach (var res in _themesResources[ActiveTheme])
{
Application.Current.Resources.MergedDictionaries.Remove(res);

// Additionally search by Source:
// as Theme resources in samples placed explicitly in App.xaml for xaml preview purposes
// (however ThemeManager doesn't find those as already preloaded on startup,
// because MainWindow not initialized yet)
var duplicates = Application.Current.Resources.MergedDictionaries.Where(r => r.Source == res.Source).ToList();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we detect duplicates in the PreloadThemes function? This may be the reason why changing themes is now slower than before (verified with ~400 nodes in Playground)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its possible, but i guess the order is more important, not necessarily the duplication. The performance issue is likely related to Output Warning messages.

foreach (var resFallback in duplicates)
{
Application.Current.Resources.MergedDictionaries.Remove(resFallback);
}
}
}

// Load new theme
foreach (var res in resources)
{
Application.Current.Resources.MergedDictionaries.Add(res);
}

ActiveTheme = themeName;
}
}
Expand Down
53 changes: 52 additions & 1 deletion docs/Getting-Started.md
Original file line number Diff line number Diff line change
Expand Up @@ -536,4 +536,55 @@ Use the `ViewportTransform` dependency property to have the grid move with the v
</Window>
```

> Tip: Right-click and drag the screen around to move the view and use the mouse wheel to zoom in and out.
> Tip: Right-click and drag the screen around to move the view and use the mouse wheel to zoom in and out.

## Applying custom theme

Firstly define the theme in your assembly, if you don't have custom `Brushes` or `Controls` in your assembly then ommit those from the `MergedDictionary`.

Create the file in your project under `Themes/Custom.xaml`:

```xml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Nodify.Shared;component/Themes/Brushes.xaml" />
<ResourceDictionary Source="pack://application:,,,/Nodify.Shared;component/Themes/Controls.xaml" />
<ResourceDictionary Source="Brushes.xaml" />
<ResourceDictionary Source="Controls.xaml" />
</ResourceDictionary.MergedDictionaries>

<!-- from shared -->
<Color x:Key="ForegroundColor">White</Color>

</ResourceDictionary>
```

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you meant: by copying them

Redefine all colors you dislike by copying and them to your theme from:

- `Nodify/Themes/Nodify.xaml`
- `Nodify.Shared/Themes/Nodify.xaml`

To load a custom theme inside your application (from the assembly where your theme is defined) call `ThemeManager.PreloadThemes`:

```cs
// Preload now themes with correct assembly setup
ThemeManager.PreloadThemes(Assembly.GetExecutingAssembly()?.GetName().Name,
new string[]
{
"Dark",
"Light",
"Nodify",
"Custom",
}
); //
```

This will also add your theme to the theme cycle of the manager. If you want to remove the default themes from the cycle, ommit them from the preload call.

Finally apply your theme:

```cs
ThemeManager.SetTheme("Custom");
```
Loading