2014-12-23 11:22:02 -08:00
|
|
|
/**
|
|
|
|
* @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
|
2016-06-03 16:12:59 -07:00
|
|
|
//goog.require('Blockly.BlockSvg');
|
2016-03-10 15:23:15 -08:00
|
|
|
goog.require('Blockly.ConnectionDB');
|
2016-08-25 15:18:53 -07:00
|
|
|
goog.require('Blockly.constants');
|
2016-04-07 13:38:36 -07:00
|
|
|
goog.require('Blockly.Options');
|
2014-12-23 11:22:02 -08:00
|
|
|
goog.require('Blockly.ScrollbarPair');
|
2016-09-07 18:10:25 -07:00
|
|
|
goog.require('Blockly.Touch');
|
2014-12-23 11:22:02 -08:00
|
|
|
goog.require('Blockly.Trashcan');
|
|
|
|
goog.require('Blockly.Workspace');
|
|
|
|
goog.require('Blockly.Xml');
|
2016-03-18 15:19:26 -07:00
|
|
|
goog.require('Blockly.ZoomControls');
|
2015-04-28 13:51:25 -07:00
|
|
|
|
2015-02-06 15:27:25 -08:00
|
|
|
goog.require('goog.dom');
|
2014-12-23 11:22:02 -08:00
|
|
|
goog.require('goog.math.Coordinate');
|
2015-04-28 13:51:25 -07:00
|
|
|
goog.require('goog.userAgent');
|
2014-12-23 11:22:02 -08:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Class for a workspace. This is an onscreen area with optional trashcan,
|
|
|
|
* scrollbars, bubbles, and dragging.
|
2016-03-29 14:26:56 -07:00
|
|
|
* @param {!Blockly.Options} options Dictionary of options.
|
2014-12-23 11:22:02 -08:00
|
|
|
* @extends {Blockly.Workspace}
|
|
|
|
* @constructor
|
|
|
|
*/
|
2015-04-28 13:51:25 -07:00
|
|
|
Blockly.WorkspaceSvg = function(options) {
|
|
|
|
Blockly.WorkspaceSvg.superClass_.constructor.call(this, options);
|
2016-08-19 14:13:20 -07:00
|
|
|
this.getMetrics =
|
|
|
|
options.getMetrics || Blockly.WorkspaceSvg.getTopLevelWorkspaceMetrics_;
|
|
|
|
this.setMetrics =
|
|
|
|
options.setMetrics || Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_;
|
2014-12-23 11:22:02 -08:00
|
|
|
|
|
|
|
Blockly.ConnectionDB.init(this);
|
2015-08-20 15:46:44 -07:00
|
|
|
|
2015-04-28 13:51:25 -07:00
|
|
|
/**
|
|
|
|
* Database of pre-loaded sounds.
|
|
|
|
* @private
|
|
|
|
* @const
|
|
|
|
*/
|
|
|
|
this.SOUNDS_ = Object.create(null);
|
2014-12-23 11:22:02 -08:00
|
|
|
};
|
|
|
|
goog.inherits(Blockly.WorkspaceSvg, Blockly.Workspace);
|
|
|
|
|
2016-06-13 14:46:58 -07:00
|
|
|
/**
|
2016-09-27 17:47:36 -04:00
|
|
|
* A wrapper function called when a resize event occurs. You can pass the result to `unbindEvent_`.
|
|
|
|
* @type {Array.<!Array>}
|
2016-06-13 14:46:58 -07:00
|
|
|
*/
|
|
|
|
Blockly.WorkspaceSvg.prototype.resizeHandlerWrapper_ = null;
|
|
|
|
|
2014-12-23 11:22:02 -08:00
|
|
|
/**
|
2016-10-11 12:16:17 -07:00
|
|
|
* The render status of an SVG workspace.
|
|
|
|
* Returns `true` for visible workspaces and `false` for non-visible, or headless, workspaces.
|
2016-09-27 17:47:36 -04:00
|
|
|
* @type {boolean}
|
2014-12-23 11:22:02 -08:00
|
|
|
*/
|
|
|
|
Blockly.WorkspaceSvg.prototype.rendered = true;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Is this workspace the surface for a flyout?
|
|
|
|
* @type {boolean}
|
|
|
|
*/
|
|
|
|
Blockly.WorkspaceSvg.prototype.isFlyout = false;
|
|
|
|
|
2016-08-11 16:09:22 -07:00
|
|
|
/**
|
|
|
|
* Is this workspace the surface for a mutator?
|
|
|
|
* @type {boolean}
|
|
|
|
* @package
|
|
|
|
*/
|
|
|
|
Blockly.WorkspaceSvg.prototype.isMutator = false;
|
|
|
|
|
2014-12-23 11:22:02 -08:00
|
|
|
/**
|
2015-04-28 13:51:25 -07:00
|
|
|
* Is this workspace currently being dragged around?
|
2016-07-12 20:34:02 +02:00
|
|
|
* DRAG_NONE - No drag operation.
|
|
|
|
* DRAG_BEGIN - Still inside the initial DRAG_RADIUS.
|
|
|
|
* DRAG_FREE - Workspace has been dragged further than DRAG_RADIUS.
|
|
|
|
* @private
|
2014-12-23 11:22:02 -08:00
|
|
|
*/
|
2016-07-12 20:34:02 +02:00
|
|
|
Blockly.WorkspaceSvg.prototype.dragMode_ = Blockly.DRAG_NONE;
|
2014-12-23 11:22:02 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Current horizontal scrolling offset.
|
|
|
|
* @type {number}
|
|
|
|
*/
|
|
|
|
Blockly.WorkspaceSvg.prototype.scrollX = 0;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Current vertical scrolling offset.
|
|
|
|
* @type {number}
|
|
|
|
*/
|
|
|
|
Blockly.WorkspaceSvg.prototype.scrollY = 0;
|
|
|
|
|
2015-10-25 23:50:20 -07:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
|
2015-08-19 17:21:05 -07:00
|
|
|
/**
|
2016-04-19 23:24:42 -07:00
|
|
|
* Distance from mouse to object being dragged.
|
|
|
|
* @type {goog.math.Coordinate}
|
2015-08-19 17:21:05 -07:00
|
|
|
* @private
|
|
|
|
*/
|
2016-04-19 23:24:42 -07:00
|
|
|
Blockly.WorkspaceSvg.prototype.dragDeltaXY_ = null;
|
2015-08-19 17:21:05 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Current scale.
|
|
|
|
* @type {number}
|
|
|
|
*/
|
|
|
|
Blockly.WorkspaceSvg.prototype.scale = 1;
|
|
|
|
|
2014-12-23 11:22:02 -08:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
|
2016-05-21 05:55:24 -07:00
|
|
|
/**
|
|
|
|
* Time that the last sound was played.
|
|
|
|
* @type {Date}
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
Blockly.WorkspaceSvg.prototype.lastSound_ = null;
|
|
|
|
|
2016-08-10 18:00:15 -07:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
|
2016-06-17 14:26:04 -07:00
|
|
|
/**
|
|
|
|
* 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() {
|
2016-10-14 02:20:25 -07:00
|
|
|
var ctm = this.getParentSvg().getScreenCTM();
|
|
|
|
if (ctm) {
|
|
|
|
this.inverseScreenCTM_ = ctm.inverse();
|
|
|
|
}
|
2016-06-17 14:26:04 -07:00
|
|
|
};
|
|
|
|
|
2016-06-13 14:46:58 -07:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
};
|
|
|
|
|
2014-12-23 11:22:02 -08:00
|
|
|
/**
|
2015-09-28 14:17:53 -07:00
|
|
|
* Create the workspace DOM elements.
|
2015-03-26 20:47:36 -07:00
|
|
|
* @param {string=} opt_backgroundClass Either 'blocklyMainBackground' or
|
2015-03-07 19:44:58 -06:00
|
|
|
* 'blocklyMutatorBackground'.
|
2014-12-23 11:22:02 -08:00
|
|
|
* @return {!Element} The workspace's SVG group.
|
|
|
|
*/
|
2015-03-07 19:44:58 -06:00
|
|
|
Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) {
|
2015-12-02 11:09:50 -06:00
|
|
|
/**
|
|
|
|
* <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}
|
|
|
|
*/
|
2015-07-14 23:13:09 -07:00
|
|
|
this.svgGroup_ = Blockly.createSvgElement('g',
|
2015-09-28 12:17:54 -07:00
|
|
|
{'class': 'blocklyWorkspace'}, null);
|
2015-03-07 19:44:58 -06:00
|
|
|
if (opt_backgroundClass) {
|
2015-12-01 14:04:54 -06:00
|
|
|
/** @type {SVGElement} */
|
2015-03-07 19:44:58 -06:00
|
|
|
this.svgBackground_ = Blockly.createSvgElement('rect',
|
2016-01-07 17:01:01 -08:00
|
|
|
{'height': '100%', 'width': '100%', 'class': opt_backgroundClass},
|
|
|
|
this.svgGroup_);
|
2015-04-28 13:51:25 -07:00
|
|
|
if (opt_backgroundClass == 'blocklyMainBackground') {
|
|
|
|
this.svgBackground_.style.fill =
|
|
|
|
'url(#' + this.options.gridPattern.id + ')';
|
|
|
|
}
|
2015-03-07 19:44:58 -06:00
|
|
|
}
|
2015-12-01 14:04:54 -06:00
|
|
|
/** @type {SVGElement} */
|
2015-07-14 23:13:09 -07:00
|
|
|
this.svgBlockCanvas_ = Blockly.createSvgElement('g',
|
2015-08-19 17:21:05 -07:00
|
|
|
{'class': 'blocklyBlockCanvas'}, this.svgGroup_, this);
|
2015-12-01 14:04:54 -06:00
|
|
|
/** @type {SVGElement} */
|
2015-07-14 23:13:09 -07:00
|
|
|
this.svgBubbleCanvas_ = Blockly.createSvgElement('g',
|
2015-08-19 17:21:05 -07:00
|
|
|
{'class': 'blocklyBubbleCanvas'}, this.svgGroup_, this);
|
2015-09-01 22:44:33 +01:00
|
|
|
var bottom = Blockly.Scrollbar.scrollbarThickness;
|
2015-04-28 13:51:25 -07:00
|
|
|
if (this.options.hasTrashcan) {
|
2015-09-01 22:44:33 +01:00
|
|
|
bottom = this.addTrashcan_(bottom);
|
2015-04-28 13:51:25 -07:00
|
|
|
}
|
2015-08-19 17:21:05 -07:00
|
|
|
if (this.options.zoomOptions && this.options.zoomOptions.controls) {
|
2015-09-01 22:44:33 +01:00
|
|
|
bottom = this.addZoomControls_(bottom);
|
2015-08-19 17:21:05 -07:00
|
|
|
}
|
2016-07-01 13:36:20 -07:00
|
|
|
|
|
|
|
if (!this.isFlyout) {
|
2016-09-23 13:46:11 -07:00
|
|
|
Blockly.bindEventWithChecks_(this.svgGroup_, 'mousedown', this,
|
|
|
|
this.onMouseDown_);
|
2016-07-01 13:36:20 -07:00
|
|
|
var thisWorkspace = this;
|
2016-09-23 13:46:11 -07:00
|
|
|
Blockly.bindEventWithChecks_(this.svgGroup_, 'touchstart', null,
|
2016-07-01 13:36:20 -07:00
|
|
|
function(e) {Blockly.longStart_(e, thisWorkspace);});
|
|
|
|
if (this.options.zoomOptions && this.options.zoomOptions.wheel) {
|
|
|
|
// Mouse-wheel.
|
2016-09-23 13:46:11 -07:00
|
|
|
Blockly.bindEventWithChecks_(this.svgGroup_, 'wheel', this,
|
|
|
|
this.onMouseWheel_);
|
2016-07-01 13:36:20 -07:00
|
|
|
}
|
2015-08-19 17:21:05 -07:00
|
|
|
}
|
2015-04-28 13:51:25 -07:00
|
|
|
|
|
|
|
// 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) {
|
2016-09-30 16:26:44 -05:00
|
|
|
/**
|
|
|
|
* @type {Blockly.Toolbox}
|
|
|
|
* @private
|
|
|
|
*/
|
2015-04-28 13:51:25 -07:00
|
|
|
this.toolbox_ = new Blockly.Toolbox(this);
|
|
|
|
} else if (this.options.languageTree) {
|
|
|
|
this.addFlyout_();
|
|
|
|
}
|
2015-08-19 17:21:05 -07:00
|
|
|
this.updateGridPattern_();
|
2016-06-17 12:39:18 -07:00
|
|
|
this.recordDeleteAreas();
|
2014-12-23 11:22:02 -08:00
|
|
|
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;
|
2015-06-04 12:04:43 -07:00
|
|
|
if (this.toolbox_) {
|
|
|
|
this.toolbox_.dispose();
|
|
|
|
this.toolbox_ = null;
|
|
|
|
}
|
2014-12-23 11:22:02 -08:00
|
|
|
if (this.flyout_) {
|
|
|
|
this.flyout_.dispose();
|
|
|
|
this.flyout_ = null;
|
|
|
|
}
|
|
|
|
if (this.trashcan) {
|
|
|
|
this.trashcan.dispose();
|
|
|
|
this.trashcan = null;
|
|
|
|
}
|
2015-08-20 15:46:44 -07:00
|
|
|
if (this.scrollbar) {
|
|
|
|
this.scrollbar.dispose();
|
|
|
|
this.scrollbar = null;
|
|
|
|
}
|
|
|
|
if (this.zoomControls_) {
|
|
|
|
this.zoomControls_.dispose();
|
|
|
|
this.zoomControls_ = null;
|
2015-08-19 17:21:05 -07:00
|
|
|
}
|
2015-05-02 21:08:18 -07:00
|
|
|
if (!this.options.parentWorkspace) {
|
2016-08-11 11:00:02 -07:00
|
|
|
// Top-most workspace. Dispose of the div that the
|
|
|
|
// svg is injected into (i.e. injectionDiv).
|
|
|
|
goog.dom.removeNode(this.getParentSvg().parentNode);
|
2015-05-02 21:08:18 -07:00
|
|
|
}
|
2016-06-13 14:46:58 -07:00
|
|
|
if (this.resizeHandlerWrapper_) {
|
|
|
|
Blockly.unbindEvent_(this.resizeHandlerWrapper_);
|
|
|
|
this.resizeHandlerWrapper_ = null;
|
|
|
|
}
|
2014-12-23 11:22:02 -08:00
|
|
|
};
|
|
|
|
|
2015-12-07 16:40:45 +01:00
|
|
|
/**
|
|
|
|
* Obtain a newly created block.
|
|
|
|
* @param {?string} prototypeName Name of the language object containing
|
|
|
|
* type-specific functions for this block.
|
2016-09-27 17:47:36 -04:00
|
|
|
* @param {string=} opt_id Optional ID. Use this ID if provided, otherwise
|
2016-10-11 12:16:17 -07:00
|
|
|
* create a new ID.
|
2015-12-07 16:40:45 +01:00
|
|
|
* @return {!Blockly.BlockSvg} The created block.
|
|
|
|
*/
|
2015-12-09 10:02:42 +01:00
|
|
|
Blockly.WorkspaceSvg.prototype.newBlock = function(prototypeName, opt_id) {
|
|
|
|
return new Blockly.BlockSvg(this, prototypeName, opt_id);
|
2015-12-07 16:40:45 +01:00
|
|
|
};
|
|
|
|
|
2014-12-23 11:22:02 -08:00
|
|
|
/**
|
|
|
|
* Add a trashcan.
|
2015-09-01 22:44:33 +01:00
|
|
|
* @param {number} bottom Distance from workspace bottom to bottom of trashcan.
|
|
|
|
* @return {number} Distance from workspace bottom to the top of trashcan.
|
2015-04-28 13:51:25 -07:00
|
|
|
* @private
|
2014-12-23 11:22:02 -08:00
|
|
|
*/
|
2015-09-01 22:44:33 +01:00
|
|
|
Blockly.WorkspaceSvg.prototype.addTrashcan_ = function(bottom) {
|
2015-08-20 15:46:44 -07:00
|
|
|
/** @type {Blockly.Trashcan} */
|
2015-04-28 13:51:25 -07:00
|
|
|
this.trashcan = new Blockly.Trashcan(this);
|
|
|
|
var svgTrashcan = this.trashcan.createDom();
|
|
|
|
this.svgGroup_.insertBefore(svgTrashcan, this.svgBlockCanvas_);
|
2015-09-01 22:44:33 +01:00
|
|
|
return this.trashcan.init(bottom);
|
2014-12-23 11:22:02 -08:00
|
|
|
};
|
|
|
|
|
2015-08-19 17:21:05 -07:00
|
|
|
/**
|
|
|
|
* Add zoom controls.
|
2015-09-01 22:44:33 +01:00
|
|
|
* @param {number} bottom Distance from workspace bottom to bottom of controls.
|
|
|
|
* @return {number} Distance from workspace bottom to the top of controls.
|
2015-08-19 17:21:05 -07:00
|
|
|
* @private
|
|
|
|
*/
|
2015-09-01 22:44:33 +01:00
|
|
|
Blockly.WorkspaceSvg.prototype.addZoomControls_ = function(bottom) {
|
2015-08-20 15:46:44 -07:00
|
|
|
/** @type {Blockly.ZoomControls} */
|
|
|
|
this.zoomControls_ = new Blockly.ZoomControls(this);
|
|
|
|
var svgZoomControls = this.zoomControls_.createDom();
|
2015-08-19 17:21:05 -07:00
|
|
|
this.svgGroup_.appendChild(svgZoomControls);
|
2015-09-01 22:44:33 +01:00
|
|
|
return this.zoomControls_.init(bottom);
|
2015-08-19 17:21:05 -07:00
|
|
|
};
|
|
|
|
|
2015-03-06 15:27:41 -06:00
|
|
|
/**
|
|
|
|
* Add a flyout.
|
2015-04-28 13:51:25 -07:00
|
|
|
* @private
|
2015-03-06 15:27:41 -06:00
|
|
|
*/
|
2015-04-28 13:51:25 -07:00
|
|
|
Blockly.WorkspaceSvg.prototype.addFlyout_ = function() {
|
|
|
|
var workspaceOptions = {
|
2015-10-21 15:21:51 -07:00
|
|
|
disabledPatternId: this.options.disabledPatternId,
|
2015-04-28 13:51:25 -07:00
|
|
|
parentWorkspace: this,
|
2016-03-17 15:46:22 -07:00
|
|
|
RTL: this.RTL,
|
2016-10-11 11:03:03 -07:00
|
|
|
oneBasedIndex: this.options.oneBasedIndex,
|
2016-03-17 15:46:22 -07:00
|
|
|
horizontalLayout: this.horizontalLayout,
|
2016-05-25 12:53:42 -07:00
|
|
|
toolboxPosition: this.options.toolboxPosition
|
2015-04-28 13:51:25 -07:00
|
|
|
};
|
2015-08-20 15:46:44 -07:00
|
|
|
/** @type {Blockly.Flyout} */
|
2015-04-28 13:51:25 -07:00
|
|
|
this.flyout_ = new Blockly.Flyout(workspaceOptions);
|
2015-03-07 19:44:58 -06:00
|
|
|
this.flyout_.autoClose = false;
|
|
|
|
var svgFlyout = this.flyout_.createDom();
|
|
|
|
this.svgGroup_.insertBefore(svgFlyout, this.svgBlockCanvas_);
|
2015-03-06 15:27:41 -06:00
|
|
|
};
|
|
|
|
|
2016-08-10 18:00:15 -07:00
|
|
|
/**
|
|
|
|
* 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();
|
|
|
|
};
|
|
|
|
|
2015-04-28 17:55:45 -07:00
|
|
|
/**
|
2016-06-03 16:11:55 -07:00
|
|
|
* 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();
|
|
|
|
}
|
2016-06-17 14:26:04 -07:00
|
|
|
this.updateInverseScreenCTM();
|
2016-06-03 16:11:55 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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).
|
2015-04-28 17:55:45 -07:00
|
|
|
*/
|
|
|
|
Blockly.WorkspaceSvg.prototype.resize = function() {
|
|
|
|
if (this.toolbox_) {
|
|
|
|
this.toolbox_.position();
|
|
|
|
}
|
|
|
|
if (this.flyout_) {
|
|
|
|
this.flyout_.position();
|
|
|
|
}
|
|
|
|
if (this.trashcan) {
|
|
|
|
this.trashcan.position();
|
|
|
|
}
|
2015-08-20 15:46:44 -07:00
|
|
|
if (this.zoomControls_) {
|
|
|
|
this.zoomControls_.position();
|
2015-08-19 17:21:05 -07:00
|
|
|
}
|
2015-04-28 17:55:45 -07:00
|
|
|
if (this.scrollbar) {
|
|
|
|
this.scrollbar.resize();
|
|
|
|
}
|
2016-08-10 18:00:15 -07:00
|
|
|
this.updateScreenCalculations_();
|
2015-04-28 17:55:45 -07:00
|
|
|
};
|
|
|
|
|
2016-08-10 18:00:15 -07:00
|
|
|
/**
|
|
|
|
* 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 */
|
|
|
|
|
2014-12-23 11:22:02 -08:00
|
|
|
/**
|
|
|
|
* 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_;
|
|
|
|
};
|
|
|
|
|
2016-01-07 17:01:01 -08:00
|
|
|
/**
|
2016-01-08 13:03:22 -08:00
|
|
|
* Get the SVG element that contains this workspace.
|
2016-01-07 17:01:01 -08:00
|
|
|
* @return {!Element} SVG element.
|
|
|
|
*/
|
|
|
|
Blockly.WorkspaceSvg.prototype.getParentSvg = function() {
|
2016-01-08 13:58:18 -08:00
|
|
|
if (this.cachedParentSvg_) {
|
|
|
|
return this.cachedParentSvg_;
|
2016-01-07 17:01:01 -08:00
|
|
|
}
|
|
|
|
var element = this.svgGroup_;
|
|
|
|
while (element) {
|
|
|
|
if (element.tagName == 'svg') {
|
2016-01-08 13:58:18 -08:00
|
|
|
this.cachedParentSvg_ = element;
|
2016-01-07 17:01:01 -08:00
|
|
|
return element;
|
|
|
|
}
|
|
|
|
element = element.parentNode;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
};
|
|
|
|
|
2015-03-06 15:27:41 -06:00
|
|
|
/**
|
|
|
|
* Translate this workspace to new coordinates.
|
|
|
|
* @param {number} x Horizontal translation.
|
|
|
|
* @param {number} y Vertical translation.
|
|
|
|
*/
|
|
|
|
Blockly.WorkspaceSvg.prototype.translate = function(x, y) {
|
2015-10-21 14:38:39 -07:00
|
|
|
var translation = 'translate(' + x + ',' + y + ') ' +
|
2015-08-19 17:21:05 -07:00
|
|
|
'scale(' + this.scale + ')';
|
2015-03-06 15:27:41 -06:00
|
|
|
this.svgBlockCanvas_.setAttribute('transform', translation);
|
|
|
|
this.svgBubbleCanvas_.setAttribute('transform', translation);
|
|
|
|
};
|
|
|
|
|
2014-12-23 11:22:02 -08:00
|
|
|
/**
|
|
|
|
* Returns the horizontal offset of the workspace.
|
|
|
|
* Intended for LTR/RTL compatibility in XML.
|
|
|
|
* @return {number} Width.
|
|
|
|
*/
|
|
|
|
Blockly.WorkspaceSvg.prototype.getWidth = function() {
|
2016-02-24 16:52:55 -08:00
|
|
|
var metrics = this.getMetrics();
|
2016-02-25 11:08:50 -08:00
|
|
|
return metrics ? metrics.viewWidth / this.scale : 0;
|
2014-12-23 11:22:02 -08:00
|
|
|
};
|
|
|
|
|
2015-05-22 17:08:59 -07:00
|
|
|
/**
|
|
|
|
* 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) {
|
2016-01-07 17:01:01 -08:00
|
|
|
this.getParentSvg().style.display = isVisible ? 'block' : 'none';
|
2015-05-22 17:08:59 -07:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2014-12-23 11:22:02 -08:00
|
|
|
/**
|
|
|
|
* Render all blocks in workspace.
|
|
|
|
*/
|
|
|
|
Blockly.WorkspaceSvg.prototype.render = function() {
|
2015-10-24 23:51:27 -04:00
|
|
|
// 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);
|
2014-12-23 11:22:02 -08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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) {
|
2016-09-23 13:46:11 -07:00
|
|
|
this.traceWrapper_ = Blockly.bindEventWithChecks_(this.svgBlockCanvas_,
|
2016-05-04 15:05:45 -07:00
|
|
|
'blocklySelectChange', this, function() {this.traceOn_ = false;});
|
2014-12-23 11:22:02 -08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Highlight a block in the workspace.
|
|
|
|
* @param {?string} id ID of block to find.
|
|
|
|
*/
|
|
|
|
Blockly.WorkspaceSvg.prototype.highlightBlock = function(id) {
|
2016-03-29 08:36:11 -07:00
|
|
|
if (this.traceOn_ && Blockly.dragMode_ != Blockly.DRAG_NONE) {
|
2014-12-23 11:22:02 -08:00
|
|
|
// 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) {
|
2016-04-05 18:43:39 -07:00
|
|
|
block = this.getBlockById(id);
|
2014-12-23 11:22:02 -08:00
|
|
|
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) {
|
2015-04-28 13:51:25 -07:00
|
|
|
if (!this.rendered || xmlBlock.getElementsByTagName('block').length >=
|
2014-12-23 11:22:02 -08:00
|
|
|
this.remainingCapacity()) {
|
|
|
|
return;
|
|
|
|
}
|
2015-04-28 13:51:25 -07:00
|
|
|
Blockly.terminateDrag_(); // Dragging while pasting? No.
|
2016-02-16 21:57:22 -08:00
|
|
|
Blockly.Events.disable();
|
2016-04-27 14:21:22 +02:00
|
|
|
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;
|
2014-12-23 11:22:02 -08:00
|
|
|
}
|
2016-04-27 14:21:22 +02:00
|
|
|
// 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) {
|
2015-07-04 05:34:59 +01:00
|
|
|
collide = true;
|
2015-07-12 15:38:59 -07:00
|
|
|
break;
|
2015-07-04 05:34:59 +01:00
|
|
|
}
|
|
|
|
}
|
2016-04-27 14:21:22 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
2015-07-04 05:34:59 +01:00
|
|
|
}
|
2016-04-27 14:21:22 +02:00
|
|
|
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();
|
2014-12-23 11:22:02 -08:00
|
|
|
}
|
2016-02-16 21:57:22 -08:00
|
|
|
if (Blockly.Events.isEnabled() && !block.isShadow()) {
|
|
|
|
Blockly.Events.fire(new Blockly.Events.Create(block));
|
|
|
|
}
|
2014-12-23 11:22:02 -08:00
|
|
|
block.select();
|
|
|
|
};
|
|
|
|
|
2016-07-01 15:51:59 -07:00
|
|
|
/**
|
|
|
|
* 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);
|
2016-09-07 15:49:20 -07:00
|
|
|
// Don't refresh the toolbox if there's a drag in progress.
|
|
|
|
if (this.toolbox_ && this.toolbox_.flyout_ && !Blockly.Flyout.startFlyout_) {
|
2016-07-01 15:51:59 -07:00
|
|
|
this.toolbox_.refreshSelection();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2014-12-23 11:22:02 -08:00
|
|
|
/**
|
|
|
|
* Make a list of all the delete areas for this workspace.
|
|
|
|
*/
|
|
|
|
Blockly.WorkspaceSvg.prototype.recordDeleteAreas = function() {
|
|
|
|
if (this.trashcan) {
|
2016-02-03 15:28:29 -08:00
|
|
|
this.deleteAreaTrash_ = this.trashcan.getClientRect();
|
2014-12-23 11:22:02 -08:00
|
|
|
} else {
|
|
|
|
this.deleteAreaTrash_ = null;
|
|
|
|
}
|
|
|
|
if (this.flyout_) {
|
2016-02-03 15:28:29 -08:00
|
|
|
this.deleteAreaToolbox_ = this.flyout_.getClientRect();
|
2014-12-23 11:22:02 -08:00
|
|
|
} else if (this.toolbox_) {
|
2016-02-03 15:28:29 -08:00
|
|
|
this.deleteAreaToolbox_ = this.toolbox_.getClientRect();
|
2014-12-23 11:22:02 -08:00
|
|
|
} else {
|
|
|
|
this.deleteAreaToolbox_ = null;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2016-05-04 14:49:00 -07:00
|
|
|
* Is the mouse event over a delete area (toolbox or non-closing flyout)?
|
2014-12-23 11:22:02 -08:00
|
|
|
* 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) {
|
2016-02-03 15:28:29 -08:00
|
|
|
var xy = new goog.math.Coordinate(e.clientX, e.clientY);
|
2014-12-23 11:22:02 -08:00
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2015-04-28 13:51:25 -07:00
|
|
|
/**
|
|
|
|
* 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)) {
|
2016-09-07 17:42:09 -07:00
|
|
|
Blockly.Touch.clearTouchIdentifier();
|
2015-04-28 13:51:25 -07:00
|
|
|
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);
|
2016-09-07 17:42:09 -07:00
|
|
|
// Since this was a click, not a drag, end the gesture immediately.
|
|
|
|
Blockly.Touch.clearTouchIdentifier();
|
2015-04-28 13:51:25 -07:00
|
|
|
} else if (this.scrollbar) {
|
2016-07-12 20:34:02 +02:00
|
|
|
this.dragMode_ = Blockly.DRAG_BEGIN;
|
2015-04-28 13:51:25 -07:00
|
|
|
// 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.
|
2016-09-07 17:42:09 -07:00
|
|
|
if ('mouseup' in Blockly.Touch.TOUCH_MAP) {
|
|
|
|
Blockly.Touch.onTouchUpWrapper_ = Blockly.Touch.onTouchUpWrapper_ || [];
|
|
|
|
Blockly.Touch.onTouchUpWrapper_ = Blockly.Touch.onTouchUpWrapper_.concat(
|
2016-09-23 13:46:11 -07:00
|
|
|
Blockly.bindEventWithChecks_(document, 'mouseup', null,
|
|
|
|
Blockly.onMouseUp_));
|
2015-04-28 13:51:25 -07:00
|
|
|
}
|
2016-01-05 13:54:52 +08:00
|
|
|
Blockly.onMouseMoveWrapper_ = Blockly.onMouseMoveWrapper_ || [];
|
|
|
|
Blockly.onMouseMoveWrapper_ = Blockly.onMouseMoveWrapper_.concat(
|
2016-09-23 13:46:11 -07:00
|
|
|
Blockly.bindEventWithChecks_(document, 'mousemove', null,
|
|
|
|
Blockly.onMouseMove_));
|
2015-04-28 13:51:25 -07:00
|
|
|
}
|
|
|
|
// This event has been handled. No need to bubble up to the document.
|
|
|
|
e.stopPropagation();
|
2016-05-04 19:12:38 -07:00
|
|
|
e.preventDefault();
|
2015-04-28 13:51:25 -07:00
|
|
|
};
|
|
|
|
|
2015-08-19 17:21:05 -07:00
|
|
|
/**
|
|
|
|
* Start tracking a drag of an object on this workspace.
|
|
|
|
* @param {!Event} e Mouse down event.
|
2016-04-19 23:24:42 -07:00
|
|
|
* @param {!goog.math.Coordinate} xy Starting location of object.
|
2015-08-19 17:21:05 -07:00
|
|
|
*/
|
2016-04-19 23:24:42 -07:00
|
|
|
Blockly.WorkspaceSvg.prototype.startDrag = function(e, xy) {
|
2015-08-19 17:21:05 -07:00
|
|
|
// Record the starting offset between the bubble's location and the mouse.
|
2016-06-17 14:26:04 -07:00
|
|
|
var point = Blockly.mouseToSvg(e, this.getParentSvg(),
|
|
|
|
this.getInverseScreenCTM());
|
2015-08-19 17:21:05 -07:00
|
|
|
// Fix scale of mouse event.
|
|
|
|
point.x /= this.scale;
|
|
|
|
point.y /= this.scale;
|
2016-04-19 23:24:42 -07:00
|
|
|
this.dragDeltaXY_ = goog.math.Coordinate.difference(xy, point);
|
2015-08-19 17:21:05 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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) {
|
2016-06-17 14:26:04 -07:00
|
|
|
var point = Blockly.mouseToSvg(e, this.getParentSvg(),
|
|
|
|
this.getInverseScreenCTM());
|
2015-08-19 17:21:05 -07:00
|
|
|
// Fix scale of mouse event.
|
|
|
|
point.x /= this.scale;
|
|
|
|
point.y /= this.scale;
|
2016-04-19 23:24:42 -07:00
|
|
|
return goog.math.Coordinate.sum(this.dragDeltaXY_, point);
|
2015-08-19 17:21:05 -07:00
|
|
|
};
|
|
|
|
|
2016-06-29 03:11:48 +02:00
|
|
|
/**
|
2016-07-15 10:35:45 +02:00
|
|
|
* Is the user currently dragging a block or scrolling the flyout/workspace?
|
2016-06-29 17:44:12 -07:00
|
|
|
* @return {boolean} True if currently dragging or scrolling.
|
2016-06-29 03:11:48 +02:00
|
|
|
*/
|
|
|
|
Blockly.WorkspaceSvg.prototype.isDragging = function() {
|
2016-07-12 20:34:02 +02:00
|
|
|
return Blockly.dragMode_ == Blockly.DRAG_FREE ||
|
2016-07-15 10:35:45 +02:00
|
|
|
(Blockly.Flyout.startFlyout_ &&
|
|
|
|
Blockly.Flyout.startFlyout_.dragMode_ == Blockly.DRAG_FREE) ||
|
2016-07-12 20:34:02 +02:00
|
|
|
this.dragMode_ == Blockly.DRAG_FREE;
|
2016-06-29 03:11:48 +02:00
|
|
|
};
|
|
|
|
|
2015-08-19 17:21:05 -07:00
|
|
|
/**
|
|
|
|
* 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;
|
2016-06-17 14:26:04 -07:00
|
|
|
var position = Blockly.mouseToSvg(e, this.getParentSvg(),
|
|
|
|
this.getInverseScreenCTM());
|
2015-08-19 17:21:05 -07:00
|
|
|
this.zoom(position.x, position.y, delta);
|
|
|
|
e.preventDefault();
|
|
|
|
};
|
|
|
|
|
2016-02-29 12:52:38 -08:00
|
|
|
/**
|
|
|
|
* 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() {
|
2016-07-08 15:50:09 -07:00
|
|
|
var topBlocks = this.getTopBlocks(false);
|
2016-02-29 12:52:38 -08:00
|
|
|
// There are no blocks, return empty rectangle.
|
2016-03-14 16:00:25 -07:00
|
|
|
if (!topBlocks.length) {
|
2016-02-29 12:52:38 -08:00
|
|
|
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) {
|
2016-03-18 15:19:26 -07:00
|
|
|
boundary.bottomRight.x = blockBoundary.bottomRight.x;
|
2016-02-29 12:52:38 -08:00
|
|
|
}
|
|
|
|
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
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2015-10-09 19:22:22 -07:00
|
|
|
/**
|
|
|
|
* Clean up the workspace by ordering all the blocks in a column.
|
|
|
|
*/
|
2016-08-02 17:18:12 -07:00
|
|
|
Blockly.WorkspaceSvg.prototype.cleanUp = function() {
|
2016-03-03 17:48:54 -08:00
|
|
|
Blockly.Events.setGroup(true);
|
2015-10-09 19:22:22 -07:00
|
|
|
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();
|
2015-10-12 16:14:03 -07:00
|
|
|
cursorY = block.getRelativeToSurfaceXY().y +
|
|
|
|
block.getHeightWidth().height + Blockly.BlockSvg.MIN_BLOCK_Y;
|
2015-10-09 19:22:22 -07:00
|
|
|
}
|
2016-03-03 17:48:54 -08:00
|
|
|
Blockly.Events.setGroup(false);
|
2015-10-09 19:22:22 -07:00
|
|
|
// Fire an event to allow scrollbars to resize.
|
2016-08-19 14:13:20 -07:00
|
|
|
this.resizeContents();
|
2015-10-09 19:22:22 -07:00
|
|
|
};
|
|
|
|
|
2015-04-28 13:51:25 -07:00
|
|
|
/**
|
|
|
|
* Show the context menu for the workspace.
|
|
|
|
* @param {!Event} e Mouse event.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
Blockly.WorkspaceSvg.prototype.showContextMenu_ = function(e) {
|
2015-12-17 14:16:04 -08:00
|
|
|
if (this.options.readOnly || this.isFlyout) {
|
2015-04-28 13:51:25 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
var menuOptions = [];
|
2015-10-09 19:22:22 -07:00
|
|
|
var topBlocks = this.getTopBlocks(true);
|
2016-03-17 14:44:26 -07:00
|
|
|
var eventGroup = Blockly.genUid();
|
|
|
|
|
|
|
|
// Options to undo/redo previous action.
|
|
|
|
var undoOption = {};
|
|
|
|
undoOption.text = Blockly.Msg.UNDO;
|
2016-03-18 14:17:31 -07:00
|
|
|
undoOption.enabled = this.undoStack_.length > 0;
|
2016-03-17 14:44:26 -07:00
|
|
|
undoOption.callback = this.undo.bind(this, false);
|
|
|
|
menuOptions.push(undoOption);
|
|
|
|
var redoOption = {};
|
|
|
|
redoOption.text = Blockly.Msg.REDO;
|
2016-03-18 14:17:31 -07:00
|
|
|
redoOption.enabled = this.redoStack_.length > 0;
|
2016-03-17 14:44:26 -07:00
|
|
|
redoOption.callback = this.undo.bind(this, true);
|
|
|
|
menuOptions.push(redoOption);
|
|
|
|
|
2015-10-09 19:22:22 -07:00
|
|
|
// Option to clean up blocks.
|
2016-03-24 21:31:13 -07:00
|
|
|
if (this.scrollbar) {
|
|
|
|
var cleanOption = {};
|
|
|
|
cleanOption.text = Blockly.Msg.CLEAN_UP;
|
|
|
|
cleanOption.enabled = topBlocks.length > 1;
|
2016-08-02 17:18:12 -07:00
|
|
|
cleanOption.callback = this.cleanUp.bind(this);
|
2016-03-24 21:31:13 -07:00
|
|
|
menuOptions.push(cleanOption);
|
|
|
|
}
|
2015-10-09 19:22:22 -07:00
|
|
|
|
2015-04-28 13:51:25 -07:00
|
|
|
// Add a little animation to collapsing and expanding.
|
2015-12-02 22:10:09 -08:00
|
|
|
var DELAY = 10;
|
2015-04-28 13:51:25 -07:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-14 03:50:35 -07:00
|
|
|
/**
|
|
|
|
* Option to collapse or expand top blocks.
|
2015-10-09 13:53:14 -07:00
|
|
|
* @param {boolean} shouldCollapse Whether a block should collapse.
|
2015-10-05 21:26:16 +08:00
|
|
|
* @private
|
|
|
|
*/
|
2015-10-09 13:53:14 -07:00
|
|
|
var toggleOption = function(shouldCollapse) {
|
2015-04-28 13:51:25 -07:00
|
|
|
var ms = 0;
|
|
|
|
for (var i = 0; i < topBlocks.length; i++) {
|
|
|
|
var block = topBlocks[i];
|
|
|
|
while (block) {
|
2015-10-05 21:26:16 +08:00
|
|
|
setTimeout(block.setCollapsed.bind(block, shouldCollapse), ms);
|
2015-04-28 13:51:25 -07:00
|
|
|
block = block.getNextBlock();
|
2015-12-02 22:10:09 -08:00
|
|
|
ms += DELAY;
|
2015-04-28 13:51:25 -07:00
|
|
|
}
|
|
|
|
}
|
2015-10-09 13:53:14 -07:00
|
|
|
};
|
2015-10-05 21:26:16 +08:00
|
|
|
|
|
|
|
// Option to collapse top blocks.
|
|
|
|
var collapseOption = {enabled: hasExpandedBlocks};
|
|
|
|
collapseOption.text = Blockly.Msg.COLLAPSE_ALL;
|
|
|
|
collapseOption.callback = function() {
|
|
|
|
toggleOption(true);
|
2015-04-28 13:51:25 -07:00
|
|
|
};
|
|
|
|
menuOptions.push(collapseOption);
|
|
|
|
|
|
|
|
// Option to expand top blocks.
|
|
|
|
var expandOption = {enabled: hasCollapsedBlocks};
|
|
|
|
expandOption.text = Blockly.Msg.EXPAND_ALL;
|
|
|
|
expandOption.callback = function() {
|
2015-10-05 21:26:16 +08:00
|
|
|
toggleOption(false);
|
2015-04-28 13:51:25 -07:00
|
|
|
};
|
|
|
|
menuOptions.push(expandOption);
|
|
|
|
}
|
|
|
|
|
2015-12-02 22:10:09 -08:00
|
|
|
// 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]);
|
|
|
|
}
|
2016-05-25 12:53:42 -07:00
|
|
|
|
2015-12-02 22:10:09 -08:00
|
|
|
function deleteNext() {
|
2016-03-17 14:44:26 -07:00
|
|
|
Blockly.Events.setGroup(eventGroup);
|
2015-12-02 22:10:09 -08:00
|
|
|
var block = deleteList.shift();
|
|
|
|
if (block) {
|
|
|
|
if (block.workspace) {
|
|
|
|
block.dispose(false, true);
|
|
|
|
setTimeout(deleteNext, DELAY);
|
|
|
|
} else {
|
|
|
|
deleteNext();
|
|
|
|
}
|
|
|
|
}
|
2016-03-17 14:44:26 -07:00
|
|
|
Blockly.Events.setGroup(false);
|
2015-12-02 22:10:09 -08:00
|
|
|
}
|
2016-05-25 12:53:42 -07:00
|
|
|
|
|
|
|
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() {
|
2016-10-20 09:30:45 -07:00
|
|
|
if (deleteList.length < 2 ) {
|
2016-05-25 12:53:42 -07:00
|
|
|
deleteNext();
|
2016-10-20 09:30:45 -07:00
|
|
|
} else {
|
|
|
|
Blockly.confirm(Blockly.Msg.DELETE_ALL_BLOCKS.
|
|
|
|
replace('%1',String(deleteList.length)),
|
|
|
|
function(ok) {
|
|
|
|
if (ok) {
|
|
|
|
deleteNext();
|
|
|
|
}
|
|
|
|
});
|
2016-05-25 12:53:42 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2015-12-02 22:10:09 -08:00
|
|
|
menuOptions.push(deleteOption);
|
|
|
|
|
2015-04-28 13:51:25 -07:00
|
|
|
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) {
|
2015-08-19 18:42:35 -07:00
|
|
|
if (!filenames.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
var audioTest = new window['Audio']();
|
2016-03-18 15:19:26 -07:00
|
|
|
} catch (e) {
|
2015-04-28 13:51:25 -07:00
|
|
|
// No browser support for Audio.
|
2015-08-19 18:42:35 -07:00
|
|
|
// IE can throw an error even if the Audio object exists.
|
2015-04-28 13:51:25 -07:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2016-07-15 14:55:00 -07:00
|
|
|
* Play a named sound at specified volume. If volume is not specified,
|
2015-04-28 13:51:25 -07:00
|
|
|
* use full volume (1).
|
|
|
|
* @param {string} name Name of sound.
|
2015-07-13 15:03:22 -07:00
|
|
|
* @param {number=} opt_volume Volume of sound (0-1).
|
2015-04-28 13:51:25 -07:00
|
|
|
*/
|
|
|
|
Blockly.WorkspaceSvg.prototype.playAudio = function(name, opt_volume) {
|
|
|
|
var sound = this.SOUNDS_[name];
|
|
|
|
if (sound) {
|
2016-05-21 05:55:24 -07:00
|
|
|
// Don't play one sound on top of another.
|
2016-06-13 18:49:18 -07:00
|
|
|
var now = new Date;
|
2016-05-21 06:14:33 -07:00
|
|
|
if (now - this.lastSound_ < Blockly.SOUND_LIMIT) {
|
2016-05-21 05:55:24 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.lastSound_ = now;
|
2015-04-28 13:51:25 -07:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-07-09 11:57:06 -07:00
|
|
|
/**
|
|
|
|
* 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) {
|
2016-04-07 13:38:36 -07:00
|
|
|
tree = Blockly.Options.parseToolboxTree(tree);
|
2015-07-09 11:57:06 -07:00
|
|
|
if (!tree) {
|
|
|
|
if (this.options.languageTree) {
|
|
|
|
throw 'Can\'t nullify an existing toolbox.';
|
|
|
|
}
|
2016-02-05 18:41:47 -08:00
|
|
|
return; // No change (null to null).
|
2015-07-09 11:57:06 -07:00
|
|
|
}
|
|
|
|
if (!this.options.languageTree) {
|
|
|
|
throw 'Existing toolbox is null. Can\'t create new toolbox.';
|
|
|
|
}
|
2016-02-05 18:41:47 -08:00
|
|
|
if (tree.getElementsByTagName('category').length) {
|
2015-07-09 11:57:06 -07:00
|
|
|
if (!this.toolbox_) {
|
|
|
|
throw 'Existing toolbox has no categories. Can\'t change mode.';
|
|
|
|
}
|
|
|
|
this.options.languageTree = tree;
|
|
|
|
this.toolbox_.populate_(tree);
|
2015-12-01 12:28:19 -08:00
|
|
|
this.toolbox_.addColour_();
|
2015-07-09 11:57:06 -07:00
|
|
|
} else {
|
|
|
|
if (!this.flyout_) {
|
|
|
|
throw 'Existing toolbox has categories. Can\'t change mode.';
|
|
|
|
}
|
|
|
|
this.options.languageTree = tree;
|
|
|
|
this.flyout_.show(tree.childNodes);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-04-28 13:51:25 -07:00
|
|
|
/**
|
|
|
|
* Mark this workspace as the currently focused main workspace.
|
|
|
|
*/
|
|
|
|
Blockly.WorkspaceSvg.prototype.markFocused = function() {
|
2015-09-23 14:46:29 -07:00
|
|
|
if (this.options.parentWorkspace) {
|
|
|
|
this.options.parentWorkspace.markFocused();
|
|
|
|
} else {
|
|
|
|
Blockly.mainWorkspace = this;
|
|
|
|
}
|
2015-04-28 13:51:25 -07:00
|
|
|
};
|
|
|
|
|
2015-08-19 17:21:05 -07:00
|
|
|
/**
|
|
|
|
* 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();
|
2016-01-07 17:01:01 -08:00
|
|
|
var center = this.getParentSvg().createSVGPoint();
|
2015-08-19 17:21:05 -07:00
|
|
|
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;
|
|
|
|
}
|
2016-04-14 00:23:17 -07:00
|
|
|
if (this.scale == newScale) {
|
2015-09-01 20:00:13 +01:00
|
|
|
return; // No change in zoom.
|
|
|
|
}
|
2015-10-21 14:38:39 -07:00
|
|
|
if (this.scrollbar) {
|
2016-04-14 00:23:17 -07:00
|
|
|
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);
|
2015-08-19 17:21:05 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
};
|
|
|
|
|
2015-10-15 17:51:52 -05:00
|
|
|
/**
|
2016-02-04 13:25:30 -08:00
|
|
|
* Zoom the blocks to fit in the workspace if possible.
|
|
|
|
*/
|
2015-10-15 17:51:52 -05:00
|
|
|
Blockly.WorkspaceSvg.prototype.zoomToFit = function() {
|
2016-04-14 00:23:17 -07:00
|
|
|
var metrics = this.getMetrics();
|
|
|
|
var blocksBox = this.getBlocksBoundingBox();
|
|
|
|
var blocksWidth = blocksBox.width;
|
|
|
|
var blocksHeight = blocksBox.height;
|
|
|
|
if (!blocksWidth) {
|
2016-02-04 13:25:30 -08:00
|
|
|
return; // Prevents zooming to infinity.
|
2015-10-15 17:51:52 -05:00
|
|
|
}
|
2016-04-14 00:23:17 -07:00
|
|
|
var workspaceWidth = metrics.viewWidth;
|
|
|
|
var workspaceHeight = metrics.viewHeight;
|
2015-10-15 17:51:52 -05:00
|
|
|
if (this.flyout_) {
|
2016-04-14 00:23:17 -07:00
|
|
|
workspaceWidth -= this.flyout_.width_;
|
2015-10-15 17:51:52 -05:00
|
|
|
}
|
2016-04-14 00:23:17 -07:00
|
|
|
if (!this.scrollbar) {
|
|
|
|
// Orgin point of 0,0 is fixed, blocks will not scroll to center.
|
|
|
|
blocksWidth += metrics.contentLeft;
|
|
|
|
blocksHeight += metrics.contentTop;
|
|
|
|
}
|
2015-10-15 17:51:52 -05:00
|
|
|
var ratioX = workspaceWidth / blocksWidth;
|
|
|
|
var ratioY = workspaceHeight / blocksHeight;
|
2016-04-14 00:23:17 -07:00
|
|
|
this.setScale(Math.min(ratioX, ratioY));
|
2016-04-21 04:52:43 -07:00
|
|
|
this.scrollCenter();
|
2016-04-14 00:23:17 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Center the workspace.
|
|
|
|
*/
|
2016-04-21 04:52:43 -07:00
|
|
|
Blockly.WorkspaceSvg.prototype.scrollCenter = function() {
|
2016-04-14 00:23:17 -07:00
|
|
|
if (!this.scrollbar) {
|
|
|
|
// Can't center a non-scrolling workspace.
|
|
|
|
return;
|
2015-10-15 17:51:52 -05:00
|
|
|
}
|
2016-04-14 00:23:17 -07:00
|
|
|
var metrics = this.getMetrics();
|
|
|
|
var x = (metrics.contentWidth - metrics.viewWidth) / 2;
|
2015-10-15 17:51:52 -05:00
|
|
|
if (this.flyout_) {
|
2016-04-14 00:23:17 -07:00
|
|
|
x -= this.flyout_.width_ / 2;
|
2015-10-15 17:51:52 -05:00
|
|
|
}
|
2016-04-14 00:23:17 -07:00
|
|
|
var y = (metrics.contentHeight - metrics.viewHeight) / 2;
|
|
|
|
this.scrollbar.set(x, y);
|
2015-10-15 17:51:52 -05:00
|
|
|
};
|
|
|
|
|
2015-08-19 17:21:05 -07:00
|
|
|
/**
|
2016-04-14 00:23:17 -07:00
|
|
|
* Set the workspace's zoom factor.
|
|
|
|
* @param {number} newScale Zoom factor.
|
2015-08-19 17:21:05 -07:00
|
|
|
*/
|
2016-04-14 00:23:17 -07:00
|
|
|
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;
|
2015-09-22 17:51:21 -07:00
|
|
|
}
|
2016-04-14 00:23:17 -07:00
|
|
|
this.scale = newScale;
|
2015-08-19 17:21:05 -07:00
|
|
|
this.updateGridPattern_();
|
2015-10-21 14:38:39 -07:00
|
|
|
if (this.scrollbar) {
|
2016-04-14 00:23:17 -07:00
|
|
|
this.scrollbar.resize();
|
2015-10-21 14:38:39 -07:00
|
|
|
} else {
|
2016-04-14 00:23:17 -07:00
|
|
|
this.translate(this.scrollX, this.scrollY);
|
|
|
|
}
|
2015-09-02 00:09:49 +01:00
|
|
|
Blockly.hideChaff(false);
|
|
|
|
if (this.flyout_) {
|
|
|
|
// No toolbox, resize flyout.
|
|
|
|
this.flyout_.reflow();
|
|
|
|
}
|
2015-08-19 17:21:05 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-08-19 14:13:20 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
2016-10-06 18:52:25 -07:00
|
|
|
* @return {!Object} Contains size and position metrics of a top level
|
|
|
|
* workspace.
|
2016-08-19 14:13:20 -07:00
|
|
|
* @private
|
2016-10-06 18:06:41 -07:00
|
|
|
* @this Blockly.WorkspaceSvg
|
2016-08-19 14:13:20 -07:00
|
|
|
*/
|
|
|
|
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
|
2016-10-06 18:06:41 -07:00
|
|
|
* @this Blockly.WorkspaceSvg
|
2016-08-19 14:13:20 -07:00
|
|
|
*/
|
|
|
|
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_();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2014-12-23 11:22:02 -08:00
|
|
|
// Export symbols that would otherwise be renamed by Closure compiler.
|
2015-05-22 17:08:59 -07:00
|
|
|
Blockly.WorkspaceSvg.prototype['setVisible'] =
|
|
|
|
Blockly.WorkspaceSvg.prototype.setVisible;
|