From 57193e739e3dfc2d765aea9b3da9194e63cfaa38 Mon Sep 17 00:00:00 2001 From: Robert Beekman Date: Thu, 13 May 2021 19:55:10 +0200 Subject: [PATCH] Profiles - Added profile adaption based on configurable hints Layers - Added hints UI accesible from profile tree Surface - Added device categories used by profile adaption --- .../Artemis.Core.csproj.DotSettings | 3 + .../AdaptionHints/CategoryAdaptionHint.cs | 75 +++++++++ .../AdaptionHints/DeviceAdaptionHint.cs | 76 +++++++++ .../Profile/AdaptionHints/IAdaptionHint.cs | 23 +++ .../KeyboardSectionAdaptionHint.cs | 83 ++++++++++ src/Artemis.Core/Models/Profile/Layer.cs | 95 ++++++------ .../Models/Profile/LayerAdapter.cs | 144 ++++++++++++++++++ .../Models/Surface/ArtemisDevice.cs | 67 ++++++++ src/Artemis.Core/Services/RgbService.cs | 3 + .../Storage/Interfaces/IProfileService.cs | 6 + .../Services/Storage/ProfileService.cs | 13 +- .../CategoryAdaptionHintEntity.cs | 13 ++ .../AdaptionHints/DeviceAdaptionHintEntity.cs | 11 ++ .../AdaptionHints/IAdaptionHintEntity.cs | 6 + .../KeyboardSectionAdaptionHintEntity.cs | 7 + .../Entities/Profile/LayerEntity.cs | 3 + .../Entities/Profile/LedEntity.cs | 2 + .../Entities/Surface/DeviceEntity.cs | 5 +- .../Ninject/Factories/IVMFactory.cs | 10 ++ .../ProfileEditor/ProfileEditorView.xaml | 6 + .../ProfileEditor/ProfileEditorViewModel.cs | 14 ++ .../AdaptionHints/AdaptionHintViewModel.cs | 20 +++ .../CategoryAdaptionHintView.xaml | 78 ++++++++++ .../CategoryAdaptionHintViewModel.cs | 46 ++++++ .../AdaptionHints/DeviceAdaptionHintView.xaml | 79 ++++++++++ .../DeviceAdaptionHintViewModel.cs | 47 ++++++ .../KeyboardSectionAdaptionHintView.xaml | 53 +++++++ .../KeyboardSectionAdaptionHintViewModel.cs | 19 +++ .../Dialogs/LayerHintsDialogView.xaml | 112 ++++++++++++++ .../Dialogs/LayerHintsDialogViewModel.cs | 114 ++++++++++++++ .../ProfileTree/TreeItem/LayerView.xaml | 6 + .../ProfileTree/TreeItem/LayerViewModel.cs | 15 +- .../Device/Tabs/DevicePropertiesTabView.xaml | 38 ++++- .../Tabs/DevicePropertiesTabViewModel.cs | 61 +++++++- 34 files changed, 1295 insertions(+), 58 deletions(-) create mode 100644 src/Artemis.Core/Models/Profile/AdaptionHints/CategoryAdaptionHint.cs create mode 100644 src/Artemis.Core/Models/Profile/AdaptionHints/DeviceAdaptionHint.cs create mode 100644 src/Artemis.Core/Models/Profile/AdaptionHints/IAdaptionHint.cs create mode 100644 src/Artemis.Core/Models/Profile/AdaptionHints/KeyboardSectionAdaptionHint.cs create mode 100644 src/Artemis.Core/Models/Profile/LayerAdapter.cs create mode 100644 src/Artemis.Storage/Entities/Profile/AdaptionHints/CategoryAdaptionHintEntity.cs create mode 100644 src/Artemis.Storage/Entities/Profile/AdaptionHints/DeviceAdaptionHintEntity.cs create mode 100644 src/Artemis.Storage/Entities/Profile/AdaptionHints/IAdaptionHintEntity.cs create mode 100644 src/Artemis.Storage/Entities/Profile/AdaptionHints/KeyboardSectionAdaptionHintEntity.cs create mode 100644 src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/AdaptionHintViewModel.cs create mode 100644 src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/CategoryAdaptionHintView.xaml create mode 100644 src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/CategoryAdaptionHintViewModel.cs create mode 100644 src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/DeviceAdaptionHintView.xaml create mode 100644 src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/DeviceAdaptionHintViewModel.cs create mode 100644 src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/KeyboardSectionAdaptionHintView.xaml create mode 100644 src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/KeyboardSectionAdaptionHintViewModel.cs create mode 100644 src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/LayerHintsDialogView.xaml create mode 100644 src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/LayerHintsDialogViewModel.cs diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings index 00c64717e..abd028c5b 100644 --- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings +++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings @@ -1,5 +1,8 @@  True + True + True + True True True True diff --git a/src/Artemis.Core/Models/Profile/AdaptionHints/CategoryAdaptionHint.cs b/src/Artemis.Core/Models/Profile/AdaptionHints/CategoryAdaptionHint.cs new file mode 100644 index 000000000..1e31bb371 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/AdaptionHints/CategoryAdaptionHint.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using System.Linq; +using Artemis.Storage.Entities.Profile.AdaptionHints; + +namespace Artemis.Core +{ + /// + /// Represents a hint that adapts layers to a certain category of devices + /// + public class CategoryAdaptionHint : IAdaptionHint + { + public CategoryAdaptionHint() + { + } + + internal CategoryAdaptionHint(CategoryAdaptionHintEntity entity) + { + Category = (DeviceCategory) entity.Category; + Skip = entity.Skip; + LimitAmount = entity.LimitAmount; + Amount = entity.Amount; + } + + /// + /// Gets or sets the category of devices LEDs will be applied to + /// + public DeviceCategory Category { get; set; } + + /// + /// Gets or sets the amount of devices to skip + /// + public int Skip { get; set; } + + /// + /// Gets or sets a boolean indicating whether a limited amount of devices should be used + /// + public bool LimitAmount { get; set; } + + /// + /// Gets or sets the amount of devices to limit to if is + /// + public int Amount { get; set; } + + #region Implementation of IAdaptionHint + + /// + public void Apply(Layer layer, List devices) + { + IEnumerable matches = devices + .Where(d => d.Categories.Contains(Category)) + .OrderBy(d => d.Rectangle.Top) + .ThenBy(d => d.Rectangle.Left) + .Skip(Skip); + if (LimitAmount) + matches = matches.Take(Amount); + + foreach (ArtemisDevice artemisDevice in matches) + layer.AddLeds(artemisDevice.Leds); + } + + /// + public IAdaptionHintEntity GetEntry() + { + return new CategoryAdaptionHintEntity {Amount = Amount, LimitAmount = LimitAmount, Category = (int) Category, Skip = Skip}; + } + + #endregion + + /// + public override string ToString() + { + return $"Category adaption - {nameof(Category)}: {Category}, {nameof(Skip)}: {Skip}, {nameof(LimitAmount)}: {LimitAmount}, {nameof(Amount)}: {Amount}"; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/AdaptionHints/DeviceAdaptionHint.cs b/src/Artemis.Core/Models/Profile/AdaptionHints/DeviceAdaptionHint.cs new file mode 100644 index 000000000..2a831e820 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/AdaptionHints/DeviceAdaptionHint.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Linq; +using Artemis.Storage.Entities.Profile.AdaptionHints; +using RGB.NET.Core; + +namespace Artemis.Core +{ + /// + /// Represents a hint that adapts layers to a certain type of devices + /// + public class DeviceAdaptionHint : IAdaptionHint + { + public DeviceAdaptionHint() + { + } + + internal DeviceAdaptionHint(DeviceAdaptionHintEntity entity) + { + DeviceType = (RGBDeviceType) entity.DeviceType; + Skip = entity.Skip; + LimitAmount = entity.LimitAmount; + Amount = entity.Amount; + } + + /// + /// Gets or sets the type of devices LEDs will be applied to + /// + public RGBDeviceType DeviceType { get; set; } + + /// + /// Gets or sets the amount of devices to skip + /// + public int Skip { get; set; } + + /// + /// Gets or sets a boolean indicating whether a limited amount of devices should be used + /// + public bool LimitAmount { get; set; } + + /// + /// Gets or sets the amount of devices to limit to if is + /// + public int Amount { get; set; } + + #region Implementation of IAdaptionHint + + /// + public void Apply(Layer layer, List devices) + { + IEnumerable matches = devices + .Where(d => d.RgbDevice.DeviceInfo.DeviceType == DeviceType) + .OrderBy(d => d.Rectangle.Top) + .ThenBy(d => d.Rectangle.Left) + .Skip(Skip); + if (LimitAmount) + matches = matches.Take(Amount); + + foreach (ArtemisDevice artemisDevice in matches) + layer.AddLeds(artemisDevice.Leds); + } + + /// + public IAdaptionHintEntity GetEntry() + { + return new DeviceAdaptionHintEntity {Amount = Amount, LimitAmount = LimitAmount, DeviceType = (int) DeviceType, Skip = Skip}; + } + + /// + public override string ToString() + { + return $"Device adaption - {nameof(DeviceType)}: {DeviceType}, {nameof(Skip)}: {Skip}, {nameof(LimitAmount)}: {LimitAmount}, {nameof(Amount)}: {Amount}"; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/AdaptionHints/IAdaptionHint.cs b/src/Artemis.Core/Models/Profile/AdaptionHints/IAdaptionHint.cs new file mode 100644 index 000000000..48c3dc29a --- /dev/null +++ b/src/Artemis.Core/Models/Profile/AdaptionHints/IAdaptionHint.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using Artemis.Storage.Entities.Profile.AdaptionHints; + +namespace Artemis.Core +{ + /// + /// Represents an adaption hint that's used to adapt a layer to a set of devices + /// + public interface IAdaptionHint + { + /// + /// Applies the adaptive action to the provided layer + /// + /// The layer to adapt + /// The devices to adapt the layer for + void Apply(Layer layer, List devices); + + /// + /// Returns an adaption hint entry for this adaption hint used for persistent storage + /// + IAdaptionHintEntity GetEntry(); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/AdaptionHints/KeyboardSectionAdaptionHint.cs b/src/Artemis.Core/Models/Profile/AdaptionHints/KeyboardSectionAdaptionHint.cs new file mode 100644 index 000000000..e8c6f09ba --- /dev/null +++ b/src/Artemis.Core/Models/Profile/AdaptionHints/KeyboardSectionAdaptionHint.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Artemis.Storage.Entities.Profile.AdaptionHints; +using RGB.NET.Core; + +namespace Artemis.Core +{ + /// + /// Represents a hint that adapts layers to a certain region of keyboards + /// + public class KeyboardSectionAdaptionHint : IAdaptionHint + { + private static readonly Dictionary> RegionLedIds = new() + { + {KeyboardSection.MacroKeys, Enum.GetValues().Where(l => l >= LedId.Keyboard_Programmable1 && l <= LedId.Keyboard_Programmable32).ToList()}, + {KeyboardSection.LedStrips, Enum.GetValues().Where(l => l >= LedId.LedStripe1 && l <= LedId.LedStripe128).ToList()}, + {KeyboardSection.Extra, Enum.GetValues().Where(l => l >= LedId.Keyboard_Custom1 && l <= LedId.Keyboard_Custom64).ToList()} + }; + + public KeyboardSectionAdaptionHint() + { + } + + internal KeyboardSectionAdaptionHint(KeyboardSectionAdaptionHintEntity entity) + { + Section = (KeyboardSection) entity.Section; + } + + /// + /// Gets or sets the section this hint will apply LEDs to + /// + public KeyboardSection Section { get; set; } + + #region Implementation of IAdaptionHint + + /// + public void Apply(Layer layer, List devices) + { + // Only keyboards should have the LEDs we care about + foreach (ArtemisDevice keyboard in devices.Where(d => d.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Keyboard)) + { + List ledIds = RegionLedIds[Section]; + layer.AddLeds(keyboard.Leds.Where(l => ledIds.Contains(l.RgbLed.Id))); + } + } + + /// + public IAdaptionHintEntity GetEntry() + { + return new KeyboardSectionAdaptionHintEntity {Section = (int) Section}; + } + + /// + public override string ToString() + { + return $"Keyboard section adaption - {nameof(Section)}: {Section}"; + } + + #endregion + } + + /// + /// Represents a section of LEDs on a keyboard + /// + public enum KeyboardSection + { + /// + /// A region containing the macro keys of a keyboard + /// + MacroKeys, + + /// + /// A region containing the LED strips of a keyboard + /// + LedStrips, + + /// + /// A region containing extra non-standard LEDs of a keyboard + /// + Extra + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index c7a17bc46..2956469dd 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -7,6 +7,7 @@ using Artemis.Core.LayerBrushes; using Artemis.Core.LayerEffects; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile.Abstract; +using RGB.NET.Core; using SkiaSharp; namespace Artemis.Core @@ -42,6 +43,7 @@ namespace Artemis.Core _leds = new List(); + Adapter = new LayerAdapter(this); Initialize(); Parent.AddChild(this, 0); } @@ -65,6 +67,7 @@ namespace Artemis.Core _leds = new List(); Load(); + Adapter = new LayerAdapter(this); Initialize(); } @@ -121,6 +124,11 @@ namespace Artemis.Core /// public LayerEntity LayerEntity { get; internal set; } + /// + /// Gets the layer adapter that can be used to adapt this layer to a different set of devices + /// + public LayerAdapter Adapter { get; } + /// public override bool ShouldBeEnabled => !Suspended && DisplayConditionMet; @@ -147,7 +155,15 @@ namespace Artemis.Core return $"[Layer] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}"; } - #region IDisposable + /// + /// Occurs when a property affecting the rendering properties of this layer has been updated + /// + public event EventHandler? RenderPropertiesUpdated; + + /// + /// Occurs when the layer brush of this layer has been updated + /// + public event EventHandler? LayerBrushUpdated; /// protected override void Dispose(bool disposing) @@ -162,7 +178,10 @@ namespace Artemis.Core base.Dispose(disposing); } - #endregion + internal void OnLayerBrushUpdated() + { + LayerBrushUpdated?.Invoke(this, EventArgs.Empty); + } private void Initialize() { @@ -190,6 +209,28 @@ namespace Artemis.Core Reset(); } + private void LayerBrushStoreOnLayerBrushRemoved(object? sender, LayerBrushStoreEvent e) + { + if (LayerBrush?.Descriptor == e.Registration.LayerBrushDescriptor) + DeactivateLayerBrush(); + } + + private void LayerBrushStoreOnLayerBrushAdded(object? sender, LayerBrushStoreEvent e) + { + if (LayerBrush != null || !General.PropertiesInitialized) + return; + + LayerBrushReference? current = General.BrushReference.CurrentValue; + if (e.Registration.PluginFeature.Id == current?.LayerBrushProviderId && + e.Registration.LayerBrushDescriptor.LayerBrushType.Name == current.BrushType) + ActivateLayerBrush(); + } + + private void OnRenderPropertiesUpdated() + { + RenderPropertiesUpdated?.Invoke(this, EventArgs.Empty); + } + #region Storage internal override void Load() @@ -229,7 +270,8 @@ namespace Artemis.Core LedEntity ledEntity = new() { DeviceIdentifier = artemisLed.Device.Identifier, - LedName = artemisLed.RgbLed.Id.ToString() + LedName = artemisLed.RgbLed.Id.ToString(), + PhysicalLayout = artemisLed.Device.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Keyboard ? (int) artemisLed.Device.PhysicalLayout : null }; LayerEntity.Leds.Add(ledEntity); } @@ -575,7 +617,7 @@ namespace Artemis.Core if (Disposed) throw new ObjectDisposedException("Layer"); - _leds.AddRange(leds); + _leds.AddRange(leds.Except(_leds)); CalculateRenderProperties(); } @@ -692,51 +734,6 @@ namespace Artemis.Core } #endregion - - #region Event handlers - - private void LayerBrushStoreOnLayerBrushRemoved(object? sender, LayerBrushStoreEvent e) - { - if (LayerBrush?.Descriptor == e.Registration.LayerBrushDescriptor) - DeactivateLayerBrush(); - } - - private void LayerBrushStoreOnLayerBrushAdded(object? sender, LayerBrushStoreEvent e) - { - if (LayerBrush != null || !General.PropertiesInitialized) - return; - - LayerBrushReference? current = General.BrushReference.CurrentValue; - if (e.Registration.PluginFeature.Id == current?.LayerBrushProviderId && - e.Registration.LayerBrushDescriptor.LayerBrushType.Name == current.BrushType) - ActivateLayerBrush(); - } - - #endregion - - #region Events - - /// - /// Occurs when a property affecting the rendering properties of this layer has been updated - /// - public event EventHandler? RenderPropertiesUpdated; - - /// - /// Occurs when the layer brush of this layer has been updated - /// - public event EventHandler? LayerBrushUpdated; - - private void OnRenderPropertiesUpdated() - { - RenderPropertiesUpdated?.Invoke(this, EventArgs.Empty); - } - - internal void OnLayerBrushUpdated() - { - LayerBrushUpdated?.Invoke(this, EventArgs.Empty); - } - - #endregion } /// diff --git a/src/Artemis.Core/Models/Profile/LayerAdapter.cs b/src/Artemis.Core/Models/Profile/LayerAdapter.cs new file mode 100644 index 000000000..e2eb104ee --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerAdapter.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Artemis.Storage.Entities.Profile; +using Artemis.Storage.Entities.Profile.AdaptionHints; +using RGB.NET.Core; + +namespace Artemis.Core +{ + /// + /// Represents an adapter that adapts a layer to a certain set of devices using s + /// + public class LayerAdapter : IStorageModel + { + internal LayerAdapter(Layer layer) + { + Layer = layer; + AdaptionHints = new List(); + } + + /// + /// Gets the layer this adapter can adapt + /// + public Layer Layer { get; } + + /// + /// Gets or sets a list containing the adaption hints used by this adapter + /// + public List AdaptionHints { get; set; } + + /// + /// Modifies the layer, adapting it to the provided + /// + /// The devices to adapt the layer to + public void Adapt(List devices) + { + // Use adaption hints if provided + if (AdaptionHints.Any()) + { + foreach (IAdaptionHint adaptionHint in AdaptionHints) + adaptionHint.Apply(Layer, devices); + } + // If there are no hints, try to find matching LEDs anyway + else + { + List availableLeds = devices.SelectMany(d => d.Leds).ToList(); + List usedLeds = new(); + + foreach (LedEntity ledEntity in Layer.LayerEntity.Leds) + { + // TODO: If this is a keyboard LED and the layouts don't match, convert it before looking for it on the devices + + LedId ledId = Enum.Parse(ledEntity.LedName); + ArtemisLed? led = availableLeds.FirstOrDefault(l => l.RgbLed.Id == ledId); + + if (led != null) + { + availableLeds.Remove(led); + usedLeds.Add(led); + } + } + + Layer.AddLeds(usedLeds); + } + } + + /// + /// Automatically determine hints for this layer + /// + public List DetermineHints(IEnumerable devices) + { + List newHints = new(); + // Any fully covered device will add a device adaption hint for that type + foreach (IGrouping deviceLeds in Layer.Leds.GroupBy(l => l.Device)) + { + ArtemisDevice device = deviceLeds.Key; + // If there is already an adaption hint for this type, don't add another + if (AdaptionHints.Any(h => h is DeviceAdaptionHint d && d.DeviceType == device.RgbDevice.DeviceInfo.DeviceType)) + continue; + if (DoesLayerCoverDevice(device)) + { + DeviceAdaptionHint hint = new() {DeviceType = device.RgbDevice.DeviceInfo.DeviceType}; + AdaptionHints.Add(hint); + newHints.Add(hint); + } + } + + // Any fully covered category will add a category adaption hint for its category + foreach (DeviceCategory deviceCategory in Enum.GetValues()) + { + if (AdaptionHints.Any(h => h is CategoryAdaptionHint c && c.Category == deviceCategory)) + continue; + + List categoryDevices = devices.Where(d => d.Categories.Contains(deviceCategory)).ToList(); + if (categoryDevices.Any() && categoryDevices.All(DoesLayerCoverDevice)) + { + CategoryAdaptionHint hint = new() {Category = deviceCategory}; + AdaptionHints.Add(hint); + newHints.Add(hint); + } + } + + return newHints; + } + + private bool DoesLayerCoverDevice(ArtemisDevice device) + { + return device.Leds.All(l => Layer.Leds.Contains(l)); + } + + #region Implementation of IStorageModel + + /// + public void Load() + { + AdaptionHints.Clear(); + // Kind of meh. + // This leaves the adapter responsible for finding the right hint for the right entity, but it's gotta be done somewhere.. + foreach (IAdaptionHintEntity hintEntity in Layer.LayerEntity.AdaptionHints) + switch (hintEntity) + { + case DeviceAdaptionHintEntity entity: + AdaptionHints.Add(new DeviceAdaptionHint(entity)); + break; + case CategoryAdaptionHintEntity entity: + AdaptionHints.Add(new CategoryAdaptionHint(entity)); + break; + case KeyboardSectionAdaptionHintEntity entity: + AdaptionHints.Add(new KeyboardSectionAdaptionHint(entity)); + break; + } + } + + /// + public void Save() + { + Layer.LayerEntity.AdaptionHints.Clear(); + foreach (IAdaptionHint adaptionHint in AdaptionHints) + Layer.LayerEntity.AdaptionHints.Add(adaptionHint.GetEntry()); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs index ca1ae6094..1c0930793 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs @@ -36,10 +36,12 @@ namespace Artemis.Core InputIdentifiers = new List(); InputMappings = new Dictionary(); + Categories = new HashSet(); UpdateLeds(); ApplyKeyboardLayout(); ApplyToEntity(); + ApplyDefaultCategories(); CalculateRenderProperties(); } @@ -52,6 +54,7 @@ namespace Artemis.Core InputIdentifiers = new List(); InputMappings = new Dictionary(); + Categories = new HashSet(); foreach (DeviceInputIdentifierEntity identifierEntity in DeviceEntity.InputIdentifiers) InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(identifierEntity.InputProvider, identifierEntity.Identifier)); @@ -114,6 +117,11 @@ namespace Artemis.Core /// 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 /// @@ -383,6 +391,46 @@ namespace Artemis.Core 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 @@ -401,6 +449,10 @@ namespace Artemis.Core 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() @@ -419,6 +471,12 @@ namespace Artemis.Core 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(); @@ -472,4 +530,13 @@ namespace Artemis.Core LogicalLayout = DeviceEntity.LogicalLayout; } } + + public enum DeviceCategory + { + Desk, + Monitor, + Case, + Room, + Peripherals + } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/RgbService.cs b/src/Artemis.Core/Services/RgbService.cs index b794930af..f5bb3fb5b 100644 --- a/src/Artemis.Core/Services/RgbService.cs +++ b/src/Artemis.Core/Services/RgbService.cs @@ -314,6 +314,9 @@ namespace Artemis.Core.Services { SurfaceArrangement surfaceArrangement = SurfaceArrangement.GetDefaultArrangement(); surfaceArrangement.Arrange(_devices); + foreach (ArtemisDevice artemisDevice in _devices) + artemisDevice.ApplyDefaultCategories(); + SaveDevices(); } diff --git a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs index bd34ed32b..9a11fbf1c 100644 --- a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs +++ b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs @@ -121,5 +121,11 @@ namespace Artemis.Core.Services /// Text to add after the name of the profile (separated by a dash) /// ProfileDescriptor ImportProfile(string json, ProfileModule profileModule, string nameAffix = "imported"); + + /// + /// Adapts a given profile to the currently active devices + /// + /// The profile to adapt + void AdaptProfile(Profile profile); } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs index bdae901c8..22660a00c 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -122,7 +122,7 @@ namespace Artemis.Core.Services ProfileEntity entity = _profileRepository.Get(module.ActiveProfile.EntityId); Profile profile = new(module, entity); InstantiateProfile(profile); - + module.ChangeActiveProfile(null, _rgbService.EnabledDevices); module.ChangeActiveProfile(profile, _rgbService.EnabledDevices); } @@ -166,7 +166,6 @@ namespace Artemis.Core.Services } - public void ClearActiveProfile(ProfileModule module) { module.ChangeActiveProfile(null, _rgbService.EnabledDevices); @@ -294,13 +293,21 @@ namespace Artemis.Core.Services return new ProfileDescriptor(profileModule, profileEntity); } + /// + public void AdaptProfile(Profile profile) + { + List devices = _rgbService.EnabledDevices.ToList(); + foreach (Layer layer in profile.GetAllLayers()) + layer.Adapter.Adapt(devices); + } + #region Event handlers private void RgbServiceOnLedsChanged(object? sender, EventArgs e) { ActiveProfilesPopulateLeds(); } - + #endregion } } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/AdaptionHints/CategoryAdaptionHintEntity.cs b/src/Artemis.Storage/Entities/Profile/AdaptionHints/CategoryAdaptionHintEntity.cs new file mode 100644 index 000000000..bd5fb6f08 --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/AdaptionHints/CategoryAdaptionHintEntity.cs @@ -0,0 +1,13 @@ +namespace Artemis.Storage.Entities.Profile.AdaptionHints +{ + public class CategoryAdaptionHintEntity : IAdaptionHintEntity + { + public int Category { get; set; } + + public bool LimitAmount { get; set; } + public int Skip { get; set; } + public int Amount { get; set; } + } + + +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/AdaptionHints/DeviceAdaptionHintEntity.cs b/src/Artemis.Storage/Entities/Profile/AdaptionHints/DeviceAdaptionHintEntity.cs new file mode 100644 index 000000000..ec6912505 --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/AdaptionHints/DeviceAdaptionHintEntity.cs @@ -0,0 +1,11 @@ +namespace Artemis.Storage.Entities.Profile.AdaptionHints +{ + public class DeviceAdaptionHintEntity : IAdaptionHintEntity + { + public int DeviceType { get; set; } + + public bool LimitAmount { get; set; } + public int Skip { get; set; } + public int Amount { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/AdaptionHints/IAdaptionHintEntity.cs b/src/Artemis.Storage/Entities/Profile/AdaptionHints/IAdaptionHintEntity.cs new file mode 100644 index 000000000..9b81e630d --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/AdaptionHints/IAdaptionHintEntity.cs @@ -0,0 +1,6 @@ +namespace Artemis.Storage.Entities.Profile.AdaptionHints +{ + public interface IAdaptionHintEntity + { + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/AdaptionHints/KeyboardSectionAdaptionHintEntity.cs b/src/Artemis.Storage/Entities/Profile/AdaptionHints/KeyboardSectionAdaptionHintEntity.cs new file mode 100644 index 000000000..e5a2ddb64 --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/AdaptionHints/KeyboardSectionAdaptionHintEntity.cs @@ -0,0 +1,7 @@ +namespace Artemis.Storage.Entities.Profile.AdaptionHints +{ + public class KeyboardSectionAdaptionHintEntity : IAdaptionHintEntity + { + public int Section { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/LayerEntity.cs b/src/Artemis.Storage/Entities/Profile/LayerEntity.cs index 035e160ec..4be0f1d02 100644 --- a/src/Artemis.Storage/Entities/Profile/LayerEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/LayerEntity.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Artemis.Storage.Entities.Profile.Abstract; +using Artemis.Storage.Entities.Profile.AdaptionHints; using LiteDB; namespace Artemis.Storage.Entities.Profile @@ -10,6 +11,7 @@ namespace Artemis.Storage.Entities.Profile public LayerEntity() { Leds = new List(); + AdaptionHints = new List(); PropertyEntities = new List(); LayerEffects = new List(); ExpandedPropertyGroups = new List(); @@ -20,6 +22,7 @@ namespace Artemis.Storage.Entities.Profile public bool Suspended { get; set; } public List Leds { get; set; } + public List AdaptionHints { get; set; } [BsonRef("ProfileEntity")] public ProfileEntity Profile { get; set; } diff --git a/src/Artemis.Storage/Entities/Profile/LedEntity.cs b/src/Artemis.Storage/Entities/Profile/LedEntity.cs index 1b09e9111..248fbef45 100644 --- a/src/Artemis.Storage/Entities/Profile/LedEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/LedEntity.cs @@ -4,5 +4,7 @@ { public string LedName { get; set; } public string DeviceIdentifier { get; set; } + + public int? PhysicalLayout { get; set; } } } \ 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 bc1fb4e82..ecacd69dd 100644 --- a/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs +++ b/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs @@ -8,8 +8,9 @@ namespace Artemis.Storage.Entities.Surface { InputIdentifiers = new List(); InputMappings = new List(); + Categories = new List(); } - + public string Id { get; set; } public float X { get; set; } public float Y { get; set; } @@ -27,7 +28,7 @@ namespace Artemis.Storage.Entities.Surface public List InputIdentifiers { get; set; } public List InputMappings { get; set; } - + public List Categories { get; set; } } public class InputMappingEntity diff --git a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs index a5ed2e94f..2f8a72426 100644 --- a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -12,6 +12,8 @@ using Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDataBi using Artemis.UI.Screens.ProfileEditor.LayerProperties.LayerEffects; using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline; using Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree; +using Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs; +using Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints; using Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem; using Artemis.UI.Screens.ProfileEditor.Visualization; using Artemis.UI.Screens.ProfileEditor.Visualization.Tools; @@ -59,6 +61,14 @@ namespace Artemis.UI.Ninject.Factories LayerViewModel LayerViewModel(ProfileElement layer); } + public interface ILayerHintVmFactory : IVmFactory + { + LayerHintsDialogViewModel LayerHintsDialogViewModel(Layer layer); + CategoryAdaptionHintViewModel CategoryAdaptionHintViewModel(CategoryAdaptionHint adaptionHint); + DeviceAdaptionHintViewModel DeviceAdaptionHintViewModel(DeviceAdaptionHint adaptionHint); + KeyboardSectionAdaptionHintViewModel KeyboardSectionAdaptionHintViewModel(KeyboardSectionAdaptionHint adaptionHint); + } + public interface IProfileLayerVmFactory : IVmFactory { ProfileLayerViewModel Create(Layer layer, PanZoomViewModel panZoomViewModel); diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.xaml b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.xaml index 3a47534f6..24468acc2 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.xaml @@ -169,6 +169,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/CategoryAdaptionHintViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/CategoryAdaptionHintViewModel.cs new file mode 100644 index 000000000..b6ff0db12 --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/CategoryAdaptionHintViewModel.cs @@ -0,0 +1,46 @@ +using Artemis.Core; +using Artemis.UI.Shared; +using Stylet; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints +{ + public class CategoryAdaptionHintViewModel : AdaptionHintViewModel + { + private bool _takeAllDevices; + + /// + public CategoryAdaptionHintViewModel(CategoryAdaptionHint adaptionHint) : base(adaptionHint) + { + CategoryAdaptionHint = adaptionHint; + Categories = new BindableCollection(EnumUtilities.GetAllValuesAndDescriptions(typeof(DeviceCategory))); + } + + public CategoryAdaptionHint CategoryAdaptionHint { get; } + public BindableCollection Categories { get; } + + public bool TakeAllDevices + { + get => _takeAllDevices; + set => SetAndNotify(ref _takeAllDevices, value); + } + + #region Overrides of Screen + + /// + protected override void OnInitialActivate() + { + TakeAllDevices = !CategoryAdaptionHint.LimitAmount; + base.OnInitialActivate(); + } + + + /// + protected override void OnClose() + { + CategoryAdaptionHint.LimitAmount = !TakeAllDevices; + base.OnClose(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/DeviceAdaptionHintView.xaml b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/DeviceAdaptionHintView.xaml new file mode 100644 index 000000000..601bd6854 --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/DeviceAdaptionHintView.xaml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/DeviceAdaptionHintViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/DeviceAdaptionHintViewModel.cs new file mode 100644 index 000000000..bf307ac5c --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/DeviceAdaptionHintViewModel.cs @@ -0,0 +1,47 @@ +using Artemis.Core; +using Artemis.UI.Shared; +using RGB.NET.Core; +using Stylet; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints +{ + public class DeviceAdaptionHintViewModel : AdaptionHintViewModel + { + private bool _takeAllDevices; + + /// + public DeviceAdaptionHintViewModel(DeviceAdaptionHint adaptionHint) : base(adaptionHint) + { + DeviceAdaptionHint = adaptionHint; + DeviceTypes = new BindableCollection(EnumUtilities.GetAllValuesAndDescriptions(typeof(RGBDeviceType))); + } + + public DeviceAdaptionHint DeviceAdaptionHint { get; } + public BindableCollection DeviceTypes { get; } + + public bool TakeAllDevices + { + get => _takeAllDevices; + set => SetAndNotify(ref _takeAllDevices, value); + } + + #region Overrides of Screen + + /// + protected override void OnInitialActivate() + { + TakeAllDevices = !DeviceAdaptionHint.LimitAmount; + base.OnInitialActivate(); + } + + + /// + protected override void OnClose() + { + DeviceAdaptionHint.LimitAmount = !TakeAllDevices; + base.OnClose(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/KeyboardSectionAdaptionHintView.xaml b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/KeyboardSectionAdaptionHintView.xaml new file mode 100644 index 000000000..2d5a3ecb9 --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/KeyboardSectionAdaptionHintView.xaml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/KeyboardSectionAdaptionHintViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/KeyboardSectionAdaptionHintViewModel.cs new file mode 100644 index 000000000..1796757d9 --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/KeyboardSectionAdaptionHintViewModel.cs @@ -0,0 +1,19 @@ +using Artemis.Core; +using Artemis.UI.Shared; +using Stylet; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints +{ + public class KeyboardSectionAdaptionHintViewModel : AdaptionHintViewModel + { + /// + public KeyboardSectionAdaptionHintViewModel(KeyboardSectionAdaptionHint adaptionHint) : base(adaptionHint) + { + KeyboardSectionAdaptionHint = adaptionHint; + Sections = new BindableCollection(EnumUtilities.GetAllValuesAndDescriptions(typeof(KeyboardSection))); + } + + public KeyboardSectionAdaptionHint KeyboardSectionAdaptionHint { get; } + public BindableCollection Sections { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/LayerHintsDialogView.xaml b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/LayerHintsDialogView.xaml new file mode 100644 index 000000000..c20cb440c --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/LayerHintsDialogView.xaml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Introduction + + + In this window you can tell Artemis how this layer should be adapted when the profile is applied to a different set of devices by providing so-called adaption hints. + + When sharing your profile with other people good hints help them import your profile without the need for manual adjustments. + + + + Adaption hints + + + + + + + + + + + + + + + + + + + + + + + +