workspace.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  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.array');
  27. goog.require('goog.math');
  28. /**
  29. * Class for a workspace. This is a data structure that contains blocks.
  30. * There is no UI, and can be created headlessly.
  31. * @param {Blockly.Options} opt_options Dictionary of options.
  32. * @constructor
  33. */
  34. Blockly.Workspace = function(opt_options) {
  35. /** @type {string} */
  36. this.id = Blockly.genUid();
  37. Blockly.Workspace.WorkspaceDB_[this.id] = this;
  38. /** @type {!Blockly.Options} */
  39. this.options = opt_options || {};
  40. /** @type {boolean} */
  41. this.RTL = !!this.options.RTL;
  42. /** @type {boolean} */
  43. this.horizontalLayout = !!this.options.horizontalLayout;
  44. /** @type {number} */
  45. this.toolboxPosition = this.options.toolboxPosition;
  46. /**
  47. * @type {!Array.<!Blockly.Block>}
  48. * @private
  49. */
  50. this.topBlocks_ = [];
  51. /**
  52. * @type {!Array.<!Function>}
  53. * @private
  54. */
  55. this.listeners_ = [];
  56. /**
  57. * @type {!Array.<!Blockly.Events.Abstract>}
  58. * @private
  59. */
  60. this.undoStack_ = [];
  61. /**
  62. * @type {!Array.<!Blockly.Events.Abstract>}
  63. * @private
  64. */
  65. this.redoStack_ = [];
  66. /**
  67. * @type {!Object}
  68. * @private
  69. */
  70. this.blockDB_ = Object.create(null);
  71. /*
  72. * @type {!Array.<string>}
  73. * A list of all of the named variables in the workspace, including variables
  74. * that are not currently in use.
  75. */
  76. this.variableList = [];
  77. };
  78. /**
  79. * Returns `true` if the workspace is visible and `false` if it's headless.
  80. * @type {boolean}
  81. */
  82. Blockly.Workspace.prototype.rendered = false;
  83. /**
  84. * Maximum number of undo events in stack. `0` turns off undo, `Infinity` sets it to unlimited.
  85. * @type {number}
  86. */
  87. Blockly.Workspace.prototype.MAX_UNDO = 1024;
  88. /**
  89. * Dispose of this workspace.
  90. * Unlink from all DOM elements to prevent memory leaks.
  91. */
  92. Blockly.Workspace.prototype.dispose = function() {
  93. this.listeners_.length = 0;
  94. this.clear();
  95. // Remove from workspace database.
  96. delete Blockly.Workspace.WorkspaceDB_[this.id];
  97. };
  98. /**
  99. * Angle away from the horizontal to sweep for blocks. Order of execution is
  100. * generally top to bottom, but a small angle changes the scan to give a bit of
  101. * a left to right bias (reversed in RTL). Units are in degrees.
  102. * See: http://tvtropes.org/pmwiki/pmwiki.php/Main/DiagonalBilling.
  103. */
  104. Blockly.Workspace.SCAN_ANGLE = 3;
  105. /**
  106. * Add a block to the list of top blocks.
  107. * @param {!Blockly.Block} block Block to remove.
  108. */
  109. Blockly.Workspace.prototype.addTopBlock = function(block) {
  110. this.topBlocks_.push(block);
  111. if (this.isFlyout) {
  112. // This is for the (unlikely) case where you have a variable in a block in
  113. // an always-open flyout. It needs to be possible to edit the block in the
  114. // flyout, so the contents of the dropdown need to be correct.
  115. var variables = Blockly.Variables.allUsedVariables(block);
  116. for (var i = 0; i < variables.length; i++) {
  117. if (this.variableList.indexOf(variables[i]) == -1) {
  118. this.variableList.push(variables[i]);
  119. }
  120. }
  121. }
  122. };
  123. /**
  124. * Remove a block from the list of top blocks.
  125. * @param {!Blockly.Block} block Block to remove.
  126. */
  127. Blockly.Workspace.prototype.removeTopBlock = function(block) {
  128. if (!goog.array.remove(this.topBlocks_, block)) {
  129. throw 'Block not present in workspace\'s list of top-most blocks.';
  130. }
  131. };
  132. /**
  133. * Finds the top-level blocks and returns them. Blocks are optionally sorted
  134. * by position; top to bottom (with slight LTR or RTL bias).
  135. * @param {boolean} ordered Sort the list if true.
  136. * @return {!Array.<!Blockly.Block>} The top-level block objects.
  137. */
  138. Blockly.Workspace.prototype.getTopBlocks = function(ordered) {
  139. // Copy the topBlocks_ list.
  140. var blocks = [].concat(this.topBlocks_);
  141. if (ordered && blocks.length > 1) {
  142. var offset = Math.sin(goog.math.toRadians(Blockly.Workspace.SCAN_ANGLE));
  143. if (this.RTL) {
  144. offset *= -1;
  145. }
  146. blocks.sort(function(a, b) {
  147. var aXY = a.getRelativeToSurfaceXY();
  148. var bXY = b.getRelativeToSurfaceXY();
  149. return (aXY.y + offset * aXY.x) - (bXY.y + offset * bXY.x);
  150. });
  151. }
  152. return blocks;
  153. };
  154. /**
  155. * Find all blocks in workspace. No particular order.
  156. * @return {!Array.<!Blockly.Block>} Array of blocks.
  157. */
  158. Blockly.Workspace.prototype.getAllBlocks = function() {
  159. var blocks = this.getTopBlocks(false);
  160. for (var i = 0; i < blocks.length; i++) {
  161. blocks.push.apply(blocks, blocks[i].getChildren());
  162. }
  163. return blocks;
  164. };
  165. /**
  166. * Dispose of all blocks in workspace.
  167. */
  168. Blockly.Workspace.prototype.clear = function() {
  169. var existingGroup = Blockly.Events.getGroup();
  170. if (!existingGroup) {
  171. Blockly.Events.setGroup(true);
  172. }
  173. while (this.topBlocks_.length) {
  174. this.topBlocks_[0].dispose();
  175. }
  176. if (!existingGroup) {
  177. Blockly.Events.setGroup(false);
  178. }
  179. this.variableList.length = 0;
  180. };
  181. /**
  182. * Walk the workspace and update the list of variables to only contain ones in
  183. * use on the workspace. Use when loading new workspaces from disk.
  184. * @param {boolean} clearList True if the old variable list should be cleared.
  185. */
  186. Blockly.Workspace.prototype.updateVariableList = function(clearList) {
  187. // TODO: Sort
  188. if (!this.isFlyout) {
  189. // Update the list in place so that the flyout's references stay correct.
  190. if (clearList) {
  191. this.variableList.length = 0;
  192. }
  193. var allVariables = Blockly.Variables.allUsedVariables(this);
  194. for (var i = 0; i < allVariables.length; i++) {
  195. this.createVariable(allVariables[i]);
  196. }
  197. }
  198. };
  199. /**
  200. * Rename a variable by updating its name in the variable list.
  201. * TODO: #468
  202. * @param {string} oldName Variable to rename.
  203. * @param {string} newName New variable name.
  204. */
  205. Blockly.Workspace.prototype.renameVariable = function(oldName, newName) {
  206. // Find the old name in the list.
  207. var variableIndex = this.variableIndexOf(oldName);
  208. var newVariableIndex = this.variableIndexOf(newName);
  209. // We might be renaming to an existing name but with different case. If so,
  210. // we will also update all of the blocks using the new name to have the
  211. // correct case.
  212. if (newVariableIndex != -1 &&
  213. this.variableList[newVariableIndex] != newName) {
  214. var oldCase = this.variableList[newVariableIndex];
  215. }
  216. Blockly.Events.setGroup(true);
  217. var blocks = this.getAllBlocks();
  218. // Iterate through every block.
  219. for (var i = 0; i < blocks.length; i++) {
  220. blocks[i].renameVar(oldName, newName);
  221. if (oldCase) {
  222. blocks[i].renameVar(oldCase, newName);
  223. }
  224. }
  225. Blockly.Events.setGroup(false);
  226. if (variableIndex == newVariableIndex ||
  227. variableIndex != -1 && newVariableIndex == -1) {
  228. // Only changing case, or renaming to a completely novel name.
  229. this.variableList[variableIndex] = newName;
  230. } else if (variableIndex != -1 && newVariableIndex != -1) {
  231. // Renaming one existing variable to another existing variable.
  232. // The case might have changed, so we update the destination ID.
  233. this.variableList[newVariableIndex] = newName;
  234. this.variableList.splice(variableIndex, 1);
  235. } else {
  236. this.variableList.push(newName);
  237. console.log('Tried to rename an non-existent variable.');
  238. }
  239. };
  240. /**
  241. * Create a variable with the given name.
  242. * TODO: #468
  243. * @param {string} name The new variable's name.
  244. */
  245. Blockly.Workspace.prototype.createVariable = function(name) {
  246. var index = this.variableIndexOf(name);
  247. if (index == -1) {
  248. this.variableList.push(name);
  249. }
  250. };
  251. /**
  252. * Find all the uses of a named variable.
  253. * @param {string} name Name of variable.
  254. * @return {!Array.<!Blockly.Block>} Array of block usages.
  255. */
  256. Blockly.Workspace.prototype.getVariableUses = function(name) {
  257. var uses = [];
  258. var blocks = this.getAllBlocks();
  259. // Iterate through every block and check the name.
  260. for (var i = 0; i < blocks.length; i++) {
  261. var blockVariables = blocks[i].getVars();
  262. if (blockVariables) {
  263. for (var j = 0; j < blockVariables.length; j++) {
  264. var varName = blockVariables[j];
  265. // Variable name may be null if the block is only half-built.
  266. if (varName && Blockly.Names.equals(varName, name)) {
  267. uses.push(blocks[i]);
  268. }
  269. }
  270. }
  271. }
  272. return uses;
  273. };
  274. /**
  275. * Delete a variables and all of its uses from this workspace.
  276. * @param {string} name Name of variable to delete.
  277. */
  278. Blockly.Workspace.prototype.deleteVariable = function(name) {
  279. var workspace = this;
  280. var variableIndex = this.variableIndexOf(name);
  281. if (variableIndex != -1) {
  282. // Check whether this variable is a function parameter before deleting.
  283. var uses = this.getVariableUses(name);
  284. for (var i = 0, block; block = uses[i]; i++) {
  285. if (block.type == 'procedures_defnoreturn' ||
  286. block.type == 'procedures_defreturn') {
  287. var procedureName = block.getFieldValue('NAME');
  288. Blockly.alert(
  289. Blockly.Msg.CANNOT_DELETE_VARIABLE_PROCEDURE.
  290. replace('%1', name).
  291. replace('%2', procedureName));
  292. return;
  293. }
  294. }
  295. function doDeletion() {
  296. Blockly.Events.setGroup(true);
  297. for (var i = 0; i < uses.length; i++) {
  298. uses[i].dispose(true, false);
  299. }
  300. Blockly.Events.setGroup(false);
  301. workspace.variableList.splice(variableIndex, 1);
  302. }
  303. if (uses.length > 1) {
  304. // Confirm before deleting multiple blocks.
  305. Blockly.confirm(
  306. Blockly.Msg.DELETE_VARIABLE_CONFIRMATION.replace('%1', uses.length).
  307. replace('%2', name),
  308. function(ok) {
  309. if (ok) {
  310. doDeletion();
  311. }
  312. });
  313. } else {
  314. // No confirmation necessary for a single block.
  315. doDeletion();
  316. }
  317. }
  318. };
  319. /**
  320. * Check whether a variable exists with the given name. The check is
  321. * case-insensitive.
  322. * @param {string} name The name to check for.
  323. * @return {number} The index of the name in the variable list, or -1 if it is
  324. * not present.
  325. */
  326. Blockly.Workspace.prototype.variableIndexOf = function(name) {
  327. for (var i = 0, varname; varname = this.variableList[i]; i++) {
  328. if (Blockly.Names.equals(varname, name)) {
  329. return i;
  330. }
  331. }
  332. return -1;
  333. };
  334. /**
  335. * Returns the horizontal offset of the workspace.
  336. * Intended for LTR/RTL compatibility in XML.
  337. * Not relevant for a headless workspace.
  338. * @return {number} Width.
  339. */
  340. Blockly.Workspace.prototype.getWidth = function() {
  341. return 0;
  342. };
  343. /**
  344. * Obtain a newly created block.
  345. * @param {?string} prototypeName Name of the language object containing
  346. * type-specific functions for this block.
  347. * @param {string=} opt_id Optional ID. Use this ID if provided, otherwise
  348. * create a new id.
  349. * @return {!Blockly.Block} The created block.
  350. */
  351. Blockly.Workspace.prototype.newBlock = function(prototypeName, opt_id) {
  352. return new Blockly.Block(this, prototypeName, opt_id);
  353. };
  354. /**
  355. * The number of blocks that may be added to the workspace before reaching
  356. * the maxBlocks.
  357. * @return {number} Number of blocks left.
  358. */
  359. Blockly.Workspace.prototype.remainingCapacity = function() {
  360. if (isNaN(this.options.maxBlocks)) {
  361. return Infinity;
  362. }
  363. return this.options.maxBlocks - this.getAllBlocks().length;
  364. };
  365. /**
  366. * Undo or redo the previous action.
  367. * @param {boolean} redo False if undo, true if redo.
  368. */
  369. Blockly.Workspace.prototype.undo = function(redo) {
  370. var inputStack = redo ? this.redoStack_ : this.undoStack_;
  371. var outputStack = redo ? this.undoStack_ : this.redoStack_;
  372. var inputEvent = inputStack.pop();
  373. if (!inputEvent) {
  374. return;
  375. }
  376. var events = [inputEvent];
  377. // Do another undo/redo if the next one is of the same group.
  378. while (inputStack.length && inputEvent.group &&
  379. inputEvent.group == inputStack[inputStack.length - 1].group) {
  380. events.push(inputStack.pop());
  381. }
  382. // Push these popped events on the opposite stack.
  383. for (var i = 0, event; event = events[i]; i++) {
  384. outputStack.push(event);
  385. }
  386. events = Blockly.Events.filter(events, redo);
  387. Blockly.Events.recordUndo = false;
  388. for (var i = 0, event; event = events[i]; i++) {
  389. event.run(redo);
  390. }
  391. Blockly.Events.recordUndo = true;
  392. };
  393. /**
  394. * Clear the undo/redo stacks.
  395. */
  396. Blockly.Workspace.prototype.clearUndo = function() {
  397. this.undoStack_.length = 0;
  398. this.redoStack_.length = 0;
  399. // Stop any events already in the firing queue from being undoable.
  400. Blockly.Events.clearPendingUndo();
  401. };
  402. /**
  403. * When something in this workspace changes, call a function.
  404. * @param {!Function} func Function to call.
  405. * @return {!Function} Function that can be passed to
  406. * removeChangeListener.
  407. */
  408. Blockly.Workspace.prototype.addChangeListener = function(func) {
  409. this.listeners_.push(func);
  410. return func;
  411. };
  412. /**
  413. * Stop listening for this workspace's changes.
  414. * @param {Function} func Function to stop calling.
  415. */
  416. Blockly.Workspace.prototype.removeChangeListener = function(func) {
  417. goog.array.remove(this.listeners_, func);
  418. };
  419. /**
  420. * Fire a change event.
  421. * @param {!Blockly.Events.Abstract} event Event to fire.
  422. */
  423. Blockly.Workspace.prototype.fireChangeListener = function(event) {
  424. if (event.recordUndo) {
  425. this.undoStack_.push(event);
  426. this.redoStack_.length = 0;
  427. if (this.undoStack_.length > this.MAX_UNDO) {
  428. this.undoStack_.unshift();
  429. }
  430. }
  431. for (var i = 0, func; func = this.listeners_[i]; i++) {
  432. func(event);
  433. }
  434. };
  435. /**
  436. * Find the block on this workspace with the specified ID.
  437. * @param {string} id ID of block to find.
  438. * @return {Blockly.Block} The sought after block or null if not found.
  439. */
  440. Blockly.Workspace.prototype.getBlockById = function(id) {
  441. return this.blockDB_[id] || null;
  442. };
  443. /**
  444. * Database of all workspaces.
  445. * @private
  446. */
  447. Blockly.Workspace.WorkspaceDB_ = Object.create(null);
  448. /**
  449. * Find the workspace with the specified ID.
  450. * @param {string} id ID of workspace to find.
  451. * @return {Blockly.Workspace} The sought after workspace or null if not found.
  452. */
  453. Blockly.Workspace.getById = function(id) {
  454. return Blockly.Workspace.WorkspaceDB_[id] || null;
  455. };
  456. // Export symbols that would otherwise be renamed by Closure compiler.
  457. Blockly.Workspace.prototype['clear'] = Blockly.Workspace.prototype.clear;
  458. Blockly.Workspace.prototype['clearUndo'] =
  459. Blockly.Workspace.prototype.clearUndo;
  460. Blockly.Workspace.prototype['addChangeListener'] =
  461. Blockly.Workspace.prototype.addChangeListener;
  462. Blockly.Workspace.prototype['removeChangeListener'] =
  463. Blockly.Workspace.prototype.removeChangeListener;