Initial commit for changing key mappings (#2787)

* Added ability to easily change key mappings
This commit is contained in:
alschmiedt
2019-08-13 13:37:01 -07:00
committed by GitHub
parent 4758b4aa55
commit 74fa3bb71a
11 changed files with 494 additions and 226 deletions

View File

@@ -218,7 +218,7 @@ Blockly.onKeyDown_ = function(e) {
// Pressing esc closes the context menu.
Blockly.hideChaff();
if (Blockly.keyboardAccessibilityMode) {
Blockly.Navigation.navigate(e);
Blockly.Navigation.onKeyPress(e);
}
} else if (e.keyCode == 8 || e.keyCode == 46) {
// Delete or backspace.
@@ -274,9 +274,12 @@ Blockly.onKeyDown_ = function(e) {
// 'z' for undo 'Z' is for redo.
Blockly.hideChaff();
mainWorkspace.undo(e.shiftKey);
} else if (Blockly.keyboardAccessibilityMode &&
Blockly.Navigation.onKeyPress(e)) {
return;
}
} else if (Blockly.keyboardAccessibilityMode &&
Blockly.Navigation.navigate(e)) {
Blockly.Navigation.onKeyPress(e)) {
return;
}
// Common code for delete and cut.

View File

@@ -74,6 +74,7 @@ Blockly.inject = function(container, opt_options) {
var workspace = Blockly.createMainWorkspace_(svg, options, blockDragSurface,
workspaceDragSurface);
Blockly.setTheme(options.theme);
Blockly.user.keyMap.setKeyMap(options.keyMap);
Blockly.init_(workspace);
Blockly.mainWorkspace = workspace;

View File

@@ -0,0 +1,42 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2019 Google Inc.
* https://developers.google.com/blockly/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview The class representing an action.
* Used primarily for keyboard navigation.
*/
'use strict';
goog.provide('Blockly.Action');
/**
* Class for a single action.
* There can be one action for each key. If the action only applies to a
* single state (toolbox, flyout, workspace) then the function should handle this.
* @param {string} name The name of the action.
* @param {string} desc The description of the action.
* @param {Function} func The function to be called when the key is pressed.
* @constructor
*/
Blockly.Action = function(name, desc, func) {
this.name = name;
this.desc = desc;
this.func = func;
};

View File

@@ -0,0 +1,142 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2019 Google Inc.
* https://developers.google.com/blockly/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview The namespace used to keep track of keyboard actions and the
* key codes used to execute those actions.
* This is used primarily for keyboard navigation.
*/
'use strict';
goog.provide('Blockly.user.keyMap');
/**
* Holds the serialized key to key action mapping.
* @type {Object<string, Blockly.Action>}
*/
Blockly.user.keyMap.map_ = {};
/**
* List of modifier keys checked when serializing the key event.
* @type {Array<string>}
*/
Blockly.user.keyMap.modifierKeys = ['Shift','Control','Alt','Meta'];
/**
* Update the key map to contain the new action.
* @param {!string} keyCode The key code serialized by the serializeKeyEvent.
* @param {!Blockly.Action} action The action to be executed when the keys
* corresponding to the serialized key code is pressed.
* @package
*/
Blockly.user.keyMap.setActionForKey = function(keyCode, action) {
var oldKey = Blockly.user.keyMap.getKeyByAction(action);
// If the action already exists in the key map remove it and add the new mapping.
if (oldKey) {
delete Blockly.user.keyMap.map_[oldKey];
}
Blockly.user.keyMap.map_[keyCode] = action;
};
/**
* Creates a new key map.
* @param {Object<string, Blockly.Action>} keyMap The object holding the key
* to action mapping.
* @package
*/
Blockly.user.keyMap.setKeyMap = function(keyMap) {
Blockly.user.keyMap.map_ = keyMap;
};
/**
* Gets the current key map.
* @return {Object<string,Blockly.Action>} The object holding the key to
* action mapping.
* @package
*/
Blockly.user.keyMap.getKeyMap = function() {
return Object.assign({}, Blockly.user.keyMap.map_);
};
/**
* Get the action by the serialized key code.
* @param {string} keyCode The serialized key code.
* @return {Blockly.Action|undefined} The action holding the function to
* call when the given keyCode is used or undefined if no action exists.
* @package
*/
Blockly.user.keyMap.getActionByKeyCode = function(keyCode) {
return Blockly.user.keyMap.map_[keyCode];
};
/**
* Get the serialized key that corresponds to the action.
* @param {!Blockly.Action} action The action for which we want to get
* the key.
* @return {string} The serialized key or null if the action does not have
* a key mapping.
* @package
*/
Blockly.user.keyMap.getKeyByAction = function(action) {
var keys = Object.keys(Blockly.user.keyMap.map_);
for (var i = 0, key; key = keys[i]; i++) {
if (Blockly.user.keyMap.map_[key].name === action.name) {
return key;
}
}
return null;
};
/**
* Serialize the key event.
* @param {!Event} e A key up event holding the key code.
* @return {!string} A string containing the serialized key event.
*/
Blockly.user.keyMap.serializeKeyEvent = function(e) {
var modifierKeys = Blockly.user.keyMap.modifierKeys;
var key = '';
for (var i = 0, keyName; keyName = modifierKeys[i]; i++) {
if (e.getModifierState(keyName)) {
key += keyName;
}
}
key += e.keyCode;
return key;
};
/**
* Creates the default key map.
* @return {!Object<string,Blockly.Action>} An object holding the default key
* to action mapping.
*/
Blockly.user.keyMap.createDefaultKeyMap = function() {
var map = {};
map[goog.events.KeyCodes.W] = Blockly.Navigation.ACTION_PREVIOUS;
map[goog.events.KeyCodes.A] = Blockly.Navigation.ACTION_OUT;
map[goog.events.KeyCodes.S] = Blockly.Navigation.ACTION_NEXT;
map[goog.events.KeyCodes.D] = Blockly.Navigation.ACTION_IN;
map[goog.events.KeyCodes.I] = Blockly.Navigation.ACTION_INSERT;
map[goog.events.KeyCodes.ENTER] = Blockly.Navigation.ACTION_MARK;
map[goog.events.KeyCodes.X] = Blockly.Navigation.ACTION_DISCONNECT;
map[goog.events.KeyCodes.T] = Blockly.Navigation.ACTION_TOOLBOX;
map[goog.events.KeyCodes.E] = Blockly.Navigation.ACTION_EXIT;
map[goog.events.KeyCodes.ESC] = Blockly.Navigation.ACTION_EXIT;
return map;
};

View File

@@ -20,7 +20,9 @@
goog.provide('Blockly.Navigation');
goog.require('Blockly.Action');
goog.require('Blockly.ASTNode');
goog.require('Blockly.user.keyMap');
/**
@@ -357,8 +359,8 @@ Blockly.Navigation.insertFromFlyout = function() {
var prevConnection = newBlock.previousConnection;
var outConnection = newBlock.outputConnection;
var topConnection = prevConnection ? prevConnection : outConnection;
//TODO: This will have to be fixed when we add in a block that does not have
//a previous or output connection
// TODO: This will have to be fixed when we add in a block that does not have
// a previous or output connection
var astNode = Blockly.ASTNode.createConnectionNode(topConnection);
Blockly.Navigation.cursor_.setLocation(astNode);
Blockly.Navigation.removeMark();
@@ -606,8 +608,8 @@ Blockly.Navigation.focusWorkspace = function() {
if (Blockly.selected) {
var previousConnection = Blockly.selected.previousConnection;
var outputConnection = Blockly.selected.outputConnection;
//TODO: This still needs to work with blocks that have neither previous
//or output connection.
// TODO: This still needs to work with blocks that have neither previous
// or output connection.
var connection = previousConnection ? previousConnection : outputConnection;
var newAstNode = Blockly.ASTNode.createConnectionNode(connection);
cursor.setLocation(newAstNode);
@@ -647,133 +649,15 @@ Blockly.Navigation.handleEnterForWS = function() {
/**
* TODO: Revisit keycodes before releasing
* Handler for all the keyboard navigation events.
* @param {Event} e The keyboard event.
* @return {!boolean} True if the key was handled false otherwise.
*/
Blockly.Navigation.navigate = function(e) {
var curState = Blockly.Navigation.currentState_;
if (e.keyCode === goog.events.KeyCodes.T) {
var workspace = Blockly.getMainWorkspace();
if (!workspace.getToolbox()) {
Blockly.Navigation.focusFlyout();
Blockly.Navigation.log('T: Focus Flyout');
} else {
Blockly.Navigation.focusToolbox();
Blockly.Navigation.log('T: Focus Toolbox');
}
return true;
} else if (curState === Blockly.Navigation.STATE_FLYOUT) {
return Blockly.Navigation.flyoutKeyHandler(e);
} else if (curState === Blockly.Navigation.STATE_WS) {
return Blockly.Navigation.workspaceKeyHandler(e);
} else if (curState === Blockly.Navigation.STATE_TOOLBOX) {
return Blockly.Navigation.toolboxKeyHandler(e);
} else {
Blockly.Navigation.log('Not a valid key ');
}
return false;
};
/**
* Handles all keyboard events when the user is focused on the flyout.
* @param {Event} e The keyboard event.
* @return {!boolean} True if the key was handled false otherwise.
*/
Blockly.Navigation.flyoutKeyHandler = function(e) {
if (e.keyCode === goog.events.KeyCodes.W) {
Blockly.Navigation.selectPreviousBlockInFlyout();
Blockly.Navigation.log('W: Flyout : Previous');
return true;
} else if (e.keyCode === goog.events.KeyCodes.A) {
Blockly.Navigation.focusToolbox();
Blockly.Navigation.log('A: Flyout : Go To Toolbox');
return true;
} else if (e.keyCode === goog.events.KeyCodes.S) {
Blockly.Navigation.selectNextBlockInFlyout();
Blockly.Navigation.log('S: Flyout : Next');
return true;
} else if (e.keyCode === goog.events.KeyCodes.ENTER) {
Blockly.Navigation.insertFromFlyout();
Blockly.Navigation.log('Enter: Flyout : Select');
return true;
} else if (e.keyCode === goog.events.KeyCodes.E ||
e.keyCode === goog.events.KeyCodes.ESC) {
Blockly.Navigation.focusWorkspace();
Blockly.Navigation.log('E or ESC: Flyout: Exit');
return true;
}
return false;
};
/**
* Handles all keyboard events when the user is focused on the toolbox.
* @param {Event} e The keyboard event.
* @return {!boolean} True if the key was handled false otherwise.
*/
Blockly.Navigation.toolboxKeyHandler = function(e) {
if (e.keyCode === goog.events.KeyCodes.W) {
Blockly.Navigation.previousCategory();
Blockly.Navigation.log('W: Toolbox : Previous');
return true;
} else if (e.keyCode === goog.events.KeyCodes.A) {
Blockly.Navigation.outCategory();
Blockly.Navigation.log('A: Toolbox : Out');
return true;
} else if (e.keyCode === goog.events.KeyCodes.S) {
Blockly.Navigation.nextCategory();
Blockly.Navigation.log('S: Toolbox : Next');
return true;
} else if (e.keyCode === goog.events.KeyCodes.D) {
Blockly.Navigation.inCategory();
Blockly.Navigation.log('D: Toolbox : Go to flyout');
return true;
} else if (e.keyCode === goog.events.KeyCodes.ENTER) {
//TODO: focus on flyout OR open if the category is nested
return true;
} else if (e.keyCode === goog.events.KeyCodes.E ||
e.keyCode === goog.events.KeyCodes.ESC) {
Blockly.Navigation.log('E or ESC: Toolbox: Exit');
Blockly.Navigation.focusWorkspace();
return true;
}
return false;
};
/**
* Handles all keyboard events when the user is focused on the workspace.
* @param {Event} e The keyboard event.
* @return {!boolean} True if the key was handled false otherwise.
*/
Blockly.Navigation.workspaceKeyHandler = function(e) {
if (e.keyCode === goog.events.KeyCodes.W) {
Blockly.Navigation.cursor_.prev();
Blockly.Navigation.log('W: Workspace : Out');
return true;
} else if (e.keyCode === goog.events.KeyCodes.A) {
Blockly.Navigation.cursor_.out();
Blockly.Navigation.log('S: Workspace : Previous');
return true;
} else if (e.keyCode === goog.events.KeyCodes.S) {
Blockly.Navigation.cursor_.next();
Blockly.Navigation.log('S: Workspace : In');
return true;
} else if (e.keyCode === goog.events.KeyCodes.D) {
Blockly.Navigation.cursor_.in();
Blockly.Navigation.log('S: Workspace : Next');
return true;
} else if (e.keyCode === goog.events.KeyCodes.I) {
Blockly.Navigation.modify();
Blockly.Navigation.log('I: Workspace : Insert/Connect Blocks');
return true;
} else if (e.keyCode === goog.events.KeyCodes.ENTER) {
Blockly.Navigation.handleEnterForWS();
Blockly.Navigation.log('Enter: Workspace : Mark');
return true;
} else if (e.keyCode === goog.events.KeyCodes.X) {
Blockly.Navigation.log('X: Workspace: Disconnect Blocks');
Blockly.Navigation.disconnectBlocks();
Blockly.Navigation.onKeyPress = function(e) {
var key = Blockly.user.keyMap.serializeKeyEvent(e);
var action = Blockly.user.keyMap.getActionByKeyCode(key);
if (action) {
action.func.call();
return true;
}
return false;
@@ -834,3 +718,113 @@ Blockly.Navigation.error = function(msg) {
console.error(msg);
}
};
/**
* The previous action.
* @type Blockly.Action
*/
Blockly.Navigation.ACTION_PREVIOUS = new Blockly.Action('previous', 'Goes to the previous location', function() {
if (Blockly.Navigation.currentState_ === Blockly.Navigation.STATE_WS) {
Blockly.Navigation.cursor_.prev();
} else if (Blockly.Navigation.currentState_ === Blockly.Navigation.STATE_FLYOUT) {
Blockly.Navigation.selectPreviousBlockInFlyout();
} else if (Blockly.Navigation.currentState_ === Blockly.Navigation.STATE_TOOLBOX) {
Blockly.Navigation.previousCategory();
}
});
/**
* The previous action.
* @type Blockly.Action
*/
Blockly.Navigation.ACTION_OUT = new Blockly.Action('out', 'Goes out', function() {
if (Blockly.Navigation.currentState_ === Blockly.Navigation.STATE_WS) {
Blockly.Navigation.cursor_.out();
} else if (Blockly.Navigation.currentState_ === Blockly.Navigation.STATE_FLYOUT) {
Blockly.Navigation.focusToolbox();
} else if (Blockly.Navigation.currentState_ === Blockly.Navigation.STATE_TOOLBOX) {
Blockly.Navigation.outCategory();
}
});
/**
* The previous action.
* @type Blockly.Action
*/
Blockly.Navigation.ACTION_NEXT = new Blockly.Action('next', 'Goes to the next location', function() {
if (Blockly.Navigation.currentState_ === Blockly.Navigation.STATE_WS) {
Blockly.Navigation.cursor_.next();
} else if (Blockly.Navigation.currentState_ === Blockly.Navigation.STATE_FLYOUT) {
Blockly.Navigation.selectNextBlockInFlyout();
} else if (Blockly.Navigation.currentState_ === Blockly.Navigation.STATE_TOOLBOX) {
Blockly.Navigation.nextCategory();
}
});
/**
* The action to go in.
* @type Blockly.Action
*/
Blockly.Navigation.ACTION_IN = new Blockly.Action('in', 'Goes in', function() {
if (Blockly.Navigation.currentState_ === Blockly.Navigation.STATE_WS) {
Blockly.Navigation.cursor_.in();
} else if (Blockly.Navigation.currentState_ === Blockly.Navigation.STATE_TOOLBOX) {
Blockly.Navigation.inCategory();
}
});
/**
* The action to try to insert a block.
* @type Blockly.Action
*/
Blockly.Navigation.ACTION_INSERT = new Blockly.Action('insert',
'Tries to connect the current location to the marked location', function() {
if (Blockly.Navigation.currentState_ === Blockly.Navigation.STATE_WS) {
Blockly.Navigation.modify();
}
});
/**
* The action to mark a certain location.
* @type Blockly.Action
*/
Blockly.Navigation.ACTION_MARK = new Blockly.Action('mark', 'Marks the current location', function() {
if (Blockly.Navigation.currentState_ === Blockly.Navigation.STATE_WS) {
Blockly.Navigation.handleEnterForWS();
} else if (Blockly.Navigation.currentState_ === Blockly.Navigation.STATE_FLYOUT) {
Blockly.Navigation.insertFromFlyout();
}
});
/**
* The action to disconnect a block.
* @type Blockly.Action
*/
Blockly.Navigation.ACTION_DISCONNECT = new Blockly.Action('disconnect', 'Disconnect the blocks', function() {
if (Blockly.Navigation.currentState_ === Blockly.Navigation.STATE_WS) {
Blockly.Navigation.disconnectBlocks();
}
});
/**
* The action to open the toolbox.
* @type Blockly.Action
*/
Blockly.Navigation.ACTION_TOOLBOX = new Blockly.Action('toolbox', 'Open the toolbox', function() {
if (!Blockly.getMainWorkspace().getToolbox()) {
Blockly.Navigation.focusFlyout();
} else {
Blockly.Navigation.focusToolbox();
}
});
/**
* The action to exit the toolbox or flyout.
* @type Blockly.Action
*/
Blockly.Navigation.ACTION_EXIT = new Blockly.Action('exit', 'Exit the toolbox', function() {
if (Blockly.Navigation.currentState_ === Blockly.Navigation.STATE_TOOLBOX ||
Blockly.Navigation.currentState_ === Blockly.Navigation.STATE_FLYOUT) {
Blockly.Navigation.focusWorkspace();
}
});

View File

@@ -119,6 +119,7 @@ Blockly.Options = function(options) {
var oneBasedIndex = !!options['oneBasedIndex'];
}
var theme = options['theme'] || Blockly.Themes.Classic;
var keyMap = options['keyMap'] || Blockly.user.keyMap.createDefaultKeyMap();
this.RTL = rtl;
this.oneBasedIndex = oneBasedIndex;
@@ -143,6 +144,7 @@ Blockly.Options = function(options) {
this.zoomOptions = Blockly.Options.parseZoomOptions_(options);
this.toolboxPosition = toolboxPosition;
this.theme = theme;
this.keyMap = keyMap;
};
/**