// InsertElement worker. Adds a TextElement to the tree. // If element is already in a tree, we remove it, do a deep copy of its content, // and insert that too. internal void InsertElementInternal(TextPointer startPosition, TextPointer endPosition, TextElement element) { TextTreeTextElementNode elementNode; int symbolOffset; int childSymbolCount; TextPointer startEdgePosition; TextPointer endEdgePosition; char[] elementText; ExtractChangeEventArgs extractChangeEventArgs; DependencyObject parentLogicalNode; bool newElementNode; int deltaCharCount; Invariant.Assert(!this.PlainTextOnly); Invariant.Assert(startPosition.TextContainer == this); Invariant.Assert(endPosition.TextContainer == this); DemandCreateText(); startPosition.SyncToTreeGeneration(); endPosition.SyncToTreeGeneration(); bool scopesExistingContent = startPosition.CompareTo(endPosition) != 0; BeforeAddChange(); // Remove element from any previous tree. // When called from a public method we already checked all the // illegal cases in CanInsertElementInternal. if (element.TextElementNode != null) { // This element is already in a tree. Remove it! bool sameTextContainer = (this == element.TextContainer); if (!sameTextContainer) { // This is a cross-tree extract. // We need to start a change block now, so that we can // raise a Changing event inside ExtractElementInternal // before raising the LogicalTree events below. // We'll make an EndChange call to wrap up below. element.TextContainer.BeginChange(); } bool exceptionThrown = true; try { // ExtractElementInternal will raise LogicalTree events which // could raise exceptions from external code. elementText = element.TextContainer.ExtractElementInternal(element, true /* deep */, out extractChangeEventArgs); exceptionThrown = false; } finally { if (exceptionThrown && !sameTextContainer) { // If an exception is thrown, make sure we close the // change block we opened above before unwinding. element.TextContainer.EndChange(); } } elementNode = element.TextElementNode; deltaCharCount = extractChangeEventArgs.ChildIMECharCount; if (sameTextContainer) { // Re-[....] the TextPointers in case we just extracted from this tree. startPosition.SyncToTreeGeneration(); endPosition.SyncToTreeGeneration(); // We must add the extract change now, before we move on to the insert. // (When !sameTextContainer we want to delay the notification in the extract // tree until the insert tree is in an accessible state, ie at the end of this method.) extractChangeEventArgs.AddChange(); // Don't re-raise the change below. extractChangeEventArgs = null; } newElementNode = false; } else { // Allocate a node in the tree to hold the element. elementText = null; elementNode = new TextTreeTextElementNode(); deltaCharCount = 0; newElementNode = true; extractChangeEventArgs = null; } parentLogicalNode = startPosition.GetLogicalTreeNode(); // Invalidate any TextElementCollection that depends on the parent. // Make sure we do that before raising any public events. TextElementCollectionHelper.MarkDirty(parentLogicalNode); // Link the TextElement to the TextElementNode. if (newElementNode) { elementNode.TextElement = element; element.TextElementNode = (TextTreeTextElementNode)elementNode; } // If the new element will become the parent of an old element, // the old element may become a firstIMEVisibleSibling. TextTreeTextElementNode newFirstIMEVisibleNode = null; int newFirstIMEVisibleNodeCharDelta = 0; if (scopesExistingContent) { newFirstIMEVisibleNode = startPosition.GetAdjacentTextElementNodeSibling(LogicalDirection.Forward); if (newFirstIMEVisibleNode != null) { newFirstIMEVisibleNodeCharDelta = -newFirstIMEVisibleNode.IMELeftEdgeCharCount; newFirstIMEVisibleNode.IMECharCount += newFirstIMEVisibleNodeCharDelta; } } // Attach the element node. childSymbolCount = InsertElementToSiblingTree(startPosition, endPosition, elementNode); // Add the edge char count to our delta. We couldn't get this before // because it depends on the position of the element in the tree. deltaCharCount += elementNode.IMELeftEdgeCharCount; TextTreeTextElementNode formerFirstIMEVisibleNode = null; int formerFirstIMEVisibleNodeCharDelta = 0; if (element.IsFirstIMEVisibleSibling && !scopesExistingContent) { formerFirstIMEVisibleNode = (TextTreeTextElementNode)elementNode.GetNextNode(); if (formerFirstIMEVisibleNode != null) { // The following node was the former first ime visible sibling. // It just moved, and gains an edge character. formerFirstIMEVisibleNodeCharDelta = formerFirstIMEVisibleNode.IMELeftEdgeCharCount; formerFirstIMEVisibleNode.IMECharCount += formerFirstIMEVisibleNodeCharDelta; } } // Ancester nodes gain the two edge symbols. UpdateContainerSymbolCount(elementNode.GetContainingNode(), /* symbolCount */ elementText == null ? 2 : elementText.Length, deltaCharCount + formerFirstIMEVisibleNodeCharDelta + newFirstIMEVisibleNodeCharDelta); symbolOffset = elementNode.GetSymbolOffset(this.Generation); if (newElementNode) { // Insert text to account for the element edges. TextTreeText.InsertElementEdges(_rootNode.RootTextBlock, symbolOffset, childSymbolCount); } else { // element already has an existing child, just copy over the corresponding text. TextTreeText.InsertText(_rootNode.RootTextBlock, symbolOffset, elementText); } NextGeneration(false /* deletedContent */); // Handle undo. TextTreeUndo.CreateInsertElementUndoUnit(this, symbolOffset, elementText != null /* deep */); // If we extracted the TextElement from another tree, raise that event now. // We can't raise this event any earlier, because prior to now _this_ tree // is in an invalid state and this tree could be referenced by a listener // to changes on the other tree. if (extractChangeEventArgs != null) { // Announce the extract from the old tree. // NB: we already Removed the element from the original logical tree with LogicalTreeHelper, // and did a BeginChange above. extractChangeEventArgs.AddChange(); extractChangeEventArgs.TextContainer.EndChange(); } // Raise the public event for the insert into this tree. // During document load we won't have listeners and we can save // an allocation on every insert. This can easily save 1000's of allocations during boot. if (this.HasListeners) { // startEdgePosition = new TextPointer(this, elementNode, ElementEdge.BeforeStart); if (childSymbolCount == 0 || elementText != null) { AddChange(startEdgePosition, elementText == null ? 2 : elementText.Length, deltaCharCount, PrecursorTextChangeType.ContentAdded); } else { endEdgePosition = new TextPointer(this, elementNode, ElementEdge.BeforeEnd); AddChange(startEdgePosition, endEdgePosition, elementNode.SymbolCount, elementNode.IMELeftEdgeCharCount, elementNode.IMECharCount - elementNode.IMELeftEdgeCharCount, PrecursorTextChangeType.ElementAdded, null, false); } if (formerFirstIMEVisibleNodeCharDelta != 0) { RaiseEventForFormerFirstIMEVisibleNode(formerFirstIMEVisibleNode); } if (newFirstIMEVisibleNodeCharDelta != 0) { RaiseEventForNewFirstIMEVisibleNode(newFirstIMEVisibleNode); } } // Insert the element into a Framework logical tree element.BeforeLogicalTreeChange(); try { LogicalTreeHelper.AddLogicalChild(parentLogicalNode, element); } finally { element.AfterLogicalTreeChange(); } // Reparent all children. // We only need to do this if we created a new element node. if (newElementNode) { ReparentLogicalChildren(elementNode, elementNode.TextElement, parentLogicalNode /* oldParent */); } // Notify the TextElement of a content change if it was moved to parent new content. This // can happen when Runs get merged. if (scopesExistingContent) { element.OnTextUpdated(); } }
// Validation for SetValue. private void ValidateSetValue(TextPointer position) { TextElement element; if (position.TextContainer != this) { throw new InvalidOperationException(SR.Get(SRID.NotInThisTree, "position")); } position.SyncToTreeGeneration(); element = position.Parent as TextElement; if (element == null) { throw new InvalidOperationException(SR.Get(SRID.NoElement)); } }
// The InsertText worker. Adds text to the tree at a specified position. // text is either a string or char[] to insert. internal void InsertTextInternal(TextPointer position, object text) { TextTreeTextNode textNode; SplayTreeNode containingNode; TextPointer originalPosition; int symbolOffset; int textLength; LogicalDirection direction; Invariant.Assert(text is string || text is char[], "Unexpected type for 'text' parameter!"); textLength = GetTextLength(text); if (textLength == 0) return; DemandCreateText(); position.SyncToTreeGeneration(); if (Invariant.Strict) { if (position.Node.SymbolCount == 0) { // We expect only TextTreeTextNodes ever have zero symbol counts. // This can happen in two cases: // // <TextNode referencedEdge=BeforeStart symbolCount=1+/> <TextNode referencedEdge=AfterEnd symbolCount=0/> // or // <TextNode referencedEdge=BeforeStart symbolCount=0/> <TextNode referencedEdge=AfterEnd symbolCount=1+/> // Invariant.Assert(position.Node is TextTreeTextNode); Invariant.Assert((position.Edge == ElementEdge.AfterEnd && position.Node.GetPreviousNode() is TextTreeTextNode && position.Node.GetPreviousNode().SymbolCount > 0) || (position.Edge == ElementEdge.BeforeStart && position.Node.GetNextNode() is TextTreeTextNode && position.Node.GetNextNode().SymbolCount > 0)); } } BeforeAddChange(); // During document load we won't have listeners and we can save // an allocation on every insert. This can easily save 1000's of allocations during boot. originalPosition = this.HasListeners ? new TextPointer(position, LogicalDirection.Backward) : null; // Find a bordering TextTreeTextNode, if any. // We know position already points to the current TextNode, if there is one, so // we can't append text to that node (it would disrespect position's gravity to do so). // So we either have to find a neighboring text node with no position references, or // create a new node. // Look for a bordering text node. if (position.Edge == ElementEdge.BeforeStart || position.Edge == ElementEdge.BeforeEnd) { direction = LogicalDirection.Backward; } else { direction = LogicalDirection.Forward; } textNode = position.GetAdjacentTextNodeSibling(direction); if (textNode != null) { // We can't use a text node that is already referred to by text positions. // Doing so could displace the positions, since they expect to remain at // the node edge no matter what happens. if ((direction == LogicalDirection.Backward && textNode.AfterEndReferenceCount) || (direction == LogicalDirection.Forward && textNode.BeforeStartReferenceCount)) { textNode = null; } } if (textNode == null) { // No text node available. Create and insert one. textNode = new TextTreeTextNode(); textNode.InsertAtPosition(position); containingNode = textNode.GetContainingNode(); } else { // We didn't insert a new node, so splay textNode to the root so // we don't invalidate any LeftSymbolCounts of ancestor nodes. textNode.Splay(); containingNode = textNode.ParentNode; } // Update the symbol counts. textNode.SymbolCount += textLength; // This simultaneously updates textNode.IMECharCount. UpdateContainerSymbolCount(containingNode, /* symbolCount */ textLength, /* charCount */ textLength); // Insert the raw text. symbolOffset = textNode.GetSymbolOffset(this.Generation); TextTreeText.InsertText(_rootNode.RootTextBlock, symbolOffset, text); // Handle undo. TextTreeUndo.CreateInsertUndoUnit(this, symbolOffset, textLength); // Announce the change. NextGeneration(false /* deletedContent */); AddChange(originalPosition, /* symbolCount */ textLength, /* charCount */ textLength, PrecursorTextChangeType.ContentAdded); // Notify the TextElement of a content change. TextElement textElement = position.Parent as TextElement; if (textElement != null) { textElement.OnTextUpdated(); } }
// DeleteContent worker. Removes content from the tree. internal void DeleteContentInternal(TextPointer startPosition, TextPointer endPosition) { TextTreeNode containingNode; int symbolCount; int charCount; TextTreeUndoUnit undoUnit; TextPointer deletePosition; startPosition.SyncToTreeGeneration(); endPosition.SyncToTreeGeneration(); if (startPosition.CompareTo(endPosition) == 0) return; BeforeAddChange(); undoUnit = TextTreeUndo.CreateDeleteContentUndoUnit(this, startPosition, endPosition); containingNode = startPosition.GetScopingNode(); // Invalidate any TextElementCollection that depends on the parent. // Make sure we do that before raising any public events. TextElementCollectionHelper.MarkDirty(containingNode.GetLogicalTreeNode()); int nextIMEVisibleNodeCharDelta = 0; TextTreeTextElementNode nextIMEVisibleNode = GetNextIMEVisibleNode(startPosition, endPosition); if (nextIMEVisibleNode != null) { // The node following the delete just became the first sibling. // This might affect its ime char count. nextIMEVisibleNodeCharDelta = -nextIMEVisibleNode.IMELeftEdgeCharCount; nextIMEVisibleNode.IMECharCount += nextIMEVisibleNodeCharDelta; } // First cut: remove all top-level TextElements and their chilren. // We need to put each TextElement in its own tree, so that any outside // references can still play with the TextElements safely. symbolCount = CutTopLevelLogicalNodes(containingNode, startPosition, endPosition, out charCount); // Cut what's left. int remainingCharCount; symbolCount += DeleteContentFromSiblingTree(containingNode, startPosition, endPosition, nextIMEVisibleNodeCharDelta != 0, out remainingCharCount); charCount += remainingCharCount; Invariant.Assert(symbolCount > 0); if (undoUnit != null) { undoUnit.SetTreeHashCode(); } // Public tree event. deletePosition = new TextPointer(startPosition, LogicalDirection.Forward); AddChange(deletePosition, symbolCount, charCount, PrecursorTextChangeType.ContentRemoved); if (nextIMEVisibleNodeCharDelta != 0) { RaiseEventForNewFirstIMEVisibleNode(nextIMEVisibleNode); } }
// Does a deep extract of all top-level TextElements between two positions. // Returns the combined symbol count of all extracted elements. // Each extracted element (and its children) are moved into a private tree. // This insures that outside references to the TextElement can still use // the TextElements freely, inserting or removing content, etc. // // Also calls AddLogicalChild on any top-level UIElements encountered. private int CutTopLevelLogicalNodes(TextTreeNode containingNode, TextPointer startPosition, TextPointer endPosition, out int charCount) { SplayTreeNode node; SplayTreeNode nextNode; SplayTreeNode stopNode; TextTreeTextElementNode elementNode; TextTreeObjectNode uiElementNode; char[] elementText; int symbolCount; TextContainer tree; TextPointer newTreeStart; DependencyObject logicalParent; object currentLogicalChild; Invariant.Assert(startPosition.GetScopingNode() == endPosition.GetScopingNode(), "startPosition/endPosition not in same sibling tree!"); node = startPosition.GetAdjacentSiblingNode(LogicalDirection.Forward); stopNode = endPosition.GetAdjacentSiblingNode(LogicalDirection.Forward); symbolCount = 0; charCount = 0; logicalParent = containingNode.GetLogicalTreeNode(); while (node != stopNode) { currentLogicalChild = null; // Get the next node now, before we extract any TextElementNodes. nextNode = node.GetNextNode(); elementNode = node as TextTreeTextElementNode; if (elementNode != null) { // Grab the IMECharCount before we modify the node. // This value depends on the node's current context. int imeCharCountInOriginalContainer = elementNode.IMECharCount; // Cut and record the matching symbols. elementText = TextTreeText.CutText(_rootNode.RootTextBlock, elementNode.GetSymbolOffset(this.Generation), elementNode.SymbolCount); // Rip the element out of its sibling tree. // textElementNode.TextElement's TextElementNode will be updated // with a deep copy of all contained nodes. We need a deep copy // to ensure the new element/tree has no TextPointer references. ExtractElementFromSiblingTree(containingNode, elementNode, true /* deep */); // Assert that the TextElement now points to a new TextElementNode, not the original one. Invariant.Assert(elementNode.TextElement.TextElementNode != elementNode); // We want to start referring to the copied node, update elementNode. elementNode = elementNode.TextElement.TextElementNode; UpdateContainerSymbolCount(containingNode, -elementNode.SymbolCount, -imeCharCountInOriginalContainer); NextGeneration(true /* deletedContent */); // Stick it in a private tree so it's safe for the outside world to play with. tree = new TextContainer(null, false /* plainTextOnly */); newTreeStart = tree.Start; tree.InsertElementToSiblingTree(newTreeStart, newTreeStart, elementNode); Invariant.Assert(elementText.Length == elementNode.SymbolCount); tree.UpdateContainerSymbolCount(elementNode.GetContainingNode(), elementNode.SymbolCount, elementNode.IMECharCount); tree.DemandCreateText(); TextTreeText.InsertText(tree.RootTextBlock, 1 /* symbolOffset */, elementText); tree.NextGeneration(false /* deletedContent */); currentLogicalChild = elementNode.TextElement; // Keep a running total of how many symbols we've removed. symbolCount += elementNode.SymbolCount; charCount += imeCharCountInOriginalContainer; } else { uiElementNode = node as TextTreeObjectNode; if (uiElementNode != null) { currentLogicalChild = uiElementNode.EmbeddedElement; } } // Remove the child from the logical tree LogicalTreeHelper.RemoveLogicalChild(logicalParent, currentLogicalChild); node = nextNode; } if (symbolCount > 0) { startPosition.SyncToTreeGeneration(); endPosition.SyncToTreeGeneration(); } return symbolCount; }
// InsertEmbeddedObject worker. Adds a UIElement to the tree. internal void InsertEmbeddedObjectInternal(TextPointer position, DependencyObject embeddedObject) { TextTreeNode objectNode; int symbolOffset; DependencyObject parentLogicalNode; TextPointer insertPosition; Invariant.Assert(!this.PlainTextOnly); DemandCreateText(); position.SyncToTreeGeneration(); BeforeAddChange(); parentLogicalNode = position.GetLogicalTreeNode(); // Insert a node. objectNode = new TextTreeObjectNode(embeddedObject); objectNode.InsertAtPosition(position); // Update the symbol count. UpdateContainerSymbolCount(objectNode.GetContainingNode(), objectNode.SymbolCount, objectNode.IMECharCount); // Insert the corresponding text. symbolOffset = objectNode.GetSymbolOffset(this.Generation); TextTreeText.InsertObject(_rootNode.RootTextBlock, symbolOffset); NextGeneration(false /* deletedContent */); // Handle undo. TextTreeUndo.CreateInsertUndoUnit(this, symbolOffset, 1); // Tell parent to update Logical Tree LogicalTreeHelper.AddLogicalChild(parentLogicalNode, embeddedObject); // Raise the public event. // During document load we won't have listeners and we can save // an allocation on every insert. This can easily save 1000's of allocations during boot. if (this.HasListeners) { insertPosition = new TextPointer(this, objectNode, ElementEdge.BeforeStart); AddChange(insertPosition, 1, 1, PrecursorTextChangeType.ContentAdded); } }
/// <summary> /// Returns the distance between this TextPointer and another. /// </summary> /// <param name="position"> /// TextPointer to compare. /// </param> /// <exception cref="System.ArgumentException"> /// Throws an ArgumentException if the TextPointer position is not /// positioned within the same document as this TextPointer. /// </exception> /// <returns> /// <para>The return value will be negative if the TextPointer position /// preceeds this TextPointer, zero if the two TextPointers /// are equally positioned, or positive if position follows this /// TextPointer.</para> /// </returns> /// <remarks> /// <para>The distance is represented as a number of "symbols" /// between these two pointers.</para> /// <para>Each opening and each closing tag of any TextElement /// is considered as one symbol. So an empty TextElement contributes /// two symbols - one for each of tags.</para> /// <para>UIElement placed within InlineUIContainer or BlockUIContainer /// represented as one symbol - independently of how complex /// is its content. Even if the UIElement contains or is a /// text container it is treated as atomic entity - single symbol. /// This may be confusing especially if you do not pay /// muchy attention to a difference between the <see cref="TextElement"/> /// the <see cref="UIElement"/> class.</para> /// <para>Each 16-bit unicode character inside a <see cref="Run"/> element /// is considered as one symbol.</para> /// <para>For instance, for the following xaml: /// <Run>abc</Run><InlineUIContainer><Button>OK</Button></InlineUIContainer> /// the offset from itw content start to content end will be 8 - /// one for each of: (1) Run start, (2) "a", (3) "b", (4) "c", (5) Run end, (6) InlineUIContainer start, /// (7) whole Button element, (8) InlineUIContainer end. Note that <c>Button</c> /// element considered as one symbol even though it is represented /// by two tags and two characters.</para> /// </remarks> /// <example> /// <para>In this example we show how to use TextPointer offsets for /// persisting positional information. Assuming that the content of /// a RichTextBox is not changed between calls of /// GetPersistedSelection and RestoreSelectionFromPersistedRange /// methods, the selection will be restored to its original state.</para> /// <code> /// struct PersistedTextRange { int Start; int End; } /// /// PersistedTextRange GetPersistedSelection(RichTextBox richTextBox) /// { /// PersistedTextRange persistedSelection; /// /// TextPointer contentStart = richTextBox.Document.ContentStart; /// persistedSelection.Start = contentStart.GetOffsetToPosition(richTextBox.Selection.Start); /// persistedSelection.End = contentStart.GetOffsetToPosition(richTextBox.Selection.End); /// /// return persistedSelection; /// } /// /// RestoreSelectionFromPersistedRange(RichTextBox richTextBox, PersistedTextRange persistedRange) /// { /// TextPointer contentStart = richTextBox.Document.ContentStart; /// /// richTextBox.Selection.Select( /// contentStart.GetPositionAtOffset(persistedRange.Start), /// contentStart.GetPositionAtOffset(persistedRange.End)); /// } /// /// </code> /// </example> public int GetOffsetToPosition(TextPointer position) { _tree.EmptyDeadPositionList(); ValidationHelper.VerifyPosition(_tree, position); SyncToTreeGeneration(); position.SyncToTreeGeneration(); return (position.GetSymbolOffset() - GetSymbolOffset()); }
/// <summary> /// Compares positions of this TextPointer with another TextPointer. /// </summary> /// <param name="position"> /// The TextPointer to compare with. /// </param> /// <returns> /// Less than zero: this TextPointer preceeds position. /// Zero: this TextPointer is at the same location as position. /// Greater than zero: this TextPointer follows position. /// </returns> /// <exception cref="System.ArgumentException"> /// Throws ArgumentException if position does not belong to the same /// text container as this TextPointer (you can use <see cref="TextPointer.IsInSameDocument"/> /// method to detect whether comparison is possible). /// </exception> public int CompareTo(TextPointer position) { int offsetThis; int offsetPosition; int result; _tree.EmptyDeadPositionList(); ValidationHelper.VerifyPosition(_tree, position); SyncToTreeGeneration(); position.SyncToTreeGeneration(); offsetThis = GetSymbolOffset(); offsetPosition = position.GetSymbolOffset(); if (offsetThis < offsetPosition) { result = -1; } else if (offsetThis > offsetPosition) { result = +1; } else { result = 0; } return result; }
//------------------------------------------------------ // // Private Methods // //------------------------------------------------------ #region Private Methods // Called by the TextPointer ctor. Initializes this instance. private void InitializeOffset(TextPointer position, int distance, LogicalDirection direction) { SplayTreeNode node; ElementEdge edge; int offset; bool isCaretUnitBoundaryCacheValid; // We MUST [....] to the current tree, otherwise we could addref // an orphaned node, resulting in a future unmatched release... // Ref counts on orphaned nodes are only considered at the time // of removal, not afterwards. position.SyncToTreeGeneration(); if (distance != 0) { offset = position.GetSymbolOffset() + distance; if (offset < 1 || offset > position.TextContainer.InternalSymbolCount - 1) { throw new ArgumentException(SR.Get(SRID.BadDistance)); } position.TextContainer.GetNodeAndEdgeAtOffset(offset, out node, out edge); isCaretUnitBoundaryCacheValid = false; } else { node = position.Node; edge = position.Edge; isCaretUnitBoundaryCacheValid = position.IsCaretUnitBoundaryCacheValid; } Initialize(position.TextContainer, (TextTreeNode)node, edge, direction, position.TextContainer.PositionGeneration, position.CaretUnitBoundaryCache, isCaretUnitBoundaryCacheValid, position._layoutGeneration); }
/// <summary> /// Moves this TextPointer to another TextPointer's position. /// </summary> /// <param name="textPosition"> /// Position to move to. /// </param> /// <exception cref="System.ArgumentException"> /// Throws an ArgumentException if textPosition is not /// positioned within the same document. /// </exception> /// <exception cref="System.InvalidOperationException"> /// Throws an InvalidOperationException if this TextPointer's IsFrozen /// property is set true. Frozen TextPointers may not be repositioned. /// </exception> internal void MoveToPosition(TextPointer textPosition) { ValidationHelper.VerifyPosition(_tree, textPosition); VerifyNotFrozen(); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); textPosition.SyncToTreeGeneration(); MoveToNode(_tree, textPosition.Node, textPosition.Edge); }