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
This commit is contained in:
RoboErikG
2019-04-15 16:23:19 -07:00
committed by GitHub
parent f88c704d69
commit ec78eeb39b
5 changed files with 395 additions and 1 deletions

View File

@@ -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;

View File

@@ -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);
}
};
/**

View File

@@ -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.<!Blockly.Block>} 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();
}
}

View File

@@ -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);
})
});
});
});

View File

@@ -21,9 +21,16 @@
<script src="test_helpers.js"></script>
<script src="block_test.js"></script>
<script src="event_test.js"></script>
<script src="connection_test.js"></script>
<script src="field_variable_test.js"></script>
<script src="utils_test.js"></script>
<div id="blocklyDiv"></div>
<xml id="toolbox-connections" style="display: none">
<block type="stack_block"></block>
<block type="row_block"></block>
</xml>
<script>
mocha.run();
</script>