From ec78eeb39b2e82882a98747617f912fe66f79df6 Mon Sep 17 00:00:00 2001 From: RoboErikG Date: Mon, 15 Apr 2019 16:23:19 -0700 Subject: [PATCH] Propagate the visible state when blocks connect (#2003) * Propagate the visible state when blocks connect This fixes #1967. In rendered connections when connecting: - If the superior connection is hidden this hides the newly connected block. - If the superior connection isn't hidden it makes sure the block is visible. In rendered connections when disconnecting: - If the superior connection is hidden, make the disconnected block stack visible. TODO before review: - write tests. - update collapsed message * Add missing overrides * Add tests for hidden connections and fix a bug while disposing --- core/block_render_svg.js | 4 + core/connection.js | 15 ++ core/rendered_connection.js | 48 ++++- tests/mocha/connection_test.js | 322 +++++++++++++++++++++++++++++++++ tests/mocha/index.html | 7 + 5 files changed, 395 insertions(+), 1 deletion(-) create mode 100644 tests/mocha/connection_test.js diff --git a/core/block_render_svg.js b/core/block_render_svg.js index 53719b8e3..0b8998104 100644 --- a/core/block_render_svg.js +++ b/core/block_render_svg.js @@ -316,6 +316,10 @@ Blockly.BlockSvg.prototype.getHeightWidth = function() { * If true, also render block's parent, grandparent, etc. Defaults to true. */ Blockly.BlockSvg.prototype.render = function(opt_bubble) { + if (!this.workspace) { + // This block is being deleted so don't try to render it. + return; + } Blockly.Field.startCache(); this.rendered = true; diff --git a/core/connection.js b/core/connection.js index f61d0e792..f75ae9bf3 100644 --- a/core/connection.js +++ b/core/connection.js @@ -445,6 +445,10 @@ Blockly.Connection.prototype.connect = function(otherConnection) { return; } this.checkConnection_(otherConnection); + var eventGroup = Blockly.Events.getGroup(); + if (!eventGroup) { + Blockly.Events.setGroup(true); + } // Determine which block is superior (higher in the source stack). if (this.isSuperior()) { // Superior block. @@ -453,6 +457,9 @@ Blockly.Connection.prototype.connect = function(otherConnection) { // Inferior block. otherConnection.connect_(this); } + if (!eventGroup) { + Blockly.Events.setGroup(false); + } }; /** @@ -542,8 +549,16 @@ Blockly.Connection.prototype.disconnect = function() { childBlock = this.sourceBlock_; parentConnection = otherConnection; } + + var eventGroup = Blockly.Events.getGroup(); + if (!eventGroup) { + Blockly.Events.setGroup(true); + } this.disconnectInternal_(parentBlock, childBlock); parentConnection.respawnShadow_(); + if (!eventGroup) { + Blockly.Events.setGroup(false); + } }; /** diff --git a/core/rendered_connection.js b/core/rendered_connection.js index b4bebbbbb..730053088 100644 --- a/core/rendered_connection.js +++ b/core/rendered_connection.js @@ -225,6 +225,7 @@ Blockly.RenderedConnection.prototype.highlight = function() { * attached to this connection. This happens when a block is expanded. * Also unhides down-stream comments. * @return {!Array.} List of blocks to render. + * @protected */ Blockly.RenderedConnection.prototype.unhideAll = function() { this.setHidden(false); @@ -272,6 +273,7 @@ Blockly.RenderedConnection.prototype.unhighlight = function() { /** * Set whether this connections is hidden (not tracked in a database) or not. * @param {boolean} hidden True if connection is hidden. + * @protected */ Blockly.RenderedConnection.prototype.setHidden = function(hidden) { this.hidden_ = hidden; @@ -286,6 +288,7 @@ Blockly.RenderedConnection.prototype.setHidden = function(hidden) { * Hide this connection, as well as all down-stream connections on any block * attached to this connection. This happens when a block is collapsed. * Also hides down-stream comments. + * @protected */ Blockly.RenderedConnection.prototype.hideAll = function() { this.setHidden(true); @@ -324,6 +327,49 @@ Blockly.RenderedConnection.prototype.isConnectionAllowed = function(candidate, candidate); }; +/** + * Connect this connection to another connection. + * @param {!Blockly.Connection} otherConnection Connection to connect to. + * @override + */ +Blockly.RenderedConnection.prototype.connect = function(otherConnection) { + Blockly.RenderedConnection.superClass_.connect.call(this, otherConnection); + + // This is a quick check to make sure we aren't doing unecessary work. + if (this.hidden_ || otherConnection.hidden_) { + var superiorConnection = this.isSuperior() ? this : otherConnection; + if (superiorConnection.hidden_) { + superiorConnection.hideAll(); + } else { + superiorConnection.unhideAll(); + } + + var renderedBlock = superiorConnection.targetBlock(); + var display = superiorConnection.hidden_ ? 'none' : 'block'; + renderedBlock.getSvgRoot().style.display = display; + renderedBlock.rendered = !superiorConnection.hidden_; + } +}; + +/** + * Disconnect this connection. + * @override + */ +Blockly.RenderedConnection.prototype.disconnect = function() { + var superiorConnection = this.isSuperior() ? this : this.targetConnection; + if (this.targetConnection && superiorConnection.hidden_) { + superiorConnection.unhideAll(); + var renderedBlock = superiorConnection.targetBlock(); + renderedBlock.getSvgRoot().style.display = 'block'; + renderedBlock.rendered = true; + + // Set the hidden state for the connection back to true so shadow blocks + // will be hidden. + superiorConnection.setHidden(true); + } + Blockly.RenderedConnection.superClass_.disconnect.call(this); +}; + /** * Disconnect two blocks that are connected by this connection. * @param {!Blockly.Block} parentBlock The superior block. @@ -361,7 +407,7 @@ Blockly.RenderedConnection.prototype.respawnShadow_ = function() { } blockShadow.initSvg(); blockShadow.render(false); - if (parentBlock.rendered) { + if (parentBlock.rendered && !this.hidden_) { parentBlock.render(); } } diff --git a/tests/mocha/connection_test.js b/tests/mocha/connection_test.js new file mode 100644 index 000000000..b3cb1ddd9 --- /dev/null +++ b/tests/mocha/connection_test.js @@ -0,0 +1,322 @@ + + +suite('Connections', function() { + + suite('Rendered', function() { + function assertAllConnectionsHidden(block) { + assertAllConnectionsHiddenState(block, true); + } + function assertAllConnectionsVisible(block) { + assertAllConnectionsHiddenState(block, false); + } + function assertAllConnectionsHiddenState(block, hidden) { + var connections = block.getConnections_(true); + for (var i = 0; i < connections.length; i++) { + var connection = connections[i]; + if (connection.type == Blockly.PREVIOUS_STATEMENT + || connection.type == Blockly.OUTPUT_VALUE) { + // Only superior connections on inputs get hidden + continue; + } + if (block.nextConnection && connection === block.nextConnection) { + // The next connection is not hidden when collapsed + continue; + } + assertEquals('Connection ' + i + ' failed', hidden, connections[i].hidden_) + } + } + + setup(function() { + Blockly.defineBlocksWithJsonArray([{ + "type": "stack_block", + "message0": "", + "previousStatement": null, + "nextStatement": null + }, + { + "type": "row_block", + "message0": "%1", + "args0": [ + { + "type": "input_value", + "name": "INPUT" + } + ], + "output": null + }, + { + "type": "inputs_block", + "message0": "%1 %2", + "args0": [ + { + "type": "input_value", + "name": "INPUT" + }, + { + "type": "input_statement", + "name": "STATEMENT" + } + ], + "previousStatement": null, + "nextStatement": null + },]); + + var toolbox = document.getElementById('toolbox-connections'); + this.workspace = Blockly.inject('blocklyDiv', {toolbox: toolbox}); + }); + + teardown(function() { + delete Blockly.Blocks['stack_block']; + delete Blockly.Blocks['row_block']; + delete Blockly.Blocks['inputs_block']; + + this.workspace.dispose(); + }); + + suite('Row collapsing', function() { + setup(function() { + var blockA = this.workspace.newBlock('row_block'); + var blockB = this.workspace.newBlock('row_block'); + var blockC = this.workspace.newBlock('row_block'); + + blockA.inputList[0].connection.connect(blockB.outputConnection); + blockA.setCollapsed(true); + + assertEquals(blockA, blockB.getParent()); + assertNull(blockC.getParent()) + assertTrue(blockA.isCollapsed()); + assertAllConnectionsHidden(blockA); + assertAllConnectionsHidden(blockB); + assertAllConnectionsVisible(blockC); + + this.blocks = { + A: blockA, + B: blockB, + C: blockC + }; + }); + + test('Add to end', function() { + var blocks = this.blocks; + blocks.B.inputList[0].connection.connect(blocks.C.outputConnection); + assertAllConnectionsHidden(blocks.C); + }); + + test('Add to end w/inferior', function() { + var blocks = this.blocks; + blocks.C.outputConnection.connect(blocks.B.inputList[0].connection); + assertAllConnectionsHidden(blocks.C); + }); + + test('Add to middle', function() { + var blocks = this.blocks; + blocks.A.inputList[0].connection.connect(blocks.C.outputConnection); + assertAllConnectionsHidden(blocks.C); + }); + + test('Add to middle w/inferior', function() { + var blocks = this.blocks; + blocks.C.outputConnection.connect(blocks.A.inputList[0].connection); + assertAllConnectionsHidden(blocks.C); + }); + + test('Remove simple', function() { + var blocks = this.blocks; + blocks.B.unplug(); + assertAllConnectionsVisible(blocks.B); + }); + + test('Remove middle', function() { + var blocks = this.blocks; + blocks.B.inputList[0].connection.connect(blocks.C.outputConnection); + blocks.B.unplug(false); + assertAllConnectionsVisible(blocks.B); + assertAllConnectionsVisible(blocks.C); + }); + + test('Remove middle healing', function() { + var blocks = this.blocks; + blocks.B.inputList[0].connection.connect(blocks.C.outputConnection); + blocks.B.unplug(true); + assertAllConnectionsVisible(blocks.B); + assertAllConnectionsHidden(blocks.C); + }); + + test('Add before', function() { + var blocks = this.blocks; + blocks.C.inputList[0].connection.connect(blocks.A.outputConnection); + // Connecting a collapsed block to another block doesn't change any hidden state + assertAllConnectionsHidden(blocks.A); + assertAllConnectionsVisible(blocks.C); + }); + + test('Remove front', function() { + var blocks = this.blocks; + blocks.B.inputList[0].connection.connect(blocks.C.outputConnection); + blocks.A.inputList[0].connection.disconnect(); + assertTrue(blocks.A.isCollapsed()); + assertAllConnectionsHidden(blocks.A); + assertAllConnectionsVisible(blocks.B); + assertAllConnectionsVisible(blocks.C); + }); + + test('Uncollapse', function() { + var blocks = this.blocks; + blocks.B.inputList[0].connection.connect(blocks.C.outputConnection); + blocks.A.setCollapsed(false); + assertFalse(blocks.A.isCollapsed()); + assertAllConnectionsVisible(blocks.A); + assertAllConnectionsVisible(blocks.B); + assertAllConnectionsVisible(blocks.C); + }); + }); + suite('Statement collapsing', function() { + setup(function() { + var blockA = this.workspace.newBlock('inputs_block'); + var blockB = this.workspace.newBlock('inputs_block'); + var blockC = this.workspace.newBlock('inputs_block'); + + blockA.getInput('STATEMENT').connection.connect(blockB.previousConnection); + blockA.setCollapsed(true); + + assertEquals(blockA, blockB.getParent()); + assertNull(blockC.getParent()) + assertTrue(blockA.isCollapsed()); + assertAllConnectionsHidden(blockA); + assertAllConnectionsHidden(blockB); + assertAllConnectionsVisible(blockC); + + this.blocks = { + A: blockA, + B: blockB, + C: blockC + }; + }); + + test('Add to statement', function() { + var blocks = this.blocks; + blocks.B.getInput('STATEMENT').connection.connect(blocks.C.previousConnection); + assertAllConnectionsHidden(blocks.C); + }); + + test('Insert in statement', function() { + var blocks = this.blocks; + blocks.A.getInput('STATEMENT').connection.connect(blocks.C.previousConnection); + assertAllConnectionsHidden(blocks.C); + }); + + test('Add to hidden next', function() { + var blocks = this.blocks; + blocks.B.nextConnection.connect(blocks.C.previousConnection); + assertAllConnectionsHidden(blocks.C); + }); + + test('Remove simple', function() { + var blocks = this.blocks; + blocks.B.unplug(); + assertAllConnectionsVisible(blocks.B); + }); + + test('Remove middle', function() { + var blocks = this.blocks; + blocks.B.nextConnection.connect(blocks.C.previousConnection); + blocks.B.unplug(false); + assertAllConnectionsVisible(blocks.B); + assertAllConnectionsVisible(blocks.C); + }); + + test('Remove middle healing', function() { + var blocks = this.blocks; + blocks.B.nextConnection.connect(blocks.C.previousConnection); + blocks.B.unplug(true); + assertAllConnectionsVisible(blocks.B); + assertAllConnectionsHidden(blocks.C); + }); + + test('Add before', function() { + var blocks = this.blocks; + blocks.C.getInput('STATEMENT').connection.connect(blocks.A.previousConnection); + assertAllConnectionsHidden(blocks.A); + assertAllConnectionsHidden(blocks.B); + assertAllConnectionsVisible(blocks.C); + }); + + test('Remove front', function() { + var blocks = this.blocks; + blocks.B.nextConnection.connect(blocks.C.previousConnection); + blocks.A.getInput('STATEMENT').connection.disconnect(); + assertTrue(blocks.A.isCollapsed()); + assertAllConnectionsHidden(blocks.A); + assertAllConnectionsVisible(blocks.B); + assertAllConnectionsVisible(blocks.C); + }); + + test('Uncollapse', function() { + var blocks = this.blocks; + blocks.B.nextConnection.connect(blocks.C.previousConnection); + blocks.A.setCollapsed(false); + assertFalse(blocks.A.isCollapsed()); + assertAllConnectionsVisible(blocks.A); + assertAllConnectionsVisible(blocks.B); + assertAllConnectionsVisible(blocks.C); + }); + }); + + suite('Collapsing with shadows', function() { + setup(function() { + var blockA = this.workspace.newBlock('inputs_block'); + var blockB = this.workspace.newBlock('inputs_block'); + var blockC = this.workspace.newBlock('inputs_block'); + var blockD = this.workspace.newBlock('row_block'); + + blockB.setShadow(true); + var shadowStatement = Blockly.Xml.blockToDom(blockB, true /*noid*/); + blockB.setShadow(false); + + blockD.setShadow(true); + var shadowValue = Blockly.Xml.blockToDom(blockD, true /*noid*/); + blockD.setShadow(false); + + var connection = blockA.getInput('STATEMENT').connection; + connection.setShadowDom(shadowStatement); + connection.connect(blockB.previousConnection); + connection = blockA.getInput('INPUT').connection; + connection.setShadowDom(shadowValue); + connection.connect(blockD.outputConnection); + blockA.setCollapsed(true); + + assertEquals(blockA, blockB.getParent()); + assertNull(blockC.getParent()) + assertTrue(blockA.isCollapsed()); + assertAllConnectionsHidden(blockA); + assertAllConnectionsHidden(blockB); + assertAllConnectionsVisible(blockC); + + this.blocks = { + A: blockA, + B: blockB, + C: blockC, + D: blockD + }; + }); + + test('Reveal shadow statement', function() { + var blocks = this.blocks; + var connection = blocks.A.getInput('STATEMENT').connection; + connection.disconnect(); + var shadowBlock = connection.targetBlock(); + assertTrue(shadowBlock.isShadow()); + assertAllConnectionsHidden(shadowBlock); + }) + + test('Reveal shadow value', function() { + var blocks = this.blocks; + var connection = blocks.A.getInput('INPUT').connection; + connection.disconnect(); + var shadowBlock = connection.targetBlock(); + assertTrue(shadowBlock.isShadow()); + assertAllConnectionsHidden(shadowBlock); + }) + }); + }); +}); diff --git a/tests/mocha/index.html b/tests/mocha/index.html index 6dcef5ce6..1a1f601a8 100644 --- a/tests/mocha/index.html +++ b/tests/mocha/index.html @@ -21,9 +21,16 @@ + +
+ +