From 72bb08a4ec448f7a0210c2b03c88a8542bc0b822 Mon Sep 17 00:00:00 2001 From: Neil Fraser Date: Mon, 14 Mar 2016 23:14:05 -0700 Subject: [PATCH] Add undo/redo. Some bugs in undoing function argument changes. --- blocks/lists.js | 13 +------------ blocks/logic.js | 12 +++--------- blocks/text.js | 28 ++++++++++++---------------- core/events.js | 26 ++++++++++++++++++++++---- core/mutator.js | 32 ++++++++++++++++++++++++++++++++ core/workspace.js | 19 ++++++++++++------- 6 files changed, 82 insertions(+), 48 deletions(-) diff --git a/blocks/lists.js b/blocks/lists.js index 2e2da5a55..5dcbadb75 100644 --- a/blocks/lists.js +++ b/blocks/lists.js @@ -123,18 +123,7 @@ Blockly.Blocks['lists_create_with'] = { this.updateShape_(); // Reconnect any child blocks. for (var i = 0; i < this.itemCount_; i++) { - var connectionChild = connections[i]; - if (connectionChild) { - var parent = connectionChild.targetBlock(); - var connectionParent = this.getInput('ADD' + i).connection; - if (connectionParent.targetConnection != connectionChild && - (!parent || parent == this)) { - if (connectionParent.targetConnection) { - connectionParent.disconnect(); - } - connectionParent.connect(connectionChild); - } - } + Blockly.Mutator.reconnect(connections[i], this, 'ADD' + i); } }, /** diff --git a/blocks/logic.js b/blocks/logic.js index 2704bc8b0..e07287edb 100644 --- a/blocks/logic.js +++ b/blocks/logic.js @@ -152,16 +152,10 @@ Blockly.Blocks['controls_if'] = { this.updateShape_(); // Reconnect any child blocks. for (var i = 1; i <= this.elseifCount_; i++) { - if (valueConnections[i]) { - this.getInput('IF' + i).connection.connect(valueConnections[i]); - } - if (statementConnections[i]) { - this.getInput('DO' + i).connection.connect(statementConnections[i]); - } - } - if (elseStatementConnection) { - this.getInput('ELSE').connection.connect(elseStatementConnection); + Blockly.Mutator.reconnect(valueConnections[i], this, 'IF' + i); + Blockly.Mutator.reconnect(statementConnections[i], this, 'DO' + i); } + Blockly.Mutator.reconnect(elseStatementConnection, this, 'ELSE'); }, /** * Store pointers to any connected child blocks. diff --git a/blocks/text.js b/blocks/text.js index ddf619735..ff485dacf 100644 --- a/blocks/text.js +++ b/blocks/text.js @@ -141,9 +141,7 @@ Blockly.Blocks['text_join'] = { this.updateShape_(); // Reconnect any child blocks. for (var i = 0; i < this.itemCount_; i++) { - if (connections[i]) { - this.getInput('ADD' + i).connection.connect(connections[i]); - } + Blockly.Mutator.reconnect(connections[i], this, 'ADD' + i); } }, /** @@ -168,29 +166,27 @@ Blockly.Blocks['text_join'] = { * @this Blockly.Block */ updateShape_: function() { - // Delete everything. - if (this.getInput('EMPTY')) { + if (this.itemCount_ && this.getInput('EMPTY')) { this.removeInput('EMPTY'); - } else { - var i = 0; - while (this.getInput('ADD' + i)) { - this.removeInput('ADD' + i); - i++; - } - } - // Rebuild block. - if (this.itemCount_ == 0) { + } else if (!this.itemCount_ && !this.getInput('EMPTY')) { this.appendDummyInput('EMPTY') .appendField(this.newQuote_(true)) .appendField(this.newQuote_(false)); - } else { - for (var i = 0; i < this.itemCount_; i++) { + } + // Add new inputs. + for (var i = 0; i < this.itemCount_; i++) { + if (!this.getInput('ADD' + i)) { var input = this.appendValueInput('ADD' + i); if (i == 0) { input.appendField(Blockly.Msg.TEXT_JOIN_TITLE_CREATEWITH); } } } + // Remove deleted inputs. + while (this.getInput('ADD' + i)) { + this.removeInput('ADD' + i); + i++; + } }, newQuote_: Blockly.Blocks['text'].newQuote_ }; diff --git a/core/events.js b/core/events.js index 9cf1c9217..8b741d8a1 100644 --- a/core/events.js +++ b/core/events.js @@ -97,7 +97,7 @@ Blockly.Events.fire = function(event) { * @private */ Blockly.Events.fireNow_ = function() { - var queue = Blockly.Events.filter(Blockly.Events.FIRE_QUEUE_); + var queue = Blockly.Events.filter(Blockly.Events.FIRE_QUEUE_, true); Blockly.Events.FIRE_QUEUE_.length = 0; for (var i = 0, event; event = queue[i]; i++) { var workspace = Blockly.Workspace.getById(event.workspaceId); @@ -110,10 +110,15 @@ Blockly.Events.fireNow_ = function() { /** * Filter the queued events and merge duplicates. * @param {!Array.} queueIn Array of events. + * @param {boolean} forward True if forward (redo), false if backward (undo). * @return {!Array.} Array of filtered events. */ -Blockly.Events.filter = function(queueIn) { +Blockly.Events.filter = function(queueIn, forward) { var queue = goog.array.clone(queueIn); + if (!forward) { + // Undo is merged in reverse order. + queue.reverse(); + } // Merge duplicates. O(n^2), but n should be very small. for (var i = 0, event1; event1 = queue[i]; i++) { for (var j = i + 1, event2; event2 = queue[j]; j++) { @@ -144,6 +149,10 @@ Blockly.Events.filter = function(queueIn) { queue.splice(i, 1); } } + if (!forward) { + // Restore undo order. + queue.reverse(); + } // Move mutation events to the top of the queue. // Intentionally skip first event. for (var i = 1, event; event = queue[i]; i++) { @@ -251,6 +260,8 @@ Blockly.Events.Create.prototype.run = function(forward) { var block = Blockly.Block.getById(this.blockId); if (block) { block.dispose(false, true); + } else { + console.warn("Can't delete non-existant block: " + this.blockId); } } }; @@ -285,6 +296,8 @@ Blockly.Events.Delete.prototype.run = function(forward) { var block = Blockly.Block.getById(this.blockId); if (block) { block.dispose(false, true); + } else { + console.warn("Can't delete non-existant block: " + this.blockId); } } else { var workspace = Blockly.Workspace.getById(this.workspaceId); @@ -334,6 +347,7 @@ Blockly.Events.Change.prototype.isNull = function() { Blockly.Events.Change.prototype.run = function(forward) { var block = Blockly.Block.getById(this.blockId); if (!block) { + console.warn("Can't change non-existant block: " + this.blockId); return; } var value = forward ? this.newValue : this.oldValue; @@ -375,6 +389,8 @@ Blockly.Events.Change.prototype.run = function(forward) { Blockly.Events.fire(new Blockly.Events.Change( block, 'mutation', null, oldMutation, value)); break; + default: + console.warn("Unknown change type: " + this.element); } }; @@ -448,6 +464,7 @@ Blockly.Events.Move.prototype.isNull = function() { Blockly.Events.Move.prototype.run = function(forward) { var block = Blockly.Block.getById(this.blockId); if (!block) { + console.warn("Can't move non-existant block: " + this.blockId); return; } var parentId = forward ? this.newParentId : this.oldParentId; @@ -457,6 +474,7 @@ Blockly.Events.Move.prototype.run = function(forward) { if (parentId) { parentBlock = Blockly.Block.getById(parentId); if (!parentBlock) { + console.warn("Can't connect to non-existant block: " + parentId); return; } } @@ -473,14 +491,14 @@ Blockly.Events.Move.prototype.run = function(forward) { var input = parentBlock.getInput(inputName); if (input) { parentConnection = input.connection; - } else { - console.warn("Can't connect to non-existant input: " + inputName); } } else if (blockConnection.type == Blockly.PREVIOUS_STATEMENT) { parentConnection = parentBlock.nextConnection; } if (parentConnection) { blockConnection.connect(parentConnection); + } else { + console.warn("Can't connect to non-existant input: " + inputName); } } }; diff --git a/core/mutator.js b/core/mutator.js index b3e9d90e8..8488ee0fb 100644 --- a/core/mutator.js +++ b/core/mutator.js @@ -340,3 +340,35 @@ Blockly.Mutator.prototype.dispose = function() { this.block_.mutator = null; Blockly.Icon.prototype.dispose.call(this); }; + +/** + * Reconnect an block to a mutated input. + * @param {Blockly.Connection} connectionChild Connection on child block. + * @param {!Blockly.Block} block Parent block. + * @param {string} inputName Name of input on parent block. + */ +Blockly.Mutator.reconnect = function(connectionChild, block, inputName) { + if (!connectionChild) { + return; + } + var connectionParent = block.getInput(inputName).connection; + var currentParent = connectionChild.targetBlock(); + if ((!currentParent || currentParent == block) && + connectionParent.targetConnection != connectionChild) { + if (connectionParent.targetConnection) { + // There's already something connected here. Get rid of it. + connectionParent.disconnect(); + } + connectionParent.connect(connectionChild); + } +}; + +// Export symbols that would otherwise be renamed by Closure compiler. +if (!goog.global['Blockly']) { + goog.global['Blockly'] = {}; +} +if (!goog.global['Blockly']['Mutator']) { + goog.global['Blockly']['Mutator'] = {}; +} +goog.global['Blockly']['Mutator']['reconnect'] = Blockly.Mutator.reconnect; + diff --git a/core/workspace.js b/core/workspace.js index 49f3fac88..7cc7886a4 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -209,22 +209,27 @@ Blockly.Workspace.prototype.remainingCapacity = function() { * @param {boolean} redo False if undo, true if redo. */ Blockly.Workspace.prototype.undo = function(redo) { - var sourceStack = redo ? this.redoStack_ : this.undoStack_; - var event = sourceStack.pop(); + var inputStack = redo ? this.redoStack_ : this.undoStack_; + var outputStack = redo ? this.undoStack_ : this.redoStack_; + var event = inputStack.pop(); if (!event) { return; } var events = [event]; // Do another undo/redo if the next one is of the same group. - while (sourceStack.length && event.group && - event.group == sourceStack[sourceStack.length - 1].group) { - events.push(sourceStack.pop()); + while (inputStack.length && event.group && + event.group == inputStack[inputStack.length - 1].group) { + events.push(inputStack.pop()); } - events = Blockly.Events.filter(events); + // Push these popped events on the opposite stack. + for (var i = 0, event; event = events[i]; i++) { + outputStack.push(event); + } + events = Blockly.Events.filter(events, redo); Blockly.Events.recordUndo = false; for (var i = 0, event; event = events[i]; i++) { + console.log(event); event.run(redo); - (redo ? this.undoStack_ : this.redoStack_).push(event); } Blockly.Events.recordUndo = true; };