How to Detect Special Key Presses in a Notebook?

9

2

We can use:

SetOptions[EvaluationNotebook[], NotebookEventActions -> {{"KeyDown","s"} :> Print["triggered"]}]

to make the current notebook respond to s button press, also we can do the same to Modifier Keys like Ctrl, Shift, Alt or so.

Can we do the same with Backspace, Delete or F1~F12?

Additionally, can we detect the mouse-wheel's scrolling state?

Wjx

Posted 2017-03-10T01:43:21.847

Reputation: 8 808

If you are Windows you probably can rig something with .NET I can't find the code but I have done this once before with some obscure keys otherwise you can't. Mac probably has no way and linux/Ubuntu probably has a way but it is quite complex. – William – 2017-03-10T02:18:35.687

@Liam On the topic of .NET I was going back through a scrape for this answer and I believe I saw some internal stuff where they were adding .NET event handlers there, so it should be possible at that level. Wjx, I don't really know why WRI hasn't put in a "BackspaceKeyDown" option for EventHandler. Maybe try to contact tech support there. They'd be the final authority.

– b3m2a1 – 2017-03-10T06:06:22.330

@Liam I'm using windows, but unfortunately I know nothing about .NET, could you show some way? Thanks! – Wjx – 2017-03-10T10:48:25.893

What exactly do you mean by "scrolling state"? If you look here you'll notice we have "MouseMoved" and "MouseDragged". If you mean a mouse-wheel scroll on Windows I'm not sure how to get that.

– b3m2a1 – 2017-03-10T16:12:42.250

I mean the mouse-wheel scroll, sorry for the confusion...... – Wjx – 2017-03-10T16:17:03.170

Answers

14

Following detects backspace on Mac:

SetOptions[EvaluationNotebook[],NotebookEventActions->
     {{"KeyDown","\.08"}:>Print["triggered"]}]

This code can be helpful for finding out different codes for non-standard keys.

SetOptions[EvaluationNotebook[],NotebookEventActions->
     {"KeyDown":>Print[FullForm@CurrentValue["EventKey"]]}]

BlacKow

Posted 2017-03-10T01:43:21.847

Reputation: 6 158

1(+1) {"KeyDown","\.08"} works also on Windows. But {"KeyDown":>Print[FullForm@CurrentValue["EventKey"]]} doesn't catch Backspace. – Alexey Popkov – 2017-03-10T19:08:28.860

@AlexeyPopkov are you saying that {"KeyDown","\.08"} , but {"KeyDown":>Print[FullForm@CurrentValue["EventKey"]]} doesn't print ".008"??!! Does it catch ESC? – BlacKow – 2017-03-10T19:39:07.367

Oh fascinating! I guess I should have found this from: here. It prints something invisible and I assumed it was a blank cell. I will have to back and figure out what all of these do.

– b3m2a1 – 2017-03-11T01:09:58.803

1@BlacKow Yes, {"KeyDown":>Print[FullForm@CurrentValue["EventKey"]]} prints None when I press Backspace or ESC. I'm on Windows 7 x64, checked with versions 8.0.4, 10.4.1 and 11.0.1. – Alexey Popkov – 2017-03-11T04:33:03.710

6

@BlacKow's answer works well on Mac, but Mathematica cannot handle special keys (s.a. F2 or PAUSE) on Windows. Thus,

Here is a Windows workaround:

The idea is to create a dynamic link library that runs in a separate thread, as explained here. In this separate thread, the keyboard status is continuously tested. Once the specified key is pressed, an event is raised to the MMA kernel. The following code requires a working C-compiler on a Windows PC:

Needs["CCompilerDriver`GenericCCompiler`"];

keyListen[keycode_, action_] := With[{
    libMain = First@FileNames["async-examples-libmain.c", {$InstallationDirectory}, Infinity],
    src = "
      #include <stdlib.h>
      #include \"WolframLibrary.h\"
      #include \"WolframCompileLibrary.h\"
      #include \"WolframStreamsLibrary.h\"
      #include \"windows.h\"                    // for GetAsyncKeyState()

      /**********************************************************/
      typedef struct keyTrackerArgs_st
      {
          WolframIOLibrary_Functions ioLibrary;
          mint key;
      }* keyTrackerArgs;

      static void key_tracker(mint asyncObjID, void* vtarg)
      {
          keyTrackerArgs targ = (keyTrackerArgs)vtarg;
          WolframIOLibrary_Functions ioLibrary = targ->ioLibrary;
          const mint key = targ->key;
          free(targ);

          while(ioLibrary->asynchronousTaskAliveQ(asyncObjID))
          {
              if(GetAsyncKeyState(key) & 0x0001) {
                  ioLibrary->raiseAsyncEvent(asyncObjID, \"KeyPress\", NULL);
              }
          }
      }

      DLLEXPORT int start_key_tracker(WolframLibraryData libData,
          mint Argc, MArgument *Args, MArgument Res) 
      {
          mint asyncObjID;
          WolframIOLibrary_Functions ioLibrary = libData->ioLibraryFunctions;
          keyTrackerArgs threadArg = (keyTrackerArgs)malloc(sizeof(struct keyTrackerArgs_st));

          if(Argc != 1)
              return LIBRARY_FUNCTION_ERROR;

          threadArg->ioLibrary = ioLibrary;
          threadArg->key = MArgument_getInteger(Args[0]);
          asyncObjID = ioLibrary->createAsynchronousTaskWithThread(key_tracker, threadArg);
          MArgument_setInteger(Res, asyncObjID);

          return LIBRARY_NO_ERROR;
      }
      "
  },
  Module[{srcFile},
    srcFile = CreateFile["src.c"];
    WriteString[srcFile, src];
    Close[srcFile];
    lib = CreateLibrary[{srcFile, libMain}, "keyLib", "IncludeDirectories" -> {DirectoryName[libMain]}];
    DeleteFile[srcFile];
    keyListener = LibraryFunctionLoad[lib, "start_key_tracker", {Integer}, Integer];
    Internal`CreateAsynchronousTask[keyListener, {keycode}, action];
  ]
]

Let's say we want to make a sound (Beep[]) every time the "PAUSE" key is hit. Then we evaluate

keyListen[19, Beep[] &]

where 19 is the code for "VK_PAUSE". Here are all the Windows key codes, in case you need them:

virtualKeyList = {"VK_LBUTTON" -> 1, "VK_RBUTTON" -> 2, 
   "VK_CANCEL" -> 3, "VK_MBUTTON" -> 4, "VK_XBUTTON1" -> 5, 
   "VK_XBUTTON2" -> 6, "VK_BACK" -> 8, "VK_TAB" -> 9, 
   "VK_CLEAR" -> 12, "VK_RETURN" -> 13, "VK_SHIFT" -> 16, 
   "VK_CONTROL" -> 17, "VK_MENU" -> 18, "VK_PAUSE" -> 19, 
   "VK_CAPITAL" -> 20, "VK_KANA" -> 21, "VK_HANGUEL" -> 21, 
   "VK_HANGUL" -> 21, "VK_JUNJA" -> 23, "VK_FINAL" -> 24, 
   "VK_HANJA" -> 25, "VK_KANJI" -> 25, "VK_ESCAPE" -> 27, 
   "VK_CONVERT" -> 28, "VK_NONCONVERT" -> 29, "VK_ACCEPT" -> 30, 
   "VK_MODECHANGE" -> 31, "VK_SPACE" -> 32, "VK_PRIOR" -> 33, 
   "VK_NEXT" -> 34, "VK_END" -> 35, "VK_HOME" -> 36, "VK_LEFT" -> 37, 
   "VK_UP" -> 38, "VK_RIGHT" -> 39, "VK_DOWN" -> 40, 
   "VK_SELECT" -> 41, "VK_PRINT" -> 42, "VK_EXECUTE" -> 43, 
   "VK_SNAPSHOT" -> 44, "VK_INSERT" -> 45, "VK_DELETE" -> 46, 
   "VK_HELP" -> 47, "VK_LWIN" -> 91, "VK_RWIN" -> 92, "VK_APPS" -> 93,
    "VK_SLEEP" -> 95, "VK_NUMPAD0" -> 96, "VK_NUMPAD1" -> 97, 
   "VK_NUMPAD2" -> 98, "VK_NUMPAD3" -> 99, "VK_NUMPAD4" -> 100, 
   "VK_NUMPAD5" -> 101, "VK_NUMPAD6" -> 102, "VK_NUMPAD7" -> 103, 
   "VK_NUMPAD8" -> 104, "VK_NUMPAD9" -> 105, "VK_MULTIPLY" -> 106, 
   "VK_ADD" -> 107, "VK_SEPARATOR" -> 108, "VK_SUBTRACT" -> 109, 
   "VK_DECIMAL" -> 110, "VK_DIVIDE" -> 111, "VK_F1" -> 112, 
   "VK_F2" -> 113, "VK_F3" -> 114, "VK_F4" -> 115, "VK_F5" -> 116, 
   "VK_F6" -> 117, "VK_F7" -> 118, "VK_F8" -> 119, "VK_F9" -> 120, 
   "VK_F10" -> 121, "VK_F11" -> 122, "VK_F12" -> 123, "VK_F13" -> 124,
    "VK_F14" -> 125, "VK_F15" -> 126, "VK_F16" -> 127, 
   "VK_F17" -> 128, "VK_F18" -> 129, "VK_F19" -> 130, "VK_F20" -> 131,
    "VK_F21" -> 132, "VK_F22" -> 133, "VK_F23" -> 134, 
   "VK_F24" -> 135, "VK_NUMLOCK" -> 144, "VK_SCROLL" -> 145, 
   "VK_OEM_PLUS" -> 187, "VK_OEM_COMMA" -> 188, "VK_OEM_MINUS" -> 189,
    "VK_OEM_PERIOD" -> 190, "VK_OEM_2" -> 191, "VK_OEM_5" -> 220, 
   "VK_OEM_8" -> 223, "VK_PACKET" -> 231};

JEM_Mosig

Posted 2017-03-10T01:43:21.847

Reputation: 2 559

5

So after BlacKow's insight I found more of these by playing with this:

EventHandler[
 Panel["", ImageSize -> 100],
 "KeyDown" :>
  Print[Row@{FullForm@CurrentValue@"EventKey", " pressed"}]
 ]

A few more points of interest (this is potentially Mac specific as I am a Mac user):

"\[RawEscape]" is the escape key

"\.1c" is "LeftArrowKeyDown"

"\.1e" is "UpArrowKeyDown"

"\.1d" is "RightArrowKeyDown"

"\.1f" is "DownArrowKeyDown"

"\r" is "ReturnKeyDown"

"\[GreaterEqual]" is control-. (and various other special characters can be found as a control-key combo, but these can be easily figured out I think)

"\.10" is function-(any function key not bound)

Hopefully someone can extend this further and to Windows / Linux? Useful collection of keys to have out there.

b3m2a1

Posted 2017-03-10T01:43:21.847

Reputation: 42 610

I don't have ColoredPanel in version 11.0.1. What is this? – Alexey Popkov – 2017-03-11T04:36:32.980

Oops. That's something I wrote to generate a panel with that sort of rounded background like in the docked cells of the documentation notebooks (I actually pulled the appearance from there then tweaked). I'll change that to just Panel in a bit. – b3m2a1 – 2017-03-11T05:57:47.680

3

About the issue of capturing inputs from the mouse scroll wheel: I recently found a way to do this after a suggestion by John Fultz. It's a pretty dumb hack, but it works and it seems to be the only way to do it.

The main idea is that there is one avenue for capturing something directly affected by the mouse wheel: the scroll bar in a Pane can be made dynamic with ScrollPosition -> Dynamic[...]. We can abuse this as follows:

DynamicModule[{x = {0.1, 0.}, font = 8},
 Pane[
  Pane[
   Style["Hello world", FontSize -> Dynamic[font]],
   ImageSize -> {Full, 1100}, AppearanceElements -> None, 
   Scrollbars -> None],
  ImageSize -> {Full, 1000},
  AppearanceElements -> None
  Scrollbars -> {False, True},
  ScrollPosition -> Dynamic[x,
    Function[
     If[#[[2]] >= x[[2]],
      font--,
      font++
     ];
     x[[2]] = Mod[x[[2]] + 0.001, 0.1, 0.1];
    ]
   ]
  ]
]

The inner Pane is bigger than the outer one to ensure that the outer Pane needs to show a scrollbar. The outer scrollbar is then used to capture scroll events. It's necessary keep the scrollbar away from the two most extreme positions and it also needs to move slightly to prevent outer scrollbars (like the one from the notebook) from scrolling. That's why the Function contains a line that slightly moves the position of the scrollbar and loops it around.

To hide the scroll bar, you can put another, slightly slimmer, Pane around the whole thing, or you can make the scrollbar drop out of the existing Pane with something like:

ImageSize -> {Dynamic[CurrentValue[WindowSize][[1]] - 100], 1000}

instead of ImageSize -> Full.

Sjoerd Smit

Posted 2017-03-10T01:43:21.847

Reputation: 15 418