diff --git a/Brushes/ProfileBrush.cs b/Brushes/ProfileBrush.cs new file mode 100644 index 0000000..4008b2c --- /dev/null +++ b/Brushes/ProfileBrush.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Drawing; +using CUE.NET.Devices.Keyboard.Enums; +using CUE.NET.Devices.Keyboard.Keys; + +namespace CUE.NET.Brushes +{ + public class ProfileBrush : AbstractBrush + { + #region Properties & Fields + + private Dictionary _keyLights; + + #endregion + + #region Constructors + + internal ProfileBrush(Dictionary keyLights) + { + this._keyLights = keyLights; + } + + #endregion + + #region Methods + + public override Color GetColorAtPoint(RectangleF rectangle, PointF point) + { + CorsairKey key = CueSDK.KeyboardSDK[point]; + if (key == null) return Color.Transparent; + + Color color; + if (!_keyLights.TryGetValue(key.KeyId, out color)) + return Color.Transparent; + + return FinalizeColor(color); + } + + #endregion + } +} diff --git a/CUE.NET.csproj b/CUE.NET.csproj index 141a06d..299d8a2 100644 --- a/CUE.NET.csproj +++ b/CUE.NET.csproj @@ -45,6 +45,7 @@ + @@ -97,6 +98,10 @@ + + + + diff --git a/Examples/SimpleDevTest/Program.cs b/Examples/SimpleDevTest/Program.cs index 119a98c..ad9274c 100644 --- a/Examples/SimpleDevTest/Program.cs +++ b/Examples/SimpleDevTest/Program.cs @@ -11,6 +11,7 @@ using CUE.NET.Devices.Keyboard.Extensions; using CUE.NET.Devices.Keyboard.Keys; using CUE.NET.Exceptions; using CUE.NET.Gradients; +using CUE.NET.Profiles; namespace SimpleDevTest { @@ -38,6 +39,40 @@ namespace SimpleDevTest if (keyboard == null) throw new WrapperException("No keyboard found"); + keyboard.Brush = new SolidColorBrush(Color.Black); + keyboard.Update(); + + Wait(3); + + keyboard.Brush = CueProfiles.LoadProfileByID()[null]; + keyboard.Update(); + + Wait(3); + + // My Profile 'K95 RGB Default 2' is all black - this could lead to different behavior than cue has since transparent isn't black in CUE.NET + // To swap a profile like CUE does we would need to black out the keyboard before + // OR work with a key group containing all keys and leave the background black - this should be always the prefered solution + keyboard.Brush = new SolidColorBrush(Color.Black); + keyboard.Update(); + keyboard.Brush = CueProfiles.LoadProfileByID()["K95 RGB Default 2"]; + keyboard.Update(); + + Wait(3); + + return; + + ListKeyGroup keyGroup = new ListKeyGroup(keyboard, keyboard['R'].KeyId); + keyGroup.Brush = new SolidColorBrush(Color.White); + keyboard.Update(); + Wait(2); + keyGroup.RemoveKey(keyboard['R'].KeyId); + keyboard['R'].Led.Color = Color.Black; + keyGroup.AddKey(keyboard['T'].KeyId); + keyboard.Update(); + + Wait(10); + + return; // --------------------------------------------------------------------------- // First we'll look at some basic coloring diff --git a/Profiles/CueProfile.cs b/Profiles/CueProfile.cs new file mode 100644 index 0000000..c69b5cf --- /dev/null +++ b/Profiles/CueProfile.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using CUE.NET.Brushes; + +namespace CUE.NET.Profiles +{ + public class CueProfile + { + #region Properties & Fields + + public string Id { get; } + public string Name { get; } + + private Dictionary _devices; + + public IEnumerable Modes + { + get + { + string device = _devices.Keys.FirstOrDefault(); + + CueProfileDevice cpd; + return (device != null && _devices.TryGetValue(device, out cpd)) ? cpd.Modes : new string[0]; + } + } + + public ProfileBrush this[string mode] + { + get + { + string device = CueSDK.KeyboardSDK?.KeyboardDeviceInfo?.Model; + CueProfileDevice cpd; + return (device != null && _devices.TryGetValue(device, out cpd)) ? cpd[mode] : null; + } + } + + #endregion + + #region Constructors + + private CueProfile(string id, string name) + { + this.Id = id; + this.Name = name; + } + + #endregion + + #region Methods + + internal static CueProfile Load(string file) + { + // ReSharper disable PossibleNullReferenceException - Just let it fail - no need to check anything here ... + try + { + if (!File.Exists(file)) return null; + + XElement profileRoot = XDocument.Load(file).Root; + return new CueProfile(profileRoot.Element("id").Value, profileRoot.Element("name").Value) + { + _devices = profileRoot.Element("devices").Elements("device") + .Select(CueProfileDevice.Load) + .Where(x => x != null) + .ToDictionary(x => x.Name) + }; + } + catch // I have no idea how the factory pattern should handle such a case - time to read :p + { + return null; + } + // ReSharper restore PossibleNullReferenceException + } + + #endregion + } +} diff --git a/Profiles/CueProfileDevice.cs b/Profiles/CueProfileDevice.cs new file mode 100644 index 0000000..15102d6 --- /dev/null +++ b/Profiles/CueProfileDevice.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using CUE.NET.Brushes; + +namespace CUE.NET.Profiles +{ + internal class CueProfileDevice + { + #region Properties & Fields + + internal string Name { get; } + + internal IEnumerable Modes => _modes.Keys.ToList(); + + private Dictionary _modes; + + #endregion + + #region Brush Conversion + + internal ProfileBrush this[string mode] + { + get + { + if (mode == null) + mode = _modes.Keys.FirstOrDefault(); + + CueProfileMode cpm; + return (mode != null && _modes.TryGetValue(mode, out cpm)) ? cpm : null; + } + } + + #endregion + + #region Constructors + + private CueProfileDevice(string name) + { + this.Name = name; + } + + #endregion + + #region Methods + + internal static CueProfileDevice Load(XElement deviceRoot) + { + // ReSharper disable PossibleNullReferenceException - Just let it fail - no need to check anything here ... + try + { + if (deviceRoot == null) return null; + + return new CueProfileDevice(deviceRoot.Element("modelName").Value) + { + _modes = deviceRoot.Element("modes").Elements("mode") + .Select(CueProfileMode.Load) + .Where(x => x != null) + .ToDictionary(x => x.Name) + }; + } + catch // I have no idea how the factory pattern should handle such a case - time to read :p + { + return null; + } + // ReSharper restore PossibleNullReferenceException + } + + #endregion + } +} diff --git a/Profiles/CueProfileMode.cs b/Profiles/CueProfileMode.cs new file mode 100644 index 0000000..16bf9e8 --- /dev/null +++ b/Profiles/CueProfileMode.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Xml.Linq; +using CUE.NET.Brushes; +using CUE.NET.Devices.Keyboard.Enums; + +namespace CUE.NET.Profiles +{ + internal class CueProfileMode + { + #region Properties & Fields + + internal string Name { get; } + + private Dictionary _keyLights; + + #endregion + + #region Brush Conversion + + public static implicit operator ProfileBrush(CueProfileMode profile) + { + return profile != null ? new ProfileBrush(profile._keyLights) : null; + } + + #endregion + + #region Constructors + + private CueProfileMode(string name) + { + this.Name = name; + } + + #endregion + + #region Methods + + internal static CueProfileMode Load(XElement modeRoot) + { + // ReSharper disable PossibleNullReferenceException - Just let it fail - no need to check anything here ... + try + { + if (modeRoot == null) return null; + + return new CueProfileMode(modeRoot.Element("name").Value) + { + _keyLights = modeRoot.Element("lightBackgrounds").Element("keyBgLights").Elements("lightBackground") + .Select(x => + { + string name = x.Attribute("key").Value; + if (name.Length == 1 && char.IsDigit(name[0])) // Our enum names can't be digit only so we need to map them + name = 'D' + name; + + return new + { + key = (CorsairKeyboardKeyId)Enum.Parse(typeof(CorsairKeyboardKeyId), name), + color = ColorTranslator.FromHtml(x.Attribute("color").Value) + }; + }) + .ToDictionary(x => x.key, x => x.color) + }; + } + catch (Exception ex) // I have no idea how the factory pattern should handle such a case - time to read :p + { + return null; + } + // ReSharper restore PossibleNullReferenceException + } + + #endregion + } +} diff --git a/Profiles/CueProfiles.cs b/Profiles/CueProfiles.cs new file mode 100644 index 0000000..d35b9cd --- /dev/null +++ b/Profiles/CueProfiles.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; + +namespace CUE.NET.Profiles +{ + public static class CueProfiles + { + #region Constants + + private const string PROFILE_EXTENSION = ".prf"; + private static readonly string PROFILE_FOLDER = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Corsair", "HID", "Profiles"); + private static readonly string CONFIG_FILE = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Corsair", "HID", "config.cfg"); + + #endregion + + #region Properties & Fields + + private static Dictionary _profileNameMapping = new Dictionary(); + + public static List ProfileNames + { + get + { + LoadProfileNames(); + return _profileNameMapping.Keys.ToList(); + } + } + + public static List ProfileIds + { + get + { + LoadProfileNames(); + return _profileNameMapping.Values.ToList(); + } + } + + #endregion + + #region Methods + + public static CueProfile LoadProfileByName(string name = null) + { + string id = null; + if (name != null && !_profileNameMapping.TryGetValue(name, out id)) + { + LoadProfileNames(); // Reload and try again + if (!_profileNameMapping.TryGetValue(name, out id)) + return null; + } + + return LoadProfileByID(id); + } + + public static CueProfile LoadProfileByID(string id = null) + { + if (id == null) id = GetDefaultProfileId(); + return CueProfile.Load(Path.Combine(PROFILE_FOLDER, id + PROFILE_EXTENSION)); + } + + private static string GetDefaultProfileId() + { + try + { + return XDocument.Load(CONFIG_FILE).Root?.Elements("value").FirstOrDefault(x => string.Equals(x.Attribute("name")?.Value, "InitialProfile", StringComparison.OrdinalIgnoreCase))?.Value; + } + catch // This shouldn't happen but you never know ... + { + return null; + } + } + + private static void LoadProfileNames() + { + try + { + IEnumerable profileFiles = Directory.GetFiles(PROFILE_FOLDER).Where(x => x.EndsWith(PROFILE_EXTENSION)); + foreach (string profileFile in profileFiles) + { + XElement profileNode = XDocument.Load(profileFile).Root; + if (profileNode == null) continue; + + string name = profileNode.Element("name")?.Value; + string id = profileNode.Element("id")?.Value; + + if (!string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(id) && !_profileNameMapping.ContainsKey(name)) // I think duplicates are an error case + _profileNameMapping.Add(name, id); + } + } + catch // This shouldn't happen but you never know ... + { + _profileNameMapping.Clear(); + } + } + + #endregion + } +}