Merge pull request from ericrosenbaum/feature/key-droppability

Re-write keyboard IO for droppability support
This commit is contained in:
Eric Rosenbaum 2018-05-03 11:10:24 -04:00 committed by GitHub
commit 9979391200
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 147 additions and 70 deletions

View file

@ -1,10 +1,33 @@
const Cast = require('../util/cast'); const Cast = require('../util/cast');
/**
* Names used internally for keys used in scratch, also known as "scratch keys".
* @enum {string}
*/
const KEY_NAME = {
SPACE: 'space',
LEFT: 'left arrow',
UP: 'up arrow',
RIGHT: 'right arrow',
DOWN: 'down arrow'
};
/**
* An array of the names of scratch keys.
* @type {Array<string>}
*/
const KEY_NAME_LIST = Object.keys(KEY_NAME).map(name => KEY_NAME[name]);
class Keyboard { class Keyboard {
constructor (runtime) { constructor (runtime) {
/** /**
* List of currently pressed keys. * List of currently pressed scratch keys.
* @type{Array.<number>} * A scratch key is:
* A key you can press on a keyboard, excluding modifier keys.
* An uppercase string of length one;
* except for special key names for arrow keys and space (e.g. 'left arrow').
* Can be a non-english unicode letter like: æ ø ש נ 廿.
* @type{Array.<string>}
*/ */
this._keysPressed = []; this._keysPressed = [];
/** /**
@ -16,48 +39,71 @@ class Keyboard {
} }
/** /**
* Convert a Scratch key name to a DOM keyCode. * Convert from a keyboard event key name to a Scratch key name.
* @param {Any} keyName Scratch key argument. * @param {string} keyString the input key string.
* @return {number} Key code corresponding to a DOM event. * @return {string} the corresponding Scratch key, or an empty string.
* @private
*/ */
_scratchKeyToKeyCode (keyName) { _keyStringToScratchKey (keyString) {
if (typeof keyName === 'number') { keyString = Cast.toString(keyString);
// Key codes placed in with number blocks. // Convert space and arrow keys to their Scratch key names.
return keyName;
}
const keyString = Cast.toString(keyName);
switch (keyString) { switch (keyString) {
case 'space': return 32; case ' ': return KEY_NAME.SPACE;
case 'left arrow': return 37; case 'ArrowLeft':
case 'up arrow': return 38; case 'Left': return KEY_NAME.LEFT;
case 'right arrow': return 39; case 'ArrowUp':
case 'down arrow': return 40; case 'Up': return KEY_NAME.UP;
// @todo: Consider adding other special keys here. case 'Right':
case 'ArrowRight': return KEY_NAME.RIGHT;
case 'Down':
case 'ArrowDown': return KEY_NAME.DOWN;
} }
// Keys reported by DOM keyCode are upper case. // Ignore modifier keys
return keyString.toUpperCase().charCodeAt(0); if (keyString.length > 1) {
return '';
}
return keyString.toUpperCase();
} }
/** /**
* Convert a DOM keyCode into a Scratch key name. * Convert from a block argument to a Scratch key name.
* @param {number} keyCode Key code from DOM event. * @param {string} keyArg the input arg.
* @return {Any} Scratch key argument. * @return {string} the corresponding Scratch key.
* @private
*/ */
_keyCodeToScratchKey (keyCode) { _keyArgToScratchKey (keyArg) {
if (keyCode >= 48 && keyCode <= 90) { // If a number was dropped in, try to convert from ASCII to Scratch key.
// Standard letter. if (typeof keyArg === 'number') {
return String.fromCharCode(keyCode).toLowerCase(); // Check for the ASCII range containing numbers, some punctuation,
// and uppercase letters.
if (keyArg >= 48 && keyArg <= 90) {
return String.fromCharCode(keyArg);
}
switch (keyArg) {
case 32: return KEY_NAME.SPACE;
case 37: return KEY_NAME.LEFT;
case 38: return KEY_NAME.UP;
case 39: return KEY_NAME.RIGHT;
case 40: return KEY_NAME.DOWN;
}
} }
switch (keyCode) {
case 32: return 'space'; keyArg = Cast.toString(keyArg);
case 37: return 'left arrow';
case 38: return 'up arrow'; // If the arg matches a special key name, return it.
case 39: return 'right arrow'; if (KEY_NAME_LIST.includes(keyArg)) {
case 40: return 'down arrow'; return keyArg;
} }
return '';
// Use only the first character.
if (keyArg.length > 1) {
keyArg = keyArg[0];
}
// Check for the space character.
if (keyArg === ' ') {
return KEY_NAME.SPACE;
}
return keyArg.toUpperCase();
} }
/** /**
@ -65,38 +111,39 @@ class Keyboard {
* @param {object} data Data from DOM event. * @param {object} data Data from DOM event.
*/ */
postData (data) { postData (data) {
if (data.keyCode) { if (!data.key) return;
const index = this._keysPressed.indexOf(data.keyCode); const scratchKey = this._keyStringToScratchKey(data.key);
if (data.isDown) { if (scratchKey === '') return;
// If not already present, add to the list. const index = this._keysPressed.indexOf(scratchKey);
if (index < 0) { if (data.isDown) {
this._keysPressed.push(data.keyCode); // If not already present, add to the list.
} if (index < 0) {
// Always trigger hats, even if it was already pressed. this._keysPressed.push(scratchKey);
this.runtime.startHats('event_whenkeypressed', {
KEY_OPTION: this._keyCodeToScratchKey(data.keyCode)
});
this.runtime.startHats('event_whenkeypressed', {
KEY_OPTION: 'any'
});
} else if (index > -1) {
// If already present, remove from the list.
this._keysPressed.splice(index, 1);
} }
// Always trigger hats, even if it was already pressed.
this.runtime.startHats('event_whenkeypressed', {
KEY_OPTION: scratchKey
});
this.runtime.startHats('event_whenkeypressed', {
KEY_OPTION: 'any'
});
} else if (index > -1) {
// If already present, remove from the list.
this._keysPressed.splice(index, 1);
} }
} }
/** /**
* Get key down state for a specified Scratch key name. * Get key down state for a specified key.
* @param {Any} key Scratch key argument. * @param {Any} keyArg key argument.
* @return {boolean} Is the specified key down? * @return {boolean} Is the specified key down?
*/ */
getKeyIsDown (key) { getKeyIsDown (keyArg) {
if (key === 'any') { if (keyArg === 'any') {
return this._keysPressed.length > 0; return this._keysPressed.length > 0;
} }
const keyCode = this._scratchKeyToKeyCode(key); const scratchKey = this._keyArgToScratchKey(keyArg);
return this._keysPressed.indexOf(keyCode) > -1; return this._keysPressed.indexOf(scratchKey) > -1;
} }
} }

View file

@ -12,62 +12,92 @@ test('spec', t => {
t.end(); t.end();
}); });
test('space', t => { test('space key', t => {
const rt = new Runtime(); const rt = new Runtime();
const k = new Keyboard(rt); const k = new Keyboard(rt);
k.postData({ k.postData({
keyCode: 32, key: ' ',
isDown: true isDown: true
}); });
t.strictDeepEquals(k._keysPressed, [32]); t.strictDeepEquals(k._keysPressed, ['space']);
t.strictEquals(k.getKeyIsDown('space'), true); t.strictEquals(k.getKeyIsDown('space'), true);
t.strictEquals(k.getKeyIsDown('any'), true); t.strictEquals(k.getKeyIsDown('any'), true);
t.end(); t.end();
}); });
test('letter', t => { test('letter key', t => {
const rt = new Runtime(); const rt = new Runtime();
const k = new Keyboard(rt); const k = new Keyboard(rt);
k.postData({ k.postData({
keyCode: 65, key: 'a',
isDown: true isDown: true
}); });
t.strictDeepEquals(k._keysPressed, [65]); t.strictDeepEquals(k._keysPressed, ['A']);
t.strictEquals(k.getKeyIsDown(65), true);
t.strictEquals(k.getKeyIsDown('a'), true); t.strictEquals(k.getKeyIsDown('a'), true);
t.strictEquals(k.getKeyIsDown('A'), true);
t.strictEquals(k.getKeyIsDown('any'), true); t.strictEquals(k.getKeyIsDown('any'), true);
t.end(); t.end();
}); });
test('number', t => { test('number key', t => {
const rt = new Runtime(); const rt = new Runtime();
const k = new Keyboard(rt); const k = new Keyboard(rt);
k.postData({ k.postData({
keyCode: 49, key: '1',
isDown: true isDown: true
}); });
t.strictDeepEquals(k._keysPressed, [49]); t.strictDeepEquals(k._keysPressed, ['1']);
t.strictEquals(k.getKeyIsDown(49), true); t.strictEquals(k.getKeyIsDown(49), true);
t.strictEquals(k.getKeyIsDown('1'), true);
t.strictEquals(k.getKeyIsDown('any'), true); t.strictEquals(k.getKeyIsDown('any'), true);
t.end(); t.end();
}); });
test('non-english key', t => {
const rt = new Runtime();
const k = new Keyboard(rt);
k.postData({
key: '日',
isDown: true
});
t.strictDeepEquals(k._keysPressed, ['日']);
t.strictEquals(k.getKeyIsDown('日'), true);
t.strictEquals(k.getKeyIsDown('any'), true);
t.end();
});
test('ignore modifier key', t => {
const rt = new Runtime();
const k = new Keyboard(rt);
k.postData({
key: 'Shift',
isDown: true
});
t.strictDeepEquals(k._keysPressed, []);
t.strictEquals(k.getKeyIsDown('any'), false);
t.end();
});
test('keyup', t => { test('keyup', t => {
const rt = new Runtime(); const rt = new Runtime();
const k = new Keyboard(rt); const k = new Keyboard(rt);
k.postData({ k.postData({
keyCode: 37, key: 'ArrowLeft',
isDown: true isDown: true
}); });
k.postData({ k.postData({
keyCode: 37, key: 'ArrowLeft',
isDown: false isDown: false
}); });
t.strictDeepEquals(k._keysPressed, []); t.strictDeepEquals(k._keysPressed, []);
t.strictEquals(k.getKeyIsDown(37), false); t.strictEquals(k.getKeyIsDown('left arrow'), false);
t.strictEquals(k.getKeyIsDown('any'), false); t.strictEquals(k.getKeyIsDown('any'), false);
t.end(); t.end();
}); });