1 /+
2               Copyright Elias Batek 2017 - 2018.
3      Distributed under the Boost Software License, Version 1.0.
4         (See accompanying file LICENSE_1_0.txt or copy at
5               https://www.boost.org/LICENSE_1_0.txt)
6  +/
7 module midigamepad.lib.keyboard;
8 
9 import core.sys.windows.winbase;
10 import core.sys.windows.windef : HKL;
11 import core.sys.windows.winuser;
12 import std.conv : to;
13 
14 /++
15     Determines whether the specified vKey is an extended key or not
16 
17     See_Also:
18         https://msdn.microsoft.com/en-us/library/windows/desktop/ms646267(v=vs.85).aspx#extended_key_flag
19  +/
20 bool hasExtendedScancode(uint vkey)
21 {
22     switch (vkey)
23     {
24     case VK_INSERT:
25     case VK_DELETE:
26     case VK_HOME:
27     case VK_END:
28     case VK_PRIOR:
29     case VK_NEXT:
30     case VK_LEFT:
31     case VK_UP:
32     case VK_RIGHT:
33     case VK_DOWN:
34     case VK_NUMLOCK:
35     case VK_PRINT:
36         return true;
37 
38     default:
39         return false;
40     }
41 }
42 
43 /++
44     Sends a keydown event to the OS
45 
46     Params:
47         vKey =  virtual key to press
48  +/
49 void synthesizeKeyDown(uint vKey)
50 {
51     synthesize(Scancode(vKey.toScancode.to!ushort, vKey.hasExtendedScancode), false);
52 }
53 
54 /++
55     Sends a keyup event to the OS
56 
57     Params:
58         vKey =  virtual key to release
59  +/
60 void synthesizeKeyUp(uint vKey)
61 {
62     synthesize(Scancode(vKey.toScancode.to!ushort, vKey.hasExtendedScancode), true);
63 }
64 
65 /++
66     Synthesizes a key press/release by sending its scancode
67 
68     Params:
69         scancode =  scancode of the key to press
70         
71         up =        true  ... UP
72                     false ... DOWN
73  +/
74 void synthesize(Scancode scancode, bool up) @nogc nothrow
75 {
76     INPUT input = INPUT();
77     input.type = INPUT_KEYBOARD;
78 
79     input.ki.time = 0;
80     input.ki.wVk = 0;
81     input.ki.dwExtraInfo = 0;
82 
83     input.ki.dwFlags = KEYEVENTF_SCANCODE;
84     if (scancode.extended)
85     {
86         input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
87     }
88     if (up)
89     {
90         input.ki.dwFlags |= KEYEVENTF_KEYUP;
91     }
92 
93     input.ki.wScan = scancode.scancode;
94 
95     const uint rslt = SendInput(1, &input, INPUT.sizeof);
96     assert(rslt != 0);
97 }
98 
99 /++
100     Returns: The scancode of the specified vKey
101  +/
102 uint toScancode(uint vKey) @nogc nothrow
103 {
104     static HKL keyboardLayout = null;
105 
106     if (keyboardLayout is null)
107     {
108         keyboardLayout = GetKeyboardLayout(0);
109     }
110     return MapVirtualKeyEx(vKey, MAPVK_VK_TO_VSC_EX, keyboardLayout);
111 }
112 
113 /++
114     Manager class for a key press/release loop
115  +/
116 final class KeyboardSynthesizerLoopMgr
117 {
118     import core.thread : Thread;
119     import core.time : dur;
120     import std.algorithm.mutation : remove;
121     import std.algorithm.searching : canFind, countUntil;
122     import std.range : empty;
123 
124     private
125     {
126         Scancode[] _down;
127         Scancode[] _requestUp;
128         Scancode[] _requestDown;
129 
130         Thread _loopThread;
131         bool _loopThreadKill;
132         long _loopThreadTimeout = 50;
133     }
134 
135     public
136     {
137         @property
138         {
139             /++
140                 Returns: true = loop thread is running
141              +/
142             bool isRunning() @nogc nothrow
143             {
144                 return ((this._loopThread !is null) && this._loopThread.isRunning);
145             }
146         }
147     }
148 
149     public
150     {
151         /++
152             Adds a key that will be "hold down" by the loop
153          +/
154         void press(Scancode scancode) nothrow pure @safe
155         {
156             if (!_down.canFind(scancode))
157                 this._requestDown ~= scancode;
158         }
159 
160         /++ ditto +/
161         void pressKey(uint vKey)
162         {
163             this.press(Scancode(vKey.toScancode.to!ushort, vKey.hasExtendedScancode));
164         }
165 
166         /++
167             "Releases" a key that is "hold down" by the loop
168          +/
169         void release(Scancode scancode) nothrow pure @safe
170         in
171         {
172             assert(this._down.canFind(scancode));
173         }
174         body
175         {
176             if (!this._requestUp.canFind(scancode))
177                 this._requestUp ~= scancode;
178         }
179 
180         /++ ditto +/
181         void releaseKey(uint vKey)
182         {
183             this.release(Scancode(vKey.toScancode.to!ushort, vKey.hasExtendedScancode));
184         }
185 
186         /++
187             Starts the sending loop thread
188 
189             This will crash if the loop thread is already running.
190             Check the .isRunning property first or call .tryStart() instead.
191          +/
192         void start()
193         in
194         {
195             assert(!this.isRunning);
196         }
197         body
198         {
199             this._loopThread.destroy();
200             this._loopThread = new Thread(&loop);
201             this._loopThreadKill = false;
202             this._loopThread.start();
203         }
204 
205         /++
206             Kills the sending loop thread
207          +/
208         void stop() @nogc nothrow pure @safe
209         {
210             this._loopThreadKill = true;
211         }
212 
213         /++
214             "Releases" a key if it has been "held down" by the loop
215 
216             Returns:
217                 true = if the key has been "held down" (which means the release will get performed)
218          +/
219         bool tryRelease(Scancode scancode) nothrow pure @safe
220         {
221             if (!this._down.canFind(scancode))
222                 return false;
223 
224             release(scancode);
225             return true;
226         }
227 
228         /++ ditto +/
229         bool tryReleaseKey(uint vKey)
230         {
231             return tryRelease(Scancode(vKey.toScancode.to!ushort, vKey.hasExtendedScancode));
232         }
233 
234         /++
235             Starts the sending loop thread if it is not already running
236 
237             Returns:
238                 true  = loop thread got started
239                 false = loop thread has already been running
240          +/
241         bool tryStart()
242         {
243             if (this.isRunning)
244                 return false;
245 
246             this.start();
247             return true;
248         }
249     }
250 
251     private
252     {
253         void loop()
254         {
255             do
256             {
257                 if (!this._requestUp.empty)
258                 {
259                     foreach (Scancode scancode; this._requestUp)
260                     {
261                         // send keyUp
262                         synthesize(scancode, true);
263 
264                         // can only occurre once, so just remove its first occurrence
265                         const ptrdiff_t idx = this._down.countUntil(scancode);
266                         this._down = this._down.remove(idx);
267                     }
268 
269                     this._requestUp = [];
270                 }
271 
272                 if (!this._requestDown.empty)
273                 {
274                     this._down ~= this._requestDown;
275                     this._requestDown = [];
276                 }
277 
278                 foreach (Scancode scancode; this._down)
279                 {
280                     // send keyDown
281                     synthesize(scancode, false);
282                 }
283 
284                 // wait for a while
285                 Thread.sleep(dur!"msecs"(this._loopThreadTimeout));
286             }
287             while (!this._loopThreadKill);
288         }
289     }
290 }
291 
292 /++
293     Represents a keyboard scancode
294  +/
295 struct Scancode
296 {
297 @nogc nothrow pure @safe:
298 
299     /++
300         Specifies whether the scancode is an extended one
301      +/
302     bool extended;
303 
304     /++
305         Keyboard scancode
306      +/
307     ushort scancode;
308 
309     /++
310         ctor
311      +/
312     this(ushort scancode, bool extended)
313     {
314         this.scancode = scancode;
315         this.extended = extended;
316     }
317 }