Browse Source

Added KeyboardShortcut

ManlyMarco 4 years ago
parent
commit
841cdd5b77

+ 2 - 0
BepInEx/BepInEx.csproj

@@ -75,8 +75,10 @@
     <Compile Include="Configuration\ConfigEntry.cs" />
     <Compile Include="Configuration\ConfigFile.cs" />
     <Compile Include="Configuration\ConfigWrapper.cs" />
+    <Compile Include="Configuration\KeyboardShortcut.cs" />
     <Compile Include="Configuration\SettingChangedEventArgs.cs" />
     <Compile Include="Configuration\TomlTypeConverter.cs" />
+    <Compile Include="Configuration\TypeConverter.cs" />
     <Compile Include="Contract\Attributes.cs" />
     <Compile Include="ConsoleUtil\ConsoleEncoding\ConsoleEncoding.Buffers.cs" />
     <Compile Include="ConsoleUtil\ConsoleEncoding\ConsoleEncoding.cs" />

+ 179 - 0
BepInEx/Configuration/KeyboardShortcut.cs

@@ -0,0 +1,179 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using BepInEx.Logging;
+using UnityEngine;
+using Logger = BepInEx.Logging.Logger;
+
+namespace BepInEx.Configuration
+{
+	/// <summary>
+	/// A keyboard shortcut that can be used in Update method to check if user presses a key combo. The shortcut is only
+	/// triggered when the user presses the exact combination. For example, <c>F + LeftCtrl</c> will trigger only if user 
+	/// presses and holds only LeftCtrl, and then presses F. If any other keys are pressed, the shortcut will not trigger.
+	/// 
+	/// Can be used as a value of a setting in <see cref="ConfigFile.Wrap{T}(ConfigDefinition,T,ConfigDescription)"/> 
+	/// to allow user to change this shortcut and have the changes saved.
+	/// 
+	/// How to use: Use <see cref="IsDown"/> in this class instead of <see cref="Input.GetKeyDown(KeyCode)"/> in the Update loop.
+	/// </summary>
+	public class KeyboardShortcut
+	{
+		static KeyboardShortcut()
+		{
+			TomlTypeConverter.AddConverter(
+				typeof(KeyboardShortcut),
+				new TypeConverter
+				{
+					ConvertToString = (o, type) => (o as KeyboardShortcut)?.Serialize(),
+					ConvertToObject = (s, type) => Deserialize(s)
+				});
+		}
+
+		/// <summary>
+		/// All KeyCode values that can be used in a keyboard shortcut.
+		/// </summary>
+		public static readonly IEnumerable<KeyCode> AllKeyCodes = (KeyCode[])Enum.GetValues(typeof(KeyCode));
+
+		private readonly KeyCode[] _allKeys;
+		private readonly HashSet<KeyCode> _allKeysLookup;
+
+		/// <summary>
+		/// Create a new keyboard shortcut.
+		/// </summary>
+		/// <param name="mainKey">Main key to press</param>
+		/// <param name="modifiers">Keys that should be held down before main key is registered</param>
+		public KeyboardShortcut(KeyCode mainKey, params KeyCode[] modifiers) : this(new[] { mainKey }.Concat(modifiers).ToArray())
+		{
+			if (mainKey == KeyCode.None && modifiers.Any())
+				throw new ArgumentException($"Can't set {nameof(mainKey)} to KeyCode.None if there are any {nameof(modifiers)}");
+		}
+
+		private KeyboardShortcut(KeyCode[] keys)
+		{
+			_allKeys = SanitizeKeys(keys);
+			_allKeysLookup = new HashSet<KeyCode>(_allKeys);
+		}
+
+		public KeyboardShortcut()
+		{
+			_allKeys = SanitizeKeys();
+			_allKeysLookup = new HashSet<KeyCode>(_allKeys);
+		}
+
+		private static KeyCode[] SanitizeKeys(params KeyCode[] keys)
+		{
+			if (keys.Length == 0 || keys[0] == KeyCode.None)
+				return new[] { KeyCode.None };
+
+			return new[] { keys[0] }.Concat(keys.Skip(1).Distinct().Where(x => x != keys[0]).OrderBy(x => (int)x)).ToArray();
+		}
+
+		/// <summary>
+		/// Main key of the key combination. It has to be pressed / let go last for the combination to be triggered.
+		/// If the combination is empty, <see cref="KeyCode.None"/> is returned.
+		/// </summary>
+		public KeyCode MainKey => _allKeys.Length > 0 ? _allKeys[0] : KeyCode.None;
+
+		/// <summary>
+		/// Modifiers of the key combination, if any.
+		/// </summary>
+		public IEnumerable<KeyCode> Modifiers => _allKeys.Skip(1);
+
+		/// <summary>
+		/// Attempt to deserialize key combination from the string.
+		/// </summary>
+		public static KeyboardShortcut Deserialize(string str)
+		{
+			try
+			{
+				var parts = str.Split(new[] { ' ', '+', ',', ';', '|' }, StringSplitOptions.RemoveEmptyEntries)
+							   .Select(x => (KeyCode)Enum.Parse(typeof(KeyCode), x)).ToArray();
+				return new KeyboardShortcut(parts);
+			}
+			catch (SystemException ex)
+			{
+				Logger.Log(LogLevel.Error, "Failed to read keybind from settings: " + ex.Message);
+				return new KeyboardShortcut();
+			}
+		}
+
+		/// <summary>
+		/// Serialize the key combination into a user readable string.
+		/// </summary>
+		public string Serialize()
+		{
+			return string.Join(" + ", _allKeys.Select(x => x.ToString()).ToArray());
+		}
+
+		/// <summary>
+		/// Check if the main key was just pressed (Input.GetKeyDown), and specified modifier keys are all pressed
+		/// </summary>
+		public bool IsDown()
+		{
+			var mainKey = MainKey;
+			if (mainKey == KeyCode.None) return false;
+
+			return Input.GetKeyDown(mainKey) && ModifierKeyTest();
+		}
+
+		/// <summary>
+		/// Check if the main key is currently held down (Input.GetKey), and specified modifier keys are all pressed
+		/// </summary>
+		public bool IsPressed()
+		{
+			var mainKey = MainKey;
+			if (mainKey == KeyCode.None) return false;
+
+			return Input.GetKey(mainKey) && ModifierKeyTest();
+		}
+
+		/// <summary>
+		/// Check if the main key was just lifted (Input.GetKeyUp), and specified modifier keys are all pressed.
+		/// </summary>
+		public bool IsUp()
+		{
+			var mainKey = MainKey;
+			if (mainKey == KeyCode.None) return false;
+
+			return Input.GetKeyUp(mainKey) && ModifierKeyTest();
+		}
+
+		private bool ModifierKeyTest()
+		{
+			return AllKeyCodes.All(c =>
+			{
+				if (_allKeysLookup.Contains(c))
+				{
+					if (_allKeys[0] == c)
+						return true;
+					return Input.GetKey(c);
+				}
+				return !Input.GetKey(c);
+			});
+		}
+
+		/// <inheritdoc />
+		public override string ToString()
+		{
+			if (MainKey == KeyCode.None) return "Not set";
+
+			return string.Join(" + ", _allKeys.Select(c => c.ToString()).ToArray());
+		}
+
+		/// <inheritdoc />
+		public override bool Equals(object obj)
+		{
+			return obj is KeyboardShortcut shortcut && _allKeys.SequenceEqual(shortcut._allKeys);
+		}
+
+		/// <inheritdoc />
+		public override int GetHashCode()
+		{
+			var hc = _allKeys.Length;
+			for (var i = 0; i < _allKeys.Length; i++)
+				hc = unchecked(hc * 31 + (int)_allKeys[i]);
+			return hc;
+		}
+	}
+}

+ 22 - 11
BepInEx/Configuration/TomlTypeConverter.cs

@@ -4,15 +4,12 @@ using System.Globalization;
 
 namespace BepInEx.Configuration
 {
-	internal class TypeConverter
+	/// <summary>
+	/// Serializer/deserializer used by the config system.
+	/// </summary>
+	public static class TomlTypeConverter
 	{
-		public Func<object, Type, string> ConvertToString { get; set; }
-		public Func<string, Type, object> ConvertToObject { get; set; }
-	}
-
-	internal static class TomlTypeConverter
-	{
-		public static Dictionary<Type, TypeConverter> TypeConverters { get; } = new Dictionary<Type, TypeConverter>
+		private static Dictionary<Type, TypeConverter> TypeConverters { get; } = new Dictionary<Type, TypeConverter>
 		{
 			[typeof(string)] = new TypeConverter
 			{
@@ -91,6 +88,8 @@ namespace BepInEx.Configuration
 				ConvertToObject = (str, type) => decimal.Parse(str, NumberFormatInfo.InvariantInfo),
 			},
 
+			//enums are special
+
 			[typeof(Enum)] = new TypeConverter
 			{
 				ConvertToString = (obj, type) => obj.ToString(),
@@ -116,17 +115,29 @@ namespace BepInEx.Configuration
 		{
 			var conv = GetConverter(valueType);
 			if (conv == null)
-				throw new InvalidOperationException($"Cannot convert to type {valueType}");
+				throw new InvalidOperationException($"Cannot convert to type {valueType.Name}");
 
 			return conv.ConvertToObject(value, valueType);
 		}
 
-		private static TypeConverter GetConverter(Type valueType)
+		public static TypeConverter GetConverter(Type valueType)
 		{
+			if (valueType == null) throw new ArgumentNullException(nameof(valueType));
+
 			if (valueType.IsEnum)
 				return TypeConverters[typeof(Enum)];
 
-			return TypeConverters[valueType];
+			TypeConverters.TryGetValue(valueType, out var result);
+			return result;
+		}
+
+		public static void AddConverter(Type type, TypeConverter converter)
+		{
+			if (type == null) throw new ArgumentNullException(nameof(type));
+			if (converter == null) throw new ArgumentNullException(nameof(converter));
+			if (CanConvert(type)) throw new ArgumentException("The specified type already has a converter assigned to it", nameof(type));
+
+			TypeConverters.Add(type, converter);
 		}
 
 		public static bool CanConvert(Type type)

+ 22 - 0
BepInEx/Configuration/TypeConverter.cs

@@ -0,0 +1,22 @@
+using System;
+
+namespace BepInEx.Configuration
+{
+	/// <summary>
+	/// A serializer/deserializer combo for some type(s). Used by the config system.
+	/// </summary>
+	public class TypeConverter
+	{
+		/// <summary>
+		/// Used to serialize the type into a (hopefully) human-readable string.
+		/// Object is the instance to serialize, Type is the object's type.
+		/// </summary>
+		public Func<object, Type, string> ConvertToString { get; set; }
+
+		/// <summary>
+		/// Used to deserialize the type from a string.
+		/// String is the data to deserialize, Type is the object's type, should return instance to an object of Type.
+		/// </summary>
+		public Func<string, Type, object> ConvertToObject { get; set; }
+	}
+}

+ 18 - 0
BepInExTests/Configuration/ConfigFileTests.cs

@@ -4,6 +4,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
 using System.IO;
 using System.Linq;
 using System.Threading;
+using UnityEngine;
 
 namespace BepInEx.Configuration.Tests
 {
@@ -235,5 +236,22 @@ namespace BepInEx.Configuration.Tests
 			w.Value = null;
 			Assert.AreEqual("lel", w.Value);
 		}
+
+		[TestMethod]
+		public void KeyShortcutTest()
+		{
+			var shortcut = new KeyboardShortcut(KeyCode.H, KeyCode.O, KeyCode.R, KeyCode.S, KeyCode.E, KeyCode.Y);
+			var s = shortcut.Serialize();
+			var d = KeyboardShortcut.Deserialize(s);
+			Assert.AreEqual(shortcut, d);
+
+			var c = MakeConfig();
+			var w = c.Wrap("Cat", "Key", new KeyboardShortcut(KeyCode.A, KeyCode.LeftShift));
+			Assert.AreEqual(new KeyboardShortcut(KeyCode.A, KeyCode.LeftShift), w.Value);
+
+			w.Value = shortcut;
+			c.Reload();
+			Assert.AreEqual(shortcut, w.Value);
+		}
 	}
 }