1 module pixelperfectengine.system.input.handler; 2 3 import bindbc.sdl; 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 }