Skip to content

Commit 442e35c

Browse files
Merge pull request #127 from rachel-fenichel/feature/complicated_ghosts
Reconnect blocks after a ghost disappears.
2 parents 14715ff + 8c44d5f commit 442e35c

File tree

6 files changed

+194
-67
lines changed

6 files changed

+194
-67
lines changed

core/block.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,28 @@ Blockly.Block.prototype.setConnectionsHidden = function(hidden) {
637637
}
638638
};
639639

640+
/**
641+
* Find the connection on this block that corresponds to the given connection
642+
* on the other block.
643+
* Used to match connections between a block and its ghost.
644+
* @param {!Blockly.Block} otherBlock The other block to match against.
645+
* @param {!Blockly.Connection} conn The other connection to match.
646+
* @return {Blockly.Connection} the matching connection on this block, or null.
647+
*/
648+
Blockly.Block.prototype.getMatchingConnection = function(otherBlock, conn) {
649+
var connections = this.getConnections_(true);
650+
var otherConnections = otherBlock.getConnections_(true);
651+
if (connections.length != otherConnections.length) {
652+
throw "Connection lists did not match in length.";
653+
}
654+
for (var i = 0; i < otherConnections.length; i++) {
655+
if (otherConnections[i] == conn) {
656+
return connections[i];
657+
}
658+
}
659+
return null;
660+
};
661+
640662
/**
641663
* Set the URL of this block's help page.
642664
* @param {string|Function} url URL string for block help, or function that
@@ -708,12 +730,14 @@ Blockly.Block.prototype.setColour = function(colour, colourSecondary, colourTert
708730
if (colourSecondary !== undefined) {
709731
this.colourSecondary_ = this.makeColour_(colourSecondary);
710732
} else {
711-
this.colourSecondary_ = goog.color.darken(colour, 0.1);
733+
this.colourSecondary_ = goog.color.darken(goog.color.hexToRgb(this.colour_),
734+
0.1);
712735
}
713736
if (colourTertiary !== undefined) {
714737
this.colourTertiary_ = this.makeColour_(colourTertiary);
715738
} else {
716-
this.colourTertiary_ = goog.color.darken(colour, 0.2);
739+
this.colourTertiary_ = goog.color.darken(goog.color.hexToRgb(this.colour_),
740+
0.2);
717741
}
718742
if (this.rendered) {
719743
this.updateColour();

core/block_render_svg_horizontal.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,6 @@ Blockly.BlockSvg.prototype.renderDraw_ = function(metrics) {
358358
// Fetch the block's coordinates on the surface for use in anchoring
359359
// the connections.
360360
var connectionsXY = this.getRelativeToSurfaceXY();
361-
362361
// Assemble the block's path.
363362
var steps = [];
364363

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

379378
// Position icon
380-
if (!this.isGhost() && metrics.icon) {
379+
if (metrics.icon) {
381380
var icon = metrics.icon.getSvgRoot();
382381
var iconSize = metrics.icon.getSize();
383382
// Icon's position is calculated relative to the "end" edge of the block.
@@ -394,6 +393,9 @@ Blockly.BlockSvg.prototype.renderDraw_ = function(metrics) {
394393
iconX = -metrics.width + Blockly.BlockSvg.SEP_SPACE_X / 1.5;
395394
}
396395
}
396+
if (this.isGhost()) {
397+
icon.setAttribute('display', 'none');
398+
}
397399
icon.setAttribute('transform',
398400
'translate(' + iconX + ',' + iconY + ') ' + iconScale);
399401
}

core/block_svg.js

Lines changed: 119 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,9 @@ Blockly.BlockSvg.terminateDrag_ = function() {
219219
if (selected) {
220220
if (selected.ghostBlock_) {
221221
Blockly.Events.disable();
222-
selected.ghostBlock_.unplug(true /* healStack */);
222+
if (Blockly.localGhostConnection_) {
223+
selected.disconnectGhost();
224+
}
223225
selected.ghostBlock_.dispose();
224226
selected.ghostBlock_ = null;
225227
Blockly.Events.enable();
@@ -594,7 +596,6 @@ Blockly.BlockSvg.prototype.onMouseUp_ = function(e) {
594596
Blockly.fireUiEvent(window, 'resize');
595597
}
596598
if (Blockly.highlightedConnection_) {
597-
Blockly.highlightedConnection_.unhighlight();
598599
Blockly.highlightedConnection_ = null;
599600
}
600601
Blockly.Css.setCursor(Blockly.Css.Cursor.OPEN);
@@ -814,50 +815,128 @@ Blockly.BlockSvg.prototype.onMouseMove_ = function(e) {
814815
}
815816
}
816817

817-
// Remove connection highlighting if needed.
818-
if (Blockly.highlightedConnection_ &&
819-
Blockly.highlightedConnection_ != closestConnection) {
820-
if (this.ghostBlock_) {
821-
// Don't fire events for ghost block creation or movement.
822-
Blockly.Events.disable();
823-
this.ghostBlock_.unplug(true /* healStack */);
824-
this.ghostBlock_.dispose();
825-
this.ghostBlock_ = null;
826-
Blockly.Events.enable();
827-
}
828-
Blockly.highlightedConnection_.unhighlight();
829-
Blockly.highlightedConnection_ = null;
830-
Blockly.localConnection_ = null;
818+
this.updatePreviews(closestConnection, localConnection, radiusConnection,
819+
e, newXY.x - this.dragStartXY_.x, newXY.y - this.dragStartXY_.y);
820+
}
821+
// This event has been handled. No need to bubble up to the document.
822+
e.stopPropagation();
823+
};
824+
825+
/**
826+
* Preview the results of the drag if the mouse is released immediately.
827+
* @param {Blockly.Connection} closestConnection The closest connection found
828+
* during the search
829+
* @param {Blockly.Connection} localConnection The connection on the moving
830+
* block.
831+
* @param {number} radiusConnection The distance between closestConnection and
832+
* localConnection.
833+
* @param {!Event} e Mouse move event.
834+
* @param {number} dx The x distance the block has moved onscreen up to this
835+
* point in the drag.
836+
* @param {number} dy The y distance the block has moved onscreen up to this
837+
* point in the drag.
838+
*/
839+
Blockly.BlockSvg.prototype.updatePreviews = function(closestConnection,
840+
localConnection, radiusConnection, e, dx, dy) {
841+
// Don't fire events for ghost block creation or movement.
842+
Blockly.Events.disable();
843+
// Remove a ghost if needed. For Scratch-Blockly we are using ghosts instead
844+
// of highlighting the connection; for compatibility with Web Blockly the
845+
// name "highlightedConnection" will still be used.
846+
if (Blockly.highlightedConnection_ &&
847+
Blockly.highlightedConnection_ != closestConnection) {
848+
if (this.ghostBlock_ && Blockly.localGhostConnection_) {
849+
this.disconnectGhost();
831850
}
832-
// Add connection highlighting if needed.
833-
if (closestConnection &&
834-
closestConnection != Blockly.highlightedConnection_) {
835-
closestConnection.highlight();
836-
Blockly.highlightedConnection_ = closestConnection;
837-
Blockly.localConnection_ = localConnection;
838-
Blockly.Events.disable();
839-
if (!this.ghostBlock_){
840-
this.ghostBlock_ = this.workspace.newBlock(this.type);
841-
this.ghostBlock_.setGhost(true);
842-
this.ghostBlock_.moveConnections_(radiusConnection);
851+
Blockly.highlightedConnection_ = null;
852+
Blockly.localConnection_ = null;
853+
}
854+
855+
// Add a ghost if needed.
856+
if (closestConnection &&
857+
closestConnection != Blockly.highlightedConnection_ &&
858+
!closestConnection.sourceBlock_.isGhost()) {
859+
Blockly.highlightedConnection_ = closestConnection;
860+
Blockly.localConnection_ = localConnection;
861+
if (!this.ghostBlock_){
862+
this.ghostBlock_ = this.workspace.newBlock(this.type);
863+
this.ghostBlock_.setGhost(true);
864+
this.ghostBlock_.initSvg();
865+
}
866+
867+
var ghostBlock = this.ghostBlock_;
868+
var localGhostConnection = ghostBlock.getMatchingConnection(this,
869+
localConnection);
870+
if (localGhostConnection != Blockly.localGhostConnection_) {
871+
ghostBlock.getSvgRoot().setAttribute('visibility', 'visible');
872+
ghostBlock.rendered = true;
873+
// Move the preview to the correct location before the existing block.
874+
if (localGhostConnection.type == Blockly.NEXT_STATEMENT) {
875+
var relativeXy = this.getRelativeToSurfaceXY();
876+
var connectionOffsetX = (localConnection.x_ - (relativeXy.x - dx));
877+
var connectionOffsetY = (localConnection.y_ - (relativeXy.y - dy));
878+
var newX = closestConnection.x_ - connectionOffsetX;
879+
var newY = closestConnection.y_ - connectionOffsetY;
880+
var ghostPosition = ghostBlock.getRelativeToSurfaceXY();
881+
ghostBlock.moveBy(newX - ghostPosition.x, newY - ghostPosition.y, true);
882+
843883
}
844-
if (Blockly.localConnection_ == this.previousConnection) {
845-
// Setting the block to rendered will actually change the connection
846-
// behaviour :/
847-
this.ghostBlock_.rendered = true;
848-
this.ghostBlock_.previousConnection.connect(closestConnection);
884+
if (localGhostConnection.type == Blockly.PREVIOUS_STATEMENT &&
885+
!ghostBlock.nextConnection) {
886+
Blockly.bumpedConnection_ = closestConnection.targetConnection;
849887
}
850-
this.ghostBlock_.render(true);
851-
Blockly.Events.enable();
888+
// Renders ghost.
889+
localGhostConnection.connect(closestConnection);
890+
// Render dragging block so it appears on top.
891+
this.workspace.getCanvas().appendChild(this.getSvgRoot());
892+
Blockly.localGhostConnection_ = localGhostConnection;
852893
}
853-
// Provide visual indication of whether the block will be deleted if
854-
// dropped here.
855-
if (this.isDeletable()) {
856-
this.workspace.isDeleteArea(e);
894+
}
895+
// Reenable events.
896+
Blockly.Events.enable();
897+
898+
// Provide visual indication of whether the block will be deleted if
899+
// dropped here.
900+
if (this.isDeletable()) {
901+
this.workspace.isDeleteArea(e);
902+
}
903+
};
904+
905+
/**
906+
* Disconnect the current ghost block from the stack, and heal the stack to its
907+
* previous state.
908+
*/
909+
Blockly.BlockSvg.prototype.disconnectGhost = function() {
910+
// The ghost block is the first block in a stack, either because it doesn't
911+
// have a previous connection or because the previous connection is not
912+
// connection. Unplug won't do anything in that case. Instead, unplug the
913+
// following block.
914+
if (Blockly.localGhostConnection_ == this.ghostBlock_.nextConnection &&
915+
(!this.ghostBlock_.previousConnection ||
916+
!this.ghostBlock_.previousConnection.targetConnection)) {
917+
Blockly.localGhostConnection_.targetBlock().unplug(false);
918+
}
919+
// Inside of a C-block, first statement connection.
920+
else if (Blockly.localGhostConnection_.type == Blockly.NEXT_STATEMENT &&
921+
Blockly.localGhostConnection_ != this.ghostBlock_.nextConnection) {
922+
var innerConnection = Blockly.localGhostConnection_.targetConnection;
923+
innerConnection.sourceBlock_.unplug(false);
924+
var previousBlockNextConnection =
925+
this.ghostBlock_.previousConnection.targetConnection;
926+
this.ghostBlock_.unplug(true);
927+
if (previousBlockNextConnection) {
928+
previousBlockNextConnection.connect(innerConnection);
857929
}
858930
}
859-
// This event has been handled. No need to bubble up to the document.
860-
e.stopPropagation();
931+
else {
932+
this.ghostBlock_.unplug(true /* healStack */);
933+
}
934+
935+
if (Blockly.localGhostConnection_.targetConnection) {
936+
throw 'LocalGhostConnection still connected at the end of disconnectGhost';
937+
}
938+
Blockly.localGhostConnection_ = null;
939+
this.ghostBlock_.getSvgRoot().setAttribute('visibility', 'hidden');
861940
};
862941

863942
/**

core/blockly.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,22 @@ Blockly.highlightedConnection_ = null;
191191
*/
192192
Blockly.localConnection_ = null;
193193

194+
/**
195+
* Connection on ghost block that matches Blockly.localConnecxtion_ on the
196+
* dragged block.
197+
* @type {Blockly.Connection}
198+
* @private
199+
*/
200+
Blockly.localGhostConnection_ = null;
201+
202+
/**
203+
* Connection that was bumped out of the way by a ghost block, and may need
204+
* to be put back as the drag continues.
205+
* @type {Blockly.Connection}
206+
* @private
207+
*/
208+
Blockly.bumpedConnection_ = null;
209+
194210
/**
195211
* Number of pixels the mouse must move before a drag starts.
196212
*/
@@ -199,7 +215,7 @@ Blockly.DRAG_RADIUS = 5;
199215
/**
200216
* Maximum misalignment between connections for them to snap together.
201217
*/
202-
Blockly.SNAP_RADIUS = 20;
218+
Blockly.SNAP_RADIUS = 50;
203219

204220
/**
205221
* Delay in ms between trigger and bumping unconnected block out of alignment.

core/connection.js

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,14 @@ Blockly.Connection.prototype.dispose = function() {
295295
this.dbOpposite_ = null;
296296
};
297297

298+
/**
299+
* @return true if the connection is not connected or is connected to a ghost
300+
* block, false otherwise.
301+
*/
302+
Blockly.Connection.prototype.isConnectedToNonGhost = function() {
303+
return this.targetConnection && !this.targetBlock().isGhost();
304+
};
305+
298306
/**
299307
* Does the connection belong to a superior block (higher in the source stack)?
300308
* @return {boolean} True if connection faces down or right.
@@ -404,6 +412,21 @@ Blockly.Connection.prototype.isConnectionAllowed = function(candidate,
404412
this.sourceBlock_.getFirstStatementConnection();
405413

406414
if (candidate.type == Blockly.PREVIOUS_STATEMENT) {
415+
if (!firstStatementConnection || this != firstStatementConnection) {
416+
if (this.targetConnection) {
417+
return false;
418+
}
419+
if (candidate.targetConnection) {
420+
// If the other side of this connection is the active ghost connection,
421+
// we've obviously already decided that this is a good connection.
422+
if (candidate.targetConnection == Blockly.localGhostConnection_) {
423+
return true;
424+
} else {
425+
return false;
426+
}
427+
}
428+
}
429+
407430
// Scratch-specific behaviour:
408431
// If this is a c-shaped block, statement blocks cannot be connected
409432
// anywhere other than inside the first statement input.
@@ -416,32 +439,15 @@ Blockly.Connection.prototype.isConnectionAllowed = function(candidate,
416439
}
417440
}
418441
// The only other eligible connection of this type is the next connection
419-
// when the candidate is not already connection (connecting at the start
442+
// when the candidate is not already connected (connecting at the start
420443
// of the stack).
421444
else if (this == this.sourceBlock_.nextConnection &&
422-
candidate.targetConnection) {
423-
return false;
424-
}
425-
} else {
426-
// Otherwise, don't offer to connect the bottom of a statement block to
427-
// the top of a block that's already connected. And don't connect the
428-
// bottom of a statement block that's already connected.
429-
if (this.targetConnection || candidate.targetConnection) {
445+
candidate.isConnectedToNonGhost()) {
430446
return false;
431447
}
432448
}
433449
}
434450

435-
// Don't offer to connect the bottom of a statement block to one that's
436-
// already connected.
437-
// But the first statement input on c-block can connect to the start of a
438-
// block in a stack.
439-
if (candidate.type == Blockly.PREVIOUS_STATEMENT &&
440-
this != this.sourceBlock_.getFirstStatementConnection() &&
441-
(this.targetConnection || candidate.targetConnection)) {
442-
return false;
443-
}
444-
445451
// Offering to connect the left (male) of a value block to an already
446452
// connected value pair is ok, we'll splice it in.
447453
// However, don't offer to splice into an unmovable block.
@@ -455,8 +461,7 @@ Blockly.Connection.prototype.isConnectionAllowed = function(candidate,
455461
// Don't let a block with no next connection bump other blocks out of the
456462
// stack.
457463
if (this.type == Blockly.PREVIOUS_STATEMENT &&
458-
candidate.targetConnection &&
459-
!this.sourceBlock_.nextConnection) {
464+
candidate.isConnectedToNonGhost() && !this.sourceBlock_.nextConnection) {
460465
return false;
461466
}
462467

tests/jsunit/connection_db_test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,8 @@ function helper_makeSourceBlock(sharedWorkspace) {
271271
movable_: true,
272272
isMovable: function() { return true; },
273273
isShadow: function() { return false; },
274-
isGhost: function() { return false; }
274+
isGhost: function() { return false; },
275+
getFirstStatementConnection: function() { return null; }
275276
};
276277
}
277278

0 commit comments

Comments
 (0)