Skip to content

Reconnect blocks after a ghost disappears. #127

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions core/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,28 @@ Blockly.Block.prototype.setConnectionsHidden = function(hidden) {
}
};

/**
* Find the connection on this block that corresponds to the given connection
* on the other block.
* Used to match connections between a block and its ghost.
* @param {!Blockly.Block} otherBlock The other block to match against.
* @param {!Blockly.Connection} conn The other connection to match.
* @return {Blockly.Connection} the matching connection on this block, or null.
*/
Blockly.Block.prototype.getMatchingConnection = function(otherBlock, conn) {
var connections = this.getConnections_(true);
var otherConnections = otherBlock.getConnections_(true);
if (connections.length != otherConnections.length) {
throw "Connection lists did not match in length.";
}
for (var i = 0; i < otherConnections.length; i++) {
if (otherConnections[i] == conn) {
return connections[i];
}
}
return null;
};

/**
* Set the URL of this block's help page.
* @param {string|Function} url URL string for block help, or function that
Expand Down Expand Up @@ -708,12 +730,14 @@ Blockly.Block.prototype.setColour = function(colour, colourSecondary, colourTert
if (colourSecondary !== undefined) {
this.colourSecondary_ = this.makeColour_(colourSecondary);
} else {
this.colourSecondary_ = goog.color.darken(colour, 0.1);
this.colourSecondary_ = goog.color.darken(goog.color.hexToRgb(this.colour_),
0.1);
}
if (colourTertiary !== undefined) {
this.colourTertiary_ = this.makeColour_(colourTertiary);
} else {
this.colourTertiary_ = goog.color.darken(colour, 0.2);
this.colourTertiary_ = goog.color.darken(goog.color.hexToRgb(this.colour_),
0.2);
}
if (this.rendered) {
this.updateColour();
Expand Down
6 changes: 4 additions & 2 deletions core/block_render_svg_horizontal.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,6 @@ Blockly.BlockSvg.prototype.renderDraw_ = function(metrics) {
// Fetch the block's coordinates on the surface for use in anchoring
// the connections.
var connectionsXY = this.getRelativeToSurfaceXY();

// Assemble the block's path.
var steps = [];

Expand All @@ -377,7 +376,7 @@ Blockly.BlockSvg.prototype.renderDraw_ = function(metrics) {
}

// Position icon
if (!this.isGhost() && metrics.icon) {
if (metrics.icon) {
var icon = metrics.icon.getSvgRoot();
var iconSize = metrics.icon.getSize();
// Icon's position is calculated relative to the "end" edge of the block.
Expand All @@ -394,6 +393,9 @@ Blockly.BlockSvg.prototype.renderDraw_ = function(metrics) {
iconX = -metrics.width + Blockly.BlockSvg.SEP_SPACE_X / 1.5;
}
}
if (this.isGhost()) {
icon.setAttribute('display', 'none');
}
icon.setAttribute('transform',
'translate(' + iconX + ',' + iconY + ') ' + iconScale);
}
Expand Down
159 changes: 119 additions & 40 deletions core/block_svg.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,9 @@ Blockly.BlockSvg.terminateDrag_ = function() {
if (selected) {
if (selected.ghostBlock_) {
Blockly.Events.disable();
selected.ghostBlock_.unplug(true /* healStack */);
if (Blockly.localGhostConnection_) {
selected.disconnectGhost();
}
selected.ghostBlock_.dispose();
selected.ghostBlock_ = null;
Blockly.Events.enable();
Expand Down Expand Up @@ -594,7 +596,6 @@ Blockly.BlockSvg.prototype.onMouseUp_ = function(e) {
Blockly.fireUiEvent(window, 'resize');
}
if (Blockly.highlightedConnection_) {
Blockly.highlightedConnection_.unhighlight();
Blockly.highlightedConnection_ = null;
}
Blockly.Css.setCursor(Blockly.Css.Cursor.OPEN);
Expand Down Expand Up @@ -814,50 +815,128 @@ Blockly.BlockSvg.prototype.onMouseMove_ = function(e) {
}
}

// Remove connection highlighting if needed.
if (Blockly.highlightedConnection_ &&
Blockly.highlightedConnection_ != closestConnection) {
if (this.ghostBlock_) {
// Don't fire events for ghost block creation or movement.
Blockly.Events.disable();
this.ghostBlock_.unplug(true /* healStack */);
this.ghostBlock_.dispose();
this.ghostBlock_ = null;
Blockly.Events.enable();
}
Blockly.highlightedConnection_.unhighlight();
Blockly.highlightedConnection_ = null;
Blockly.localConnection_ = null;
this.updatePreviews(closestConnection, localConnection, radiusConnection,
e, newXY.x - this.dragStartXY_.x, newXY.y - this.dragStartXY_.y);
}
// This event has been handled. No need to bubble up to the document.
e.stopPropagation();
};

/**
* Preview the results of the drag if the mouse is released immediately.
* @param {Blockly.Connection} closestConnection The closest connection found
* during the search
* @param {Blockly.Connection} localConnection The connection on the moving
* block.
* @param {number} radiusConnection The distance between closestConnection and
* localConnection.
* @param {!Event} e Mouse move event.
* @param {number} dx The x distance the block has moved onscreen up to this
* point in the drag.
* @param {number} dy The y distance the block has moved onscreen up to this
* point in the drag.
*/
Blockly.BlockSvg.prototype.updatePreviews = function(closestConnection,
localConnection, radiusConnection, e, dx, dy) {
// Don't fire events for ghost block creation or movement.
Blockly.Events.disable();
// Remove a ghost if needed. For Scratch-Blockly we are using ghosts instead
// of highlighting the connection; for compatibility with Web Blockly the
// name "highlightedConnection" will still be used.
if (Blockly.highlightedConnection_ &&
Blockly.highlightedConnection_ != closestConnection) {
if (this.ghostBlock_ && Blockly.localGhostConnection_) {
this.disconnectGhost();
}
// Add connection highlighting if needed.
if (closestConnection &&
closestConnection != Blockly.highlightedConnection_) {
closestConnection.highlight();
Blockly.highlightedConnection_ = closestConnection;
Blockly.localConnection_ = localConnection;
Blockly.Events.disable();
if (!this.ghostBlock_){
this.ghostBlock_ = this.workspace.newBlock(this.type);
this.ghostBlock_.setGhost(true);
this.ghostBlock_.moveConnections_(radiusConnection);
Blockly.highlightedConnection_ = null;
Blockly.localConnection_ = null;
}

// Add a ghost if needed.
if (closestConnection &&
closestConnection != Blockly.highlightedConnection_ &&
!closestConnection.sourceBlock_.isGhost()) {
Blockly.highlightedConnection_ = closestConnection;
Blockly.localConnection_ = localConnection;
if (!this.ghostBlock_){
this.ghostBlock_ = this.workspace.newBlock(this.type);
this.ghostBlock_.setGhost(true);
this.ghostBlock_.initSvg();
}

var ghostBlock = this.ghostBlock_;
var localGhostConnection = ghostBlock.getMatchingConnection(this,
localConnection);
if (localGhostConnection != Blockly.localGhostConnection_) {
ghostBlock.getSvgRoot().setAttribute('visibility', 'visible');
ghostBlock.rendered = true;
// Move the preview to the correct location before the existing block.
if (localGhostConnection.type == Blockly.NEXT_STATEMENT) {
var relativeXy = this.getRelativeToSurfaceXY();
var connectionOffsetX = (localConnection.x_ - (relativeXy.x - dx));
var connectionOffsetY = (localConnection.y_ - (relativeXy.y - dy));
var newX = closestConnection.x_ - connectionOffsetX;
var newY = closestConnection.y_ - connectionOffsetY;
var ghostPosition = ghostBlock.getRelativeToSurfaceXY();
ghostBlock.moveBy(newX - ghostPosition.x, newY - ghostPosition.y, true);

}
if (Blockly.localConnection_ == this.previousConnection) {
// Setting the block to rendered will actually change the connection
// behaviour :/
this.ghostBlock_.rendered = true;
this.ghostBlock_.previousConnection.connect(closestConnection);
if (localGhostConnection.type == Blockly.PREVIOUS_STATEMENT &&
!ghostBlock.nextConnection) {
Blockly.bumpedConnection_ = closestConnection.targetConnection;
}
this.ghostBlock_.render(true);
Blockly.Events.enable();
// Renders ghost.
localGhostConnection.connect(closestConnection);
// Render dragging block so it appears on top.
this.workspace.getCanvas().appendChild(this.getSvgRoot());
Blockly.localGhostConnection_ = localGhostConnection;
}
// Provide visual indication of whether the block will be deleted if
// dropped here.
if (this.isDeletable()) {
this.workspace.isDeleteArea(e);
}
// Reenable events.
Blockly.Events.enable();

// Provide visual indication of whether the block will be deleted if
// dropped here.
if (this.isDeletable()) {
this.workspace.isDeleteArea(e);
}
};

/**
* Disconnect the current ghost block from the stack, and heal the stack to its
* previous state.
*/
Blockly.BlockSvg.prototype.disconnectGhost = function() {
// The ghost block is the first block in a stack, either because it doesn't
// have a previous connection or because the previous connection is not
// connection. Unplug won't do anything in that case. Instead, unplug the
// following block.
if (Blockly.localGhostConnection_ == this.ghostBlock_.nextConnection &&
(!this.ghostBlock_.previousConnection ||
!this.ghostBlock_.previousConnection.targetConnection)) {
Blockly.localGhostConnection_.targetBlock().unplug(false);
}
// Inside of a C-block, first statement connection.
else if (Blockly.localGhostConnection_.type == Blockly.NEXT_STATEMENT &&
Blockly.localGhostConnection_ != this.ghostBlock_.nextConnection) {
var innerConnection = Blockly.localGhostConnection_.targetConnection;
innerConnection.sourceBlock_.unplug(false);
var previousBlockNextConnection =
this.ghostBlock_.previousConnection.targetConnection;
this.ghostBlock_.unplug(true);
if (previousBlockNextConnection) {
previousBlockNextConnection.connect(innerConnection);
}
}
// This event has been handled. No need to bubble up to the document.
e.stopPropagation();
else {
this.ghostBlock_.unplug(true /* healStack */);
}

if (Blockly.localGhostConnection_.targetConnection) {
throw 'LocalGhostConnection still connected at the end of disconnectGhost';
}
Blockly.localGhostConnection_ = null;
this.ghostBlock_.getSvgRoot().setAttribute('visibility', 'hidden');
};

/**
Expand Down
18 changes: 17 additions & 1 deletion core/blockly.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,22 @@ Blockly.highlightedConnection_ = null;
*/
Blockly.localConnection_ = null;

/**
* Connection on ghost block that matches Blockly.localConnecxtion_ on the
* dragged block.
* @type {Blockly.Connection}
* @private
*/
Blockly.localGhostConnection_ = null;

/**
* Connection that was bumped out of the way by a ghost block, and may need
* to be put back as the drag continues.
* @type {Blockly.Connection}
* @private
*/
Blockly.bumpedConnection_ = null;

/**
* Number of pixels the mouse must move before a drag starts.
*/
Expand All @@ -199,7 +215,7 @@ Blockly.DRAG_RADIUS = 5;
/**
* Maximum misalignment between connections for them to snap together.
*/
Blockly.SNAP_RADIUS = 20;
Blockly.SNAP_RADIUS = 50;

/**
* Delay in ms between trigger and bumping unconnected block out of alignment.
Expand Down
47 changes: 26 additions & 21 deletions core/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,14 @@ Blockly.Connection.prototype.dispose = function() {
this.dbOpposite_ = null;
};

/**
* @return true if the connection is not connected or is connected to a ghost
* block, false otherwise.
*/
Blockly.Connection.prototype.isConnectedToNonGhost = function() {
return this.targetConnection && !this.targetBlock().isGhost();
};

/**
* Does the connection belong to a superior block (higher in the source stack)?
* @return {boolean} True if connection faces down or right.
Expand Down Expand Up @@ -404,6 +412,21 @@ Blockly.Connection.prototype.isConnectionAllowed = function(candidate,
this.sourceBlock_.getFirstStatementConnection();

if (candidate.type == Blockly.PREVIOUS_STATEMENT) {
if (!firstStatementConnection || this != firstStatementConnection) {
if (this.targetConnection) {
return false;
}
if (candidate.targetConnection) {
// If the other side of this connection is the active ghost connection,
// we've obviously already decided that this is a good connection.
if (candidate.targetConnection == Blockly.localGhostConnection_) {
return true;
} else {
return false;
}
}
}

// Scratch-specific behaviour:
// If this is a c-shaped block, statement blocks cannot be connected
// anywhere other than inside the first statement input.
Expand All @@ -416,32 +439,15 @@ Blockly.Connection.prototype.isConnectionAllowed = function(candidate,
}
}
// The only other eligible connection of this type is the next connection
// when the candidate is not already connection (connecting at the start
// when the candidate is not already connected (connecting at the start
// of the stack).
else if (this == this.sourceBlock_.nextConnection &&
candidate.targetConnection) {
return false;
}
} else {
// Otherwise, don't offer to connect the bottom of a statement block to
// the top of a block that's already connected. And don't connect the
// bottom of a statement block that's already connected.
if (this.targetConnection || candidate.targetConnection) {
candidate.isConnectedToNonGhost()) {
return false;
}
}
}

// Don't offer to connect the bottom of a statement block to one that's
// already connected.
// But the first statement input on c-block can connect to the start of a
// block in a stack.
if (candidate.type == Blockly.PREVIOUS_STATEMENT &&
this != this.sourceBlock_.getFirstStatementConnection() &&
(this.targetConnection || candidate.targetConnection)) {
return false;
}

// Offering to connect the left (male) of a value block to an already
// connected value pair is ok, we'll splice it in.
// However, don't offer to splice into an unmovable block.
Expand All @@ -455,8 +461,7 @@ Blockly.Connection.prototype.isConnectionAllowed = function(candidate,
// Don't let a block with no next connection bump other blocks out of the
// stack.
if (this.type == Blockly.PREVIOUS_STATEMENT &&
candidate.targetConnection &&
!this.sourceBlock_.nextConnection) {
candidate.isConnectedToNonGhost() && !this.sourceBlock_.nextConnection) {
return false;
}

Expand Down
3 changes: 2 additions & 1 deletion tests/jsunit/connection_db_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,8 @@ function helper_makeSourceBlock(sharedWorkspace) {
movable_: true,
isMovable: function() { return true; },
isShadow: function() { return false; },
isGhost: function() { return false; }
isGhost: function() { return false; },
getFirstStatementConnection: function() { return null; }
};
}

Expand Down