1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Merge branch 'development'

This commit is contained in:
Robert 2023-10-17 19:54:24 +02:00
commit 1f3f213405
27 changed files with 490 additions and 387 deletions

View File

@ -1,4 +1,5 @@
using System.Text; using System.Linq;
using System.Text;
using RGB.NET.Core; using RGB.NET.Core;
using SkiaSharp; using SkiaSharp;
@ -18,6 +19,18 @@ internal static class RgbDeviceExtensions
builder.Append(rgbDevice.DeviceInfo.DeviceType); builder.Append(rgbDevice.DeviceInfo.DeviceType);
return builder.ToString(); return builder.ToString();
} }
public static void EnsureValidDimensions(this IRGBDevice rgbDevice)
{
if (rgbDevice.Location == Point.Invalid)
rgbDevice.Location = new Point(0, 0);
if (rgbDevice.Size == Size.Invalid)
{
Rectangle ledRectangle = new(rgbDevice.Select(x => x.Boundary));
rgbDevice.Size = ledRectangle.Size + new Size(ledRectangle.Location.X, ledRectangle.Location.Y);
}
}
} }
internal static class RgbRectangleExtensions internal static class RgbRectangleExtensions

View File

@ -1,9 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq; using System.Linq;
using Artemis.Core.DeviceProviders; using Artemis.Core.DeviceProviders;
using Artemis.Core.Services;
using Artemis.Storage.Entities.Surface; using Artemis.Storage.Entities.Surface;
using RGB.NET.Core; using RGB.NET.Core;
using SkiaSharp; using SkiaSharp;
@ -22,13 +23,13 @@ public class ArtemisDevice : CorePropertyChanged
internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider) internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider)
{ {
rgbDevice.EnsureValidDimensions();
_originalLeds = new List<OriginalLed>(rgbDevice.Select(l => new OriginalLed(l))); _originalLeds = new List<OriginalLed>(rgbDevice.Select(l => new OriginalLed(l)));
Rectangle ledRectangle = new(rgbDevice.Select(x => x.Boundary)); _originalSize = rgbDevice.Size;
_originalSize = ledRectangle.Size + new Size(ledRectangle.Location.X, ledRectangle.Location.Y);
RgbDevice = rgbDevice;
Identifier = rgbDevice.GetDeviceIdentifier(); Identifier = rgbDevice.GetDeviceIdentifier();
DeviceEntity = new DeviceEntity(); DeviceEntity = new DeviceEntity();
RgbDevice = rgbDevice;
DeviceProvider = deviceProvider; DeviceProvider = deviceProvider;
Rotation = 0; Rotation = 0;
@ -45,22 +46,26 @@ public class ArtemisDevice : CorePropertyChanged
InputMappings = new Dictionary<ArtemisLed, ArtemisLed>(); InputMappings = new Dictionary<ArtemisLed, ArtemisLed>();
Categories = new HashSet<DeviceCategory>(); Categories = new HashSet<DeviceCategory>();
UpdateLeds(); RgbDevice.ColorCorrections.Clear();
RgbDevice.ColorCorrections.Add(new ScaleColorCorrection(this));
CreateArtemisLeds(false);
ApplyKeyboardLayout(); ApplyKeyboardLayout();
ApplyToEntity();
ApplyDefaultCategories();
CalculateRenderProperties(); CalculateRenderProperties();
Save();
RgbDevice.PropertyChanged += RgbDeviceOnPropertyChanged;
} }
internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider, DeviceEntity deviceEntity) internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider, DeviceEntity deviceEntity)
{ {
rgbDevice.EnsureValidDimensions();
_originalLeds = new List<OriginalLed>(rgbDevice.Select(l => new OriginalLed(l))); _originalLeds = new List<OriginalLed>(rgbDevice.Select(l => new OriginalLed(l)));
Rectangle ledRectangle = new(rgbDevice.Select(x => x.Boundary)); _originalSize = rgbDevice.Size;
_originalSize = ledRectangle.Size + new Size(ledRectangle.Location.X, ledRectangle.Location.Y);
RgbDevice = rgbDevice;
Identifier = rgbDevice.GetDeviceIdentifier(); Identifier = rgbDevice.GetDeviceIdentifier();
DeviceEntity = deviceEntity; DeviceEntity = deviceEntity;
RgbDevice = rgbDevice;
DeviceProvider = deviceProvider; DeviceProvider = deviceProvider;
LedIds = new ReadOnlyDictionary<LedId, ArtemisLed>(new Dictionary<LedId, ArtemisLed>()); LedIds = new ReadOnlyDictionary<LedId, ArtemisLed>(new Dictionary<LedId, ArtemisLed>());
@ -72,8 +77,15 @@ public class ArtemisDevice : CorePropertyChanged
foreach (DeviceInputIdentifierEntity identifierEntity in DeviceEntity.InputIdentifiers) foreach (DeviceInputIdentifierEntity identifierEntity in DeviceEntity.InputIdentifiers)
InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(identifierEntity.InputProvider, identifierEntity.Identifier)); InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(identifierEntity.InputProvider, identifierEntity.Identifier));
UpdateLeds(); RgbDevice.ColorCorrections.Clear();
RgbDevice.ColorCorrections.Add(new ScaleColorCorrection(this));
CreateArtemisLeds(false);
Load();
ApplyKeyboardLayout(); ApplyKeyboardLayout();
CalculateRenderProperties();
RgbDevice.PropertyChanged += RgbDeviceOnPropertyChanged;
} }
/// <summary> /// <summary>
@ -149,6 +161,8 @@ public class ArtemisDevice : CorePropertyChanged
set set
{ {
DeviceEntity.X = value; DeviceEntity.X = value;
if (RgbDevice.Surface != null)
ApplyLocation(DeviceEntity.X, DeviceEntity.Y);
OnPropertyChanged(nameof(X)); OnPropertyChanged(nameof(X));
} }
} }
@ -162,6 +176,8 @@ public class ArtemisDevice : CorePropertyChanged
set set
{ {
DeviceEntity.Y = value; DeviceEntity.Y = value;
if (RgbDevice.Surface != null)
ApplyLocation(DeviceEntity.X, DeviceEntity.Y);
OnPropertyChanged(nameof(Y)); OnPropertyChanged(nameof(Y));
} }
} }
@ -175,6 +191,8 @@ public class ArtemisDevice : CorePropertyChanged
set set
{ {
DeviceEntity.Rotation = value; DeviceEntity.Rotation = value;
if (RgbDevice.Surface != null)
RgbDevice.Rotation = DeviceEntity.Rotation;
OnPropertyChanged(nameof(Rotation)); OnPropertyChanged(nameof(Rotation));
} }
} }
@ -188,6 +206,8 @@ public class ArtemisDevice : CorePropertyChanged
set set
{ {
DeviceEntity.Scale = value; DeviceEntity.Scale = value;
if (RgbDevice.Surface != null)
RgbDevice.Scale = DeviceEntity.Scale;
OnPropertyChanged(nameof(Scale)); OnPropertyChanged(nameof(Scale));
} }
} }
@ -412,8 +432,7 @@ public class ArtemisDevice : CorePropertyChanged
case RGBDeviceType.Mousepad: case RGBDeviceType.Mousepad:
case RGBDeviceType.HeadsetStand: case RGBDeviceType.HeadsetStand:
case RGBDeviceType.Keypad: case RGBDeviceType.Keypad:
if (!Categories.Contains(DeviceCategory.Peripherals)) Categories.Add(DeviceCategory.Peripherals);
Categories.Add(DeviceCategory.Peripherals);
break; break;
case RGBDeviceType.Mainboard: case RGBDeviceType.Mainboard:
case RGBDeviceType.GraphicsCard: case RGBDeviceType.GraphicsCard:
@ -421,20 +440,16 @@ public class ArtemisDevice : CorePropertyChanged
case RGBDeviceType.Fan: case RGBDeviceType.Fan:
case RGBDeviceType.LedStripe: case RGBDeviceType.LedStripe:
case RGBDeviceType.Cooler: case RGBDeviceType.Cooler:
if (!Categories.Contains(DeviceCategory.Case)) Categories.Add(DeviceCategory.Case);
Categories.Add(DeviceCategory.Case);
break; break;
case RGBDeviceType.Speaker: case RGBDeviceType.Speaker:
if (!Categories.Contains(DeviceCategory.Desk)) Categories.Add(DeviceCategory.Desk);
Categories.Add(DeviceCategory.Desk);
break; break;
case RGBDeviceType.Monitor: case RGBDeviceType.Monitor:
if (!Categories.Contains(DeviceCategory.Monitor)) Categories.Add(DeviceCategory.Monitor);
Categories.Add(DeviceCategory.Monitor);
break; break;
case RGBDeviceType.LedMatrix: case RGBDeviceType.LedMatrix:
if (!Categories.Contains(DeviceCategory.Room)) Categories.Add(DeviceCategory.Room);
Categories.Add(DeviceCategory.Room);
break; break;
} }
} }
@ -461,40 +476,36 @@ public class ArtemisDevice : CorePropertyChanged
/// </param> /// </param>
internal void ApplyLayout(ArtemisLayout? layout, bool createMissingLeds, bool removeExcessiveLeds) internal void ApplyLayout(ArtemisLayout? layout, bool createMissingLeds, bool removeExcessiveLeds)
{ {
if (layout == null) if (layout != null && layout.IsValid && createMissingLeds && !DeviceProvider.CreateMissingLedsSupported)
throw new ArtemisCoreException($"Cannot apply layout with {nameof(createMissingLeds)} set to true because the device provider does not support it");
if (layout != null && layout.IsValid && removeExcessiveLeds && !DeviceProvider.RemoveExcessiveLedsSupported)
throw new ArtemisCoreException($"Cannot apply layout with {nameof(removeExcessiveLeds)} set to true because the device provider does not support it");
// Always clear the current layout
ClearLayout();
// If a valid layout was supplied, apply the layout to the device
if (layout != null && layout.IsValid)
{ {
ClearLayout(); layout.ApplyToDevice(RgbDevice, createMissingLeds, removeExcessiveLeds);
UpdateLeds(); Layout = layout;
}
CalculateRenderProperties(); else
OnDeviceUpdated(); {
return; Layout = null;
} }
if (createMissingLeds && !DeviceProvider.CreateMissingLedsSupported) // Recreate Artemis LEDs
throw new ArtemisCoreException($"Cannot apply layout with {nameof(createMissingLeds)} " + CreateArtemisLeds(true);
"set to true because the device provider does not support it"); // Calculate render properties with the new layout
if (removeExcessiveLeds && !DeviceProvider.RemoveExcessiveLedsSupported)
throw new ArtemisCoreException($"Cannot apply layout with {nameof(removeExcessiveLeds)} " +
"set to true because the device provider does not support it");
ClearLayout();
if (layout.IsValid)
layout.ApplyTo(RgbDevice, createMissingLeds, removeExcessiveLeds);
UpdateLeds();
Layout = layout;
Layout.ApplyDevice(this);
CalculateRenderProperties(); CalculateRenderProperties();
OnDeviceUpdated();
} }
private void ClearLayout() private void ClearLayout()
{ {
if (Layout == null) if (Layout == null)
return; return;
RgbDevice.DeviceInfo.LayoutMetadata = null; RgbDevice.DeviceInfo.LayoutMetadata = null;
RgbDevice.Size = _originalSize; RgbDevice.Size = _originalSize;
Layout = null; Layout = null;
@ -505,7 +516,7 @@ public class ArtemisDevice : CorePropertyChanged
RgbDevice.AddLed(originalLed.Id, originalLed.Location, originalLed.Size, originalLed.CustomData); RgbDevice.AddLed(originalLed.Id, originalLed.Location, originalLed.Size, originalLed.CustomData);
} }
internal void ApplyToEntity() internal void Save()
{ {
// Other properties are computed // Other properties are computed
DeviceEntity.Id = Identifier; DeviceEntity.Id = Identifier;
@ -513,13 +524,7 @@ public class ArtemisDevice : CorePropertyChanged
DeviceEntity.InputIdentifiers.Clear(); DeviceEntity.InputIdentifiers.Clear();
foreach (ArtemisDeviceInputIdentifier identifier in InputIdentifiers) foreach (ArtemisDeviceInputIdentifier identifier in InputIdentifiers)
{ DeviceEntity.InputIdentifiers.Add(new DeviceInputIdentifierEntity {InputProvider = identifier.InputProvider, Identifier = identifier.Identifier});
DeviceEntity.InputIdentifiers.Add(new DeviceInputIdentifierEntity
{
InputProvider = identifier.InputProvider,
Identifier = identifier.Identifier
});
}
DeviceEntity.InputMappings.Clear(); DeviceEntity.InputMappings.Clear();
foreach ((ArtemisLed? original, ArtemisLed? mapped) in InputMappings) foreach ((ArtemisLed? original, ArtemisLed? mapped) in InputMappings)
@ -530,31 +535,19 @@ public class ArtemisDevice : CorePropertyChanged
DeviceEntity.Categories.Add((int) deviceCategory); DeviceEntity.Categories.Add((int) deviceCategory);
} }
internal void ApplyToRgbDevice() internal void Load()
{ {
RgbDevice.Rotation = DeviceEntity.Rotation;
RgbDevice.Scale = DeviceEntity.Scale;
// Workaround for device rotation not applying
if (DeviceEntity.X == 0 && DeviceEntity.Y == 0)
RgbDevice.Location = new Point(1, 1);
RgbDevice.Location = new Point(DeviceEntity.X, DeviceEntity.Y);
InputIdentifiers.Clear(); InputIdentifiers.Clear();
foreach (DeviceInputIdentifierEntity identifierEntity in DeviceEntity.InputIdentifiers) foreach (DeviceInputIdentifierEntity identifierEntity in DeviceEntity.InputIdentifiers)
InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(identifierEntity.InputProvider, identifierEntity.Identifier)); InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(identifierEntity.InputProvider, identifierEntity.Identifier));
if (!RgbDevice.ColorCorrections.Any())
RgbDevice.ColorCorrections.Add(new ScaleColorCorrection(this));
Categories.Clear(); Categories.Clear();
foreach (int deviceEntityCategory in DeviceEntity.Categories) foreach (int deviceEntityCategory in DeviceEntity.Categories)
Categories.Add((DeviceCategory) deviceEntityCategory); Categories.Add((DeviceCategory) deviceEntityCategory);
if (!Categories.Any()) if (!Categories.Any())
ApplyDefaultCategories(); ApplyDefaultCategories();
CalculateRenderProperties(); LoadInputMappings();
OnDeviceUpdated();
} }
internal void CalculateRenderProperties() internal void CalculateRenderProperties()
@ -571,13 +564,27 @@ public class ArtemisDevice : CorePropertyChanged
path.AddRect(artemisLed.AbsoluteRectangle); path.AddRect(artemisLed.AbsoluteRectangle);
Path = path; Path = path;
OnDeviceUpdated();
} }
private void UpdateLeds() private void CreateArtemisLeds(bool loadInputMappings)
{ {
Leds = RgbDevice.Select(l => new ArtemisLed(l, this)).ToList().AsReadOnly(); Leds = RgbDevice.Select(l => new ArtemisLed(l, this)).ToList().AsReadOnly();
LedIds = new ReadOnlyDictionary<LedId, ArtemisLed>(Leds.ToDictionary(l => l.RgbLed.Id, l => l)); LedIds = new ReadOnlyDictionary<LedId, ArtemisLed>(Leds.ToDictionary(l => l.RgbLed.Id, l => l));
if (loadInputMappings)
LoadInputMappings();
}
private void UpdateArtemisLeds()
{
foreach (ArtemisLed artemisLed in Leds)
artemisLed.CalculateRectangles();
}
private void LoadInputMappings()
{
InputMappings.Clear(); InputMappings.Clear();
foreach (InputMappingEntity deviceEntityInputMapping in DeviceEntity.InputMappings) foreach (InputMappingEntity deviceEntityInputMapping in DeviceEntity.InputMappings)
{ {
@ -604,6 +611,27 @@ public class ArtemisDevice : CorePropertyChanged
else else
LogicalLayout = DeviceEntity.LogicalLayout; LogicalLayout = DeviceEntity.LogicalLayout;
} }
private void RgbDeviceOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(IRGBDevice.Surface) || RgbDevice.Surface == null)
return;
RgbDevice.Rotation = DeviceEntity.Rotation;
RgbDevice.Scale = DeviceEntity.Scale;
ApplyLocation(DeviceEntity.X, DeviceEntity.Y);
}
private void ApplyLocation(float x, float y)
{
// Workaround for device rotation not applying
if (x == 0 && y == 0)
RgbDevice.Location = new Point(1, 1);
RgbDevice.Location = new Point(x, y);
UpdateArtemisLeds();
CalculateRenderProperties();
}
} }
/// <summary> /// <summary>

View File

@ -1,4 +1,5 @@
using RGB.NET.Core; using System.Linq;
using RGB.NET.Core;
using SkiaSharp; using SkiaSharp;
namespace Artemis.Core; namespace Artemis.Core;
@ -15,6 +16,9 @@ public class ArtemisLed : CorePropertyChanged
{ {
RgbLed = led; RgbLed = led;
Device = device; Device = device;
Layout = device.Layout?.Leds.FirstOrDefault(l => l.RgbLayout.Id == led.Id.ToString());
Layout?.ApplyCustomLedData(Device);
CalculateRectangles(); CalculateRectangles();
} }

View File

@ -36,11 +36,6 @@ public class ArtemisLayout
/// </summary> /// </summary>
public LayoutSource Source { get; } public LayoutSource Source { get; }
/// <summary>
/// Gets the device this layout is applied to
/// </summary>
public ArtemisDevice? Device { get; private set; }
/// <summary> /// <summary>
/// Gets a boolean indicating whether a valid layout was loaded /// Gets a boolean indicating whether a valid layout was loaded
/// </summary> /// </summary>
@ -69,7 +64,7 @@ public class ArtemisLayout
/// <summary> /// <summary>
/// Applies the layout to the provided device /// Applies the layout to the provided device
/// </summary> /// </summary>
public void ApplyTo(IRGBDevice device, bool createMissingLeds = false, bool removeExcessiveLeds = false) public void ApplyToDevice(IRGBDevice device, bool createMissingLeds = false, bool removeExcessiveLeds = false)
{ {
device.Size = new Size(MathF.Round(RgbLayout.Width), MathF.Round(RgbLayout.Height)); device.Size = new Size(MathF.Round(RgbLayout.Width), MathF.Round(RgbLayout.Height));
device.DeviceInfo.LayoutMetadata = RgbLayout.CustomData; device.DeviceInfo.LayoutMetadata = RgbLayout.CustomData;
@ -124,13 +119,6 @@ public class ArtemisLayout
} }
} }
internal void ApplyDevice(ArtemisDevice artemisDevice)
{
Device = artemisDevice;
foreach (ArtemisLedLayout artemisLedLayout in Leds)
artemisLedLayout.ApplyDevice(Device);
}
internal static ArtemisLayout? GetDefaultLayout(ArtemisDevice device) internal static ArtemisLayout? GetDefaultLayout(ArtemisDevice device)
{ {
string layoutFolder = Path.Combine(Constants.ApplicationFolder, "DefaultLayouts", "Artemis"); string layoutFolder = Path.Combine(Constants.ApplicationFolder, "DefaultLayouts", "Artemis");

View File

@ -27,11 +27,6 @@ public class ArtemisLedLayout
/// </summary> /// </summary>
public ILedLayout RgbLayout { get; } public ILedLayout RgbLayout { get; }
/// <summary>
/// Gets the LED this layout is applied to
/// </summary>
public ArtemisLed? Led { get; protected set; }
/// <summary> /// <summary>
/// Gets the name of the logical layout this LED belongs to /// Gets the name of the logical layout this LED belongs to
/// </summary> /// </summary>
@ -46,17 +41,9 @@ public class ArtemisLedLayout
/// Gets the custom layout data embedded in the RGB.NET layout /// Gets the custom layout data embedded in the RGB.NET layout
/// </summary> /// </summary>
public LayoutCustomLedData LayoutCustomLedData { get; } public LayoutCustomLedData LayoutCustomLedData { get; }
internal void ApplyDevice(ArtemisDevice device) internal void ApplyCustomLedData(ArtemisDevice artemisDevice)
{
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()) if (LayoutCustomLedData.LogicalLayouts == null || !LayoutCustomLedData.LogicalLayouts.Any())
return; return;

View File

@ -14,8 +14,9 @@ namespace Artemis.Core.Modules;
/// </summary> /// </summary>
public abstract class DataModel public abstract class DataModel
{ {
private const StringComparison PathsStringComparison = StringComparison.OrdinalIgnoreCase;
private readonly List<DataModelPath> _activePaths = new(); private readonly List<DataModelPath> _activePaths = new();
private readonly HashSet<string> _activePathsHashSet = new(); private readonly HashSet<string> _activePathsHashSet = new(StringComparer.FromComparison(PathsStringComparison));
private readonly Dictionary<string, DynamicChild> _dynamicChildren = new(); private readonly Dictionary<string, DynamicChild> _dynamicChildren = new();
/// <summary> /// <summary>
@ -332,11 +333,10 @@ public abstract class DataModel
/// </param> /// </param>
internal bool IsPropertyInUse(string path, bool includeChildren) internal bool IsPropertyInUse(string path, bool includeChildren)
{ {
path = path.ToUpperInvariant();
lock (_activePaths) lock (_activePaths)
{ {
return includeChildren return includeChildren
? _activePathsHashSet.Any(p => p.StartsWith(path, StringComparison.Ordinal)) ? _activePathsHashSet.Any(p => p.StartsWith(path, PathsStringComparison))
: _activePathsHashSet.Contains(path); : _activePathsHashSet.Contains(path);
} }
} }
@ -351,9 +351,7 @@ public abstract class DataModel
_activePaths.Add(path); _activePaths.Add(path);
// Add to the hashset if this is the first path pointing // Add to the hashset if this is the first path pointing
string hashPath = path.Path.ToUpperInvariant(); _activePathsHashSet.Add(path.Path);
if (!_activePathsHashSet.Contains(hashPath))
_activePathsHashSet.Add(hashPath);
} }
OnActivePathAdded(new DataModelPathEventArgs(path)); OnActivePathAdded(new DataModelPathEventArgs(path));
@ -368,7 +366,7 @@ public abstract class DataModel
// Remove from the hashset if this was the last path pointing there // Remove from the hashset if this was the last path pointing there
if (_activePaths.All(p => p.Path != path.Path)) if (_activePaths.All(p => p.Path != path.Path))
_activePathsHashSet.Remove(path.Path.ToUpperInvariant()); _activePathsHashSet.Remove(path.Path);
} }
OnActivePathRemoved(new DataModelPathEventArgs(path)); OnActivePathRemoved(new DataModelPathEventArgs(path));

View File

@ -14,7 +14,6 @@ internal sealed class SurfaceManager : IDisposable
{ {
private readonly IRenderer _renderer; private readonly IRenderer _renderer;
private readonly TimerUpdateTrigger _updateTrigger; private readonly TimerUpdateTrigger _updateTrigger;
private readonly object _renderLock = new();
private readonly List<ArtemisDevice> _devices = new(); private readonly List<ArtemisDevice> _devices = new();
private readonly SKTextureBrush _textureBrush = new(null) {CalculationMode = RenderMode.Absolute}; private readonly SKTextureBrush _textureBrush = new(null) {CalculationMode = RenderMode.Absolute};
@ -24,7 +23,7 @@ internal sealed class SurfaceManager : IDisposable
public SurfaceManager(IRenderer renderer, IManagedGraphicsContext? graphicsContext, int targetFrameRate, float renderScale) public SurfaceManager(IRenderer renderer, IManagedGraphicsContext? graphicsContext, int targetFrameRate, float renderScale)
{ {
_renderer = renderer; _renderer = renderer;
_updateTrigger = new TimerUpdateTrigger {UpdateFrequency = 1.0 / targetFrameRate}; _updateTrigger = new TimerUpdateTrigger(false) {UpdateFrequency = 1.0 / targetFrameRate};
GraphicsContext = graphicsContext; GraphicsContext = graphicsContext;
TargetFrameRate = targetFrameRate; TargetFrameRate = targetFrameRate;
@ -36,47 +35,6 @@ internal sealed class SurfaceManager : IDisposable
SetPaused(true); SetPaused(true);
} }
private void SurfaceOnUpdating(UpdatingEventArgs args)
{
lock (_renderLock)
{
SKTexture? texture = _texture;
if (texture == null || texture.IsInvalid)
texture = CreateTexture();
// Prepare a canvas
SKCanvas canvas = texture.Surface.Canvas;
canvas.Save();
// Apply scaling if necessary
if (Math.Abs(texture.RenderScale - 1) > 0.001)
canvas.Scale(texture.RenderScale);
// Fresh start!
canvas.Clear(new SKColor(0, 0, 0));
try
{
_renderer.Render(canvas, args.DeltaTime);
}
finally
{
canvas.RestoreToCount(-1);
canvas.Flush();
texture.CopyPixelData();
}
try
{
_renderer.PostRender(texture);
}
catch
{
// ignored
}
}
}
public IManagedGraphicsContext? GraphicsContext { get; private set; } public IManagedGraphicsContext? GraphicsContext { get; private set; }
public int TargetFrameRate { get; private set; } public int TargetFrameRate { get; private set; }
public float RenderScale { get; private set; } public float RenderScale { get; private set; }
@ -86,34 +44,46 @@ internal sealed class SurfaceManager : IDisposable
public void AddDevices(IEnumerable<ArtemisDevice> devices) public void AddDevices(IEnumerable<ArtemisDevice> devices)
{ {
lock (_renderLock) List<IRGBDevice> newDevices = new();
lock (_devices)
{ {
foreach (ArtemisDevice artemisDevice in devices) foreach (ArtemisDevice artemisDevice in devices)
{ {
if (_devices.Contains(artemisDevice)) if (_devices.Contains(artemisDevice))
continue; continue;
_devices.Add(artemisDevice); _devices.Add(artemisDevice);
Surface.Attach(artemisDevice.RgbDevice); newDevices.Add(artemisDevice.RgbDevice);
artemisDevice.DeviceUpdated += ArtemisDeviceOnDeviceUpdated; artemisDevice.DeviceUpdated += ArtemisDeviceOnDeviceUpdated;
} }
Update();
} }
if (!newDevices.Any())
return;
Surface.Attach(newDevices);
_texture?.Invalidate();
} }
public void RemoveDevices(IEnumerable<ArtemisDevice> devices) public void RemoveDevices(IEnumerable<ArtemisDevice> devices)
{ {
lock (_renderLock) List<IRGBDevice> removedDevices = new();
lock (_devices)
{ {
foreach (ArtemisDevice artemisDevice in devices) foreach (ArtemisDevice artemisDevice in devices)
{ {
if (!_devices.Remove(artemisDevice))
continue;
artemisDevice.DeviceUpdated -= ArtemisDeviceOnDeviceUpdated; artemisDevice.DeviceUpdated -= ArtemisDeviceOnDeviceUpdated;
Surface.Detach(artemisDevice.RgbDevice); removedDevices.Add(artemisDevice.RgbDevice);
_devices.Remove(artemisDevice); _devices.Remove(artemisDevice);
} }
Update();
} }
if (!removedDevices.Any())
return;
Surface.Detach(removedDevices);
_texture?.Invalidate();
} }
public bool SetPaused(bool paused) public bool SetPaused(bool paused)
@ -130,32 +100,33 @@ internal sealed class SurfaceManager : IDisposable
return true; return true;
} }
private void Update() public void UpdateTargetFrameRate(int targetFrameRate)
{ {
lock (_renderLock) TargetFrameRate = targetFrameRate;
{ _updateTrigger.UpdateFrequency = 1.0 / TargetFrameRate;
UpdateLedGroup();
CreateTexture();
}
} }
private void UpdateLedGroup() public void UpdateRenderScale(float renderScale)
{ {
List<Led> leds = _devices.SelectMany(d => d.Leds).Select(l => l.RgbLed).ToList(); RenderScale = renderScale;
_texture?.Invalidate();
}
if (_surfaceLedGroup == null) public void UpdateGraphicsContext(IManagedGraphicsContext? graphicsContext)
{ {
_surfaceLedGroup = new ListLedGroup(Surface, leds) {Brush = _textureBrush}; GraphicsContext = graphicsContext;
LedsChanged?.Invoke(this, EventArgs.Empty); _texture?.Invalidate();
return; }
}
// Clean up the old background /// <inheritdoc />
_surfaceLedGroup.Detach(); public void Dispose()
{
SetPaused(true);
Surface.UnregisterUpdateTrigger(_updateTrigger);
// Apply the application wide brush and decorator _updateTrigger.Dispose();
_surfaceLedGroup = new ListLedGroup(Surface, leds) {Brush = _textureBrush}; _texture?.Dispose();
LedsChanged?.Invoke(this, EventArgs.Empty); Surface.Dispose();
} }
private SKTexture CreateTexture() private SKTexture CreateTexture()
@ -170,53 +141,59 @@ internal sealed class SurfaceManager : IDisposable
int width = Math.Max(1, MathF.Min(evenWidth * RenderScale, 4096).RoundToInt()); int width = Math.Max(1, MathF.Min(evenWidth * RenderScale, 4096).RoundToInt());
int height = Math.Max(1, MathF.Min(evenHeight * RenderScale, 4096).RoundToInt()); int height = Math.Max(1, MathF.Min(evenHeight * RenderScale, 4096).RoundToInt());
_texture?.Dispose(); lock (_devices)
_texture = new SKTexture(GraphicsContext, width, height, RenderScale, _devices); {
_textureBrush.Texture = _texture; _texture?.Dispose();
_texture = new SKTexture(GraphicsContext, width, height, RenderScale, _devices);
_textureBrush.Texture = _texture;
_surfaceLedGroup?.Detach();
_surfaceLedGroup = new ListLedGroup(Surface, _devices.SelectMany(d => d.Leds).Select(l => l.RgbLed)) {Brush = _textureBrush};
}
return _texture; return _texture;
} }
private void SurfaceOnUpdating(UpdatingEventArgs args)
{
SKTexture? texture = _texture;
if (texture == null || texture.IsInvalid)
texture = CreateTexture();
// Prepare a canvas
SKCanvas canvas = texture.Surface.Canvas;
canvas.Save();
// Apply scaling if necessary
if (Math.Abs(texture.RenderScale - 1) > 0.001)
canvas.Scale(texture.RenderScale);
// Fresh start!
canvas.Clear(new SKColor(0, 0, 0));
try
{
_renderer.Render(canvas, args.DeltaTime);
}
finally
{
canvas.RestoreToCount(-1);
canvas.Flush();
texture.CopyPixelData();
}
try
{
_renderer.PostRender(texture);
}
catch
{
// ignored
}
}
private void ArtemisDeviceOnDeviceUpdated(object? sender, EventArgs e) private void ArtemisDeviceOnDeviceUpdated(object? sender, EventArgs e)
{ {
Update(); _texture?.Invalidate();
}
public event EventHandler? LedsChanged;
/// <inheritdoc />
public void Dispose()
{
SetPaused(true);
Surface.UnregisterUpdateTrigger(_updateTrigger);
_updateTrigger.Dispose();
_texture?.Dispose();
Surface.Dispose();
}
public void UpdateTargetFrameRate(int targetFrameRate)
{
TargetFrameRate = targetFrameRate;
_updateTrigger.UpdateFrequency = 1.0 / TargetFrameRate;
}
public void UpdateRenderScale(float renderScale)
{
lock (_renderLock)
{
RenderScale = renderScale;
_texture?.Invalidate();
}
}
public void UpdateGraphicsContext(IManagedGraphicsContext? graphicsContext)
{
lock (_renderLock)
{
GraphicsContext = graphicsContext;
_texture?.Dispose();
_texture = null;
}
} }
} }

View File

@ -0,0 +1,42 @@
using Artemis.Core.ScriptingProviders;
using Artemis.Core.Services.Core;
using SkiaSharp;
namespace Artemis.Core.Services;
internal class CoreRenderer : IRenderer
{
private readonly IModuleService _moduleService;
private readonly IScriptingService _scriptingService;
private readonly IProfileService _profileService;
public CoreRenderer(IModuleService moduleService, IScriptingService scriptingService, IProfileService profileService)
{
_moduleService = moduleService;
_scriptingService = scriptingService;
_profileService = profileService;
}
/// <inheritdoc />
public void Render(SKCanvas canvas, double delta)
{
foreach (GlobalScript scriptingServiceGlobalScript in _scriptingService.GlobalScripts)
scriptingServiceGlobalScript.OnCoreUpdating(delta);
_moduleService.UpdateActiveModules(delta);
if (!_profileService.ProfileRenderingDisabled)
{
_profileService.UpdateProfiles(delta);
_profileService.RenderProfiles(canvas);
}
foreach (GlobalScript scriptingServiceGlobalScript in _scriptingService.GlobalScripts)
scriptingServiceGlobalScript.OnCoreUpdated(delta);
}
/// <inheritdoc />
public void PostRender(SKTexture texture)
{
}
}

View File

@ -30,13 +30,13 @@ internal class DeviceService : IDeviceService
EnabledDevices = new ReadOnlyCollection<ArtemisDevice>(_enabledDevices); EnabledDevices = new ReadOnlyCollection<ArtemisDevice>(_enabledDevices);
Devices = new ReadOnlyCollection<ArtemisDevice>(_devices); Devices = new ReadOnlyCollection<ArtemisDevice>(_devices);
RenderScale.RenderScaleMultiplierChanged += RenderScaleOnRenderScaleMultiplierChanged; RenderScale.RenderScaleMultiplierChanged += RenderScaleOnRenderScaleMultiplierChanged;
} }
public IReadOnlyCollection<ArtemisDevice> EnabledDevices { get; } public IReadOnlyCollection<ArtemisDevice> EnabledDevices { get; }
public IReadOnlyCollection<ArtemisDevice> Devices { get; } public IReadOnlyCollection<ArtemisDevice> Devices { get; }
/// <inheritdoc /> /// <inheritdoc />
public void IdentifyDevice(ArtemisDevice device) public void IdentifyDevice(ArtemisDevice device)
{ {
@ -52,11 +52,12 @@ internal class DeviceService : IDeviceService
try try
{ {
// Can't see why this would happen, RgbService used to do this though // Can't see why this would happen, RgbService used to do this though
List<ArtemisDevice> toRemove = _devices.Where(a => rgbDeviceProvider.Devices.Any(d => a.RgbDevice == d)).ToList(); List<ArtemisDevice> toRemove = _devices.Where(a => a.DeviceProvider.Id == deviceProvider.Id).ToList();
_logger.Verbose("[AddDeviceProvider] Removing {Count} old device(s)", toRemove.Count); _logger.Verbose("[AddDeviceProvider] Removing {Count} old device(s)", toRemove.Count);
foreach (ArtemisDevice device in toRemove) foreach (ArtemisDevice device in toRemove)
{ {
_devices.Remove(device); _devices.Remove(device);
_enabledDevices.Remove(device);
OnDeviceRemoved(new DeviceEventArgs(device)); OnDeviceRemoved(new DeviceEventArgs(device));
} }
@ -94,7 +95,7 @@ internal class DeviceService : IDeviceService
_devices.Add(artemisDevice); _devices.Add(artemisDevice);
if (artemisDevice.IsEnabled) if (artemisDevice.IsEnabled)
_enabledDevices.Add(artemisDevice); _enabledDevices.Add(artemisDevice);
_logger.Debug("Device provider {deviceProvider} added {deviceName}", deviceProvider.GetType().Name, rgbDevice.DeviceInfo.DeviceName); _logger.Debug("Device provider {deviceProvider} added {deviceName}", deviceProvider.GetType().Name, rgbDevice.DeviceInfo.DeviceName);
} }
@ -104,7 +105,7 @@ internal class DeviceService : IDeviceService
OnDeviceProviderAdded(new DeviceProviderEventArgs(deviceProvider, addedDevices)); OnDeviceProviderAdded(new DeviceProviderEventArgs(deviceProvider, addedDevices));
foreach (ArtemisDevice artemisDevice in addedDevices) foreach (ArtemisDevice artemisDevice in addedDevices)
OnDeviceAdded(new DeviceEventArgs(artemisDevice)); OnDeviceAdded(new DeviceEventArgs(artemisDevice));
UpdateLeds(); UpdateLeds();
} }
catch (Exception e) catch (Exception e)
@ -118,9 +119,8 @@ internal class DeviceService : IDeviceService
public void RemoveDeviceProvider(DeviceProvider deviceProvider) public void RemoveDeviceProvider(DeviceProvider deviceProvider)
{ {
_logger.Verbose("[RemoveDeviceProvider] Pausing rendering to remove {DeviceProvider}", deviceProvider.GetType().Name); _logger.Verbose("[RemoveDeviceProvider] Pausing rendering to remove {DeviceProvider}", deviceProvider.GetType().Name);
IRGBDeviceProvider rgbDeviceProvider = deviceProvider.RgbDeviceProvider; List<ArtemisDevice> toRemove = _devices.Where(a => a.DeviceProvider.Id == deviceProvider.Id).ToList();
List<ArtemisDevice> toRemove = _devices.Where(a => rgbDeviceProvider.Devices.Any(d => a.RgbDevice == d)).ToList();
try try
{ {
_logger.Verbose("[RemoveDeviceProvider] Removing {Count} old device(s)", toRemove.Count); _logger.Verbose("[RemoveDeviceProvider] Removing {Count} old device(s)", toRemove.Count);
@ -131,11 +131,11 @@ internal class DeviceService : IDeviceService
} }
_devices.Sort((a, b) => a.ZIndex - b.ZIndex); _devices.Sort((a, b) => a.ZIndex - b.ZIndex);
OnDeviceProviderRemoved(new DeviceProviderEventArgs(deviceProvider, toRemove)); OnDeviceProviderRemoved(new DeviceProviderEventArgs(deviceProvider, toRemove));
foreach (ArtemisDevice artemisDevice in toRemove) foreach (ArtemisDevice artemisDevice in toRemove)
OnDeviceRemoved(new DeviceEventArgs(artemisDevice)); OnDeviceRemoved(new DeviceEventArgs(artemisDevice));
UpdateLeds(); UpdateLeds();
} }
catch (Exception e) catch (Exception e)
@ -155,7 +155,7 @@ internal class DeviceService : IDeviceService
SaveDevices(); SaveDevices();
} }
/// <inheritdoc /> /// <inheritdoc />
public void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout? layout) public void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout? layout)
{ {
@ -163,10 +163,10 @@ internal class DeviceService : IDeviceService
device.ApplyLayout(layout, false, false); device.ApplyLayout(layout, false, false);
else else
device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported); device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported);
UpdateLeds(); UpdateLeds();
} }
/// <inheritdoc /> /// <inheritdoc />
public void EnableDevice(ArtemisDevice device) public void EnableDevice(ArtemisDevice device)
{ {
@ -175,7 +175,7 @@ internal class DeviceService : IDeviceService
_enabledDevices.Add(device); _enabledDevices.Add(device);
device.IsEnabled = true; device.IsEnabled = true;
device.ApplyToEntity(); device.Save();
_deviceRepository.Save(device.DeviceEntity); _deviceRepository.Save(device.DeviceEntity);
OnDeviceEnabled(new DeviceEventArgs(device)); OnDeviceEnabled(new DeviceEventArgs(device));
@ -190,7 +190,7 @@ internal class DeviceService : IDeviceService
_enabledDevices.Remove(device); _enabledDevices.Remove(device);
device.IsEnabled = false; device.IsEnabled = false;
device.ApplyToEntity(); device.Save();
_deviceRepository.Save(device.DeviceEntity); _deviceRepository.Save(device.DeviceEntity);
OnDeviceDisabled(new DeviceEventArgs(device)); OnDeviceDisabled(new DeviceEventArgs(device));
@ -200,9 +200,7 @@ internal class DeviceService : IDeviceService
/// <inheritdoc /> /// <inheritdoc />
public void SaveDevice(ArtemisDevice artemisDevice) public void SaveDevice(ArtemisDevice artemisDevice)
{ {
artemisDevice.ApplyToEntity(); artemisDevice.Save();
artemisDevice.ApplyToRgbDevice();
_deviceRepository.Save(artemisDevice.DeviceEntity); _deviceRepository.Save(artemisDevice.DeviceEntity);
UpdateLeds(); UpdateLeds();
} }
@ -211,15 +209,11 @@ internal class DeviceService : IDeviceService
public void SaveDevices() public void SaveDevices()
{ {
foreach (ArtemisDevice artemisDevice in _devices) foreach (ArtemisDevice artemisDevice in _devices)
{ artemisDevice.Save();
artemisDevice.ApplyToEntity();
artemisDevice.ApplyToRgbDevice();
}
_deviceRepository.Save(_devices.Select(d => d.DeviceEntity)); _deviceRepository.Save(_devices.Select(d => d.DeviceEntity));
UpdateLeds(); UpdateLeds();
} }
private ArtemisDevice GetArtemisDevice(IRGBDevice rgbDevice) private ArtemisDevice GetArtemisDevice(IRGBDevice rgbDevice)
{ {
string deviceIdentifier = rgbDevice.GetDeviceIdentifier(); string deviceIdentifier = rgbDevice.GetDeviceIdentifier();
@ -236,11 +230,10 @@ internal class DeviceService : IDeviceService
device = new ArtemisDevice(rgbDevice, deviceProvider); device = new ArtemisDevice(rgbDevice, deviceProvider);
} }
device.ApplyToRgbDevice();
ApplyDeviceLayout(device, device.GetBestDeviceLayout()); ApplyDeviceLayout(device, device.GetBestDeviceLayout());
return device; return device;
} }
private void BlinkDevice(ArtemisDevice device, int blinkCount) private void BlinkDevice(ArtemisDevice device, int blinkCount)
{ {
RGBSurface surface = _renderService.Value.Surface; RGBSurface surface = _renderService.Value.Surface;
@ -266,7 +259,7 @@ internal class DeviceService : IDeviceService
} }
}); });
} }
private void CalculateRenderProperties() private void CalculateRenderProperties()
{ {
foreach (ArtemisDevice artemisDevice in Devices) foreach (ArtemisDevice artemisDevice in Devices)
@ -278,7 +271,7 @@ internal class DeviceService : IDeviceService
{ {
OnLedsChanged(); OnLedsChanged();
} }
private void RenderScaleOnRenderScaleMultiplierChanged(object? sender, EventArgs e) private void RenderScaleOnRenderScaleMultiplierChanged(object? sender, EventArgs e)
{ {
CalculateRenderProperties(); CalculateRenderProperties();
@ -303,7 +296,7 @@ internal class DeviceService : IDeviceService
/// <inheritdoc /> /// <inheritdoc />
public event EventHandler<DeviceProviderEventArgs>? DeviceProviderRemoved; public event EventHandler<DeviceProviderEventArgs>? DeviceProviderRemoved;
/// <inheritdoc /> /// <inheritdoc />
public event EventHandler? LedsChanged; public event EventHandler? LedsChanged;
@ -336,7 +329,7 @@ internal class DeviceService : IDeviceService
{ {
DeviceProviderRemoved?.Invoke(this, e); DeviceProviderRemoved?.Invoke(this, e);
} }
protected virtual void OnLedsChanged() protected virtual void OnLedsChanged()
{ {
LedsChanged?.Invoke(this, EventArgs.Empty); LedsChanged?.Invoke(this, EventArgs.Empty);

View File

@ -21,11 +21,6 @@ public interface IRenderService : IArtemisService
/// </summary> /// </summary>
RGBSurface Surface { get; } RGBSurface Surface { get; }
/// <summary>
/// Gets a list of registered renderers.
/// </summary>
List<IRenderer> Renderers { get; }
/// <summary> /// <summary>
/// Gets or sets a boolean indicating whether rendering is paused. /// Gets or sets a boolean indicating whether rendering is paused.
/// </summary> /// </summary>

View File

@ -19,22 +19,24 @@ internal class RenderService : IRenderService, IRenderer, IDisposable
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IDeviceService _deviceService; private readonly IDeviceService _deviceService;
private readonly CoreRenderer _coreRenderer;
private readonly LazyEnumerable<IGraphicsContextProvider> _graphicsContextProviders; private readonly LazyEnumerable<IGraphicsContextProvider> _graphicsContextProviders;
private readonly PluginSetting<int> _targetFrameRateSetting; private readonly PluginSetting<int> _targetFrameRateSetting;
private readonly PluginSetting<double> _renderScaleSetting; private readonly PluginSetting<double> _renderScaleSetting;
private readonly PluginSetting<string> _preferredGraphicsContext; private readonly PluginSetting<string> _preferredGraphicsContext;
private readonly SurfaceManager _surfaceManager;
private SurfaceManager _surfaceManager;
private int _frames; private int _frames;
private DateTime _lastExceptionLog; private DateTime _lastExceptionLog;
private DateTime _lastFrameRateSample; private DateTime _lastFrameRateSample;
private bool _initialized; private bool _initialized;
public RenderService(ILogger logger, ISettingsService settingsService, IDeviceService deviceService, LazyEnumerable<IGraphicsContextProvider> graphicsContextProviders) public RenderService(ILogger logger, ISettingsService settingsService, IDeviceService deviceService, CoreRenderer coreRenderer, LazyEnumerable<IGraphicsContextProvider> graphicsContextProviders)
{ {
_frameStopWatch = new Stopwatch(); _frameStopWatch = new Stopwatch();
_logger = logger; _logger = logger;
_deviceService = deviceService; _deviceService = deviceService;
_coreRenderer = coreRenderer;
_graphicsContextProviders = graphicsContextProviders; _graphicsContextProviders = graphicsContextProviders;
_targetFrameRateSetting = settingsService.GetSetting("Core.TargetFrameRate", 30); _targetFrameRateSetting = settingsService.GetSetting("Core.TargetFrameRate", 30);
@ -54,9 +56,6 @@ internal class RenderService : IRenderService, IRenderer, IDisposable
/// <inheritdoc /> /// <inheritdoc />
public RGBSurface Surface => _surfaceManager.Surface; public RGBSurface Surface => _surfaceManager.Surface;
/// <inheritdoc />
public List<IRenderer> Renderers { get; } = new();
/// <inheritdoc /> /// <inheritdoc />
public bool IsPaused public bool IsPaused
{ {
@ -77,8 +76,7 @@ internal class RenderService : IRenderService, IRenderer, IDisposable
try try
{ {
OnFrameRendering(new FrameRenderingEventArgs(canvas, delta, _surfaceManager.Surface)); OnFrameRendering(new FrameRenderingEventArgs(canvas, delta, _surfaceManager.Surface));
foreach (IRenderer renderer in Renderers) _coreRenderer.Render(canvas, delta);
renderer.Render(canvas, delta);
} }
catch (Exception e) catch (Exception e)
{ {
@ -91,8 +89,7 @@ internal class RenderService : IRenderService, IRenderer, IDisposable
{ {
try try
{ {
foreach (IRenderer renderer in Renderers) _coreRenderer.PostRender(texture);
renderer.PostRender(texture);
OnFrameRendered(new FrameRenderedEventArgs(texture, _surfaceManager.Surface)); OnFrameRendered(new FrameRenderedEventArgs(texture, _surfaceManager.Surface));
} }
catch (Exception e) catch (Exception e)

View File

@ -12,7 +12,7 @@ internal class ScriptingService : IScriptingService
private readonly IPluginManagementService _pluginManagementService; private readonly IPluginManagementService _pluginManagementService;
private readonly IProfileService _profileService; private readonly IProfileService _profileService;
private readonly List<ScriptingProvider> _scriptingProviders; private readonly List<ScriptingProvider> _scriptingProviders;
public ScriptingService(IPluginManagementService pluginManagementService, IProfileService profileService) public ScriptingService(IPluginManagementService pluginManagementService, IProfileService profileService)
{ {
_pluginManagementService = pluginManagementService; _pluginManagementService = pluginManagementService;

View File

@ -27,7 +27,6 @@ internal class SurfaceArrangement
{ {
surfaceDevice.X = 0; surfaceDevice.X = 0;
surfaceDevice.Y = 0; surfaceDevice.Y = 0;
surfaceDevice.ApplyToRgbDevice();
} }
foreach (SurfaceArrangementType surfaceArrangementType in Types) foreach (SurfaceArrangementType surfaceArrangementType in Types)
@ -37,18 +36,16 @@ internal class SurfaceArrangement
float x = devices.Min(d => d.RgbDevice.Location.X); float x = devices.Min(d => d.RgbDevice.Location.X);
float y = devices.Min(d => d.RgbDevice.Location.Y); float y = devices.Min(d => d.RgbDevice.Location.Y);
if (x < 0) if (x < 0)
{
foreach (ArtemisDevice surfaceDevice in devices) foreach (ArtemisDevice surfaceDevice in devices)
{
surfaceDevice.X += x * -1; surfaceDevice.X += x * -1;
surfaceDevice.ApplyToRgbDevice(); }
}
if (y < 0) if (y < 0)
{
foreach (ArtemisDevice surfaceDevice in devices) foreach (ArtemisDevice surfaceDevice in devices)
{
surfaceDevice.Y += y * -1; surfaceDevice.Y += y * -1;
surfaceDevice.ApplyToRgbDevice(); }
}
} }
internal static SurfaceArrangement GetDefaultArrangement() internal static SurfaceArrangement GetDefaultArrangement()

View File

@ -80,9 +80,7 @@ internal class SurfaceArrangementConfiguration
}; };
} }
artemisDevice.ApplyToRgbDevice();
previous = artemisDevice; previous = artemisDevice;
SurfaceArrangement.ArrangedDevices.Add(artemisDevice); SurfaceArrangement.ArrangedDevices.Add(artemisDevice);
} }

View File

@ -7,7 +7,6 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.Core.Modules; using Artemis.Core.Modules;
using Artemis.Core.Services.Core;
using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile;
using Artemis.Storage.Repositories.Interfaces; using Artemis.Storage.Repositories.Interfaces;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -16,7 +15,7 @@ using SkiaSharp;
namespace Artemis.Core.Services; namespace Artemis.Core.Services;
internal class ProfileService : IProfileService, IRenderer internal class ProfileService : IProfileService
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IProfileCategoryRepository _profileCategoryRepository; private readonly IProfileCategoryRepository _profileCategoryRepository;
@ -36,7 +35,6 @@ internal class ProfileService : IProfileService, IRenderer
IPluginManagementService pluginManagementService, IPluginManagementService pluginManagementService,
IInputService inputService, IInputService inputService,
IDeviceService deviceService, IDeviceService deviceService,
IRenderService renderService,
IProfileRepository profileRepository) IProfileRepository profileRepository)
{ {
_logger = logger; _logger = logger;
@ -55,8 +53,6 @@ internal class ProfileService : IProfileService, IRenderer
if (!_profileCategories.Any()) if (!_profileCategories.Any())
CreateDefaultProfileCategories(); CreateDefaultProfileCategories();
UpdateModules(); UpdateModules();
renderService.Renderers.Add(this);
} }
public ProfileConfiguration? FocusProfile { get; set; } public ProfileConfiguration? FocusProfile { get; set; }
@ -187,21 +183,6 @@ internal class ProfileService : IProfileService, IRenderer
} }
} }
} }
/// <inheritdoc />
public void Render(SKCanvas canvas, double delta)
{
if (ProfileRenderingDisabled)
return;
UpdateProfiles(delta);
RenderProfiles(canvas);
}
/// <inheritdoc />
public void PostRender(SKTexture texture)
{
}
/// <inheritdoc /> /// <inheritdoc />
public void LoadProfileConfigurationIcon(ProfileConfiguration profileConfiguration) public void LoadProfileConfigurationIcon(ProfileConfiguration profileConfiguration)

View File

@ -95,35 +95,29 @@ public interface IWebServerService : IArtemisService
/// Adds a new Web API controller and restarts the web server /// Adds a new Web API controller and restarts the web server
/// </summary> /// </summary>
/// <typeparam name="T">The type of Web API controller to remove</typeparam> /// <typeparam name="T">The type of Web API controller to remove</typeparam>
void AddController<T>(PluginFeature feature) where T : WebApiController; WebApiControllerRegistration AddController<T>(PluginFeature feature) where T : WebApiController;
/// <summary> /// <summary>
/// Removes an existing Web API controller and restarts the web server /// Removes an existing Web API controller and restarts the web server
/// </summary> /// </summary>
/// <typeparam name="T">The type of Web API controller to remove</typeparam> /// <typeparam name="T">The type of Web API controller to remove</typeparam>
void RemoveController<T>() where T : WebApiController; void RemoveController(WebApiControllerRegistration registration);
/// <summary> /// <summary>
/// Adds a new EmbedIO module and restarts the web server /// Adds a new EmbedIO module and restarts the web server
/// </summary> /// </summary>
void AddModule(PluginFeature feature, Func<IWebModule> create); WebModuleRegistration AddModule(PluginFeature feature, Func<IWebModule> create);
/// <summary> /// <summary>
/// Removes a EmbedIO module and restarts the web server /// Removes a EmbedIO module and restarts the web server
/// </summary> /// </summary>
void RemoveModule(Func<IWebModule> create); void RemoveModule(WebModuleRegistration create);
/// <summary> /// <summary>
/// Adds a new EmbedIO module and restarts the web server /// Adds a new EmbedIO module and restarts the web server
/// </summary> /// </summary>
/// <typeparam name="T">The type of module to add</typeparam> /// <typeparam name="T">The type of module to add</typeparam>
void AddModule<T>(PluginFeature feature) where T : IWebModule; WebModuleRegistration AddModule<T>(PluginFeature feature) where T : IWebModule;
/// <summary>
/// Removes a EmbedIO module and restarts the web server
/// </summary>
/// <typeparam name="T">The type of module to remove</typeparam>
void RemoveModule<T>() where T : IWebModule;
/// <summary> /// <summary>
/// Occurs when the web server has been created and is about to start. This is the ideal place to add your own modules. /// Occurs when the web server has been created and is about to start. This is the ideal place to add your own modules.

View File

@ -3,26 +3,55 @@ using EmbedIO.WebApi;
namespace Artemis.Core.Services; namespace Artemis.Core.Services;
internal class WebApiControllerRegistration<T> : WebApiControllerRegistration where T : WebApiController /// <summary>
/// Represents a web API controller registration.
/// </summary>
/// <typeparam name="T">The type of the web API controller.</typeparam>
public class WebApiControllerRegistration<T> : WebApiControllerRegistration where T : WebApiController
{ {
public WebApiControllerRegistration(PluginFeature feature) : base(feature, typeof(T)) internal WebApiControllerRegistration(IWebServerService webServerService, PluginFeature feature) : base(webServerService, feature, typeof(T))
{ {
Factory = () => feature.Plugin.Resolve<T>(); Factory = () => feature.Plugin.Resolve<T>();
} }
public Func<T> Factory { get; set; } internal Func<T> Factory { get; set; }
public override object UntypedFactory => Factory; internal override object UntypedFactory => Factory;
} }
internal abstract class WebApiControllerRegistration /// <summary>
/// Represents a web API controller registration.
/// </summary>
public abstract class WebApiControllerRegistration
{ {
protected WebApiControllerRegistration(PluginFeature feature, Type controllerType) private readonly IWebServerService _webServerService;
/// <summary>
/// Creates a new instance of the <see cref="WebApiControllerRegistration"/> class.
/// </summary>
protected internal WebApiControllerRegistration(IWebServerService webServerService, PluginFeature feature, Type controllerType)
{ {
_webServerService = webServerService;
Feature = feature; Feature = feature;
ControllerType = controllerType; ControllerType = controllerType;
Feature.Disabled += FeatureOnDisabled;
} }
public abstract object UntypedFactory { get; } private void FeatureOnDisabled(object? sender, EventArgs e)
public Type ControllerType { get; set; } {
_webServerService.RemoveController(this);
Feature.Disabled -= FeatureOnDisabled;
}
internal abstract object UntypedFactory { get; }
/// <summary>
/// Gets the type of the web API controller.
/// </summary>
public Type ControllerType { get; }
/// <summary>
/// Gets the plugin feature that provided the web API controller.
/// </summary>
public PluginFeature Feature { get; } public PluginFeature Feature { get; }
} }

View File

@ -3,25 +3,44 @@ using EmbedIO;
namespace Artemis.Core.Services; namespace Artemis.Core.Services;
internal class WebModuleRegistration /// <summary>
/// Represents a registration for a web module.
/// </summary>
public class WebModuleRegistration
{ {
public WebModuleRegistration(PluginFeature feature, Type webModuleType) private readonly IWebServerService _webServerService;
internal WebModuleRegistration(IWebServerService webServerService, PluginFeature feature, Type webModuleType)
{ {
_webServerService = webServerService;
Feature = feature ?? throw new ArgumentNullException(nameof(feature)); Feature = feature ?? throw new ArgumentNullException(nameof(feature));
WebModuleType = webModuleType ?? throw new ArgumentNullException(nameof(webModuleType)); WebModuleType = webModuleType ?? throw new ArgumentNullException(nameof(webModuleType));
Feature.Disabled += FeatureOnDisabled;
} }
public WebModuleRegistration(PluginFeature feature, Func<IWebModule> create) internal WebModuleRegistration(IWebServerService webServerService, PluginFeature feature, Func<IWebModule> create)
{ {
_webServerService = webServerService;
Feature = feature ?? throw new ArgumentNullException(nameof(feature)); Feature = feature ?? throw new ArgumentNullException(nameof(feature));
Create = create ?? throw new ArgumentNullException(nameof(create)); Create = create ?? throw new ArgumentNullException(nameof(create));
Feature.Disabled += FeatureOnDisabled;
} }
/// <summary>
/// The plugin feature that provided the web module.
/// </summary>
public PluginFeature Feature { get; } public PluginFeature Feature { get; }
public Type? WebModuleType { get; }
public Func<IWebModule>? Create { get; }
public IWebModule CreateInstance() /// <summary>
/// The type of the web module.
/// </summary>
public Type? WebModuleType { get; }
internal Func<IWebModule>? Create { get; }
internal IWebModule CreateInstance()
{ {
if (Create != null) if (Create != null)
return Create(); return Create();
@ -29,4 +48,10 @@ internal class WebModuleRegistration
return (IWebModule) Feature.Plugin.Resolve(WebModuleType); return (IWebModule) Feature.Plugin.Resolve(WebModuleType);
throw new ArtemisCoreException("WebModuleRegistration doesn't have a create function nor a web module type :("); throw new ArtemisCoreException("WebModuleRegistration doesn't have a create function nor a web module type :(");
} }
private void FeatureOnDisabled(object? sender, EventArgs e)
{
_webServerService.RemoveModule(this);
Feature.Disabled -= FeatureOnDisabled;
}
} }

View File

@ -39,9 +39,9 @@ internal class WebServerService : IWebServerService, IDisposable
PluginsModule = new PluginsModule("/plugins"); PluginsModule = new PluginsModule("/plugins");
if (coreService.IsInitialized) if (coreService.IsInitialized)
StartWebServer(); AutoStartWebServer();
else else
coreService.Initialized += (_, _) => StartWebServer(); coreService.Initialized += (sender, args) => AutoStartWebServer();
} }
public event EventHandler? WebServerStopped; public event EventHandler? WebServerStopped;
@ -138,7 +138,7 @@ internal class WebServerService : IWebServerService, IDisposable
// Add registered controllers to the API module // Add registered controllers to the API module
foreach (WebApiControllerRegistration registration in _controllers) foreach (WebApiControllerRegistration registration in _controllers)
apiModule.RegisterController(registration.ControllerType, (Func<WebApiController>) registration.UntypedFactory); apiModule.RegisterController(registration.ControllerType, (Func<WebApiController>) registration.UntypedFactory);
// Listen for state changes. // Listen for state changes.
server.StateChanged += (s, e) => _logger.Verbose("WebServer new state - {state}", e.NewState); server.StateChanged += (s, e) => _logger.Verbose("WebServer new state - {state}", e.NewState);
@ -173,6 +173,18 @@ internal class WebServerService : IWebServerService, IDisposable
OnWebServerStarted(); OnWebServerStarted();
} }
} }
private void AutoStartWebServer()
{
try
{
StartWebServer();
}
catch (Exception exception)
{
_logger.Warning(exception, "Failed to initially start webserver");
}
}
#endregion #endregion
@ -237,10 +249,6 @@ internal class WebServerService : IWebServerService, IDisposable
return endPoint; return endPoint;
} }
private void HandleDataModelRequest<T>(Module<T> module, T value) where T : DataModel, new()
{
}
public void RemovePluginEndPoint(PluginEndPoint endPoint) public void RemovePluginEndPoint(PluginEndPoint endPoint)
{ {
PluginsModule.RemovePluginEndPoint(endPoint); PluginsModule.RemovePluginEndPoint(endPoint);
@ -250,15 +258,20 @@ internal class WebServerService : IWebServerService, IDisposable
#region Controller management #region Controller management
public void AddController<T>(PluginFeature feature) where T : WebApiController public WebApiControllerRegistration AddController<T>(PluginFeature feature) where T : WebApiController
{ {
_controllers.Add(new WebApiControllerRegistration<T>(feature)); if (feature == null) throw new ArgumentNullException(nameof(feature));
WebApiControllerRegistration<T> registration = new(this, feature);
_controllers.Add(registration);
StartWebServer(); StartWebServer();
return registration;
} }
public void RemoveController<T>() where T : WebApiController public void RemoveController(WebApiControllerRegistration registration)
{ {
_controllers.RemoveAll(r => r.ControllerType == typeof(T)); _controllers.Remove(registration);
StartWebServer(); StartWebServer();
} }
@ -266,33 +279,31 @@ internal class WebServerService : IWebServerService, IDisposable
#region Module management #region Module management
public void AddModule(PluginFeature feature, Func<IWebModule> create) public WebModuleRegistration AddModule(PluginFeature feature, Func<IWebModule> create)
{ {
if (feature == null) throw new ArgumentNullException(nameof(feature)); if (feature == null) throw new ArgumentNullException(nameof(feature));
_modules.Add(new WebModuleRegistration(feature, create)); WebModuleRegistration registration = new(this, feature, create);
_modules.Add(registration);
StartWebServer(); StartWebServer();
return registration;
} }
public void RemoveModule(Func<IWebModule> create) public WebModuleRegistration AddModule<T>(PluginFeature feature) where T : IWebModule
{
_modules.RemoveAll(r => r.Create == create);
StartWebServer();
}
public void AddModule<T>(PluginFeature feature) where T : IWebModule
{ {
if (feature == null) throw new ArgumentNullException(nameof(feature)); if (feature == null) throw new ArgumentNullException(nameof(feature));
if (_modules.Any(r => r.WebModuleType == typeof(T)))
return;
_modules.Add(new WebModuleRegistration(feature, typeof(T))); WebModuleRegistration registration = new(this, feature, typeof(T));
_modules.Add(registration);
StartWebServer(); StartWebServer();
return registration;
} }
public void RemoveModule<T>() where T : IWebModule public void RemoveModule(WebModuleRegistration registration)
{ {
_modules.RemoveAll(r => r.WebModuleType == typeof(T)); _modules.Remove(registration);
StartWebServer(); StartWebServer();
} }

View File

@ -123,27 +123,35 @@ public class DeviceVisualizer : Control
if (Device == null) if (Device == null)
return false; return false;
bool difference = false; // Device might be modified mid-check, in that case just pretend it was not dirty
try
int newLedCount = Device.RgbDevice.Count();
if (_previousState.Length != newLedCount)
{ {
_previousState = new Color[newLedCount]; bool difference = false;
difference = true;
}
// Check all LEDs for differences and copy the colors to a new state int newLedCount = Device.RgbDevice.Count();
int index = 0; if (_previousState.Length != newLedCount)
foreach (Led led in Device.RgbDevice) {
{ _previousState = new Color[newLedCount];
if (_previousState[index] != led.Color)
difference = true; difference = true;
}
_previousState[index] = led.Color; // Check all LEDs for differences and copy the colors to a new state
index++; int index = 0;
foreach (Led led in Device.RgbDevice)
{
if (_previousState[index] != led.Color)
difference = true;
_previousState[index] = led.Color;
index++;
}
return difference;
}
catch (Exception)
{
return false;
} }
return difference;
} }
private void Update() private void Update()
@ -153,9 +161,9 @@ public class DeviceVisualizer : Control
private Rect MeasureDevice() private Rect MeasureDevice()
{ {
if (Device == null) if (Device == null || float.IsNaN(Device.RgbDevice.ActualSize.Width) || float.IsNaN(Device.RgbDevice.ActualSize.Height))
return new Rect(); return new Rect();
Rect deviceRect = new(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height); Rect deviceRect = new(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height);
Geometry geometry = new RectangleGeometry(deviceRect); Geometry geometry = new RectangleGeometry(deviceRect);
geometry.Transform = new RotateTransform(Device.Rotation); geometry.Transform = new RotateTransform(Device.Rotation);
@ -335,7 +343,7 @@ public class DeviceVisualizer : Control
deviceVisualizerLed.DrawBitmap(context, 2 * device.Scale); deviceVisualizerLed.DrawBitmap(context, 2 * device.Scale);
} }
BitmapCache[path] = renderTargetBitmap; // BitmapCache[path] = renderTargetBitmap;
return renderTargetBitmap; return renderTargetBitmap;
} }

View File

@ -45,9 +45,9 @@ public class RemoteController : WebApiController
} }
[Route(HttpVerbs.Post, "/remote/restart")] [Route(HttpVerbs.Post, "/remote/restart")]
public void PostRestart() public void PostRestart([FormField] string[] args)
{ {
Utilities.Restart(_coreService.IsElevated, TimeSpan.FromMilliseconds(500)); Utilities.Restart(_coreService.IsElevated, TimeSpan.FromMilliseconds(500), args);
} }
[Route(HttpVerbs.Post, "/remote/shutdown")] [Route(HttpVerbs.Post, "/remote/shutdown")]

View File

@ -52,7 +52,7 @@
<TextBlock Classes="subtitle" FontSize="12" Text="With this checked, Artemis will not load a layout for this device unless you specifically provide one." /> <TextBlock Classes="subtitle" FontSize="12" Text="With this checked, Artemis will not load a layout for this device unless you specifically provide one." />
</StackPanel> </StackPanel>
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center"> <StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center">
<CheckBox HorizontalAlignment="Right" Margin="0,0,-10,0" IsChecked="{CompiledBinding Device.DisableDefaultLayout}" /> <CheckBox HorizontalAlignment="Right" Margin="0,0,-10,0" IsChecked="{CompiledBinding Device.DisableDefaultLayout}"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
<Border Classes="card-separator" /> <Border Classes="card-separator" />

View File

@ -33,7 +33,7 @@ public class DeviceLayoutTabViewModel : ActivatableViewModelBase
Device = device; Device = device;
DisplayName = "Layout"; DisplayName = "Layout";
DefaultLayoutPath = Device.DeviceProvider.LoadLayout(Device).FilePath; DefaultLayoutPath = Device.DeviceProvider.LoadLayout(Device).FilePath;
this.WhenActivated(d => this.WhenActivated(d =>
{ {
Device.PropertyChanged += DeviceOnPropertyChanged; Device.PropertyChanged += DeviceOnPropertyChanged;
@ -42,23 +42,21 @@ public class DeviceLayoutTabViewModel : ActivatableViewModelBase
} }
public ArtemisDevice Device { get; } public ArtemisDevice Device { get; }
public string DefaultLayoutPath { get; } public string DefaultLayoutPath { get; }
public string? ImagePath => Device.Layout?.Image?.LocalPath; public string? ImagePath => Device.Layout?.Image?.LocalPath;
public string? CustomLayoutPath => Device.CustomLayoutPath; public string? CustomLayoutPath => Device.CustomLayoutPath;
public bool HasCustomLayout => Device.CustomLayoutPath != null; public bool HasCustomLayout => Device.CustomLayoutPath != null;
public void ClearCustomLayout() public void ClearCustomLayout()
{ {
Device.CustomLayoutPath = null; Device.CustomLayoutPath = null;
_notificationService.CreateNotification() _notificationService.CreateNotification()
.WithMessage("Cleared imported layout.") .WithMessage("Cleared imported layout.")
.WithSeverity(NotificationSeverity.Informational); .WithSeverity(NotificationSeverity.Informational);
_deviceService.SaveDevice(Device);
} }
public async Task BrowseCustomLayout() public async Task BrowseCustomLayout()
@ -75,8 +73,6 @@ public class DeviceLayoutTabViewModel : ActivatableViewModelBase
.WithTitle("Imported layout") .WithTitle("Imported layout")
.WithMessage($"File loaded from {files[0]}") .WithMessage($"File loaded from {files[0]}")
.WithSeverity(NotificationSeverity.Informational); .WithSeverity(NotificationSeverity.Informational);
_deviceService.SaveDevice(Device);
} }
} }
@ -154,7 +150,12 @@ public class DeviceLayoutTabViewModel : ActivatableViewModelBase
{ {
if (e.PropertyName is nameof(Device.CustomLayoutPath) or nameof(Device.DisableDefaultLayout)) if (e.PropertyName is nameof(Device.CustomLayoutPath) or nameof(Device.DisableDefaultLayout))
{ {
Task.Run(() => _deviceService.ApplyDeviceLayout(Device, Device.GetBestDeviceLayout())); Task.Run(() =>
{
_deviceService.ApplyDeviceLayout(Device, Device.GetBestDeviceLayout());
_deviceService.SaveDevice(Device);
});
this.RaisePropertyChanged(nameof(CustomLayoutPath)); this.RaisePropertyChanged(nameof(CustomLayoutPath));
this.RaisePropertyChanged(nameof(HasCustomLayout)); this.RaisePropertyChanged(nameof(HasCustomLayout));
} }

View File

@ -10,8 +10,6 @@ namespace Artemis.UI.Screens.Device;
public partial class DeviceLogicalLayoutDialogView : ReactiveUserControl<DeviceLogicalLayoutDialogViewModel> public partial class DeviceLogicalLayoutDialogView : ReactiveUserControl<DeviceLogicalLayoutDialogViewModel>
{ {
private readonly AutoCompleteBox _autoCompleteBox;
public DeviceLogicalLayoutDialogView() public DeviceLogicalLayoutDialogView()
{ {
InitializeComponent(); InitializeComponent();
@ -23,8 +21,8 @@ public partial class DeviceLogicalLayoutDialogView : ReactiveUserControl<DeviceL
private async Task DelayedAutoFocus() private async Task DelayedAutoFocus()
{ {
await Task.Delay(200); await Task.Delay(200);
_autoCompleteBox.Focus(); RegionsAutoCompleteBox.Focus();
_autoCompleteBox.PopulateComplete(); RegionsAutoCompleteBox.PopulateComplete();
} }
private bool SearchRegions(string search, object item) private bool SearchRegions(string search, object item)

View File

@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Reactive; using System.Reactive;
using System.Reactive.Disposables;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
@ -23,6 +25,8 @@ public class SurfaceDeviceViewModel : ActivatableViewModelBase
private double _dragOffsetX; private double _dragOffsetX;
private double _dragOffsetY; private double _dragOffsetY;
private bool _isSelected; private bool _isSelected;
private float _x;
private float _y;
public SurfaceDeviceViewModel(ArtemisDevice device, SurfaceEditorViewModel surfaceEditorViewModel, IDeviceService deviceService, ISettingsService settingsService, IWindowService windowService) public SurfaceDeviceViewModel(ArtemisDevice device, SurfaceEditorViewModel surfaceEditorViewModel, IDeviceService deviceService, ISettingsService settingsService, IWindowService windowService)
{ {
@ -33,6 +37,14 @@ public class SurfaceDeviceViewModel : ActivatableViewModelBase
Device = device; Device = device;
SurfaceEditorViewModel = surfaceEditorViewModel; SurfaceEditorViewModel = surfaceEditorViewModel;
DetectInput = ReactiveCommand.CreateFromTask(ExecuteDetectInput, this.WhenAnyValue(vm => vm.CanDetectInput)); DetectInput = ReactiveCommand.CreateFromTask(ExecuteDetectInput, this.WhenAnyValue(vm => vm.CanDetectInput));
X = device.X;
Y = device.Y;
this.WhenActivated(d =>
{
Device.PropertyChanged += DeviceOnPropertyChanged;
Disposable.Create(() => Device.PropertyChanged -= DeviceOnPropertyChanged).DisposeWith(d);
});
} }
public ReactiveCommand<Unit, Unit> DetectInput { get; } public ReactiveCommand<Unit, Unit> DetectInput { get; }
@ -47,6 +59,18 @@ public class SurfaceDeviceViewModel : ActivatableViewModelBase
set => RaiseAndSetIfChanged(ref _isSelected, value); set => RaiseAndSetIfChanged(ref _isSelected, value);
} }
public float X
{
get => _x;
set => RaiseAndSetIfChanged(ref _x, value);
}
public float Y
{
get => _y;
set => RaiseAndSetIfChanged(ref _y, value);
}
public void StartMouseDrag(Point mouseStartPosition) public void StartMouseDrag(Point mouseStartPosition)
{ {
if (!IsSelected) if (!IsSelected)
@ -73,16 +97,16 @@ public class SurfaceDeviceViewModel : ActivatableViewModelBase
if (Fits(x, y, ignoreOverlap)) if (Fits(x, y, ignoreOverlap))
{ {
Device.X = x; X = x;
Device.Y = y; Y = y;
} }
else if (Fits(x, Device.Y, ignoreOverlap)) else if (Fits(x, Device.Y, ignoreOverlap))
{ {
Device.X = x; X = x;
} }
else if (Fits(Device.X, y, ignoreOverlap)) else if (Fits(Device.X, y, ignoreOverlap))
{ {
Device.Y = y; Y = y;
} }
} }
@ -100,10 +124,9 @@ public class SurfaceDeviceViewModel : ActivatableViewModelBase
IEnumerable<SKRect> own = Device.Leds IEnumerable<SKRect> own = Device.Leds
.Select(l => SKRect.Create(l.Rectangle.Left + x, l.Rectangle.Top + y, l.Rectangle.Width, l.Rectangle.Height)); .Select(l => SKRect.Create(l.Rectangle.Left + x, l.Rectangle.Top + y, l.Rectangle.Width, l.Rectangle.Height));
IEnumerable<SKRect> others = _deviceService.EnabledDevices IEnumerable<SKRect> others = SurfaceEditorViewModel.SurfaceDeviceViewModels
.Where(d => d != Device && d.IsEnabled) .Where(vm => vm != this && vm.Device.IsEnabled)
.SelectMany(d => d.Leds) .SelectMany(vm => vm.Device.Leds.Select(l => SKRect.Create(l.Rectangle.Left + vm.X, l.Rectangle.Top + vm.Y, l.Rectangle.Width, l.Rectangle.Height)));
.Select(l => SKRect.Create(l.Rectangle.Left + l.Device.X, l.Rectangle.Top + l.Device.Y, l.Rectangle.Width, l.Rectangle.Height));
return !own.Any(o => others.Any(l => l.IntersectsWith(o))); return !own.Any(o => others.Any(l => l.IntersectsWith(o)));
} }
@ -122,4 +145,18 @@ public class SurfaceDeviceViewModel : ActivatableViewModelBase
if (viewModel.MadeChanges) if (viewModel.MadeChanges)
_deviceService.SaveDevice(Device); _deviceService.SaveDevice(Device);
} }
public void Apply()
{
Device.X = X;
Device.Y = Y;
}
private void DeviceOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Device.X))
X = Device.X;
if (e.PropertyName == nameof(Device.Y))
Y = Device.Y;
}
} }

View File

@ -82,8 +82,8 @@
<ItemsControl Name="DeviceContainer" ItemsSource="{CompiledBinding SurfaceDeviceViewModels}" ClipToBounds="False"> <ItemsControl Name="DeviceContainer" ItemsSource="{CompiledBinding SurfaceDeviceViewModels}" ClipToBounds="False">
<ItemsControl.Styles> <ItemsControl.Styles>
<Style Selector="ContentPresenter"> <Style Selector="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding Device.X}" /> <Setter Property="Canvas.Left" Value="{CompiledBinding X, DataType=surfaceEditor:SurfaceDeviceViewModel}" />
<Setter Property="Canvas.Top" Value="{Binding Device.Y}" /> <Setter Property="Canvas.Top" Value="{CompiledBinding Y, DataType=surfaceEditor:SurfaceDeviceViewModel}" />
</Style> </Style>
</ItemsControl.Styles> </ItemsControl.Styles>
<ItemsControl.ItemsPanel> <ItemsControl.ItemsPanel>

View File

@ -135,6 +135,8 @@ public class SurfaceEditorViewModel : RoutableScreen, IMainScreenViewModel
try try
{ {
_saving = true; _saving = true;
foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in SurfaceDeviceViewModels)
surfaceDeviceViewModel.Apply();
_deviceService.SaveDevices(); _deviceService.SaveDevices();
} }
catch (Exception e) catch (Exception e)