diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index effc348c2..96e72a0f6 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -46,7 +46,14 @@ namespace Artemis.Core /// public static readonly BuildInfo BuildInfo = File.Exists(Path.Combine(ApplicationFolder, "buildinfo.json")) ? JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(ApplicationFolder, "buildinfo.json"))) - : new BuildInfo(); + : new BuildInfo + { + IsLocalBuild = true, + BuildId = 1337, + BuildNumber = 1337, + SourceBranch = "local", + SourceVersion = "local" + }; /// /// The plugin used by core components of Artemis @@ -108,6 +115,6 @@ namespace Artemis.Core typeof(float), typeof(double), typeof(decimal) - }; + }; } } \ No newline at end of file diff --git a/src/Artemis.Core/Events/Plugins/PluginFeatureEventArgs.cs b/src/Artemis.Core/Events/Plugins/PluginFeatureEventArgs.cs index 8d33d0f4b..d16789b82 100644 --- a/src/Artemis.Core/Events/Plugins/PluginFeatureEventArgs.cs +++ b/src/Artemis.Core/Events/Plugins/PluginFeatureEventArgs.cs @@ -17,4 +17,20 @@ namespace Artemis.Core /// public PluginFeature PluginFeature { get; } } + + /// + /// Provides data about plugin feature info related events + /// + public class PluginFeatureInfoEventArgs : EventArgs + { + internal PluginFeatureInfoEventArgs(PluginFeatureInfo pluginFeatureInfo) + { + PluginFeatureInfo = pluginFeatureInfo; + } + + /// + /// Gets the plugin feature this event is related to + /// + public PluginFeatureInfo PluginFeatureInfo { get; } + } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionEvent.cs b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionEvent.cs index 64529c531..dd849741c 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionEvent.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionEvent.cs @@ -38,6 +38,9 @@ namespace Artemis.Core /// public DataModelPath? EventPath { get; private set; } + /// + /// Gets the last time the event this condition is applied to was triggered + /// public DateTime LastTrigger { get; private set; } /// @@ -53,7 +56,7 @@ namespace Artemis.Core if (_disposed) throw new ObjectDisposedException("DataModelConditionEvent"); - if (EventPath?.GetValue() is not IDataModelEvent dataModelEvent) + if (EventPath?.GetValue() is not IDataModelEvent dataModelEvent) return false; // Only evaluate to true once every time the event has been triggered since the last evaluation if (dataModelEvent.LastTrigger <= LastTrigger) @@ -67,7 +70,6 @@ namespace Artemis.Core // If there are no children, we always evaluate to true whenever the event triggered return true; - } /// @@ -131,7 +133,7 @@ namespace Artemis.Core // Target list EventPath?.Save(); Entity.EventPath = EventPath?.Entity; - + // Children Entity.Children.Clear(); Entity.Children.AddRange(Children.Select(c => c.GetEntity())); diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 9a98dfcc2..31d8d8ae4 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -225,7 +225,7 @@ namespace Artemis.Core { LedEntity ledEntity = new() { - DeviceIdentifier = artemisLed.Device.RgbDevice.GetDeviceIdentifier(), + DeviceIdentifier = artemisLed.Device.Identifier, LedName = artemisLed.RgbLed.Id.ToString() }; LayerEntity.Leds.Add(ledEntity); @@ -578,7 +578,7 @@ namespace Artemis.Core List availableLeds = devices.SelectMany(d => d.Leds).ToList(); foreach (LedEntity ledEntity in LayerEntity.Leds) { - ArtemisLed? match = availableLeds.FirstOrDefault(a => a.Device.RgbDevice.GetDeviceIdentifier() == ledEntity.DeviceIdentifier && + ArtemisLed? match = availableLeds.FirstOrDefault(a => a.Device.Identifier == ledEntity.DeviceIdentifier && a.RgbLed.Id.ToString() == ledEntity.LedName); if (match != null) leds.Add(match); diff --git a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs index 9146097e2..cee4d8dfd 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs @@ -21,6 +21,7 @@ namespace Artemis.Core internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider) { + Identifier = rgbDevice.GetDeviceIdentifier(); DeviceEntity = new DeviceEntity(); RgbDevice = rgbDevice; DeviceProvider = deviceProvider; @@ -45,6 +46,7 @@ namespace Artemis.Core internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider, DeviceEntity deviceEntity) { + Identifier = rgbDevice.GetDeviceIdentifier(); DeviceEntity = deviceEntity; RgbDevice = rgbDevice; DeviceProvider = deviceProvider; @@ -59,6 +61,11 @@ namespace Artemis.Core ApplyKeyboardLayout(); } + /// + /// Gets the (hopefully unique and persistent) ID of this device + /// + public string Identifier { get; } + /// /// Gets the rectangle covering the device /// @@ -336,7 +343,7 @@ namespace Artemis.Core internal void ApplyToEntity() { // Other properties are computed - DeviceEntity.Id = RgbDevice.GetDeviceIdentifier(); + DeviceEntity.Id = Identifier; DeviceEntity.InputIdentifiers.Clear(); foreach (ArtemisDeviceInputIdentifier identifier in InputIdentifiers) diff --git a/src/Artemis.Core/Models/Surface/ArtemisLed.cs b/src/Artemis.Core/Models/Surface/ArtemisLed.cs index 473757f76..b8b81ec5b 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisLed.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisLed.cs @@ -46,6 +46,9 @@ namespace Artemis.Core private set => SetAndNotify(ref _absoluteRectangle, value); } + /// + /// Gets the layout applied to this LED + /// public ArtemisLedLayout? Layout { get; internal set; } /// @@ -53,7 +56,7 @@ namespace Artemis.Core { return RgbLed.ToString(); } - + internal void CalculateRectangles() { Rectangle = RgbLed.Boundary.ToSKRect(); diff --git a/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs b/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs index e0ee9fa07..838c62881 100644 --- a/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs +++ b/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs @@ -5,6 +5,9 @@ using RGB.NET.Layout; namespace Artemis.Core { + /// + /// Represents a LED layout decorated with extra Artemis-specific data + /// public class ArtemisLedLayout { internal ArtemisLedLayout(ArtemisLayout deviceLayout, ILedLayout led) diff --git a/src/Artemis.Core/Plugins/LayerBrushes/RgbNetLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/RgbNetLayerBrush.cs index 843321c99..7dd558a16 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/RgbNetLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/RgbNetLayerBrush.cs @@ -29,6 +29,9 @@ namespace Artemis.Core.LayerBrushes /// public ListLedGroup? LedGroup { get; internal set; } + /// + /// For internal use only, is public for dependency injection but ignore pl0x + /// [Inject] public IRgbService? RgbService { get; set; } @@ -38,35 +41,6 @@ namespace Artemis.Core.LayerBrushes /// Your RGB.NET effect public abstract IBrush GetBrush(); - internal void UpdateLedGroup() - { - if (LedGroup == null) - return; - - if (Layer.Parent != null) - LedGroup.ZIndex = Layer.Parent.Children.Count - Layer.Parent.Children.IndexOf(Layer); - else - LedGroup.ZIndex = 1; - - List missingLeds = Layer.Leds.Where(l => !LedGroup.ContainsLed(l.RgbLed)).Select(l => l.RgbLed).ToList(); - List extraLeds = LedGroup.Where(l => Layer.Leds.All(layerLed => layerLed.RgbLed != l)).ToList(); - LedGroup.AddLeds(missingLeds); - LedGroup.RemoveLeds(extraLeds); - LedGroup.Brush = GetBrush(); - } - - internal override void Initialize() - { - if (RgbService == null) - throw new ArtemisCoreException("Cannot initialize RGB.NET layer brush because RgbService is not set"); - - LedGroup = new ListLedGroup(RgbService.Surface); - Layer.RenderPropertiesUpdated += LayerOnRenderPropertiesUpdated; - - InitializeProperties(); - UpdateLedGroup(); - } - #region IDisposable /// @@ -87,6 +61,35 @@ namespace Artemis.Core.LayerBrushes #endregion + internal void UpdateLedGroup() + { + if (LedGroup == null) + return; + + if (Layer.Parent != null) + LedGroup.ZIndex = Layer.Parent.Children.Count - Layer.Parent.Children.IndexOf(Layer); + else + LedGroup.ZIndex = 1; + + List missingLeds = Layer.Leds.Where(l => !LedGroup.ContainsLed(l.RgbLed)).Select(l => l.RgbLed).ToList(); + List extraLeds = LedGroup.Where(l => Layer.Leds.All(layerLed => layerLed.RgbLed != l)).ToList(); + LedGroup.AddLeds(missingLeds); + LedGroup.RemoveLeds(extraLeds); + LedGroup.Brush = GetBrush(); + } + + internal override void Initialize() + { + if (RgbService == null) + throw new ArtemisCoreException("Cannot initialize RGB.NET layer brush because RgbService is not set"); + + LedGroup = new ListLedGroup(RgbService.Surface); + Layer.RenderPropertiesUpdated += LayerOnRenderPropertiesUpdated; + + InitializeProperties(); + UpdateLedGroup(); + } + // Not used in this effect type internal override void InternalRender(SKCanvas canvas, SKRect bounds, SKPaint paint) { diff --git a/src/Artemis.Core/Plugins/Plugin.cs b/src/Artemis.Core/Plugins/Plugin.cs index 53c581e7c..3ab1a99ef 100644 --- a/src/Artemis.Core/Plugins/Plugin.cs +++ b/src/Artemis.Core/Plugins/Plugin.cs @@ -16,7 +16,7 @@ namespace Artemis.Core /// public class Plugin : CorePropertyChanged, IDisposable { - private readonly List _features; + private readonly List _features; private bool _isEnabled; @@ -26,7 +26,7 @@ namespace Artemis.Core Directory = directory; Entity = pluginEntity ?? new PluginEntity {Id = Guid, IsEnabled = true}; - _features = new List(); + _features = new List(); } /// @@ -61,7 +61,7 @@ namespace Artemis.Core /// /// Gets a read-only collection of all features this plugin provides /// - public ReadOnlyCollection Features => _features.AsReadOnly(); + public ReadOnlyCollection Features => _features.AsReadOnly(); /// /// The assembly the plugin code lives in @@ -107,7 +107,7 @@ namespace Artemis.Core /// If found, the instance of the feature public T? GetFeature() where T : PluginFeature { - return _features.FirstOrDefault(i => i is T) as T; + return _features.FirstOrDefault(i => i.Instance is T) as T; } /// @@ -122,31 +122,32 @@ namespace Artemis.Core Entity.IsEnabled = IsEnabled; } - internal void AddFeature(PluginFeature feature) + internal void AddFeature(PluginFeatureInfo featureInfo) { - feature.Plugin = this; - _features.Add(feature); + if (featureInfo.Plugin != this) + throw new ArtemisCoreException("Feature is not associated with this plugin"); + _features.Add(featureInfo); - OnFeatureAdded(new PluginFeatureEventArgs(feature)); + OnFeatureAdded(new PluginFeatureInfoEventArgs(featureInfo)); } - internal void RemoveFeature(PluginFeature feature) + internal void RemoveFeature(PluginFeatureInfo featureInfo) { - if (feature.IsEnabled) + if (featureInfo.Instance != null && featureInfo.Instance.IsEnabled) throw new ArtemisCoreException("Cannot remove an enabled feature from a plugin"); - - _features.Remove(feature); - feature.Dispose(); - OnFeatureRemoved(new PluginFeatureEventArgs(feature)); + _features.Remove(featureInfo); + featureInfo.Instance?.Dispose(); + + OnFeatureRemoved(new PluginFeatureInfoEventArgs(featureInfo)); } - + internal void SetEnabled(bool enable) { if (IsEnabled == enable) return; - if (!enable && Features.Any(e => e.IsEnabled)) + if (!enable && Features.Any(e => e.Instance != null && e.Instance.IsEnabled)) throw new ArtemisCoreException("Cannot disable this plugin because it still has enabled features"); IsEnabled = enable; @@ -176,8 +177,8 @@ namespace Artemis.Core { if (disposing) { - foreach (PluginFeature feature in Features) - feature.Dispose(); + foreach (PluginFeatureInfo feature in Features) + feature.Instance?.Dispose(); SetEnabled(false); Kernel?.Dispose(); @@ -189,7 +190,7 @@ namespace Artemis.Core _features.Clear(); } } - + /// public void Dispose() { @@ -198,7 +199,7 @@ namespace Artemis.Core } #endregion - + #region Events /// @@ -214,12 +215,12 @@ namespace Artemis.Core /// /// Occurs when an feature is loaded and added to the plugin /// - public event EventHandler? FeatureAdded; + public event EventHandler? FeatureAdded; /// /// Occurs when an feature is disabled and removed from the plugin /// - public event EventHandler? FeatureRemoved; + public event EventHandler? FeatureRemoved; /// /// Invokes the Enabled event @@ -240,7 +241,7 @@ namespace Artemis.Core /// /// Invokes the FeatureAdded event /// - protected virtual void OnFeatureAdded(PluginFeatureEventArgs e) + protected virtual void OnFeatureAdded(PluginFeatureInfoEventArgs e) { FeatureAdded?.Invoke(this, e); } @@ -248,7 +249,7 @@ namespace Artemis.Core /// /// Invokes the FeatureRemoved event /// - protected virtual void OnFeatureRemoved(PluginFeatureEventArgs e) + protected virtual void OnFeatureRemoved(PluginFeatureInfoEventArgs e) { FeatureRemoved?.Invoke(this, e); } diff --git a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs index 7fe5a20de..b8515197f 100644 --- a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs +++ b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs @@ -1,4 +1,5 @@ -using Artemis.Core.DataModelExpansions; +using System; +using Artemis.Core.DataModelExpansions; using Artemis.Core.DeviceProviders; using Artemis.Core.LayerBrushes; using Artemis.Core.LayerEffects; @@ -16,22 +17,48 @@ namespace Artemis.Core { private string? _description; private string? _icon; + private PluginFeature? _instance; private string _name = null!; - private PluginFeature _pluginFeature = null!; - internal PluginFeatureInfo() + internal PluginFeatureInfo(Plugin plugin, Type featureType, PluginFeatureAttribute? attribute) { + Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin)); + FeatureType = featureType ?? throw new ArgumentNullException(nameof(featureType)); + + Name = attribute?.Name ?? featureType.Name.Humanize(LetterCasing.Title); + Description = attribute?.Description; + Icon = attribute?.Icon; + + if (Icon != null) return; + if (typeof(BaseDataModelExpansion).IsAssignableFrom(featureType)) + Icon = "TableAdd"; + else if (typeof(DeviceProvider).IsAssignableFrom(featureType)) + Icon = "Devices"; + else if (typeof(ProfileModule).IsAssignableFrom(featureType)) + Icon = "VectorRectangle"; + else if (typeof(Module).IsAssignableFrom(featureType)) + Icon = "GearBox"; + else if (typeof(LayerBrushProvider).IsAssignableFrom(featureType)) + Icon = "Brush"; + else if (typeof(LayerEffectProvider).IsAssignableFrom(featureType)) + Icon = "AutoAwesome"; + else + Icon = "Plugin"; } - internal PluginFeatureInfo(PluginFeature instance, PluginFeatureAttribute? attribute) + internal PluginFeatureInfo(Plugin plugin, PluginFeatureAttribute? attribute, PluginFeature instance) { + if (instance == null) throw new ArgumentNullException(nameof(instance)); + Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin)); + FeatureType = instance.GetType(); + Name = attribute?.Name ?? instance.GetType().Name.Humanize(LetterCasing.Title); Description = attribute?.Description; Icon = attribute?.Icon; - PluginFeature = instance; + Instance = instance; if (Icon != null) return; - Icon = PluginFeature switch + Icon = Instance switch { BaseDataModelExpansion => "TableAdd", DeviceProvider => "Devices", @@ -43,6 +70,16 @@ namespace Artemis.Core }; } + /// + /// Gets the plugin this feature info is associated with + /// + public Plugin Plugin { get; } + + /// + /// Gets the type of the feature + /// + public Type FeatureType { get; } + /// /// The name of the plugin /// @@ -75,18 +112,18 @@ namespace Artemis.Core } /// - /// Gets the plugin this info is associated with + /// Gets the feature this info is associated with /// - public PluginFeature PluginFeature + public PluginFeature? Instance { - get => _pluginFeature; - internal set => SetAndNotify(ref _pluginFeature, value); + get => _instance; + internal set => SetAndNotify(ref _instance, value); } /// public override string ToString() { - return PluginFeature.Id; + return Instance?.Id ?? "Uninitialized feature"; } } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Core/BuildInfo.cs b/src/Artemis.Core/Services/Core/BuildInfo.cs index a2caaa3cf..5de246d43 100644 --- a/src/Artemis.Core/Services/Core/BuildInfo.cs +++ b/src/Artemis.Core/Services/Core/BuildInfo.cs @@ -38,5 +38,10 @@ namespace Artemis.Core.Services.Core /// [JsonProperty] public string SourceVersion { get; internal set; } = null!; + + /// + /// Gets a boolean indicating whether the current build is a local build + /// + public bool IsLocalBuild { get; internal set; } } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs b/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs index 887321a72..7643c93ae 100644 --- a/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs +++ b/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs @@ -71,7 +71,7 @@ namespace Artemis.Core.Services Plugin ImportPlugin(string fileName); /// - /// Unloads and permanently removes the provided plugin + /// Unloads and permanently removes the provided plugin /// /// The plugin to remove void RemovePlugin(Plugin plugin); @@ -127,6 +127,13 @@ namespace Artemis.Core.Services /// DeviceProvider GetDeviceProviderByDevice(IRGBDevice device); + /// + /// Queues an action for the provided plugin for the next time Artemis starts, before plugins are loaded + /// + /// The plugin to queue the action for + /// The action to take + void QueuePluginAction(Plugin plugin, PluginManagementAction pluginAction); + #region Events /// diff --git a/src/Artemis.Core/Services/Interfaces/IRgbService.cs b/src/Artemis.Core/Services/Interfaces/IRgbService.cs index d478d8a00..565fb31a3 100644 --- a/src/Artemis.Core/Services/Interfaces/IRgbService.cs +++ b/src/Artemis.Core/Services/Interfaces/IRgbService.cs @@ -52,7 +52,7 @@ namespace Artemis.Core.Services bool IsRenderPaused { get; set; } /// - /// Recreates the Texture to use the given + /// Recreates the Texture to use the given /// /// void UpdateTexture(SKBitmap bitmap); @@ -119,7 +119,16 @@ namespace Artemis.Core.Services /// void SaveDevices(); + /// + /// Enables the provided device + /// + /// The device to enable void EnableDevice(ArtemisDevice device); + + /// + /// Disables the provided device + /// + /// The device to disable void DisableDevice(ArtemisDevice device); /// @@ -133,7 +142,7 @@ namespace Artemis.Core.Services event EventHandler DeviceRemoved; /// - /// Occurs when the surface has had modifications to its LED collection + /// Occurs when the surface has had modifications to its LED collection /// event EventHandler LedsChanged; } diff --git a/src/Artemis.Core/Services/ModuleService.cs b/src/Artemis.Core/Services/ModuleService.cs index 6d44ecb53..f71cfac76 100644 --- a/src/Artemis.Core/Services/ModuleService.cs +++ b/src/Artemis.Core/Services/ModuleService.cs @@ -67,7 +67,7 @@ namespace Artemis.Core.Services // If this is a profile module, animate profile disable // module.Deactivate would do the same but without animation if (module.IsActivated && module is ProfileModule profileModule) - await profileModule.ChangeActiveProfileAnimated(null, null); + await profileModule.ChangeActiveProfileAnimated(null, Enumerable.Empty()); module.Deactivate(false); } @@ -210,6 +210,7 @@ namespace Artemis.Core.Services List modules = _pluginManagementService.GetFeaturesOfType().ToList(); List tasks = new(); foreach (Module module in modules) + { lock (module) { bool shouldBeActivated = module.EvaluateActivationRequirements() && module.IsEnabled; @@ -218,6 +219,7 @@ namespace Artemis.Core.Services else if (!shouldBeActivated && module.IsActivated) tasks.Add(DeactivateModule(module)); } + } await Task.WhenAll(tasks); diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 66b7933ec..cadb77caf 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -5,7 +5,6 @@ using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; -using System.Runtime.Loader; using Artemis.Core.DeviceProviders; using Artemis.Core.Ninject; using Artemis.Storage.Entities.Plugins; @@ -28,6 +27,7 @@ namespace Artemis.Core.Services private readonly ILogger _logger; private readonly IPluginRepository _pluginRepository; private readonly List _plugins; + private bool _isElevated; public PluginManagementService(IKernel kernel, ILogger logger, IPluginRepository pluginRepository) { @@ -35,6 +35,8 @@ namespace Artemis.Core.Services _logger = logger; _pluginRepository = pluginRepository; _plugins = new List(); + + ProcessQueuedActions(); } private void CopyBuiltInPlugin(FileInfo zipFileInfo, ZipArchive zipArchive) @@ -130,7 +132,11 @@ namespace Artemis.Core.Services { lock (_plugins) { - return _plugins.Where(p => p.IsEnabled).SelectMany(p => p.Features.Where(i => i.IsEnabled && i is T)).Cast().ToList(); + return _plugins.Where(p => p.IsEnabled) + .SelectMany(p => p.Features.Where(f => f.Instance != null && f.Instance.IsEnabled && f.Instance is T)) + .Select(f => f.Instance) + .Cast() + .ToList(); } } @@ -178,6 +184,7 @@ namespace Artemis.Core.Services if (LoadingPlugins) throw new ArtemisCoreException("Cannot load plugins while a previous load hasn't been completed yet."); + _isElevated = isElevated; LoadingPlugins = true; // Unload all currently loaded plugins first @@ -186,6 +193,7 @@ namespace Artemis.Core.Services // Load the plugin assemblies into the plugin context DirectoryInfo pluginDirectory = new(Path.Combine(Constants.DataFolder, "plugins")); foreach (DirectoryInfo subDirectory in pluginDirectory.EnumerateDirectories()) + { try { LoadPlugin(subDirectory); @@ -194,11 +202,12 @@ namespace Artemis.Core.Services { _logger.Warning(new ArtemisPluginException("Failed to load plugin", e), "Plugin exception"); } + } // ReSharper disable InconsistentlySynchronizedField - It's read-only, idc _logger.Debug("Loaded {count} plugin(s)", _plugins.Count); - bool adminRequired = _plugins.Any(p => p.Entity.IsEnabled && p.Info.RequiresAdmin); + bool adminRequired = _plugins.Any(p => p.Info.RequiresAdmin && p.Entity.IsEnabled && p.Entity.Features.Any(f => f.IsEnabled)); if (!isElevated && adminRequired) { _logger.Information("Restarting because one or more plugins requires elevation"); @@ -218,7 +227,7 @@ namespace Artemis.Core.Services foreach (Plugin plugin in _plugins.Where(p => p.Entity.IsEnabled)) EnablePlugin(plugin, false, ignorePluginLock); - _logger.Debug("Enabled {count} plugin(s)", _plugins.Where(p => p.IsEnabled).Sum(p => p.Features.Count(f => f.IsEnabled))); + _logger.Debug("Enabled {count} plugin(s)", _plugins.Count(p => p.IsEnabled)); // ReSharper restore InconsistentlySynchronizedField LoadingPlugins = false; @@ -289,6 +298,28 @@ namespace Artemis.Core.Services throw new ArtemisPluginException(plugin, "Failed to load the plugins assembly", e); } + // Get the Plugin feature from the main assembly and if there is only one, instantiate it + List featureTypes; + try + { + featureTypes = plugin.Assembly.GetTypes().Where(t => typeof(PluginFeature).IsAssignableFrom(t)).ToList(); + } + catch (ReflectionTypeLoadException e) + { + throw new ArtemisPluginException( + plugin, + "Failed to initialize the plugin assembly", + // ReSharper disable once RedundantEnumerableCastCall - Casting from nullable to non-nullable here + new AggregateException(e.LoaderExceptions.Where(le => le != null).Cast().ToArray()) + ); + } + + foreach (Type featureType in featureTypes) + plugin.AddFeature(new PluginFeatureInfo(plugin, featureType, (PluginFeatureAttribute?) Attribute.GetCustomAttribute(featureType, typeof(PluginFeatureAttribute)))); + + if (!featureTypes.Any()) + _logger.Warning("Plugin {plugin} contains no features", plugin); + List bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(IPluginBootstrapper).IsAssignableFrom(t)).ToList(); if (bootstrappers.Count > 1) _logger.Warning($"{plugin} has more than one bootstrapper, only initializing {bootstrappers.First().FullName}"); @@ -309,66 +340,64 @@ namespace Artemis.Core.Services if (plugin.Assembly == null) throw new ArtemisPluginException(plugin, "Cannot enable a plugin that hasn't successfully been loaded"); + if (plugin.Info.RequiresAdmin && plugin.Entity.Features.Any(f => f.IsEnabled) && !_isElevated) + { + if (!saveState) + throw new ArtemisCoreException("Cannot enable a plugin that requires elevation without saving it's state."); + + plugin.Entity.IsEnabled = true; + SavePlugin(plugin); + + _logger.Information("Restarting because a newly enabled plugin requires elevation"); + Utilities.Restart(true, TimeSpan.FromMilliseconds(500)); + return; + } + // Create the Ninject child kernel and load the module plugin.Kernel = new ChildKernel(_kernel, new PluginModule(plugin)); OnPluginEnabling(new PluginEventArgs(plugin)); plugin.SetEnabled(true); - // Get the Plugin feature from the main assembly and if there is only one, instantiate it - List featureTypes; - try - { - featureTypes = plugin.Assembly.GetTypes().Where(t => typeof(PluginFeature).IsAssignableFrom(t)).ToList(); - } - catch (ReflectionTypeLoadException e) - { - throw new ArtemisPluginException( - plugin, - "Failed to initialize the plugin assembly", - // ReSharper disable once RedundantEnumerableCastCall - Casting from nullable to non-nullable here - new AggregateException(e.LoaderExceptions.Where(le => le != null).Cast().ToArray()) - ); - } - - if (!featureTypes.Any()) - _logger.Warning("Plugin {plugin} contains no features", plugin); - - // Create instances of each feature and add them to the plugin + // Create instances of each feature // Construction should be simple and not contain any logic so failure at this point means the entire plugin fails - foreach (Type featureType in featureTypes) + foreach (PluginFeatureInfo featureInfo in plugin.Features) + { try { - plugin.Kernel.Bind(featureType).ToSelf().InSingletonScope(); + plugin.Kernel.Bind(featureInfo.FeatureType).ToSelf().InSingletonScope(); // Include Plugin as a parameter for the PluginSettingsProvider IParameter[] parameters = {new Parameter("Plugin", plugin, false)}; - PluginFeature instance = (PluginFeature) plugin.Kernel.Get(featureType, parameters); + PluginFeature instance = (PluginFeature) plugin.Kernel.Get(featureInfo.FeatureType, parameters); // Get the PluginFeature attribute which contains extra info on the feature - PluginFeatureAttribute? pluginFeatureAttribute = (PluginFeatureAttribute?) Attribute.GetCustomAttribute(featureType, typeof(PluginFeatureAttribute)); - instance.Info = new PluginFeatureInfo(instance, pluginFeatureAttribute); - plugin.AddFeature(instance); + featureInfo.Instance = instance; + instance.Info = featureInfo; + instance.Plugin = plugin; // Load the enabled state and if not found, default to true - instance.Entity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureType.FullName) ?? - new PluginFeatureEntity {IsEnabled = plugin.Info.AutoEnableFeatures, Type = featureType.FullName!}; + instance.Entity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureInfo.FeatureType.FullName) ?? + new PluginFeatureEntity {IsEnabled = plugin.Info.AutoEnableFeatures, Type = featureInfo.FeatureType.FullName!}; } catch (Exception e) { _logger.Warning(new ArtemisPluginException(plugin, "Failed to instantiate feature", e), "Failed to instantiate feature", plugin); } + } - // Activate plugins after they are all loaded - foreach (PluginFeature pluginFeature in plugin.Features.Where(i => i.Entity.IsEnabled)) + // Activate features after they are all loaded + foreach (PluginFeatureInfo pluginFeature in plugin.Features.Where(f => f.Instance != null && f.Instance.Entity.IsEnabled)) + { try { - EnablePluginFeature(pluginFeature, false, !ignorePluginLock); + EnablePluginFeature(pluginFeature.Instance!, false, !ignorePluginLock); } catch (Exception) { // ignored, logged in EnablePluginFeature } + } if (saveState) { @@ -406,12 +435,10 @@ namespace Artemis.Core.Services if (!plugin.IsEnabled) return; - while (plugin.Features.Any()) + foreach (PluginFeatureInfo pluginFeatureInfo in plugin.Features) { - PluginFeature feature = plugin.Features[0]; - if (feature.IsEnabled) - DisablePluginFeature(feature, false); - plugin.RemoveFeature(feature); + if (pluginFeatureInfo.Instance != null && pluginFeatureInfo.Instance.IsEnabled) + DisablePluginFeature(pluginFeatureInfo.Instance, false); } plugin.SetEnabled(false); @@ -450,7 +477,6 @@ namespace Artemis.Core.Services Plugin? existing = _plugins.FirstOrDefault(p => p.Guid == pluginInfo.Guid); if (existing != null) - { try { RemovePlugin(existing); @@ -459,7 +485,6 @@ 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; @@ -515,6 +540,21 @@ namespace Artemis.Core.Services _logger.Verbose("Enabling plugin feature {feature} - {plugin}", pluginFeature, pluginFeature.Plugin); OnPluginFeatureEnabling(new PluginFeatureEventArgs(pluginFeature)); + + if (pluginFeature.Plugin.Info.RequiresAdmin && !_isElevated) + { + if (!saveState) + throw new ArtemisCoreException("Cannot enable a feature that requires elevation without saving it's state."); + + pluginFeature.Entity.IsEnabled = true; + pluginFeature.Plugin.Entity.IsEnabled = true; + SavePlugin(pluginFeature.Plugin); + + _logger.Information("Restarting because a newly enabled feature requires elevation"); + Utilities.Restart(true, TimeSpan.FromMilliseconds(500)); + return; + } + try { pluginFeature.SetEnabled(true, isAutoEnable); @@ -578,13 +618,43 @@ namespace Artemis.Core.Services #endregion + #region Queued actions + + public void QueuePluginAction(Plugin plugin, PluginManagementAction pluginAction) + { + List existing = _pluginRepository.GetQueuedActions(plugin.Guid); + if (existing.Any(e => pluginAction == PluginManagementAction.Delete && e is PluginQueuedDeleteEntity)) + return; + + if (pluginAction == PluginManagementAction.Delete) + _pluginRepository.AddQueuedAction(new PluginQueuedDeleteEntity {PluginGuid = plugin.Guid, Directory = plugin.Directory.FullName}); + } + + private void ProcessQueuedActions() + { + foreach (PluginQueuedActionEntity pluginQueuedActionEntity in _pluginRepository.GetQueuedActions()) + { + if (pluginQueuedActionEntity is PluginQueuedDeleteEntity deleteAction) + { + if (Directory.Exists(deleteAction.Directory)) + Directory.Delete(deleteAction.Directory, true); + } + + _pluginRepository.RemoveQueuedAction(pluginQueuedActionEntity); + } + } + + #endregion + #region Storage private void SavePlugin(Plugin plugin) { - foreach (PluginFeature pluginFeature in plugin.Features) - if (plugin.Entity.Features.All(i => i.Type != pluginFeature.GetType().FullName)) - plugin.Entity.Features.Add(pluginFeature.Entity); + foreach (PluginFeatureInfo featureInfo in plugin.Features.Where(i => i.Instance != null)) + { + if (plugin.Entity.Features.All(i => i.Type != featureInfo.FeatureType.FullName)) + plugin.Entity.Features.Add(featureInfo.Instance!.Entity); + } _pluginRepository.SavePlugin(plugin.Entity); } @@ -663,4 +733,20 @@ namespace Artemis.Core.Services #endregion } + + /// + /// Represents a type of plugin management action + /// + public enum PluginManagementAction + { + /// + /// A plugin management action that removes a plugin + /// + Delete, + + // /// + // /// A plugin management action that updates a plugin + // /// + // Update + } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/RgbService.cs b/src/Artemis.Core/Services/RgbService.cs index 30e359658..6d322b8d0 100644 --- a/src/Artemis.Core/Services/RgbService.cs +++ b/src/Artemis.Core/Services/RgbService.cs @@ -197,7 +197,7 @@ namespace Artemis.Core.Services public ArtemisLayout ApplyBestDeviceLayout(ArtemisDevice device) { ArtemisLayout layout; - + // Configured layout path takes precedence over all other options if (device.CustomLayoutPath != null) { @@ -239,6 +239,7 @@ namespace Artemis.Core.Services public void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout layout, bool createMissingLeds, bool removeExessiveLeds) { device.ApplyLayout(layout, createMissingLeds, removeExessiveLeds); + UpdateLedGroup(); } public ArtemisDevice? GetDevice(IRGBDevice rgbDevice) diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs index e69ca7f4f..bdae901c8 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -146,7 +146,8 @@ namespace Artemis.Core.Services void ActivatingProfilePluginToggle(object? sender, PluginEventArgs e) { - InstantiateProfile(profile); + if (!profile.Disposed) + InstantiateProfile(profile); } // This could happen during activation so subscribe to it diff --git a/src/Artemis.Core/Services/WebServer/WebServerService.cs b/src/Artemis.Core/Services/WebServer/WebServerService.cs index d74035da2..55c7cbcfb 100644 --- a/src/Artemis.Core/Services/WebServer/WebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/WebServerService.cs @@ -61,7 +61,7 @@ namespace Artemis.Core.Services server.StateChanged += (s, e) => _logger.Verbose("WebServer new state - {state}", e.NewState); // Store the URL in a webserver.txt file so that remote applications can find it - File.WriteAllText(Path.Combine(Constants.DataFolder, "webserver.txt"), url); + File.WriteAllText(Path.Combine(Constants.DataFolder, "webserver.txt"), $"http://localhost:{_webServerPortSetting.Value}/"); return server; } diff --git a/src/Artemis.Core/Utilities/CorePluginFeature.cs b/src/Artemis.Core/Utilities/CorePluginFeature.cs index 897c46b88..66dcb40ff 100644 --- a/src/Artemis.Core/Utilities/CorePluginFeature.cs +++ b/src/Artemis.Core/Utilities/CorePluginFeature.cs @@ -9,7 +9,7 @@ namespace Artemis.Core { public CorePluginFeature() { - Constants.CorePlugin.AddFeature(this); + Constants.CorePlugin.AddFeature(new PluginFeatureInfo(Constants.CorePlugin, null, this)); IsEnabled = true; } diff --git a/src/Artemis.Core/Utilities/IntroAnimation.cs b/src/Artemis.Core/Utilities/IntroAnimation.cs index f2f1bb4fb..4b6d36ffd 100644 --- a/src/Artemis.Core/Utilities/IntroAnimation.cs +++ b/src/Artemis.Core/Utilities/IntroAnimation.cs @@ -44,7 +44,7 @@ namespace Artemis.Core foreach (LayerEntity profileEntityLayer in profileEntity.Layers) profileEntityLayer.Leds.AddRange(_devices.SelectMany(d => d.Leds).Select(l => new LedEntity { - DeviceIdentifier = l.Device.RgbDevice.GetDeviceIdentifier(), + DeviceIdentifier = l.Device.Identifier, LedName = l.RgbLed.Id.ToString() })); diff --git a/src/Artemis.Storage/Entities/Plugins/PluginSettingEntity.cs b/src/Artemis.Storage/Entities/Plugins/PluginSettingEntity.cs index ae4827426..08a17f922 100644 --- a/src/Artemis.Storage/Entities/Plugins/PluginSettingEntity.cs +++ b/src/Artemis.Storage/Entities/Plugins/PluginSettingEntity.cs @@ -13,4 +13,21 @@ namespace Artemis.Storage.Entities.Plugins public string Name { get; set; } public string Value { get; set; } } + + /// + /// Represents a queued action for a plugin + /// + public abstract class PluginQueuedActionEntity + { + public Guid Id { get; set; } + public Guid PluginGuid { get; set; } + } + + /// + /// Represents a queued delete action for a plugin + /// + public class PluginQueuedDeleteEntity : PluginQueuedActionEntity + { + public string Directory { get; set; } + } } \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/Interfaces/IPluginRepository.cs b/src/Artemis.Storage/Repositories/Interfaces/IPluginRepository.cs index 9a1258416..d93ba4628 100644 --- a/src/Artemis.Storage/Repositories/Interfaces/IPluginRepository.cs +++ b/src/Artemis.Storage/Repositories/Interfaces/IPluginRepository.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Artemis.Storage.Entities.Plugins; namespace Artemis.Storage.Repositories.Interfaces @@ -8,9 +9,15 @@ namespace Artemis.Storage.Repositories.Interfaces void AddPlugin(PluginEntity pluginEntity); PluginEntity GetPluginByGuid(Guid pluginGuid); void SavePlugin(PluginEntity pluginEntity); + void AddSetting(PluginSettingEntity pluginSettingEntity); PluginSettingEntity GetSettingByGuid(Guid pluginGuid); PluginSettingEntity GetSettingByNameAndGuid(string name, Guid pluginGuid); void SaveSetting(PluginSettingEntity pluginSettingEntity); + + void AddQueuedAction(PluginQueuedActionEntity pluginQueuedActionEntity); + List GetQueuedActions(); + List GetQueuedActions(Guid pluginGuid); + void RemoveQueuedAction(PluginQueuedActionEntity pluginQueuedActionEntity); } } \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/PluginRepository.cs b/src/Artemis.Storage/Repositories/PluginRepository.cs index b97c8fc83..5a923a60a 100644 --- a/src/Artemis.Storage/Repositories/PluginRepository.cs +++ b/src/Artemis.Storage/Repositories/PluginRepository.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Artemis.Storage.Entities.Plugins; using Artemis.Storage.Repositories.Interfaces; using LiteDB; @@ -14,6 +15,7 @@ namespace Artemis.Storage.Repositories _repository = repository; _repository.Database.GetCollection().EnsureIndex(s => new {s.Name, s.PluginGuid}, true); + _repository.Database.GetCollection().EnsureIndex(s => s.PluginGuid); } public void AddPlugin(PluginEntity pluginEntity) @@ -51,5 +53,24 @@ namespace Artemis.Storage.Repositories { _repository.Upsert(pluginSettingEntity); } + + public List GetQueuedActions() + { + return _repository.Query().ToList(); + } + + public List GetQueuedActions(Guid pluginGuid) + { + return _repository.Query().Where(q => q.PluginGuid == pluginGuid).ToList(); + } + + public void AddQueuedAction(PluginQueuedActionEntity pluginQueuedActionEntity) + { + _repository.Upsert(pluginQueuedActionEntity); + } + public void RemoveQueuedAction(PluginQueuedActionEntity pluginQueuedActionEntity) + { + _repository.Delete(pluginQueuedActionEntity.Id); + } } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs b/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs index 35adaf092..640812016 100644 --- a/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs +++ b/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs @@ -17,7 +17,7 @@ namespace Artemis.UI.Shared private SolidColorBrush? _renderColorBrush; private Color _renderColor; - + public DeviceVisualizerLed(ArtemisLed led) { Led = led; @@ -51,7 +51,7 @@ namespace Artemis.UI.Shared byte g = Led.RgbLed.Color.GetG(); byte b = Led.RgbLed.Color.GetB(); - _renderColor.A = (byte)(isDimmed ? 100 : 255); + _renderColor.A = (byte) (isDimmed ? 100 : 255); _renderColor.A = isDimmed ? Dimmed : NonDimmed; _renderColor.R = r; _renderColor.G = g; @@ -135,6 +135,8 @@ namespace Artemis.UI.Shared { try { + double width = Led.RgbLed.Size.Width - deflateAmount; + double height = Led.RgbLed.Size.Height - deflateAmount; // DisplayGeometry = Geometry.Parse(Led.RgbLed.ShapeData); DisplayGeometry = Geometry.Combine( Geometry.Empty, @@ -144,11 +146,27 @@ namespace Artemis.UI.Shared { Children = new TransformCollection { - new ScaleTransform(Led.RgbLed.Size.Width - deflateAmount, Led.RgbLed.Size.Height - deflateAmount), + new ScaleTransform(width, height), new TranslateTransform(deflateAmount / 2, deflateAmount / 2) } } ); + + if (DisplayGeometry.Bounds.Width > width) + { + DisplayGeometry = Geometry.Combine(Geometry.Empty, DisplayGeometry, GeometryCombineMode.Union, new TransformGroup + { + Children = new TransformCollection {new ScaleTransform(width / DisplayGeometry.Bounds.Width, 1)} + }); + } + + if (DisplayGeometry.Bounds.Height > height) + { + DisplayGeometry = Geometry.Combine(Geometry.Empty, DisplayGeometry, GeometryCombineMode.Union, new TransformGroup + { + Children = new TransformCollection {new ScaleTransform(1, height / DisplayGeometry.Bounds.Height)} + }); + } } catch (Exception) { diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs index 55ac8e376..f7ead0cbb 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Timers; @@ -139,7 +140,11 @@ namespace Artemis.UI.Shared.Input set { if (!SetAndNotify(ref _isDataModelViewModelOpen, value)) return; - if (value) UpdateDataModelVisualization(); + if (value) + { + UpdateDataModelVisualization(); + OpenSelectedValue(DataModelViewModel); + } } } @@ -303,6 +308,21 @@ namespace Artemis.UI.Shared.Input extraDataModelViewModel.Update(_dataModelUIService, new DataModelUpdateConfiguration(LoadEventChildren)); } + private void OpenSelectedValue(DataModelVisualizationViewModel dataModelPropertiesViewModel) + { + if (DataModelPath == null) + return; + + if (dataModelPropertiesViewModel.Children.Any(c => c.DataModelPath != null && DataModelPath.Path.StartsWith(c.DataModelPath.Path))) + { + dataModelPropertiesViewModel.IsVisualizationExpanded = true; + foreach (DataModelVisualizationViewModel dataModelVisualizationViewModel in dataModelPropertiesViewModel.Children) + { + OpenSelectedValue(dataModelVisualizationViewModel); + } + } + } + #endregion #region Events diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index 451ff0b03..127c6a5c9 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -334,9 +334,6 @@ - - PreserveNewest - SettingsSingleFileGenerator Settings.Designer.cs diff --git a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs index 8dd40960e..02d543a4c 100644 --- a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -39,7 +39,7 @@ namespace Artemis.UI.Ninject.Factories public interface ISettingsVmFactory : IVmFactory { PluginSettingsViewModel CreatePluginSettingsViewModel(Plugin plugin); - PluginFeatureViewModel CreatePluginFeatureViewModel(PluginFeature feature); + PluginFeatureViewModel CreatePluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, bool showShield); DeviceSettingsViewModel CreateDeviceSettingsViewModel(ArtemisDevice device); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs index b6bbf11aa..59856ce6f 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs @@ -136,7 +136,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions RenderProfileElement.DisplayCondition.ChildRemoved += DisplayConditionOnChildrenModified; } - private void CoreServiceOnFrameRendered(object? sender, FrameRenderedEventArgs e) + private void CoreServiceOnFrameRendered(object sender, FrameRenderedEventArgs e) { ActiveItem?.Evaluate(); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsViewModel.cs index 00c72fc34..aee7265a0 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsViewModel.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; using Artemis.Core; using Artemis.UI.Ninject.Factories; @@ -14,7 +13,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings { private readonly IDataBindingsVmFactory _dataBindingsVmFactory; private readonly IProfileEditorService _profileEditorService; - private ILayerProperty? _selectedDataBinding; + private ILayerProperty _selectedDataBinding; private int _selectedItemIndex; private bool _updating; @@ -95,7 +94,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings SubscribeToSelectedDataBinding(); base.OnInitialActivate(); } - + protected override void OnActivate() { SelectedItemIndex = 0; diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLayerViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLayerViewModel.cs index b84d36b14..a54c9e995 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLayerViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLayerViewModel.cs @@ -14,8 +14,8 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization public class ProfileLayerViewModel : CanvasViewModel { private readonly ILayerEditorService _layerEditorService; - private readonly IProfileEditorService _profileEditorService; private readonly PanZoomViewModel _panZoomViewModel; + private readonly IProfileEditorService _profileEditorService; private bool _isSelected; private Geometry _shapeGeometry; private Rect _viewportRectangle; diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileView.xaml index 8441cd11d..5e025b167 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileView.xaml @@ -11,6 +11,7 @@ d:DesignHeight="510.9" d:DesignWidth="800" d:DataContext="{d:DesignInstance {x:Type visualization:ProfileViewModel}}"> + @@ -23,16 +24,16 @@ - + - + - + - + @@ -133,9 +134,9 @@ - Highlight LEDs of selected layer + Focus selected layer .Collection.AllActive, IProfileEditorPanelViewModel, IHandle, IHandle + public class ProfileViewModel : Conductor.Collection.AllActive, IProfileEditorPanelViewModel, IHandle { private readonly ICoreService _coreService; private readonly IProfileEditorService _profileEditorService; @@ -31,7 +31,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization private bool _canSelectEditTool; private BindableCollection _devices; private BindableCollection _highlightedLeds; - private PluginSetting _highlightSelectedLayer; + private PluginSetting _focusSelectedLayer; private DateTime _lastUpdate; private PanZoomViewModel _panZoomViewModel; private Layer _previousSelectedLayer; @@ -55,7 +55,6 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization eventAggregator.Subscribe(this); } - public bool CanSelectEditTool { get => _canSelectEditTool; @@ -86,10 +85,10 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization set => SetAndNotify(ref _alwaysApplyDataBindings, value); } - public PluginSetting HighlightSelectedLayer + public PluginSetting FocusSelectedLayer { - get => _highlightSelectedLayer; - set => SetAndNotify(ref _highlightSelectedLayer, value); + get => _focusSelectedLayer; + set => SetAndNotify(ref _focusSelectedLayer, value); } public VisualizationToolViewModel ActiveToolViewModel @@ -146,12 +145,12 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization ApplyActiveProfile(); AlwaysApplyDataBindings = _settingsService.GetSetting("ProfileEditor.AlwaysApplyDataBindings", true); - HighlightSelectedLayer = _settingsService.GetSetting("ProfileEditor.HighlightSelectedLayer", true); + FocusSelectedLayer = _settingsService.GetSetting("ProfileEditor.FocusSelectedLayer", true); _lastUpdate = DateTime.Now; _coreService.FrameRendered += OnFrameRendered; - HighlightSelectedLayer.SettingChanged += HighlightSelectedLayerOnSettingChanged; + FocusSelectedLayer.SettingChanged += HighlightSelectedLayerOnSettingChanged; _rgbService.DeviceAdded += RgbServiceOnDevicesModified; _rgbService.DeviceRemoved += RgbServiceOnDevicesModified; _profileEditorService.ProfileSelected += OnProfileSelected; @@ -164,7 +163,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization protected override void OnClose() { _coreService.FrameRendered -= OnFrameRendered; - HighlightSelectedLayer.SettingChanged -= HighlightSelectedLayerOnSettingChanged; + FocusSelectedLayer.SettingChanged -= HighlightSelectedLayerOnSettingChanged; _rgbService.DeviceAdded -= RgbServiceOnDevicesModified; _rgbService.DeviceRemoved -= RgbServiceOnDevicesModified; _profileEditorService.ProfileSelected -= OnProfileSelected; @@ -174,7 +173,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization _previousSelectedLayer.LayerBrushUpdated -= SelectedLayerOnLayerBrushUpdated; AlwaysApplyDataBindings.Save(); - HighlightSelectedLayer.Save(); + FocusSelectedLayer.Save(); base.OnClose(); } @@ -191,8 +190,10 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization // Add new layers missing a VM foreach (Layer layer in layers) + { if (layerViewModels.All(vm => vm.Layer != layer)) Items.Add(_profileLayerVmFactory.Create(layer, PanZoomViewModel)); + } // Remove layers that no longer exist IEnumerable toRemove = layerViewModels.Where(vm => !layers.Contains(vm.Layer)); @@ -209,7 +210,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization private void UpdateLedsDimStatus() { HighlightedLeds.Clear(); - if (HighlightSelectedLayer.Value && _profileEditorService.SelectedProfileElement is Layer layer) + if (FocusSelectedLayer.Value && _profileEditorService.SelectedProfileElement is Layer layer) HighlightedLeds.AddRange(layer.Leds); } @@ -234,6 +235,9 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization private void ActivateToolByIndex(int value) { + if (value == 1 && !CanSelectEditTool) + return; + switch (value) { case 0: @@ -262,7 +266,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization Devices.Max(d => d.Rectangle.Right), Devices.Max(d => d.Rectangle.Bottom) ); - + PanZoomViewModel.Reset(rect); } @@ -274,12 +278,14 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization { ((IInputElement) sender).CaptureMouse(); ActiveToolViewModel?.MouseDown(sender, e); + e.Handled = false; } public void CanvasMouseUp(object sender, MouseButtonEventArgs e) { ((IInputElement) sender).ReleaseMouseCapture(); ActiveToolViewModel?.MouseUp(sender, e); + e.Handled = false; } public void CanvasMouseMove(object sender, MouseEventArgs e) @@ -382,24 +388,6 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization ActivateToolByIndex(2); } - public void Handle(MainWindowFocusChangedEvent message) - { - // if (PauseRenderingOnFocusLoss == null || ScreenState != ScreenState.Active) - // return; - // - // try - // { - // if (PauseRenderingOnFocusLoss.Value && !message.IsFocused) - // _updateTrigger.Stop(); - // else if (PauseRenderingOnFocusLoss.Value && message.IsFocused) - // _updateTrigger.Start(); - // } - // catch (NullReferenceException) - // { - // // TODO: Remove when fixed in RGB.NET, or avoid double stopping - // } - } - public void Handle(MainWindowKeyEvent message) { if (message.KeyDown) @@ -412,6 +400,17 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization } ActiveToolViewModel?.KeyDown(message.EventArgs); + + // If T is pressed while Ctrl is down, that makes it Ctrl+T > swap to transformation tool on Ctrl release + if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.LeftCtrl)) + { + if (message.EventArgs.Key == Key.T) + _previousTool = 1; + else if (message.EventArgs.Key == Key.Q) + _previousTool = 2; + else if (message.EventArgs.Key == Key.W) + _previousTool = 3; + } } else { diff --git a/src/Artemis.UI/Screens/Settings/Device/DeviceDialogView.xaml b/src/Artemis.UI/Screens/Settings/Device/DeviceDialogView.xaml index 7b82c904e..9e8dc0c23 100644 --- a/src/Artemis.UI/Screens/Settings/Device/DeviceDialogView.xaml +++ b/src/Artemis.UI/Screens/Settings/Device/DeviceDialogView.xaml @@ -24,110 +24,124 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Device/DeviceDialogViewModel.cs b/src/Artemis.UI/Screens/Settings/Device/DeviceDialogViewModel.cs index 97d3ce8fb..c1472c721 100644 --- a/src/Artemis.UI/Screens/Settings/Device/DeviceDialogViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Device/DeviceDialogViewModel.cs @@ -9,6 +9,7 @@ using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.Shared; using Artemis.UI.Shared.Services; +using MaterialDesignThemes.Wpf; using Ookii.Dialogs.Wpf; using RGB.NET.Layout; using Stylet; @@ -21,6 +22,7 @@ namespace Artemis.UI.Screens.Settings.Device private readonly IDialogService _dialogService; private readonly IRgbService _rgbService; private ArtemisLed _selectedLed; + private SnackbarMessageQueue _deviceMessageQueue; public DeviceDialogViewModel(ArtemisDevice device, IDeviceService deviceService, IRgbService rgbService, IDialogService dialogService, IDeviceDebugVmFactory factory) { @@ -38,9 +40,28 @@ namespace Artemis.UI.Screens.Settings.Device DisplayName = $"{device.RgbDevice.DeviceInfo.Model} | Artemis"; } + protected override void OnInitialActivate() + { + DeviceMessageQueue = new SnackbarMessageQueue(TimeSpan.FromSeconds(5)); + Device.DeviceUpdated += DeviceOnDeviceUpdated; + base.OnInitialActivate(); + } + + protected override void OnClose() + { + Device.DeviceUpdated -= DeviceOnDeviceUpdated; + base.OnClose(); + } + public ArtemisDevice Device { get; } public PanZoomViewModel PanZoomViewModel { get; } - + + public SnackbarMessageQueue DeviceMessageQueue + { + get => _deviceMessageQueue; + set => SetAndNotify(ref _deviceMessageQueue, value); + } + public ArtemisLed SelectedLed { get => _selectedLed; @@ -50,7 +71,10 @@ namespace Artemis.UI.Screens.Settings.Device NotifyOfPropertyChange(nameof(SelectedLeds)); } } - public List SelectedLeds => SelectedLed != null ? new List { SelectedLed } : null; + + public bool CanExportLayout => Device.Layout?.IsValid ?? false; + + public List SelectedLeds => SelectedLed != null ? new List {SelectedLed} : null; public bool CanOpenImageDirectory => Device.Layout?.Image != null; @@ -165,5 +189,14 @@ namespace Artemis.UI.Screens.Settings.Device #endregion // ReSharper restore UnusedMember.Global + + #region Event handlers + + private void DeviceOnDeviceUpdated(object? sender, EventArgs e) + { + NotifyOfPropertyChange(nameof(CanExportLayout)); + } + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Device/Tabs/DeviceInfoTabView.xaml b/src/Artemis.UI/Screens/Settings/Device/Tabs/DeviceInfoTabView.xaml index 5b4ce48a4..3f5047fb9 100644 --- a/src/Artemis.UI/Screens/Settings/Device/Tabs/DeviceInfoTabView.xaml +++ b/src/Artemis.UI/Screens/Settings/Device/Tabs/DeviceInfoTabView.xaml @@ -91,15 +91,21 @@ - Layout file path - + Text="{Binding DefaultLayoutPath}" /> @@ -108,14 +114,19 @@ Image file path - + Text="{Binding Device.Layout.Image.LocalPath}" /> \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Device/Tabs/DeviceInfoTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Device/Tabs/DeviceInfoTabViewModel.cs index e386b676a..bf97b4e21 100644 --- a/src/Artemis.UI/Screens/Settings/Device/Tabs/DeviceInfoTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Device/Tabs/DeviceInfoTabViewModel.cs @@ -1,4 +1,5 @@ -using Artemis.Core; +using System.Windows; +using Artemis.Core; using RGB.NET.Core; using Stylet; @@ -6,6 +7,8 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs { public class DeviceInfoTabViewModel : Screen { + private string _defaultLayoutPath; + public DeviceInfoTabViewModel(ArtemisDevice device) { Device = device; @@ -14,5 +17,23 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs public bool IsKeyboard => Device.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Keyboard; public ArtemisDevice Device { get; } + + public string DefaultLayoutPath + { + get => _defaultLayoutPath; + set => SetAndNotify(ref _defaultLayoutPath, value); + } + + public void CopyToClipboard(string content) + { + Clipboard.SetText(content); + ((DeviceDialogViewModel) Parent).DeviceMessageQueue.Enqueue("Copied path to clipboard."); + } + + protected override void OnInitialActivate() + { + DefaultLayoutPath = Device.DeviceProvider.LoadLayout(Device).FilePath; + base.OnInitialActivate(); + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabView.xaml b/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabView.xaml index b6e4afd4e..0d69ed4ee 100644 --- a/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabView.xaml +++ b/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabView.xaml @@ -174,20 +174,31 @@ - - - - - - + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabViewModel.cs index 19f425e70..59f3ddc57 100644 --- a/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabViewModel.cs @@ -15,7 +15,6 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs public class DevicePropertiesTabViewModel : Screen { private readonly ICoreService _coreService; - private readonly IMessageService _messageService; private readonly IDialogService _dialogService; private readonly IRgbService _rgbService; private float _blueScale; @@ -34,13 +33,11 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs public DevicePropertiesTabViewModel(ArtemisDevice device, ICoreService coreService, IRgbService rgbService, - IMessageService messageService, IDialogService dialogService, IModelValidator validator) : base(validator) { _coreService = coreService; _rgbService = rgbService; - _messageService = messageService; _dialogService = dialogService; Device = device; @@ -115,7 +112,7 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs if (e.OriginalSource is Button) { Device.CustomLayoutPath = null; - _messageService.ShowMessage("Cleared imported layout"); + ((DeviceDialogViewModel) Parent).DeviceMessageQueue.Enqueue("Cleared imported layout."); return; } @@ -126,13 +123,13 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs if (result == true) { Device.CustomLayoutPath = dialog.FileName; - _messageService.ShowMessage($"Imported layout from {dialog.FileName}"); + ((DeviceDialogViewModel) Parent).DeviceMessageQueue.Enqueue($"Imported layout from {dialog.FileName}."); } } public async Task SelectPhysicalLayout() { - await _dialogService.ShowDialog(new Dictionary {{"device", Device}}); + await _dialogService.ShowDialogAt("DeviceDialog", new Dictionary {{"device", Device}}); } public async Task Apply() @@ -151,7 +148,8 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs Device.RedScale = RedScale / 100f; Device.GreenScale = GreenScale / 100f; Device.BlueScale = BlueScale / 100f; - + _rgbService.SaveDevice(Device); + _coreService.ModuleRenderingDisabled = false; } @@ -195,7 +193,8 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs private void DeviceOnPropertyChanged(object sender, PropertyChangedEventArgs e) { - if (e.PropertyName == nameof(Device.CustomLayoutPath)) _rgbService.ApplyBestDeviceLayout(Device); + if (e.PropertyName == nameof(Device.CustomLayoutPath)) + _rgbService.ApplyBestDeviceLayout(Device); } private void OnFrameRendering(object sender, FrameRenderingEventArgs e) diff --git a/src/Artemis.UI/Screens/Settings/Dialogs/UpdateDialogView.xaml b/src/Artemis.UI/Screens/Settings/Dialogs/UpdateDialogView.xaml index 88ab9c7de..583f3828f 100644 --- a/src/Artemis.UI/Screens/Settings/Dialogs/UpdateDialogView.xaml +++ b/src/Artemis.UI/Screens/Settings/Dialogs/UpdateDialogView.xaml @@ -32,13 +32,20 @@ - - - - - - - + + + + + + + + + + Couldn't retrieve changes, sorry :( + +