2013-10-30 14:46:03 -07:00
|
|
|
/**
|
2014-01-28 03:00:09 -08:00
|
|
|
* @license
|
2013-10-30 14:46:03 -07:00
|
|
|
* Visual Blocks Editor
|
|
|
|
*
|
|
|
|
* Copyright 2012 Google Inc.
|
2014-10-07 13:09:55 -07:00
|
|
|
* https://developers.google.com/blockly/
|
2013-10-30 14:46:03 -07:00
|
|
|
*
|
|
|
|
* 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.
|
|
|
|
* @author fraser@google.com (Neil Fraser)
|
|
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
goog.provide('Blockly.Workspace');
|
|
|
|
|
2015-02-06 15:27:25 -08:00
|
|
|
goog.require('goog.math');
|
|
|
|
|
2013-10-30 14:46:03 -07:00
|
|
|
|
|
|
|
/**
|
2014-12-23 11:22:02 -08:00
|
|
|
* Class for a workspace. This is a data structure that contains blocks.
|
|
|
|
* There is no UI, and can be created headlessly.
|
2015-07-13 15:03:22 -07:00
|
|
|
* @param {Object=} opt_options Dictionary of options.
|
2013-10-30 14:46:03 -07:00
|
|
|
* @constructor
|
|
|
|
*/
|
2015-04-28 13:51:25 -07:00
|
|
|
Blockly.Workspace = function(opt_options) {
|
2015-12-09 10:17:40 +01:00
|
|
|
/** @type {string} */
|
|
|
|
this.id = Blockly.genUid();
|
|
|
|
Blockly.Workspace.WorkspaceDB_[this.id] = this;
|
2015-08-19 17:21:05 -07:00
|
|
|
/** @type {!Object} */
|
2015-04-28 13:51:25 -07:00
|
|
|
this.options = opt_options || {};
|
2015-08-19 17:21:05 -07:00
|
|
|
/** @type {boolean} */
|
2015-04-28 13:51:25 -07:00
|
|
|
this.RTL = !!this.options.RTL;
|
2015-08-19 17:21:05 -07:00
|
|
|
/** @type {!Array.<!Blockly.Block>} */
|
|
|
|
this.topBlocks_ = [];
|
2016-02-11 21:40:33 -08:00
|
|
|
/** @type {!Array.<!Function>} */
|
|
|
|
this.listeners_ = [];
|
2016-03-03 17:48:54 -08:00
|
|
|
/** @type {!Array.<!Blockly.Events.Abstract>} */
|
|
|
|
this.undoStack_ = [];
|
|
|
|
/** @type {!Array.<!Blockly.Events.Abstract>} */
|
|
|
|
this.redoStack_ = [];
|
2013-10-30 14:46:03 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2014-12-23 11:22:02 -08:00
|
|
|
* Workspaces may be headless.
|
|
|
|
* @type {boolean} True if visible. False if headless.
|
2013-10-30 14:46:03 -07:00
|
|
|
*/
|
2014-12-23 11:22:02 -08:00
|
|
|
Blockly.Workspace.prototype.rendered = false;
|
2013-10-30 14:46:03 -07:00
|
|
|
|
2016-03-03 17:48:54 -08:00
|
|
|
/**
|
|
|
|
* Maximum number of undo events in stack.
|
|
|
|
* @type {number} 0 to turn off undo, Infinity for unlimited.
|
|
|
|
*/
|
|
|
|
Blockly.Workspace.prototype.MAX_UNDO = 1024;
|
|
|
|
|
2013-10-30 14:46:03 -07:00
|
|
|
/**
|
|
|
|
* Dispose of this workspace.
|
|
|
|
* Unlink from all DOM elements to prevent memory leaks.
|
|
|
|
*/
|
|
|
|
Blockly.Workspace.prototype.dispose = function() {
|
2016-02-11 21:40:33 -08:00
|
|
|
this.listeners_.length = 0;
|
2014-12-23 11:22:02 -08:00
|
|
|
this.clear();
|
2015-12-09 10:17:40 +01:00
|
|
|
// Remove from workspace database.
|
|
|
|
delete Blockly.Workspace.WorkspaceDB_[this.id];
|
2013-10-30 14:46:03 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2014-12-23 11:22:02 -08:00
|
|
|
* Angle away from the horizontal to sweep for blocks. Order of execution is
|
|
|
|
* generally top to bottom, but a small angle changes the scan to give a bit of
|
|
|
|
* a left to right bias (reversed in RTL). Units are in degrees.
|
|
|
|
* See: http://tvtropes.org/pmwiki/pmwiki.php/Main/DiagonalBilling.
|
2013-10-30 14:46:03 -07:00
|
|
|
*/
|
2014-12-23 11:22:02 -08:00
|
|
|
Blockly.Workspace.SCAN_ANGLE = 3;
|
2013-10-30 14:46:03 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Add a block to the list of top blocks.
|
|
|
|
* @param {!Blockly.Block} block Block to remove.
|
|
|
|
*/
|
|
|
|
Blockly.Workspace.prototype.addTopBlock = function(block) {
|
|
|
|
this.topBlocks_.push(block);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove a block from the list of top blocks.
|
|
|
|
* @param {!Blockly.Block} block Block to remove.
|
|
|
|
*/
|
|
|
|
Blockly.Workspace.prototype.removeTopBlock = function(block) {
|
|
|
|
var found = false;
|
2014-12-23 11:22:02 -08:00
|
|
|
for (var child, i = 0; child = this.topBlocks_[i]; i++) {
|
2013-10-30 14:46:03 -07:00
|
|
|
if (child == block) {
|
2014-12-23 11:22:02 -08:00
|
|
|
this.topBlocks_.splice(i, 1);
|
2013-10-30 14:46:03 -07:00
|
|
|
found = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!found) {
|
|
|
|
throw 'Block not present in workspace\'s list of top-most blocks.';
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Finds the top-level blocks and returns them. Blocks are optionally sorted
|
|
|
|
* by position; top to bottom (with slight LTR or RTL bias).
|
|
|
|
* @param {boolean} ordered Sort the list if true.
|
|
|
|
* @return {!Array.<!Blockly.Block>} The top-level block objects.
|
|
|
|
*/
|
|
|
|
Blockly.Workspace.prototype.getTopBlocks = function(ordered) {
|
|
|
|
// Copy the topBlocks_ list.
|
|
|
|
var blocks = [].concat(this.topBlocks_);
|
|
|
|
if (ordered && blocks.length > 1) {
|
2014-11-24 15:08:19 -08:00
|
|
|
var offset = Math.sin(goog.math.toRadians(Blockly.Workspace.SCAN_ANGLE));
|
2015-04-28 13:51:25 -07:00
|
|
|
if (this.RTL) {
|
2013-10-30 14:46:03 -07:00
|
|
|
offset *= -1;
|
|
|
|
}
|
|
|
|
blocks.sort(function(a, b) {
|
|
|
|
var aXY = a.getRelativeToSurfaceXY();
|
|
|
|
var bXY = b.getRelativeToSurfaceXY();
|
|
|
|
return (aXY.y + offset * aXY.x) - (bXY.y + offset * bXY.x);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return blocks;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find all blocks in workspace. No particular order.
|
|
|
|
* @return {!Array.<!Blockly.Block>} Array of blocks.
|
|
|
|
*/
|
|
|
|
Blockly.Workspace.prototype.getAllBlocks = function() {
|
|
|
|
var blocks = this.getTopBlocks(false);
|
2014-12-23 11:22:02 -08:00
|
|
|
for (var i = 0; i < blocks.length; i++) {
|
|
|
|
blocks.push.apply(blocks, blocks[i].getChildren());
|
2013-10-30 14:46:03 -07:00
|
|
|
}
|
|
|
|
return blocks;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Dispose of all blocks in workspace.
|
|
|
|
*/
|
|
|
|
Blockly.Workspace.prototype.clear = function() {
|
|
|
|
while (this.topBlocks_.length) {
|
|
|
|
this.topBlocks_[0].dispose();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2014-12-23 11:22:02 -08:00
|
|
|
* Returns the horizontal offset of the workspace.
|
|
|
|
* Intended for LTR/RTL compatibility in XML.
|
|
|
|
* Not relevant for a headless workspace.
|
|
|
|
* @return {number} Width.
|
2013-10-30 14:46:03 -07:00
|
|
|
*/
|
2014-12-23 11:22:02 -08:00
|
|
|
Blockly.Workspace.prototype.getWidth = function() {
|
|
|
|
return 0;
|
2013-10-30 14:46:03 -07: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.
|
2015-12-09 10:02:42 +01:00
|
|
|
* @param {=string} opt_id Optional ID. Use this ID if provided, otherwise
|
|
|
|
* create a new id.
|
2015-12-07 16:40:45 +01:00
|
|
|
* @return {!Blockly.Block} The created block.
|
|
|
|
*/
|
2015-12-09 10:02:42 +01:00
|
|
|
Blockly.Workspace.prototype.newBlock = function(prototypeName, opt_id) {
|
|
|
|
return new Blockly.Block(this, prototypeName, opt_id);
|
2015-12-07 16:40:45 +01:00
|
|
|
};
|
|
|
|
|
2013-10-30 14:46:03 -07:00
|
|
|
/**
|
|
|
|
* Finds the block with the specified ID in this workspace.
|
|
|
|
* @param {string} id ID of block to find.
|
|
|
|
* @return {Blockly.Block} The matching block, or null if not found.
|
|
|
|
*/
|
|
|
|
Blockly.Workspace.prototype.getBlockById = function(id) {
|
|
|
|
// If this O(n) function fails to scale well, maintain a hash table of IDs.
|
|
|
|
var blocks = this.getAllBlocks();
|
2014-12-23 11:22:02 -08:00
|
|
|
for (var i = 0, block; block = blocks[i]; i++) {
|
2013-10-30 14:46:03 -07:00
|
|
|
if (block.id == id) {
|
|
|
|
return block;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The number of blocks that may be added to the workspace before reaching
|
|
|
|
* the maxBlocks.
|
|
|
|
* @return {number} Number of blocks left.
|
|
|
|
*/
|
|
|
|
Blockly.Workspace.prototype.remainingCapacity = function() {
|
2015-04-28 13:51:25 -07:00
|
|
|
if (isNaN(this.options.maxBlocks)) {
|
2013-10-30 14:46:03 -07:00
|
|
|
return Infinity;
|
|
|
|
}
|
2015-04-28 13:51:25 -07:00
|
|
|
return this.options.maxBlocks - this.getAllBlocks().length;
|
2013-10-30 14:46:03 -07:00
|
|
|
};
|
|
|
|
|
2016-03-03 17:48:54 -08:00
|
|
|
/**
|
|
|
|
* Undo or redo the previous action.
|
|
|
|
* @param {boolean} redo False if undo, true if redo.
|
|
|
|
*/
|
|
|
|
Blockly.Workspace.prototype.undo = function(redo) {
|
|
|
|
var sourceStack = redo ? this.redoStack_ : this.undoStack_;
|
|
|
|
var event = sourceStack.pop();
|
|
|
|
if (!event) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
Blockly.Events.recordUndo = false;
|
|
|
|
event.run(redo);
|
|
|
|
Blockly.Events.recordUndo = true;
|
|
|
|
(redo ? this.undoStack_ : this.redoStack_).push(event);
|
|
|
|
// Do another undo/redo if the next one is of the same group.
|
|
|
|
if (sourceStack.length && event.group &&
|
|
|
|
event.group == sourceStack[sourceStack.length - 1].group) {
|
|
|
|
this.undo(redo);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-02-11 21:40:33 -08:00
|
|
|
/**
|
|
|
|
* When something in this workspace changes, call a function.
|
|
|
|
* @param {!Function} func Function to call.
|
|
|
|
* @return {!Function} Function that can be passed to
|
|
|
|
* removeChangeListener.
|
|
|
|
*/
|
|
|
|
Blockly.Workspace.prototype.addChangeListener = function(func) {
|
|
|
|
this.listeners_.push(func);
|
|
|
|
return func;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stop listening for this workspace's changes.
|
|
|
|
* @param {Function} func Function to stop calling.
|
|
|
|
*/
|
|
|
|
Blockly.Workspace.prototype.removeChangeListener = function(func) {
|
|
|
|
var i = this.listeners_.indexOf(func);
|
|
|
|
if (i != -1) {
|
|
|
|
this.listeners_.splice(i, 1);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fire a change event.
|
|
|
|
* @param {!Blockly.Events.Abstract} event Event to fire.
|
|
|
|
*/
|
|
|
|
Blockly.Workspace.prototype.fireChangeListener = function(event) {
|
2016-03-03 17:48:54 -08:00
|
|
|
if (event.recordUndo) {
|
|
|
|
this.undoStack_.push(event);
|
|
|
|
this.redoStack_.length = 0;
|
|
|
|
if (this.undoStack_.length > this.MAX_UNDO) {
|
|
|
|
this.undoStack_.unshift();
|
|
|
|
}
|
|
|
|
}
|
2016-02-11 21:40:33 -08:00
|
|
|
for (var i = 0, func; func = this.listeners_[i]; i++) {
|
|
|
|
func(event);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-12-09 10:17:40 +01:00
|
|
|
/**
|
|
|
|
* Database of all workspaces.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
Blockly.Workspace.WorkspaceDB_ = Object.create(null);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find the workspace with the specified ID.
|
|
|
|
* @param {string} id ID of workspace to find.
|
|
|
|
* @return {Blockly.Workspace} The sought after workspace or null if not found.
|
|
|
|
*/
|
|
|
|
Blockly.Workspace.getById = function(id) {
|
|
|
|
return Blockly.Workspace.WorkspaceDB_[id] || null;
|
|
|
|
};
|
|
|
|
|
2015-05-22 17:08:59 -07:00
|
|
|
// Export symbols that would otherwise be renamed by Closure compiler.
|
|
|
|
Blockly.Workspace.prototype['clear'] = Blockly.Workspace.prototype.clear;
|
2016-02-11 21:40:33 -08:00
|
|
|
Blockly.Workspace.prototype['addChangeListener'] =
|
|
|
|
Blockly.Workspace.prototype.addChangeListener;
|
|
|
|
Blockly.Workspace.prototype['removeChangeListener'] =
|
|
|
|
Blockly.Workspace.prototype.removeChangeListener;
|