1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-31 17:53:32 +00:00

Plugins - Load features by their info before enabling plugins

General - Added missing XML comments
This commit is contained in:
Robert 2021-03-10 20:53:37 +01:00
parent fa26e6b7da
commit d5d1211649
24 changed files with 241 additions and 180 deletions

View File

@ -17,4 +17,20 @@ namespace Artemis.Core
/// </summary> /// </summary>
public PluginFeature PluginFeature { get; } public PluginFeature PluginFeature { get; }
} }
/// <summary>
/// Provides data about plugin feature info related events
/// </summary>
public class PluginFeatureInfoEventArgs : EventArgs
{
internal PluginFeatureInfoEventArgs(PluginFeatureInfo pluginFeatureInfo)
{
PluginFeatureInfo = pluginFeatureInfo;
}
/// <summary>
/// Gets the plugin feature this event is related to
/// </summary>
public PluginFeatureInfo PluginFeatureInfo { get; }
}
} }

View File

@ -38,6 +38,9 @@ namespace Artemis.Core
/// </summary> /// </summary>
public DataModelPath? EventPath { get; private set; } public DataModelPath? EventPath { get; private set; }
/// <summary>
/// Gets the last time the event this condition is applied to was triggered
/// </summary>
public DateTime LastTrigger { get; private set; } public DateTime LastTrigger { get; private set; }
/// <summary> /// <summary>
@ -53,7 +56,7 @@ namespace Artemis.Core
if (_disposed) if (_disposed)
throw new ObjectDisposedException("DataModelConditionEvent"); throw new ObjectDisposedException("DataModelConditionEvent");
if (EventPath?.GetValue() is not IDataModelEvent dataModelEvent) if (EventPath?.GetValue() is not IDataModelEvent dataModelEvent)
return false; return false;
// Only evaluate to true once every time the event has been triggered since the last evaluation // Only evaluate to true once every time the event has been triggered since the last evaluation
if (dataModelEvent.LastTrigger <= LastTrigger) 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 // If there are no children, we always evaluate to true whenever the event triggered
return true; return true;
} }
/// <summary> /// <summary>
@ -131,7 +133,7 @@ namespace Artemis.Core
// Target list // Target list
EventPath?.Save(); EventPath?.Save();
Entity.EventPath = EventPath?.Entity; Entity.EventPath = EventPath?.Entity;
// Children // Children
Entity.Children.Clear(); Entity.Children.Clear();
Entity.Children.AddRange(Children.Select(c => c.GetEntity())); Entity.Children.AddRange(Children.Select(c => c.GetEntity()));

View File

@ -225,7 +225,7 @@ namespace Artemis.Core
{ {
LedEntity ledEntity = new() LedEntity ledEntity = new()
{ {
DeviceIdentifier = artemisLed.Device.RgbDevice.GetDeviceIdentifier(), DeviceIdentifier = artemisLed.Device.Identifier,
LedName = artemisLed.RgbLed.Id.ToString() LedName = artemisLed.RgbLed.Id.ToString()
}; };
LayerEntity.Leds.Add(ledEntity); LayerEntity.Leds.Add(ledEntity);
@ -578,7 +578,7 @@ namespace Artemis.Core
List<ArtemisLed> availableLeds = devices.SelectMany(d => d.Leds).ToList(); List<ArtemisLed> availableLeds = devices.SelectMany(d => d.Leds).ToList();
foreach (LedEntity ledEntity in LayerEntity.Leds) 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); a.RgbLed.Id.ToString() == ledEntity.LedName);
if (match != null) if (match != null)
leds.Add(match); leds.Add(match);

View File

@ -21,6 +21,7 @@ namespace Artemis.Core
internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider) internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider)
{ {
Identifier = rgbDevice.GetDeviceIdentifier();
DeviceEntity = new DeviceEntity(); DeviceEntity = new DeviceEntity();
RgbDevice = rgbDevice; RgbDevice = rgbDevice;
DeviceProvider = deviceProvider; DeviceProvider = deviceProvider;
@ -45,6 +46,7 @@ namespace Artemis.Core
internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider, DeviceEntity deviceEntity) internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider, DeviceEntity deviceEntity)
{ {
Identifier = rgbDevice.GetDeviceIdentifier();
DeviceEntity = deviceEntity; DeviceEntity = deviceEntity;
RgbDevice = rgbDevice; RgbDevice = rgbDevice;
DeviceProvider = deviceProvider; DeviceProvider = deviceProvider;
@ -59,6 +61,11 @@ namespace Artemis.Core
ApplyKeyboardLayout(); ApplyKeyboardLayout();
} }
/// <summary>
/// Gets the (hopefully unique and persistent) ID of this device
/// </summary>
public string Identifier { get; }
/// <summary> /// <summary>
/// Gets the rectangle covering the device /// Gets the rectangle covering the device
/// </summary> /// </summary>
@ -336,7 +343,7 @@ namespace Artemis.Core
internal void ApplyToEntity() internal void ApplyToEntity()
{ {
// Other properties are computed // Other properties are computed
DeviceEntity.Id = RgbDevice.GetDeviceIdentifier(); DeviceEntity.Id = Identifier;
DeviceEntity.InputIdentifiers.Clear(); DeviceEntity.InputIdentifiers.Clear();
foreach (ArtemisDeviceInputIdentifier identifier in InputIdentifiers) foreach (ArtemisDeviceInputIdentifier identifier in InputIdentifiers)

View File

@ -46,6 +46,9 @@ namespace Artemis.Core
private set => SetAndNotify(ref _absoluteRectangle, value); private set => SetAndNotify(ref _absoluteRectangle, value);
} }
/// <summary>
/// Gets the layout applied to this LED
/// </summary>
public ArtemisLedLayout? Layout { get; internal set; } public ArtemisLedLayout? Layout { get; internal set; }
/// <inheritdoc /> /// <inheritdoc />
@ -53,7 +56,7 @@ namespace Artemis.Core
{ {
return RgbLed.ToString(); return RgbLed.ToString();
} }
internal void CalculateRectangles() internal void CalculateRectangles()
{ {
Rectangle = RgbLed.Boundary.ToSKRect(); Rectangle = RgbLed.Boundary.ToSKRect();

View File

@ -5,6 +5,9 @@ using RGB.NET.Layout;
namespace Artemis.Core namespace Artemis.Core
{ {
/// <summary>
/// Represents a LED layout decorated with extra Artemis-specific data
/// </summary>
public class ArtemisLedLayout public class ArtemisLedLayout
{ {
internal ArtemisLedLayout(ArtemisLayout deviceLayout, ILedLayout led) internal ArtemisLedLayout(ArtemisLayout deviceLayout, ILedLayout led)

View File

@ -29,6 +29,9 @@ namespace Artemis.Core.LayerBrushes
/// </summary> /// </summary>
public ListLedGroup? LedGroup { get; internal set; } public ListLedGroup? LedGroup { get; internal set; }
/// <summary>
/// For internal use only, is public for dependency injection but ignore pl0x
/// </summary>
[Inject] [Inject]
public IRgbService? RgbService { get; set; } public IRgbService? RgbService { get; set; }
@ -38,35 +41,6 @@ namespace Artemis.Core.LayerBrushes
/// <returns>Your RGB.NET effect</returns> /// <returns>Your RGB.NET effect</returns>
public abstract IBrush GetBrush(); 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<Led> missingLeds = Layer.Leds.Where(l => !LedGroup.ContainsLed(l.RgbLed)).Select(l => l.RgbLed).ToList();
List<Led> 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 #region IDisposable
/// <inheritdoc /> /// <inheritdoc />
@ -87,6 +61,35 @@ namespace Artemis.Core.LayerBrushes
#endregion #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<Led> missingLeds = Layer.Leds.Where(l => !LedGroup.ContainsLed(l.RgbLed)).Select(l => l.RgbLed).ToList();
List<Led> 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 // Not used in this effect type
internal override void InternalRender(SKCanvas canvas, SKRect bounds, SKPaint paint) internal override void InternalRender(SKCanvas canvas, SKRect bounds, SKPaint paint)
{ {

View File

@ -16,7 +16,7 @@ namespace Artemis.Core
/// </summary> /// </summary>
public class Plugin : CorePropertyChanged, IDisposable public class Plugin : CorePropertyChanged, IDisposable
{ {
private readonly List<PluginFeature> _features; private readonly List<PluginFeatureInfo> _features;
private bool _isEnabled; private bool _isEnabled;
@ -26,7 +26,7 @@ namespace Artemis.Core
Directory = directory; Directory = directory;
Entity = pluginEntity ?? new PluginEntity {Id = Guid, IsEnabled = true}; Entity = pluginEntity ?? new PluginEntity {Id = Guid, IsEnabled = true};
_features = new List<PluginFeature>(); _features = new List<PluginFeatureInfo>();
} }
/// <summary> /// <summary>
@ -61,7 +61,7 @@ namespace Artemis.Core
/// <summary> /// <summary>
/// Gets a read-only collection of all features this plugin provides /// Gets a read-only collection of all features this plugin provides
/// </summary> /// </summary>
public ReadOnlyCollection<PluginFeature> Features => _features.AsReadOnly(); public ReadOnlyCollection<PluginFeatureInfo> Features => _features.AsReadOnly();
/// <summary> /// <summary>
/// The assembly the plugin code lives in /// The assembly the plugin code lives in
@ -107,7 +107,7 @@ namespace Artemis.Core
/// <returns>If found, the instance of the feature</returns> /// <returns>If found, the instance of the feature</returns>
public T? GetFeature<T>() where T : PluginFeature public T? GetFeature<T>() where T : PluginFeature
{ {
return _features.FirstOrDefault(i => i is T) as T; return _features.FirstOrDefault(i => i.Instance is T) as T;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -122,31 +122,32 @@ namespace Artemis.Core
Entity.IsEnabled = IsEnabled; Entity.IsEnabled = IsEnabled;
} }
internal void AddFeature(PluginFeature feature) internal void AddFeature(PluginFeatureInfo featureInfo)
{ {
feature.Plugin = this; if (featureInfo.Plugin != this)
_features.Add(feature); 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"); 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) internal void SetEnabled(bool enable)
{ {
if (IsEnabled == enable) if (IsEnabled == enable)
return; 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"); throw new ArtemisCoreException("Cannot disable this plugin because it still has enabled features");
IsEnabled = enable; IsEnabled = enable;
@ -176,8 +177,8 @@ namespace Artemis.Core
{ {
if (disposing) if (disposing)
{ {
foreach (PluginFeature feature in Features) foreach (PluginFeatureInfo feature in Features)
feature.Dispose(); feature.Instance?.Dispose();
SetEnabled(false); SetEnabled(false);
Kernel?.Dispose(); Kernel?.Dispose();
@ -189,7 +190,7 @@ namespace Artemis.Core
_features.Clear(); _features.Clear();
} }
} }
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
{ {
@ -198,7 +199,7 @@ namespace Artemis.Core
} }
#endregion #endregion
#region Events #region Events
/// <summary> /// <summary>
@ -214,12 +215,12 @@ namespace Artemis.Core
/// <summary> /// <summary>
/// Occurs when an feature is loaded and added to the plugin /// Occurs when an feature is loaded and added to the plugin
/// </summary> /// </summary>
public event EventHandler<PluginFeatureEventArgs>? FeatureAdded; public event EventHandler<PluginFeatureInfoEventArgs>? FeatureAdded;
/// <summary> /// <summary>
/// Occurs when an feature is disabled and removed from the plugin /// Occurs when an feature is disabled and removed from the plugin
/// </summary> /// </summary>
public event EventHandler<PluginFeatureEventArgs>? FeatureRemoved; public event EventHandler<PluginFeatureInfoEventArgs>? FeatureRemoved;
/// <summary> /// <summary>
/// Invokes the Enabled event /// Invokes the Enabled event
@ -240,7 +241,7 @@ namespace Artemis.Core
/// <summary> /// <summary>
/// Invokes the FeatureAdded event /// Invokes the FeatureAdded event
/// </summary> /// </summary>
protected virtual void OnFeatureAdded(PluginFeatureEventArgs e) protected virtual void OnFeatureAdded(PluginFeatureInfoEventArgs e)
{ {
FeatureAdded?.Invoke(this, e); FeatureAdded?.Invoke(this, e);
} }
@ -248,7 +249,7 @@ namespace Artemis.Core
/// <summary> /// <summary>
/// Invokes the FeatureRemoved event /// Invokes the FeatureRemoved event
/// </summary> /// </summary>
protected virtual void OnFeatureRemoved(PluginFeatureEventArgs e) protected virtual void OnFeatureRemoved(PluginFeatureInfoEventArgs e)
{ {
FeatureRemoved?.Invoke(this, e); FeatureRemoved?.Invoke(this, e);
} }

View File

@ -1,4 +1,5 @@
using Artemis.Core.DataModelExpansions; using System;
using Artemis.Core.DataModelExpansions;
using Artemis.Core.DeviceProviders; using Artemis.Core.DeviceProviders;
using Artemis.Core.LayerBrushes; using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects; using Artemis.Core.LayerEffects;
@ -16,22 +17,48 @@ namespace Artemis.Core
{ {
private string? _description; private string? _description;
private string? _icon; private string? _icon;
private PluginFeature? _instance;
private string _name = null!; 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); Name = attribute?.Name ?? instance.GetType().Name.Humanize(LetterCasing.Title);
Description = attribute?.Description; Description = attribute?.Description;
Icon = attribute?.Icon; Icon = attribute?.Icon;
PluginFeature = instance; Instance = instance;
if (Icon != null) return; if (Icon != null) return;
Icon = PluginFeature switch Icon = Instance switch
{ {
BaseDataModelExpansion => "TableAdd", BaseDataModelExpansion => "TableAdd",
DeviceProvider => "Devices", DeviceProvider => "Devices",
@ -43,6 +70,16 @@ namespace Artemis.Core
}; };
} }
/// <summary>
/// Gets the plugin this feature info is associated with
/// </summary>
public Plugin Plugin { get; }
/// <summary>
/// Gets the type of the feature
/// </summary>
public Type FeatureType { get; }
/// <summary> /// <summary>
/// The name of the plugin /// The name of the plugin
/// </summary> /// </summary>
@ -75,18 +112,18 @@ namespace Artemis.Core
} }
/// <summary> /// <summary>
/// Gets the plugin this info is associated with /// Gets the feature this info is associated with
/// </summary> /// </summary>
public PluginFeature PluginFeature public PluginFeature? Instance
{ {
get => _pluginFeature; get => _instance;
internal set => SetAndNotify(ref _pluginFeature, value); internal set => SetAndNotify(ref _instance, value);
} }
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() public override string ToString()
{ {
return PluginFeature.Id; return Instance?.Id ?? "Uninitialized feature";
} }
} }
} }

View File

@ -52,7 +52,7 @@ namespace Artemis.Core.Services
bool IsRenderPaused { get; set; } bool IsRenderPaused { get; set; }
/// <summary> /// <summary>
/// Recreates the Texture to use the given <see cref="SKBitmap"/> /// Recreates the Texture to use the given <see cref="SKBitmap" />
/// </summary> /// </summary>
/// <param name="bitmap"></param> /// <param name="bitmap"></param>
void UpdateTexture(SKBitmap bitmap); void UpdateTexture(SKBitmap bitmap);
@ -119,7 +119,16 @@ namespace Artemis.Core.Services
/// </summary> /// </summary>
void SaveDevices(); void SaveDevices();
/// <summary>
/// Enables the provided device
/// </summary>
/// <param name="device">The device to enable</param>
void EnableDevice(ArtemisDevice device); void EnableDevice(ArtemisDevice device);
/// <summary>
/// Disables the provided device
/// </summary>
/// <param name="device">The device to disable</param>
void DisableDevice(ArtemisDevice device); void DisableDevice(ArtemisDevice device);
/// <summary> /// <summary>
@ -133,7 +142,7 @@ namespace Artemis.Core.Services
event EventHandler<DeviceEventArgs> DeviceRemoved; event EventHandler<DeviceEventArgs> DeviceRemoved;
/// <summary> /// <summary>
/// Occurs when the surface has had modifications to its LED collection /// Occurs when the surface has had modifications to its LED collection
/// </summary> /// </summary>
event EventHandler LedsChanged; event EventHandler LedsChanged;
} }

View File

@ -67,7 +67,7 @@ namespace Artemis.Core.Services
// If this is a profile module, animate profile disable // If this is a profile module, animate profile disable
// module.Deactivate would do the same but without animation // module.Deactivate would do the same but without animation
if (module.IsActivated && module is ProfileModule profileModule) if (module.IsActivated && module is ProfileModule profileModule)
await profileModule.ChangeActiveProfileAnimated(null, null); await profileModule.ChangeActiveProfileAnimated(null, Enumerable.Empty<ArtemisDevice>());
module.Deactivate(false); module.Deactivate(false);
} }
@ -210,6 +210,7 @@ namespace Artemis.Core.Services
List<Module> modules = _pluginManagementService.GetFeaturesOfType<Module>().ToList(); List<Module> modules = _pluginManagementService.GetFeaturesOfType<Module>().ToList();
List<Task> tasks = new(); List<Task> tasks = new();
foreach (Module module in modules) foreach (Module module in modules)
{
lock (module) lock (module)
{ {
bool shouldBeActivated = module.EvaluateActivationRequirements() && module.IsEnabled; bool shouldBeActivated = module.EvaluateActivationRequirements() && module.IsEnabled;
@ -218,6 +219,7 @@ namespace Artemis.Core.Services
else if (!shouldBeActivated && module.IsActivated) else if (!shouldBeActivated && module.IsActivated)
tasks.Add(DeactivateModule(module)); tasks.Add(DeactivateModule(module));
} }
}
await Task.WhenAll(tasks); await Task.WhenAll(tasks);

View File

@ -5,7 +5,6 @@ using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.Loader;
using Artemis.Core.DeviceProviders; using Artemis.Core.DeviceProviders;
using Artemis.Core.Ninject; using Artemis.Core.Ninject;
using Artemis.Storage.Entities.Plugins; using Artemis.Storage.Entities.Plugins;
@ -130,7 +129,11 @@ namespace Artemis.Core.Services
{ {
lock (_plugins) lock (_plugins)
{ {
return _plugins.Where(p => p.IsEnabled).SelectMany(p => p.Features.Where(i => i.IsEnabled && i is T)).Cast<T>().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<T>()
.ToList();
} }
} }
@ -186,6 +189,7 @@ namespace Artemis.Core.Services
// Load the plugin assemblies into the plugin context // Load the plugin assemblies into the plugin context
DirectoryInfo pluginDirectory = new(Path.Combine(Constants.DataFolder, "plugins")); DirectoryInfo pluginDirectory = new(Path.Combine(Constants.DataFolder, "plugins"));
foreach (DirectoryInfo subDirectory in pluginDirectory.EnumerateDirectories()) foreach (DirectoryInfo subDirectory in pluginDirectory.EnumerateDirectories())
{
try try
{ {
LoadPlugin(subDirectory); LoadPlugin(subDirectory);
@ -194,6 +198,7 @@ namespace Artemis.Core.Services
{ {
_logger.Warning(new ArtemisPluginException("Failed to load plugin", e), "Plugin exception"); _logger.Warning(new ArtemisPluginException("Failed to load plugin", e), "Plugin exception");
} }
}
// ReSharper disable InconsistentlySynchronizedField - It's read-only, idc // ReSharper disable InconsistentlySynchronizedField - It's read-only, idc
_logger.Debug("Loaded {count} plugin(s)", _plugins.Count); _logger.Debug("Loaded {count} plugin(s)", _plugins.Count);
@ -218,7 +223,7 @@ namespace Artemis.Core.Services
foreach (Plugin plugin in _plugins.Where(p => p.Entity.IsEnabled)) foreach (Plugin plugin in _plugins.Where(p => p.Entity.IsEnabled))
EnablePlugin(plugin, false, ignorePluginLock); 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 // ReSharper restore InconsistentlySynchronizedField
LoadingPlugins = false; LoadingPlugins = false;
@ -289,6 +294,28 @@ namespace Artemis.Core.Services
throw new ArtemisPluginException(plugin, "Failed to load the plugins assembly", e); 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<Type> 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<Exception>().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<Type> bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(IPluginBootstrapper).IsAssignableFrom(t)).ToList(); List<Type> bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(IPluginBootstrapper).IsAssignableFrom(t)).ToList();
if (bootstrappers.Count > 1) if (bootstrappers.Count > 1)
_logger.Warning($"{plugin} has more than one bootstrapper, only initializing {bootstrappers.First().FullName}"); _logger.Warning($"{plugin} has more than one bootstrapper, only initializing {bootstrappers.First().FullName}");
@ -315,60 +342,45 @@ namespace Artemis.Core.Services
plugin.SetEnabled(true); plugin.SetEnabled(true);
// Get the Plugin feature from the main assembly and if there is only one, instantiate it // Create instances of each feature
List<Type> 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<Exception>().ToArray())
);
}
if (!featureTypes.Any())
_logger.Warning("Plugin {plugin} contains no features", plugin);
// Create instances of each feature and add them to the plugin
// Construction should be simple and not contain any logic so failure at this point means the entire plugin fails // 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 try
{ {
plugin.Kernel.Bind(featureType).ToSelf().InSingletonScope(); plugin.Kernel.Bind(featureInfo.FeatureType).ToSelf().InSingletonScope();
// Include Plugin as a parameter for the PluginSettingsProvider // Include Plugin as a parameter for the PluginSettingsProvider
IParameter[] parameters = {new Parameter("Plugin", plugin, false)}; 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 // Get the PluginFeature attribute which contains extra info on the feature
PluginFeatureAttribute? pluginFeatureAttribute = (PluginFeatureAttribute?) Attribute.GetCustomAttribute(featureType, typeof(PluginFeatureAttribute)); featureInfo.Instance = instance;
instance.Info = new PluginFeatureInfo(instance, pluginFeatureAttribute); instance.Info = featureInfo;
plugin.AddFeature(instance); instance.Plugin = plugin;
// Load the enabled state and if not found, default to true // Load the enabled state and if not found, default to true
instance.Entity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureType.FullName) ?? instance.Entity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureInfo.FeatureType.FullName) ??
new PluginFeatureEntity {IsEnabled = plugin.Info.AutoEnableFeatures, Type = featureType.FullName!}; new PluginFeatureEntity {IsEnabled = plugin.Info.AutoEnableFeatures, Type = featureInfo.FeatureType.FullName!};
} }
catch (Exception e) catch (Exception e)
{ {
_logger.Warning(new ArtemisPluginException(plugin, "Failed to instantiate feature", e), "Failed to instantiate feature", plugin); _logger.Warning(new ArtemisPluginException(plugin, "Failed to instantiate feature", e), "Failed to instantiate feature", plugin);
} }
}
// Activate plugins after they are all loaded // Activate features after they are all loaded
foreach (PluginFeature pluginFeature in plugin.Features.Where(i => i.Entity.IsEnabled)) foreach (PluginFeatureInfo pluginFeature in plugin.Features.Where(f => f.Instance != null && f.Instance.Entity.IsEnabled))
{
try try
{ {
EnablePluginFeature(pluginFeature, false, !ignorePluginLock); EnablePluginFeature(pluginFeature.Instance!, false, !ignorePluginLock);
} }
catch (Exception) catch (Exception)
{ {
// ignored, logged in EnablePluginFeature // ignored, logged in EnablePluginFeature
} }
}
if (saveState) if (saveState)
{ {
@ -406,12 +418,10 @@ namespace Artemis.Core.Services
if (!plugin.IsEnabled) if (!plugin.IsEnabled)
return; return;
while (plugin.Features.Any()) foreach (PluginFeatureInfo pluginFeatureInfo in plugin.Features)
{ {
PluginFeature feature = plugin.Features[0]; if (pluginFeatureInfo.Instance != null && pluginFeatureInfo.Instance.IsEnabled)
if (feature.IsEnabled) DisablePluginFeature(pluginFeatureInfo.Instance, false);
DisablePluginFeature(feature, false);
plugin.RemoveFeature(feature);
} }
plugin.SetEnabled(false); plugin.SetEnabled(false);
@ -450,7 +460,6 @@ namespace Artemis.Core.Services
Plugin? existing = _plugins.FirstOrDefault(p => p.Guid == pluginInfo.Guid); Plugin? existing = _plugins.FirstOrDefault(p => p.Guid == pluginInfo.Guid);
if (existing != null) if (existing != null)
{
try try
{ {
RemovePlugin(existing); RemovePlugin(existing);
@ -459,7 +468,6 @@ namespace Artemis.Core.Services
{ {
throw new ArtemisPluginException("A plugin with the same GUID is already loaded, failed to remove old version", e); 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 targetDirectory = pluginInfo.Main.Split(".dll")[0].Replace("/", "").Replace("\\", "");
string uniqueTargetDirectory = targetDirectory; string uniqueTargetDirectory = targetDirectory;
@ -582,9 +590,11 @@ namespace Artemis.Core.Services
private void SavePlugin(Plugin plugin) private void SavePlugin(Plugin plugin)
{ {
foreach (PluginFeature pluginFeature in plugin.Features) foreach (PluginFeatureInfo featureInfo in plugin.Features.Where(i => i.Instance != null))
if (plugin.Entity.Features.All(i => i.Type != pluginFeature.GetType().FullName)) {
plugin.Entity.Features.Add(pluginFeature.Entity); if (plugin.Entity.Features.All(i => i.Type != featureInfo.FeatureType.FullName))
plugin.Entity.Features.Add(featureInfo.Instance!.Entity);
}
_pluginRepository.SavePlugin(plugin.Entity); _pluginRepository.SavePlugin(plugin.Entity);
} }

View File

@ -146,7 +146,8 @@ namespace Artemis.Core.Services
void ActivatingProfilePluginToggle(object? sender, PluginEventArgs e) void ActivatingProfilePluginToggle(object? sender, PluginEventArgs e)
{ {
InstantiateProfile(profile); if (!profile.Disposed)
InstantiateProfile(profile);
} }
// This could happen during activation so subscribe to it // This could happen during activation so subscribe to it

View File

@ -9,7 +9,7 @@ namespace Artemis.Core
{ {
public CorePluginFeature() public CorePluginFeature()
{ {
Constants.CorePlugin.AddFeature(this); Constants.CorePlugin.AddFeature(new PluginFeatureInfo(Constants.CorePlugin, null, this));
IsEnabled = true; IsEnabled = true;
} }

View File

@ -44,7 +44,7 @@ namespace Artemis.Core
foreach (LayerEntity profileEntityLayer in profileEntity.Layers) foreach (LayerEntity profileEntityLayer in profileEntity.Layers)
profileEntityLayer.Leds.AddRange(_devices.SelectMany(d => d.Leds).Select(l => new LedEntity 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() LedName = l.RgbLed.Id.ToString()
})); }));

View File

@ -39,7 +39,7 @@ namespace Artemis.UI.Ninject.Factories
public interface ISettingsVmFactory : IVmFactory public interface ISettingsVmFactory : IVmFactory
{ {
PluginSettingsViewModel CreatePluginSettingsViewModel(Plugin plugin); PluginSettingsViewModel CreatePluginSettingsViewModel(Plugin plugin);
PluginFeatureViewModel CreatePluginFeatureViewModel(PluginFeature feature); PluginFeatureViewModel CreatePluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo);
DeviceSettingsViewModel CreateDeviceSettingsViewModel(ArtemisDevice device); DeviceSettingsViewModel CreateDeviceSettingsViewModel(ArtemisDevice device);
} }

View File

@ -136,7 +136,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions
RenderProfileElement.DisplayCondition.ChildRemoved += DisplayConditionOnChildrenModified; RenderProfileElement.DisplayCondition.ChildRemoved += DisplayConditionOnChildrenModified;
} }
private void CoreServiceOnFrameRendered(object? sender, FrameRenderedEventArgs e) private void CoreServiceOnFrameRendered(object sender, FrameRenderedEventArgs e)
{ {
ActiveItem?.Evaluate(); ActiveItem?.Evaluate();
} }

View File

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
@ -14,7 +13,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings
{ {
private readonly IDataBindingsVmFactory _dataBindingsVmFactory; private readonly IDataBindingsVmFactory _dataBindingsVmFactory;
private readonly IProfileEditorService _profileEditorService; private readonly IProfileEditorService _profileEditorService;
private ILayerProperty? _selectedDataBinding; private ILayerProperty _selectedDataBinding;
private int _selectedItemIndex; private int _selectedItemIndex;
private bool _updating; private bool _updating;
@ -95,7 +94,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings
SubscribeToSelectedDataBinding(); SubscribeToSelectedDataBinding();
base.OnInitialActivate(); base.OnInitialActivate();
} }
protected override void OnActivate() protected override void OnActivate()
{ {
SelectedItemIndex = 0; SelectedItemIndex = 0;

View File

@ -23,7 +23,7 @@
<!-- Icon column --> <!-- Icon column -->
<shared:ArtemisIcon Grid.Column="0" <shared:ArtemisIcon Grid.Column="0"
Icon="{Binding Feature.Info.Icon}" Icon="{Binding FeatureInfo.Icon}"
Width="20" Width="20"
VerticalAlignment="Center" VerticalAlignment="Center"
HorizontalAlignment="Center" HorizontalAlignment="Center"
@ -42,12 +42,12 @@
</Button> </Button>
<!-- Display name column --> <!-- Display name column -->
<TextBlock Grid.Column="1" Text="{Binding Feature.Info.Name}" Style="{StaticResource MaterialDesignTextBlock}" VerticalAlignment="Center" ToolTip="{Binding Feature.Info.Description}" /> <TextBlock Grid.Column="1" Text="{Binding FeatureInfo.Name}" Style="{StaticResource MaterialDesignTextBlock}" VerticalAlignment="Center" ToolTip="{Binding FeatureInfo.Description}" />
<!-- Enable toggle column --> <!-- Enable toggle column -->
<StackPanel Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="8" <StackPanel Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="8"
Visibility="{Binding Enabling, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay, FallbackValue=Collapsed}"> Visibility="{Binding Enabling, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay, FallbackValue=Collapsed}">
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" IsChecked="{Binding IsEnabled}"> <CheckBox Style="{StaticResource MaterialDesignCheckBox}" IsChecked="{Binding IsEnabled}" IsEnabled="{Binding FeatureInfo.Plugin.IsEnabled}">
Feature enabled Feature enabled
</CheckBox> </CheckBox>
</StackPanel> </StackPanel>

View File

@ -16,7 +16,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
private bool _enabling; private bool _enabling;
private readonly IMessageService _messageService; private readonly IMessageService _messageService;
public PluginFeatureViewModel(PluginFeature feature, public PluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo,
IDialogService dialogService, IDialogService dialogService,
IPluginManagementService pluginManagementService, IPluginManagementService pluginManagementService,
IMessageService messageService) IMessageService messageService)
@ -25,11 +25,11 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
_pluginManagementService = pluginManagementService; _pluginManagementService = pluginManagementService;
_messageService = messageService; _messageService = messageService;
Feature = feature; FeatureInfo = pluginFeatureInfo;
} }
public PluginFeature Feature { get; } public PluginFeatureInfo FeatureInfo { get; }
public Exception LoadException => Feature.LoadException; public Exception LoadException => FeatureInfo.Instance?.LoadException;
public bool Enabling public bool Enabling
{ {
@ -39,7 +39,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
public bool IsEnabled public bool IsEnabled
{ {
get => Feature.IsEnabled; get => FeatureInfo.Instance != null && FeatureInfo.Instance.IsEnabled;
set => Task.Run(() => UpdateEnabled(value)); set => Task.Run(() => UpdateEnabled(value));
} }
@ -60,7 +60,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
if (LoadException == null) if (LoadException == null)
return; return;
_dialogService.ShowExceptionDialog("Feature failed to enable", Feature.LoadException); _dialogService.ShowExceptionDialog("Feature failed to enable", LoadException);
} }
protected override void OnInitialActivate() protected override void OnInitialActivate()
@ -83,7 +83,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
private async Task UpdateEnabled(bool enable) private async Task UpdateEnabled(bool enable)
{ {
if (IsEnabled == enable) if (IsEnabled == enable || FeatureInfo.Instance == null)
{ {
NotifyOfPropertyChange(nameof(IsEnabled)); NotifyOfPropertyChange(nameof(IsEnabled));
return; return;
@ -95,11 +95,11 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
try try
{ {
await Task.Run(() => _pluginManagementService.EnablePluginFeature(Feature, true)); await Task.Run(() => _pluginManagementService.EnablePluginFeature(FeatureInfo.Instance, true));
} }
catch (Exception e) catch (Exception e)
{ {
_messageService.ShowMessage($"Failed to enable {Feature.Info.Name}\r\n{e.Message}", "VIEW LOGS", ShowLogsFolder); _messageService.ShowMessage($"Failed to enable {FeatureInfo.Name}\r\n{e.Message}", "VIEW LOGS", ShowLogsFolder);
} }
finally finally
{ {
@ -108,7 +108,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
} }
else else
{ {
_pluginManagementService.DisablePluginFeature(Feature, true); _pluginManagementService.DisablePluginFeature(FeatureInfo.Instance, true);
NotifyOfPropertyChange(nameof(IsEnabled)); NotifyOfPropertyChange(nameof(IsEnabled));
} }
} }
@ -117,13 +117,13 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
private void OnFeatureEnabling(object sender, PluginFeatureEventArgs e) private void OnFeatureEnabling(object sender, PluginFeatureEventArgs e)
{ {
if (e.PluginFeature != Feature) return; if (e.PluginFeature != FeatureInfo.Instance) return;
Enabling = true; Enabling = true;
} }
private void OnFeatureEnableStopped(object sender, PluginFeatureEventArgs e) private void OnFeatureEnableStopped(object sender, PluginFeatureEventArgs e)
{ {
if (e.PluginFeature != Feature) return; if (e.PluginFeature != FeatureInfo.Instance) return;
Enabling = false; Enabling = false;
NotifyOfPropertyChange(nameof(IsEnabled)); NotifyOfPropertyChange(nameof(IsEnabled));

View File

@ -116,21 +116,11 @@
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Margin="10 10 0 5" Style="{StaticResource MaterialDesignBody2TextBlock}">Plugin features</TextBlock> <TextBlock Margin="10 10 0 5" Style="{StaticResource MaterialDesignBody2TextBlock}">Plugin features</TextBlock>
<TextBlock Grid.Row="1"
HorizontalAlignment="Center"
Margin="0 30 0 0"
Style="{StaticResource MaterialDesignTextBlock}"
Foreground="{DynamicResource MaterialDesignNavigationItemSubheader}"
Visibility="{Binding IsEnabled, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}">
Enable the plugin to view its features
</TextBlock>
<ListBox Grid.Row="1" <ListBox Grid.Row="1"
MaxHeight="135" MaxHeight="135"
ItemsSource="{Binding Items}" ItemsSource="{Binding Items}"
HorizontalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"
VirtualizingPanel.ScrollUnit="Pixel" VirtualizingPanel.ScrollUnit="Pixel">
Visibility="{Binding IsEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
<b:Interaction.Behaviors> <b:Interaction.Behaviors>
<shared:ScrollParentWhenAtMax /> <shared:ScrollParentWhenAtMax />
</b:Interaction.Behaviors> </b:Interaction.Behaviors>

View File

@ -117,22 +117,12 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
protected override void OnInitialActivate() protected override void OnInitialActivate()
{ {
Plugin.FeatureAdded += PluginOnFeatureAdded; foreach (PluginFeatureInfo pluginFeatureInfo in Plugin.Features)
Plugin.FeatureRemoved += PluginOnFeatureRemoved; Items.Add(_settingsVmFactory.CreatePluginFeatureViewModel(pluginFeatureInfo));
foreach (PluginFeature pluginFeature in Plugin.Features)
Items.Add(_settingsVmFactory.CreatePluginFeatureViewModel(pluginFeature));
base.OnInitialActivate(); base.OnInitialActivate();
} }
protected override void OnClose()
{
Plugin.FeatureAdded -= PluginOnFeatureAdded;
Plugin.FeatureRemoved -= PluginOnFeatureRemoved;
base.OnClose();
}
private async Task UpdateEnabled(bool enable) private async Task UpdateEnabled(bool enable)
{ {
if (IsEnabled == enable) if (IsEnabled == enable)
@ -178,17 +168,5 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
NotifyOfPropertyChange(nameof(IsEnabled)); NotifyOfPropertyChange(nameof(IsEnabled));
NotifyOfPropertyChange(nameof(CanOpenSettings)); NotifyOfPropertyChange(nameof(CanOpenSettings));
} }
private void PluginOnFeatureRemoved(object sender, PluginFeatureEventArgs e)
{
PluginFeatureViewModel viewModel = Items.FirstOrDefault(i => i.Feature == e.PluginFeature);
if (viewModel != null)
Items.Remove(viewModel);
}
private void PluginOnFeatureAdded(object sender, PluginFeatureEventArgs e)
{
Items.Add(_settingsVmFactory.CreatePluginFeatureViewModel(e.PluginFeature));
}
} }
} }

View File

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Artemis.Core;
using Artemis.Core.DeviceProviders; using Artemis.Core.DeviceProviders;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
@ -27,9 +28,8 @@ namespace Artemis.UI.Screens.StartupWizard.Steps
Items.Clear(); Items.Clear();
// _pluginManagementService.GetFeaturesOfType<>() will only give us enabled features so lets get all of them this way // _pluginManagementService.GetFeaturesOfType<>() will only give us enabled features so lets get all of them this way
IEnumerable<DeviceProvider> features = _pluginManagementService.GetAllPlugins() IEnumerable<PluginFeatureInfo> features = _pluginManagementService.GetAllPlugins()
.SelectMany(p => p.Features.Where(f => f is DeviceProvider)) .SelectMany(p => p.Features.Where(f => typeof(DeviceProvider).IsAssignableFrom(f.FeatureType)))
.Cast<DeviceProvider>()
.OrderBy(d => d.GetType().Name); .OrderBy(d => d.GetType().Name);
Items.AddRange(features.Select(d => _settingsVmFactory.CreatePluginFeatureViewModel(d))); Items.AddRange(features.Select(d => _settingsVmFactory.CreatePluginFeatureViewModel(d)));

View File

@ -59,7 +59,7 @@ namespace Artemis.UI.Services
#region Event handlers #region Event handlers
private async void WindowServiceOnMainWindowOpened(object? sender, EventArgs e) private async void WindowServiceOnMainWindowOpened(object sender, EventArgs e)
{ {
List<ArtemisDevice> devices = _rgbService.Devices.Where(device => DeviceNeedsLayout(device) && !_ignoredDevices.Contains(device)).ToList(); List<ArtemisDevice> devices = _rgbService.Devices.Where(device => DeviceNeedsLayout(device) && !_ignoredDevices.Contains(device)).ToList();
foreach (ArtemisDevice artemisDevice in devices) foreach (ArtemisDevice artemisDevice in devices)