Movement Updates (#2247)

This is a squash and merge of a large set of changes by @BeksOmega 

* Added functionality to scrolling, dragging, and zooming.

* Fixed incorrect changes to workspaceChanged function.

* Fixed comment.

* Fixed typo.

* Removed scrollbar.set calls from workspace_svg.

* Removed scrollbar.resize() call.

* Added move options to playground.

* Fixed scroll_ calls that replaced scrollbar.set calls.

* Removed this.scrollbar checks.

* Changed zoom so that it always zooms towards the coordinates. Changed isContentBounded_ to be separate from isMovable_ (b/c of the previous change zoomControls had to be added to the bounded check). Fixed scroll_() calls... again.

* Changed procedures so the Highlight definition option is only available if the workspace is moveable.

* Fixed scrollCenter so that it works with flyout toolboxes.

* Fixed zoomToFit so that it works with horizontal flyout toolboxes.

* Fixed Typo.

* Fixed bumping blocks when the workspace is not movable.

* Fixed bumping not working with left and top toolbox positions.

* Re-Added not allowing scrollCenter if the workspace is not movable. Disabled scrollCenter button for this case.

* Cleaned up formatting.

* Fixed bumping... again. Reformatted workspaceChanged a bit.

* Changed blocks to be completely bumped into the workspace.

* Reorganized metrics-getting for workspaceChanged.

* Added bumping workspace comments. Moved event checking.

* Renamed workspaceChanged to bumpObjects.

* Added a bumpObjects developer reminder.

* Added warning to zoomToFit.

* Cleaned up some text.

* Added better inline documentation.

* Fixed up inline docs.

* Cleaned up comments.

* Fixed zoomCenter not actually zooming towards the center.

* Fixed zoomControls error on unmovable bottom-toolbox workspaces

* Fixed programatically placing blocks in an unmovable workspace.

* Removed unnecessary translate call in inject.

* Reversed removal of translate. (apparently it was necessary)

* Cleaned up code in response to first round of reviews.

* Added unit comments to the zoom function.

* Removed bumpObjectsEventChecker. Added BUMP_EVENTS list to Blockly.Events.

* Changed getWorkspaceObjectMetrics call to getBoundingRectangle().

* Fixed utils.mouseToSvg (was causing problems with zoom on wheel if the page was scrolled).

* Fixed zoom when page is scrolled (actually this time). Reverted changes to utils.mouseToSvg.

* Fixed centerOnBlock.

* Added unit docs to translate. Moved setting the grid position to the translate function.

* Added TODO's.
This commit is contained in:
Beka Westberg
2019-02-18 15:28:51 -08:00
committed by Rachel Fenichel
parent c2447e7e8b
commit 9dec2da5c5
8 changed files with 508 additions and 199 deletions

View File

@@ -948,6 +948,12 @@ Blockly.Blocks['procedures_callnoreturn'] = {
* @this Blockly.Block
*/
customContextMenu: function(options) {
if (!this.workspace.isMovable()) {
// If we center on the block and the workspace isn't movable we could
// loose blocks at the edges of the workspace.
return;
}
var option = {enabled: true};
option.text = Blockly.Msg['PROCEDURES_HIGHLIGHT_DEF'];
var name = this.getProcedureCall();

View File

@@ -154,6 +154,21 @@ Blockly.Events.COMMENT_MOVE = 'comment_move';
*/
Blockly.Events.FINISHED_LOADING = 'finished_loading';
/**
* List of events that cause objects to be bumped back into the visible
* portion of the workspace (only used for non-movable workspaces).
*
* Not to be confused with bumping so that disconnected connections to do
* not appear connected.
* @const
*/
Blockly.Events.BUMP_EVENTS = [
Blockly.Events.BLOCK_CREATE,
Blockly.Events.BLOCK_MOVE,
Blockly.Events.COMMENT_CREATE,
Blockly.Events.COMMENT_MOVE
];
/**
* List of events queued for firing.
* @private

View File

@@ -232,71 +232,112 @@ Blockly.createMainWorkspace_ = function(svg, options, blockDragSurface,
mainWorkspace.translate(0, 0);
Blockly.mainWorkspace = mainWorkspace;
if (!options.readOnly && !options.hasScrollbars) {
var workspaceChanged = function(e) {
if (!mainWorkspace.isDragging()) {
var metrics = mainWorkspace.getMetrics();
var edgeLeft = metrics.viewLeft + metrics.absoluteLeft;
var edgeTop = metrics.viewTop + metrics.absoluteTop;
if (metrics.contentTop < edgeTop ||
metrics.contentTop + metrics.contentHeight >
metrics.viewHeight + edgeTop ||
metrics.contentLeft <
(options.RTL ? metrics.viewLeft : edgeLeft) ||
metrics.contentLeft + metrics.contentWidth > (options.RTL ?
metrics.viewWidth : metrics.viewWidth + edgeLeft)) {
// One or more blocks may be out of bounds. Bump them back in.
var MARGIN = 25;
var blocks = mainWorkspace.getTopBlocks(false);
if (!options.readOnly && !mainWorkspace.isMovable()) {
// Helper function for the workspaceChanged callback.
// TODO (#2300): Move metrics math back to the WorkspaceSvg.
var getWorkspaceMetrics = function() {
var workspaceMetrics = Object.create(null);
var defaultMetrics = mainWorkspace.getMetrics();
var scale = mainWorkspace.scale;
// Get the view metrics in workspace units.
workspaceMetrics.viewLeft = defaultMetrics.viewLeft / scale;
workspaceMetrics.viewTop = defaultMetrics.viewTop / scale;
workspaceMetrics.viewRight =
(defaultMetrics.viewLeft + defaultMetrics.viewWidth) / scale;
workspaceMetrics.viewBottom =
(defaultMetrics.viewTop + defaultMetrics.viewHeight) / scale;
// Get the exact content metrics (in workspace units), even if the
// content is bounded.
if (mainWorkspace.isContentBounded()) {
// Already in workspace units, no need to divide by scale.
var blocksBoundingBox = mainWorkspace.getBlocksBoundingBox();
workspaceMetrics.contentLeft = blocksBoundingBox.x;
workspaceMetrics.contentTop = blocksBoundingBox.y;
workspaceMetrics.contentRight =
blocksBoundingBox.x + blocksBoundingBox.width;
workspaceMetrics.contentBottom =
blocksBoundingBox.y + blocksBoundingBox.height;
} else {
workspaceMetrics.contentLeft = defaultMetrics.contentLeft / scale;
workspaceMetrics.contentTop = defaultMetrics.contentTop / scale;
workspaceMetrics.contentRight =
(defaultMetrics.contentLeft + defaultMetrics.contentWidth) / scale;
workspaceMetrics.contentBottom =
(defaultMetrics.contentTop + defaultMetrics.contentHeight) / scale;
}
return workspaceMetrics;
};
var bumpObjects = function(e) {
// We always check isMovable_ again because the original
// "not movable" state of isMovable_ could have been changed.
if (!mainWorkspace.isDragging() && !mainWorkspace.isMovable() &&
(Blockly.Events.BUMP_EVENTS.indexOf(e.type) != -1)) {
var metrics = getWorkspaceMetrics();
if (metrics.contentTop < metrics.viewTop ||
metrics.contentBottom > metrics.viewBottom ||
metrics.contentLeft < metrics.viewLeft ||
metrics.contentRight > metrics.viewRight) {
// Handle undo.
var oldGroup = null;
if (e) {
oldGroup = Blockly.Events.getGroup();
Blockly.Events.setGroup(e.group);
}
var movedBlocks = false;
for (var b = 0, block; block = blocks[b]; b++) {
var blockXY = block.getRelativeToSurfaceXY();
var blockHW = block.getHeightWidth();
// Bump any block that's above the top back inside.
var overflowTop = edgeTop + MARGIN - blockHW.height - blockXY.y;
if (overflowTop > 0) {
block.moveBy(0, overflowTop);
movedBlocks = true;
}
// Bump any block that's below the bottom back inside.
var overflowBottom =
edgeTop + metrics.viewHeight - MARGIN - blockXY.y;
if (overflowBottom < 0) {
block.moveBy(0, overflowBottom);
movedBlocks = true;
}
// Bump any block that's off the left back inside.
var overflowLeft = MARGIN + edgeLeft -
blockXY.x - (options.RTL ? 0 : blockHW.width);
if (overflowLeft > 0) {
block.moveBy(overflowLeft, 0);
movedBlocks = true;
}
// Bump any block that's off the right back inside.
var overflowRight = edgeLeft + metrics.viewWidth - MARGIN -
blockXY.x + (options.RTL ? blockHW.width : 0);
if (overflowRight < 0) {
block.moveBy(overflowRight, 0);
movedBlocks = true;
}
switch (e.type) {
case Blockly.Events.BLOCK_CREATE:
case Blockly.Events.BLOCK_MOVE:
var object = mainWorkspace.getBlockById(e.blockId);
break;
case Blockly.Events.COMMENT_CREATE:
case Blockly.Events.COMMENT_MOVE:
var object = mainWorkspace.getCommentById(e.commentId);
break;
}
var objectMetrics = object.getBoundingRectangle();
// Bump any object that's above the top back inside.
var overflowTop = metrics.viewTop - objectMetrics.topLeft.y;
if (overflowTop > 0) {
object.moveBy(0, overflowTop);
}
// Bump any object that's below the bottom back inside.
var overflowBottom = metrics.viewBottom - objectMetrics.bottomRight.y;
if (overflowBottom < 0) {
object.moveBy(0, overflowBottom);
}
// Bump any object that's off the left back inside.
var overflowLeft = metrics.viewLeft - objectMetrics.topLeft.x;
if (overflowLeft > 0) {
object.moveBy(overflowLeft, 0);
}
// Bump any object that's off the right back inside.
var overflowRight = metrics.viewRight - objectMetrics.bottomRight.x;
if (overflowRight < 0) {
object.moveBy(overflowRight, 0);
}
if (e) {
if (!e.group && movedBlocks) {
console.log('WARNING: Moved blocks in bounds but there was no event group.'
+ ' This may break undo.');
if (!e.group) {
console.log('WARNING: Moved object in bounds but there was no' +
' event group. This may break undo.');
}
Blockly.Events.setGroup(oldGroup);
}
}
}
};
mainWorkspace.addChangeListener(workspaceChanged);
mainWorkspace.addChangeListener(bumpObjects);
}
// The SVG is now fully assembled.
Blockly.svgResize(mainWorkspace);
Blockly.WidgetDiv.createDom();
@@ -339,12 +380,6 @@ Blockly.init_ = function(mainWorkspace) {
mainWorkspace.flyout_.init(mainWorkspace);
mainWorkspace.flyout_.show(options.languageTree.childNodes);
mainWorkspace.flyout_.scrollToStart();
// Translate the workspace sideways to avoid the fixed flyout.
mainWorkspace.scrollX = mainWorkspace.flyout_.width_;
if (options.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) {
mainWorkspace.scrollX *= -1;
}
mainWorkspace.translate(mainWorkspace.scrollX, 0);
}
}
@@ -356,9 +391,31 @@ Blockly.init_ = function(mainWorkspace) {
mainWorkspace.zoomControls_.init(verticalSpacing);
}
if (options.hasScrollbars) {
if (options.moveOptions && options.moveOptions.scrollbars) {
mainWorkspace.scrollbar = new Blockly.ScrollbarPair(mainWorkspace);
mainWorkspace.scrollbar.resize();
} else {
mainWorkspace.setMetrics({x: .5, y: .5});
}
if (mainWorkspace.flyout_) {
// Translate the workspace sideways to avoid the fixed flyout.
switch (mainWorkspace.toolboxPosition) {
case Blockly.TOOLBOX_AT_LEFT:
mainWorkspace.scrollX =
mainWorkspace.RTL ? 0 : mainWorkspace.flyout_.width_;
break;
case Blockly.TOOLBOX_AT_RIGHT:
mainWorkspace.scrollX =
mainWorkspace.RTL ? -mainWorkspace.flyout_.width_ : 0;
break;
case Blockly.TOOLBOX_AT_TOP:
mainWorkspace.scrollY = mainWorkspace.flyout_.height_;
break;
// If the toolbox is at the top left (workspace origin) is untouched,
// so no need to include it.
}
mainWorkspace.translate(mainWorkspace.scrollX, mainWorkspace.scrollY);
}
// Load the sounds.
@@ -380,6 +437,9 @@ Blockly.init_ = function(mainWorkspace) {
*/
Blockly.inject.bindDocumentEvents_ = function() {
if (!Blockly.documentEventsBound_) {
Blockly.bindEventWithChecks_(document, 'scroll', null,
Blockly.mainWorkspace.updateInverseScreenCTM
.bind(Blockly.mainWorkspace));
Blockly.bindEventWithChecks_(document, 'keydown', null, Blockly.onKeyDown_);
// longStop needs to run to stop the context menu from showing up. It
// should run regardless of what other touch event handlers have run.

View File

@@ -100,10 +100,6 @@ Blockly.Options = function(options) {
Blockly.TOOLBOX_AT_RIGHT : Blockly.TOOLBOX_AT_LEFT;
}
var hasScrollbars = options['scrollbars'];
if (hasScrollbars === undefined) {
hasScrollbars = hasCategories;
}
var hasCss = options['css'];
if (hasCss === undefined) {
hasCss = true;
@@ -135,7 +131,9 @@ Blockly.Options = function(options) {
this.maxInstances = options['maxInstances'];
this.pathToMedia = pathToMedia;
this.hasCategories = hasCategories;
this.hasScrollbars = hasScrollbars;
this.moveOptions = Blockly.Options.parseMoveOptions(options, hasCategories);
/** @deprecated January 2019 */
this.hasScrollbars = this.moveOptions.scrollbars;
this.hasTrashcan = hasTrashcan;
this.maxTrashcanContents = maxTrashcanContents;
this.hasSounds = hasSounds;
@@ -165,6 +163,40 @@ Blockly.Options.prototype.setMetrics = null;
*/
Blockly.Options.prototype.getMetrics = null;
/**
* Parse the user-specified move options, using reasonable defaults where
* behavior is unspecified.
* @param {!Object} options Dictionary of options.
* @param {!boolean} hasCategories Whether the workspace has categories or not.
* @return {!Object} A dictionary of normalized options.
* @private
*/
Blockly.Options.parseMoveOptions = function(options, hasCategories) {
var move = options['move'] || {};
var moveOptions = {};
if (move['scrollbars'] === undefined
&& options['scrollbars'] === undefined) {
moveOptions.scrollbars = hasCategories;
} else {
moveOptions.scrollbars = !!move['scrollbars'] || !!options['scrollbars'];
}
if (!moveOptions.scrollbars || move['wheel'] === undefined) {
// Defaults to false so that developers' settings don't appear to change.
moveOptions.wheel = false;
} else {
moveOptions.wheel = !!move['wheel'];
}
if (!moveOptions.scrollbars) {
moveOptions.drag = false;
} else if (move['drag'] === undefined) {
// Defaults to true if scrollbars is true.
moveOptions.drag = true;
} else {
moveOptions.drag = !!move['drag'];
}
return moveOptions;
};
/**
* Parse the user-specified zoom options, using reasonable defaults where
* behaviour is unspecified. See zoom documentation:

View File

@@ -45,15 +45,6 @@ Blockly.WorkspaceDragger = function(workspace) {
*/
this.workspace_ = workspace;
/**
* The workspace's metrics object at the beginning of the drag. Contains size
* and position metrics of a workspace.
* Coordinate system: pixel coordinates.
* @type {!Object}
* @private
*/
this.startDragMetrics_ = workspace.getMetrics();
/**
* The scroll position of the workspace at the beginning of the drag.
* Coordinate system: pixel coordinates.
@@ -102,30 +93,6 @@ Blockly.WorkspaceDragger.prototype.endDrag = function(currentDragDeltaXY) {
* @package
*/
Blockly.WorkspaceDragger.prototype.drag = function(currentDragDeltaXY) {
var metrics = this.startDragMetrics_;
var newXY = goog.math.Coordinate.sum(this.startScrollXY_, currentDragDeltaXY);
// Bound the new XY based on workspace bounds.
var x = Math.min(newXY.x, -metrics.contentLeft);
var y = Math.min(newXY.y, -metrics.contentTop);
x = Math.max(x, metrics.viewWidth - metrics.contentLeft -
metrics.contentWidth);
y = Math.max(y, metrics.viewHeight - metrics.contentTop -
metrics.contentHeight);
x = -x - metrics.contentLeft;
y = -y - metrics.contentTop;
this.updateScroll_(x, y);
};
/**
* Move the scrollbars to drag the workspace.
* x and y are in pixels.
* @param {number} x The new x position to move the scrollbar to.
* @param {number} y The new y position to move the scrollbar to.
* @private
*/
Blockly.WorkspaceDragger.prototype.updateScroll_ = function(x, y) {
this.workspace_.scrollbar.set(x, y);
this.workspace_.scroll(newXY.x, newXY.y);
};

View File

@@ -168,13 +168,69 @@ Blockly.WorkspaceSvg.prototype.isMutator = false;
Blockly.WorkspaceSvg.prototype.resizesEnabled_ = true;
/**
* Current horizontal scrolling offset in pixel units.
* Current horizontal scrolling offset in pixel units, relative to the
* workspace origin.
*
* It is useful to think about a view, and a canvas moving beneath that
* view. As the canvas moves right, this value becomes more positive, and
* the view is now "seeing" the left side of the canvas. As the canvas moves
* left, this value becomes more negative, and the view is now "seeing" the
* right side of the canvas.
*
* The confusing thing about this value is that it does not, and must not
* include the absoluteLeft offset. This is because it is used to calculate
* the viewLeft value.
*
* The viewLeft is relative to the workspace origin (although in pixel
* units). The workspace origin is the top-left corner of the workspace (at
* least when it is enabled). It is shifted from the top-left of the blocklyDiv
* so as not to be beneath the toolbox.
*
* When the workspace is enabled the viewLeft and workspace origin are at
* the same X location. As the canvas slides towards the right beneath the view
* this value (scrollX) becomes more positive, and the viewLeft becomes more
* negative relative to the workspace origin (imagine the workspace origin
* as a dot on the canvas sliding to the right as the canvas moves).
*
* So if the scrollX were to include the absoluteLeft this would in a way
* "unshift" the workspace origin. This means that the viewLeft would be
* representing the left edge of the blocklyDiv, rather than the left edge
* of the workspace.
*
* @type {number}
*/
Blockly.WorkspaceSvg.prototype.scrollX = 0;
/**
* Current vertical scrolling offset in pixel units.
* Current vertical scrolling offset in pixel units, relative to the
* workspace origin.
*
* It is useful to think about a view, and a canvas moving beneath that
* view. As the canvas moves down, this value becomes more positive, and the
* view is now "seeing" the upper part of the canvas. As the canvas moves
* up, this value becomes more negative, and the view is "seeing" the lower
* part of the canvas.
*
* This confusing thing about this value is that it does not, and must not
* include the absoluteTop offset. This is because it is used to calculate
* the viewTop value.
*
* The viewTop is relative to the workspace origin (although in pixel
* units). The workspace origin is the top-left corner of the workspace (at
* least when it is enabled). It is shifted from the top-left of the
* blocklyDiv so as not to be beneath the toolbox.
*
* When the workspace is enabled the viewTop and workspace origin are at the
* same Y location. As the canvas slides towards the bottom this value
* (scrollY) becomes more positive, and the viewTop becomes more negative
* relative to the workspace origin (image in the workspace origin as a dot
* on the canvas sliding downwards as the canvas moves).
*
* So if the scrollY were to include the absoluteTop this would in a way
* "unshift" the workspace origin. This means that the viewTop would be
* representing the top edge of the blocklyDiv, rather than the top edge of
* the workspace.
*
* @type {number}
*/
Blockly.WorkspaceSvg.prototype.scrollY = 0;
@@ -470,11 +526,8 @@ Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) {
if (!this.isFlyout) {
Blockly.bindEventWithChecks_(this.svgGroup_, 'mousedown', this,
this.onMouseDown_, false, true);
if (this.options.zoomOptions && this.options.zoomOptions.wheel) {
// Mouse-wheel.
Blockly.bindEventWithChecks_(this.svgGroup_, 'wheel', this,
this.onMouseWheel_);
}
Blockly.bindEventWithChecks_(this.svgGroup_, 'wheel', this,
this.onMouseWheel_);
}
// Determine if there needs to be a category tree, or a simple list of
@@ -675,10 +728,9 @@ Blockly.WorkspaceSvg.prototype.resizeContents = function() {
return;
}
if (this.scrollbar) {
// TODO(picklesrus): Once rachel-fenichel's scrollbar refactoring
// is complete, call the method that only resizes scrollbar
// based on contents.
this.scrollbar.resize();
var metrics = this.getMetrics();
this.scrollbar.hScroll.resizeContentHorizontal(metrics);
this.scrollbar.vScroll.resizeContentVertical(metrics);
}
this.updateInverseScreenCTM();
};
@@ -762,8 +814,10 @@ Blockly.WorkspaceSvg.prototype.getParentSvg = function() {
/**
* Translate this workspace to new coordinates.
* @param {number} x Horizontal translation.
* @param {number} y Vertical translation.
* @param {number} x Horizontal translation, in pixel units relative to the
* top left of the blockly div.
* @param {number} y Vertical translation, in pixel units relative to the
* top left of the blockly div.
*/
Blockly.WorkspaceSvg.prototype.translate = function(x, y) {
if (this.useWorkspaceDragSurface_ && this.isDragSurfaceActive_) {
@@ -778,6 +832,10 @@ Blockly.WorkspaceSvg.prototype.translate = function(x, y) {
if (this.blockDragSurface_) {
this.blockDragSurface_.translateAndScaleGroup(x, y, this.scale);
}
// And update the grid if we're using one.
if (this.grid_) {
this.grid_.moveTo(x, y);
}
};
/**
@@ -1200,11 +1258,44 @@ Blockly.WorkspaceSvg.prototype.isDragging = function() {
};
/**
* Is this workspace draggable and scrollable?
* Is this workspace draggable?
* @return {boolean} True if this workspace may be dragged.
*/
Blockly.WorkspaceSvg.prototype.isDraggable = function() {
return !!this.scrollbar;
return this.options.moveOptions && this.options.moveOptions.drag;
};
/**
* Should the workspace have bounded content? Used to tell if the
* workspace's content should be sized so that it can move (bounded) or not
* (exact sizing).
* @returns {boolean} True if the workspace should be bounded, false otherwise.
* @package
*/
Blockly.WorkspaceSvg.prototype.isContentBounded = function() {
return (this.options.moveOptions && this.options.moveOptions.scrollbars)
|| (this.options.moveOptions && this.options.moveOptions.wheel)
|| (this.options.moveOptions && this.options.moveOptions.drag)
|| (this.options.zoomOptions && this.options.zoomOptions.controls)
|| (this.options.zoomOptions && this.options.zoomOptions.wheel);
};
/**
* Is this workspace movable?
*
* This means the user can reposition the X Y coordinates of the workspace
* through input. This can be through scrollbars, scroll wheel, dragging, or
* through zooming with the scroll wheel (since the zoom is centered on the
* mouse position). This does not include zooming with the zoom controls
* since the X Y coordinates are decided programmatically.
* @returns {boolean} True if the workspace is movable, false otherwise.
* @package
*/
Blockly.WorkspaceSvg.prototype.isMovable = function() {
return (this.options.moveOptions && this.options.moveOptions.scrollbars)
|| (this.options.moveOptions && this.options.moveOptions.wheel)
|| (this.options.moveOptions && this.options.moveOptions.drag)
|| (this.options.zoomOptions && this.options.zoomOptions.wheel);
};
/**
@@ -1213,17 +1304,43 @@ Blockly.WorkspaceSvg.prototype.isDraggable = function() {
* @private
*/
Blockly.WorkspaceSvg.prototype.onMouseWheel_ = function(e) {
var canWheelZoom = this.options.zoomOptions && this.options.zoomOptions.wheel;
var canWheelMove = this.options.moveOptions && this.options.moveOptions.wheel;
if (!canWheelZoom && !canWheelMove) {
return;
}
// TODO: Remove gesture cancellation and compensate for coordinate skew during
// zoom.
if (this.currentGesture_) {
this.currentGesture_.cancel();
}
// The vertical scroll distance that corresponds to a click of a zoom button.
var PIXELS_PER_ZOOM_STEP = 50;
var delta = -e.deltaY / PIXELS_PER_ZOOM_STEP;
var position = Blockly.utils.mouseToSvg(e, this.getParentSvg(),
this.getInverseScreenCTM());
this.zoom(position.x, position.y, delta);
// TODO (#2301): Change '10' from magic number to constant variable. Also
// change in flyout_vertical.js and flyout_horizontal.js.
// Multiplier variable, so that non-pixel-deltaModes are supported.
var multiplier = e.deltaMode === 0x1 ? 10 : 1;
if (canWheelZoom && (e.ctrlKey || !canWheelMove)) {
// The vertical scroll distance that corresponds to a click of a zoom button.
var PIXELS_PER_ZOOM_STEP = 50;
var delta = -e.deltaY / PIXELS_PER_ZOOM_STEP * multiplier;
var position = Blockly.utils.mouseToSvg(e, this.getParentSvg(),
this.getInverseScreenCTM());
this.zoom(position.x, position.y, delta);
} else {
var x = this.scrollX - e.deltaX * multiplier;
var y = this.scrollY - e.deltaY * multiplier;
if (e.shiftKey && e.deltaX === 0) {
// Scroll horizontally (based on vertical scroll delta)
// This is needed as for some browser/system combinations which do not
// set deltaX.
x = this.scrollX - e.deltaY * multiplier;
y = this.scrollY; // Don't scroll vertically
}
this.scroll(x, y);
}
e.preventDefault();
};
@@ -1319,7 +1436,7 @@ Blockly.WorkspaceSvg.prototype.showContextMenu_ = function(e) {
menuOptions.push(redoOption);
// Option to clean up blocks.
if (this.scrollbar) {
if (this.isMovable()) {
var cleanOption = {};
cleanOption.text = Blockly.Msg['CLEAN_UP'];
cleanOption.enabled = topBlocks.length > 1;
@@ -1516,43 +1633,53 @@ Blockly.WorkspaceSvg.prototype.setBrowserFocus = function() {
};
/**
* Zooming the blocks centered in (x, y) coordinate with zooming in or out.
* @param {number} x X coordinate of center.
* @param {number} y Y coordinate of center.
* @param {number} amount Amount of zooming
* (negative zooms out and positive zooms in).
* Zooms the workspace in or out relative to/centered on the given (x, y)
* coordinate.
* @param {number} x X coordinate of center, in pixel units relative to the
* top-left corner of the parentSVG.
* @param {number} y Y coordinate of center, in pixel units relative to the
* top-left corner of the parentSVG.
* @param {number} amount Amount of zooming. The formula for the new scale
* is newScale = currentScale * (scaleSpeed^amount). scaleSpeed is set in
* the workspace options. Negative amount values zoom out, and positive
* amount values zoom in.
*/
Blockly.WorkspaceSvg.prototype.zoom = function(x, y, amount) {
var speed = this.options.zoomOptions.scaleSpeed;
var metrics = this.getMetrics();
var center = this.getParentSvg().createSVGPoint();
center.x = x;
center.y = y;
center = center.matrixTransform(this.getCanvas().getCTM().inverse());
x = center.x;
y = center.y;
var canvas = this.getCanvas();
// Scale factor.
var speed = this.options.zoomOptions.scaleSpeed;
var scaleChange = Math.pow(speed, amount);
// Clamp scale within valid range.
var newScale = this.scale * scaleChange;
if (this.scale == newScale) {
return; // No change in zoom.
}
// Clamp scale within valid range.
if (newScale > this.options.zoomOptions.maxScale) {
scaleChange = this.options.zoomOptions.maxScale / this.scale;
} else if (newScale < this.options.zoomOptions.minScale) {
scaleChange = this.options.zoomOptions.minScale / this.scale;
}
if (this.scale == newScale) {
return; // No change in zoom.
}
if (this.scrollbar) {
var matrix = canvas.getCTM()
.translate(x * (1 - scaleChange), y * (1 - scaleChange))
.scale(scaleChange);
// newScale and matrix.a should be identical (within a rounding error).
// ScrollX and scrollY are in pixels.
this.scrollX = matrix.e - metrics.absoluteLeft;
this.scrollY = matrix.f - metrics.absoluteTop;
}
// Transform the x/y coordinates from the parentSVG's space into the
// canvas' space, so that they are in workspace units relative to the top
// left of the visible portion of the workspace.
var matrix = this.getCanvas().getCTM();
var center = this.getParentSvg().createSVGPoint();
center.x = x;
center.y = y;
center = center.matrixTransform(matrix.inverse());
x = center.x;
y = center.y;
// Find the new scrollX/scrollY so that the center remains in the same
// position (relative to the center) after we zoom.
var matrix = matrix.translate(x * (1 - scaleChange), y * (1 - scaleChange))
.scale(scaleChange);
// newScale and matrix.a should be identical (within a rounding error).
// ScrollX and scrollY are in pixels.
var metrics = this.getMetrics();
this.scrollX = matrix.e - metrics.absoluteLeft;
this.scrollY = matrix.f - metrics.absoluteTop;
this.setScale(newScale);
};
@@ -1562,8 +1689,8 @@ Blockly.WorkspaceSvg.prototype.zoom = function(x, y, amount) {
*/
Blockly.WorkspaceSvg.prototype.zoomCenter = function(type) {
var metrics = this.getMetrics();
var x = metrics.viewWidth / 2;
var y = metrics.viewHeight / 2;
var x = (metrics.viewWidth / 2) + metrics.absoluteLeft;
var y = (metrics.viewHeight / 2) + metrics.absoluteTop;
this.zoom(x, y, type);
};
@@ -1571,6 +1698,12 @@ Blockly.WorkspaceSvg.prototype.zoomCenter = function(type) {
* Zoom the blocks to fit in the workspace if possible.
*/
Blockly.WorkspaceSvg.prototype.zoomToFit = function() {
if (!this.isMovable()) {
console.warn('Tried to move a non-movable workspace. This could result' +
' in blocks becoming inaccessible.');
return;
}
var metrics = this.getMetrics();
var blocksBox = this.getBlocksBoundingBox();
var blocksWidth = blocksBox.width;
@@ -1578,18 +1711,22 @@ Blockly.WorkspaceSvg.prototype.zoomToFit = function() {
if (!blocksWidth) {
return; // Prevents zooming to infinity.
}
var workspaceWidth = metrics.viewWidth;
var workspaceHeight = metrics.viewHeight;
if (this.flyout_) {
workspaceWidth -= this.flyout_.width_;
// We add the flyout size to the block size because the flyout contains
// blocks, and we want all of the blocks to fit within the view. If we
// don't add them, they'll end up overlapping.
if (this.horizontalLayout) {
// Convert from pixels to workspace coordinates.
blocksHeight += this.flyout_.height_ / this.scale;
} else {
// Convert from pixels to workspace coordinates.
blocksWidth += this.flyout_.width_ / this.scale;
}
}
if (!this.scrollbar) {
// Origin point of 0,0 is fixed, blocks will not scroll to center.
blocksWidth += metrics.contentLeft;
blocksHeight += metrics.contentTop;
}
var ratioX = workspaceWidth / blocksWidth;
var ratioY = workspaceHeight / blocksHeight;
// Scale Units: (pixels / workspaceUnit)
var ratioX = metrics.viewWidth / blocksWidth;
var ratioY = metrics.viewHeight / blocksHeight;
this.setScale(Math.min(ratioX, ratioY));
this.scrollCenter();
};
@@ -1620,22 +1757,42 @@ Blockly.WorkspaceSvg.prototype.endCanvasTransition = function() {
/** @type {!SVGElement} */ (this.svgBubbleCanvas_),
'blocklyCanvasTransitioning');
};
/**
* Center the workspace.
*/
Blockly.WorkspaceSvg.prototype.scrollCenter = function() {
if (!this.scrollbar) {
// Can't center a non-scrolling workspace.
console.warn('Tried to scroll a non-scrollable workspace.');
if (!this.isMovable()) {
console.warn('Tried to move a non-movable workspace. This could result' +
' in blocks becoming inaccessible.');
return;
}
var metrics = this.getMetrics();
var x = (metrics.contentWidth - metrics.viewWidth) / 2;
if (this.flyout_) {
x -= this.flyout_.width_ / 2;
}
var y = (metrics.contentHeight - metrics.viewHeight) / 2;
this.scrollbar.set(x, y);
// Shift the x and y to disregard the permanent flyout (if it exists).
if (this.flyout_) {
switch (this.toolboxPosition) {
case Blockly.TOOLBOX_AT_LEFT:
x -= this.flyout_.width_ / 2;
break;
case Blockly.TOOLBOX_AT_RIGHT:
x += this.flyout_.width_ / 2;
break;
case Blockly.TOOLBOX_AT_TOP:
y -= this.flyout_.height_ / 2;
break;
case Blockly.TOOLBOX_AT_BOTTOM:
y += this.flyout_.height_ / 2;
break;
}
}
// Convert from workspace directions to canvas directions.
x = -x - metrics.contentLeft;
y = -y - metrics.contentTop;
this.scroll(x, y);
};
/**
@@ -1644,8 +1801,9 @@ Blockly.WorkspaceSvg.prototype.scrollCenter = function() {
* @public
*/
Blockly.WorkspaceSvg.prototype.centerOnBlock = function(id) {
if (!this.scrollbar) {
console.warn('Tried to scroll a non-scrollable workspace.');
if (!this.isMovable()) {
console.warn('Tried to move a non-movable workspace. This could result' +
' in blocks becoming inaccessible.');
return;
}
@@ -1689,13 +1847,17 @@ Blockly.WorkspaceSvg.prototype.centerOnBlock = function(id) {
var scrollToCenterX = scrollToBlockX - halfViewWidth;
var scrollToCenterY = scrollToBlockY - halfViewHeight;
// Convert from workspace directions to canvas directions.
var x = -scrollToCenterX - metrics.contentLeft;
var y = -scrollToCenterY - metrics.contentTop;
Blockly.hideChaff();
this.scrollbar.set(scrollToCenterX, scrollToCenterY);
this.scroll(x, y);
};
/**
* Set the workspace's zoom factor.
* @param {number} newScale Zoom factor.
* @param {number} newScale Zoom factor. Units: (pixels / workspaceUnit).
*/
Blockly.WorkspaceSvg.prototype.setScale = function(newScale) {
if (this.options.zoomOptions.maxScale &&
@@ -1709,10 +1871,14 @@ Blockly.WorkspaceSvg.prototype.setScale = function(newScale) {
if (this.grid_) {
this.grid_.update(this.scale);
}
// We call scroll instead of scrollbar.resize() so that we can center the
// zoom correctly without scrollbars, but scroll does not resize the
// scrollbars so we have to call resizeContent as well.
this.scroll(this.scrollX, this.scrollY);
if (this.scrollbar) {
this.scrollbar.resize();
} else {
this.translate(this.scrollX, this.scrollY);
var metrics = this.getMetrics();
this.scrollbar.hScroll.resizeContentHorizontal(metrics);
this.scrollbar.vScroll.resizeContentVertical(metrics);
}
Blockly.hideChaff(false);
if (this.flyout_) {
@@ -1721,6 +1887,58 @@ Blockly.WorkspaceSvg.prototype.setScale = function(newScale) {
}
};
/**
* Scroll the workspace to a specified offset (in pixels), keeping in the
* workspace bounds. See comment on workspaceSvg.scrollX for more detail on
* the meaning of these values.
* @param {number} x Target X to scroll to.
* @param {number} y Target Y to scroll to.
* @package
*/
Blockly.WorkspaceSvg.prototype.scroll = function(x, y) {
// Keep scrolling within the bounds of the content.
var metrics = this.getMetrics();
// This is the offset of the top-left corner of the view from the
// workspace origin when the view is "seeing" the bottom-right corner of
// the content.
var maxOffsetOfViewFromOriginX = metrics.contentWidth + metrics.contentLeft -
metrics.viewWidth;
var maxOffsetOfViewFromOriginY = metrics.contentHeight + metrics.contentTop -
metrics.viewHeight;
// Canvas coordinates (aka scroll coordinates) have inverse directionality
// to workspace coordinates so we have to inverse them.
x = Math.min(x, -metrics.contentLeft);
y = Math.min(y, -metrics.contentTop);
x = Math.max(x, -maxOffsetOfViewFromOriginX);
y = Math.max(y, -maxOffsetOfViewFromOriginY);
this.scrollX = x;
this.scrollY = y;
if (this.scrollbar) {
// The content position (displacement from the content's top-left to the
// origin) plus the scroll position (displacement from the view's top-left
// to the origin) gives us the distance from the view's top-left to the
// content's top-left. Then we negate this so we get the displacement from
// the content's top-left to the view's top-left, matching the
// directionality of the scrollbars.
// TODO (#2299): Change these to not use the internal ratio_ property.
this.scrollbar.hScroll.setHandlePosition(-(x + metrics.contentLeft) *
this.scrollbar.hScroll.ratio_);
this.scrollbar.vScroll.setHandlePosition(-(y + metrics.contentTop) *
this.scrollbar.vScroll.ratio_);
}
// Hide the WidgetDiv without animation. This is to prevent a disposal
// animation from happening in the wrong location.
Blockly.WidgetDiv.hide(true);
// We have to shift the translation so that when the canvas is at 0, 0 the
// workspace origin is not underneath the toolbox.
x += metrics.absoluteLeft;
y += metrics.absoluteTop;
this.translate(x, y);
};
/**
* Get the dimensions of the given workspace component, in pixels.
* @param {Blockly.Toolbox|Blockly.Flyout} elem The element to get the
@@ -1757,7 +1975,7 @@ Blockly.WorkspaceSvg.getDimensionsPx_ = function(elem) {
* @private
*/
Blockly.WorkspaceSvg.getContentDimensions_ = function(ws, svgSize) {
if (ws.scrollbar) {
if (ws.isContentBounded()) {
return Blockly.WorkspaceSvg.getContentDimensionsBounded_(ws, svgSize);
} else {
return Blockly.WorkspaceSvg.getContentDimensionsExact_(ws);
@@ -1836,22 +2054,27 @@ Blockly.WorkspaceSvg.getContentDimensionsBounded_ = function(ws, svgSize) {
/**
* Return an object with all the metrics required to size scrollbars for a
* top level workspace. The following properties are computed:
* Coordinate system: pixel coordinates.
* .viewHeight: Height of the visible rectangle,
* .viewWidth: Width of the visible rectangle,
* .contentHeight: Height of the contents,
* .contentWidth: Width of the content,
* .viewTop: Offset of top edge of visible rectangle from parent,
* .viewLeft: Offset of left edge of visible rectangle from parent,
* .contentTop: Offset of the top-most content from the y=0 coordinate,
* .contentLeft: Offset of the left-most content from the x=0 coordinate.
* .absoluteTop: Top-edge of view.
* .absoluteLeft: Left-edge of view.
* .toolboxWidth: Width of toolbox, if it exists. Otherwise zero.
* .toolboxHeight: Height of toolbox, if it exists. Otherwise zero.
* Coordinate system: pixel coordinates, -left, -up, +right, +down
* .viewHeight: Height of the visible portion of the workspace.
* .viewWidth: Width of the visible portion of the workspace.
* .contentHeight: Height of the content.
* .contentWidth: Width of the content.
* .viewTop: Top-edge of the visible portion of the workspace, relative to
* the workspace origin.
* .viewLeft: Left-edge of the visible portion of the workspace, relative to
* the workspace origin.
* .contentTop: Top-edge of the content, relative to the workspace origin.
* .contentLeft: Left-edge of the content relative to the workspace origin.
* .absoluteTop: Top-edge of the visible portion of the workspace, relative
* to the blocklyDiv.
* .absoluteLeft: Left-edge of the visible portion of the workspace, relative
* to the blocklyDiv.
* .toolboxWidth: Width of the toolbox, if it exists. Otherwise zero.
* .toolboxHeight: Height of the toolbox, if it exists. Otherwise zero.
* .flyoutWidth: Width of the flyout if it is always open. Otherwise zero.
* .flyoutHeight: Height of flyout if it is always open. Otherwise zero.
* .toolboxPosition: Top, bottom, left or right.
* .flyoutHeight: Height of the flyout if it is always open. Otherwise zero.
* .toolboxPosition: Top, bottom, left or right. Use TOOLBOX_AT constants to
* compare.
* @return {!Object} Contains size and position metrics of a top level
* workspace.
* @private
@@ -1917,17 +2140,13 @@ Blockly.WorkspaceSvg.getTopLevelWorkspaceMetrics_ = function() {
};
/**
* Sets the X/Y translations of a top level workspace to match the scrollbars.
* Sets the X/Y translations of a top level workspace.
* @param {!Object} xyRatio Contains an x and/or y property which is a float
* between 0 and 1 specifying the degree of scrolling.
* @private
* @this Blockly.WorkspaceSvg
*/
Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_ = function(xyRatio) {
if (!this.scrollbar) {
throw Error('Attempt to set top level workspace scroll without ' +
'scrollbars.');
}
var metrics = this.getMetrics();
if (typeof xyRatio.x == 'number') {
this.scrollX = -metrics.contentWidth * xyRatio.x - metrics.contentLeft;
@@ -1935,12 +2154,12 @@ Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_ = function(xyRatio) {
if (typeof xyRatio.y == 'number') {
this.scrollY = -metrics.contentHeight * xyRatio.y - metrics.contentTop;
}
// We have to shift the translation so that when the canvas is at 0, 0 the
// workspace origin is not underneath the toolbox.
var x = this.scrollX + metrics.absoluteLeft;
var y = this.scrollY + metrics.absoluteTop;
// We could call scroll here, but that has extra checks we don't need to do.
this.translate(x, y);
if (this.grid_) {
this.grid_.moveTo(x, y);
}
};
/**

View File

@@ -102,7 +102,11 @@ Blockly.ZoomControls.prototype.createDom = function() {
var rnd = String(Math.random()).substring(2);
this.createZoomOutSvg_(rnd);
this.createZoomInSvg_(rnd);
this.createZoomResetSvg_(rnd);
if (this.workspace_.isMovable()) {
// If we zoom to the center and the workspace isn't movable we could
// loose blocks at the edges of the workspace.
this.createZoomResetSvg_(rnd);
}
return this.svgGroup_;
};
@@ -158,7 +162,9 @@ Blockly.ZoomControls.prototype.position = function() {
if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) {
this.top_ = this.verticalSpacing_;
this.zoomInGroup_.setAttribute('transform', 'translate(0, 34)');
this.zoomResetGroup_.setAttribute('transform', 'translate(0, 77)');
if (this.zoomResetGroup_) {
this.zoomResetGroup_.setAttribute('transform', 'translate(0, 77)');
}
} else {
this.top_ = metrics.viewHeight + metrics.absoluteTop -
this.HEIGHT_ - this.verticalSpacing_;

View File

@@ -109,7 +109,11 @@ function start() {
oneBasedIndex: true,
readOnly: false,
rtl: rtl,
scrollbars: true,
move: {
scrollbars: true,
drag: true,
wheel: false,
},
toolbox: toolbox,
toolboxPosition: side == 'top' || side == 'start' ? 'start' : 'end',
zoom: