Last active
August 23, 2018 13:22
-
-
Save jollytoad/7a7df5272aa7d767463fcb391a215f86 to your computer and use it in GitHub Desktop.
Multi-type variable support for Blockly
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Check whether the given variable type is allowed on this field. | |
* @param {string|Array<string>} type The type (or types) to check. | |
* @return {boolean} True if the type (or one of the types) is in the list of allowed types. | |
* @private | |
*/ | |
Blockly.FieldVariable.prototype.typeIsAllowed_ = function (type) { | |
var typeList = this.getVariableTypes_() | |
if (!typeList) { | |
return true // If it's null, all types are valid. | |
} | |
var types = Array.isArray(type) ? type : [type] | |
// Find any intersection in the type lists. | |
for (var i = 0; i < types.length; i++) { | |
if (typeList.indexOf(types[i]) != -1) { | |
return true | |
} | |
} | |
return false | |
} | |
/** | |
* Parse the optional arguments representing the allowed variable types and the | |
* default variable type. | |
* @param {Array.<string>=} opt_variableTypes A list of the types of variables | |
* to include in the dropdown. If null or undefined, variables of all types | |
* will be displayed in the dropdown. | |
* @param {string|Array<string>=} opt_defaultType The type of the variable to create if this | |
* field's value is not explicitly set. Defaults to ''. | |
* @private | |
*/ | |
Blockly.FieldVariable.prototype.setTypes_ = function (opt_variableTypes, | |
opt_defaultType) { | |
// If you expected that the default type would be the same as the only entry | |
// in the variable types array, tell the Blockly team by commenting on #1499. | |
var defaultTypes = Array.isArray(opt_defaultType) ? opt_defaultType : [opt_defaultType || ''] | |
// Set the allowable variable types. Null means all types on the workspace. | |
if (opt_variableTypes == null || opt_variableTypes == undefined) { | |
var variableTypes = null | |
} else if (Array.isArray(opt_variableTypes)) { | |
var variableTypes = opt_variableTypes | |
// Make sure the default type is valid. | |
var isInArray = false | |
// Find any intersection in the type lists. | |
loop: | |
for (var i = 0; i < variableTypes.length; i++) { | |
for (var j = 0; j < defaultTypes.length; j++) { | |
if (variableTypes[i].indexOf(defaultTypes[j]) != -1) { | |
isInArray = true | |
break loop; | |
} | |
} | |
} | |
if (!isInArray) { | |
throw new Error('Invalid default type \'' + defaultTypes + '\' in ' + | |
'the definition of a FieldVariable') | |
} | |
} else { | |
throw new Error('\'variableTypes\' was not an array in the definition of ' + | |
'a FieldVariable') | |
} | |
// Only update the field once all checks pass. | |
this.defaultType_ = opt_defaultType || '' | |
this.variableTypes = variableTypes | |
} | |
/** | |
* Return a sorted list of variable names for variable dropdown menus. | |
* Include a special option at the end for creating a new variable name. | |
* @return {!Array.<string>} Array of variable names. | |
* @this {Blockly.FieldVariable} | |
*/ | |
Blockly.FieldVariable.dropdownCreate = function () { | |
if (!this.variable_) { | |
throw new Error('Tried to call dropdownCreate on a variable field with no' + | |
' variable selected.') | |
} | |
var name = this.getText() | |
var workspace = null | |
if (this.sourceBlock_) { | |
workspace = this.sourceBlock_.workspace | |
} | |
var variableModelList = [] | |
if (workspace) { | |
var variableTypes = this.getVariableTypes_() | |
// Get a copy of the list, so that adding rename and new variable options | |
// doesn't modify the workspace's list. | |
for (var i = 0; i < variableTypes.length; i++) { | |
var variableType = variableTypes[i] | |
var variables = workspace.getVariablesOfType(variableType) | |
variableModelList = variableModelList.concat(variables) | |
} | |
} | |
variableModelList.sort(Blockly.VariableModel.compareByName) | |
var options = [] | |
for (var i = 0, prevId = null; i < variableModelList.length; i++) { | |
var id = variableModelList[i].getId() | |
if (id !== prevId) { | |
// Set the UUID as the internal representation of the variable. | |
options.push([variableModelList[i].name, id]) | |
prevId = id | |
} | |
} | |
options.push([Blockly.Msg['RENAME_VARIABLE'], Blockly.RENAME_VARIABLE_ID]) | |
if (Blockly.Msg['DELETE_VARIABLE']) { | |
options.push( | |
[ | |
Blockly.Msg['DELETE_VARIABLE'].replace('%1', name), | |
Blockly.DELETE_VARIABLE_ID | |
] | |
) | |
} | |
return options | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Rename the given variable by updating its name in the variable map. | |
* @param {!Blockly.VariableModel} variable Variable to rename. | |
* @param {string} newName New variable name. | |
* @package | |
*/ | |
Blockly.VariableMap.prototype.renameVariable = function (variable, newName) { | |
var types = Array.isArray(variable.type) ? variable.type : [variable.type] | |
Blockly.Events.setGroup(true) | |
try { | |
for (var i = 0; i < types.length; i++) { | |
var type = types[i] | |
var conflictVar = this.getVariable(newName, type) | |
var blocks = this.workspace.getAllBlocks() | |
// The IDs may match if the rename is a simple case change (name1 -> Name1). | |
if (!conflictVar || conflictVar.getId() == variable.getId()) { | |
this.renameVariableAndUses_(variable, newName, blocks) | |
} else { | |
this.renameVariableWithConflict_(variable, newName, conflictVar, blocks) | |
} | |
} | |
} finally { | |
Blockly.Events.setGroup(false) | |
} | |
} | |
/** | |
* Create a variable with a given name, optional type(s), and optional ID. | |
* @param {string} name The name of the variable. This must be unique across | |
* variables and procedures. | |
* @param {string|Array<string>=} opt_type The type of the variable like 'int' or 'string'. | |
* Does not need to be unique. Field_variable can filter variables based on | |
* their type. This will default to '' which is a specific type. | |
* @param {string=} opt_id The unique ID of the variable. This will default to | |
* a UUID. | |
* @return {Blockly.VariableModel} The newly created variable. | |
*/ | |
Blockly.VariableMap.prototype.createVariable = function (name, opt_type, opt_id) { | |
var variable = this.getVariable(name, opt_type) | |
if (variable) { | |
if (opt_id && variable.getId() != opt_id) { | |
throw Error('Variable "' + name + '" is already in use and its id is "' + | |
variable.getId() + '" which conflicts with the passed in ' + | |
'id, "' + opt_id + '".') | |
} | |
// The variable already exists and has the same ID. | |
return variable | |
} | |
if (opt_id && this.getVariableById(opt_id)) { | |
throw Error('Variable id, "' + opt_id + '", is already in use.') | |
} | |
opt_id = opt_id || Blockly.utils.genUid() | |
opt_type = opt_type || '' | |
variable = new Blockly.VariableModel(this.workspace, name, opt_type, opt_id) | |
this.addVariable_(variable) | |
return variable | |
} | |
/** | |
* Add the variable to the maps by its type | |
* @param {Blockly.VariableModel} variable | |
* @private | |
*/ | |
Blockly.VariableMap.prototype.addVariable_ = function (variable) { | |
var types = Array.isArray(variable.type) ? variable.type : [variable.type] | |
for (var i = 0; i < types.length; i++) { | |
var type = types[i] | |
// If opt_type is not a key, create a new list. | |
if (!this.variableMap_[type]) { | |
this.variableMap_[type] = [variable] | |
} else { | |
// Else append the variable to the preexisting list. | |
this.variableMap_[type].push(variable) | |
} | |
} | |
} | |
/* Begin functions for variable deletion. */ | |
/** | |
* Delete a variable. | |
* @param {!Blockly.VariableModel} variable Variable to delete. | |
*/ | |
Blockly.VariableMap.prototype.deleteVariable = function (variable) { | |
if (this.removeVariable_(variable)) { | |
Blockly.Events.fire(new Blockly.Events.VarDelete(variable)) | |
} | |
} | |
/** | |
* Remove the variable from the maps by its type | |
* @param {Blockly.VariableModel} variable | |
* @return {boolean} True if the variable was removed from at least one of the maps | |
* @private | |
*/ | |
Blockly.VariableMap.prototype.removeVariable_ = function (variable) { | |
var types = Array.isArray(variable.type) ? variable.type : [variable.type] | |
var removed = false | |
for (var i = 0; i < types.length; i++) { | |
var type = types[i] | |
var variableList = this.variableMap_[type] | |
for (var j = 0, tempVar; tempVar = variableList[j]; j++) { | |
if (tempVar.getId() == variable.getId()) { | |
variableList.splice(j, 1) | |
removed = true | |
break | |
} | |
} | |
} | |
return removed | |
} | |
/** | |
* Find the variable by the given name and type and return it. Return null if | |
* it is not found. | |
* @param {string} name The name to check for. | |
* @param {string|Array<string>=} opt_type The type of the variable. If not provided it | |
* defaults to the empty string, which is a specific type. | |
* @return {Blockly.VariableModel} The variable with the given name, or null if | |
* it was not found. | |
*/ | |
Blockly.VariableMap.prototype.getVariable = function (name, opt_type) { | |
var types = Array.isArray(opt_type) ? opt_type : [opt_type || ''] | |
for (var i = 0; i < types.length; i++) { | |
var type = types[i] | |
var list = this.variableMap_[type] | |
if (list) { | |
for (var j = 0, variable; variable = list[j]; j++) { | |
if (Blockly.Names.equals(variable.name, name)) { | |
return variable | |
} | |
} | |
} | |
} | |
return null | |
} | |
/** | |
* Find the variable by the given ID and return it. Return null if it is not | |
* found. | |
* @param {string} id The ID to check for. | |
* @return {Blockly.VariableModel} The variable with the given ID. | |
*/ | |
Blockly.VariableMap.prototype.getVariableById = function (id) { | |
var keys = Object.keys(this.variableMap_) | |
for (var i = 0; i < keys.length; i++) { | |
var key = keys[i] | |
for (var j = 0, variable; variable = this.variableMap_[key][j]; j++) { | |
if (variable.getId() == id) { | |
return variable | |
} | |
} | |
} | |
return null | |
} | |
/** | |
* Return all variable types. This list always contains the empty string. | |
* @return {!Array.<string>} List of variable types. | |
* @package | |
*/ | |
Blockly.VariableMap.prototype.getVariableTypes = function () { | |
var types = Object.keys(this.variableMap_) | |
var hasEmpty = false | |
for (var i = 0; i < types.length; i++) { | |
if (types[i] == '') { | |
hasEmpty = true | |
} | |
} | |
if (!hasEmpty) { | |
types.push('') | |
} | |
return types | |
} | |
/** | |
* Return all variables of all types. | |
* @return {!Array.<!Blockly.VariableModel>} List of variable models. | |
*/ | |
Blockly.VariableMap.prototype.getAllVariables = function () { | |
var variable_ids = {} | |
var all_variables = [] | |
var keys = Object.keys(this.variableMap_) | |
for (var i = 0; i < keys.length; i++) { | |
var variables = this.variableMap_[keys[i]] | |
for (var j = 0; j < variables.length; j++) { | |
var variable = variables[j] | |
if (!variable_ids[variable.getId()]) { | |
all_variables.push(variable) | |
variable_ids[variable.getId()] = true | |
} | |
} | |
} | |
return all_variables | |
} | |
// EXTENSION (to allow changing of a variable type) | |
/** | |
* Change the type of the given variable by moving it to the appropriate variable map. | |
* @param {!Blockly.VariableModel} variable Variable to change. | |
* @param {string|Array<string>=} opt_type New type for the variable. | |
*/ | |
Blockly.VariableMap.prototype.changeVariableType = function (variable, opt_type) { | |
var oldType = variable.type | |
var newType = opt_type || '' | |
if (String(oldType) !== String(newType)) { | |
this.removeVariable_(variable) | |
variable.type = newType | |
this.addVariable_(variable) | |
// TODO: consider firing a variable event? may need to be a new event type... | |
// Blockly.Events.fire(new Blockly.Events.VarTypeChanged(variable, oldType, newType)) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Decode an XML list of variables and add the variables to the workspace. | |
* @param {!Element} xmlVariables List of XML variable elements. | |
* @param {!Blockly.Workspace} workspace The workspace to which the variable | |
* should be added. | |
*/ | |
Blockly.Xml.domToVariables = function(xmlVariables, workspace) { | |
for (var i = 0, xmlChild; xmlChild = xmlVariables.children[i]; i++) { | |
var type = xmlChild.getAttribute('type'); | |
var id = xmlChild.getAttribute('id'); | |
var name = xmlChild.textContent; | |
if (type == null) { | |
throw Error('Variable with id, ' + id + ' is without a type'); | |
} | |
type = type.indexOf(',') > -1 ? type.split(',') : type | |
workspace.createVariable(name, type, id); | |
} | |
}; | |
/** | |
* Decode an XML variable field tag and set the value of that field. | |
* @param {!Blockly.Workspace} workspace The workspace that is currently being | |
* deserialized. | |
* @param {!Element} xml The field tag to decode. | |
* @param {string} text The text content of the XML tag. | |
* @param {!Blockly.FieldVariable} field The field on which the value will be | |
* set. | |
* @private | |
*/ | |
Blockly.Xml.domToFieldVariable_ = function (workspace, xml, text, field) { | |
var type = xml.getAttribute('variabletype') || '' | |
// TODO (fenichel): Does this need to be explicit or not? | |
if (type == '\'\'') { | |
type = '' | |
} | |
type = type.indexOf(',') > -1 ? type.split(',') : type | |
var variable = Blockly.Variables.getOrCreateVariablePackage(workspace, xml.id, text, type) | |
// This should never happen :) | |
if (type != null && String(type) !== String(variable.type)) { | |
console.debug('Serialized variable type with id \'' + | |
variable.getId() + '\' had type ' + variable.type + ', and ' + | |
'does not match variable field that references it: ' + | |
Blockly.Xml.domToText(xml) + '.') | |
} | |
field.setValue(variable.getId()) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment