public void handle(Word.ContentControl copiedCC) { // In the move case, we know that if it is a FabDocx content control, it is valid // in this docx if (copiedCC.Tag == null) { // Its just some content control we don't care about fabDocxState.registerKnownSdt(copiedCC); return; // this is OK, since if it contains a repeat or something, that event will fire } else if (copiedCC.Tag.Contains("od:xpath")) { td = new TagData(copiedCC.Tag); // RULE: A variable cc can copied wherever. // If its repeat ancestors change, its "vary in repeat" // will need to change (to lowest common denominator). handleXPath(td.getXPathID(), true); } else if (copiedCC.Tag.Contains("od:repeat")) { // RULE: A repeat can be moved wherever under // the same repeat ancestor cc. // It can be moved elsewhere, provided it is // the only cc using that repeat. (change // AF and XPaths accordingly). handleRepeat(copiedCC); } else if (copiedCC.Tag.Contains("od:condition")) { // Identify child content controls Microsoft.Office.Interop.Word.Range rng = copiedCC.Range; Word.ContentControls ccs = rng.ContentControls; foreach (Word.ContentControl desc in ccs) { if (desc.ParentContentControl.ID.Equals(copiedCC.ID)) { handle(desc); } } } }
/// <summary> /// Handle Word's OnEnter event for content controls, to set the selection in the pane (if the option is set). /// </summary> /// <param name="ccEntered">A ContentControl object specifying the control that was entered.</param> private void doc_ContentControlOnEnter(Word.ContentControl ccEntered) { log.Debug("Document.ContentControlOnEnter fired."); if (ccEntered.XMLMapping.IsMapped) { log.Debug("control mapped to " + ccEntered.XMLMapping.XPath); if (ccEntered.XMLMapping.CustomXMLNode != null) { m_cmTaskPane.RefreshControls(Controls.ControlMain.ChangeReason.OnEnter, null, null, null, ccEntered.XMLMapping.CustomXMLNode, null); } else { // Not mapped to anything; probably because the part was replaced? log.Debug(".. but XMLMapping.CustomXMLNode is null"); m_cmTaskPane.controlTreeView.DeselectNode(); m_cmTaskPane.WarnViaProperties(ccEntered.XMLMapping.XPath); } } else if (ccEntered.Tag!=null) { TagData td = new TagData(ccEntered.Tag); string xpathid = td.getXPathID(); if (xpathid==null) { xpathid = td.getRepeatID(); } if (xpathid == null) { // Visually indicate in the task pane that we're no longer in a mapped control m_cmTaskPane.controlTreeView.DeselectNode(); m_cmTaskPane.PropertiesClear(); } else { log.Debug("control mapped via tag to " + xpathid); // Repeats, escaped XHTML; we don't show anything for conditions XPathsPartEntry xppe = new XPathsPartEntry(m_cmTaskPane.model); xpathsXpath xx = xppe.getXPathByID(xpathid); Office.CustomXMLNode customXMLNode = null; if (xx != null) { log.Debug(xx.dataBinding.xpath); customXMLNode = m_currentPart.SelectSingleNode(xx.dataBinding.xpath); } if (customXMLNode == null) { // Not mapped to anything; probably because the part was replaced? m_cmTaskPane.controlTreeView.DeselectNode(); if (xx == null) { log.Error("Couldn't find xpath for " + xpathid); m_cmTaskPane.WarnViaProperties("Missing"); } else { log.Warn("Couldn't find target node for " + xx.dataBinding.xpath); m_cmTaskPane.WarnViaProperties(xx.dataBinding.xpath); } } else { m_cmTaskPane.RefreshControls(Controls.ControlMain.ChangeReason.OnEnter, null, null, null, customXMLNode, null); } } } Ribbon.buttonEditEnabled = true; Ribbon.buttonDeleteEnabled = true; Ribbon.myInvalidate(); }
public void handle(Word.ContentControl copiedCC) { if (copiedCC.Tag == null) { // Its just some content control we don't care about fabDocxState.registerKnownSdt(copiedCC); return; // this is OK, since if it contains a repeat or something, that event will fire } log.Info("copy handler invoked on CC: " + copiedCC.Tag); if (copiedCC.Tag.Contains("od:xpath")) { td = new TagData(copiedCC.Tag); string xpathID = td.getXPathID(); // Check it is known to this docx if (xppe.getXPathByID(xpathID) == null) { removeButKeepContents(copiedCC); return; } // RULE: A variable cc can copied wherever. // If its repeat ancestors change, its "vary in repeat" // will need to change (to lowest common denominator). handleXPath(td.getXPathID(), true); } else if (copiedCC.Tag.Contains("od:repeat")) { td = new TagData(copiedCC.Tag); string xpathID = td.getRepeatID(); // Check it is known to this docx if (xppe.getXPathByID(xpathID) == null) { removeButKeepContents(copiedCC); return; } // RULE: A repeat can only be copied if destination // has same repeat ancestors (in which case no change // to answer file is required). handleRepeat(copiedCC); } else if (copiedCC.Tag.Contains("od:condition")) { // Find child CC Microsoft.Office.Interop.Word.Range rng = copiedCC.Range; Word.ContentControls ccs = rng.ContentControls; foreach (Word.ContentControl desc in ccs) { if (desc.ParentContentControl.ID.Equals(copiedCC.ID)) { handle(desc); } } } else { // Its just some content control we don't care about fabDocxState.registerKnownSdt(copiedCC); return; } }
/// <summary> /// We've already written the correct binding part id, in our xpaths. We need to do it in the cc'sas well. /// </summary> /// <param name="range"></param> /// <param name="answersPart"></param> public void updateBindings(Word.Range range, Office.CustomXMLPart answersPart) { foreach (Word.ContentControl cc in range.ContentControls) { if (cc.Tag == null) { // Its just some content control we don't care about continue; } TagData td = new TagData(cc.Tag); if (cc.Tag.Contains("od:xpath")) { xpathsXpath xp = srcXppe.getXPathByID(td.getXPathID()); cc.XMLMapping.SetMapping(xp.dataBinding.xpath, "xmlns:oda='http://opendope.org/answers'", answersPart); } } }
public void identifyLogic(Word.Range range) { foreach (Word.ContentControl cc in range.ContentControls) { if (cc.Tag == null) { // Its just some content control we don't care about continue; } TagData td = new TagData(cc.Tag); if (cc.Tag.Contains("od:xpath")) { xpathsXpath xp = srcXppe.getXPathByID( td.getXPathID() ); identifyLogicInXPath(xp, range, cc); } else if (cc.Tag.Contains("od:repeat")) { testOldestRepeatIncluded(range, cc); xpathsXpath xp = srcXppe.getXPathByID(td.getRepeatID()); // TODO: Clone, so we can write source to just the clone //xp.Serialize //xpathsXpath.Deserialize // TODO consider cloning issue further. I've implemented it // below for xpaths, but since we're throwing the objects // away, it doesn't matter that we're altering them? BBxpaths.Add(xp); string questionID = xp.questionID; question q = srcQuestionnaire.getQuestion(questionID); BBquestions.Add(q); // Answer - just need to do this for the outermost repeat Word.ContentControl oldestRepeat = RepeatHelper.getOldestRepeatAncestor(cc); if (oldestRepeat.ID.Equals(cc.ID)) { foreach (object o in srcAnswers.Items) { if (o is repeat) { repeat a = (repeat)o; if (a.qref.Equals(questionID)) // question ID == answer ID { BBrepeats.Add(a); log.Debug("Added outermost repeat!"); break; } } } } } else if (cc.Tag.Contains("od:condition")) { // Add the condition condition c = srcCpe.getConditionByID(td.getConditionID()); BBconditions.Add(c); // Find and add questions and xpaths used within it //List<xpathsXpath> xpaths = ConditionsPartEntry.getXPathsUsedInCondition(c, srcXppe); List<xpathsXpath> xpaths = new List<xpathsXpath>(); c.listXPaths(xpaths, srcCpe.conditions, srcXppe.getXPaths()); foreach (xpathsXpath xp in xpaths) { identifyLogicInXPath(xp, range, cc); } } } }
public void buttonRepeat_Click(Office.IRibbonControl control) { Word.Document document = Globals.ThisAddIn.Application.ActiveDocument; FabDocxState fabDocxState = (FabDocxState)Globals.ThisAddIn.Application.ActiveDocument.GetVstoObject(Globals.Factory).Tag; Model model = fabDocxState.model; Office.CustomXMLPart answersPart = model.answersPart; //.userParts[0]; // TODO: make this better XPathsPartEntry xppe = new XPathsPartEntry(model); // used to get entries questionnaire questionnaire = new questionnaire(); questionnaire.Deserialize(model.questionsPart.XML, out questionnaire); Microsoft.Office.Interop.Word.Range rng = document.ActiveWindow.Selection.Range; // Are there any content controls in the selection? Word.ContentControls ccs = rng.ContentControls; // First identify nested repeats List<Word.ContentControl> nestedRepeats = new List<Word.ContentControl>(); foreach (Word.ContentControl desc in ccs) { if (desc.Tag.Contains("od:repeat")) { nestedRepeats.Add(desc); } } // questions contains questions wrapped by repeat, // but not nested inside another repeat List<question> questions = new List<question>(); foreach (Word.ContentControl desc in ccs) { if (desc.Tag.Contains("od:repeat")) { continue; // will handle repeats later } // exclude if in nested repeat, since // this question will have previously been dealt with // (ie it already varies with that repeat, or the // use has said they don't want it to) if (isInside(nestedRepeats, desc)) { continue; } //log.Warn("got a desc with tag " + desc.Tag); // Get the tag if (desc.Tag.Contains("od:xpath")) { TagData td = new TagData(desc.Tag); string xpathID = td.getXPathID(); //log.Warn("xpath is " + xpathID); xpathsXpath xp = xppe.getXPathByID(xpathID); log.Warn("qid is " + xp.questionID); question q = questionnaire.getQuestion(xp.questionID); if (q == null) { log.Error("Consistency issue: couldn't find question {0} used in xpath {1}", xp.questionID, xpathID); } else { questions.Add(q); } } else if (desc.Tag.Contains("od:condition")) { // TODO: find questions inside conditions } } if (questions.Count > 0) { // Rule: Only questions which aren't used elsewhere can vary in a repeat. // Check that none of the questions that will be // inside the repeat are also used outside of it. List<question> questionsUsedOutside = new List<question>(); foreach (Word.ContentControl ccx in document.ContentControls) { if (isListed(ccs, ccx)) { // this control is inside the repeat } else { // its outside, so look at its question // TODO: conditions, repeats if (ccx.Tag.Contains("od:xpath")) { TagData td = new TagData(ccx.Tag); string xpathID = td.getXPathID(); //log.Warn("xpath is " + xpathID); xpathsXpath xp = xppe.getXPathByID(xpathID); //log.Warn("qid is " + xp.questionID); question q = questionnaire.getQuestion(xp.questionID); if (q == null) { log.Error("Consistency issue: couldn't find question {0} used in xpath {1}", xp.questionID, xpathID); } else { if (questions.Contains(q)) { questionsUsedOutside.Add(q); } } } } } // foreach // If they are, they can't vary in repeat. Get the user to OK this. if (questionsUsedOutside.Count == 0) { log.Info("None of the questions in wrapping repeat are used elsewhere"); } else { log.Info(questionsUsedOutside.Count + " of the questions in wrapping repeat are used elsewhere"); DialogResult dresult = MessageBox.Show( questionsUsedOutside.Count + " of the questions here are also used elsewhere. If you continue, these won't vary in each repeat.", "Questions used elsewhere", MessageBoxButtons.OKCancel); if (dresult == DialogResult.OK) { // Just remove them from the list foreach (question qx in questionsUsedOutside) { questions.Remove(qx); } } else { log.Info("User cancelled wrapping repeat coz questions used elsewhere"); return; } } } // Create control Word.ContentControl wrappingRepeatCC = null; object oRng = rng; try { fabDocxState.inPlutextAdd = true; wrappingRepeatCC = document.ContentControls.Add(Word.WdContentControlType.wdContentControlRichText, ref oRng); //cc.MultiLine = true; // Causes error for RichText } catch (System.Exception ex) { log.Warn(ex); MessageBox.Show("Selection must be either part of a single paragraph, or one or more whole paragraphs"); fabDocxState.inPlutextAdd = false; return; } FormRepeat formRepeat = new FormRepeat(model.questionsPart, answersPart, model, wrappingRepeatCC); formRepeat.ShowDialog(); string repeatId = formRepeat.ID; formRepeat.Dispose(); // necessary here? shouldn't be.. //answersPart.NamespaceManager.AddNamespace("answers", "http://opendope.org/answers"); // Destination for moves Office.CustomXMLNode destination = answersPart.SelectSingleNode("//oda:repeat[@qref='" + repeatId + "']/oda:row"); if (destination == null) { log.Error("no rpt node " + repeatId); } Dictionary<string, string> xpathChanges = new Dictionary<string, string>(); // Questions, Conditions // ^^^^^^^^^^^^^^^^^^^^^ // If so, the associated questions may need to be moved into the repeat in the answers XML. // Present a table of questions, where the user can say yes/no to each, // then move.. table excludes: // 1. any that are used outside the repeat, since these can't be made to vary (see above) // 2. any that are in a nested repeat if (questions.Count > 0) { FormRepeatWhichVariables formRepeatWhichVariables = new FormRepeatWhichVariables(questions); formRepeatWhichVariables.ShowDialog(); List<question> questionsWhichRepeat = formRepeatWhichVariables.getVars(); formRepeatWhichVariables.Dispose(); log.Info(answersPart.XML); foreach (question q in questionsWhichRepeat) { // Find the relevant answer (by ID) // (easiest to do using XPath on XML document Office.CustomXMLNode node = answersPart.SelectSingleNode("//oda:answer[@id='" + q.id + "']"); if (node == null) { log.Error("no node " + q.id); } string fromXPath = NodeToXPath.getXPath(node); log.Info("from: " + fromXPath); // Move it String nodeXML = node.XML; // No API to add a node! node.ParentNode.RemoveChild(node); destination.AppendChildSubtree(nodeXML); // So we'll have to change its xpath in XPaths part // eg from: // "/oda:answers/oda:answer[@id='qa_2']" // to: // "/oda:answers/oda:repeat[@qref='rpt1"]/oda:row[1]/oda:answer[@id='qa_2']" // // CustomXMLNode's Xpath produces something like: /ns2:answers[1]/ns2:answer[1] // which we don't want string toXPath = NodeToXPath.getXPath(destination.LastChild); log.Info("to: " + toXPath); xpathChanges.Add(fromXPath, toXPath); } } // nested repeats // ^^^^^^^^^^^^^^ // 1. move the nested repeat answer structure // 2. change the xpath for all questions re anything in the nested repeat // Note: if wrapping repeat r0 around r1 which in turn contains r2, // must avoid explicitly processing r2, since doing stuff to r1 here is // enough to take care of r2. // So, step 0. Find top level nested repeats // Already have nestedRepeats list, so just remove from it // those which aren't top level. foreach (Word.ContentControl desc in nestedRepeats) { if (!desc.ParentContentControl.ID.Equals(wrappingRepeatCC.ID) ) { // not top level, so remove nestedRepeats.Remove(desc); } } if (nestedRepeats.Count > 0) { foreach (Word.ContentControl desc in nestedRepeats) { TagData td = new TagData(desc.Tag); string nestedRepeatXPathID = td.getRepeatID(); // Get the XPath, to find the question ID, // which is what we use to find the repeat answer. xpathsXpath xp = xppe.getXPathByID(nestedRepeatXPathID); // 1. move the nested repeat answer structure Office.CustomXMLNode node = answersPart.SelectSingleNode("//oda:repeat[@qref='" + xp.questionID + "']"); if (node == null) { log.Error("no node for nested repeat " + xp.questionID); } string fromXPath = NodeToXPath.getXPath(node); log.Info("from: " + fromXPath); // Move it String nodeXML = node.XML; // No API to add a node! node.ParentNode.RemoveChild(node); destination.AppendChildSubtree(nodeXML); // 2. change the xpath for all questions re anything in the nested repeat // With a bit of luck, this will just work! string toXPath = NodeToXPath.getXPath(destination.LastChild); log.Info("to: " + toXPath); xpathChanges.Add(fromXPath, toXPath); } } // Now do the substitutions in the XPaths part - for all string xpaths = model.xpathsPart.XML; foreach (KeyValuePair<string, string> entry in xpathChanges) { xpaths = xpaths.Replace(entry.Key, entry.Value); } CustomXmlUtilities.replaceXmlDoc(model.xpathsPart, xpaths); //log.Info(model.xpathsPart.XML); //log.Info(answersPart.XML); // Now do the substitutions in the content control databindings // (Added 2012 12 16, since docx4j relies on the databinding element to do its bit) foreach (Word.ContentControl cc in wrappingRepeatCC.Range.ContentControls) //foreach (Word.ContentControl cc in Globals.ThisAddIn.Application.ActiveDocument.ContentControls) { // XMLMapping.IsMapped returns false here, // in cases where it is mapped! So avoid using that. // (Could be a defect elsewhere .. check for usage) if (cc.XMLMapping!=null && cc.XMLMapping.XPath!=null && cc.XMLMapping.PrefixMappings!=null) { foreach (KeyValuePair<string, string> entry in xpathChanges) { //log.Info("Comparing " + cc.XMLMapping.XPath + " with " + entry.Key); if (cc.XMLMapping.XPath.Equals(entry.Key)) { // matched, so replace cc.XMLMapping.SetMapping(entry.Value, cc.XMLMapping.PrefixMappings, answersPart); break; } } } } }