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