Merge pull request #1987 from rachel-fenichel/feature/insertion_markers

Insertion markers in Blockly!
This commit is contained in:
Rachel Fenichel
2018-11-19 16:32:15 -08:00
committed by GitHub
11 changed files with 966 additions and 18 deletions

View File

@@ -37,7 +37,7 @@ this.BLOCKLY_BOOT = function(root) {
goog.addDependency("../../../" + dir + "/core/block.js", ['Blockly.Block'], ['Blockly.Blocks', 'Blockly.Comment', 'Blockly.Connection', 'Blockly.Events.BlockChange', 'Blockly.Events.BlockCreate', 'Blockly.Events.BlockDelete', 'Blockly.Events.BlockMove', 'Blockly.Extensions', 'Blockly.Input', 'Blockly.Mutator', 'Blockly.utils', 'Blockly.Warning', 'Blockly.Workspace', 'goog.math.Coordinate']);
goog.addDependency("../../../" + dir + "/core/block_animations.js", ['Blockly.BlockAnimations'], ['Blockly.utils']);
goog.addDependency("../../../" + dir + "/core/block_drag_surface.js", ['Blockly.BlockDragSurfaceSvg'], ['Blockly.utils', 'goog.math.Coordinate']);
goog.addDependency("../../../" + dir + "/core/block_dragger.js", ['Blockly.BlockDragger'], ['Blockly.BlockAnimations', 'Blockly.DraggedConnectionManager', 'Blockly.Events.BlockMove', 'goog.math.Coordinate']);
goog.addDependency("../../../" + dir + "/core/block_dragger.js", ['Blockly.BlockDragger'], ['Blockly.BlockAnimations', 'Blockly.InsertionMarkerManager', 'Blockly.Events.BlockMove', 'goog.math.Coordinate']);
goog.addDependency("../../../" + dir + "/core/block_events.js", ['Blockly.Events.BlockBase', 'Blockly.Events.BlockChange', 'Blockly.Events.BlockCreate', 'Blockly.Events.BlockDelete', 'Blockly.Events.BlockMove', 'Blockly.Events.Change', 'Blockly.Events.Create', 'Blockly.Events.Delete', 'Blockly.Events.Move'], ['Blockly.Events', 'Blockly.Events.Abstract', 'Blockly.Xml.utils', 'goog.math.Coordinate']);
goog.addDependency("../../../" + dir + "/core/block_render_svg.js", ['Blockly.BlockSvg.render'], ['Blockly.BlockSvg']);
goog.addDependency("../../../" + dir + "/core/block_svg.js", ['Blockly.BlockSvg'], ['Blockly.Block', 'Blockly.BlockAnimations', 'Blockly.ContextMenu', 'Blockly.Events.Ui', 'Blockly.Events.BlockMove', 'Blockly.Grid', 'Blockly.RenderedConnection', 'Blockly.Tooltip', 'Blockly.Touch', 'Blockly.utils', 'goog.color', 'goog.math.Coordinate']);
@@ -77,6 +77,7 @@ goog.addDependency("../../../" + dir + "/core/grid.js", ['Blockly.Grid'], ['Bloc
goog.addDependency("../../../" + dir + "/core/icon.js", ['Blockly.Icon'], ['Blockly.utils', 'goog.math.Coordinate']);
goog.addDependency("../../../" + dir + "/core/inject.js", ['Blockly.inject'], ['Blockly.BlockDragSurfaceSvg', 'Blockly.Css', 'Blockly.Grid', 'Blockly.Options', 'Blockly.utils', 'Blockly.WorkspaceSvg', 'Blockly.WorkspaceDragSurfaceSvg', 'goog.ui.Component', 'goog.userAgent']);
goog.addDependency("../../../" + dir + "/core/input.js", ['Blockly.Input'], ['Blockly.Connection', 'Blockly.FieldLabel']);
goog.addDependency("../../../" + dir + "/core/insertion_marker_manager.js", ['Blockly.InsertionMarkerManager'], ['Blockly.BlockAnimations', 'Blockly.Events.BlockMove', 'Blockly.RenderedConnection', 'goog.math.Coordinate']);
goog.addDependency("../../../" + dir + "/core/msg.js", ['Blockly.Msg'], []);
goog.addDependency("../../../" + dir + "/core/mutator.js", ['Blockly.Mutator'], ['Blockly.Bubble', 'Blockly.Events.BlockChange', 'Blockly.Events.Ui', 'Blockly.Icon', 'Blockly.utils', 'Blockly.WorkspaceSvg', 'Blockly.Xml', 'Blockly.Xml.utils']);
goog.addDependency("../../../" + dir + "/core/names.js", ['Blockly.Names'], []);
@@ -1786,6 +1787,7 @@ goog.require('Blockly.Grid');
goog.require('Blockly.HorizontalFlyout');
goog.require('Blockly.Icon');
goog.require('Blockly.Input');
goog.require('Blockly.InsertionMarkerManager');
goog.require('Blockly.Msg');
goog.require('Blockly.Mutator');
goog.require('Blockly.Names');

View File

@@ -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_);
/**
@@ -162,6 +162,9 @@ Blockly.BlockDragger.prototype.startBlockDrag = function(currentDragDeltaXY,
this.draggingBlock_.bringToFront();
}
// During a drag there may be a lot of rerenders, but not field changes.
// Turn the cache on so we don't do spurious remeasures during the drag.
Blockly.Field.startCache();
this.workspace_.setResizesEnabled(false);
Blockly.BlockAnimations.disconnectUiStop();
@@ -222,6 +225,8 @@ Blockly.BlockDragger.prototype.endBlockDrag = function(e, currentDragDeltaXY) {
this.dragBlock(e, currentDragDeltaXY);
this.dragIconData_ = [];
Blockly.Field.stopCache();
Blockly.BlockAnimations.disconnectUiStop();
var delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);

View File

@@ -385,6 +385,11 @@ 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 +1149,43 @@ Blockly.BlockSvg.prototype.renderStatementInput_ = function(pathObject, row,
cursor.y += Blockly.BlockSvg.SEP_SPACE_Y;
}
};
/**
* 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');
}
};

View File

@@ -334,12 +334,67 @@ Blockly.Connection.prototype.checkConnection_ = function(target) {
}
};
/**
* Check if the two connections can be dragged to connect to each other.
* This is used by the connection database when searching for the closest
* connection.
* @param {!Blockly.Connection} candidate A nearby connection to check, which
* must be a previous connection.
* @return {boolean} True if the connection is allowed, false otherwise.
* @private
*/
Blockly.Connection.prototype.canConnectToPrevious_ = function(candidate) {
if (this.targetConnection) {
// This connection is already occupied.
// A next connection will never disconnect itself mid-drag.
return false;
}
// Don't let blocks try to connect to themselves or ones they nest.
if (Blockly.draggingConnections_.indexOf(candidate) != -1) {
return false;
}
var firstStatementConnection =
this.sourceBlock_.getFirstStatementConnection();
var isFirstStatementConnection = this == firstStatementConnection;
var isNextConnection = this == this.sourceBlock_.nextConnection;
// Complex blocks with no previous connection will not be allowed to connect
// mid-stack.
var sourceHasPreviousConn = this.sourceBlock_.previousConnection != null;
if (isNextConnection ||
(isFirstStatementConnection && !sourceHasPreviousConn)) {
// If the candidate is the first connection in a stack, we can connect.
if (!candidate.targetConnection) {
return true;
}
var targetBlock = candidate.targetBlock();
// If it is connected to a real block, game over.
if (!targetBlock.isInsertionMarker()) {
return false;
}
// If it's connected to an insertion marker but that insertion marker
// is the first block in a stack, it's still fine. If that insertion
// marker is in the middle of a stack, it won't work.
return !targetBlock.getPreviousBlock();
}
console.warn('Returning false by default from canConnectToPrevious_.');
return false;
};
/**
* Check if the two connections can be dragged to connect to each other.
* @param {!Blockly.Connection} candidate A nearby connection to check.
* @return {boolean} True if the connection is allowed, false otherwise.
*/
Blockly.Connection.prototype.isConnectionAllowed = function(candidate) {
// Don't consider insertion markers.
if (candidate.sourceBlock_.isInsertionMarker()) {
return false;
}
// Type checking.
var canConnect = this.canConnectWithReason_(candidate);
if (canConnect != Blockly.Connection.CAN_CONNECT) {
@@ -348,30 +403,27 @@ Blockly.Connection.prototype.isConnectionAllowed = function(candidate) {
switch (candidate.type) {
case Blockly.PREVIOUS_STATEMENT:
// Don't offer to connect the bottom of a statement block to one that's
// already connected.
if (candidate.isConnected() || this.isConnected()) {
return false;
}
break;
case Blockly.OUTPUT_VALUE:
return this.canConnectToPrevious_(candidate);
case Blockly.OUTPUT_VALUE: {
// Don't offer to connect an already connected left (male) value plug to
// an available right (female) value plug.
if (candidate.isConnected() || this.isConnected()) {
return false;
}
break;
case Blockly.INPUT_VALUE:
}
case Blockly.INPUT_VALUE: {
// 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.
if (candidate.targetConnection &&
// However, don't offer to splice into an immovable block.
if (candidate.isConnected() &&
!candidate.targetBlock().isMovable() &&
!candidate.targetBlock().isShadow()) {
return false;
}
break;
case Blockly.NEXT_STATEMENT:
}
case Blockly.NEXT_STATEMENT: {
// Don't let a block with no next connection bump other blocks out of the
// stack. But covering up a shadow block or stack of shadow blocks is
// fine. Similarly, replacing a terminal statement with another terminal
@@ -383,6 +435,7 @@ Blockly.Connection.prototype.isConnectionAllowed = function(candidate) {
return false;
}
break;
}
default:
throw Error('Unknown connection type in isConnectionAllowed');
}

View File

@@ -42,7 +42,26 @@ Blockly.FLYOUT_DRAG_RADIUS = 10;
/**
* Maximum misalignment between connections for them to snap together.
*/
Blockly.SNAP_RADIUS = 20;
Blockly.SNAP_RADIUS = 36;
/**
* Maximum misalignment between connections for them to snap together,
* when a connection is already highlighted.
*/
Blockly.CONNECTING_SNAP_RADIUS = 48;
/**
* 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 = 0;
/**
* The main colour of insertion markers, in hex. The block is rendered a
* transparent grey by changing the fill opacity in CSS.
*/
Blockly.INSERTION_MARKER_COLOUR = '#000000';
/**
* Delay in ms between trigger and bumping unconnected block out of alignment.

View File

@@ -284,6 +284,22 @@ Blockly.Css.CONTENT = [
'display: none;',
'}',
'.blocklyInsertionMarker>.blocklyPath,',
'.blocklyInsertionMarker>.blocklyPathLight,',
'.blocklyInsertionMarker>.blocklyPathDark {',
'fill-opacity: .2;',
'stroke: none',
'}',
'.blocklyReplaceable .blocklyPath {',
'fill-opacity: 0.5;',
'}',
'.blocklyReplaceable .blocklyPathLight,',
'.blocklyReplaceable .blocklyPathDark {',
'display: none;',
'}',
'.blocklyText {',
'cursor: default;',
'fill: #fff;',

View File

@@ -36,6 +36,7 @@ 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.
* @deprecated July 2018. Use InsertionMarkerManager.
* @param {!Blockly.BlockSvg} block The top block in the stack being dragged.
* @constructor
*/

View File

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

View File

@@ -0,0 +1,697 @@
/**
* @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.
* @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.
* @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.<!Blockly.RenderedConnection>}
* @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);
}
}
// Copy field values from the other block. These values may impact the
// rendered size of the insertion marker. Note that we do not care about
// child blocks here.
for (var i = 0; i < sourceBlock.inputList.length; i++) {
var input = sourceBlock.inputList[i];
for (var j = 0; j < input.fieldRow.length; j++) {
var field = input.fieldRow[j];
result.setFieldValue(field.getValue(), field.name);
}
}
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.<!Blockly.RenderedConnection>} 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) {
// 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.
if (local.type == Blockly.OUTPUT_VALUE) {
// Insert the dragged block into the stack if possible.
if (!closest.isConnected() ||
Blockly.Connection.lastConnectionInRow_(this.topBlock_,
closest.targetConnection.getSourceBlock())) {
return false; // Insert.
}
// Otherwise replace the existing block and bump it out.
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()) {
console.log("trying to connect to an insertion marker");
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_();
}
// Also highlight the actual connection, as a nod to previous behaviour.
if (this.closestConnection_) {
this.closestConnection_.highlight();
}
};
/**
* 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_();
} else {
// 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.closestConnection_) {
this.closestConnection_.unhighlight();
}
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.
*/
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_;
// TODO: remove?
closest.sourceBlock_.highlightShapeForInput(closest, true);
}
this.highlightingBlock_ = true;
};
/**
* Get rid of the highlighting marking the block that will be replaced.
*/
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');
// Position based on the calculated connection locations.
imBlock.positionNewBlock(imBlock, imConn, closest);
// Connect() also renders the insertion marker.
imConn.connect(closest);
this.markerConnection_ = imConn;
};
/**** End insertion marker display functions ****/

View File

@@ -279,6 +279,7 @@ function helper_searchDB(db, x, y, radius, shared_workspace) {
var tempConn = helper_createConnection(x, y,
Blockly.NEXT_STATEMENT, shared_workspace, true);
tempConn.sourceBlock_ = helper_makeSourceBlock(shared_workspace);
tempConn.sourceBlock_.nextConnection = tempConn;
var closest = db.searchForClosest(tempConn, radius, {x: 0, y: 0});
return closest.connection;
}
@@ -289,7 +290,9 @@ function helper_makeSourceBlock(sharedWorkspace) {
getParent: function() { return null; },
movable_: true,
isMovable: function() { return true; },
isShadow: function() { return false; }
isShadow: function() { return false; },
isInsertionMarker: function() { return false; },
getFirstStatementConnection: function() { return null; }
};
}

View File

@@ -313,6 +313,112 @@ function test_isConnectionAllowed_NoNext() {
assertTrue(two.isConnectionAllowed(one));
}
function test_canConnectToPrevious_alreadyConnected() {
var sharedWorkspace = {};
var one = helper_createConnection(0, 0, Blockly.NEXT_STATEMENT);
one.sourceBlock_ = helper_makeSourceBlock(sharedWorkspace);
one.sourceBlock_.nextConnection = one;
var two = helper_createConnection(0, 0, Blockly.PREVIOUS_STATEMENT);
two.sourceBlock_ = helper_makeSourceBlock(sharedWorkspace);
two.sourceBlock_.previousConnection = two;
Blockly.Connection.connectReciprocally_(one, two);
var three = helper_createConnection(0, 0, Blockly.PREVIOUS_STATEMENT);
three.sourceBlock_ = helper_makeSourceBlock(sharedWorkspace);
three.sourceBlock_.previousConnection = three;
// The next connection is already occupied and cannot disconnect itself
// mid-drag.
assertFalse(one.canConnectToPrevious_(three));
}
function test_canConnect_dragging() {
var sharedWorkspace = {};
var one = helper_createConnection(0, 0, Blockly.NEXT_STATEMENT);
one.sourceBlock_ = helper_makeSourceBlock(sharedWorkspace);
one.sourceBlock_.nextConnection = one;
var two = helper_createConnection(0, 0, Blockly.PREVIOUS_STATEMENT);
two.sourceBlock_ = helper_makeSourceBlock(sharedWorkspace);
two.sourceBlock_.previousConnection = two;
Blockly.Connection.connectReciprocally_(one, two);
Blockly.draggingConnections_.push(one);
Blockly.draggingConnections_.push(two);
assertFalse(two.isConnectionAllowed(one));
assertFalse(one.isConnectionAllowed(two));
}
function test_canConnect_stackStart() {
var sharedWorkspace = {};
var block1Next = helper_createConnection(0, 0, Blockly.NEXT_STATEMENT);
var block1 = helper_makeSourceBlock(sharedWorkspace);
block1Next.sourceBlock_ = block1
block1.nextConnection = block1Next;
var block1Prev = helper_createConnection(0, 0, Blockly.PREVIOUS_STATEMENT);
block1Prev.sourceBlock_ = block1;
block1.previousConnection = block1Prev
var block2Prev = helper_createConnection(0, 0, Blockly.PREVIOUS_STATEMENT);
block2Prev.sourceBlock_ = helper_makeSourceBlock(sharedWorkspace);
block2Prev.sourceBlock_.previousConnection = block2Prev;
Blockly.Connection.connectReciprocally_(block1Next, block2Prev);
var three = helper_createConnection(0, 0, Blockly.NEXT_STATEMENT);
three.sourceBlock_ = helper_makeSourceBlock(sharedWorkspace);
three.sourceBlock_.nextConnection = three;
// Can connect at the beginning of the stack.
assertTrue(three.canConnectToPrevious_(block1Prev));
// But not in the middle of the stack.
assertFalse(three.canConnectToPrevious_(block2Prev));
}
function test_canConnect_stackStart_insertionMarker() {
var sharedWorkspace = {};
var block1Next = helper_createConnection(0, 0, Blockly.NEXT_STATEMENT);
var block1 = helper_makeSourceBlock(sharedWorkspace);
block1.isInsertionMarker = function() {
return true;
}
block1.getPreviousBlock = function() {
return false;
}
block1Next.sourceBlock_ = block1
block1.nextConnection = block1Next;
var block1Prev = helper_createConnection(0, 0, Blockly.PREVIOUS_STATEMENT);
block1Prev.sourceBlock_ = block1;
block1.previousConnection = block1Prev
var block2Prev = helper_createConnection(0, 0, Blockly.PREVIOUS_STATEMENT);
var block2 = helper_makeSourceBlock(sharedWorkspace);
block2Prev.sourceBlock_ = block2;
block2.previousConnection = block2Prev;
block2.getPreviousBlock = function() {
return block1;
}
Blockly.Connection.connectReciprocally_(block1Next, block2Prev);
var three = helper_createConnection(0, 0, Blockly.NEXT_STATEMENT);
three.sourceBlock_ = helper_makeSourceBlock(sharedWorkspace);
three.sourceBlock_.nextConnection = three;
// Can't connect to the previous connection of an insertion marker.
assertFalse(three.isConnectionAllowed(block1Prev));
// But can connect to a previous connection that is already connected to an
// insertion marker.
assertTrue(three.isConnectionAllowed(block2Prev));
}
function testCheckConnection_Okay() {
connectionTest_setUp();
previous.checkConnection_(next);