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/Models/Surface/ArtemisDevice.cs b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs index 0317d99fa..e9efb8a36 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs @@ -32,7 +32,7 @@ namespace Artemis.Core GreenScale = 1; BlueScale = 1; IsEnabled = true; - + InputIdentifiers = new List(); Leds = rgbDevice.Select(l => new ArtemisLed(l, this)).ToList().AsReadOnly(); @@ -49,7 +49,7 @@ namespace Artemis.Core RgbDevice = rgbDevice; DeviceProvider = deviceProvider; Surface = surface; - + InputIdentifiers = new List(); foreach (DeviceInputIdentifierEntity identifierEntity in DeviceEntity.InputIdentifiers) InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(identifierEntity.InputProvider, identifierEntity.Identifier)); @@ -95,13 +95,13 @@ namespace Artemis.Core /// /// 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 @@ -225,6 +225,34 @@ namespace Artemis.Core } } + /// + /// Gets or sets the physical layout of the device e.g. ISO or ANSI. + /// Only applicable to keyboards + /// + public string? PhysicalLayout + { + get => DeviceEntity.PhysicalLayout; + set + { + DeviceEntity.PhysicalLayout = 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 the layout of the device expanded with Artemis-specific data /// @@ -259,13 +287,58 @@ 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 (PhysicalLayout != null) + fileName = $"{fileName}-{PhysicalLayout.ToUpper()}"; + if (includeExtension) + fileName = $"{fileName}.xml"; + + return fileName; + } + + /// + /// Loads the best layout for this device, preferring user layouts and falling back to default layouts + /// public void LoadBestLayout() { - ArtemisLayout artemisLayout = DeviceProvider.LoadLayout(RgbDevice); - if (artemisLayout.IsValid) - artemisLayout.DeviceLayout!.ApplyTo(RgbDevice); + // If supported, detect the device layout so that we can load the correct one + if (DeviceProvider.CanDetectLogicalLayout || DeviceProvider.CanDetectPhysicalLayout && RgbDevice is IKeyboard) + DeviceProvider.DetectDeviceLayout(this); - Layout = artemisLayout; + // Look for a user layout + // ... here + + // Try loading a device provider layout, if that comes back valid we use that + ArtemisLayout deviceProviderLayout = DeviceProvider.LoadLayout(this); + + // Finally fall back to a default layout + // .. do it! + + ApplyLayout(deviceProviderLayout); + } + + /// + /// Applies the provided layout to the device + /// + /// The layout to apply + public void ApplyLayout(ArtemisLayout layout) + { + if (layout.IsValid) + layout.RgbLayout!.ApplyTo(RgbDevice); + + Layout = layout; + Leds = RgbDevice.Select(l => new ArtemisLed(l, this)).ToList().AsReadOnly(); + LedIds = new ReadOnlyDictionary(Leds.ToDictionary(l => l.RgbLed.Id, l => l)); + Layout.ApplyDevice(this); + Leds = Layout.Leds.Select(l => l.Led); } internal void ApplyToEntity() diff --git a/src/Artemis.Core/Models/Surface/ArtemisLayout.cs b/src/Artemis.Core/Models/Surface/ArtemisLayout.cs deleted file mode 100644 index cf61d8752..000000000 --- a/src/Artemis.Core/Models/Surface/ArtemisLayout.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -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 - public ArtemisLayout(string filePath) - { - FilePath = filePath; - DeviceLayout = DeviceLayout.Load(FilePath); - IsValid = DeviceLayout != null; - } - - /// - /// Gets the file path the layout was (attempted to be) loaded from - /// - public string FilePath { get; } - - /// - /// Gets the RGB.NET device layout - /// - public DeviceLayout? DeviceLayout { get; } - - /// - /// Gets a boolean indicating whether a valid layout was loaded - /// - public bool IsValid { get; } - - /// - /// Gets the device this image is applied to - /// - public ArtemisDevice? Device { get; internal set; } - - /// - /// Gets or sets the physical layout of the device. Only applicable to keyboards - /// - public string? PhysicalLayout { get; set; } - - /// - /// Gets or sets the logical layout of the device. Only applicable to keyboards - /// - public string? LogicalLayout { get; set; } - - /// - /// Gets or sets the image of the device - /// - public Uri? Image { get; set; } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/ArtemisLed.cs b/src/Artemis.Core/Models/Surface/ArtemisLed.cs index cd946fd6d..4122b870d 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisLed.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisLed.cs @@ -1,4 +1,5 @@ -using RGB.NET.Core; +using System.Linq; +using RGB.NET.Core; using SkiaSharp; namespace Artemis.Core @@ -16,6 +17,9 @@ namespace Artemis.Core RgbLed = led; Device = device; CalculateRectangles(); + + Layout = Device.Layout?.Leds.FirstOrDefault(l => l.RgbLayout.Id == led.Id.ToString()); + } /// @@ -46,7 +50,7 @@ namespace Artemis.Core private set => SetAndNotify(ref _absoluteRectangle, value); } - public ArtemisLedLayout? Layout { get; set; } + public ArtemisLedLayout? Layout { get; } internal void CalculateRectangles() { diff --git a/src/Artemis.Core/Models/Surface/ArtemisLedLayout.cs b/src/Artemis.Core/Models/Surface/ArtemisLedLayout.cs deleted file mode 100644 index 884e0b226..000000000 --- a/src/Artemis.Core/Models/Surface/ArtemisLedLayout.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Artemis.Core -{ - public class ArtemisLedLayout - { - public ArtemisLed Led { get; set; } - public Uri Image { get; set; } - } -} \ 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..29553b14e --- /dev/null +++ b/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs @@ -0,0 +1,85 @@ +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 + public ArtemisLayout(string filePath) + { + FilePath = filePath; + Leds = new List(); + + 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(); + } + + /// + /// Gets the file path the layout was (attempted to be) loaded from + /// + public string FilePath { get; } + + /// + /// Gets the RGB.NET device layout + /// + public DeviceLayout RgbLayout { 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; } + + /// + /// Gets or sets the image of the device + /// + public Uri? Image { get; set; } + + public List Leds { get; } + + internal LayoutCustomDeviceData LayoutCustomDeviceData { get; set; } + + internal void ApplyDevice(ArtemisDevice artemisDevice) + { + Device = artemisDevice; + foreach (ArtemisLedLayout artemisLedLayout in Leds) + artemisLedLayout.ApplyDevice(Device); + } + + private void ApplyCustomDeviceData() + { + Uri layoutDirectory = new(Path.GetDirectoryName(FilePath)! + "\\", UriKind.Absolute); + if (LayoutCustomDeviceData.DeviceImage != null) + Image = new Uri(layoutDirectory, new Uri(LayoutCustomDeviceData.DeviceImage, UriKind.Relative)); + } + } +} \ 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..ab41cb1d5 --- /dev/null +++ b/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using System.Linq; +using RGB.NET.Layout; + +namespace Artemis.Core +{ + public class ArtemisLedLayout + { + internal ArtemisLedLayout(ArtemisLayout layout, ILedLayout led) + { + Layout = layout; + RgbLayout = led; + LayoutCustomLedData = (LayoutCustomLedData?) led.CustomData ?? new LayoutCustomLedData(); + } + + public ArtemisLayout Layout { get; } + + /// + /// Gets the RGB.NET LED Layout of this Artemis 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; } + + internal LayoutCustomLedData LayoutCustomLedData { get; set; } + + public void ApplyDevice(ArtemisDevice device) + { + Led = device.Leds.FirstOrDefault(d => d.RgbLed.Id.ToString() == RgbLayout.Id); + ApplyCustomLedData(); + } + + private void ApplyCustomLedData() + { + if (Led == null) + return; + + Uri layoutDirectory = new(Path.GetDirectoryName(Layout.FilePath)! + "\\", UriKind.Absolute); + } + } +} \ 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 9ae9f0e20..d069d821a 100644 --- a/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs +++ b/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Linq; using Ninject; using RGB.NET.Core; using Serilog; @@ -35,12 +34,20 @@ namespace Artemis.Core.DeviceProviders public ILogger? Logger { get; set; } /// - /// A boolean indicating whether this device provider detects the physical layout of connected keyboards + /// 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; } @@ -55,33 +62,27 @@ namespace Artemis.Core.DeviceProviders /// /// The device to load the layout for /// The resulting Artemis layout - public virtual ArtemisLayout LoadLayout(IRGBDevice rgbDevice) + public virtual ArtemisLayout LoadLayout(ArtemisDevice rgbDevice) { - // Take out invalid file name chars, may not be perfect but neither are you - string model = Path.GetInvalidFileNameChars().Aggregate(rgbDevice.DeviceInfo.Model, (current, c) => current.Replace(c, '-')); string layoutDir = Path.Combine(Plugin.Directory.FullName, "Layouts"); - string filePath; - // if (rgbDevice.DeviceInfo is IPhysicalLayoutDeviceInfo) - // { - // filePath = Path.Combine( - // layoutDir, - // rgbDevice.DeviceInfo.Manufacturer, - // rgbDevice.DeviceInfo.DeviceType.ToString(), - // model, - // keyboard.DeviceInfo. - // ) + ".xml"; - // } - // else - // { - filePath = Path.Combine( + string filePath = Path.Combine( layoutDir, - rgbDevice.DeviceInfo.Manufacturer, - rgbDevice.DeviceInfo.DeviceType.ToString(), - model - ) + ".xml"; - // } - + rgbDevice.RgbDevice.DeviceInfo.Manufacturer, + rgbDevice.RgbDevice.DeviceInfo.DeviceType.ToString(), + rgbDevice.GetLayoutFileName() + ); return new ArtemisLayout(filePath); } + + /// + /// Called when a specific RGB device's logical and physical layout must be detected + /// + /// Note: Only called when or is . + /// + /// + /// The device to detect the layout for, always a keyboard + public virtual void DetectDeviceLayout(ArtemisDevice rgbDevice) + { + } } } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs b/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs index b952fc74c..20c986cd1 100644 --- a/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs +++ b/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs @@ -20,6 +20,9 @@ namespace Artemis.Storage.Entities.Surface public double BlueScale { get; set; } public bool IsEnabled { get; set; } + public string PhysicalLayout { get; set; } + public string LogicalLayout { get; set; } + public List InputIdentifiers { get; set; } } diff --git a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs index 0b3410643..fa6667b99 100644 --- a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs +++ b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs @@ -235,8 +235,8 @@ namespace Artemis.UI.Shared 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) diff --git a/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs b/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs index 724a8555c..c3bc25d34 100644 --- a/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs +++ b/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs @@ -11,6 +11,8 @@ namespace Artemis.UI.Shared { internal class DeviceVisualizerLed { + private SolidColorBrush? _renderColorBrush; + public DeviceVisualizerLed(ArtemisLed led) { Led = led; @@ -43,9 +45,9 @@ namespace Artemis.UI.Shared 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); + _renderColorBrush ??= new SolidColorBrush(); + _renderColorBrush.Color = isDimmed ? Color.FromArgb(100, r, g, b) : Color.FromRgb(r, g, b); + drawingContext.DrawRectangle(_renderColorBrush, null, LedRect); } public void RenderImage(DrawingContext drawingContext)