workspace.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. /**
  2. * @license
  3. * Visual Blocks Editor
  4. *
  5. * Copyright 2012 Google Inc.
  6. * https://developers.google.com/blockly/
  7. *
  8. * Licensed under the Apache License, Version 2.0 (the "License");
  9. * you may not use this file except in compliance with the License.
  10. * You may obtain a copy of the License at
  11. *
  12. * http://www.apache.org/licenses/LICENSE-2.0
  13. *
  14. * Unless required by applicable law or agreed to in writing, software
  15. * distributed under the License is distributed on an "AS IS" BASIS,
  16. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. * See the License for the specific language governing permissions and
  18. * limitations under the License.
  19. */
  20. /**
  21. * @fileoverview Object representing a workspace.
  22. * @author fraser@google.com (Neil Fraser)
  23. */
  24. 'use strict';
  25. goog.provide('Blockly.Workspace');
  26. goog.require('goog.math');
  27. /**
  28. * Class for a workspace. This is a data structure that contains blocks.
  29. * There is no UI, and can be created headlessly.
  30. * @param {Blockly.Options} opt_options Dictionary of options.
  31. * @constructor
  32. */
  33. Blockly.Workspace = function(opt_options) {
  34. /** @type {string} */
  35. this.id = Blockly.genUid();
  36. Blockly.Workspace.WorkspaceDB_[this.id] = this;
  37. /** @type {!Blockly.Options} */
  38. this.options = opt_options || {};
  39. /** @type {boolean} */
  40. this.RTL = !!this.options.RTL;
  41. /** @type {boolean} */
  42. this.horizontalLayout = !!this.options.horizontalLayout;
  43. /** @type {number} */
  44. this.toolboxPosition = this.options.toolboxPosition;
  45. /**
  46. * @type {!Array.<!Blockly.Block>}
  47. * @private
  48. */
  49. this.topBlocks_ = [];
  50. /**
  51. * @type {!Array.<!Function>}
  52. * @private
  53. */
  54. this.listeners_ = [];
  55. /**
  56. * @type {!Array.<!Blockly.Events.Abstract>}
  57. * @private
  58. */
  59. this.undoStack_ = [];
  60. /**
  61. * @type {!Array.<!Blockly.Events.Abstract>}
  62. * @private
  63. */
  64. this.redoStack_ = [];
  65. /**
  66. * @type {!Object}
  67. * @private
  68. */
  69. this.blockDB_ = Object.create(null);
  70. };
  71. /**
  72. * Workspaces may be headless.
  73. * @type {boolean} True if visible. False if headless.
  74. */
  75. Blockly.Workspace.prototype.rendered = false;
  76. /**
  77. * Maximum number of undo events in stack.
  78. * @type {number} 0 to turn off undo, Infinity for unlimited.
  79. */
  80. Blockly.Workspace.prototype.MAX_UNDO = 1024;
  81. /**
  82. * Dispose of this workspace.
  83. * Unlink from all DOM elements to prevent memory leaks.
  84. */
  85. Blockly.Workspace.prototype.dispose = function() {
  86. this.listeners_.length = 0;
  87. this.clear();
  88. // Remove from workspace database.
  89. delete Blockly.Workspace.WorkspaceDB_[this.id];
  90. };
  91. /**
  92. * Angle away from the horizontal to sweep for blocks. Order of execution is
  93. * generally top to bottom, but a small angle changes the scan to give a bit of
  94. * a left to right bias (reversed in RTL). Units are in degrees.
  95. * See: http://tvtropes.org/pmwiki/pmwiki.php/Main/DiagonalBilling.
  96. */
  97. Blockly.Workspace.SCAN_ANGLE = 3;
  98. /**
  99. * Add a block to the list of top blocks.
  100. * @param {!Blockly.Block} block Block to remove.
  101. */
  102. Blockly.Workspace.prototype.addTopBlock = function(block) {
  103. this.topBlocks_.push(block);
  104. };
  105. /**
  106. * Remove a block from the list of top blocks.
  107. * @param {!Blockly.Block} block Block to remove.
  108. */
  109. Blockly.Workspace.prototype.removeTopBlock = function(block) {
  110. var found = false;
  111. for (var child, i = 0; child = this.topBlocks_[i]; i++) {
  112. if (child == block) {
  113. this.topBlocks_.splice(i, 1);
  114. found = true;
  115. break;
  116. }
  117. }
  118. if (!found) {
  119. throw 'Block not present in workspace\'s list of top-most blocks.';
  120. }
  121. };
  122. /**
  123. * Finds the top-level blocks and returns them. Blocks are optionally sorted
  124. * by position; top to bottom (with slight LTR or RTL bias).
  125. * @param {boolean} ordered Sort the list if true.
  126. * @return {!Array.<!Blockly.Block>} The top-level block objects.
  127. */
  128. Blockly.Workspace.prototype.getTopBlocks = function(ordered) {
  129. // Copy the topBlocks_ list.
  130. var blocks = [].concat(this.topBlocks_);
  131. if (ordered && blocks.length > 1) {
  132. var offset = Math.sin(goog.math.toRadians(Blockly.Workspace.SCAN_ANGLE));
  133. if (this.RTL) {
  134. offset *= -1;
  135. }
  136. blocks.sort(function(a, b) {
  137. var aXY = a.getRelativeToSurfaceXY();
  138. var bXY = b.getRelativeToSurfaceXY();
  139. return (aXY.y + offset * aXY.x) - (bXY.y + offset * bXY.x);
  140. });
  141. }
  142. return blocks;
  143. };
  144. /**
  145. * Find all blocks in workspace. No particular order.
  146. * @return {!Array.<!Blockly.Block>} Array of blocks.
  147. */
  148. Blockly.Workspace.prototype.getAllBlocks = function() {
  149. var blocks = this.getTopBlocks(false);
  150. for (var i = 0; i < blocks.length; i++) {
  151. blocks.push.apply(blocks, blocks[i].getChildren());
  152. }
  153. return blocks;
  154. };
  155. /**
  156. * Dispose of all blocks in workspace.
  157. */
  158. Blockly.Workspace.prototype.clear = function() {
  159. var existingGroup = Blockly.Events.getGroup();
  160. if (!existingGroup) {
  161. Blockly.Events.setGroup(true);
  162. }
  163. while (this.topBlocks_.length) {
  164. this.topBlocks_[0].dispose();
  165. }
  166. if (!existingGroup) {
  167. Blockly.Events.setGroup(false);
  168. }
  169. };
  170. /**
  171. * Returns the horizontal offset of the workspace.
  172. * Intended for LTR/RTL compatibility in XML.
  173. * Not relevant for a headless workspace.
  174. * @return {number} Width.
  175. */
  176. Blockly.Workspace.prototype.getWidth = function() {
  177. return 0;
  178. };
  179. /**
  180. * Obtain a newly created block.
  181. * @param {?string} prototypeName Name of the language object containing
  182. * type-specific functions for this block.
  183. * @param {=string} opt_id Optional ID. Use this ID if provided, otherwise
  184. * create a new id.
  185. * @return {!Blockly.Block} The created block.
  186. */
  187. Blockly.Workspace.prototype.newBlock = function(prototypeName, opt_id) {
  188. return new Blockly.Block(this, prototypeName, opt_id);
  189. };
  190. /**
  191. * The number of blocks that may be added to the workspace before reaching
  192. * the maxBlocks.
  193. * @return {number} Number of blocks left.
  194. */
  195. Blockly.Workspace.prototype.remainingCapacity = function() {
  196. if (isNaN(this.options.maxBlocks)) {
  197. return Infinity;
  198. }
  199. return this.options.maxBlocks - this.getAllBlocks().length;
  200. };
  201. /**
  202. * Undo or redo the previous action.
  203. * @param {boolean} redo False if undo, true if redo.
  204. */
  205. Blockly.Workspace.prototype.undo = function(redo) {
  206. var inputStack = redo ? this.redoStack_ : this.undoStack_;
  207. var outputStack = redo ? this.undoStack_ : this.redoStack_;
  208. var inputEvent = inputStack.pop();
  209. if (!inputEvent) {
  210. return;
  211. }
  212. var events = [inputEvent];
  213. // Do another undo/redo if the next one is of the same group.
  214. while (inputStack.length && inputEvent.group &&
  215. inputEvent.group == inputStack[inputStack.length - 1].group) {
  216. events.push(inputStack.pop());
  217. }
  218. // Push these popped events on the opposite stack.
  219. for (var i = 0, event; event = events[i]; i++) {
  220. outputStack.push(event);
  221. }
  222. events = Blockly.Events.filter(events, redo);
  223. Blockly.Events.recordUndo = false;
  224. for (var i = 0, event; event = events[i]; i++) {
  225. event.run(redo);
  226. }
  227. Blockly.Events.recordUndo = true;
  228. };
  229. /**
  230. * Clear the undo/redo stacks.
  231. */
  232. Blockly.Workspace.prototype.clearUndo = function() {
  233. this.undoStack_.length = 0;
  234. this.redoStack_.length = 0;
  235. // Stop any events already in the firing queue from being undoable.
  236. Blockly.Events.clearPendingUndo();
  237. };
  238. /**
  239. * When something in this workspace changes, call a function.
  240. * @param {!Function} func Function to call.
  241. * @return {!Function} Function that can be passed to
  242. * removeChangeListener.
  243. */
  244. Blockly.Workspace.prototype.addChangeListener = function(func) {
  245. this.listeners_.push(func);
  246. return func;
  247. };
  248. /**
  249. * Stop listening for this workspace's changes.
  250. * @param {Function} func Function to stop calling.
  251. */
  252. Blockly.Workspace.prototype.removeChangeListener = function(func) {
  253. var i = this.listeners_.indexOf(func);
  254. if (i != -1) {
  255. this.listeners_.splice(i, 1);
  256. }
  257. };
  258. /**
  259. * Fire a change event.
  260. * @param {!Blockly.Events.Abstract} event Event to fire.
  261. */
  262. Blockly.Workspace.prototype.fireChangeListener = function(event) {
  263. if (event.recordUndo) {
  264. this.undoStack_.push(event);
  265. this.redoStack_.length = 0;
  266. if (this.undoStack_.length > this.MAX_UNDO) {
  267. this.undoStack_.unshift();
  268. }
  269. }
  270. for (var i = 0, func; func = this.listeners_[i]; i++) {
  271. func(event);
  272. }
  273. };
  274. /**
  275. * Find the block on this workspace with the specified ID.
  276. * @param {string} id ID of block to find.
  277. * @return {Blockly.Block} The sought after block or null if not found.
  278. */
  279. Blockly.Workspace.prototype.getBlockById = function(id) {
  280. return this.blockDB_[id] || null;
  281. };
  282. /**
  283. * Database of all workspaces.
  284. * @private
  285. */
  286. Blockly.Workspace.WorkspaceDB_ = Object.create(null);
  287. /**
  288. * Find the workspace with the specified ID.
  289. * @param {string} id ID of workspace to find.
  290. * @return {Blockly.Workspace} The sought after workspace or null if not found.
  291. */
  292. Blockly.Workspace.getById = function(id) {
  293. return Blockly.Workspace.WorkspaceDB_[id] || null;
  294. };
  295. // Export symbols that would otherwise be renamed by Closure compiler.
  296. Blockly.Workspace.prototype['clear'] = Blockly.Workspace.prototype.clear;
  297. Blockly.Workspace.prototype['clearUndo'] =
  298. Blockly.Workspace.prototype.clearUndo;
  299. Blockly.Workspace.prototype['addChangeListener'] =
  300. Blockly.Workspace.prototype.addChangeListener;
  301. Blockly.Workspace.prototype['removeChangeListener'] =
  302. Blockly.Workspace.prototype.removeChangeListener;