diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings
index abd028c5b..daa70a5ef 100644
--- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings
+++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings
@@ -58,6 +58,7 @@
True
True
True
+ True
True
True
True
diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs
index 4c22e8728..6f8f1c27d 100644
--- a/src/Artemis.Core/Constants.cs
+++ b/src/Artemis.Core/Constants.cs
@@ -62,8 +62,8 @@ namespace Artemis.Core
///
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()
{
diff --git a/src/Artemis.Core/Plugins/DataModelPluginFeature.cs b/src/Artemis.Core/Plugins/DataModelPluginFeature.cs
index 83649847a..099fba242 100644
--- a/src/Artemis.Core/Plugins/DataModelPluginFeature.cs
+++ b/src/Artemis.Core/Plugins/DataModelPluginFeature.cs
@@ -8,40 +8,6 @@ namespace Artemis.Core
///
public abstract class DataModelPluginFeature : PluginFeature
{
- ///
- /// Registers a timed update that whenever the plugin is enabled calls the provided at the
- /// provided
- ///
- ///
- /// The interval at which the update should occur
- ///
- /// The action to call every time the interval has passed. The delta time parameter represents the
- /// time passed since the last update in seconds
- ///
- /// The resulting plugin update registration which can be used to stop the update
- public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Action action)
- {
- if (action == null)
- throw new ArgumentNullException(nameof(action));
- return new TimedUpdateRegistration(this, interval, action);
- }
-
- ///
- /// Registers a timed update that whenever the plugin is enabled calls the provided at the
- /// provided
- ///
- ///
- /// The interval at which the update should occur
- ///
- /// 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
- ///
- /// The resulting plugin update registration
- public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Func asyncAction)
- {
- if (asyncAction == null)
- throw new ArgumentNullException(nameof(asyncAction));
- return new TimedUpdateRegistration(this, interval, asyncAction);
- }
+
}
}
\ No newline at end of file
diff --git a/src/Artemis.Core/Plugins/Modules/ProfileModule.cs b/src/Artemis.Core/Plugins/Modules/ProfileModule.cs
index cfbdc89e0..081959517 100644
--- a/src/Artemis.Core/Plugins/Modules/ProfileModule.cs
+++ b/src/Artemis.Core/Plugins/Modules/ProfileModule.cs
@@ -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;
diff --git a/src/Artemis.Core/Plugins/Plugin.cs b/src/Artemis.Core/Plugins/Plugin.cs
index ddd090bfd..bb6235ac0 100644
--- a/src/Artemis.Core/Plugins/Plugin.cs
+++ b/src/Artemis.Core/Plugins/Plugin.cs
@@ -17,6 +17,7 @@ namespace Artemis.Core
public class Plugin : CorePropertyChanged, IDisposable
{
private readonly List _features;
+ private readonly List _profilers;
private bool _isEnabled;
@@ -28,6 +29,7 @@ namespace Artemis.Core
Info.Plugin = this;
_features = new List();
+ _profilers = new List();
}
///
@@ -64,6 +66,8 @@ namespace Artemis.Core
///
public ReadOnlyCollection Features => _features.AsReadOnly();
+ public ReadOnlyCollection Profilers => _profilers.AsReadOnly();
+
///
/// The assembly the plugin code lives in
///
@@ -114,7 +118,7 @@ namespace Artemis.Core
{
return _features.FirstOrDefault(i => i.Instance is T)?.Instance as T;
}
-
+
///
/// Looks up the feature info the feature of type
///
@@ -126,6 +130,31 @@ namespace Artemis.Core
return _features.First(i => i.FeatureType == typeof(T));
}
+ ///
+ /// Gets a profiler with the provided , if it does not yet exist it will be created.
+ ///
+ /// The name of the profiler
+ /// A new or existing profiler with the provided
+ 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;
+ }
+
+ ///
+ /// Removes a profiler from the plugin
+ ///
+ /// The profiler to remove
+ public void RemoveProfiler(Profiler profiler)
+ {
+ _profilers.Remove(profiler);
+ }
+
///
public override string ToString()
{
diff --git a/src/Artemis.Core/Plugins/PluginFeature.cs b/src/Artemis.Core/Plugins/PluginFeature.cs
index aa661c951..983f20438 100644
--- a/src/Artemis.Core/Plugins/PluginFeature.cs
+++ b/src/Artemis.Core/Plugins/PluginFeature.cs
@@ -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
///
public abstract class PluginFeature : CorePropertyChanged, IDisposable
{
- private readonly Stopwatch _renderStopwatch = new();
- private readonly Stopwatch _updateStopwatch = new();
private bool _isEnabled;
private Exception? _loadException;
-
+
///
/// Gets the plugin feature info related to this feature
///
@@ -27,6 +23,11 @@ namespace Artemis.Core
///
public Plugin Plugin { get; internal set; } = null!; // Will be set right after construction
+ ///
+ /// Gets the profiler that can be used to take profiling measurements
+ ///
+ public Profiler Profiler { get; internal set; } = null!; // Will be set right after construction
+
///
/// Gets whether the plugin is enabled
///
@@ -50,16 +51,6 @@ namespace Artemis.Core
///
public string Id => $"{GetType().FullName}-{Plugin.Guid.ToString().Substring(0, 8)}"; // Not as unique as a GUID but good enough and stays readable
- ///
- /// Gets the last measured update time of the feature
- ///
- public TimeSpan UpdateTime { get; private set; }
-
- ///
- /// Gets the last measured render time of the feature
- ///
- public TimeSpan RenderTime { get; private set; }
-
internal PluginFeatureEntity Entity { get; set; } = null!; // Will be set right after construction
///
@@ -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
+
+ ///
+ /// Registers a timed update that whenever the plugin is enabled calls the provided at the
+ /// provided
+ ///
+ ///
+ /// The interval at which the update should occur
+ ///
+ /// The action to call every time the interval has passed. The delta time parameter represents the
+ /// time passed since the last update in seconds
+ ///
+ /// An optional name used in exceptions and profiling
+ /// The resulting plugin update registration which can be used to stop the update
+ public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Action action, string? name = null)
+ {
+ if (action == null)
+ throw new ArgumentNullException(nameof(action));
+ return new TimedUpdateRegistration(this, interval, action, name);
+ }
+
+ ///
+ /// Registers a timed update that whenever the plugin is enabled calls the provided at the
+ /// provided
+ ///
+ ///
+ /// The interval at which the update should occur
+ ///
+ /// 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
+ ///
+ /// An optional name used in exceptions and profiling
+ /// The resulting plugin update registration
+ public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Func asyncAction, string? name = null)
+ {
+ if (asyncAction == null)
+ throw new ArgumentNullException(nameof(asyncAction));
+ return new TimedUpdateRegistration(this, interval, asyncAction, name);
+ }
+
+ #endregion
}
}
\ No newline at end of file
diff --git a/src/Artemis.Core/Plugins/PluginInfo.cs b/src/Artemis.Core/Plugins/PluginInfo.cs
index de8a77e7f..2773a51c3 100644
--- a/src/Artemis.Core/Plugins/PluginInfo.cs
+++ b/src/Artemis.Core/Plugins/PluginInfo.cs
@@ -58,8 +58,7 @@ namespace Artemis.Core
///
/// The plugins display icon that's shown in the settings see for
- /// available
- /// icons
+ /// available icons
///
[JsonProperty]
public string? Icon
@@ -125,6 +124,8 @@ namespace Artemis.Core
///
public bool ArePrerequisitesMet() => Prerequisites.All(p => p.IsMet());
+ internal string PreferredPluginDirectory => $"{Main.Split(".dll")[0].Replace("/", "").Replace("\\", "")}-{Guid.ToString().Substring(0, 8)}";
+
///
public override string ToString()
{
diff --git a/src/Artemis.Core/Plugins/Profiling/Profiler.cs b/src/Artemis.Core/Plugins/Profiling/Profiler.cs
new file mode 100644
index 000000000..c424c19f1
--- /dev/null
+++ b/src/Artemis.Core/Plugins/Profiling/Profiler.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace Artemis.Core
+{
+ ///
+ /// Represents a profiler that can measure time between calls distinguished by identifiers
+ ///
+ public class Profiler
+ {
+ internal Profiler(Plugin plugin, string name)
+ {
+ Plugin = plugin;
+ Name = name;
+ }
+
+ ///
+ /// Gets the plugin this profiler belongs to
+ ///
+ public Plugin Plugin { get; }
+
+ ///
+ /// Gets the name of this profiler
+ ///
+ public string Name { get; }
+
+
+ ///
+ /// Gets a dictionary containing measurements by their identifiers
+ ///
+ public Dictionary Measurements { get; set; } = new();
+
+ ///
+ /// Starts measuring time for the provided
+ ///
+ /// A unique identifier for this measurement
+ public void StartMeasurement(string identifier)
+ {
+ lock (Measurements)
+ {
+ if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement))
+ {
+ measurement = new ProfilingMeasurement(identifier);
+ Measurements.Add(identifier, measurement);
+ }
+
+ measurement.Start();
+ }
+ }
+
+ ///
+ /// Stops measuring time for the provided
+ ///
+ /// A unique identifier for this measurement
+ /// The number of ticks that passed since the call with the same identifier
+ 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);
+ }
+ }
+
+ ///
+ /// Clears measurements with the the provided
+ ///
+ ///
+ public void ClearMeasurements(string identifier)
+ {
+ lock (Measurements)
+ {
+ Measurements.Remove(identifier);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Plugins/Profiling/ProfilingMeasurement.cs b/src/Artemis.Core/Plugins/Profiling/ProfilingMeasurement.cs
new file mode 100644
index 000000000..eed3a391d
--- /dev/null
+++ b/src/Artemis.Core/Plugins/Profiling/ProfilingMeasurement.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+
+namespace Artemis.Core
+{
+ ///
+ /// Represents a set of profiling measurements
+ ///
+ public class ProfilingMeasurement
+ {
+ private bool _filledArray;
+ private int _index;
+ private long _last;
+ private bool _open;
+ private long _start;
+
+ internal ProfilingMeasurement(string identifier)
+ {
+ Identifier = identifier;
+ }
+
+ ///
+ /// Gets the unique identifier of this measurement
+ ///
+ public string Identifier { get; }
+
+ ///
+ /// Gets the last 1000 measurements
+ ///
+ public long[] Measurements { get; } = new long[1000];
+
+ ///
+ /// Starts measuring time until is called
+ ///
+ public void Start()
+ {
+ _start = Stopwatch.GetTimestamp();
+ _open = true;
+ }
+
+ ///
+ /// Stops measuring time and stores the time passed in the list
+ ///
+ /// An optional correction in ticks to subtract from the measurement
+ /// The time passed since the last call
+ 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;
+ }
+
+ ///
+ /// Gets the last measured time
+ ///
+ public TimeSpan GetLast()
+ {
+ return new(_last);
+ }
+
+ ///
+ /// Gets the average time of the last 1000 measurements
+ ///
+ 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));
+ }
+
+ ///
+ /// Gets the min time of the last 1000 measurements
+ ///
+ public TimeSpan GetMin()
+ {
+ if (!_filledArray && _index == 0)
+ return TimeSpan.Zero;
+
+ return _filledArray
+ ? new TimeSpan(Measurements.Min())
+ : new TimeSpan(Measurements.Take(_index).Min());
+ }
+
+ ///
+ /// Gets the max time of the last 1000 measurements
+ ///
+ public TimeSpan GetMax()
+ {
+ if (!_filledArray && _index == 0)
+ return TimeSpan.Zero;
+
+ return _filledArray
+ ? new TimeSpan(Measurements.Max())
+ : new TimeSpan(Measurements.Take(_index).Max());
+ }
+
+ ///
+ /// Gets the nth percentile of the last 1000 measurements
+ ///
+ 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];
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Plugins/TimedUpdateRegistration.cs b/src/Artemis.Core/Plugins/TimedUpdateRegistration.cs
index f93225481..65eede731 100644
--- a/src/Artemis.Core/Plugins/TimedUpdateRegistration.cs
+++ b/src/Artemis.Core/Plugins/TimedUpdateRegistration.cs
@@ -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
///
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 action)
+ internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Action action, string? name)
{
_logger = CoreService.Kernel.Get();
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 asyncAction)
+ internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Func asyncAction, string? name)
{
_logger = CoreService.Kernel.Get();
-
+
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? AsyncAction { get; }
///
- /// Starts calling the or at the configured
+ /// Gets the name of this timed update
+ ///
+ public string Name { get; }
+
+ ///
+ /// Starts calling the or at the configured
/// Note: Called automatically when the plugin enables
///
public void Start()
@@ -93,7 +101,7 @@ namespace Artemis.Core
}
///
- /// Stops calling the or at the configured
+ /// Stops calling the or at the configured
/// Note: Called automatically when the plugin disables
///
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
-
///
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
///
@@ -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();
+ }
+
///
public void Dispose()
{
@@ -183,6 +197,12 @@ namespace Artemis.Core
GC.SuppressFinalize(this);
}
- #endregion
+ ///
+ public sealed override string ToString()
+ {
+ if (Interval.TotalSeconds >= 1)
+ return $"{Name} ({Interval.TotalSeconds} sec)";
+ return $"{Name} ({Interval.TotalMilliseconds} ms)";
+ }
}
}
\ No newline at end of file
diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs
index 7b3e6dea1..5655b8112 100644
--- a/src/Artemis.Core/Services/PluginManagementService.cs
+++ b/src/Artemis.Core/Services/PluginManagementService.cs
@@ -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(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)
diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs
index 61c5f147a..e405cdfed 100644
--- a/src/Artemis.Core/Services/Storage/ProfileService.cs
+++ b/src/Artemis.Core/Services/Storage/ProfileService.cs
@@ -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);
}
diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj
index ce31292e3..5ae4e77bb 100644
--- a/src/Artemis.UI/Artemis.UI.csproj
+++ b/src/Artemis.UI/Artemis.UI.csproj
@@ -372,5 +372,8 @@
$(DefaultXamlRuntime)
+
+ $(DefaultXamlRuntime)
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Providers/ToastNotificationProvider.cs b/src/Artemis.UI/Providers/ToastNotificationProvider.cs
index dea437566..53e8d5d25 100644
--- a/src/Artemis.UI/Providers/ToastNotificationProvider.cs
+++ b/src/Artemis.UI/Providers/ToastNotificationProvider.cs
@@ -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()
diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs
index 59856ce6f..b203639b2 100644
--- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs
+++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs
@@ -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)
diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentViewModel.cs
index 09be9d296..804e39dee 100644
--- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentViewModel.cs
+++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentViewModel.cs
@@ -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()
diff --git a/src/Artemis.UI/Screens/Settings/Debug/DebugViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/DebugViewModel.cs
index e661158ea..55939628d 100644
--- a/src/Artemis.UI/Screens/Settings/Debug/DebugViewModel.cs
+++ b/src/Artemis.UI/Screens/Settings/Debug/DebugViewModel.cs
@@ -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);
diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugMeasurementViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugMeasurementViewModel.cs
new file mode 100644
index 000000000..4b47d5e0a
--- /dev/null
+++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugMeasurementViewModel.cs
@@ -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";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugPluginView.xaml b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugPluginView.xaml
new file mode 100644
index 000000000..4a3d11955
--- /dev/null
+++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugPluginView.xaml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugPluginViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugPluginViewModel.cs
new file mode 100644
index 000000000..719eff26b
--- /dev/null
+++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugPluginViewModel.cs
@@ -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 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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugProfilerView.xaml b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugProfilerView.xaml
new file mode 100644
index 000000000..6fa8f1dba
--- /dev/null
+++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugProfilerView.xaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugProfilerViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugProfilerViewModel.cs
new file mode 100644
index 000000000..c0921b87a
--- /dev/null
+++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugProfilerViewModel.cs
@@ -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 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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugView.xaml b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugView.xaml
new file mode 100644
index 000000000..a63321f76
--- /dev/null
+++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugView.xaml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+ 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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugView.xaml.cs b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugView.xaml.cs
new file mode 100644
index 000000000..6492ab859
--- /dev/null
+++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugView.xaml.cs
@@ -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
+{
+ ///
+ /// Interaction logic for PerformanceDebugView.xaml
+ ///
+ 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;
+ }
+ }
+}
diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugViewModel.cs
new file mode 100644
index 000000000..b30ebe72e
--- /dev/null
+++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/Performance/PerformanceDebugViewModel.cs
@@ -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.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
+
+ ///
+ protected override void OnActivate()
+ {
+ PopulateItems();
+ _updateTimer.Start();
+ _pluginManagementService.PluginDisabled += PluginToggled;
+ _pluginManagementService.PluginDisabled += PluginToggled;
+ _pluginManagementService.PluginFeatureEnabled += FeatureToggled;
+ _pluginManagementService.PluginFeatureDisabled += FeatureToggled;
+ base.OnActivate();
+ }
+
+ ///
+ protected override void OnDeactivate()
+ {
+ _updateTimer.Stop();
+ _pluginManagementService.PluginDisabled -= PluginToggled;
+ _pluginManagementService.PluginDisabled -= PluginToggled;
+ _pluginManagementService.PluginFeatureEnabled -= FeatureToggled;
+ _pluginManagementService.PluginFeatureDisabled -= FeatureToggled;
+ Items.Clear();
+ base.OnDeactivate();
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/TrayView.xaml b/src/Artemis.UI/Screens/TrayView.xaml
index de13735d3..746897449 100644
--- a/src/Artemis.UI/Screens/TrayView.xaml
+++ b/src/Artemis.UI/Screens/TrayView.xaml
@@ -10,8 +10,7 @@
+ DoubleClickCommand="{s:Action TrayBringToForeground}">
diff --git a/src/Artemis.UI/Screens/TrayViewModel.cs b/src/Artemis.UI/Screens/TrayViewModel.cs
index 37e0a87be..b8d96909d 100644
--- a/src/Artemis.UI/Screens/TrayViewModel.cs
+++ b/src/Artemis.UI/Screens/TrayViewModel.cs
@@ -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.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.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);
}
diff --git a/src/Artemis.UI/Utilities/ThemeWatcher.cs b/src/Artemis.UI/Utilities/ThemeWatcher.cs
index 29e577079..cc0989ad5 100644
--- a/src/Artemis.UI/Utilities/ThemeWatcher.cs
+++ b/src/Artemis.UI/Utilities/ThemeWatcher.cs
@@ -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 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 AppsThemeChanged;
+ public event EventHandler SystemThemeChanged;
+
+ protected virtual void OnAppsThemeChanged(WindowsThemeEventArgs e)
+ {
+ AppsThemeChanged?.Invoke(this, e);
+ }
+
+ protected virtual void OnSystemThemeChanged(WindowsThemeEventArgs e)
+ {
+ SystemThemeChanged?.Invoke(this, e);
}
public enum WindowsTheme