ba58faf2aa95cf73f9f8b36b1985993d7a7ba0a1
[pwl6.git] / src / yuu / input.js
1 /* Copyright 2014 Yukkuri Games
2 Licensed under the terms of the GNU GPL v2 or later
3 @license http://www.gnu.org/licenses/gpl-2.0.html
4 @source: http://yukkurigames.com/yuu/
5 */
6
7 (function (yuu) {
8 "use strict";
9
10 var yT = this.yT || require("./yT");
11 var yf = this.yf || require("./yf");
12
13 yuu.KEY_NAMES = {
14 32: "space",
15 13: "return",
16 16: "shift",
17 18: "alt",
18 17: "control",
19
20 37: "left",
21 38: "up",
22 39: "right",
23 40: "down",
24 9: "tab",
25 27: "escape",
26 8: "backspace",
27 191: "slash",
28 192: "`",
29 };
30
31 // Fill in the key name tables.
32 for (var i = "A".charCodeAt(0); i <= "Z".charCodeAt(0); ++i)
33 yuu.KEY_NAMES[i] = String.fromCharCode(i).toLowerCase();
34 for (i = "0".charCodeAt(0); i <= "9".charCodeAt(0); ++i)
35 yuu.KEY_NAMES[i] = String.fromCharCode(i);
36 for (i = 1; i <= 12; ++i)
37 yuu.KEY_NAMES[111 + i] = "f" + i;
38
39 function splitKeys (keystring) {
40 return keystring.toLowerCase().split("+").sort();
41 }
42
43 var KeyBind = yT({
44 constructor: function (keystring, command) {
45 /** An individual key to command binding.
46
47 The key string is e.g. "a", "control+f", "left+alt+z".
48 "f+control" is equivalent to "control+f", and binding one
49 in a set will override the other. */
50 this.keystring = keystring;
51 this.command = command;
52 this.keys = splitKeys(keystring);
53 },
54
55 uses: function (name) {
56 /** True if the given key name is relevant to this binding. */
57 return yf.contains(this.keys, name.toLowerCase());
58 },
59
60 on: function (pressed) {
61 /** True if all keys in this binding are pressed. */
62 return yf.every.call(pressed, yf.getter, this.keys);
63 }
64 });
65
66 function longerBind (a, b) {
67 return a.keys.length > b.keys.length ? a : b;
68 }
69
70 function longestBind (binds) {
71 return yf.foldl(longerBind, binds);
72 }
73
74 function isActivate (bind) {
75 return bind.command[0] === '+' && bind.command[1] !== '+';
76 }
77
78 function anticommand (bind) {
79 return "-" + bind.command.substring(1);
80 }
81
82 yuu.KeyBindSet = yT({
83 constructor: function (binds) {
84 /** A group of key bindings.
85
86 A set may only have one bind per key combination
87 (regardless of order) at a time. Binding already-bound
88 keys to a different command will overwrite, not duplicate,
89 that binding.
90 */
91 this.binds = [];
92 yf.ipairs.call(this, this.bind, binds || {});
93 },
94
95 bind: function (keystring, command) {
96 /** Bind keys to a command in this set. */
97 var bind = new KeyBind(keystring, command);
98 this.binds = this.binds.filter(function (b) {
99 return !yf.seqEqual(b.keys, bind.keys);
100 }).concat(bind);
101 },
102
103 unbind: function (keystring) {
104 /** Unbind keys from this set. */
105 var keys = splitKeys(keystring);
106 this.binds = this.binds.filter(function (b) {
107 return !yf.seqEqual(b.keys, keys);
108 });
109 }
110 });
111
112 yuu.keyEventName = function (event) {
113 return yuu.keyCodeName(event.keyCode, event.key || event.keyIdentifier);
114 };
115
116 yuu.keyCodeName = function (code, defaultName) {
117 if (defaultName)
118 defaultName = defaultName.toLowerCase();
119 if (defaultName === "unidentified")
120 defaultName = null;
121 var name = yuu.KEY_NAMES[code] || defaultName;
122 if (!name)
123 name = "key:" + code;
124 return name;
125 };
126
127 yuu.InputState = yT({
128 constructor: function (bindsets) {
129 this._bindsets = yf.slice(bindsets || [yuu.defaultKeybinds]);
130 this.pressed = {};
131 /** The current state of each key; 0 if not pressed,
132 the time it was pressed if it is pressed. */
133 },
134
135 push: function (bindset) {
136 /** Add a key bind set to the handler stack. */
137 this._bindsets = this._bindsets.concat(bindset);
138 },
139
140 remove: function (bindset) {
141 /** Remove a key bind set from the handler stack. */
142 this._bindsets = yf.without(this._bindsets, bindset);
143 },
144
145 insertBefore: function (bindset, before) {
146 this._bindsets = yf.insertBefore(
147 this._bindsets.slice(), bindset, before);
148 },
149
150 _triggeredBinds: function (name) {
151 var pressed = this.pressed;
152 function triggered (bind) {
153 return bind.uses(name) && bind.on(pressed);
154 }
155 var binds = [];
156 yf.each(function (bindset) {
157 binds.push.apply(binds, yf.filter(triggered, bindset.binds));
158 }, this._bindsets);
159 return binds;
160 },
161
162 _down: function (name) {
163 /** Mark the input as down, return an array of commands.
164
165 This array is always of length 1 for down/change events.
166
167 Returns null if no binds were triggered, which is slightly
168 different than binds being triggered but no commands are
169 to be executed.
170 */
171 var pressed = this.pressed[name];
172 this.pressed[name] = Date.now();
173 var bind = longestBind(this._triggeredBinds(name));
174 return bind ? pressed ? [] : [bind.command] : null;
175 },
176
177 _up: function (name) {
178 /** Mark the input as down, return an array of commands.
179
180 Only binds for commands with the special + form are
181 returned on release, and the + is converted to a -.
182
183 Returns null if no binds were triggered, which is slightly
184 different than binds being triggered but no commands are
185 to be executed.
186 */
187 if (!this.pressed[name])
188 return null;
189 var cmds = yf.map(anticommand, yf.filter(
190 isActivate, this._triggeredBinds(name)));
191 this.pressed[name] = 0;
192 return cmds.length ? cmds : null;
193 },
194
195 change: function (name) {
196 /** Mark the input as changed, return an array of commands.
197
198 `change` is for inputs that do not have meaningful
199 "down" or "up" states, like moving a mouse. Instead,
200 it fires when the input's state changes - e.g. when
201 the x and y position change.
202
203 The `pressed` table remains unmodified as a result of
204 `change` inputs. Like `down`, `change` returns only the
205 first match it finds.
206 */
207 this.pressed[name] = Date.now();
208 var bind = longestBind(this._triggeredBinds(name));
209 this.pressed[name] = 0;
210 return bind ? [bind.command] : null;
211 },
212
213 keydown: { proxy: "_down" },
214 keyup: { proxy: "_up" },
215
216 gamepadbuttondown: function (gamepad, button) {
217 return this._down("gamepad" + gamepad.index + "button" + button)
218 || this._down("gamepadbutton" + button);
219 },
220
221 gamepadbuttonup: function (gamepad, button) {
222 return this._up("gamepad" + gamepad.index + "button" + button)
223 || this._up("gamepadbutton" + button);
224 },
225
226 mousemove: function () {
227 return this.change("mousemove");
228 },
229
230 mousedown: function (button) {
231 return this._down("mouse" + button);
232 },
233
234 mouseup: function (button) {
235 return this._up("mouse" + button);
236 },
237
238 reset: function () {
239 this.pressed = {};
240 }
241 });
242
243 yuu.stopPropagation = function stopPropagation (event, preventDefault) {
244 event.stopPropagation();
245 if (preventDefault)
246 event.preventDefault();
247 if (event.stopImmediatePropagation)
248 event.stopImmediatePropagation();
249 if (event.gesture && event.gesture !== event)
250 stopPropagation(event.gesture, preventDefault);
251 };
252
253 yuu.registerInitHook(function () {
254 yuu.defaultCommands.bind = yuu.cmd(function (key, command) {
255 yuu.defaultKeybinds.bind(key, command);
256 }, "<key> <command>", "bind a key to a command");
257
258 yuu.defaultCommands.unbind = yuu.cmd(function (key) {
259 yuu.defaultKeybinds.unbind(key);
260 }, "<key>", "unbind a key");
261
262 yuu.defaultKeybinds = new yuu.KeyBindSet();
263 /** The default / debugging bind set */
264 });
265
266 }).call(typeof exports === "undefined" ? this : exports,
267 typeof exports === "undefined"
268 ? this.yuu : (module.exports = require('./core')));