summaryrefslogtreecommitdiff
path: root/src/blockly/core/workspace_svg.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/blockly/core/workspace_svg.js')
-rw-r--r--src/blockly/core/workspace_svg.js1401
1 files changed, 1401 insertions, 0 deletions
diff --git a/src/blockly/core/workspace_svg.js b/src/blockly/core/workspace_svg.js
new file mode 100644
index 0000000..a3c0b53
--- /dev/null
+++ b/src/blockly/core/workspace_svg.js
@@ -0,0 +1,1401 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2014 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 Object representing a workspace rendered as SVG.
+ * @author fraser@google.com (Neil Fraser)
+ */
+'use strict';
+
+goog.provide('Blockly.WorkspaceSvg');
+
+// TODO(scr): Fix circular dependencies
+//goog.require('Blockly.BlockSvg');
+goog.require('Blockly.ConnectionDB');
+goog.require('Blockly.constants');
+goog.require('Blockly.Options');
+goog.require('Blockly.ScrollbarPair');
+goog.require('Blockly.Trashcan');
+goog.require('Blockly.Workspace');
+goog.require('Blockly.Xml');
+goog.require('Blockly.ZoomControls');
+
+goog.require('goog.dom');
+goog.require('goog.math.Coordinate');
+goog.require('goog.userAgent');
+
+
+/**
+ * Class for a workspace. This is an onscreen area with optional trashcan,
+ * scrollbars, bubbles, and dragging.
+ * @param {!Blockly.Options} options Dictionary of options.
+ * @extends {Blockly.Workspace}
+ * @constructor
+ */
+Blockly.WorkspaceSvg = function(options) {
+ Blockly.WorkspaceSvg.superClass_.constructor.call(this, options);
+ this.getMetrics =
+ options.getMetrics || Blockly.WorkspaceSvg.getTopLevelWorkspaceMetrics_;
+ this.setMetrics =
+ options.setMetrics || Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_;
+
+ Blockly.ConnectionDB.init(this);
+
+ /**
+ * Database of pre-loaded sounds.
+ * @private
+ * @const
+ */
+ this.SOUNDS_ = Object.create(null);
+};
+goog.inherits(Blockly.WorkspaceSvg, Blockly.Workspace);
+
+/**
+ * Wrapper function called when a resize event occurs.
+ * @type {Array.<!Array>} Data that can be passed to unbindEvent_
+ */
+Blockly.WorkspaceSvg.prototype.resizeHandlerWrapper_ = null;
+
+/**
+ * Svg workspaces are user-visible (as opposed to a headless workspace).
+ * @type {boolean} True if visible. False if headless.
+ */
+Blockly.WorkspaceSvg.prototype.rendered = true;
+
+/**
+ * Is this workspace the surface for a flyout?
+ * @type {boolean}
+ */
+Blockly.WorkspaceSvg.prototype.isFlyout = false;
+
+/**
+ * Is this workspace the surface for a mutator?
+ * @type {boolean}
+ * @package
+ */
+Blockly.WorkspaceSvg.prototype.isMutator = false;
+
+/**
+ * Is this workspace currently being dragged around?
+ * DRAG_NONE - No drag operation.
+ * DRAG_BEGIN - Still inside the initial DRAG_RADIUS.
+ * DRAG_FREE - Workspace has been dragged further than DRAG_RADIUS.
+ * @private
+ */
+Blockly.WorkspaceSvg.prototype.dragMode_ = Blockly.DRAG_NONE;
+
+/**
+ * Current horizontal scrolling offset.
+ * @type {number}
+ */
+Blockly.WorkspaceSvg.prototype.scrollX = 0;
+
+/**
+ * Current vertical scrolling offset.
+ * @type {number}
+ */
+Blockly.WorkspaceSvg.prototype.scrollY = 0;
+
+/**
+ * Horizontal scroll value when scrolling started.
+ * @type {number}
+ */
+Blockly.WorkspaceSvg.prototype.startScrollX = 0;
+
+/**
+ * Vertical scroll value when scrolling started.
+ * @type {number}
+ */
+Blockly.WorkspaceSvg.prototype.startScrollY = 0;
+
+/**
+ * Distance from mouse to object being dragged.
+ * @type {goog.math.Coordinate}
+ * @private
+ */
+Blockly.WorkspaceSvg.prototype.dragDeltaXY_ = null;
+
+/**
+ * Current scale.
+ * @type {number}
+ */
+Blockly.WorkspaceSvg.prototype.scale = 1;
+
+/**
+ * The workspace's trashcan (if any).
+ * @type {Blockly.Trashcan}
+ */
+Blockly.WorkspaceSvg.prototype.trashcan = null;
+
+/**
+ * This workspace's scrollbars, if they exist.
+ * @type {Blockly.ScrollbarPair}
+ */
+Blockly.WorkspaceSvg.prototype.scrollbar = null;
+
+/**
+ * Time that the last sound was played.
+ * @type {Date}
+ * @private
+ */
+Blockly.WorkspaceSvg.prototype.lastSound_ = null;
+
+/**
+ * Last known position of the page scroll.
+ * This is used to determine whether we have recalculated screen coordinate
+ * stuff since the page scrolled.
+ * @type {!goog.math.Coordinate}
+ * @private
+ */
+Blockly.WorkspaceSvg.prototype.lastRecordedPageScroll_ = null;
+
+/**
+ * Inverted screen CTM, for use in mouseToSvg.
+ * @type {SVGMatrix}
+ * @private
+ */
+Blockly.WorkspaceSvg.prototype.inverseScreenCTM_ = null;
+
+/**
+ * Getter for the inverted screen CTM.
+ * @return {SVGMatrix} The matrix to use in mouseToSvg
+ */
+Blockly.WorkspaceSvg.prototype.getInverseScreenCTM = function() {
+ return this.inverseScreenCTM_;
+};
+
+/**
+ * Update the inverted screen CTM.
+ */
+Blockly.WorkspaceSvg.prototype.updateInverseScreenCTM = function() {
+ this.inverseScreenCTM_ = this.getParentSvg().getScreenCTM().inverse();
+};
+
+/**
+ * Save resize handler data so we can delete it later in dispose.
+ * @param {!Array.<!Array>} handler Data that can be passed to unbindEvent_.
+ */
+Blockly.WorkspaceSvg.prototype.setResizeHandlerWrapper = function(handler) {
+ this.resizeHandlerWrapper_ = handler;
+};
+
+/**
+ * Create the workspace DOM elements.
+ * @param {string=} opt_backgroundClass Either 'blocklyMainBackground' or
+ * 'blocklyMutatorBackground'.
+ * @return {!Element} The workspace's SVG group.
+ */
+Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) {
+ /**
+ * <g class="blocklyWorkspace">
+ * <rect class="blocklyMainBackground" height="100%" width="100%"></rect>
+ * [Trashcan and/or flyout may go here]
+ * <g class="blocklyBlockCanvas"></g>
+ * <g class="blocklyBubbleCanvas"></g>
+ * [Scrollbars may go here]
+ * </g>
+ * @type {SVGElement}
+ */
+ this.svgGroup_ = Blockly.createSvgElement('g',
+ {'class': 'blocklyWorkspace'}, null);
+ if (opt_backgroundClass) {
+ /** @type {SVGElement} */
+ this.svgBackground_ = Blockly.createSvgElement('rect',
+ {'height': '100%', 'width': '100%', 'class': opt_backgroundClass},
+ this.svgGroup_);
+ if (opt_backgroundClass == 'blocklyMainBackground') {
+ this.svgBackground_.style.fill =
+ 'url(#' + this.options.gridPattern.id + ')';
+ }
+ }
+ /** @type {SVGElement} */
+ this.svgBlockCanvas_ = Blockly.createSvgElement('g',
+ {'class': 'blocklyBlockCanvas'}, this.svgGroup_, this);
+ /** @type {SVGElement} */
+ this.svgBubbleCanvas_ = Blockly.createSvgElement('g',
+ {'class': 'blocklyBubbleCanvas'}, this.svgGroup_, this);
+ var bottom = Blockly.Scrollbar.scrollbarThickness;
+ if (this.options.hasTrashcan) {
+ bottom = this.addTrashcan_(bottom);
+ }
+ if (this.options.zoomOptions && this.options.zoomOptions.controls) {
+ bottom = this.addZoomControls_(bottom);
+ }
+
+ if (!this.isFlyout) {
+ Blockly.bindEvent_(this.svgGroup_, 'mousedown', this, this.onMouseDown_);
+ var thisWorkspace = this;
+ Blockly.bindEvent_(this.svgGroup_, 'touchstart', null,
+ function(e) {Blockly.longStart_(e, thisWorkspace);});
+ if (this.options.zoomOptions && this.options.zoomOptions.wheel) {
+ // Mouse-wheel.
+ Blockly.bindEvent_(this.svgGroup_, 'wheel', this, this.onMouseWheel_);
+ }
+ }
+
+ // Determine if there needs to be a category tree, or a simple list of
+ // blocks. This cannot be changed later, since the UI is very different.
+ if (this.options.hasCategories) {
+ this.toolbox_ = new Blockly.Toolbox(this);
+ } else if (this.options.languageTree) {
+ this.addFlyout_();
+ }
+ this.updateGridPattern_();
+ this.recordDeleteAreas();
+ return this.svgGroup_;
+};
+
+/**
+ * Dispose of this workspace.
+ * Unlink from all DOM elements to prevent memory leaks.
+ */
+Blockly.WorkspaceSvg.prototype.dispose = function() {
+ // Stop rerendering.
+ this.rendered = false;
+ Blockly.WorkspaceSvg.superClass_.dispose.call(this);
+ if (this.svgGroup_) {
+ goog.dom.removeNode(this.svgGroup_);
+ this.svgGroup_ = null;
+ }
+ this.svgBlockCanvas_ = null;
+ this.svgBubbleCanvas_ = null;
+ if (this.toolbox_) {
+ this.toolbox_.dispose();
+ this.toolbox_ = null;
+ }
+ if (this.flyout_) {
+ this.flyout_.dispose();
+ this.flyout_ = null;
+ }
+ if (this.trashcan) {
+ this.trashcan.dispose();
+ this.trashcan = null;
+ }
+ if (this.scrollbar) {
+ this.scrollbar.dispose();
+ this.scrollbar = null;
+ }
+ if (this.zoomControls_) {
+ this.zoomControls_.dispose();
+ this.zoomControls_ = null;
+ }
+ if (!this.options.parentWorkspace) {
+ // Top-most workspace. Dispose of the div that the
+ // svg is injected into (i.e. injectionDiv).
+ goog.dom.removeNode(this.getParentSvg().parentNode);
+ }
+ if (this.resizeHandlerWrapper_) {
+ Blockly.unbindEvent_(this.resizeHandlerWrapper_);
+ this.resizeHandlerWrapper_ = null;
+ }
+};
+
+/**
+ * Obtain a newly created block.
+ * @param {?string} prototypeName Name of the language object containing
+ * type-specific functions for this block.
+ * @param {=string} opt_id Optional ID. Use this ID if provided, otherwise
+ * create a new id.
+ * @return {!Blockly.BlockSvg} The created block.
+ */
+Blockly.WorkspaceSvg.prototype.newBlock = function(prototypeName, opt_id) {
+ return new Blockly.BlockSvg(this, prototypeName, opt_id);
+};
+
+/**
+ * Add a trashcan.
+ * @param {number} bottom Distance from workspace bottom to bottom of trashcan.
+ * @return {number} Distance from workspace bottom to the top of trashcan.
+ * @private
+ */
+Blockly.WorkspaceSvg.prototype.addTrashcan_ = function(bottom) {
+ /** @type {Blockly.Trashcan} */
+ this.trashcan = new Blockly.Trashcan(this);
+ var svgTrashcan = this.trashcan.createDom();
+ this.svgGroup_.insertBefore(svgTrashcan, this.svgBlockCanvas_);
+ return this.trashcan.init(bottom);
+};
+
+/**
+ * Add zoom controls.
+ * @param {number} bottom Distance from workspace bottom to bottom of controls.
+ * @return {number} Distance from workspace bottom to the top of controls.
+ * @private
+ */
+Blockly.WorkspaceSvg.prototype.addZoomControls_ = function(bottom) {
+ /** @type {Blockly.ZoomControls} */
+ this.zoomControls_ = new Blockly.ZoomControls(this);
+ var svgZoomControls = this.zoomControls_.createDom();
+ this.svgGroup_.appendChild(svgZoomControls);
+ return this.zoomControls_.init(bottom);
+};
+
+/**
+ * Add a flyout.
+ * @private
+ */
+Blockly.WorkspaceSvg.prototype.addFlyout_ = function() {
+ var workspaceOptions = {
+ disabledPatternId: this.options.disabledPatternId,
+ parentWorkspace: this,
+ RTL: this.RTL,
+ horizontalLayout: this.horizontalLayout,
+ toolboxPosition: this.options.toolboxPosition
+ };
+ /** @type {Blockly.Flyout} */
+ this.flyout_ = new Blockly.Flyout(workspaceOptions);
+ this.flyout_.autoClose = false;
+ var svgFlyout = this.flyout_.createDom();
+ this.svgGroup_.insertBefore(svgFlyout, this.svgBlockCanvas_);
+};
+
+/**
+ * Update items that use screen coordinate calculations
+ * because something has changed (e.g. scroll position, window size).
+ * @private
+ */
+Blockly.WorkspaceSvg.prototype.updateScreenCalculations_ = function() {
+ this.updateInverseScreenCTM();
+ this.recordDeleteAreas();
+};
+
+/**
+ * Resize the parts of the workspace that change when the workspace
+ * contents (e.g. block positions) change. This will also scroll the
+ * workspace contents if needed.
+ * @package
+ */
+Blockly.WorkspaceSvg.prototype.resizeContents = function() {
+ 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();
+ }
+ this.updateInverseScreenCTM();
+};
+
+/**
+ * Resize and reposition all of the workspace chrome (toolbox,
+ * trash, scrollbars etc.)
+ * This should be called when something changes that
+ * requires recalculating dimensions and positions of the
+ * trash, zoom, toolbox, etc. (e.g. window resize).
+ */
+Blockly.WorkspaceSvg.prototype.resize = function() {
+ if (this.toolbox_) {
+ this.toolbox_.position();
+ }
+ if (this.flyout_) {
+ this.flyout_.position();
+ }
+ if (this.trashcan) {
+ this.trashcan.position();
+ }
+ if (this.zoomControls_) {
+ this.zoomControls_.position();
+ }
+ if (this.scrollbar) {
+ this.scrollbar.resize();
+ }
+ this.updateScreenCalculations_();
+};
+
+/**
+ * Resizes and repositions workspace chrome if the page has a new
+ * scroll position.
+ * @package
+ */
+Blockly.WorkspaceSvg.prototype.updateScreenCalculationsIfScrolled
+ = function() {
+ /* eslint-disable indent */
+ var currScroll = goog.dom.getDocumentScroll();
+ if (!goog.math.Coordinate.equals(this.lastRecordedPageScroll_,
+ currScroll)) {
+ this.lastRecordedPageScroll_ = currScroll;
+ this.updateScreenCalculations_();
+ }
+}; /* eslint-enable indent */
+
+/**
+ * Get the SVG element that forms the drawing surface.
+ * @return {!Element} SVG element.
+ */
+Blockly.WorkspaceSvg.prototype.getCanvas = function() {
+ return this.svgBlockCanvas_;
+};
+
+/**
+ * Get the SVG element that forms the bubble surface.
+ * @return {!SVGGElement} SVG element.
+ */
+Blockly.WorkspaceSvg.prototype.getBubbleCanvas = function() {
+ return this.svgBubbleCanvas_;
+};
+
+/**
+ * Get the SVG element that contains this workspace.
+ * @return {!Element} SVG element.
+ */
+Blockly.WorkspaceSvg.prototype.getParentSvg = function() {
+ if (this.cachedParentSvg_) {
+ return this.cachedParentSvg_;
+ }
+ var element = this.svgGroup_;
+ while (element) {
+ if (element.tagName == 'svg') {
+ this.cachedParentSvg_ = element;
+ return element;
+ }
+ element = element.parentNode;
+ }
+ return null;
+};
+
+/**
+ * Translate this workspace to new coordinates.
+ * @param {number} x Horizontal translation.
+ * @param {number} y Vertical translation.
+ */
+Blockly.WorkspaceSvg.prototype.translate = function(x, y) {
+ var translation = 'translate(' + x + ',' + y + ') ' +
+ 'scale(' + this.scale + ')';
+ this.svgBlockCanvas_.setAttribute('transform', translation);
+ this.svgBubbleCanvas_.setAttribute('transform', translation);
+};
+
+/**
+ * Returns the horizontal offset of the workspace.
+ * Intended for LTR/RTL compatibility in XML.
+ * @return {number} Width.
+ */
+Blockly.WorkspaceSvg.prototype.getWidth = function() {
+ var metrics = this.getMetrics();
+ return metrics ? metrics.viewWidth / this.scale : 0;
+};
+
+/**
+ * Toggles the visibility of the workspace.
+ * Currently only intended for main workspace.
+ * @param {boolean} isVisible True if workspace should be visible.
+ */
+Blockly.WorkspaceSvg.prototype.setVisible = function(isVisible) {
+ this.getParentSvg().style.display = isVisible ? 'block' : 'none';
+ if (this.toolbox_) {
+ // Currently does not support toolboxes in mutators.
+ this.toolbox_.HtmlDiv.style.display = isVisible ? 'block' : 'none';
+ }
+ if (isVisible) {
+ this.render();
+ if (this.toolbox_) {
+ this.toolbox_.position();
+ }
+ } else {
+ Blockly.hideChaff(true);
+ }
+};
+
+/**
+ * Render all blocks in workspace.
+ */
+Blockly.WorkspaceSvg.prototype.render = function() {
+ // Generate list of all blocks.
+ var blocks = this.getAllBlocks();
+ // Render each block.
+ for (var i = blocks.length - 1; i >= 0; i--) {
+ blocks[i].render(false);
+ }
+};
+
+/**
+ * Turn the visual trace functionality on or off.
+ * @param {boolean} armed True if the trace should be on.
+ */
+Blockly.WorkspaceSvg.prototype.traceOn = function(armed) {
+ this.traceOn_ = armed;
+ if (this.traceWrapper_) {
+ Blockly.unbindEvent_(this.traceWrapper_);
+ this.traceWrapper_ = null;
+ }
+ if (armed) {
+ this.traceWrapper_ = Blockly.bindEvent_(this.svgBlockCanvas_,
+ 'blocklySelectChange', this, function() {this.traceOn_ = false;});
+ }
+};
+
+/**
+ * Highlight a block in the workspace.
+ * @param {?string} id ID of block to find.
+ */
+Blockly.WorkspaceSvg.prototype.highlightBlock = function(id) {
+ if (this.traceOn_ && Blockly.dragMode_ != Blockly.DRAG_NONE) {
+ // The blocklySelectChange event normally prevents this, but sometimes
+ // there is a race condition on fast-executing apps.
+ this.traceOn(false);
+ }
+ if (!this.traceOn_) {
+ return;
+ }
+ var block = null;
+ if (id) {
+ block = this.getBlockById(id);
+ if (!block) {
+ return;
+ }
+ }
+ // Temporary turn off the listener for selection changes, so that we don't
+ // trip the monitor for detecting user activity.
+ this.traceOn(false);
+ // Select the current block.
+ if (block) {
+ block.select();
+ } else if (Blockly.selected) {
+ Blockly.selected.unselect();
+ }
+ // Restore the monitor for user activity after the selection event has fired.
+ var thisWorkspace = this;
+ setTimeout(function() {thisWorkspace.traceOn(true);}, 1);
+};
+
+/**
+ * Paste the provided block onto the workspace.
+ * @param {!Element} xmlBlock XML block element.
+ */
+Blockly.WorkspaceSvg.prototype.paste = function(xmlBlock) {
+ if (!this.rendered || xmlBlock.getElementsByTagName('block').length >=
+ this.remainingCapacity()) {
+ return;
+ }
+ Blockly.terminateDrag_(); // Dragging while pasting? No.
+ Blockly.Events.disable();
+ try {
+ var block = Blockly.Xml.domToBlock(xmlBlock, this);
+ // Move the duplicate to original position.
+ var blockX = parseInt(xmlBlock.getAttribute('x'), 10);
+ var blockY = parseInt(xmlBlock.getAttribute('y'), 10);
+ if (!isNaN(blockX) && !isNaN(blockY)) {
+ if (this.RTL) {
+ blockX = -blockX;
+ }
+ // Offset block until not clobbering another block and not in connection
+ // distance with neighbouring blocks.
+ do {
+ var collide = false;
+ var allBlocks = this.getAllBlocks();
+ for (var i = 0, otherBlock; otherBlock = allBlocks[i]; i++) {
+ var otherXY = otherBlock.getRelativeToSurfaceXY();
+ if (Math.abs(blockX - otherXY.x) <= 1 &&
+ Math.abs(blockY - otherXY.y) <= 1) {
+ collide = true;
+ break;
+ }
+ }
+ if (!collide) {
+ // Check for blocks in snap range to any of its connections.
+ var connections = block.getConnections_(false);
+ for (var i = 0, connection; connection = connections[i]; i++) {
+ var neighbour = connection.closest(Blockly.SNAP_RADIUS,
+ new goog.math.Coordinate(blockX, blockY));
+ if (neighbour.connection) {
+ collide = true;
+ break;
+ }
+ }
+ }
+ if (collide) {
+ if (this.RTL) {
+ blockX -= Blockly.SNAP_RADIUS;
+ } else {
+ blockX += Blockly.SNAP_RADIUS;
+ }
+ blockY += Blockly.SNAP_RADIUS * 2;
+ }
+ } while (collide);
+ block.moveBy(blockX, blockY);
+ }
+ } finally {
+ Blockly.Events.enable();
+ }
+ if (Blockly.Events.isEnabled() && !block.isShadow()) {
+ Blockly.Events.fire(new Blockly.Events.Create(block));
+ }
+ block.select();
+};
+
+/**
+ * Create a new variable with the given name. Update the flyout to show the new
+ * variable immediately.
+ * TODO: #468
+ * @param {string} name The new variable's name.
+ */
+Blockly.WorkspaceSvg.prototype.createVariable = function(name) {
+ Blockly.WorkspaceSvg.superClass_.createVariable.call(this, name);
+ if (this.toolbox_ && this.toolbox_.flyout_) {
+ this.toolbox_.refreshSelection();
+ }
+};
+
+/**
+ * Make a list of all the delete areas for this workspace.
+ */
+Blockly.WorkspaceSvg.prototype.recordDeleteAreas = function() {
+ if (this.trashcan) {
+ this.deleteAreaTrash_ = this.trashcan.getClientRect();
+ } else {
+ this.deleteAreaTrash_ = null;
+ }
+ if (this.flyout_) {
+ this.deleteAreaToolbox_ = this.flyout_.getClientRect();
+ } else if (this.toolbox_) {
+ this.deleteAreaToolbox_ = this.toolbox_.getClientRect();
+ } else {
+ this.deleteAreaToolbox_ = null;
+ }
+};
+
+/**
+ * Is the mouse event over a delete area (toolbox or non-closing flyout)?
+ * Opens or closes the trashcan and sets the cursor as a side effect.
+ * @param {!Event} e Mouse move event.
+ * @return {boolean} True if event is in a delete area.
+ */
+Blockly.WorkspaceSvg.prototype.isDeleteArea = function(e) {
+ var xy = new goog.math.Coordinate(e.clientX, e.clientY);
+ if (this.deleteAreaTrash_) {
+ if (this.deleteAreaTrash_.contains(xy)) {
+ this.trashcan.setOpen_(true);
+ Blockly.Css.setCursor(Blockly.Css.Cursor.DELETE);
+ return true;
+ }
+ this.trashcan.setOpen_(false);
+ }
+ if (this.deleteAreaToolbox_) {
+ if (this.deleteAreaToolbox_.contains(xy)) {
+ Blockly.Css.setCursor(Blockly.Css.Cursor.DELETE);
+ return true;
+ }
+ }
+ Blockly.Css.setCursor(Blockly.Css.Cursor.CLOSED);
+ return false;
+};
+
+/**
+ * Handle a mouse-down on SVG drawing surface.
+ * @param {!Event} e Mouse down event.
+ * @private
+ */
+Blockly.WorkspaceSvg.prototype.onMouseDown_ = function(e) {
+ this.markFocused();
+ if (Blockly.isTargetInput_(e)) {
+ return;
+ }
+ Blockly.terminateDrag_(); // In case mouse-up event was lost.
+ Blockly.hideChaff();
+ var isTargetWorkspace = e.target && e.target.nodeName &&
+ (e.target.nodeName.toLowerCase() == 'svg' ||
+ e.target == this.svgBackground_);
+ if (isTargetWorkspace && Blockly.selected && !this.options.readOnly) {
+ // Clicking on the document clears the selection.
+ Blockly.selected.unselect();
+ }
+ if (Blockly.isRightButton(e)) {
+ // Right-click.
+ this.showContextMenu_(e);
+ } else if (this.scrollbar) {
+ this.dragMode_ = Blockly.DRAG_BEGIN;
+ // Record the current mouse position.
+ this.startDragMouseX = e.clientX;
+ this.startDragMouseY = e.clientY;
+ this.startDragMetrics = this.getMetrics();
+ this.startScrollX = this.scrollX;
+ this.startScrollY = this.scrollY;
+
+ // If this is a touch event then bind to the mouseup so workspace drag mode
+ // is turned off and double move events are not performed on a block.
+ // See comment in inject.js Blockly.init_ as to why mouseup events are
+ // bound to the document instead of the SVG's surface.
+ if ('mouseup' in Blockly.bindEvent_.TOUCH_MAP) {
+ Blockly.onTouchUpWrapper_ = Blockly.onTouchUpWrapper_ || [];
+ Blockly.onTouchUpWrapper_ = Blockly.onTouchUpWrapper_.concat(
+ Blockly.bindEvent_(document, 'mouseup', null, Blockly.onMouseUp_));
+ }
+ Blockly.onMouseMoveWrapper_ = Blockly.onMouseMoveWrapper_ || [];
+ Blockly.onMouseMoveWrapper_ = Blockly.onMouseMoveWrapper_.concat(
+ Blockly.bindEvent_(document, 'mousemove', null, Blockly.onMouseMove_));
+ }
+ // This event has been handled. No need to bubble up to the document.
+ e.stopPropagation();
+ e.preventDefault();
+};
+
+/**
+ * Start tracking a drag of an object on this workspace.
+ * @param {!Event} e Mouse down event.
+ * @param {!goog.math.Coordinate} xy Starting location of object.
+ */
+Blockly.WorkspaceSvg.prototype.startDrag = function(e, xy) {
+ // Record the starting offset between the bubble's location and the mouse.
+ var point = Blockly.mouseToSvg(e, this.getParentSvg(),
+ this.getInverseScreenCTM());
+ // Fix scale of mouse event.
+ point.x /= this.scale;
+ point.y /= this.scale;
+ this.dragDeltaXY_ = goog.math.Coordinate.difference(xy, point);
+};
+
+/**
+ * Track a drag of an object on this workspace.
+ * @param {!Event} e Mouse move event.
+ * @return {!goog.math.Coordinate} New location of object.
+ */
+Blockly.WorkspaceSvg.prototype.moveDrag = function(e) {
+ var point = Blockly.mouseToSvg(e, this.getParentSvg(),
+ this.getInverseScreenCTM());
+ // Fix scale of mouse event.
+ point.x /= this.scale;
+ point.y /= this.scale;
+ return goog.math.Coordinate.sum(this.dragDeltaXY_, point);
+};
+
+/**
+ * Is the user currently dragging a block or scrolling the flyout/workspace?
+ * @return {boolean} True if currently dragging or scrolling.
+ */
+Blockly.WorkspaceSvg.prototype.isDragging = function() {
+ return Blockly.dragMode_ == Blockly.DRAG_FREE ||
+ (Blockly.Flyout.startFlyout_ &&
+ Blockly.Flyout.startFlyout_.dragMode_ == Blockly.DRAG_FREE) ||
+ this.dragMode_ == Blockly.DRAG_FREE;
+};
+
+/**
+ * Handle a mouse-wheel on SVG drawing surface.
+ * @param {!Event} e Mouse wheel event.
+ * @private
+ */
+Blockly.WorkspaceSvg.prototype.onMouseWheel_ = function(e) {
+ // TODO: Remove terminateDrag and compensate for coordinate skew during zoom.
+ Blockly.terminateDrag_();
+ var delta = e.deltaY > 0 ? -1 : 1;
+ var position = Blockly.mouseToSvg(e, this.getParentSvg(),
+ this.getInverseScreenCTM());
+ this.zoom(position.x, position.y, delta);
+ e.preventDefault();
+};
+
+/**
+ * Calculate the bounding box for the blocks on the workspace.
+ *
+ * @return {Object} Contains the position and size of the bounding box
+ * containing the blocks on the workspace.
+ */
+Blockly.WorkspaceSvg.prototype.getBlocksBoundingBox = function() {
+ var topBlocks = this.getTopBlocks(false);
+ // There are no blocks, return empty rectangle.
+ if (!topBlocks.length) {
+ return {x: 0, y: 0, width: 0, height: 0};
+ }
+
+ // Initialize boundary using the first block.
+ var boundary = topBlocks[0].getBoundingRectangle();
+
+ // Start at 1 since the 0th block was used for initialization
+ for (var i = 1; i < topBlocks.length; i++) {
+ var blockBoundary = topBlocks[i].getBoundingRectangle();
+ if (blockBoundary.topLeft.x < boundary.topLeft.x) {
+ boundary.topLeft.x = blockBoundary.topLeft.x;
+ }
+ if (blockBoundary.bottomRight.x > boundary.bottomRight.x) {
+ boundary.bottomRight.x = blockBoundary.bottomRight.x;
+ }
+ if (blockBoundary.topLeft.y < boundary.topLeft.y) {
+ boundary.topLeft.y = blockBoundary.topLeft.y;
+ }
+ if (blockBoundary.bottomRight.y > boundary.bottomRight.y) {
+ boundary.bottomRight.y = blockBoundary.bottomRight.y;
+ }
+ }
+ return {
+ x: boundary.topLeft.x,
+ y: boundary.topLeft.y,
+ width: boundary.bottomRight.x - boundary.topLeft.x,
+ height: boundary.bottomRight.y - boundary.topLeft.y
+ };
+};
+
+/**
+ * Clean up the workspace by ordering all the blocks in a column.
+ */
+Blockly.WorkspaceSvg.prototype.cleanUp = function() {
+ Blockly.Events.setGroup(true);
+ var topBlocks = this.getTopBlocks(true);
+ var cursorY = 0;
+ for (var i = 0, block; block = topBlocks[i]; i++) {
+ var xy = block.getRelativeToSurfaceXY();
+ block.moveBy(-xy.x, cursorY - xy.y);
+ block.snapToGrid();
+ cursorY = block.getRelativeToSurfaceXY().y +
+ block.getHeightWidth().height + Blockly.BlockSvg.MIN_BLOCK_Y;
+ }
+ Blockly.Events.setGroup(false);
+ // Fire an event to allow scrollbars to resize.
+ this.resizeContents();
+};
+
+/**
+ * Show the context menu for the workspace.
+ * @param {!Event} e Mouse event.
+ * @private
+ */
+Blockly.WorkspaceSvg.prototype.showContextMenu_ = function(e) {
+ if (this.options.readOnly || this.isFlyout) {
+ return;
+ }
+ var menuOptions = [];
+ var topBlocks = this.getTopBlocks(true);
+ var eventGroup = Blockly.genUid();
+
+ // Options to undo/redo previous action.
+ var undoOption = {};
+ undoOption.text = Blockly.Msg.UNDO;
+ undoOption.enabled = this.undoStack_.length > 0;
+ undoOption.callback = this.undo.bind(this, false);
+ menuOptions.push(undoOption);
+ var redoOption = {};
+ redoOption.text = Blockly.Msg.REDO;
+ redoOption.enabled = this.redoStack_.length > 0;
+ redoOption.callback = this.undo.bind(this, true);
+ menuOptions.push(redoOption);
+
+ // Option to clean up blocks.
+ if (this.scrollbar) {
+ var cleanOption = {};
+ cleanOption.text = Blockly.Msg.CLEAN_UP;
+ cleanOption.enabled = topBlocks.length > 1;
+ cleanOption.callback = this.cleanUp.bind(this);
+ menuOptions.push(cleanOption);
+ }
+
+ // Add a little animation to collapsing and expanding.
+ var DELAY = 10;
+ if (this.options.collapse) {
+ var hasCollapsedBlocks = false;
+ var hasExpandedBlocks = false;
+ for (var i = 0; i < topBlocks.length; i++) {
+ var block = topBlocks[i];
+ while (block) {
+ if (block.isCollapsed()) {
+ hasCollapsedBlocks = true;
+ } else {
+ hasExpandedBlocks = true;
+ }
+ block = block.getNextBlock();
+ }
+ }
+
+ /**
+ * Option to collapse or expand top blocks.
+ * @param {boolean} shouldCollapse Whether a block should collapse.
+ * @private
+ */
+ var toggleOption = function(shouldCollapse) {
+ var ms = 0;
+ for (var i = 0; i < topBlocks.length; i++) {
+ var block = topBlocks[i];
+ while (block) {
+ setTimeout(block.setCollapsed.bind(block, shouldCollapse), ms);
+ block = block.getNextBlock();
+ ms += DELAY;
+ }
+ }
+ };
+
+ // Option to collapse top blocks.
+ var collapseOption = {enabled: hasExpandedBlocks};
+ collapseOption.text = Blockly.Msg.COLLAPSE_ALL;
+ collapseOption.callback = function() {
+ toggleOption(true);
+ };
+ menuOptions.push(collapseOption);
+
+ // Option to expand top blocks.
+ var expandOption = {enabled: hasCollapsedBlocks};
+ expandOption.text = Blockly.Msg.EXPAND_ALL;
+ expandOption.callback = function() {
+ toggleOption(false);
+ };
+ menuOptions.push(expandOption);
+ }
+
+ // Option to delete all blocks.
+ // Count the number of blocks that are deletable.
+ var deleteList = [];
+ function addDeletableBlocks(block) {
+ if (block.isDeletable()) {
+ deleteList = deleteList.concat(block.getDescendants());
+ } else {
+ var children = block.getChildren();
+ for (var i = 0; i < children.length; i++) {
+ addDeletableBlocks(children[i]);
+ }
+ }
+ }
+ for (var i = 0; i < topBlocks.length; i++) {
+ addDeletableBlocks(topBlocks[i]);
+ }
+
+ function deleteNext() {
+ Blockly.Events.setGroup(eventGroup);
+ var block = deleteList.shift();
+ if (block) {
+ if (block.workspace) {
+ block.dispose(false, true);
+ setTimeout(deleteNext, DELAY);
+ } else {
+ deleteNext();
+ }
+ }
+ Blockly.Events.setGroup(false);
+ }
+
+ var deleteOption = {
+ text: deleteList.length == 1 ? Blockly.Msg.DELETE_BLOCK :
+ Blockly.Msg.DELETE_X_BLOCKS.replace('%1', String(deleteList.length)),
+ enabled: deleteList.length > 0,
+ callback: function() {
+ if (deleteList.length < 2 ||
+ window.confirm(Blockly.Msg.DELETE_ALL_BLOCKS.replace('%1',
+ String(deleteList.length)))) {
+ deleteNext();
+ }
+ }
+ };
+ menuOptions.push(deleteOption);
+
+ Blockly.ContextMenu.show(e, menuOptions, this.RTL);
+};
+
+/**
+ * Load an audio file. Cache it, ready for instantaneous playing.
+ * @param {!Array.<string>} filenames List of file types in decreasing order of
+ * preference (i.e. increasing size). E.g. ['media/go.mp3', 'media/go.wav']
+ * Filenames include path from Blockly's root. File extensions matter.
+ * @param {string} name Name of sound.
+ * @private
+ */
+Blockly.WorkspaceSvg.prototype.loadAudio_ = function(filenames, name) {
+ if (!filenames.length) {
+ return;
+ }
+ try {
+ var audioTest = new window['Audio']();
+ } catch (e) {
+ // No browser support for Audio.
+ // IE can throw an error even if the Audio object exists.
+ return;
+ }
+ var sound;
+ for (var i = 0; i < filenames.length; i++) {
+ var filename = filenames[i];
+ var ext = filename.match(/\.(\w+)$/);
+ if (ext && audioTest.canPlayType('audio/' + ext[1])) {
+ // Found an audio format we can play.
+ sound = new window['Audio'](filename);
+ break;
+ }
+ }
+ if (sound && sound.play) {
+ this.SOUNDS_[name] = sound;
+ }
+};
+
+/**
+ * Preload all the audio files so that they play quickly when asked for.
+ * @private
+ */
+Blockly.WorkspaceSvg.prototype.preloadAudio_ = function() {
+ for (var name in this.SOUNDS_) {
+ var sound = this.SOUNDS_[name];
+ sound.volume = .01;
+ sound.play();
+ sound.pause();
+ // iOS can only process one sound at a time. Trying to load more than one
+ // corrupts the earlier ones. Just load one and leave the others uncached.
+ if (goog.userAgent.IPAD || goog.userAgent.IPHONE) {
+ break;
+ }
+ }
+};
+
+/**
+ * Play a named sound at specified volume. If volume is not specified,
+ * use full volume (1).
+ * @param {string} name Name of sound.
+ * @param {number=} opt_volume Volume of sound (0-1).
+ */
+Blockly.WorkspaceSvg.prototype.playAudio = function(name, opt_volume) {
+ var sound = this.SOUNDS_[name];
+ if (sound) {
+ // Don't play one sound on top of another.
+ var now = new Date;
+ if (now - this.lastSound_ < Blockly.SOUND_LIMIT) {
+ return;
+ }
+ this.lastSound_ = now;
+ var mySound;
+ var ie9 = goog.userAgent.DOCUMENT_MODE &&
+ goog.userAgent.DOCUMENT_MODE === 9;
+ if (ie9 || goog.userAgent.IPAD || goog.userAgent.ANDROID) {
+ // Creating a new audio node causes lag in IE9, Android and iPad. Android
+ // and IE9 refetch the file from the server, iPad uses a singleton audio
+ // node which must be deleted and recreated for each new audio tag.
+ mySound = sound;
+ } else {
+ mySound = sound.cloneNode();
+ }
+ mySound.volume = (opt_volume === undefined ? 1 : opt_volume);
+ mySound.play();
+ } else if (this.options.parentWorkspace) {
+ // Maybe a workspace on a lower level knows about this sound.
+ this.options.parentWorkspace.playAudio(name, opt_volume);
+ }
+};
+
+/**
+ * Modify the block tree on the existing toolbox.
+ * @param {Node|string} tree DOM tree of blocks, or text representation of same.
+ */
+Blockly.WorkspaceSvg.prototype.updateToolbox = function(tree) {
+ tree = Blockly.Options.parseToolboxTree(tree);
+ if (!tree) {
+ if (this.options.languageTree) {
+ throw 'Can\'t nullify an existing toolbox.';
+ }
+ return; // No change (null to null).
+ }
+ if (!this.options.languageTree) {
+ throw 'Existing toolbox is null. Can\'t create new toolbox.';
+ }
+ if (tree.getElementsByTagName('category').length) {
+ if (!this.toolbox_) {
+ throw 'Existing toolbox has no categories. Can\'t change mode.';
+ }
+ this.options.languageTree = tree;
+ this.toolbox_.populate_(tree);
+ this.toolbox_.addColour_();
+ } else {
+ if (!this.flyout_) {
+ throw 'Existing toolbox has categories. Can\'t change mode.';
+ }
+ this.options.languageTree = tree;
+ this.flyout_.show(tree.childNodes);
+ }
+};
+
+/**
+ * Mark this workspace as the currently focused main workspace.
+ */
+Blockly.WorkspaceSvg.prototype.markFocused = function() {
+ if (this.options.parentWorkspace) {
+ this.options.parentWorkspace.markFocused();
+ } else {
+ Blockly.mainWorkspace = this;
+ }
+};
+
+/**
+ * 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} type Type of zooming (-1 zooming out and 1 zooming in).
+ */
+Blockly.WorkspaceSvg.prototype.zoom = function(x, y, type) {
+ 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 scaleChange = (type == 1) ? speed : 1 / speed;
+ // Clamp scale within valid range.
+ var newScale = this.scale * scaleChange;
+ 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).
+ this.scrollX = matrix.e - metrics.absoluteLeft;
+ this.scrollY = matrix.f - metrics.absoluteTop;
+ }
+ this.setScale(newScale);
+};
+
+/**
+ * Zooming the blocks centered in the center of view with zooming in or out.
+ * @param {number} type Type of zooming (-1 zooming out and 1 zooming in).
+ */
+Blockly.WorkspaceSvg.prototype.zoomCenter = function(type) {
+ var metrics = this.getMetrics();
+ var x = metrics.viewWidth / 2;
+ var y = metrics.viewHeight / 2;
+ this.zoom(x, y, type);
+};
+
+/**
+ * Zoom the blocks to fit in the workspace if possible.
+ */
+Blockly.WorkspaceSvg.prototype.zoomToFit = function() {
+ var metrics = this.getMetrics();
+ var blocksBox = this.getBlocksBoundingBox();
+ var blocksWidth = blocksBox.width;
+ var blocksHeight = blocksBox.height;
+ if (!blocksWidth) {
+ return; // Prevents zooming to infinity.
+ }
+ var workspaceWidth = metrics.viewWidth;
+ var workspaceHeight = metrics.viewHeight;
+ if (this.flyout_) {
+ workspaceWidth -= this.flyout_.width_;
+ }
+ if (!this.scrollbar) {
+ // Orgin 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;
+ this.setScale(Math.min(ratioX, ratioY));
+ this.scrollCenter();
+};
+
+/**
+ * Center the workspace.
+ */
+Blockly.WorkspaceSvg.prototype.scrollCenter = function() {
+ if (!this.scrollbar) {
+ // Can't center a non-scrolling workspace.
+ 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);
+};
+
+/**
+ * Set the workspace's zoom factor.
+ * @param {number} newScale Zoom factor.
+ */
+Blockly.WorkspaceSvg.prototype.setScale = function(newScale) {
+ if (this.options.zoomOptions.maxScale &&
+ newScale > this.options.zoomOptions.maxScale) {
+ newScale = this.options.zoomOptions.maxScale;
+ } else if (this.options.zoomOptions.minScale &&
+ newScale < this.options.zoomOptions.minScale) {
+ newScale = this.options.zoomOptions.minScale;
+ }
+ this.scale = newScale;
+ this.updateGridPattern_();
+ if (this.scrollbar) {
+ this.scrollbar.resize();
+ } else {
+ this.translate(this.scrollX, this.scrollY);
+ }
+ Blockly.hideChaff(false);
+ if (this.flyout_) {
+ // No toolbox, resize flyout.
+ this.flyout_.reflow();
+ }
+};
+
+/**
+ * Updates the grid pattern.
+ * @private
+ */
+Blockly.WorkspaceSvg.prototype.updateGridPattern_ = function() {
+ if (!this.options.gridPattern) {
+ return; // No grid.
+ }
+ // MSIE freaks if it sees a 0x0 pattern, so set empty patterns to 100x100.
+ var safeSpacing = (this.options.gridOptions['spacing'] * this.scale) || 100;
+ this.options.gridPattern.setAttribute('width', safeSpacing);
+ this.options.gridPattern.setAttribute('height', safeSpacing);
+ var half = Math.floor(this.options.gridOptions['spacing'] / 2) + 0.5;
+ var start = half - this.options.gridOptions['length'] / 2;
+ var end = half + this.options.gridOptions['length'] / 2;
+ var line1 = this.options.gridPattern.firstChild;
+ var line2 = line1 && line1.nextSibling;
+ half *= this.scale;
+ start *= this.scale;
+ end *= this.scale;
+ if (line1) {
+ line1.setAttribute('stroke-width', this.scale);
+ line1.setAttribute('x1', start);
+ line1.setAttribute('y1', half);
+ line1.setAttribute('x2', end);
+ line1.setAttribute('y2', half);
+ }
+ if (line2) {
+ line2.setAttribute('stroke-width', this.scale);
+ line2.setAttribute('x1', half);
+ line2.setAttribute('y1', start);
+ line2.setAttribute('x2', half);
+ line2.setAttribute('y2', end);
+ }
+};
+
+
+/**
+ * Return an object with all the metrics required to size scrollbars for a
+ * top level workspace. The following properties are computed:
+ * .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.
+ * .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.
+ * @return {Object} Contains size and position metrics of a top level workspace.
+ * @private
+ */
+Blockly.WorkspaceSvg.getTopLevelWorkspaceMetrics_ = function() {
+ var svgSize = Blockly.svgSize(this.getParentSvg());
+ if (this.toolbox_) {
+ if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP ||
+ this.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) {
+ svgSize.height -= this.toolbox_.getHeight();
+ } else if (this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT ||
+ this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) {
+ svgSize.width -= this.toolbox_.getWidth();
+ }
+ }
+ // Set the margin to match the flyout's margin so that the workspace does
+ // not jump as blocks are added.
+ var MARGIN = Blockly.Flyout.prototype.CORNER_RADIUS - 1;
+ var viewWidth = svgSize.width - MARGIN;
+ var viewHeight = svgSize.height - MARGIN;
+ var blockBox = this.getBlocksBoundingBox();
+
+ // Fix scale.
+ var contentWidth = blockBox.width * this.scale;
+ var contentHeight = blockBox.height * this.scale;
+ var contentX = blockBox.x * this.scale;
+ var contentY = blockBox.y * this.scale;
+ if (this.scrollbar) {
+ // Add a border around the content that is at least half a screenful wide.
+ // Ensure border is wide enough that blocks can scroll over entire screen.
+ var leftEdge = Math.min(contentX - viewWidth / 2,
+ contentX + contentWidth - viewWidth);
+ var rightEdge = Math.max(contentX + contentWidth + viewWidth / 2,
+ contentX + viewWidth);
+ var topEdge = Math.min(contentY - viewHeight / 2,
+ contentY + contentHeight - viewHeight);
+ var bottomEdge = Math.max(contentY + contentHeight + viewHeight / 2,
+ contentY + viewHeight);
+ } else {
+ var leftEdge = blockBox.x;
+ var rightEdge = leftEdge + blockBox.width;
+ var topEdge = blockBox.y;
+ var bottomEdge = topEdge + blockBox.height;
+ }
+ var absoluteLeft = 0;
+ if (this.toolbox_ && this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) {
+ absoluteLeft = this.toolbox_.getWidth();
+ }
+ var absoluteTop = 0;
+ if (this.toolbox_ && this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) {
+ absoluteTop = this.toolbox_.getHeight();
+ }
+
+ var metrics = {
+ viewHeight: svgSize.height,
+ viewWidth: svgSize.width,
+ contentHeight: bottomEdge - topEdge,
+ contentWidth: rightEdge - leftEdge,
+ viewTop: -this.scrollY,
+ viewLeft: -this.scrollX,
+ contentTop: topEdge,
+ contentLeft: leftEdge,
+ absoluteTop: absoluteTop,
+ absoluteLeft: absoluteLeft,
+ toolboxWidth: this.toolbox_ ? this.toolbox_.getWidth() : 0,
+ toolboxHeight: this.toolbox_ ? this.toolbox_.getHeight() : 0,
+ flyoutWidth: this.flyout_ ? this.flyout_.getWidth() : 0,
+ flyoutHeight: this.flyout_ ? this.flyout_.getHeight() : 0,
+ toolboxPosition: this.toolboxPosition
+ };
+ return metrics;
+};
+
+/**
+ * Sets the X/Y translations of a top level workspace to match the scrollbars.
+ * @param {!Object} xyRatio Contains an x and/or y property which is a float
+ * between 0 and 1 specifying the degree of scrolling.
+ * @private
+ */
+Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_ = function(xyRatio) {
+ if (!this.scrollbar) {
+ throw 'Attempt to set top level workspace scroll without scrollbars.';
+ }
+ var metrics = this.getMetrics();
+ if (goog.isNumber(xyRatio.x)) {
+ this.scrollX = -metrics.contentWidth * xyRatio.x - metrics.contentLeft;
+ }
+ if (goog.isNumber(xyRatio.y)) {
+ this.scrollY = -metrics.contentHeight * xyRatio.y - metrics.contentTop;
+ }
+ var x = this.scrollX + metrics.absoluteLeft;
+ var y = this.scrollY + metrics.absoluteTop;
+ this.translate(x, y);
+ if (this.options.gridPattern) {
+ this.options.gridPattern.setAttribute('x', x);
+ this.options.gridPattern.setAttribute('y', y);
+ if (goog.userAgent.IE) {
+ // IE doesn't notice that the x/y offsets have changed. Force an update.
+ this.updateGridPattern_();
+ }
+ }
+};
+// Export symbols that would otherwise be renamed by Closure compiler.
+Blockly.WorkspaceSvg.prototype['setVisible'] =
+ Blockly.WorkspaceSvg.prototype.setVisible;