procedures.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  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_ifreturn']) {
  166. // <block type="procedures_ifreturn" gap="16"></block>
  167. var block = goog.dom.createDom('block');
  168. block.setAttribute('type', 'procedures_ifreturn');
  169. block.setAttribute('gap', 16);
  170. xmlList.push(block);
  171. }
  172. if (xmlList.length) {
  173. // Add slightly larger gap between system blocks and user calls.
  174. xmlList[xmlList.length - 1].setAttribute('gap', 24);
  175. }
  176. function populateProcedures(procedureList, templateName) {
  177. for (var i = 0; i < procedureList.length; i++) {
  178. var name = procedureList[i][0];
  179. var args = procedureList[i][1];
  180. // <block type="procedures_callnoreturn" gap="16">
  181. // <mutation name="do something">
  182. // <arg name="x"></arg>
  183. // </mutation>
  184. // </block>
  185. var block = goog.dom.createDom('block');
  186. block.setAttribute('type', templateName);
  187. block.setAttribute('gap', 16);
  188. var mutation = goog.dom.createDom('mutation');
  189. mutation.setAttribute('name', name);
  190. block.appendChild(mutation);
  191. for (var j = 0; j < args.length; j++) {
  192. var arg = goog.dom.createDom('arg');
  193. arg.setAttribute('name', args[j]);
  194. mutation.appendChild(arg);
  195. }
  196. xmlList.push(block);
  197. }
  198. }
  199. var tuple = Blockly.Procedures.allProcedures(workspace);
  200. populateProcedures(tuple[0], 'procedures_callnoreturn');
  201. populateProcedures(tuple[1], 'procedures_callreturn');
  202. return xmlList;
  203. };
  204. /**
  205. * Find all the callers of a named procedure.
  206. * @param {string} name Name of procedure.
  207. * @param {!Blockly.Workspace} workspace The workspace to find callers in.
  208. * @return {!Array.<!Blockly.Block>} Array of caller blocks.
  209. */
  210. Blockly.Procedures.getCallers = function(name, workspace) {
  211. var callers = [];
  212. var blocks = workspace.getAllBlocks();
  213. // Iterate through every block and check the name.
  214. for (var i = 0; i < blocks.length; i++) {
  215. if (blocks[i].getProcedureCall) {
  216. var procName = blocks[i].getProcedureCall();
  217. // Procedure name may be null if the block is only half-built.
  218. if (procName && Blockly.Names.equals(procName, name)) {
  219. callers.push(blocks[i]);
  220. }
  221. }
  222. }
  223. return callers;
  224. };
  225. /**
  226. * When a procedure definition changes its parameters, find and edit all its
  227. * callers.
  228. * @param {!Blockly.Block} defBlock Procedure definition block.
  229. */
  230. Blockly.Procedures.mutateCallers = function(defBlock) {
  231. var oldRecordUndo = Blockly.Events.recordUndo;
  232. var name = defBlock.getProcedureDef()[0];
  233. var xmlElement = defBlock.mutationToDom(true);
  234. var callers = Blockly.Procedures.getCallers(name, defBlock.workspace);
  235. for (var i = 0, caller; caller = callers[i]; i++) {
  236. var oldMutationDom = caller.mutationToDom();
  237. var oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom);
  238. caller.domToMutation(xmlElement);
  239. var newMutationDom = caller.mutationToDom();
  240. var newMutation = newMutationDom && Blockly.Xml.domToText(newMutationDom);
  241. if (oldMutation != newMutation) {
  242. // Fire a mutation on every caller block. But don't record this as an
  243. // undo action since it is deterministically tied to the procedure's
  244. // definition mutation.
  245. Blockly.Events.recordUndo = false;
  246. Blockly.Events.fire(new Blockly.Events.Change(
  247. caller, 'mutation', null, oldMutation, newMutation));
  248. Blockly.Events.recordUndo = oldRecordUndo;
  249. }
  250. }
  251. };
  252. /**
  253. * Find the definition block for the named procedure.
  254. * @param {string} name Name of procedure.
  255. * @param {!Blockly.Workspace} workspace The workspace to search.
  256. * @return {Blockly.Block} The procedure definition block, or null not found.
  257. */
  258. Blockly.Procedures.getDefinition = function(name, workspace) {
  259. // Assume that a procedure definition is a top block.
  260. var blocks = workspace.getTopBlocks(false);
  261. for (var i = 0; i < blocks.length; i++) {
  262. if (blocks[i].getProcedureDef) {
  263. var tuple = blocks[i].getProcedureDef();
  264. if (tuple && Blockly.Names.equals(tuple[0], name)) {
  265. return blocks[i];
  266. }
  267. }
  268. }
  269. return null;
  270. };