Mercurial > projects > mde
view mde/input/Input.d @ 132:264028f4115a
Cleaned up mde.imde and a couple of widget functions.
New mde.menus module to add default menus.
The input singleton is now created in mde.input.Input instead of mde.imde.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Fri, 23 Jan 2009 14:59:05 +0000 |
parents | a2ef6b549101 |
children | 4084f07f2c7a |
line wrap: on
line source
/* LICENSE BLOCK Part of mde: a Modular D game-oriented Engine Copyright © 2007-2008 Diggory Hardy This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ /** * This module contains the interface to the input system; it should be the only module of the * input package imported from outside this package. */ module mde.input.Input; // package imports import mde.input.Config; import mde.input.exception; // sdl imports import derelict.sdl.events; import derelict.sdl.keyboard; import derelict.sdl.types; // only SDL_PRESSED import derelict.sdl.joystick; // SDL_HAT_* import Utf = tango.text.convert.Utf; import tango.util.log.Log : Log, Logger; /************************************************************************************************** * Class encapsulating all input functionality. * * This class has several modes which affect output: interaction mode (default), text input mode, * mouse gui mode and axis/button binding modes. * * TODO: Gui mode and button capture and axis capture modes for key binding, disabling all * other modes (except gui-type mouse info?). * TODO: Possible revisions: remove by-index lookup, only providing callbacks? * TODO: Make callbacks send the time of the event? * TODO: Adjusters, e.g. double-press, hold/click differences. Axis output: via short or double? * TODO: add an Axis1Callback similar to getAxis1? Or remove getAxis1 and provide a conversion function? * TODO: allow callbacks to be removed. Currently not needed. * TODO: modifiers in text-input mode: shortcut handling? Global shortcuts - either mode? * * The primary mode is the interaction mode, mapping each button and axis to a configurable index, * and allowing event callback functions to be bound per index as well as allowing the state to be * looked up directly. * --- * // For keyboard, joystick and mouse button input * bool getButton (inputID id); * void addButtonCallback (inputID id, ButtonCallback dg); // callback receives both up and down events * * // For joystick axis input * short getAxis (inputID id); // range: -32767 .. 32767 * double getAxis1 (inputID id); // range: -1.0 .. 1.0 * void addAxisCallback (inputID id, AxisCallback dg); * * // For mouse (and joystick ball) relative motion input * void getRelMotion (inputID id, out double x, out double y); * void addRelMotionCallback (inputID id, RelMotionCallback dg); * --- * * The keyboard can be put in text input mode, disabling interaction-mode keyboard access and * providing a callback called on each letter press with it's UTF-8 code. Setting a LetterCallback * activates text input mode and removing it disables this mode; only one may be active at once. * --- * void setLetterCallback (LetterCallback dg); * --- * * Mouse input can be recieved via gui-oriented click/coordinate callbacks in both interaction * mode and gui mode, however interaction-mode button and relative motion input is not received in * gui mode. * --- * void getMouseScreenPos (out uint x, out uint y); * void addMouseClickCallback (MouseClickCallback dg); * void addMouseMotionCallback (MouseMotionCallback dg); * --- * * The following methods are provided for setup & posting events: * --- * bool opCall (ref SDL_Event event); // Handles an event, making all the above work * void frameReset (); // Needs to be called once per frame for correct relative input * void loadConfig (char[] profile = "Default"); // Configuration for interaction-mode indexes * --- ***************************************************/ class Input { /// Typedef for all indexes (type is uint). typedef uint inputID; alias void delegate(inputID, bool) ButtonCallback; alias void delegate(inputID, short) AxisCallback; alias void delegate(inputID, double,double) RelMotionCallback; alias void delegate(ushort, ushort, ubyte, bool) MouseClickCallback; alias void delegate(ushort, ushort) MouseMotionCallback; alias void delegate(ushort, char[]) LetterCallback; /** Get key status at this ID. * * Returns: value (true = down, false = up) or false if no value at this ID. */ bool getButton (inputID id) { bool* retp = id in button; if (retp) return *retp; else return false; } /** Get axis status at this ID. * * Returns: value (short; range -32767 .. 32767) or 0 if no value at this ID. */ short getAxis (inputID id) { short* retp = id in axis; if (retp) return *retp; else return 0; } /** Get axis status at this ID. * * Returns: value (double; range roughly -1.0 .. 1.0) or 0 if no value at this ID. */ double getAxis1 (inputID id) { short* retp = id in axis; if (retp) return (*retp) * 3.0518509475997192e-05; else return 0.0; } /** Get the relative motion of the mouse or a joystick ball (since last frameReset() call). * * Future: Converts to a double via sensitivity settings (defaults may be set and overriden per item). * * To avoid confusion over the ID here, the idea is for the input-layer upward to support * multiple mice, in case future platforms do. * Also joystick balls (supported by SDL) can be used in the same way as a mouse for relative * positions. */ void getRelMotion (inputID id, out double x = 0.0, out double y = 0.0) { RelPair* rp = id in relMotion; if (rp) { x = rp.x; y = rp.y; } } /** Adds a callback delegate for key events (both DOWN and UP) with this ID. * * Delegate receives event status. */ Input addButtonCallback (inputID id, ButtonCallback dg) { buttonCallbacks[id] ~= dg; return this; } /** Adds a callback delegate for axis events with this ID. * * Delegate receives event status (as per what getAxis returns). */ Input addAxisCallback (inputID id, AxisCallback dg) { axisCallbacks[id] ~= dg; return this; } /** Adds a callback delegate for mouse motion/joystick ball events with this ID. * * Delegate receives event status. As the name suggests, this is relative motion not screen * position, with sensitivity adjustments applied. * * (A separate callback for mouse screen position changes is not * necessary since this will be triggered by the same event - use mouseScreenPos from within the * function to get new screen coordinates.) */ Input addRelMotionCallback (inputID id, RelMotionCallback dg) { relMotionCallbacks[id] ~= dg; return this; } /** Adds a callback delegate for all mouse clicks & releases. * * Delegate recieves x,y screen position (at time of click/release), button index (1 for left, * 2 for middle, 3 for right, 4/5 for wheel, etc.), and whether the button was pressed or * released (true if pressed). * * The point of this over a standard button callback is firstly to avoid mouse configuration for * the GUI, and secondly to give the pointer position at the time of the event, not the time the * callback gets called. */ Input addMouseClickCallback (MouseClickCallback dg) { mouseClickCallbacks ~= dg; return this; } /** Adds a callback delegate for all mouse motion events. * * Really just for graphical user interfaces. Use addRelMotionCallback for relative motion (for * manipulating 3D views, etc.). */ Input addMouseMotionCallback (MouseMotionCallback dg) { mouseMotionCallbacks ~= dg; return this; } /** Sets a callback delegate to recieve key presses as a Utf-8 char[]. * * Since it is normal to type into only one location at once, setting a new LetterCallback * removes the last set one (however active ButtonCallbacks will still receive events). * Supplying a null delegate will turn off the slight overhead of unicode conversion. * * The char[] received by the delegate must be copied and not stored or edited directly. */ void setLetterCallback (LetterCallback dg = null) { if (dg) { SDL_EnableUNICODE (1); SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL); } else { SDL_EnableUNICODE (0); SDL_EnableKeyRepeat(0, SDL_DEFAULT_REPEAT_INTERVAL); } letterCallback = dg; } /** Feed an SDL_Event struct (only uses if it's a key, mouse or joystick event). * * Other types of event functions may be added. Returns true if the event * was used, false if not or no config was available. Hmm... doesn't seem * very useful, but has practically no cost. * * May throw InputClassExceptions (on configuration errors). Catching the * exception and continuing should be fine. */ bool send (ref SDL_Event event) { /* Non-config events. * * Handle these first so that if no config exists some functionality at * least is retained. * * Coordinates don't need adjusting (they put the top-left most pixel at 0,0). * * If no config, exit from this switch. */ switch (event.type) { case SDL_KEYDOWN: if (letterCallback) { try letterCallback (event.key.keysym.sym, Utf.toString ([cast(wchar)event.key.keysym.unicode], cast(char[])utfBuf)); catch (Exception e) logger.error (CB_EXC ~ e.msg); } break; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: foreach (dg; mouseClickCallbacks) { try dg (event.button.x, event.button.y, event.button.button, event.button.state == SDL_PRESSED); catch (Exception e) logger.error (CB_EXC ~ e.msg); } break; case SDL_MOUSEMOTION: foreach (dg; mouseMotionCallbacks) { try dg (event.motion.x, event.motion.y); catch (Exception e) logger.error (CB_EXC ~ e.msg); } break; default: if (!config) return false; // event not used } if (!config) return true; // event used switch (event.type) { // Keyboard events: case SDL_KEYDOWN: case SDL_KEYUP: if (letterCallback) break; // text input mode; no keyboard input from mappings outQueue[]* p = (Config.B.SDLKEY | event.key.keysym.sym) in config.button; if (p) foreach (outQueue q; *p) { bEvent (this, event.key.state == SDL_PRESSED, readOutQueue(q)); } break; // Mouse events: case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: // Button events: outQueue[]* p = (Config.B.MOUSE | event.button.button) in config.button; if (p) foreach (outQueue q; *p) { bEvent (this, event.button.state == SDL_PRESSED, readOutQueue(q)); } break; case SDL_MOUSEMOTION: // Relative motion: outQueue[]* p = (Config.M.WMMOUSE) in config.relMotion; if (p) foreach (outQueue q; *p) { mEvent (this, event.motion.xrel, event.motion.yrel, readOutQueue(q)); } break; // Joystick events: case SDL_JOYBUTTONDOWN: case SDL_JOYBUTTONUP: outQueue[]* p = (Config.B.JOYBUTTON | (event.jbutton.which << 12) | event.jbutton.button) in config.button; if (p) foreach (outQueue q; *p) { bEvent (this, event.jbutton.state == SDL_PRESSED, readOutQueue(q)); } break; case SDL_JOYAXISMOTION: outQueue[]* p = (Config.A.JOYAXIS | (event.jaxis.which << 12) | event.jaxis.axis) in config.axis; if (p) foreach (outQueue q; *p) { aEvent (this, event.jaxis.value, readOutQueue(q)); } break; case SDL_JOYBALLMOTION: outQueue[]* p = (Config.M.JOYBALL | (event.jball.which << 12) | event.jball.ball) in config.relMotion; if (p) foreach (outQueue q; *p) { mEvent (this, event.jball.xrel, event.jball.yrel, readOutQueue(q)); } break; case SDL_JOYHATMOTION: static ubyte[uint] oldJHatVals; // necessary to store this to know which "axis" changed uint index = (event.jhat.which << 12) | event.jhat.hat; ubyte* oVal_p = index in oldJHatVals; ubyte oldJHatVal = (oVal_p) ? *oVal_p : SDL_HAT_CENTERED; // Carry out functionality for an axis. void hatExamine (ubyte neg, ubyte pos, Config.B neg_b, Config.B pos_b, Config.A axis) { // Check if there's any change on this axis (if not, nothing to do): ubyte filter = neg | pos; if ((oldJHatVal & filter) != (event.jhat.value & filter)) { // Now we know this axis changed position, so can unset old value and set // new value (if not centre). // Cancel old button status: if (oldJHatVal & neg) { outQueue[]* p = (neg_b | index) in config.button; if (p) foreach (outQueue q; *p) bEvent (this, false, readOutQueue(q)); } else if (oldJHatVal & pos) { outQueue[]* p = (pos_b | index) in config.button; if (p) foreach (outQueue q; *p) bEvent (this, false, readOutQueue(q)); } // Set new button status and position: short position = 0; if (event.jhat.value & neg) { position = -32767; outQueue[]* p = (neg_b | index) in config.button; if (p) foreach (outQueue q; *p) bEvent (this, true, readOutQueue(q)); } else if (event.jhat.value & pos) { position = 32767; outQueue[]* p = (pos_b | index) in config.button; if (p) foreach (outQueue q; *p) bEvent (this, true, readOutQueue(q)); } // New axis event: outQueue[]* p = (axis | index) in config.axis; if (p) foreach (outQueue q; *p) aEvent (this, position, readOutQueue(q)); } } // Now run the code for each axis: hatExamine (SDL_HAT_UP, SDL_HAT_DOWN, Config.B.JOYHAT_U, Config.B.JOYHAT_D, Config.A.JOYHAT_UD); hatExamine (SDL_HAT_LEFT, SDL_HAT_RIGHT, Config.B.JOYHAT_L, Config.B.JOYHAT_R, Config.A.JOYHAT_LR); oldJHatVals[index] = event.jhat.value; break; // Other events: default: return false; // event not used } return true; // event used } /** Resets relative movement of mice / joystick balls to zero. * * Should be called once-per-frame if these are used, but must be called after their state has * been read (e.g. just before updating the input). */ void frameReset () { foreach (rp; relMotion) { rp.x = rp.y = 0.0; } } /** Loads all configs, activating the requested id. * * Throws: ConfigLoadException if unable to load any configs or the requested config id wasn't * found. */ void loadConfig (char[] file, char[] profile = "Default") { Config.load(file); Config* c_p = profile in Config.configs; if (c_p) config = *c_p; else { logger.error ("Config profile \""~profile~"\" not found: input won't work unless a valid profile is loaded!"); throw new ConfigLoadException; } } /** For now, use as a singleton. (Could be changed later to allow multiple * "players". */ static Input singleton () { if (instance is null) instance = new Input(); return instance; } private: // Static constructor for event stream (fills es_*_fcts tables). static this () { es_b_fcts = [ ES_B.OUT : &es_b_out ]; es_a_fcts = [ ES_A.OUT : &es_a_out, ES_A.REVERSE : &es_a_reverse ]; es_m_fcts = [ ES_M.OUT : &es_m_out ]; logger = Log.getLogger ("mde.input.Input"); } struct RelPair { // for mouse/joystick ball motion double x, y; static RelPair opCall (double a, double b) { RelPair ret; ret.x = a; ret.y = b; return ret; } } static const CB_EXC = "Callback exception: "; static Input instance; static Logger logger; Config config; // Configuration char[6] utfBuf; // Buffer for Utf.toString; reallocates if less than 5. bool[inputID] button; // Table of button states short[inputID] axis; // Table of axes states RelPair[inputID] relMotion; // Table of relative mouse / joystick ball motions // NOTE: currently no means of removal ButtonCallback[][inputID] buttonCallbacks; AxisCallback[][inputID] axisCallbacks; RelMotionCallback[][inputID] relMotionCallbacks; MouseClickCallback[] mouseClickCallbacks; MouseMotionCallback[] mouseMotionCallbacks; LetterCallback letterCallback; //BEGIN Event stream functionality /* This section contains functions called on an event, which may modify the event (adjuster * functions), and finally output to one (or more) of the state tables (the event stream). * * Adjuster and other event functions should have a format to fit the ES_X_Func types, for X is * B (button event), A (axis event) or M (mouse relative motion event or joystick ball event). * Adjusters should call one of the xEvent() functions with their output and the remainder of * the readOutQueue. * * To control which adjusters get called and pass parameters, a stack of sorts is used: * outQueue. */ //BEGIN ES Definitions /* Note: We really want an array, not a stack. We cannot edit the lists, so we can either * copy to a stack or just iterate through it as an array. */ alias Config.outQueue outQueue; struct readOutQueue { // A convenient structure for reading an outQueue item by item. private Config.outQueue _q; // the queue, stored by reference to the original private uint p = 0; // current read position (start at beginning) static readOutQueue opCall (Config.outQueue q) { // Static constructor readOutQueue ret; ret._q = q; return ret; } uint pop () { // Get the next element and advance. Throws an exception if there isn't another. if (p >= _q.length) throw new InputClassException ("Input: Invalid configuration: incomplete config stack"); uint ret = _q[p]; ++p; return ret; } debug uint next () { // Get the next element. No checks; for debug use only. return _q[p]; } } // These aliases are for pointers to the event functions. // These need to use "this", but cannot be delegates because they musn't be bound to a // particular instance of Input, hence this must be passed when called. alias void function (Input, bool, readOutQueue) ES_B_Func; alias void function (Input, short, readOutQueue) ES_A_Func; alias void function (Input, short, short, readOutQueue) ES_M_Func; /* These are the codes allowing the config to specify event functions. * * They are organised as defined in doc/input_ID_assignments. */ enum ES_B : uint { OUT = 0x1000u, } enum ES_A : uint { OUT = 0x1000u, REVERSE = 0x2000u, } enum ES_M : uint { OUT = 0x1000u, } //END ES Definitions // ES Data: // These are the tables for looking up which event function to call. static ES_B_Func[uint] es_b_fcts; static ES_A_Func[uint] es_a_fcts; static ES_M_Func[uint] es_m_fcts; //BEGIN ES Functions // These 3 functions pass an event to the appropriate event function (adjuster or output func). // They are used to start and continue an event stream. // They must be static to allow calling from event functions. const EVCONF_ERR = "Invalid configuration: bad event function code"; static void bEvent (Input myThis, bool b, readOutQueue s) { ES_B_Func* func = (s.pop() in es_b_fcts); if (func != null) (*func)(myThis, b, s); else throw new InputClassException (EVCONF_ERR); } static void aEvent (Input myThis, short x, readOutQueue s) { ES_A_Func* func = (s.pop() in es_a_fcts); if (func != null) (*func)(myThis, x, s); else throw new InputClassException (EVCONF_ERR); } // Only handles relative output since position-on-screen is not stored with an ID: static void mEvent (Input myThis, short x, short y, readOutQueue s) { ES_M_Func* func = (s.pop() in es_m_fcts); if (func != null) (*func)(myThis, x, y, s); else throw new InputClassException (EVCONF_ERR); } // The remaining functions are the stream functions, for adjusting and outputting an event. // They need to work like non-static functions, but are called via a function pointer, hencne // should be static with their first parameter being instead of this. // Simple output function static void es_b_out (Input myThis, bool b, readOutQueue s) { inputID id = cast(inputID) s.pop(); myThis.button[id] = b; ButtonCallback[]* cb_p = id in myThis.buttonCallbacks; if (cb_p) foreach (cb; *cb_p) { try cb (id, b); catch (Exception e) logger.error (CB_EXC ~ e.msg); } } // Adjuster to check modifier keys static void es_b_modifier (Input myThis, bool b, readOutQueue s); // Simple output function static void es_a_out (Input myThis, short x, readOutQueue s) { inputID id = cast(inputID) s.pop(); myThis.axis[id] = x; AxisCallback[]* cb_p = id in myThis.axisCallbacks; if (cb_p) foreach (cb; *cb_p) { try cb (id, x); catch (Exception e) logger.error (CB_EXC ~ e.msg); } } // Just reverses an axis's value static void es_a_reverse (Input myThis, short x, readOutQueue s) { aEvent (myThis, -x, s); } // Simple output function static void es_m_out (Input myThis, short x, short y, readOutQueue s) { inputID id = cast(inputID) s.pop(); myThis.relMotion[id] = RelPair(x,y); RelMotionCallback[]* cb_p = id in myThis.relMotionCallbacks; if (cb_p) foreach (cb; *cb_p) { try cb (id, x,y); catch (Exception e) logger.error (CB_EXC ~ e.msg); } } //END ES Functions //END Event stream functionality /* This unittest covers input.config.Config and input.input.Input for some events of all types. * It is not bullet-proof, and does not cover the stream-functions other than the direct output * ones (largely because these may get added at any time and tested via input tests). * * What it does test: * Events of all types. * Callbacks of all types. * Status set from events for all types (button,axis,relMotion). * * It relies on config loaded from a file (dependant on where input bindings are loaded from; * currently conf/input.mtt). */ debug (mdeUnitTest) unittest { Input ut = new Input(); ut.loadConfig ("InputConfig"); int[6] counters; // counters for callbacks // Should become: [2,2,0,2,1,1] //BEGIN Set up some callbacks ut.addButtonCallback (0x03F0, delegate void(inputID id, bool status) { assert (status == !counters[0]); // true first call, false next counters[0] += 1; }); ut.addButtonCallback (0x06F0, delegate void(inputID id, bool status) { assert (status == !counters[1]); // true first call, false next counters[1] += 1; }); ut.addButtonCallback (0x07F0, delegate void(inputID id, bool status) { counters[2] += 1; }); ut.addAxisCallback (0x22F0, delegate void(inputID id, short x) { assert (x == (counters[3] ? 0 : 32767)); counters[3] += 1; }); ut.addRelMotionCallback (0x11F0, delegate void(inputID id, double x, double y) { assert (x == 14.0); assert (y == -1.0); counters[4] += 1; }); ut.addMouseClickCallback (delegate void(ushort x, ushort y, ubyte b, bool s) { assert (x == 291); assert (y == 10010); assert (b == 3); assert (s == false); counters[5] += 1; }); //END Set up some callbacks //BEGIN Post a lot of events SDL_Event e; // F11 down: e.type = SDL_KEYDOWN; e.key.state = SDL_PRESSED; e.key.keysym.sym = 292; // SDLK_F11 ut(e); // Right mouse button up: e.type = SDL_MOUSEBUTTONUP; e.button.button = 3; // SDL_BUTTON_RIGHT e.button.state = SDL_RELEASED; e.button.x = 291; e.button.y = 10010; ut(e); // Mouse motion: e.type = SDL_MOUSEMOTION; e.motion.x = 63; e.motion.y = 44; e.motion.xrel = 14; e.motion.yrel = -1; ut(e); // Joystick 2 button 5 down: e.type = SDL_JOYBUTTONDOWN; e.jbutton.which = 2; e.jbutton.button = 5; e.jbutton.state = SDL_PRESSED; ut(e); // Same button released: e.jbutton.state = SDL_RELEASED; ut(e); // Joystick 1 axis 8 motion: e.type = SDL_JOYAXISMOTION; e.jaxis.which = 1; e.jaxis.axis = 8; e.jaxis.value = 32767; ut(e); // Joystick 22 ball 100 motion: e.type = SDL_JOYBALLMOTION; e.jball.which = 22; e.jball.ball = 100; e.jball.xrel = -21; e.jball.yrel = 1024; ut(e); // Joystick 214 hat 12 DOWN-RIGHT: e.type = SDL_JOYHATMOTION; e.jhat.which = 214; e.jhat.hat = 12; e.jhat.value = SDL_HAT_RIGHTDOWN; ut(e); // Same hat LEFT: e.jhat.value = SDL_HAT_LEFT; ut(e); //END Post a lot of events //BEGIN Check states assert (ut.getButton(0xF0) == true); assert (ut.getButton(0x1F0) == false); assert (ut.getButton(0x2F0) == false); assert (ut.getButton(0x4F0) == false); assert (ut.getButton(0x5F0) == true); assert (ut.getButton(0x6F0) == false); assert (ut.getButton(0x7F0) == false); assert (ut.getAxis(0x20F0) == 32767); assert (ut.getAxis(0x21F0) == -32767); assert (ut.getAxis1(0x22F0) == 0.0); double s,t; ut.getRelMotion(0x10F0, s,t); assert (s == 14.0); assert (t == -1.0); ut.getRelMotion(0x12F0, s,t); assert (s == -21.0); assert (t == 1024.0); assert (counters == [2,2,0,2,1,1]); //END Check states logger.info ("Unittest complete."); } }