diff --git a/blocks/lists.js b/blocks/lists.js index 5dcbadb75..c32728cac 100644 --- a/blocks/lists.js +++ b/blocks/lists.js @@ -119,6 +119,13 @@ Blockly.Blocks['lists_create_with'] = { itemBlock = itemBlock.nextConnection && itemBlock.nextConnection.targetBlock(); } + // Disconnect any children that don't belong. + for (var i = 0; i < this.itemCount_; i++) { + var connection = this.getInput('ADD' + i).connection.targetConnection; + if (connection && connections.indexOf(connection) == -1) { + connection.disconnect(); + } + } this.itemCount_ = connections.length; this.updateShape_(); // Reconnect any child blocks. diff --git a/blocks/procedures.js b/blocks/procedures.js index 26ab03447..c74c15f6a 100644 --- a/blocks/procedures.js +++ b/blocks/procedures.js @@ -117,18 +117,30 @@ Blockly.Blocks['procedures_defnoreturn'] = { paramString = Blockly.Msg.PROCEDURES_BEFORE_PARAMS + ' ' + this.arguments_.join(', '); } + // The params field is deterministic based on the mutation, + // no need to fire a change event. + Blockly.Events.disable(); this.setFieldValue(paramString, 'PARAMS'); + Blockly.Events.enable(); }, /** * Create XML to represent the argument inputs. + * @param {=boolean} opt_paramIds If true include the IDs of the parameter + * quarks. Used by Blockly.Procedures.mutateCallers for reconnection. * @return {!Element} XML storage element. * @this Blockly.Block */ - mutationToDom: function() { + mutationToDom: function(opt_paramIds) { var container = document.createElement('mutation'); + if (opt_paramIds) { + container.setAttribute('name', this.getFieldValue('NAME')); + } for (var i = 0; i < this.arguments_.length; i++) { var parameter = document.createElement('arg'); parameter.setAttribute('name', this.arguments_[i]); + if (opt_paramIds && this.paramIds_) { + parameter.setAttribute('paramId', this.paramIds_[i]); + } container.appendChild(parameter); } @@ -151,6 +163,7 @@ Blockly.Blocks['procedures_defnoreturn'] = { } } this.updateParams_(); + Blockly.Procedures.mutateCallers(this); // Show or hide the statement input. this.setStatements_(xmlElement.getAttribute('statements') !== 'false'); @@ -185,8 +198,7 @@ Blockly.Blocks['procedures_defnoreturn'] = { connection = paramBlock.nextConnection; } // Initialize procedure's callers with blank IDs. - Blockly.Procedures.mutateCallers(this.getFieldValue('NAME'), - this.workspace, this.arguments_, null); + Blockly.Procedures.mutateCallers(this); return containerBlock; }, /** @@ -206,8 +218,7 @@ Blockly.Blocks['procedures_defnoreturn'] = { paramBlock.nextConnection.targetBlock(); } this.updateParams_(); - Blockly.Procedures.mutateCallers(this.getFieldValue('NAME'), - this.workspace, this.arguments_, this.paramIds_); + Blockly.Procedures.mutateCallers(this); // Show/hide the statement input. var hasStatements = containerBlock.getFieldValue('STATEMENTS'); @@ -439,15 +450,15 @@ Blockly.Blocks['procedures_callnoreturn'] = { */ init: function() { this.appendDummyInput('TOPROW') - .appendField('', 'NAME'); + .appendField(this.id, 'NAME'); this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(Blockly.Blocks.procedures.HUE); - // Tooltip is set in domToMutation. + // Tooltip is set in renameProcedure. this.setHelpUrl(Blockly.Msg.PROCEDURES_CALLNORETURN_HELPURL); this.arguments_ = []; this.quarkConnections_ = {}; - this.quarkArguments_ = null; + this.quarkIds_ = null; }, /** * Returns the name of the procedure this block calls. @@ -480,63 +491,69 @@ Blockly.Blocks['procedures_callnoreturn'] = { * @param {!Array.} paramIds IDs of params (consistent for each * parameter through the life of a mutator, regardless of param renaming), * e.g. ['piua', 'f8b_', 'oi.o']. + * @private * @this Blockly.Block */ - setProcedureParameters: function(paramNames, paramIds) { + setProcedureParameters_: function(paramNames, paramIds) { // Data structures: // this.arguments = ['x', 'y'] // Existing param names. // this.quarkConnections_ {piua: null, f8b_: Blockly.Connection} // Look-up of paramIds to connections plugged into the call block. - // this.quarkArguments_ = ['piua', 'f8b_'] + // this.quarkIds_ = ['piua', 'f8b_'] // Existing param IDs. // Note that quarkConnections_ may include IDs that no longer exist, but // which might reappear if a param is reattached in the mutator. if (!paramIds) { // Reset the quarks (a mutator is about to open). this.quarkConnections_ = {}; - this.quarkArguments_ = null; + this.quarkIds_ = null; return; } if (goog.array.equals(this.arguments_, paramNames)) { // No change. - this.quarkArguments_ = paramIds; + this.quarkIds_ = paramIds; return; } if (paramIds.length != paramNames.length) { throw 'Error: paramNames and paramIds must be the same length.'; } this.setCollapsed(false); - if (!this.quarkArguments_) { + if (!this.quarkIds_) { // Initialize tracking for this block. this.quarkConnections_ = {}; if (paramNames.join('\n') == this.arguments_.join('\n')) { // No change to the parameters, allow quarkConnections_ to be // populated with the existing connections. - this.quarkArguments_ = paramIds; + this.quarkIds_ = paramIds; } else { - this.quarkArguments_ = []; + this.quarkIds_ = []; } } // Switch off rendering while the block is rebuilt. var savedRendered = this.rendered; this.rendered = false; // Update the quarkConnections_ with existing connections. - for (var i = this.arguments_.length - 1; i >= 0; i--) { + for (var i = 0; i < this.arguments_.length; i++) { var input = this.getInput('ARG' + i); if (input) { var connection = input.connection.targetConnection; - this.quarkConnections_[this.quarkArguments_[i]] = connection; + this.quarkConnections_[this.quarkIds_[i]] = connection; + if (connection && paramIds.indexOf(this.quarkIds_[i]) == -1) { + // This connection should no longer be attached to this block. + connection.disconnect(); + connection.getSourceBlock().bumpNeighbours_(); + } } } // Rebuild the block's arguments. this.arguments_ = [].concat(paramNames); this.updateShape_(); - this.quarkArguments_ = paramIds; + this.quarkIds_ = paramIds; // Reconnect any child blocks. - if (this.quarkArguments_) { + if (this.quarkIds_) { for (var i = 0; i < this.arguments_.length; i++) { - var quarkId = this.quarkArguments_[i]; + var quarkId = this.quarkIds_[i]; if (quarkId in this.quarkConnections_) { var connection = this.quarkConnections_[quarkId]; if (!Blockly.Mutator.reconnect(connection, this, 'ARG' + i)) { @@ -558,12 +575,21 @@ Blockly.Blocks['procedures_callnoreturn'] = { * @this Blockly.Block */ updateShape_: function() { - // Add new inputs. for (var i = 0; i < this.arguments_.length; i++) { - if (!this.getInput('ARG' + i)) { + var field = this.getField('ARGNAME' + i); + if (field) { + // Ensure argument name is up to date. + // The argument name field is deterministic based on the mutation, + // no need to fire a change event. + Blockly.Events.disable(); + field.setValue(this.arguments_[i]); + Blockly.Events.enable(); + } else { + // Add new input. + field = new Blockly.FieldLabel(this.arguments_[i]); var input = this.appendValueInput('ARG' + i) .setAlign(Blockly.ALIGN_RIGHT) - .appendField(this.arguments_[i]); + .appendField(field, 'ARGNAME' + i); input.init(); } } @@ -609,25 +635,16 @@ Blockly.Blocks['procedures_callnoreturn'] = { */ domToMutation: function(xmlElement) { var name = xmlElement.getAttribute('name'); - this.setFieldValue(name, 'NAME'); - this.setTooltip( - (this.outputConnection ? Blockly.Msg.PROCEDURES_CALLRETURN_TOOLTIP : - Blockly.Msg.PROCEDURES_CALLNORETURN_TOOLTIP).replace('%1', name)); - var def = Blockly.Procedures.getDefinition(name, this.workspace); - if (def && def.mutator && def.mutator.isVisible()) { - // Initialize caller with the mutator's IDs. - this.setProcedureParameters(def.arguments_, def.paramIds_); - } else { - var args = []; - for (var i = 0, childNode; childNode = xmlElement.childNodes[i]; i++) { - if (childNode.nodeName.toLowerCase() == 'arg') { - args.push(childNode.getAttribute('name')); - } + this.renameProcedure(this.getProcedureCall(), name); + var args = []; + var paramIds = []; + for (var i = 0, childNode; childNode = xmlElement.childNodes[i]; i++) { + if (childNode.nodeName.toLowerCase() == 'arg') { + args.push(childNode.getAttribute('name')); + paramIds.push(childNode.getAttribute('paramId')); } - // For the second argument (paramIds) use the arguments list as a dummy - // list. - this.setProcedureParameters(args, args); } + this.setProcedureParameters_(args, paramIds); }, /** * Notification that a variable is renaming. @@ -640,7 +657,7 @@ Blockly.Blocks['procedures_callnoreturn'] = { for (var i = 0; i < this.arguments_.length; i++) { if (Blockly.Names.equals(oldName, this.arguments_[i])) { this.arguments_[i] = newName; - this.getInput('ARG' + i).fieldRow[0].setValue(newName); + this.getField('ARGNAME' + i).setValue(newName); } } }, @@ -676,12 +693,12 @@ Blockly.Blocks['procedures_callreturn'] = { this.setHelpUrl(Blockly.Msg.PROCEDURES_CALLRETURN_HELPURL); this.arguments_ = []; this.quarkConnections_ = {}; - this.quarkArguments_ = null; + this.quarkIds_ = null; }, getProcedureCall: Blockly.Blocks['procedures_callnoreturn'].getProcedureCall, renameProcedure: Blockly.Blocks['procedures_callnoreturn'].renameProcedure, - setProcedureParameters: - Blockly.Blocks['procedures_callnoreturn'].setProcedureParameters, + setProcedureParameters_: + Blockly.Blocks['procedures_callnoreturn'].setProcedureParameters_, updateShape_: Blockly.Blocks['procedures_callnoreturn'].updateShape_, mutationToDom: Blockly.Blocks['procedures_callnoreturn'].mutationToDom, domToMutation: Blockly.Blocks['procedures_callnoreturn'].domToMutation, diff --git a/blocks/text.js b/blocks/text.js index ff485dacf..96ca1cc71 100644 --- a/blocks/text.js +++ b/blocks/text.js @@ -137,6 +137,13 @@ Blockly.Blocks['text_join'] = { itemBlock = itemBlock.nextConnection && itemBlock.nextConnection.targetBlock(); } + // Disconnect any children that don't belong. + for (var i = 0; i < this.itemCount_; i++) { + var connection = this.getInput('ADD' + i).connection.targetConnection; + if (connection && connections.indexOf(connection) == -1) { + connection.disconnect(); + } + } this.itemCount_ = connections.length; this.updateShape_(); // Reconnect any child blocks. diff --git a/core/procedures.js b/core/procedures.js index e7ba5981e..594f82e12 100644 --- a/core/procedures.js +++ b/core/procedures.js @@ -251,16 +251,28 @@ Blockly.Procedures.disposeCallers = function(name, workspace) { /** * When a procedure definition changes its parameters, find and edit all its * callers. - * @param {string} name Name of edited procedure definition. - * @param {!Blockly.Workspace} workspace The workspace to delete callers from. - * @param {!Array.} paramNames Array of new parameter names. - * @param {!Array.} paramIds Array of unique parameter IDs. + * @param {!Blockly.Block} defBlock Procedure definition block. */ -Blockly.Procedures.mutateCallers = function(name, workspace, - paramNames, paramIds) { - var callers = Blockly.Procedures.getCallers(name, workspace); - for (var i = 0; i < callers.length; i++) { - callers[i].setProcedureParameters(paramNames, paramIds); +Blockly.Procedures.mutateCallers = function(defBlock) { + var oldRecordUndo = Blockly.Events.recordUndo; + var name = defBlock.getProcedureDef()[0]; + var xmlElement = defBlock.mutationToDom(true); + var callers = Blockly.Procedures.getCallers(name, defBlock.workspace); + for (var i = 0, caller; caller = callers[i]; i++) { + var oldMutationDom = caller.mutationToDom(); + var oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom); + caller.domToMutation(xmlElement); + var newMutationDom = caller.mutationToDom(); + var newMutation = newMutationDom && Blockly.Xml.domToText(newMutationDom); + if (oldMutation != newMutation) { + // Fire a mutation on every caller block. But don't record this as an + // undo action since it is deterministically tied to the procedure's + // definition mutation. + Blockly.Events.recordUndo = false; + Blockly.Events.fire(new Blockly.Events.Change( + caller, 'mutation', null, oldMutation, newMutation)); + Blockly.Events.recordUndo = oldRecordUndo; + } } }; diff --git a/core/utils.js b/core/utils.js index 35b9e5a2d..062909415 100644 --- a/core/utils.js +++ b/core/utils.js @@ -558,30 +558,12 @@ Blockly.genUid = function() { var length = 20; var soupLength = Blockly.genUid.soup_.length; var id = []; - if (Blockly.genUid.crypto_) { - // Cryptographically strong randomness is supported. - var array = new Uint32Array(length); - Blockly.genUid.crypto_.getRandomValues(array); - for (var i = 0; i < length; i++) { - id[i] = Blockly.genUid.soup_.charAt(array[i] % soupLength); - } - } else { - // Fall back to Math.random for IE 10. - for (var i = 0; i < length; i++) { - id[i] = Blockly.genUid.soup_.charAt(Math.random() * soupLength); - } + for (var i = 0; i < length; i++) { + id[i] = Blockly.genUid.soup_.charAt(Math.random() * soupLength); } return id.join(''); }; -/** - * Determine if window.crypto or global.crypto exists. - * @this {Object} - * @type {=RandomSource} - * @private - */ -Blockly.genUid.crypto_ = this.crypto; - /** * Legal characters for the unique ID. * Should be all on a US keyboard. No XML special characters or control codes. diff --git a/core/zoom_controls.js b/core/zoom_controls.js index a4b674ac7..31a0b2126 100644 --- a/core/zoom_controls.js +++ b/core/zoom_controls.js @@ -98,17 +98,17 @@ Blockly.ZoomControls.prototype.createDom = function() { - + - + - + */