From a9ab2531fc78c606126f496ec0bcfcec7042fa7c Mon Sep 17 00:00:00 2001 From: Clive Galway Date: Mon, 17 Jan 2022 17:23:33 +0000 Subject: [PATCH 01/33] Update KeyCode notes.xlsx --- C#/AutoHotInterception/KeyCode notes.xlsx | Bin 12542 -> 12583 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/C#/AutoHotInterception/KeyCode notes.xlsx b/C#/AutoHotInterception/KeyCode notes.xlsx index 749caa007cc607ff0538119dfed51f623702fa81..21bc857500cd125b0fd1df59556fe6e636c62e01 100644 GIT binary patch delta 4257 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 Date: Mon, 17 Jan 2022 18:11:41 +0000 Subject: [PATCH 02/33] Add ScanCodeHelper class --- .../AutoHotInterception.csproj | 1 + .../Helpers/ScanCodeHelper.cs | 86 +++++++++++++++++++ C#/TestApp/Program.cs | 4 +- 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 C#/AutoHotInterception/Helpers/ScanCodeHelper.cs diff --git a/C#/AutoHotInterception/AutoHotInterception.csproj b/C#/AutoHotInterception/AutoHotInterception.csproj index 4c2b71c..8a2a4b5 100644 --- a/C#/AutoHotInterception/AutoHotInterception.csproj +++ b/C#/AutoHotInterception/AutoHotInterception.csproj @@ -46,6 +46,7 @@ + diff --git a/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs b/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs new file mode 100644 index 0000000..1970e65 --- /dev/null +++ b/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using static AutoHotInterception.Helpers.ManagedWrapper; + +/* +AHK uses a single ScanCode (0...512) to identify each key, whereas Interception uses two codes - the ScanCode, plus the state. +For example: +in Interception - Code 29 with State 0 or 1 is LCtrl, and 29 with state 2/3 (With state of 2/3 meaning "Extended") is RCtrl +In AHK, code 29 is LCtrl, whereas state 285 (29 + 256) - ie "Extended" versions of keys are Interception code + 256 + +Furthermore, for some keys (eg Arrow keys and the 6 keys above the arrow key block - Ins, Del etc), Interception will sometimes receive TWO strokes... +... when one of these keys are pressed - a modifier key (Ctrl or Shift) and the key itself... + +Also, AHK assigns "high" (+256) codes to some keys, even when state is 0/1... +... And also assigns a "low" code to Pause, even though it is wrapped in an Extended LCtrl + +The purpose of this class is to encapsulate the logic required to deal with this scenario, and assign one code to any key, as AHK does +*/ +namespace AutoHotInterception.Helpers +{ + class TranslatedKey + { + public ushort AhkCode { get; set; } + //public List Strokes { get; set; } + public KeyStroke FirstStroke { get; } + public KeyStroke SecondStroke { get; set; } + public bool IsExtended { get; } + + public TranslatedKey(KeyStroke stroke, bool isExtended) + { + //Strokes = new List() { stroke }; + FirstStroke = stroke; + } + } + + + class ScanCodeHelper + { + //private KeyStroke? _extendedBuffer; + private TranslatedKey _translatedKey; + + // Keys which AHK assigns a + private Dictionary _highCodes = new Dictionary() + { + { 28, "Nummpad Enter" }, + { 54, "Right Shift" }, + { 69, "Numlock" }, + + }; + + public TranslatedKey TranslateScanCode(KeyStroke stroke) + { + if(stroke.state > 1) + { + // Stroke is part of Extended key sequence of 2 keys + if (_translatedKey == null) + { + // Stroke is first of an Extended key sequence + // Add this stroke to a buffer, so that we can examine it when the next stroke comes through... + // ... and instruct ProcessStroke to take no action for this stroke, as we do not know what the sequence is yet + _translatedKey = new TranslatedKey(stroke, true); + return null; + } else + { + // Stroke is 2nd of Extended key sequence - we now know what the full sequence is + _translatedKey.SecondStroke = stroke; + } + } + else + { + // Regular key + _translatedKey = new TranslatedKey(stroke, false); + var code = stroke.code; + if (_highCodes.ContainsKey(code)) + { + code += 256; + } + } + var returnValue = _translatedKey; + if (!_translatedKey.IsExtended) + { + _translatedKey = null; + } + return returnValue; + } + } +} diff --git a/C#/TestApp/Program.cs b/C#/TestApp/Program.cs index ccbe7ee..4877bc8 100644 --- a/C#/TestApp/Program.cs +++ b/C#/TestApp/Program.cs @@ -7,13 +7,13 @@ namespace TestApp { private static void Main() { - var mmt = new MouseMoveTester(TestDevices.LogitechWheelMouse); + //var mmt = new MouseMoveTester(TestDevices.LogitechWheelMouse); //var mbt = new MouseButtonTester(TestDevices.LogitechWheelMouse, MouseButtons.Left, true); //var ambt = new MouseButtonsTester(TestDevices.LogitechWheelMouse, true); //var kt = new KeyboardTester(TestDevices.WyseKeyboard, true); //var kkt = new KeyboardKeyTester(TestDevices.WyseKeyboard, AhkKeys.Obj("1"), true); //var tt = new TabletTester(TestDevices.ParbloIslandA609); - //var sct = new ScanCodeTester(TestDevices.WyseKeyboard, true); + var sct = new ScanCodeTester(TestDevices.WyseKeyboard, true); //var sst = new SetStateTester(TestDevices.WyseKeyboard, AhkKeys.Obj("1")); Console.ReadLine(); } From 64cab136eaf3c0d83c92fb5cdabd6a058a678a00 Mon Sep 17 00:00:00 2001 From: Clive Galway Date: Tue, 18 Jan 2022 01:06:52 +0000 Subject: [PATCH 03/33] Start UnitTests --- C#/AutoHotInterception.sln | 6 + .../Helpers/ScanCodeHelper.cs | 98 ++++++++++++- C#/UnitTests/TestClass.cs | 135 ++++++++++++++++++ C#/UnitTests/UnitTests.csproj | 69 +++++++++ C#/UnitTests/packages.config | 5 + 5 files changed, 308 insertions(+), 5 deletions(-) create mode 100644 C#/UnitTests/TestClass.cs create mode 100644 C#/UnitTests/UnitTests.csproj create mode 100644 C#/UnitTests/packages.config diff --git a/C#/AutoHotInterception.sln b/C#/AutoHotInterception.sln index 351e265..5bbe268 100644 --- a/C#/AutoHotInterception.sln +++ b/C#/AutoHotInterception.sln @@ -12,6 +12,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencie Dependencies\Readme.md = Dependencies\Readme.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{8EDF4429-251A-416D-BB68-93F227191BCF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -26,6 +28,10 @@ Global {02CBCBB9-C17F-4C6A-8F93-D7EAF038CAED}.Debug|Any CPU.Build.0 = Debug|Any CPU {02CBCBB9-C17F-4C6A-8F93-D7EAF038CAED}.Release|Any CPU.ActiveCfg = Release|Any CPU {02CBCBB9-C17F-4C6A-8F93-D7EAF038CAED}.Release|Any CPU.Build.0 = Release|Any CPU + {8EDF4429-251A-416D-BB68-93F227191BCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8EDF4429-251A-416D-BB68-93F227191BCF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8EDF4429-251A-416D-BB68-93F227191BCF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8EDF4429-251A-416D-BB68-93F227191BCF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs b/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs index 1970e65..602daae 100644 --- a/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs +++ b/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs @@ -17,13 +17,14 @@ The purpose of this class is to encapsulate the logic required to deal with this */ namespace AutoHotInterception.Helpers { - class TranslatedKey + public class TranslatedKey { public ushort AhkCode { get; set; } //public List Strokes { get; set; } public KeyStroke FirstStroke { get; } public KeyStroke SecondStroke { get; set; } public bool IsExtended { get; } + public int State { get; set; } public TranslatedKey(KeyStroke stroke, bool isExtended) { @@ -32,13 +33,64 @@ namespace AutoHotInterception.Helpers } } + public static class SpecialKeys + { + public static SpecialKey NumpadEnter { get; set; } = new SpecialKey("Numpad Enter", 28, ExtMode.E0, CodeType.High, Order.Normal); + public static SpecialKey RightControl { get; } = new SpecialKey("Right Control", 29, ExtMode.E1, CodeType.High, Order.Normal); + public static SpecialKey NumpadDiv { get; } = new SpecialKey("Numpad Div", 53, ExtMode.E1, CodeType.High, Order.Normal); + public static SpecialKey RightShift { get; set; } = new SpecialKey("Right Shift", 54, ExtMode.E0, CodeType.High, Order.Normal); + public static SpecialKey Pause { get; set; } = new SpecialKey("Pause", 69, ExtMode.E0, CodeType.Low, Order.Prefixed); + public static SpecialKey Home { get; set; } = new SpecialKey("Home", 71, ExtMode.E1, CodeType.High, Order.Wrapped); + public static List List { get; set; } = new List() + { + NumpadEnter, RightControl, NumpadDiv, Pause, Home + }; + } + + // Whether the AHK ScanCode is Low (same as Interception) or Hight (Interception + 256) + public enum CodeType { Low, High }; + + // Whether Press/Release states are 0/1 (E0), 2/3 (E1) or 4/5 (E2) + public enum ExtMode { E0, E1, E2}; + + // Order of the strokes received + public enum Order { Normal /* Stroke order is Key press, Key release (No Extended Modifier) */ + , Wrapped /* Stroke order is Ext Modifier press, Key press, Key release, Ext Modifier Release */ + , Prefixed /* Stroke order is Ext Modifier press, Key press, Ext Modifier release, Key release */}; + + public class SpecialKey + { + public string Name { get; } + public ushort Code { get; } + public ExtMode ExtendedMode { get; } + public CodeType CodeType { get; } + public Order StrokeOrder { get; } + + public SpecialKey(string name, ushort code, ExtMode extendedMode, CodeType codeType, Order strokeOrderW) + { + // The name of the key + Name = name; + // The code that identifies this key + Code = code; + // What values will be reported for press/release states for this key + ExtendedMode = extendedMode; + // Whether AHK uses a High (+256) or Low code for this key + CodeType = codeType; + } + } - class ScanCodeHelper + public class ScanCodeHelper { //private KeyStroke? _extendedBuffer; private TranslatedKey _translatedKey; + // Converts Interception state to AHK state + private static List _stateConverter = new List() { 1, 0 , 1, 0, 1, 0 }; + // Converts state to extended mode + private static List _stateToExtendedMode = new List() { 0, 0, 1, 1, 2, 2 }; - // Keys which AHK assigns a + /* + // Keys which AHK assigns a high code to, even though state is 0/1 + // These keys also do not generate extended key codes private Dictionary _highCodes = new Dictionary() { { 28, "Nummpad Enter" }, @@ -47,9 +99,43 @@ namespace AutoHotInterception.Helpers }; + + // Keys which have an extended state, but extended modifiers are not sent + private Dictionary _noExtendedModifier = new Dictionary() + { + {29, "Right Control" }, + {53, "Numpad Div" }, + {56, "Right Alt" }, + {91, "Left Windows" }, + {92, "Right Windows" }, + {93, "Apps" } + }; + */ + + public ScanCodeHelper() + { + for (int i = 0; i < SpecialKeys.List.Count; i++) + { + var specialKey = SpecialKeys.List[i]; + var dict = specialKey.CodeType == CodeType.Low ? _lowCodes : _highCodes; + dict.Add(specialKey.Code, specialKey.Name); + if (specialKey.ExtendedMode != ExtMode.E0 && specialKey.StrokeOrder == Order.Normal) + { + _noExtendedModifier.Add(specialKey.Code, specialKey.Name); + } + } + } + + private Dictionary _highCodes = new Dictionary(); + private Dictionary _lowCodes = new Dictionary(); + // Keys which have E1 or E2 state, but do not send extended modifier + private Dictionary _noExtendedModifier = new Dictionary(); + public TranslatedKey TranslateScanCode(KeyStroke stroke) { - if(stroke.state > 1) + //if(stroke.state > 1 && !_noExtendedModifier.ContainsKey(stroke.code)) + //if (stroke.state > 1 || stroke.code == SpecialKeys.Pause.Code ) + if (stroke.state > 1 && !_noExtendedModifier.ContainsKey(stroke.code)) { // Stroke is part of Extended key sequence of 2 keys if (_translatedKey == null) @@ -67,13 +153,15 @@ namespace AutoHotInterception.Helpers } else { - // Regular key + // Stroke is a single key sequence _translatedKey = new TranslatedKey(stroke, false); var code = stroke.code; if (_highCodes.ContainsKey(code)) { code += 256; } + _translatedKey.AhkCode = code; + _translatedKey.State = _stateConverter[stroke.state]; } var returnValue = _translatedKey; if (!_translatedKey.IsExtended) diff --git a/C#/UnitTests/TestClass.cs b/C#/UnitTests/TestClass.cs new file mode 100644 index 0000000..04cf5fe --- /dev/null +++ b/C#/UnitTests/TestClass.cs @@ -0,0 +1,135 @@ +// NUnit 3 tests +// See documentation : https://github.com/nunit/docs/wiki/NUnit-Documentation +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using AutoHotInterception.Helpers; +using NUnit.Framework; +using static AutoHotInterception.Helpers.ManagedWrapper; + +namespace UnitTests +{ + public class TestKey + { + public string Name { get; } + public List PressStrokes { get; } + public List ReleaseStrokes { get; } + public List PressResults { get; } + public List ReleaseResults { get; } + + public TestKey(string name, List pressStrokes, List releaseStrokes, + List pressResults, List releaseResults) + { + Name = name; + PressStrokes = pressStrokes; + ReleaseStrokes = releaseStrokes; + PressResults = pressResults; + ReleaseResults = releaseResults; + } + } + + public class ExpectedResult + { + public ushort Code { get; } + public ushort State { get; } + + public ExpectedResult(ushort code, ushort state) + { + Code = code; + State = state; + } + } + + [TestFixture] + public class ScanCodeHelperTests + { + ScanCodeHelper sch; + private static List _testKeys = new List() + { + new TestKey("Numpad Enter", Stroke(28, 0), Stroke(28, 1), Result(284, 1), Result(284, 0)), + new TestKey("Right Control", Stroke(29, 2), Stroke(29, 3), Result(285, 1), Result(285, 0)), + new TestKey("Numpad Div", Stroke(53, 2), Stroke(53, 3), Result(309, 1), Result(309, 0)), + new TestKey("Right Shift", Stroke(54, 0), Stroke(54, 1), Result(310, 1), Result(310, 0)), + + }; + + [SetUp] + public void SetUpBeforeEachTest() + { + sch = new ScanCodeHelper(); + } + + private static List Stroke (ushort code1, ushort state1, ushort code2 = 0, ushort state2 = 0) + { + var strokes = new List(); + strokes.Add(new KeyStroke() { code = code1, state = state1 }); + if (code2 != 0) + { + strokes.Add(new KeyStroke() { code = code2, state = state2 }); + } + return strokes; + } + + private static List Result(ushort? code1, ushort? state1, ushort? code2 = null, ushort? state2 = null) + { + var results = new List(); + if (code1 == null) results.Add(null); + else results.Add(new ExpectedResult((ushort)code1, (ushort)state1)); + if (code2 == null) results.Add(null); + else results.Add(new ExpectedResult((ushort)code2, (ushort)state2)); + return results; + } + + [Test] + public void PressReleaseTests() + { + DoTest(_testKeys[3]); + //foreach (var testKey in _testKeys) + //{ + // DoTest(testKey); + //} + } + + private void DoTest(TestKey testKey) + { + Debug.WriteLine($"\nTesting key {testKey.Name}..."); + Debug.WriteLine("Testing Press"); + for (int i = 0; i < testKey.PressStrokes.Count; i++) + { + var stroke = testKey.PressStrokes[i]; + Debug.WriteLine($"Sending stroke #{i+1} with code {stroke.code}, state {stroke.state}"); + var expectedResult = testKey.PressResults[i]; + var actualResult = sch.TranslateScanCode(stroke); + AssertResult(actualResult, expectedResult); + } + + Debug.WriteLine("Testing Release"); + for (int i = 0; i < testKey.ReleaseStrokes.Count; i++) + { + var stroke = testKey.ReleaseStrokes[i]; + Debug.WriteLine($"Sending stroke #{i+1} with code {stroke.code}, state {stroke.state}"); + var expectedResult = testKey.ReleaseResults[i]; + var actualResult = sch.TranslateScanCode(stroke); + AssertResult(actualResult, expectedResult); + } + Debug.WriteLine("OK!"); + } + + void AssertResult(TranslatedKey actualResult, ExpectedResult expectedResult) + { + if (expectedResult == null) + { + Debug.WriteLine($"Expecting result of null"); + Assert.That(actualResult == null, "Result should be null"); + } + else + { + Debug.WriteLine($"Expecting code of {expectedResult.Code}, state of {expectedResult.State}"); + Assert.That(actualResult != null, "Result should not be null"); + Assert.That(actualResult.AhkCode, Is.EqualTo(expectedResult.Code), $"Code should be {expectedResult.Code}, got {actualResult.AhkCode}"); + Assert.That(actualResult.State, Is.EqualTo(expectedResult.State), $"State should be {expectedResult.State}, got {actualResult.State}"); + } + } + } +} diff --git a/C#/UnitTests/UnitTests.csproj b/C#/UnitTests/UnitTests.csproj new file mode 100644 index 0000000..5528cda --- /dev/null +++ b/C#/UnitTests/UnitTests.csproj @@ -0,0 +1,69 @@ + + + + + + + Debug + AnyCPU + {8EDF4429-251A-416D-BB68-93F227191BCF} + Library + Properties + UnitTests + UnitTests + v4.8 + 512 + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\NUnit.3.13.2\lib\net45\nunit.framework.dll + + + + + + + + + + + + {68fa4bc3-c277-44d0-8333-18d51dc3ca19} + AutoHotInterception + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/C#/UnitTests/packages.config b/C#/UnitTests/packages.config new file mode 100644 index 0000000..7147696 --- /dev/null +++ b/C#/UnitTests/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file From c271b2d97fd5d8445285e1f5850b9a1f140f85a6 Mon Sep 17 00:00:00 2001 From: Clive Galway Date: Tue, 18 Jan 2022 02:35:16 +0000 Subject: [PATCH 04/33] Logic good for E1 extended modifier types --- .../Helpers/ScanCodeHelper.cs | 94 ++++++++++--------- C#/UnitTests/TestClass.cs | 15 +-- 2 files changed, 58 insertions(+), 51 deletions(-) diff --git a/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs b/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs index 602daae..1782e04 100644 --- a/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs +++ b/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using static AutoHotInterception.Helpers.ManagedWrapper; /* @@ -20,16 +21,13 @@ namespace AutoHotInterception.Helpers public class TranslatedKey { public ushort AhkCode { get; set; } - //public List Strokes { get; set; } - public KeyStroke FirstStroke { get; } - public KeyStroke SecondStroke { get; set; } + public List Strokes { get; set; } public bool IsExtended { get; } public int State { get; set; } public TranslatedKey(KeyStroke stroke, bool isExtended) { - //Strokes = new List() { stroke }; - FirstStroke = stroke; + Strokes = new List() { stroke }; } } @@ -39,11 +37,13 @@ namespace AutoHotInterception.Helpers public static SpecialKey RightControl { get; } = new SpecialKey("Right Control", 29, ExtMode.E1, CodeType.High, Order.Normal); public static SpecialKey NumpadDiv { get; } = new SpecialKey("Numpad Div", 53, ExtMode.E1, CodeType.High, Order.Normal); public static SpecialKey RightShift { get; set; } = new SpecialKey("Right Shift", 54, ExtMode.E0, CodeType.High, Order.Normal); + public static SpecialKey RightAlt { get; } = new SpecialKey("Right Alt", 56, ExtMode.E1, CodeType.High, Order.Normal); + public static SpecialKey Numlock { get; set; } = new SpecialKey("Numlock", 69, ExtMode.E0, CodeType.High, Order.Normal); public static SpecialKey Pause { get; set; } = new SpecialKey("Pause", 69, ExtMode.E0, CodeType.Low, Order.Prefixed); public static SpecialKey Home { get; set; } = new SpecialKey("Home", 71, ExtMode.E1, CodeType.High, Order.Wrapped); public static List List { get; set; } = new List() { - NumpadEnter, RightControl, NumpadDiv, Pause, Home + NumpadEnter, RightControl, NumpadDiv, RightShift, RightAlt, Numlock, Pause, Home }; } @@ -66,7 +66,7 @@ namespace AutoHotInterception.Helpers public CodeType CodeType { get; } public Order StrokeOrder { get; } - public SpecialKey(string name, ushort code, ExtMode extendedMode, CodeType codeType, Order strokeOrderW) + public SpecialKey(string name, ushort code, ExtMode extendedMode, CodeType codeType, Order strokeOrder) { // The name of the key Name = name; @@ -76,68 +76,50 @@ namespace AutoHotInterception.Helpers ExtendedMode = extendedMode; // Whether AHK uses a High (+256) or Low code for this key CodeType = codeType; + // Whether part of two stroke extended set, and if so, which order the strokes come in + StrokeOrder = strokeOrder; } } public class ScanCodeHelper { - //private KeyStroke? _extendedBuffer; private TranslatedKey _translatedKey; // Converts Interception state to AHK state - private static List _stateConverter = new List() { 1, 0 , 1, 0, 1, 0 }; + private static List _stateConverter = new List() { 1, 0, 1, 0, 1, 0 }; // Converts state to extended mode private static List _stateToExtendedMode = new List() { 0, 0, 1, 1, 2, 2 }; + // List of code/states which signify first stroke of a two stroke set + private static HashSet> _extendedCodeAndStates = new HashSet>(); - /* - // Keys which AHK assigns a high code to, even though state is 0/1 - // These keys also do not generate extended key codes - private Dictionary _highCodes = new Dictionary() - { - { 28, "Nummpad Enter" }, - { 54, "Right Shift" }, - { 69, "Numlock" }, - - }; - - - // Keys which have an extended state, but extended modifiers are not sent - private Dictionary _noExtendedModifier = new Dictionary() - { - {29, "Right Control" }, - {53, "Numpad Div" }, - {56, "Right Alt" }, - {91, "Left Windows" }, - {92, "Right Windows" }, - {93, "Apps" } - }; - */ - - public ScanCodeHelper() + public ScanCodeHelper() { for (int i = 0; i < SpecialKeys.List.Count; i++) { var specialKey = SpecialKeys.List[i]; var dict = specialKey.CodeType == CodeType.Low ? _lowCodes : _highCodes; dict.Add(specialKey.Code, specialKey.Name); - if (specialKey.ExtendedMode != ExtMode.E0 && specialKey.StrokeOrder == Order.Normal) + // Build list of codes which signify that this is the first stroke of an extended set + if (specialKey.StrokeOrder == Order.Wrapped) { - _noExtendedModifier.Add(specialKey.Code, specialKey.Name); + _extendedCodeAndStates.Add(new Tuple(42, 2)); // LShift with E1 state on press + _extendedCodeAndStates.Add(new Tuple(specialKey.Code, 3)); // ScanCode with E1 state on release + } + else if (specialKey.StrokeOrder == Order.Prefixed) + { + _extendedCodeAndStates.Add(new Tuple(29, 4)); // LCtrl with E2 state on press + _extendedCodeAndStates.Add(new Tuple(29, 5)); // LCtrl with E2 state on release } } } private Dictionary _highCodes = new Dictionary(); private Dictionary _lowCodes = new Dictionary(); - // Keys which have E1 or E2 state, but do not send extended modifier - private Dictionary _noExtendedModifier = new Dictionary(); public TranslatedKey TranslateScanCode(KeyStroke stroke) { - //if(stroke.state > 1 && !_noExtendedModifier.ContainsKey(stroke.code)) - //if (stroke.state > 1 || stroke.code == SpecialKeys.Pause.Code ) - if (stroke.state > 1 && !_noExtendedModifier.ContainsKey(stroke.code)) + if (_extendedCodeAndStates.Contains(new Tuple(stroke.code, stroke.state)) || _translatedKey != null) { - // Stroke is part of Extended key sequence of 2 keys + // Stroke is first key of Extended key sequence of 2 keys if (_translatedKey == null) { // Stroke is first of an Extended key sequence @@ -145,10 +127,31 @@ namespace AutoHotInterception.Helpers // ... and instruct ProcessStroke to take no action for this stroke, as we do not know what the sequence is yet _translatedKey = new TranslatedKey(stroke, true); return null; - } else + } + else { // Stroke is 2nd of Extended key sequence - we now know what the full sequence is - _translatedKey.SecondStroke = stroke; + _translatedKey.Strokes.Add(stroke); + var extMode = _stateToExtendedMode[stroke.state]; + switch (extMode) + { + case 0: + throw new Exception("Expecting E1 or E2 state"); + case 1: + // Which state to report (1 = press, 0 = release) + var state = _stateConverter[_translatedKey.Strokes[0].state]; + // Which code to use depends on whether this is a press or release + // On press, use the second stroke (index 1) + // On release, use the first stroke (index 0) + var whichStroke = _translatedKey.Strokes[state]; + _translatedKey.AhkCode = (ushort)(whichStroke.code + 256); + _translatedKey.State = state; + break; + case 2: + throw new NotImplementedException(); + default: + throw new Exception("state can only be E0, E1 or E2"); + } } } else @@ -164,6 +167,7 @@ namespace AutoHotInterception.Helpers _translatedKey.State = _stateConverter[stroke.state]; } var returnValue = _translatedKey; + // ToDo - can this always be nulled? if (!_translatedKey.IsExtended) { _translatedKey = null; diff --git a/C#/UnitTests/TestClass.cs b/C#/UnitTests/TestClass.cs index 04cf5fe..d4bf7cd 100644 --- a/C#/UnitTests/TestClass.cs +++ b/C#/UnitTests/TestClass.cs @@ -51,7 +51,10 @@ namespace UnitTests new TestKey("Right Control", Stroke(29, 2), Stroke(29, 3), Result(285, 1), Result(285, 0)), new TestKey("Numpad Div", Stroke(53, 2), Stroke(53, 3), Result(309, 1), Result(309, 0)), new TestKey("Right Shift", Stroke(54, 0), Stroke(54, 1), Result(310, 1), Result(310, 0)), - + new TestKey("Right Alt", Stroke(56, 2), Stroke(56, 3), Result(312, 1), Result(312, 0)), + new TestKey("Numlock", Stroke(69, 0), Stroke(69, 1), Result(325, 1), Result(325, 0)), + //new TestKey("Pause", Stroke(69, 0), Stroke(69, 1), Result(325, 1), Result(325, 0)), + new TestKey("Home", Stroke(42, 2, 71, 2), Stroke(71, 3, 42, 3), Result(null, null, 327, 1), Result(null, null, 327, 0)), }; [SetUp] @@ -84,11 +87,11 @@ namespace UnitTests [Test] public void PressReleaseTests() { - DoTest(_testKeys[3]); - //foreach (var testKey in _testKeys) - //{ - // DoTest(testKey); - //} + //DoTest(_testKeys[6]); + foreach (var testKey in _testKeys) + { + DoTest(testKey); + } } private void DoTest(TestKey testKey) From 14f82c0dae7ae0d8a46c8f7544c030d573623c9e Mon Sep 17 00:00:00 2001 From: Clive Galway Date: Tue, 18 Jan 2022 02:59:35 +0000 Subject: [PATCH 05/33] Code working for E2 keys (Pause) --- .../Helpers/ScanCodeHelper.cs | 31 +++++++++++++------ C#/UnitTests/TestClass.cs | 3 +- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs b/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs index 1782e04..6dff2ff 100644 --- a/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs +++ b/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs @@ -22,7 +22,7 @@ namespace AutoHotInterception.Helpers { public ushort AhkCode { get; set; } public List Strokes { get; set; } - public bool IsExtended { get; } + //public bool IsExtended { get; } public int State { get; set; } public TranslatedKey(KeyStroke stroke, bool isExtended) @@ -132,23 +132,35 @@ namespace AutoHotInterception.Helpers { // Stroke is 2nd of Extended key sequence - we now know what the full sequence is _translatedKey.Strokes.Add(stroke); - var extMode = _stateToExtendedMode[stroke.state]; + //var extMode = _stateToExtendedMode[stroke.state]; + var extMode = _stateToExtendedMode[_translatedKey.Strokes[0].state]; + ushort state; + KeyStroke whichStroke; switch (extMode) { case 0: throw new Exception("Expecting E1 or E2 state"); case 1: + // E1 // Which state to report (1 = press, 0 = release) - var state = _stateConverter[_translatedKey.Strokes[0].state]; + state = _stateConverter[_translatedKey.Strokes[0].state]; // Which code to use depends on whether this is a press or release // On press, use the second stroke (index 1) // On release, use the first stroke (index 0) - var whichStroke = _translatedKey.Strokes[state]; + whichStroke = _translatedKey.Strokes[state]; _translatedKey.AhkCode = (ushort)(whichStroke.code + 256); _translatedKey.State = state; break; case 2: - throw new NotImplementedException(); + // E2 + // Which state to report (1 = press, 0 = release) + state = _stateConverter[_translatedKey.Strokes[0].state]; + // Which code to use depends on whether this is a press or release + // Always use the second stroke (index 1) + whichStroke = _translatedKey.Strokes[1]; + _translatedKey.AhkCode = whichStroke.code; + _translatedKey.State = state; + break; default: throw new Exception("state can only be E0, E1 or E2"); } @@ -166,12 +178,11 @@ namespace AutoHotInterception.Helpers _translatedKey.AhkCode = code; _translatedKey.State = _stateConverter[stroke.state]; } + + // Code will only get here if the stroke was a single key, or the second key of an extended sequence + // Return _translatedKey and clear it, ready for the next key var returnValue = _translatedKey; - // ToDo - can this always be nulled? - if (!_translatedKey.IsExtended) - { - _translatedKey = null; - } + _translatedKey = null; return returnValue; } } diff --git a/C#/UnitTests/TestClass.cs b/C#/UnitTests/TestClass.cs index d4bf7cd..8aa9037 100644 --- a/C#/UnitTests/TestClass.cs +++ b/C#/UnitTests/TestClass.cs @@ -53,7 +53,7 @@ namespace UnitTests new TestKey("Right Shift", Stroke(54, 0), Stroke(54, 1), Result(310, 1), Result(310, 0)), new TestKey("Right Alt", Stroke(56, 2), Stroke(56, 3), Result(312, 1), Result(312, 0)), new TestKey("Numlock", Stroke(69, 0), Stroke(69, 1), Result(325, 1), Result(325, 0)), - //new TestKey("Pause", Stroke(69, 0), Stroke(69, 1), Result(325, 1), Result(325, 0)), + new TestKey("Pause", Stroke(29, 4, 69, 0), Stroke(29, 5, 69, 1), Result(null, null, 69, 1), Result(null, null, 69, 0)), new TestKey("Home", Stroke(42, 2, 71, 2), Stroke(71, 3, 42, 3), Result(null, null, 327, 1), Result(null, null, 327, 0)), }; @@ -87,7 +87,6 @@ namespace UnitTests [Test] public void PressReleaseTests() { - //DoTest(_testKeys[6]); foreach (var testKey in _testKeys) { DoTest(testKey); From dddf8ec6e6c64e81a1f1699e179b790d8d7c9aa8 Mon Sep 17 00:00:00 2001 From: Clive Galway Date: Tue, 18 Jan 2022 03:15:26 +0000 Subject: [PATCH 06/33] Unit Tests and ScanCodeHelper complete --- .../Helpers/ScanCodeHelper.cs | 17 ++++++++++++++++- C#/UnitTests/TestClass.cs | 12 ++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs b/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs index 6dff2ff..6677bcb 100644 --- a/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs +++ b/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs @@ -41,9 +41,24 @@ namespace AutoHotInterception.Helpers public static SpecialKey Numlock { get; set; } = new SpecialKey("Numlock", 69, ExtMode.E0, CodeType.High, Order.Normal); public static SpecialKey Pause { get; set; } = new SpecialKey("Pause", 69, ExtMode.E0, CodeType.Low, Order.Prefixed); public static SpecialKey Home { get; set; } = new SpecialKey("Home", 71, ExtMode.E1, CodeType.High, Order.Wrapped); + public static SpecialKey Up { get; set; } = new SpecialKey("Up", 72, ExtMode.E1, CodeType.High, Order.Wrapped); + public static SpecialKey PgUp { get; set; } = new SpecialKey("PgUp", 73, ExtMode.E1, CodeType.High, Order.Wrapped); + public static SpecialKey Left { get; set; } = new SpecialKey("Left", 75, ExtMode.E1, CodeType.High, Order.Wrapped); + public static SpecialKey Right { get; set; } = new SpecialKey("Right", 77, ExtMode.E1, CodeType.High, Order.Wrapped); + public static SpecialKey End { get; set; } = new SpecialKey("End", 79, ExtMode.E1, CodeType.High, Order.Wrapped); + public static SpecialKey Down { get; set; } = new SpecialKey("Down", 80, ExtMode.E1, CodeType.High, Order.Wrapped); + public static SpecialKey PgDn { get; set; } = new SpecialKey("PgDn", 81, ExtMode.E1, CodeType.High, Order.Wrapped); + public static SpecialKey Insert { get; set; } = new SpecialKey("Insert", 82, ExtMode.E1, CodeType.High, Order.Wrapped); + public static SpecialKey Delete { get; set; } = new SpecialKey("Delete", 83, ExtMode.E1, CodeType.High, Order.Wrapped); + public static SpecialKey LeftWindows { get; } = new SpecialKey("Left Windows", 91, ExtMode.E1, CodeType.High, Order.Normal); + public static SpecialKey RightWindows { get; } = new SpecialKey("Right Windows", 92, ExtMode.E1, CodeType.High, Order.Normal); + public static SpecialKey Apps { get; } = new SpecialKey("Apps", 93, ExtMode.E1, CodeType.High, Order.Normal); + public static List List { get; set; } = new List() { - NumpadEnter, RightControl, NumpadDiv, RightShift, RightAlt, Numlock, Pause, Home + NumpadEnter, RightControl, NumpadDiv, RightShift, RightAlt, Numlock, Pause, + Home, Up, PgUp, Left, Right, End, Down, PgDn, Insert, Delete, + LeftWindows, RightWindows, Apps }; } diff --git a/C#/UnitTests/TestClass.cs b/C#/UnitTests/TestClass.cs index 8aa9037..e0fedd7 100644 --- a/C#/UnitTests/TestClass.cs +++ b/C#/UnitTests/TestClass.cs @@ -55,6 +55,18 @@ namespace UnitTests new TestKey("Numlock", Stroke(69, 0), Stroke(69, 1), Result(325, 1), Result(325, 0)), new TestKey("Pause", Stroke(29, 4, 69, 0), Stroke(29, 5, 69, 1), Result(null, null, 69, 1), Result(null, null, 69, 0)), new TestKey("Home", Stroke(42, 2, 71, 2), Stroke(71, 3, 42, 3), Result(null, null, 327, 1), Result(null, null, 327, 0)), + new TestKey("Up", Stroke(42, 2, 72, 2), Stroke(72, 3, 42, 3), Result(null, null, 328, 1), Result(null, null, 328, 0)), + new TestKey("PgUp", Stroke(42, 2, 73, 2), Stroke(73, 3, 42, 3), Result(null, null, 329, 1), Result(null, null, 329, 0)), + new TestKey("Left", Stroke(42, 2, 75, 2), Stroke(75, 3, 42, 3), Result(null, null, 331, 1), Result(null, null, 331, 0)), + new TestKey("Right", Stroke(42, 2, 77, 2), Stroke(77, 3, 42, 3), Result(null, null, 333, 1), Result(null, null, 333, 0)), + new TestKey("End", Stroke(42, 2, 79, 2), Stroke(79, 3, 42, 3), Result(null, null, 335, 1), Result(null, null, 335, 0)), + new TestKey("Down", Stroke(42, 2, 80, 2), Stroke(80, 3, 42, 3), Result(null, null, 336, 1), Result(null, null, 336, 0)), + new TestKey("PgDn", Stroke(42, 2, 81, 2), Stroke(81, 3, 42, 3), Result(null, null, 337, 1), Result(null, null, 337, 0)), + new TestKey("Insert", Stroke(42, 2, 82, 2), Stroke(82, 3, 42, 3), Result(null, null, 338, 1), Result(null, null, 338, 0)), + new TestKey("Delete", Stroke(42, 2, 83, 2), Stroke(83, 3, 42, 3), Result(null, null, 339, 1), Result(null, null, 339, 0)), + new TestKey("Left Windows", Stroke(91, 2), Stroke(91, 3), Result(347, 1), Result(347, 0)), + new TestKey("Right Windows", Stroke(92, 2), Stroke(92, 3), Result(348, 1), Result(348, 0)), + new TestKey("Apps", Stroke(93, 2), Stroke(93, 3), Result(349, 1), Result(349, 0)), }; [SetUp] From 9ee124da7e6b15658e27e9e8851d44d1825591d8 Mon Sep 17 00:00:00 2001 From: Clive Galway Date: Tue, 18 Jan 2022 03:19:37 +0000 Subject: [PATCH 07/33] Update comments --- C#/AutoHotInterception/Helpers/ScanCodeHelper.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs b/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs index 6677bcb..5103d01 100644 --- a/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs +++ b/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs @@ -156,7 +156,7 @@ namespace AutoHotInterception.Helpers case 0: throw new Exception("Expecting E1 or E2 state"); case 1: - // E1 + // E1 (Home, Up, PgUp, Left, Right, End, Down, PgDn, Insert, Delete) // Which state to report (1 = press, 0 = release) state = _stateConverter[_translatedKey.Strokes[0].state]; // Which code to use depends on whether this is a press or release @@ -167,11 +167,10 @@ namespace AutoHotInterception.Helpers _translatedKey.State = state; break; case 2: - // E2 + // E2 (Pause only) // Which state to report (1 = press, 0 = release) state = _stateConverter[_translatedKey.Strokes[0].state]; - // Which code to use depends on whether this is a press or release - // Always use the second stroke (index 1) + // Always use the code of the second stroke (index 1) whichStroke = _translatedKey.Strokes[1]; _translatedKey.AhkCode = whichStroke.code; _translatedKey.State = state; From ae06a4f1ee68ed6bcd8eb6820aed3f28a29d5717 Mon Sep 17 00:00:00 2001 From: Clive Galway Date: Tue, 18 Jan 2022 03:32:08 +0000 Subject: [PATCH 08/33] Remove _lowCodes, tidy --- .../Helpers/ScanCodeHelper.cs | 66 ++++++++++--------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs b/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs index 5103d01..3604283 100644 --- a/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs +++ b/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs @@ -18,11 +18,12 @@ The purpose of this class is to encapsulate the logic required to deal with this */ namespace AutoHotInterception.Helpers { + // Returned to the ProcessStroke function to indicate the ultimate result of processing the stroke(s) + // Can be the result of processing a two-stroke Extended Keycode set public class TranslatedKey { public ushort AhkCode { get; set; } public List Strokes { get; set; } - //public bool IsExtended { get; } public int State { get; set; } public TranslatedKey(KeyStroke stroke, bool isExtended) @@ -31,6 +32,31 @@ namespace AutoHotInterception.Helpers } } + // Holds information for a special key to describe how it behaves + public class SpecialKey + { + public string Name { get; } + public ushort Code { get; } + public ExtMode ExtendedMode { get; } + public CodeType CodeType { get; } + public Order StrokeOrder { get; } + + public SpecialKey(string name, ushort code, ExtMode extendedMode, CodeType codeType, Order strokeOrder) + { + // The name of the key + Name = name; + // The code that identifies this key + Code = code; + // What values will be reported for press/release states for this key + ExtendedMode = extendedMode; + // Whether AHK uses a High (+256) or Low code for this key + CodeType = codeType; + // Whether part of two stroke extended set, and if so, which order the strokes come in + StrokeOrder = strokeOrder; + } + } + + // List of all the special keys ahd how they behave public static class SpecialKeys { public static SpecialKey NumpadEnter { get; set; } = new SpecialKey("Numpad Enter", 28, ExtMode.E0, CodeType.High, Order.Normal); @@ -73,29 +99,7 @@ namespace AutoHotInterception.Helpers , Wrapped /* Stroke order is Ext Modifier press, Key press, Key release, Ext Modifier Release */ , Prefixed /* Stroke order is Ext Modifier press, Key press, Ext Modifier release, Key release */}; - public class SpecialKey - { - public string Name { get; } - public ushort Code { get; } - public ExtMode ExtendedMode { get; } - public CodeType CodeType { get; } - public Order StrokeOrder { get; } - - public SpecialKey(string name, ushort code, ExtMode extendedMode, CodeType codeType, Order strokeOrder) - { - // The name of the key - Name = name; - // The code that identifies this key - Code = code; - // What values will be reported for press/release states for this key - ExtendedMode = extendedMode; - // Whether AHK uses a High (+256) or Low code for this key - CodeType = codeType; - // Whether part of two stroke extended set, and if so, which order the strokes come in - StrokeOrder = strokeOrder; - } - } - + // Processes strokes and translates them into AHK style code and state public class ScanCodeHelper { private TranslatedKey _translatedKey; @@ -105,14 +109,16 @@ namespace AutoHotInterception.Helpers private static List _stateToExtendedMode = new List() { 0, 0, 1, 1, 2, 2 }; // List of code/states which signify first stroke of a two stroke set private static HashSet> _extendedCodeAndStates = new HashSet>(); + // Keys which are E0, but AHK still assigns a High (+256) code to them + private Dictionary _highCodes = new Dictionary(); - public ScanCodeHelper() + public ScanCodeHelper() { + // Read the SpecialKeys list and build lookup tables to help in logic processing for (int i = 0; i < SpecialKeys.List.Count; i++) { var specialKey = SpecialKeys.List[i]; - var dict = specialKey.CodeType == CodeType.Low ? _lowCodes : _highCodes; - dict.Add(specialKey.Code, specialKey.Name); + if (specialKey.CodeType == CodeType.High) _highCodes.Add(specialKey.Code, specialKey.Name); // Build list of codes which signify that this is the first stroke of an extended set if (specialKey.StrokeOrder == Order.Wrapped) { @@ -127,9 +133,8 @@ namespace AutoHotInterception.Helpers } } - private Dictionary _highCodes = new Dictionary(); - private Dictionary _lowCodes = new Dictionary(); - + // Process stroke(s) and return ONE TranslatedKey + // May need to be called twice to return a TranslatedKey - it maay return null for the first stroke public TranslatedKey TranslateScanCode(KeyStroke stroke) { if (_extendedCodeAndStates.Contains(new Tuple(stroke.code, stroke.state)) || _translatedKey != null) @@ -147,7 +152,6 @@ namespace AutoHotInterception.Helpers { // Stroke is 2nd of Extended key sequence - we now know what the full sequence is _translatedKey.Strokes.Add(stroke); - //var extMode = _stateToExtendedMode[stroke.state]; var extMode = _stateToExtendedMode[_translatedKey.Strokes[0].state]; ushort state; KeyStroke whichStroke; From ccb6a0f02c71693977f9abc106df6b652b6e745f Mon Sep 17 00:00:00 2001 From: Clive Galway Date: Tue, 18 Jan 2022 16:25:46 +0000 Subject: [PATCH 09/33] POC for handling extended keys with WaitWithTimeout of 0 --- C#/AutoHotInterception/ScanCodeChecker.cs | 27 +++++++++++++++++++---- C#/TestApp/ScanCodeTester.cs | 12 +++++++--- C#/TestApp/TestDevices.cs | 1 + 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/C#/AutoHotInterception/ScanCodeChecker.cs b/C#/AutoHotInterception/ScanCodeChecker.cs index c33052d..2908fa3 100644 --- a/C#/AutoHotInterception/ScanCodeChecker.cs +++ b/C#/AutoHotInterception/ScanCodeChecker.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using AutoHotInterception.Helpers; namespace AutoHotInterception @@ -29,10 +30,27 @@ namespace AutoHotInterception ManagedWrapper.SetFilter(_deviceContext, IsMonitoredDevice, ManagedWrapper.Filter.All); int i; var stroke = new ManagedWrapper.Stroke(); - while (ManagedWrapper.Receive(_deviceContext, i = ManagedWrapper.Wait(_deviceContext), ref stroke, 1) > 0) + while (true) { - if (!_block) ManagedWrapper.Send(_deviceContext, _deviceId, ref stroke, 1); - _callback(new KeyEvent { Code = stroke.key.code, State = stroke.key.state }); + var strokes = new List(); + while (ManagedWrapper.Receive(_deviceContext, i = ManagedWrapper.WaitWithTimeout(_deviceContext, 0), ref stroke, 1) > 0) + { + strokes.Add(stroke); + } + if (!block) + { + foreach (var s in strokes) + { + ManagedWrapper.Send(_deviceContext, _deviceId, ref stroke, 1); + } + } + if (strokes.Count == 0) continue; + var keyEvents = new List(); + foreach (var s in strokes) + { + keyEvents.Add(new KeyEvent { Code = stroke.key.code, State = stroke.key.state }); + } + _callback(keyEvents); } } @@ -43,7 +61,8 @@ namespace AutoHotInterception private int IsMonitoredDevice(int device) { - return Convert.ToInt32(_deviceId == device); + return (Convert.ToInt32(_deviceId == device) ); + //return (Convert.ToInt32(_deviceId == device || device == 12) ); } } diff --git a/C#/TestApp/ScanCodeTester.cs b/C#/TestApp/ScanCodeTester.cs index cbac41d..854e396 100644 --- a/C#/TestApp/ScanCodeTester.cs +++ b/C#/TestApp/ScanCodeTester.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using AutoHotInterception; @@ -11,12 +12,17 @@ namespace TestApp var scc = new ScanCodeChecker(); var devId = device.GetDeviceId(); if (devId == 0) return; - scc.Subscribe(devId, new Action(OnKeyEvent), block); + scc.Subscribe(devId, new Action>(OnKeyEvent), block); } - public void OnKeyEvent(KeyEvent keyEvent) + public void OnKeyEvent(List keyEvents) { - Debug.WriteLine($"Code: {keyEvent.Code} (0x{keyEvent.Code.ToString("X")}) - {keyEvent.Code + 256}, State: {keyEvent.State}"); + var str = $"{keyEvents.Count} - "; + foreach (var keyEvent in keyEvents) + { + str += $"Code: {keyEvent.Code} (0x{keyEvent.Code.ToString("X")}) - {keyEvent.Code + 256}, State: {keyEvent.State} | "; + } + Debug.WriteLine(str); } } } diff --git a/C#/TestApp/TestDevices.cs b/C#/TestApp/TestDevices.cs index 7ace67a..44c051e 100644 --- a/C#/TestApp/TestDevices.cs +++ b/C#/TestApp/TestDevices.cs @@ -12,6 +12,7 @@ namespace TestApp public static TestDevice WyseKeyboard { get; } = new TestDevice { IsMouse = false, Vid = 0x04F2, Pid = 0x0112 }; public static TestDevice LogitechWheelMouse { get; } = new TestDevice { IsMouse = true, Vid = 0x046D, Pid = 0xC00C }; public static TestDevice ParbloIslandA609 { get; } = new TestDevice { IsMouse = true, Handle = "HID\\VID_0B57&PID_9091&REV_0101&Col01" }; + public static TestDevice LogitechG604Mouse { get; } = new TestDevice { IsMouse = true, Vid = 0x046D, Pid = 0xC539 }; } public class TestDevice From 644cef468270b5e9b146d419f4a4976ae33312dc Mon Sep 17 00:00:00 2001 From: Clive Galway Date: Tue, 18 Jan 2022 17:11:00 +0000 Subject: [PATCH 10/33] Add missing using --- C#/TestApp/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/C#/TestApp/Program.cs b/C#/TestApp/Program.cs index 4877bc8..db44fe9 100644 --- a/C#/TestApp/Program.cs +++ b/C#/TestApp/Program.cs @@ -1,5 +1,5 @@ using System; -using AutoHotInterception; +using TestApp.Helpers; namespace TestApp { From de947bfcf4dbd7405aa385851c076c5b03c4a954 Mon Sep 17 00:00:00 2001 From: Clive Galway Date: Tue, 18 Jan 2022 17:42:30 +0000 Subject: [PATCH 11/33] Start to rewrite unit tests --- .../AutoHotInterception.csproj | 1 + .../Helpers/OldScanCodeHelper.cs | 207 ++++++++++++++++++ .../Helpers/ScanCodeHelper.cs | 203 +---------------- C#/UnitTests/ScanCodeHelperTests.cs | 152 +++++++++++++ C#/UnitTests/TestClass.cs | 27 ++- C#/UnitTests/UnitTests.csproj | 1 + 6 files changed, 385 insertions(+), 206 deletions(-) create mode 100644 C#/AutoHotInterception/Helpers/OldScanCodeHelper.cs create mode 100644 C#/UnitTests/ScanCodeHelperTests.cs diff --git a/C#/AutoHotInterception/AutoHotInterception.csproj b/C#/AutoHotInterception/AutoHotInterception.csproj index 8a2a4b5..ad2158b 100644 --- a/C#/AutoHotInterception/AutoHotInterception.csproj +++ b/C#/AutoHotInterception/AutoHotInterception.csproj @@ -46,6 +46,7 @@ + diff --git a/C#/AutoHotInterception/Helpers/OldScanCodeHelper.cs b/C#/AutoHotInterception/Helpers/OldScanCodeHelper.cs new file mode 100644 index 0000000..478da31 --- /dev/null +++ b/C#/AutoHotInterception/Helpers/OldScanCodeHelper.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using static AutoHotInterception.Helpers.ManagedWrapper; + +/* +AHK uses a single ScanCode (0...512) to identify each key, whereas Interception uses two codes - the ScanCode, plus the state. +For example: +in Interception - Code 29 with State 0 or 1 is LCtrl, and 29 with state 2/3 (With state of 2/3 meaning "Extended") is RCtrl +In AHK, code 29 is LCtrl, whereas state 285 (29 + 256) - ie "Extended" versions of keys are Interception code + 256 + +Furthermore, for some keys (eg Arrow keys and the 6 keys above the arrow key block - Ins, Del etc), Interception will sometimes receive TWO strokes... +... when one of these keys are pressed - a modifier key (Ctrl or Shift) and the key itself... + +Also, AHK assigns "high" (+256) codes to some keys, even when state is 0/1... +... And also assigns a "low" code to Pause, even though it is wrapped in an Extended LCtrl + +The purpose of this class is to encapsulate the logic required to deal with this scenario, and assign one code to any key, as AHK does +*/ +namespace AutoHotInterception.HelpersFoo +{ + // Returned to the ProcessStroke function to indicate the ultimate result of processing the stroke(s) + // Can be the result of processing a two-stroke Extended Keycode set + public class TranslatedKey + { + public ushort AhkCode { get; set; } + public List Strokes { get; set; } + public int State { get; set; } + + public TranslatedKey(KeyStroke stroke, bool isExtended) + { + Strokes = new List() { stroke }; + } + } + + // Holds information for a special key to describe how it behaves + public class SpecialKey + { + public string Name { get; } + public ushort Code { get; } + public ExtMode ExtendedMode { get; } + public CodeType CodeType { get; } + public Order StrokeOrder { get; } + + public SpecialKey(string name, ushort code, ExtMode extendedMode, CodeType codeType, Order strokeOrder) + { + // The name of the key + Name = name; + // The code that identifies this key + Code = code; + // What values will be reported for press/release states for this key + ExtendedMode = extendedMode; + // Whether AHK uses a High (+256) or Low code for this key + CodeType = codeType; + // Whether part of two stroke extended set, and if so, which order the strokes come in + StrokeOrder = strokeOrder; + } + } + + // List of all the special keys ahd how they behave + public static class SpecialKeys + { + public static SpecialKey NumpadEnter { get; set; } = new SpecialKey("Numpad Enter", 28, ExtMode.E0, CodeType.High, Order.Normal); + public static SpecialKey RightControl { get; } = new SpecialKey("Right Control", 29, ExtMode.E1, CodeType.High, Order.Normal); + public static SpecialKey NumpadDiv { get; } = new SpecialKey("Numpad Div", 53, ExtMode.E1, CodeType.High, Order.Normal); + public static SpecialKey RightShift { get; set; } = new SpecialKey("Right Shift", 54, ExtMode.E0, CodeType.High, Order.Normal); + public static SpecialKey RightAlt { get; } = new SpecialKey("Right Alt", 56, ExtMode.E1, CodeType.High, Order.Normal); + public static SpecialKey Numlock { get; set; } = new SpecialKey("Numlock", 69, ExtMode.E0, CodeType.High, Order.Normal); + public static SpecialKey Pause { get; set; } = new SpecialKey("Pause", 69, ExtMode.E0, CodeType.Low, Order.Prefixed); + public static SpecialKey Home { get; set; } = new SpecialKey("Home", 71, ExtMode.E1, CodeType.High, Order.Wrapped); + public static SpecialKey Up { get; set; } = new SpecialKey("Up", 72, ExtMode.E1, CodeType.High, Order.Wrapped); + public static SpecialKey PgUp { get; set; } = new SpecialKey("PgUp", 73, ExtMode.E1, CodeType.High, Order.Wrapped); + public static SpecialKey Left { get; set; } = new SpecialKey("Left", 75, ExtMode.E1, CodeType.High, Order.Wrapped); + public static SpecialKey Right { get; set; } = new SpecialKey("Right", 77, ExtMode.E1, CodeType.High, Order.Wrapped); + public static SpecialKey End { get; set; } = new SpecialKey("End", 79, ExtMode.E1, CodeType.High, Order.Wrapped); + public static SpecialKey Down { get; set; } = new SpecialKey("Down", 80, ExtMode.E1, CodeType.High, Order.Wrapped); + public static SpecialKey PgDn { get; set; } = new SpecialKey("PgDn", 81, ExtMode.E1, CodeType.High, Order.Wrapped); + public static SpecialKey Insert { get; set; } = new SpecialKey("Insert", 82, ExtMode.E1, CodeType.High, Order.Wrapped); + public static SpecialKey Delete { get; set; } = new SpecialKey("Delete", 83, ExtMode.E1, CodeType.High, Order.Wrapped); + public static SpecialKey LeftWindows { get; } = new SpecialKey("Left Windows", 91, ExtMode.E1, CodeType.High, Order.Normal); + public static SpecialKey RightWindows { get; } = new SpecialKey("Right Windows", 92, ExtMode.E1, CodeType.High, Order.Normal); + public static SpecialKey Apps { get; } = new SpecialKey("Apps", 93, ExtMode.E1, CodeType.High, Order.Normal); + + public static List List { get; set; } = new List() + { + NumpadEnter, RightControl, NumpadDiv, RightShift, RightAlt, Numlock, Pause, + Home, Up, PgUp, Left, Right, End, Down, PgDn, Insert, Delete, + LeftWindows, RightWindows, Apps + }; + } + + // Whether the AHK ScanCode is Low (same as Interception) or Hight (Interception + 256) + public enum CodeType { Low, High }; + + // Whether Press/Release states are 0/1 (E0), 2/3 (E1) or 4/5 (E2) + public enum ExtMode { E0, E1, E2}; + + // Order of the strokes received + public enum Order { Normal /* Stroke order is Key press, Key release (No Extended Modifier) */ + , Wrapped /* Stroke order is Ext Modifier press, Key press, Key release, Ext Modifier Release */ + , Prefixed /* Stroke order is Ext Modifier press, Key press, Ext Modifier release, Key release */}; + + // Processes strokes and translates them into AHK style code and state + public class ScanCodeHelper + { + private TranslatedKey _translatedKey; + // Converts Interception state to AHK state + private static List _stateConverter = new List() { 1, 0, 1, 0, 1, 0 }; + // Converts state to extended mode + private static List _stateToExtendedMode = new List() { 0, 0, 1, 1, 2, 2 }; + // List of code/states which signify first stroke of a two stroke set + private static HashSet> _extendedCodeAndStates = new HashSet>(); + // Keys which are E0, but AHK still assigns a High (+256) code to them + private Dictionary _highCodes = new Dictionary(); + + public ScanCodeHelper() + { + // Read the SpecialKeys list and build lookup tables to help in logic processing + for (int i = 0; i < SpecialKeys.List.Count; i++) + { + var specialKey = SpecialKeys.List[i]; + if (specialKey.CodeType == CodeType.High) _highCodes.Add(specialKey.Code, specialKey.Name); + // Build list of codes which signify that this is the first stroke of an extended set + if (specialKey.StrokeOrder == Order.Wrapped) + { + _extendedCodeAndStates.Add(new Tuple(42, 2)); // LShift with E1 state on press + _extendedCodeAndStates.Add(new Tuple(specialKey.Code, 3)); // ScanCode with E1 state on release + } + else if (specialKey.StrokeOrder == Order.Prefixed) + { + _extendedCodeAndStates.Add(new Tuple(29, 4)); // LCtrl with E2 state on press + _extendedCodeAndStates.Add(new Tuple(29, 5)); // LCtrl with E2 state on release + } + } + } + + // Process stroke(s) and return ONE TranslatedKey + // May need to be called twice to return a TranslatedKey - it maay return null for the first stroke + public TranslatedKey TranslateScanCode(KeyStroke stroke) + { + if (_extendedCodeAndStates.Contains(new Tuple(stroke.code, stroke.state)) || _translatedKey != null) + { + // Stroke is first key of Extended key sequence of 2 keys + if (_translatedKey == null) + { + // Stroke is first of an Extended key sequence + // Add this stroke to a buffer, so that we can examine it when the next stroke comes through... + // ... and instruct ProcessStroke to take no action for this stroke, as we do not know what the sequence is yet + _translatedKey = new TranslatedKey(stroke, true); + return null; + } + else + { + // Stroke is 2nd of Extended key sequence - we now know what the full sequence is + _translatedKey.Strokes.Add(stroke); + var extMode = _stateToExtendedMode[_translatedKey.Strokes[0].state]; + ushort state; + KeyStroke whichStroke; + switch (extMode) + { + case 0: + throw new Exception("Expecting E1 or E2 state"); + case 1: + // E1 (Home, Up, PgUp, Left, Right, End, Down, PgDn, Insert, Delete) + // Which state to report (1 = press, 0 = release) + state = _stateConverter[_translatedKey.Strokes[0].state]; + // Which code to use depends on whether this is a press or release + // On press, use the second stroke (index 1) + // On release, use the first stroke (index 0) + whichStroke = _translatedKey.Strokes[state]; + _translatedKey.AhkCode = (ushort)(whichStroke.code + 256); + _translatedKey.State = state; + break; + case 2: + // E2 (Pause only) + // Which state to report (1 = press, 0 = release) + state = _stateConverter[_translatedKey.Strokes[0].state]; + // Always use the code of the second stroke (index 1) + whichStroke = _translatedKey.Strokes[1]; + _translatedKey.AhkCode = whichStroke.code; + _translatedKey.State = state; + break; + default: + throw new Exception("state can only be E0, E1 or E2"); + } + } + } + else + { + // Stroke is a single key sequence + _translatedKey = new TranslatedKey(stroke, false); + var code = stroke.code; + if (_highCodes.ContainsKey(code)) + { + code += 256; + } + _translatedKey.AhkCode = code; + _translatedKey.State = _stateConverter[stroke.state]; + } + + // Code will only get here if the stroke was a single key, or the second key of an extended sequence + // Return _translatedKey and clear it, ready for the next key + var returnValue = _translatedKey; + _translatedKey = null; + return returnValue; + } + } +} diff --git a/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs b/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs index 3604283..e732d2a 100644 --- a/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs +++ b/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs @@ -1,207 +1,22 @@ using System; using System.Collections.Generic; -using static AutoHotInterception.Helpers.ManagedWrapper; +using System.Linq; +using System.Text; +using System.Threading.Tasks; -/* -AHK uses a single ScanCode (0...512) to identify each key, whereas Interception uses two codes - the ScanCode, plus the state. -For example: -in Interception - Code 29 with State 0 or 1 is LCtrl, and 29 with state 2/3 (With state of 2/3 meaning "Extended") is RCtrl -In AHK, code 29 is LCtrl, whereas state 285 (29 + 256) - ie "Extended" versions of keys are Interception code + 256 - -Furthermore, for some keys (eg Arrow keys and the 6 keys above the arrow key block - Ins, Del etc), Interception will sometimes receive TWO strokes... -... when one of these keys are pressed - a modifier key (Ctrl or Shift) and the key itself... - -Also, AHK assigns "high" (+256) codes to some keys, even when state is 0/1... -... And also assigns a "low" code to Pause, even though it is wrapped in an Extended LCtrl - -The purpose of this class is to encapsulate the logic required to deal with this scenario, and assign one code to any key, as AHK does -*/ namespace AutoHotInterception.Helpers { - // Returned to the ProcessStroke function to indicate the ultimate result of processing the stroke(s) - // Can be the result of processing a two-stroke Extended Keycode set - public class TranslatedKey - { - public ushort AhkCode { get; set; } - public List Strokes { get; set; } - public int State { get; set; } - - public TranslatedKey(KeyStroke stroke, bool isExtended) - { - Strokes = new List() { stroke }; - } - } - - // Holds information for a special key to describe how it behaves - public class SpecialKey + public class ScanCodeHelper { - public string Name { get; } - public ushort Code { get; } - public ExtMode ExtendedMode { get; } - public CodeType CodeType { get; } - public Order StrokeOrder { get; } - - public SpecialKey(string name, ushort code, ExtMode extendedMode, CodeType codeType, Order strokeOrder) + public TranslatedKey TranslateScanCodes(List strokes) { - // The name of the key - Name = name; - // The code that identifies this key - Code = code; - // What values will be reported for press/release states for this key - ExtendedMode = extendedMode; - // Whether AHK uses a High (+256) or Low code for this key - CodeType = codeType; - // Whether part of two stroke extended set, and if so, which order the strokes come in - StrokeOrder = strokeOrder; + return null; } } - // List of all the special keys ahd how they behave - public static class SpecialKeys - { - public static SpecialKey NumpadEnter { get; set; } = new SpecialKey("Numpad Enter", 28, ExtMode.E0, CodeType.High, Order.Normal); - public static SpecialKey RightControl { get; } = new SpecialKey("Right Control", 29, ExtMode.E1, CodeType.High, Order.Normal); - public static SpecialKey NumpadDiv { get; } = new SpecialKey("Numpad Div", 53, ExtMode.E1, CodeType.High, Order.Normal); - public static SpecialKey RightShift { get; set; } = new SpecialKey("Right Shift", 54, ExtMode.E0, CodeType.High, Order.Normal); - public static SpecialKey RightAlt { get; } = new SpecialKey("Right Alt", 56, ExtMode.E1, CodeType.High, Order.Normal); - public static SpecialKey Numlock { get; set; } = new SpecialKey("Numlock", 69, ExtMode.E0, CodeType.High, Order.Normal); - public static SpecialKey Pause { get; set; } = new SpecialKey("Pause", 69, ExtMode.E0, CodeType.Low, Order.Prefixed); - public static SpecialKey Home { get; set; } = new SpecialKey("Home", 71, ExtMode.E1, CodeType.High, Order.Wrapped); - public static SpecialKey Up { get; set; } = new SpecialKey("Up", 72, ExtMode.E1, CodeType.High, Order.Wrapped); - public static SpecialKey PgUp { get; set; } = new SpecialKey("PgUp", 73, ExtMode.E1, CodeType.High, Order.Wrapped); - public static SpecialKey Left { get; set; } = new SpecialKey("Left", 75, ExtMode.E1, CodeType.High, Order.Wrapped); - public static SpecialKey Right { get; set; } = new SpecialKey("Right", 77, ExtMode.E1, CodeType.High, Order.Wrapped); - public static SpecialKey End { get; set; } = new SpecialKey("End", 79, ExtMode.E1, CodeType.High, Order.Wrapped); - public static SpecialKey Down { get; set; } = new SpecialKey("Down", 80, ExtMode.E1, CodeType.High, Order.Wrapped); - public static SpecialKey PgDn { get; set; } = new SpecialKey("PgDn", 81, ExtMode.E1, CodeType.High, Order.Wrapped); - public static SpecialKey Insert { get; set; } = new SpecialKey("Insert", 82, ExtMode.E1, CodeType.High, Order.Wrapped); - public static SpecialKey Delete { get; set; } = new SpecialKey("Delete", 83, ExtMode.E1, CodeType.High, Order.Wrapped); - public static SpecialKey LeftWindows { get; } = new SpecialKey("Left Windows", 91, ExtMode.E1, CodeType.High, Order.Normal); - public static SpecialKey RightWindows { get; } = new SpecialKey("Right Windows", 92, ExtMode.E1, CodeType.High, Order.Normal); - public static SpecialKey Apps { get; } = new SpecialKey("Apps", 93, ExtMode.E1, CodeType.High, Order.Normal); - - public static List List { get; set; } = new List() - { - NumpadEnter, RightControl, NumpadDiv, RightShift, RightAlt, Numlock, Pause, - Home, Up, PgUp, Left, Right, End, Down, PgDn, Insert, Delete, - LeftWindows, RightWindows, Apps - }; - } - - // Whether the AHK ScanCode is Low (same as Interception) or Hight (Interception + 256) - public enum CodeType { Low, High }; - - // Whether Press/Release states are 0/1 (E0), 2/3 (E1) or 4/5 (E2) - public enum ExtMode { E0, E1, E2}; - - // Order of the strokes received - public enum Order { Normal /* Stroke order is Key press, Key release (No Extended Modifier) */ - , Wrapped /* Stroke order is Ext Modifier press, Key press, Key release, Ext Modifier Release */ - , Prefixed /* Stroke order is Ext Modifier press, Key press, Ext Modifier release, Key release */}; - - // Processes strokes and translates them into AHK style code and state - public class ScanCodeHelper + public class TranslatedKey { - private TranslatedKey _translatedKey; - // Converts Interception state to AHK state - private static List _stateConverter = new List() { 1, 0, 1, 0, 1, 0 }; - // Converts state to extended mode - private static List _stateToExtendedMode = new List() { 0, 0, 1, 1, 2, 2 }; - // List of code/states which signify first stroke of a two stroke set - private static HashSet> _extendedCodeAndStates = new HashSet>(); - // Keys which are E0, but AHK still assigns a High (+256) code to them - private Dictionary _highCodes = new Dictionary(); - - public ScanCodeHelper() - { - // Read the SpecialKeys list and build lookup tables to help in logic processing - for (int i = 0; i < SpecialKeys.List.Count; i++) - { - var specialKey = SpecialKeys.List[i]; - if (specialKey.CodeType == CodeType.High) _highCodes.Add(specialKey.Code, specialKey.Name); - // Build list of codes which signify that this is the first stroke of an extended set - if (specialKey.StrokeOrder == Order.Wrapped) - { - _extendedCodeAndStates.Add(new Tuple(42, 2)); // LShift with E1 state on press - _extendedCodeAndStates.Add(new Tuple(specialKey.Code, 3)); // ScanCode with E1 state on release - } - else if (specialKey.StrokeOrder == Order.Prefixed) - { - _extendedCodeAndStates.Add(new Tuple(29, 4)); // LCtrl with E2 state on press - _extendedCodeAndStates.Add(new Tuple(29, 5)); // LCtrl with E2 state on release - } - } - } - - // Process stroke(s) and return ONE TranslatedKey - // May need to be called twice to return a TranslatedKey - it maay return null for the first stroke - public TranslatedKey TranslateScanCode(KeyStroke stroke) - { - if (_extendedCodeAndStates.Contains(new Tuple(stroke.code, stroke.state)) || _translatedKey != null) - { - // Stroke is first key of Extended key sequence of 2 keys - if (_translatedKey == null) - { - // Stroke is first of an Extended key sequence - // Add this stroke to a buffer, so that we can examine it when the next stroke comes through... - // ... and instruct ProcessStroke to take no action for this stroke, as we do not know what the sequence is yet - _translatedKey = new TranslatedKey(stroke, true); - return null; - } - else - { - // Stroke is 2nd of Extended key sequence - we now know what the full sequence is - _translatedKey.Strokes.Add(stroke); - var extMode = _stateToExtendedMode[_translatedKey.Strokes[0].state]; - ushort state; - KeyStroke whichStroke; - switch (extMode) - { - case 0: - throw new Exception("Expecting E1 or E2 state"); - case 1: - // E1 (Home, Up, PgUp, Left, Right, End, Down, PgDn, Insert, Delete) - // Which state to report (1 = press, 0 = release) - state = _stateConverter[_translatedKey.Strokes[0].state]; - // Which code to use depends on whether this is a press or release - // On press, use the second stroke (index 1) - // On release, use the first stroke (index 0) - whichStroke = _translatedKey.Strokes[state]; - _translatedKey.AhkCode = (ushort)(whichStroke.code + 256); - _translatedKey.State = state; - break; - case 2: - // E2 (Pause only) - // Which state to report (1 = press, 0 = release) - state = _stateConverter[_translatedKey.Strokes[0].state]; - // Always use the code of the second stroke (index 1) - whichStroke = _translatedKey.Strokes[1]; - _translatedKey.AhkCode = whichStroke.code; - _translatedKey.State = state; - break; - default: - throw new Exception("state can only be E0, E1 or E2"); - } - } - } - else - { - // Stroke is a single key sequence - _translatedKey = new TranslatedKey(stroke, false); - var code = stroke.code; - if (_highCodes.ContainsKey(code)) - { - code += 256; - } - _translatedKey.AhkCode = code; - _translatedKey.State = _stateConverter[stroke.state]; - } - - // Code will only get here if the stroke was a single key, or the second key of an extended sequence - // Return _translatedKey and clear it, ready for the next key - var returnValue = _translatedKey; - _translatedKey = null; - return returnValue; - } + public ushort AhkCode { get; set; } + public int State { get; set; } } } diff --git a/C#/UnitTests/ScanCodeHelperTests.cs b/C#/UnitTests/ScanCodeHelperTests.cs new file mode 100644 index 0000000..66e76c0 --- /dev/null +++ b/C#/UnitTests/ScanCodeHelperTests.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AutoHotInterception.Helpers; +using NUnit.Framework; +using static AutoHotInterception.Helpers.ManagedWrapper; + +namespace UnitTests +{ + public class TestKey + { + public string Name { get; } + public List PressStrokes { get; } + public List ReleaseStrokes { get; } + public ExpectedResult PressResult { get; } + public ExpectedResult ReleaseResult { get; } + + public TestKey(string name, List pressStrokes, List releaseStrokes, + ExpectedResult pressResult, ExpectedResult releaseResult) + { + Name = name; + PressStrokes = pressStrokes; + ReleaseStrokes = releaseStrokes; + PressResult = pressResult; + ReleaseResult = releaseResult; + } + } + + public class ExpectedResult + { + public ushort Code { get; } + public ushort State { get; } + + public ExpectedResult(ushort code, ushort state) + { + Code = code; + State = state; + } + } + + [TestFixture] + class ScanCodeHelperTests + { + ScanCodeHelper sch; + private static List _testKeys = new List() + { + new TestKey("Numpad Enter", Stroke(28, 0), Stroke(28, 1), Result(284, 1), Result(284, 0)), + new TestKey("Right Control", Stroke(29, 2), Stroke(29, 3), Result(285, 1), Result(285, 0)), + new TestKey("Numpad Div", Stroke(53, 2), Stroke(53, 3), Result(309, 1), Result(309, 0)), + new TestKey("Right Shift", Stroke(54, 0), Stroke(54, 1), Result(310, 1), Result(310, 0)), + new TestKey("Right Alt", Stroke(56, 2), Stroke(56, 3), Result(312, 1), Result(312, 0)), + new TestKey("Numlock", Stroke(69, 0), Stroke(69, 1), Result(325, 1), Result(325, 0)), + new TestKey("Pause", Stroke(29, 4, 69, 0), Stroke(29, 5, 69, 1), Result(69, 1), Result(69, 0)), + new TestKey("Home", Stroke(42, 2, 71, 2), Stroke(71, 3, 42, 3), Result(327, 1), Result(327, 0)), + new TestKey("Up", Stroke(42, 2, 72, 2), Stroke(72, 3, 42, 3), Result(328, 1), Result(328, 0)), + new TestKey("PgUp", Stroke(42, 2, 73, 2), Stroke(73, 3, 42, 3), Result(329, 1), Result(329, 0)), + new TestKey("Left", Stroke(42, 2, 75, 2), Stroke(75, 3, 42, 3), Result(331, 1), Result(331, 0)), + new TestKey("Right", Stroke(42, 2, 77, 2), Stroke(77, 3, 42, 3), Result(333, 1), Result(333, 0)), + new TestKey("End", Stroke(42, 2, 79, 2), Stroke(79, 3, 42, 3), Result(335, 1), Result(335, 0)), + new TestKey("Down", Stroke(42, 2, 80, 2), Stroke(80, 3, 42, 3), Result(336, 1), Result(336, 0)), + new TestKey("PgDn", Stroke(42, 2, 81, 2), Stroke(81, 3, 42, 3), Result(337, 1), Result(337, 0)), + new TestKey("Insert", Stroke(42, 2, 82, 2), Stroke(82, 3, 42, 3), Result(338, 1), Result(338, 0)), + new TestKey("Delete", Stroke(42, 2, 83, 2), Stroke(83, 3, 42, 3), Result(339, 1), Result(339, 0)), + new TestKey("Left Windows", Stroke(91, 2), Stroke(91, 3), Result(347, 1), Result(347, 0)), + new TestKey("Right Windows", Stroke(92, 2), Stroke(92, 3), Result(348, 1), Result(348, 0)), + new TestKey("Apps", Stroke(93, 2), Stroke(93, 3), Result(349, 1), Result(349, 0)), + + new TestKey("Delete", Stroke(83, 2), Stroke(83, 3), Result(339, 1), Result(339, 0)), + }; + + private static List Stroke(ushort code1, ushort state1, ushort code2 = 0, ushort state2 = 0) + { + var strokes = new List(); + strokes.Add(new Stroke() { key = { code = code1, state = state1 } }); + if (code2 != 0) + { + strokes.Add(new Stroke() { key = { code = code2, state = state2 } }); + } + return strokes; + } + + private static ExpectedResult Result(ushort code, ushort state) + { + var results = new ExpectedResult(code, state); + return results; + } + + [SetUp] + public void SetUpBeforeEachTest() + { + sch = new ScanCodeHelper(); + } + + [Test] + public void PressReleaseTests() + { + DoTest(_testKeys[0]); + //foreach (var testKey in _testKeys) + //{ + // DoTest(testKey); + //} + } + + private void DoTest(TestKey testKey) + { + Debug.WriteLine($"\nTesting key {testKey.Name}..."); + Debug.WriteLine("Testing Press"); + var expectedResult = testKey.PressResult; + var actualResult = sch.TranslateScanCodes(testKey.PressStrokes); + AssertResult(actualResult, expectedResult); + + //for (int i = 0; i < testKey.PressStrokes.Count; i++) + //{ + // var stroke = testKey.PressStrokes[i].key; + // Debug.WriteLine($"Sending stroke #{i + 1} with code {stroke.code}, state {stroke.state}"); + // var expectedResult = testKey.PressResults[i]; + // var actualResult = sch.TranslateScanCode(stroke); + // AssertResult(actualResult, expectedResult); + //} + + //Debug.WriteLine("Testing Release"); + //for (int i = 0; i < testKey.ReleaseStrokes.Count; i++) + //{ + // var stroke = testKey.ReleaseStrokes[i].key; + // Debug.WriteLine($"Sending stroke #{i + 1} with code {stroke.code}, state {stroke.state}"); + // var expectedResult = testKey.ReleaseResults[i]; + // var actualResult = sch.TranslateScanCode(stroke); + // AssertResult(actualResult, expectedResult); + //} + Debug.WriteLine("OK!"); + } + + void AssertResult(TranslatedKey actualResult, ExpectedResult expectedResult) + { + if (expectedResult == null) + { + Debug.WriteLine($"Expecting result of null"); + Assert.That(actualResult == null, "Result should be null"); + } + else + { + Debug.WriteLine($"Expecting code of {expectedResult.Code}, state of {expectedResult.State}"); + Assert.That(actualResult != null, "Result should not be null"); + Assert.That(actualResult.AhkCode, Is.EqualTo(expectedResult.Code), $"Code should be {expectedResult.Code}, got {actualResult.AhkCode}"); + Assert.That(actualResult.State, Is.EqualTo(expectedResult.State), $"State should be {expectedResult.State}, got {actualResult.State}"); + } + } + } +} diff --git a/C#/UnitTests/TestClass.cs b/C#/UnitTests/TestClass.cs index e0fedd7..ddf8f17 100644 --- a/C#/UnitTests/TestClass.cs +++ b/C#/UnitTests/TestClass.cs @@ -8,7 +8,7 @@ using AutoHotInterception.Helpers; using NUnit.Framework; using static AutoHotInterception.Helpers.ManagedWrapper; -namespace UnitTests +namespace UnitTestsFoo { public class TestKey { @@ -42,9 +42,9 @@ namespace UnitTests } [TestFixture] - public class ScanCodeHelperTests + public class OldScanCodeHelperTests { - ScanCodeHelper sch; + //ScanCodeHelper sch; private static List _testKeys = new List() { new TestKey("Numpad Enter", Stroke(28, 0), Stroke(28, 1), Result(284, 1), Result(284, 0)), @@ -67,12 +67,14 @@ namespace UnitTests new TestKey("Left Windows", Stroke(91, 2), Stroke(91, 3), Result(347, 1), Result(347, 0)), new TestKey("Right Windows", Stroke(92, 2), Stroke(92, 3), Result(348, 1), Result(348, 0)), new TestKey("Apps", Stroke(93, 2), Stroke(93, 3), Result(349, 1), Result(349, 0)), + + new TestKey("Delete", Stroke(83, 2), Stroke(83, 3), Result(339, 1), Result(339, 0)), }; [SetUp] public void SetUpBeforeEachTest() { - sch = new ScanCodeHelper(); + //sch = new ScanCodeHelper(); } private static List Stroke (ushort code1, ushort state1, ushort code2 = 0, ushort state2 = 0) @@ -99,10 +101,11 @@ namespace UnitTests [Test] public void PressReleaseTests() { - foreach (var testKey in _testKeys) - { - DoTest(testKey); - } + DoTest(_testKeys[20]); + //foreach (var testKey in _testKeys) + //{ + // DoTest(testKey); + //} } private void DoTest(TestKey testKey) @@ -114,8 +117,8 @@ namespace UnitTests var stroke = testKey.PressStrokes[i]; Debug.WriteLine($"Sending stroke #{i+1} with code {stroke.code}, state {stroke.state}"); var expectedResult = testKey.PressResults[i]; - var actualResult = sch.TranslateScanCode(stroke); - AssertResult(actualResult, expectedResult); + //var actualResult = sch.TranslateScanCode(stroke); + //AssertResult(actualResult, expectedResult); } Debug.WriteLine("Testing Release"); @@ -124,8 +127,8 @@ namespace UnitTests var stroke = testKey.ReleaseStrokes[i]; Debug.WriteLine($"Sending stroke #{i+1} with code {stroke.code}, state {stroke.state}"); var expectedResult = testKey.ReleaseResults[i]; - var actualResult = sch.TranslateScanCode(stroke); - AssertResult(actualResult, expectedResult); + //var actualResult = sch.TranslateScanCode(stroke); + //AssertResult(actualResult, expectedResult); } Debug.WriteLine("OK!"); } diff --git a/C#/UnitTests/UnitTests.csproj b/C#/UnitTests/UnitTests.csproj index 5528cda..04aaa22 100644 --- a/C#/UnitTests/UnitTests.csproj +++ b/C#/UnitTests/UnitTests.csproj @@ -40,6 +40,7 @@ + From 18cf00441d0ac2acace8a75538f4aa89a3888811 Mon Sep 17 00:00:00 2001 From: Clive Galway Date: Tue, 18 Jan 2022 19:29:30 +0000 Subject: [PATCH 12/33] Re-implement ScanCodeHelper and Unit Tests --- .../Helpers/ScanCodeHelper.cs | 130 +++++++++++++++++- C#/UnitTests/ScanCodeHelperTests.cs | 49 ++----- 2 files changed, 138 insertions(+), 41 deletions(-) diff --git a/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs b/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs index e732d2a..98a1dab 100644 --- a/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs +++ b/C#/AutoHotInterception/Helpers/ScanCodeHelper.cs @@ -1,22 +1,138 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using static AutoHotInterception.Helpers.ManagedWrapper; namespace AutoHotInterception.Helpers { + // Order of the strokes received + public enum Order + { + Normal, // Stroke order is Key press, Key release (No Extended Modifier) + Wrapped, // Stroke order is Ext Modifier press, Key press, Key release, Ext Modifier Release + Prefixed // Stroke order is Ext Modifier press, Key press, Ext Modifier release, Key release + }; + public class ScanCodeHelper { - public TranslatedKey TranslateScanCodes(List strokes) + // Converts Interception state to AHK state + private static List _stateConverter = new List() { 1, 0, 1, 0, 1, 0 }; + // Converts state to extended mode + private static List _stateToExtendedMode = new List() { 0, 0, 1, 1, 2, 2 }; + + // Keys which have an E0 state, but AHK uses High (+256) code + private HashSet _highCodeE0Keys = new HashSet() + { + 28, // Numpad Enter + 54, // Right Shift + 69, // Numlock + }; + + // List of two-stroke keys, used to build _twoStrokeKeyConverter + private static Dictionary _twoStrokeKeys = new Dictionary() + { + { 69, Order.Prefixed }, // Pause + { 71, Order.Wrapped }, // Home + { 72, Order.Wrapped }, // Up + { 73, Order.Wrapped }, // PgUp + { 75, Order.Wrapped }, // Left + { 77, Order.Wrapped }, // Right + { 79, Order.Wrapped }, // End + { 80, Order.Wrapped }, // Down + { 81, Order.Wrapped }, // PgDn + { 82, Order.Wrapped }, // Insert + { 83, Order.Wrapped }, // Delete + }; + + // Lookup table to convert two-stroke keys to code and state + private static Dictionary, TranslatedKey> + _twoStrokeKeyConverter = new Dictionary, TranslatedKey>(); + + public ScanCodeHelper() + { + foreach (var item in _twoStrokeKeys) + { + var twoStrokeKey = new TwoStrokeKey(item.Key, item.Value); + _twoStrokeKeyConverter.Add(twoStrokeKey.PressTuple, twoStrokeKey.PressKey); + _twoStrokeKeyConverter.Add(twoStrokeKey.ReleaseTuple, twoStrokeKey.ReleaseKey); + } + } + + public TranslatedKey TranslateScanCodes(List strokes) { - return null; + if (strokes.Count == 2) + { + return _twoStrokeKeyConverter[ + new Tuple(strokes[0].key.code, strokes[0].key.state, strokes[1].key.code, strokes[1].key.state)]; + } + else if (strokes.Count == 1) + { + var stroke = strokes[0]; + var code = stroke.key.code; + var state = _stateConverter[stroke.key.state]; + if (_highCodeE0Keys.Contains(stroke.key.code)) + { + // Stroke is E0, but AHK code is High (+256) + code += 256; + } + else + { + if (_stateToExtendedMode[stroke.key.state] > 0) + { + // Stroke is E1 or E2 + code += 256; + } + } + return new TranslatedKey(code, state); + } + else + { + throw new Exception($"Expected 1 or 2 strokes, but got {strokes.Count}"); + } } } + // Holds the AHK code and state equivalent of a one or two-stroke set public class TranslatedKey { - public ushort AhkCode { get; set; } - public int State { get; set; } + public ushort AhkCode { get; } + public int State { get; } + + public TranslatedKey(ushort code, int state) + { + AhkCode = code; + State = state; + } + } + + // Builds entries for _twoStrokeKeyConverter + public class TwoStrokeKey + { + public Tuple PressTuple { get; } + public Tuple ReleaseTuple { get; } + public TranslatedKey PressKey { get; } + public TranslatedKey ReleaseKey { get; } + + public TwoStrokeKey(ushort code, Order order) + { + if (order == Order.Prefixed) + { + PressTuple = new Tuple(29, 4, code, 0); + ReleaseTuple = new Tuple(29, 5, code, 1); + PressKey = new TranslatedKey(code, 1); + ReleaseKey = new TranslatedKey(code, 0); + } + else if (order == Order.Wrapped) + { + PressTuple = new Tuple(42, 2, code, 2); + ReleaseTuple = new Tuple(code, 3, 42, 3); + code += 256; + PressKey = new TranslatedKey(code, 1); + ReleaseKey = new TranslatedKey(code, 0); + } + else + { + throw new Exception("Is not a two-stroke key"); + } + } } } diff --git a/C#/UnitTests/ScanCodeHelperTests.cs b/C#/UnitTests/ScanCodeHelperTests.cs index 66e76c0..901cb88 100644 --- a/C#/UnitTests/ScanCodeHelperTests.cs +++ b/C#/UnitTests/ScanCodeHelperTests.cs @@ -97,11 +97,14 @@ namespace UnitTests [Test] public void PressReleaseTests() { - DoTest(_testKeys[0]); - //foreach (var testKey in _testKeys) - //{ - // DoTest(testKey); - //} + //DoTest(_testKeys[0]); // Numpad Enter + //DoTest(_testKeys[6]); // Pause + //DoTest(_testKeys[5]); // Numlock + //DoTest(_testKeys[7]); // Home + foreach (var testKey in _testKeys) + { + DoTest(testKey); + } } private void DoTest(TestKey testKey) @@ -112,41 +115,19 @@ namespace UnitTests var actualResult = sch.TranslateScanCodes(testKey.PressStrokes); AssertResult(actualResult, expectedResult); - //for (int i = 0; i < testKey.PressStrokes.Count; i++) - //{ - // var stroke = testKey.PressStrokes[i].key; - // Debug.WriteLine($"Sending stroke #{i + 1} with code {stroke.code}, state {stroke.state}"); - // var expectedResult = testKey.PressResults[i]; - // var actualResult = sch.TranslateScanCode(stroke); - // AssertResult(actualResult, expectedResult); - //} + Debug.WriteLine("Testing Release"); + expectedResult = testKey.ReleaseResult; + actualResult = sch.TranslateScanCodes(testKey.ReleaseStrokes); + AssertResult(actualResult, expectedResult); - //Debug.WriteLine("Testing Release"); - //for (int i = 0; i < testKey.ReleaseStrokes.Count; i++) - //{ - // var stroke = testKey.ReleaseStrokes[i].key; - // Debug.WriteLine($"Sending stroke #{i + 1} with code {stroke.code}, state {stroke.state}"); - // var expectedResult = testKey.ReleaseResults[i]; - // var actualResult = sch.TranslateScanCode(stroke); - // AssertResult(actualResult, expectedResult); - //} Debug.WriteLine("OK!"); } void AssertResult(TranslatedKey actualResult, ExpectedResult expectedResult) { - if (expectedResult == null) - { - Debug.WriteLine($"Expecting result of null"); - Assert.That(actualResult == null, "Result should be null"); - } - else - { - Debug.WriteLine($"Expecting code of {expectedResult.Code}, state of {expectedResult.State}"); - Assert.That(actualResult != null, "Result should not be null"); - Assert.That(actualResult.AhkCode, Is.EqualTo(expectedResult.Code), $"Code should be {expectedResult.Code}, got {actualResult.AhkCode}"); - Assert.That(actualResult.State, Is.EqualTo(expectedResult.State), $"State should be {expectedResult.State}, got {actualResult.State}"); - } + Debug.WriteLine($"Expecting code of {expectedResult.Code}, state of {expectedResult.State}"); + Assert.That(actualResult.AhkCode, Is.EqualTo(expectedResult.Code), $"Code should be {expectedResult.Code}, got {actualResult.AhkCode}"); + Assert.That(actualResult.State, Is.EqualTo(expectedResult.State), $"State should be {expectedResult.State}, got {actualResult.State}"); } } } From b4cd165a09813f0cb2fea8330dc19102c7de2026 Mon Sep 17 00:00:00 2001 From: Clive Galway Date: Tue, 18 Jan 2022 20:47:42 +0000 Subject: [PATCH 13/33] Parameterized Unit Tests, add Home Keys in E0 mode --- C#/UnitTests/ScanCodeHelperTests.cs | 102 +++++++++++++--------------- C#/UnitTests/UnitTests.csproj | 4 +- C#/UnitTests/packages.config | 2 +- 3 files changed, 49 insertions(+), 59 deletions(-) diff --git a/C#/UnitTests/ScanCodeHelperTests.cs b/C#/UnitTests/ScanCodeHelperTests.cs index 901cb88..f1b45a0 100644 --- a/C#/UnitTests/ScanCodeHelperTests.cs +++ b/C#/UnitTests/ScanCodeHelperTests.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using AutoHotInterception.Helpers; using NUnit.Framework; using static AutoHotInterception.Helpers.ManagedWrapper; @@ -44,32 +40,7 @@ namespace UnitTests [TestFixture] class ScanCodeHelperTests { - ScanCodeHelper sch; - private static List _testKeys = new List() - { - new TestKey("Numpad Enter", Stroke(28, 0), Stroke(28, 1), Result(284, 1), Result(284, 0)), - new TestKey("Right Control", Stroke(29, 2), Stroke(29, 3), Result(285, 1), Result(285, 0)), - new TestKey("Numpad Div", Stroke(53, 2), Stroke(53, 3), Result(309, 1), Result(309, 0)), - new TestKey("Right Shift", Stroke(54, 0), Stroke(54, 1), Result(310, 1), Result(310, 0)), - new TestKey("Right Alt", Stroke(56, 2), Stroke(56, 3), Result(312, 1), Result(312, 0)), - new TestKey("Numlock", Stroke(69, 0), Stroke(69, 1), Result(325, 1), Result(325, 0)), - new TestKey("Pause", Stroke(29, 4, 69, 0), Stroke(29, 5, 69, 1), Result(69, 1), Result(69, 0)), - new TestKey("Home", Stroke(42, 2, 71, 2), Stroke(71, 3, 42, 3), Result(327, 1), Result(327, 0)), - new TestKey("Up", Stroke(42, 2, 72, 2), Stroke(72, 3, 42, 3), Result(328, 1), Result(328, 0)), - new TestKey("PgUp", Stroke(42, 2, 73, 2), Stroke(73, 3, 42, 3), Result(329, 1), Result(329, 0)), - new TestKey("Left", Stroke(42, 2, 75, 2), Stroke(75, 3, 42, 3), Result(331, 1), Result(331, 0)), - new TestKey("Right", Stroke(42, 2, 77, 2), Stroke(77, 3, 42, 3), Result(333, 1), Result(333, 0)), - new TestKey("End", Stroke(42, 2, 79, 2), Stroke(79, 3, 42, 3), Result(335, 1), Result(335, 0)), - new TestKey("Down", Stroke(42, 2, 80, 2), Stroke(80, 3, 42, 3), Result(336, 1), Result(336, 0)), - new TestKey("PgDn", Stroke(42, 2, 81, 2), Stroke(81, 3, 42, 3), Result(337, 1), Result(337, 0)), - new TestKey("Insert", Stroke(42, 2, 82, 2), Stroke(82, 3, 42, 3), Result(338, 1), Result(338, 0)), - new TestKey("Delete", Stroke(42, 2, 83, 2), Stroke(83, 3, 42, 3), Result(339, 1), Result(339, 0)), - new TestKey("Left Windows", Stroke(91, 2), Stroke(91, 3), Result(347, 1), Result(347, 0)), - new TestKey("Right Windows", Stroke(92, 2), Stroke(92, 3), Result(348, 1), Result(348, 0)), - new TestKey("Apps", Stroke(93, 2), Stroke(93, 3), Result(349, 1), Result(349, 0)), - - new TestKey("Delete", Stroke(83, 2), Stroke(83, 3), Result(339, 1), Result(339, 0)), - }; + ScanCodeHelper sch = new ScanCodeHelper(); private static List Stroke(ushort code1, ushort state1, ushort code2 = 0, ushort state2 = 0) { @@ -88,41 +59,60 @@ namespace UnitTests return results; } - [SetUp] - public void SetUpBeforeEachTest() - { - sch = new ScanCodeHelper(); - } - - [Test] - public void PressReleaseTests() + [Test, TestCaseSource("TestKeyProvider")] + public void PressRelease(string name, List pressStrokes, List releaseStrokes, ExpectedResult pressResult, ExpectedResult releaseResult ) { - //DoTest(_testKeys[0]); // Numpad Enter - //DoTest(_testKeys[6]); // Pause - //DoTest(_testKeys[5]); // Numlock - //DoTest(_testKeys[7]); // Home - foreach (var testKey in _testKeys) - { - DoTest(testKey); - } - } - - private void DoTest(TestKey testKey) - { - Debug.WriteLine($"\nTesting key {testKey.Name}..."); + Debug.WriteLine($"\nTesting key {name}..."); Debug.WriteLine("Testing Press"); - var expectedResult = testKey.PressResult; - var actualResult = sch.TranslateScanCodes(testKey.PressStrokes); + var expectedResult = pressResult; + var actualResult = sch.TranslateScanCodes(pressStrokes); AssertResult(actualResult, expectedResult); Debug.WriteLine("Testing Release"); - expectedResult = testKey.ReleaseResult; - actualResult = sch.TranslateScanCodes(testKey.ReleaseStrokes); + expectedResult = releaseResult; + actualResult = sch.TranslateScanCodes(releaseStrokes); AssertResult(actualResult, expectedResult); Debug.WriteLine("OK!"); } + private static IEnumerable TestKeyProvider() + { + yield return new TestCaseData("Numpad Enter", Stroke(28, 0), Stroke(28, 1), Result(284, 1), Result(284, 0)); + yield return new TestCaseData("Right Control", Stroke(29, 2), Stroke(29, 3), Result(285, 1), Result(285, 0)); + yield return new TestCaseData("Numpad Div", Stroke(53, 2), Stroke(53, 3), Result(309, 1), Result(309, 0)); + yield return new TestCaseData("Right Shift", Stroke(54, 0), Stroke(54, 1), Result(310, 1), Result(310, 0)); + yield return new TestCaseData("Right Alt", Stroke(56, 2), Stroke(56, 3), Result(312, 1), Result(312, 0)); + yield return new TestCaseData("Numlock", Stroke(69, 0), Stroke(69, 1), Result(325, 1), Result(325, 0)); + yield return new TestCaseData("Pause", Stroke(29, 4, 69, 0), Stroke(29, 5, 69, 1), Result(69, 1), Result(69, 0)); + yield return new TestCaseData("Home", Stroke(42, 2, 71, 2), Stroke(71, 3, 42, 3), Result(327, 1), Result(327, 0)); + yield return new TestCaseData("Up", Stroke(42, 2, 72, 2), Stroke(72, 3, 42, 3), Result(328, 1), Result(328, 0)); + yield return new TestCaseData("PgUp", Stroke(42, 2, 73, 2), Stroke(73, 3, 42, 3), Result(329, 1), Result(329, 0)); + yield return new TestCaseData("Left", Stroke(42, 2, 75, 2), Stroke(75, 3, 42, 3), Result(331, 1), Result(331, 0)); + yield return new TestCaseData("Right", Stroke(42, 2, 77, 2), Stroke(77, 3, 42, 3), Result(333, 1), Result(333, 0)); + yield return new TestCaseData("End", Stroke(42, 2, 79, 2), Stroke(79, 3, 42, 3), Result(335, 1), Result(335, 0)); + yield return new TestCaseData("Down", Stroke(42, 2, 80, 2), Stroke(80, 3, 42, 3), Result(336, 1), Result(336, 0)); + yield return new TestCaseData("PgDn", Stroke(42, 2, 81, 2), Stroke(81, 3, 42, 3), Result(337, 1), Result(337, 0)); + yield return new TestCaseData("Insert", Stroke(42, 2, 82, 2), Stroke(82, 3, 42, 3), Result(338, 1), Result(338, 0)); + yield return new TestCaseData("Delete", Stroke(42, 2, 83, 2), Stroke(83, 3, 42, 3), Result(339, 1), Result(339, 0)); + yield return new TestCaseData("Left Windows", Stroke(91, 2), Stroke(91, 3), Result(347, 1), Result(347, 0)); + yield return new TestCaseData("Right Windows", Stroke(92, 2), Stroke(92, 3), Result(348, 1), Result(348, 0)); + yield return new TestCaseData("Apps", Stroke(93, 2), Stroke(93, 3), Result(349, 1), Result(349, 0)); + + // Test Home block in E0 mode (Numlock on) + yield return new TestCaseData("HomeE0", Stroke(71, 2), Stroke(71, 3), Result(327, 1), Result(327, 0)); + yield return new TestCaseData("UpE0", Stroke(72, 2), Stroke(72, 3), Result(328, 1), Result(328, 0)); + yield return new TestCaseData("PgUpE0", Stroke(73, 2), Stroke(73, 3), Result(329, 1), Result(329, 0)); + yield return new TestCaseData("LeftE0", Stroke(75, 2), Stroke(75, 3), Result(331, 1), Result(331, 0)); + yield return new TestCaseData("RightE0", Stroke(77, 2), Stroke(77, 3), Result(333, 1), Result(333, 0)); + yield return new TestCaseData("EndE0", Stroke(79, 2), Stroke(79, 3), Result(335, 1), Result(335, 0)); + yield return new TestCaseData("DownE0", Stroke(80, 2), Stroke(80, 3), Result(336, 1), Result(336, 0)); + yield return new TestCaseData("PgDnE0", Stroke(81, 2), Stroke(81, 3), Result(337, 1), Result(337, 0)); + yield return new TestCaseData("InsertE0", Stroke(82, 2), Stroke(82, 3), Result(338, 1), Result(338, 0)); + yield return new TestCaseData("DeleteE0", Stroke(83, 2), Stroke(83, 3), Result(339, 1), Result(339, 0)); + + } + void AssertResult(TranslatedKey actualResult, ExpectedResult expectedResult) { Debug.WriteLine($"Expecting code of {expectedResult.Code}, state of {expectedResult.State}"); diff --git a/C#/UnitTests/UnitTests.csproj b/C#/UnitTests/UnitTests.csproj index 04aaa22..2ed86e4 100644 --- a/C#/UnitTests/UnitTests.csproj +++ b/C#/UnitTests/UnitTests.csproj @@ -1,6 +1,6 @@  - + @@ -58,7 +58,7 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - +