1 module pixelperfectengine.system.input.handler;
2 
3 import bindbc.sdl.bind;
4 
5 import collections.treemap;
6 import collections.linkedlist;
7 import collections.commons : defaultHash;
8 
9 public import pixelperfectengine.system.input.types;
10 public import pixelperfectengine.system.input.interfaces;
11 public import pixelperfectengine.graphics.common : Box;
12 import pixelperfectengine.system.input.scancode;
13 
14 /**
15  * Converts and redirects inputs as events.
16  */
17 public class InputHandler {
18 	/**
19 	 * Contains data related to joysticks.
20 	 */
21 	public struct JoyInfo {
22 		string			name;			///The name of the joystick
23 		int				buttons;		///The amount of buttons on this joystick
24 		int				axis;			///The amount of axes of this joystick
25 		int				hats;			///The amount of hats (d-pads) of this joystick
26 	}
27 	protected enum StatusFlags {
28 		none				=	0,
29 		TextInputEnable		=	1<<0,
30 		TI_ReportTextEditingEvents	=	1<<1,
31 		CaptureEvent		=	1<<2,
32 		CE_DelConflCodes	=	1<<3,	///If set, the code will be removed from all other keys
33 		CE_DelConflKeys		=	1<<4,	///If set, alternative codes of the key will be removed.
34 		CE_CancelOnSysEsc	=	1<<5,	///If set, bindings with the `SysEsc` ID will cancel event capture
35 		CE_AllowMouse		=	1<<6,	///If set, allows mouse buttons to be recorded
36 		AnyKey				=	1<<7	///Enables an anykey event.
37 	}
38 	///Code that is emmitted on an any key event.
39 	public static immutable uint anykeyCode = 1402_508_842; //= defaultHash("AnyKey");
40 	///Code that is emmitted on a system escape key (keyboard Esc key, joystick start button, etc) event.
41 	public static immutable uint sysescCode = 2320_826_867; //= defaultHash("SysEsc");
42 	//alias CodeTreeSet = TreeMap!(uint, void);
43 	
44 	alias CodeTreeSet = TreeMap!(InputBinding, void);
45 	alias InputBindingLookupTree = TreeMap!(BindingCode, CodeTreeSet);//alias InputBindingLookupTree = TreeMap!(BindingCode, InputBinding[]);
46 	alias JoyInfoMap = TreeMap!(int, JoyInfo);
47 	alias JoyMap = TreeMap!(int, SDL_Joystick*);
48 	/**
49 	 * The main input lookup tree.
50 	 */
51 	protected InputBindingLookupTree	inputLookup;
52 	///Contains pointers related to joystick handling
53 	protected JoyMap					joysticks;
54 	///Contains info related to each joystick
55 	protected JoyInfoMap				joyInfo;
56 	///Contains info about the current status of the Input Handler.
57 	///See the enum `StatusFlags` for more info.
58 	protected uint						statusFlags;
59 	///The currently recorded code.
60 	protected InputBinding				recordedCode;
61 	///Passes all codes from keybindings to this interface.
62 	///Multiple listeners at once are removed from newer versions to reduce overhead.
63 	///Functionality can be restored with an event hub.
64 	public InputListener				inputListener;
65 	///Passes all system events to this interface.
66 	///Multiple listeners at once are removed from newer versions to reduce overhead.
67 	///Functionality can be restored with an event hub.
68 	public SystemEventListener			systemEventListener;
69 	///Passes all text input events to this interface.
70 	///Only a single listener should get text input at once.
71 	protected TextInputListener			textInputListener;
72 	///Passes all mouse events to this interface.
73 	///Multiple listeners at once are removed from newer versions to reduce overhead.
74 	///Functionality can be restored with an event hub.
75 	public MouseListener				mouseListener;
76 	/**
77 	 * CTOR.
78 	 * Detects joysticks upon construction.
79 	 * IMPORTANT: Only a single instance of this class should exist!
80 	 */
81 	public this() {
82 		SDL_InitSubSystem(SDL_INIT_JOYSTICK);
83 		detectJoys();
84 	}
85 	~this() {
86 		foreach(joy; joysticks) {
87 			SDL_JoystickClose(joy);
88 		}
89 	}
90 	/**
91 	 * Adds a single inputbinding to the inputhandler.
92 	 */
93 	public void addBinding(BindingCode bc, InputBinding ib) @safe nothrow {
94 		CodeTreeSet* i = inputLookup.ptrOf(bc);
95 		if (i) 
96 			i.put(ib);
97 		else 
98 			inputLookup[bc] = CodeTreeSet([ib]);
99 	}
100 	/**
101 	 * Removes a single inputbinding from the inputhandler.
102 	 */
103 	public void removeBinding(BindingCode bc, InputBinding ib) @safe nothrow {
104 		CodeTreeSet* i = inputLookup.ptrOf(bc);
105 		if (i) {
106 			if (i.length == 1)
107 				inputLookup.remove(bc);
108 			else
109 				i.removeByElem(ib);
110 		}
111 	}
112 	/**
113 	 * Replaces the keycode lookup tree.
114 	 * Returns a copy of the current one as a backup.
115 	 */
116 	public InputBindingLookupTree replaceLookupTree(InputBindingLookupTree newTree) @safe nothrow {
117 		InputBindingLookupTree backup = inputLookup;
118 		inputLookup = newTree;
119 		return backup;
120 	}
121 	///Static CTOR to init joystick handling
122 	///Only one input handler should be made per application to avoid issues from SDL_Poll indiscriminately polling all events
123 	/+static this() {
124 		SDL_InitSubSystem(SDL_INIT_JOYSTICK);
125 	}+/
126 	/**
127 	 * Detects all connected joysticks.
128 	 */
129 	public void detectJoys() {
130 		import std.string : fromStringz;
131 		import std.conv : to;
132 		const int numOfJoysticks = SDL_NumJoysticks();
133 		for(int i ; i < numOfJoysticks ; i++){
134 			if(!joysticks.ptrOf(i)){
135 				SDL_Joystick* joy = SDL_JoystickOpen(i);
136 				if(joy) {
137 					joysticks[i] = joy;
138 					joyInfo[i] = JoyInfo(fromStringz(SDL_JoystickName(joy)).dup, SDL_JoystickNumButtons(joy), SDL_JoystickNumAxes(joy),
139 							SDL_JoystickNumHats(joy));
140 				}
141 			}
142 		}
143 	}
144 	/**
145 	 * Removes info related to disconnected joysticks.
146 	 */
147 	public void removeJoy(int i) {
148 		SDL_JoystickClose(joysticks[i]);
149 		joysticks.remove(i);
150 	}
151 	/**
152 	 * Starts text input handling.
153 	 */
154 	public void startTextInput(TextInputListener listener, bool reportTextEditingEvents = false, Box textEditingArea = Box.init) {
155 		if (textInputListener !is null) textInputListener.dropTextInput();
156 		textInputListener = listener;
157 		statusFlags |= StatusFlags.TextInputEnable;
158 		if (reportTextEditingEvents) statusFlags |= StatusFlags.TI_ReportTextEditingEvents;
159 		SDL_StartTextInput();
160 		listener.initTextInput();
161 	}
162 	/**
163 	 * Stops text input handling.
164 	 */
165 	public void stopTextInput() {
166 		textInputListener.dropTextInput();
167 		SDL_StopTextInput();
168 		textInputListener = null;
169 		statusFlags = 0;
170 	}
171 	/**
172 	 * Initializes event recording.
173 	 */
174 	public void recordEvent(InputBinding code, bool delConflKeys, bool delConflCodes, bool cancelOnSysEsc, bool allowMouseEvents) {
175 		recordedCode = code;
176 		statusFlags |= StatusFlags.CaptureEvent;
177 		if (delConflCodes) statusFlags |= StatusFlags.CE_DelConflCodes;
178 		if (delConflKeys) statusFlags |= StatusFlags.CE_DelConflKeys;
179 		if (cancelOnSysEsc) statusFlags |= StatusFlags.CE_CancelOnSysEsc;
180 		if (allowMouseEvents) statusFlags |= StatusFlags.CE_AllowMouse;
181 	}
182 	/**
183 	 * Enables an anykey event for once.
184 	 */
185 	public void expectAnyKeyEvent() {
186 		statusFlags |= StatusFlags.AnyKey;
187 	}
188 	/**
189 	 * Tests for input.
190 	 */
191 	public void test() {
192 		SDL_Event event;
193 		while(SDL_PollEvent(&event)) {
194 			BindingCode bc;
195 			bool release;
196 			float axisOffset;
197 			uint timestamp;
198 			switch(event.type) {
199 				case SDL_KEYUP:
200 					release = true;
201 					goto case SDL_KEYDOWN;
202 				case SDL_KEYDOWN:
203 					bc.buttonNum = cast(ushort)event.key.keysym.scancode;
204 					bc.deviceTypeID = Devicetype.Keyboard;
205 					bc.modifierFlags = keyModConv(event.key.keysym.mod);
206 					timestamp = event.key.timestamp;
207 					break;
208 				case SDL_JOYBUTTONUP:
209 					release = true;
210 					goto case SDL_JOYBUTTONDOWN;
211 				case SDL_JOYBUTTONDOWN:
212 					bc.deviceNum = cast(ubyte)event.jbutton.which;
213 					bc.deviceTypeID = Devicetype.Joystick;
214 					bc.buttonNum = event.jbutton.button;
215 					break;
216 				case SDL_JOYHATMOTION:
217 					bc.deviceNum = cast(ubyte)event.jhat.which;
218 					bc.deviceTypeID = Devicetype.Joystick;
219 					bc.buttonNum = event.jhat.value;
220 					bc.modifierFlags = JoyModifier.DPad;
221 					bc.extArea = event.jhat.hat;
222 					break;
223 				case SDL_JOYAXISMOTION:
224 					bc.deviceNum = cast(ubyte)event.jaxis.which;
225 					bc.deviceTypeID = Devicetype.Joystick;
226 					bc.buttonNum = event.jaxis.axis;
227 					bc.modifierFlags = JoyModifier.Axis;
228 					axisOffset = cast(real)event.jaxis.value / short.max;
229 					break;
230 				case SDL_MOUSEBUTTONUP:
231 					release = true;
232 					goto case SDL_MOUSEBUTTONDOWN;
233 				case SDL_MOUSEBUTTONDOWN:
234 					bc.deviceNum = cast(ubyte)event.button.which;
235 					bc.deviceTypeID = Devicetype.Mouse;
236 					bc.buttonNum = event.button.button;
237 					if (mouseListener) mouseListener.mouseClickEvent(MouseEventCommons(event.button.timestamp, event.button.windowID, 
238 							event.button.which), MouseClickEvent(event.button.x, event.button.y, event.button.button, event.button.clicks, 
239 							event.button.state == 1));
240 					break;
241 				case SDL_MOUSEMOTION:
242 					if (mouseListener) mouseListener.mouseMotionEvent(MouseEventCommons(event.motion.timestamp, event.motion.windowID, 
243 							event.motion.which), MouseMotionEvent(event.motion.state, event.motion.x, event.motion.y, event.motion.xrel, 
244 							event.motion.yrel));
245 					break;
246 				case SDL_MOUSEWHEEL:
247 					if (mouseListener) mouseListener.mouseWheelEvent(MouseEventCommons(event.wheel.timestamp, event.wheel.windowID, event.wheel.which),
248 							MouseWheelEvent(event.wheel.x, event.wheel.y));
249 					break;
250 				case SDL_TEXTINPUT:
251 					import std.utf : toUTF32;
252 					import std.string : fromStringz;
253 					if (statusFlags & StatusFlags.TextInputEnable) {
254 						dstring eventText = toUTF32(fromStringz(event.text.text.ptr));
255 						textInputListener.textInputEvent(event.text.timestamp, event.text.windowID, eventText);
256 					}
257 					break;
258 				case SDL_TEXTEDITING:
259 					import std.utf : toUTF32;
260 					import std.string : fromStringz;
261 					if (statusFlags & StatusFlags.TI_ReportTextEditingEvents) {
262 						dstring eventText = toUTF32(fromStringz(event.edit.text.ptr));
263 						textInputListener.textEditingEvent(event.edit.timestamp, event.edit.windowID, eventText, event.edit.start, 
264 								event.edit.length);
265 					}
266 					break;
267 				case SDL_QUIT:
268 					if (systemEventListener) systemEventListener.onQuit();
269 					break;
270 				case SDL_JOYDEVICEADDED:
271 					detectJoys;
272 					if (systemEventListener) systemEventListener.controllerAdded(event.jdevice.which);
273 					break;
274 				case SDL_JOYDEVICEREMOVED:
275 					removeJoy(event.jdevice.which);
276 					if (systemEventListener) systemEventListener.controllerRemoved(event.jdevice.which); 
277 					break;
278 				default: break;
279 			}
280 			if (bc.base){
281 				if (!statusFlags) {	//Generate input event
282 					CodeTreeSet hashcodeSet = inputLookup[bc];
283 					if (hashcodeSet.length) {
284 						if (bc.isJoyAxis) {
285 							foreach (InputBinding key; hashcodeSet) {
286 								if (key.flags & key.IS_AXIS_AS_BUTTON) {
287 									if (key.deadzone[1] >= axisOffset || key.deadzone[0] <= axisOffset)
288 										inputListener.keyEvent(key.code, bc, timestamp, true);
289 									else
290 										inputListener.keyEvent(key.code, bc, timestamp, false);
291 								} else {
292 									inputListener.axisEvent(key.code, bc, timestamp, axisOffset);
293 								}
294 							}
295 						} else {
296 							foreach (InputBinding key; hashcodeSet) {
297 								/+foreach(InputListener il; inputListeners) il.keyEvent(key, bc, timestamp);+/
298 								inputListener.keyEvent(key.code, bc, timestamp, !release);
299 							}
300 						}
301 					}
302 				} else if (statusFlags & StatusFlags.TextInputEnable && bc.deviceTypeID == Devicetype.Keyboard && !release) {		//Generate text editing input
303 					switch(bc.buttonNum){
304 						case ScanCode.ENTER, ScanCode.ENTER2, ScanCode.NP_ENTER:
305 							textInputListener.textInputKeyEvent(event.key.timestamp, event.key.windowID, TextInputKey.Enter, 
306 									bc.modifierFlags);
307 							break;
308 						case ScanCode.ESCAPE:
309 							textInputListener.textInputKeyEvent(event.key.timestamp, event.key.windowID, TextInputKey.Escape, 
310 									bc.modifierFlags);
311 							break;
312 						case ScanCode.BACKSPACE, ScanCode.NP_BACKSPACE, ScanCode.ALTERASE:
313 							textInputListener.textInputKeyEvent(event.key.timestamp, event.key.windowID, TextInputKey.Backspace, 
314 									bc.modifierFlags);
315 							break;
316 						case ScanCode.UP:
317 							textInputListener.textInputKeyEvent(event.key.timestamp, event.key.windowID, TextInputKey.CursorUp, 
318 									bc.modifierFlags);
319 							break;
320 						case ScanCode.DOWN:
321 							textInputListener.textInputKeyEvent(event.key.timestamp, event.key.windowID, TextInputKey.CursorDown, 
322 									bc.modifierFlags);
323 							break;
324 						case ScanCode.LEFT:
325 							textInputListener.textInputKeyEvent(event.key.timestamp, event.key.windowID, TextInputKey.CursorLeft, 
326 									bc.modifierFlags);
327 							break;
328 						case ScanCode.RIGHT:
329 							textInputListener.textInputKeyEvent(event.key.timestamp, event.key.windowID, TextInputKey.CursorRight, 
330 									bc.modifierFlags);
331 							break;
332 						case ScanCode.INSERT:
333 							textInputListener.textInputKeyEvent(event.key.timestamp, event.key.windowID, TextInputKey.Insert, 
334 									bc.modifierFlags);
335 							break;
336 						case ScanCode.DELETE:
337 							textInputListener.textInputKeyEvent(event.key.timestamp, event.key.windowID, TextInputKey.Delete, 
338 									bc.modifierFlags);
339 							break;
340 						case ScanCode.HOME:
341 							textInputListener.textInputKeyEvent(event.key.timestamp, event.key.windowID, TextInputKey.Home, 
342 									bc.modifierFlags);
343 							break;
344 						case ScanCode.END:
345 							textInputListener.textInputKeyEvent(event.key.timestamp, event.key.windowID, TextInputKey.End, 
346 									bc.modifierFlags);
347 							break;
348 						case ScanCode.PAGEUP:
349 							textInputListener.textInputKeyEvent(event.key.timestamp, event.key.windowID, TextInputKey.PageUp, 
350 									bc.modifierFlags);
351 							break;
352 						case ScanCode.PAGEDOWN:
353 							textInputListener.textInputKeyEvent(event.key.timestamp, event.key.windowID, TextInputKey.PageDown, 
354 									bc.modifierFlags);
355 							break;
356 						default: break;
357 					}
358 				} else if (statusFlags & StatusFlags.CaptureEvent) {			//Record event as keybinding
359 					CodeTreeSet* hashcodeSet = inputLookup.ptrOf(bc);
360 					if (hashcodeSet && !(hashcodeSet.has(sysescCode) && statusFlags & StatusFlags.CE_CancelOnSysEsc)) {
361 						if (StatusFlags.CE_DelConflCodes) {
362 							BindingCode[] toRemove;
363 							foreach (BindingCode key, ref CodeTreeSet hashCodes; inputLookup) {
364 								hashCodes.removeByElem(recordedCode);
365 								if (!hashCodes.length) toRemove ~= key;
366 							}
367 							foreach (key; toRemove) {
368 								inputLookup.remove(key);
369 							}
370 						}
371 						if (hashcodeSet.length) {
372 							if (!(statusFlags & StatusFlags.CE_DelConflKeys)) hashcodeSet.put(recordedCode);
373 							else inputLookup[bc] = CodeTreeSet([recordedCode]);
374 						} else inputLookup[bc] = CodeTreeSet([recordedCode]);
375 					}
376 					statusFlags = 0;
377 				} else if (statusFlags & StatusFlags.AnyKey) {					//Any key event
378 					inputListener.keyEvent(anykeyCode, bc, timestamp, true);
379 					statusFlags = 0;
380 				}
381 			}
382 		}
383 	}
384 	/**
385 	 * Returns a default SysEsc binding.
386 	 */
387 	public static BindingCode getSysEscKey() @nogc @safe pure nothrow {
388 		import pixelperfectengine.system.input.scancode : ScanCode;
389 		const BindingCode bc = BindingCode(ScanCode.ESCAPE, 0, Devicetype.Keyboard, 0);
390 		return bc;
391 	}
392 }