From 7d775908b975257ed394598477a4427b56618dfd Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Mon, 23 Jul 2018 16:46:33 -0700 Subject: [PATCH] Add insertion markers. --- core/block_dragger.js | 6 +- core/block_render_svg.js | 49 +++ core/block_svg.js | 19 + core/constants.js | 21 +- core/css.js | 18 + core/icon.js | 3 +- core/insertion_marker_manager.js | 681 +++++++++++++++++++++++++++++++ 7 files changed, 792 insertions(+), 5 deletions(-) create mode 100644 core/insertion_marker_manager.js diff --git a/core/block_dragger.js b/core/block_dragger.js index 7de40fbf5..9204e1a00 100644 --- a/core/block_dragger.js +++ b/core/block_dragger.js @@ -27,7 +27,7 @@ goog.provide('Blockly.BlockDragger'); goog.require('Blockly.BlockAnimations'); -goog.require('Blockly.DraggedConnectionManager'); +goog.require('Blockly.InsertionMarkerManager'); goog.require('Blockly.Events.BlockMove'); goog.require('goog.math.Coordinate'); @@ -57,10 +57,10 @@ Blockly.BlockDragger = function(block, workspace) { /** * Object that keeps track of connections on dragged blocks. - * @type {!Blockly.DraggedConnectionManager} + * @type {!Blockly.InsertionMarkerManager} * @private */ - this.draggedConnectionManager_ = new Blockly.DraggedConnectionManager( + this.draggedConnectionManager_ = new Blockly.InsertionMarkerManager( this.draggingBlock_); /** diff --git a/core/block_render_svg.js b/core/block_render_svg.js index f9a36d66f..fa0ac73b1 100644 --- a/core/block_render_svg.js +++ b/core/block_render_svg.js @@ -385,7 +385,14 @@ Blockly.BlockSvg.prototype.renderFields_ = function(fieldList, Blockly.BlockSvg.SEP_SPACE_X; } } + // Fields are invisible on insertion marker. They still have to be rendered + // so that the block can be sized correctly. + if (this.isInsertionMarker()) { + root.setAttribute('display', 'none'); + } } + + return this.RTL ? -cursorX : cursorX; }; @@ -1144,3 +1151,45 @@ Blockly.BlockSvg.prototype.renderStatementInput_ = function(pathObject, row, cursor.y += Blockly.BlockSvg.SEP_SPACE_Y; } }; + +/** + * TODO: fenichel: consider moving to block_svg if this is in blockly and + * scratch-blocks. + * Position an new block correctly, so that it doesn't move the existing block + * when connected to it. + * @param {!Blockly.Block} newBlock The block to position - either the first + * block in a dragged stack or an insertion marker. + * @param {!Blockly.Connection} newConnection The connection on the new block's + * stack - either a connection on newBlock, or the last NEXT_STATEMENT + * connection on the stack if the stack's being dropped before another + * block. + * @param {!Blockly.Connection} existingConnection The connection on the + * existing block, which newBlock should line up with. + */ +Blockly.BlockSvg.prototype.positionNewBlock = function(newBlock, newConnection, + existingConnection) { + // We only need to position the new block if it's before the existing one, + // otherwise its position is set by the previous block. + if (newConnection.type == Blockly.NEXT_STATEMENT || + newConnection.type == Blockly.INPUT_VALUE) { + var dx = existingConnection.x_ - newConnection.x_; + var dy = existingConnection.y_ - newConnection.y_; + + newBlock.moveBy(dx, dy); + } +}; + +/** + * Visual effect to show that if the dragging block is dropped, this block will + * be replaced. If a shadow block it will disappear. Otherwise it will bump. + * @param {boolean} add True if highlighting should be added. + */ +Blockly.BlockSvg.prototype.highlightForReplacement = function(add) { + if (add) { + Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_), + 'blocklyReplaceable'); + } else { + Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_), + 'blocklyReplaceable'); + } +}; diff --git a/core/block_svg.js b/core/block_svg.js index 4cd15dde5..f7e15ec57 100644 --- a/core/block_svg.js +++ b/core/block_svg.js @@ -487,6 +487,25 @@ Blockly.BlockSvg.prototype.getBoundingRectangle = function() { return {topLeft: topLeft, bottomRight: bottomRight}; }; +/** + * Set block opacity for SVG rendering. + * @param {number} opacity Intended opacity, betweeen 0 and 1 + */ +Blockly.BlockSvg.prototype.setOpacity = function(opacity) { + this.opacity_ = opacity; + if (this.rendered) { + this.updateColour(); + } +}; + +/** + * Get block opacity for SVG rendering. + * @return {number} Intended opacity, betweeen 0 and 1 + */ +Blockly.BlockSvg.prototype.getOpacity = function() { + return this.opacity_; +}; + /** * Set whether the block is collapsed or not. * @param {boolean} collapsed True if collapsed. diff --git a/core/constants.js b/core/constants.js index 54ed1fb49..6bf3d9236 100644 --- a/core/constants.js +++ b/core/constants.js @@ -42,7 +42,20 @@ Blockly.FLYOUT_DRAG_RADIUS = 10; /** * Maximum misalignment between connections for them to snap together. */ -Blockly.SNAP_RADIUS = 20; +Blockly.SNAP_RADIUS = 48; + +/** + * Maximum misalignment between connections for them to snap together, + * when a connection is already highlighted. + */ +Blockly.CONNECTING_SNAP_RADIUS = 68; + +/** + * How much to prefer staying connected to the current connection over moving to + * a new connection. The current previewed connection is considered to be this + * much closer to the matching connection on the block than it actually is. + */ +Blockly.CURRENT_CONNECTION_PREFERENCE = 20; /** * Delay in ms between trigger and bumping unconnected block out of alignment. @@ -272,3 +285,9 @@ Blockly.RENAME_VARIABLE_ID = 'RENAME_VARIABLE_ID'; * @const {string} */ Blockly.DELETE_VARIABLE_ID = 'DELETE_VARIABLE_ID'; + +/** + * TODO: Put this somewhere else. colours.js? + * + */ +Blockly.INSERTION_MARKER_COLOUR = '#000000'; diff --git a/core/css.js b/core/css.js index e19e2dfe0..7a52a9454 100644 --- a/core/css.js +++ b/core/css.js @@ -284,6 +284,24 @@ Blockly.Css.CONTENT = [ 'display: none;', '}', + '.blocklyInsertionMarker>.blocklyPath,', + '.blocklyInsertionMarker>.blocklyPathLight,', + '.blocklyInsertionMarker>.blocklyPathDark {', + 'fill-opacity: .2;', + 'stroke: none', + '}', + + '.blocklyReplaceable>.blocklyPath {', + 'fill-opacity: 0.5;', + 'stroke-width: 3px;', + 'stroke: #ffdb70;', + '}', + + '.blocklyReplaceable>.blocklyPathLight,', + '.blocklyReplaceable>.blocklyPathDark {', + 'display: none;', + '}', + '.blocklyText {', 'cursor: default;', 'fill: #fff;', diff --git a/core/icon.js b/core/icon.js index 6c259245d..0d3f2ddd3 100644 --- a/core/icon.js +++ b/core/icon.js @@ -147,7 +147,8 @@ Blockly.Icon.prototype.updateColour = function() { * @return {number} Horizontal offset for next item to draw. */ Blockly.Icon.prototype.renderIcon = function(cursorX) { - if (this.collapseHidden && this.block_.isCollapsed()) { + if ((this.collapseHidden && this.block_.isCollapsed()) || + this.block_.isInsertionMarker()) { this.iconGroup_.setAttribute('display', 'none'); return cursorX; } diff --git a/core/insertion_marker_manager.js b/core/insertion_marker_manager.js new file mode 100644 index 000000000..e041f4bb2 --- /dev/null +++ b/core/insertion_marker_manager.js @@ -0,0 +1,681 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2017 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Class that controls updates to connections during drags. + * @author fenichel@google.com (Rachel Fenichel) + */ +'use strict'; + +goog.provide('Blockly.InsertionMarkerManager'); + +goog.require('Blockly.BlockAnimations'); +goog.require('Blockly.Events.BlockMove'); +goog.require('Blockly.RenderedConnection'); + +goog.require('goog.math.Coordinate'); + + +/** + * Class that controls updates to connections during drags. It is primarily + * responsible for finding the closest eligible connection and highlighting or + * unhiglighting it as needed during a drag. + * @param {!Blockly.BlockSvg} block The top block in the stack being dragged. + * @constructor + */ +Blockly.InsertionMarkerManager = function(block) { + Blockly.selected = block; + + /** + * The top block in the stack being dragged. + * Does not change during a drag. + * @type {!Blockly.Block} + * @private + */ + this.topBlock_ = block; + + /** + * The workspace on which these connections are being dragged. + * Does not change during a drag. + * @type {!Blockly.WorkspaceSvg} + * @private + */ + this.workspace_ = block.workspace; + + /** + * The last connection on the stack, if it's not the last connection on the + * first block. + * Set in initAvailableConnections, if at all. + * @type {Blockly.RenderedConnection} + * @private + */ + this.lastOnStack_ = null; + + /** + * The insertion marker corresponding to the last block in the stack, if + * that's not the same as the first block in the stack. + * Set in initAvailableConnections, if at all + * @type {Blockly.BlockSvg} + * @private + */ + this.lastMarker_ = null; + + /** + * The insertion marker that shows up between blocks to show where a block + * would go if dropped immediately. + * This is the scratch-blocks equivalent of connection highlighting. + * @type {Blockly.BlockSvg} + * @private + */ + this.firstMarker_ = this.createMarkerBlock_(this.topBlock_); + + /** + * The connection that this block would connect to if released immediately. + * Updated on every mouse move. + * This is not on any of the blocks that are being dragged. + * @type {Blockly.RenderedConnection} + * @private + */ + this.closestConnection_ = null; + + /** + * The connection that would connect to this.closestConnection_ if this block + * were released immediately. + * Updated on every mouse move. + * This is on the top block that is being dragged or the last block in the + * dragging stack. + * @type {Blockly.RenderedConnection} + * @private + */ + this.localConnection_ = null; + + /** + * Whether the block would be deleted if it were dropped immediately. + * Updated on every mouse move. + * @type {boolean} + * @private + */ + this.wouldDeleteBlock_ = false; + + /** + * Connection on the insertion marker block that corresponds to + * this.localConnection_ on the currently dragged block. + * This is part of the scratch-blocks equivalent of connection highlighting. + * @type {Blockly.RenderedConnection} + * @private + */ + this.markerConnection_ = null; + + /** + * Whether we are currently highlighting the block (shadow or real) that would + * be replaced if the drag were released immediately. + * @type {boolean} + * @private + */ + this.highlightingBlock_ = false; + + /** + * The block that is being highlighted for replacement, or null. + * @type {Blockly.BlockSvg} + * @private + */ + this.highlightedBlock_ = null; + + /** + * The connections on the dragging blocks that are available to connect to + * other blocks. This includes all open connections on the top block, as well + * as the last connection on the block stack. + * Does not change during a drag. + * @type {!Array.} + * @private + */ + this.availableConnections_ = this.initAvailableConnections_(); +}; + +/** + * Sever all links from this object. + * @package + */ +Blockly.InsertionMarkerManager.prototype.dispose = function() { + this.topBlock_ = null; + this.workspace_ = null; + this.availableConnections_.length = 0; + this.closestConnection_ = null; + this.localConnection_ = null; + + Blockly.Events.disable(); + try { + if (this.firstMarker_) { + this.firstMarker_.dispose(); + this.firstMarker_ = null; + } + if (this.lastMarker_) { + this.lastMarker_.dispose(); + this.lastMarker_ = null; + } + } finally { + Blockly.Events.enable(); + } + + this.highlightedBlock_ = null; +}; + +/** + * Return whether the block would be deleted if dropped immediately, based on + * information from the most recent move event. + * @return {boolean} true if the block would be deleted if dropped immediately. + * @package + */ +Blockly.InsertionMarkerManager.prototype.wouldDeleteBlock = function() { + return this.wouldDeleteBlock_; +}; + +/** + * Return whether the block would be connected if dropped immediately, based on + * information from the most recent move event. + * @return {boolean} true if the block would be connected if dropped immediately. + * @package + */ +Blockly.InsertionMarkerManager.prototype.wouldConnectBlock = function() { + return !!this.closestConnection_; +}; + +/** + * Connect to the closest connection and render the results. + * This should be called at the end of a drag. + * @package + */ +Blockly.InsertionMarkerManager.prototype.applyConnections = function() { + if (this.closestConnection_) { + // Don't fire events for insertion markers. + Blockly.Events.disable(); + this.hidePreview_(); + Blockly.Events.enable(); + // Connect two blocks together. + this.localConnection_.connect(this.closestConnection_); + if (this.topBlock_.rendered) { + // Trigger a connection animation. + // Determine which connection is inferior (lower in the source stack). + var inferiorConnection = this.localConnection_.isSuperior() ? + this.closestConnection_ : this.localConnection_; + Blockly.BlockAnimations.connectionUiEffect( + inferiorConnection.getSourceBlock()); + // Bring the just-edited stack to the front. + var rootBlock = this.topBlock_.getRootBlock(); + rootBlock.bringToFront(); + } + } +}; + +/** + * Update highlighted connections based on the most recent move location. + * @param {!goog.math.Coordinate} dxy Position relative to drag start, + * in workspace units. + * @param {?number} deleteArea One of {@link Blockly.DELETE_AREA_TRASH}, + * {@link Blockly.DELETE_AREA_TOOLBOX}, or {@link Blockly.DELETE_AREA_NONE}. + * @package + */ +Blockly.InsertionMarkerManager.prototype.update = function(dxy, deleteArea) { + var candidate = this.getCandidate_(dxy); + + this.wouldDeleteBlock_ = this.shouldDelete_(candidate, deleteArea); + var shouldUpdate = this.wouldDeleteBlock_ || + this.shouldUpdatePreviews_(candidate, dxy); + + if (shouldUpdate) { + // Don't fire events for insertion marker creation or movement. + Blockly.Events.disable(); + this.maybeHidePreview_(candidate); + this.maybeShowPreview_(candidate); + Blockly.Events.enable(); + } +}; + +/**** Begin initialization functions ****/ + +/** + * Create an insertion marker that represents the given block. + * @param {!Blockly.BlockSvg} sourceBlock The block that the insertion marker + * will represent. + * @return {!Blockly.BlockSvg} The insertion marker that represents the given + * block. + * @private + */ +Blockly.InsertionMarkerManager.prototype.createMarkerBlock_ = function(sourceBlock) { + var imType = sourceBlock.type; + + Blockly.Events.disable(); + try { + var result = this.workspace_.newBlock(imType); + result.setInsertionMarker(true, sourceBlock.width); + if (sourceBlock.mutationToDom) { + var oldMutationDom = sourceBlock.mutationToDom(); + if (oldMutationDom) { + result.domToMutation(oldMutationDom); + } + } + result.initSvg(); + result.getSvgRoot().setAttribute('visibility', 'hidden'); + } finally { + Blockly.Events.enable(); + } + + return result; +}; + +/** + * Populate the list of available connections on this block stack. This should + * only be called once, at the beginning of a drag. + * If the stack has more than one block, this function will populate + * lastOnStack_ and create the corresponding insertion marker. + * @return {!Array.} a list of available + * connections. + * @private + */ +Blockly.InsertionMarkerManager.prototype.initAvailableConnections_ = function() { + var available = this.topBlock_.getConnections_(false); + // Also check the last connection on this stack + var lastOnStack = this.topBlock_.lastConnectionInStack(); + if (lastOnStack && lastOnStack != this.topBlock_.nextConnection) { + available.push(lastOnStack); + this.lastOnStack_ = lastOnStack; + this.lastMarker_ = this.createMarkerBlock_(lastOnStack.sourceBlock_); + } + return available; +}; + +/**** End initialization functions ****/ + + +/** + * Whether the previews (insertion marker and replacement marker) should be + * updated based on the closest candidate and the current drag distance. + * @param {!Object} candidate An object containing a local connection, a closest + * connection, and a radius. Returned by getCandidate_. + * @param {!goog.math.Coordinate} dxy Position relative to drag start, + * in workspace units. + * @return {boolean} whether the preview should be updated. + * @private + */ +Blockly.InsertionMarkerManager.prototype.shouldUpdatePreviews_ = function( + candidate, dxy) { + var candidateLocal = candidate.local; + var candidateClosest = candidate.closest; + var radius = candidate.radius; + + // Found a connection! + if (candidateLocal && candidateClosest) { + // if (candidateLocal.type == Blockly.OUTPUT_VALUE) { + // // Always update previews for output connections. + // return true; + // } + // We're already showing an insertion marker. + // Decide whether the new connection has higher priority. + if (this.localConnection_ && this.closestConnection_) { + // The connection was the same as the current connection. + if (this.closestConnection_ == candidateClosest) { + return false; + } + var xDiff = this.localConnection_.x_ + dxy.x - this.closestConnection_.x_; + var yDiff = this.localConnection_.y_ + dxy.y - this.closestConnection_.y_; + var curDistance = Math.sqrt(xDiff * xDiff + yDiff * yDiff); + // Slightly prefer the existing preview over a new preview. + return !(candidateClosest && radius > curDistance - + Blockly.CURRENT_CONNECTION_PREFERENCE); + } else if (!this.localConnection_ && !this.closestConnection_) { + // We weren't showing a preview before, but we should now. + return true; + } else { + console.error('Only one of localConnection_ and closestConnection_ was set.'); + } + } else { // No connection found. + // Only need to update if we were showing a preview before. + return !!(this.localConnection_ && this.closestConnection_); + } + + console.error('Returning true from shouldUpdatePreviews, but it\'s not clear why.'); + return true; +}; + +/** + * Find the nearest valid connection, which may be the same as the current + * closest connection. + * @param {!goog.math.Coordinate} dxy Position relative to drag start, + * in workspace units. + * @return {!Object} candidate An object containing a local connection, a closest + * connection, and a radius. + */ +Blockly.InsertionMarkerManager.prototype.getCandidate_ = function(dxy) { + var radius = this.getStartRadius_(); + var candidateClosest = null; + var candidateLocal = null; + + for (var i = 0; i < this.availableConnections_.length; i++) { + var myConnection = this.availableConnections_[i]; + var neighbour = myConnection.closest(radius, dxy); + if (neighbour.connection) { + candidateClosest = neighbour.connection; + candidateLocal = myConnection; + radius = neighbour.radius; + } + } + return { + closest: candidateClosest, + local: candidateLocal, + radius: radius + }; +}; + +/** + * Decide the radius at which to start searching for the closest connection. + * @return {number} The radius at which to start the search for the closest + * connection. + * @private + */ +Blockly.InsertionMarkerManager.prototype.getStartRadius_ = function() { + // If there is already a connection highlighted, + // increase the radius we check for making new connections. + // Why? When a connection is highlighted, blocks move around when the insertion + // marker is created, which could cause the connection became out of range. + // By increasing radiusConnection when a connection already exists, + // we never "lose" the connection from the offset. + if (this.closestConnection_ && this.localConnection_) { + return Blockly.CONNECTING_SNAP_RADIUS; + } + return Blockly.SNAP_RADIUS; +}; + +/** + * Whether ending the drag would replace a block or insert a block. + * @return {boolean} True if dropping the block immediately would replace + * another block. False if dropping the block immediately would result in + * the block being inserted in a block stack. + * @private + */ +Blockly.InsertionMarkerManager.prototype.shouldReplace_ = function() { + var closest = this.closestConnection_; + var local = this.localConnection_; + + // Dragging a block over an existing block in an input should replace the + // existing block and bump it out. + if (local.type == Blockly.OUTPUT_VALUE && closest.isConnected()) { + return true; // Replace. + } + + // Connecting to a statement input of c-block is an insertion, even if that + // c-block is terminal (e.g. forever). + if (local == local.sourceBlock_.getFirstStatementConnection()) { + return false; // Insert. + } + + // Dragging a terminal block over another (connected) terminal block will + // replace, not insert. + var isTerminalBlock = !this.topBlock_.nextConnection; + var isConnectedTerminal = isTerminalBlock && + local.type == Blockly.PREVIOUS_STATEMENT && closest.isConnected(); + if (isConnectedTerminal) { + return true; // Replace. + } + + // Otherwise it's an insertion. + return false; +}; + +/** + * Whether ending the drag would delete the block. + * @param {!Object} candidate An object containing a local connection, a closest + * connection, and a radius. + * @param {?number} deleteArea One of {@link Blockly.DELETE_AREA_TRASH}, + * {@link Blockly.DELETE_AREA_TOOLBOX}, or {@link Blockly.DELETE_AREA_NONE}. + * @return {boolean} True if dropping the block immediately would replace + * delete the block. False otherwise. + * @private + */ +Blockly.InsertionMarkerManager.prototype.shouldDelete_ = function(candidate, + deleteArea) { + // Prefer connecting over dropping into the trash can, but prefer dragging to + // the toolbox over connecting to other blocks. + var wouldConnect = candidate && !!candidate.closest && + deleteArea != Blockly.DELETE_AREA_TOOLBOX; + var wouldDelete = !!deleteArea && !this.topBlock_.getParent() && + this.topBlock_.isDeletable(); + + return wouldDelete && !wouldConnect; +}; + +/**** Begin preview visibility functions ****/ + +/** + * Show an insertion marker or replacement highlighting during a drag, if + * needed. + * At the beginning of this function, this.localConnection_ and + * this.closestConnection_ should both be null. + * @param {!Object} candidate An object containing a local connection, a closest + * connection, and a radius. + * @private + */ +Blockly.InsertionMarkerManager.prototype.maybeShowPreview_ = function(candidate) { + // Nope, don't add a marker. + if (this.wouldDeleteBlock_) { + return; + } + var closest = candidate.closest; + var local = candidate.local; + + // Nothing to connect to. + if (!closest) { + return; + } + + // Something went wrong and we're trying to connect to an invalid connection. + if (closest == this.closestConnection_ || + closest.sourceBlock_.isInsertionMarker()) { + return; + } + // Add an insertion marker or replacement marker. + this.closestConnection_ = closest; + this.localConnection_ = local; + this.showPreview_(); +}; + +/** + * A preview should be shown. This function figures out if it should be a block + * highlight or an insertion marker, and shows the appropriate one. + * @private + */ +Blockly.InsertionMarkerManager.prototype.showPreview_ = function() { + if (this.shouldReplace_()) { + this.highlightBlock_(); + } else { // Should insert + this.connectMarker_(); + } +}; + +/** + * Show an insertion marker or replacement highlighting during a drag, if + * needed. + * At the end of this function, this.localConnection_ and + * this.closestConnection_ should both be null. + * @param {!Object} candidate An object containing a local connection, a closest + * connection, and a radius. + * @private + */ +Blockly.InsertionMarkerManager.prototype.maybeHidePreview_ = function(candidate) { + // If there's no new preview, remove the old one but don't bother deleting it. + // We might need it later, and this saves disposing of it and recreating it. + if (!candidate.closest) { + this.hidePreview_(); + } + // If there's a new preview and there was an preview before, and either + // connection has changed, remove the old preview. + var hadPreview = this.closestConnection_ && this.localConnection_; + var closestChanged = this.closestConnection_ != candidate.closest; + var localChanged = this.localConnection_ != candidate.local; + + // Also hide if we had a preview before but now we're going to delete instead. + if (hadPreview && (closestChanged || localChanged || this.wouldDeleteBlock_)) { + this.hidePreview_(); + } + + // Either way, clear out old state. + this.markerConnection_ = null; + this.closestConnection_ = null; + this.localConnection_ = null; +}; + +/** + * A preview should be hidden. This function figures out if it is a block + * highlight or an insertion marker, and hides the appropriate one. + * @private + */ +Blockly.InsertionMarkerManager.prototype.hidePreview_ = function() { + if (this.highlightingBlock_) { + this.unhighlightBlock_(); + } else if (this.markerConnection_) { + this.disconnectMarker_(); + } +}; + +/**** End preview visibility functions ****/ + +/**** Begin block highlighting functions ****/ + +/** + * Add highlighting showing which block will be replaced. + * Scratch-specific code, where "highlighting" applies to a block rather than + * a connection. + */ +Blockly.InsertionMarkerManager.prototype.highlightBlock_ = function() { + var closest = this.closestConnection_; + var local = this.localConnection_; + if (closest.targetBlock()) { + this.highlightedBlock_ = closest.targetBlock(); + closest.targetBlock().highlightForReplacement(true); + } else if(local.type == Blockly.OUTPUT_VALUE) { + this.highlightedBlock_ = closest.sourceBlock_; + closest.sourceBlock_.highlightShapeForInput(closest, true); + } + this.highlightingBlock_ = true; +}; + +/** + * Get rid of the highlighting marking the block that will be replaced. + * Scratch-specific code, where "highlighting" applies to a block rather than + * a connection. + */ +Blockly.InsertionMarkerManager.prototype.unhighlightBlock_ = function() { + var closest = this.closestConnection_; + // If there's no block in place, but we're still connecting to a value input, + // then we must have been highlighting an input shape. + if (closest.type == Blockly.INPUT_VALUE && !closest.isConnected()) { + this.highlightedBlock_.highlightShapeForInput(closest, false); + } else { + this.highlightedBlock_.highlightForReplacement(false); + } + this.highlightedBlock_ = null; + this.highlightingBlock_ = false; +}; + +/**** End block highlighting functions ****/ + +/**** Begin insertion marker display functions ****/ + +/** + * Disconnect the insertion marker block in a manner that returns the stack to + * original state. + * @private + */ +Blockly.InsertionMarkerManager.prototype.disconnectMarker_ = function() { + if (!this.markerConnection_) { + console.log('No insertion marker connection to disconnect'); + return; + } + + var imConn = this.markerConnection_; + var imBlock = imConn.sourceBlock_; + var markerNext = imBlock.nextConnection; + var markerPrev = imBlock.previousConnection; + var markerOutput = imBlock.outputConnection; + + var isFirstInStatementStack = + (imConn == markerNext && !(markerPrev && markerPrev.targetConnection)); + + var isFirstInOutputStack = imConn.type == Blockly.INPUT_VALUE && + !(markerOutput && markerOutput.targetConnection); + // The insertion marker is the first block in a stack. Unplug won't do + // anything in that case. Instead, unplug the following block. + if (isFirstInStatementStack || isFirstInOutputStack) { + imConn.targetBlock().unplug(false); + } + // Inside of a C-block, first statement connection. + else if (imConn.type == Blockly.NEXT_STATEMENT && imConn != markerNext) { + var innerConnection = imConn.targetConnection; + innerConnection.sourceBlock_.unplug(false); + + var previousBlockNextConnection = + markerPrev ? markerPrev.targetConnection : null; + + imBlock.unplug(true); + if (previousBlockNextConnection) { + previousBlockNextConnection.connect(innerConnection); + } + } else { + imBlock.unplug(true /* healStack */); + } + + if (imConn.targetConnection) { + throw 'markerConnection_ still connected at the end of disconnectInsertionMarker'; + } + + this.markerConnection_ = null; + imBlock.getSvgRoot().setAttribute('visibility', 'hidden'); +}; + +/** + * Add an insertion marker connected to the appropriate blocks. + * @private + */ +Blockly.InsertionMarkerManager.prototype.connectMarker_ = function() { + var local = this.localConnection_; + var closest = this.closestConnection_; + + var isLastInStack = this.lastOnStack_ && local == this.lastOnStack_; + var imBlock = isLastInStack ? this.lastMarker_ : this.firstMarker_; + var imConn = imBlock.getMatchingConnection(local.sourceBlock_, local); + + goog.asserts.assert(imConn != this.markerConnection_, + 'Made it to connectMarker_ even though the marker isn\'t changing'); + + // Render disconnected from everything else so that we have a valid + // connection location. + imBlock.render(); + imBlock.rendered = true; + imBlock.getSvgRoot().setAttribute('visibility', 'visible'); + + // TODO: positionNewBlock should be on Blockly.BlockSvg, not prototype, + // because it doesn't rely on anything in the block it's called on. + imBlock.positionNewBlock(imBlock, imConn, closest); + + // Connect() also renders the insertion marker. + imConn.connect(closest); + this.markerConnection_ = imConn; +}; + +/**** End insertion marker display functions ****/