Merge branch 'layouts' into development

This commit is contained in:
Robert committed 2021-02-21 16:58:22 +01:00
commit 77bd62ab9c
71 files changed
+2130 -1632

No files matched your search

+3
View File
@@ -73,6 +73,9 @@
<Reference Include="RGB.NET.Groups">
<HintPath>..\..\..\RGB.NET\bin\net5.0\RGB.NET.Groups.dll</HintPath>
</Reference>
<Reference Include="RGB.NET.Layout">
<HintPath>..\..\..\RGB.NET\bin\net5.0\RGB.NET.Layout.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<None Update="Resources\intro-profile.json">
@@ -43,6 +43,7 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Clayerproperties_005Ctypes/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Clayershapes/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Csurface/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Csurface_005Clayout/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=mvvm/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=placeholders/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins/@EntryIndexedValue">True</s:Boolean>
@@ -1,20 +0,0 @@
using System;
namespace Artemis.Core
{
/// <summary>
/// Provides data about surface configuration related events
/// </summary>
public class SurfaceConfigurationEventArgs : EventArgs
{
internal SurfaceConfigurationEventArgs(ArtemisSurface surface)
{
Surface = surface;
}
/// <summary>
/// Gets the active surface at the time the event fired
/// </summary>
public ArtemisSurface Surface { get; }
}
}
+2 -3
View File
@@ -1,5 +1,4 @@
using System;
using RGB.NET.Core;
namespace Artemis.Core
{
@@ -8,7 +7,7 @@ namespace Artemis.Core
/// </summary>
public class DeviceEventArgs : EventArgs
{
internal DeviceEventArgs(IRGBDevice device)
internal DeviceEventArgs(ArtemisDevice device)
{
Device = device;
}
@@ -16,6 +15,6 @@ namespace Artemis.Core
/// <summary>
/// Gets the device this event is related to
/// </summary>
public IRGBDevice Device { get; }
public ArtemisDevice Device { get; }
}
}
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core
{
/// <summary>
/// Provides data about device configuration related events
/// </summary>
public class SurfaceConfigurationEventArgs : EventArgs
{
internal SurfaceConfigurationEventArgs(List<ArtemisDevice> devices)
{
Devices = devices;
}
/// <summary>
/// Gets the current list of devices
/// </summary>
public List<ArtemisDevice> Devices { get; }
}
}
@@ -16,8 +16,6 @@ namespace Artemis.Core
builder.Append(rgbDevice.DeviceInfo.Model);
builder.Append('-');
builder.Append(rgbDevice.DeviceInfo.DeviceType);
builder.Append('-');
builder.Append(rgbDevice.DeviceInfo.Lighting);
return builder.ToString();
}
}
+6 -3
View File
@@ -287,7 +287,7 @@ namespace Artemis.Core
if (!Enabled || Path == null || LayerShape?.Path == null || !General.PropertiesInitialized || !Transform.PropertiesInitialized)
return;
// Ensure the brush is ready
if (LayerBrush?.BaseProperties?.PropertiesInitialized == false || LayerBrush?.BrushType != LayerBrushType.Regular)
if (LayerBrush?.BaseProperties?.PropertiesInitialized == false)
return;
RenderTimeline(Timeline, canvas);
@@ -322,6 +322,9 @@ namespace Artemis.Core
return;
ApplyTimeline(timeline);
if (LayerBrush?.BrushType != LayerBrushType.Regular)
return;
try
{
@@ -564,7 +567,7 @@ namespace Artemis.Core
CalculateRenderProperties();
}
internal void PopulateLeds(ArtemisSurface surface)
internal void PopulateLeds(IEnumerable<ArtemisDevice> devices)
{
if (Disposed)
throw new ObjectDisposedException("Layer");
@@ -572,7 +575,7 @@ namespace Artemis.Core
List<ArtemisLed> leds = new();
// Get the surface LEDs for this layer
List<ArtemisLed> availableLeds = surface.Devices.Where(d => d.IsEnabled).SelectMany(d => d.Leds).ToList();
List<ArtemisLed> availableLeds = devices.SelectMany(d => d.Leds).ToList();
foreach (LedEntity ledEntity in LayerEntity.Leds)
{
ArtemisLed? match = availableLeds.FirstOrDefault(a => a.Device.RgbDevice.GetDeviceIdentifier() == ledEntity.DeviceIdentifier &&
+5 -5
View File
@@ -124,14 +124,14 @@ namespace Artemis.Core
/// <summary>
/// Populates all the LEDs on the elements in this profile
/// </summary>
/// <param name="surface">The currently active surface that contains the LEDs</param>
public void PopulateLeds(ArtemisSurface surface)
/// <param name="devices">The devices to use while populating LEDs</param>
public void PopulateLeds(IEnumerable<ArtemisDevice> devices)
{
if (Disposed)
throw new ObjectDisposedException("Profile");
foreach (Layer layer in GetAllLayers())
layer.PopulateLeds(surface);
layer.PopulateLeds(devices);
}
/// <inheritdoc />
@@ -197,7 +197,7 @@ namespace Artemis.Core
ProfileEntity.Layers.AddRange(GetAllLayers().Select(f => f.LayerEntity));
}
internal void Activate(ArtemisSurface surface)
internal void Activate(IEnumerable<ArtemisDevice> devices)
{
lock (_lock)
{
@@ -206,7 +206,7 @@ namespace Artemis.Core
if (IsActivated)
return;
PopulateLeds(surface);
PopulateLeds(devices);
OnActivated();
IsActivated = true;
}
+107 -23
View File
@@ -3,8 +3,10 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Core.DeviceProviders;
using Artemis.Core.Services;
using Artemis.Storage.Entities.Surface;
using RGB.NET.Core;
using RGB.NET.Layout;
using SkiaSharp;
namespace Artemis.Core
@@ -17,12 +19,11 @@ namespace Artemis.Core
private SKPath? _path;
private SKRect _rectangle;
internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider, ArtemisSurface surface)
internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider)
{
DeviceEntity = new DeviceEntity();
RgbDevice = rgbDevice;
DeviceProvider = deviceProvider;
Surface = surface;
Rotation = 0;
Scale = 1;
@@ -31,27 +32,22 @@ namespace Artemis.Core
GreenScale = 1;
BlueScale = 1;
IsEnabled = true;
deviceProvider.DeviceLayoutPaths.TryGetValue(rgbDevice, out string? layoutPath);
LayoutPath = layoutPath;
InputIdentifiers = new List<ArtemisDeviceInputIdentifier>();
Leds = rgbDevice.Select(l => new ArtemisLed(l, this)).ToList().AsReadOnly();
LedIds = new ReadOnlyDictionary<LedId, ArtemisLed>(Leds.ToDictionary(l => l.RgbLed.Id, l => l));
ApplyKeyboardLayout();
ApplyToEntity();
CalculateRenderProperties();
}
internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider, ArtemisSurface surface, DeviceEntity deviceEntity)
internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider, DeviceEntity deviceEntity)
{
DeviceEntity = deviceEntity;
RgbDevice = rgbDevice;
DeviceProvider = deviceProvider;
Surface = surface;
deviceProvider.DeviceLayoutPaths.TryGetValue(rgbDevice, out string? layoutPath);
LayoutPath = layoutPath;
InputIdentifiers = new List<ArtemisDeviceInputIdentifier>();
foreach (DeviceInputIdentifierEntity identifierEntity in DeviceEntity.InputIdentifiers)
@@ -59,6 +55,8 @@ namespace Artemis.Core
Leds = rgbDevice.Select(l => new ArtemisLed(l, this)).ToList().AsReadOnly();
LedIds = new ReadOnlyDictionary<LedId, ArtemisLed>(Leds.ToDictionary(l => l.RgbLed.Id, l => l));
ApplyKeyboardLayout();
}
/// <summary>
@@ -89,21 +87,16 @@ namespace Artemis.Core
/// </summary>
public DeviceProvider DeviceProvider { get; }
/// <summary>
/// Gets the surface containing this device
/// </summary>
public ArtemisSurface Surface { get; }
/// <summary>
/// Gets a read only collection containing the LEDs of this device
/// </summary>
public ReadOnlyCollection<ArtemisLed> Leds { get; }
public ReadOnlyCollection<ArtemisLed> Leds { get; private set; }
/// <summary>
/// Gets a dictionary containing all the LEDs of this device with their corresponding RGB.NET <see cref="LedId" /> as
/// key
/// </summary>
public ReadOnlyDictionary<LedId, ArtemisLed> LedIds { get; }
public ReadOnlyDictionary<LedId, ArtemisLed> LedIds { get; private set; }
/// <summary>
/// Gets a list of input identifiers associated with this device
@@ -215,12 +208,13 @@ namespace Artemis.Core
}
/// <summary>
/// Gets or sets a boolean indicating whether this devices is enabled or not
/// Gets a boolean indicating whether this devices is enabled or not
/// <para>Note: To enable/disable a device use the methods provided by <see cref="IRgbService" /></para>
/// </summary>
public bool IsEnabled
{
get => DeviceEntity.IsEnabled;
set
internal set
{
DeviceEntity.IsEnabled = value;
OnPropertyChanged(nameof(IsEnabled));
@@ -228,9 +222,51 @@ namespace Artemis.Core
}
/// <summary>
/// Gets the path to where the layout of the device was (attempted to be) loaded from
/// Gets or sets the physical layout of the device e.g. ISO or ANSI.
/// <para>Only applicable to keyboards</para>
/// </summary>
public string? LayoutPath { get; internal set; }
public KeyboardLayoutType PhysicalLayout
{
get => (KeyboardLayoutType) DeviceEntity.PhysicalLayout;
set
{
DeviceEntity.PhysicalLayout = (int) value;
OnPropertyChanged(nameof(PhysicalLayout));
}
}
/// <summary>
/// Gets or sets the logical layout of the device e.g. DE, UK or US.
/// <para>Only applicable to keyboards</para>
/// </summary>
public string? LogicalLayout
{
get => DeviceEntity.LogicalLayout;
set
{
DeviceEntity.LogicalLayout = value;
OnPropertyChanged(nameof(LogicalLayout));
}
}
/// <summary>
/// Gets or sets the path of the custom layout to load when calling <see cref="IRgbService.ApplyBestDeviceLayout" />
/// for this device
/// </summary>
public string? CustomLayoutPath
{
get => DeviceEntity.CustomLayoutPath;
set
{
DeviceEntity.CustomLayoutPath = value;
OnPropertyChanged(nameof(CustomLayoutPath));
}
}
/// <summary>
/// Gets the layout of the device expanded with Artemis-specific data
/// </summary>
public ArtemisLayout? Layout { get; internal set; }
internal DeviceEntity DeviceEntity { get; }
@@ -261,10 +297,46 @@ namespace Artemis.Core
return artemisLed;
}
/// <summary>
/// Generates the default layout file name of the device
/// </summary>
/// <param name="includeExtension">If true, the .xml extension is added to the file name</param>
/// <returns>The resulting file name e.g. CORSAIR GLAIVE.xml or K95 RGB-ISO.xml</returns>
public string GetLayoutFileName(bool includeExtension = true)
{
// Take out invalid file name chars, may not be perfect but neither are you
string fileName = System.IO.Path.GetInvalidFileNameChars().Aggregate(RgbDevice.DeviceInfo.Model, (current, c) => current.Replace(c, '-'));
if (RgbDevice is IKeyboard)
fileName = $"{fileName}-{PhysicalLayout.ToString().ToUpper()}";
if (includeExtension)
fileName = $"{fileName}.xml";
return fileName;
}
/// <summary>
/// Applies the provided layout to the device
/// </summary>
/// <param name="layout">The layout to apply</param>
/// <param name="createMissingLeds">A boolean indicating whether to add missing LEDs defined in the layout but missing on the device</param>
/// <param name="removeExcessiveLeds">A boolean indicating whether to remove excess LEDs present in the device but missing in the layout</param>
internal void ApplyLayout(ArtemisLayout layout, bool createMissingLeds, bool removeExcessiveLeds)
{
if (layout.IsValid)
layout.RgbLayout!.ApplyTo(RgbDevice, createMissingLeds, removeExcessiveLeds);
Leds = RgbDevice.Select(l => new ArtemisLed(l, this)).ToList().AsReadOnly();
LedIds = new ReadOnlyDictionary<LedId, ArtemisLed>(Leds.ToDictionary(l => l.RgbLed.Id, l => l));
Layout = layout;
Layout.ApplyDevice(this);
OnDeviceUpdated();
}
internal void ApplyToEntity()
{
// Other properties are computed
DeviceEntity.DeviceIdentifier = RgbDevice.GetDeviceIdentifier();
DeviceEntity.Id = RgbDevice.GetDeviceIdentifier();
DeviceEntity.InputIdentifiers.Clear();
foreach (ArtemisDeviceInputIdentifier identifier in InputIdentifiers)
@@ -295,7 +367,7 @@ namespace Artemis.Core
internal void CalculateRenderProperties()
{
Rectangle = RgbDevice.DeviceRectangle.ToSKRect();
Rectangle = RgbDevice.Boundary.ToSKRect();
if (!Leds.Any())
return;
@@ -309,6 +381,18 @@ namespace Artemis.Core
Path = path;
}
private void ApplyKeyboardLayout()
{
if (!(RgbDevice is IKeyboard keyboard))
return;
// If supported, detect the device layout so that we can load the correct one
if (DeviceProvider.CanDetectLogicalLayout)
LogicalLayout = DeviceProvider.GetLogicalLayout(keyboard);
if (DeviceProvider.CanDetectPhysicalLayout)
PhysicalLayout = (KeyboardLayoutType) keyboard.DeviceInfo.Layout;
}
#region Events
/// <summary>
@@ -46,11 +46,7 @@ namespace Artemis.Core
private set => SetAndNotify(ref _absoluteRectangle, value);
}
internal void CalculateRectangles()
{
Rectangle = RgbLed.LedRectangle.ToSKRect();
AbsoluteRectangle = RgbLed.AbsoluteLedRectangle.ToSKRect();
}
public ArtemisLedLayout? Layout { get; internal set; }
/// <inheritdoc />
public override string ToString()
@@ -66,5 +62,11 @@ namespace Artemis.Core
{
return RgbLed.Color.DivideRGB(Device.RedScale, Device.GreenScale, Device.BlueScale);
}
internal void CalculateRectangles()
{
Rectangle = RgbLed.Boundary.ToSKRect();
AbsoluteRectangle = RgbLed.AbsoluteBoundary.ToSKRect();
}
}
}
@@ -1,134 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Storage.Entities.Surface;
using RGB.NET.Core;
namespace Artemis.Core
{
/// <summary>
/// Represents a surface of a specific scale, containing all the available <see cref="ArtemisDevice" />s
/// </summary>
public class ArtemisSurface : CorePropertyChanged
{
private List<ArtemisDevice> _devices = new();
private ReadOnlyDictionary<Led, ArtemisLed> _ledMap = new(new Dictionary<Led, ArtemisLed>());
private bool _isActive;
private string _name;
internal ArtemisSurface(RGBSurface rgbSurface, string name)
{
SurfaceEntity = new SurfaceEntity {DeviceEntities = new List<DeviceEntity>()};
EntityId = Guid.NewGuid();
RgbSurface = rgbSurface;
_name = name;
_isActive = false;
ApplyToEntity();
}
internal ArtemisSurface(RGBSurface rgbSurface, SurfaceEntity surfaceEntity)
{
SurfaceEntity = surfaceEntity;
EntityId = surfaceEntity.Id;
RgbSurface = rgbSurface;
_name = surfaceEntity.Name;
_isActive = surfaceEntity.IsActive;
}
/// <summary>
/// Gets the RGB.NET surface backing this Artemis surface
/// </summary>
public RGBSurface RgbSurface { get; }
/// <summary>
/// Gets the name of the surface
/// </summary>
public string Name
{
get => _name;
set => SetAndNotify(ref _name, value);
}
/// <summary>
/// Gets a boolean indicating whether this surface is the currently active surface
/// </summary>
public bool IsActive
{
get => _isActive;
internal set => SetAndNotify(ref _isActive, value);
}
/// <summary>
/// Gets a list of devices this surface contains
/// </summary>
public List<ArtemisDevice> Devices
{
get => _devices;
internal set => SetAndNotify(ref _devices, value);
}
/// <summary>
/// Gets a dictionary containing all <see cref="ArtemisLed" />s on the surface with their corresponding RGB.NET
/// <see cref="Led" /> as key
/// </summary>
public ReadOnlyDictionary<Led, ArtemisLed> LedMap
{
get => _ledMap;
private set => SetAndNotify(ref _ledMap, value);
}
internal SurfaceEntity SurfaceEntity { get; set; }
internal Guid EntityId { get; set; }
/// <summary>
/// Attempts to retrieve the <see cref="ArtemisLed" /> that corresponds the provided RGB.NET <see cref="Led" />
/// </summary>
/// <param name="led">The RGB.NET <see cref="Led" /> to find the corresponding <see cref="ArtemisLed" /> for </param>
/// <returns>If found, the corresponding <see cref="ArtemisLed" />; otherwise <see langword="null" />.</returns>
public ArtemisLed? GetArtemisLed(Led led)
{
LedMap.TryGetValue(led, out ArtemisLed? artemisLed);
return artemisLed;
}
internal void UpdateLedMap()
{
LedMap = new ReadOnlyDictionary<Led, ArtemisLed>(
_devices.Where(d => d.IsEnabled).SelectMany(d => d.Leds.Select(al => new KeyValuePair<Led, ArtemisLed>(al.RgbLed, al))).ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
);
}
internal void ApplyToEntity()
{
SurfaceEntity.Id = EntityId;
SurfaceEntity.Name = Name;
SurfaceEntity.IsActive = IsActive;
// Add missing device entities, don't remove old ones in case they come back later
foreach (DeviceEntity deviceEntity in Devices.Select(d => d.DeviceEntity).ToList())
if (!SurfaceEntity.DeviceEntities.Contains(deviceEntity))
SurfaceEntity.DeviceEntities.Add(deviceEntity);
}
#region Events
/// <summary>
/// Occurs when the scale of the surface is changed
/// </summary>
public event EventHandler<EventArgs>? ScaleChanged;
/// <summary>
/// Invokes the <see cref="ScaleChanged" /> event
/// </summary>
protected virtual void OnScaleChanged()
{
ScaleChanged?.Invoke(this, EventArgs.Empty);
}
#endregion
}
}
@@ -0,0 +1,41 @@
// ReSharper disable InconsistentNaming
namespace Artemis.Core
{
// Copied from RGB.NET to avoid needing to reference RGB.NET
/// <summary>
/// Represents a physical layout type for a keyboard
/// </summary>
public enum KeyboardLayoutType
{
/// <summary>
/// An unknown layout type
/// </summary>
Unknown = 0,
/// <summary>
/// The ANSI layout type, often used in the US (uses a short enter)
/// </summary>
ANSI = 1,
/// <summary>
/// The ISO layout type, often used in the EU (uses a tall enter)
/// </summary>
ISO = 2,
/// <summary>
/// The JIS layout type, often used in Japan (based on ISO)
/// </summary>
JIS = 3,
/// <summary>
/// The ABNT layout type, often used in Brazil/Portugal (based on ISO)
/// </summary>
ABNT = 4,
/// <summary>
/// The KS layout type, often used in South Korea
/// </summary>
KS = 5
}
}
@@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using RGB.NET.Layout;
namespace Artemis.Core
{
/// <summary>
/// Represents a device layout decorated with extra Artemis-specific data
/// </summary>
public class ArtemisLayout
{
/// <summary>
/// Creates a new instance of the <see cref="ArtemisLayout" /> class
/// </summary>
/// <param name="filePath">The path of the layout XML file</param>
/// <param name="source">The source from where this layout is being loaded</param>
public ArtemisLayout(string filePath, LayoutSource source)
{
FilePath = filePath;
Source = source;
Leds = new List<ArtemisLedLayout>();
LoadLayout();
}
/// <summary>
/// Gets the file path the layout was (attempted to be) loaded from
/// </summary>
public string FilePath { get; }
/// <summary>
/// Gets the source from where this layout was loaded
/// </summary>
public LayoutSource Source { get; }
/// <summary>
/// Gets the device this layout is applied to
/// </summary>
public ArtemisDevice? Device { get; private set; }
/// <summary>
/// Gets a boolean indicating whether a valid layout was loaded
/// </summary>
public bool IsValid { get; private set; }
/// <summary>
/// Gets the image of the device
/// </summary>
public Uri? Image { get; private set; }
/// <summary>
/// Gets a list of LEDs this layout contains
/// </summary>
public List<ArtemisLedLayout> Leds { get; }
/// <summary>
/// Gets the RGB.NET device layout
/// </summary>
public DeviceLayout RgbLayout { get; private set; } = null!;
/// <summary>
/// Gets the custom layout data embedded in the RGB.NET layout
/// </summary>
public LayoutCustomDeviceData LayoutCustomDeviceData { get; private set; } = null!;
public void ReloadFromDisk()
{
Leds.Clear();
LoadLayout();
}
internal void ApplyDevice(ArtemisDevice artemisDevice)
{
Device = artemisDevice;
foreach (ArtemisLedLayout artemisLedLayout in Leds)
artemisLedLayout.ApplyDevice(Device);
}
private void LoadLayout()
{
DeviceLayout? deviceLayout = DeviceLayout.Load(FilePath, typeof(LayoutCustomDeviceData), typeof(LayoutCustomLedData));
if (deviceLayout != null)
{
RgbLayout = deviceLayout;
IsValid = true;
}
else
{
RgbLayout = new DeviceLayout();
IsValid = false;
}
if (IsValid)
Leds.AddRange(RgbLayout.Leds.Select(l => new ArtemisLedLayout(this, l)));
LayoutCustomDeviceData = (LayoutCustomDeviceData?) RgbLayout.CustomData ?? new LayoutCustomDeviceData();
ApplyCustomDeviceData();
}
private void ApplyCustomDeviceData()
{
if (!IsValid)
{
Image = null;
return;
}
Uri layoutDirectory = new(Path.GetDirectoryName(FilePath)! + "/", UriKind.Absolute);
if (LayoutCustomDeviceData.DeviceImage != null)
Image = new Uri(layoutDirectory, new Uri(LayoutCustomDeviceData.DeviceImage, UriKind.Relative));
else
Image = null;
}
}
/// <summary>
/// Represents a source from where a layout came
/// </summary>
public enum LayoutSource
{
/// <summary>
/// A layout loaded from config
/// </summary>
Configured,
/// <summary>
/// A layout loaded from the user layout folder
/// </summary>
User,
/// <summary>
/// A layout loaded from the plugin folder
/// </summary>
Plugin,
/// <summary>
/// A default layout loaded as a fallback option
/// </summary>
Default
}
}
@@ -0,0 +1,72 @@
using System;
using System.IO;
using System.Linq;
using RGB.NET.Layout;
namespace Artemis.Core
{
public class ArtemisLedLayout
{
internal ArtemisLedLayout(ArtemisLayout deviceLayout, ILedLayout led)
{
DeviceLayout = deviceLayout;
RgbLayout = led;
LayoutCustomLedData = (LayoutCustomLedData?) led.CustomData ?? new LayoutCustomLedData();
}
/// <summary>
/// Gets the device layout of this LED layout
/// </summary>
public ArtemisLayout DeviceLayout { get; }
/// <summary>
/// Gets the RGB.NET LED Layout of this LED layout
/// </summary>
public ILedLayout RgbLayout { get; }
/// <summary>
/// Gets the LED this layout is applied to
/// </summary>
public ArtemisLed? Led { get; protected set; }
/// <summary>
/// Gets the name of the logical layout this LED belongs to
/// </summary>
public string? LogicalName { get; private set; }
/// <summary>
/// Gets the image of the LED
/// </summary>
public Uri? Image { get; private set; }
/// <summary>
/// Gets the custom layout data embedded in the RGB.NET layout
/// </summary>
public LayoutCustomLedData LayoutCustomLedData { get; }
internal void ApplyDevice(ArtemisDevice device)
{
Led = device.Leds.FirstOrDefault(d => d.RgbLed.Id.ToString() == RgbLayout.Id);
if (Led != null)
Led.Layout = this;
ApplyCustomLedData(device);
}
private void ApplyCustomLedData(ArtemisDevice artemisDevice)
{
if (LayoutCustomLedData.LogicalLayouts == null || !LayoutCustomLedData.LogicalLayouts.Any())
return;
Uri layoutDirectory = new(Path.GetDirectoryName(DeviceLayout.FilePath)! + "\\", UriKind.Absolute);
// Prefer a matching layout or else a default layout (that has no name)
LayoutCustomLedDataLogicalLayout logicalLayout = LayoutCustomLedData.LogicalLayouts
.OrderBy(l => l.Name == artemisDevice.LogicalLayout)
.ThenBy(l => l.Name == null)
.First();
LogicalName = logicalLayout.Name;
Image = new Uri(layoutDirectory, logicalLayout.Image);
}
}
}
@@ -0,0 +1,15 @@
using System.Xml.Serialization;
#pragma warning disable 1591
namespace Artemis.Core
{
/// <summary>
/// Represents extra Artemis-specific information stored in RGB.NET layouts
/// </summary>
[XmlRoot("CustomData")]
public class LayoutCustomDeviceData
{
[XmlElement("DeviceImage")]
public string? DeviceImage { get; set; }
}
}
@@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.Xml.Serialization;
#pragma warning disable 1591
namespace Artemis.Core
{
/// <summary>
/// Represents extra Artemis-specific information stored in RGB.NET layouts
/// </summary>
[XmlRoot("CustomData")]
public class LayoutCustomLedData
{
[XmlArray("LogicalLayouts")]
public List<LayoutCustomLedDataLogicalLayout>? LogicalLayouts { get; set; }
}
/// <summary>
/// Represents extra Artemis-specific information stored in RGB.NET layouts
/// </summary>
[XmlType("LogicalLayout")]
public class LayoutCustomLedDataLogicalLayout
{
[XmlAttribute("Name")]
public string? Name { get; set; }
[XmlAttribute("Image")]
public string? Image { get; set; }
}
}
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using Ninject;
using RGB.NET.Core;
@@ -34,7 +33,23 @@ namespace Artemis.Core.DeviceProviders
[Inject]
public ILogger? Logger { get; set; }
internal Dictionary<IRGBDevice, string> DeviceLayoutPaths { get; set; } = new();
/// <summary>
/// A boolean indicating whether this device provider detects the physical layout of connected keyboards.
/// <para>
/// Note: <see cref="GetLogicalLayout" /> is only called when this or <see cref="CanDetectLogicalLayout" />
/// is <see langword="true" />.
/// </para>
/// </summary>
public bool CanDetectPhysicalLayout { get; protected set; }
/// <summary>
/// A boolean indicating whether this device provider detects the logical layout of connected keyboards
/// <para>
/// Note: <see cref="GetLogicalLayout" /> is only called when this or <see cref="CanDetectPhysicalLayout" />
/// is <see langword="true" />.
/// </para>
/// </summary>
public bool CanDetectLogicalLayout { get; protected set; }
/// <inheritdoc />
public override void Disable()
@@ -43,31 +58,49 @@ namespace Artemis.Core.DeviceProviders
}
/// <summary>
/// Loads a layout for the specified device and wraps it in an <see cref="ArtemisLayout" />
/// </summary>
/// <param name="type"></param>
/// <param name="sender"></param>
/// <param name="e"></param>
protected void ResolveAbsolutePath(Type type, object sender, ResolvePathEventArgs e)
/// <param name="device">The device to load the layout for</param>
/// <returns>The resulting Artemis layout</returns>
public virtual ArtemisLayout LoadLayout(ArtemisDevice device)
{
if (sender.GetType() == type || sender.GetType().IsGenericType(type))
{
// Start from the plugin directory
if (e.RelativePart != null && e.FileName != null)
e.FinalPath = Path.Combine(Plugin.Directory.FullName, e.RelativePart, e.FileName);
else if (e.RelativePath != null)
e.FinalPath = Path.Combine(Plugin.Directory.FullName, e.RelativePath);
string layoutDir = Path.Combine(Plugin.Directory.FullName, "Layouts");
string filePath = Path.Combine(
layoutDir,
device.RgbDevice.DeviceInfo.Manufacturer,
device.RgbDevice.DeviceInfo.DeviceType.ToString(),
device.GetLayoutFileName()
);
return new ArtemisLayout(filePath, LayoutSource.Plugin);
}
IRGBDevice device = (IRGBDevice) sender;
IRGBDeviceInfo deviceInfo = device.DeviceInfo;
if (e.FileName != null && !File.Exists(e.FinalPath))
{
Logger?.Information("Couldn't find a layout for device {deviceName}, model {deviceModel} at {filePath}",
deviceInfo.DeviceName, deviceInfo.Model, e.FinalPath);
}
/// <summary>
/// Loads a layout from the user layout folder for the specified device and wraps it in an <see cref="ArtemisLayout" />
/// </summary>
/// <param name="device">The device to load the layout for</param>
/// <returns>The resulting Artemis layout</returns>
public virtual ArtemisLayout LoadUserLayout(ArtemisDevice device)
{
string layoutDir = Path.Combine(Constants.DataFolder, "user layouts");
string filePath = Path.Combine(
layoutDir,
device.RgbDevice.DeviceInfo.Manufacturer,
device.RgbDevice.DeviceInfo.DeviceType.ToString(),
device.GetLayoutFileName()
);
return new ArtemisLayout(filePath, LayoutSource.User);
}
if (e.FileName != null)
DeviceLayoutPaths[device] = e.FinalPath;
}
/// <summary>
/// Called when a specific RGB device's logical and physical layout must be detected
/// <para>
/// Note: Only called when <see cref="CanDetectLogicalLayout" /> is <see langword="true" />.
/// </para>
/// </summary>
/// <param name="keyboard">The device to detect the layout for, always a keyboard</param>
public virtual string GetLogicalLayout(IKeyboard keyboard)
{
throw new NotImplementedException("Device provider does not support detecting logical layouts (don't call base.GetLogicalLayout())");
}
}
}
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using Artemis.Core.Services;
using Ninject;
using RGB.NET.Core;
using RGB.NET.Groups;
using SkiaSharp;
@@ -20,7 +21,6 @@ namespace Artemis.Core.LayerBrushes
/// </summary>
protected RgbNetLayerBrush()
{
LedGroup = new ListLedGroup();
BrushType = LayerBrushType.RgbNet;
SupportsTransformation = false;
}
@@ -28,7 +28,10 @@ namespace Artemis.Core.LayerBrushes
/// <summary>
/// The LED group this layer effect is applied to
/// </summary>
public ListLedGroup LedGroup { get; internal set; }
public ListLedGroup? LedGroup { get; internal set; }
[Inject]
public IRgbService? RgbService { get; set; }
/// <summary>
/// Called when Artemis needs an instance of the RGB.NET effect you are implementing
@@ -38,9 +41,14 @@ namespace Artemis.Core.LayerBrushes
internal void UpdateLedGroup()
{
// TODO: This simply renders it on top of the rest, get a ZIndex based on layer position
LedGroup.ZIndex = 1;
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.GetLeds().Where(l => Layer.Leds.All(layerLed => layerLed.RgbLed != l)).ToList();
LedGroup.AddLeds(missingLeds);
@@ -50,7 +58,10 @@ namespace Artemis.Core.LayerBrushes
internal override void Initialize()
{
LedGroup = new ListLedGroup();
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();
@@ -64,8 +75,12 @@ namespace Artemis.Core.LayerBrushes
{
if (disposing)
{
if (RgbService == null)
throw new ArtemisCoreException("Cannot dispose RGB.NET layer brush because RgbService is not set");
Layer.RenderPropertiesUpdated -= LayerOnRenderPropertiesUpdated;
LedGroup.Detach();
LedGroup?.Detach(RgbService.Surface);
LedGroup = null;
}
base.Dispose(disposing);
+3 -4
View File
@@ -146,10 +146,9 @@ namespace Artemis.Core.Modules
/// Called each frame when the module should render
/// </summary>
/// <param name="deltaTime">Time since the last render</param>
/// <param name="surface">The RGB Surface to render to</param>
/// <param name="canvas"></param>
/// <param name="canvasInfo"></param>
public abstract void Render(double deltaTime, ArtemisSurface surface, SKCanvas canvas, SKImageInfo canvasInfo);
public abstract void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo);
/// <summary>
/// Called when the <see cref="ActivationRequirements" /> are met or during an override
@@ -191,9 +190,9 @@ namespace Artemis.Core.Modules
Update(deltaTime);
}
internal virtual void InternalRender(double deltaTime, ArtemisSurface surface, SKCanvas canvas, SKImageInfo canvasInfo)
internal virtual void InternalRender(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo)
{
Render(deltaTime, surface, canvas, canvasInfo);
Render(deltaTime, canvas, canvasInfo);
}
internal virtual void Activate(bool isOverride)
@@ -142,10 +142,9 @@ namespace Artemis.Core.Modules
/// Called after the profile has rendered
/// </summary>
/// <param name="deltaTime">Time since the last render</param>
/// <param name="surface">The RGB Surface to render to</param>
/// <param name="canvas"></param>
/// <param name="canvasInfo"></param>
public virtual void ProfileRendered(double deltaTime, ArtemisSurface surface, SKCanvas canvas, SKImageInfo canvasInfo)
public virtual void ProfileRendered(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo)
{
}
@@ -168,9 +167,9 @@ namespace Artemis.Core.Modules
ProfileUpdated(deltaTime);
}
internal override void InternalRender(double deltaTime, ArtemisSurface surface, SKCanvas canvas, SKImageInfo canvasInfo)
internal override void InternalRender(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo)
{
Render(deltaTime, surface, canvas, canvasInfo);
Render(deltaTime, canvas, canvasInfo);
lock (_lock)
{
@@ -178,10 +177,10 @@ namespace Artemis.Core.Modules
ActiveProfile?.Render(canvas);
}
ProfileRendered(deltaTime, surface, canvas, canvasInfo);
ProfileRendered(deltaTime, canvas, canvasInfo);
}
internal async Task ChangeActiveProfileAnimated(Profile? profile, ArtemisSurface? surface)
internal async Task ChangeActiveProfileAnimated(Profile? profile, IEnumerable<ArtemisDevice> devices)
{
if (profile != null && profile.Module != this)
throw new ArtemisCoreException($"Cannot activate a profile of module {profile.Module} on a module of plugin {this}.");
@@ -196,21 +195,19 @@ namespace Artemis.Core.Modules
while (OpacityOverride > 0)
await Task.Delay(50);
ChangeActiveProfile(profile, surface);
ChangeActiveProfile(profile, devices);
AnimatingProfileChange = false;
while (OpacityOverride < 1)
await Task.Delay(50);
}
internal void ChangeActiveProfile(Profile? profile, ArtemisSurface? surface)
internal void ChangeActiveProfile(Profile? profile, IEnumerable<ArtemisDevice> devices)
{
if (profile != null && profile.Module != this)
throw new ArtemisCoreException($"Cannot activate a profile of module {profile.Module} on a module of plugin {this}.");
if (!IsActivated)
throw new ArtemisCoreException("Cannot activate a profile on a deactivated module");
if (profile != null && surface == null)
throw new ArtemisCoreException("If changing the active profile to a non-null profile, a surface is required");
lock (_lock)
{
@@ -220,7 +217,7 @@ namespace Artemis.Core.Modules
ActiveProfile?.Dispose();
ActiveProfile = profile;
ActiveProfile?.Activate(surface!);
ActiveProfile?.Activate(devices);
}
OnActiveProfileChanged();
+6 -4
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Artemis.Core.Services;
using RGB.NET.Core;
using SkiaSharp;
@@ -12,13 +13,15 @@ namespace Artemis.Core
{
private readonly object _disposeLock;
private readonly PluginSetting<int> _sampleSizeSetting;
private readonly IRgbService _rgbService;
#region Constructors
internal BitmapBrush(Scale scale, PluginSetting<int> sampleSizeSetting)
internal BitmapBrush(Scale scale, PluginSetting<int> sampleSizeSetting, IRgbService rgbService)
{
_disposeLock = new object();
_sampleSizeSetting = sampleSizeSetting;
_rgbService = rgbService;
Scale = scale;
}
@@ -103,7 +106,7 @@ namespace Artemis.Core
if (scaledLocation.X < Bitmap.Width && scaledLocation.Y < Bitmap.Height)
{
Color pixel = Bitmap.GetPixel(scaledLocation.X.RoundToInt(), scaledLocation.Y.RoundToInt()).ToRgbColor();
ArtemisDevice? artemisDevice = Surface?.GetArtemisLed(renderTarget.Led)?.Device;
ArtemisDevice? artemisDevice = _rgbService.GetLed(renderTarget.Led)?.Device;
if (artemisDevice != null)
pixel = pixel.MultiplyRGB(artemisDevice.RedScale, artemisDevice.GreenScale, artemisDevice.BlueScale);
RenderedTargets[renderTarget] = pixel;
@@ -157,7 +160,7 @@ namespace Artemis.Core
Color pixel = new(a / sampleSize, r / sampleSize, g / sampleSize, b / sampleSize);
ArtemisDevice? artemisDevice = Surface?.GetArtemisLed(renderTarget.Led)?.Device;
ArtemisDevice? artemisDevice = _rgbService.GetLed(renderTarget.Led)?.Device;
if (artemisDevice is not null)
pixel = pixel.MultiplyRGB(artemisDevice.RedScale, artemisDevice.GreenScale, artemisDevice.BlueScale);
@@ -188,7 +191,6 @@ namespace Artemis.Core
}
internal bool IsDisposed { get; set; }
internal ArtemisSurface? Surface { get; set; }
#endregion
}
+2 -8
View File
@@ -34,7 +34,6 @@ namespace Artemis.Core.Services
private readonly IProfileService _profileService;
private readonly PluginSetting<double> _renderScale;
private readonly IRgbService _rgbService;
private readonly ISurfaceService _surfaceService;
private readonly List<Exception> _updateExceptions = new();
private List<BaseDataModelExpansion> _dataModelExpansions = new();
private DateTime _lastExceptionLog;
@@ -47,7 +46,6 @@ namespace Artemis.Core.Services
ISettingsService settingsService,
IPluginManagementService pluginManagementService,
IRgbService rgbService,
ISurfaceService surfaceService,
IProfileService profileService,
IModuleService moduleService // injected to ensure module priorities get applied
)
@@ -58,7 +56,6 @@ namespace Artemis.Core.Services
_logger = logger;
_pluginManagementService = pluginManagementService;
_rgbService = rgbService;
_surfaceService = surfaceService;
_profileService = profileService;
_loggingLevel = settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Debug);
_renderScale = settingsService.GetSetting("Core.RenderScale", 0.5);
@@ -121,9 +118,6 @@ namespace Artemis.Core.Services
IsElevated
);
ArtemisSurface surfaceConfig = _surfaceService.ActiveSurface;
_logger.Information("Initialized with active surface entity {surfaceConfig}-{guid}", surfaceConfig.Name, surfaceConfig.EntityId);
OnInitialized();
}
@@ -139,7 +133,7 @@ namespace Artemis.Core.Services
public void PlayIntroAnimation()
{
IntroAnimation intro = new(_logger, _profileService, _surfaceService);
IntroAnimation intro = new(_logger, _profileService, _rgbService.EnabledDevices);
// Draw a white overlay over the device
void DrawOverlay(object? sender, FrameRenderingEventArgs args)
@@ -236,7 +230,7 @@ namespace Artemis.Core.Services
if (!ModuleRenderingDisabled)
// While non-activated modules may be updated above if they expand the main data model, they may never render
foreach (Module module in modules.Where(m => m.IsActivated))
module.InternalRender(args.DeltaTime, _surfaceService.ActiveSurface, canvas, _rgbService.BitmapBrush.Bitmap.Info);
module.InternalRender(args.DeltaTime, canvas, _rgbService.BitmapBrush.Bitmap.Info);
OnFrameRendering(new FrameRenderingEventArgs(canvas, args.DeltaTime, _rgbService.Surface));
}
+10 -3
View File
@@ -8,6 +8,13 @@ namespace Artemis.Core.Services
{
internal class DeviceService : IDeviceService
{
private readonly IRgbService _rgbService;
public DeviceService(IRgbService rgbService)
{
_rgbService = rgbService;
}
public void IdentifyDevice(ArtemisDevice device)
{
BlinkDevice(device, 0);
@@ -16,9 +23,9 @@ namespace Artemis.Core.Services
private void BlinkDevice(ArtemisDevice device, int blinkCount)
{
// Create a LED group way at the top
ListLedGroup ledGroup = new(device.Leds.Select(l => l.RgbLed))
ListLedGroup ledGroup = new(_rgbService.Surface, device.Leds.Select(l => l.RgbLed))
{
Brush = new SolidColorBrush(new Color(255, 255, 255)),
Brush = new SolidColorBrush(new Color(255, 255, 255)),
ZIndex = 999
};
@@ -26,7 +33,7 @@ namespace Artemis.Core.Services
Task.Run(async () =>
{
await Task.Delay(200);
ledGroup.Detach();
ledGroup.Detach(_rgbService.Surface);
if (blinkCount < 5)
{
@@ -9,15 +9,13 @@ namespace Artemis.Core.Services
internal class InputService : IInputService
{
private readonly ILogger _logger;
private readonly ISurfaceService _surfaceService;
private readonly IRgbService _rgbService;
public InputService(ILogger logger, ISurfaceService surfaceService)
public InputService(ILogger logger, IRgbService rgbService)
{
_logger = logger;
_surfaceService = surfaceService;
_rgbService = rgbService;
_surfaceService.ActiveSurfaceConfigurationSelected += SurfaceConfigurationChanged;
_surfaceService.SurfaceConfigurationUpdated += SurfaceConfigurationChanged;
BustIdentifierCache();
}
@@ -91,7 +89,7 @@ namespace Artemis.Core.Services
_logger.Debug("Stop identifying device {device}", _identifyingDevice);
_identifyingDevice = null;
_surfaceService.UpdateSurfaceConfiguration(_surfaceService.ActiveSurface, true);
_rgbService.SaveDevices();
BustIdentifierCache();
}
@@ -123,7 +121,7 @@ namespace Artemis.Core.Services
{
if (_cachedFallbackKeyboard != null)
return _cachedFallbackKeyboard;
_cachedFallbackKeyboard = _surfaceService.ActiveSurface.Devices.FirstOrDefault(d => d.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Keyboard);
_cachedFallbackKeyboard = _rgbService.EnabledDevices.FirstOrDefault(d => d.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Keyboard);
return _cachedFallbackKeyboard;
}
@@ -131,7 +129,7 @@ namespace Artemis.Core.Services
{
if (_cachedFallbackMouse != null)
return _cachedFallbackMouse;
_cachedFallbackMouse = _surfaceService.ActiveSurface.Devices.FirstOrDefault(d => d.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Mouse);
_cachedFallbackMouse = _rgbService.EnabledDevices.FirstOrDefault(d => d.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Mouse);
return _cachedFallbackMouse;
}
@@ -144,7 +142,7 @@ namespace Artemis.Core.Services
_cachedFallbackKeyboard = null;
_cachedFallbackMouse = null;
_devices = _surfaceService.ActiveSurface.Devices.Where(d => d.InputIdentifiers.Any()).ToList();
_devices = _rgbService.EnabledDevices.Where(d => d.InputIdentifiers.Any()).ToList();
}
private void AddDeviceToCache(ArtemisDevice match, InputProvider provider, object identifier)
@@ -9,6 +9,22 @@ namespace Artemis.Core.Services
/// </summary>
public interface IRgbService : IArtemisService, IDisposable
{
/// <summary>
/// Gets a read-only collection containing all enabled devices
/// </summary>
IReadOnlyCollection<ArtemisDevice> EnabledDevices { get; }
/// <summary>
/// Gets a read-only collection containing all registered devices
/// </summary>
IReadOnlyCollection<ArtemisDevice> Devices { get; }
/// <summary>
/// Gets a dictionary containing all <see cref="ArtemisLed" />s on the surface with their corresponding RGB.NET
/// <see cref="Led" /> as key
/// </summary>
IReadOnlyDictionary<Led, ArtemisLed> LedMap { get; }
/// <summary>
/// Gets or sets the RGB surface rendering is performed on
/// </summary>
@@ -19,16 +35,6 @@ namespace Artemis.Core.Services
/// </summary>
BitmapBrush? BitmapBrush { get; }
/// <summary>
/// Gets the scale the frames are rendered on, a scale of 1.0 means 1 pixel = 1mm
/// </summary>
double RenderScale { get; }
/// <summary>
/// Gets all loaded RGB devices
/// </summary>
IReadOnlyCollection<IRGBDevice> LoadedDevices { get; }
/// <summary>
/// Gets the update trigger that drives the render loop
/// </summary>
@@ -46,19 +52,77 @@ namespace Artemis.Core.Services
void AddDeviceProvider(IRGBDeviceProvider deviceProvider);
/// <summary>
/// Occurs when a single device has loaded
/// Removes the given device provider from the <see cref="Surface" />
/// </summary>
event EventHandler<DeviceEventArgs> DeviceLoaded;
/// <param name="deviceProvider"></param>
void RemoveDeviceProvider(IRGBDeviceProvider deviceProvider);
/// <summary>
/// Occurs when a single device has reloaded
/// Applies auto-arranging logic to the surface
/// </summary>
event EventHandler<DeviceEventArgs> DeviceReloaded;
void AutoArrangeDevices();
/// <summary>
/// Recalculates the LED group used by the <see cref="BitmapBrush" />
/// Applies the best available layout for the given <see cref="ArtemisDevice" />
/// </summary>
/// <param name="artemisSurface"></param>
void UpdateSurfaceLedGroup(ArtemisSurface artemisSurface);
/// <param name="device">The device to apply the best available layout to</param>
/// <returns>The layout that was applied to the device</returns>
ArtemisLayout ApplyBestDeviceLayout(ArtemisDevice device);
/// <summary>
/// Apples the provided <see cref="ArtemisLayout" /> to the provided <see cref="ArtemisDevice" />
/// </summary>
/// <param name="device"></param>
/// <param name="layout"></param>
/// <param name="createMissingLeds"></param>
/// <param name="removeExessiveLeds"></param>
void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout layout, bool createMissingLeds, bool removeExessiveLeds);
/// <summary>
/// Attempts to retrieve the <see cref="ArtemisDevice" /> that corresponds the provided RGB.NET
/// <see cref="IRGBDevice" />
/// </summary>
/// <param name="rgbDevice">
/// The RGB.NET <see cref="IRGBDevice" /> to find the corresponding <see cref="ArtemisDevice" />
/// for
/// </param>
/// <returns>If found, the corresponding <see cref="ArtemisDevice" />; otherwise <see langword="null" />.</returns>
ArtemisDevice? GetDevice(IRGBDevice rgbDevice);
/// <summary>
/// Attempts to retrieve the <see cref="ArtemisLed" /> that corresponds the provided RGB.NET <see cref="Led" />
/// </summary>
/// <param name="led">The RGB.NET <see cref="Led" /> to find the corresponding <see cref="ArtemisLed" /> for </param>
/// <returns>If found, the corresponding <see cref="ArtemisLed" />; otherwise <see langword="null" />.</returns>
ArtemisLed? GetLed(Led led);
/// <summary>
/// Saves the configuration of the provided device to persistent storage
/// </summary>
/// <param name="artemisDevice"></param>
void SaveDevice(ArtemisDevice artemisDevice);
/// <summary>
/// Saves the configuration of all current devices to persistent storage
/// </summary>
void SaveDevices();
void EnableDevice(ArtemisDevice device);
void DisableDevice(ArtemisDevice device);
/// <summary>
/// Occurs when a single device was added
/// </summary>
event EventHandler<DeviceEventArgs> DeviceAdded;
/// <summary>
/// Occurs when a single device was removed
/// </summary>
event EventHandler<DeviceEventArgs> DeviceRemoved;
/// <summary>
/// Occurs when the surface has had modifications to its LED collection
/// </summary>
event EventHandler LedsChanged;
}
}
@@ -9,7 +9,6 @@ using Artemis.Core.DeviceProviders;
using Artemis.Core.Ninject;
using Artemis.Storage.Entities.Plugins;
using Artemis.Storage.Repositories.Interfaces;
using Humanizer;
using McMaster.NETCore.Plugins;
using Ninject;
using Ninject.Extensions.ChildKernel;
@@ -147,7 +146,7 @@ namespace Artemis.Core.Services
// TODO: move to a more appropriate service
public DeviceProvider GetDeviceProviderByDevice(IRGBDevice rgbDevice)
{
return GetFeaturesOfType<DeviceProvider>().First(d => d.RgbDeviceProvider.Devices != null && d.RgbDeviceProvider.Devices.Contains(rgbDevice));
return GetFeaturesOfType<DeviceProvider>().First(d => d.RgbDeviceProvider.Devices.Contains(rgbDevice));
}
public Plugin? GetCallingPlugin()
@@ -186,7 +185,6 @@ 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);
@@ -195,7 +193,6 @@ 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);
@@ -338,7 +335,6 @@ namespace Artemis.Core.Services
// 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
foreach (Type featureType in featureTypes)
{
try
{
plugin.Kernel.Bind(featureType).ToSelf().InSingletonScope();
@@ -358,13 +354,11 @@ namespace Artemis.Core.Services
}
catch (Exception e)
{
throw new ArtemisPluginException(plugin, "Failed to instantiate feature", 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))
{
try
{
EnablePluginFeature(pluginFeature, false, !ignorePluginLock);
@@ -373,7 +367,6 @@ namespace Artemis.Core.Services
{
// ignored, logged in EnablePluginFeature
}
}
if (saveState)
{
@@ -474,13 +467,11 @@ namespace Artemis.Core.Services
Directory.CreateDirectory(directoryInfo.FullName);
string metaDataDirectory = metaDataFileEntry.FullName.Replace(metaDataFileEntry.Name, "");
foreach (ZipArchiveEntry zipArchiveEntry in archive.Entries)
{
if (zipArchiveEntry.FullName.StartsWith(metaDataDirectory))
{
string target = Path.Combine(directoryInfo.FullName, zipArchiveEntry.FullName.Remove(0, metaDataDirectory.Length));
zipArchiveEntry.ExtractToFile(target);
}
}
// Load the newly extracted plugin and return the result
return LoadPlugin(directoryInfo);
@@ -563,10 +554,8 @@ namespace Artemis.Core.Services
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);
}
_pluginRepository.SavePlugin(plugin.Entity);
}
+310 -64
View File
@@ -1,6 +1,11 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Core.DeviceProviders;
using Artemis.Core.Services.Models;
using Artemis.Storage.Entities.Surface;
using Artemis.Storage.Repositories.Interfaces;
using RGB.NET.Core;
using RGB.NET.Groups;
using Serilog;
@@ -12,72 +17,159 @@ namespace Artemis.Core.Services
/// </summary>
internal class RgbService : IRgbService
{
private readonly List<IRGBDevice> _loadedDevices;
private readonly ILogger _logger;
private readonly PluginSetting<double> _renderScaleSetting;
private readonly PluginSetting<int> _sampleSizeSetting;
private readonly PluginSetting<int> _targetFrameRateSetting;
private ListLedGroup? _surfaceLedGroup;
private readonly List<ArtemisDevice> _devices;
private readonly List<ArtemisDevice> _enabledDevices;
private Dictionary<Led, ArtemisLed> _ledMap;
public RgbService(ILogger logger, ISettingsService settingsService)
private readonly ILogger _logger;
private readonly IPluginManagementService _pluginManagementService;
private readonly IDeviceRepository _deviceRepository;
private readonly PluginSetting<double> _renderScaleSetting;
private readonly PluginSetting<int> _targetFrameRateSetting;
private readonly PluginSetting<int> _sampleSizeSetting;
private ListLedGroup? _surfaceLedGroup;
private bool _modifyingProviders;
public RgbService(ILogger logger, ISettingsService settingsService, IPluginManagementService pluginManagementService, IDeviceRepository deviceRepository)
{
_logger = logger;
_pluginManagementService = pluginManagementService;
_deviceRepository = deviceRepository;
_renderScaleSetting = settingsService.GetSetting("Core.RenderScale", 0.5);
_targetFrameRateSetting = settingsService.GetSetting("Core.TargetFrameRate", 25);
_sampleSizeSetting = settingsService.GetSetting("Core.SampleSize", 1);
Surface = RGBSurface.Instance;
Surface = new RGBSurface();
// Let's throw these for now
Surface.Exception += SurfaceOnException;
_renderScaleSetting.SettingChanged += RenderScaleSettingOnSettingChanged;
_targetFrameRateSetting.SettingChanged += TargetFrameRateSettingOnSettingChanged;
_loadedDevices = new List<IRGBDevice>();
_enabledDevices = new List<ArtemisDevice>();
_devices = new List<ArtemisDevice>();
_ledMap = new Dictionary<Led, ArtemisLed>();
UpdateTrigger = new TimerUpdateTrigger {UpdateFrequency = 1.0 / _targetFrameRateSetting.Value};
Surface.RegisterUpdateTrigger(UpdateTrigger);
}
public IReadOnlyCollection<ArtemisDevice> EnabledDevices => _enabledDevices.AsReadOnly();
public IReadOnlyCollection<ArtemisDevice> Devices => _devices.AsReadOnly();
public IReadOnlyDictionary<Led, ArtemisLed> LedMap => new ReadOnlyDictionary<Led, ArtemisLed>(_ledMap);
/// <inheritdoc />
public RGBSurface Surface { get; set; }
public TimerUpdateTrigger UpdateTrigger { get; }
public BitmapBrush? BitmapBrush { get; private set; }
public IReadOnlyCollection<IRGBDevice> LoadedDevices => _loadedDevices.AsReadOnly();
public double RenderScale => _renderScaleSetting.Value;
public bool IsRenderPaused { get; set; }
public void AddDeviceProvider(IRGBDeviceProvider deviceProvider)
{
try
lock (_devices)
{
Surface.LoadDevices(deviceProvider, RGBDeviceType.All, false, true);
}
catch (Exception e)
{
_logger.Error(e, "Exception during device loading for device provider {deviceProvider}", deviceProvider.GetType().Name);
throw;
}
if (deviceProvider.Devices == null || !deviceProvider.Devices.Any())
{
_logger.Warning("Device provider {deviceProvider} has no devices", deviceProvider.GetType().Name);
return;
}
foreach (IRGBDevice surfaceDevice in deviceProvider.Devices)
{
_logger.Debug("Device provider {deviceProvider} added {deviceName}",
deviceProvider.GetType().Name, surfaceDevice.DeviceInfo?.DeviceName);
if (!_loadedDevices.Contains(surfaceDevice))
try
{
_loadedDevices.Add(surfaceDevice);
OnDeviceLoaded(new DeviceEventArgs(surfaceDevice));
_modifyingProviders = true;
List<ArtemisDevice> toRemove = _devices.Where(a => deviceProvider.Devices.Any(d => a.RgbDevice == d)).ToList();
Surface.Detach(deviceProvider.Devices);
foreach (ArtemisDevice device in toRemove)
RemoveDevice(device);
deviceProvider.Initialize(RGBDeviceType.All, true);
Surface.Attach(deviceProvider.Devices);
if (!deviceProvider.Devices.Any())
{
_logger.Warning("Device provider {deviceProvider} has no devices", deviceProvider.GetType().Name);
return;
}
foreach (IRGBDevice rgbDevice in deviceProvider.Devices)
{
ArtemisDevice artemisDevice = GetArtemisDevice(rgbDevice);
AddDevice(artemisDevice);
_logger.Debug("Device provider {deviceProvider} added {deviceName}", deviceProvider.GetType().Name, rgbDevice.DeviceInfo?.DeviceName);
}
_devices.Sort((a, b) => a.ZIndex - b.ZIndex);
}
catch (Exception e)
{
_logger.Error(e, "Exception during device loading for device provider {deviceProvider}", deviceProvider.GetType().Name);
throw;
}
finally
{
_modifyingProviders = false;
UpdateBitmapBrush();
}
else
OnDeviceReloaded(new DeviceEventArgs(surfaceDevice));
}
}
public void RemoveDeviceProvider(IRGBDeviceProvider deviceProvider)
{
lock (_devices)
{
try
{
_modifyingProviders = true;
List<ArtemisDevice> toRemove = _devices.Where(a => deviceProvider.Devices.Any(d => a.RgbDevice == d)).ToList();
Surface.Detach(deviceProvider.Devices);
foreach (ArtemisDevice device in toRemove)
RemoveDevice(device);
_devices.Sort((a, b) => a.ZIndex - b.ZIndex);
}
catch (Exception e)
{
_logger.Error(e, "Exception during device removal for device provider {deviceProvider}", deviceProvider.GetType().Name);
throw;
}
finally
{
_modifyingProviders = false;
UpdateBitmapBrush();
}
}
}
private void UpdateBitmapBrush()
{
lock (_devices)
{
if (_modifyingProviders)
return;
_ledMap = new Dictionary<Led, ArtemisLed>(_devices.SelectMany(d => d.Leds).ToDictionary(l => l.RgbLed));
if (_surfaceLedGroup == null || BitmapBrush == null)
{
// Apply the application wide brush and decorator
BitmapBrush = new BitmapBrush(new Scale(_renderScaleSetting.Value), _sampleSizeSetting, this);
_surfaceLedGroup = new ListLedGroup(Surface, LedMap.Select(l => l.Key)) {Brush = BitmapBrush};
OnLedsChanged();
return;
}
lock (_surfaceLedGroup)
{
// Clean up the old background
_surfaceLedGroup.Detach(Surface);
// Apply the application wide brush and decorator
BitmapBrush.Scale = new Scale(_renderScaleSetting.Value);
_surfaceLedGroup = new ListLedGroup(Surface, LedMap.Select(l => l.Key)) {Brush = BitmapBrush};
OnLedsChanged();
}
}
}
#region IDisposable
public void Dispose()
{
Surface.UnregisterUpdateTrigger(UpdateTrigger);
@@ -86,10 +178,181 @@ namespace Artemis.Core.Services
Surface.Dispose();
}
#endregion
#region EnabledDevices
public void AutoArrangeDevices()
{
SurfaceArrangement surfaceArrangement = SurfaceArrangement.GetDefaultArrangement();
surfaceArrangement.Arrange(_devices);
SaveDevices();
}
public ArtemisLayout ApplyBestDeviceLayout(ArtemisDevice device)
{
ArtemisLayout layout;
// Configured layout path takes precedence over all other options
if (device.CustomLayoutPath != null)
{
layout = new ArtemisLayout(device.CustomLayoutPath, LayoutSource.Configured);
if (layout.IsValid)
{
ApplyDeviceLayout(device, layout, true, true);
return layout;
}
}
// Look for a layout provided by the user
layout = device.DeviceProvider.LoadUserLayout(device);
if (layout.IsValid)
{
ApplyDeviceLayout(device, layout, true, true);
return layout;
}
// Look for a layout provided by the plugin
layout = device.DeviceProvider.LoadLayout(device);
if (layout.IsValid)
{
ApplyDeviceLayout(device, layout, true, true);
return layout;
}
// Finally fall back to a default layout
layout = LoadDefaultLayout(device);
ApplyDeviceLayout(device, layout, true, true);
return layout;
}
private ArtemisLayout LoadDefaultLayout(ArtemisDevice device)
{
return new ArtemisLayout("NYI", LayoutSource.Default);
}
public void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout layout, bool createMissingLeds, bool removeExessiveLeds)
{
device.ApplyLayout(layout, createMissingLeds, removeExessiveLeds);
// Applying layouts can affect LEDs, update LED group
UpdateBitmapBrush();
}
public ArtemisDevice? GetDevice(IRGBDevice rgbDevice)
{
return _devices.FirstOrDefault(d => d.RgbDevice == rgbDevice);
}
public ArtemisLed? GetLed(Led led)
{
LedMap.TryGetValue(led, out ArtemisLed? artemisLed);
return artemisLed;
}
public void EnableDevice(ArtemisDevice device)
{
if (device.IsEnabled)
return;
_enabledDevices.Add(device);
device.IsEnabled = true;
device.ApplyToEntity();
_deviceRepository.Save(device.DeviceEntity);
UpdateBitmapBrush();
OnDeviceAdded(new DeviceEventArgs(device));
}
public void DisableDevice(ArtemisDevice device)
{
if (!device.IsEnabled)
return;
_enabledDevices.Remove(device);
device.IsEnabled = false;
device.ApplyToEntity();
_deviceRepository.Save(device.DeviceEntity);
UpdateBitmapBrush();
OnDeviceRemoved(new DeviceEventArgs(device));
}
private void AddDevice(ArtemisDevice device)
{
if (_devices.Any(d => d.RgbDevice == device.RgbDevice))
throw new ArtemisCoreException("Attempted to add a duplicate device to the RGB Service");
device.ApplyToRgbDevice();
_devices.Add(device);
if (device.IsEnabled)
_enabledDevices.Add(device);
// Will call UpdateBitmapBrush()
ApplyBestDeviceLayout(device);
OnDeviceAdded(new DeviceEventArgs(device));
}
private void RemoveDevice(ArtemisDevice device)
{
_devices.Remove(device);
if (device.IsEnabled)
_enabledDevices.Remove(device);
UpdateBitmapBrush();
OnDeviceRemoved(new DeviceEventArgs(device));
}
#endregion
#region Storage
private ArtemisDevice GetArtemisDevice(IRGBDevice rgbDevice)
{
string deviceIdentifier = rgbDevice.GetDeviceIdentifier();
DeviceEntity? deviceEntity = _deviceRepository.Get(deviceIdentifier);
DeviceProvider deviceProvider = _pluginManagementService.GetDeviceProviderByDevice(rgbDevice);
if (deviceEntity != null)
return new ArtemisDevice(rgbDevice, deviceProvider, deviceEntity);
// Fall back on creating a new device
_logger.Information(
"No device config found for {deviceInfo}, device hash: {deviceHashCode}. Adding a new entry.",
rgbDevice.DeviceInfo,
deviceIdentifier
);
return new ArtemisDevice(rgbDevice, deviceProvider);
}
public void SaveDevice(ArtemisDevice artemisDevice)
{
artemisDevice.ApplyToEntity();
artemisDevice.ApplyToRgbDevice();
_deviceRepository.Save(artemisDevice.DeviceEntity);
OnLedsChanged();
}
public void SaveDevices()
{
foreach (ArtemisDevice artemisDevice in _devices)
{
artemisDevice.ApplyToEntity();
artemisDevice.ApplyToRgbDevice();
}
_deviceRepository.Save(_devices.Select(d => d.DeviceEntity));
OnLedsChanged();
}
#endregion
#region Event handlers
private void RenderScaleSettingOnSettingChanged(object? sender, EventArgs e)
{
// The surface hasn't changed so we can safely reuse it
UpdateSurfaceLedGroup(BitmapBrush?.Surface);
UpdateBitmapBrush();
}
private void TargetFrameRateSettingOnSettingChanged(object? sender, EventArgs e)
@@ -103,44 +366,27 @@ namespace Artemis.Core.Services
throw args.Exception;
}
#endregion
#region Events
public event EventHandler<DeviceEventArgs>? DeviceLoaded;
public event EventHandler<DeviceEventArgs>? DeviceReloaded;
public event EventHandler<DeviceEventArgs>? DeviceAdded;
public event EventHandler<DeviceEventArgs>? DeviceRemoved;
public event EventHandler? LedsChanged;
public void UpdateSurfaceLedGroup(ArtemisSurface? artemisSurface)
private void OnDeviceAdded(DeviceEventArgs e)
{
if (artemisSurface == null)
return;
if (_surfaceLedGroup == null || BitmapBrush == null)
{
// Apply the application wide brush and decorator
BitmapBrush = new BitmapBrush(new Scale(_renderScaleSetting.Value), _sampleSizeSetting);
_surfaceLedGroup = new ListLedGroup(artemisSurface.LedMap.Select(l => l.Key)) {Brush = BitmapBrush};
return;
}
lock (_surfaceLedGroup)
{
// Clean up the old background
_surfaceLedGroup.Detach();
// Apply the application wide brush and decorator
BitmapBrush.Scale = new Scale(_renderScaleSetting.Value);
BitmapBrush.Surface = artemisSurface;
_surfaceLedGroup = new ListLedGroup(artemisSurface.LedMap.Select(l => l.Key)) {Brush = BitmapBrush};
}
DeviceAdded?.Invoke(this, e);
}
private void OnDeviceLoaded(DeviceEventArgs e)
protected virtual void OnDeviceRemoved(DeviceEventArgs e)
{
DeviceLoaded?.Invoke(this, e);
DeviceRemoved?.Invoke(this, e);
}
private void OnDeviceReloaded(DeviceEventArgs e)
protected virtual void OnLedsChanged()
{
DeviceReloaded?.Invoke(this, e);
LedsChanged?.Invoke(this, EventArgs.Empty);
}
#endregion
@@ -1,66 +0,0 @@
using System;
using System.Collections.ObjectModel;
namespace Artemis.Core.Services
{
/// <summary>
/// Provides access to the device surface and its configuration
/// </summary>
public interface ISurfaceService : IArtemisService
{
/// <summary>
/// Gets the currently active surface entity, to change config use <see cref="SetActiveSurfaceConfiguration" />
/// </summary>
ArtemisSurface ActiveSurface { get; }
/// <summary>
/// Gets a read-only list of all surface configurations
/// </summary>
ReadOnlyCollection<ArtemisSurface> SurfaceConfigurations { get; }
/// <summary>
/// Creates a new surface entity with the supplied name
/// </summary>
/// <param name="name">The name for the new surface entity</param>
/// <returns></returns>
ArtemisSurface CreateSurfaceConfiguration(string name);
/// <summary>
/// Sets the provided entity as active and applies it to the surface
/// </summary>
/// <param name="surface">The entity to activate and apply</param>
void SetActiveSurfaceConfiguration(ArtemisSurface surface);
/// <summary>
/// Saves the provided surface entity to permanent storage and if config is active, applies it to the surface
/// </summary>
/// <param name="surface">The entity to save (and apply if active)</param>
/// <param name="includeDevices">Whether to also save devices. If false, devices changes won't be applied either</param>
void UpdateSurfaceConfiguration(ArtemisSurface surface, bool includeDevices);
/// <summary>
/// Deletes the supplied surface entity, surface entity may not be the active surface entity
/// </summary>
/// <param name="surface">The surface entity to delete, may not be the active surface entity</param>
void DeleteSurfaceConfiguration(ArtemisSurface surface);
/// <summary>
/// Applies auto-arranging logic to a surface
/// </summary>
/// <param name="artemisSurface">
/// The surface to apply auto-arrange to. If <see langword="null" /> the
/// <see cref="ActiveSurface" /> is used.
/// </param>
void AutoArrange(ArtemisSurface? artemisSurface = null);
/// <summary>
/// Occurs when the active device entity has been changed
/// </summary>
event EventHandler<SurfaceConfigurationEventArgs> ActiveSurfaceConfigurationSelected;
/// <summary>
/// Occurs when a surface configuration has been updated
/// </summary>
event EventHandler<SurfaceConfigurationEventArgs> SurfaceConfigurationUpdated;
}
}
@@ -76,10 +76,10 @@ namespace Artemis.Core.Services.Models
return surfaceArrangementType;
}
public void Arrange(ArtemisSurface surface)
public void Arrange(List<ArtemisDevice> devices)
{
ArrangedDevices.Clear();
foreach (ArtemisDevice surfaceDevice in surface.Devices)
foreach (ArtemisDevice surfaceDevice in devices)
{
surfaceDevice.X = 0;
surfaceDevice.Y = 0;
@@ -87,14 +87,14 @@ namespace Artemis.Core.Services.Models
}
foreach (SurfaceArrangementType surfaceArrangementType in Types)
surfaceArrangementType.Arrange(surface);
surfaceArrangementType.Arrange(devices);
// See if we need to move the surface to keep X and Y values positive
double x = surface.Devices.Min(d => d.RgbDevice.Location.X);
double y = surface.Devices.Min(d => d.RgbDevice.Location.Y);
double x = devices.Min(d => d.RgbDevice.Location.X);
double y = devices.Min(d => d.RgbDevice.Location.Y);
if (x < 0)
{
foreach (ArtemisDevice surfaceDevice in surface.Devices)
foreach (ArtemisDevice surfaceDevice in devices)
{
surfaceDevice.X += x * -1;
surfaceDevice.ApplyToRgbDevice();
@@ -103,7 +103,7 @@ namespace Artemis.Core.Services.Models
if (y < 0)
{
foreach (ArtemisDevice surfaceDevice in surface.Devices)
foreach (ArtemisDevice surfaceDevice in devices)
{
surfaceDevice.Y += y * -1;
surfaceDevice.ApplyToRgbDevice();
@@ -32,14 +32,14 @@ namespace Artemis.Core.Services.Models
public int MarginBottom { get; }
public SurfaceArrangement SurfaceArrangement { get; set; }
public bool Apply(List<ArtemisDevice> devices, ArtemisSurface surface)
public bool Apply(List<ArtemisDevice> devices)
{
if (Anchor != null && !Anchor.HasDevices(surface))
if (Anchor != null && !Anchor.HasDevices(devices))
return false;
// Start at the edge of the anchor, if there is no anchor start at any device
Point startPoint = Anchor?.GetEdge(HorizontalPosition, VerticalPosition, surface) ??
new SurfaceArrangementType(SurfaceArrangement, RGBDeviceType.All, 1).GetEdge(HorizontalPosition, VerticalPosition, surface);
Point startPoint = Anchor?.GetEdge(HorizontalPosition, VerticalPosition) ??
new SurfaceArrangementType(SurfaceArrangement, RGBDeviceType.All, 1).GetEdge(HorizontalPosition, VerticalPosition);
// Stack multiple devices of the same type vertically if they are wider than they are tall
bool stackVertically = devices.Average(d => d.RgbDevice.Size.Width) >= devices.Average(d => d.RgbDevice.Size.Height);
@@ -21,21 +21,21 @@ namespace Artemis.Core.Services.Models
public List<SurfaceArrangementConfiguration> Configurations { get; }
public SurfaceArrangementConfiguration? AppliedConfiguration { get; set; }
public bool HasDevices(ArtemisSurface surface)
public bool HasDevices(List<ArtemisDevice> devices)
{
return surface.Devices.Any(d => d.RgbDevice.DeviceInfo.DeviceType == DeviceType);
return devices.Any(d => d.RgbDevice.DeviceInfo.DeviceType == DeviceType);
}
public void Arrange(ArtemisSurface surface)
public void Arrange(List<ArtemisDevice> devices)
{
List<ArtemisDevice> devices = surface.Devices.Where(d => d.RgbDevice.DeviceInfo.DeviceType == DeviceType).ToList();
devices = devices.Where(d => d.RgbDevice.DeviceInfo.DeviceType == DeviceType).ToList();
if (!devices.Any())
return;
AppliedConfiguration = null;
foreach (SurfaceArrangementConfiguration configuration in Configurations)
{
bool applied = configuration.Apply(devices, surface);
bool applied = configuration.Apply(devices);
if (applied)
{
AppliedConfiguration = configuration;
@@ -52,11 +52,11 @@ namespace Artemis.Core.Services.Models
VerticalArrangementPosition.Equal,
10
) {SurfaceArrangement = SurfaceArrangement};
fallback.Apply(devices, surface);
fallback.Apply(devices);
AppliedConfiguration = fallback;
}
public Point GetEdge(HorizontalArrangementPosition horizontalPosition, VerticalArrangementPosition verticalPosition, ArtemisSurface surface)
public Point GetEdge(HorizontalArrangementPosition horizontalPosition, VerticalArrangementPosition verticalPosition)
{
List<ArtemisDevice> devices = SurfaceArrangement.ArrangedDevices.Where(d => d.RgbDevice.DeviceInfo.DeviceType == DeviceType || DeviceType == RGBDeviceType.All).ToList();
if (!devices.Any())
@@ -66,7 +66,7 @@ namespace Artemis.Core.Services.Models
{
HorizontalArrangementPosition.Left => devices.Min(d => d.RgbDevice.Location.X) - (AppliedConfiguration?.MarginLeft ?? 0.0),
HorizontalArrangementPosition.Right => devices.Max(d => d.RgbDevice.Location.X + d.RgbDevice.Size.Width) + (AppliedConfiguration?.MarginRight ?? 0.0),
HorizontalArrangementPosition.Center => devices.First().RgbDevice.DeviceRectangle.Center.X,
HorizontalArrangementPosition.Center => devices.First().RgbDevice.Boundary.Center.X,
HorizontalArrangementPosition.Equal => devices.First().RgbDevice.Location.X,
_ => throw new ArgumentOutOfRangeException(nameof(horizontalPosition), horizontalPosition, null)
};
@@ -74,7 +74,7 @@ namespace Artemis.Core.Services.Models
{
VerticalArrangementPosition.Top => devices.Min(d => d.RgbDevice.Location.Y) - (AppliedConfiguration?.MarginTop ?? 0.0),
VerticalArrangementPosition.Bottom => devices.Max(d => d.RgbDevice.Location.Y + d.RgbDevice.Size.Height) + (AppliedConfiguration?.MarginBottom ?? 0.0),
VerticalArrangementPosition.Center => devices.First().RgbDevice.DeviceRectangle.Center.Y,
VerticalArrangementPosition.Center => devices.First().RgbDevice.Boundary.Center.Y,
VerticalArrangementPosition.Equal => devices.First().RgbDevice.Location.Y,
_ => throw new ArgumentOutOfRangeException(nameof(verticalPosition), verticalPosition, null)
};
@@ -14,23 +14,22 @@ namespace Artemis.Core.Services
{
private readonly ILogger _logger;
private readonly IPluginManagementService _pluginManagementService;
private readonly IRgbService _rgbService;
private readonly IProfileRepository _profileRepository;
private readonly ISurfaceService _surfaceService;
public ProfileService(ILogger logger,
IPluginManagementService pluginManagementService,
ISurfaceService surfaceService,
IRgbService rgbService,
IConditionOperatorService conditionOperatorService,
IDataBindingService dataBindingService,
IProfileRepository profileRepository)
{
_logger = logger;
_pluginManagementService = pluginManagementService;
_surfaceService = surfaceService;
_rgbService = rgbService;
_profileRepository = profileRepository;
_surfaceService.ActiveSurfaceConfigurationSelected += OnActiveSurfaceConfigurationSelected;
_surfaceService.SurfaceConfigurationUpdated += OnSurfaceConfigurationUpdated;
_rgbService.LedsChanged += RgbServiceOnLedsChanged;
}
public JsonSerializerSettings MementoSettings { get; set; } = new() {TypeNameHandling = TypeNameHandling.All};
@@ -62,12 +61,11 @@ namespace Artemis.Core.Services
/// <summary>
/// Populates all missing LEDs on all currently active profiles
/// </summary>
/// <param name="surface"></param>
private void ActiveProfilesPopulateLeds(ArtemisSurface surface)
private void ActiveProfilesPopulateLeds()
{
List<ProfileModule> profileModules = _pluginManagementService.GetFeaturesOfType<ProfileModule>();
foreach (ProfileModule profileModule in profileModules.Where(p => p.ActiveProfile != null).ToList())
profileModule.ActiveProfile?.PopulateLeds(surface); // Avoid race condition
profileModule.ActiveProfile?.PopulateLeds(_rgbService.EnabledDevices); // Avoid race condition
}
public List<ProfileDescriptor> GetProfileDescriptors(ProfileModule module)
@@ -110,7 +108,7 @@ namespace Artemis.Core.Services
Profile profile = new(profileDescriptor.ProfileModule, profileEntity);
InstantiateProfile(profile);
profileDescriptor.ProfileModule.ChangeActiveProfile(profile, _surfaceService.ActiveSurface);
profileDescriptor.ProfileModule.ChangeActiveProfile(profile, _rgbService.EnabledDevices);
SaveActiveProfile(profileDescriptor.ProfileModule);
return profile;
@@ -124,9 +122,9 @@ namespace Artemis.Core.Services
ProfileEntity entity = _profileRepository.Get(module.ActiveProfile.EntityId);
Profile profile = new(module, entity);
InstantiateProfile(profile);
module.ChangeActiveProfile(null, _surfaceService.ActiveSurface);
module.ChangeActiveProfile(profile, _surfaceService.ActiveSurface);
module.ChangeActiveProfile(null, _rgbService.EnabledDevices);
module.ChangeActiveProfile(profile, _rgbService.EnabledDevices);
}
public async Task<Profile> ActivateProfileAnimated(ProfileDescriptor profileDescriptor)
@@ -141,9 +139,9 @@ namespace Artemis.Core.Services
Profile profile = new(profileDescriptor.ProfileModule, profileEntity);
InstantiateProfile(profile);
void ActivatingProfileSurfaceUpdate(object? sender, SurfaceConfigurationEventArgs e)
void ActivatingRgbServiceOnLedsChanged(object? sender, EventArgs e)
{
profile.PopulateLeds(e.Surface);
profile.PopulateLeds(_rgbService.EnabledDevices);
}
void ActivatingProfilePluginToggle(object? sender, PluginEventArgs e)
@@ -154,28 +152,29 @@ namespace Artemis.Core.Services
// This could happen during activation so subscribe to it
_pluginManagementService.PluginEnabled += ActivatingProfilePluginToggle;
_pluginManagementService.PluginDisabled += ActivatingProfilePluginToggle;
_surfaceService.SurfaceConfigurationUpdated += ActivatingProfileSurfaceUpdate;
_rgbService.LedsChanged += ActivatingRgbServiceOnLedsChanged;
await profileDescriptor.ProfileModule.ChangeActiveProfileAnimated(profile, _surfaceService.ActiveSurface);
await profileDescriptor.ProfileModule.ChangeActiveProfileAnimated(profile, _rgbService.EnabledDevices);
SaveActiveProfile(profileDescriptor.ProfileModule);
_pluginManagementService.PluginEnabled -= ActivatingProfilePluginToggle;
_pluginManagementService.PluginDisabled -= ActivatingProfilePluginToggle;
_surfaceService.SurfaceConfigurationUpdated -= ActivatingProfileSurfaceUpdate;
_rgbService.LedsChanged -= ActivatingRgbServiceOnLedsChanged;
return profile;
}
public void ClearActiveProfile(ProfileModule module)
{
module.ChangeActiveProfile(null, _surfaceService.ActiveSurface);
module.ChangeActiveProfile(null, _rgbService.EnabledDevices);
SaveActiveProfile(module);
}
public async Task ClearActiveProfileAnimated(ProfileModule module)
{
await module.ChangeActiveProfileAnimated(null, _surfaceService.ActiveSurface);
await module.ChangeActiveProfileAnimated(null, _rgbService.EnabledDevices);
}
public void DeleteProfile(Profile profile)
@@ -255,7 +254,7 @@ namespace Artemis.Core.Services
string top = profile.RedoStack.Pop();
string memento = JsonConvert.SerializeObject(profile.ProfileEntity, MementoSettings);
profile.UndoStack.Push(memento);
profile.ProfileEntity = JsonConvert.DeserializeObject<ProfileEntity>(top, MementoSettings)
profile.ProfileEntity = JsonConvert.DeserializeObject<ProfileEntity>(top, MementoSettings)
?? throw new InvalidOperationException("Failed to deserialize memento");
profile.Load();
@@ -268,7 +267,7 @@ namespace Artemis.Core.Services
public void InstantiateProfile(Profile profile)
{
profile.PopulateLeds(_surfaceService.ActiveSurface);
profile.PopulateLeds(_rgbService.EnabledDevices);
}
public string ExportProfile(ProfileDescriptor profileDescriptor)
@@ -296,17 +295,11 @@ namespace Artemis.Core.Services
#region Event handlers
private void OnActiveSurfaceConfigurationSelected(object? sender, SurfaceConfigurationEventArgs e)
private void RgbServiceOnLedsChanged(object? sender, EventArgs e)
{
ActiveProfilesPopulateLeds(e.Surface);
ActiveProfilesPopulateLeds();
}
private void OnSurfaceConfigurationUpdated(object? sender, SurfaceConfigurationEventArgs e)
{
if (e.Surface.IsActive)
ActiveProfilesPopulateLeds(e.Surface);
}
#endregion
}
}
@@ -1,252 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Core.DeviceProviders;
using Artemis.Core.Services.Models;
using Artemis.Storage.Entities.Surface;
using Artemis.Storage.Repositories.Interfaces;
using RGB.NET.Core;
using Serilog;
namespace Artemis.Core.Services
{
internal class SurfaceService : ISurfaceService
{
private readonly ILogger _logger;
private readonly IPluginManagementService _pluginManagementService;
private readonly IRgbService _rgbService;
private readonly List<ArtemisSurface> _surfaceConfigurations;
private readonly ISurfaceRepository _surfaceRepository;
public SurfaceService(ILogger logger, ISurfaceRepository surfaceRepository, IRgbService rgbService, IPluginManagementService pluginManagementService)
{
_logger = logger;
_surfaceRepository = surfaceRepository;
_rgbService = rgbService;
_pluginManagementService = pluginManagementService;
_surfaceConfigurations = new List<ArtemisSurface>();
// LoadFromRepository is guaranteed to set the ActiveSurface
ActiveSurface = null!;
LoadFromRepository();
_rgbService.DeviceLoaded += RgbServiceOnDeviceLoaded;
}
public ArtemisSurface ActiveSurface { get; private set; }
public ReadOnlyCollection<ArtemisSurface> SurfaceConfigurations => _surfaceConfigurations.AsReadOnly();
public ArtemisSurface CreateSurfaceConfiguration(string name)
{
// Create a blank config
ArtemisSurface configuration = new(_rgbService.Surface, name);
// Add all current devices
foreach (IRGBDevice rgbDevice in _rgbService.LoadedDevices)
{
DeviceProvider deviceProvider = _pluginManagementService.GetDeviceProviderByDevice(rgbDevice);
configuration.Devices.Add(new ArtemisDevice(rgbDevice, deviceProvider, configuration));
}
// Auto-arrange the new config
AutoArrange(configuration);
lock (_surfaceConfigurations)
{
_surfaceRepository.Add(configuration.SurfaceEntity);
_surfaceConfigurations.Add(configuration);
UpdateSurfaceConfiguration(configuration, true);
return configuration;
}
}
public void SetActiveSurfaceConfiguration(ArtemisSurface surface)
{
if (surface == null) throw new ArgumentNullException(nameof(surface));
if (ActiveSurface == surface)
return;
// Set the new entity
ActiveSurface = surface;
// Ensure only the new entity is marked as active
lock (_surfaceConfigurations)
{
// Mark only the new surface as active
foreach (ArtemisSurface configuration in _surfaceConfigurations)
{
configuration.IsActive = configuration == ActiveSurface;
configuration.ApplyToEntity();
_surfaceRepository.Save(configuration.SurfaceEntity);
}
}
// Apply the active surface entity to the devices
foreach (ArtemisDevice device in ActiveSurface.Devices)
device.ApplyToRgbDevice();
// Update the RGB service's graphics decorator to work with the new surface entity
_rgbService.UpdateSurfaceLedGroup(ActiveSurface);
OnActiveSurfaceConfigurationChanged(new SurfaceConfigurationEventArgs(ActiveSurface));
}
public void UpdateSurfaceConfiguration(ArtemisSurface surface, bool includeDevices)
{
surface.ApplyToEntity();
if (includeDevices)
{
foreach (ArtemisDevice deviceConfiguration in surface.Devices)
{
deviceConfiguration.ApplyToEntity();
if (surface.IsActive)
deviceConfiguration.ApplyToRgbDevice();
}
}
surface.UpdateLedMap();
_surfaceRepository.Save(surface.SurfaceEntity);
_rgbService.UpdateSurfaceLedGroup(ActiveSurface);
OnSurfaceConfigurationUpdated(new SurfaceConfigurationEventArgs(surface));
}
public void DeleteSurfaceConfiguration(ArtemisSurface surface)
{
if (surface == ActiveSurface)
throw new ArtemisCoreException($"Cannot delete surface entity '{surface.Name}' because it is active.");
lock (_surfaceConfigurations)
{
SurfaceEntity entity = surface.SurfaceEntity;
_surfaceConfigurations.Remove(surface);
_surfaceRepository.Remove(entity);
}
}
#region Repository
private void LoadFromRepository()
{
List<SurfaceEntity> configs = _surfaceRepository.GetAll();
foreach (SurfaceEntity surfaceEntity in configs)
{
// Create the surface entity
ArtemisSurface surfaceConfiguration = new(_rgbService.Surface, surfaceEntity);
foreach (DeviceEntity position in surfaceEntity.DeviceEntities)
{
IRGBDevice? device = _rgbService.Surface.Devices.FirstOrDefault(d => d.GetDeviceIdentifier() == position.DeviceIdentifier);
if (device != null)
{
DeviceProvider deviceProvider = _pluginManagementService.GetDeviceProviderByDevice(device);
surfaceConfiguration.Devices.Add(new ArtemisDevice(device, deviceProvider, surfaceConfiguration, position));
}
}
// Finally, add the surface config to the collection
lock (_surfaceConfigurations)
{
_surfaceConfigurations.Add(surfaceConfiguration);
}
}
// When all surface configs are loaded, apply the active surface config
ArtemisSurface? active = SurfaceConfigurations.FirstOrDefault(c => c.IsActive);
if (active != null)
SetActiveSurfaceConfiguration(active);
else
{
active = SurfaceConfigurations.FirstOrDefault();
if (active != null)
SetActiveSurfaceConfiguration(active);
else
SetActiveSurfaceConfiguration(CreateSurfaceConfiguration("Default"));
}
}
#endregion
#region Utilities
private void AddDeviceIfMissing(IRGBDevice rgbDevice, ArtemisSurface surface)
{
string deviceIdentifier = rgbDevice.GetDeviceIdentifier();
ArtemisDevice? device = surface.Devices.FirstOrDefault(d => d.DeviceEntity.DeviceIdentifier == deviceIdentifier);
if (device != null)
return;
// Find an existing device config and use that
DeviceEntity? existingDeviceConfig = surface.SurfaceEntity.DeviceEntities.FirstOrDefault(d => d.DeviceIdentifier == deviceIdentifier);
if (existingDeviceConfig != null)
{
DeviceProvider deviceProvider = _pluginManagementService.GetDeviceProviderByDevice(rgbDevice);
device = new ArtemisDevice(rgbDevice, deviceProvider, surface, existingDeviceConfig);
}
// Fall back on creating a new device
else
{
_logger.Information(
"No device config found for {deviceInfo}, device hash: {deviceHashCode}. Adding a new entry.",
rgbDevice.DeviceInfo,
deviceIdentifier
);
DeviceProvider deviceProvider = _pluginManagementService.GetDeviceProviderByDevice(rgbDevice);
device = new ArtemisDevice(rgbDevice, deviceProvider, surface);
}
surface.Devices.Add(device);
}
#endregion
#region AutoLayout
public void AutoArrange(ArtemisSurface? artemisSurface = null)
{
artemisSurface ??= ActiveSurface;
if (!artemisSurface.Devices.Any())
return;
SurfaceArrangement surfaceArrangement = SurfaceArrangement.GetDefaultArrangement();
surfaceArrangement.Arrange(artemisSurface);
UpdateSurfaceConfiguration(artemisSurface, true);
}
#endregion
#region Event handlers
private void RgbServiceOnDeviceLoaded(object? sender, DeviceEventArgs e)
{
lock (_surfaceConfigurations)
{
foreach (ArtemisSurface surfaceConfiguration in _surfaceConfigurations)
AddDeviceIfMissing(e.Device, surfaceConfiguration);
}
UpdateSurfaceConfiguration(ActiveSurface, true);
}
#endregion
#region Events
public event EventHandler<SurfaceConfigurationEventArgs>? ActiveSurfaceConfigurationSelected;
public event EventHandler<SurfaceConfigurationEventArgs>? SurfaceConfigurationUpdated;
protected virtual void OnActiveSurfaceConfigurationChanged(SurfaceConfigurationEventArgs e)
{
ActiveSurfaceConfigurationSelected?.Invoke(this, e);
}
protected virtual void OnSurfaceConfigurationUpdated(SurfaceConfigurationEventArgs e)
{
SurfaceConfigurationUpdated?.Invoke(this, e);
}
#endregion
}
}
@@ -29,7 +29,7 @@ namespace Artemis.Core.Services
/// <summary>
/// Gets the full URL of the end point
/// </summary>
public string Url => $"{_pluginsModule.ServerUrl.TrimEnd('/')}{_pluginsModule.BaseRoute}{PluginFeature.Plugin.Guid}/{Name}";
public string Url => $"{_pluginsModule.ServerUrl?.TrimEnd('/')}{_pluginsModule.BaseRoute}{PluginFeature.Plugin.Guid}/{Name}";
/// <summary>
/// Gets the plugin the end point is associated with
@@ -45,12 +45,12 @@ namespace Artemis.Core.Services
/// <summary>
/// Gets the mime type of the input this end point accepts
/// </summary>
public string Accepts { get; protected set; }
public string? Accepts { get; protected set; }
/// <summary>
/// Gets the mime type of the output this end point returns
/// </summary>
public string Returns { get; protected set; }
public string? Returns { get; protected set; }
/// <summary>
/// Called whenever the end point has to process a request
+7 -6
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Artemis.Core.Modules;
@@ -13,13 +14,13 @@ namespace Artemis.Core
{
private readonly ILogger _logger;
private readonly IProfileService _profileService;
private readonly ISurfaceService _surfaceService;
private readonly IEnumerable<ArtemisDevice> _devices;
public IntroAnimation(ILogger logger, IProfileService profileService, ISurfaceService surfaceService)
public IntroAnimation(ILogger logger, IProfileService profileService, IEnumerable<ArtemisDevice> devices)
{
_logger = logger;
_profileService = profileService;
_surfaceService = surfaceService;
_devices = devices;
AnimationProfile = CreateIntroProfile();
}
@@ -41,14 +42,14 @@ namespace Artemis.Core
ProfileEntity profileEntity = CoreJson.DeserializeObject<ProfileEntity>(json)!;
// Inject every LED on the surface into each layer
foreach (LayerEntity profileEntityLayer in profileEntity.Layers)
profileEntityLayer.Leds.AddRange(_surfaceService.ActiveSurface.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(),
LedName = l.RgbLed.Id.ToString()
}));
Profile profile = new(new DummyModule(), profileEntity);
profile.Activate(_surfaceService.ActiveSurface);
profile.Activate(_devices);
_profileService.InstantiateProfile(profile);
return profile;
@@ -79,7 +80,7 @@ namespace Artemis.Core
throw new NotImplementedException();
}
public override void Render(double deltaTime, ArtemisSurface surface, SKCanvas canvas, SKImageInfo canvasInfo)
public override void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo)
{
throw new NotImplementedException();
}
+16 -11
View File
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
@@ -18,8 +17,9 @@ namespace Artemis.Core
/// </summary>
public static void PrepareFirstLaunch()
{
CreateArtemisFolderIfMissing(Constants.DataFolder);
CreateArtemisFolderIfMissing(Constants.DataFolder + "plugins");
CreateAccessibleDirectory(Constants.DataFolder);
CreateAccessibleDirectory(Constants.DataFolder + "plugins");
CreateAccessibleDirectory(Constants.DataFolder + "user layouts");
}
/// <summary>
@@ -62,15 +62,11 @@ namespace Artemis.Core
}
/// <summary>
/// Gets the current application location
/// Creates all directories and subdirectories in the specified path unless they already exist with permissions
/// allowing access by everyone.
/// </summary>
/// <returns></returns>
internal static string GetCurrentLocation()
{
return Process.GetCurrentProcess().MainModule!.FileName!;
}
private static void CreateArtemisFolderIfMissing(string path)
/// <param name="path">The directory to create.</param>
public static void CreateAccessibleDirectory(string path)
{
if (!Directory.Exists(path))
{
@@ -92,6 +88,15 @@ namespace Artemis.Core
}
}
/// <summary>
/// Gets the current application location
/// </summary>
/// <returns></returns>
internal static string GetCurrentLocation()
{
return Process.GetCurrentProcess().MainModule!.FileName!;
}
private static void OnRestartRequested(RestartEventArgs e)
{
RestartRequested?.Invoke(null, e);