/// <summary> /// Encode a block subtree as XML. /// </summary> /// <param name="block">The root block to encode.</param> /// <param name="optNoId">True if the encoder should skip the block id.</param> /// <returns></returns> public static XmlNode BlockToDom(Block block, bool optNoId = false) { var element = XmlUtil.CreateDom(block.IsShadow ? "shadow" : "block"); element.SetAttribute("type", block.Type); if (!optNoId) { element.SetAttribute("id", block.ID); } if (block.Mutator != null) { // Custom data for an advanced block. var container = block.Mutator.ToXml(); if (container != null) { element.AppendChild(container); } } Action <Field> fieldToDom = delegate(Field field) { // if TODO: editable is true (!string.IsNullOrEmpty(field.Name) && field.Editable) if (!string.IsNullOrEmpty(field.Name)) { var container = XmlUtil.CreateDom("field", null, field.GetValue()); container.SetAttribute("name", field.Name); if (field is FieldVariable) { var variable = block.Workspace.GetVariable(field.GetValue()); if (null != variable) { container.SetAttribute("id", variable.ID); container.SetAttribute("variableType", variable.Type); } } element.AppendChild(container); } }; foreach (var input in block.InputList) { foreach (var field in input.FieldRow) { fieldToDom(field); } } if (!string.IsNullOrEmpty(block.Data)) { var dataElement = XmlUtil.CreateDom("data", null, block.Data); element.AppendChild(dataElement); } foreach (var input in block.InputList) { XmlNode container = null; var empty = true; if (input.Type == Define.EConnection.DummyInput) { continue; } else { var childBlock = input.Connection.TargetBlock; if (input.Type == Define.EConnection.InputValue) { container = XmlUtil.CreateDom("value"); } else if (input.Type == Define.EConnection.NextStatement) { container = XmlUtil.CreateDom("statement"); } var shadow = input.Connection.ShadowDom; if (null != shadow && (null == childBlock || !childBlock.IsShadow)) { // container.appendChild(Blockly.Xml.cloneShadow_(shadow)); } if (null != childBlock) { container.AppendChild(Xml.BlockToDom(childBlock, optNoId)); empty = false; } } container.SetAttribute("name", input.Name); if (!empty) { element.AppendChild(container); } } if (block.Collapsed) { element.SetAttribute("collapsed", "true"); } if (block.Disabled) { element.SetAttribute("disabled", "true"); } if (!block.Deletable && !block.IsShadow) { element.SetAttribute("deletable", "false"); } if (!block.Movable && !block.IsShadow) { element.SetAttribute("movable", "false"); } if (!block.Editable) { element.SetAttribute("editable", "false"); } var nextBlock = block.NextBlock; if (null != nextBlock) { var container = XmlUtil.CreateDom("next", null, BlockToDom(nextBlock, optNoId)); element.AppendChild(container); } return(element); }
/// <summary> /// Decode an XML block tag and crete a block (and possbly sub blocks) on the /// workspace. /// </summary> /// <param name="xmlBlock"> XML block element.</param> /// <param name="workspace"> The workspace</param> public static Block DomToBlockHeadless(XmlNode xmlBlock, Workspace workspace) { var prototypeName = xmlBlock.GetAttribute("type"); var id = xmlBlock.GetAttribute("id"); Block block = workspace.NewBlock(prototypeName, id); Block blockChild = null; for (var i = 0; i < xmlBlock.ChildNodes.Count; i++) { var xmlChild = xmlBlock.ChildNodes[i]; if (xmlChild.NodeType == (XmlNodeType)3) { // Ignore any text at the <block>level. It's all whitespace anyway. continue; } Input input = null; // Find any enclosed blocks or shadows in this tag. XmlNode childBlockNode = null; XmlNode childShadowNode = null; for (var j = 0; j < xmlChild.ChildNodes.Count; j++) { var grandchildNode = xmlChild.ChildNodes[j]; if (grandchildNode.NodeType == (XmlNodeType)1) { if (string.Equals(grandchildNode.NodeName().ToLower(), "block")) { childBlockNode = grandchildNode; } else if (string.Equals(grandchildNode.NodeName().ToLower(), "shadow")) { childShadowNode = grandchildNode; } } } // Use the shadow block if there is no child block. if (null == childBlockNode && null != childShadowNode) { childBlockNode = childShadowNode; } var name = xmlChild.GetAttribute("name"); switch (xmlChild.NodeName().ToLower()) { case "mutation": // Custom data for an advanced block. if (block.Mutator != null) { block.Mutator.FromXml((XmlElement)xmlChild); } break; case "data": block.Data = xmlChild.TextContent(); break; case "title": case "field": { var field = block.GetField(name); var text = xmlChild.TextContent(); if (field is FieldVariable) { // TODO (marisaleung): When we change setValue and getValue to // interact with id's instead of names,update this so that we get // the variable based on id instead of textContent. var type = string.IsNullOrEmpty(xmlChild.GetAttribute("variableType")) ? xmlChild.GetAttribute("variableType") : ""; var variable = workspace.GetVariable(text); if (null == variable) { variable = workspace.CreateVariable(text, type, xmlChild.GetAttribute(id)); } if (!string.IsNullOrEmpty(type)) { if (!string.Equals(variable.Type, type)) { throw new Exception("Serialized variable type with id \'" + variable.ID + "\' had type " + variable.Type + ", and " + "does not match variable field that references it: " + Xml.DomToText(xmlChild) + "."); } } } if (null == field) { Console.WriteLine("Ignoring non-existent field " + name + " in block " + prototypeName); break; } field.SetValue(text); } break; case "value": case "statement": input = block.GetInput(name); if (null == input) { Console.WriteLine("Ignoring non-existent input " + name + " in block " + prototypeName); break; } if (null != childShadowNode) { // input.connection.setShadowDom(childShadowNode); } if (null != childBlockNode) { blockChild = Xml.DomToBlockHeadless(childBlockNode, workspace); if (null != blockChild.OutputConnection) { input.Connection.Connect(blockChild.OutputConnection); } else if (null != blockChild.PreviousConnection) { input.Connection.Connect(blockChild.PreviousConnection); } else { throw new Exception("Child block does not have output or previous statement."); } } break; case "next": if (null != childShadowNode && null != block.NextConnection) { // block.nextConnection.setShadowDom(childShadowNode); } if (null != childBlockNode) { blockChild = Xml.DomToBlockHeadless(childBlockNode, workspace); block.NextConnection.Connect(blockChild.PreviousConnection); } break; default: // Unknown tag; ignore. Same principle as HTML parsers. Console.WriteLine("Ignoring unknown tag: " + xmlChild.NodeName()); break; } } var inline = xmlBlock.GetAttribute("inline"); if (!string.IsNullOrEmpty(inline)) { block.SetInputsInline(string.Equals(inline, "true")); } var disabled = xmlBlock.GetAttribute("disabled"); if (!string.IsNullOrEmpty(disabled)) { block.Disabled = string.Equals(disabled, "true"); } var deletable = xmlBlock.GetAttribute("deletable"); if (!string.IsNullOrEmpty(deletable)) { block.Deletable = string.Equals(deletable, "true"); } var movable = xmlBlock.GetAttribute("movable"); if (!string.IsNullOrEmpty(movable)) { block.Movable = string.Equals(movable, "true"); } var editable = xmlBlock.GetAttribute("editable"); if (!string.IsNullOrEmpty(editable)) { block.Editable = string.Equals(editable, "true"); } var collapsed = xmlBlock.GetAttribute("collapsed"); if (!string.IsNullOrEmpty(collapsed)) { block.Collapsed = string.Equals(collapsed, "true"); } return(block); }
/// <summary> /// Connect two connections together. This is the connection on the superior block. /// </summary> /// <param name="childConnection"></param> private void ConnectInternal(Connection childConnection) { var parentConnection = this; var parentBlock = parentConnection.SourceBlock; var childBlock = childConnection.SourceBlock; // Disconnect any existing parent on the child connection. if (childConnection.IsConnected) { childConnection.Disconnect(); } // Other connection is already connected to something. // Disconnect it and reattach it or bump it as needed. if (parentConnection.IsConnected) { Block orphanBlock = parentConnection.TargetBlock; XmlNode shadowDom = parentConnection.ShadowDom; parentConnection.ShadowDom = null; if (orphanBlock.IsShadow) { // Save the shadow block so that field values are preserved. shadowDom = Xml.BlockToDom(orphanBlock); orphanBlock.Dispose(); orphanBlock = null; } else if (parentConnection.Type == Define.EConnection.InputValue) { //value connnections if (orphanBlock.OutputConnection == null) { throw new Exception("Orphan block does not have an output connection."); } // Attempt to reattach the orphan at the end of the newly inserted // block. Since this block may be a row, walk down to the end // or to the first (and only) shadow block. var connection = Connection.LastConnectionInRow(childBlock, orphanBlock); if (connection != null) { orphanBlock.OutputConnection.Connect(connection); orphanBlock = null; } } else if (parentConnection.Type == Define.EConnection.NextStatement) { // Statement connections. // Statement blocks may be inserted into the middle of a stack. // Split the stack. if (orphanBlock.PreviousConnection == null) { throw new Exception("Orphan block does not have a previous connection."); } // Attempt to reattach the orphan at the bottom of the newly inserted // block. Since this block may be a stack, walk down to the end. var newBlock = childBlock; while (newBlock.NextConnection != null) { var nextBlock = newBlock.NextBlock; if (nextBlock != null && !nextBlock.IsShadow) { newBlock = nextBlock; } else { if (orphanBlock.PreviousConnection.CheckType(newBlock.NextConnection)) { newBlock.NextConnection.Connect(orphanBlock.PreviousConnection); orphanBlock = null; } break; } } } if (orphanBlock != null) { // Unable to reattach orphan. parentConnection.Disconnect(); Connection orphanBlockCon = orphanBlock.OutputConnection != null ? orphanBlock.OutputConnection : orphanBlock.PreviousConnection; orphanBlockCon.FireUpdate(UpdateState.BumpedAway); } // Restore the shadow DOM. parentConnection.ShadowDom = shadowDom; } // Establish the connections. Connection.ConnectReciprocally(parentConnection, childConnection); // Demote the inferior block so that one is a child of the superior one. childBlock.SetParent(parentBlock); FireUpdate(UpdateState.Connected); }