/// <summary> /// Define blocks from imported block definitions. /// </summary> /// <param name="blockDefsString"></param> Block definition(s). /// <param name="format"> Block definition format ("JSON" or "JavaScript").</param> /// <returns>Array of block types defined.</returns> public static JsArray <string> defineAndGetBlockTypes(string blockDefsString, string format) { var blockTypes = new JsArray <string>(); // Define blocks and get block types. if (format == "JSON") { var blockDefArray = FactoryUtils.parseJsonBlockDefinitions(blockDefsString); // Populate array of blocktypes and define each block. for (var i = 0; i < blockDefArray.Length; i++) { var blockDef = blockDefArray[i]; var json = (Dictionary <string, object>)JSON.Parse(blockDef); blockTypes.Push((string)json["type"]); // Define the block. Blockly.Core.Blocks.Add((string)json["type"], blockDef); } } else if (format == "JavaScript") { var blockDefArray = FactoryUtils.parseJsBlockDefinitions(blockDefsString); // Populate array of block types. for (var i = 0; i < blockDefArray.Length; i++) { var blockDef = blockDefArray[i]; var blockType = FactoryUtils.getBlockTypeFromJsDefinition(blockDef); blockTypes.Push(blockType); } // Define all blocks. Script.Eval(blockDefsString); } return(blockTypes); }
/// <summary> /// Generate selector dom from block library storage. For each block in the /// library, it has a block option, which consists of a checkbox, a label, /// and a fixed size preview workspace. /// </summary> /// <param name="blockLibStorage"> Block Library Storage object.</param> /// <param name="blockSelectorId"> ID of the div element that will contain /// the block options.</param> /// <returns>Map of block type to Block Option object.</returns> public Dictionary <string, BlockOption> createBlockSelectorFromLib(BlockLibraryStorage blockLibStorage, string blockSelectorId) { // Object mapping each stored block type to XML. var allBlockTypes = blockLibStorage.getBlockTypes(); var blockXmlMap = blockLibStorage.getBlockXmlMap(allBlockTypes); // Define the custom blocks in order to be able to create instances of // them in the exporter workspace. this.addBlockDefinitions(blockXmlMap); var blockSelector = Document.GetElementById(blockSelectorId); // Clear the block selector. Node child; while ((child = blockSelector.FirstChild) != null) { blockSelector.RemoveChild(child); } // Append each block option's dom to the selector. var blockOptions = new Dictionary <string, BlockOption>(); foreach (var blockType in blockXmlMap.Keys) { // Get preview block's XML. var block = FactoryUtils.getDefinedBlock(blockType, this.hiddenWorkspace); var previewBlockXml = Blockly.Xml.workspaceToDom(this.hiddenWorkspace); // Create block option, inject block into preview workspace, and append // option to block selector. var blockOpt = new BlockOption(blockSelector, blockType, previewBlockXml); blockOpt.createDom(); blockSelector.AppendChild(blockOpt.dom); blockOpt.showPreviewBlock(); blockOptions[blockType] = blockOpt; } return(blockOptions); }
/// <summary> /// Saves current block to local storage and updates dropdown. /// </summary> public void saveToBlockLibrary() { var blockType = this.getCurrentBlockType(); // If user has not changed the name of the starter block. if (blockType == "block_type") { // Do not save block if it has the default type, "block_type". Window.Alert("You cannot save a block under the name \"block_type\". Try changing " + "the name before saving. Then, click on the \"Block Library\" button " + "to view your saved blocks."); return; } // Create block XML. var xmlElement = goog.dom.createDom("xml"); var block = FactoryUtils.getRootBlock(BlockFactory.mainWorkspace); xmlElement.AppendChild(Blockly.Xml.blockToDomWithXY(block)); // Do not add option again if block type is already in library. if (!this.has(blockType)) { this.view.addOption(blockType, true); } // Save block. this.storage.addBlock(blockType, xmlElement); this.storage.saveToLocalStorage(); // Show saved block without other stray blocks sitting in Block Factory's // main workspace. this.openBlock(blockType); // Add select handler to the new option. this.addOptionSelectHandler(blockType); }
/// <summary> /// Pulls information about all blocks in the block library to generate XML /// for the selector workpace's toolbox. /// </summary> /// <param name="blockLibStorage"> Block Library Storage object.</param> /// <returns>XML representation of the toolbox.</returns> Element generateToolboxFromLibrary(BlockLibraryStorage blockLibStorage) { // Create DOM for XML. var xmlDom = goog.dom.createDom("xml", new Dictionary <string, string> { { "id", "blockExporterTools_toolbox" }, { "style", "display:none" } }); var allBlockTypes = blockLibStorage.getBlockTypes(); // Object mapping block type to XML. var blockXmlMap = blockLibStorage.getBlockXmlMap(allBlockTypes); // Define the custom blocks in order to be able to create instances of // them in the exporter workspace. this.addBlockDefinitions(blockXmlMap); foreach (var blockType in blockXmlMap.Keys) { // Get block. var block = FactoryUtils.getDefinedBlock(blockType, this.hiddenWorkspace); var category = FactoryUtils.generateCategoryXml(new JsArray <Blockly.Block> { block }, blockType); xmlDom.AppendChild(category); } // If there are no blocks in library and the map is empty, append dummy // category. if (blockXmlMap.Count == 0) { var category = goog.dom.createDom("category"); category.SetAttribute("name", "Next Saved Block"); xmlDom.AppendChild(category); } return(xmlDom); }
/// <summary> /// Update the preview display. /// </summary> public static void updatePreview() { // Toggle between LTR/RTL if needed (also used in first display). var newDir = ((HTMLSelectElement)Document.GetElementById("direction")).Value; if (BlockFactory.oldDir != newDir) { if (BlockFactory.previewWorkspace != null) { BlockFactory.previewWorkspace.dispose(); } var rtl = newDir == "rtl"; BlockFactory.previewWorkspace = Blockly.Core.inject("preview", new Dictionary <string, object> { { "rtl", rtl }, { "media", "../../media/" }, { "scrollbars", true } }); BlockFactory.oldDir = newDir; } BlockFactory.previewWorkspace.clear(); // Fetch the code and determine its format (JSON or JavaScript). var format = ((HTMLSelectElement)Document.GetElementById("format")).Value; string code; if (format == "Manual") { code = ((HTMLTextAreaElement)Document.GetElementById("languageTA")).Value; // If the code is JSON, it will parse, otherwise treat as JS. try { JSON.Parse(code); format = "JSON"; } catch (Exception) { format = "JavaScript"; } } else { code = Document.GetElementById("languagePre").TextContent; } if (String.IsNullOrEmpty(code.Trim())) { // Nothing to render. Happens while cloud storage is loading. return; } // Backup Blockly.Core.Blocks object so that main workspace and preview don't // collide if user creates a "factory_base" block, for instance. var backupBlocks = Blockly.Core.Blocks; try { // Make a shallow copy. Blockly.Core.Blocks = new Blockly.Blocks(); foreach (var prop in backupBlocks.Keys) { Blockly.Core.Blocks[prop] = backupBlocks[prop]; } if (format == "JSON") { var json = (Dictionary <string, object>)JSON.Parse(code); Blockly.Core.Blocks.Add(json.ContainsKey("type") ? (string)json["type"] : BlockFactory.UNNAMED, code); } else if (format == "JavaScript") { Script.Eval(code); } else { throw new Exception("Unknown format: " + format); } // Look for a block on Blockly.Core.Blocks that does not match the backup. string blockType = null; foreach (var type in Blockly.Core.Blocks.Keys) { if (!backupBlocks.ContainsKey(type) || Blockly.Core.Blocks[type] != backupBlocks[type]) { blockType = type; break; } } if (String.IsNullOrEmpty(blockType)) { return; } // Create the preview block. var previewBlock = (Blockly.BlockSvg)BlockFactory.previewWorkspace.newBlock(blockType); previewBlock.initSvg(); previewBlock.render(); previewBlock.setMovable(false); previewBlock.setDeletable(false); previewBlock.moveBy(15, 10); BlockFactory.previewWorkspace.clearUndo(); BlockFactory.updateGenerator(previewBlock); // Warn user only if their block type is already exists in Blockly's // standard library. var rootBlock = FactoryUtils.getRootBlock(BlockFactory.mainWorkspace); if (Array.IndexOf(StandardCategories.coreBlockTypes, blockType) != -1) { rootBlock.setWarningText("A core Blockly block already exists " + "under this name."); } else if (blockType == "block_type") { // Warn user to let them know they can't save a block under the default // name "block_type" rootBlock.setWarningText("You cannot save a block with the default " + "name, \"block_type\""); } else { rootBlock.setWarningText(null); } } finally { Blockly.Core.Blocks = backupBlocks; } }
/// <summary> /// Called on each tab click. Hides and shows specific content based on which tab /// (Block Factory, Workspace Factory, or Exporter) is selected. /// </summary> public void onTab() { // Get tab div elements. var blockFactoryTab = this.tabMap[AppController.BLOCK_FACTORY]; var exporterTab = this.tabMap[AppController.EXPORTER]; var workspaceFactoryTab = this.tabMap[AppController.WORKSPACE_FACTORY]; // Warn user if they have unsaved changes when leaving Block Factory. if (this.lastSelectedTab == AppController.BLOCK_FACTORY && this.selectedTab != AppController.BLOCK_FACTORY) { var hasUnsavedChanges = !FactoryUtils.savedBlockChanges(this.blockLibraryController); if (hasUnsavedChanges && !Window.Confirm("You have unsaved changes in Block Factory.")) { // If the user doesn't want to switch tabs with unsaved changes, // stay on Block Factory Tab. this.setSelected_(AppController.BLOCK_FACTORY); this.lastSelectedTab = AppController.BLOCK_FACTORY; return; } } // Only enable key events in workspace factory if workspace factory tab is // selected. this.workspaceFactoryController.keyEventsEnabled = this.selectedTab == AppController.WORKSPACE_FACTORY; // Turn selected tab on and other tabs off. this.styleTabs_(); if (this.selectedTab == AppController.EXPORTER) { // Hide other tabs. FactoryUtils.hide("workspaceFactoryContent"); FactoryUtils.hide("blockFactoryContent"); // Show exporter tab. FactoryUtils.show("blockLibraryExporter"); // Need accurate state in order to know which blocks are used in workspace // factory. this.workspaceFactoryController.saveStateFromWorkspace(); // Update exporter's list of the types of blocks used in workspace factory. var usedBlockTypes = this.workspaceFactoryController.getAllUsedBlockTypes(); this.exporter.setUsedBlockTypes(usedBlockTypes); // Update exporter's block selector to reflect current block library. this.exporter.updateSelector(); // Update the exporter's preview to reflect any changes made to the blocks. this.exporter.updatePreview(); } else if (this.selectedTab == AppController.BLOCK_FACTORY) { // Hide other tabs. FactoryUtils.hide("blockLibraryExporter"); FactoryUtils.hide("workspaceFactoryContent"); // Show Block Factory. FactoryUtils.show("blockFactoryContent"); } else if (this.selectedTab == AppController.WORKSPACE_FACTORY) { // Hide other tabs. FactoryUtils.hide("blockLibraryExporter"); FactoryUtils.hide("blockFactoryContent"); // Show workspace factory container. FactoryUtils.show("workspaceFactoryContent"); // Update block library category. var categoryXml = this.exporter.getBlockLibraryCategory(); var blockTypes = this.blockLibraryController.getStoredBlockTypes(); this.workspaceFactoryController.setBlockLibCategory(categoryXml, blockTypes); } // Resize to render workspaces" toolboxes correctly for all tabs. Window.DispatchEvent(new Event("resize")); }
/// <summary> /// Returns field strings and any config. /// </summary> /// <param name="block">Input block.</param> /// <returns>Field strings.</returns> private static JsArray <string> getFieldsJs_(Blockly.Block block) { var fields = new JsArray <string>(); while (block != null) { if (!block.disabled && !block.getInheritedDisabled()) { switch (block.type) { case "field_static": // Result: "hello" fields.Push(FactoryUtils.escapeString(block.getFieldValue("TEXT"))); break; case "field_input": // Result: new Blockly.FieldTextInput("Hello"), "GREET" fields.Push("new Blockly.FieldTextInput(" + FactoryUtils.escapeString(block.getFieldValue("TEXT")) + "), " + FactoryUtils.escapeString(block.getFieldValue("FIELDNAME"))); break; case "field_number": // Result: new Blockly.FieldNumber(10, 0, 100, 1), "NUMBER" var args = new JsArray <double> { Script.ParseFloat(block.getFieldValue("VALUE")), Script.ParseFloat(block.getFieldValue("MIN")), Script.ParseFloat(block.getFieldValue("MAX")), Script.ParseFloat(block.getFieldValue("PRECISION")) }; // Remove any trailing arguments that aren't needed. if (args[3] == 0) { args.Pop(); if (args[2] == Double.PositiveInfinity) { args.Pop(); if (args[1] == Double.NegativeInfinity) { args.Pop(); } } } fields.Push("new Blockly.FieldNumber(" + args.Join(", ") + "), " + FactoryUtils.escapeString(block.getFieldValue("FIELDNAME"))); break; case "field_angle": // Result: new Blockly.FieldAngle(90), "ANGLE" fields.Push("new Blockly.FieldAngle(" + Script.ParseFloat(block.getFieldValue("ANGLE")) + "), " + FactoryUtils.escapeString(block.getFieldValue("FIELDNAME"))); break; case "field_checkbox": // Result: new Blockly.FieldCheckbox("TRUE"), "CHECK" fields.Push("new Blockly.FieldCheckbox(" + FactoryUtils.escapeString(block.getFieldValue("CHECKED")) + "), " + FactoryUtils.escapeString(block.getFieldValue("FIELDNAME"))); break; case "field_colour": // Result: new Blockly.FieldColour("#ff0000"), "COLOUR" fields.Push("new Blockly.FieldColour(" + FactoryUtils.escapeString(block.getFieldValue("COLOUR")) + "), " + FactoryUtils.escapeString(block.getFieldValue("FIELDNAME"))); break; case "field_date": // Result: new Blockly.FieldDate("2015-02-04"), "DATE" fields.Push("new Blockly.FieldDate(" + FactoryUtils.escapeString(block.getFieldValue("DATE")) + "), " + FactoryUtils.escapeString(block.getFieldValue("FIELDNAME"))); break; case "field_variable": // Result: new Blockly.FieldVariable("item"), "VAR" var varname = FactoryUtils.escapeString(block.getFieldValue("TEXT") ?? null); fields.Push("new Blockly.FieldVariable(" + varname + "), " + FactoryUtils.escapeString(block.getFieldValue("FIELDNAME"))); break; case "field_dropdown": // Result: // new Blockly.FieldDropdown([["yes", "1"], ["no", "0"]]), "TOGGLE" var options = new JsArray <string>(); for (var i = 0; i < ((FieldDropdown)block).optionCount_; i++) { options[i] = "[" + FactoryUtils.escapeString(block.getFieldValue("USER" + i)) + ", " + FactoryUtils.escapeString(block.getFieldValue("CPU" + i)) + "]"; } if (options.Length > 0) { fields.Push("new Blockly.FieldDropdown([" + options.Join(", ") + "]), " + FactoryUtils.escapeString(block.getFieldValue("FIELDNAME"))); } break; case "field_image": // Result: new Blockly.FieldImage("http://...", 80, 60) var src = FactoryUtils.escapeString(block.getFieldValue("SRC")); var width = Convert.ToDouble(block.getFieldValue("WIDTH")); var height = Convert.ToDouble(block.getFieldValue("HEIGHT")); var alt = FactoryUtils.escapeString(block.getFieldValue("ALT")); fields.Push("new Blockly.FieldImage(" + src + ", " + width + ", " + height + ", " + alt + ")"); break; } } block = block.nextConnection == null ? null : block.nextConnection.targetBlock(); } return(fields); }
/// <summary> /// Update the language code as JavaScript. /// </summary> /// <param name="blockType"> Name of block.</param> /// <param name="rootBlock"> Factory_base block.</param> /// <param name="workspace"></param> Where the root block lives. /// <returns>Generated language code.</returns> private static string formatJavaScript_(string blockType, Blockly.Block rootBlock, Blockly.Workspace workspace) { var code = new JsArray <string>(); code.Push("Blockly.Core.Blocks[\"" + blockType + "\"] = {"); code.Push(" init: () => {"); // Generate inputs. var TYPES = new Dictionary <string, string>() { { "input_value", "appendValueInput" }, { "input_statement", "appendStatementInput" }, { "input_dummy", "appendDummyInput" } }; var contentsBlock = rootBlock.getInputTargetBlock("INPUTS"); while (contentsBlock != null) { if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) { var name = ""; // Dummy inputs don't have names. Other inputs do. if (contentsBlock.type != "input_dummy") { name = FactoryUtils.escapeString(contentsBlock.getFieldValue("INPUTNAME")); } code.Push(" this." + TYPES[contentsBlock.type] + "(" + name + ")"); var check = FactoryUtils.getOptTypesFrom(contentsBlock, "TYPE"); if (!String.IsNullOrEmpty(check)) { code.Push(" .setCheck(" + check + ")"); } var align = contentsBlock.getFieldValue("ALIGN"); if (align != "LEFT") { code.Push(" .setAlign(Blockly.ALIGN_" + align + ")"); } var fields = FactoryUtils.getFieldsJs_( contentsBlock.getInputTargetBlock("FIELDS")); for (var i = 0; i < fields.Length; i++) { code.Push(" .appendField(" + fields[i] + ")"); } // Add semicolon to last line to finish the statement. code[code.Length - 1] += ";"; } contentsBlock = contentsBlock.nextConnection == null ? null : contentsBlock.nextConnection.targetBlock(); } // Generate inline/external switch. if (rootBlock.getFieldValue("INLINE") == "EXT") { code.Push(" this.setInputsInline(false);"); } else if (rootBlock.getFieldValue("INLINE") == "INT") { code.Push(" this.setInputsInline(true);"); } // Generate output, or next/previous connections. switch (rootBlock.getFieldValue("CONNECTIONS")) { case "LEFT": code.Push(FactoryUtils.connectionLineJs_("setOutput", "OUTPUTTYPE", workspace)); break; case "BOTH": code.Push( FactoryUtils.connectionLineJs_("setPreviousStatement", "TOPTYPE", workspace)); code.Push( FactoryUtils.connectionLineJs_("setNextStatement", "BOTTOMTYPE", workspace)); break; case "TOP": code.Push( FactoryUtils.connectionLineJs_("setPreviousStatement", "TOPTYPE", workspace)); break; case "BOTTOM": code.Push( FactoryUtils.connectionLineJs_("setNextStatement", "BOTTOMTYPE", workspace)); break; } // Generate colour. var colourBlock = rootBlock.getInputTargetBlock("COLOUR"); if (colourBlock != null && !colourBlock.disabled) { var hue = Script.ParseFloat(colourBlock.getFieldValue("HUE")); if (!Double.IsNaN(hue)) { code.Push(" this.setColour(" + hue + ");"); } } code.Push(" this.setTooltip(\"\");"); code.Push(" this.setHelpUrl(\"http://www.example.com/\");"); code.Push(" }"); code.Push("};"); return(code.Join("\n")); }
/// <summary> /// Update the language code as JSON. /// </summary> /// <param name="blockType"> Name of block.</param> /// <param name="rootBlock">Factory_base block.</param> /// <returns>Generanted language code.</returns> private static string formatJson_(string blockType, Blockly.Block rootBlock) { var JS = new Dictionary <string, object>(); // Type is not used by Blockly, but may be used by a loader. JS["type"] = blockType; // Generate inputs. var message = new JsArray <string>(); var args = new JsArray <object>(); var contentsBlock = rootBlock.getInputTargetBlock("INPUTS"); Blockly.Block lastInput = null; while (contentsBlock != null) { if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) { var fields = FactoryUtils.getFieldsJson_( contentsBlock.getInputTargetBlock("FIELDS")); for (var i = 0; i < fields.Length; i++) { if (fields[i] is string str) { message.Push(str.Replace(new Regex("%", RegexOptions.Multiline), "%%")); } else { args.Push(fields[i]); message.Push("%" + args.Length); } } var input = new Dictionary <string, object>(); input.Add("type", contentsBlock.type); // Dummy inputs don't have names. Other inputs do. if (contentsBlock.type != "input_dummy") { input["name"] = contentsBlock.getFieldValue("INPUTNAME"); } var check = JSON.Parse( FactoryUtils.getOptTypesFrom(contentsBlock, "TYPE") ?? "null"); if (check != null) { input["check"] = check; } var align = contentsBlock.getFieldValue("ALIGN"); if (align != "LEFT") { input["align"] = align; } args.Push(input); message.Push("%" + args.Length); lastInput = contentsBlock; } contentsBlock = contentsBlock.nextConnection == null ? null : contentsBlock.nextConnection.targetBlock(); } // Remove last input if dummy and not empty. if (lastInput != null && lastInput.type == "input_dummy") { var fields = lastInput.getInputTargetBlock("FIELDS"); if (fields != null && FactoryUtils.getFieldsJson_(fields).Join("").Trim() != "") { var align = lastInput.getFieldValue("ALIGN"); if (align != "LEFT") { JS["lastDummyAlign0"] = align; } args.Pop(); message.Pop(); } } JS["message0"] = message.Join(" "); if (args.Length > 0) { JS["args0"] = args; } // Generate inline/external switch. if (rootBlock.getFieldValue("INLINE") == "EXT") { JS["inputsInline"] = false; } else if (rootBlock.getFieldValue("INLINE") == "INT") { JS["inputsInline"] = true; } // Generate output, or next/previous connections. switch (rootBlock.getFieldValue("CONNECTIONS")) { case "LEFT": JS["output"] = JSON.Parse( FactoryUtils.getOptTypesFrom(rootBlock, "OUTPUTTYPE") ?? "null"); break; case "BOTH": JS["previousStatement"] = JSON.Parse( FactoryUtils.getOptTypesFrom(rootBlock, "TOPTYPE") ?? "null"); JS["nextStatement"] = JSON.Parse( FactoryUtils.getOptTypesFrom(rootBlock, "BOTTOMTYPE") ?? "null"); break; case "TOP": JS["previousStatement"] = JSON.Parse( FactoryUtils.getOptTypesFrom(rootBlock, "TOPTYPE") ?? "null"); break; case "BOTTOM": JS["nextStatement"] = JSON.Parse( FactoryUtils.getOptTypesFrom(rootBlock, "BOTTOMTYPE") ?? "null"); break; } // Generate colour. var colourBlock = rootBlock.getInputTargetBlock("COLOUR"); if (colourBlock != null && !colourBlock.disabled) { var hue = Script.ParseFloat(colourBlock.getFieldValue("HUE")); JS["colour"] = hue; } JS["tooltip"] = ""; JS["helpUrl"] = "http://www.example.com/"; return(JSON.Stringify(JS, null, " ")); }
/// <summary> /// Add event listeners for workspace factory. /// </summary> /// <param name="controller"> The controller for the workspace /// factory tab.</param> private static void addWorkspaceFactoryEventListeners_(WorkspaceFactoryController controller) { // Use up and down arrow keys to move categories. Window.AddEventListener("keydown", new Action <Event>((e) => { // Don't let arrow keys have any effect if not in Workspace Factory // editing the toolbox. if (!(controller.keyEventsEnabled && controller.selectedMode == WorkspaceFactoryController.MODE_TOOLBOX)) { return; } if (e.KeyCode == 38) { // Arrow up. controller.moveElement(-1); } else if (e.KeyCode == 40) { // Arrow down. controller.moveElement(1); } })); // Determines if a block breaks shadow block placement rules. // Breaks rules if (1) a shadow block no longer has a valid // parent, or (2) a normal block is inside of a shadow block. var isInvalidBlockPlacement = new Func <Blockly.Block, bool>((block) => { return((controller.isUserGenShadowBlock(block.id) && block.getSurroundParent() == null) || (!controller.isUserGenShadowBlock(block.id) && block.getSurroundParent() != null && controller.isUserGenShadowBlock(block.getSurroundParent().id))); }); // Add change listeners for toolbox workspace in workspace factory. controller.toolboxWorkspace.addChangeListener((e) => { // Listen for Blockly move and delete events to update preview. // Not listening for Blockly create events because causes the user to drop // blocks when dragging them into workspace. Could cause problems if ever // load blocks into workspace directly without calling updatePreview. if (e.type == Blockly.Events.MOVE || e.type == Blockly.Events.DELETE || e.type == Blockly.Events.CHANGE) { controller.saveStateFromWorkspace(); controller.updatePreview(); } // Listen for Blockly UI events to correctly enable the "Edit Block" button. // Only enable "Edit Block" when a block is selected and it has a // surrounding parent, meaning it is nested in another block (blocks that // are not nested in parents cannot be shadow blocks). if (e.type == Blockly.Events.MOVE || (e.type == Blockly.Events.UI && ((Blockly.Events.Ui)e).element == "selected")) { var selected = Blockly.Core.selected; // Show shadow button if a block is selected. Show "Add Shadow" if // a block is not a shadow block, show "Remove Shadow" if it is a // shadow block. if (selected != null) { var isShadow = controller.isUserGenShadowBlock(selected.id); WorkspaceFactoryInit.displayAddShadow_(!isShadow); WorkspaceFactoryInit.displayRemoveShadow_(isShadow); } else { WorkspaceFactoryInit.displayAddShadow_(false); WorkspaceFactoryInit.displayRemoveShadow_(false); } if (selected != null && selected.getSurroundParent() != null && !controller.isUserGenShadowBlock(selected.getSurroundParent().id)) { // Selected block is a valid shadow block or could be a valid shadow // block. // Enable block editing and remove warnings if the block is not a // variable user-generated shadow block. ((HTMLButtonElement)Document.GetElementById("button_addShadow")).Disabled = false; ((HTMLButtonElement)Document.GetElementById("button_removeShadow")).Disabled = false; if (!FactoryUtils.hasVariableField(selected) && controller.isDefinedBlock(selected)) { selected.setWarningText(null); } } else { // Selected block cannot be a valid shadow block. if (selected != null && isInvalidBlockPlacement(selected)) { // Selected block breaks shadow block rules. // Invalid shadow block if (1) a shadow block no longer has a valid // parent, or (2) a normal block is inside of a shadow block. if (!controller.isUserGenShadowBlock(selected.id)) { // Warn if a non-shadow block is nested inside a shadow block. selected.setWarningText("Only shadow blocks can be nested inside\n" + "other shadow blocks."); } else if (!FactoryUtils.hasVariableField(selected)) { // Warn if a shadow block is invalid only if not replacing // warning for variables. selected.setWarningText("Shadow blocks must be nested inside other" + " blocks."); } // Give editing options so that the user can make an invalid shadow // block a normal block. ((HTMLButtonElement)Document.GetElementById("button_removeShadow")).Disabled = false; ((HTMLButtonElement)Document.GetElementById("button_addShadow")).Disabled = true; } else { // Selected block does not break any shadow block rules, but cannot // be a shadow block. // Remove possible "invalid shadow block placement" warning. if (selected != null && controller.isDefinedBlock(selected) && (!FactoryUtils.hasVariableField(selected) || !controller.isUserGenShadowBlock(selected.id))) { selected.setWarningText(null); } // No block selected that is a shadow block or could be a valid shadow // block. Disable block editing. ((HTMLButtonElement)Document.GetElementById("button_addShadow")).Disabled = true; ((HTMLButtonElement)Document.GetElementById("button_removeShadow")).Disabled = true; } } } // Convert actual shadow blocks added from the toolbox to user-generated // shadow blocks. if (e.type == Blockly.Events.CREATE) { controller.convertShadowBlocks(); // Let the user create a Variables or Functions category if they use // blocks from either category. // Get all children of a block and add them to childList. Action <Blockly.Block, JsArray <Blockly.Block> > getAllChildren = null; getAllChildren = new Action <Blockly.Block, JsArray <Blockly.Block> >((block, childList) => { childList.Push(block); var children = block.getChildren(); for (var i = 0; i < children.Length; i++) { var child = children[i]; getAllChildren(child, childList); } }); var newBaseBlock = controller.toolboxWorkspace.getBlockById(e.blockId); var allNewBlocks = new JsArray <Blockly.Block>(); getAllChildren(newBaseBlock, allNewBlocks); var variableCreated = false; var procedureCreated = false; // Check if the newly created block or any of its children are variable // or procedure blocks. for (var i = 0; i < allNewBlocks.Length; i++) { var block = allNewBlocks[i]; if (FactoryUtils.hasVariableField(block)) { variableCreated = true; } else if (FactoryUtils.isProcedureBlock(block)) { procedureCreated = true; } } // If any of the newly created blocks are variable or procedure blocks, // prompt the user to create the corresponding standard category. if (variableCreated && !controller.hasVariablesCategory()) { if (Window.Confirm("Your new block has a variables field. To use this block " + "fully, you will need a Variables category. Do you want to add " + "a Variables category to your custom toolbox?")) { controller.setMode(WorkspaceFactoryController.MODE_TOOLBOX); controller.loadCategoryByName("variables"); } } if (procedureCreated && !controller.hasProceduresCategory()) { if (Window.Confirm("Your new block is a function block. To use this block " + "fully, you will need a Functions category. Do you want to add " + "a Functions category to your custom toolbox?")) { controller.setMode(WorkspaceFactoryController.MODE_TOOLBOX); controller.loadCategoryByName("functions"); } } } }); }