From c66c21152f6d3af4b44666dfc7b5e27ad19e1ce7 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 10 Apr 2021 12:52:43 +0200 Subject: [PATCH] Device dialog - Added input remapping for keyboards Device dialog - Display layout author, if I missed anyone let me know! --- .../Models/Surface/ArtemisDevice.cs | 48 ++++++-- .../Surface/ArtemisDeviceInputIdentifier.cs | 15 ++- .../Services/Input/InputKeyLedIdMap.cs | 15 ++- .../Services/Input/InputService.cs | 2 +- .../Entities/Surface/DeviceEntity.cs | 8 ++ .../Ninject/Factories/IVMFactory.cs | 1 + .../Tree/TreeGroupViewModel.cs | 6 + .../Settings/Device/DeviceDialogView.xaml | 11 ++ .../Settings/Device/DeviceDialogViewModel.cs | 7 +- .../Device/Tabs/InputMappingsTabView.xaml | 83 ++++++++++++++ .../Device/Tabs/InputMappingsTabViewModel.cs | 103 ++++++++++++++++++ .../Tabs/General/GeneralSettingsTabView.xaml | 41 +++---- 12 files changed, 302 insertions(+), 38 deletions(-) create mode 100644 src/Artemis.UI/Screens/Settings/Device/Tabs/InputMappingsTabView.xaml create mode 100644 src/Artemis.UI/Screens/Settings/Device/Tabs/InputMappingsTabViewModel.cs diff --git a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs index 0baf7d834..a8ae3a17d 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs @@ -35,10 +35,9 @@ namespace Artemis.Core IsEnabled = true; InputIdentifiers = new List(); + InputMappings = new Dictionary(); - Leds = rgbDevice.Select(l => new ArtemisLed(l, this)).ToList().AsReadOnly(); - LedIds = new ReadOnlyDictionary(Leds.ToDictionary(l => l.RgbLed.Id, l => l)); - + UpdateLeds(); ApplyKeyboardLayout(); ApplyToEntity(); CalculateRenderProperties(); @@ -52,12 +51,12 @@ namespace Artemis.Core DeviceProvider = deviceProvider; InputIdentifiers = new List(); + InputMappings = new Dictionary(); + foreach (DeviceInputIdentifierEntity identifierEntity in DeviceEntity.InputIdentifiers) InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(identifierEntity.InputProvider, identifierEntity.Identifier)); - Leds = rgbDevice.Select(l => new ArtemisLed(l, this)).ToList().AsReadOnly(); - LedIds = new ReadOnlyDictionary(Leds.ToDictionary(l => l.RgbLed.Id, l => l)); - + UpdateLeds(); ApplyKeyboardLayout(); } @@ -110,6 +109,8 @@ namespace Artemis.Core /// public List InputIdentifiers { get; } + public Dictionary InputMappings { get; } + /// /// Gets or sets the X-position of the device /// @@ -287,20 +288,27 @@ namespace Artemis.Core /// 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) + public ArtemisLed? GetLed(Led led, bool applyInputMapping) { - return GetLed(led.Id); + 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) + 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; } @@ -339,8 +347,7 @@ namespace Artemis.Core 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)); + UpdateLeds(); Layout = layout; Layout.ApplyDevice(this); @@ -348,6 +355,21 @@ namespace Artemis.Core OnDeviceUpdated(); } + 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); + } + } + internal void ApplyToEntity() { // Other properties are computed @@ -362,6 +384,10 @@ namespace Artemis.Core 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}); } internal void ApplyToRgbDevice() diff --git a/src/Artemis.Core/Models/Surface/ArtemisDeviceInputIdentifier.cs b/src/Artemis.Core/Models/Surface/ArtemisDeviceInputIdentifier.cs index 6af52fccb..3eab18dff 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisDeviceInputIdentifier.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisDeviceInputIdentifier.cs @@ -1,4 +1,5 @@ using Artemis.Core.Services; +using RGB.NET.Core; namespace Artemis.Core { @@ -12,7 +13,7 @@ namespace Artemis.Core /// /// The full type and namespace of the this identifier is used by /// A value used to identify the device - public ArtemisDeviceInputIdentifier(string inputProvider, object identifier) + internal ArtemisDeviceInputIdentifier(string inputProvider, object identifier) { InputProvider = inputProvider; Identifier = identifier; @@ -28,4 +29,16 @@ namespace Artemis.Core /// public object Identifier { get; set; } } + + public class ArtemisDeviceInputMapping + { + public ArtemisLed OriginalLed { get; } + public ArtemisLed MappedLed { get; } + + internal ArtemisDeviceInputMapping(ArtemisLed originalLed, ArtemisLed mappedLed) + { + OriginalLed = originalLed; + MappedLed = mappedLed; + } + } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Input/InputKeyLedIdMap.cs b/src/Artemis.Core/Services/Input/InputKeyLedIdMap.cs index ad67b1a92..f2881c804 100644 --- a/src/Artemis.Core/Services/Input/InputKeyLedIdMap.cs +++ b/src/Artemis.Core/Services/Input/InputKeyLedIdMap.cs @@ -3,9 +3,15 @@ using RGB.NET.Core; namespace Artemis.Core.Services { - internal static class InputKeyUtilities + /// + /// Utilities for mapping keys and buttons to LEDs + /// + public static class InputKeyUtilities { - internal static readonly Dictionary KeyboardKeyLedIdMap = new() + /// + /// A dictionary of mappings between and + /// + public static readonly Dictionary KeyboardKeyLedIdMap = new() { {KeyboardKey.None, LedId.Keyboard_Custom1}, {KeyboardKey.Cancel, LedId.Keyboard_Custom2}, @@ -182,7 +188,10 @@ namespace Artemis.Core.Services {KeyboardKey.NumPadEnter, LedId.Keyboard_NumEnter} }; - internal static readonly Dictionary MouseButtonLedIdMap = new() + /// + /// A dictionary of mappings between and + /// + public static readonly Dictionary MouseButtonLedIdMap = new() { {MouseButton.Left, LedId.Mouse1}, {MouseButton.Middle, LedId.Mouse2}, diff --git a/src/Artemis.Core/Services/Input/InputService.cs b/src/Artemis.Core/Services/Input/InputService.cs index 6ca8aa735..bed2d47de 100644 --- a/src/Artemis.Core/Services/Input/InputService.cs +++ b/src/Artemis.Core/Services/Input/InputService.cs @@ -201,7 +201,7 @@ namespace Artemis.Core.Services bool foundLedId = InputKeyUtilities.KeyboardKeyLedIdMap.TryGetValue(e.Key, out LedId ledId); ArtemisLed? led = null; if (foundLedId && e.Device != null) - led = e.Device.GetLed(ledId); + led = e.Device.GetLed(ledId, true); // Create the UpDown event args because it can be used for every event ArtemisKeyboardKeyUpDownEventArgs eventArgs = new(e.Device, led, e.Key, keyboardModifierKey, e.IsDown); diff --git a/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs b/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs index 258902d5e..bc1fb4e82 100644 --- a/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs +++ b/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs @@ -7,6 +7,7 @@ namespace Artemis.Storage.Entities.Surface public DeviceEntity() { InputIdentifiers = new List(); + InputMappings = new List(); } public string Id { get; set; } @@ -25,9 +26,16 @@ namespace Artemis.Storage.Entities.Surface public string CustomLayoutPath { get; set; } public List InputIdentifiers { get; set; } + public List InputMappings { get; set; } } + public class InputMappingEntity + { + public int OriginalLedId { get; set; } + public int MappedLedId { get; set; } + } + public class DeviceInputIdentifierEntity { public string InputProvider { get; set; } diff --git a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs index 02d543a4c..e24134dce 100644 --- a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -49,6 +49,7 @@ namespace Artemis.UI.Ninject.Factories DevicePropertiesTabViewModel DevicePropertiesTabViewModel(ArtemisDevice device); DeviceInfoTabViewModel DeviceInfoTabViewModel(ArtemisDevice device); DeviceLedsTabViewModel DeviceLedsTabViewModel(ArtemisDevice device); + InputMappingsTabViewModel InputMappingsTabViewModel(ArtemisDevice device); } public interface IProfileTreeVmFactory : IVmFactory diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreeGroupViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreeGroupViewModel.cs index eb77d5816..580071f43 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreeGroupViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreeGroupViewModel.cs @@ -77,6 +77,9 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree _layerBrushSettingsWindowVm = new LayerBrushSettingsWindowViewModel(viewModel, configurationViewModel); _windowManager.ShowDialog(_layerBrushSettingsWindowVm); + + // Save changes after the dialog closes + _profileEditorService.UpdateSelectedProfile(); } catch (Exception e) { @@ -104,6 +107,9 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree _layerEffectSettingsWindowVm = new LayerEffectSettingsWindowViewModel(viewModel, configurationViewModel); _windowManager.ShowDialog(_layerEffectSettingsWindowVm); + + // Save changes after the dialog closes + _profileEditorService.UpdateSelectedProfile(); } catch (Exception e) { diff --git a/src/Artemis.UI/Screens/Settings/Device/DeviceDialogView.xaml b/src/Artemis.UI/Screens/Settings/Device/DeviceDialogView.xaml index 8134150e7..fe4ab6371 100644 --- a/src/Artemis.UI/Screens/Settings/Device/DeviceDialogView.xaml +++ b/src/Artemis.UI/Screens/Settings/Device/DeviceDialogView.xaml @@ -21,6 +21,9 @@ d:DesignHeight="800" d:DesignWidth="800" d:DataContext="{d:DesignInstance device:DeviceDialogViewModel}" Icon="/Resources/Images/Logo/logo-512.png"> + + + @@ -111,6 +114,14 @@ VerticalAlignment="Center" ShowColors="True" LedClicked="{s:Action OnLedClicked}"/> + + + Device layout by + diff --git a/src/Artemis.UI/Screens/Settings/Device/DeviceDialogViewModel.cs b/src/Artemis.UI/Screens/Settings/Device/DeviceDialogViewModel.cs index be35288c6..62ec00072 100644 --- a/src/Artemis.UI/Screens/Settings/Device/DeviceDialogViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Device/DeviceDialogViewModel.cs @@ -12,6 +12,7 @@ using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using MaterialDesignThemes.Wpf; using Ookii.Dialogs.Wpf; +using RGB.NET.Core; using RGB.NET.Layout; using SkiaSharp; using Stylet; @@ -42,14 +43,16 @@ namespace Artemis.UI.Screens.Settings.Device Device = device; PanZoomViewModel = new PanZoomViewModel(); + SelectedLeds = new BindableCollection(); Items.Add(factory.DevicePropertiesTabViewModel(device)); + if (device.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Keyboard) + Items.Add(factory.InputMappingsTabViewModel(device)); Items.Add(factory.DeviceInfoTabViewModel(device)); Items.Add(factory.DeviceLedsTabViewModel(device)); + ActiveItem = Items.First(); DisplayName = $"{device.RgbDevice.DeviceInfo.Model} | Artemis"; - - SelectedLeds = new BindableCollection(); } public ArtemisDevice Device { get; } diff --git a/src/Artemis.UI/Screens/Settings/Device/Tabs/InputMappingsTabView.xaml b/src/Artemis.UI/Screens/Settings/Device/Tabs/InputMappingsTabView.xaml new file mode 100644 index 000000000..d662eb770 --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Device/Tabs/InputMappingsTabView.xaml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + In some cases you may want Artemis to map key presses to different LEDs. + This is useful when your logical layout swaps keys around (like Hungarian layouts where the Z and Y keys are swapped). + In this tab you can set up these custom input mappings, simply click on a LED and press a key, Artemis will from then on consider that LED pressed whenever you press the same key again. + + + + + Select a LED in the preview on the left side to get started... + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Device/Tabs/InputMappingsTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Device/Tabs/InputMappingsTabViewModel.cs new file mode 100644 index 000000000..9ffbe769b --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Device/Tabs/InputMappingsTabViewModel.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Specialized; +using System.Linq; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Exceptions; +using RGB.NET.Core; +using Stylet; + +namespace Artemis.UI.Screens.Settings.Device.Tabs +{ + public class InputMappingsTabViewModel : Screen + { + private readonly IRgbService _rgbService; + private readonly IInputService _inputService; + private ArtemisLed _selectedLed; + + public InputMappingsTabViewModel(ArtemisDevice device, IRgbService rgbService, IInputService inputService) + { + if (device.RgbDevice.DeviceInfo.DeviceType != RGBDeviceType.Keyboard) + throw new ArtemisUIException("The input mappings tab only supports keyboards"); + _rgbService = rgbService; + _inputService = inputService; + + Device = device; + DisplayName = "INPUT MAPPINGS"; + InputMappings = new BindableCollection>(); + } + + public ArtemisDevice Device { get; } + + public ArtemisLed SelectedLed + { + get => _selectedLed; + set => SetAndNotify(ref _selectedLed, value); + } + + public BindableCollection> InputMappings { get; } + + public void DeleteMapping(Tuple inputMapping) + { + Device.InputMappings.Remove(inputMapping.Item1); + UpdateInputMappings(); + } + + private void InputServiceOnKeyboardKeyUp(object sender, ArtemisKeyboardKeyEventArgs e) + { + if (SelectedLed == null || e.Led == null) + return; + + // Locate the original LED the same way the InputService did it, but supply false to Device.GetLed + bool foundLedId = InputKeyUtilities.KeyboardKeyLedIdMap.TryGetValue(e.Key, out LedId ledId); + if (!foundLedId) + return; + ArtemisLed artemisLed = Device.GetLed(ledId, false); + if (artemisLed == null) + return; + + // Apply the new LED mapping + Device.InputMappings[SelectedLed] = artemisLed; + _rgbService.SaveDevice(Device); + ((DeviceDialogViewModel) Parent).SelectedLeds.Clear(); + + UpdateInputMappings(); + } + + private void UpdateInputMappings() + { + if (InputMappings.Any()) + InputMappings.Clear(); + + InputMappings.AddRange(Device.InputMappings.Select(m => new Tuple(m.Key, m.Value))); + } + + private void SelectedLedsOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + SelectedLed = ((DeviceDialogViewModel) Parent).SelectedLeds.FirstOrDefault(); + } + + #region Overrides of Screen + + /// + protected override void OnActivate() + { + UpdateInputMappings(); + _inputService.KeyboardKeyUp += InputServiceOnKeyboardKeyUp; + ((DeviceDialogViewModel) Parent).SelectedLeds.CollectionChanged += SelectedLedsOnCollectionChanged; + + base.OnActivate(); + } + + /// + protected override void OnDeactivate() + { + InputMappings.Clear(); + _inputService.KeyboardKeyUp -= InputServiceOnKeyboardKeyUp; + ((DeviceDialogViewModel) Parent).SelectedLeds.CollectionChanged -= SelectedLedsOnCollectionChanged; + base.OnDeactivate(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml index 4401bf050..7153e747c 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml @@ -72,17 +72,17 @@ Startup delay - Set the amount of seconds to wait before running Artemis with Windows. + Set the amount of seconds to wait before running Artemis with Windows. If some devices don't work because Artemis starts before the manufacturer's software, try increasing this value. + materialDesign:HintAssist.IsFloating="false" /> @@ -103,13 +103,13 @@ - + SelectedValue="{Binding SelectedColorScheme}" + ItemsSource="{Binding ColorSchemes}" + SelectedValuePath="Value" + DisplayMemberPath="Description" + materialDesign:HintAssist.IsFloating="false" /> @@ -130,13 +130,13 @@ - + materialDesign:HintAssist.IsFloating="false" /> @@ -187,7 +187,7 @@ + materialDesign:HintAssist.IsFloating="false" /> @@ -343,7 +343,7 @@ Software Vulkan @@ -364,16 +364,17 @@ Render scale - Sets the resolution Artemis renders at, higher scale means more CPU-usage, especially on large surfaces. + Sets the resolution Artemis renders at, higher scale means more CPU-usage, especially on large surfaces. + A scale of 25% may be required for very large surfaces but could cause LED dimming or bleeding. + materialDesign:HintAssist.IsFloating="false" /> @@ -399,7 +400,7 @@ SelectedItem="{Binding SelectedTargetFrameRate}" ItemsSource="{Binding TargetFrameRates}" DisplayMemberPath="Item1" - materialDesign:HintAssist.IsFloating="false"/> + materialDesign:HintAssist.IsFloating="false" />