1 // Copyright Ferdinand Majerech 2014. 2 // Distributed under the Boost Software License, Version 1.0. 3 // (See accompanying file LICENSE_1_0.txt or copy at 4 // http://www.boost.org/LICENSE_1_0.txt) 5 6 /// Handles user input (keyboard, windowing input such as closing the window, etc.). 7 module platform.inputdevice; 8 9 10 import std.algorithm; 11 import std.array; 12 import std.conv; 13 import std.exception; 14 import std.experimental.logger; 15 16 import derelict.sdl2.sdl; 17 18 public import platform.inputrecording; 19 public import platform.key; 20 21 import io.yaml; 22 23 24 /// Handles user input (keyboard, mouse, windowing input such as resizing the window, etc.). 25 final class InputDevice 26 { 27 public: 28 /** Status of window resizing. 29 * 30 * Implicitly converts info bool, so checks like if(input.resized) can be made. 31 */ 32 struct ResizedStatus 33 { 34 // Has the window been resized? 35 bool resized = false; 36 37 alias resized this; 38 39 private: 40 // New width and height of the window after resizing. 41 int width_, height_; 42 43 public: 44 /// Get the new window width. Can only be called if resized. 45 int width() @safe pure nothrow const @nogc 46 { 47 assert(resized, "Trying to get new width when the window was not resized"); 48 return width_; 49 } 50 51 /// Get the new window height. Can only be called if resized. 52 int height() @safe pure nothrow const @nogc 53 { 54 assert(resized, "Trying to get new height when the window was not resized"); 55 return height_; 56 } 57 } 58 59 package: 60 // Game log. 61 Logger log_; 62 63 private: 64 // Keeps track of keyboard input. 65 Keyboard keyboard_; 66 67 // Keeps track of mouse input. 68 Mouse mouse_; 69 70 // Does the user want to quit the program? 71 bool quit_; 72 73 // Recording device used for recording input for benchmark demos. 74 InputRecordingDevice recorder_; 75 76 // State needed for replaying recorded input of one type (e.g. mouse or keyboard). 77 struct ReplayState(Input) 78 { 79 // Currently playing input recording for Input. Null if no recording is playing. 80 Recording!Input recording = null; 81 82 // A HACK to ensure game state during a replay 'lines up' to state during recording. 83 // 84 // Specifies number of frames after replay() is called to wait before really starting 85 // to replay. 86 // 87 // Without this, replaying recorded input is slightly 'off', e.g. the camera is moved 88 // slightly less far, resulting in entities that were selected when recording not 89 // being selected when replaying, etc. 90 // 91 // TODO: Try to figure out a way to fix this problem without this hack. 92 // (Note: the one-frame recording delay does not seem to be responsible) 93 // 2014-09-11 94 size_t delay; 95 96 enum blockName = "block" ~ Input.stringof; 97 // Should the real input be blocked while input is being replayed from recording? 98 Flag!blockName block; 99 } 100 101 // State needed to replay recorded mouse input. 102 ReplayState!Mouse replayM_; 103 // State needed to replay recorded keyboard input. 104 ReplayState!Keyboard replayK_; 105 106 // Status of resizing the window (converts to true if window was resized this frame). 107 ResizedStatus resized_; 108 109 /* Acts as a queue of UTF-32 code points encoded in UTF-8. 110 * 111 * If not empty, one element is popped each frame. 112 */ 113 char[] unicodeQueue_; 114 115 public: 116 /** Construct an InputDevice. 117 * 118 * Params: 119 * 120 * getHeight = Delegate that returns window height. 121 * log = Game log. 122 */ 123 this(long delegate() @safe pure nothrow @nogc getHeight, Logger log) @trusted nothrow 124 { 125 log_ = log; 126 keyboard_ = new Keyboard(); 127 mouse_ = new Mouse(getHeight); 128 recorder_ = new InputRecordingDevice(this); 129 SDL_StartTextInput(); 130 } 131 132 /// Destroy the InputDevice. Must be called to ensure deletion of manually allocated memory. 133 ~this() @trusted 134 { 135 SDL_StopTextInput(); 136 destroy(recorder_); 137 } 138 139 /// Get a reference to the recording device to record input with. 140 InputRecordingDevice recorder() @safe pure nothrow @nogc 141 { 142 return recorder_; 143 } 144 145 /** Start replaying mouse input from a recording. 146 * 147 * The recording will continue to play until it is spent. 148 * Will consume the recording. 149 * 150 * If something is already replaying (from a previous replay() call), it will be 151 * overridden. 152 * 153 * Params: 154 * 155 * recording = The recording to play. Will continue to play until spent. Will be 156 * consumed by the InputDevice. 157 * block = Should input from the real mouse be blocked while replaying? 158 */ 159 void replay(Recording!Mouse recording, Flag!"blockMouse" block) @safe pure nothrow @nogc 160 { 161 replayM_ = ReplayState!Mouse(recording, 1, block); 162 } 163 164 /** Start replaying keyboard input from a recording. 165 * 166 * If something is already replaying (from a previous replay() call), it will be 167 * overridden. 168 * 169 * Params: 170 * 171 * recording = The recording to play. Will continue to play until spent. Will be 172 * consumed by the InputDevice. 173 * block = Should input from the real keyboard be blocked while replaying? 174 */ 175 void replay(Recording!Keyboard recording, Flag!"blockKeyboard" block) 176 @safe pure nothrow @nogc 177 { 178 replayK_ = ReplayState!Keyboard(recording, 1, block); 179 } 180 181 /// Collect user input. 182 void update() @trusted nothrow // @nogc 183 { 184 import std.utf; 185 if(!unicodeQueue_.empty) try 186 { 187 unicodeQueue_.popFront(); 188 } 189 catch(UTFException e) 190 { 191 log_.warning("Error in unicode input decoding, clearing unicode input") 192 .assumeWontThrow; 193 unicodeQueue_.length = 0; 194 } 195 catch(Exception e) { assert(false, "Unexpected exception"); } 196 197 // Record input from the *previous frame* (avoids recording the current frame 198 // of a stopRecord() call, which could record the input that stopped it) 199 recorder_.update(); 200 201 SDL_PumpEvents(); 202 mouse_.clear(); 203 keyboard_.clear(); 204 205 if(!replayM_.block) { mouse_.getInput(); } 206 if(!replayK_.block) { keyboard_.getInput(); } 207 208 handleRecord(mouse_, replayM_); 209 handleRecord(keyboard_, replayK_); 210 211 resized_ = ResizedStatus.init; 212 SDL_Event e; 213 while(SDL_PollEvent(&e) != 0) 214 { 215 if(!replayM_.block) { mouse_.handleEvent(e); } 216 // Quit if the user closes the window or presses Escape. 217 if(e.type == SDL_QUIT) { quit_ = true; } 218 if(e.type == SDL_WINDOWEVENT) 219 { 220 if(e.window.event == SDL_WINDOWEVENT_RESIZED) 221 { 222 resized_ = ResizedStatus(true, e.window.data1, e.window.data2); 223 } 224 } 225 if(e.type == SDL_TEXTINPUT) 226 { 227 unicodeQueue_.assumeSafeAppend(); 228 import core.stdc..string: strlen; 229 unicodeQueue_ ~= e.text.text[0 .. strlen(e.text.text.ptr)]; 230 } 231 } 232 233 // Our GUI reads backspace/enter through unicode(). 234 if(keyboard_.pressed(Key.Return)) { unicodeQueue_ ~= 0x0D; } 235 if(keyboard_.pressed(Key.Backspace)) { unicodeQueue_ ~= 0x08; } 236 } 237 238 /// Get access to keyboard input. 239 const(Keyboard) keyboard() @safe pure nothrow const @nogc { return keyboard_; } 240 241 /// Get access to mouse input. 242 const(Mouse) mouse() @safe pure nothrow const @nogc { return mouse_; } 243 244 /// Status of resizing the window (converts to true if window was resized this frame). 245 ResizedStatus resized() @safe pure nothrow const @nogc { return resized_; } 246 247 /// Does the user want to quit the program (e.g. by pressing the close window button). 248 bool quit() @safe pure nothrow const @nogc { return quit_; } 249 250 /** Get the "current" unicode character for text input purposes. 251 * 252 * Text input is pretty complicated and the InputDevice may (at least in theory) 253 * receive more than one unicode character in some frames. These are stored in an 254 * internal queue that is popped once per frame. This accesses the popped value. 255 * 256 * If there are no characters in the queue, 0 is returned. Enter/backspace key presses 257 * are also registered as unicode characters (0x0D/0x0D respectively). 258 * 259 */ 260 dchar unicode() @safe pure nothrow const 261 { 262 if(unicodeQueue_.empty) { return 0; } 263 264 import std.utf; 265 // An error, if any, will be detected/logged on the next frame (no logging here - const) 266 try { return unicodeQueue_.front; } 267 catch(UTFException e) { return 0; } 268 catch(Exception e) { assert(false, "Unexpected exception"); } 269 } 270 271 272 private: 273 /** If specified input recording is not null, attempts to play one frame from the recording. 274 * 275 * Params: 276 * 277 * input = Input affected by the recording. E.g. mouse for a mouse input recording. 278 * replay = The recording to play with some extra state. If the recording is null, 279 * handleRecord() will do anything. If it's empty, replay will be 280 * reset to its init value. 281 */ 282 static void handleRecord(Input)(Input input, ref ReplayState!Input replay) 283 @safe 284 { 285 scope(exit) if(replay.delay > 0) { --replay.delay; } 286 if(replay.recording is null || replay.delay > 0) { return; } 287 288 if(replay.recording.empty) 289 { 290 replay = replay.init; 291 } 292 else 293 { 294 input.handleRecord(replay.recording.front); 295 replay.recording.popFront(); 296 } 297 } 298 } 299 300 301 import std.typecons; 302 303 /// Keeps track of which keys are pressed on the keyboard. 304 final class Keyboard 305 { 306 package: 307 // Keyboard data members separated into a struct for easy recording. 308 // 309 // "Base" state because all other state (movement) can be derived from this data 310 // (movement - change of BaseState between frames). 311 struct BaseState 312 { 313 // Unlikely to have more than 256 keys pressed at any one time (we ignore any more). 314 SDL_Keycode[256] pressedKeys_; 315 // The number of values used in pressedKeys_. 316 size_t pressedKeyCount_; 317 318 // Convert a BaseState to a YAML node. 319 YAMLNode toYAML() @safe nothrow const 320 { 321 auto pressedKeys = pressedKeys_[0 .. pressedKeyCount_]; 322 return YAMLNode(pressedKeys.map!(to!string).array.assumeWontThrow); 323 } 324 325 /* Load a BaseState from a YAML node (produced by BaseState.toYAML()). 326 * 327 * Throws: 328 * 329 * ConvException if any value in the YAML has unexpected format. 330 * YAMLException if the YAML has unexpected layout. 331 */ 332 static BaseState fromYAML(ref YAMLNode yaml) @safe 333 { 334 enforce(yaml.length <= pressedKeys_.length, 335 new YAMLException("Too many pressed keys in a record")); 336 BaseState result; 337 foreach(string key; yaml) 338 { 339 result.pressedKeys_[result.pressedKeyCount_++] = to!SDL_Keycode(key); 340 } 341 return result; 342 } 343 } 344 345 BaseState baseState_; 346 alias baseState_ this; 347 348 private: 349 // pressedKeys_ from the last update, to detect that a key has just been pressed/released. 350 SDL_Keycode[256] pressedKeysLastUpdate_; 351 // The number of values used in pressedKeysLastUpdate_. 352 size_t pressedKeyCountLastUpdate_; 353 354 public: 355 /// Get the state of specified keyboard key. 356 Flag!"isPressed" key(const Key keycode) @safe pure nothrow const @nogc 357 { 358 auto keys = pressedKeys_[0 .. pressedKeyCount_]; 359 return keys.canFind(cast(SDL_Keycode)keycode) ? Yes.isPressed : No.isPressed; 360 } 361 362 /// Determine if specified key was just pressed. 363 Flag!"pressed" pressed(const Key keycode) @safe pure nothrow const @nogc 364 { 365 // If it is pressed now but wasn't pressed the last frame, it has just been pressed. 366 auto keys = pressedKeysLastUpdate_[0 .. pressedKeyCountLastUpdate_]; 367 const sdlKey = cast(SDL_Keycode)keycode; 368 return (key(keycode) && !keys.canFind(sdlKey)) ? Yes.pressed : No.pressed; 369 } 370 371 private: 372 /// Clear any keyboard state that must be cleared every frame. 373 void clear() @safe pure nothrow @nogc 374 { 375 pressedKeysLastUpdate_[] = pressedKeys_[]; 376 pressedKeyCountLastUpdate_ = pressedKeyCount_; 377 pressedKeyCount_ = 0; 378 } 379 380 /// Get keyboard input that must be refreshed every frame. 381 void getInput() @system nothrow @nogc 382 { 383 int numKeys; 384 const Uint8* allKeys = SDL_GetKeyboardState(&numKeys); 385 foreach(SDL_Scancode scancode, Uint8 state; allKeys[0 .. numKeys]) 386 { 387 if(!state) { continue; } 388 pressedKeys_[pressedKeyCount_++] = SDL_GetKeyFromScancode(scancode); 389 } 390 } 391 392 /** Load base state from a record. 393 * 394 * Pressed keys on the keyboard are combined with pressed keys loaded from the record. 395 */ 396 void handleRecord(ref const BaseState state) @safe pure nothrow @nogc 397 { 398 foreach(key; state.pressedKeys_[0 .. state.pressedKeyCount_]) 399 { 400 // Ignore more than 256 keys pressed at the same time. 401 if(pressedKeyCount_ >= pressedKeys_.length) { return; } 402 pressedKeys_[pressedKeyCount_++] = key; 403 } 404 } 405 } 406 407 /// Keeps track of mouse position, buttons, dragging, etc. 408 final class Mouse 409 { 410 package: 411 // Mouse data members separated into a struct for easy recording. 412 // 413 // "Base" state because all other state (movement) can be derived from this data 414 // (movement - change of BaseState between frames). 415 struct BaseState 416 { 417 // X coordinate of mouse position. 418 int x_; 419 // Y coordinate of mouse position. 420 int y_; 421 422 // X coordinate of the mouse wheel (if the wheel supports horizontal scrolling). 423 int wheelX_; 424 // Y coordinate of the mouse wheel (aka scrolling with a normal wheel). 425 int wheelY_; 426 427 // Did the user finish a click with a button during this update? 428 Flag!"click"[Button.max + 1] click_; 429 430 // Did the user finish a doubleclick with a button during this update? 431 Flag!"doubleClick"[Button.max + 1] doubleClick_; 432 433 // State of all (well, at most 5) mouse buttons. 434 Flag!"pressed"[Button.max + 1] buttons_; 435 436 // Convert a BaseState to a YAML node. 437 YAMLNode toYAML() @safe nothrow const 438 { 439 string[] keys; 440 YAMLNode[] values; 441 keys ~= "x"; values ~= YAMLNode(x_); 442 keys ~= "y"; values ~= YAMLNode(y_); 443 keys ~= "wheelX"; values ~= YAMLNode(wheelX_); 444 keys ~= "wheelY"; values ~= YAMLNode(wheelY_); 445 446 keys ~= "click"; 447 values ~= YAMLNode(click_[].map!(to!string).array.assumeWontThrow); 448 keys ~= "doubleClick"; 449 values ~= YAMLNode(doubleClick_[].map!(to!string).array.assumeWontThrow); 450 keys ~= "buttons"; 451 values ~= YAMLNode(buttons_[].map!(to!string).array.assumeWontThrow); 452 return YAMLNode(keys, values); 453 } 454 455 /* Load a BaseState from a YAML node (produced by BaseState.toYAML()). 456 * 457 * Throws: 458 * 459 * ConvException if any value in the YAML has unexpected format. 460 * YAMLException if the YAML has unexpected layout. 461 */ 462 static BaseState fromYAML(ref YAMLNode yaml) @safe 463 { 464 // Used to load button arrays (buttons_, click_, doubleClick_) 465 void buttonsFromYAML(F)(F[] flags, ref YAMLNode seq) 466 { 467 enforce(seq.length <= flags.length, 468 new YAMLException("Too many mouse buttons in recording")); 469 size_t idx = 0; 470 foreach(string button; seq) { flags[idx++] = button.to!F; } 471 } 472 BaseState result; 473 foreach(string key, ref YAMLNode value; yaml) switch(key) 474 { 475 case "x": result.x_ = value.as!int; break; 476 case "y": result.y_ = value.as!int; break; 477 case "wheelX": result.wheelX_ = value.as!int; break; 478 case "wheelY": result.wheelY_ = value.as!int; break; 479 case "click": buttonsFromYAML(result.click_[], value); break; 480 case "doubleClick": buttonsFromYAML(result.doubleClick_[], value); break; 481 case "buttons": buttonsFromYAML(result.buttons_[], value); break; 482 default: throw new YAMLException("Unknown key in mouse record: " ~ key); 483 } 484 return result; 485 } 486 } 487 488 BaseState baseState_; 489 alias baseState_ this; 490 491 private: 492 // Y movement of mouse since the last update. 493 int xMovement_; 494 // Y movement of mouse since the last update. 495 int yMovement_; 496 497 // X movement of the wheel since the last update. 498 int wheelYMovement_; 499 // Y movement of the wheel since the last update. 500 int wheelXMovement_; 501 502 // State of all (well, at most 5) mouse buttons. 503 Flag!"pressed"[Button.max + 1] buttonsLastUpdate_; 504 505 // Coordinates where each button was last pressed (for dragging). 506 vec2i[Button.max + 1] pressedCoords_; 507 508 // Gets the current window height. 509 long delegate() @safe pure nothrow @nogc getHeight_; 510 511 import gl3n_extra.linalg; 512 513 public: 514 nothrow @nogc: 515 /// Enumerates mouse buttons. 516 enum Button: ubyte 517 { 518 Left = 0, 519 Middle = 1, 520 Right = 2, 521 X1 = 3, 522 X2 = 4, 523 // Using 16 to avoid too big BaseState arrays. 524 Unknown = 16 525 } 526 527 /** Construct a Mouse and initialize button states. 528 * 529 * Params: 530 * 531 * getHeight = Delegate that returns current window height. 532 */ 533 this(long delegate() @safe pure nothrow @nogc getHeight) @safe nothrow 534 { 535 getHeight_ = getHeight; 536 xMovement_ = yMovement_ = 0; 537 getMouseState(); 538 } 539 540 @safe pure const 541 { 542 /// Get X coordinate of mouse position. 543 int x() { return x_; } 544 /// Get Y coordinate of mouse position. 545 int y() { return y_; } 546 547 /// Get X movement of mouse since the last update. 548 int xMovement() { return xMovement_; } 549 /// Get Y movement of mouse since the last update. 550 int yMovement() { return yMovement_; } 551 552 /// Get X coordinate of the mouse wheel (if it supports horizontal scrolling). 553 int wheelX() { return wheelX_; } 554 /// Get Y coordinate of the mouse wheel. 555 int wheelY() { return wheelY_; } 556 557 /// Get the X movement of the wheel since the last update. 558 int wheelXMovement() { return wheelXMovement_; } 559 /// Get the Y movement of the wheel since the last update. 560 int wheelYMovement() { return wheelYMovement_; } 561 562 /// Did the user finish a double click during this update? 563 Flag!"doubleClick" doubleClicked(Button button) { return doubleClick_[button]; } 564 /// Did the user finish a click during this update? 565 Flag!"click" clicked(Button button) { return click_[button]; } 566 /// Get the state of specified mouse button. 567 Flag!"pressed" button(Button button) { return buttons_[button]; } 568 569 /// Get the coordinates at which button was last pressed. Useful for dragging. 570 vec2i pressedCoords(Button button) { return pressedCoords_[button]; } 571 } 572 573 private: 574 /// Handle an SDL event (which may be a mouse event). 575 void handleEvent(ref const SDL_Event e) @system nothrow 576 { 577 static Button button(Uint8 sdlButton) @safe pure nothrow @nogc 578 { 579 switch(sdlButton) 580 { 581 case SDL_BUTTON_LEFT: return Button.Left; 582 case SDL_BUTTON_MIDDLE: return Button.Middle; 583 case SDL_BUTTON_RIGHT: return Button.Right; 584 case SDL_BUTTON_X1: return Button.X1; 585 case SDL_BUTTON_X2: return Button.X2; 586 // SDL should not report any other value for mouse buttons... but it does. 587 default: return Button.Unknown; // assert(false, "Unknown mouse button"); 588 } 589 } 590 switch(e.type) 591 { 592 case SDL_MOUSEMOTION: break; 593 case SDL_MOUSEWHEEL: 594 wheelX_ += e.wheel.x; 595 wheelY_ += e.wheel.y; 596 // += is needed because there might be multiple wheel events per frame. 597 wheelXMovement_ += e.wheel.x; 598 wheelYMovement_ += e.wheel.y; 599 break; 600 case SDL_MOUSEBUTTONUP: 601 const b = button(e.button.button); 602 // Don't set to No.click so we don't override clicks from any playing recording. 603 if(e.button.clicks > 0) { click_[b] = Yes.click; } 604 if(e.button.clicks % 2 == 0) { doubleClick_[b] = Yes.doubleClick; } 605 break; 606 case SDL_MOUSEBUTTONDOWN: 607 // Save the coords where the button was pressed (for dragging). 608 pressedCoords_[button(e.button.button)] = vec2i(x_, y_); 609 break; 610 default: break; 611 } 612 } 613 614 /// Clear any mouse state that must be cleared every frame. 615 void clear() @safe pure nothrow @nogc 616 { 617 xMovement_ = yMovement_ = 0; 618 wheelXMovement_ = wheelYMovement_ = 0; 619 click_[] = No.click; 620 doubleClick_[] = No.doubleClick; 621 buttonsLastUpdate_[] = buttons_[]; 622 buttons_[] = No.pressed; 623 } 624 625 /// Get mouse input that must be refreshed every frame. 626 void getInput() @safe nothrow 627 { 628 const oldX = x_; const oldY = y_; 629 getMouseState(); 630 xMovement_ = x_ - oldX; yMovement_ = y_ - oldY; 631 } 632 633 /** Load base state from a record. 634 * 635 * Mouse cursor and wheel coordinates are considered absolute; i.e. recorded coords 636 * override current cursor/wheel position. 637 * 638 * Button clicks are not absolute; any clicks from the record are added to clicks 639 * registered from current input. Same for pressed buttons. 640 */ 641 void handleRecord(ref const BaseState state) @safe pure nothrow @nogc 642 { 643 xMovement_ += state.x_ - x_; 644 yMovement_ += state.y_ - y_; 645 x_ = state.x_; 646 y_ = state.y_; 647 648 wheelXMovement_ += state.wheelX_ - wheelX_; 649 wheelYMovement_ += state.wheelY_ - wheelY_; 650 wheelX_ = state.wheelX_; 651 wheelY_ = state.wheelY_; 652 653 foreach(button; 0 .. Button.max + 1) 654 { 655 // A button has been 'pressed' from the recording 656 const justPressed = !buttonsLastUpdate_[button] && state.buttons_[button]; 657 // If no click in record, we keep the click/no click from user input. 658 if(state.click_[button]) { click_[button] = Yes.click; } 659 if(state.doubleClick_[button]) { doubleClick_[button] = Yes.doubleClick; } 660 if(state.buttons_[button]) { buttons_[button] = Yes.pressed; } 661 if(justPressed) { pressedCoords_[button] = vec2i(x_, y_); } 662 } 663 } 664 665 /// Get mouse position and button state. 666 void getMouseState() @trusted nothrow 667 { 668 const buttons = SDL_GetMouseState(&x_, &y_); 669 buttons_[Button.Left] = buttons & SDL_BUTTON_LMASK ? Yes.pressed : No.pressed; 670 buttons_[Button.Middle] = buttons & SDL_BUTTON_MMASK ? Yes.pressed : No.pressed; 671 buttons_[Button.Right] = buttons & SDL_BUTTON_RMASK ? Yes.pressed : No.pressed; 672 buttons_[Button.X1] = buttons & SDL_BUTTON_X1MASK ? Yes.pressed : No.pressed; 673 buttons_[Button.X2] = buttons & SDL_BUTTON_X2MASK ? Yes.pressed : No.pressed; 674 y_ = cast(int)(getHeight_() - y_); 675 } 676 } 677 678 // TODO: 'MappedInput' to wrap Mouse/Keyboard. Will read keybrd/mouse mappings from YAML. 679 // Its API will use enum values (InputAction?) instead of keys, e.g. InputAction.Attack, 680 // -||-.Deploy, -||-.SelectAllThisType, etc; InputActions will map to loaded mappings, and 681 // the API will expose mouse *position* (and pos where mouse was last _pressed_), so user 682 // can e.g. detect that InputAction.Select is active (e.g. left click), and get mouse pos 683 // to know what to select. With mapping, Select can be mapped e.g. to Enter so user can 684 // select units by mouse over + Enter instead of left-click. 2014-08-26 685 // 686 // Would also allow support for e.g. gamepads (not that it makes sense... maybe Steam 687 // Controller?)