One rather drastic solution is to use LibraryLink to just get the keyboard state directly from the operating system, and then deploy a high-frequency ScheduledTask to poll the keyboard state. I do this on Windows using C.
Here's a minimal working example, showing how to get the keyboard state into the kernel as a ByteArray, and then using EventHandler to change the color of a graphic when a certain key is pressed down, and to change it back as soon as the key is released.
In a separate file, library.c:
#include "WolframLibrary.h"
#include "WolframNumericArrayLibrary.h"
#include <Windows.h>
#include <string.h>
DLLEXPORT int getKeyboardState(WolframLibraryData libData, mint Argc, MArgument *Args, MArgument Res){
GetKeyState(0);
BYTE keyboardState[256] = {0};
if (GetKeyboardState(keyboardState)){
MNumericArray keyboardByteArray;
const mint length = 256;
if (libData->numericarrayLibraryFunctions->MNumericArray_new(MNumericArray_Type_UBit8, 1, &length, &keyboardByteArray)) {
return LIBRARY_FUNCTION_ERROR;
}
memcpy((uint8_t*) libData->numericarrayLibraryFunctions->MNumericArray_getData(keyboardByteArray), keyboardState, 256);
MArgument_setMNumericArray(Res, keyboardByteArray);
return LIBRARY_NO_ERROR;
} else {
return LIBRARY_FUNCTION_ERROR;
}
}
I use CCompilerDriver to compile this into a DLL, and then I load the function into the kernel and make some appropriate definitions.
<<CCompilerDriver`
CreateLibrary[
File@"library.c",
"library.dll",
"Libraries" -> {"User32.lib"},
"Language" -> "C"
]
With[
{func =
LibraryFunctionLoad[
File@"library.dll",
"getKeyboardState",
{}, ByteArray
]
},
keyboard[] := func[];
keyboard[vk_Integer] := func[][[vk + 1]]
]
Now you can use this function in a ScheduledTask to poll the state a particular key.
color=Black;
w$pressed=.
EventHandler[
Graphics[
{Dynamic@color, Rectangle[],
White, Arrow@{
{.5, .5},
Dynamic@MousePosition["Graphics", {.5, .5}]}
}],
{{"KeyDown", "w"} :>
If[! TrueQ@w$pressed, (w$pressed = True;
color = Purple;
task = SessionSubmit@ScheduledTask[
If[(keyboard[87]~Quotient~128) == 0,
(TaskRemove@task;
w$pressed = False;
color = Black
)], 1/100])]
}
]
Select the resulting Graphics cell, and you'll see that the background color changes to purple as soon as the W key is pressed, and changes back again as soon as it's let go. Here's a GIF demonstrating: https://i.sstatic.net/xQx5B.jpg
Unfortunately, this solution is specific to Windows, but I believe it should be easy to generalize this example.