diff --git a/core/block.js b/core/block.js index db4a3f3df9..8cf2afc348 100644 --- a/core/block.js +++ b/core/block.js @@ -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 @@ -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(); diff --git a/core/block_render_svg_horizontal.js b/core/block_render_svg_horizontal.js index 99fd12dfd9..c05e09acbf 100644 --- a/core/block_render_svg_horizontal.js +++ b/core/block_render_svg_horizontal.js @@ -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 = []; @@ -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. @@ -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); } diff --git a/core/block_svg.js b/core/block_svg.js index 9ed08a2faf..fdf49be1b5 100644 --- a/core/block_svg.js +++ b/core/block_svg.js @@ -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(); @@ -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); @@ -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'); }; /** diff --git a/core/blockly.js b/core/blockly.js index d9e4e44a28..d47775d889 100644 --- a/core/blockly.js +++ b/core/blockly.js @@ -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. */ @@ -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. diff --git a/core/connection.js b/core/connection.js index 52a02629dc..3ce6692372 100644 --- a/core/connection.js +++ b/core/connection.js @@ -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. @@ -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. @@ -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. @@ -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; } diff --git a/tests/jsunit/connection_db_test.js b/tests/jsunit/connection_db_test.js index 48851950c1..8f99558dcf 100644 --- a/tests/jsunit/connection_db_test.js +++ b/tests/jsunit/connection_db_test.js @@ -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; } }; }