procedures.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  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 Utility functions for handling procedures.
  22. * @author fraser@google.com (Neil Fraser)
  23. */
  24. 'use strict';
  25. goog.provide('Blockly.Procedures');
  26. goog.require('Blockly.Blocks');
  27. goog.require('Blockly.Field');
  28. goog.require('Blockly.Names');
  29. goog.require('Blockly.Workspace');
  30. /**
  31. * Category to separate procedure names from variables and generated functions.
  32. */
  33. Blockly.Procedures.NAME_TYPE = 'PROCEDURE';
  34. /**
  35. * Find all user-created procedure definitions in a workspace.
  36. * @param {!Blockly.Workspace} root Root workspace.
  37. * @return {!Array.<!Array.<!Array>>} Pair of arrays, the
  38. * first contains procedures without return variables, the second with.
  39. * Each procedure is defined by a three-element list of name, parameter
  40. * list, and return value boolean.
  41. */
  42. Blockly.Procedures.allProcedures = function(root) {
  43. var blocks = root.getAllBlocks();
  44. var proceduresReturn = [];
  45. var proceduresNoReturn = [];
  46. for (var i = 0; i < blocks.length; i++) {
  47. if (blocks[i].getProcedureDef) {
  48. var tuple = blocks[i].getProcedureDef();
  49. if (tuple) {
  50. if (tuple[2]) {
  51. proceduresReturn.push(tuple);
  52. } else {
  53. proceduresNoReturn.push(tuple);
  54. }
  55. }
  56. }
  57. }
  58. proceduresNoReturn.sort(Blockly.Procedures.procTupleComparator_);
  59. proceduresReturn.sort(Blockly.Procedures.procTupleComparator_);
  60. return [proceduresNoReturn, proceduresReturn];
  61. };
  62. /**
  63. * Comparison function for case-insensitive sorting of the first element of
  64. * a tuple.
  65. * @param {!Array} ta First tuple.
  66. * @param {!Array} tb Second tuple.
  67. * @return {number} -1, 0, or 1 to signify greater than, equality, or less than.
  68. * @private
  69. */
  70. Blockly.Procedures.procTupleComparator_ = function(ta, tb) {
  71. return ta[0].toLowerCase().localeCompare(tb[0].toLowerCase());
  72. };
  73. /**
  74. * Ensure two identically-named procedures don't exist.
  75. * @param {string} name Proposed procedure name.
  76. * @param {!Blockly.Block} block Block to disambiguate.
  77. * @return {string} Non-colliding name.
  78. */
  79. Blockly.Procedures.findLegalName = function(name, block) {
  80. if (block.isInFlyout) {
  81. // Flyouts can have multiple procedures called 'do something'.
  82. return name;
  83. }
  84. while (!Blockly.Procedures.isLegalName_(name, block.workspace, block)) {
  85. // Collision with another procedure.
  86. var r = name.match(/^(.*?)(\d+)$/);
  87. if (!r) {
  88. name += '2';
  89. } else {
  90. name = r[1] + (parseInt(r[2], 10) + 1);
  91. }
  92. }
  93. return name;
  94. };
  95. /**
  96. * Does this procedure have a legal name? Illegal names include names of
  97. * procedures already defined.
  98. * @param {string} name The questionable name.
  99. * @param {!Blockly.Workspace} workspace The workspace to scan for collisions.
  100. * @param {Blockly.Block=} opt_exclude Optional block to exclude from
  101. * comparisons (one doesn't want to collide with oneself).
  102. * @return {boolean} True if the name is legal.
  103. * @private
  104. */
  105. Blockly.Procedures.isLegalName_ = function(name, workspace, opt_exclude) {
  106. var blocks = workspace.getAllBlocks();
  107. // Iterate through every block and check the name.
  108. for (var i = 0; i < blocks.length; i++) {
  109. if (blocks[i] == opt_exclude) {
  110. continue;
  111. }
  112. if (blocks[i].getProcedureDef) {
  113. var procName = blocks[i].getProcedureDef();
  114. if (Blockly.Names.equals(procName[0], name)) {
  115. return false;
  116. }
  117. }
  118. }
  119. return true;
  120. };
  121. /**
  122. * Rename a procedure. Called by the editable field.
  123. * @param {string} name The proposed new name.
  124. * @return {string} The accepted name.
  125. * @this {!Blockly.Field}
  126. */
  127. Blockly.Procedures.rename = function(name) {
  128. // Strip leading and trailing whitespace. Beyond this, all names are legal.
  129. name = name.replace(/^[\s\xa0]+|[\s\xa0]+$/g, '');
  130. // Ensure two identically-named procedures don't exist.
  131. var legalName = Blockly.Procedures.findLegalName(name, this.sourceBlock_);
  132. var oldName = this.text_;
  133. if (oldName != name && oldName != legalName) {
  134. // Rename any callers.
  135. var blocks = this.sourceBlock_.workspace.getAllBlocks();
  136. for (var i = 0; i < blocks.length; i++) {
  137. if (blocks[i].renameProcedure) {
  138. blocks[i].renameProcedure(oldName, legalName);
  139. }
  140. }
  141. }
  142. return legalName;
  143. };
  144. /**
  145. * Construct the blocks required by the flyout for the procedure category.
  146. * @param {!Blockly.Workspace} workspace The workspace contianing procedures.
  147. * @return {!Array.<!Element>} Array of XML block elements.
  148. */
  149. Blockly.Procedures.flyoutCategory = function(workspace) {
  150. var xmlList = [];
  151. if (Blockly.Blocks['procedures_defnoreturn']) {
  152. // <block type="procedures_defnoreturn" gap="16"></block>
  153. var block = goog.dom.createDom('block');
  154. block.setAttribute('type', 'procedures_defnoreturn');
  155. block.setAttribute('gap', 16);
  156. xmlList.push(block);
  157. }
  158. /*if (Blockly.Blocks['procedures_defreturn']) {
  159. // <block type="procedures_defreturn" gap="16"></block>
  160. var block = goog.dom.createDom('block');
  161. block.setAttribute('type', 'procedures_defreturn');
  162. block.setAttribute('gap', 16);
  163. xmlList.push(block);
  164. }*/
  165. if (Blockly.Blocks['procedures_return']) {
  166. var block = goog.dom.createDom('block');
  167. block.setAttribute('type', 'procedures_return');
  168. block.setAttribute('gap', 16);
  169. xmlList.push(block);
  170. }
  171. /*if (Blockly.Blocks['procedures_ifreturn']) {
  172. // <block type="procedures_ifreturn" gap="16"></block>
  173. var block = goog.dom.createDom('block');
  174. block.setAttribute('type', 'procedures_ifreturn');
  175. block.setAttribute('gap', 16);
  176. xmlList.push(block);
  177. }*/
  178. if (Blockly.Blocks['procedures_main']) {
  179. var block = goog.dom.createDom('block');
  180. block.setAttribute('type', 'procedures_main');
  181. block.setAttribute('gap', 16);
  182. xmlList.push(block);
  183. }
  184. if (xmlList.length) {
  185. // Add slightly larger gap between system blocks and user calls.
  186. xmlList[xmlList.length - 1].setAttribute('gap', 24);
  187. }
  188. function populateProcedures(procedureList, templateName) {
  189. for (var i = 0; i < procedureList.length; i++) {
  190. var name = procedureList[i][0];
  191. var args = procedureList[i][1];
  192. // <block type="procedures_callnoreturn" gap="16">
  193. // <mutation name="do something">
  194. // <arg name="x"></arg>
  195. // </mutation>
  196. // </block>
  197. var block = goog.dom.createDom('block');
  198. block.setAttribute('type', templateName);
  199. block.setAttribute('gap', 16);
  200. var mutation = goog.dom.createDom('mutation');
  201. mutation.setAttribute('name', name);
  202. block.appendChild(mutation);
  203. for (var j = 0; j < args.length; j++) {
  204. var arg = goog.dom.createDom('arg');
  205. arg.setAttribute('name', args[j]);
  206. mutation.appendChild(arg);
  207. }
  208. xmlList.push(block);
  209. }
  210. }
  211. var tuple = Blockly.Procedures.allProcedures(workspace);
  212. populateProcedures(tuple[0], 'procedures_callnoreturn');
  213. populateProcedures(tuple[1], 'procedures_callreturn');
  214. return xmlList;
  215. };
  216. /**
  217. * Find all the callers of a named procedure.
  218. * @param {string} name Name of procedure.
  219. * @param {!Blockly.Workspace} workspace The workspace to find callers in.
  220. * @return {!Array.<!Blockly.Block>} Array of caller blocks.
  221. */
  222. Blockly.Procedures.getCallers = function(name, workspace) {
  223. var callers = [];
  224. var blocks = workspace.getAllBlocks();
  225. // Iterate through every block and check the name.
  226. for (var i = 0; i < blocks.length; i++) {
  227. if (blocks[i].getProcedureCall) {
  228. var procName = blocks[i].getProcedureCall();
  229. // Procedure name may be null if the block is only half-built.
  230. if (procName && Blockly.Names.equals(procName, name)) {
  231. callers.push(blocks[i]);
  232. }
  233. }
  234. }
  235. return callers;
  236. };
  237. /**
  238. * When a procedure definition changes its parameters, find and edit all its
  239. * callers.
  240. * @param {!Blockly.Block} defBlock Procedure definition block.
  241. */
  242. Blockly.Procedures.mutateCallers = function(defBlock) {
  243. var oldRecordUndo = Blockly.Events.recordUndo;
  244. var name = defBlock.getProcedureDef()[0];
  245. var xmlElement = defBlock.mutationToDom(true);
  246. var callers = Blockly.Procedures.getCallers(name, defBlock.workspace);
  247. for (var i = 0, caller; caller = callers[i]; i++) {
  248. var oldMutationDom = caller.mutationToDom();
  249. var oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom);
  250. caller.domToMutation(xmlElement);
  251. var newMutationDom = caller.mutationToDom();
  252. var newMutation = newMutationDom && Blockly.Xml.domToText(newMutationDom);
  253. if (oldMutation != newMutation) {
  254. // Fire a mutation on every caller block. But don't record this as an
  255. // undo action since it is deterministically tied to the procedure's
  256. // definition mutation.
  257. Blockly.Events.recordUndo = false;
  258. Blockly.Events.fire(new Blockly.Events.Change(
  259. caller, 'mutation', null, oldMutation, newMutation));
  260. Blockly.Events.recordUndo = oldRecordUndo;
  261. }
  262. }
  263. };
  264. /**
  265. * Find the definition block for the named procedure.
  266. * @param {string} name Name of procedure.
  267. * @param {!Blockly.Workspace} workspace The workspace to search.
  268. * @return {Blockly.Block} The procedure definition block, or null not found.
  269. */
  270. Blockly.Procedures.getDefinition = function(name, workspace) {
  271. // Assume that a procedure definition is a top block.
  272. var blocks = workspace.getTopBlocks(false);
  273. for (var i = 0; i < blocks.length; i++) {
  274. if (blocks[i].getProcedureDef) {
  275. var tuple = blocks[i].getProcedureDef();
  276. if (tuple && Blockly.Names.equals(tuple[0], name)) {
  277. return blocks[i];
  278. }
  279. }
  280. }
  281. return null;
  282. };