Added an option to process all subscribed key events on a single thread instead of using a ThreadPool

- Using custom WorkerThread class to execute callback Actions placed into an Action queue
- Cleaned up code
- Needs testing...
pull/32/head
crumbl3d 5 years ago
parent 4db283f5a6
commit 5dc35cc8d8

@ -1,9 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace AutoHotInterception.Helpers
{
@ -14,15 +11,14 @@ namespace AutoHotInterception.Helpers
var start = isMouse ? 11 : 1;
var end = start + 9;
if (id < start || id > end)
{
throw new ArgumentOutOfRangeException(nameof(id), $"Invalid id ID: {id} for device type {(isMouse ? "Mouse" : "Keyboard")}. Device IDs for this type should be between {start} and {end}");
}
throw new ArgumentOutOfRangeException(nameof(id),
$"Invalid id ID: {id} for device type {(isMouse ? "Mouse" : "Keyboard")}. Device IDs for this type should be between {start} and {end}");
}
public static void GetVidPid(string str, ref int vid, ref int pid)
{
var matches = Regex.Matches(str, @"VID_(\w{4})&PID_(\w{4})");
if ((matches.Count <= 0) || (matches[0].Groups.Count <= 1)) return;
if (matches.Count <= 0 || matches[0].Groups.Count <= 1) return;
vid = Convert.ToInt32(matches[0].Groups[1].Value, 16);
pid = Convert.ToInt32(matches[0].Groups[2].Value, 16);
}
@ -38,14 +34,14 @@ namespace AutoHotInterception.Helpers
GetVidPid(handle, ref foundVid, ref foundPid);
//if (foundVid == 0 || foundPid == 0) continue;
ret.Add(new DeviceInfo { Id = i, Vid = foundVid, Pid = foundPid, IsMouse = i > 10, Handle = handle});
ret.Add(new DeviceInfo {Id = i, Vid = foundVid, Pid = foundPid, IsMouse = i > 10, Handle = handle});
}
return ret.ToArray();
}
/// <summary>
/// Converts a button index plus a state into a State value for a mouse Stroke
/// Converts a button index plus a state into a State value for a mouse Stroke
/// </summary>
/// <param name="btn">0 = LMB, 1 = RMB etc</param>
/// <param name="state">1 = Press, 0 = Release</param>
@ -54,8 +50,8 @@ namespace AutoHotInterception.Helpers
{
var stroke = new ManagedWrapper.Stroke();
var power = btn < 5 ? btn * 2 + (state == 0 ? 1 : 0) : btn + 5;
stroke.mouse.state = (ushort)(1 << power);
if (btn >= 5) stroke.mouse.rolling = (short)(state * 120);
stroke.mouse.state = (ushort) (1 << power);
if (btn >= 5) stroke.mouse.rolling = (short) (state * 120);
return stroke;
}
@ -70,6 +66,7 @@ namespace AutoHotInterception.Helpers
state >>= 2;
btn++;
}
state = 2 - state; // 1 = Pressed, 0 = Released
}
else
@ -78,6 +75,7 @@ namespace AutoHotInterception.Helpers
else if (state == 0x800) btn = 6; // Horizontal mouse wheel
state = stroke.mouse.rolling < 0 ? -1 : 1;
}
return new ButtonState {Button = btn, State = state};
}
@ -96,4 +94,4 @@ namespace AutoHotInterception.Helpers
public int State { get; set; }
}
}
}
}

@ -1,13 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using AutoHotInterception.Helpers;
using static AutoHotInterception.Helpers.HelperFunctions;
@ -15,22 +8,41 @@ namespace AutoHotInterception
{
public class Manager : IDisposable
{
private readonly IntPtr _deviceContext;
private Thread _pollThread;
private bool _pollThreadRunning = false;
private readonly ConcurrentDictionary<int, dynamic>
_contextCallbacks = new ConcurrentDictionary<int, dynamic>();
private bool _filterState = false;
private readonly ConcurrentDictionary<int, ConcurrentDictionary<ushort, MappingOptions>> _keyboardMappings = new ConcurrentDictionary<int, ConcurrentDictionary<ushort, MappingOptions>>();
private readonly ConcurrentDictionary<int, ConcurrentDictionary<ushort, MappingOptions>> _mouseButtonMappings = new ConcurrentDictionary<int, ConcurrentDictionary<ushort, MappingOptions>>();
private readonly ConcurrentDictionary<int, MappingOptions> _mouseMoveRelativeMappings = new ConcurrentDictionary<int, MappingOptions>();
private readonly ConcurrentDictionary<int, MappingOptions> _mouseMoveAbsoluteMappings = new ConcurrentDictionary<int, MappingOptions>();
private readonly ConcurrentDictionary<int, dynamic> _contextCallbacks = new ConcurrentDictionary<int, dynamic>();
private readonly IntPtr _deviceContext;
// If a the ID of a device exists as a key in this Dictionary, then that device is filtered.
// If a device ID exists as a key in this Dictionary then that device is filtered.
// Used by IsMonitoredDevice, which is handed to Interception as a "Predicate".
private readonly ConcurrentDictionary<int, bool> _filteredDevices = new ConcurrentDictionary<int, bool>();
private readonly ConcurrentDictionary<int, ConcurrentDictionary<ushort, MappingOptions>> _keyboardMappings =
new ConcurrentDictionary<int, ConcurrentDictionary<ushort, MappingOptions>>();
private readonly ConcurrentDictionary<int, ConcurrentDictionary<ushort, MappingOptions>> _mouseButtonMappings =
new ConcurrentDictionary<int, ConcurrentDictionary<ushort, MappingOptions>>();
private readonly ConcurrentDictionary<int, MappingOptions> _mouseMoveAbsoluteMappings =
new ConcurrentDictionary<int, MappingOptions>();
private readonly ConcurrentDictionary<int, MappingOptions> _mouseMoveRelativeMappings =
new ConcurrentDictionary<int, MappingOptions>();
// If an event is subscribed to with concurrent set to false then use a single worker thread to process each event.
// Makes sure the events are handled synchronously and with a FIFO order.
private readonly ConcurrentDictionary<int, ConcurrentDictionary<ushort, WorkerThread>> _workers =
new ConcurrentDictionary<int, ConcurrentDictionary<ushort, WorkerThread>>();
private bool _filterState;
private Thread _pollThread;
private bool _pollThreadRunning;
public void Dispose()
{
SetThreadState(false);
}
#region Public
#region Initialization
@ -50,90 +62,131 @@ namespace AutoHotInterception
#region Subscription Mode
/// <summary>
/// Subscribes to a Keyboard key
/// Subscribes to a Keyboard key
/// </summary>
/// <param name="id">The ID of the Keyboard</param>
/// <param name="code">The ScanCode of the key</param>
/// <param name="block">Whether or not to block the key</param>
/// <param name="callback">The callback to fire when the key changes state</param>
/// <param name="concurrent">Whether or not to execute callbacks concurrently</param>
/// <returns></returns>
public void SubscribeKey(int id, ushort code, bool block, dynamic callback)
public void SubscribeKey(int id, ushort code, bool block, dynamic callback, bool concurrent = true)
{
SetFilterState(false);
IsValidDeviceId(false, id);
if (!_keyboardMappings.ContainsKey(id))
{
_keyboardMappings.TryAdd(id, new ConcurrentDictionary<ushort, MappingOptions>());
}
_keyboardMappings[id].TryAdd(code, new MappingOptions() { Block = block, Callback = callback });
_keyboardMappings[id].TryAdd(code,
new MappingOptions {Block = block, Concurrent = concurrent, Callback = callback});
_filteredDevices[id] = true;
if (!concurrent)
{
if (!_workers.ContainsKey(id)) _workers.TryAdd(id, new ConcurrentDictionary<ushort, WorkerThread>());
var worker = new WorkerThread();
_workers[id].TryAdd(code, worker);
worker.Start();
}
SetFilterState(true);
SetThreadState(true);
}
/// <summary>
/// Subscribe to a Mouse button
/// Subscribe to a Mouse button
/// </summary>
/// <param name="id">The ID of the mouse</param>
/// <param name="btn">The button number (LMB = 0, RMB = 1, MMB = 2, X1 = 3, X2 = 4</param>
/// <param name="btn">The button number (LMB = 0, RMB = 1, MMB = 2, X1 = 3, X2 = 4, WV = 5, WH = 6)</param>
/// <param name="block">Whether or not to block the button</param>
/// <param name="callback">The callback to fire when the button changes state</param>
/// <param name="concurrent">Whether or not to execute callbacks concurrently</param>
/// <returns></returns>
public void SubscribeMouseButton(int id, ushort btn, bool block, dynamic callback)
public void SubscribeMouseButton(int id, ushort btn, bool block, dynamic callback, bool concurrent = true)
{
IsValidDeviceId(true, id);
if (!_mouseButtonMappings.ContainsKey(id))
{
_mouseButtonMappings.TryAdd(id, new ConcurrentDictionary<ushort, MappingOptions>());
}
_mouseButtonMappings[id].TryAdd(btn, new MappingOptions() { Block = block, Callback = callback });
_mouseButtonMappings[id].TryAdd(btn,
new MappingOptions {Block = block, Concurrent = concurrent, Callback = callback});
_filteredDevices[id] = true;
if (!concurrent)
{
if (!_workers.ContainsKey(id)) _workers.TryAdd(id, new ConcurrentDictionary<ushort, WorkerThread>());
var worker = new WorkerThread();
_workers[id].TryAdd(btn, worker);
worker.Start();
}
SetFilterState(true);
SetThreadState(true);
}
//Shorthand for SubscribeMouseMoveRelative
public void SubscribeMouseMove(int id, bool block, dynamic callback)
{
SubscribeMouseMoveRelative(id, block, callback);
}
/// <summary>
/// Subscribes to Relative mouse movement
/// Subscribes to Absolute mouse movement
/// </summary>
/// <param name="id">The id of the Mouse</param>
/// <param name="block">Whether or not to block the movement</param>
/// <param name="callback">The callback to fire when the mouse moves</param>
/// <param name="concurrent">Whether or not to execute callbacks concurrently</param>
/// <returns></returns>
public void SubscribeMouseMoveRelative(int id, bool block, dynamic callback)
public void SubscribeMouseMoveAbsolute(int id, bool block, dynamic callback, bool concurrent = true)
{
IsValidDeviceId(true, id);
_mouseMoveRelativeMappings[id] = new MappingOptions() { Block = block, Callback = callback };
_mouseMoveAbsoluteMappings[id] = new MappingOptions
{Block = block, Concurrent = concurrent, Callback = callback};
_filteredDevices[id] = true;
if (!concurrent)
{
if (!_workers.ContainsKey(id)) _workers.TryAdd(id, new ConcurrentDictionary<ushort, WorkerThread>());
var worker = new WorkerThread();
_workers[id].TryAdd(7, worker); // Use 7 as second index for MouseMoveAbsolute
worker.Start();
}
SetFilterState(true);
SetThreadState(true);
}
//Shorthand for SubscribeMouseMoveRelative
public void SubscribeMouseMove(int id, bool block, dynamic callback, bool concurrent = true)
{
SubscribeMouseMoveRelative(id, block, callback, concurrent);
}
/// <summary>
///
/// Subscribes to Absolute mouse movement
/// Subscribes to Relative mouse movement
/// </summary>
/// <param name="id">The id of the Mouse</param>
/// <param name="block">Whether or not to block the movement</param>
/// <param name="callback">The callback to fire when the mouse moves</param>
/// <param name="concurrent">Whether or not to execute callbacks concurrently</param>
/// <returns></returns>
public void SubscribeMouseMoveAbsolute(int id, bool block, dynamic callback)
public void SubscribeMouseMoveRelative(int id, bool block, dynamic callback, bool concurrent = true)
{
IsValidDeviceId(true, id);
_mouseMoveAbsoluteMappings[id] = new MappingOptions() { Block = block, Callback = callback };
_mouseMoveRelativeMappings[id] = new MappingOptions
{Block = block, Concurrent = concurrent, Callback = callback};
_filteredDevices[id] = true;
if (!concurrent)
{
if (!_workers.ContainsKey(id)) _workers.TryAdd(id, new ConcurrentDictionary<ushort, WorkerThread>());
var worker = new WorkerThread();
_workers[id].TryAdd(8, worker); // Use 8 as second index for MouseMoveRelative
worker.Start();
}
SetFilterState(true);
SetThreadState(true);
}
@ -143,7 +196,7 @@ namespace AutoHotInterception
#region Context Mode
/// <summary>
/// Sets a callback for Context Mode for a given device
/// Sets a callback for Context Mode for a given device
/// </summary>
/// <param name="id">The ID of the device</param>
/// <param name="callback">The callback to fire before and after each key or button press</param>
@ -152,9 +205,7 @@ namespace AutoHotInterception
{
SetFilterState(false);
if (id < 1 || id > 20)
{
throw new ArgumentOutOfRangeException(nameof(id), "DeviceIds must be between 1 and 20");
}
_contextCallbacks[id] = callback;
_filteredDevices[id] = true;
@ -168,7 +219,7 @@ namespace AutoHotInterception
#region Input Synthesis
/// <summary>
/// Sends a keyboard key event
/// Sends a keyboard key event
/// </summary>
/// <param name="id">The ID of the Keyboard to send as</param>
/// <param name="code">The ScanCode to send</param>
@ -182,17 +233,16 @@ namespace AutoHotInterception
{
code -= 256;
if (code != 54) // RShift has > 256 code, but state is 0/1
{
st += 2;
}
}
stroke.key.code = code;
stroke.key.state = (ushort)st;
stroke.key.state = (ushort) st;
ManagedWrapper.Send(_deviceContext, id, ref stroke, 1);
}
/// <summary>
/// Sends Mouse button events
/// Sends Mouse button events
/// </summary>
/// <param name="id"></param>
/// <param name="btn"></param>
@ -207,7 +257,7 @@ namespace AutoHotInterception
}
/// <summary>
/// Same as <see cref="SendMouseButtonEvent"/>, but sends button events in Absolute mode (with coordinates)
/// Same as <see cref="SendMouseButtonEvent" />, but sends button events in Absolute mode (with coordinates)
/// </summary>
/// <param name="id"></param>
/// <param name="btn"></param>
@ -229,7 +279,7 @@ namespace AutoHotInterception
}
/// <summary>
/// Sends Relative Mouse Movement
/// Sends Relative Mouse Movement
/// </summary>
/// <param name="id"></param>
/// <param name="x"></param>
@ -239,14 +289,15 @@ namespace AutoHotInterception
{
IsValidDeviceId(true, id);
var stroke = new ManagedWrapper.Stroke { mouse = { x = x, y = y, flags = (ushort)ManagedWrapper.MouseFlag.MouseMoveRelative } };
var stroke = new ManagedWrapper.Stroke
{mouse = {x = x, y = y, flags = (ushort) ManagedWrapper.MouseFlag.MouseMoveRelative}};
ManagedWrapper.Send(_deviceContext, id, ref stroke, 1);
}
/// <summary>
/// Sends Absolute Mouse Movement
/// Note: Newing up a stroke seems to make Absolute input be relative to main monitor
/// Calling Send on an actual stroke from an Absolute device results in input relative to all monitors
/// Sends Absolute Mouse Movement
/// Note: Creating a new stroke seems to make Absolute input become relative to main monitor
/// Calling Send on an actual stroke from an Absolute device results in input relative to all monitors
/// </summary>
/// <param name="id"></param>
/// <param name="x"></param>
@ -256,7 +307,8 @@ namespace AutoHotInterception
{
IsValidDeviceId(true, id);
var stroke = new ManagedWrapper.Stroke { mouse = { x = x, y = y, flags = (ushort)ManagedWrapper.MouseFlag.MouseMoveAbsolute } };
var stroke = new ManagedWrapper.Stroke
{mouse = {x = x, y = y, flags = (ushort) ManagedWrapper.MouseFlag.MouseMoveAbsolute}};
ManagedWrapper.Send(_deviceContext, id, ref stroke, 1);
}
@ -285,7 +337,7 @@ namespace AutoHotInterception
}
/// <summary>
/// Tries to get Device ID from VID/PID
/// Tries to get Device ID from VID/PID
/// </summary>
/// <param name="isMouse">Whether the device is a mouse or a keyboard</param>
/// <param name="vid">The VID of the device</param>
@ -302,10 +354,7 @@ namespace AutoHotInterception
int foundVid = 0, foundPid = 0;
GetVidPid(hardwareStr, ref foundVid, ref foundPid);
if (foundVid != vid || foundPid != pid) continue;
if (instance == 1)
{
return i;
}
if (instance == 1) return i;
instance--;
}
@ -314,7 +363,7 @@ namespace AutoHotInterception
}
/// <summary>
/// Tries to get Device ID from Hardware String
/// Tries to get Device ID from Hardware String
/// </summary>
/// <param name="isMouse">Whether the device is a mouse or a keyboard</param>
/// <param name="handle">The Hardware String (handle) of the device</param>
@ -329,10 +378,7 @@ namespace AutoHotInterception
var hardwareStr = ManagedWrapper.GetHardwareStr(_deviceContext, i, 1000);
if (hardwareStr != handle) continue;
if (instance == 1)
{
return i;
}
if (instance == 1) return i;
instance--;
}
@ -341,9 +387,9 @@ namespace AutoHotInterception
}
/// <summary>
/// Gets a list of connected devices
/// Intended to be used called via the AHK wrapper...
/// ... so it can convert the return value into an AHK array
/// Gets a list of connected devices
/// Intended to be used called via the AHK wrapper...
/// ... so it can convert the return value into an AHK array
/// </summary>
/// <returns></returns>
public DeviceInfo[] GetDeviceList()
@ -370,17 +416,17 @@ namespace AutoHotInterception
}
else
{
_pollThread.Abort();
_pollThread.Interrupt();
_pollThread.Join();
_pollThread = null;
}
}
/// <summary>
/// Predicate used by Interception to decide whether to filter this device or not.
/// WARNING! Setting this to always return true is RISKY, as you could lock yourself out of Windows...
/// ... requiring a reboot.
/// When working with AHI, it's generally best to keep this matching as little as possible....
/// Predicate used by Interception to decide whether to filter this device or not.
/// WARNING! Setting this to always return true is RISKY, as you could lock yourself out of Windows...
/// ... requiring a reboot.
/// When working with AHI, it's generally best to keep this matching as little as possible....
/// </summary>
/// <param name="device"></param>
/// <returns></returns>
@ -400,7 +446,7 @@ namespace AutoHotInterception
// ScanCode notes: https://www.win.tue.nl/~aeb/linux/kbd/scancodes-1.html
private void PollThread()
{
ManagedWrapper.Stroke stroke = new ManagedWrapper.Stroke();
var stroke = new ManagedWrapper.Stroke();
while (true)
{
@ -425,16 +471,10 @@ namespace AutoHotInterception
var state = stroke.key.state;
#region KeyCode, State, Extended Flag translation
// Begin translation of incoming key code, state, extended flag etc...
var processMappings = true;
if (code == 54)
{
// Interception seems to report Right Shift as 54 / 0x36 with state 0/1...
// ... this code is normally unused (Alt-SysRq according to linked page) ...
// ... and AHK uses 54 + 256 = 310 (0x36 + 0x100 = 0x136)...
// ... so change the code, but leave the state as 0/1
code = 310;
}
if (code == 54) code = 310;
// If state is shifted up by 2 (1 or 2 instead of 0 or 1), then this is an "Extended" key code
if (state > 1)
@ -445,7 +485,7 @@ namespace AutoHotInterception
// Example case is Delete (The one above the arrow keys, not on numpad)...
// ... this generates a stroke of 0x2a (Shift) with *extended flag set* (Normal shift does not do this)...
// ... followed by 0x53 with extended flag set.
// We do not want to fire subsriptions for the extended shift, but *do* want to let the key flow through...
// We do not want to fire subscriptions for the extended shift, but *do* want to let the key flow through...
// ... so that is handled here.
// When the extended key (Delete in the above example) subsequently comes through...
// ... it will have code 0x53, which we shift to 0x153 (Adding 256 Dec) to signify extended version...
@ -464,6 +504,7 @@ namespace AutoHotInterception
state -= 2;
}
}
#endregion
// Code and state now normalized, proceed with checking for subscriptions...
@ -471,12 +512,16 @@ namespace AutoHotInterception
{
hasSubscription = true;
var mapping = _keyboardMappings[i][code];
if (mapping.Block)
if (mapping.Block) block = true;
if (mapping.Concurrent)
{
block = true;
ThreadPool.QueueUserWorkItem(threadProc => mapping.Callback(1 - state));
}
else if (_workers.ContainsKey(i) && _workers[i].ContainsKey(code))
{
var worker = _workers[i][code];
worker?.Actions.Add(() => mapping.Callback(1 - state));
}
ThreadPool.QueueUserWorkItem(threadProc => mapping.Callback(1 - state));
}
}
@ -485,19 +530,13 @@ namespace AutoHotInterception
// If this key had no subscriptions, but Context Mode is set for this keyboard...
// ... then set the Context before sending the key
if (!hasSubscription && hasContext)
{
_contextCallbacks[i](1);
}
if (!hasSubscription && hasContext) _contextCallbacks[i](1);
// Pass the key through to the OS.
ManagedWrapper.Send(_deviceContext, i, ref stroke, 1);
// If we are processing Context Mode, then Unset the context variable after sending the key
if (!hasSubscription && hasContext)
{
_contextCallbacks[i](0);
}
if (!hasSubscription && hasContext) _contextCallbacks[i](0);
}
}
@ -523,14 +562,21 @@ namespace AutoHotInterception
{
hasSubscription = true;
var mapping = _mouseButtonMappings[i][btnState.Button];
if (mapping.Block)
{
block = true;
}
if (mapping.Block) block = true;
var state = btnState;
ThreadPool.QueueUserWorkItem(threadProc => mapping.Callback(state.State));
if (mapping.Concurrent)
{
ThreadPool.QueueUserWorkItem(threadProc => mapping.Callback(1 - state.State));
}
else if (_workers.ContainsKey(i) && _workers[i].ContainsKey(btnState.Button))
{
var worker = _workers[i][btnState.Button];
worker?.Actions.Add(() => mapping.Callback(1 - state.State));
}
}
//Console.WriteLine($"AHK| Mouse {i} seen - button {btnState.Button}, state: {stroke.mouse.state}, rolling: {stroke.mouse.rolling}");
}
else if ((stroke.mouse.flags & (ushort) ManagedWrapper.MouseFlag.MouseMoveAbsolute) ==
@ -540,51 +586,52 @@ namespace AutoHotInterception
// Absolute Mouse Move
hasSubscription = true;
var mapping = _mouseMoveAbsoluteMappings[i];
if (mapping.Block)
{
block = true;
}
if (mapping.Block) block = true;
var x = stroke.mouse.x;
var y = stroke.mouse.y;
ThreadPool.QueueUserWorkItem(threadProc => mapping.Callback(x, y));
if (mapping.Concurrent)
{
ThreadPool.QueueUserWorkItem(threadProc => mapping.Callback(x, y));
}
else if (_workers.ContainsKey(i) && _workers[i].ContainsKey(7))
{
var worker = _workers[i][7];
worker?.Actions.Add(() => mapping.Callback(x, y));
}
}
else if ((stroke.mouse.flags & (ushort)ManagedWrapper.MouseFlag.MouseMoveRelative) ==
(ushort)ManagedWrapper.MouseFlag.MouseMoveRelative
else if ((stroke.mouse.flags & (ushort) ManagedWrapper.MouseFlag.MouseMoveRelative) ==
(ushort) ManagedWrapper.MouseFlag.MouseMoveRelative
&& _mouseMoveRelativeMappings.ContainsKey(i))
{
// Relative Mouse Move
hasSubscription = true;
var mapping = _mouseMoveRelativeMappings[i];
if (mapping.Block)
{
block = true;
}
if (mapping.Block) block = true;
var x = stroke.mouse.x;
var y = stroke.mouse.y;
ThreadPool.QueueUserWorkItem(threadProc => mapping.Callback(x, y));
if (mapping.Concurrent)
{
ThreadPool.QueueUserWorkItem(threadProc => mapping.Callback(x, y));
}
else if (_workers.ContainsKey(i) && _workers[i].ContainsKey(8))
{
var worker = _workers[i][8];
worker?.Actions.Add(() => mapping.Callback(x, y));
}
}
}
// If this key had no subscriptions, but Context Mode is set for this mouse...
// ... then set the Context before sending the button
if (!hasSubscription && hasContext)
{
// Set Context
_contextCallbacks[i](1);
}
if (!(block))
{
ManagedWrapper.Send(_deviceContext, i, ref stroke, 1);
}
if (!hasSubscription && hasContext) _contextCallbacks[i](1); // Set Context
if (!block) ManagedWrapper.Send(_deviceContext, i, ref stroke, 1);
// If we are processing Context Mode, then Unset the context variable after sending the button
if (!hasSubscription && hasContext)
{
// Unset Context
_contextCallbacks[i](0);
}
if (!hasSubscription && hasContext) _contextCallbacks[i](0);
}
}
Thread.Sleep(10);
}
}
@ -592,15 +639,48 @@ namespace AutoHotInterception
private class MappingOptions
{
public bool Block { get; set; }
public bool Concurrent { get; set; }
public dynamic Callback { get; set; }
}
#endregion
public void Dispose()
private class WorkerThread : IDisposable
{
SetThreadState(false);
private readonly Thread _worker;
private bool _running;
public WorkerThread()
{
Actions = new BlockingCollection<Action>();
_worker = new Thread(Run);
_running = false;
}
public BlockingCollection<Action> Actions { get; }
public void Dispose()
{
if (!_running) return;
_worker.Interrupt();
_worker.Join();
_running = false;
}
public void Start()
{
_worker.Start();
_running = true;
}
private void Run()
{
while (true)
{
var action = Actions.Take();
action.Invoke();
}
}
}
#endregion
}
}
}

@ -1,11 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AutoHotInterception.Helpers;
using static AutoHotInterception.Helpers.HelperFunctions;
@ -14,12 +9,12 @@ namespace AutoHotInterception
public class Monitor : IDisposable
{
private readonly IntPtr _deviceContext;
private Thread _pollThread;
private bool _pollThreadRunning = false;
private readonly ConcurrentDictionary<int, bool> _filteredDevices = new ConcurrentDictionary<int, bool>();
private bool _filterState;
private dynamic _keyboardCallback;
private dynamic _mouseCallback;
private bool _filterState = false;
private readonly ConcurrentDictionary<int, bool> _filteredDevices = new ConcurrentDictionary<int, bool>();
private Thread _pollThread;
private bool _pollThreadRunning;
public Monitor()
{
@ -27,16 +22,17 @@ namespace AutoHotInterception
SetThreadState(true);
}
public void Dispose()
{
SetFilterState(false);
SetThreadState(false);
}
public string OkCheck()
{
return "OK";
}
//public void Log(string text)
//{
// Debug.WriteLine($"AHK| {text}");
//}
public void Subscribe(dynamic keyboardCallback, dynamic mouseCallback)
{
_keyboardCallback = keyboardCallback;
@ -47,20 +43,11 @@ namespace AutoHotInterception
{
SetFilterState(false);
if (state)
{
_filteredDevices[device] = true;
//Log($"Adding device {device}, count: {_filteredDevices.Count}");
}
else
{
_filteredDevices.TryRemove(device, out _);
//Log($"Removing device {device}, count: {_filteredDevices.Count}");
}
if (_filteredDevices.Count > 0)
{
SetFilterState(true);
}
if (_filteredDevices.Count > 0) SetFilterState(true);
return true;
}
@ -81,7 +68,6 @@ namespace AutoHotInterception
return Convert.ToInt32(_filteredDevices.ContainsKey(device));
}
private void SetThreadState(bool state)
{
if (state)
@ -107,40 +93,33 @@ namespace AutoHotInterception
while (true)
{
for (var i = 1; i < 11; i++)
{
while (ManagedWrapper.Receive(_deviceContext, i, ref stroke, 1) > 0)
{
ManagedWrapper.Send(_deviceContext, i, ref stroke, 1);
FireKeyboardCallback(i, stroke);
}
}
for (var i = 11; i < 21; i++)
{
while (ManagedWrapper.Receive(_deviceContext, i, ref stroke, 1) > 0)
{
ManagedWrapper.Send(_deviceContext, i, ref stroke, 1);
FireMouseCallback(i, stroke);
}
}
Thread.Sleep(10);
}
}
private void FireKeyboardCallback(int id, ManagedWrapper.Stroke stroke)
{
ThreadPool.QueueUserWorkItem(threadProc => _keyboardCallback(id, stroke.key.state, stroke.key.code, stroke.key.information));
ThreadPool.QueueUserWorkItem(threadProc =>
_keyboardCallback(id, stroke.key.state, stroke.key.code, stroke.key.information));
}
private void FireMouseCallback(int id, ManagedWrapper.Stroke stroke)
{
ThreadPool.QueueUserWorkItem(threadProc => _mouseCallback(id, stroke.mouse.state, stroke.mouse.flags, stroke.mouse.rolling, stroke.mouse.x, stroke.mouse.y, stroke.mouse.information));
}
public void Dispose()
{
SetFilterState(false);
SetThreadState(false);
ThreadPool.QueueUserWorkItem(threadProc => _mouseCallback(id, stroke.mouse.state, stroke.mouse.flags,
stroke.mouse.rolling, stroke.mouse.x, stroke.mouse.y, stroke.mouse.information));
}
}
}
}

@ -9,87 +9,6 @@ namespace AutoHotInterception
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate int Predicate(int device);
/*
typedef void *InterceptionContext;
typedef int InterceptionDevice;
typedef int InterceptionPrecedence;
typedef unsigned short InterceptionFilter;
typedef int (*InterceptionPredicate)(InterceptionDevice device);
*/
[Flags]
public enum KeyState
{
Down = 0x00,
Up = 0x01,
E0 = 0x02,
E1 = 0x04,
TermsrvSetLED = 0x08,
TermsrvShadow = 0x10,
TermsrvVKPacket = 0x20
/*
enum InterceptionKeyState
INTERCEPTION_KEY_DOWN = 0x00,
INTERCEPTION_KEY_UP = 0x01,
INTERCEPTION_KEY_E0 = 0x02,
INTERCEPTION_KEY_E1 = 0x04,
INTERCEPTION_KEY_TERMSRV_SET_LED = 0x08,
INTERCEPTION_KEY_TERMSRV_SHADOW = 0x10,
INTERCEPTION_KEY_TERMSRV_VKPACKET = 0x20
*/
}
[Flags]
public enum MouseState
{
LeftButtonDown = 0x001,
LeftButtonUp = 0x002,
RightButtonDown = 0x004,
RightButtonUp = 0x008,
MiddleButtonDown = 0x010,
MiddleButtonUp = 0x020,
Button1Down = LeftButtonDown,
Button1Up = LeftButtonUp,
Button2Down = RightButtonDown,
Button2Up = RightButtonUp,
Button3Down = MiddleButtonDown,
Button3Up = MiddleButtonUp,
Button4Down = 0x040,
Button4Up = 0x080,
Button5Down = 0x100,
Button5Up = 0x200,
Wheel = 0x400,
HWheel = 0x800
/*
enum InterceptionMouseState
{
INTERCEPTION_MOUSE_LEFT_BUTTON_DOWN = 0x001,
INTERCEPTION_MOUSE_LEFT_BUTTON_UP = 0x002,
INTERCEPTION_MOUSE_RIGHT_BUTTON_DOWN = 0x004,
INTERCEPTION_MOUSE_RIGHT_BUTTON_UP = 0x008,
INTERCEPTION_MOUSE_MIDDLE_BUTTON_DOWN = 0x010,
INTERCEPTION_MOUSE_MIDDLE_BUTTON_UP = 0x020,
INTERCEPTION_MOUSE_BUTTON_1_DOWN = INTERCEPTION_MOUSE_LEFT_BUTTON_DOWN,
INTERCEPTION_MOUSE_BUTTON_1_UP = INTERCEPTION_MOUSE_LEFT_BUTTON_UP,
INTERCEPTION_MOUSE_BUTTON_2_DOWN = INTERCEPTION_MOUSE_RIGHT_BUTTON_DOWN,
INTERCEPTION_MOUSE_BUTTON_2_UP = INTERCEPTION_MOUSE_RIGHT_BUTTON_UP,
INTERCEPTION_MOUSE_BUTTON_3_DOWN = INTERCEPTION_MOUSE_MIDDLE_BUTTON_DOWN,
INTERCEPTION_MOUSE_BUTTON_3_UP = INTERCEPTION_MOUSE_MIDDLE_BUTTON_UP,
INTERCEPTION_MOUSE_BUTTON_4_DOWN = 0x040,
INTERCEPTION_MOUSE_BUTTON_4_UP = 0x080,
INTERCEPTION_MOUSE_BUTTON_5_DOWN = 0x100,
INTERCEPTION_MOUSE_BUTTON_5_UP = 0x200,
INTERCEPTION_MOUSE_WHEEL = 0x400,
INTERCEPTION_MOUSE_HWHEEL = 0x800
};
*/
}
[Flags]
public enum Filter : ushort
@ -139,8 +58,12 @@ namespace AutoHotInterception
MouseButton4Up = MouseState.Button4Up,
MouseButton5Down = MouseState.Button5Down,
MouseButton5Up = MouseState.Button5Up,
MouseButtonAnyDown = MouseState.Button1Down | MouseState.Button2Down | MouseState.Button3Down | MouseState.Button4Down | MouseState.Button5Down,
MouseButtonAnyUp = MouseState.Button1Up | MouseState.Button2Up | MouseState.Button3Up | MouseState.Button4Up | MouseState.Button5Up,
MouseButtonAnyDown = MouseState.Button1Down | MouseState.Button2Down | MouseState.Button3Down |
MouseState.Button4Down | MouseState.Button5Down,
MouseButtonAnyUp = MouseState.Button1Up | MouseState.Button2Up | MouseState.Button3Up |
MouseState.Button4Up | MouseState.Button5Up,
MouseWheel = MouseState.Wheel,
MouseHWheel = MouseState.HWheel
@ -176,6 +99,35 @@ namespace AutoHotInterception
};
*/
}
/*
typedef void *InterceptionContext;
typedef int InterceptionDevice;
typedef int InterceptionPrecedence;
typedef unsigned short InterceptionFilter;
typedef int (*InterceptionPredicate)(InterceptionDevice device);
*/
[Flags]
public enum KeyState
{
Down = 0x00,
Up = 0x01,
E0 = 0x02,
E1 = 0x04,
TermsrvSetLED = 0x08,
TermsrvShadow = 0x10,
TermsrvVKPacket = 0x20
/*
enum InterceptionKeyState
INTERCEPTION_KEY_DOWN = 0x00,
INTERCEPTION_KEY_UP = 0x01,
INTERCEPTION_KEY_E0 = 0x02,
INTERCEPTION_KEY_E1 = 0x04,
INTERCEPTION_KEY_TERMSRV_SET_LED = 0x08,
INTERCEPTION_KEY_TERMSRV_SHADOW = 0x10,
INTERCEPTION_KEY_TERMSRV_VKPACKET = 0x20
*/
}
[Flags]
public enum MouseFlag : ushort
@ -199,33 +151,56 @@ namespace AutoHotInterception
*/
}
[StructLayout(LayoutKind.Sequential)]
public struct MouseStroke
[Flags]
public enum MouseState
{
public ushort state;
public ushort flags;
public short rolling;
public int x;
public int y;
public uint information;
}
LeftButtonDown = 0x001,
LeftButtonUp = 0x002,
RightButtonDown = 0x004,
RightButtonUp = 0x008,
MiddleButtonDown = 0x010,
MiddleButtonUp = 0x020,
[StructLayout(LayoutKind.Sequential)]
public struct KeyStroke
{
public ushort code;
public ushort state;
public uint information;
}
Button1Down = LeftButtonDown,
Button1Up = LeftButtonUp,
Button2Down = RightButtonDown,
Button2Up = RightButtonUp,
Button3Down = MiddleButtonDown,
Button3Up = MiddleButtonUp,
[StructLayout(LayoutKind.Explicit)]
public struct Stroke
{
[FieldOffset(0)]
public MouseStroke mouse;
Button4Down = 0x040,
Button4Up = 0x080,
Button5Down = 0x100,
Button5Up = 0x200,
[FieldOffset(0)]
public KeyStroke key;
Wheel = 0x400,
HWheel = 0x800
/*
enum InterceptionMouseState
{
INTERCEPTION_MOUSE_LEFT_BUTTON_DOWN = 0x001,
INTERCEPTION_MOUSE_LEFT_BUTTON_UP = 0x002,
INTERCEPTION_MOUSE_RIGHT_BUTTON_DOWN = 0x004,
INTERCEPTION_MOUSE_RIGHT_BUTTON_UP = 0x008,
INTERCEPTION_MOUSE_MIDDLE_BUTTON_DOWN = 0x010,
INTERCEPTION_MOUSE_MIDDLE_BUTTON_UP = 0x020,
INTERCEPTION_MOUSE_BUTTON_1_DOWN = INTERCEPTION_MOUSE_LEFT_BUTTON_DOWN,
INTERCEPTION_MOUSE_BUTTON_1_UP = INTERCEPTION_MOUSE_LEFT_BUTTON_UP,
INTERCEPTION_MOUSE_BUTTON_2_DOWN = INTERCEPTION_MOUSE_RIGHT_BUTTON_DOWN,
INTERCEPTION_MOUSE_BUTTON_2_UP = INTERCEPTION_MOUSE_RIGHT_BUTTON_UP,
INTERCEPTION_MOUSE_BUTTON_3_DOWN = INTERCEPTION_MOUSE_MIDDLE_BUTTON_DOWN,
INTERCEPTION_MOUSE_BUTTON_3_UP = INTERCEPTION_MOUSE_MIDDLE_BUTTON_UP,
INTERCEPTION_MOUSE_BUTTON_4_DOWN = 0x040,
INTERCEPTION_MOUSE_BUTTON_4_UP = 0x080,
INTERCEPTION_MOUSE_BUTTON_5_DOWN = 0x100,
INTERCEPTION_MOUSE_BUTTON_5_UP = 0x200,
INTERCEPTION_MOUSE_WHEEL = 0x400,
INTERCEPTION_MOUSE_HWHEEL = 0x800
};
*/
}
static ManagedWrapper()
@ -239,33 +214,41 @@ namespace AutoHotInterception
[DllImport("kernel32.dll")]
private static extern IntPtr LoadLibrary(string dllToLoad);
[DllImport("interception.dll", EntryPoint = "interception_create_context", CallingConvention = CallingConvention.Cdecl)]
[DllImport("interception.dll", EntryPoint = "interception_create_context",
CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr CreateContext();
[DllImport("interception.dll", EntryPoint = "interception_destroy_context", CallingConvention = CallingConvention.Cdecl)]
[DllImport("interception.dll", EntryPoint = "interception_destroy_context",
CallingConvention = CallingConvention.Cdecl)]
public static extern void DestroyContext(IntPtr context);
[DllImport("interception.dll", EntryPoint = "interception_set_filter", CallingConvention = CallingConvention.Cdecl)]
[DllImport("interception.dll", EntryPoint = "interception_set_filter",
CallingConvention = CallingConvention.Cdecl)]
public static extern void SetFilter(IntPtr context, Predicate predicate, Filter filter);
// public static extern void SetFilter(IntPtr context, Predicate predicate, ushort filter);
// InterceptionFilter INTERCEPTION_API interception_get_filter(InterceptionContext context, InterceptionDevice device);
[DllImport("interception.dll", EntryPoint = "interception_get_filter", CallingConvention = CallingConvention.Cdecl)]
[DllImport("interception.dll", EntryPoint = "interception_get_filter",
CallingConvention = CallingConvention.Cdecl)]
public static extern ushort GetFilter(IntPtr context, int device);
[DllImport("interception.dll", EntryPoint = "interception_receive", CallingConvention = CallingConvention.Cdecl)]
[DllImport("interception.dll", EntryPoint = "interception_receive",
CallingConvention = CallingConvention.Cdecl)]
public static extern int Receive(IntPtr context, int device, ref Stroke stroke, uint nstroke);
[DllImport("interception.dll", EntryPoint = "interception_send", CallingConvention = CallingConvention.Cdecl)]
public static extern int Send(IntPtr context, int device, ref Stroke stroke, uint nstroke);
[DllImport("interception.dll", EntryPoint = "interception_is_keyboard", CallingConvention = CallingConvention.Cdecl)]
[DllImport("interception.dll", EntryPoint = "interception_is_keyboard",
CallingConvention = CallingConvention.Cdecl)]
public static extern int IsKeyboard(int device);
[DllImport("interception.dll", EntryPoint = "interception_is_mouse", CallingConvention = CallingConvention.Cdecl)]
[DllImport("interception.dll", EntryPoint = "interception_is_mouse",
CallingConvention = CallingConvention.Cdecl)]
public static extern int IsMouse(int device);
[DllImport("interception.dll", EntryPoint = "interception_is_invalid", CallingConvention = CallingConvention.Cdecl)]
[DllImport("interception.dll", EntryPoint = "interception_is_invalid",
CallingConvention = CallingConvention.Cdecl)]
public static extern int IsInvalid(int device);
//InterceptionDevice INTERCEPTION_API interception_wait(InterceptionContext context);
@ -273,11 +256,13 @@ namespace AutoHotInterception
public static extern int Wait(IntPtr context);
//InterceptionDevice INTERCEPTION_API interception_wait_with_timeout(InterceptionContext context, unsigned long milliseconds);
[DllImport("interception.dll", EntryPoint = "interception_wait_with_timeout", CallingConvention = CallingConvention.Cdecl)]
[DllImport("interception.dll", EntryPoint = "interception_wait_with_timeout",
CallingConvention = CallingConvention.Cdecl)]
public static extern int WaitWithTimeout(int device, ulong milliseconds);
// unsigned int INTERCEPTION_API interception_get_hardware_id(InterceptionContext context, InterceptionDevice device, void *hardware_id_buffer, unsigned int buffer_size);
[DllImport("interception.dll", EntryPoint = "interception_get_hardware_id", CallingConvention = CallingConvention.Cdecl)]
[DllImport("interception.dll", EntryPoint = "interception_get_hardware_id",
CallingConvention = CallingConvention.Cdecl)]
public static extern uint GetHardwareID(IntPtr context, int device, IntPtr hardwareidbuffer, uint buffersize);
// public static extern uint GetHardwareID(IntPtr context, int device, [MarshalAs(UnmanagedType.ByValArray,SizeConst=500)]char[] hardwareidbuffer, uint buffersize);
//public static extern uint GetHardwareID(IntPtr context, int device, ref _wchar_t[] hardwareidbuffer, uint buffersize);
@ -286,15 +271,41 @@ namespace AutoHotInterception
{
if (chars == 0)
chars = 500;
String result = "";
IntPtr bufferptr = Marshal.StringToHGlobalUni(new string(new char[chars]));
uint length = GetHardwareID(context, device, bufferptr, (uint)(chars * sizeof(char)));
if (length > 0 && length < (chars * sizeof(char)))
var result = "";
var bufferptr = Marshal.StringToHGlobalUni(new string(new char[chars]));
var length = GetHardwareID(context, device, bufferptr, (uint) (chars * sizeof(char)));
if (length > 0 && length < chars * sizeof(char))
result = Marshal.PtrToStringAuto(bufferptr);
Marshal.FreeHGlobal(bufferptr);
return result;
}
[StructLayout(LayoutKind.Sequential)]
public struct MouseStroke
{
public ushort state;
public ushort flags;
public short rolling;
public int x;
public int y;
public uint information;
}
[StructLayout(LayoutKind.Sequential)]
public struct KeyStroke
{
public ushort code;
public ushort state;
public uint information;
}
[StructLayout(LayoutKind.Explicit)]
public struct Stroke
{
[FieldOffset(0)] public MouseStroke mouse;
[FieldOffset(0)] public KeyStroke key;
}
/*
InterceptionContext INTERCEPTION_API interception_create_context(void);
void INTERCEPTION_API interception_destroy_context(InterceptionContext context);
@ -311,6 +322,5 @@ namespace AutoHotInterception
int INTERCEPTION_API interception_is_keyboard(InterceptionDevice device);
int INTERCEPTION_API interception_is_mouse(InterceptionDevice device);
*/
}
}
}

@ -1,73 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AutoHotInterception;
class TestApp
namespace TestApp
{
static void Main(string[] args)
internal class TestApp
{
//var mon = new AutoHotInterception.Monitor();
//var devInfo = mon.GetDeviceList();
//mon.Subscribe(new Action<int, ushort, ushort, uint>((id, state, code, info) =>
//{
// Console.WriteLine($"Subscription Value: State={state}, Code={code}");
//}), new Action<int, ushort, ushort, short, int, int, uint>((id, state, flags, rolling, x, y, info) =>
//{
// Console.WriteLine($"Subscription Value: x={x}, y={y}");
//}));
//mon.SetDeviceFilterState(16, true);
//Console.ReadLine();
//return;
// --------------------------------------------------------------
var im = new Manager();
var keyboardId = 0;
//keyboardId = im.GetDeviceId(false, 0x04F2, 0x0112); // WYSE
//keyboardId = im.GetDeviceId(false, 0x413C, 0x2107); // Dell
var mouseId = 0;
//mouseId = im.GetDeviceId(true, 0x46D, 0xC531); // G700s
//mouseId = im.GetDeviceId(true, 0x46D, 0xC00C); // Logitech Wired
mouseId = im.GetDeviceId(true, 0xB57, 0x9091); // Parblo Tablet
//im.SendMouseButtonEvent(mouseId, 1, 1);
//Thread.Sleep(100);
//im.SendMouseButtonEvent(mouseId, 1, 0);
//im.SendMouseMoveRelative(mouseId, 100, 100);
//im.SendMouseMoveAbsolute(mouseId, 100, 100);
if (keyboardId != 0)
private static void Main()
{
im.SubscribeKey(keyboardId, 2, true, new Action<int>((value) =>
{
Console.WriteLine("Subscription Value: " + value);
}));
var im = new Manager();
im.SetContextCallback(keyboardId, new Action<int>((value) =>
{
Console.WriteLine("Context Value: " + value);
}));
}
var mouseHandle = "HID\\VID_046D&PID_C52B&REV_2407&MI_02&Qid_1028&WI_01&Class_00000004";
var mouseId = im.GetMouseIdFromHandle(mouseHandle);
if (mouseId != 0)
{
im.SubscribeMouseButton(mouseId, 1, true, new Action<int>((value) =>
{
Console.WriteLine("Mouse Button Value: " + value);
}));
var counter = 0;
im.SubscribeMouseMoveRelative(mouseId, false, new Action<int, int>((x, y) =>
if (mouseId != 0)
{
Console.WriteLine($"Mouse Axis Value: x={x}, y={y}");
}));
im.SubscribeMouseButton(mouseId, 1, true, new Action<int>(value =>
{
//Console.WriteLine("RButton Button Value: " + value);
}));
im.SubscribeMouseButton(mouseId, 3, true, new Action<int>(value =>
{
//Console.WriteLine("XButton1 Button Value: " + value);
}));
im.SubscribeMouseButton(mouseId, 4, true, new Action<int>(value =>
{
//Console.WriteLine("XButton2 Button Value: " + value);
}));
im.SubscribeMouseButton(mouseId, 5, true, new Action<int>(value =>
{
//Console.WriteLine("WheelVertical Value: " + value);
var mycounter = counter;
mycounter++;
Console.WriteLine("Counter: " + mycounter);
counter = mycounter;
}), false);
}
Console.ReadLine();
}
Console.ReadLine();
}
}
}

@ -119,24 +119,24 @@ class AutoHotInterception {
}
; ---------------------- Subscription Mode ----------------------
SubscribeKey(id, code, block, callback){
this.Instance.SubscribeKey(id, code, block, callback)
SubscribeKey(id, code, block, callback, concurrent := true){
this.Instance.SubscribeKey(id, code, block, callback, concurrent)
}
SubscribeMouseButton(id, btn, block, callback){
this.Instance.SubscribeMouseButton(id, btn, block, callback)
SubscribeMouseButton(id, btn, block, callback, concurrent := true){
this.Instance.SubscribeMouseButton(id, btn, block, callback, concurrent)
}
SubscribeMouseMove(id, block, callback){
this.Instance.SubscribeMouseMove(id, block, callback)
SubscribeMouseMove(id, block, callback, concurrent := true){
this.Instance.SubscribeMouseMove(id, block, callback, concurrent)
}
SubscribeMouseMoveRelative(id, block, callback){
this.Instance.SubscribeMouseMoveRelative(id, block, callback)
SubscribeMouseMoveRelative(id, block, callback, concurrent := true){
this.Instance.SubscribeMouseMoveRelative(id, block, callback, concurrent)
}
SubscribeMouseMoveAbsolute(id, block, callback){
this.Instance.SubscribeMouseMoveAbsolute(id, block, callback)
SubscribeMouseMoveAbsolute(id, block, callback, concurrent := true){
this.Instance.SubscribeMouseMoveAbsolute(id, block, callback, concurrent)
}
; ------------- Context Mode ----------------

Loading…
Cancel
Save