/**
* @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;
}
}
}