using System; 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 { /// /// Represents an RGB device usable by Artemis, provided by a /// public class ArtemisDevice : CorePropertyChanged { private SKPath? _path; private SKRect _rectangle; internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider) { Identifier = rgbDevice.GetDeviceIdentifier(); DeviceEntity = new DeviceEntity(); RgbDevice = rgbDevice; DeviceProvider = deviceProvider; Rotation = 0; Scale = 1; ZIndex = 1; RedScale = 1; GreenScale = 1; BlueScale = 1; IsEnabled = true; InputIdentifiers = new List(); InputMappings = new Dictionary(); Categories = new HashSet(); UpdateLeds(); ApplyKeyboardLayout(); ApplyToEntity(); ApplyDefaultCategories(); CalculateRenderProperties(); } internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider, DeviceEntity deviceEntity) { Identifier = rgbDevice.GetDeviceIdentifier(); DeviceEntity = deviceEntity; RgbDevice = rgbDevice; DeviceProvider = deviceProvider; InputIdentifiers = new List(); InputMappings = new Dictionary(); Categories = new HashSet(); foreach (DeviceInputIdentifierEntity identifierEntity in DeviceEntity.InputIdentifiers) InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(identifierEntity.InputProvider, identifierEntity.Identifier)); UpdateLeds(); ApplyKeyboardLayout(); } /// /// Gets the (hopefully unique and persistent) ID of this device /// public string Identifier { get; } /// /// Gets the rectangle covering the device /// public SKRect Rectangle { get => _rectangle; private set => SetAndNotify(ref _rectangle, value); } /// /// Gets the path surrounding the device /// public SKPath? Path { get => _path; private set => SetAndNotify(ref _path, value); } /// /// Gets the RGB.NET device backing this Artemis device /// public IRGBDevice RgbDevice { get; } /// /// Gets the device provider that provided this device /// public DeviceProvider DeviceProvider { get; } /// /// Gets a read only collection containing the LEDs of this device /// 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; private set; } /// /// Gets a list of input identifiers associated with this device /// public List InputIdentifiers { get; } /// /// Gets a list of input mappings configured on the device /// public Dictionary InputMappings { get; } /// /// Gets a list containing the categories of this device /// public HashSet Categories { get; } /// /// Gets or sets the X-position of the device /// public float X { get => DeviceEntity.X; set { DeviceEntity.X = value; OnPropertyChanged(nameof(X)); } } /// /// Gets or sets the Y-position of the device /// public float Y { get => DeviceEntity.Y; set { DeviceEntity.Y = value; OnPropertyChanged(nameof(Y)); } } /// /// Gets or sets the rotation of the device /// public float Rotation { get => DeviceEntity.Rotation; set { DeviceEntity.Rotation = value; OnPropertyChanged(nameof(Rotation)); } } /// /// Gets or sets the scale of the device /// public float Scale { get => DeviceEntity.Scale; set { DeviceEntity.Scale = value; OnPropertyChanged(nameof(Scale)); } } /// /// Gets or sets the Z-index of the device /// public int ZIndex { get => DeviceEntity.ZIndex; set { DeviceEntity.ZIndex = value; OnPropertyChanged(nameof(ZIndex)); } } /// /// Gets or sets the scale of the red color component used for calibration /// public float RedScale { get => DeviceEntity.RedScale; set { DeviceEntity.RedScale = value; OnPropertyChanged(nameof(RedScale)); } } /// /// Gets or sets the scale of the green color component used for calibration /// public float GreenScale { get => DeviceEntity.GreenScale; set { DeviceEntity.GreenScale = value; OnPropertyChanged(nameof(GreenScale)); } } /// /// Gets or sets the scale of the blue color component used for calibration /// public float BlueScale { get => DeviceEntity.BlueScale; set { DeviceEntity.BlueScale = value; OnPropertyChanged(nameof(BlueScale)); } } /// /// 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; internal set { DeviceEntity.IsEnabled = value; OnPropertyChanged(nameof(IsEnabled)); } } /// /// Gets or sets the physical layout of the device e.g. ISO or ANSI. /// Only applicable to keyboards /// 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; } /// public override string ToString() { return $"[{RgbDevice.DeviceInfo.DeviceType}] {RgbDevice.DeviceInfo.DeviceName} - {X}.{Y}.{ZIndex}"; } /// /// Attempts to retrieve the that corresponds the provided RGB.NET /// /// The RGB.NET to find the corresponding for /// /// If , LEDs mapped to different LEDs /// are taken into consideration /// /// If found, the corresponding ; otherwise . public ArtemisLed? GetLed(Led led, bool applyInputMapping) { return GetLed(led.Id, applyInputMapping); } /// /// Attempts to retrieve the that corresponds the provided RGB.NET /// /// The RGB.NET to find the corresponding for /// /// If , LEDs mapped to different LEDs /// are taken into consideration /// /// If found, the corresponding ; otherwise . public ArtemisLed? GetLed(LedId ledId, bool applyInputMapping) { LedIds.TryGetValue(ledId, out ArtemisLed? artemisLed); if (artemisLed == null) return null; if (applyInputMapping && InputMappings.TryGetValue(artemisLed, out ArtemisLed? mappedLed)) return mappedLed; 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.DeviceInfo.DeviceType == RGBDeviceType.Keyboard) fileName = $"{fileName}-{PhysicalLayout.ToString().ToUpper()}"; if (includeExtension) fileName = $"{fileName}.xml"; return fileName; } /// /// Occurs when the underlying RGB.NET device was updated /// public event EventHandler? DeviceUpdated; /// /// Invokes the event /// protected virtual void OnDeviceUpdated() { DeviceUpdated?.Invoke(this, EventArgs.Empty); } /// /// 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 (createMissingLeds && !DeviceProvider.CreateMissingLedsSupported) throw new ArtemisCoreException($"Cannot apply layout with {nameof(createMissingLeds)} " + "set to true because the device provider does not support it"); if (removeExcessiveLeds && !DeviceProvider.RemoveExcessiveLedsSupported) throw new ArtemisCoreException($"Cannot apply layout with {nameof(removeExcessiveLeds)} " + "set to true because the device provider does not support it"); if (layout.IsValid) layout.RgbLayout!.ApplyTo(RgbDevice, createMissingLeds, removeExcessiveLeds); UpdateLeds(); Layout = layout; Layout.ApplyDevice(this); CalculateRenderProperties(); OnDeviceUpdated(); } /// /// Applies the default categories for this device to the list /// public void ApplyDefaultCategories() { switch (RgbDevice.DeviceInfo.DeviceType) { case RGBDeviceType.Keyboard: case RGBDeviceType.Mouse: case RGBDeviceType.Headset: case RGBDeviceType.Mousepad: case RGBDeviceType.HeadsetStand: case RGBDeviceType.Keypad: if (!Categories.Contains(DeviceCategory.Peripherals)) Categories.Add(DeviceCategory.Peripherals); break; case RGBDeviceType.Mainboard: case RGBDeviceType.GraphicsCard: case RGBDeviceType.DRAM: case RGBDeviceType.Fan: case RGBDeviceType.LedStripe: case RGBDeviceType.Cooler: if (!Categories.Contains(DeviceCategory.Case)) Categories.Add(DeviceCategory.Case); break; case RGBDeviceType.Speaker: if (!Categories.Contains(DeviceCategory.Desk)) Categories.Add(DeviceCategory.Desk); break; case RGBDeviceType.Monitor: if (!Categories.Contains(DeviceCategory.Monitor)) Categories.Add(DeviceCategory.Monitor); break; case RGBDeviceType.LedMatrix: if (!Categories.Contains(DeviceCategory.Room)) Categories.Add(DeviceCategory.Room); break; } } internal void ApplyToEntity() { // Other properties are computed DeviceEntity.Id = Identifier; DeviceEntity.InputIdentifiers.Clear(); foreach (ArtemisDeviceInputIdentifier identifier in InputIdentifiers) { DeviceEntity.InputIdentifiers.Add(new DeviceInputIdentifierEntity { InputProvider = identifier.InputProvider, Identifier = identifier.Identifier }); } DeviceEntity.InputMappings.Clear(); foreach (var (original, mapped) in InputMappings) DeviceEntity.InputMappings.Add(new InputMappingEntity {OriginalLedId = (int) original.RgbLed.Id, MappedLedId = (int) mapped.RgbLed.Id}); DeviceEntity.Categories.Clear(); foreach (DeviceCategory deviceCategory in Categories) DeviceEntity.Categories.Add((int) deviceCategory); } internal void ApplyToRgbDevice() { RgbDevice.Rotation = DeviceEntity.Rotation; RgbDevice.Scale = DeviceEntity.Scale; // Workaround for device rotation not applying if (DeviceEntity.X == 0 && DeviceEntity.Y == 0) RgbDevice.Location = new Point(1, 1); RgbDevice.Location = new Point(DeviceEntity.X, DeviceEntity.Y); InputIdentifiers.Clear(); foreach (DeviceInputIdentifierEntity identifierEntity in DeviceEntity.InputIdentifiers) InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(identifierEntity.InputProvider, identifierEntity.Identifier)); if (!RgbDevice.ColorCorrections.Any()) RgbDevice.ColorCorrections.Add(new ScaleColorCorrection(this)); Categories.Clear(); foreach (int deviceEntityCategory in DeviceEntity.Categories) Categories.Add((DeviceCategory) deviceEntityCategory); if (!Categories.Any()) ApplyDefaultCategories(); CalculateRenderProperties(); OnDeviceUpdated(); } internal void CalculateRenderProperties() { Rectangle = RgbDevice.Boundary.ToSKRect(); if (!Leds.Any()) return; foreach (ArtemisLed led in Leds) led.CalculateRectangles(); SKPath path = new() {FillType = SKPathFillType.Winding}; foreach (ArtemisLed artemisLed in Leds) path.AddRect(artemisLed.AbsoluteRectangle); Path = path; } private void UpdateLeds() { Leds = RgbDevice.Select(l => new ArtemisLed(l, this)).ToList().AsReadOnly(); LedIds = new ReadOnlyDictionary(Leds.ToDictionary(l => l.RgbLed.Id, l => l)); InputMappings.Clear(); foreach (InputMappingEntity deviceEntityInputMapping in DeviceEntity.InputMappings) { ArtemisLed? original = Leds.FirstOrDefault(l => l.RgbLed.Id == (LedId) deviceEntityInputMapping.OriginalLedId); ArtemisLed? mapped = Leds.FirstOrDefault(l => l.RgbLed.Id == (LedId) deviceEntityInputMapping.MappedLedId); if (original != null && mapped != null) InputMappings.Add(original, mapped); } } private void ApplyKeyboardLayout() { if (RgbDevice.DeviceInfo.DeviceType != RGBDeviceType.Keyboard) return; IKeyboard? keyboard = RgbDevice as IKeyboard; // If supported, detect the device layout so that we can load the correct one if (DeviceProvider.CanDetectPhysicalLayout && keyboard != null) PhysicalLayout = (KeyboardLayoutType) keyboard.DeviceInfo.Layout; else PhysicalLayout = (KeyboardLayoutType) DeviceEntity.PhysicalLayout; if (DeviceProvider.CanDetectLogicalLayout && keyboard != null) LogicalLayout = DeviceProvider.GetLogicalLayout(keyboard); else LogicalLayout = DeviceEntity.LogicalLayout; } } public enum DeviceCategory { Desk, Monitor, Case, Room, Peripherals } }