mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-03-13 17:04:39 -04:00
Merge pull request #1085 from ericrosenbaum/feature/key-droppability
Re-write keyboard IO for droppability support
This commit is contained in:
commit
9979391200
2 changed files with 147 additions and 70 deletions
|
@ -1,10 +1,33 @@
|
|||
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 {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* List of currently pressed keys.
|
||||
* @type{Array.<number>}
|
||||
* List of currently pressed scratch keys.
|
||||
* 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 = [];
|
||||
/**
|
||||
|
@ -16,48 +39,71 @@ class Keyboard {
|
|||
}
|
||||
|
||||
/**
|
||||
* Convert a Scratch key name to a DOM keyCode.
|
||||
* @param {Any} keyName Scratch key argument.
|
||||
* @return {number} Key code corresponding to a DOM event.
|
||||
* @private
|
||||
* Convert from a keyboard event key name to a Scratch key name.
|
||||
* @param {string} keyString the input key string.
|
||||
* @return {string} the corresponding Scratch key, or an empty string.
|
||||
*/
|
||||
_scratchKeyToKeyCode (keyName) {
|
||||
if (typeof keyName === 'number') {
|
||||
// Key codes placed in with number blocks.
|
||||
return keyName;
|
||||
}
|
||||
const keyString = Cast.toString(keyName);
|
||||
_keyStringToScratchKey (keyString) {
|
||||
keyString = Cast.toString(keyString);
|
||||
// Convert space and arrow keys to their Scratch key names.
|
||||
switch (keyString) {
|
||||
case 'space': return 32;
|
||||
case 'left arrow': return 37;
|
||||
case 'up arrow': return 38;
|
||||
case 'right arrow': return 39;
|
||||
case 'down arrow': return 40;
|
||||
// @todo: Consider adding other special keys here.
|
||||
case ' ': return KEY_NAME.SPACE;
|
||||
case 'ArrowLeft':
|
||||
case 'Left': return KEY_NAME.LEFT;
|
||||
case 'ArrowUp':
|
||||
case 'Up': return KEY_NAME.UP;
|
||||
case 'Right':
|
||||
case 'ArrowRight': return KEY_NAME.RIGHT;
|
||||
case 'Down':
|
||||
case 'ArrowDown': return KEY_NAME.DOWN;
|
||||
}
|
||||
// Keys reported by DOM keyCode are upper case.
|
||||
return keyString.toUpperCase().charCodeAt(0);
|
||||
// Ignore modifier keys
|
||||
if (keyString.length > 1) {
|
||||
return '';
|
||||
}
|
||||
return keyString.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a DOM keyCode into a Scratch key name.
|
||||
* @param {number} keyCode Key code from DOM event.
|
||||
* @return {Any} Scratch key argument.
|
||||
* @private
|
||||
* Convert from a block argument to a Scratch key name.
|
||||
* @param {string} keyArg the input arg.
|
||||
* @return {string} the corresponding Scratch key.
|
||||
*/
|
||||
_keyCodeToScratchKey (keyCode) {
|
||||
if (keyCode >= 48 && keyCode <= 90) {
|
||||
// Standard letter.
|
||||
return String.fromCharCode(keyCode).toLowerCase();
|
||||
_keyArgToScratchKey (keyArg) {
|
||||
// If a number was dropped in, try to convert from ASCII to Scratch key.
|
||||
if (typeof keyArg === 'number') {
|
||||
// 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';
|
||||
case 37: return 'left arrow';
|
||||
case 38: return 'up arrow';
|
||||
case 39: return 'right arrow';
|
||||
case 40: return 'down arrow';
|
||||
|
||||
keyArg = Cast.toString(keyArg);
|
||||
|
||||
// If the arg matches a special key name, return it.
|
||||
if (KEY_NAME_LIST.includes(keyArg)) {
|
||||
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.
|
||||
*/
|
||||
postData (data) {
|
||||
if (data.keyCode) {
|
||||
const index = this._keysPressed.indexOf(data.keyCode);
|
||||
if (data.isDown) {
|
||||
// If not already present, add to the list.
|
||||
if (index < 0) {
|
||||
this._keysPressed.push(data.keyCode);
|
||||
}
|
||||
// Always trigger hats, even if it was already pressed.
|
||||
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);
|
||||
if (!data.key) return;
|
||||
const scratchKey = this._keyStringToScratchKey(data.key);
|
||||
if (scratchKey === '') return;
|
||||
const index = this._keysPressed.indexOf(scratchKey);
|
||||
if (data.isDown) {
|
||||
// If not already present, add to the list.
|
||||
if (index < 0) {
|
||||
this._keysPressed.push(scratchKey);
|
||||
}
|
||||
// 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.
|
||||
* @param {Any} key Scratch key argument.
|
||||
* Get key down state for a specified key.
|
||||
* @param {Any} keyArg key argument.
|
||||
* @return {boolean} Is the specified key down?
|
||||
*/
|
||||
getKeyIsDown (key) {
|
||||
if (key === 'any') {
|
||||
getKeyIsDown (keyArg) {
|
||||
if (keyArg === 'any') {
|
||||
return this._keysPressed.length > 0;
|
||||
}
|
||||
const keyCode = this._scratchKeyToKeyCode(key);
|
||||
return this._keysPressed.indexOf(keyCode) > -1;
|
||||
const scratchKey = this._keyArgToScratchKey(keyArg);
|
||||
return this._keysPressed.indexOf(scratchKey) > -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,62 +12,92 @@ test('spec', t => {
|
|||
t.end();
|
||||
});
|
||||
|
||||
test('space', t => {
|
||||
test('space key', t => {
|
||||
const rt = new Runtime();
|
||||
const k = new Keyboard(rt);
|
||||
|
||||
k.postData({
|
||||
keyCode: 32,
|
||||
key: ' ',
|
||||
isDown: true
|
||||
});
|
||||
t.strictDeepEquals(k._keysPressed, [32]);
|
||||
t.strictDeepEquals(k._keysPressed, ['space']);
|
||||
t.strictEquals(k.getKeyIsDown('space'), true);
|
||||
t.strictEquals(k.getKeyIsDown('any'), true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('letter', t => {
|
||||
test('letter key', t => {
|
||||
const rt = new Runtime();
|
||||
const k = new Keyboard(rt);
|
||||
|
||||
k.postData({
|
||||
keyCode: 65,
|
||||
key: 'a',
|
||||
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('any'), true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('number', t => {
|
||||
test('number key', t => {
|
||||
const rt = new Runtime();
|
||||
const k = new Keyboard(rt);
|
||||
|
||||
k.postData({
|
||||
keyCode: 49,
|
||||
key: '1',
|
||||
isDown: true
|
||||
});
|
||||
t.strictDeepEquals(k._keysPressed, [49]);
|
||||
t.strictDeepEquals(k._keysPressed, ['1']);
|
||||
t.strictEquals(k.getKeyIsDown(49), true);
|
||||
t.strictEquals(k.getKeyIsDown('1'), true);
|
||||
t.strictEquals(k.getKeyIsDown('any'), true);
|
||||
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 => {
|
||||
const rt = new Runtime();
|
||||
const k = new Keyboard(rt);
|
||||
|
||||
k.postData({
|
||||
keyCode: 37,
|
||||
key: 'ArrowLeft',
|
||||
isDown: true
|
||||
});
|
||||
k.postData({
|
||||
keyCode: 37,
|
||||
key: 'ArrowLeft',
|
||||
isDown: false
|
||||
});
|
||||
t.strictDeepEquals(k._keysPressed, []);
|
||||
t.strictEquals(k.getKeyIsDown(37), false);
|
||||
t.strictEquals(k.getKeyIsDown('left arrow'), false);
|
||||
t.strictEquals(k.getKeyIsDown('any'), false);
|
||||
t.end();
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue