Theme aware plugin (#4499)

* Migrate theme manager to infrastructure and added it as input to public API instance

* Working event-delegate for PublicAPIInstance

* Theme aware UWP applications

* Theme aware program plugin

* Update query icon on theme change

* Theme aware calculator plugin

* Fix issue with query running before theme change

* Theme based changes in ImageLoader

* Removed ErrorIcon direct references and added references from ImageLoader

* Nit fixes

* Removed unnecessary TODO in UWP.cs

* Added preference to theme based icons

* Added IDisposable interfaces to unsubscribe events
This commit is contained in:
Divyansh Srivastava 2020-06-26 17:42:06 -07:00 committed by GitHub
parent d9597d5ad5
commit d3b10d0d4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 255 additions and 41 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

View File

@ -1,4 +1,6 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
@ -8,7 +10,7 @@ using Wox.Plugin;
namespace Microsoft.Plugin.Calculator
{
public class Main : IPlugin, IPluginI18n
public class Main : IPlugin, IPluginI18n, IDisposable
{
private static readonly Regex RegValidExpressChar = new Regex(
@"^(" +
@ -22,6 +24,8 @@ namespace Microsoft.Plugin.Calculator
private static readonly Regex RegBrackets = new Regex(@"[\(\)\[\]]", RegexOptions.Compiled);
private static readonly Engine MagesEngine;
private PluginInitContext Context { get; set; }
private string IconPath { get; set; }
private bool _disposed = false;
static Main()
{
@ -58,7 +62,7 @@ namespace Microsoft.Plugin.Calculator
new Result
{
Title = result.ToString(),
IcoPath = "Images/calculator.png",
IcoPath = IconPath,
Score = 300,
SubTitle = Context.API.GetTranslation("wox_plugin_calculator_copy_number_to_clipboard"),
Action = c =>
@ -115,6 +119,26 @@ namespace Microsoft.Plugin.Calculator
public void Init(PluginInitContext context)
{
Context = context;
Context.API.ThemeChanged += OnThemeChanged;
UpdateIconPath(Context.API.GetCurrentTheme());
}
// Todo : Update with theme based IconPath
private void UpdateIconPath(Theme theme)
{
if (theme == Theme.Light || theme == Theme.HighContrastWhite)
{
IconPath = "Images/calculator_light.png";
}
else
{
IconPath = "Images/calculator_dark.png";
}
}
private void OnThemeChanged(Theme _, Theme newTheme)
{
UpdateIconPath(newTheme);
}
public string GetTranslatedPluginTitle()
@ -126,5 +150,23 @@ namespace Microsoft.Plugin.Calculator
{
return Context.API.GetTranslation("wox_plugin_calculator_plugin_description");
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
Context.API.ThemeChanged -= OnThemeChanged;
_disposed = true;
}
}
}
}
}

View File

@ -49,7 +49,7 @@
</ItemGroup>
<ItemGroup>
<None Include="Images\calculator.png">
<None Include="Images\calculator_light.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
@ -107,5 +107,11 @@
<PackageReference Include="Mages" Version="1.6.0" />
<PackageReference Include="System.Runtime" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
<None Update="Images\calculator_dark.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -13,11 +13,11 @@ using Wox.Plugin;
using Microsoft.Plugin.Program.Views;
using Stopwatch = Wox.Infrastructure.Stopwatch;
using Microsoft.Plugin.Program.Programs;
namespace Microsoft.Plugin.Program
{
public class Main : ISettingProvider, IPlugin, IPluginI18n, IContextMenu, ISavable, IReloadable
public class Main : ISettingProvider, IPlugin, IPluginI18n, IContextMenu, ISavable, IReloadable, IDisposable
{
private static readonly object IndexLock = new object();
internal static Programs.Win32[] _win32s { get; set; }
@ -34,6 +34,7 @@ namespace Microsoft.Plugin.Program
private static BinaryStorage<Programs.Win32[]> _win32Storage;
private static BinaryStorage<Programs.UWP.Application[]> _uwpStorage;
private readonly PluginJsonStorage<Settings> _settingsStorage;
private bool _disposed = false;
public Main()
{
@ -104,6 +105,21 @@ namespace Microsoft.Plugin.Program
public void Init(PluginInitContext context)
{
_context = context;
_context.API.ThemeChanged += OnThemeChanged;
UpdateUWPIconPath(_context.API.GetCurrentTheme());
}
public void OnThemeChanged(Theme _, Theme currentTheme)
{
UpdateUWPIconPath(currentTheme);
}
public void UpdateUWPIconPath(Theme theme)
{
foreach (UWP.Application app in _uwps)
{
app.UpdatePath(theme);
}
}
public static void IndexWin32Programs()
@ -186,6 +202,25 @@ namespace Microsoft.Plugin.Program
public void UpdateSettings(PowerLauncherSettings settings)
{
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_context.API.ThemeChanged -= OnThemeChanged;
_disposed = true;
}
}
}
void InitializeFileWatchers()
{
// Create a new FileSystemWatcher and set its properties.

View File

@ -21,6 +21,7 @@ using System.Windows.Input;
using System.Runtime.InteropServices.ComTypes;
using Wox.Plugin.SharedCommands;
using System.Reflection;
using Wox.Infrastructure.Image;
namespace Microsoft.Plugin.Program.Programs
{
@ -404,7 +405,6 @@ namespace Microsoft.Plugin.Program.Programs
DisplayName = ResourceFromPri(package.FullName, DisplayName);
Description = ResourceFromPri(package.FullName, Description);
LogoUri = LogoUriFromManifest(manifestApp);
LogoPath = LogoPathFromUri(LogoUri);
Enabled = true;
CanRunElevated = IfApplicationcanRunElevated();
@ -516,7 +516,19 @@ namespace Microsoft.Plugin.Program.Programs
}
}
internal string LogoPathFromUri(string uri)
public void UpdatePath(Theme theme)
{
if (theme == Theme.Light || theme == Theme.HighContrastWhite)
{
LogoPath = LogoPathFromUri(LogoUri, "contrast-white");
}
else
{
LogoPath = LogoPathFromUri(LogoUri, "contrast-black");
}
}
internal string LogoPathFromUri(string uri, string theme)
{
// all https://msdn.microsoft.com/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets
// windows 10 https://msdn.microsoft.com/en-us/library/windows/apps/dn934817.aspx
@ -539,11 +551,7 @@ namespace Microsoft.Plugin.Program.Programs
{
var end = path.Length - extension.Length;
var prefix = path.Substring(0, end);
var paths = new List<string> { path };
// TODO: This value must be set in accordance to the WPF theme (work in progress).
// Must be set to `contrast-white` for light theme and to `contrast-black` for dark theme to get an icon of the contrasting color.
var theme = "contrast-black";
var paths = new List<string> { path };
var scaleFactors = new Dictionary<PackageVersion, List<int>>
{
@ -563,6 +571,7 @@ namespace Microsoft.Plugin.Program.Programs
}
}
paths = paths.OrderByDescending(x => x.Contains(theme)).ToList();
var selected = paths.FirstOrDefault(File.Exists);
if (!string.IsNullOrEmpty(selected))
{
@ -589,6 +598,7 @@ namespace Microsoft.Plugin.Program.Programs
pathFactorPairs.Add(prefixThemePath, factor);
}
paths = paths.OrderByDescending(x => x.Contains(theme)).ToList();
var selectedIconPath = paths.OrderBy(x => Math.Abs(pathFactorPairs.GetValueOrDefault(x) - appIconSize)).FirstOrDefault(File.Exists);
if (!string.IsNullOrEmpty(selectedIconPath))
{
@ -630,7 +640,7 @@ namespace Microsoft.Plugin.Program.Programs
ProgramLogger.LogException($"|UWP|ImageFromPath|{path}" +
$"|Unable to get logo for {UserModelId} from {path} and" +
$" located in {Package.Location}", new FileNotFoundException());
return new BitmapImage(new Uri(Constant.ErrorIcon));
return new BitmapImage(new Uri(ImageLoader.ErrorIconPath));
}
}

View File

@ -17,6 +17,7 @@ using Wox.Infrastructure.Http;
using Wox.Infrastructure.Image;
using Wox.Infrastructure.Logger;
using Wox.Infrastructure.UserSettings;
using Wox.Plugin;
using Wox.ViewModel;
using Stopwatch = Wox.Infrastructure.Stopwatch;
@ -31,6 +32,7 @@ namespace PowerLauncher
private Settings _settings;
private MainViewModel _mainVM;
private MainWindow _mainWindow;
private ThemeManager _themeManager;
private SettingWindowViewModel _settingsVM;
private readonly Alphabet _alphabet = new Alphabet();
private StringMatcher _stringMatcher;
@ -70,7 +72,8 @@ namespace PowerLauncher
RegisterAppDomainExceptions();
RegisterDispatcherUnhandledException();
ImageLoader.Initialize();
_themeManager = new ThemeManager(this);
ImageLoader.Initialize(_themeManager.GetCurrentTheme());
_settingsVM = new SettingWindowViewModel();
_settings = _settingsVM.Settings;
@ -80,11 +83,10 @@ namespace PowerLauncher
StringMatcher.Instance = _stringMatcher;
_stringMatcher.UserSettingSearchPrecision = _settings.QuerySearchPrecision;
ThemeManager themeManager = new ThemeManager(this);
PluginManager.LoadPlugins(_settings.PluginSettings);
_mainVM = new MainViewModel(_settings);
_mainWindow = new MainWindow(_settings, _mainVM);
API = new PublicAPIInstance(_settingsVM, _mainVM, _alphabet);
API = new PublicAPIInstance(_settingsVM, _mainVM, _alphabet, _themeManager);
PluginManager.InitializePlugins(API);
Current.MainWindow = _mainWindow;
@ -105,6 +107,7 @@ namespace PowerLauncher
_mainVM.MainWindowVisibility = Visibility.Visible;
_mainVM.ColdStartFix();
_themeManager.ThemeChanged += OnThemeChanged;
Log.Info("|App.OnStartup|End PowerToys Run startup ---------------------------------------------------- ");
bootTime.Stop();
@ -125,6 +128,17 @@ namespace PowerLauncher
Current.SessionEnding += (s, e) => Dispose();
}
/// <summary>
/// Callback when windows theme is changed.
/// </summary>
/// <param name="oldTheme">Previous Theme</param>
/// <param name="newTheme">Current Theme</param>
private void OnThemeChanged(Theme oldTheme, Theme newTheme)
{
ImageLoader.UpdateIconPath(newTheme);
_mainVM.Query();
}
/// <summary>
/// let exception throw as normal is better for Debug
/// </summary>
@ -158,9 +172,13 @@ namespace PowerLauncher
Log.Info("|App.OnExit| Start PowerToys Run Exit---------------------------------------------------- ");
if (disposing)
{
_mainWindow.Dispose();
_themeManager.ThemeChanged -= OnThemeChanged;
API.SaveAppAllSettings();
PluginManager.Dispose();
_mainWindow.Dispose();
API.Dispose();
_mainVM.Dispose();
_themeManager.Dispose();
_disposed = true;
}

View File

@ -287,5 +287,14 @@ namespace Wox.Core.Plugin
RemoveActionKeyword(id, oldActionKeyword);
}
}
public static void Dispose()
{
foreach (var plugin in AllPlugins)
{
var disposablePlugin = plugin as IDisposable;
disposablePlugin?.Dispose();
}
}
}
}

View File

@ -57,7 +57,6 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="JetBrains.Annotations" Version="2020.1.0" />
<PackageReference Include="MahApps.Metro" Version="2.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="PropertyChanged.Fody" Version="3.2.8" />
<PackageReference Include="SharpZipLib" Version="1.2.0" />

View File

@ -8,6 +8,7 @@ using System.Windows.Media;
using System.Windows.Media.Imaging;
using Wox.Infrastructure.Logger;
using Wox.Infrastructure.Storage;
using Wox.Plugin;
namespace Wox.Infrastructure.Image
{
@ -17,7 +18,8 @@ namespace Wox.Infrastructure.Image
private static BinaryStorage<Dictionary<string, int>> _storage;
private static readonly ConcurrentDictionary<string, string> GuidToKey = new ConcurrentDictionary<string, string>();
private static IImageHashGenerator _hashGenerator;
public static string ErrorIconPath;
public static string DefaultIconPath;
private static readonly string[] ImageExtensions =
{
@ -31,18 +33,20 @@ namespace Wox.Infrastructure.Image
};
public static void Initialize()
public static void Initialize(Theme theme)
{
_storage = new BinaryStorage<Dictionary<string, int>>("Image");
_hashGenerator = new ImageHashGenerator();
ImageCache.SetUsageAsDictionary(_storage.TryLoad(new Dictionary<string, int>()));
// Todo : Add error and default icon specific to each theme
foreach (var icon in new[] { Constant.DefaultIcon, Constant.ErrorIcon })
{
ImageSource img = new BitmapImage(new Uri(icon));
img.Freeze();
ImageCache[icon] = img;
}
UpdateIconPath(theme);
Task.Run(() =>
{
Stopwatch.Normal("|ImageLoader.Initialize|Preload images cost", () =>
@ -62,6 +66,21 @@ namespace Wox.Infrastructure.Image
_storage.Save(ImageCache.GetUsageAsDictionary());
}
//Todo : Update it with icons specific to each theme.
public static void UpdateIconPath(Theme theme)
{
if (theme == Theme.Light || theme == Theme.HighContrastWhite)
{
ErrorIconPath = Constant.ErrorIcon;
DefaultIconPath = Constant.DefaultIcon;
}
else
{
ErrorIconPath = Constant.ErrorIcon;
DefaultIconPath = Constant.DefaultIcon;
}
}
private class ImageResult
{
public ImageResult(ImageSource imageSource, ImageType imageType)
@ -92,7 +111,7 @@ namespace Wox.Infrastructure.Image
{
if (string.IsNullOrEmpty(path))
{
return new ImageResult(ImageCache[Constant.ErrorIcon], ImageType.Error);
return new ImageResult(ImageCache[ErrorIconPath], ImageType.Error);
}
if (ImageCache.ContainsKey(path))
{
@ -154,8 +173,8 @@ namespace Wox.Infrastructure.Image
}
else
{
image = ImageCache[Constant.ErrorIcon];
path = Constant.ErrorIcon;
image = ImageCache[ErrorIconPath];
path = ErrorIconPath;
}
if (type != ImageType.Error)
@ -167,7 +186,7 @@ namespace Wox.Infrastructure.Image
{
Log.Exception($"|ImageLoader.Load|Failed to get thumbnail for {path}", e);
type = ImageType.Error;
image = ImageCache[Constant.ErrorIcon];
image = ImageCache[ErrorIconPath];
ImageCache[path] = image;
}
return new ImageResult(image, type);

View File

@ -57,6 +57,16 @@ namespace Wox.Plugin
[Obsolete]
void ShowApp();
/// <summary>
/// Get current theme
/// </summary>
Theme GetCurrentTheme();
/// <summary>
/// Theme change event
/// </summary>
event ThemeChangedHandler ThemeChanged;
/// <summary>
/// Save all Wox settings
/// </summary>

View File

@ -6,12 +6,13 @@ using System.Diagnostics;
using System.Linq;
using System.Windows;
namespace Wox.Core.Resource
namespace Wox.Plugin
{
public class ThemeManager
public class ThemeManager : IDisposable
{
private Theme currentTheme;
private readonly Application App;
private bool _disposed = false;
private readonly string LightTheme = "Light.Accent1";
private readonly string DarkTheme = "Dark.Accent1";
private readonly string HighContrastOneTheme = "HighContrast.Accent2";
@ -19,6 +20,8 @@ namespace Wox.Core.Resource
private readonly string HighContrastBlackTheme = "HighContrast.Accent4";
private readonly string HighContrastWhiteTheme = "HighContrast.Accent5";
public event ThemeChangedHandler ThemeChanged;
public ThemeManager(Application app)
{
this.App = app;
@ -52,17 +55,23 @@ namespace Wox.Core.Resource
ResetTheme();
ControlzEx.Theming.ThemeManager.Current.ThemeSyncMode = ThemeSyncMode.SyncWithAppMode;
ControlzEx.Theming.ThemeManager.Current.ThemeChanged += Current_ThemeChanged;
SystemParameters.StaticPropertyChanged += (sender, args) =>
SystemParameters.StaticPropertyChanged += SystemParameters_StaticPropertyChanged;
}
private void SystemParameters_StaticPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(SystemParameters.HighContrast))
{
if (args.PropertyName == nameof(SystemParameters.HighContrast))
{
ResetTheme();
}
};
ResetTheme();
}
}
public Theme GetCurrentTheme()
{
return currentTheme;
}
public static Theme GetHighContrastBaseType()
private static Theme GetHighContrastBaseType()
{
string RegistryKey = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes";
string theme = (string) Registry.GetValue(RegistryKey, "CurrentTheme", string.Empty);
@ -80,7 +89,7 @@ namespace Wox.Core.Resource
return Theme.None;
}
public void ResetTheme()
private void ResetTheme()
{
if (SystemParameters.HighContrast)
{
@ -96,6 +105,7 @@ namespace Wox.Core.Resource
private void ChangeTheme(Theme theme)
{
Theme oldTheme = currentTheme;
if (theme == currentTheme)
return;
if (theme == Theme.HighContrastOne)
@ -132,15 +142,36 @@ namespace Wox.Core.Resource
{
currentTheme = Theme.None;
}
Debug.WriteLine("Theme Changed to :" + currentTheme);
ThemeChanged?.Invoke(oldTheme, currentTheme);
}
private void Current_ThemeChanged(object sender, ThemeChangedEventArgs e)
{
ResetTheme();
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
ControlzEx.Theming.ThemeManager.Current.ThemeChanged -= Current_ThemeChanged;
SystemParameters.StaticPropertyChanged -= SystemParameters_StaticPropertyChanged;
_disposed = true;
}
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
public delegate void ThemeChangedHandler(Theme oldTheme, Theme newTheme);
public enum Theme
{
None,

View File

@ -57,6 +57,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="JetBrains.Annotations" Version="2020.1.0" />
<PackageReference Include="MahApps.Metro" Version="2.1.0" />
<PackageReference Include="ControlzEx" Version="4.3.0" />
<PackageReference Include="Mono.Cecil" Version="0.11.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="PropertyChanged.Fody" Version="3.2.8" />

View File

@ -14,19 +14,25 @@ using Wox.ViewModel;
namespace Wox
{
public class PublicAPIInstance : IPublicAPI
public class PublicAPIInstance : IPublicAPI, IDisposable
{
private readonly SettingWindowViewModel _settingsVM;
private readonly MainViewModel _mainVM;
private readonly Alphabet _alphabet;
private bool _disposed = false;
private readonly ThemeManager _themeManager;
public event ThemeChangedHandler ThemeChanged;
#region Constructor
public PublicAPIInstance(SettingWindowViewModel settingsVM, MainViewModel mainVM, Alphabet alphabet)
public PublicAPIInstance(SettingWindowViewModel settingsVM, MainViewModel mainVM, Alphabet alphabet, ThemeManager themeManager)
{
_settingsVM = settingsVM;
_mainVM = mainVM;
_alphabet = alphabet;
_themeManager = themeManager;
_themeManager.ThemeChanged += OnThemeChanged;
WebRequest.RegisterPrefix("data", new DataWebRequestFactory());
}
@ -138,10 +144,37 @@ namespace Wox
});
}
public Theme GetCurrentTheme()
{
return _themeManager.GetCurrentTheme();
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
#endregion
#region Private Methods
#region Protected Methods
protected void OnThemeChanged(Theme oldTheme, Theme newTheme)
{
ThemeChanged?.Invoke(oldTheme, newTheme);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_themeManager.ThemeChanged -= OnThemeChanged;
_disposed = true;
}
}
}
#endregion
}
}

View File

@ -172,7 +172,7 @@ namespace Wox.ViewModel
catch (Exception e)
{
Log.Exception($"|ResultViewModel.Image|IcoPath is empty and exception when calling Icon() for result <{Result.Title}> of plugin <{Result.PluginDirectory}>", e);
imagePath = Constant.ErrorIcon;
imagePath = ImageLoader.ErrorIconPath;
}
}