Merge pull request #86 from evilC/wait-with-timeout-zero

Extended keycode fixes
ahk-v2-support
Clive Galway 2 years ago committed by GitHub
commit b3f76fb2b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -46,6 +46,7 @@
<Compile Include="DeviceHandlers\MouseHandler.cs" />
<Compile Include="Helpers\HelperFunctions.cs" />
<Compile Include="Helpers\ManagedWrapper.cs" />
<Compile Include="Helpers\ScanCodeHelper.cs" />
<Compile Include="MappingOptions.cs" />
<Compile Include="ScanCodeChecker.cs" />
<Compile Include="Manager.cs" />

@ -1,6 +1,7 @@
using AutoHotInterception.Helpers;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
namespace AutoHotInterception.DeviceHandlers
{
@ -37,7 +38,6 @@ namespace AutoHotInterception.DeviceHandlers
if (!mappingOptions.Concurrent && !WorkerThreads.ContainsKey(code))
{
WorkerThreads.TryAdd(code, new WorkerThread());
WorkerThreads[code].Start();
}
_isFiltered = true;
}
@ -68,7 +68,6 @@ namespace AutoHotInterception.DeviceHandlers
if (!mappingOptions.Concurrent && DeviceWorkerThread == null)
{
DeviceWorkerThread = new WorkerThread();
DeviceWorkerThread.Start();
}
_isFiltered = true;
}
@ -83,6 +82,7 @@ namespace AutoHotInterception.DeviceHandlers
if (!AllButtonsMapping.Concurrent && DeviceWorkerThread != null)
{
DeviceWorkerThread.Dispose();
DeviceWorkerThread = null;
}
AllButtonsMapping = null;
DisableFilterIfNeeded();
@ -120,9 +120,9 @@ namespace AutoHotInterception.DeviceHandlers
public abstract void DisableFilterIfNeeded();
/// <summary>
/// Process an incoming stroke
/// Process an incoming stroke, or a pair of extended keycode strokes
/// </summary>
/// <param name="stroke">The stroke to process</param>
public abstract void ProcessStroke(ManagedWrapper.Stroke stroke);
/// <param name="strokes">The stroke(s) to process</param>
public abstract void ProcessStroke(List<ManagedWrapper.Stroke> strokes);
}
}

@ -1,4 +1,5 @@
using AutoHotInterception.Helpers;
using System.Collections.Generic;
namespace AutoHotInterception.DeviceHandlers
{
@ -53,9 +54,9 @@ namespace AutoHotInterception.DeviceHandlers
void DisableFilterIfNeeded();
/// <summary>
/// Process an incoming stroke
/// Process an incoming stroke, or a pair of extended keycode strokes
/// </summary>
/// <param name="stroke">The stroke to process</param>
void ProcessStroke(ManagedWrapper.Stroke stroke);
/// <param name="strokes">The stroke(s) to process</param>
void ProcessStroke(List<ManagedWrapper.Stroke> strokes);
}
}

@ -1,6 +1,7 @@
using AutoHotInterception.Helpers;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
namespace AutoHotInterception.DeviceHandlers
@ -34,25 +35,18 @@ namespace AutoHotInterception.DeviceHandlers
/// <param name="state">The State to send (1 = pressed, 0 = released)</param>
public void SendKeyEvent(ushort code, int state)
{
var st = 1 - state;
var stroke = new ManagedWrapper.Stroke();
if (code > 255)
var strokes = ScanCodeHelper.TranslateAhkCode(code, state);
for (int i = 0; i < strokes.Count; i++)
{
code -= 256;
if (code != 54) // RShift has > 256 code, but state is 0/1
st += 2;
var stroke = strokes[i];
ManagedWrapper.Send(DeviceContext, DeviceId, ref stroke, 1);
}
stroke.key.code = code;
stroke.key.state = (ushort)st;
ManagedWrapper.Send(DeviceContext, DeviceId, ref stroke, 1);
}
#endregion
// ScanCode notes: https://www.win.tue.nl/~aeb/linux/kbd/scancodes-1.html
public override void ProcessStroke(ManagedWrapper.Stroke stroke)
public override void ProcessStroke(List<ManagedWrapper.Stroke> strokes)
{
//ManagedWrapper.Send(DeviceContext, _deviceId, ref stroke, 1);
var hasSubscription = false;
var hasContext = ContextCallback != null;
@ -62,7 +56,7 @@ namespace AutoHotInterception.DeviceHandlers
if (_isFiltered)
{
var isKeyMapping = false; // True if this is a mapping to a single key, else it would be a mapping to a whole device
var processedState = HelperFunctions.KeyboardStrokeToKeyboardState(stroke);
var processedState = ScanCodeHelper.TranslateScanCodes(strokes);
var code = processedState.Code;
var state = processedState.State;
MappingOptions mapping = null;
@ -81,41 +75,30 @@ namespace AutoHotInterception.DeviceHandlers
if (mapping != null)
{
// Begin translation of incoming key code, state, extended flag etc...
var processMappings = true;
if (processedState.Ignore)
hasSubscription = true;
if (mapping.Block) block = true;
if (mapping.Concurrent)
{
// Set flag to stop Context Mode from firing
hasSubscription = true;
// Set flag to indicate disable mapping processing
processMappings = false;
if (isKeyMapping)
{
ThreadPool.QueueUserWorkItem(threadProc => mapping.Callback(state));
}
else
{
ThreadPool.QueueUserWorkItem(threadProc => mapping.Callback(code, state));
}
}
if (processMappings)
else
{
hasSubscription = true;
if (mapping.Block) block = true;
if (mapping.Concurrent)
//mapping.Callback(code, state);
if (isKeyMapping)
{
if (isKeyMapping)
{
ThreadPool.QueueUserWorkItem(threadProc => mapping.Callback(state));
}
else
{
ThreadPool.QueueUserWorkItem(threadProc => mapping.Callback(code, state));
}
WorkerThreads[code]?.Actions.Add(() => mapping.Callback(state));
}
else
{
if (isKeyMapping)
{
WorkerThreads[code]?.Actions.Add(() => mapping.Callback(state));
}
else
{
DeviceWorkerThread?.Actions.Add(() => mapping.Callback(code, state));
}
DeviceWorkerThread?.Actions.Add(() => mapping.Callback(code, state));
}
}
@ -127,8 +110,12 @@ namespace AutoHotInterception.DeviceHandlers
// ... then set the Context before sending the key
if (!hasSubscription && hasContext) ContextCallback(1);
// Pass the key through to the OS.
ManagedWrapper.Send(DeviceContext, DeviceId, ref stroke, 1);
// Pass the key(s) through to the OS.
for (int i = 0; i < strokes.Count; i++)
{
var stroke = strokes[i];
ManagedWrapper.Send(DeviceContext, DeviceId, ref stroke, 1);
}
// If we are processing Context Mode, then Unset the context variable after sending the key
if (!hasSubscription && hasContext) ContextCallback(0);

@ -1,5 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using AutoHotInterception.Helpers;
@ -45,7 +45,6 @@ namespace AutoHotInterception.DeviceHandlers
if (!mappingOptions.Concurrent && !WorkerThreads.ContainsKey(7))
{
WorkerThreads.TryAdd(7, new WorkerThread());
WorkerThreads[7].Start();
}
_isFiltered = true;
}
@ -75,7 +74,6 @@ namespace AutoHotInterception.DeviceHandlers
if (!mappingOptions.Concurrent && !WorkerThreads.ContainsKey(8))
{
WorkerThreads.TryAdd(8, new WorkerThread());
WorkerThreads[8].Start();
}
_isFiltered = true;
}
@ -153,215 +151,220 @@ namespace AutoHotInterception.DeviceHandlers
}
#endregion
public override void ProcessStroke(ManagedWrapper.Stroke stroke)
public override void ProcessStroke(List<ManagedWrapper.Stroke> strokes)
{
var hasSubscription = false;
var hasContext = ContextCallback != null;
var moveRemoved = false;
var hasMove = false;
var x = stroke.mouse.x;
var y = stroke.mouse.y;
// Process mouse movement
var isAbsolute = (stroke.mouse.flags & (ushort)ManagedWrapper.MouseFlag.MouseMoveAbsolute) ==
(ushort)ManagedWrapper.MouseFlag.MouseMoveAbsolute;
//Determine whether or not to report mouse movement.
// For Relative mode, this is fairly simple - if x and y are both 0, no movement was reported (Since a real mouse never reports x=0/y=0)
// For Absolute mode, x=0/y=0 is reported, but we should limit this to only reporting once...
// ... so when x=0/y=0 is seen in absolute mode, set the flag _absoluteMode00Reported to true and allow it to be reported...
// then on subsequent reports of x=0/y=0 for absolute mode, if _absoluteMode00Reported is already true, then do not report movement...
// ... In absolute mode, when x!=0/y!=0 is received, clear the _absoluteMode00Reported flag
if (isAbsolute)
for (int i = 0; i < strokes.Count; i++)
{
if (x == 0 && y == 0)
var stroke = strokes[i];
var hasSubscription = false;
var hasContext = ContextCallback != null;
var moveRemoved = false;
var hasMove = false;
var x = stroke.mouse.x;
var y = stroke.mouse.y;
// Process mouse movement
var isAbsolute = (stroke.mouse.flags & (ushort)ManagedWrapper.MouseFlag.MouseMoveAbsolute) ==
(ushort)ManagedWrapper.MouseFlag.MouseMoveAbsolute;
//Determine whether or not to report mouse movement.
// For Relative mode, this is fairly simple - if x and y are both 0, no movement was reported (Since a real mouse never reports x=0/y=0)
// For Absolute mode, x=0/y=0 is reported, but we should limit this to only reporting once...
// ... so when x=0/y=0 is seen in absolute mode, set the flag _absoluteMode00Reported to true and allow it to be reported...
// then on subsequent reports of x=0/y=0 for absolute mode, if _absoluteMode00Reported is already true, then do not report movement...
// ... In absolute mode, when x!=0/y!=0 is received, clear the _absoluteMode00Reported flag
if (isAbsolute)
{
if (!_absoluteMode00Reported)
if (x == 0 && y == 0)
{
hasMove = true;
_absoluteMode00Reported = true;
if (!_absoluteMode00Reported)
{
hasMove = true;
_absoluteMode00Reported = true;
}
else
{
hasMove = false;
}
}
else
{
hasMove = false;
hasMove = true;
_absoluteMode00Reported = false;
}
}
else
{
hasMove = true;
_absoluteMode00Reported = false;
hasMove = (x != 0 || y != 0);
}
}
else
{
hasMove = (x != 0 || y != 0);
}
if (hasMove)
{
// Process Absolute Mouse Move
if (isAbsolute)
if (hasMove)
{
if (_mouseMoveAbsoluteMapping != null)
// Process Absolute Mouse Move
if (isAbsolute)
{
var mapping = _mouseMoveAbsoluteMapping;
hasSubscription = true;
//var debugStr = $"AHK| Mouse stroke has absolute move of {x}, {y}...";
if (mapping.Concurrent)
ThreadPool.QueueUserWorkItem(threadProc => mapping.Callback(x, y));
else if (WorkerThreads.ContainsKey(7))
WorkerThreads[7]?.Actions.Add(() => mapping.Callback(x, y));
if (mapping.Block)
if (_mouseMoveAbsoluteMapping != null)
{
moveRemoved = true;
stroke.mouse.x = 0;
stroke.mouse.y = 0;
//debugStr += "Blocking";
}
else
{
//debugStr += "Not Blocking";
}
var mapping = _mouseMoveAbsoluteMapping;
hasSubscription = true;
//var debugStr = $"AHK| Mouse stroke has absolute move of {x}, {y}...";
if (mapping.Concurrent)
ThreadPool.QueueUserWorkItem(threadProc => mapping.Callback(x, y));
else if (WorkerThreads.ContainsKey(7))
WorkerThreads[7]?.Actions.Add(() => mapping.Callback(x, y));
if (mapping.Block)
{
moveRemoved = true;
stroke.mouse.x = 0;
stroke.mouse.y = 0;
//debugStr += "Blocking";
}
else
{
//debugStr += "Not Blocking";
}
//Debug.WriteLine(debugStr);
//Debug.WriteLine(debugStr);
}
}
}
else
{
if (_mouseMoveRelativeMapping != null)
else
{
var mapping = _mouseMoveRelativeMapping;
hasSubscription = true;
//var debugStr = $"AHK| Mouse stroke has relative move of {x}, {y}...";
if (mapping.Concurrent)
ThreadPool.QueueUserWorkItem(threadProc => mapping.Callback(x, y));
else if (WorkerThreads.ContainsKey(8))
WorkerThreads[8]?.Actions.Add(() => mapping.Callback(x, y));
if (mapping.Block)
if (_mouseMoveRelativeMapping != null)
{
moveRemoved = true;
stroke.mouse.x = 0;
stroke.mouse.y = 0;
//debugStr += "Blocking";
}
else
{
//debugStr += "Not Blocking";
}
var mapping = _mouseMoveRelativeMapping;
hasSubscription = true;
//var debugStr = $"AHK| Mouse stroke has relative move of {x}, {y}...";
if (mapping.Concurrent)
ThreadPool.QueueUserWorkItem(threadProc => mapping.Callback(x, y));
else if (WorkerThreads.ContainsKey(8))
WorkerThreads[8]?.Actions.Add(() => mapping.Callback(x, y));
if (mapping.Block)
{
moveRemoved = true;
stroke.mouse.x = 0;
stroke.mouse.y = 0;
//debugStr += "Blocking";
}
else
{
//debugStr += "Not Blocking";
}
//Debug.WriteLine(debugStr);
//Debug.WriteLine(debugStr);
}
}
}
}
}
var isMouseButtonsMapping = AllButtonsMapping != null;
var isMouseButtonsMapping = AllButtonsMapping != null;
// Process Mouse Buttons - do this AFTER mouse movement, so that absolute mode has coordinates available at the point that the button callback is fired
if (stroke.mouse.state != 0 && SingleButtonMappings.Count > 0 || isMouseButtonsMapping)
{
var btnStates = HelperFunctions.MouseStrokeToButtonStates(stroke);
foreach (var btnState in btnStates)
// Process Mouse Buttons - do this AFTER mouse movement, so that absolute mode has coordinates available at the point that the button callback is fired
if (stroke.mouse.state != 0 && SingleButtonMappings.Count > 0 || isMouseButtonsMapping)
{
if (!isMouseButtonsMapping && !SingleButtonMappings.ContainsKey(btnState.Button))
continue;
hasSubscription = true;
MappingOptions mapping = null;
if (isMouseButtonsMapping)
{
mapping = AllButtonsMapping;
}
else
var btnStates = HelperFunctions.MouseStrokeToButtonStates(stroke);
foreach (var btnState in btnStates)
{
mapping = SingleButtonMappings[btnState.Button];
}
if (!isMouseButtonsMapping && !SingleButtonMappings.ContainsKey(btnState.Button))
continue;
var state = btnState;
if (mapping.Concurrent)
{
hasSubscription = true;
MappingOptions mapping = null;
if (isMouseButtonsMapping)
{
ThreadPool.QueueUserWorkItem(threadProc =>
mapping.Callback(btnState.Button, state.State));
mapping = AllButtonsMapping;
}
else
{
ThreadPool.QueueUserWorkItem(threadProc => mapping.Callback(state.State));
mapping = SingleButtonMappings[btnState.Button];
}
}
else
{
if (isMouseButtonsMapping)
var state = btnState;
if (mapping.Concurrent)
{
DeviceWorkerThread?.Actions
.Add(() => mapping.Callback(btnState.Button, state.State));
if (isMouseButtonsMapping)
{
ThreadPool.QueueUserWorkItem(threadProc =>
mapping.Callback(btnState.Button, state.State));
}
else
{
ThreadPool.QueueUserWorkItem(threadProc => mapping.Callback(state.State));
}
}
else
{
WorkerThreads[btnState.Button]?.Actions
.Add(() => mapping.Callback(state.State));
if (isMouseButtonsMapping)
{
DeviceWorkerThread?.Actions
.Add(() => mapping.Callback(btnState.Button, state.State));
}
else
{
WorkerThreads[btnState.Button]?.Actions
.Add(() => mapping.Callback(state.State));
}
}
}
if (mapping.Block)
{
// Remove the event for this button from the stroke, leaving other button events intact
stroke.mouse.state -= btnState.Flag;
// If we are removing a mouse wheel event, then set rolling to 0 if no mouse wheel event left
if (btnState.Flag == 0x400 || btnState.Flag == 0x800)
if (mapping.Block)
{
if ((stroke.mouse.state & 0x400) != 0x400 &&
(stroke.mouse.state & 0x800) != 0x800)
// Remove the event for this button from the stroke, leaving other button events intact
stroke.mouse.state -= btnState.Flag;
// If we are removing a mouse wheel event, then set rolling to 0 if no mouse wheel event left
if (btnState.Flag == 0x400 || btnState.Flag == 0x800)
{
//Debug.WriteLine("AHK| Removing rolling flag from stroke");
stroke.mouse.rolling = 0;
if ((stroke.mouse.state & 0x400) != 0x400 &&
(stroke.mouse.state & 0x800) != 0x800)
{
//Debug.WriteLine("AHK| Removing rolling flag from stroke");
stroke.mouse.rolling = 0;
}
}
//Debug.WriteLine($"AHK| Removing flag {btnState.Flag} from stoke, leaving state {stroke.mouse.state}");
}
else
{
//Debug.WriteLine($"AHK| Leaving flag {btnState.Flag} in stroke");
}
}
}
//Debug.WriteLine($"AHK| Removing flag {btnState.Flag} from stoke, leaving state {stroke.mouse.state}");
// Forward on the stroke if required
if (hasSubscription)
{
// Subscription mode
// If the stroke has a move that was not removed, OR it has remaining button events, then forward on the stroke
if ((hasMove && !moveRemoved) || stroke.mouse.state != 0)
{
//Debug.WriteLine($"AHK| Sending stroke. State = {stroke.mouse.state}. hasMove={hasMove}, moveRemoved={moveRemoved}");
ManagedWrapper.Send(DeviceContext, DeviceId, ref stroke, 1);
}
else
{
//Debug.WriteLine($"AHK| Leaving flag {btnState.Flag} in stroke");
// Everything removed from stroke, do not forward
//Debug.WriteLine("AHK| Mouse stroke now empty, not forwarding");
}
}
}
// Forward on the stroke if required
if (hasSubscription)
{
// Subscription mode
// If the stroke has a move that was not removed, OR it has remaining button events, then forward on the stroke
if ((hasMove && !moveRemoved) || stroke.mouse.state != 0)
else if (hasContext)
{
//Debug.WriteLine($"AHK| Sending stroke. State = {stroke.mouse.state}. hasMove={hasMove}, moveRemoved={moveRemoved}");
// Context Mode - forward stroke with context wrapping
ContextCallback(1);
ManagedWrapper.Send(DeviceContext, DeviceId, ref stroke, 1);
ContextCallback(0);
}
else
{
// Everything removed from stroke, do not forward
//Debug.WriteLine("AHK| Mouse stroke now empty, not forwarding");
// No subscription or context mode - forward on
//Debug.WriteLine($"AHK| Sending stroke. State = {stroke.mouse.state}. hasMove={hasMove}, moveRemoved={moveRemoved}");
ManagedWrapper.Send(DeviceContext, DeviceId, ref stroke, 1);
}
}
else if (hasContext)
{
// Context Mode - forward stroke with context wrapping
ContextCallback(1);
ManagedWrapper.Send(DeviceContext, DeviceId, ref stroke, 1);
ContextCallback(0);
}
else
{
// No subscription or context mode - forward on
//Debug.WriteLine($"AHK| Sending stroke. State = {stroke.mouse.state}. hasMove={hasMove}, moveRemoved={moveRemoved}");
ManagedWrapper.Send(DeviceContext, DeviceId, ref stroke, 1);
}
}
}
}

@ -0,0 +1,249 @@
using System;
using System.Collections.Generic;
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 static class ScanCodeHelper
{
// Converts Interception state to AHK state
private static List<ushort> _stateConverter = new List<ushort>() { 1, 0, 1, 0, 1, 0 };
// Converts state to extended mode
private static List<ushort> _stateToExtendedMode = new List<ushort>() { 0, 0, 1, 1, 2, 2 };
// Keys which have an E0 state, but AHK uses High (+256) code
private static HashSet<ushort> _highCodeE0Keys = new HashSet<ushort>()
{
28, // Numpad Enter
54, // Right Shift
69, // Numlock
};
// Keys which have an E0 state, but have no extended modifier
private static HashSet<ushort> _e1Keys = new HashSet<ushort>()
{
29, // Right Control
53, // Numpad Div
56, // Right Alt
91, // Left Windows
92, // Right Windows
93, // Apps
};
// List of two-stroke keys, used to build _twoStrokeKeyConverter
// Also used by SendKeyEvent to work out what extended keys to send
public static readonly Dictionary<ushort, Order> _twoStrokeKeys = new Dictionary<ushort, Order>()
{
{ 55, Order.Wrapped }, // PrtScr
{ 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<Tuple<ushort, ushort, ushort, ushort>, TranslatedKey>
_twoStrokeKeyConverter = new Dictionary<Tuple<ushort, ushort, ushort, ushort>, TranslatedKey>();
static 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 static bool IsDoubleScanCode(List<Stroke> strokes)
{
return _twoStrokeKeyConverter.ContainsKey(new Tuple<ushort, ushort, ushort, ushort>(strokes[0].key.code, strokes[0].key.state, strokes[1].key.code, strokes[1].key.state));
}
/// <summary>
/// Used by ProcessStrokes() KeyboardHandler to translate incoming key(s) from Interception to AHK format
/// </summary>
/// <param name="strokes">A list of one or two Strokes that describe a single key</param>
/// <returns>An AHK ScanCode and State</returns>
public static TranslatedKey TranslateScanCodes(List<Stroke> strokes)
{
if (strokes.Count == 2)
{
return _twoStrokeKeyConverter[
new Tuple<ushort, ushort, ushort, ushort>(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}");
}
}
/// <summary>
/// Used by SendKeyEvent() in KeyboardHandler to translate from AHK code / state into Interception Stroke(s)
/// </summary>
/// <param name="code">The AHK code of the key</param>
/// <param name="state">The AH< state of the key/param>
/// <returns>A list of Strokes to send to simulate this key being pressed</returns>
public static List<Stroke> TranslateAhkCode(ushort code, int ahkState)
{
var strokes = new List<Stroke>();
Order order;
ushort state = (ushort)(1 - ahkState);
var wasHighCode = false;
if (code > 256)
{
wasHighCode = true;
code -= 256;
if (_highCodeE0Keys.Contains(code) || _e1Keys.Contains(code))
{
order = Order.Normal;
}
else if (_twoStrokeKeys.ContainsKey(code))
{
order = _twoStrokeKeys[code];
}
else
{
throw new Exception($"Do not know how to handle ScanCode of {code}");
}
}
else if (code == 69)
{
order = Order.Prefixed;
}
else
{
order = Order.Normal;
}
if (wasHighCode && _e1Keys.Contains(code))
{
state += 2;
}
else
{
state += (ushort)((ushort)order * 2);
}
if (order == Order.Normal)
{
strokes.Add(new Stroke() { key = { code = code, state = state } });
}
else if (order == Order.Wrapped)
{
// Wrapped (E1)
if (ahkState == 1)
{
// Press
strokes.Add(new Stroke() { key = { code = 42, state = state } });
strokes.Add(new Stroke() { key = { code = code, state = state } });
}
else
{
// Release
strokes.Add(new Stroke() { key = { code = code, state = state } });
strokes.Add(new Stroke() { key = { code = 42, state = state } });
}
}
else
{
// Prefixed (E2)
if (ahkState == 1)
{
// Press
strokes.Add(new Stroke() { key = { code = 29, state = state } });
strokes.Add(new Stroke() { key = { code = code, state = state } });
}
else
{
// Release
strokes.Add(new Stroke() { key = { code = 29, state = state } });
strokes.Add(new Stroke() { key = { code = code, state = state } });
}
}
return strokes;
}
}
// Holds the AHK code and state equivalent of a one or two-stroke set
public class TranslatedKey
{
public ushort Code { get; }
public int State { get; }
public TranslatedKey(ushort code, int state)
{
Code = code;
State = state;
}
}
// Builds entries for _twoStrokeKeyConverter
public class TwoStrokeKey
{
public Tuple<ushort, ushort, ushort, ushort> PressTuple { get; }
public Tuple<ushort, ushort, ushort, ushort> ReleaseTuple { get; }
public TranslatedKey PressKey { get; }
public TranslatedKey ReleaseKey { get; }
public TwoStrokeKey(ushort code, Order order)
{
if (order == Order.Prefixed)
{
PressTuple = new Tuple<ushort, ushort, ushort, ushort>(29, 4, code, 0);
ReleaseTuple = new Tuple<ushort, ushort, ushort, ushort>(29, 5, code, 1);
PressKey = new TranslatedKey(code, 1);
ReleaseKey = new TranslatedKey(code, 0);
}
else if (order == Order.Wrapped)
{
PressTuple = new Tuple<ushort, ushort, ushort, ushort>(42, 2, code, 2);
ReleaseTuple = new Tuple<ushort, ushort, ushort, ushort>(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");
}
}
}
}

@ -1,5 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using AutoHotInterception.DeviceHandlers;
@ -518,21 +519,63 @@ namespace AutoHotInterception
state ? ManagedWrapper.Filter.All : ManagedWrapper.Filter.None);
}
private static string RenderStroke(ManagedWrapper.Stroke stroke)
{
return $"key code: {stroke.key.code}, key state: {stroke.key.state}, mouse x/y: {stroke.mouse.x}, {stroke.mouse.y}";
}
private static void PollThread(object obj)
{
var token = (CancellationToken)obj;
//Debug.WriteLine($"AHK| Poll Thread Started");
_pollThreadRunning = true;
var stroke = new ManagedWrapper.Stroke();
int i;
int stroke1DeviceId;
int stroke2DeviceId;
while (!token.IsCancellationRequested)
{
// While no input happens, this loop will exit every 1ms to allow us to check if cancellation has been requested
var stroke = new ManagedWrapper.Stroke();
// While no input happens, this loop will exit every 10ms to allow us to check if cancellation has been requested
// WaitWithTimeout is used with a timeout of 10ms instead of Wait, so that when we eg use SetState to turn the thread off...
// ... any input which was filtered and is waiting to be processed can be processed (eg lots of mouse moves buffered)
while (ManagedWrapper.Receive(DeviceContext, i = ManagedWrapper.WaitWithTimeout(DeviceContext, 10), ref stroke, 1) > 0)
if (ManagedWrapper.Receive(DeviceContext, stroke1DeviceId = ManagedWrapper.WaitWithTimeout(DeviceContext, 10), ref stroke, 1) > 0)
{
DeviceHandlers[i].ProcessStroke(stroke);
var strokes = new List<ManagedWrapper.Stroke>();
strokes.Add(stroke);
if (stroke1DeviceId < 11)
{
//Debug.WriteLine($"Stroke 1: {RenderStroke(stroke)}");
// If this is a keyboard stroke, then keep performing more Receives immediately with a timeout of 0...
// ... this is to check whether an extended stroke is waiting.
// Unfortunately, at this point, it's entirely possible that two single-stroke keys end up in strokes...
// ... or even 3 strokes or more (eg one single-stroke key followed by a two-stroke key)
//while ((stroke2DeviceId = ManagedWrapper.WaitWithTimeout(DeviceContext, 0)) == stroke1DeviceId)
while ((stroke2DeviceId = ManagedWrapper.WaitWithTimeout(DeviceContext, 0)) != 0)
{
ManagedWrapper.Receive(DeviceContext, stroke2DeviceId, ref stroke, 1);
strokes.Add(stroke);
//Debug.WriteLine($"Stroke {strokes.Count}: {RenderStroke(stroke)}");
}
// Loop through the list checking the first 2 indexes for valid "two-code" key combinations.
// If no combo is found, send index 0 on its way, remove it off the top of the list, repeat
while (strokes.Count > 0)
{
if (strokes.Count >= 2 && ScanCodeHelper.IsDoubleScanCode(new List<ManagedWrapper.Stroke> { strokes[0], strokes[1] }))
{
DeviceHandlers[stroke1DeviceId].ProcessStroke(new List<ManagedWrapper.Stroke> { strokes[0], strokes[1] });
strokes.RemoveRange(0, 2);
}
else
{
DeviceHandlers[stroke1DeviceId].ProcessStroke(new List<ManagedWrapper.Stroke> { strokes[0] });
strokes.RemoveAt(0);
}
}
}
else
{
DeviceHandlers[stroke1DeviceId].ProcessStroke(strokes);
}
}
}
_pollThreadRunning = false;

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using AutoHotInterception.Helpers;
namespace AutoHotInterception
@ -8,12 +10,13 @@ namespace AutoHotInterception
Tool to check Scan Codes and Press / Release states
Note that these are raw scancodes and states as they come from Interception. Some keys (eg extended code keys) will not match AHK key codes!
*/
public class ScanCodeChecker
public class ScanCodeChecker : IDisposable
{
private readonly IntPtr _deviceContext;
private dynamic _callback;
private int _deviceId;
private bool _block;
private Thread _pollThread;
public ScanCodeChecker()
{
@ -26,13 +29,47 @@ namespace AutoHotInterception
_deviceId = deviceId;
_block = block;
_pollThread = new Thread(PollThread);
_pollThread.Start();
}
private void PollThread()
{
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)
int deviceId1;
int deviceId2;
var stroke1 = new ManagedWrapper.Stroke();
var stroke2 = new ManagedWrapper.Stroke();
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<ManagedWrapper.Stroke>();
if (ManagedWrapper.Receive(_deviceContext, deviceId1 = ManagedWrapper.WaitWithTimeout(_deviceContext, 10), ref stroke1, 1) > 0)
{
strokes.Add(stroke1);
if (deviceId1 < 11)
{
if (ManagedWrapper.Receive(_deviceContext, deviceId2 = ManagedWrapper.WaitWithTimeout(_deviceContext, 0), ref stroke2, 1) > 0)
{
strokes.Add(stroke2);
}
}
if (!_block)
{
for (int i = 0; i < strokes.Count; i++)
{
var stroke = strokes[i];
ManagedWrapper.Send(_deviceContext, _deviceId, ref stroke, 1);
}
}
// Use array for callback, as the callback may be AHK code, and dealing with arrays in AHK is way simpler that Lists
var keyEvents = new KeyEvent[strokes.Count];
for (int i = 0; i < strokes.Count; i++)
{
var s = strokes[i];
keyEvents[i] = new KeyEvent { Code = s.key.code, State = s.key.state };
}
_callback(keyEvents);
}
}
}
@ -43,7 +80,13 @@ namespace AutoHotInterception
private int IsMonitoredDevice(int device)
{
return Convert.ToInt32(_deviceId == device);
return (Convert.ToInt32(_deviceId == device) );
}
public void Dispose()
{
_pollThread.Abort();
_pollThread.Join();
}
}

@ -1,23 +1,15 @@
# Fix issues with Numlock/Pause
BlueChipps in AHI Discord has a proposed fix [here](https://github.com/bluechipps/AutoHotInterception/tree/pause_numlock_support)
# Poor coding of extended keys
Currently, there is an `.ignore` flag on `KeyboardState`
When an extended code comes in (eg extended Shift or Ctrl), then the key may be ignored and `block` could never be set
so the extended modifier will always be passed through.
Implement better system:
* When an extended modifier is seen, add it to a buffer
* When the next key comes in, check if it is subscribed and blocked
* If it is, then purge the buffer
* If it is not, play back the buffered extended key code and then play back the unblocked key
Del is a good example:
If Del (Above arrow keys, not on Numpad) is pressed while NumLock is off, it sends an Extended LCtrl, then the Pause Key
# Send before firing callback in Subscription Mode?
If trying to write a system like AHK's hotstrings, but using AHI,
if you had subscriptions with block set to false, you would want the key to be sent before firing the callback.
That way, if you intended to replace `btw` with `By the way`, when the callback fired for `w`,
the `w` would already be in the text editor.
As it stands, when the callback for `w` fired, the `w` would not bein the text editor, and after AHI sent the `w`
(After firing the callback), you would have no chance to delete the `w` (So you would end up with `by the wayw` in the text editor)
(After firing the callback), you would have no chance to delete the `w` (So you would end up with `by the wayw` in the text editor)
# Disablling filter whilst sub or unsub is happening
When a Subscribe or Unsubscribe call is made, the filter is turned off before the sub/unsub, and then turned on again afterwards.
This seems un-necesarry, and could result in some blocked input "leaking" through.
Consider removing this

@ -1,8 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@ -10,40 +7,32 @@ namespace AutoHotInterception
{
class WorkerThread : IDisposable
{
private readonly Thread _worker;
private volatile bool _running;
private Task _worker;
private CancellationTokenSource _cancellationToken;
public BlockingCollection<Action> Actions { get; }
public WorkerThread()
{
Actions = new BlockingCollection<Action>();
_worker = new Thread(Run);
_running = false;
}
public BlockingCollection<Action> Actions { get; }
public void Dispose()
{
if (!_running) return;
_running = false;
_worker.Abort();
_worker.Join();
}
public void Start()
{
if (_running) return;
_running = true;
_cancellationToken = new CancellationTokenSource();
_worker = new Task(Run, _cancellationToken.Token);
_worker.Start();
}
private void Run()
private void Run(Object obj)
{
while (_running)
var token = (CancellationToken)obj;
while (!token.IsCancellationRequested)
{
var action = Actions.Take();
action.Invoke();
}
}
public void Dispose()
{
_cancellationToken.Cancel();
_cancellationToken.Dispose();
}
}
}

@ -150,7 +150,7 @@ namespace TestApp.Helpers
{81 /*(0x51)*/, "NumpadPgDn"},
{82 /*(0x52)*/, "NumpadIns"},
{83 /*(0x53)*/, "NumpadDel"},
{84 /*(0x54)*/, "PrintScreen"},
//{84 /*(0x54)*/, "PrintScreen"}, Use 311 instead
{86 /*(0x56)*/, "\\"},
{87 /*(0x57)*/, "F11"},
{88 /*(0x58)*/, "F12"},
@ -180,6 +180,7 @@ namespace TestApp.Helpers
{306 /*(0x132)*/, "Browser_Home"},
{309 /*(0x135)*/, "NumpadDiv"},
{310 /*(0x136)*/, "RShift"},
{311 /*(0x136)*/, "PrintScreen"}, // Note: GetKeySC("PrintScreen") yields 311, but GetKeyName("sc54") (Dec of 84) yields "PrintScreen"
{312 /*(0x138)*/, "RAlt"},
{325 /*(0x145)*/, "Numlock"},
{326 /*(0x146)*/, "CtrlBreak"},

@ -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

@ -0,0 +1,82 @@
using AutoHotInterception;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TestApp.Helpers;
namespace TestApp
{
/// <summary>
/// Allows testing of Keyboard all keys and mouse movement at the same time
/// </summary>
public class KeyboardAndMouseTester
{
private Manager im = new Manager();
private Dictionary<int, bool> _deviceStates = new Dictionary<int, bool>();
private Dictionary<int, bool> _blockingEnabled = new Dictionary<int, bool>();
public KeyboardAndMouseTester(TestDevice testDevice, bool block = false)
{
AddDevice(testDevice, block);
}
public KeyboardAndMouseTester AddDevice(TestDevice testDevice, bool block = false)
{
var devId = testDevice.GetDeviceId();
if (devId == 0) return this;
_blockingEnabled[devId] = block;
SetDeviceState(devId, true);
return this;
}
private void SetDeviceState(int devId, bool state)
{
if (devId < 11)
{
if (state)
{
im.SubscribeKeyboard(devId, _blockingEnabled[devId], new Action<ushort, int>((code, value) =>
{
var keyObj = AhkKeys.Obj(code);
Console.WriteLine($"Name: {keyObj.Name}, Code: {keyObj.LogCode()}, State: {value}");
}));
}
else
{
im.UnsubscribeKeyboard(devId);
}
}
else
{
if (state)
{
im.SubscribeMouseMove(devId, _blockingEnabled[devId], new Action<int, int>((x, y) =>
{
Console.WriteLine($"Mouse Move: x: {x}, y: {y}");
}));
}
else
{
im.UnsubscribeMouseMove(devId);
}
}
_deviceStates[devId] = state;
}
// Allows toggling on and off of keyboard subscription whilst mouse sub active
public void Toggle(TestDevice testDevice)
{
var devId = testDevice.GetDeviceId();
while (true)
{
Console.WriteLine($"Subscribe: {_deviceStates[devId]} (Enter to toggle)");
Console.ReadLine();
SetDeviceState(devId, !_deviceStates[devId]);
}
}
}
}

@ -1,5 +1,5 @@
using System;
using AutoHotInterception;
using TestApp.Helpers;
namespace TestApp
{
@ -7,13 +7,15 @@ 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 kmt = new KeyboardAndMouseTester(TestDevices.WyseKeyboard, true).AddDevice(TestDevices.LogitechWheelMouse, true);
kmt.Toggle(TestDevices.WyseKeyboard);
//var kkt = new KeyboardKeyTester(TestDevices.WyseKeyboard, AhkKeys.Obj("1"), true);
//var tt = new TabletTester(TestDevices.ParbloIslandA609);
//var sct = new ScanCodeTester(TestDevices.WyseKeyboard, true);
//var scc = new ScanCodeTester(TestDevices.WyseKeyboard, true);
//var sst = new SetStateTester(TestDevices.WyseKeyboard, AhkKeys.Obj("1"));
Console.ReadLine();
}

@ -1,22 +1,34 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using AutoHotInterception;
namespace TestApp
{
public class ScanCodeTester
public class ScanCodeTester : IDisposable
{
private ScanCodeChecker scc;
public ScanCodeTester(TestDevice device, bool block = false)
{
var scc = new ScanCodeChecker();
scc = new ScanCodeChecker();
var devId = device.GetDeviceId();
if (devId == 0) return;
scc.Subscribe(devId, new Action<KeyEvent>(OnKeyEvent), block);
scc.Subscribe(devId, new Action<KeyEvent[]>(OnKeyEvent), block);
}
public void OnKeyEvent(KeyEvent keyEvent)
public void Dispose()
{
Debug.WriteLine($"Code: {keyEvent.Code} (0x{keyEvent.Code.ToString("X")}) - {keyEvent.Code + 256}, State: {keyEvent.State}");
scc.Dispose();
}
public void OnKeyEvent(KeyEvent[] keyEvents)
{
var str = "";
foreach (var keyEvent in keyEvents)
{
str += $"Code: {keyEvent.Code} (0x{keyEvent.Code.ToString("X")}) - {keyEvent.Code + 256}, State: {keyEvent.State} | ";
}
Debug.WriteLine(str);
}
}
}

@ -44,8 +44,9 @@
<ItemGroup>
<Compile Include="Helpers\AhkKeys.cs" />
<Compile Include="Helpers\MouseButtons.cs" />
<Compile Include="KeyboardAndMouseTester.cs" />
<Compile Include="MouseButtonTester.cs" />
<Compile Include="TestDevices.cs" />
<Compile Include="Helpers\TestDevices.cs" />
<Compile Include="KeyboardTester.cs" />
<Compile Include="KeyboardKeyTester.cs" />
<Compile Include="MouseButtonsTester.cs" />

@ -0,0 +1,74 @@
using AutoHotInterception.Helpers;
using NUnit.Framework;
using System.Collections.Generic;
namespace UnitTests
{
[TestFixture]
class TranslateAhkCodeTests
{
[Test, TestCaseSource("TestKeyProvider")]
public void PressRelease(string name, int code, List<ManagedWrapper.Stroke> pressResult, List<ManagedWrapper.Stroke> releaseResult)
{
var actualResult = ScanCodeHelper.TranslateAhkCode((ushort)code, 1);
AssertResults(pressResult, actualResult);
actualResult = ScanCodeHelper.TranslateAhkCode((ushort)code, 0);
AssertResults(releaseResult, actualResult);
}
private void AssertResults(List<ManagedWrapper.Stroke> expectedResult, List<ManagedWrapper.Stroke> actualResult)
{
Assert.That(actualResult.Count == expectedResult.Count, $"Expecting {expectedResult.Count} strokes, but got {actualResult.Count}");
for (int i = 0; i < expectedResult.Count; i++)
{
Assert.That(actualResult[i].key.code, Is.EqualTo(expectedResult[i].key.code),
$"Code should be {expectedResult[i].key.code}, got {actualResult[i].key.code}");
Assert.That(actualResult[i].key.state, Is.EqualTo(expectedResult[i].key.state),
$"Code should be {expectedResult[i].key.state}, got {actualResult[i].key.state}");
}
}
private static List<ManagedWrapper.Stroke> Result(ushort code1, ushort state1, ushort? code2 = null, ushort? state2 = null)
{
var strokes = new List<ManagedWrapper.Stroke>();
strokes.Add(new ManagedWrapper.Stroke() { key = { code = code1, state = state1 } });
if (code2 != null)
{
strokes.Add(new ManagedWrapper.Stroke() { key = { code = (ushort)code2, state = (ushort)state2 } });
}
return strokes;
}
private static IEnumerable<TestCaseData> TestKeyProvider()
{
yield return new TestCaseData("One", 2, Result(2, 0), Result(2, 1));
yield return new TestCaseData("Scroll Lock", 70, Result(70, 0), Result(70, 1));
yield return new TestCaseData("Div", 53, Result(53, 0), Result(53, 1));
yield return new TestCaseData("Numpad Enter", 284, Result(28, 0), Result(28, 1));
yield return new TestCaseData("Right Control", 285, Result(29, 2), Result(29, 3));
yield return new TestCaseData("Numpad Div", 309, Result(53, 2), Result(53, 3));
yield return new TestCaseData("Right Shift", 310, Result(54, 0), Result(54, 1));
yield return new TestCaseData("Print Screen", 311, Result(42, 2, 55, 2), Result(55, 3, 42, 3));
yield return new TestCaseData("Right Alt", 312, Result(56, 2), Result(56, 3));
yield return new TestCaseData("Numlock", 325, Result(69, 0), Result(69, 1));
yield return new TestCaseData("Pause", 69, Result(29, 4, 69, 4), Result(29, 5, 69, 5));
yield return new TestCaseData("Home", 327, Result(42, 2, 71, 2), Result(71, 3, 42, 3));
yield return new TestCaseData("Up", 328, Result(42, 2, 72, 2), Result(72, 3, 42, 3));
yield return new TestCaseData("PgUp", 329, Result(42, 2, 73, 2), Result(73, 3, 42, 3));
yield return new TestCaseData("Left", 331, Result(42, 2, 75, 2), Result(75, 3, 42, 3));
yield return new TestCaseData("Right", 333, Result(42, 2, 77, 2), Result(77, 3, 42, 3));
yield return new TestCaseData("End", 335, Result(42, 2, 79, 2), Result(79, 3, 42, 3));
yield return new TestCaseData("Down", 336, Result(42, 2, 80, 2), Result(80, 3, 42, 3));
yield return new TestCaseData("PgDn", 337, Result(42, 2, 81, 2), Result(81, 3, 42, 3));
yield return new TestCaseData("PgDn", 338, Result(42, 2, 82, 2), Result(82, 3, 42, 3));
yield return new TestCaseData("Delete", 339, Result(42, 2, 83, 2), Result(83, 3, 42, 3));
yield return new TestCaseData("Left Win", 347, Result(91, 2), Result(91, 3));
yield return new TestCaseData("Right Win", 348, Result(92, 2), Result(92, 3));
yield return new TestCaseData("Apps", 349, Result(93, 2), Result(93, 3));
}
}
}

@ -0,0 +1,125 @@
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<Stroke> PressStrokes { get; }
public List<Stroke> ReleaseStrokes { get; }
public ExpectedResult PressResult { get; }
public ExpectedResult ReleaseResult { get; }
public TestKey(string name, List<Stroke> pressStrokes, List<Stroke> 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 TranslateScanCodesTests
{
private static List<Stroke> Stroke(ushort code1, ushort state1, ushort code2 = 0, ushort state2 = 0)
{
var strokes = new List<Stroke>();
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;
}
[Test, TestCaseSource("TestKeyProvider")]
public void PressRelease(string name, List<Stroke> pressStrokes, List<Stroke> releaseStrokes, ExpectedResult pressResult, ExpectedResult releaseResult )
{
Debug.WriteLine($"\nTesting key {name}...");
Debug.WriteLine("Testing Press");
var expectedResult = pressResult;
var actualResult = ScanCodeHelper.TranslateScanCodes(pressStrokes);
AssertResult(actualResult, expectedResult);
Debug.WriteLine("Testing Release");
expectedResult = releaseResult;
actualResult = ScanCodeHelper.TranslateScanCodes(releaseStrokes);
AssertResult(actualResult, expectedResult);
Debug.WriteLine("OK!");
}
private static IEnumerable<TestCaseData> TestKeyProvider()
{
yield return new TestCaseData("One", Stroke(2, 0), Stroke(2, 1), Result(2, 1), Result(2, 0));
yield return new TestCaseData("Scroll Lock", Stroke(70, 0), Stroke(70, 1), Result(70, 1), Result(70, 0));
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("Print Screen", Stroke(42, 2, 55, 2), Stroke(55, 3, 42, 3), Result(311, 1), Result(311, 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}");
Assert.That(actualResult.Code, Is.EqualTo(expectedResult.Code), $"Code should be {expectedResult.Code}, got {actualResult.Code}");
Assert.That(actualResult.State, Is.EqualTo(expectedResult.State), $"State should be {expectedResult.State}, got {actualResult.State}");
}
}
}

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="16.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\packages\NUnit3TestAdapter.4.2.0\build\net35\NUnit3TestAdapter.props" Condition="Exists('..\packages\NUnit3TestAdapter.4.2.0\build\net35\NUnit3TestAdapter.props')" />
<Import Project="..\packages\NUnit.3.13.2\build\NUnit.props" Condition="Exists('..\packages\NUnit.3.13.2\build\NUnit.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{8EDF4429-251A-416D-BB68-93F227191BCF}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>UnitTests</RootNamespace>
<AssemblyName>UnitTests</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="nunit.framework, Version=3.13.2.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
<HintPath>..\packages\NUnit.3.13.2\lib\net45\nunit.framework.dll</HintPath>
</Reference>
<Reference Include="System" />
</ItemGroup>
<ItemGroup>
<Compile Include="TranslateAhkCodeTests.cs" />
<Compile Include="TranslateScanCodesTests.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AutoHotInterception\AutoHotInterception.csproj">
<Project>{68fa4bc3-c277-44d0-8333-18d51dc3ca19}</Project>
<Name>AutoHotInterception</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>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}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\NUnit.3.13.2\build\NUnit.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\NUnit.3.13.2\build\NUnit.props'))" />
<Error Condition="!Exists('..\packages\NUnit3TestAdapter.4.2.0\build\net35\NUnit3TestAdapter.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\NUnit3TestAdapter.4.2.0\build\net35\NUnit3TestAdapter.props'))" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="NUnit" version="3.13.2" targetFramework="net48" />
<package id="NUnit3TestAdapter" version="4.2.0" targetFramework="net48" />
</packages>

@ -5,10 +5,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
## [Unreleased]
### Added
- Add two SubscribeAbsolute example scripts which show how to process movement data coming from a tablet
### Changed
- Input is now processed even faster
Since 0.6.0, there could have been a 10ms delay between processing one piece of input and the next
Now it should be effectively instant
### Deprecated
### Removed
### Fixed
- Subscription / Context mode Extended keycodes fixed
Previously, some keys were not properly subscribe-able (Pause / NumLock)
Also, if any key was subscribed to with block enabled, and that key generated Extended Modifier keys when pressed
(eg when you press Home with NumLock off, keyboards send LShift with a state of 2, then Home with a state of 2)
then the Extended Modifier key (LShift with a state of 2 in the above example) was NOT blocked.
- SendKeyEvent() now sends exactly the same ScanCodes that would be sent if you really pressed it
Similar to the above example, if you sent Home, previously, only Home would be sent with a state of 2
LShift would not have been sent with a state of 2 as it should
Also, Pause should send a state of 4, whereas before it sent a state of 2
- AhiScanCodeTester.ahk in Development Tools now works again
- Bug introduced in 0.7.0 whereby unsubscribing whilst a callback is still running would lock up the script is now fixed
(WorkerThreads now use Tasks and CancellationTokens)
## [0.7.0] - 2022-01 -17
### Added

@ -15,15 +15,20 @@ Note that for some keys (eg Pause), AHI will see TWO key events for that key, he
All ScanCodes are in Decimal
*/
#include ..\Lib\AutoHotInterception.ahk
AHI := new AutoHotInterception()
vid := 0x04F2, pid := 0x0112 ; Wyse Keyboard
keyboardId := AHI.GetKeyboardId(vid, pid)
OutputDebug, DBGVIEWCLEAR
AhkKeyBuffer := []
asm := CLR_LoadLibrary("..\Lib\AutoHotInterception.dll")
sct := asm.CreateInstance("AutoHotInterception.ScanCodeChecker")
sct.Subscribe(vid, pid, Func("AhiKeyEvent"))
sct.Subscribe(keyboardId, Func("AhiKeyEvent"), false)
ih := InputHook()
ih.KeyOpt("{All}", "SN")
@ -66,7 +71,7 @@ AhiKeyEvent(keyEvents){
}
; Note that keyEvents is a ZERO-BASED array!
ahiSc1 := keyEvents[0].Code
ahiState1 := keyEvents[0].state

@ -202,6 +202,9 @@ All forms of input are supported in Subscription Mode.
Subscription Mode overrides Context Mode - that is, if a key on a keyboard has been subscribed to with Subscription Mode, then Context Mode will not fire for that key on that keyboard.
SubscribeKey overrides SubscribeKeyboard - that is, if you have subscribed to all keys and a specific key on the same keyboard, then if you press the specific key, it's callback will fire and the callback for SubscribeKeyboard will not.
Each Subscribe endpoint also has a corresponding Unsubscribe endpoint, which removes the subscription and any block associated with it.
Both keyboard and mouse subscription functions have an optional `concurrent` parameter. This controls whether callbacks are fired sequentially or not.
False (Default) means that a new callback will not be fired until the last one has completed. This is especially useful for subscriptions involving mouse movement.
True means that a new thread will be used for each callback. If your callback has a long-running loop in it, this could mean that a callback could interrupt the previous callback, resulting in a steady buildup of callbacks (Read memory leak). Use at own risk!
#### Subscribing to Keyboard keys
##### Subscribe to a specific key on a specific keyboard
@ -216,7 +219,6 @@ KeyEvent(state){
ToolTip % "State: " state
}
```
Parameter `<concurrent>` is optional and is <b>false</b> by default meaning that all the events raised for that key will be handled sequentially (i.e. callback function will be called on a single thread). If set to <b>true</b>, a new thread will be created for each event and the callback function will be called on it.
##### Subscribe to all keys on a specific keyboard
`SubscribeKeyboard(<deviceId>, <block>, <callback>, <concurrent>)`

@ -0,0 +1,56 @@
/*
This code illustrates how to process drags (Press down pen and move) with a tablet
*/
#SingleInstance force
#Persistent
#include Lib\AutoHotInterception.ahk
Coordmode, Tooltip, Screen
; Initialize AHI
AHI := new AutoHotInterception()
mouseId := AHI.GetMouseId(0x0B57, 0x9091) ; Get VID/PID of your device from the Monitor app and paste it in here
; Subscribe to the "Left Mouse" (AKA pressing the pen on the tablet) event from the tablet
AHI.SubscribeMouseButton(mouseId, 0, true, Func("ProcessInput"))
; Subscribe to movement coming from the tablet
; We use .Bind("") here to pass "" to the newState parameter of the function
AHI.SubscribeMouseMoveAbsolute(mouseId, true, Func("ProcessInput").Bind(""))
return
ProcessInput(newState, nx := "", ny := ""){
static state := 0
static x := 0, y := 0
if (newState != "" && newState != state){
stateChanged := 1
state := newState
} else {
stateChanged := 0
}
if (nx != ""){
x := nx
y := ny
}
; At this point, regardless of whether the input was a move or click, the following variables tell us what has happened:
; x and y hold current x and current y
; state is the current state of the button
; stateChanged indicates whether state just changed or not
; ---- Start of your code ----
static dragStartX, dragStartY
tooltip % "Current Coords: " x ", " y, 0, 0, 1
if (stateChanged){
if (state){
; Process start of drag here
dragStartX := x
dragStartY := y
} else {
; Process end of drag here
tooltip % "NOT DRAGGING. Last drag was: x " x - dragStartX ", y " y - dragStartY, 0, 30, 2
}
} else if (state){
; If you want to do something during a drag, you would do it here
tooltip % "DRAGGING: dragged x " x - dragStartX ", y " y - dragStartY, 0, 30, 2
}
}
^Esc::
ExitApp

@ -0,0 +1,36 @@
/*
This code illustrates how to detect movement and mouse event information coming from a Tablet
Bear in mind that when AHI receives input from a tablet...
... the "packet" of information always includes movement AND button state information
When AHI fires the callbacks, it always fires movement callbacks before button callbacks...
... so you can be sure that when ButtonEvent() fires, it always has the latest coordinates
*/
#SingleInstance force
#Persistent ; If you have no hotkeys or GUI in the script, you need this else the script will instantly exit
#include Lib\AutoHotInterception.ahk
Coordmode, Tooltip, Screen
; Initialize AHI
AHI := new AutoHotInterception()
mouseId := AHI.GetMouseId(0x0B57, 0x9091) ; Get VID/PID of your device from the Monitor app and paste it in here
; Subscribe to the "Left Mouse" (AKA pressing the pen on the tablet) event from the tablet
AHI.SubscribeMouseButton(mouseId, 0, true, Func("ButtonEvent"))
; Subscribe to movement coming from the tablet
AHI.SubscribeMouseMoveAbsolute(mouseId, true, Func("OnMouseMove"))
return
ButtonEvent(state){
global lastX, lastY
tooltip % "LMB State: " state " ( Event was at " lastX ", " lastY " )", 0, 30, 2
}
; Store coordinates on move, so they can be used in ButtonEvent
OnMouseMove(x, y){
global lastX, lastY
lastX := x
lastY := y
tooltip % "Coords: " x ", " y, 0, 0, 1
}
^Esc::
ExitApp

@ -0,0 +1,58 @@
#SingleInstance force
;~ #Persistent
#include Lib\AutoHotInterception.ahk
;~ pk := GetKeySC("Pause")
;~ dk := GetKeySC("Delete")
;~ RCtrl := GetKeySC("RCtrl")
msgbox % GetKeySC("ScrollLock")
;~ return
;~ msgbox % Format("{:d}", GetKeySC("Pause"))
return
clipboard := ""
log := "`n// ============ DUPES ==================`n"
keys := {}
Loop 512 {
hex := Format("{:x}", A_Index)
name := GetKeyName("sc" hex)
if (name == "")
continue
str := "{" A_Index " /*(0x" hex ")*/, " """" name """" "}"
;~ if (A_Index == 86 || A_Index = 342)
;~ break = true
if (keys.HasKey(name)){
log .= "// " str " (Also " keys[name] ")`n"
} else {
clipboard .= str ",`n"
keys[name] := A_Index
}
}
clipboard .= log
return
state := true
AHI := new AutoHotInterception()
;~ keyboardId := AHI.GetKeyboardId(0x04F2, 0x0112)
;~ AHI.SubscribeKey(keyboardId, GetKeySC("1"), true, Func("KeyEvent"))
mouseId := AHI.GetMouseId(0x046D, 0xC00C)
AHI.SubscribeMouseMove(mouseId, true, Func("OnMouseMove"))
return
KeyEvent(state){
ToolTip % "State: " state
}
OnMouseMove(x, y){
Tooltip % x ", " y, 0, 0
}
F1::
state := !state
AHI.SetState(state)
Tooltip
return
^Esc::
ExitApp
Loading…
Cancel
Save