/** * @license * Visual Blocks Editor * * Copyright 2012 Google Inc. * https://developers.google.com/blockly/ * * 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 Utility functions for handling procedures. * @author fraser@google.com (Neil Fraser) */ using System; using System.Collections.Generic; using System.Runtime.InteropServices; using Bridge; using Bridge.Html5; using Bridge.Text.RegularExpressions; using WebMrbc; namespace WebMrbc { public class Procedures { /// /// Category to separate procedure names from variables and generated functions. /// public string NAME_TYPE = "PROCEDURE"; /** * Common HSV hue for all blocks in this category. */ public int HUE = 290; /// /// Find all user-created procedure definitions in a workspace. /// /// Root workspace. /// Pair of arrays, the first contains procedures without return variables, the /// second with. Each procedure is defined by a three-element list of name, parameter /// list, and return value boolean. public Tuple[][] allProcedures(Workspace root) { var blocks = root.getAllBlocks(); var proceduresReturn = new Tuple[0]; var proceduresNoReturn = new Tuple[0]; for (var i = 0; i < blocks.Length; i++) { ProceduresDefBlock block = (dynamic)blocks[i]; if ((block.type == ProceduresDefnoreturnBlock.type_name) || (block.type == ProceduresDefreturnBlock.type_name)) { var tuple = block.getProcedureDef(); if (tuple != null) { if (tuple.Item3) { proceduresReturn.Push(tuple); } else { proceduresNoReturn.Push(tuple); } } } } proceduresNoReturn.Sort(procTupleComparator_); proceduresReturn.Sort(procTupleComparator_); return new Tuple[][] { proceduresNoReturn, proceduresReturn }; } /// /// Comparison function for case-insensitive sorting of the first element of /// a tuple. /// /// First tuple. /// Second tuple. /// -1, 0, or 1 to signify greater than, equality, or less than. public int procTupleComparator_(Tuple ta, Tuple tb) { return ta.Item1.ToLower().LocaleCompare(tb.Item1.ToLower()); } /// /// Ensure two identically-named procedures don't exist. /// /// Proposed procedure name. /// Block to disambiguate. /// Non-colliding name. public static string findLegalName(string name, Block block) { if (block.isInFlyout) { // Flyouts can have multiple procedures called 'do something'. return name; } while (!isLegalName_(name, block.workspace, block)) { // Collision with another procedure. var r = new Regex(@"^(.*?)(\d+)$").Exec(name); if (r == null) { name += "2"; } else { name = r[1] + (Bridge.Script.ParseInt(r[2], 10) + 1); } } return name; } /// /// Does this procedure have a legal name? Illegal names include names of /// procedures already defined. /// /// The questionable name. /// The workspace to scan for collisions. /// Optional block to exclude from /// comparisons (one doesn't want to collide with oneself). /// True if the name is legal. public static bool isLegalName_(string name, Workspace workspace, Block opt_exclude = null) { var blocks = workspace.getAllBlocks(); // Iterate through every block and check the name. for (var i = 0; i < blocks.Length; i++) { if (blocks[i] == opt_exclude) { continue; } ProceduresDefBlock block = (dynamic)blocks[i]; if ((block.type == ProceduresDefnoreturnBlock.type_name) || (block.type == ProceduresDefreturnBlock.type_name)) { var procName = block.getProcedureDef(); if (Blockly.Names.equals(procName.Item1, name)) { return false; } } } return true; } /// /// Rename a procedure. Called by the editable field. /// /// /// The proposed new name. /// The accepted name. public string rename(Field field, string name) { // Strip leading and trailing whitespace. Beyond this, all names are legal. name = name.Replace(new Regex(@"^[\s\xa0] +|[\s\xa0] +$"), ""); // Ensure two identically-named procedures don't exist. var legalName = findLegalName(name, field.sourceBlock_); var oldName = field.text_; if (oldName != name && oldName != legalName) { // Rename any callers. var blocks = field.sourceBlock_.workspace.getAllBlocks(); for (var i = 0; i < blocks.Length; i++) { ProceduresCallBlock block = (dynamic)blocks[i]; if ((block.type == ProceduresCallnoreturnBlock.type_name) || (block.type == ProceduresCallreturnBlock.type_name)) { block.renameProcedure(oldName, legalName); } } } return legalName; } /// /// Construct the blocks required by the flyout for the procedure category. /// /// The workspace contianing procedures. /// Array of XML block elements. public Element[] flyoutCategory(Workspace workspace) { var xmlList = new Element[0]; if (Script.Get(Blockly.Blocks, ProceduresDefnoreturnBlock.type_name) != null) { // var block = goog.dom.createDom("block"); block.SetAttribute("type", ProceduresDefnoreturnBlock.type_name); block.SetAttribute("gap", "16"); xmlList.Push(block); } if (Script.Get(Blockly.Blocks, ProceduresDefreturnBlock.type_name) != null) { // var block = goog.dom.createDom("block"); block.SetAttribute("type", ProceduresDefreturnBlock.type_name); block.SetAttribute("gap", "16"); xmlList.Push(block); } if (Script.Get(Blockly.Blocks, ProceduresIfreturnBlock.type_name) != null) { // var block = goog.dom.createDom("block"); block.SetAttribute("type", ProceduresIfreturnBlock.type_name); block.SetAttribute("gap", "16"); xmlList.Push(block); } if (xmlList.Length != 0) { // Add slightly larger gap between system blocks and user calls. xmlList[xmlList.Length - 1].SetAttribute("gap", "24"); } var populateProcedures = new Action[], string>((procedureList, templateName) => { for (var i = 0; i < procedureList.Length; i++) { var name = procedureList[i].Item1; var args = procedureList[i].Item2; // // // // // var block = goog.dom.createDom("block"); block.SetAttribute("type", templateName); block.SetAttribute("gap", "16"); var mutation = goog.dom.createDom("mutation"); mutation.SetAttribute("name", name); block.AppendChild(mutation); for (var j = 0; j < args.Length; j++) { var arg = goog.dom.createDom("arg"); arg.SetAttribute("name", args[j]); mutation.AppendChild(arg); } xmlList.Push(block); } }); var tuple = allProcedures(workspace); populateProcedures(tuple[0], ProceduresCallnoreturnBlock.type_name); populateProcedures(tuple[1], ProceduresCallreturnBlock.type_name); return xmlList; } /// /// Find all the callers of a named procedure. /// /// Name of procedure. /// The workspace to find callers in. /// Array of caller blocks. public ProceduresCallBlock[] getCallers(string name, Workspace workspace) { var callers = new ProceduresCallBlock[0]; var blocks = workspace.getAllBlocks(); // Iterate through every block and check the name. for (var i = 0; i < blocks.Length; i++) { ProceduresCallBlock block = (dynamic)blocks[i]; if ((block.type == ProceduresCallnoreturnBlock.type_name) || (block.type == ProceduresCallreturnBlock.type_name)) { var procName = block.getProcedureCall(); // Procedure name may be null if the block is only half-built. if (procName != null && Blockly.Names.equals(procName, name)) { callers.Push(block); } } } return callers; } /// /// When a procedure definition changes its parameters, find and edit all its /// callers. /// /// Procedure definition block. public void mutateCallers(ProceduresDefBlock defBlock) { var oldRecordUndo = Blockly.Events.recordUndo; var name = defBlock.getProcedureDef().Item1; var xmlElement = defBlock.mutationToDom(true); var callers = getCallers(name, defBlock.workspace); foreach (var caller in callers) { var oldMutationDom = caller.mutationToDom(); var oldMutation = oldMutationDom != null ? Blockly.Xml.domToText(oldMutationDom) : null; caller.domToMutation(xmlElement); var newMutationDom = caller.mutationToDom(); var newMutation = newMutationDom != null ? Blockly.Xml.domToText(newMutationDom) : null; if (oldMutation != newMutation) { // Fire a mutation on every caller block. But don't record this as an // undo action since it is deterministically tied to the procedure's // definition mutation. Blockly.Events.recordUndo = false; Blockly.Events.fire(new Change( caller, "mutation", null, oldMutation, newMutation)); Blockly.Events.recordUndo = oldRecordUndo; } } } /// /// Find the definition block for the named procedure. /// /// Name of procedure. /// The workspace to search. /// The procedure definition block, or null not found. public Block getDefinition(string name, Workspace workspace) { // Assume that a procedure definition is a top block. var blocks = workspace.getTopBlocks(false); for (var i = 0; i < blocks.Length; i++) { ProceduresDefBlock block = (dynamic)blocks[i]; if ((block.type == ProceduresDefnoreturnBlock.type_name) || (block.type == ProceduresDefreturnBlock.type_name)) { var tuple = block.getProcedureDef(); if (tuple != null && Blockly.Names.equals(tuple.Item1, name)) { return block; } } } return null; } } }