1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Merge branch 'development'

This commit is contained in:
Robert 2021-05-17 17:29:35 +02:00
commit fb250ca405
28 changed files with 901 additions and 211 deletions

View File

@ -58,6 +58,7 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins_005Cmodules_005Cactivationrequirements/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins_005Cprerequisites/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins_005Cprerequisites_005Cprerequisiteaction/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins_005Cprofiling/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins_005Csettings/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=rgb_002Enet/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Ccolorquantizer/@EntryIndexedValue">True</s:Boolean>

View File

@ -62,8 +62,8 @@ namespace Artemis.Core
/// </summary>
public static readonly Plugin CorePlugin = new(CorePluginInfo, new DirectoryInfo(ApplicationFolder), null);
internal static readonly CorePluginFeature CorePluginFeature = new() {Plugin = CorePlugin};
internal static readonly EffectPlaceholderPlugin EffectPlaceholderPlugin = new() {Plugin = CorePlugin};
internal static readonly CorePluginFeature CorePluginFeature = new() {Plugin = CorePlugin, Profiler = CorePlugin.GetProfiler("Feature - Core")};
internal static readonly EffectPlaceholderPlugin EffectPlaceholderPlugin = new() {Plugin = CorePlugin, Profiler = CorePlugin.GetProfiler("Feature - Effect Placeholder")};
internal static JsonSerializerSettings JsonConvertSettings = new()
{

View File

@ -8,40 +8,6 @@ namespace Artemis.Core
/// </summary>
public abstract class DataModelPluginFeature : PluginFeature
{
/// <summary>
/// Registers a timed update that whenever the plugin is enabled calls the provided <paramref name="action" /> at the
/// provided
/// <paramref name="interval" />
/// </summary>
/// <param name="interval">The interval at which the update should occur</param>
/// <param name="action">
/// The action to call every time the interval has passed. The delta time parameter represents the
/// time passed since the last update in seconds
/// </param>
/// <returns>The resulting plugin update registration which can be used to stop the update</returns>
public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Action<double> action)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
return new TimedUpdateRegistration(this, interval, action);
}
/// <summary>
/// Registers a timed update that whenever the plugin is enabled calls the provided <paramref name="asyncAction" /> at the
/// provided
/// <paramref name="interval" />
/// </summary>
/// <param name="interval">The interval at which the update should occur</param>
/// <param name="asyncAction">
/// The async action to call every time the interval has passed. The delta time parameter
/// represents the time passed since the last update in seconds
/// </param>
/// <returns>The resulting plugin update registration</returns>
public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Func<double, Task> asyncAction)
{
if (asyncAction == null)
throw new ArgumentNullException(nameof(asyncAction));
return new TimedUpdateRegistration(this, interval, asyncAction);
}
}
}

View File

@ -205,6 +205,7 @@ namespace Artemis.Core.Modules
throw new ArtemisPluginFeatureException(this, $"Cannot add default profile from {file}, profile ID {profileEntity.Id} already in use.");
profileEntity.IsFreshImport = true;
profileEntity.IsActive = false;
_defaultProfiles.Add(profileEntity);
return true;

View File

@ -17,6 +17,7 @@ namespace Artemis.Core
public class Plugin : CorePropertyChanged, IDisposable
{
private readonly List<PluginFeatureInfo> _features;
private readonly List<Profiler> _profilers;
private bool _isEnabled;
@ -28,6 +29,7 @@ namespace Artemis.Core
Info.Plugin = this;
_features = new List<PluginFeatureInfo>();
_profilers = new List<Profiler>();
}
/// <summary>
@ -64,6 +66,8 @@ namespace Artemis.Core
/// </summary>
public ReadOnlyCollection<PluginFeatureInfo> Features => _features.AsReadOnly();
public ReadOnlyCollection<Profiler> Profilers => _profilers.AsReadOnly();
/// <summary>
/// The assembly the plugin code lives in
/// </summary>
@ -114,7 +118,7 @@ namespace Artemis.Core
{
return _features.FirstOrDefault(i => i.Instance is T)?.Instance as T;
}
/// <summary>
/// Looks up the feature info the feature of type <typeparamref name="T" />
/// </summary>
@ -126,6 +130,31 @@ namespace Artemis.Core
return _features.First(i => i.FeatureType == typeof(T));
}
/// <summary>
/// Gets a profiler with the provided <paramref name="name" />, if it does not yet exist it will be created.
/// </summary>
/// <param name="name">The name of the profiler</param>
/// <returns>A new or existing profiler with the provided <paramref name="name" /></returns>
public Profiler GetProfiler(string name)
{
Profiler? profiler = _profilers.FirstOrDefault(p => p.Name == name);
if (profiler != null)
return profiler;
profiler = new Profiler(this, name);
_profilers.Add(profiler);
return profiler;
}
/// <summary>
/// Removes a profiler from the plugin
/// </summary>
/// <param name="profiler">The profiler to remove</param>
public void RemoveProfiler(Profiler profiler)
{
_profilers.Remove(profiler);
}
/// <inheritdoc />
public override string ToString()
{

View File

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Artemis.Storage.Entities.Plugins;
@ -12,11 +10,9 @@ namespace Artemis.Core
/// </summary>
public abstract class PluginFeature : CorePropertyChanged, IDisposable
{
private readonly Stopwatch _renderStopwatch = new();
private readonly Stopwatch _updateStopwatch = new();
private bool _isEnabled;
private Exception? _loadException;
/// <summary>
/// Gets the plugin feature info related to this feature
/// </summary>
@ -27,6 +23,11 @@ namespace Artemis.Core
/// </summary>
public Plugin Plugin { get; internal set; } = null!; // Will be set right after construction
/// <summary>
/// Gets the profiler that can be used to take profiling measurements
/// </summary>
public Profiler Profiler { get; internal set; } = null!; // Will be set right after construction
/// <summary>
/// Gets whether the plugin is enabled
/// </summary>
@ -50,16 +51,6 @@ namespace Artemis.Core
/// </summary>
public string Id => $"{GetType().FullName}-{Plugin.Guid.ToString().Substring(0, 8)}"; // Not as unique as a GUID but good enough and stays readable
/// <summary>
/// Gets the last measured update time of the feature
/// </summary>
public TimeSpan UpdateTime { get; private set; }
/// <summary>
/// Gets the last measured render time of the feature
/// </summary>
public TimeSpan RenderTime { get; private set; }
internal PluginFeatureEntity Entity { get; set; } = null!; // Will be set right after construction
/// <summary>
@ -112,24 +103,22 @@ namespace Artemis.Core
internal void StartUpdateMeasure()
{
_updateStopwatch.Start();
Profiler.StartMeasurement("Update");
}
internal void StopUpdateMeasure()
{
UpdateTime = _updateStopwatch.Elapsed;
_updateStopwatch.Reset();
Profiler.StopMeasurement("Update");
}
internal void StartRenderMeasure()
{
_renderStopwatch.Start();
Profiler.StartMeasurement("Render");
}
internal void StopRenderMeasure()
{
RenderTime = _renderStopwatch.Elapsed;
_renderStopwatch.Reset();
Profiler.StopMeasurement("Render");
}
internal void SetEnabled(bool enable, bool isAutoEnable = false)
@ -242,5 +231,47 @@ namespace Artemis.Core
}
#endregion
#region Timed updates
/// <summary>
/// Registers a timed update that whenever the plugin is enabled calls the provided <paramref name="action" /> at the
/// provided
/// <paramref name="interval" />
/// </summary>
/// <param name="interval">The interval at which the update should occur</param>
/// <param name="action">
/// The action to call every time the interval has passed. The delta time parameter represents the
/// time passed since the last update in seconds
/// </param>
/// <param name="name">An optional name used in exceptions and profiling</param>
/// <returns>The resulting plugin update registration which can be used to stop the update</returns>
public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Action<double> action, string? name = null)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
return new TimedUpdateRegistration(this, interval, action, name);
}
/// <summary>
/// Registers a timed update that whenever the plugin is enabled calls the provided <paramref name="asyncAction" /> at the
/// provided
/// <paramref name="interval" />
/// </summary>
/// <param name="interval">The interval at which the update should occur</param>
/// <param name="asyncAction">
/// The async action to call every time the interval has passed. The delta time parameter
/// represents the time passed since the last update in seconds
/// </param>
/// <param name="name">An optional name used in exceptions and profiling</param>
/// <returns>The resulting plugin update registration</returns>
public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Func<double, Task> asyncAction, string? name = null)
{
if (asyncAction == null)
throw new ArgumentNullException(nameof(asyncAction));
return new TimedUpdateRegistration(this, interval, asyncAction, name);
}
#endregion
}
}

View File

@ -58,8 +58,7 @@ namespace Artemis.Core
/// <summary>
/// The plugins display icon that's shown in the settings see <see href="https://materialdesignicons.com" /> for
/// available
/// icons
/// available icons
/// </summary>
[JsonProperty]
public string? Icon
@ -125,6 +124,8 @@ namespace Artemis.Core
/// <inheritdoc />
public bool ArePrerequisitesMet() => Prerequisites.All(p => p.IsMet());
internal string PreferredPluginDirectory => $"{Main.Split(".dll")[0].Replace("/", "").Replace("\\", "")}-{Guid.ToString().Substring(0, 8)}";
/// <inheritdoc />
public override string ToString()
{

View File

@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Artemis.Core
{
/// <summary>
/// Represents a profiler that can measure time between calls distinguished by identifiers
/// </summary>
public class Profiler
{
internal Profiler(Plugin plugin, string name)
{
Plugin = plugin;
Name = name;
}
/// <summary>
/// Gets the plugin this profiler belongs to
/// </summary>
public Plugin Plugin { get; }
/// <summary>
/// Gets the name of this profiler
/// </summary>
public string Name { get; }
/// <summary>
/// Gets a dictionary containing measurements by their identifiers
/// </summary>
public Dictionary<string, ProfilingMeasurement> Measurements { get; set; } = new();
/// <summary>
/// Starts measuring time for the provided <paramref name="identifier" />
/// </summary>
/// <param name="identifier">A unique identifier for this measurement</param>
public void StartMeasurement(string identifier)
{
lock (Measurements)
{
if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement))
{
measurement = new ProfilingMeasurement(identifier);
Measurements.Add(identifier, measurement);
}
measurement.Start();
}
}
/// <summary>
/// Stops measuring time for the provided <paramref name="identifier" />
/// </summary>
/// <param name="identifier">A unique identifier for this measurement</param>
/// <returns>The number of ticks that passed since the <see cref="StartMeasurement" /> call with the same identifier</returns>
public long StopMeasurement(string identifier)
{
long lockRequestedAt = Stopwatch.GetTimestamp();
lock (Measurements)
{
if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement))
{
measurement = new ProfilingMeasurement(identifier);
Measurements.Add(identifier, measurement);
}
return measurement.Stop(Stopwatch.GetTimestamp() - lockRequestedAt);
}
}
/// <summary>
/// Clears measurements with the the provided <paramref name="identifier" />
/// </summary>
/// <param name="identifier"></param>
public void ClearMeasurements(string identifier)
{
lock (Measurements)
{
Measurements.Remove(identifier);
}
}
}
}

View File

@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Artemis.Core
{
/// <summary>
/// Represents a set of profiling measurements
/// </summary>
public class ProfilingMeasurement
{
private bool _filledArray;
private int _index;
private long _last;
private bool _open;
private long _start;
internal ProfilingMeasurement(string identifier)
{
Identifier = identifier;
}
/// <summary>
/// Gets the unique identifier of this measurement
/// </summary>
public string Identifier { get; }
/// <summary>
/// Gets the last 1000 measurements
/// </summary>
public long[] Measurements { get; } = new long[1000];
/// <summary>
/// Starts measuring time until <see cref="Stop" /> is called
/// </summary>
public void Start()
{
_start = Stopwatch.GetTimestamp();
_open = true;
}
/// <summary>
/// Stops measuring time and stores the time passed in the <see cref="Measurements" /> list
/// </summary>
/// <param name="correction">An optional correction in ticks to subtract from the measurement</param>
/// <returns>The time passed since the last <see cref="Start" /> call</returns>
public long Stop(long correction = 0)
{
if (!_open)
return 0;
long difference = Stopwatch.GetTimestamp() - _start - correction;
_open = false;
Measurements[_index] = difference;
_index++;
if (_index >= 1000)
{
_filledArray = true;
_index = 0;
}
_last = difference;
return difference;
}
/// <summary>
/// Gets the last measured time
/// </summary>
public TimeSpan GetLast()
{
return new(_last);
}
/// <summary>
/// Gets the average time of the last 1000 measurements
/// </summary>
public TimeSpan GetAverage()
{
if (!_filledArray && _index == 0)
return TimeSpan.Zero;
return _filledArray
? new TimeSpan((long) Measurements.Average(m => m))
: new TimeSpan((long) Measurements.Take(_index).Average(m => m));
}
/// <summary>
/// Gets the min time of the last 1000 measurements
/// </summary>
public TimeSpan GetMin()
{
if (!_filledArray && _index == 0)
return TimeSpan.Zero;
return _filledArray
? new TimeSpan(Measurements.Min())
: new TimeSpan(Measurements.Take(_index).Min());
}
/// <summary>
/// Gets the max time of the last 1000 measurements
/// </summary>
public TimeSpan GetMax()
{
if (!_filledArray && _index == 0)
return TimeSpan.Zero;
return _filledArray
? new TimeSpan(Measurements.Max())
: new TimeSpan(Measurements.Take(_index).Max());
}
/// <summary>
/// Gets the nth percentile of the last 1000 measurements
/// </summary>
public TimeSpan GetPercentile(double percentile)
{
if (!_filledArray && _index == 0)
return TimeSpan.Zero;
long[] collection = _filledArray
? Measurements.OrderBy(l => l).ToArray()
: Measurements.Take(_index).OrderBy(l => l).ToArray();
return new TimeSpan((long) Percentile(collection, percentile));
}
private static double Percentile(long[] elements, double percentile)
{
Array.Sort(elements);
double realIndex = percentile * (elements.Length - 1);
int index = (int) realIndex;
double frac = realIndex - index;
if (index + 1 < elements.Length)
return elements[index] * (1 - frac) + elements[index + 1] * frac;
return elements[index];
}
}
}

View File

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using System.Timers;
using Artemis.Core.Modules;
using Artemis.Core.Services;
using Humanizer;
using Ninject;
using Serilog;
@ -13,19 +14,20 @@ namespace Artemis.Core
/// </summary>
public class TimedUpdateRegistration : IDisposable
{
private DateTime _lastEvent;
private Timer? _timer;
private bool _disposed;
private readonly object _lock = new();
private ILogger _logger;
private bool _disposed;
private DateTime _lastEvent;
private readonly ILogger _logger;
private Timer? _timer;
internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Action<double> action)
internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Action<double> action, string? name)
{
_logger = CoreService.Kernel.Get<ILogger>();
Feature = feature;
Interval = interval;
Action = action;
Name = name ?? $"TimedUpdate-{Guid.NewGuid().ToString().Substring(0, 8)}";
Feature.Enabled += FeatureOnEnabled;
Feature.Disabled += FeatureOnDisabled;
@ -33,13 +35,14 @@ namespace Artemis.Core
Start();
}
internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Func<double, Task> asyncAction)
internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Func<double, Task> asyncAction, string? name)
{
_logger = CoreService.Kernel.Get<ILogger>();
Feature = feature;
Interval = interval;
AsyncAction = asyncAction;
Name = name ?? $"TimedUpdate-{Guid.NewGuid().ToString().Substring(0, 8)}";
Feature.Enabled += FeatureOnEnabled;
Feature.Disabled += FeatureOnDisabled;
@ -69,7 +72,12 @@ namespace Artemis.Core
public Func<double, Task>? AsyncAction { get; }
/// <summary>
/// Starts calling the <see cref="Action" /> or <see cref="AsyncAction"/> at the configured <see cref="Interval" />
/// Gets the name of this timed update
/// </summary>
public string Name { get; }
/// <summary>
/// Starts calling the <see cref="Action" /> or <see cref="AsyncAction" /> at the configured <see cref="Interval" />
/// <para>Note: Called automatically when the plugin enables</para>
/// </summary>
public void Start()
@ -93,7 +101,7 @@ namespace Artemis.Core
}
/// <summary>
/// Stops calling the <see cref="Action" /> or <see cref="AsyncAction"/> at the configured <see cref="Interval" />
/// Stops calling the <see cref="Action" /> or <see cref="AsyncAction" /> at the configured <see cref="Interval" />
/// <para>Note: Called automatically when the plugin disables</para>
/// </summary>
public void Stop()
@ -113,49 +121,6 @@ namespace Artemis.Core
}
}
private void TimerOnElapsed(object? sender, ElapsedEventArgs e)
{
if (!Feature.IsEnabled)
return;
lock (_lock)
{
TimeSpan interval = DateTime.Now - _lastEvent;
_lastEvent = DateTime.Now;
// Modules don't always want to update, honor that
if (Feature is Module module && !module.IsUpdateAllowed)
return;
try
{
if (Action != null)
Action(interval.TotalSeconds);
else if (AsyncAction != null)
{
Task task = AsyncAction(interval.TotalSeconds);
task.Wait();
}
}
catch (Exception exception)
{
_logger.Error(exception, "Timed update uncaught exception in plugin {plugin}", Feature.Plugin);
}
}
}
private void FeatureOnEnabled(object? sender, EventArgs e)
{
Start();
}
private void FeatureOnDisabled(object? sender, EventArgs e)
{
Stop();
}
#region IDisposable
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
@ -176,6 +141,55 @@ namespace Artemis.Core
}
}
private void TimerOnElapsed(object? sender, ElapsedEventArgs e)
{
if (!Feature.IsEnabled)
return;
lock (_lock)
{
Feature.Profiler.StartMeasurement(ToString());
TimeSpan interval = DateTime.Now - _lastEvent;
_lastEvent = DateTime.Now;
// Modules don't always want to update, honor that
if (Feature is Module module && !module.IsUpdateAllowed)
return;
try
{
if (Action != null)
{
Action(interval.TotalSeconds);
}
else if (AsyncAction != null)
{
Task task = AsyncAction(interval.TotalSeconds);
task.Wait();
}
}
catch (Exception exception)
{
_logger.Error(exception, "{timedUpdate} uncaught exception in plugin {plugin}", this, Feature.Plugin);
}
finally
{
Feature.Profiler.StopMeasurement(ToString());
}
}
}
private void FeatureOnEnabled(object? sender, EventArgs e)
{
Start();
}
private void FeatureOnDisabled(object? sender, EventArgs e)
{
Stop();
}
/// <inheritdoc />
public void Dispose()
{
@ -183,6 +197,12 @@ namespace Artemis.Core
GC.SuppressFinalize(this);
}
#endregion
/// <inheritdoc />
public sealed override string ToString()
{
if (Interval.TotalSeconds >= 1)
return $"{Name} ({Interval.TotalSeconds} sec)";
return $"{Name} ({Interval.TotalMilliseconds} ms)";
}
}
}

View File

@ -39,9 +39,9 @@ namespace Artemis.Core.Services
ProcessQueuedActions();
}
private void CopyBuiltInPlugin(FileInfo zipFileInfo, ZipArchive zipArchive)
private void CopyBuiltInPlugin(ZipArchive zipArchive, string targetDirectory)
{
DirectoryInfo pluginDirectory = new(Path.Combine(Constants.DataFolder, "plugins", Path.GetFileNameWithoutExtension(zipFileInfo.Name)));
DirectoryInfo pluginDirectory = new(Path.Combine(Constants.DataFolder, "plugins", targetDirectory));
bool createLockFile = File.Exists(Path.Combine(pluginDirectory.FullName, "artemis.lock"));
// Remove the old directory if it exists
@ -81,12 +81,18 @@ namespace Artemis.Core.Services
using StreamReader reader = new(metaDataFileEntry.Open());
PluginInfo builtInPluginInfo = CoreJson.DeserializeObject<PluginInfo>(reader.ReadToEnd())!;
string preferred = builtInPluginInfo.PreferredPluginDirectory;
string oldPreferred = Path.GetFileNameWithoutExtension(zipFile.Name);
// Rename folders to the new format
// TODO: Get rid of this eventually, it's nice to keep around but it's extra IO that's best avoided
if (pluginDirectory.EnumerateDirectories().FirstOrDefault(d => d.Name == oldPreferred) != null)
Directory.Move(Path.Combine(pluginDirectory.FullName, oldPreferred), Path.Combine(pluginDirectory.FullName, preferred));
// Find the matching plugin in the plugin folder
DirectoryInfo? match = pluginDirectory.EnumerateDirectories().FirstOrDefault(d => d.Name == Path.GetFileNameWithoutExtension(zipFile.Name));
DirectoryInfo? match = pluginDirectory.EnumerateDirectories().FirstOrDefault(d => d.Name == preferred);
if (match == null)
{
CopyBuiltInPlugin(zipFile, archive);
CopyBuiltInPlugin(archive, preferred);
}
else
{
@ -94,7 +100,7 @@ namespace Artemis.Core.Services
if (!File.Exists(metadataFile))
{
_logger.Debug("Copying missing built-in plugin {builtInPluginInfo}", builtInPluginInfo);
CopyBuiltInPlugin(zipFile, archive);
CopyBuiltInPlugin(archive, preferred);
}
else
{
@ -114,7 +120,7 @@ namespace Artemis.Core.Services
if (builtInPluginInfo.Version > pluginInfo.Version)
{
_logger.Debug("Copying updated built-in plugin from {pluginInfo} to {builtInPluginInfo}", pluginInfo, builtInPluginInfo);
CopyBuiltInPlugin(zipFile, archive);
CopyBuiltInPlugin(archive, preferred);
}
}
catch (Exception e)
@ -342,8 +348,8 @@ namespace Artemis.Core.Services
foreach (Type featureType in featureTypes)
{
// Load the enabled state and if not found, default to true
PluginFeatureEntity featureEntity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureType.FullName) ??
new PluginFeatureEntity { IsEnabled = plugin.Info.AutoEnableFeatures, Type = featureType.FullName! };
PluginFeatureEntity featureEntity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureType.FullName) ??
new PluginFeatureEntity {IsEnabled = plugin.Info.AutoEnableFeatures, Type = featureType.FullName!};
plugin.AddFeature(new PluginFeatureInfo(plugin, featureType, featureEntity, (PluginFeatureAttribute?) Attribute.GetCustomAttribute(featureType, typeof(PluginFeatureAttribute))));
}
@ -411,6 +417,7 @@ namespace Artemis.Core.Services
featureInfo.Instance = instance;
instance.Info = featureInfo;
instance.Plugin = plugin;
instance.Profiler = plugin.GetProfiler("Feature - " + featureInfo.Name);
instance.Entity = featureInfo.Entity;
}
catch (Exception e)
@ -501,6 +508,7 @@ namespace Artemis.Core.Services
Plugin? existing = _plugins.FirstOrDefault(p => p.Guid == pluginInfo.Guid);
if (existing != null)
{
try
{
RemovePlugin(existing, false);
@ -509,20 +517,14 @@ namespace Artemis.Core.Services
{
throw new ArtemisPluginException("A plugin with the same GUID is already loaded, failed to remove old version", e);
}
string targetDirectory = pluginInfo.Main.Split(".dll")[0].Replace("/", "").Replace("\\", "");
string uniqueTargetDirectory = targetDirectory;
int attempt = 2;
// Find a unique folder
while (pluginDirectory.EnumerateDirectories().Any(d => d.Name == uniqueTargetDirectory))
{
uniqueTargetDirectory = targetDirectory + "-" + attempt;
attempt++;
}
string targetDirectory = pluginInfo.PreferredPluginDirectory;
if (Directory.Exists(Path.Combine(pluginDirectory.FullName, targetDirectory)))
throw new ArtemisPluginException($"A directory for this plugin already exists {Path.Combine(pluginDirectory.FullName, targetDirectory)}");
// Extract everything in the same archive directory to the unique plugin directory
DirectoryInfo directoryInfo = new(Path.Combine(pluginDirectory.FullName, uniqueTargetDirectory));
DirectoryInfo directoryInfo = new(Path.Combine(pluginDirectory.FullName, targetDirectory));
Utilities.CreateAccessibleDirectory(directoryInfo.FullName);
string metaDataDirectory = metaDataFileEntry.FullName.Replace(metaDataFileEntry.Name, "");
foreach (ZipArchiveEntry zipArchiveEntry in archive.Entries)

View File

@ -311,8 +311,9 @@ namespace Artemis.Core.Services
// Assign a new GUID to make sure it is unique in case of a previous import of the same content
profileEntity.UpdateGuid(Guid.NewGuid());
profileEntity.Name = $"{profileEntity.Name} - {nameAffix}";
profileEntity.IsFreshImport = true;
profileEntity.IsActive = false;
_profileRepository.Add(profileEntity);
return new ProfileDescriptor(profileModule, profileEntity);
}

View File

@ -372,5 +372,8 @@
<Page Update="Screens\ProfileEditor\Dialogs\ProfileEditView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
</Page>
<Page Update="Screens\Settings\Debug\Tabs\Performance\PerformanceDebugView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
</Page>
</ItemGroup>
</Project>

View File

@ -71,7 +71,7 @@ namespace Artemis.UI.Providers
Execute.OnUIThreadSync(() =>
{
using FileStream stream = File.OpenWrite(imagePath);
GetEncoderForIcon(icon, _themeWatcher.GetWindowsTheme() == ThemeWatcher.WindowsTheme.Dark ? Colors.White : Colors.Black).Save(stream);
GetEncoderForIcon(icon, _themeWatcher.GetSystemTheme() == ThemeWatcher.WindowsTheme.Dark ? Colors.White : Colors.Black).Save(stream);
});
new ToastContentBuilder()

View File

@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using System.Linq;
using Artemis.Core;
using Artemis.Core.Services;
@ -107,6 +108,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions
{
RenderProfileElement.DisplayCondition.ChildAdded -= DisplayConditionOnChildrenModified;
RenderProfileElement.DisplayCondition.ChildRemoved -= DisplayConditionOnChildrenModified;
RenderProfileElement.Timeline.PropertyChanged -= TimelineOnPropertyChanged;
}
RenderProfileElement = e.RenderProfileElement;
@ -134,6 +136,14 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions
RenderProfileElement.DisplayCondition.ChildAdded += DisplayConditionOnChildrenModified;
RenderProfileElement.DisplayCondition.ChildRemoved += DisplayConditionOnChildrenModified;
RenderProfileElement.Timeline.PropertyChanged += TimelineOnPropertyChanged;
}
private void TimelineOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
NotifyOfPropertyChange(nameof(DisplayContinuously));
NotifyOfPropertyChange(nameof(AlwaysFinishTimeline));
NotifyOfPropertyChange(nameof(EventOverlapMode));
}
private void CoreServiceOnFrameRendered(object sender, FrameRenderedEventArgs e)

View File

@ -190,6 +190,9 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
ShowRepeatButton = SegmentWidth > 45 && IsMainSegment;
ShowDisableButton = SegmentWidth > 25;
if (Segment == SegmentViewModelType.Main)
NotifyOfPropertyChange(nameof(RepeatSegment));
}
private void Update()

View File

@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Screens.Settings.Debug.Tabs;
using Artemis.UI.Screens.Settings.Debug.Tabs.Performance;
using Stylet;
namespace Artemis.UI.Screens.Settings.Debug
@ -16,12 +16,14 @@ namespace Artemis.UI.Screens.Settings.Debug
ICoreService coreService,
RenderDebugViewModel renderDebugViewModel,
DataModelDebugViewModel dataModelDebugViewModel,
LogsDebugViewModel logsDebugViewModel)
LogsDebugViewModel logsDebugViewModel,
PerformanceDebugViewModel performanceDebugViewModel)
{
_coreService = coreService;
Items.Add(renderDebugViewModel);
Items.Add(dataModelDebugViewModel);
Items.Add(logsDebugViewModel);
Items.Add(performanceDebugViewModel);
ActiveItem = renderDebugViewModel;
StayOnTopSetting = settingsService.GetSetting("Debugger.StayOnTop", false);

View File

@ -0,0 +1,60 @@
using Artemis.Core;
using Stylet;
namespace Artemis.UI.Screens.Settings.Debug.Tabs.Performance
{
public class PerformanceDebugMeasurementViewModel : PropertyChangedBase
{
private string _average;
private string _last;
private string _max;
private string _min;
private string _percentile;
public PerformanceDebugMeasurementViewModel(ProfilingMeasurement measurement)
{
Measurement = measurement;
}
public ProfilingMeasurement Measurement { get; }
public string Last
{
get => _last;
set => SetAndNotify(ref _last, value);
}
public string Average
{
get => _average;
set => SetAndNotify(ref _average, value);
}
public string Min
{
get => _min;
set => SetAndNotify(ref _min, value);
}
public string Max
{
get => _max;
set => SetAndNotify(ref _max, value);
}
public string Percentile
{
get => _percentile;
set => SetAndNotify(ref _percentile, value);
}
public void Update()
{
Last = Measurement.GetLast().TotalMilliseconds + " ms";
Average = Measurement.GetAverage().TotalMilliseconds + " ms";
Min = Measurement.GetMin().TotalMilliseconds + " ms";
Max = Measurement.GetMax().TotalMilliseconds + " ms";
Percentile = Measurement.GetPercentile(0.95).TotalMilliseconds + " ms";
}
}
}

View File

@ -0,0 +1,33 @@
<UserControl x:Class="Artemis.UI.Screens.Settings.Debug.Tabs.Performance.PerformanceDebugPluginView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.Settings.Debug.Tabs.Performance"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
xmlns:s="https://github.com/canton7/Stylet"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:PerformanceDebugPluginViewModel}">
<materialDesign:Card Margin="0 5" Padding="10">
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<shared:ArtemisIcon Grid.Column="0" Icon="{Binding Icon}" Width="24" Height="24" />
<TextBlock Grid.Column="1" VerticalAlignment="Center" Style="{StaticResource MaterialDesignSubtitle1TextBlock}" Text="{Binding Plugin.Info.Name}" />
</Grid>
<ItemsControl ItemsSource="{Binding Profilers}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl s:View.Model="{Binding}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" IsTabStop="False" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</materialDesign:Card>
</UserControl>

View File

@ -0,0 +1,32 @@
using System.Linq;
using Artemis.Core;
using Artemis.UI.Shared;
using Stylet;
namespace Artemis.UI.Screens.Settings.Debug.Tabs.Performance
{
public class PerformanceDebugPluginViewModel : Screen
{
public Plugin Plugin { get; }
public object Icon { get; }
public PerformanceDebugPluginViewModel(Plugin plugin)
{
Plugin = plugin;
Icon = PluginUtilities.GetPluginIcon(Plugin, Plugin.Info.Icon);
}
public BindableCollection<PerformanceDebugProfilerViewModel> Profilers { get; } = new();
public void Update()
{
foreach (Profiler pluginProfiler in Plugin.Profilers.Where(p => p.Measurements.Any()))
{
if (Profilers.All(p => p.Profiler != pluginProfiler))
Profilers.Add(new PerformanceDebugProfilerViewModel(pluginProfiler));
}
foreach (PerformanceDebugProfilerViewModel profilerViewModel in Profilers)
profilerViewModel.Update();
}
}
}

View File

@ -0,0 +1,34 @@
<UserControl x:Class="Artemis.UI.Screens.Settings.Debug.Tabs.Performance.PerformanceDebugProfilerView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.Settings.Debug.Tabs.Performance"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:PerformanceDebugProfilerViewModel}">
<StackPanel>
<TextBlock Style="{StaticResource MaterialDesignBody2TextBlock}" Text="{Binding Profiler.Name}" Margin="10 10 0 0" />
<DataGrid ItemsSource="{Binding Measurements}"
d:DataContext="{d:DesignInstance Type={x:Type local:PerformanceDebugMeasurementViewModel}}"
CanUserSortColumns="True"
IsReadOnly="True"
CanUserAddRows="False"
AutoGenerateColumns="False"
materialDesign:DataGridAssist.CellPadding="16 5 5 5"
materialDesign:DataGridAssist.ColumnHeaderPadding="16 5 5 5"
CanUserResizeRows="False"
Margin="10 5 10 10">
<DataGrid.Columns>
<materialDesign:DataGridTextColumn Binding="{Binding Measurement.Identifier}" Header="Identifier" />
<materialDesign:DataGridTextColumn Binding="{Binding Last}" Header="Last" />
<materialDesign:DataGridTextColumn Binding="{Binding Min}" Header="Min" />
<materialDesign:DataGridTextColumn Binding="{Binding Max}" Header="Max" />
<materialDesign:DataGridTextColumn Binding="{Binding Average}" Header="Average" />
<materialDesign:DataGridTextColumn Binding="{Binding Percentile}" Header="95th percentile" />
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,30 @@
using System.Linq;
using Artemis.Core;
using Stylet;
namespace Artemis.UI.Screens.Settings.Debug.Tabs.Performance
{
public class PerformanceDebugProfilerViewModel : Screen
{
public Profiler Profiler { get; }
public PerformanceDebugProfilerViewModel(Profiler profiler)
{
Profiler = profiler;
}
public BindableCollection<PerformanceDebugMeasurementViewModel> Measurements { get; } = new();
public void Update()
{
foreach ((string _, ProfilingMeasurement measurement) in Profiler.Measurements)
{
if (Measurements.All(m => m.Measurement != measurement))
Measurements.Add(new PerformanceDebugMeasurementViewModel(measurement));
}
foreach (PerformanceDebugMeasurementViewModel profilingMeasurementViewModel in Measurements)
profilingMeasurementViewModel.Update();
}
}
}

View File

@ -0,0 +1,35 @@
<UserControl x:Class="Artemis.UI.Screens.Settings.Debug.Tabs.Performance.PerformanceDebugView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.Settings.Debug.Tabs.Performance"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:PerformanceDebugViewModel}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" TextWrapping="Wrap">
In this window you can see how much CPU time different plugin features are taking.
If you are having performance issues, below you can find out which plugin might be the culprit.
</TextBlock>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Hidden" PreviewMouseWheel="ScrollViewer_PreviewMouseWheel">
<ItemsControl ItemsSource="{Binding Items}" Margin="0 0 10 0">
<ItemsControl.ItemTemplate>
<DataTemplate>
<materialDesign:TransitioningContent OpeningEffect="{materialDesign:TransitionEffect SlideInFromLeft}"
OpeningEffectsOffset="{materialDesign:IndexedItemOffsetMultiplier 0:0:0.05}">
<ContentControl s:View.Model="{Binding}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" IsTabStop="False" />
</materialDesign:TransitioningContent>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</UserControl>

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Artemis.UI.Screens.Settings.Debug.Tabs.Performance
{
/// <summary>
/// Interaction logic for PerformanceDebugView.xaml
/// </summary>
public partial class PerformanceDebugView : UserControl
{
public PerformanceDebugView()
{
InitializeComponent();
}
private void ScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
ScrollViewer scv = (ScrollViewer)sender;
scv.ScrollToVerticalOffset(scv.VerticalOffset - e.Delta);
e.Handled = true;
}
}
}

View File

@ -0,0 +1,77 @@
using System.Linq;
using System.Timers;
using Artemis.Core;
using Artemis.Core.Services;
using Stylet;
namespace Artemis.UI.Screens.Settings.Debug.Tabs.Performance
{
public class PerformanceDebugViewModel : Conductor<PerformanceDebugPluginViewModel>.Collection.AllActive
{
private readonly IPluginManagementService _pluginManagementService;
private readonly Timer _updateTimer;
public PerformanceDebugViewModel(IPluginManagementService pluginManagementService)
{
_pluginManagementService = pluginManagementService;
_updateTimer = new Timer(500);
DisplayName = "PERFORMANCE";
_updateTimer.Elapsed += UpdateTimerOnElapsed;
}
private void UpdateTimerOnElapsed(object sender, ElapsedEventArgs e)
{
foreach (PerformanceDebugPluginViewModel viewModel in Items)
viewModel.Update();
}
private void FeatureToggled(object? sender, PluginFeatureEventArgs e)
{
Items.Clear();
PopulateItems();
}
private void PluginToggled(object? sender, PluginEventArgs e)
{
Items.Clear();
PopulateItems();
}
private void PopulateItems()
{
Items.AddRange(_pluginManagementService.GetAllPlugins()
.Where(p => p.IsEnabled && p.Profilers.Any(pr => pr.Measurements.Any()))
.OrderBy(p => p.Info.Name)
.Select(p => new PerformanceDebugPluginViewModel(p)));
}
#region Overrides of Screen
/// <inheritdoc />
protected override void OnActivate()
{
PopulateItems();
_updateTimer.Start();
_pluginManagementService.PluginDisabled += PluginToggled;
_pluginManagementService.PluginDisabled += PluginToggled;
_pluginManagementService.PluginFeatureEnabled += FeatureToggled;
_pluginManagementService.PluginFeatureDisabled += FeatureToggled;
base.OnActivate();
}
/// <inheritdoc />
protected override void OnDeactivate()
{
_updateTimer.Stop();
_pluginManagementService.PluginDisabled -= PluginToggled;
_pluginManagementService.PluginDisabled -= PluginToggled;
_pluginManagementService.PluginFeatureEnabled -= FeatureToggled;
_pluginManagementService.PluginFeatureDisabled -= FeatureToggled;
Items.Clear();
base.OnDeactivate();
}
#endregion
}
}

View File

@ -10,8 +10,7 @@
<tb:TaskbarIcon IconSource="{Binding Icon}"
MenuActivation="LeftOrRightClick"
PopupActivation="DoubleClick"
DoubleClickCommand="{s:Action TrayBringToForeground}"
TrayBalloonTipClicked="{s:Action OnTrayBalloonTipClicked}">
DoubleClickCommand="{s:Action TrayBringToForeground}">
<tb:TaskbarIcon.TrayToolTip>
<Border Background="{DynamicResource MaterialDesignToolTipBackground}" CornerRadius="2" Padding="5">

View File

@ -26,10 +26,11 @@ namespace Artemis.UI.Screens
private readonly IKernel _kernel;
private readonly ThemeWatcher _themeWatcher;
private readonly IWindowManager _windowManager;
private ImageSource _icon;
private bool _openingMainWindow;
private RootViewModel _rootViewModel;
private SplashViewModel _splashViewModel;
private TaskbarIcon _taskBarIcon;
private ImageSource _icon;
public TrayViewModel(IKernel kernel,
IWindowManager windowManager,
@ -51,9 +52,11 @@ namespace Artemis.UI.Screens
_themeWatcher = new ThemeWatcher();
_colorScheme = settingsService.GetSetting("UI.ColorScheme", ApplicationColorScheme.Automatic);
_colorScheme.SettingChanged += ColorSchemeOnSettingChanged;
_themeWatcher.ThemeChanged += ThemeWatcherOnThemeChanged;
_themeWatcher.SystemThemeChanged += _themeWatcher_SystemThemeChanged;
_themeWatcher.AppsThemeChanged += _themeWatcher_AppsThemeChanged;
ApplyColorSchemeSetting();
ApplyTrayIconTheme(_themeWatcher.GetSystemTheme());
windowService.ConfigureMainWindowProvider(this);
bool autoRunning = Bootstrapper.StartupArguments.Contains("--autorun");
@ -61,7 +64,9 @@ namespace Artemis.UI.Screens
bool showOnAutoRun = settingsService.GetSetting("UI.ShowOnStartup", true).Value;
if (autoRunning && !showOnAutoRun || minimized)
{
coreService.Initialized += (_, _) => updateService.AutoUpdate();
}
else
{
ShowSplashScreen();
@ -69,36 +74,55 @@ namespace Artemis.UI.Screens
}
}
public void TrayBringToForeground()
{
if (IsMainWindowOpen)
{
Execute.PostToUIThread(FocusMainWindow);
return;
}
// Initialize the shared UI when first showing the window
if (!UI.Shared.Bootstrapper.Initialized)
UI.Shared.Bootstrapper.Initialize(_kernel);
Execute.OnUIThreadSync(() =>
{
_splashViewModel?.RequestClose();
_splashViewModel = null;
_rootViewModel = _kernel.Get<RootViewModel>();
_rootViewModel.Closed += RootViewModelOnClosed;
_windowManager.ShowWindow(_rootViewModel);
});
OnMainWindowOpened();
}
public ImageSource Icon
{
get => _icon;
set => SetAndNotify(ref _icon, value);
}
public void TrayBringToForeground()
{
if (_openingMainWindow)
return;
try
{
_openingMainWindow = true;
if (IsMainWindowOpen)
{
Execute.OnUIThreadSync(() =>
{
FocusMainWindow();
_openingMainWindow = false;
});
return;
}
// Initialize the shared UI when first showing the window
if (!UI.Shared.Bootstrapper.Initialized)
UI.Shared.Bootstrapper.Initialize(_kernel);
Execute.OnUIThreadSync(() =>
{
_splashViewModel?.RequestClose();
_splashViewModel = null;
_rootViewModel = _kernel.Get<RootViewModel>();
_rootViewModel.Closed += RootViewModelOnClosed;
_windowManager.ShowWindow(_rootViewModel);
IsMainWindowOpen = true;
_openingMainWindow = false;
});
OnMainWindowOpened();
}
finally
{
_openingMainWindow = false;
}
}
public void TrayActivateSidebarItem(string sidebarItem)
{
TrayBringToForeground();
@ -120,14 +144,6 @@ namespace Artemis.UI.Screens
_taskBarIcon = (TaskbarIcon) ((ContentControl) view).Content;
}
public void OnTrayBalloonTipClicked(object sender, EventArgs e)
{
if (!IsMainWindowOpen)
TrayBringToForeground();
else
FocusMainWindow();
}
private void FocusMainWindow()
{
// Wrestle the main window to the front
@ -156,10 +172,15 @@ namespace Artemis.UI.Screens
private void RootViewModelOnClosed(object sender, CloseEventArgs e)
{
if (_rootViewModel != null)
lock (this)
{
_rootViewModel.Closed -= RootViewModelOnClosed;
_rootViewModel = null;
if (_rootViewModel != null)
{
_rootViewModel.Closed -= RootViewModelOnClosed;
_rootViewModel = null;
}
IsMainWindowOpen = false;
}
OnMainWindowClosed();
@ -170,28 +191,31 @@ namespace Artemis.UI.Screens
private void ApplyColorSchemeSetting()
{
if (_colorScheme.Value == ApplicationColorScheme.Automatic)
ApplyWindowsTheme(_themeWatcher.GetWindowsTheme());
ApplyUITheme(_themeWatcher.GetAppsTheme());
else
ChangeMaterialColors(_colorScheme.Value);
}
private void ApplyWindowsTheme(ThemeWatcher.WindowsTheme windowsTheme)
private void ApplyUITheme(ThemeWatcher.WindowsTheme theme)
{
Execute.PostToUIThread(() =>
{
Icon = windowsTheme == ThemeWatcher.WindowsTheme.Dark
? new BitmapImage(new Uri("pack://application:,,,/Artemis.UI;component/Resources/Images/Logo/bow-white.ico"))
: new BitmapImage(new Uri("pack://application:,,,/Artemis.UI;component/Resources/Images/Logo/bow-black.ico"));
});
if (_colorScheme.Value != ApplicationColorScheme.Automatic)
return;
if (windowsTheme == ThemeWatcher.WindowsTheme.Dark)
if (theme == ThemeWatcher.WindowsTheme.Dark)
ChangeMaterialColors(ApplicationColorScheme.Dark);
else
ChangeMaterialColors(ApplicationColorScheme.Light);
}
private void ApplyTrayIconTheme(ThemeWatcher.WindowsTheme theme)
{
Execute.PostToUIThread(() =>
{
Icon = theme == ThemeWatcher.WindowsTheme.Dark
? new BitmapImage(new Uri("pack://application:,,,/Artemis.UI;component/Resources/Images/Logo/bow-white.ico"))
: new BitmapImage(new Uri("pack://application:,,,/Artemis.UI;component/Resources/Images/Logo/bow-black.ico"));
});
}
private void ChangeMaterialColors(ApplicationColorScheme colorScheme)
{
PaletteHelper paletteHelper = new();
@ -203,9 +227,14 @@ namespace Artemis.UI.Screens
extensionsPaletteHelper.SetLightDark(colorScheme == ApplicationColorScheme.Dark);
}
private void ThemeWatcherOnThemeChanged(object sender, WindowsThemeEventArgs e)
private void _themeWatcher_AppsThemeChanged(object sender, WindowsThemeEventArgs e)
{
ApplyWindowsTheme(e.Theme);
ApplyUITheme(e.Theme);
}
private void _themeWatcher_SystemThemeChanged(object sender, WindowsThemeEventArgs e)
{
ApplyTrayIconTheme(e.Theme);
}
private void ColorSchemeOnSettingChanged(object sender, EventArgs e)
@ -221,10 +250,7 @@ namespace Artemis.UI.Screens
public bool OpenMainWindow()
{
if (IsMainWindowOpen)
Execute.OnUIThread(FocusMainWindow);
else
TrayBringToForeground();
TrayBringToForeground();
return _rootViewModel.ScreenState == ScreenState.Active;
}
@ -240,13 +266,11 @@ namespace Artemis.UI.Screens
protected virtual void OnMainWindowOpened()
{
IsMainWindowOpen = true;
MainWindowOpened?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnMainWindowClosed()
{
IsMainWindowOpen = false;
MainWindowClosed?.Invoke(this, EventArgs.Empty);
}

View File

@ -11,7 +11,8 @@ namespace Artemis.UI.Utilities
{
private const string RegistryKeyPath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
private const string RegistryValueName = "AppsUseLightTheme";
private const string appsThemeRegistryValueName = "AppsUseLightTheme";
private const string systemThemeRegistryValueName = "SystemUsesLightTheme";
public ThemeWatcher()
{
@ -21,24 +22,44 @@ namespace Artemis.UI.Utilities
public void WatchTheme()
{
WindowsIdentity currentUser = WindowsIdentity.GetCurrent();
string query = string.Format(
string appsThemequery = string.Format(
CultureInfo.InvariantCulture,
@"SELECT * FROM RegistryValueChangeEvent WHERE Hive = 'HKEY_USERS' AND KeyPath = '{0}\\{1}' AND ValueName = '{2}'",
currentUser.User.Value,
RegistryKeyPath.Replace(@"\", @"\\"),
RegistryValueName);
appsThemeRegistryValueName);
string systemThemequery = string.Format(
CultureInfo.InvariantCulture,
@"SELECT * FROM RegistryValueChangeEvent WHERE Hive = 'HKEY_USERS' AND KeyPath = '{0}\\{1}' AND ValueName = '{2}'",
currentUser.User.Value,
RegistryKeyPath.Replace(@"\", @"\\"),
systemThemeRegistryValueName);
try
{
ManagementEventWatcher watcher = new(query);
watcher.EventArrived += (sender, args) =>
// For Apps theme
ManagementEventWatcher appsThemWatcher = new(appsThemequery);
appsThemWatcher.EventArrived += (_, _) =>
{
WindowsTheme newWindowsTheme = GetWindowsTheme();
OnThemeChanged(new WindowsThemeEventArgs(newWindowsTheme));
WindowsTheme newWindowsTheme = GetAppsTheme();
OnAppsThemeChanged(new WindowsThemeEventArgs(newWindowsTheme));
};
// Start listening for events
watcher.Start();
// Start listening for apps theme events
appsThemWatcher.Start();
// For System theme
ManagementEventWatcher systemThemWatcher = new(systemThemequery);
systemThemWatcher.EventArrived += (_, _) =>
{
WindowsTheme newWindowsTheme = GetSystemTheme();
OnSystemThemeChanged(new WindowsThemeEventArgs(newWindowsTheme));
};
// Start listening for system theme events
systemThemWatcher.Start();
}
catch (Exception)
{
@ -46,25 +67,40 @@ namespace Artemis.UI.Utilities
}
}
public WindowsTheme GetWindowsTheme()
private WindowsTheme GetTheme(string themeKeyName)
{
using (RegistryKey key = Registry.CurrentUser.OpenSubKey(RegistryKeyPath))
{
object registryValueObject = key?.GetValue(RegistryValueName);
object registryValueObject = key?.GetValue(themeKeyName);
if (registryValueObject == null) return WindowsTheme.Light;
int registryValue = (int) registryValueObject;
int registryValue = (int)registryValueObject;
return registryValue > 0 ? WindowsTheme.Light : WindowsTheme.Dark;
}
}
public event EventHandler<WindowsThemeEventArgs> ThemeChanged;
protected virtual void OnThemeChanged(WindowsThemeEventArgs e)
public WindowsTheme GetAppsTheme()
{
ThemeChanged?.Invoke(this, e);
return GetTheme(appsThemeRegistryValueName);
}
public WindowsTheme GetSystemTheme()
{
return GetTheme(systemThemeRegistryValueName);
}
public event EventHandler<WindowsThemeEventArgs> AppsThemeChanged;
public event EventHandler<WindowsThemeEventArgs> SystemThemeChanged;
protected virtual void OnAppsThemeChanged(WindowsThemeEventArgs e)
{
AppsThemeChanged?.Invoke(this, e);
}
protected virtual void OnSystemThemeChanged(WindowsThemeEventArgs e)
{
SystemThemeChanged?.Invoke(this, e);
}
public enum WindowsTheme