diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj index bd0e5eb30..b13eae70e 100644 --- a/src/Artemis.Core/Artemis.Core.csproj +++ b/src/Artemis.Core/Artemis.Core.csproj @@ -73,6 +73,9 @@ ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Groups.dll + + ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Layout.dll + diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings index b1ea041c1..7d6c52363 100644 --- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings +++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings @@ -43,6 +43,7 @@ True True True + True True True True diff --git a/src/Artemis.Core/Events/DeviceConfigurationEventArgs.cs b/src/Artemis.Core/Events/DeviceConfigurationEventArgs.cs deleted file mode 100644 index dbc66515e..000000000 --- a/src/Artemis.Core/Events/DeviceConfigurationEventArgs.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Artemis.Core -{ - /// - /// Provides data about surface configuration related events - /// - public class SurfaceConfigurationEventArgs : EventArgs - { - internal SurfaceConfigurationEventArgs(ArtemisSurface surface) - { - Surface = surface; - } - - /// - /// Gets the active surface at the time the event fired - /// - public ArtemisSurface Surface { get; } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Events/DeviceEventArgs.cs b/src/Artemis.Core/Events/DeviceEventArgs.cs index 3999e9f83..3eaf045bf 100644 --- a/src/Artemis.Core/Events/DeviceEventArgs.cs +++ b/src/Artemis.Core/Events/DeviceEventArgs.cs @@ -1,5 +1,4 @@ using System; -using RGB.NET.Core; namespace Artemis.Core { @@ -8,7 +7,7 @@ namespace Artemis.Core /// public class DeviceEventArgs : EventArgs { - internal DeviceEventArgs(IRGBDevice device) + internal DeviceEventArgs(ArtemisDevice device) { Device = device; } @@ -16,6 +15,6 @@ namespace Artemis.Core /// /// Gets the device this event is related to /// - public IRGBDevice Device { get; } + public ArtemisDevice Device { get; } } } \ No newline at end of file diff --git a/src/Artemis.Core/Events/SurfaceConfigurationEventArgs.cs b/src/Artemis.Core/Events/SurfaceConfigurationEventArgs.cs new file mode 100644 index 000000000..52f8192a9 --- /dev/null +++ b/src/Artemis.Core/Events/SurfaceConfigurationEventArgs.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace Artemis.Core +{ + /// + /// Provides data about device configuration related events + /// + public class SurfaceConfigurationEventArgs : EventArgs + { + internal SurfaceConfigurationEventArgs(List devices) + { + Devices = devices; + } + + /// + /// Gets the current list of devices + /// + public List Devices { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Extensions/RgbDeviceExtensions.cs b/src/Artemis.Core/Extensions/RgbDeviceExtensions.cs index 12c6988d4..5c1e11353 100644 --- a/src/Artemis.Core/Extensions/RgbDeviceExtensions.cs +++ b/src/Artemis.Core/Extensions/RgbDeviceExtensions.cs @@ -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(); } } diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 5d7bcdb99..ace2734a9 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -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 devices) { if (Disposed) throw new ObjectDisposedException("Layer"); @@ -572,7 +575,7 @@ namespace Artemis.Core List leds = new(); // Get the surface LEDs for this layer - List availableLeds = surface.Devices.Where(d => d.IsEnabled).SelectMany(d => d.Leds).ToList(); + List availableLeds = devices.SelectMany(d => d.Leds).ToList(); foreach (LedEntity ledEntity in LayerEntity.Leds) { ArtemisLed? match = availableLeds.FirstOrDefault(a => a.Device.RgbDevice.GetDeviceIdentifier() == ledEntity.DeviceIdentifier && diff --git a/src/Artemis.Core/Models/Profile/Profile.cs b/src/Artemis.Core/Models/Profile/Profile.cs index 28ee0ac5a..1432d3157 100644 --- a/src/Artemis.Core/Models/Profile/Profile.cs +++ b/src/Artemis.Core/Models/Profile/Profile.cs @@ -124,14 +124,14 @@ namespace Artemis.Core /// /// Populates all the LEDs on the elements in this profile /// - /// The currently active surface that contains the LEDs - public void PopulateLeds(ArtemisSurface surface) + /// The devices to use while populating LEDs + public void PopulateLeds(IEnumerable devices) { if (Disposed) throw new ObjectDisposedException("Profile"); foreach (Layer layer in GetAllLayers()) - layer.PopulateLeds(surface); + layer.PopulateLeds(devices); } /// @@ -197,7 +197,7 @@ namespace Artemis.Core ProfileEntity.Layers.AddRange(GetAllLayers().Select(f => f.LayerEntity)); } - internal void Activate(ArtemisSurface surface) + internal void Activate(IEnumerable devices) { lock (_lock) { @@ -206,7 +206,7 @@ namespace Artemis.Core if (IsActivated) return; - PopulateLeds(surface); + PopulateLeds(devices); OnActivated(); IsActivated = true; } diff --git a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs index 107287d53..90aee62a2 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs @@ -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(); Leds = rgbDevice.Select(l => new ArtemisLed(l, this)).ToList().AsReadOnly(); LedIds = new ReadOnlyDictionary(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(); 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(Leds.ToDictionary(l => l.RgbLed.Id, l => l)); + + ApplyKeyboardLayout(); } /// @@ -89,21 +87,16 @@ namespace Artemis.Core /// public DeviceProvider DeviceProvider { get; } - /// - /// Gets the surface containing this device - /// - public ArtemisSurface Surface { get; } - /// /// Gets a read only collection containing the LEDs of this device /// - public ReadOnlyCollection Leds { get; } + public ReadOnlyCollection Leds { get; private set; } /// /// Gets a dictionary containing all the LEDs of this device with their corresponding RGB.NET as /// key /// - public ReadOnlyDictionary LedIds { get; } + public ReadOnlyDictionary LedIds { get; private set; } /// /// Gets a list of input identifiers associated with this device @@ -215,12 +208,13 @@ namespace Artemis.Core } /// - /// Gets or sets a boolean indicating whether this devices is enabled or not + /// Gets a boolean indicating whether this devices is enabled or not + /// Note: To enable/disable a device use the methods provided by /// public bool IsEnabled { get => DeviceEntity.IsEnabled; - set + internal set { DeviceEntity.IsEnabled = value; OnPropertyChanged(nameof(IsEnabled)); @@ -228,9 +222,51 @@ namespace Artemis.Core } /// - /// 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. + /// Only applicable to keyboards /// - public string? LayoutPath { get; internal set; } + public KeyboardLayoutType PhysicalLayout + { + get => (KeyboardLayoutType) DeviceEntity.PhysicalLayout; + set + { + DeviceEntity.PhysicalLayout = (int) value; + OnPropertyChanged(nameof(PhysicalLayout)); + } + } + + /// + /// Gets or sets the logical layout of the device e.g. DE, UK or US. + /// Only applicable to keyboards + /// + public string? LogicalLayout + { + get => DeviceEntity.LogicalLayout; + set + { + DeviceEntity.LogicalLayout = value; + OnPropertyChanged(nameof(LogicalLayout)); + } + } + + /// + /// Gets or sets the path of the custom layout to load when calling + /// for this device + /// + public string? CustomLayoutPath + { + get => DeviceEntity.CustomLayoutPath; + set + { + DeviceEntity.CustomLayoutPath = value; + OnPropertyChanged(nameof(CustomLayoutPath)); + } + } + + /// + /// Gets the layout of the device expanded with Artemis-specific data + /// + public ArtemisLayout? Layout { get; internal set; } internal DeviceEntity DeviceEntity { get; } @@ -261,10 +297,46 @@ namespace Artemis.Core return artemisLed; } + /// + /// Generates the default layout file name of the device + /// + /// If true, the .xml extension is added to the file name + /// The resulting file name e.g. CORSAIR GLAIVE.xml or K95 RGB-ISO.xml + 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; + } + + /// + /// Applies the provided layout to the device + /// + /// The layout to apply + /// A boolean indicating whether to add missing LEDs defined in the layout but missing on the device + /// A boolean indicating whether to remove excess LEDs present in the device but missing in the layout + 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(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 /// diff --git a/src/Artemis.Core/Models/Surface/ArtemisLed.cs b/src/Artemis.Core/Models/Surface/ArtemisLed.cs index 8085e6e8f..eaf06b73a 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisLed.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisLed.cs @@ -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; } /// 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(); + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/ArtemisSurface.cs b/src/Artemis.Core/Models/Surface/ArtemisSurface.cs deleted file mode 100644 index c94f26543..000000000 --- a/src/Artemis.Core/Models/Surface/ArtemisSurface.cs +++ /dev/null @@ -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 -{ - /// - /// Represents a surface of a specific scale, containing all the available s - /// - public class ArtemisSurface : CorePropertyChanged - { - private List _devices = new(); - private ReadOnlyDictionary _ledMap = new(new Dictionary()); - private bool _isActive; - private string _name; - - internal ArtemisSurface(RGBSurface rgbSurface, string name) - { - SurfaceEntity = new SurfaceEntity {DeviceEntities = new List()}; - 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; - } - - /// - /// Gets the RGB.NET surface backing this Artemis surface - /// - public RGBSurface RgbSurface { get; } - - /// - /// Gets the name of the surface - /// - public string Name - { - get => _name; - set => SetAndNotify(ref _name, value); - } - - /// - /// Gets a boolean indicating whether this surface is the currently active surface - /// - public bool IsActive - { - get => _isActive; - internal set => SetAndNotify(ref _isActive, value); - } - - /// - /// Gets a list of devices this surface contains - /// - public List Devices - { - get => _devices; - internal set => SetAndNotify(ref _devices, value); - } - - /// - /// Gets a dictionary containing all s on the surface with their corresponding RGB.NET - /// as key - /// - public ReadOnlyDictionary LedMap - { - get => _ledMap; - private set => SetAndNotify(ref _ledMap, value); - } - - internal SurfaceEntity SurfaceEntity { get; set; } - internal Guid EntityId { get; set; } - - /// - /// Attempts to retrieve the that corresponds the provided RGB.NET - /// - /// The RGB.NET to find the corresponding for - /// If found, the corresponding ; otherwise . - public ArtemisLed? GetArtemisLed(Led led) - { - LedMap.TryGetValue(led, out ArtemisLed? artemisLed); - return artemisLed; - } - - internal void UpdateLedMap() - { - LedMap = new ReadOnlyDictionary( - _devices.Where(d => d.IsEnabled).SelectMany(d => d.Leds.Select(al => new KeyValuePair(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 - - /// - /// Occurs when the scale of the surface is changed - /// - public event EventHandler? ScaleChanged; - - /// - /// Invokes the event - /// - protected virtual void OnScaleChanged() - { - ScaleChanged?.Invoke(this, EventArgs.Empty); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/KeyboardLayoutType.cs b/src/Artemis.Core/Models/Surface/KeyboardLayoutType.cs new file mode 100644 index 000000000..43cd2597f --- /dev/null +++ b/src/Artemis.Core/Models/Surface/KeyboardLayoutType.cs @@ -0,0 +1,41 @@ +// ReSharper disable InconsistentNaming + +namespace Artemis.Core +{ + // Copied from RGB.NET to avoid needing to reference RGB.NET + /// + /// Represents a physical layout type for a keyboard + /// + public enum KeyboardLayoutType + { + /// + /// An unknown layout type + /// + Unknown = 0, + + /// + /// The ANSI layout type, often used in the US (uses a short enter) + /// + ANSI = 1, + + /// + /// The ISO layout type, often used in the EU (uses a tall enter) + /// + ISO = 2, + + /// + /// The JIS layout type, often used in Japan (based on ISO) + /// + JIS = 3, + + /// + /// The ABNT layout type, often used in Brazil/Portugal (based on ISO) + /// + ABNT = 4, + + /// + /// The KS layout type, often used in South Korea + /// + KS = 5 + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs b/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs new file mode 100644 index 000000000..cc837ca3b --- /dev/null +++ b/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using RGB.NET.Layout; + +namespace Artemis.Core +{ + /// + /// Represents a device layout decorated with extra Artemis-specific data + /// + public class ArtemisLayout + { + /// + /// Creates a new instance of the class + /// + /// The path of the layout XML file + /// The source from where this layout is being loaded + public ArtemisLayout(string filePath, LayoutSource source) + { + FilePath = filePath; + Source = source; + Leds = new List(); + + LoadLayout(); + } + + /// + /// Gets the file path the layout was (attempted to be) loaded from + /// + public string FilePath { get; } + + /// + /// Gets the source from where this layout was loaded + /// + public LayoutSource Source { get; } + + /// + /// Gets the device this layout is applied to + /// + public ArtemisDevice? Device { get; private set; } + + /// + /// Gets a boolean indicating whether a valid layout was loaded + /// + public bool IsValid { get; private set; } + + /// + /// Gets the image of the device + /// + public Uri? Image { get; private set; } + + /// + /// Gets a list of LEDs this layout contains + /// + public List Leds { get; } + + /// + /// Gets the RGB.NET device layout + /// + public DeviceLayout RgbLayout { get; private set; } = null!; + + /// + /// Gets the custom layout data embedded in the RGB.NET layout + /// + 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; + } + } + + /// + /// Represents a source from where a layout came + /// + public enum LayoutSource + { + /// + /// A layout loaded from config + /// + Configured, + + /// + /// A layout loaded from the user layout folder + /// + User, + + /// + /// A layout loaded from the plugin folder + /// + Plugin, + + /// + /// A default layout loaded as a fallback option + /// + Default + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs b/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs new file mode 100644 index 000000000..e0ee9fa07 --- /dev/null +++ b/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs @@ -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(); + } + + /// + /// Gets the device layout of this LED layout + /// + public ArtemisLayout DeviceLayout { get; } + + /// + /// Gets the RGB.NET LED Layout of this LED layout + /// + public ILedLayout RgbLayout { get; } + + /// + /// Gets the LED this layout is applied to + /// + public ArtemisLed? Led { get; protected set; } + + /// + /// Gets the name of the logical layout this LED belongs to + /// + public string? LogicalName { get; private set; } + + /// + /// Gets the image of the LED + /// + public Uri? Image { get; private set; } + + /// + /// Gets the custom layout data embedded in the RGB.NET layout + /// + 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); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/Layout/LayoutCustomDeviceData.cs b/src/Artemis.Core/Models/Surface/Layout/LayoutCustomDeviceData.cs new file mode 100644 index 000000000..56144ca56 --- /dev/null +++ b/src/Artemis.Core/Models/Surface/Layout/LayoutCustomDeviceData.cs @@ -0,0 +1,15 @@ +using System.Xml.Serialization; +#pragma warning disable 1591 + +namespace Artemis.Core +{ + /// + /// Represents extra Artemis-specific information stored in RGB.NET layouts + /// + [XmlRoot("CustomData")] + public class LayoutCustomDeviceData + { + [XmlElement("DeviceImage")] + public string? DeviceImage { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/Layout/LayoutCustomLedData.cs b/src/Artemis.Core/Models/Surface/Layout/LayoutCustomLedData.cs new file mode 100644 index 000000000..62e029e53 --- /dev/null +++ b/src/Artemis.Core/Models/Surface/Layout/LayoutCustomLedData.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Xml.Serialization; +#pragma warning disable 1591 + +namespace Artemis.Core +{ + /// + /// Represents extra Artemis-specific information stored in RGB.NET layouts + /// + [XmlRoot("CustomData")] + public class LayoutCustomLedData + { + [XmlArray("LogicalLayouts")] + public List? LogicalLayouts { get; set; } + } + + /// + /// Represents extra Artemis-specific information stored in RGB.NET layouts + /// + [XmlType("LogicalLayout")] + public class LayoutCustomLedDataLogicalLayout + { + [XmlAttribute("Name")] + public string? Name { get; set; } + + [XmlAttribute("Image")] + public string? Image { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs b/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs index 84b276192..dd3d7ad5b 100644 --- a/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs +++ b/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs @@ -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 DeviceLayoutPaths { get; set; } = new(); + /// + /// A boolean indicating whether this device provider detects the physical layout of connected keyboards. + /// + /// Note: is only called when this or + /// is . + /// + /// + public bool CanDetectPhysicalLayout { get; protected set; } + + /// + /// A boolean indicating whether this device provider detects the logical layout of connected keyboards + /// + /// Note: is only called when this or + /// is . + /// + /// + public bool CanDetectLogicalLayout { get; protected set; } /// public override void Disable() @@ -43,31 +58,49 @@ namespace Artemis.Core.DeviceProviders } /// + /// Loads a layout for the specified device and wraps it in an /// - /// - /// - /// - protected void ResolveAbsolutePath(Type type, object sender, ResolvePathEventArgs e) + /// The device to load the layout for + /// The resulting Artemis layout + 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); - } + /// + /// Loads a layout from the user layout folder for the specified device and wraps it in an + /// + /// The device to load the layout for + /// The resulting Artemis layout + 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; - } + /// + /// Called when a specific RGB device's logical and physical layout must be detected + /// + /// Note: Only called when is . + /// + /// + /// The device to detect the layout for, always a keyboard + public virtual string GetLogicalLayout(IKeyboard keyboard) + { + throw new NotImplementedException("Device provider does not support detecting logical layouts (don't call base.GetLogicalLayout())"); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrushes/RgbNetLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/RgbNetLayerBrush.cs index 4028228a7..6424b4f06 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/RgbNetLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/RgbNetLayerBrush.cs @@ -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 /// protected RgbNetLayerBrush() { - LedGroup = new ListLedGroup(); BrushType = LayerBrushType.RgbNet; SupportsTransformation = false; } @@ -28,7 +28,10 @@ namespace Artemis.Core.LayerBrushes /// /// The LED group this layer effect is applied to /// - public ListLedGroup LedGroup { get; internal set; } + public ListLedGroup? LedGroup { get; internal set; } + + [Inject] + public IRgbService? RgbService { get; set; } /// /// 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 missingLeds = Layer.Leds.Where(l => !LedGroup.ContainsLed(l.RgbLed)).Select(l => l.RgbLed).ToList(); List 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); diff --git a/src/Artemis.Core/Plugins/Modules/Module.cs b/src/Artemis.Core/Plugins/Modules/Module.cs index 99a7b5980..70d4e5bac 100644 --- a/src/Artemis.Core/Plugins/Modules/Module.cs +++ b/src/Artemis.Core/Plugins/Modules/Module.cs @@ -146,10 +146,9 @@ namespace Artemis.Core.Modules /// Called each frame when the module should render /// /// Time since the last render - /// The RGB Surface to render to /// /// - public abstract void Render(double deltaTime, ArtemisSurface surface, SKCanvas canvas, SKImageInfo canvasInfo); + public abstract void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo); /// /// Called when the 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) diff --git a/src/Artemis.Core/Plugins/Modules/ProfileModule.cs b/src/Artemis.Core/Plugins/Modules/ProfileModule.cs index 6571d4c89..94fbd3125 100644 --- a/src/Artemis.Core/Plugins/Modules/ProfileModule.cs +++ b/src/Artemis.Core/Plugins/Modules/ProfileModule.cs @@ -142,10 +142,9 @@ namespace Artemis.Core.Modules /// Called after the profile has rendered /// /// Time since the last render - /// The RGB Surface to render to /// /// - 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 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 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(); diff --git a/src/Artemis.Core/RGB.NET/BitmapBrush.cs b/src/Artemis.Core/RGB.NET/BitmapBrush.cs index 59814336e..11021f317 100644 --- a/src/Artemis.Core/RGB.NET/BitmapBrush.cs +++ b/src/Artemis.Core/RGB.NET/BitmapBrush.cs @@ -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 _sampleSizeSetting; + private readonly IRgbService _rgbService; #region Constructors - internal BitmapBrush(Scale scale, PluginSetting sampleSizeSetting) + internal BitmapBrush(Scale scale, PluginSetting 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 } diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index 86c8cb571..ff820f4cb 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -34,7 +34,6 @@ namespace Artemis.Core.Services private readonly IProfileService _profileService; private readonly PluginSetting _renderScale; private readonly IRgbService _rgbService; - private readonly ISurfaceService _surfaceService; private readonly List _updateExceptions = new(); private List _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)); } diff --git a/src/Artemis.Core/Services/DeviceService.cs b/src/Artemis.Core/Services/DeviceService.cs index 48c64a50f..032c0db53 100644 --- a/src/Artemis.Core/Services/DeviceService.cs +++ b/src/Artemis.Core/Services/DeviceService.cs @@ -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) { diff --git a/src/Artemis.Core/Services/Input/InputService.cs b/src/Artemis.Core/Services/Input/InputService.cs index dff8d7d91..6ca8aa735 100644 --- a/src/Artemis.Core/Services/Input/InputService.cs +++ b/src/Artemis.Core/Services/Input/InputService.cs @@ -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) diff --git a/src/Artemis.Core/Services/Interfaces/IRgbService.cs b/src/Artemis.Core/Services/Interfaces/IRgbService.cs index 6e50576fa..838f15ff8 100644 --- a/src/Artemis.Core/Services/Interfaces/IRgbService.cs +++ b/src/Artemis.Core/Services/Interfaces/IRgbService.cs @@ -9,6 +9,22 @@ namespace Artemis.Core.Services /// public interface IRgbService : IArtemisService, IDisposable { + /// + /// Gets a read-only collection containing all enabled devices + /// + IReadOnlyCollection EnabledDevices { get; } + + /// + /// Gets a read-only collection containing all registered devices + /// + IReadOnlyCollection Devices { get; } + + /// + /// Gets a dictionary containing all s on the surface with their corresponding RGB.NET + /// as key + /// + IReadOnlyDictionary LedMap { get; } + /// /// Gets or sets the RGB surface rendering is performed on /// @@ -19,16 +35,6 @@ namespace Artemis.Core.Services /// BitmapBrush? BitmapBrush { get; } - /// - /// Gets the scale the frames are rendered on, a scale of 1.0 means 1 pixel = 1mm - /// - double RenderScale { get; } - - /// - /// Gets all loaded RGB devices - /// - IReadOnlyCollection LoadedDevices { get; } - /// /// Gets the update trigger that drives the render loop /// @@ -46,19 +52,77 @@ namespace Artemis.Core.Services void AddDeviceProvider(IRGBDeviceProvider deviceProvider); /// - /// Occurs when a single device has loaded + /// Removes the given device provider from the /// - event EventHandler DeviceLoaded; + /// + void RemoveDeviceProvider(IRGBDeviceProvider deviceProvider); /// - /// Occurs when a single device has reloaded + /// Applies auto-arranging logic to the surface /// - event EventHandler DeviceReloaded; + void AutoArrangeDevices(); /// - /// Recalculates the LED group used by the + /// Applies the best available layout for the given /// - /// - void UpdateSurfaceLedGroup(ArtemisSurface artemisSurface); + /// The device to apply the best available layout to + /// The layout that was applied to the device + ArtemisLayout ApplyBestDeviceLayout(ArtemisDevice device); + + /// + /// Apples the provided to the provided + /// + /// + /// + /// + /// + void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout layout, bool createMissingLeds, bool removeExessiveLeds); + + /// + /// Attempts to retrieve the that corresponds the provided RGB.NET + /// + /// + /// + /// The RGB.NET to find the corresponding + /// for + /// + /// If found, the corresponding ; otherwise . + ArtemisDevice? GetDevice(IRGBDevice rgbDevice); + + /// + /// Attempts to retrieve the that corresponds the provided RGB.NET + /// + /// The RGB.NET to find the corresponding for + /// If found, the corresponding ; otherwise . + ArtemisLed? GetLed(Led led); + + /// + /// Saves the configuration of the provided device to persistent storage + /// + /// + void SaveDevice(ArtemisDevice artemisDevice); + + /// + /// Saves the configuration of all current devices to persistent storage + /// + void SaveDevices(); + + void EnableDevice(ArtemisDevice device); + void DisableDevice(ArtemisDevice device); + + /// + /// Occurs when a single device was added + /// + event EventHandler DeviceAdded; + + /// + /// Occurs when a single device was removed + /// + event EventHandler DeviceRemoved; + + /// + /// Occurs when the surface has had modifications to its LED collection + /// + event EventHandler LedsChanged; } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index b67c27470..31dc0070c 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -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().First(d => d.RgbDeviceProvider.Devices != null && d.RgbDeviceProvider.Devices.Contains(rgbDevice)); + return GetFeaturesOfType().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); } diff --git a/src/Artemis.Core/Services/RgbService.cs b/src/Artemis.Core/Services/RgbService.cs index 6d7492100..3cec517d6 100644 --- a/src/Artemis.Core/Services/RgbService.cs +++ b/src/Artemis.Core/Services/RgbService.cs @@ -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 /// internal class RgbService : IRgbService { - private readonly List _loadedDevices; - private readonly ILogger _logger; - private readonly PluginSetting _renderScaleSetting; - private readonly PluginSetting _sampleSizeSetting; - private readonly PluginSetting _targetFrameRateSetting; - private ListLedGroup? _surfaceLedGroup; + private readonly List _devices; + private readonly List _enabledDevices; + private Dictionary _ledMap; - public RgbService(ILogger logger, ISettingsService settingsService) + private readonly ILogger _logger; + private readonly IPluginManagementService _pluginManagementService; + private readonly IDeviceRepository _deviceRepository; + private readonly PluginSetting _renderScaleSetting; + private readonly PluginSetting _targetFrameRateSetting; + private readonly PluginSetting _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(); + _enabledDevices = new List(); + _devices = new List(); + _ledMap = new Dictionary(); + UpdateTrigger = new TimerUpdateTrigger {UpdateFrequency = 1.0 / _targetFrameRateSetting.Value}; Surface.RegisterUpdateTrigger(UpdateTrigger); } + public IReadOnlyCollection EnabledDevices => _enabledDevices.AsReadOnly(); + public IReadOnlyCollection Devices => _devices.AsReadOnly(); + public IReadOnlyDictionary LedMap => new ReadOnlyDictionary(_ledMap); + /// public RGBSurface Surface { get; set; } public TimerUpdateTrigger UpdateTrigger { get; } public BitmapBrush? BitmapBrush { get; private set; } - public IReadOnlyCollection 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 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 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(_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? DeviceLoaded; - public event EventHandler? DeviceReloaded; + public event EventHandler? DeviceAdded; + public event EventHandler? 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 diff --git a/src/Artemis.Core/Services/Storage/Interfaces/ISurfaceService.cs b/src/Artemis.Core/Services/Storage/Interfaces/ISurfaceService.cs deleted file mode 100644 index 630a65220..000000000 --- a/src/Artemis.Core/Services/Storage/Interfaces/ISurfaceService.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Collections.ObjectModel; - -namespace Artemis.Core.Services -{ - /// - /// Provides access to the device surface and its configuration - /// - public interface ISurfaceService : IArtemisService - { - /// - /// Gets the currently active surface entity, to change config use - /// - ArtemisSurface ActiveSurface { get; } - - /// - /// Gets a read-only list of all surface configurations - /// - ReadOnlyCollection SurfaceConfigurations { get; } - - /// - /// Creates a new surface entity with the supplied name - /// - /// The name for the new surface entity - /// - ArtemisSurface CreateSurfaceConfiguration(string name); - - /// - /// Sets the provided entity as active and applies it to the surface - /// - /// The entity to activate and apply - void SetActiveSurfaceConfiguration(ArtemisSurface surface); - - /// - /// Saves the provided surface entity to permanent storage and if config is active, applies it to the surface - /// - /// The entity to save (and apply if active) - /// Whether to also save devices. If false, devices changes won't be applied either - void UpdateSurfaceConfiguration(ArtemisSurface surface, bool includeDevices); - - /// - /// Deletes the supplied surface entity, surface entity may not be the active surface entity - /// - /// The surface entity to delete, may not be the active surface entity - void DeleteSurfaceConfiguration(ArtemisSurface surface); - - /// - /// Applies auto-arranging logic to a surface - /// - /// - /// The surface to apply auto-arrange to. If the - /// is used. - /// - void AutoArrange(ArtemisSurface? artemisSurface = null); - - /// - /// Occurs when the active device entity has been changed - /// - event EventHandler ActiveSurfaceConfigurationSelected; - - /// - /// Occurs when a surface configuration has been updated - /// - event EventHandler SurfaceConfigurationUpdated; - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Services/Storage/Models/SurfaceArrangement.cs b/src/Artemis.Core/Services/Storage/Models/SurfaceArrangement.cs index a2bfd0b49..ca0d1ce60 100644 --- a/src/Artemis.Core/Services/Storage/Models/SurfaceArrangement.cs +++ b/src/Artemis.Core/Services/Storage/Models/SurfaceArrangement.cs @@ -76,10 +76,10 @@ namespace Artemis.Core.Services.Models return surfaceArrangementType; } - public void Arrange(ArtemisSurface surface) + public void Arrange(List 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(); diff --git a/src/Artemis.Core/Services/Storage/Models/SurfaceArrangementConfiguration.cs b/src/Artemis.Core/Services/Storage/Models/SurfaceArrangementConfiguration.cs index fb346fa21..a3ada512d 100644 --- a/src/Artemis.Core/Services/Storage/Models/SurfaceArrangementConfiguration.cs +++ b/src/Artemis.Core/Services/Storage/Models/SurfaceArrangementConfiguration.cs @@ -32,14 +32,14 @@ namespace Artemis.Core.Services.Models public int MarginBottom { get; } public SurfaceArrangement SurfaceArrangement { get; set; } - public bool Apply(List devices, ArtemisSurface surface) + public bool Apply(List 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); diff --git a/src/Artemis.Core/Services/Storage/Models/SurfaceArrangementType.cs b/src/Artemis.Core/Services/Storage/Models/SurfaceArrangementType.cs index 2166f34df..6a2c4f185 100644 --- a/src/Artemis.Core/Services/Storage/Models/SurfaceArrangementType.cs +++ b/src/Artemis.Core/Services/Storage/Models/SurfaceArrangementType.cs @@ -21,21 +21,21 @@ namespace Artemis.Core.Services.Models public List Configurations { get; } public SurfaceArrangementConfiguration? AppliedConfiguration { get; set; } - public bool HasDevices(ArtemisSurface surface) + public bool HasDevices(List 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 devices) { - List 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 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) }; diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs index 1e5b825e9..e69ca7f4f 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -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 /// /// Populates all missing LEDs on all currently active profiles /// - /// - private void ActiveProfilesPopulateLeds(ArtemisSurface surface) + private void ActiveProfilesPopulateLeds() { List profileModules = _pluginManagementService.GetFeaturesOfType(); 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 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 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(top, MementoSettings) + profile.ProfileEntity = JsonConvert.DeserializeObject(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 } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Storage/SurfaceService.cs b/src/Artemis.Core/Services/Storage/SurfaceService.cs deleted file mode 100644 index ed8aed266..000000000 --- a/src/Artemis.Core/Services/Storage/SurfaceService.cs +++ /dev/null @@ -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 _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(); - - // LoadFromRepository is guaranteed to set the ActiveSurface - ActiveSurface = null!; - LoadFromRepository(); - - _rgbService.DeviceLoaded += RgbServiceOnDeviceLoaded; - } - - public ArtemisSurface ActiveSurface { get; private set; } - public ReadOnlyCollection 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 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? ActiveSurfaceConfigurationSelected; - public event EventHandler? SurfaceConfigurationUpdated; - - protected virtual void OnActiveSurfaceConfigurationChanged(SurfaceConfigurationEventArgs e) - { - ActiveSurfaceConfigurationSelected?.Invoke(this, e); - } - - protected virtual void OnSurfaceConfigurationUpdated(SurfaceConfigurationEventArgs e) - { - SurfaceConfigurationUpdated?.Invoke(this, e); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs index 061bd984e..ffee5bc63 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs @@ -43,9 +43,9 @@ namespace Artemis.Core.Services /// protected override async Task ProcessRequest(IHttpContext context) { - if (context.Request.HttpVerb != HttpVerbs.Post) - throw HttpException.MethodNotAllowed("This end point only accepts POST calls"); - + if (context.Request.HttpVerb != HttpVerbs.Post && context.Request.HttpVerb != HttpVerbs.Put) + throw HttpException.MethodNotAllowed("This end point only accepts POST and PUT calls"); + context.Response.ContentType = MimeType.Json; using TextReader reader = context.OpenRequestText(); diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs index c81c772ee..c456ff220 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs @@ -29,7 +29,7 @@ namespace Artemis.Core.Services /// /// Gets the full URL of the end point /// - public string Url => $"{_pluginsModule.ServerUrl.TrimEnd('/')}{_pluginsModule.BaseRoute}{PluginFeature.Plugin.Guid}/{Name}"; + public string Url => $"{_pluginsModule.ServerUrl?.TrimEnd('/')}{_pluginsModule.BaseRoute}{PluginFeature.Plugin.Guid}/{Name}"; /// /// Gets the plugin the end point is associated with @@ -45,12 +45,12 @@ namespace Artemis.Core.Services /// /// Gets the mime type of the input this end point accepts /// - public string Accepts { get; protected set; } + public string? Accepts { get; protected set; } /// /// Gets the mime type of the output this end point returns /// - public string Returns { get; protected set; } + public string? Returns { get; protected set; } /// /// Called whenever the end point has to process a request diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs index b5f65bb2d..b8f996d1c 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/StringPluginEndPoint.cs @@ -32,8 +32,8 @@ namespace Artemis.Core.Services /// protected override async Task ProcessRequest(IHttpContext context) { - if (context.Request.HttpVerb != HttpVerbs.Post) - throw HttpException.MethodNotAllowed("This end point only accepts POST calls"); + if (context.Request.HttpVerb != HttpVerbs.Post && context.Request.HttpVerb != HttpVerbs.Put) + throw HttpException.MethodNotAllowed("This end point only accepts POST and PUT calls"); context.Response.ContentType = MimeType.PlainText; diff --git a/src/Artemis.Core/Utilities/IntroAnimation.cs b/src/Artemis.Core/Utilities/IntroAnimation.cs index 56b92f1f5..f2f1bb4fb 100644 --- a/src/Artemis.Core/Utilities/IntroAnimation.cs +++ b/src/Artemis.Core/Utilities/IntroAnimation.cs @@ -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 _devices; - public IntroAnimation(ILogger logger, IProfileService profileService, ISurfaceService surfaceService) + public IntroAnimation(ILogger logger, IProfileService profileService, IEnumerable devices) { _logger = logger; _profileService = profileService; - _surfaceService = surfaceService; + _devices = devices; AnimationProfile = CreateIntroProfile(); } @@ -41,14 +42,14 @@ namespace Artemis.Core ProfileEntity profileEntity = CoreJson.DeserializeObject(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(); } diff --git a/src/Artemis.Core/Utilities/Utilities.cs b/src/Artemis.Core/Utilities/Utilities.cs index 9d7e7de8e..8577185bc 100644 --- a/src/Artemis.Core/Utilities/Utilities.cs +++ b/src/Artemis.Core/Utilities/Utilities.cs @@ -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 /// public static void PrepareFirstLaunch() { - CreateArtemisFolderIfMissing(Constants.DataFolder); - CreateArtemisFolderIfMissing(Constants.DataFolder + "plugins"); + CreateAccessibleDirectory(Constants.DataFolder); + CreateAccessibleDirectory(Constants.DataFolder + "plugins"); + CreateAccessibleDirectory(Constants.DataFolder + "user layouts"); } /// @@ -62,15 +62,11 @@ namespace Artemis.Core } /// - /// Gets the current application location + /// Creates all directories and subdirectories in the specified path unless they already exist with permissions + /// allowing access by everyone. /// - /// - internal static string GetCurrentLocation() - { - return Process.GetCurrentProcess().MainModule!.FileName!; - } - - private static void CreateArtemisFolderIfMissing(string path) + /// The directory to create. + public static void CreateAccessibleDirectory(string path) { if (!Directory.Exists(path)) { @@ -92,6 +88,15 @@ namespace Artemis.Core } } + /// + /// Gets the current application location + /// + /// + internal static string GetCurrentLocation() + { + return Process.GetCurrentProcess().MainModule!.FileName!; + } + private static void OnRestartRequested(RestartEventArgs e) { RestartRequested?.Invoke(null, e); diff --git a/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs b/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs index b952fc74c..670cec969 100644 --- a/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs +++ b/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs @@ -8,8 +8,8 @@ namespace Artemis.Storage.Entities.Surface { InputIdentifiers = new List(); } - - public string DeviceIdentifier { get; set; } + + public string Id { get; set; } public double X { get; set; } public double Y { get; set; } public double Rotation { get; set; } @@ -20,7 +20,12 @@ namespace Artemis.Storage.Entities.Surface public double BlueScale { get; set; } public bool IsEnabled { get; set; } + public int PhysicalLayout { get; set; } + public string LogicalLayout { get; set; } + public string CustomLayoutPath { get; set; } + public List InputIdentifiers { get; set; } + } public class DeviceInputIdentifierEntity diff --git a/src/Artemis.Storage/Entities/Surface/SurfaceEntity.cs b/src/Artemis.Storage/Entities/Surface/SurfaceEntity.cs deleted file mode 100644 index 2ff49e492..000000000 --- a/src/Artemis.Storage/Entities/Surface/SurfaceEntity.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Artemis.Storage.Entities.Surface -{ - public class SurfaceEntity - { - public Guid Id { get; set; } - - public string Name { get; set; } - public bool IsActive { get; set; } - - public List DeviceEntities { get; set; } - } -} \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/DeviceRepository.cs b/src/Artemis.Storage/Repositories/DeviceRepository.cs new file mode 100644 index 000000000..9892c328e --- /dev/null +++ b/src/Artemis.Storage/Repositories/DeviceRepository.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using Artemis.Storage.Entities.Surface; +using Artemis.Storage.Repositories.Interfaces; +using LiteDB; + +namespace Artemis.Storage.Repositories +{ + internal class DeviceRepository : IDeviceRepository + { + private readonly LiteRepository _repository; + + public DeviceRepository(LiteRepository repository) + { + _repository = repository; + _repository.Database.GetCollection().EnsureIndex(s => s.Id); + } + + public void Add(DeviceEntity deviceEntity) + { + _repository.Insert(deviceEntity); + } + + public void Remove(DeviceEntity deviceEntity) + { + _repository.Delete(deviceEntity.Id); + } + + public DeviceEntity Get(string id) + { + return _repository.FirstOrDefault(s => s.Id == id); + } + + public List GetAll() + { + return _repository.Query().Include(s => s.InputIdentifiers).ToList(); + } + + public void Save(DeviceEntity deviceEntity) + { + _repository.Upsert(deviceEntity); + } + + public void Save(IEnumerable deviceEntities) + { + _repository.Upsert(deviceEntities); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/Interfaces/IDeviceRepository.cs b/src/Artemis.Storage/Repositories/Interfaces/IDeviceRepository.cs new file mode 100644 index 000000000..fc2c8375d --- /dev/null +++ b/src/Artemis.Storage/Repositories/Interfaces/IDeviceRepository.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using Artemis.Storage.Entities.Surface; + +namespace Artemis.Storage.Repositories.Interfaces +{ + public interface IDeviceRepository : IRepository + { + void Add(DeviceEntity deviceEntity); + void Remove(DeviceEntity deviceEntity); + DeviceEntity Get(string id); + List GetAll(); + void Save(DeviceEntity deviceEntity); + void Save(IEnumerable deviceEntities); + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/Interfaces/ISurfaceRepository.cs b/src/Artemis.Storage/Repositories/Interfaces/ISurfaceRepository.cs deleted file mode 100644 index afbd7743b..000000000 --- a/src/Artemis.Storage/Repositories/Interfaces/ISurfaceRepository.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; -using Artemis.Storage.Entities.Surface; - -namespace Artemis.Storage.Repositories.Interfaces -{ - public interface ISurfaceRepository : IRepository - { - void Add(SurfaceEntity surfaceEntity); - void Remove(SurfaceEntity surfaceEntity); - SurfaceEntity GetByName(string name); - List GetAll(); - - void Save(SurfaceEntity surfaceEntity); - } -} \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/SurfaceRepository.cs b/src/Artemis.Storage/Repositories/SurfaceRepository.cs deleted file mode 100644 index 46b8b9e6b..000000000 --- a/src/Artemis.Storage/Repositories/SurfaceRepository.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Artemis.Storage.Entities.Surface; -using Artemis.Storage.Repositories.Interfaces; -using LiteDB; - -namespace Artemis.Storage.Repositories -{ - internal class SurfaceRepository : ISurfaceRepository - { - private readonly LiteRepository _repository; - - public SurfaceRepository(LiteRepository repository) - { - _repository = repository; - _repository.Database.GetCollection().EnsureIndex(s => s.Name); - } - - public void Add(SurfaceEntity surfaceEntity) - { - _repository.Insert(surfaceEntity); - } - - public void Remove(SurfaceEntity surfaceEntity) - { - _repository.Delete(surfaceEntity.Id); - } - - public SurfaceEntity GetByName(string name) - { - return _repository.FirstOrDefault(s => s.Name == name); - } - - public List GetAll() - { - return _repository.Query().Include(s => s.DeviceEntities.Select(de => de.InputIdentifiers)).ToList(); - } - - public void Save(SurfaceEntity surfaceEntity) - { - _repository.Upsert(surfaceEntity); - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs index 6021653ba..397dd2dae 100644 --- a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs +++ b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs @@ -182,7 +182,11 @@ namespace Artemis.UI.Shared if (_oldDevice != null) { if (Device != null) + { Device.RgbDevice.PropertyChanged -= DevicePropertyChanged; + Device.DeviceUpdated -= DeviceUpdated; + } + _oldDevice = null; } } @@ -228,15 +232,20 @@ namespace Artemis.UI.Shared return; if (_oldDevice != null) + { Device.RgbDevice.PropertyChanged -= DevicePropertyChanged; + Device.DeviceUpdated -= DeviceUpdated; + } + _oldDevice = Device; Device.RgbDevice.PropertyChanged += DevicePropertyChanged; + Device.DeviceUpdated += DeviceUpdated; UpdateTransform(); // Load the device main image - if (Device.RgbDevice.DeviceInfo?.Image?.AbsolutePath != null && File.Exists(Device.RgbDevice.DeviceInfo.Image.AbsolutePath)) - _deviceImage = new BitmapImage(Device.RgbDevice.DeviceInfo.Image); + if (Device.Layout?.Image != null && File.Exists(Device.Layout.Image.LocalPath)) + _deviceImage = new BitmapImage(Device.Layout.Image); // Create all the LEDs foreach (ArtemisLed artemisLed in Device.Leds) @@ -277,13 +286,16 @@ namespace Artemis.UI.Shared InvalidateMeasure(); } - private void DevicePropertyChanged(object? sender, PropertyChangedEventArgs e) + private void DeviceUpdated(object? sender, EventArgs e) { - if (e.PropertyName == nameof(Device.RgbDevice.Scale) || e.PropertyName == nameof(Device.RgbDevice.Rotation)) - UpdateTransform(); + SetupForDevice(); } - + private void DevicePropertyChanged(object? sender, PropertyChangedEventArgs e) + { + SetupForDevice(); + } + private void Render() { DrawingContext drawingContext = _backingStore.Open(); diff --git a/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs b/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs index b271c9c29..c3ba1fd6a 100644 --- a/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs +++ b/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs @@ -11,6 +11,9 @@ namespace Artemis.UI.Shared { internal class DeviceVisualizerLed { + private SolidColorBrush? _renderColorBrush; + private Color _renderColor; + public DeviceVisualizerLed(ArtemisLed led) { Led = led; @@ -21,9 +24,9 @@ namespace Artemis.UI.Shared Led.RgbLed.Size.Height ); - if (Led.RgbLed.Image != null && File.Exists(Led.RgbLed.Image.AbsolutePath)) - LedImage = new BitmapImage(Led.RgbLed.Image); - + if (Led.Layout?.Image != null && File.Exists(Led.Layout.Image.LocalPath)) + LedImage = new BitmapImage(Led.Layout.Image); + CreateLedGeometry(); } @@ -38,14 +41,19 @@ namespace Artemis.UI.Shared if (DisplayGeometry == null) return; + _renderColorBrush ??= new SolidColorBrush(); + RGB.NET.Core.Color originalColor = Led.GetOriginalColor(); byte r = originalColor.GetR(); byte g = originalColor.GetG(); byte b = originalColor.GetB(); - drawingContext.DrawRectangle(isDimmed - ? new SolidColorBrush(Color.FromArgb(100, r, g, b)) - : new SolidColorBrush(Color.FromRgb(r, g, b)), null, LedRect); + _renderColor.A = isDimmed ? 100 : 255; + _renderColor.R = r; + _renderColor.G = g; + _renderColor.B = b; + _renderColorBrush.Color = _renderColor; + drawingContext.DrawRectangle(_renderColorBrush, null, LedRect); } public void RenderImage(DrawingContext drawingContext) diff --git a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs index ee678f4fc..720bf0ad3 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs @@ -8,7 +8,6 @@ using Artemis.Core.Modules; using Artemis.Core.Services; using Artemis.Storage.Entities.Profile; using Artemis.UI.Shared.Services.Models; -using Newtonsoft.Json; using Ninject; using Ninject.Parameters; using Serilog; @@ -20,23 +19,23 @@ namespace Artemis.UI.Shared.Services internal class ProfileEditorService : IProfileEditorService { private readonly ICoreService _coreService; - private readonly ISurfaceService _surfaceService; private readonly IKernel _kernel; private readonly ILogger _logger; private readonly IProfileService _profileService; private readonly List _registeredPropertyEditors; + private readonly IRgbService _rgbService; private readonly object _selectedProfileElementLock = new(); private readonly object _selectedProfileLock = new(); private TimeSpan _currentTime; private int _pixelsPerSecond; - public ProfileEditorService(IKernel kernel, ILogger logger, IProfileService profileService, ICoreService coreService, ISurfaceService surfaceService) + public ProfileEditorService(IKernel kernel, ILogger logger, IProfileService profileService, ICoreService coreService, IRgbService rgbService) { _kernel = kernel; _logger = logger; _profileService = profileService; _coreService = coreService; - _surfaceService = surfaceService; + _rgbService = rgbService; _registeredPropertyEditors = new List(); PixelsPerSecond = 100; @@ -351,7 +350,7 @@ namespace Artemis.UI.Shared.Services public List GetLedsInRectangle(Rect rect) { - return _surfaceService.ActiveSurface.Devices.SelectMany(d => d.Leds).Where(led => led.AbsoluteRectangle.IntersectsWith(rect.ToSKRect())).ToList(); + return _rgbService.EnabledDevices.SelectMany(d => d.Leds).Where(led => led.AbsoluteRectangle.IntersectsWith(rect.ToSKRect())).ToList(); } #region Copy/paste @@ -424,7 +423,7 @@ namespace Artemis.UI.Shared.Services if (pasted != null) { - target.Profile.PopulateLeds(_surfaceService.ActiveSurface); + target.Profile.PopulateLeds(_rgbService.EnabledDevices); UpdateSelectedProfile(); ChangeSelectedProfileElement(pasted); } diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index c0eacf441..a78e9658a 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -1,361 +1,364 @@  - - WinExe - net5.0-windows - {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - true - Artemis - Artemis - en-US - Provides advanced unified lighting across many different brands RGB peripherals - Copyright © Robert Beekman - 2021 - 2.0.0.0 - bin\$(Platform)\$(Configuration)\ - true - x64 - - - x64 - + + WinExe + net5.0-windows + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + true + Artemis + Artemis + en-US + Provides advanced unified lighting across many different brands RGB peripherals + Copyright © Robert Beekman - 2021 + 2.0.0.0 + bin\$(Platform)\$(Configuration)\ + true + x64 + windows + + + x64 + - - Resources\Images\Logo\logo-512.ico - - - - 2.0.0.0 - 2.0.0 - - - 2.0-{chash:6} - true - true - true - v[0-9]* - true - git - true - - - - - - - false - - - - - - - - - - - - - - - ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Brushes.dll - true - - - ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll - - - ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Groups.dll - - - - - - ResXFileCodeGenerator - Designer - Resources.Designer.cs - - - - - true - - - true - - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - True - Resources.resx - - - True - True - Settings.settings - - - - - PreserveNewest - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - - - Designer - - - Designer - - - Designer - - - Designer - - - Designer - - - Designer - - - $(DefaultXamlRuntime) - - + + Resources\Images\Logo\logo-512.ico + + + + 2.0.0.0 + 2.0.0 + + + 2.0-{chash:6} + true + true + true + v[0-9]* + true + git + true + + + + + + + false + + + + + + + + + + + + + + + ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Brushes.dll + + + ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll + + + ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Groups.dll + + + ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Layout.dll + + + + + + ResXFileCodeGenerator + Designer + Resources.Designer.cs + + + + + true + + + true + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + True + True + Settings.settings + + + + + PreserveNewest + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + Designer + + + Designer + + + Designer + + + Designer + + + Designer + + + Designer + + + $(DefaultXamlRuntime) + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/FolderViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/FolderViewModel.cs index 310e1324f..ba8de22bd 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/FolderViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/FolderViewModel.cs @@ -9,12 +9,12 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem { // I hate this about DI, oh well public FolderViewModel(ProfileElement folder, + IRgbService rgbService, IProfileEditorService profileEditorService, IDialogService dialogService, IProfileTreeVmFactory profileTreeVmFactory, - ILayerBrushService layerBrushService, - ISurfaceService surfaceService) : - base(folder, profileEditorService, dialogService, profileTreeVmFactory, layerBrushService, surfaceService) + ILayerBrushService layerBrushService) : + base(folder, rgbService, profileEditorService, dialogService, profileTreeVmFactory, layerBrushService) { } diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerViewModel.cs index b338ddd13..56b559862 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerViewModel.cs @@ -8,12 +8,12 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem public class LayerViewModel : TreeItemViewModel { public LayerViewModel(ProfileElement layer, + IRgbService rgbService, IProfileEditorService profileEditorService, IDialogService dialogService, IProfileTreeVmFactory profileTreeVmFactory, - ILayerBrushService layerBrushService, - ISurfaceService surfaceService) : - base(layer, profileEditorService, dialogService, profileTreeVmFactory, layerBrushService, surfaceService) + ILayerBrushService layerBrushService) : + base(layer, rgbService, profileEditorService, dialogService, profileTreeVmFactory, layerBrushService) { } diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs index a0086373f..08eeaac02 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs @@ -18,23 +18,23 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem { private readonly IDialogService _dialogService; private readonly ILayerBrushService _layerBrushService; + private readonly IRgbService _rgbService; private readonly IProfileEditorService _profileEditorService; private readonly IProfileTreeVmFactory _profileTreeVmFactory; - private readonly ISurfaceService _surfaceService; private ProfileElement _profileElement; protected TreeItemViewModel(ProfileElement profileElement, + IRgbService rgbService, IProfileEditorService profileEditorService, IDialogService dialogService, IProfileTreeVmFactory profileTreeVmFactory, - ILayerBrushService layerBrushService, - ISurfaceService surfaceService) + ILayerBrushService layerBrushService) { + _rgbService = rgbService; _profileEditorService = profileEditorService; _dialogService = dialogService; _profileTreeVmFactory = profileTreeVmFactory; _layerBrushService = layerBrushService; - _surfaceService = surfaceService; ProfileElement = profileElement; @@ -144,7 +144,7 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem if (brush != null) layer.ChangeLayerBrush(brush); - layer.AddLeds(_surfaceService.ActiveSurface.Devices.SelectMany(d => d.Leds)); + layer.AddLeds(_rgbService.EnabledDevices.SelectMany(d => d.Leds)); _profileEditorService.UpdateSelectedProfile(); _profileEditorService.ChangeSelectedProfileElement(layer); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLayerViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLayerViewModel.cs index afc9c6c54..b84d36b14 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLayerViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLayerViewModel.cs @@ -134,19 +134,19 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization private Geometry CreateRectangleGeometry(ArtemisLed led) { - Rect rect = led.RgbLed.AbsoluteLedRectangle.ToWindowsRect(1); + Rect rect = led.RgbLed.AbsoluteBoundary.ToWindowsRect(1); return new RectangleGeometry(rect); } private Geometry CreateCircleGeometry(ArtemisLed led) { - Rect rect = led.RgbLed.AbsoluteLedRectangle.ToWindowsRect(1); + Rect rect = led.RgbLed.AbsoluteBoundary.ToWindowsRect(1); return new EllipseGeometry(rect); } private Geometry CreateCustomGeometry(ArtemisLed led, double deflateAmount) { - Rect rect = led.RgbLed.AbsoluteLedRectangle.ToWindowsRect(1); + Rect rect = led.RgbLed.AbsoluteBoundary.ToWindowsRect(1); try { PathGeometry geometry = Geometry.Combine( diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileView.xaml index 1237727f8..8441cd11d 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileView.xaml @@ -99,7 +99,10 @@ - + diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs index e559a30fc..071884dcd 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs @@ -6,48 +6,48 @@ using System.Windows.Input; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Events; -using Artemis.UI.Extensions; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.ProfileEditor.Visualization.Tools; using Artemis.UI.Screens.Shared; using Artemis.UI.Shared.Services; +using SkiaSharp; using Stylet; namespace Artemis.UI.Screens.ProfileEditor.Visualization { public class ProfileViewModel : Conductor.Collection.AllActive, IProfileEditorPanelViewModel, IHandle, IHandle { - private readonly IProfileEditorService _profileEditorService; private readonly ICoreService _coreService; + private readonly IProfileEditorService _profileEditorService; private readonly IProfileLayerVmFactory _profileLayerVmFactory; + private readonly IRgbService _rgbService; private readonly ISettingsService _settingsService; - private readonly ISurfaceService _surfaceService; private readonly IVisualizationToolVmFactory _visualizationToolVmFactory; private int _activeToolIndex; private VisualizationToolViewModel _activeToolViewModel; + private PluginSetting _alwaysApplyDataBindings; private bool _canApplyToLayer; private bool _canSelectEditTool; private BindableCollection _devices; private BindableCollection _highlightedLeds; private PluginSetting _highlightSelectedLayer; - private PluginSetting _alwaysApplyDataBindings; + private DateTime _lastUpdate; private PanZoomViewModel _panZoomViewModel; private Layer _previousSelectedLayer; private int _previousTool; - private DateTime _lastUpdate; public ProfileViewModel(IProfileEditorService profileEditorService, + IRgbService rgbService, ICoreService coreService, - ISurfaceService surfaceService, ISettingsService settingsService, IEventAggregator eventAggregator, IVisualizationToolVmFactory visualizationToolVmFactory, IProfileLayerVmFactory profileLayerVmFactory) { _profileEditorService = profileEditorService; + _rgbService = rgbService; _coreService = coreService; - _surfaceService = surfaceService; _settingsService = settingsService; _visualizationToolVmFactory = visualizationToolVmFactory; _profileLayerVmFactory = profileLayerVmFactory; @@ -99,25 +99,21 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization { // Remove the tool from the canvas if (_activeToolViewModel != null) - { lock (Items) { Items.Remove(_activeToolViewModel); NotifyOfPropertyChange(() => Items); } - } // Set the new tool SetAndNotify(ref _activeToolViewModel, value); // Add the new tool to the canvas if (_activeToolViewModel != null) - { lock (Items) { Items.Add(_activeToolViewModel); NotifyOfPropertyChange(() => Items); } - } } } @@ -144,7 +140,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization Devices = new BindableCollection(); HighlightedLeds = new BindableCollection(); - ApplySurfaceConfiguration(_surfaceService.ActiveSurface); + ApplyDevices(); ActivateToolByIndex(0); ApplyActiveProfile(); @@ -156,7 +152,8 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization _coreService.FrameRendered += OnFrameRendered; HighlightSelectedLayer.SettingChanged += HighlightSelectedLayerOnSettingChanged; - _surfaceService.ActiveSurfaceConfigurationSelected += OnActiveSurfaceConfigurationSelected; + _rgbService.DeviceAdded += RgbServiceOnDevicesModified; + _rgbService.DeviceRemoved += RgbServiceOnDevicesModified; _profileEditorService.ProfileSelected += OnProfileSelected; _profileEditorService.ProfileElementSelected += OnProfileElementSelected; _profileEditorService.SelectedProfileElementUpdated += OnSelectedProfileElementUpdated; @@ -168,7 +165,8 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization { _coreService.FrameRendered -= OnFrameRendered; HighlightSelectedLayer.SettingChanged -= HighlightSelectedLayerOnSettingChanged; - _surfaceService.ActiveSurfaceConfigurationSelected -= OnActiveSurfaceConfigurationSelected; + _rgbService.DeviceAdded -= RgbServiceOnDevicesModified; + _rgbService.DeviceRemoved -= RgbServiceOnDevicesModified; _profileEditorService.ProfileSelected -= OnProfileSelected; _profileEditorService.ProfileElementSelected -= OnProfileElementSelected; _profileEditorService.SelectedProfileElementUpdated -= OnSelectedProfileElementUpdated; @@ -181,9 +179,9 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization base.OnClose(); } - private void OnActiveSurfaceConfigurationSelected(object sender, SurfaceConfigurationEventArgs e) + private void RgbServiceOnDevicesModified(object sender, DeviceEventArgs e) { - ApplySurfaceConfiguration(e.Surface); + ApplyDevices(); } private void ApplyActiveProfile() @@ -193,10 +191,8 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization // Add new layers missing a VM foreach (Layer layer in layers) - { if (layerViewModels.All(vm => vm.Layer != layer)) Items.Add(_profileLayerVmFactory.Create(layer, PanZoomViewModel)); - } // Remove layers that no longer exist IEnumerable toRemove = layerViewModels.Where(vm => !layers.Contains(vm.Layer)); @@ -204,10 +200,10 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization Items.Remove(profileLayerViewModel); } - private void ApplySurfaceConfiguration(ArtemisSurface surface) + private void ApplyDevices() { Devices.Clear(); - Devices.AddRange(surface.Devices.Where(d => d.IsEnabled).OrderBy(d => d.ZIndex)); + Devices.AddRange(_rgbService.EnabledDevices.Where(d => d.IsEnabled).OrderBy(d => d.ZIndex)); } private void UpdateLedsDimStatus() @@ -259,7 +255,15 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization public void ResetZoomAndPan() { - PanZoomViewModel.Reset(); + // Create a rect surrounding all devices + SKRect rect = new SKRect( + Devices.Min(d => d.Rectangle.Left), + Devices.Min(d => d.Rectangle.Top), + Devices.Max(d => d.Rectangle.Right), + Devices.Max(d => d.Rectangle.Bottom) + ); + + PanZoomViewModel.Reset(rect); } #endregion @@ -339,7 +343,9 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization _previousSelectedLayer.Transform.LayerPropertyOnCurrentValueSet += TransformValueChanged; } else + { _previousSelectedLayer = null; + } ApplyActiveProfile(); UpdateLedsDimStatus(); diff --git a/src/Artemis.UI/Screens/Settings/Debug/DeviceDebugView.xaml b/src/Artemis.UI/Screens/Settings/Debug/DeviceDebugView.xaml index 6ab2f00bd..80dc57da4 100644 --- a/src/Artemis.UI/Screens/Settings/Debug/DeviceDebugView.xaml +++ b/src/Artemis.UI/Screens/Settings/Debug/DeviceDebugView.xaml @@ -40,8 +40,31 @@ - + + + + @@ -55,27 +78,27 @@ - - In this window you can view detailed information of the device. - Please note that having this window open can have a performance impact on your system. + + In this window you can view detailed information of the device. + Please note that having this window open can have a performance impact on your system. - - + + - - - - - - - - + + + + + + + + @@ -87,12 +110,12 @@ Device name + Foreground="{DynamicResource MaterialDesignNavigationItemSubheader}" + TextWrapping="Wrap" + Text="{Binding Device.RgbDevice.DeviceInfo.DeviceName}" /> - + @@ -105,30 +128,12 @@ Manufacturer + Foreground="{DynamicResource MaterialDesignNavigationItemSubheader}" + TextWrapping="Wrap" + Text="{Binding Device.RgbDevice.DeviceInfo.Manufacturer}" /> - - - - - - - - - - - - Lighting support - - - - + @@ -141,12 +146,30 @@ Device type + Foreground="{DynamicResource MaterialDesignNavigationItemSubheader}" + TextWrapping="Wrap" + Text="{Binding Device.RgbDevice.DeviceInfo.DeviceType}" /> - + + + + + + + + + + + + Physical layout + + + + @@ -159,15 +182,13 @@ Device image + TextWrapping="Wrap" + Text="{Binding Device.Layout.Image, Mode=OneWay}" + IsReadOnly="True" /> - - - + @@ -179,12 +200,12 @@ Size (1px = 1mm) + Foreground="{DynamicResource MaterialDesignNavigationItemSubheader}" + TextWrapping="Wrap" + Text="{Binding Device.RgbDevice.Size}" /> - + @@ -196,12 +217,12 @@ Location (1px = 1mm) + Foreground="{DynamicResource MaterialDesignNavigationItemSubheader}" + TextWrapping="Wrap" + Text="{Binding Device.RgbDevice.Location}" /> - + @@ -214,12 +235,12 @@ Rotation (degrees) + Foreground="{DynamicResource MaterialDesignNavigationItemSubheader}" + TextWrapping="Wrap" + Text="{Binding Device.RgbDevice.Rotation.Degrees}" /> - + @@ -230,14 +251,14 @@ - Syncback supported + Logical layout + Foreground="{DynamicResource MaterialDesignNavigationItemSubheader}" + TextWrapping="Wrap" + Text="{Binding Device.LogicalLayout}" /> - + @@ -250,17 +271,17 @@ Layout file path + TextWrapping="Wrap" + Text="{Binding Device.Layout.FilePath, Mode=OneWay}" + IsReadOnly="True" /> - - - + + + - + + CanUserResizeRows="False"> @@ -282,9 +302,9 @@ - + - + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Debug/DeviceDebugViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/DeviceDebugViewModel.cs index 9bfac70e9..0eac26446 100644 --- a/src/Artemis.UI/Screens/Settings/Debug/DeviceDebugViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Debug/DeviceDebugViewModel.cs @@ -2,10 +2,15 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Xml.Serialization; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Shared.Services; -using Stylet; // using PropertyChanged; +using Ookii.Dialogs.Wpf; +using RGB.NET.Layout; +using Stylet; + +// using PropertyChanged; namespace Artemis.UI.Screens.Settings.Debug { @@ -13,11 +18,13 @@ namespace Artemis.UI.Screens.Settings.Debug { private readonly IDeviceService _deviceService; private readonly IDialogService _dialogService; + private readonly IRgbService _rgbService; private ArtemisLed _selectedLed; - public DeviceDebugViewModel(ArtemisDevice device, IDeviceService deviceService, IDialogService dialogService) + public DeviceDebugViewModel(ArtemisDevice device, IDeviceService deviceService, IRgbService rgbService, IDialogService dialogService) { _deviceService = deviceService; + _rgbService = rgbService; _dialogService = dialogService; Device = device; } @@ -35,7 +42,7 @@ namespace Artemis.UI.Screens.Settings.Debug } } - public bool CanOpenImageDirectory => Device.RgbDevice.DeviceInfo.Image != null; + public bool CanOpenImageDirectory => Device.Layout?.Image != null; // ReSharper disable UnusedMember.Global @@ -70,7 +77,7 @@ namespace Artemis.UI.Screens.Settings.Debug try { - Process.Start(Environment.GetEnvironmentVariable("WINDIR") + @"\explorer.exe", Path.GetDirectoryName(Device.RgbDevice.DeviceInfo.Image.AbsolutePath)); + Process.Start(Environment.GetEnvironmentVariable("WINDIR") + @"\explorer.exe", Path.GetDirectoryName(Device.Layout.Image.AbsolutePath)); } catch (Exception e) { @@ -78,6 +85,73 @@ namespace Artemis.UI.Screens.Settings.Debug } } + public void ReloadLayout() + { + _rgbService.ApplyBestDeviceLayout(Device); + } + + public void ExportLayout() + { + if (Device.Layout == null) + return; + + VistaFolderBrowserDialog dialog = new() + { + Description = "Select layout export target folder", + UseDescriptionForTitle = true, + ShowNewFolderButton = true, + SelectedPath = Path.Combine(Constants.DataFolder, "user layouts") + }; + + bool? result = dialog.ShowDialog(); + if (result != true) + return; + + string directory = Path.Combine( + dialog.SelectedPath, + Device.RgbDevice.DeviceInfo.Manufacturer, + Device.RgbDevice.DeviceInfo.DeviceType.ToString() + ); + string filePath = Path.Combine(directory, Device.GetLayoutFileName()); + Core.Utilities.CreateAccessibleDirectory(directory); + + // XML + XmlSerializer serializer = new(typeof(DeviceLayout)); + using StreamWriter writer = new(filePath); + serializer.Serialize(writer, Device.Layout!.RgbLayout); + + // Device images + if (!Uri.IsWellFormedUriString(Device.Layout.FilePath, UriKind.Absolute)) + return; + + Uri targetDirectory = new(directory + "/", UriKind.Absolute); + Uri sourceDirectory = new(Path.GetDirectoryName(Device.Layout.FilePath)! + "/", UriKind.Absolute); + Uri deviceImageTarget = new(targetDirectory, Device.Layout.LayoutCustomDeviceData.DeviceImage); + + // Create folder (if needed) and copy image + Core.Utilities.CreateAccessibleDirectory(Path.GetDirectoryName(deviceImageTarget.LocalPath)!); + if (Device.Layout.Image != null && File.Exists(Device.Layout.Image.LocalPath) && !File.Exists(deviceImageTarget.LocalPath)) + File.Copy(Device.Layout.Image.LocalPath, deviceImageTarget.LocalPath); + + foreach (ArtemisLedLayout ledLayout in Device.Layout.Leds) + { + if (ledLayout.LayoutCustomLedData.LogicalLayouts == null) + continue; + + // Only the image of the current logical layout is available as an URI, iterate each layout and find the images manually + foreach (LayoutCustomLedDataLogicalLayout logicalLayout in ledLayout.LayoutCustomLedData.LogicalLayouts) + { + Uri image = new(sourceDirectory, logicalLayout.Image); + Uri imageTarget = new(targetDirectory, logicalLayout.Image); + + // Create folder (if needed) and copy image + Core.Utilities.CreateAccessibleDirectory(Path.GetDirectoryName(imageTarget.LocalPath)!); + if (File.Exists(image.LocalPath) && !File.Exists(imageTarget.LocalPath)) + File.Copy(image.LocalPath, imageTarget.LocalPath); + } + } + } + #endregion // ReSharper restore UnusedMember.Global diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Devices/DeviceSettingsTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/Devices/DeviceSettingsTabViewModel.cs index 31ba945e7..6c13a7266 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Devices/DeviceSettingsTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/Devices/DeviceSettingsTabViewModel.cs @@ -11,15 +11,15 @@ namespace Artemis.UI.Screens.Settings.Tabs.Devices public class DeviceSettingsTabViewModel : Conductor.Collection.AllActive { private readonly ISettingsVmFactory _settingsVmFactory; - private readonly ISurfaceService _surfaceService; + private readonly IRgbService _rgbService; private readonly IDialogService _dialogService; private bool _confirmedDisable; - public DeviceSettingsTabViewModel(ISurfaceService surfaceService, IDialogService dialogService, ISettingsVmFactory settingsVmFactory) + public DeviceSettingsTabViewModel(IRgbService rgbService, IDialogService dialogService, ISettingsVmFactory settingsVmFactory) { DisplayName = "DEVICES"; - _surfaceService = surfaceService; + _rgbService = rgbService; _dialogService = dialogService; _settingsVmFactory = settingsVmFactory; } @@ -32,7 +32,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Devices Items.Clear(); await Task.Delay(200); - List instances = _surfaceService.ActiveSurface.Devices.Select(d => _settingsVmFactory.CreateDeviceSettingsViewModel(d)).ToList(); + List instances = _rgbService.Devices.Select(d => _settingsVmFactory.CreateDeviceSettingsViewModel(d)).ToList(); foreach (DeviceSettingsViewModel deviceSettingsViewModel in instances) Items.Add(deviceSettingsViewModel); }); diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Devices/DeviceSettingsView.xaml b/src/Artemis.UI/Screens/Settings/Tabs/Devices/DeviceSettingsView.xaml index 8b4be68fa..ced4c9131 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Devices/DeviceSettingsView.xaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/Devices/DeviceSettingsView.xaml @@ -76,7 +76,7 @@ diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Devices/DeviceSettingsViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/Devices/DeviceSettingsViewModel.cs index 4ae2e45e2..f9de3cfe3 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Devices/DeviceSettingsViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/Devices/DeviceSettingsViewModel.cs @@ -6,7 +6,6 @@ using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.SurfaceEditor.Dialogs; -using Artemis.UI.Screens.SurfaceEditor.Visualization; using Artemis.UI.Shared.Services; using Humanizer; using RGB.NET.Core; @@ -17,9 +16,9 @@ namespace Artemis.UI.Screens.Settings.Tabs.Devices public class DeviceSettingsViewModel : Screen { private readonly IDeviceDebugVmFactory _deviceDebugVmFactory; - private readonly ISurfaceService _surfaceService; private readonly IDeviceService _deviceService; private readonly IDialogService _dialogService; + private readonly IRgbService _rgbService; private readonly IWindowManager _windowManager; public DeviceSettingsViewModel(ArtemisDevice device, @@ -27,13 +26,13 @@ namespace Artemis.UI.Screens.Settings.Tabs.Devices IDialogService dialogService, IWindowManager windowManager, IDeviceDebugVmFactory deviceDebugVmFactory, - ISurfaceService surfaceService) + IRgbService rgbService) { _deviceService = deviceService; _dialogService = dialogService; _windowManager = windowManager; _deviceDebugVmFactory = deviceDebugVmFactory; - _surfaceService = surfaceService; + _rgbService = rgbService; Device = device; Type = Device.RgbDevice.DeviceInfo.DeviceType.ToString().Humanize(); @@ -53,26 +52,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Devices public bool IsDeviceEnabled { get => Device.IsEnabled; - set - { - Task.Run(() => UpdateIsDeviceEnabled(value)); - - } - } - - private async Task UpdateIsDeviceEnabled(bool value) - { - if (!value) - value = !await ((DeviceSettingsTabViewModel)Parent).ShowDeviceDisableDialog(); - - Device.IsEnabled = value; - NotifyOfPropertyChange(nameof(IsDeviceEnabled)); - SaveDevice(); - } - - private void SaveDevice() - { - _surfaceService.UpdateSurfaceConfiguration(_surfaceService.ActiveSurface, true); + set { Task.Run(() => UpdateIsDeviceEnabled(value)); } } public void IdentifyDevice() @@ -104,7 +84,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Devices ); if ((bool) madeChanges) - _surfaceService.UpdateSurfaceConfiguration(_surfaceService.ActiveSurface, true); + _rgbService.SaveDevice(Device); } public async Task ViewProperties() @@ -114,7 +94,25 @@ namespace Artemis.UI.Screens.Settings.Tabs.Devices ); if ((bool) madeChanges) - _surfaceService.UpdateSurfaceConfiguration(_surfaceService.ActiveSurface, true); + _rgbService.SaveDevice(Device); + } + + private async Task UpdateIsDeviceEnabled(bool value) + { + if (!value) + value = !await ((DeviceSettingsTabViewModel) Parent).ShowDeviceDisableDialog(); + + if (value) + _rgbService.EnableDevice(Device); + else + _rgbService.DisableDevice(Device); + NotifyOfPropertyChange(nameof(IsDeviceEnabled)); + SaveDevice(); + } + + private void SaveDevice() + { + _rgbService.SaveDevice(Device); } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsTabViewModel.cs index 57f54847c..5d17a2c72 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsTabViewModel.cs @@ -57,6 +57,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins // Take it off the UI thread to avoid freezing on tab change Task.Run(async () => { + Items.Clear(); await Task.Delay(200); _instances = _pluginManagementService.GetAllPlugins() .Select(p => _settingsVmFactory.CreatePluginSettingsViewModel(p)) diff --git a/src/Artemis.UI/Screens/Shared/PanZoomViewModel.cs b/src/Artemis.UI/Screens/Shared/PanZoomViewModel.cs index e751e5e51..d1e3d88fe 100644 --- a/src/Artemis.UI/Screens/Shared/PanZoomViewModel.cs +++ b/src/Artemis.UI/Screens/Shared/PanZoomViewModel.cs @@ -5,6 +5,7 @@ using System.Windows.Input; using System.Windows.Media; using Artemis.UI.Extensions; using RGB.NET.Core; +using SkiaSharp; using Stylet; using Point = System.Windows.Point; @@ -144,6 +145,13 @@ namespace Artemis.UI.Screens.Shared PanY = 0; } + public void Reset(SKRect rect) + { + Zoom = Math.Min(CanvasWidth / rect.Width, CanvasHeight / rect.Height); + PanX = rect.Left * -1 * Zoom; + PanY = rect.Top * -1 * Zoom; + } + public Rect TransformContainingRect(Rectangle rect) { return TransformContainingRect(rect.ToWindowsRect(1)); @@ -168,11 +176,15 @@ namespace Artemis.UI.Screens.Shared private void SetZoomFromPercentage(double value) { - double newZoom = value / 100; - // Focus towards the center of the zoomed area - PanX += newZoom - Zoom; - PanY += newZoom - Zoom; + Point relative = new((PanX * -1 + CanvasWidth / 2) / Zoom, (PanY * -1 + CanvasHeight / 2) / Zoom); + double absoluteX = relative.X * Zoom + PanX; + double absoluteY = relative.Y * Zoom + PanY; + Zoom = value / 100; + + // Focus towards the center of the zoomed area + PanX = absoluteX - relative.X * Zoom; + PanY = absoluteY - relative.Y * Zoom; } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/DevicesStepView.xaml b/src/Artemis.UI/Screens/StartupWizard/Steps/DevicesStepView.xaml index bd9c7ddc2..df4f7c13f 100644 --- a/src/Artemis.UI/Screens/StartupWizard/Steps/DevicesStepView.xaml +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/DevicesStepView.xaml @@ -16,7 +16,7 @@ - Devices are supported through the use of device providers. + EnabledDevices are supported through the use of device providers. In the list below you can enable device providers for each brand you own by checking . diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/LayoutStepViewModel.cs b/src/Artemis.UI/Screens/StartupWizard/Steps/LayoutStepViewModel.cs index dac1ba898..4977b25b2 100644 --- a/src/Artemis.UI/Screens/StartupWizard/Steps/LayoutStepViewModel.cs +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/LayoutStepViewModel.cs @@ -5,23 +5,23 @@ namespace Artemis.UI.Screens.StartupWizard.Steps { public class LayoutStepViewModel : Screen { - private readonly ISurfaceService _surfaceService; + private readonly IRgbService _rgbService; - public LayoutStepViewModel(ISurfaceService surfaceService) + public LayoutStepViewModel(IRgbService rgbService) { - _surfaceService = surfaceService; + _rgbService = rgbService; } public void ApplyLeftHandedPreset() { - _surfaceService.AutoArrange(); + _rgbService.AutoArrangeDevices(); StartupWizardViewModel startupWizardViewModel = (StartupWizardViewModel) Parent; startupWizardViewModel.Continue(); } public void ApplyRightHandedPreset() { - _surfaceService.AutoArrange(); + _rgbService.AutoArrangeDevices(); StartupWizardViewModel startupWizardViewModel = (StartupWizardViewModel) Parent; startupWizardViewModel.Continue(); } diff --git a/src/Artemis.UI/Screens/SurfaceEditor/Dialogs/SurfaceDeviceConfigView.xaml b/src/Artemis.UI/Screens/SurfaceEditor/Dialogs/SurfaceDeviceConfigView.xaml index 52b34bab4..561b259f7 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/Dialogs/SurfaceDeviceConfigView.xaml +++ b/src/Artemis.UI/Screens/SurfaceEditor/Dialogs/SurfaceDeviceConfigView.xaml @@ -155,12 +155,39 @@ + Color="{Binding CurrentColor, Converter={StaticResource SKColorToColorConverter}}" + VerticalAlignment="Center" /> + + + Custom layout + + + Select a custom layout below if you want to change the appearance and/or LEDs of this device. + + + + + + + + Layout path + + + + + diff --git a/src/Artemis.UI/Screens/SurfaceEditor/Dialogs/SurfaceDeviceConfigViewModel.cs b/src/Artemis.UI/Screens/SurfaceEditor/Dialogs/SurfaceDeviceConfigViewModel.cs index 8d96c2147..ad06f0ca0 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/Dialogs/SurfaceDeviceConfigViewModel.cs +++ b/src/Artemis.UI/Screens/SurfaceEditor/Dialogs/SurfaceDeviceConfigViewModel.cs @@ -1,7 +1,12 @@ -using System.Threading.Tasks; +using System.ComponentModel; +using System.Threading.Tasks; +using System.Windows.Controls; +using System.Windows.Input; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Shared.Services; +using MaterialDesignThemes.Wpf; +using Ookii.Dialogs.Wpf; using SkiaSharp; using Stylet; @@ -10,6 +15,8 @@ namespace Artemis.UI.Screens.SurfaceEditor.Dialogs public class SurfaceDeviceConfigViewModel : DialogViewModelBase { private readonly ICoreService _coreService; + private readonly IRgbService _rgbService; + private readonly IMessageService _messageService; private readonly double _initialRedScale; private readonly double _initialGreenScale; private readonly double _initialBlueScale; @@ -23,41 +30,43 @@ namespace Artemis.UI.Screens.SurfaceEditor.Dialogs private SKColor _currentColor; private bool _displayOnDevices; - public SurfaceDeviceConfigViewModel(ArtemisDevice device, ICoreService coreService, IModelValidator validator) : base(validator) + public SurfaceDeviceConfigViewModel(ArtemisDevice device, + ICoreService coreService, + IRgbService rgbService, + IMessageService messageService, + IModelValidator validator) : base(validator) { _coreService = coreService; + _rgbService = rgbService; + _messageService = messageService; Device = device; - X = (int)Device.X; - Y = (int)Device.Y; + X = (int) Device.X; + Y = (int) Device.Y; Scale = Device.Scale; - Rotation = (int)Device.Rotation; + Rotation = (int) Device.Rotation; RedScale = Device.RedScale * 100d; GreenScale = Device.GreenScale * 100d; BlueScale = Device.BlueScale * 100d; //we need to store the initial values to be able to restore them when the user clicks "Cancel" - _initialRedScale = Device.RedScale; + _initialRedScale = Device.RedScale; _initialGreenScale = Device.GreenScale; _initialBlueScale = Device.BlueScale; CurrentColor = SKColors.White; _coreService.FrameRendering += OnFrameRendering; - } - - private void OnFrameRendering(object sender, FrameRenderingEventArgs e) - { - if (!_displayOnDevices) - return; - - using SKPaint overlayPaint = new() - { - Color = CurrentColor - }; - e.Canvas.DrawRect(0, 0, e.Canvas.LocalClipBounds.Width, e.Canvas.LocalClipBounds.Height, overlayPaint); + Device.PropertyChanged += DeviceOnPropertyChanged; } public ArtemisDevice Device { get; } + public override void OnDialogClosed(object sender, DialogClosingEventArgs e) + { + _coreService.FrameRendering -= OnFrameRendering; + Device.PropertyChanged -= DeviceOnPropertyChanged; + base.OnDialogClosed(sender, e); + } + public int X { get => _x; @@ -130,7 +139,6 @@ namespace Artemis.UI.Screens.SurfaceEditor.Dialogs Device.BlueScale = BlueScale / 100d; _coreService.ModuleRenderingDisabled = false; - _coreService.FrameRendering -= OnFrameRendering; Session.Close(true); } @@ -141,6 +149,26 @@ namespace Artemis.UI.Screens.SurfaceEditor.Dialogs Device.BlueScale = BlueScale / 100d; } + public void BrowseCustomLayout(object sender, MouseEventArgs e) + { + if (e.OriginalSource is Button) + { + Device.CustomLayoutPath = null; + _messageService.ShowMessage("Cleared imported layout"); + return; + } + + VistaOpenFileDialog dialog = new(); + dialog.Filter = "Layout files (*.xml)|*.xml"; + dialog.Title = "Select device layout file"; + bool? result = dialog.ShowDialog(); + if (result == true) + { + Device.CustomLayoutPath = dialog.FileName; + _messageService.ShowMessage($"Imported layout from {dialog.FileName}"); + } + } + public override void Cancel() { Device.RedScale = _initialRedScale; @@ -149,5 +177,25 @@ namespace Artemis.UI.Screens.SurfaceEditor.Dialogs base.Cancel(); } + + private void DeviceOnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(Device.CustomLayoutPath)) + { + _rgbService.ApplyBestDeviceLayout(Device); + } + } + + private void OnFrameRendering(object sender, FrameRenderingEventArgs e) + { + if (!_displayOnDevices) + return; + + using SKPaint overlayPaint = new() + { + Color = CurrentColor + }; + e.Canvas.DrawRect(0, 0, e.Canvas.LocalClipBounds.Width, e.Canvas.LocalClipBounds.Height, overlayPaint); + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/SurfaceEditor/Dialogs/SurfaceDeviceDetectInputViewModel.cs b/src/Artemis.UI/Screens/SurfaceEditor/Dialogs/SurfaceDeviceDetectInputViewModel.cs index a3c7d41f9..07f04fce9 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/Dialogs/SurfaceDeviceDetectInputViewModel.cs +++ b/src/Artemis.UI/Screens/SurfaceEditor/Dialogs/SurfaceDeviceDetectInputViewModel.cs @@ -14,9 +14,10 @@ namespace Artemis.UI.Screens.SurfaceEditor.Dialogs { private readonly IInputService _inputService; private readonly IMessageService _messageService; + private readonly IRgbService _rgbService; private readonly ListLedGroup _ledGroup; - public SurfaceDeviceDetectInputViewModel(ArtemisDevice device, IInputService inputService, IMessageService messageService) + public SurfaceDeviceDetectInputViewModel(ArtemisDevice device, IInputService inputService, IMessageService messageService, IRgbService rgbService) { Device = device; Title = $"{Device.RgbDevice.DeviceInfo.DeviceName} - Detect input"; @@ -24,11 +25,12 @@ namespace Artemis.UI.Screens.SurfaceEditor.Dialogs _inputService = inputService; _messageService = messageService; + _rgbService = rgbService; _inputService.IdentifyDevice(Device); _inputService.DeviceIdentified += InputServiceOnDeviceIdentified; // Create a LED group way at the top - _ledGroup = new ListLedGroup(Device.Leds.Select(l => l.RgbLed)) + _ledGroup = new ListLedGroup(_rgbService.Surface, Device.Leds.Select(l => l.RgbLed)) { Brush = new SolidColorBrush(new Color(255, 255, 0)), ZIndex = 999 @@ -43,7 +45,7 @@ namespace Artemis.UI.Screens.SurfaceEditor.Dialogs { base.OnDialogClosed(sender, e); _inputService.DeviceIdentified -= InputServiceOnDeviceIdentified; - _ledGroup.Detach(); + _ledGroup.Detach(_rgbService.Surface); } private void InputServiceOnDeviceIdentified(object sender, EventArgs e) diff --git a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.xaml b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.xaml index bf909b5d2..6cbafa329 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.xaml +++ b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.xaml @@ -5,11 +5,12 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:s="https://github.com/canton7/Stylet" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" - xmlns:Converters="clr-namespace:Artemis.UI.Converters" - xmlns:models="clr-namespace:Artemis.Core;assembly=Artemis.Core" + xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" + xmlns:local="clr-namespace:Artemis.UI.Screens.SurfaceEditor" x:Class="Artemis.UI.Screens.SurfaceEditor.SurfaceEditorView" mc:Ignorable="d" - d:DesignHeight="600" d:DesignWidth="600"> + d:DesignHeight="600" d:DesignWidth="600" + d:DataContext="{d:DesignInstance {x:Type local:SurfaceEditorViewModel}}"> @@ -18,7 +19,6 @@ - @@ -113,14 +113,17 @@ - + - + @@ -137,28 +140,28 @@ - + - + - + - + - + @@ -167,14 +170,14 @@ - + @@ -231,45 +234,67 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs index aa285c954..8210be706 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs +++ b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using System.Windows; @@ -10,11 +9,13 @@ using System.Windows.Media; using System.Windows.Navigation; using Artemis.Core; using Artemis.Core.Services; +using Artemis.UI.Extensions; using Artemis.UI.Screens.Shared; using Artemis.UI.Screens.SurfaceEditor.Dialogs; using Artemis.UI.Screens.SurfaceEditor.Visualization; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; +using SkiaSharp; using Stylet; using MouseButton = System.Windows.Input.MouseButton; @@ -22,54 +23,42 @@ namespace Artemis.UI.Screens.SurfaceEditor { public class SurfaceEditorViewModel : Screen, IMainScreenViewModel { + private readonly ICoreService _coreService; private readonly IDeviceService _deviceService; - private readonly IInputService _inputService; private readonly IDialogService _dialogService; + private readonly IInputService _inputService; private readonly IRgbService _rgbService; private readonly ISettingsService _settingsService; - private readonly ISurfaceService _surfaceService; private Cursor _cursor; - private ObservableCollection _devices; private PanZoomViewModel _panZoomViewModel; - private ArtemisSurface _selectedSurface; private RectangleGeometry _selectionRectangle; - private ObservableCollection _surfaceConfigurations; private PluginSetting _surfaceListWidth; public SurfaceEditorViewModel(IRgbService rgbService, - ISurfaceService surfaceService, + ICoreService coreService, IDialogService dialogService, ISettingsService settingsService, IDeviceService deviceService, IInputService inputService) { DisplayName = "Surface Editor"; - - Devices = new ObservableCollection(); - SurfaceConfigurations = new ObservableCollection(); SelectionRectangle = new RectangleGeometry(); PanZoomViewModel = new PanZoomViewModel(); Cursor = null; + SurfaceDeviceViewModels = new BindableCollection(); + ListDeviceViewModels = new BindableCollection(); + _rgbService = rgbService; - _surfaceService = surfaceService; + _coreService = coreService; _dialogService = dialogService; _settingsService = settingsService; _deviceService = deviceService; _inputService = inputService; } - public ObservableCollection Devices - { - get => _devices; - set => SetAndNotify(ref _devices, value); - } - - public ObservableCollection SurfaceConfigurations - { - get => _surfaceConfigurations; - set => SetAndNotify(ref _surfaceConfigurations, value); - } + public BindableCollection SurfaceDeviceViewModels { get; } + public BindableCollection ListDeviceViewModels { get; } public RectangleGeometry SelectionRectangle { @@ -95,26 +84,6 @@ namespace Artemis.UI.Screens.SurfaceEditor set => SetAndNotify(ref _cursor, value); } - public ArtemisSurface SelectedSurface - { - get => _selectedSurface; - set - { - if (value == null) - return; - - SetAndNotify(ref _selectedSurface, value); - ApplySelectedSurfaceConfiguration(); - } - } - - public ArtemisSurface CreateSurfaceConfiguration(string name) - { - ArtemisSurface config = _surfaceService.CreateSurfaceConfiguration(name); - Execute.PostToUIThread(() => SurfaceConfigurations.Add(config)); - return config; - } - public void OpenHyperlink(object sender, RequestNavigateEventArgs e) { Core.Utilities.OpenUrl(e.Uri.AbsoluteUri); @@ -126,7 +95,7 @@ namespace Artemis.UI.Screens.SurfaceEditor if (!confirmed) return; - _surfaceService.AutoArrange(); + _rgbService.AutoArrangeDevices(); } private void LoadWorkspaceSettings() @@ -139,184 +108,139 @@ namespace Artemis.UI.Screens.SurfaceEditor SurfaceListWidth.Save(); } - private void LoadSurfaceConfigurations() + + private void CoreServiceOnFrameRendering(object sender, FrameRenderingEventArgs e) { - // Get surface configs - List configs = _surfaceService.SurfaceConfigurations.ToList(); - - // Get the active config, if empty, create a default config - ArtemisSurface activeConfig = _surfaceService.ActiveSurface; - if (activeConfig == null) - { - activeConfig = CreateSurfaceConfiguration("Default"); - configs.Add(activeConfig); - _surfaceService.SetActiveSurfaceConfiguration(activeConfig); - } - - Execute.PostToUIThread(() => - { - // Populate the UI collection - SurfaceConfigurations.Clear(); - foreach (ArtemisSurface surfaceConfiguration in configs) - SurfaceConfigurations.Add(surfaceConfiguration); - - // Set the active config - SelectedSurface = activeConfig; - }); - } - - private void ApplySelectedSurfaceConfiguration() - { - // Make sure all devices have an up-to-date VM - Execute.PostToUIThread(() => - { - lock (Devices) - { - List existing = Devices.ToList(); - List deviceViewModels = new(); - - // Add missing/update existing - foreach (ArtemisDevice surfaceDeviceConfiguration in SelectedSurface.Devices.Where(d => d.IsEnabled).OrderBy(d => d.ZIndex).ToList()) - { - // Create VMs for missing devices - SurfaceDeviceViewModel viewModel = existing.FirstOrDefault(vm => vm.Device.RgbDevice == surfaceDeviceConfiguration.RgbDevice); - if (viewModel == null) - viewModel = new SurfaceDeviceViewModel(surfaceDeviceConfiguration); - // Update existing devices - else - viewModel.Device = surfaceDeviceConfiguration; - - // Add the viewModel to the list of VMs we want to keep - deviceViewModels.Add(viewModel); - } - - Devices = new ObservableCollection(deviceViewModels); - } - }); - - _surfaceService.SetActiveSurfaceConfiguration(SelectedSurface); + foreach (ListDeviceViewModel listDeviceViewModel in ListDeviceViewModels) + foreach (ArtemisLed artemisLed in listDeviceViewModel.Device.Leds) + e.Canvas.DrawRect(artemisLed.AbsoluteRectangle, new SKPaint {Color = listDeviceViewModel.Color}); } #region Overrides of Screen protected override void OnInitialActivate() { - LoadSurfaceConfigurations(); LoadWorkspaceSettings(); + SurfaceDeviceViewModels.AddRange(_rgbService.EnabledDevices.OrderBy(d => d.ZIndex).Select(d => new SurfaceDeviceViewModel(d, _rgbService))); + ListDeviceViewModels.AddRange(_rgbService.EnabledDevices.OrderBy(d => d.ZIndex * -1).Select(d => new ListDeviceViewModel(d))); + + List shuffledDevices = _rgbService.EnabledDevices.OrderBy(d => Guid.NewGuid()).ToList(); + float amount = 360f / shuffledDevices.Count; + for (int i = 0; i < shuffledDevices.Count; i++) + { + ArtemisDevice rgbServiceDevice = shuffledDevices[i]; + ListDeviceViewModel vm = ListDeviceViewModels.First(l => l.Device == rgbServiceDevice); + vm.Color = SKColor.FromHsv(amount * i, 100, 100); + } + + _coreService.FrameRendering += CoreServiceOnFrameRendering; + base.OnInitialActivate(); } protected override void OnClose() { SaveWorkspaceSettings(); + SurfaceDeviceViewModels.Clear(); + ListDeviceViewModels.Clear(); + + _coreService.FrameRendering -= CoreServiceOnFrameRendering; + base.OnClose(); } #endregion - #region Configuration management - - public async Task DeleteSurfaceConfiguration(ArtemisSurface surface) - { - bool result = await _dialogService.ShowConfirmDialogAt( - "SurfaceListDialogHost", - "Delete surface configuration", - "Are you sure you want to delete\nthis surface configuration?" - ); - if (result) - { - SurfaceConfigurations.Remove(surface); - _surfaceService.DeleteSurfaceConfiguration(surface); - } - } - - public async Task AddSurfaceConfiguration() - { - object result = await _dialogService.ShowDialogAt("SurfaceListDialogHost"); - if (result is string name) - CreateSurfaceConfiguration(name); - } - - #endregion - #region Context menu actions - public void IdentifyDevice(SurfaceDeviceViewModel surfaceDeviceViewModel) + public void IdentifyDevice(ArtemisDevice device) { - _deviceService.IdentifyDevice(surfaceDeviceViewModel.Device); + _deviceService.IdentifyDevice(device); } - public void BringToFront(SurfaceDeviceViewModel surfaceDeviceViewModel) + public void BringToFront(ArtemisDevice device) { - Devices.Move(Devices.IndexOf(surfaceDeviceViewModel), Devices.Count - 1); - for (int i = 0; i < Devices.Count; i++) + SurfaceDeviceViewModel surfaceDeviceViewModel = SurfaceDeviceViewModels.First(d => d.Device == device); + SurfaceDeviceViewModels.Move(SurfaceDeviceViewModels.IndexOf(surfaceDeviceViewModel), SurfaceDeviceViewModels.Count - 1); + for (int i = 0; i < SurfaceDeviceViewModels.Count; i++) { - SurfaceDeviceViewModel deviceViewModel = Devices[i]; + SurfaceDeviceViewModel deviceViewModel = SurfaceDeviceViewModels[i]; deviceViewModel.Device.ZIndex = i + 1; } - _surfaceService.UpdateSurfaceConfiguration(SelectedSurface, true); + ListDeviceViewModels.Sort(l => l.Device.ZIndex * -1); + + _rgbService.SaveDevices(); } - public void BringForward(SurfaceDeviceViewModel surfaceDeviceViewModel) + public void BringForward(ArtemisDevice device) { - int currentIndex = Devices.IndexOf(surfaceDeviceViewModel); - int newIndex = Math.Min(currentIndex + 1, Devices.Count - 1); - Devices.Move(currentIndex, newIndex); + SurfaceDeviceViewModel surfaceDeviceViewModel = SurfaceDeviceViewModels.First(d => d.Device == device); + int currentIndex = SurfaceDeviceViewModels.IndexOf(surfaceDeviceViewModel); + int newIndex = Math.Min(currentIndex + 1, SurfaceDeviceViewModels.Count - 1); + SurfaceDeviceViewModels.Move(currentIndex, newIndex); - for (int i = 0; i < Devices.Count; i++) + for (int i = 0; i < SurfaceDeviceViewModels.Count; i++) { - SurfaceDeviceViewModel deviceViewModel = Devices[i]; + SurfaceDeviceViewModel deviceViewModel = SurfaceDeviceViewModels[i]; deviceViewModel.Device.ZIndex = i + 1; } - _surfaceService.UpdateSurfaceConfiguration(SelectedSurface, true); + ListDeviceViewModels.Sort(l => l.Device.ZIndex * -1); + + _rgbService.SaveDevices(); } - public void SendToBack(SurfaceDeviceViewModel surfaceDeviceViewModel) + public void SendToBack(ArtemisDevice device) { - Devices.Move(Devices.IndexOf(surfaceDeviceViewModel), 0); - for (int i = 0; i < Devices.Count; i++) + SurfaceDeviceViewModel surfaceDeviceViewModel = SurfaceDeviceViewModels.First(d => d.Device == device); + SurfaceDeviceViewModels.Move(SurfaceDeviceViewModels.IndexOf(surfaceDeviceViewModel), 0); + for (int i = 0; i < SurfaceDeviceViewModels.Count; i++) { - SurfaceDeviceViewModel deviceViewModel = Devices[i]; + SurfaceDeviceViewModel deviceViewModel = SurfaceDeviceViewModels[i]; deviceViewModel.Device.ZIndex = i + 1; } - _surfaceService.UpdateSurfaceConfiguration(SelectedSurface, true); + ListDeviceViewModels.Sort(l => l.Device.ZIndex * -1); + + _rgbService.SaveDevices(); } - public void SendBackward(SurfaceDeviceViewModel surfaceDeviceViewModel) + public void SendBackward(ArtemisDevice device) { - int currentIndex = Devices.IndexOf(surfaceDeviceViewModel); + SurfaceDeviceViewModel surfaceDeviceViewModel = SurfaceDeviceViewModels.First(d => d.Device == device); + int currentIndex = SurfaceDeviceViewModels.IndexOf(surfaceDeviceViewModel); int newIndex = Math.Max(currentIndex - 1, 0); - Devices.Move(currentIndex, newIndex); - for (int i = 0; i < Devices.Count; i++) + SurfaceDeviceViewModels.Move(currentIndex, newIndex); + for (int i = 0; i < SurfaceDeviceViewModels.Count; i++) { - SurfaceDeviceViewModel deviceViewModel = Devices[i]; + SurfaceDeviceViewModel deviceViewModel = SurfaceDeviceViewModels[i]; deviceViewModel.Device.ZIndex = i + 1; } - _surfaceService.UpdateSurfaceConfiguration(SelectedSurface, true); + ListDeviceViewModels.Sort(l => l.Device.ZIndex * -1); + + _rgbService.SaveDevices(); } - public async Task ViewProperties(SurfaceDeviceViewModel surfaceDeviceViewModel) + public async Task ViewProperties(ArtemisDevice device) { object madeChanges = await _dialogService.ShowDialog( - new Dictionary {{"device", surfaceDeviceViewModel.Device}} + new Dictionary {{"device", device}} ); if ((bool) madeChanges) - _surfaceService.UpdateSurfaceConfiguration(SelectedSurface, true); + _rgbService.SaveDevice(device); } - public async Task DetectInput(SurfaceDeviceViewModel surfaceDeviceViewModel) + public async Task DetectInput(ArtemisDevice device) { object madeChanges = await _dialogService.ShowDialog( - new Dictionary {{"device", surfaceDeviceViewModel.Device}} + new Dictionary {{"device", device}} ); if ((bool) madeChanges) - _surfaceService.UpdateSurfaceConfiguration(SelectedSurface, true); + _rgbService.SaveDevice(device); } #endregion @@ -377,13 +301,13 @@ namespace Artemis.UI.Screens.SurfaceEditor if (device.SelectionStatus != SelectionStatus.Selected) { if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift)) - foreach (SurfaceDeviceViewModel others in Devices) + foreach (SurfaceDeviceViewModel others in SurfaceDeviceViewModels) others.SelectionStatus = SelectionStatus.None; device.SelectionStatus = SelectionStatus.Selected; } - foreach (SurfaceDeviceViewModel selectedDevice in Devices.Where(d => d.SelectionStatus == SelectionStatus.Selected)) + foreach (SurfaceDeviceViewModel selectedDevice in SurfaceDeviceViewModels.Where(d => d.SelectionStatus == SelectionStatus.Selected)) selectedDevice.StartMouseDrag(relative); } // Start multi-selection @@ -395,6 +319,7 @@ namespace Artemis.UI.Screens.SurfaceEditor // Any time dragging starts, start with a new rect SelectionRectangle.Rect = new Rect(); + ApplySurfaceSelection(); } private void StopMouseDrag(object sender, Point position) @@ -403,17 +328,20 @@ namespace Artemis.UI.Screens.SurfaceEditor { RectangleGeometry selectedRect = new(new Rect(_mouseDragStartPoint, position)); List devices = HitTestUtilities.GetHitViewModels((Visual) sender, selectedRect); - foreach (SurfaceDeviceViewModel device in Devices) + foreach (SurfaceDeviceViewModel device in SurfaceDeviceViewModels) if (devices.Contains(device)) device.SelectionStatus = SelectionStatus.Selected; else if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift)) device.SelectionStatus = SelectionStatus.None; } else - _surfaceService.UpdateSurfaceConfiguration(SelectedSurface, true); + { + _rgbService.SaveDevices(); + } _mouseDragStatus = MouseDragStatus.None; _rgbService.IsRenderPaused = false; + ApplySurfaceSelection(); } private void UpdateSelection(object sender, Point position) @@ -425,19 +353,27 @@ namespace Artemis.UI.Screens.SurfaceEditor SelectionRectangle.Rect = selectedRect; List devices = HitTestUtilities.GetHitViewModels((Visual) sender, SelectionRectangle); - foreach (SurfaceDeviceViewModel device in Devices) + foreach (SurfaceDeviceViewModel device in SurfaceDeviceViewModels) if (devices.Contains(device)) device.SelectionStatus = SelectionStatus.Selected; else if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift)) device.SelectionStatus = SelectionStatus.None; + + ApplySurfaceSelection(); } private void MoveSelected(Point position) { - foreach (SurfaceDeviceViewModel device in Devices.Where(d => d.SelectionStatus == SelectionStatus.Selected)) + foreach (SurfaceDeviceViewModel device in SurfaceDeviceViewModels.Where(d => d.SelectionStatus == SelectionStatus.Selected)) device.UpdateMouseDrag(position); } + private void ApplySurfaceSelection() + { + foreach (ListDeviceViewModel viewModel in ListDeviceViewModels) + viewModel.IsSelected = SurfaceDeviceViewModels.Any(s => s.Device == viewModel.Device && s.SelectionStatus == SelectionStatus.Selected); + } + #endregion #region Panning and zooming diff --git a/src/Artemis.UI/Screens/SurfaceEditor/Visualization/ListDeviceView.xaml b/src/Artemis.UI/Screens/SurfaceEditor/Visualization/ListDeviceView.xaml new file mode 100644 index 000000000..195b138f1 --- /dev/null +++ b/src/Artemis.UI/Screens/SurfaceEditor/Visualization/ListDeviceView.xaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/SurfaceEditor/Visualization/ListDeviceViewModel.cs b/src/Artemis.UI/Screens/SurfaceEditor/Visualization/ListDeviceViewModel.cs new file mode 100644 index 000000000..d7c6e4b88 --- /dev/null +++ b/src/Artemis.UI/Screens/SurfaceEditor/Visualization/ListDeviceViewModel.cs @@ -0,0 +1,30 @@ +using Artemis.Core; +using SkiaSharp; +using Stylet; + +namespace Artemis.UI.Screens.SurfaceEditor.Visualization +{ + public class ListDeviceViewModel : PropertyChangedBase + { + private bool _isSelected; + private SKColor _color; + public ArtemisDevice Device { get; } + + public bool IsSelected + { + get => _isSelected; + set => SetAndNotify(ref _isSelected, value); + } + + public SKColor Color + { + get => _color; + set => SetAndNotify(ref _color, value); + } + + public ListDeviceViewModel(ArtemisDevice device) + { + Device = device; + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/SurfaceEditor/Visualization/SurfaceDeviceView.xaml b/src/Artemis.UI/Screens/SurfaceEditor/Visualization/SurfaceDeviceView.xaml index 60b6a4543..0e5afdcd9 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/Visualization/SurfaceDeviceView.xaml +++ b/src/Artemis.UI/Screens/SurfaceEditor/Visualization/SurfaceDeviceView.xaml @@ -5,10 +5,10 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:s="https://github.com/canton7/Stylet" xmlns:converters="clr-namespace:Artemis.UI.Converters" - xmlns:surfaceEditor="clr-namespace:Artemis.UI.Screens.SurfaceEditor.Visualization" xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" + xmlns:local="clr-namespace:Artemis.UI.Screens.SurfaceEditor.Visualization" mc:Ignorable="d" - d:DataContext="{d:DesignInstance {x:Type surfaceEditor:SurfaceDeviceViewModel}}" + d:DataContext="{d:DesignInstance {x:Type local:SurfaceDeviceViewModel}}" d:DesignHeight="450" d:DesignWidth="800" Cursor="{Binding Cursor}" MouseEnter="{s:Action MouseEnter}" @@ -20,21 +20,22 @@ - - - + + + + @@ -47,7 +48,7 @@