Code:
/ Net / Net / 3.5.50727.3053 / DEVDIV / depot / DevDiv / releases / Orcas / SP / wpf / src / Framework / System / Windows / Documents / TextPointer.cs / 1 / TextPointer.cs
//---------------------------------------------------------------------------- // // File: TextPointer.cs // // Copyright (C) Microsoft Corporation. All rights reserved. // // Description: TextPointer object representing a location in formatted text. // //--------------------------------------------------------------------------- namespace System.Windows.Documents { using System; using MS.Internal; using System.Threading; using System.Windows; using System.Windows.Media; using System.Collections; ////// Represents a location in a formatted text content. /// ////// ///In Avalon formatted text can be contained in elements such as /// ///, , . /// We will refer to these elements as to "text containers". Using the properties and the methods of the TextPointer object, you can: ///a) Find out what kind of content is in forward or in backward directions from its position; ///b) Get a ///scoping or adjacent a position of this TextPointer; c) Get characters preceding or following the TextPointer when it is positioned within text run - ///element; d) Insert characters in a position where the TextPointer is located; ///e) Inspect line layout structure by finding line boundary positions; ///f) Perform visual hit-testing by translating back and forth positions of TextPointer objects into Point objects representing coordinates; ///g) Create an instance of a ///object and use it for formatting, copying, pasting and other editing operations; /// Positions in formatted document where TextPointer objects can be located /// are places between characters and element tags. ///As you edit a document, TextPointer objects do not move relative to their surrounding text. /// That is, if text is inserted before a text pointer, then the offset of the pointer /// from start position of a text container is incremented to reflect its new location /// further down in the document (offsets between text pointers can be calculated by /// a ///method). If multiple TextPointer objects are located at the same position and a text /// is inserted into this position, then the new characters and structural tags are /// to the right or to the left of all of the TextPointer objects depending on their /// ///property. Class ///is an enum specifying what kind of /// content can be found in immediate vicility of a TextPointer. The kinds include /// None for text container boundaries,ElementStart andElementEnd /// for opening and closing tags ofelements, EmbeddedElement /// for UIElements inserted in text as atomic objects. The kind of context can be /// get from a TextPointer using method. TextPointer objects are immutable - they cannot be repositioned in text content /// by any means; and their LogicalDirection property cannot be changed. The context /// around a TextPointer can be changed though, as a result of text editing. /// For instance, when text around a TextPointer is deleted, the TextPointer /// will appear in a new context - in a content remaining after deletion. ///To traverse a document content you can use a bunch of ///Get*Position /// methods -, , etc. /// TextPointer class does not have public constructors. /// The only way to get an instance of the TextPointer class is by /// using properties or methods of other objects: /// ///and , etc. /// and , /// and , etc. /// TextPointer objects can be also produced from other TextPointer objects /// using traversal methods like , /// , , /// etc. TextPointer can be also gotten from a visual coordinate via /// methods like . /// We use a concept of "insertion positions" in association with TextPointer objects, /// which is a key for editor behavior and for various api members. ///When caret travels over text content it can stop only at particular positions, /// skipping all non-appropriate ones. Positions appropriate for caret stopping are called /// "insertion positions". Boundary positions of ///and /// objects are always forcefully set to insertion positions, even if you pass /// arbitrary position in TextRange constructor or method. From TextPointer located at arbitrary (possibly non-insertion) position, you /// can get a TextPointer located at a nearest insertion position by calling /// ///method. To get from one insertion position to another /// you can use method. /// // // Internal comments: // // TextContainer's implementation of the Text OM ITextPointer interface. // // TextPointers represent locations in the TextContainer. They point to a // node/edge pair where operations like insert/remove/gettext take place. // // TextPointers have a property called LogicalDirection, that specifies where // they fall if content is insert at their position. We track LogicalDirection // implicitly: forward direction means the position is always at // BeforeStart/BeforeEnd edges, backward direction the reverse. // // TextPointers are guaranteed to stick with their nodes across editing // operations. For inserts, this happens automatically. However, if the // node a TextPointer points to is removed from the tree, it is expected // that a TextPointer will follow its LogicalDirection to the closest neighbor // node still living in the tree. // // Since we don't store references to TextPointers in the tree itself, // we have to wait until a method on the TextPointer is called, then // check if the position's node is still in the tree. This operation is // called synchronization, and the core method is SyncToTreeGeneration. // // SyncToTreeGeneration must be called on every public entry point before // attempting to use the TextPointer. // // Since positions always point to node/edge pairs, if we want to allocate // a position that references a character not on a node edge, we must split // the text node at the character position. If we did no other work, the // tree could become extremely fragmented, with a text node allocated for // each character. To keep the tree from fragmenting, positions ref count // the nodes they occupy. We do some gymnastics using a finalizer on // TextPointer, adding unreferenced positions to a list we check // periodically in all public TextContainer methods. Dead positions decrement // their nodes' ref counts, and a text node whose ref count drops to zero will // attempt to merge with neighbors. public class TextPointer : ContentPosition, ITextPointer { //----------------------------------------------------- // // Constructors // //----------------------------------------------------- #region Constructors ///Example 0. This code shows how to get an instance of a TextPointer. /// As TextPointer does not have any public constructors, the only way /// of getting a TextPointer is to use a property or method of other object. /// This example ContentStart and ContentEnd properties of main text containers, /// create a TextRange for the whole content of each of them and applies /// Bold formatting to it. ////// void BoldAll(FlowDocument flowDocument, TextFlow textFlow, TextBlock textBlock, RichTextBox richTextBox) /// { /// allContent = new TextRange(flowDocument.ContentStart, flowDocument.ContentEnd); /// allContent.ApplyPropertyValue(TextElement.FontWeightProperty, FontWeights.Bold); /// /// allContent = new TextRange(textFlow.ContentStart, textFlow.ContentEnd); /// allContent.ApplyPropertyValue(TextElement.FontWeightProperty, FontWeights.Bold); /// /// allContent = new TextRange(textBlock.ContentStart, textFlow.ContentEnd); /// allContent.ApplyPropertyValue(TextElement.FontWeightProperty, FontWeights.Bold); /// /// // Note that RichTextBox does not have ContentStart/ContentEnd properties, /// // we use its Document property to get to FlowDocument contained within. /// TextRange allContent = new TextRange(richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd); /// allContent.ApplyPropertyValue(TextElement.FontWeightProperty, FontWeights.Bold); /// } ///
///Example 1. This code shows how to use TextPointer for finding a first Run element /// from a particular position in forard direction. ////// Run FindNextRun(TextPointer position) /// { /// // Traverse content in forward direction until the position is /// // immediately after opening tag of a Run element. /// while (position != null && /// !(position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart /// && /// position.Parent is Run)) /// { /// position = position.GetNextContextPosition(LogicalDirection.Forward); /// } /// /// // Return a result /// return position == null ? null : position.Parent as Run; /// } ///
///Example 2. This code shows how to use TextPointer for finding a particular /// word in text content. This is a simplistic "find" algorithm, not smart enough /// for international issues and for words crossing formatting boundaries. ////// TextPointer FindWord(TextPointer position, string word) /// { /// while (position != null) /// { /// if (position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text) /// { /// string textRun = position.GetTextInRun(LogicalDirection.Forward); /// int indexInRun = textRun.IndexOf(word); /// if (indexInRun >= 0) /// { /// position = position.GetPositionAtOffset(indexInRun); /// break; /// } /// } /// else /// { /// position = position.GetNextContextPosition(LogicalDirection.Forward); /// } /// } /// /// return position; // will be null, if a word is not found. /// } ///
///Example 3. This code shows how to enumerate and count all Paragraphs in a given TextRange. ////// int GetParagraphCount(TextRange range) /// { /// int paragraphCount = 0; /// TextPointer position = range.Start; /// /// while (position != null && position.CompareTo(range.End) < 0) /// { /// if (position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && /// position.Parent is Paragraph) /// { /// // Just entered a paragraph. /// paragraphCount ++; /// /// // Jump over it. /// // Schema does not allow nested paragraphs, so we will not miss any. /// position = ((Paragraph)position.Parent).ElementEnd; /// } /// else /// { /// position = position.GetNextContextPosition(LogicalDirection.Forward); /// } /// } /// /// return paragraphCount; /// } ///
///Example 4. Idenifying whether the document is empty. The document appearing as empty /// in RichTextBox actually contains a Paragraph element with a Run child in it. So checking /// a document emptiness is a bit tricky task. In the following example we will utilize /// the insertion positions as the most natural mechanism for getting to character part or text content. ////// bool IsRichTextBoxEmpty(RichTextBox richTextBox) /// { /// FlowDocument document = richTextBox.Document; // get a document contained in a RichTextBox /// /// TextPointer normalizedStart = document.ContentStart.GetInsertionPosition(LogicalDirection.Forward); /// TextPointer normalizedEnd = document.ContentEnd.GetInsertionPosition(LogicalDirection.Backward); /// /// // The character content is empty if normalized start and end pointers are at the same position /// bool isEmpty = normalizedStart.CompareTo(normalizedEnd) == 0; /// /// return isEmpty; /// } ///
////// Creates a new instance of TextPointer object. /// /// /// TextPointer from which initial properties and location are initialized. /// ////// New TextPointers always have their IsFrozen property set to false, /// regardless of the state of the position parameter. Otherwise the /// new TextPointer instance is identical to the position parameter. /// internal TextPointer(TextPointer textPointer) { if (textPointer == null) { throw new ArgumentNullException("textPointer"); } InitializeOffset(textPointer, 0, textPointer.GetGravityInternal()); } // Creates a new TextPointer instance. internal TextPointer(TextPointer position, int offset) { if (position == null) { throw new ArgumentNullException("position"); } InitializeOffset(position, offset, position.GetGravityInternal()); } // Creates a new TextPointer instance. internal TextPointer(TextPointer position, LogicalDirection direction) { InitializeOffset(position, 0, direction); } // Creates a new TextPointer instance. internal TextPointer(TextPointer position, int offset, LogicalDirection direction) { InitializeOffset(position, offset, direction); } // Creates a new TextPointer instance. internal TextPointer(TextContainer textContainer, int offset, LogicalDirection direction) { SplayTreeNode node; ElementEdge edge; if (offset < 1 || offset > textContainer.InternalSymbolCount - 1) { throw new ArgumentException(SR.Get(SRID.BadDistance)); } textContainer.GetNodeAndEdgeAtOffset(offset, out node, out edge); Initialize(textContainer, (TextTreeNode)node, edge, direction, textContainer.PositionGeneration, false, false, textContainer.LayoutGeneration); } // Creates a new TextPointer instance. internal TextPointer(TextContainer tree, TextTreeNode node, ElementEdge edge) { Initialize(tree, node, edge, LogicalDirection.Forward, tree.PositionGeneration, false, false, tree.LayoutGeneration); } // Creates a new TextPointer instance. internal TextPointer(TextContainer tree, TextTreeNode node, ElementEdge edge, LogicalDirection direction) { Initialize(tree, node, edge, direction, tree.PositionGeneration, false, false, tree.LayoutGeneration); } // Constructor equivalent to ITextPointer.CreatePointer internal TextPointer CreatePointer() { return new TextPointer(this); } // Constructor equivalent to ITextPointer.CreatePointer internal TextPointer CreatePointer(LogicalDirection gravity) { return new TextPointer(this, gravity); } #if REFCOUNT_DEAD_TEXTPOINTERS // *** This code removed *** // The TextContainer originally was designed to ref count TextPointer references // to TextTreeNodes. When a TextPointer is created, it addrefs its node. // When moved, it addrefs the destination and decrements the old position. // When finalized, it would decrement its final TextTreeNode. // // There are two problems with this code: // - The GC will null out managed fields occasionally. This means we simply // cannot use a finalizer. // - We don't really know/can't depend on how expensive it is to use the GC, // and the whole scheme is an attempt at perf optimization. // // The current state of the code is that we still ref count on create and // move, but we've disabled the finalizer so TextPointers will reference // their final nodes "forever". This leads to fragmentation: because // we split TextTreeTextNodes as TextPointer reference individual // characters. However, there's an upper bound on the fragmentation // (we can't have more nodes than characters) and in practice no one // walks documents character by character. // // So, until we identify a specific perf problem, we're not attempting // to ressurect this code. // // If ever do identify fragmentation as a problem worth solving, // we can already think of at least three possible approaches: // // 1. Keep the existing logic, but instead of using a finalizer, // store an array of WeakReferences on each node (usually null). // Periodically check the array, pruning WeakReferences with // null Targets. // 2. As above, but introduce a TextPointerNode instead of hanging // arrays off other nodes. // 3. Keep a static array of TextContainers in memory, ref counted // by TextPointers. Restore the TextPointer finalizer, and in // addition to decrementing the node ref count, decrement the // TextContainer ref count. // This method adds the position to a list of "dead" positions (no // external references) that will be examined later to decrement // reference counts on nodes, and ultimately merge text nodes. // // It's important here that we don't do anything complicated // that might block the finalizer thread or cause too much // contention and hurt perf. The same goes for code in // TextContainer.EmptyDeadPositionList that also uses the lock. ////// ~TextPointer() { ArrayList deadPositionList; deadPositionList = _tree.DeadPositionList; lock (deadPositionList) { deadPositionList.Add(this); } } #endif // REFCOUNT_DEAD_TEXTPOINTERS #endregion Constructors //------------------------------------------------------ // // Public Methods // //----------------------------------------------------- #region Public Methods ////// Returns true if this TextPointer is positioned within the same /// text containner as another TextPointer. /// /// /// TextPointer to compare. /// ////// ///TextPointer objects positioned in different containers cannot /// participate in any operations dealing with several pointers. /// For instance, TextPointer objects from two different text containers /// cannot be compared with each other (by calling the method ///). The purpose of this method is to test whether two TextPointer /// objects belong to the same text container or not. ///Formatted text can be contained within one these elements in Avalon: /// ///, , . /// We refer to them as to "text containers". Note, that if one text container is nested within another /// TextPointer objects positioned within a nested text container /// are not considered as belonging to the enclosing one. ////// public bool IsInSameDocument(TextPointer textPosition) { if (textPosition == null) { throw new ArgumentNullException("textPosition"); } _tree.EmptyDeadPositionList(); return (this.TextContainer == textPosition.TextContainer); } ///Example 1. This example shows how to check whether a given TextPointer /// is positioned between two other TextPointer objects - in a situation /// when there is no guarantee that all three positions belong to /// the same text container ////// bool IsPositionContainedBetween(TextPointer test, TextPointer start, TextPointer end) /// { /// if (!test.IsInSameDocument(start) || !test.IsInSameDocument(end)) /// { /// return false; /// } /// return start.CompareTo(test) <= 0 && test.CompareTo(end) <= 0; /// } ///
////// Compares positions of this TextPointer with another TextPointer. /// /// /// The TextPointer to compare with. /// ////// Less than zero: this TextPointer preceeds position. /// Zero: this TextPointer is at the same location as position. /// Greater than zero: this TextPointer follows position. /// ////// Throws ArgumentException if position does not belong to the same /// text container as this TextPointer (you can use 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; } ////// method to detect whether comparison is possible). /// /// Returns the type of content to one side of this TextPointer. /// /// /// Direction to query. /// ////// ///Returns ///if this TextPointer /// is positioned at the beginning of a text container and the requested direction /// is , or if it is positioned /// at the end of a text container and the requested direction is /// . Returns ///if the TextPointer /// has an openenig tag of some of TextElements in the requested direction. Returns ///if the TextPointer /// has a closing tag of some of TextElements in the requested direction. Returns ///if the TextPointer /// is positioned within element and has some non-emty sequence of characters /// in requested direction. Returns ///is the TextPointer /// is positioned within or /// element and has as atomic symbol in a requested direction. /// public TextPointerContext GetPointerContext(LogicalDirection direction) { ValidationHelper.VerifyDirection(direction, "direction"); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); return (direction == LogicalDirection.Forward) ? GetPointerContextForward(_node, this.Edge) : GetPointerContextBackward(_node, this.Edge); } ///This example shows how to use ///GetPointerContext method in text content /// traversal algorithms. It implements an algorithm calculating a balanse of /// opening and closing tags between two TextPointer positions (each opening tag /// counted as +1, while a closing one as -1)./// int GetElementTagBalance(TextPointer start, TextPointer end) /// { /// int balanse = 0; /// /// while (start != null && start.CompareTo(end) < 0) /// { /// TextPointerContext forwardContext = start.GetPointerContext(LogicalDirection.Forward); /// /// if (forwardContext == TextPointerContext.ElementStart) /// { /// balanse++; /// } /// else if (forwardContext == TextPointerContext.ElementEnd) /// { /// balanse--; /// } /// start = start.GetNextContextPosition(LogicalDirection.Forward); /// } /// /// return balanse; /// } ///
////// Returns the count of Unicode characters between this TextPointer and the /// edge of an element in the given direction. /// /// /// Direction to query. /// ////// If the TetPointer is positioned not inside a public int GetTextRunLength(LogicalDirection direction) { ValidationHelper.VerifyDirection(direction, "direction"); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); int count = 0; // Combine adjacent text nodes into a single run. // This isn't just a perf optimization. Because text positions // split text nodes, if we just returned a single node's text // callers would see strange side effects where position.GetTextLength() != // position.GetText if a position is moved between the calls. if (_tree.PlainTextOnly) { // Optimize for TextBox, which only ever contains (sometimes // very large quantities of) text nodes. Invariant.Assert(this.GetScopingNode() is TextTreeRootNode); if (direction == LogicalDirection.Forward) { count = _tree.InternalSymbolCount - this.GetSymbolOffset() - 1; } else { count = this.GetSymbolOffset() - 1; } } else { TextTreeNode textNode = GetAdjacentTextNodeSibling(direction); while (textNode != null) { count += textNode.SymbolCount; textNode = ((direction == LogicalDirection.Forward) ? textNode.GetNextNode() : textNode.GetPreviousNode()) as TextTreeTextNode; } } return count; } ///element, /// then the method always returns zero. /// /// Returns the distance between this TextPointer and another. /// /// /// TextPointer to compare. /// ////// Throws an ArgumentException if the TextPointer position is not /// positioned within the same document as this TextPointer. /// ////// ///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. ////// ///The distance is represented as a number of "symbols" /// between these two pointers. ///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. ///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 ////// the class. Each 16-bit unicode character inside a ///element /// is considered as one symbol. 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 ///Button /// element considered as one symbol even though it is represented /// by two tags and two characters./// public int GetOffsetToPosition(TextPointer position) { _tree.EmptyDeadPositionList(); ValidationHelper.VerifyPosition(_tree, position); SyncToTreeGeneration(); position.SyncToTreeGeneration(); return (position.GetSymbolOffset() - GetSymbolOffset()); } ///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. ////// 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)); /// } /// ///
////// Returns text bordering this TextPointer from one side or another. /// /// /// Direction to query. /// ////// See GetTextInRun(direction, textBuffer, startIndex, count) method /// remarks for semantics of the returned text. /// ////// public string GetTextInRun(LogicalDirection direction) { ValidationHelper.VerifyDirection(direction, "direction"); return TextPointerBase.GetTextInRun(this, direction); } ///This is an example of simplistic plain text converter. /// This algorithm produces a string concatenating all text runs /// between two TextPointers. ///Note that this is really simplistic algorithm. You sould use /// ///property for more sophisticated /// plain text conversion. /// string GetPlainText(TextPointer start, TextPointer end) /// { /// StringBuilder buffer = new StringBuilder(); /// /// while (start != null && start.CompareTo(end) < end) /// { /// if (start.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text) /// { /// // Check if this text run reaches beyond the end position /// // and trancate the string if needed. /// string textRun = start.GetTextInRum(LogicalDirection.Forward); /// if (textRun.Length > start.GetOffsetToPosition(end)) /// { /// textRun = textRun.Substring(0, start.GetOffsetToPosition(end)); /// } /// /// // Add characters from this text run to output buffer. /// buffer.Add(textRun); /// } /// /// start = start.GetNextContextPosition(LogicalDirection.Forward); /// // Note that for text run this method skips the whole run, not just one character. /// } /// return buffer.ToString(); /// } ///
////// Copies characters bordering this TextPointer into a caller supplied char array. /// /// /// Direction to query. /// /// /// Buffer into which chars are copied. /// /// /// Index within the textBuffer array at which the copy is started. /// /// /// The maximum number of characters to copy. Must be less than /// or equal to a (textBuffer.Length - startIndex ). /// ////// The count of chars actually copied. /// ////// Is thrown in the following cases: (a) when ///startIndex is less than zero, /// (b) whenstartIndex is greater thantextBuffer.Length , /// (c) whencount is less than zero, (d) whencount /// is greater than size available for copying (textBuffer.Length - startIndex ). ////// This method only returns uninterrupted runs of text -- no text will /// be returned if any symbol type other than text borders this /// TextPointer in the specified direction. Similarly, text will only /// be returned up to the next non-text symbol. /// public int GetTextInRun(LogicalDirection direction, char[] textBuffer, int startIndex, int count) { TextTreeTextNode textNode; ValidationHelper.VerifyDirection(direction, "direction"); SyncToTreeGeneration(); textNode = GetAdjacentTextNodeSibling(direction); return textNode == null ? 0 : GetTextInRun(_tree, GetSymbolOffset(), textNode, -1, direction, textBuffer, startIndex, count); } ////// Returns an element represented by a symbol, if any, bordering /// this TextPointer in the specified direction. /// /// /// Direction to query. /// ////// The element if its opening or closing tag exists /// in a specified direction. Otherwize returns null. /// ////// public DependencyObject GetAdjacentElement(LogicalDirection direction) { ValidationHelper.VerifyDirection(direction, "direction"); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); return GetAdjacentElement(_node, this.Edge, direction); } ///The returned element may be both a ////// and a . /// object will be returned when /// this TextPointer is located before or after of either opening /// or closing tag in appropriate direction. /// object can be returned only when /// the pointer is located outside its opening or closing tag - within /// or . /// Returns a TextPointer at a new position by a specified symbol /// count. /// /// /// Number of symbols to advance. offset may be negative, in which /// case the TextPointer is moved backwards. /// ////// TextPointer located at requested position in case if requested position /// does exist, otherwize returns null. LogicalDirection of the TextPointer /// returned is the same as of this TexPointer. /// ////// ///This method, like all other TextPointer methods, defines a symbol /// as one of: ///- 16 bit Unicode character. ///- opening or closing tag of a ///. - the whole ///as atomic embedded object. /// public TextPointer GetPositionAtOffset(int offset) { return GetPositionAtOffset(offset, this.LogicalDirection); } ///This example shows how to use this method for creating TextPointers /// from a persisted index-based position representation. /// The first method returns a integer offset of a TextPointer /// from the beginning of a Paragraph. The second method re-creates /// a pointer from an integer ofset at the same relative position. ////// int GetPersistedPositionRelativeToParagraph(TextPointer position) /// { /// Paragraph paragraph = position.Paragraph; /// /// if (paragraph == null) /// { /// return 0; // Some positions may be not within any Paragraph, /// // so we need to return something; or throw exception. /// } /// /// return paragraph.ContentStart.GetOffsetToPosition(position); /// } /// /// int GetTextPointerRelativeToParagraph(Paragraph paragraph, int persistedPositionRelativeToParagraph) /// { /// // Check whether persisted position is still within this paragraph /// if (persistedPositionRelativeToParagraph > /// paragraph.ContentStart.GetOffsetToPosition(paragraph.ContentEnd)) /// { /// // the index is beyond the paragraph end. Return the farthest position within the paragraph. /// return paragraph.ContentEnd; /// } /// /// return paragraph.ContentStart.GetPositionAtOffset(persistedPositionRelativeToParagraph); /// } ///
////// Returns a TextPointer at a new position by a specified symbol /// count. /// /// /// Number of symbols to advance. offset may be negative, in which /// case the TextPointer is moved backwards. /// /// /// LogicalDirection desired for a returned TextPointer. /// ////// TextPointer located at requested position in case if requested position /// does exist, otherwize returns null. LogicalDirection of the TextPointer /// returned is as specified by a ///. /// /// public TextPointer GetPositionAtOffset(int offset, LogicalDirection direction) { TextPointer position = new TextPointer(this, direction); int actualCount = position.MoveByOffset(offset); if (actualCount == offset) { position.Freeze(); return position; } else { return null; } } ///This method, like all other TextPointer methods, defines a symbol /// as one of: ///- 16 bit Unicode character. ///- opening or closing tag of a ///. - the whole ///as atomic embedded object. See examples in ///method with one parameter. /// Returns a pointer at the next symbol in a specified /// direction, or past all following Unicode characters if the /// bordering content is Unicode text. /// /// /// Direction to move. /// ////// TextPointer in a requested direction, null if this TextPointer /// borders the start or end of the document. /// ////// ///If the following symbol is of type EmbeddedElement, ElementStart, /// or ElementEnd (as returned by the GetPointerContext method), then /// the TextPointer is advanced by exactly one symbol. ///If the following symbol is of type Text, then the TextPointer is /// advanced until it passes all following text (ie, until it reaches /// a position with a different return value for GetPointerContext). /// The exact symbol count crossed can be calculated in advance by /// calling GetTextLength. ///If there is no following symbol (start or end of the document), /// then the method returns null. ////// public TextPointer GetNextContextPosition(LogicalDirection direction) { return (TextPointer)((ITextPointer)this).GetNextContextPosition(direction); } ///This example shows how to use this method for traversing /// text content and examine its structure. The method implements /// a simplistic text content serializer, producing an xml-looking /// text. ///Note that to produce really well formed xml System.Xml /// interfaces must be used. We use this simplification only /// to make it more readable for people not familiar with System.Xml api. ////// string GetXaml(TextElement element) /// { /// StringBuilder buffer = new StringBuilder(); /// /// // Position a "navigator" pointer before the opening tag of the element. /// TextPointer navigator = element.ElementStart; /// /// while (navigator.CompareTo(element.ElementEnd) < 0) /// { /// switch (navigator.GetPointerContext(LogicalDirection.Forward)) /// { /// case TextPointerContext.ElementStart : /// // Output opening tag of the TextElement /// buffer.AddFormat("<{0}>", navigator.GetAdjacentElement(LogicalDirection.Forward).GetType().Name); /// break; /// case TextPointerContext.ElementEnd : /// // Output closing tag of the TextElement /// buffer.AddFormat("</{0}>", navigator.GetAdjacentElement(LogicalDirection.Forward).GetType().Name); /// break; /// case TextPointerContent.EmbeddedElement : /// // Output simple tag for embedded element /// buffer.AddFormat("<{0}/>", navigator.GetAdjacentElement(LogicalDirection.Forward).GetType().Name); /// break; /// case TextPointerContext.Text : /// // Output the text content of thi text run /// buffer.Add(navigator.GetTextInRun(LoigcalDirection.Forward); /// break; /// case TextPointerContext.None : /// Assert(false, "We do not expect to reach end of text container in this loop"); /// break; /// } /// /// // Advance the naviagtor to the next context position. /// navigator = navigator.GetNextContextPosition(LogicalDirection.Forward); /// /// Assert(navigator != null, "We do not expect to reach an end of a text container in this loop, as it is limited by element.ContentEnd bounadry"); /// } /// } ///
////// Returns a TextPointer at the closest insertion position in a /// specified direction. /// /// /// Direction to search a closest insertion position. /// ////// TextPointer positioned at inserion point. The value is never null. /// ////// ///The concept of insertion position is a convenience /// for traversing text content across structural boundaries, /// between table cells, paragraphs, list items etc. ///An insertion position is anywhere the containing document /// would normally place the caret. Examples of positions that are not /// insertion positions include locations between Paragraphs /// (between closing tag of a preceding paragraph and an opening tag /// of the following paragraph). A position within text runs /// in the middle of a surrogate Unicode surrogate pair is also /// not an insertion position. ///The method can be used for disambiguating insertion positions /// in two cases: when the text has two insertion positions separated by /// a sequence of formatting tags, as between "d" and "t" in this /// markup: "<Bold>Bold</Bold>text" - we have an insertion position /// before closing tag of Bold element and immediately after it. Both are /// valid insertion position and caret would stop on each of them /// depending on the direction of keyboard navigation. The method /// GetInsertionPosition allows user to pick one or another /// without moving to the "next" insertion position. ///Another important case when the method is useful is /// when a sequence of structural tags is involved. If you /// have a position, say between closing and opening paragraph tags, /// and want to fing a nearest insertion position the ///direction /// parameter will tell which of two possible positions to take: /// in the end of the preceding or in the begining of the following paragraph.If the pointer is already at insertion position /// but there is a non-empty sequence formatting in the given direction, /// then the position after all formatting tags will be returned. ///If the pointer is already at insertion position /// and there is no any formatting tags in the given direction, /// then the returned position is the same as this one. ///Somethimes the whole document does not have even /// one insertion position - it happens when the content /// is structurally incomplete, say in empty ////// or element. In such case the method /// will return the original position even though it is not /// an insertion position. The method never returns null. /// public TextPointer GetInsertionPosition(LogicalDirection direction) { return (TextPointer)((ITextPointer)this).GetInsertionPosition(direction); } // Used for pointer normalization in cases when direction does not matter. internal TextPointer GetInsertionPosition() { return GetInsertionPosition(LogicalDirection.Forward); } ///This example shows how to use the method ///GetInsertionPosition /// as a convenience of finding a starting "editable" position./// bool IsElementEmpty(TextElement element) /// { /// // Find first and last insertion positions in this element. /// // We use inward directions to make sure that insertion position /// // will be found correctly in case when the element is inline formatting one /// // (i.e. Run or Span). /// TextPointer start = element.ContentStart.GetInsertionPosition(LogicalDirection.Forward); /// TextPointer end = element.ContentEnd.GetInsertionPosition(LogicalDirection.Backward); /// /// // Element has empty printable content if its first and last /// // insertion positions are equal. /// return start.CompareTo(end) == 0; /// } ///
////// Returns a TextPointer in the direction indicated to the following /// insertion position. /// /// /// Direction to move. /// ////// A TextPointer at an insertion position in a requested direction, /// null if there is no more insertion positions in that direction. /// ////// ///The concept of insertion position is a convenience /// for traversing text content across structural boundaries, /// between table cells, paragraphs, list items etc. ///See more detailed definition of the concept of /// "insertion position" in the ////// method. If the TextPointer is not currently at an insertion position, this /// method will move the TextPointer to the next insertion position in /// the indicated direction, just like the MoveToInsertionPosition /// method. ///If the TextPointer is currently at an insertion position, this /// method will move the TextPointer to following insertion position, /// if the end of document is not encountered. ////// public TextPointer GetNextInsertionPosition(LogicalDirection direction) { return (TextPointer)((ITextPointer)this).GetNextInsertionPosition(direction); } ///In this example we use the method ///GetNextInsertionPosition /// for passing over structural boundaries in a proces of /// enumerating allin a range. /// int GetParagraphCount(TextPointer start, TextPointer end) /// { /// int paragraphCount = 0; /// /// while (start != null && start.CompareTo(end) < 0) /// { /// Paragraph paragraph = start.Paragraph; /// /// if (paragraph != null) /// { /// paragraphCount++; /// /// // Advance start to an end of the paragraph found /// start = paragraph.ContentEnd; /// } /// /// // Use GetNextInsertionPosition method to skip a sequence /// // of structural tags /// start = start.GetNextInsertionPosition(LogicalDirection.Forward); /// } /// /// return paragraphCount; /// } ///
////// Returns a TextPointer at the start of line after skipping /// a given number of line starts in forward or backward direction. /// /// /// Number of line starts to skip when finding a desired line start position. /// Negative values specify preceding lines, zero specifies the current line, /// positive values specify following lines. /// ////// Throws an InvalidOperationException if this TextPointer's HasValidLayout /// property is set false. Without a calculated layout it is not possible /// to position relative to rendered lines. /// ////// TextPointer positioned at the begining of a line requested /// (with LogicalDirection set to Forward). /// If there is no sufficient lines in requested direction, /// returns null. /// ////// public TextPointer GetLineStartPosition(int count) { int actualCount; TextPointer lineStartPosition = GetLineStartPosition(count, out actualCount); return (actualCount != count) ? null : lineStartPosition; } ///Line identification is possible only from normalized insertion positions; /// Line identification from not-normalized positions is mbigous and can produce /// unexpected results. Say, if a position is between closing and opening /// Paragraph tags, then GetInsertionPosition(LogicalDirection) is needed /// to decide whether we start from the end of previous Paragraph or /// from the start of the following one. Without such call /// ///If this TextPointer is at an otherwise ambiguous position, exactly /// between two lines, the LogicalDirection property is used to determine /// current position. So a TextPointer with backward LogicalDirection /// is considered to be at the end of line, and calling MoveToLineBoundary(0) /// would reposition it at the start of the preceding line. Making the /// same call with forward LogicalDirection would leave the TextPointer /// positioned where it started -- at the start of the following line. /// ////// Returns a TextPointer at the start of line after skipping /// a given number of line starts in forward or backward direction. /// /// /// Offset of the destination line. Negative values specify preceding /// lines, zero specifies the current line, positive values specify /// following lines. /// /// /// The offset of the line moved to. This value may be less than /// requested if the beginning or end of document is encountered. /// ////// TextPointer positioned at the begining of a line requested /// (with LogicalDirection set to Forward). /// If there is no sufficient lines in requested direction, /// returns a position at the beginning of a farthest line /// in this direction. In such case out parameter actualCount /// gets a number of lines actually skipped. /// Unlike the other override in this case the returned pointer is never null. /// ////// If this TextPointer is at an otherwise ambiguous position, exactly /// between two lines, the LogicalDirection property is used to determine /// current position. So a TextPointer with backward LogicalDirection /// is considered to be at the end of line, and calling MoveToLineBoundary(0) /// would reposition it at the start of the preceding line. Making the /// same call with forward LogicalDirection would leave the TextPointer /// positioned where it started -- at the start of the following line. /// public TextPointer GetLineStartPosition(int count, out int actualCount) { this.ValidateLayout(); TextPointer position = new TextPointer(this); if (this.HasValidLayout) { actualCount = position.MoveToLineBoundary(count); } else { actualCount = 0; } position.SetLogicalDirection(LogicalDirection.Forward); position.Freeze(); return position; } ////// Returns the bounding box of the content bordering this TextPointer /// in a specified direction. /// /// /// Direction of content. /// ////// public Rect GetCharacterRect(LogicalDirection direction) { ValidationHelper.VerifyDirection(direction, "direction"); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); this.ValidateLayout(); if (!this.HasValidLayout) { return Rect.Empty; } return TextPointerBase.GetCharacterRect(this, direction); } ///TextElement edges are not considered content for the purposes of /// this method. If the TextPointer is positioned before a TextElement /// edge, the return value will be the bounding box of the next /// non-TextElement content. ///If there is no content in the specified direction, a zero-width /// Rect is returned with height matching the preceding content. ////// Inserts text at this TextPointer's position. /// /// /// Text to insert. /// ////// The LogicalDirection property specifies whether this TextPointer /// will be positioned before or after the new text. /// public void InsertTextInRun(string textData) { if (textData == null) { throw new ArgumentNullException("textData"); } _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); TextPointer insertPosition; if (TextSchema.IsInTextContent(this)) { insertPosition = this; } else { insertPosition = TextRangeEditTables.EnsureInsertionPosition(this); } _tree.BeginChange(); try { _tree.InsertTextInternal(insertPosition, textData); } finally { _tree.EndChange(); } } ////// Deletes text in Run at this TextPointer's position /// ////// /// Number of characters to delete. /// Positive count deletes text following this TextPointer in Run. /// Negative count deletes text preceding this TextPointer in Run. /// /// /// Returns the actual count of deleted chars. /// The actual count may be less than requested in cases /// when original requested count exceeds text run length in given direction. /// public int DeleteTextInRun(int count) { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); // TextSchema Validation if (!TextSchema.IsInTextContent(this)) { return 0; } // Direction to delete text in run LogicalDirection direction = count < 0 ? LogicalDirection.Backward : LogicalDirection.Forward; // Get text run length in given direction int maxDeleteCount = this.GetTextRunLength(direction); // Truncate count if it extends past the run in given direction if (count > 0 && count > maxDeleteCount) { count = maxDeleteCount; } else if (count < 0 && count < -maxDeleteCount) { count = -maxDeleteCount; } // Get a new pointer for deletion TextPointer deleteToPosition = new TextPointer(this, count); _tree.BeginChange(); try { if (count > 0) { _tree.DeleteContentInternal(this, deleteToPosition); } else if (count < 0) { _tree.DeleteContentInternal(deleteToPosition, this); } } finally { _tree.EndChange(); } return count; } ////// Inserts a TextElement at this TextPointer's position. /// /// /// ContentElement to insert. /// ////// The LogicalDirection property specifies whether this TextPointer /// will be positioned before or after the TextElement. /// ////// Throws ArgumentException is textElement is not valid /// according to flow schema. /// ////// Throws InvalidOperationException if textElement cannot be inserted /// at this position because it belongs to another tree. /// internal void InsertTextElement(TextElement textElement) { Invariant.Assert(textElement != null); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); ValidationHelper.ValidateChild(this, textElement, "textElement"); if (textElement.Parent != null) { throw new InvalidOperationException(SR.Get(SRID.TextPointer_CannotInsertTextElementBecauseItBelongsToAnotherTree)); } textElement.RepositionWithContent(this); } ////// Insert a paragraph break at this position by splitting all elements upto its paragraph ancestor. /// ////// When this position has a paragraph parent, this method returns a /// normalized position in the beginning of a second paragraph. /// /// Otherwise, if the position is not parented by a paragraph /// (for special insertion positions such as table row end, BlockUIContainer boundaries, etc), /// this method creates a paragraph by using rules of EnsureInsertionPosition() /// and returns a normalized position at the start of the paragraph created. /// ////// Throws InvalidOperationException when this position has a non-splittable ancestor such as Hyperlink, /// since we cannot successfully split upto the parent paragraph in this case. /// public TextPointer InsertParagraphBreak() { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); Type containerType = this.TextContainer.Parent.GetType(); if (!TextSchema.IsValidChildOfContainer(containerType, typeof(Paragraph))) { throw new InvalidOperationException(SR.Get(SRID.TextSchema_IllegalElement, "Paragraph", containerType)); } Inline ancestor = this.GetNonMergeableInlineAncestor(); if (ancestor != null) { // Cannot split a hyperlink element! throw new InvalidOperationException(SR.Get(SRID.TextSchema_CannotSplitElement, ancestor.GetType().Name)); } TextPointer position; _tree.BeginChange(); try { position = TextRangeEdit.InsertParagraphBreak(this, /*moveIntoSecondParagraph:*/true); } finally { _tree.EndChange(); } return position; } ////// Insert a line break at this position. /// If the position is parented by a Run, the Run element is split at this position and then a line break inserted. /// ////// TextPointer positioned immediately after the closing tag of /// a public TextPointer InsertLineBreak() { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); TextPointer position; _tree.BeginChange(); try { position = TextRangeEdit.InsertLineBreak(this); } finally { _tree.EndChange(); } return position; } ///element inserted by this method. /// /// Debug only ToString override. /// public override string ToString() { #if DEBUG return "TextPointer Id=" + _debugId + " NodeId=" + _node.DebugId + " Edge=" + this.Edge; #else return base.ToString(); #endif // DEBUG } #endregion Public Methods //------------------------------------------------------ // // Public Properties // //------------------------------------------------------ #region Public Properties ////// Returns true if layout is calculated at the current position. /// ////// Methods that depend on layout -- GetLineStartPosition, /// GetCharacterRect, and IsAtLineStartPosition -- will attempt /// to re-calculate a dirty layout when called. Recalculating /// layout can be extremely expensive, however, and this method /// lets the caller detect when layout is dirty. /// // Internal methods that depend on this property: // - MoveToNextCaretPosition // - MoveToBackspaceCaretPosition public bool HasValidLayout { get { return _tree.TextView == null ? false : _tree.TextView.IsValid && _tree.TextView.Contains(this); } } ////// Specifies whether the TextPointer is associated with preceding or /// following content. /// ////// public LogicalDirection LogicalDirection { get { return GetGravityInternal(); } } ///If new content is insert at the TextPointer's current position, it /// will move to the edge of the new content that also borders its /// original associated content. ////// Returns the logical parent scoping this TextPointer. /// public DependencyObject Parent { get { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); return GetLogicalTreeNode(); } } ////// Returns true if this TextPointer is positioned at an insertion /// position. /// ////// public bool IsAtInsertionPosition { get { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); return TextPointerBase.IsAtInsertionPosition(this); } } ///An "insertion position" is a position where where the containing document /// would normally place the caret. Examples of positions that are not /// insertion positions include spaces between Paragraphs, or between /// Unicode surrogate pairs. ////// Returns true if this TextPointer is positioned at the start of a /// line. /// ////// Throws an InvalidOperationException if this TextPointer's HasValidLayout /// property is set false. Without a calculated layout it is not possible /// to determine where the current line starts or ends. /// ////// public bool IsAtLineStartPosition { get { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); this.ValidateLayout(); if (!this.HasValidLayout) { return false; } TextSegment lineRange = _tree.TextView.GetLineRange(this); // Null lineRange if no layout is available. if (!lineRange.IsNull) { TextPointer position = new TextPointer(this); TextPointerContext backwardContext = position.GetPointerContext(LogicalDirection.Backward); // Skip past any formatting. while ((backwardContext == TextPointerContext.ElementStart || backwardContext == TextPointerContext.ElementEnd) && TextSchema.IsFormattingType(position.GetAdjacentElement(LogicalDirection.Backward).GetType())) { position.MoveToNextContextPosition(LogicalDirection.Backward); backwardContext = position.GetPointerContext(LogicalDirection.Backward); } if (position.CompareTo((TextPointer)lineRange.Start) <= 0) { return true; } } return false; } } ///If this TextPointer is at an otherwise ambiguous position, exactly /// between two lines, the LogicalDirection property is used to determine /// current position. So a TextPointer with backward LogicalDirection /// will never have a true IsAtLineStartPosition unless it is positioned at the /// head of a document. ///This property is always false when HasValidLayout is false. ////// Returns the paragraph scoping this textpointer /// ////// public Paragraph Paragraph { get { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); return this.ParentBlock as Paragraph; } } ///When TextPointer is at insertion position it usually /// have non-null paragraph. The only exception is when /// it is positioned at the end of TableRow, where /// there is no scoping paragraph. ///When TextPointer is positioned outside of a paragraph, /// the property returns null. ////// Returns the paragraph-like parent of the pointer /// ////// If we would have a common base class for Paragraph and BlockUIContainer, /// we would return it here. /// internal Block ParagraphOrBlockUIContainer { // get { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); Block parentBlock = this.ParentBlock; return (parentBlock is Paragraph) || (parentBlock is BlockUIContainer) ? parentBlock : null; } } ////// The start position of the document's content /// ////// public TextPointer DocumentStart { get { return TextContainer.Start; } } ///This property may be useful as a base for persistent /// position indexing - for calculating offsets /// to all other pointers. ///The ///property for this /// position is not a TextElement - it is a text container, /// which can be one of , , /// . /// The end position of the document's content. /// ////// public TextPointer DocumentEnd { get { return TextContainer.End; } } #endregion Public Properties //----------------------------------------------------- // // Internal Methods // //------------------------------------------------------ #region Internal Methods // Returns this TextPointer's topmost Inline ancestor, which is not a mergeable (or splittable) Inline element. (e.g. Hyperlink) internal Inline GetNonMergeableInlineAncestor() { Inline ancestor = this.Parent as Inline; while (ancestor != null && TextSchema.IsMergeableInline(ancestor.GetType())) { ancestor = ancestor.Parent as Inline; } return ancestor; } // Returns this TextPointer's closest ListItem ancestor. internal ListItem GetListAncestor() { TextElement ancestor = this.Parent as TextElement; while (ancestor != null && !(ancestor is ListItem)) { ancestor = ancestor.Parent as TextElement; } return ancestor as ListItem; } internal static int GetTextInRun(TextContainer textContainer, int symbolOffset, TextTreeTextNode textNode, int nodeOffset, LogicalDirection direction, char[] textBuffer, int startIndex, int count) { int skipCount; int finalCount; if (textBuffer == null) { throw new ArgumentNullException("textBuffer"); } if (startIndex < 0) { throw new ArgumentException(SR.Get(SRID.NegativeValue, "startIndex")); } if (startIndex > textBuffer.Length) { throw new ArgumentException(SR.Get(SRID.StartIndexExceedsBufferSize, startIndex, textBuffer.Length)); } if (count < 0) { throw new ArgumentException(SR.Get(SRID.NegativeValue, "count")); } if (count > textBuffer.Length - startIndex) { throw new ArgumentException(SR.Get(SRID.MaxLengthExceedsBufferSize, count, textBuffer.Length, startIndex)); } Invariant.Assert(textNode != null, "textNode is expected to be non-null"); textContainer.EmptyDeadPositionList(); if (nodeOffset < 0) { skipCount = 0; } else { skipCount = (direction == LogicalDirection.Backward) ? nodeOffset : textNode.SymbolCount - nodeOffset; symbolOffset += nodeOffset; } finalCount = 0; // Loop and combine adjacent text nodes into a single run. // This isn't just a perf optimization. Because text positions // split text nodes, if we just returned a single node's text // callers would see strange side effects where position.GetTextLength() != // position.GetText() if another position is moved between the calls. while (textNode != null) { // Never return more textBuffer than the text following this position in the current text node. finalCount += Math.Min(count - finalCount, textNode.SymbolCount - skipCount); skipCount = 0; if (finalCount == count) break; textNode = ((direction == LogicalDirection.Forward) ? textNode.GetNextNode() : textNode.GetPreviousNode()) as TextTreeTextNode; } // If we're reading backwards, need to fixup symbolOffset to point into the node. if (direction == LogicalDirection.Backward) { symbolOffset -= finalCount; } if (finalCount > 0) // We may not have allocated textContainer.RootTextBlock if no text was ever inserted. { TextTreeText.ReadText(textContainer.RootTextBlock, symbolOffset, finalCount, textBuffer, startIndex); } return finalCount; } internal static DependencyObject GetAdjacentElement(TextTreeNode node, ElementEdge edge, LogicalDirection direction) { TextTreeNode adjacentNode; DependencyObject element; adjacentNode = GetAdjacentNode(node, edge, direction); if (adjacentNode is TextTreeObjectNode) { element = ((TextTreeObjectNode)adjacentNode).EmbeddedElement; } else if (adjacentNode is TextTreeTextElementNode) { element = ((TextTreeTextElementNode)adjacentNode).TextElement; } else { // We're adjacent to a text node, or have no sibling in the specified direction. element = null; } return element; } ///The ///property for this /// position is not a TextElement - it is a text container, /// which can be one of , , /// . /// Moves this TextPointer to another TextPointer's position. /// /// /// Position to move to. /// ////// Throws an ArgumentException if textPosition is not /// positioned within the same document. /// ////// Throws an InvalidOperationException if this TextPointer's IsFrozen /// property is set true. Frozen TextPointers may not be repositioned. /// internal void MoveToPosition(TextPointer textPosition) { ValidationHelper.VerifyPosition(_tree, textPosition); VerifyNotFrozen(); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); textPosition.SyncToTreeGeneration(); MoveToNode(_tree, textPosition.Node, textPosition.Edge); } ////// Advances this TextPointer to a new position by a specified symbol /// count. /// /// /// Number of symbols to advance. offset may be negative, in which /// case the TextPointer is moved backwards. /// ////// Throws an InvalidOperationException if this TextPointer's IsFrozen /// property is set true. Frozen TextPointers may not be repositioned. /// ////// This method, like all other TextPointer methods, defines a symbol /// as a /// - 16 bit Unicode character. /// - TextElement start or end edge. /// - UIElement. /// - ContentElement other than TextElement. /// ////// The number of symbols actually advanced. The absolute value of the /// count returned may be less than requested if the end of document is /// encountered while advancing. /// internal int MoveByOffset(int offset) { SplayTreeNode node; ElementEdge edge; int symbolOffset; int currentOffset; VerifyNotFrozen(); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); if (offset != 0) { currentOffset = GetSymbolOffset(); symbolOffset = unchecked(currentOffset + offset); if (symbolOffset < 1) { if (offset > 0) { // Rolled past Int32.MaxValue. Go to end of doc. symbolOffset = _tree.InternalSymbolCount - 1; offset = symbolOffset - currentOffset; } else { // Underflow. Go to start of doc. offset += (1 - symbolOffset); symbolOffset = 1; } } else if (symbolOffset > _tree.InternalSymbolCount - 1) { // Overflow. Go to end of doc. // NB: there's no symmetric check here for rolling under with distance=Int32.MinValue. // Since GetSymbolOffset is always positive, we can't roll-around with a min value. offset -= (symbolOffset - (_tree.InternalSymbolCount - 1)); symbolOffset = _tree.InternalSymbolCount - 1; } _tree.GetNodeAndEdgeAtOffset(symbolOffset, out node, out edge); MoveToNode(_tree, (TextTreeNode)node, edge); } return offset; } ////// Advances this TextPointer to the next symbol in a specified /// direction, or past all following Unicode characters if the /// bordering content is Unicode text. /// /// /// Direction to move. /// ////// Throws an InvalidOperationException if this TextPointer's IsFrozen /// property is set true. Frozen TextPointers may not be repositioned. /// ////// true if the TextPointer is repositioned, false if the TextPointer /// borders the start or end of the document. /// ////// If the following symbol is of type EmbeddedElement, ElementStart, /// or ElementEnd (as returned by the GetPointerContext method), then /// the TextPointer is advanced by exactly one symbol. /// /// If the following symbol is of type Text, then the TextPointer is /// advanced until it passes all following text (ie, until it reaches /// a position with a different return value for GetPointerContext). /// The exact symbol count crossed can be calculated in advance by /// calling GetTextLength. /// /// If there is no following symbol (start or end of the document), /// then the method does nothing and returns false. /// internal bool MoveToNextContextPosition(LogicalDirection direction) { TextTreeNode node; ElementEdge edge; bool moved; ValidationHelper.VerifyDirection(direction, "direction"); VerifyNotFrozen(); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); if (direction == LogicalDirection.Forward) { moved = GetNextNodeAndEdge(out node, out edge); } else { moved = GetPreviousNodeAndEdge(out node, out edge); } if (moved) { SetNodeAndEdge(AdjustRefCounts(node, edge, _node, this.Edge), edge); DebugAssertGeneration(); } AssertState(); return moved; } ////// Moves this TextPointer to the closest insertion position in a /// specified direction. If the pointer is already at insertion point /// but there is a non-empty sequence formatting in the given direction, /// then the position moves to the other instance of this insertion /// position. /// /// /// Direction to move. /// ////// Throws an InvalidOperationException if this TextPointer's IsFrozen /// property is set true. Frozen TextPointers may not be repositioned. /// ////// An "insertion position" is a position where new content may be added /// without breaking any semantic rules of the containing document. /// /// In practice, an insertion position is anywhere the containing document /// would normally place the caret. Examples of positions that are not /// insertion positions include spaces between Paragraphs, or between /// Unicode surrogate pairs. /// ////// True if the TextPointer is repositioned, false otherwise. /// internal bool MoveToInsertionPosition(LogicalDirection direction) { ValidationHelper.VerifyDirection(direction, "direction"); VerifyNotFrozen(); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); return TextPointerBase.MoveToInsertionPosition(this, direction); } ////// Advances this TextPointer in the direction indicated to the following /// insertion position. /// /// /// Direction to move. /// ////// Throws an InvalidOperationException if this TextPointer's IsFrozen /// property is set true. Frozen TextPointers may not be repositioned. /// ////// An "insertion position" is a position where new content may be added /// without breaking any semantic rules of the containing document. /// /// In practice, an insertion position is anywhere the containing document /// would normally place the caret. Examples of positions that are not /// insertion positions include spaces between Paragraphs, or between /// Unicode surrogate pairs. /// /// If the TextPointer is not currently at an insertion position, this /// method will move the TextPointer to the next insertion position in /// the indicated direction, just like the MoveToInsertionPosition /// method. /// /// If the TextPointer is currently at an insertion position, this /// method will move the TextPointer to following insertion position, /// if the end of document is not encountered. /// ////// True if the TextPointer is repositioned, false otherwise. /// internal bool MoveToNextInsertionPosition(LogicalDirection direction) { ValidationHelper.VerifyDirection(direction, "direction"); VerifyNotFrozen(); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); return TextPointerBase.MoveToNextInsertionPosition(this, direction); } ////// Advances this TextPointer to the start of a neighboring line. /// /// /// Offset of the destination line. Negative values specify preceding /// lines, zero specifies the current line, positive values specify /// following lines. /// ////// Throws an InvalidOperationException if this TextPointer's IsFrozen /// property is set true. Frozen TextPointers may not be repositioned. /// ////// The offset of the line moved to. This value may be less than /// requested if the beginning or end of document is encountered. /// ////// If this TextPointer is at an otherwise ambiguous position, exactly /// between two lines, the LogicalDirection property is used to determine /// current position. So a TextPointer with backward LogicalDirection /// is considered to be at the end of line, and calling MoveToLineBoundary(0) /// would reposition it at the start of the preceding line. Making the /// same call with forward LogicalDirection would leave the TextPointer /// positioned where it started -- at the start of the following line. /// internal int MoveToLineBoundary(int count) { VerifyNotFrozen(); this.ValidateLayout(); if (!this.HasValidLayout) { return 0; } _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); return TextPointerBase.MoveToLineBoundary(this, _tree.TextView, count); } ////// Inserts a UIElement at this TextPointer's position. /// /// /// UIElement to insert. /// ////// The LogicalDirection property specifies whether this TextPointer /// will be positioned before or after the UIElement. /// ////// Throws ArgumentException is contentElement is not valid /// according to flow schema. /// internal void InsertUIElement(UIElement uiElement) { if (uiElement == null) { throw new ArgumentNullException("uiElement"); } _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); ValidationHelper.ValidateChild(this, uiElement, "uiElement"); if (!((TextElement)this.Parent).IsEmpty) // the parent may be InlineUIContainer or BlockUIContainer { throw new InvalidOperationException(SR.Get(SRID.TextSchema_UIElementNotAllowedInThisPosition)); } _tree.BeginChange(); try { _tree.InsertEmbeddedObjectInternal(this, uiElement); } finally { _tree.EndChange(); } } // internal TextElement GetAdjacentElementFromOuterPosition(LogicalDirection direction) { TextTreeTextElementNode elementNode; _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); elementNode = GetAdjacentTextElementNodeSibling(direction); return (elementNode == null) ? null : elementNode.TextElement; } ////// Sets the logical direction of this textpointer. /// ////// Throws an InvalidOperationException if this TextPointer's Freeze() method has been called. /// /// internal void SetLogicalDirection(LogicalDirection direction) { SplayTreeNode newNode; ElementEdge edge; ValidationHelper.VerifyDirection(direction, "direction"); VerifyNotFrozen(); _tree.EmptyDeadPositionList(); if (GetGravityInternal() != direction) { SyncToTreeGeneration(); newNode = _node; // We need to shift nodes to match the new gravity. switch (this.Edge) { case ElementEdge.BeforeStart: newNode = _node.GetPreviousNode(); if (newNode != null) { // Move to the previous sibling. edge = ElementEdge.AfterEnd; } else { // Move to parent inner edge. newNode = _node.GetContainingNode(); Invariant.Assert(newNode != null, "Bad tree state: newNode must be non-null (BeforeStart)"); edge = ElementEdge.AfterStart; } break; case ElementEdge.AfterStart: newNode = _node.GetFirstContainedNode(); if (newNode != null) { // Move to first child. edge = ElementEdge.BeforeStart; } else { // Move to opposite edge. newNode = _node; edge = ElementEdge.BeforeEnd; } break; case ElementEdge.BeforeEnd: newNode = _node.GetLastContainedNode(); if (newNode != null) { // Move to last child. edge = ElementEdge.AfterEnd; } else { // Move to opposite edge. newNode = _node; edge = ElementEdge.AfterStart; } break; case ElementEdge.AfterEnd: newNode = _node.GetNextNode(); if (newNode != null) { // Move to the next sibling. edge = ElementEdge.BeforeStart; } else { // Move to parent inner edge. newNode = _node.GetContainingNode(); Invariant.Assert(newNode != null, "Bad tree state: newNode must be non-null (AfterEnd)"); edge = ElementEdge.BeforeEnd; } break; default: Invariant.Assert(false, "Bad ElementEdge value"); edge = this.Edge; break; } SetNodeAndEdge(AdjustRefCounts((TextTreeNode)newNode, edge, _node, this.Edge), edge); Invariant.Assert(GetGravityInternal() == direction, "Inconsistent position gravity"); } } ////// True if the Freeze method has been called, in which case /// this TextPointer is immutable and may not be repositioned. /// ////// By default, TextPointers are mutable -- they may be /// repositioned with calls to methods like MoveByOffset, and /// LogicalDirection may be changed freely. After Freeze is /// called, a TextPointer is locked down -- any attempt to set /// LogicalDirection or call repositioning methods will raise an /// InvalidOperationException. /// internal bool IsFrozen { get { _tree.EmptyDeadPositionList(); return (_flags & (uint)Flags.IsFrozen) == (uint)Flags.IsFrozen; } } ////// Makes this TextPointer immutable. /// ////// By default, TextPointers are mutable -- they may be /// repositioned with calls to methods like MoveByOffset, and /// LogicalDirection may be changed freely. After this method is /// called, a TextPointer is locked down -- any attempt to set /// LogicalDirection or call repositioning methods will raise an /// InvalidOperationException. /// /// The IsFrozen property will return true after this method is called. /// /// Calling Freeze multiple times has no additional effect. /// internal void Freeze() { _tree.EmptyDeadPositionList(); SetIsFrozen(); } ////// Returns an immutable TextPointer instance positioned equally to /// this one, with a specified LogicalDirection. /// /// /// LogicalDirection of the returned TextPointer. /// ////// The TextPointer returned will always have its IsFrozen property set /// true. /// /// The return value will be a new TextPointer instance unless this /// TextPointer is already frozen with a matching LogicalDirection, in /// which case this TextPointer will be returned. /// internal TextPointer GetFrozenPointer(LogicalDirection logicalDirection) { ValidationHelper.VerifyDirection(logicalDirection, "logicalDirection"); _tree.EmptyDeadPositionList(); return (TextPointer)TextPointerBase.GetFrozenPointer(this, logicalDirection); } void ITextPointer.SetLogicalDirection(LogicalDirection direction) { SetLogicalDirection(direction); } int ITextPointer.CompareTo(ITextPointer position) { return CompareTo((TextPointer)position); } int ITextPointer.CompareTo(StaticTextPointer position) { int offsetThis; int offsetPosition; int result; offsetThis = this.Offset + 1; offsetPosition = TextContainer.GetInternalOffset(position); if (offsetThis < offsetPosition) { result = -1; } else if (offsetThis > offsetPosition) { result = +1; } else { result = 0; } return result; } int ITextPointer.GetOffsetToPosition(ITextPointer position) { return GetOffsetToPosition((TextPointer)position); } TextPointerContext ITextPointer.GetPointerContext(LogicalDirection direction) { return GetPointerContext(direction); } int ITextPointer.GetTextRunLength(LogicalDirection direction) { return GetTextRunLength(direction); } //string ITextPointer.GetTextInRun(LogicalDirection direction) { return TextPointerBase.GetTextInRun(this, direction); } int ITextPointer.GetTextInRun(LogicalDirection direction, char[] textBuffer, int startIndex, int count) { return GetTextInRun(direction, textBuffer, startIndex, count); } object ITextPointer.GetAdjacentElement(LogicalDirection direction) { return GetAdjacentElement(direction); } Type ITextPointer.GetElementType(LogicalDirection direction) { DependencyObject element; ValidationHelper.VerifyDirection(direction, "direction"); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); element = GetElement(direction); return element != null ? element.GetType() : null; } bool ITextPointer.HasEqualScope(ITextPointer position) { TextTreeNode parent1; TextTreeNode parent2; TextPointer textPointer; _tree.EmptyDeadPositionList(); ValidationHelper.VerifyPosition(_tree, position); textPointer = (TextPointer)position; SyncToTreeGeneration(); textPointer.SyncToTreeGeneration(); parent1 = GetScopingNode(); parent2 = textPointer.GetScopingNode(); return (parent1 == parent2); } // Candidate for replacing MoveToNextContextPosition for immutable TextPointer model ITextPointer ITextPointer.GetNextContextPosition(LogicalDirection direction) { ITextPointer pointer = ((ITextPointer)this).CreatePointer(); if (pointer.MoveToNextContextPosition(direction)) { pointer.Freeze(); } else { pointer = null; } return pointer; } // Candidate for replacing MoveToInsertionPosition for immutable TextPointer model ITextPointer ITextPointer.GetInsertionPosition(LogicalDirection direction) { ITextPointer pointer = ((ITextPointer)this).CreatePointer(); pointer.MoveToInsertionPosition(direction); pointer.Freeze(); return pointer; } // Returns the closest insertion position, treating all unicode code points // as valid insertion positions. A useful performance win over // GetNextInsertionPosition when only formatting scopes are important. ITextPointer ITextPointer.GetFormatNormalizedPosition(LogicalDirection direction) { ITextPointer pointer = ((ITextPointer)this).CreatePointer(); TextPointerBase.MoveToFormatNormalizedPosition(pointer, direction); pointer.Freeze(); return pointer; } // Candidate for replacing MoveToNextInsertionPosition for immutable TextPointer model ITextPointer ITextPointer.GetNextInsertionPosition(LogicalDirection direction) { ITextPointer pointer = ((ITextPointer)this).CreatePointer(); if (pointer.MoveToNextInsertionPosition(direction)) { pointer.Freeze(); } else { pointer = null; } return pointer; } object ITextPointer.GetValue(DependencyProperty formattingProperty) { DependencyObject parent; object val; if (formattingProperty == null) { throw new ArgumentNullException("formattingProperty"); } _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); parent = GetDependencyParent(); if (parent == null) { val = DependencyProperty.UnsetValue; } else { val = parent.GetValue(formattingProperty); } return val; } object ITextPointer.ReadLocalValue(DependencyProperty formattingProperty) { TextElement element; if (formattingProperty == null) { throw new ArgumentNullException("formattingProperty"); } _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); element = this.Parent as TextElement; if (element == null) { throw new InvalidOperationException(SR.Get(SRID.NoScopingElement, "This TextPointer")); } return element.ReadLocalValue(formattingProperty); } LocalValueEnumerator ITextPointer.GetLocalValueEnumerator() { DependencyObject element; _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); element = this.Parent as TextElement; if (element == null) { // return (new DependencyObject()).GetLocalValueEnumerator(); } return element.GetLocalValueEnumerator(); } ITextPointer ITextPointer.CreatePointer() { return ((ITextPointer)this).CreatePointer(0, this.LogicalDirection); } StaticTextPointer ITextPointer.CreateStaticPointer() { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); return new StaticTextPointer(_tree, _node, _node.GetOffsetFromEdge(this.Edge)); } ITextPointer ITextPointer.CreatePointer(int offset) { return ((ITextPointer)this).CreatePointer(offset, this.LogicalDirection); } ITextPointer ITextPointer.CreatePointer(LogicalDirection gravity) { return ((ITextPointer)this).CreatePointer(0, gravity); } ITextPointer ITextPointer.CreatePointer(int offset, LogicalDirection gravity) { return new TextPointer(this, offset, gravity); } // void ITextPointer.Freeze() { Freeze(); } ITextPointer ITextPointer.GetFrozenPointer(LogicalDirection logicalDirection) { return GetFrozenPointer(logicalDirection); } // Worker for Min, accepts any ITextPointer. bool ITextPointer.MoveToNextContextPosition(LogicalDirection direction) { return MoveToNextContextPosition(direction); } int ITextPointer.MoveByOffset(int offset) { return MoveByOffset(offset); } void ITextPointer.MoveToPosition(ITextPointer position) { MoveToPosition((TextPointer)position); } void ITextPointer.MoveToElementEdge(ElementEdge edge) { MoveToElementEdge(edge); } internal void MoveToElementEdge(ElementEdge edge) { TextTreeTextElementNode elementNode; ValidationHelper.VerifyElementEdge(edge, "edge"); VerifyNotFrozen(); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); elementNode = GetScopingNode() as TextTreeTextElementNode; if (elementNode == null) { throw new InvalidOperationException(SR.Get(SRID.NoScopingElement, "This TextNavigator")); } MoveToNode(_tree, elementNode, edge); } // int ITextPointer.MoveToLineBoundary(int count) { return MoveToLineBoundary(count); } // Rect ITextPointer.GetCharacterRect(LogicalDirection direction) { return GetCharacterRect(direction); } bool ITextPointer.MoveToInsertionPosition(LogicalDirection direction) { return MoveToInsertionPosition(direction); } bool ITextPointer.MoveToNextInsertionPosition(LogicalDirection direction) { return MoveToNextInsertionPosition(direction); } // The caret methods are debug only until we actually start to use them. // #if DEBUG /// /// internal bool MoveToCaretPosition(LogicalDirection contentDirection) { TextPointer position; LogicalDirection oppositeDirection; bool moved; ValidationHelper.VerifyDirection(contentDirection, "contentDirection"); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); VerifyNotFrozen(); this.ValidateLayout(); if (!this.HasValidLayout) { return false; } moved = false; if (!_tree.TextView.IsAtCaretUnitBoundary(this)) { oppositeDirection = (contentDirection == LogicalDirection.Forward) ? LogicalDirection.Backward : LogicalDirection.Forward; position = (TextPointer)_tree.TextView.GetNextCaretUnitPosition(this, oppositeDirection); MoveToPosition(position); moved = true; } return moved; } ////// internal bool MoveToNextCaretPosition(LogicalDirection direction) { TextPointer position; bool moved; ValidationHelper.VerifyDirection(direction, "direction"); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); VerifyNotFrozen(); this.ValidateLayout(); if (!this.HasValidLayout) { return false; } position = (TextPointer)_tree.TextView.GetNextCaretUnitPosition(this, direction); moved = false; if (this.CompareTo(position) != 0) { MoveToPosition(position); moved = true; } return moved; } ////// internal bool MoveToBackspaceCaretPosition() { TextPointer position; bool moved; _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); VerifyNotFrozen(); this.ValidateLayout(); if (!this.HasValidLayout) { return false; } position = (TextPointer)_tree.TextView.GetBackspaceCaretUnitPosition(this); moved = false; if (this.CompareTo(position) != 0) { MoveToPosition(position); moved = true; } return moved; } #endif void ITextPointer.InsertTextInRun(string textData) { this.InsertTextInRun(textData); } // void ITextPointer.DeleteContentToPosition(ITextPointer limit) { _tree.BeginChange(); try { // DeleteContent is clever enough to handle the this > limit case. TextRangeEditTables.DeleteContent(this, (TextPointer)limit); } finally { _tree.EndChange(); } } ///bool ITextPointer.ValidateLayout() { return this.ValidateLayout(); } /// internal bool ValidateLayout() { return TextPointerBase.ValidateLayout(this, _tree.TextView); } // Returns the TextTreeTextNode in the direction indicated bordering // a TextPointer, or null if no such node exists. internal TextTreeTextNode GetAdjacentTextNodeSibling(LogicalDirection direction) { return GetAdjacentSiblingNode(direction) as TextTreeTextNode; } // Returns the TextTreeTextNode in the direction indicated bordering // a TextPointer, or null if no such node exists. internal static TextTreeTextNode GetAdjacentTextNodeSibling(TextTreeNode node, ElementEdge edge, LogicalDirection direction) { return GetAdjacentSiblingNode(node, edge, direction) as TextTreeTextNode; } // Returns the TextTreeTextNode in the direction indicated bordering // a TextPointer, or null if no such node exists. internal TextTreeTextElementNode GetAdjacentTextElementNodeSibling(LogicalDirection direction) { return GetAdjacentSiblingNode(direction) as TextTreeTextElementNode; } // Returns the TextTreeTextNode in the direction indicated bordering // a TextPointer, or null if no such node exists. internal TextTreeTextElementNode GetAdjacentTextElementNode(LogicalDirection direction) { return GetAdjacentNode(direction) as TextTreeTextElementNode; } // Returns the sibling node (ie, node in the same scope) in the direction indicated bordering // a TextPointer, or null if no such node exists. internal TextTreeNode GetAdjacentSiblingNode(LogicalDirection direction) { DebugAssertGeneration(); return GetAdjacentSiblingNode(_node, this.Edge, direction); } internal static TextTreeNode GetAdjacentSiblingNode(TextTreeNode node, ElementEdge edge, LogicalDirection direction) { SplayTreeNode sibling; if (direction == LogicalDirection.Forward) { switch (edge) { case ElementEdge.BeforeStart: sibling = node; break; case ElementEdge.AfterStart: sibling = node.GetFirstContainedNode(); break; case ElementEdge.BeforeEnd: default: sibling = null; break; case ElementEdge.AfterEnd: sibling = node.GetNextNode(); break; } } else // direction == LogicalDirection.Backward { switch (edge) { case ElementEdge.BeforeStart: sibling = node.GetPreviousNode(); break; case ElementEdge.AfterStart: default: sibling = null; break; case ElementEdge.BeforeEnd: sibling = node.GetLastContainedNode(); break; case ElementEdge.AfterEnd: sibling = node; break; } } return (TextTreeNode)sibling; } // Returns the symbol offset within the TextContainer of this Position. internal int GetSymbolOffset() { DebugAssertGeneration(); return GetSymbolOffset(_tree, _node, this.Edge); } // Returns the symbol offset within the TextContainer of this Position. internal static int GetSymbolOffset(TextContainer tree, TextTreeNode node, ElementEdge edge) { int offset; switch (edge) { case ElementEdge.BeforeStart: offset = node.GetSymbolOffset(tree.Generation); break; case ElementEdge.AfterStart: offset = node.GetSymbolOffset(tree.Generation) + 1; break; case ElementEdge.BeforeEnd: offset = node.GetSymbolOffset(tree.Generation) + node.SymbolCount - 1; break; case ElementEdge.AfterEnd: offset = node.GetSymbolOffset(tree.Generation) + node.SymbolCount; break; default: Invariant.Assert(false, "Unknown value for position edge"); offset = 0; break; } return offset; } // Returns the Logical Tree Node scoping this position. internal DependencyObject GetLogicalTreeNode() { DebugAssertGeneration(); return GetScopingNode().GetLogicalTreeNode(); } // Updates the position state if the node referenced by this position has // been removed from the TextContainer. This method must be called before // referencing the position's state when a public entry point is called. internal void SyncToTreeGeneration() { SplayTreeNode node; SplayTreeNode searchNode; SplayTreeNode parentNode; SplayTreeNode splayNode; ElementEdge edge; TextTreeFixupNode fixup = null; // If the tree hasn't had any deletions since the last time we // checked there's no work to do. if (_generation == _tree.PositionGeneration) return; // Invalidate the caret unit boundary cache -- the surrounding // content may have changed. this.IsCaretUnitBoundaryCacheValid = false; node = _node; edge = this.Edge; // If we can find a fixup node in the ancestor chain, this position // needs to be updated. // // It's possible to have cascading deletes -- some content was // deleted, then the nodes pointed to by a fixup node were themselves // deleted, and so forth. So we have to keep checking all the // way up to the root. while (true) { searchNode = node; splayNode = node; while (true) { parentNode = searchNode.ParentNode; if (parentNode == null) // The root node is always valid. break; fixup = parentNode as TextTreeFixupNode; if (fixup != null) break; if (searchNode.Role == SplayTreeNodeRole.LocalRoot) { splayNode.Splay(); splayNode = parentNode; } searchNode = parentNode; } if (parentNode == null) break; // Checked all the way to the root, position is valid. // If we make it here we've found a fixup node. Our gravity // tells us which direction to follow it. if (GetGravityInternal() == LogicalDirection.Forward) { if (edge == ElementEdge.BeforeStart && fixup.FirstContainedNode != null) { // We get here if and only if a single TextElementNode was removed. // Because only a single element was removed, we don't have to worry // about whether the position was originally in some contained content. // It originally pointed to the extracted node, so we can always // move to contained content. node = fixup.FirstContainedNode; Invariant.Assert(edge == ElementEdge.BeforeStart, "edge BeforeStart is expected"); } else { node = fixup.NextNode; edge = fixup.NextEdge; } } else { if (edge == ElementEdge.AfterEnd && fixup.LastContainedNode != null) { // We get here if and only if a single TextElementNode was removed. // Because only a single element was removed, we don't have to worry // about whether the position was originally in some contained content. // It originally pointed to the extracted node, so we can always // move to contained content. node = fixup.LastContainedNode; Invariant.Assert(edge == ElementEdge.AfterEnd, "edge AfterEnd is expected"); } else { node = fixup.PreviousNode; edge = fixup.PreviousEdge; } } } // Note we intentionally don't call AdjustRefCounts here. // We already incremented ref counts when the old target // node was deleted. SetNodeAndEdge((TextTreeNode)node, edge); // Update the position generation, so we don't do this work again // until the tree changes. _generation = _tree.PositionGeneration; AssertState(); } // Returns the logical parent node of a text position. internal TextTreeNode GetScopingNode() { return GetScopingNode(_node, this.Edge); } internal static TextTreeNode GetScopingNode(TextTreeNode node, ElementEdge edge) { TextTreeNode scopingNode; switch (edge) { case ElementEdge.BeforeStart: case ElementEdge.AfterEnd: scopingNode = (TextTreeNode)node.GetContainingNode(); break; case ElementEdge.AfterStart: case ElementEdge.BeforeEnd: default: scopingNode = node; break; } return scopingNode; } // Debug only -- asserts this TextPointer is synchronized to the current tree generation. internal void DebugAssertGeneration() { Invariant.Assert(_generation == _tree.PositionGeneration, "TextPointer not synchronized to tree generation!"); } internal bool GetNextNodeAndEdge(out TextTreeNode node, out ElementEdge edge) { DebugAssertGeneration(); return GetNextNodeAndEdge(_node, this.Edge, _tree.PlainTextOnly, out node, out edge); } // Finds the next run, returned as a node/edge pair. // Returns false if there is no following run, in which case node/edge will match the input position. // The returned node/edge pair respects the input position's gravity. internal static bool GetNextNodeAndEdge(TextTreeNode sourceNode, ElementEdge sourceEdge, bool plainTextOnly, out TextTreeNode node, out ElementEdge edge) { SplayTreeNode currentNode; SplayTreeNode newNode; SplayTreeNode nextNode; SplayTreeNode containingNode; bool startedAdjacentToTextNode; bool endedAdjacentToTextNode; node = sourceNode; edge = sourceEdge; newNode = node; currentNode = node; // If we started next to a TextTreeTextNode, and the next node // is also a TextTreeTextNode, then skip past the second node // as well -- multiple text nodes count as a single Move run. do { startedAdjacentToTextNode = false; endedAdjacentToTextNode = false; switch (edge) { case ElementEdge.BeforeStart: newNode = currentNode.GetFirstContainedNode(); if (newNode != null) { // Move to inner edge/first child. } else if (currentNode is TextTreeTextElementNode) { // Move to inner edge. newNode = currentNode; edge = ElementEdge.BeforeEnd; } else { // Move to next node. startedAdjacentToTextNode = currentNode is TextTreeTextNode; edge = ElementEdge.BeforeEnd; goto case ElementEdge.BeforeEnd; } break; case ElementEdge.AfterStart: newNode = currentNode.GetFirstContainedNode(); if (newNode != null) { // Move to first child/second child or first child/first child child if (newNode is TextTreeTextElementNode) { edge = ElementEdge.AfterStart; } else { startedAdjacentToTextNode = newNode is TextTreeTextNode; endedAdjacentToTextNode = newNode.GetNextNode() is TextTreeTextNode; edge = ElementEdge.AfterEnd; } } else if (currentNode is TextTreeTextElementNode) { // Move to next node. newNode = currentNode; edge = ElementEdge.AfterEnd; } else { Invariant.Assert(currentNode is TextTreeRootNode, "currentNode is expected to be TextTreeRootNode"); // This is the root node, leave newNode null. } break; case ElementEdge.BeforeEnd: newNode = currentNode.GetNextNode(); if (newNode != null) { // Move to next node; endedAdjacentToTextNode = newNode is TextTreeTextNode; edge = ElementEdge.BeforeStart; } else { // Move to inner edge of parent. newNode = currentNode.GetContainingNode(); } break; case ElementEdge.AfterEnd: nextNode = currentNode.GetNextNode(); startedAdjacentToTextNode = nextNode is TextTreeTextNode; newNode = nextNode; if (newNode != null) { // Move to next node/first child; if (newNode is TextTreeTextElementNode) { edge = ElementEdge.AfterStart; } else { // Move to next node/next next node. endedAdjacentToTextNode = newNode.GetNextNode() is TextTreeTextNode; } } else { containingNode = currentNode.GetContainingNode(); if (!(containingNode is TextTreeRootNode)) { // Move to parent. newNode = containingNode; } } break; default: Invariant.Assert(false, "Unknown ElementEdge value"); break; } currentNode = newNode; // Multiple text nodes count as a single Move run. // Instead of iterating through N text nodes, exploit // the fact (when we can) that text nodes are only ever contained in // runs with no other content. Jump straight to the end. if (startedAdjacentToTextNode && endedAdjacentToTextNode && plainTextOnly) { newNode = newNode.GetContainingNode(); Invariant.Assert(newNode is TextTreeRootNode); if (edge == ElementEdge.BeforeStart) { edge = ElementEdge.BeforeEnd; } else { newNode = newNode.GetLastContainedNode(); Invariant.Assert(newNode != null); Invariant.Assert(edge == ElementEdge.AfterEnd); } break; } } while (startedAdjacentToTextNode && endedAdjacentToTextNode); if (newNode != null) { node = (TextTreeNode)newNode; } return (newNode != null); } internal bool GetPreviousNodeAndEdge(out TextTreeNode node, out ElementEdge edge) { DebugAssertGeneration(); return GetPreviousNodeAndEdge(_node, this.Edge, _tree.PlainTextOnly, out node, out edge); } // Finds the previous run, returned as a node/edge pair. // Returns false if there is no preceding run, in which case node/edge will match the input position. // The returned node/edge pair respects the input positon's gravity. internal static bool GetPreviousNodeAndEdge(TextTreeNode sourceNode, ElementEdge sourceEdge, bool plainTextOnly, out TextTreeNode node, out ElementEdge edge) { SplayTreeNode currentNode; SplayTreeNode newNode; SplayTreeNode containingNode; bool startedAdjacentToTextNode; bool endedAdjacentToTextNode; node = sourceNode; edge = sourceEdge; newNode = node; currentNode = node; // If we started next to a TextTreeTextNode, and the next node // is also a TextTreeTextNode, then skip past the second node // as well -- multiple text nodes count as a single Move run. do { startedAdjacentToTextNode = false; endedAdjacentToTextNode = false; switch (edge) { case ElementEdge.BeforeStart: newNode = currentNode.GetPreviousNode(); if (newNode != null) { // Move to next node/last child; if (newNode is TextTreeTextElementNode) { // Move to previous node last child/previous node edge = ElementEdge.BeforeEnd; } else { // Move to previous previous node/previous node. startedAdjacentToTextNode = newNode is TextTreeTextNode; endedAdjacentToTextNode = startedAdjacentToTextNode && newNode.GetPreviousNode() is TextTreeTextNode; } } else { containingNode = currentNode.GetContainingNode(); if (!(containingNode is TextTreeRootNode)) { // Move to parent. newNode = containingNode; } } break; case ElementEdge.AfterStart: newNode = currentNode.GetPreviousNode(); if (newNode != null) { endedAdjacentToTextNode = newNode is TextTreeTextNode; // Move to previous node; edge = ElementEdge.AfterEnd; } else { // Move to inner edge of parent. newNode = currentNode.GetContainingNode(); } break; case ElementEdge.BeforeEnd: newNode = currentNode.GetLastContainedNode(); if (newNode != null) { // Move to penultimate child/last child or inner edge of last child. if (newNode is TextTreeTextElementNode) { edge = ElementEdge.BeforeEnd; } else { startedAdjacentToTextNode = newNode is TextTreeTextNode; endedAdjacentToTextNode = startedAdjacentToTextNode && newNode.GetPreviousNode() is TextTreeTextNode; edge = ElementEdge.BeforeStart; } } else if (currentNode is TextTreeTextElementNode) { // Move to next node. newNode = currentNode; edge = ElementEdge.BeforeStart; } else { Invariant.Assert(currentNode is TextTreeRootNode, "currentNode is expected to be a TextTreeRootNode"); // This is the root node, leave newNode null. } break; case ElementEdge.AfterEnd: newNode = currentNode.GetLastContainedNode(); if (newNode != null) { // Move to inner edge/last child. } else if (currentNode is TextTreeTextElementNode) { // Move to opposite edge. newNode = currentNode; edge = ElementEdge.AfterStart; } else { // Move to previous node. startedAdjacentToTextNode = currentNode is TextTreeTextNode; edge = ElementEdge.AfterStart; goto case ElementEdge.AfterStart; } break; default: Invariant.Assert(false, "Unknown ElementEdge value"); break; } currentNode = newNode; // Multiple text nodes count as a single Move run. // Instead of iterating through N text nodes, exploit // the fact (when we can) that text nodes are only ever contained in // runs with no other content. Jump straight to the start. if (startedAdjacentToTextNode && endedAdjacentToTextNode && plainTextOnly) { newNode = newNode.GetContainingNode(); Invariant.Assert(newNode is TextTreeRootNode); if (edge == ElementEdge.AfterEnd) { edge = ElementEdge.AfterStart; } else { newNode = newNode.GetFirstContainedNode(); Invariant.Assert(newNode != null); Invariant.Assert(edge == ElementEdge.BeforeStart); } break; } } while (startedAdjacentToTextNode && endedAdjacentToTextNode); if (newNode != null) { node = (TextTreeNode)newNode; } return (newNode != null); } internal static TextPointerContext GetPointerContextForward(TextTreeNode node, ElementEdge edge) { TextTreeNode nextNode; TextTreeNode firstContainedNode; TextPointerContext symbolType; switch (edge) { case ElementEdge.BeforeStart: symbolType = node.GetPointerContext(LogicalDirection.Forward); break; case ElementEdge.AfterStart: if (node.ContainedNode != null) { firstContainedNode = (TextTreeNode)node.GetFirstContainedNode(); symbolType = firstContainedNode.GetPointerContext(LogicalDirection.Forward); } else { goto case ElementEdge.BeforeEnd; } break; case ElementEdge.BeforeEnd: // The root node is special, there's no ElementStart/End, so test for null parent. Invariant.Assert(node.ParentNode != null || node is TextTreeRootNode, "Inconsistent node.ParentNode"); symbolType = (node.ParentNode != null) ? TextPointerContext.ElementEnd : TextPointerContext.None; break; case ElementEdge.AfterEnd: nextNode = (TextTreeNode)node.GetNextNode(); if (nextNode != null) { symbolType = nextNode.GetPointerContext(LogicalDirection.Forward); } else { // The root node is special, there's no ElementStart/End, so test for null parent. Invariant.Assert(node.GetContainingNode() != null, "Bad position!"); // Illegal to be at root AfterEnd. symbolType = (node.GetContainingNode() is TextTreeRootNode) ? TextPointerContext.None : TextPointerContext.ElementEnd; } break; default: Invariant.Assert(false, "Unreachable code."); symbolType = TextPointerContext.Text; break; } return symbolType; } // Returns the symbol type preceding thisPosition. internal static TextPointerContext GetPointerContextBackward(TextTreeNode node, ElementEdge edge) { TextPointerContext symbolType; TextTreeNode previousNode; TextTreeNode lastChildNode; switch (edge) { case ElementEdge.BeforeStart: previousNode = (TextTreeNode)node.GetPreviousNode(); if (previousNode != null) { symbolType = previousNode.GetPointerContext(LogicalDirection.Backward); } else { // The root node is special, there's no ElementStart/End, so test for null parent. Invariant.Assert(node.GetContainingNode() != null, "Bad position!"); // Illegal to be at root BeforeStart. symbolType = (node.GetContainingNode() is TextTreeRootNode) ? TextPointerContext.None : TextPointerContext.ElementStart; } break; case ElementEdge.AfterStart: // The root node is special, there's no ElementStart/End, so test for null parent. Invariant.Assert(node.ParentNode != null || node is TextTreeRootNode, "Inconsistent node.ParentNode"); symbolType = (node.ParentNode != null) ? TextPointerContext.ElementStart : TextPointerContext.None; break; case ElementEdge.BeforeEnd: lastChildNode = (TextTreeNode)node.GetLastContainedNode(); if (lastChildNode != null) { symbolType = lastChildNode.GetPointerContext(LogicalDirection.Backward); } else { goto case ElementEdge.AfterStart; } break; case ElementEdge.AfterEnd: symbolType = node.GetPointerContext(LogicalDirection.Backward); break; default: Invariant.Assert(false, "Unknown ElementEdge value"); symbolType = TextPointerContext.Text; break; } return symbolType; } // Inserts an Inline at the current location, adding contextual // elements as needed to enforce the schema. internal void InsertInline(Inline inline) { TextPointer position = this; // Check for hyperlink schema validity first -- we'll throw on an illegal Hyperlink descendent insert. bool isValidChild = TextSchema.ValidateChild(position, /*childType*/inline.GetType(), false /* throwIfIllegalChild */, true /* throwIfIllegalHyperlinkDescendent */); // Now, it is safe to assume that !isValidChild will be the case of incomplete content. if (!isValidChild) { // Ensure text content. position = TextRangeEditTables.EnsureInsertionPosition(this); Invariant.Assert(position.Parent is Run, "EnsureInsertionPosition() must return a position in text content"); Run run = (Run)position.Parent; if (run.IsEmpty) { // Remove the implicit (empty) Run, since we are going to insert an inline at this position. run.RepositionWithContent(null); } else { // Position is parented by Run, split formatting elements to prepare for inserting inline at this position. position = TextRangeEdit.SplitFormattingElement(position, /*keepEmptyFormatting:*/false); } Invariant.Assert(TextSchema.IsValidChild(position, /*childType*/inline.GetType())); } inline.RepositionWithContent(position); } // Helper that returns a DependencyObject which is a common ancestor of two pointers. internal static DependencyObject GetCommonAncestor(TextPointer position1, TextPointer position2) { TextElement element1 = position1.Parent as TextElement; TextElement element2 = position2.Parent as TextElement; DependencyObject commonAncestor; if (element1 == null) { commonAncestor = position1.Parent; } else if (element2 == null) { commonAncestor = position2.Parent; } else { commonAncestor = TextElement.GetCommonAncestor(element1, element2); } return commonAncestor; } #endregion Internal methods //----------------------------------------------------- // // Internal Properties // //----------------------------------------------------- #region Internal Properties // Type ITextPointer.ParentType { get { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); DependencyObject element = this.Parent; return element != null ? element.GetType() : null; } } /// /// Returns the TextContainer that this TextPointer is a part of. /// ITextContainer ITextPointer.TextContainer { get { return this.TextContainer; } } //bool ITextPointer.HasValidLayout { get { return this.HasValidLayout; } } // bool ITextPointer.IsAtCaretUnitBoundary { get { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); // NB: this call might set this.IsCaretUnitBoundaryCacheValid == false. this.ValidateLayout(); if (!this.HasValidLayout) { return false; } if (_layoutGeneration != _tree.LayoutGeneration) { this.IsCaretUnitBoundaryCacheValid = false; } if (!this.IsCaretUnitBoundaryCacheValid) { this.CaretUnitBoundaryCache = _tree.IsAtCaretUnitBoundary(this); _layoutGeneration = _tree.LayoutGeneration; this.IsCaretUnitBoundaryCacheValid = true; } return this.CaretUnitBoundaryCache; } } LogicalDirection ITextPointer.LogicalDirection { get { return this.LogicalDirection; } /* set { this.LogicalDirection = value; } */ } bool ITextPointer.IsAtInsertionPosition { get { return this.IsAtInsertionPosition; } } // bool ITextPointer.IsFrozen { get { return this.IsFrozen; } } // int ITextPointer.Offset { get { return this.Offset; } } // internal int Offset { get { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); return GetSymbolOffset() - 1; } } // Offset in unicode chars within the document. int ITextPointer.CharOffset { get { return this.CharOffset; } } // Offset in unicode chars within the document. internal int CharOffset { get { TextTreeTextElementNode elementNode; _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); int charOffset; switch (this.Edge) { case ElementEdge.BeforeStart: charOffset = _node.GetIMECharOffset(); break; case ElementEdge.AfterStart: charOffset = _node.GetIMECharOffset(); elementNode = _node as TextTreeTextElementNode; if (elementNode != null) { charOffset += elementNode.IMELeftEdgeCharCount; } break; case ElementEdge.BeforeEnd: case ElementEdge.AfterEnd: charOffset = _node.GetIMECharOffset() + _node.IMECharCount; break; default: Invariant.Assert(false, "Unknown value for position edge"); charOffset = 0; break; } return charOffset; } } /// /// Returns the TextContainer that this TextPointer is a part of. /// internal TextContainer TextContainer { get { return _tree; } } ////// A FrameworkElement owning a TextContainer to which this TextPointer belongs. /// internal FrameworkElement ContainingFrameworkElement { get { return ((FrameworkElement)_tree.Parent); } } // Position at row end (immediately before Row closing tag) is a valid stopper for a caret. // Editing operations are restricted here (e.g. typing should automatically jump // to the following character position. // This property identifies such special position. internal bool IsAtRowEnd { get { return TextPointerBase.IsAtRowEnd(this); } } #if DEBUG // Debug-only unique identifier for this instance. int DebugId { get { return _debugId; } } #endif // DEBUG // Indicates if this TextPointer has an ancestor that is not a mergeable (or splittable) Inline element. (e.g. Hyperlink) internal bool HasNonMergeableInlineAncestor { get { Inline ancestor = this.GetNonMergeableInlineAncestor(); return ancestor != null; } } // Returns true if position is at the start boundary of a non-mergeable inline ancestor (hyperlink) internal bool IsAtNonMergeableInlineStart { get { return TextPointerBase.IsAtNonMergeableInlineStart(this); } } // The node referenced by this position. internal TextTreeNode Node { get { return _node; } } // The edge referenced by this position. internal ElementEdge Edge { get { return (ElementEdge)(_flags & (uint)Flags.EdgeMask); } } // Returns the Block parenting this TextPointer, or null if none exists. internal Block ParentBlock { get { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); DependencyObject parentBlock = this.Parent; while (parentBlock is Inline && !(parentBlock is AnchoredBlock)) { parentBlock = ((Inline)parentBlock).Parent; } return parentBlock as Block; } } #endregion Internal Properties //----------------------------------------------------- // // 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); } // Called by the TextPointer ctor. Initializes this instance. private void Initialize(TextContainer tree, TextTreeNode node, ElementEdge edge, LogicalDirection gravity, uint generation, bool caretUnitBoundaryCache, bool isCaretUnitBoundaryCacheValid, uint layoutGeneration) { _tree = tree; // Fixup of the target node based on gravity. // Positions always cling to a node edge that matches their gravity, // so that insert ops never affect the position. RepositionForGravity(ref node, ref edge, gravity); SetNodeAndEdge(node.IncrementReferenceCount(edge), edge); _generation = generation; this.CaretUnitBoundaryCache = caretUnitBoundaryCache; this.IsCaretUnitBoundaryCacheValid = isCaretUnitBoundaryCacheValid; _layoutGeneration = layoutGeneration; VerifyFlags(); tree.AssertTree(); AssertState(); } // Throws an exception if this TextPointer is frozen. private void VerifyNotFrozen() { if (this.IsFrozen) { throw new InvalidOperationException(SR.Get(SRID.TextPositionIsFrozen)); } } // Inc/decs the position ref counts on TextTreeTextNodes as the navigator // is repositioned. // If the new ref is to a TextTreeTextNode, the node may be split. // Returns the actual node referenced, which will always be newNode, // unless newNode is a TextTreeTextNode that gets split. The caller // should use the returned node to position navigators. private TextTreeNode AdjustRefCounts(TextTreeNode newNode, ElementEdge newNodeEdge, TextTreeNode oldNode, ElementEdge oldNodeEdge) { TextTreeNode node; // This test should walk the tree upwards to catch all errors...probably not worth the slowdown though. Invariant.Assert(oldNode.ParentNode == null || oldNode.IsChildOfNode(oldNode.ParentNode), "Trying to add ref a dead node!"); Invariant.Assert(newNode.ParentNode == null || newNode.IsChildOfNode(newNode.ParentNode), "Trying to add ref a dead node!"); node = newNode; if (newNode != oldNode || newNodeEdge != oldNodeEdge) { node = newNode.IncrementReferenceCount(newNodeEdge); oldNode.DecrementReferenceCount(oldNodeEdge); } return node; } // For any logical position (location between two symbols) there are two // possible node/edge pairs. This method choses the pair that fits a // specified gravity, such that future inserts won't require that a text // position be moved, based on its gravity, at the node/edge pair. private static void RepositionForGravity(ref TextTreeNode node, ref ElementEdge edge, LogicalDirection gravity) { SplayTreeNode newNode; ElementEdge newEdge; newNode = node; newEdge = edge; switch (edge) { case ElementEdge.BeforeStart: if (gravity == LogicalDirection.Backward) { newNode = node.GetPreviousNode(); newEdge = ElementEdge.AfterEnd; if (newNode == null) { newNode = node.GetContainingNode(); newEdge = ElementEdge.AfterStart; } } break; case ElementEdge.AfterStart: if (gravity == LogicalDirection.Forward) { newNode = node.GetFirstContainedNode(); newEdge = ElementEdge.BeforeStart; if (newNode == null) { newNode = node; newEdge = ElementEdge.BeforeEnd; } } break; case ElementEdge.BeforeEnd: if (gravity == LogicalDirection.Backward) { newNode = node.GetLastContainedNode(); newEdge = ElementEdge.AfterEnd; if (newNode == null) { newNode = node; newEdge = ElementEdge.AfterStart; } } break; case ElementEdge.AfterEnd: if (gravity == LogicalDirection.Forward) { newNode = node.GetNextNode(); newEdge = ElementEdge.BeforeStart; if (newNode == null) { newNode = node.GetContainingNode(); newEdge = ElementEdge.BeforeEnd; } } break; } node = (TextTreeNode)newNode; edge = newEdge; } // Worker for GetGravity. No parameter validation. private LogicalDirection GetGravityInternal() { return (this.Edge == ElementEdge.BeforeStart || this.Edge == ElementEdge.BeforeEnd) ? LogicalDirection.Forward : LogicalDirection.Backward; } // Returns the DependencyObject scoping this position. private DependencyObject GetDependencyParent() { DebugAssertGeneration(); return GetScopingNode().GetDependencyParent(); } // Returns the node in the direction indicated bordering // a TextPointer, or null if no such node exists. internal TextTreeNode GetAdjacentNode(LogicalDirection direction) { return GetAdjacentNode(_node, this.Edge, direction); } internal static TextTreeNode GetAdjacentNode(TextTreeNode node, ElementEdge edge, LogicalDirection direction) { TextTreeNode adjacentNode; adjacentNode = GetAdjacentSiblingNode(node, edge, direction); if (adjacentNode == null) { // We're the first or last child, try the parent. if (edge == ElementEdge.AfterStart || edge == ElementEdge.BeforeEnd) { adjacentNode = node; } else { adjacentNode = (TextTreeNode)node.GetContainingNode(); } } return adjacentNode; } // Positions this navigator at a node/edge pair. // Node/edge are adjusted based on the current gravity. private void MoveToNode(TextContainer tree, TextTreeNode node, ElementEdge edge) { RepositionForGravity(ref node, ref edge, GetGravityInternal()); _tree = tree; SetNodeAndEdge(AdjustRefCounts(node, edge, _node, this.Edge), edge); _generation = tree.PositionGeneration; } ////// Returns the text element whose edge is in a specified direction /// from position. /// ////// If the symbol in the specified direction is /// TextPointerContext.ElementStart or TextPointerContext.ElementEnd, then this /// method will return the element whose edge preceeds this TextPointer. /// /// Otherwise, the method returns null. /// private TextElement GetElement(LogicalDirection direction) { TextTreeTextElementNode elementNode; DebugAssertGeneration(); elementNode = GetAdjacentTextElementNode(direction); return (elementNode == null) ? null : elementNode.TextElement; } // Invariant.Strict only. Asserts this position has good state. private void AssertState() { if (Invariant.Strict) { // Positions must never have a null tree pointer. Invariant.Assert(_node != null, "Null position node!"); if (GetGravityInternal() == LogicalDirection.Forward) { // Positions with forward gravity must stay at left edges, otherwise inserts could displace them. Invariant.Assert(this.Edge == ElementEdge.BeforeStart || this.Edge == ElementEdge.BeforeEnd, "Bad position edge/gravity pair! (1)"); } else { // Positions with backward gravity must stay at right edges, otherwise inserts could displace them. Invariant.Assert(this.Edge == ElementEdge.AfterStart || this.Edge == ElementEdge.AfterEnd, "Bad position edge/gravity pair! (2)"); } if (_node is TextTreeRootNode) { // Positions may never be at the outer edge of the root node, since you can't insert content there. Invariant.Assert(this.Edge != ElementEdge.BeforeStart && this.Edge != ElementEdge.AfterEnd, "Position at outer edge of root!"); } else if (_node is TextTreeTextNode || _node is TextTreeObjectNode) { // Text and object nodes have no inner edges/chilren, so you can't put a position there. Invariant.Assert(this.Edge != ElementEdge.AfterStart && this.Edge != ElementEdge.BeforeEnd, "Position at inner leaf node edge!"); } else { // Add new asserts for new node types here. Invariant.Assert(_node is TextTreeTextElementNode, "Unknown node type!"); } Invariant.Assert(_tree != null, "Position has no tree!"); #if DEBUG_SLOW // This test is so slow we can't afford to run it even with Invariant.Strict. // It grinds execution to a halt. int count; if (_tree.RootTextBlock == null) { count = 2; // Empty tree has two implicit edge symbols. } else { count = 0; for (TextTreeTextBlock textBlock = (TextTreeTextBlock)_tree.RootTextBlock.ContainedNode.GetMinSibling(); textBlock != null; textBlock = (TextTreeTextBlock)textBlock.GetNextNode()) { Invariant.Assert(textBlock.Count > 0, "Empty TextBlock!"); count += textBlock.Count; } } Invariant.Assert(_tree.InternalSymbolCount == count, "Bad root symbol count!"); Invariant.Assert((_tree.RootNode == null && count == 2) || count == GetNodeSymbolCountSlow(_tree.RootNode), "TextNode symbol count not in synch with tree!"); if (_tree.RootNode != null) { DebugWalkTree(_tree.RootNode.GetMinSibling()); } #endif // DEBUG_SLOW } } #if DEBUG_SLOW // This test is so slow we can't afford to run it even with Invariant.Strict. // It grinds execution to a halt. private static void DebugWalkTree(SplayTreeNode node) { SplayTreeNode previousNode; SplayTreeNode previousPreviousNode; previousNode = null; previousPreviousNode = null; for (; node != null; node = node.GetNextNode()) { if (node.SymbolCount == 0 && previousNode != null && previousNode.SymbolCount == 0 && previousPreviousNode != null && previousPreviousNode.SymbolCount == 0) { Invariant.Assert(false, "Found three consecuative zero length nodes!"); } previousPreviousNode = previousNode; previousNode = node; if (node.ContainedNode != null) { DebugWalkTree(node.ContainedNode.GetMinSibling()); } } } // Debug only. Walks a node and all its children to get a brute force // symbol count. private static int GetNodeSymbolCountSlow(SplayTreeNode node) { SplayTreeNode child; int count; if (node is TextTreeRootNode || node is TextTreeTextElementNode) { count = 2; for (child = node.GetFirstContainedNode(); child != null; child = child.GetNextNode()) { count += GetNodeSymbolCountSlow(child); } } else { Invariant.Assert(node.ContainedNode == null, "Expected leaf node!"); count = node.SymbolCount; } return count; } #endif // DEBUG_SLOW // Repositions the TextPointer and clears any relevant caches. private void SetNodeAndEdge(TextTreeNode node, ElementEdge edge) { Invariant.Assert(edge == ElementEdge.BeforeStart || edge == ElementEdge.AfterStart || edge == ElementEdge.BeforeEnd || edge == ElementEdge.AfterEnd); _node = node; _flags = (_flags & ~(uint)Flags.EdgeMask) | (uint)edge; VerifyFlags(); // Always clear the caret unit boundary cache when we move to a new position. this.IsCaretUnitBoundaryCacheValid = false; } // Setter for the public IsFrozen property. private void SetIsFrozen() { _flags |= (uint)Flags.IsFrozen; VerifyFlags(); } // Ensure we have a valid _flags field. // See bug 1249258. private void VerifyFlags() { ElementEdge edge = (ElementEdge)(_flags & (uint)Flags.EdgeMask); Invariant.Assert(edge == ElementEdge.BeforeStart || edge == ElementEdge.AfterStart || edge == ElementEdge.BeforeEnd || edge == ElementEdge.AfterEnd); } #endregion Private methods // True when the CaretUnitBoundaryCache is ready for use. // If false the cache is not reliable. private bool IsCaretUnitBoundaryCacheValid { get { return (_flags & (uint)Flags.IsCaretUnitBoundaryCacheValid) == (uint)Flags.IsCaretUnitBoundaryCacheValid; } set { _flags = (_flags & ~(uint)Flags.IsCaretUnitBoundaryCacheValid) | (value ? (uint)Flags.IsCaretUnitBoundaryCacheValid : 0); VerifyFlags(); } } // Cached value from this.TextContainer.TextView.IsAtCaretUnitBoundary. private bool CaretUnitBoundaryCache { get { return (_flags & (uint)Flags.CaretUnitBoundaryCache) == (uint)Flags.CaretUnitBoundaryCache; } set { _flags = (_flags & ~(uint)Flags.CaretUnitBoundaryCache) | (value ? (uint)Flags.CaretUnitBoundaryCache : 0); VerifyFlags(); } } //----------------------------------------------------- // // Private Types // //------------------------------------------------------ #region Private Types // Enum used for the _flags bitfield. [Flags] private enum Flags { EdgeMask = 15, // 4 low-order bis are an ElementEdge. IsFrozen = 16, IsCaretUnitBoundaryCacheValid = 32, CaretUnitBoundaryCache = 64, } #endregion Private Types //------------------------------------------------------ // // Private Fields // //----------------------------------------------------- #region Private Fields // The position's TextContainer. private TextContainer _tree; // The node referenced by this position. private TextTreeNode _node; // The value of TextContainer.PositionGeneration the last time this position // called SyncToTreeGeneration. private uint _generation; // The value of TextContainer.LayoutGeneration the last time // this position queried ITextView.IsAtCaretUnitBoundary. private uint _layoutGeneration; // Bitfield used by Edge, IsFrozen, IsCaretUnitBoundaryCacheValid, and // CaretUnitBoundaryCache properties. private uint _flags; #if DEBUG // Debug-only unique identifier for this instance. private readonly int _debugId = _debugIdCounter++; // Debug-only id counter. private static int _debugIdCounter; #endif // DEBUG #endregion Private Fields } } // File provided for Reference Use Only by Microsoft Corporation (c) 2007. // Copyright (c) Microsoft Corporation. All rights reserved. //---------------------------------------------------------------------------- // // File: TextPointer.cs // // Copyright (C) Microsoft Corporation. All rights reserved. // // Description: TextPointer object representing a location in formatted text. // //--------------------------------------------------------------------------- namespace System.Windows.Documents { using System; using MS.Internal; using System.Threading; using System.Windows; using System.Windows.Media; using System.Collections; ////// Represents a location in a formatted text content. /// ////// ///In Avalon formatted text can be contained in elements such as /// ///, , . /// We will refer to these elements as to "text containers". Using the properties and the methods of the TextPointer object, you can: ///a) Find out what kind of content is in forward or in backward directions from its position; ///b) Get a ///scoping or adjacent a position of this TextPointer; c) Get characters preceding or following the TextPointer when it is positioned within text run - ///element; d) Insert characters in a position where the TextPointer is located; ///e) Inspect line layout structure by finding line boundary positions; ///f) Perform visual hit-testing by translating back and forth positions of TextPointer objects into Point objects representing coordinates; ///g) Create an instance of a ///object and use it for formatting, copying, pasting and other editing operations; /// Positions in formatted document where TextPointer objects can be located /// are places between characters and element tags. ///As you edit a document, TextPointer objects do not move relative to their surrounding text. /// That is, if text is inserted before a text pointer, then the offset of the pointer /// from start position of a text container is incremented to reflect its new location /// further down in the document (offsets between text pointers can be calculated by /// a ///method). If multiple TextPointer objects are located at the same position and a text /// is inserted into this position, then the new characters and structural tags are /// to the right or to the left of all of the TextPointer objects depending on their /// ///property. Class ///is an enum specifying what kind of /// content can be found in immediate vicility of a TextPointer. The kinds include /// None for text container boundaries,ElementStart andElementEnd /// for opening and closing tags ofelements, EmbeddedElement /// for UIElements inserted in text as atomic objects. The kind of context can be /// get from a TextPointer using method. TextPointer objects are immutable - they cannot be repositioned in text content /// by any means; and their LogicalDirection property cannot be changed. The context /// around a TextPointer can be changed though, as a result of text editing. /// For instance, when text around a TextPointer is deleted, the TextPointer /// will appear in a new context - in a content remaining after deletion. ///To traverse a document content you can use a bunch of ///Get*Position /// methods -, , etc. /// TextPointer class does not have public constructors. /// The only way to get an instance of the TextPointer class is by /// using properties or methods of other objects: /// ///and , etc. /// and , /// and , etc. /// TextPointer objects can be also produced from other TextPointer objects /// using traversal methods like , /// , , /// etc. TextPointer can be also gotten from a visual coordinate via /// methods like . /// We use a concept of "insertion positions" in association with TextPointer objects, /// which is a key for editor behavior and for various api members. ///When caret travels over text content it can stop only at particular positions, /// skipping all non-appropriate ones. Positions appropriate for caret stopping are called /// "insertion positions". Boundary positions of ///and /// objects are always forcefully set to insertion positions, even if you pass /// arbitrary position in TextRange constructor or method. From TextPointer located at arbitrary (possibly non-insertion) position, you /// can get a TextPointer located at a nearest insertion position by calling /// ///method. To get from one insertion position to another /// you can use method. /// // // Internal comments: // // TextContainer's implementation of the Text OM ITextPointer interface. // // TextPointers represent locations in the TextContainer. They point to a // node/edge pair where operations like insert/remove/gettext take place. // // TextPointers have a property called LogicalDirection, that specifies where // they fall if content is insert at their position. We track LogicalDirection // implicitly: forward direction means the position is always at // BeforeStart/BeforeEnd edges, backward direction the reverse. // // TextPointers are guaranteed to stick with their nodes across editing // operations. For inserts, this happens automatically. However, if the // node a TextPointer points to is removed from the tree, it is expected // that a TextPointer will follow its LogicalDirection to the closest neighbor // node still living in the tree. // // Since we don't store references to TextPointers in the tree itself, // we have to wait until a method on the TextPointer is called, then // check if the position's node is still in the tree. This operation is // called synchronization, and the core method is SyncToTreeGeneration. // // SyncToTreeGeneration must be called on every public entry point before // attempting to use the TextPointer. // // Since positions always point to node/edge pairs, if we want to allocate // a position that references a character not on a node edge, we must split // the text node at the character position. If we did no other work, the // tree could become extremely fragmented, with a text node allocated for // each character. To keep the tree from fragmenting, positions ref count // the nodes they occupy. We do some gymnastics using a finalizer on // TextPointer, adding unreferenced positions to a list we check // periodically in all public TextContainer methods. Dead positions decrement // their nodes' ref counts, and a text node whose ref count drops to zero will // attempt to merge with neighbors. public class TextPointer : ContentPosition, ITextPointer { //----------------------------------------------------- // // Constructors // //----------------------------------------------------- #region Constructors ///Example 0. This code shows how to get an instance of a TextPointer. /// As TextPointer does not have any public constructors, the only way /// of getting a TextPointer is to use a property or method of other object. /// This example ContentStart and ContentEnd properties of main text containers, /// create a TextRange for the whole content of each of them and applies /// Bold formatting to it. ////// void BoldAll(FlowDocument flowDocument, TextFlow textFlow, TextBlock textBlock, RichTextBox richTextBox) /// { /// allContent = new TextRange(flowDocument.ContentStart, flowDocument.ContentEnd); /// allContent.ApplyPropertyValue(TextElement.FontWeightProperty, FontWeights.Bold); /// /// allContent = new TextRange(textFlow.ContentStart, textFlow.ContentEnd); /// allContent.ApplyPropertyValue(TextElement.FontWeightProperty, FontWeights.Bold); /// /// allContent = new TextRange(textBlock.ContentStart, textFlow.ContentEnd); /// allContent.ApplyPropertyValue(TextElement.FontWeightProperty, FontWeights.Bold); /// /// // Note that RichTextBox does not have ContentStart/ContentEnd properties, /// // we use its Document property to get to FlowDocument contained within. /// TextRange allContent = new TextRange(richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd); /// allContent.ApplyPropertyValue(TextElement.FontWeightProperty, FontWeights.Bold); /// } ///
///Example 1. This code shows how to use TextPointer for finding a first Run element /// from a particular position in forard direction. ////// Run FindNextRun(TextPointer position) /// { /// // Traverse content in forward direction until the position is /// // immediately after opening tag of a Run element. /// while (position != null && /// !(position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart /// && /// position.Parent is Run)) /// { /// position = position.GetNextContextPosition(LogicalDirection.Forward); /// } /// /// // Return a result /// return position == null ? null : position.Parent as Run; /// } ///
///Example 2. This code shows how to use TextPointer for finding a particular /// word in text content. This is a simplistic "find" algorithm, not smart enough /// for international issues and for words crossing formatting boundaries. ////// TextPointer FindWord(TextPointer position, string word) /// { /// while (position != null) /// { /// if (position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text) /// { /// string textRun = position.GetTextInRun(LogicalDirection.Forward); /// int indexInRun = textRun.IndexOf(word); /// if (indexInRun >= 0) /// { /// position = position.GetPositionAtOffset(indexInRun); /// break; /// } /// } /// else /// { /// position = position.GetNextContextPosition(LogicalDirection.Forward); /// } /// } /// /// return position; // will be null, if a word is not found. /// } ///
///Example 3. This code shows how to enumerate and count all Paragraphs in a given TextRange. ////// int GetParagraphCount(TextRange range) /// { /// int paragraphCount = 0; /// TextPointer position = range.Start; /// /// while (position != null && position.CompareTo(range.End) < 0) /// { /// if (position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && /// position.Parent is Paragraph) /// { /// // Just entered a paragraph. /// paragraphCount ++; /// /// // Jump over it. /// // Schema does not allow nested paragraphs, so we will not miss any. /// position = ((Paragraph)position.Parent).ElementEnd; /// } /// else /// { /// position = position.GetNextContextPosition(LogicalDirection.Forward); /// } /// } /// /// return paragraphCount; /// } ///
///Example 4. Idenifying whether the document is empty. The document appearing as empty /// in RichTextBox actually contains a Paragraph element with a Run child in it. So checking /// a document emptiness is a bit tricky task. In the following example we will utilize /// the insertion positions as the most natural mechanism for getting to character part or text content. ////// bool IsRichTextBoxEmpty(RichTextBox richTextBox) /// { /// FlowDocument document = richTextBox.Document; // get a document contained in a RichTextBox /// /// TextPointer normalizedStart = document.ContentStart.GetInsertionPosition(LogicalDirection.Forward); /// TextPointer normalizedEnd = document.ContentEnd.GetInsertionPosition(LogicalDirection.Backward); /// /// // The character content is empty if normalized start and end pointers are at the same position /// bool isEmpty = normalizedStart.CompareTo(normalizedEnd) == 0; /// /// return isEmpty; /// } ///
////// Creates a new instance of TextPointer object. /// /// /// TextPointer from which initial properties and location are initialized. /// ////// New TextPointers always have their IsFrozen property set to false, /// regardless of the state of the position parameter. Otherwise the /// new TextPointer instance is identical to the position parameter. /// internal TextPointer(TextPointer textPointer) { if (textPointer == null) { throw new ArgumentNullException("textPointer"); } InitializeOffset(textPointer, 0, textPointer.GetGravityInternal()); } // Creates a new TextPointer instance. internal TextPointer(TextPointer position, int offset) { if (position == null) { throw new ArgumentNullException("position"); } InitializeOffset(position, offset, position.GetGravityInternal()); } // Creates a new TextPointer instance. internal TextPointer(TextPointer position, LogicalDirection direction) { InitializeOffset(position, 0, direction); } // Creates a new TextPointer instance. internal TextPointer(TextPointer position, int offset, LogicalDirection direction) { InitializeOffset(position, offset, direction); } // Creates a new TextPointer instance. internal TextPointer(TextContainer textContainer, int offset, LogicalDirection direction) { SplayTreeNode node; ElementEdge edge; if (offset < 1 || offset > textContainer.InternalSymbolCount - 1) { throw new ArgumentException(SR.Get(SRID.BadDistance)); } textContainer.GetNodeAndEdgeAtOffset(offset, out node, out edge); Initialize(textContainer, (TextTreeNode)node, edge, direction, textContainer.PositionGeneration, false, false, textContainer.LayoutGeneration); } // Creates a new TextPointer instance. internal TextPointer(TextContainer tree, TextTreeNode node, ElementEdge edge) { Initialize(tree, node, edge, LogicalDirection.Forward, tree.PositionGeneration, false, false, tree.LayoutGeneration); } // Creates a new TextPointer instance. internal TextPointer(TextContainer tree, TextTreeNode node, ElementEdge edge, LogicalDirection direction) { Initialize(tree, node, edge, direction, tree.PositionGeneration, false, false, tree.LayoutGeneration); } // Constructor equivalent to ITextPointer.CreatePointer internal TextPointer CreatePointer() { return new TextPointer(this); } // Constructor equivalent to ITextPointer.CreatePointer internal TextPointer CreatePointer(LogicalDirection gravity) { return new TextPointer(this, gravity); } #if REFCOUNT_DEAD_TEXTPOINTERS // *** This code removed *** // The TextContainer originally was designed to ref count TextPointer references // to TextTreeNodes. When a TextPointer is created, it addrefs its node. // When moved, it addrefs the destination and decrements the old position. // When finalized, it would decrement its final TextTreeNode. // // There are two problems with this code: // - The GC will null out managed fields occasionally. This means we simply // cannot use a finalizer. // - We don't really know/can't depend on how expensive it is to use the GC, // and the whole scheme is an attempt at perf optimization. // // The current state of the code is that we still ref count on create and // move, but we've disabled the finalizer so TextPointers will reference // their final nodes "forever". This leads to fragmentation: because // we split TextTreeTextNodes as TextPointer reference individual // characters. However, there's an upper bound on the fragmentation // (we can't have more nodes than characters) and in practice no one // walks documents character by character. // // So, until we identify a specific perf problem, we're not attempting // to ressurect this code. // // If ever do identify fragmentation as a problem worth solving, // we can already think of at least three possible approaches: // // 1. Keep the existing logic, but instead of using a finalizer, // store an array of WeakReferences on each node (usually null). // Periodically check the array, pruning WeakReferences with // null Targets. // 2. As above, but introduce a TextPointerNode instead of hanging // arrays off other nodes. // 3. Keep a static array of TextContainers in memory, ref counted // by TextPointers. Restore the TextPointer finalizer, and in // addition to decrementing the node ref count, decrement the // TextContainer ref count. // This method adds the position to a list of "dead" positions (no // external references) that will be examined later to decrement // reference counts on nodes, and ultimately merge text nodes. // // It's important here that we don't do anything complicated // that might block the finalizer thread or cause too much // contention and hurt perf. The same goes for code in // TextContainer.EmptyDeadPositionList that also uses the lock. ////// ~TextPointer() { ArrayList deadPositionList; deadPositionList = _tree.DeadPositionList; lock (deadPositionList) { deadPositionList.Add(this); } } #endif // REFCOUNT_DEAD_TEXTPOINTERS #endregion Constructors //------------------------------------------------------ // // Public Methods // //----------------------------------------------------- #region Public Methods ////// Returns true if this TextPointer is positioned within the same /// text containner as another TextPointer. /// /// /// TextPointer to compare. /// ////// ///TextPointer objects positioned in different containers cannot /// participate in any operations dealing with several pointers. /// For instance, TextPointer objects from two different text containers /// cannot be compared with each other (by calling the method ///). The purpose of this method is to test whether two TextPointer /// objects belong to the same text container or not. ///Formatted text can be contained within one these elements in Avalon: /// ///, , . /// We refer to them as to "text containers". Note, that if one text container is nested within another /// TextPointer objects positioned within a nested text container /// are not considered as belonging to the enclosing one. ////// public bool IsInSameDocument(TextPointer textPosition) { if (textPosition == null) { throw new ArgumentNullException("textPosition"); } _tree.EmptyDeadPositionList(); return (this.TextContainer == textPosition.TextContainer); } ///Example 1. This example shows how to check whether a given TextPointer /// is positioned between two other TextPointer objects - in a situation /// when there is no guarantee that all three positions belong to /// the same text container ////// bool IsPositionContainedBetween(TextPointer test, TextPointer start, TextPointer end) /// { /// if (!test.IsInSameDocument(start) || !test.IsInSameDocument(end)) /// { /// return false; /// } /// return start.CompareTo(test) <= 0 && test.CompareTo(end) <= 0; /// } ///
////// Compares positions of this TextPointer with another TextPointer. /// /// /// The TextPointer to compare with. /// ////// Less than zero: this TextPointer preceeds position. /// Zero: this TextPointer is at the same location as position. /// Greater than zero: this TextPointer follows position. /// ////// Throws ArgumentException if position does not belong to the same /// text container as this TextPointer (you can use 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; } ////// method to detect whether comparison is possible). /// /// Returns the type of content to one side of this TextPointer. /// /// /// Direction to query. /// ////// ///Returns ///if this TextPointer /// is positioned at the beginning of a text container and the requested direction /// is , or if it is positioned /// at the end of a text container and the requested direction is /// . Returns ///if the TextPointer /// has an openenig tag of some of TextElements in the requested direction. Returns ///if the TextPointer /// has a closing tag of some of TextElements in the requested direction. Returns ///if the TextPointer /// is positioned within element and has some non-emty sequence of characters /// in requested direction. Returns ///is the TextPointer /// is positioned within or /// element and has as atomic symbol in a requested direction. /// public TextPointerContext GetPointerContext(LogicalDirection direction) { ValidationHelper.VerifyDirection(direction, "direction"); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); return (direction == LogicalDirection.Forward) ? GetPointerContextForward(_node, this.Edge) : GetPointerContextBackward(_node, this.Edge); } ///This example shows how to use ///GetPointerContext method in text content /// traversal algorithms. It implements an algorithm calculating a balanse of /// opening and closing tags between two TextPointer positions (each opening tag /// counted as +1, while a closing one as -1)./// int GetElementTagBalance(TextPointer start, TextPointer end) /// { /// int balanse = 0; /// /// while (start != null && start.CompareTo(end) < 0) /// { /// TextPointerContext forwardContext = start.GetPointerContext(LogicalDirection.Forward); /// /// if (forwardContext == TextPointerContext.ElementStart) /// { /// balanse++; /// } /// else if (forwardContext == TextPointerContext.ElementEnd) /// { /// balanse--; /// } /// start = start.GetNextContextPosition(LogicalDirection.Forward); /// } /// /// return balanse; /// } ///
////// Returns the count of Unicode characters between this TextPointer and the /// edge of an element in the given direction. /// /// /// Direction to query. /// ////// If the TetPointer is positioned not inside a public int GetTextRunLength(LogicalDirection direction) { ValidationHelper.VerifyDirection(direction, "direction"); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); int count = 0; // Combine adjacent text nodes into a single run. // This isn't just a perf optimization. Because text positions // split text nodes, if we just returned a single node's text // callers would see strange side effects where position.GetTextLength() != // position.GetText if a position is moved between the calls. if (_tree.PlainTextOnly) { // Optimize for TextBox, which only ever contains (sometimes // very large quantities of) text nodes. Invariant.Assert(this.GetScopingNode() is TextTreeRootNode); if (direction == LogicalDirection.Forward) { count = _tree.InternalSymbolCount - this.GetSymbolOffset() - 1; } else { count = this.GetSymbolOffset() - 1; } } else { TextTreeNode textNode = GetAdjacentTextNodeSibling(direction); while (textNode != null) { count += textNode.SymbolCount; textNode = ((direction == LogicalDirection.Forward) ? textNode.GetNextNode() : textNode.GetPreviousNode()) as TextTreeTextNode; } } return count; } ///element, /// then the method always returns zero. /// /// Returns the distance between this TextPointer and another. /// /// /// TextPointer to compare. /// ////// Throws an ArgumentException if the TextPointer position is not /// positioned within the same document as this TextPointer. /// ////// ///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. ////// ///The distance is represented as a number of "symbols" /// between these two pointers. ///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. ///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 ////// the class. Each 16-bit unicode character inside a ///element /// is considered as one symbol. 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 ///Button /// element considered as one symbol even though it is represented /// by two tags and two characters./// public int GetOffsetToPosition(TextPointer position) { _tree.EmptyDeadPositionList(); ValidationHelper.VerifyPosition(_tree, position); SyncToTreeGeneration(); position.SyncToTreeGeneration(); return (position.GetSymbolOffset() - GetSymbolOffset()); } ///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. ////// 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)); /// } /// ///
////// Returns text bordering this TextPointer from one side or another. /// /// /// Direction to query. /// ////// See GetTextInRun(direction, textBuffer, startIndex, count) method /// remarks for semantics of the returned text. /// ////// public string GetTextInRun(LogicalDirection direction) { ValidationHelper.VerifyDirection(direction, "direction"); return TextPointerBase.GetTextInRun(this, direction); } ///This is an example of simplistic plain text converter. /// This algorithm produces a string concatenating all text runs /// between two TextPointers. ///Note that this is really simplistic algorithm. You sould use /// ///property for more sophisticated /// plain text conversion. /// string GetPlainText(TextPointer start, TextPointer end) /// { /// StringBuilder buffer = new StringBuilder(); /// /// while (start != null && start.CompareTo(end) < end) /// { /// if (start.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text) /// { /// // Check if this text run reaches beyond the end position /// // and trancate the string if needed. /// string textRun = start.GetTextInRum(LogicalDirection.Forward); /// if (textRun.Length > start.GetOffsetToPosition(end)) /// { /// textRun = textRun.Substring(0, start.GetOffsetToPosition(end)); /// } /// /// // Add characters from this text run to output buffer. /// buffer.Add(textRun); /// } /// /// start = start.GetNextContextPosition(LogicalDirection.Forward); /// // Note that for text run this method skips the whole run, not just one character. /// } /// return buffer.ToString(); /// } ///
////// Copies characters bordering this TextPointer into a caller supplied char array. /// /// /// Direction to query. /// /// /// Buffer into which chars are copied. /// /// /// Index within the textBuffer array at which the copy is started. /// /// /// The maximum number of characters to copy. Must be less than /// or equal to a (textBuffer.Length - startIndex ). /// ////// The count of chars actually copied. /// ////// Is thrown in the following cases: (a) when ///startIndex is less than zero, /// (b) whenstartIndex is greater thantextBuffer.Length , /// (c) whencount is less than zero, (d) whencount /// is greater than size available for copying (textBuffer.Length - startIndex ). ////// This method only returns uninterrupted runs of text -- no text will /// be returned if any symbol type other than text borders this /// TextPointer in the specified direction. Similarly, text will only /// be returned up to the next non-text symbol. /// public int GetTextInRun(LogicalDirection direction, char[] textBuffer, int startIndex, int count) { TextTreeTextNode textNode; ValidationHelper.VerifyDirection(direction, "direction"); SyncToTreeGeneration(); textNode = GetAdjacentTextNodeSibling(direction); return textNode == null ? 0 : GetTextInRun(_tree, GetSymbolOffset(), textNode, -1, direction, textBuffer, startIndex, count); } ////// Returns an element represented by a symbol, if any, bordering /// this TextPointer in the specified direction. /// /// /// Direction to query. /// ////// The element if its opening or closing tag exists /// in a specified direction. Otherwize returns null. /// ////// public DependencyObject GetAdjacentElement(LogicalDirection direction) { ValidationHelper.VerifyDirection(direction, "direction"); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); return GetAdjacentElement(_node, this.Edge, direction); } ///The returned element may be both a ////// and a . /// object will be returned when /// this TextPointer is located before or after of either opening /// or closing tag in appropriate direction. /// object can be returned only when /// the pointer is located outside its opening or closing tag - within /// or . /// Returns a TextPointer at a new position by a specified symbol /// count. /// /// /// Number of symbols to advance. offset may be negative, in which /// case the TextPointer is moved backwards. /// ////// TextPointer located at requested position in case if requested position /// does exist, otherwize returns null. LogicalDirection of the TextPointer /// returned is the same as of this TexPointer. /// ////// ///This method, like all other TextPointer methods, defines a symbol /// as one of: ///- 16 bit Unicode character. ///- opening or closing tag of a ///. - the whole ///as atomic embedded object. /// public TextPointer GetPositionAtOffset(int offset) { return GetPositionAtOffset(offset, this.LogicalDirection); } ///This example shows how to use this method for creating TextPointers /// from a persisted index-based position representation. /// The first method returns a integer offset of a TextPointer /// from the beginning of a Paragraph. The second method re-creates /// a pointer from an integer ofset at the same relative position. ////// int GetPersistedPositionRelativeToParagraph(TextPointer position) /// { /// Paragraph paragraph = position.Paragraph; /// /// if (paragraph == null) /// { /// return 0; // Some positions may be not within any Paragraph, /// // so we need to return something; or throw exception. /// } /// /// return paragraph.ContentStart.GetOffsetToPosition(position); /// } /// /// int GetTextPointerRelativeToParagraph(Paragraph paragraph, int persistedPositionRelativeToParagraph) /// { /// // Check whether persisted position is still within this paragraph /// if (persistedPositionRelativeToParagraph > /// paragraph.ContentStart.GetOffsetToPosition(paragraph.ContentEnd)) /// { /// // the index is beyond the paragraph end. Return the farthest position within the paragraph. /// return paragraph.ContentEnd; /// } /// /// return paragraph.ContentStart.GetPositionAtOffset(persistedPositionRelativeToParagraph); /// } ///
////// Returns a TextPointer at a new position by a specified symbol /// count. /// /// /// Number of symbols to advance. offset may be negative, in which /// case the TextPointer is moved backwards. /// /// /// LogicalDirection desired for a returned TextPointer. /// ////// TextPointer located at requested position in case if requested position /// does exist, otherwize returns null. LogicalDirection of the TextPointer /// returned is as specified by a ///. /// /// public TextPointer GetPositionAtOffset(int offset, LogicalDirection direction) { TextPointer position = new TextPointer(this, direction); int actualCount = position.MoveByOffset(offset); if (actualCount == offset) { position.Freeze(); return position; } else { return null; } } ///This method, like all other TextPointer methods, defines a symbol /// as one of: ///- 16 bit Unicode character. ///- opening or closing tag of a ///. - the whole ///as atomic embedded object. See examples in ///method with one parameter. /// Returns a pointer at the next symbol in a specified /// direction, or past all following Unicode characters if the /// bordering content is Unicode text. /// /// /// Direction to move. /// ////// TextPointer in a requested direction, null if this TextPointer /// borders the start or end of the document. /// ////// ///If the following symbol is of type EmbeddedElement, ElementStart, /// or ElementEnd (as returned by the GetPointerContext method), then /// the TextPointer is advanced by exactly one symbol. ///If the following symbol is of type Text, then the TextPointer is /// advanced until it passes all following text (ie, until it reaches /// a position with a different return value for GetPointerContext). /// The exact symbol count crossed can be calculated in advance by /// calling GetTextLength. ///If there is no following symbol (start or end of the document), /// then the method returns null. ////// public TextPointer GetNextContextPosition(LogicalDirection direction) { return (TextPointer)((ITextPointer)this).GetNextContextPosition(direction); } ///This example shows how to use this method for traversing /// text content and examine its structure. The method implements /// a simplistic text content serializer, producing an xml-looking /// text. ///Note that to produce really well formed xml System.Xml /// interfaces must be used. We use this simplification only /// to make it more readable for people not familiar with System.Xml api. ////// string GetXaml(TextElement element) /// { /// StringBuilder buffer = new StringBuilder(); /// /// // Position a "navigator" pointer before the opening tag of the element. /// TextPointer navigator = element.ElementStart; /// /// while (navigator.CompareTo(element.ElementEnd) < 0) /// { /// switch (navigator.GetPointerContext(LogicalDirection.Forward)) /// { /// case TextPointerContext.ElementStart : /// // Output opening tag of the TextElement /// buffer.AddFormat("<{0}>", navigator.GetAdjacentElement(LogicalDirection.Forward).GetType().Name); /// break; /// case TextPointerContext.ElementEnd : /// // Output closing tag of the TextElement /// buffer.AddFormat("</{0}>", navigator.GetAdjacentElement(LogicalDirection.Forward).GetType().Name); /// break; /// case TextPointerContent.EmbeddedElement : /// // Output simple tag for embedded element /// buffer.AddFormat("<{0}/>", navigator.GetAdjacentElement(LogicalDirection.Forward).GetType().Name); /// break; /// case TextPointerContext.Text : /// // Output the text content of thi text run /// buffer.Add(navigator.GetTextInRun(LoigcalDirection.Forward); /// break; /// case TextPointerContext.None : /// Assert(false, "We do not expect to reach end of text container in this loop"); /// break; /// } /// /// // Advance the naviagtor to the next context position. /// navigator = navigator.GetNextContextPosition(LogicalDirection.Forward); /// /// Assert(navigator != null, "We do not expect to reach an end of a text container in this loop, as it is limited by element.ContentEnd bounadry"); /// } /// } ///
////// Returns a TextPointer at the closest insertion position in a /// specified direction. /// /// /// Direction to search a closest insertion position. /// ////// TextPointer positioned at inserion point. The value is never null. /// ////// ///The concept of insertion position is a convenience /// for traversing text content across structural boundaries, /// between table cells, paragraphs, list items etc. ///An insertion position is anywhere the containing document /// would normally place the caret. Examples of positions that are not /// insertion positions include locations between Paragraphs /// (between closing tag of a preceding paragraph and an opening tag /// of the following paragraph). A position within text runs /// in the middle of a surrogate Unicode surrogate pair is also /// not an insertion position. ///The method can be used for disambiguating insertion positions /// in two cases: when the text has two insertion positions separated by /// a sequence of formatting tags, as between "d" and "t" in this /// markup: "<Bold>Bold</Bold>text" - we have an insertion position /// before closing tag of Bold element and immediately after it. Both are /// valid insertion position and caret would stop on each of them /// depending on the direction of keyboard navigation. The method /// GetInsertionPosition allows user to pick one or another /// without moving to the "next" insertion position. ///Another important case when the method is useful is /// when a sequence of structural tags is involved. If you /// have a position, say between closing and opening paragraph tags, /// and want to fing a nearest insertion position the ///direction /// parameter will tell which of two possible positions to take: /// in the end of the preceding or in the begining of the following paragraph.If the pointer is already at insertion position /// but there is a non-empty sequence formatting in the given direction, /// then the position after all formatting tags will be returned. ///If the pointer is already at insertion position /// and there is no any formatting tags in the given direction, /// then the returned position is the same as this one. ///Somethimes the whole document does not have even /// one insertion position - it happens when the content /// is structurally incomplete, say in empty ////// or element. In such case the method /// will return the original position even though it is not /// an insertion position. The method never returns null. /// public TextPointer GetInsertionPosition(LogicalDirection direction) { return (TextPointer)((ITextPointer)this).GetInsertionPosition(direction); } // Used for pointer normalization in cases when direction does not matter. internal TextPointer GetInsertionPosition() { return GetInsertionPosition(LogicalDirection.Forward); } ///This example shows how to use the method ///GetInsertionPosition /// as a convenience of finding a starting "editable" position./// bool IsElementEmpty(TextElement element) /// { /// // Find first and last insertion positions in this element. /// // We use inward directions to make sure that insertion position /// // will be found correctly in case when the element is inline formatting one /// // (i.e. Run or Span). /// TextPointer start = element.ContentStart.GetInsertionPosition(LogicalDirection.Forward); /// TextPointer end = element.ContentEnd.GetInsertionPosition(LogicalDirection.Backward); /// /// // Element has empty printable content if its first and last /// // insertion positions are equal. /// return start.CompareTo(end) == 0; /// } ///
////// Returns a TextPointer in the direction indicated to the following /// insertion position. /// /// /// Direction to move. /// ////// A TextPointer at an insertion position in a requested direction, /// null if there is no more insertion positions in that direction. /// ////// ///The concept of insertion position is a convenience /// for traversing text content across structural boundaries, /// between table cells, paragraphs, list items etc. ///See more detailed definition of the concept of /// "insertion position" in the ////// method. If the TextPointer is not currently at an insertion position, this /// method will move the TextPointer to the next insertion position in /// the indicated direction, just like the MoveToInsertionPosition /// method. ///If the TextPointer is currently at an insertion position, this /// method will move the TextPointer to following insertion position, /// if the end of document is not encountered. ////// public TextPointer GetNextInsertionPosition(LogicalDirection direction) { return (TextPointer)((ITextPointer)this).GetNextInsertionPosition(direction); } ///In this example we use the method ///GetNextInsertionPosition /// for passing over structural boundaries in a proces of /// enumerating allin a range. /// int GetParagraphCount(TextPointer start, TextPointer end) /// { /// int paragraphCount = 0; /// /// while (start != null && start.CompareTo(end) < 0) /// { /// Paragraph paragraph = start.Paragraph; /// /// if (paragraph != null) /// { /// paragraphCount++; /// /// // Advance start to an end of the paragraph found /// start = paragraph.ContentEnd; /// } /// /// // Use GetNextInsertionPosition method to skip a sequence /// // of structural tags /// start = start.GetNextInsertionPosition(LogicalDirection.Forward); /// } /// /// return paragraphCount; /// } ///
////// Returns a TextPointer at the start of line after skipping /// a given number of line starts in forward or backward direction. /// /// /// Number of line starts to skip when finding a desired line start position. /// Negative values specify preceding lines, zero specifies the current line, /// positive values specify following lines. /// ////// Throws an InvalidOperationException if this TextPointer's HasValidLayout /// property is set false. Without a calculated layout it is not possible /// to position relative to rendered lines. /// ////// TextPointer positioned at the begining of a line requested /// (with LogicalDirection set to Forward). /// If there is no sufficient lines in requested direction, /// returns null. /// ////// public TextPointer GetLineStartPosition(int count) { int actualCount; TextPointer lineStartPosition = GetLineStartPosition(count, out actualCount); return (actualCount != count) ? null : lineStartPosition; } ///Line identification is possible only from normalized insertion positions; /// Line identification from not-normalized positions is mbigous and can produce /// unexpected results. Say, if a position is between closing and opening /// Paragraph tags, then GetInsertionPosition(LogicalDirection) is needed /// to decide whether we start from the end of previous Paragraph or /// from the start of the following one. Without such call /// ///If this TextPointer is at an otherwise ambiguous position, exactly /// between two lines, the LogicalDirection property is used to determine /// current position. So a TextPointer with backward LogicalDirection /// is considered to be at the end of line, and calling MoveToLineBoundary(0) /// would reposition it at the start of the preceding line. Making the /// same call with forward LogicalDirection would leave the TextPointer /// positioned where it started -- at the start of the following line. /// ////// Returns a TextPointer at the start of line after skipping /// a given number of line starts in forward or backward direction. /// /// /// Offset of the destination line. Negative values specify preceding /// lines, zero specifies the current line, positive values specify /// following lines. /// /// /// The offset of the line moved to. This value may be less than /// requested if the beginning or end of document is encountered. /// ////// TextPointer positioned at the begining of a line requested /// (with LogicalDirection set to Forward). /// If there is no sufficient lines in requested direction, /// returns a position at the beginning of a farthest line /// in this direction. In such case out parameter actualCount /// gets a number of lines actually skipped. /// Unlike the other override in this case the returned pointer is never null. /// ////// If this TextPointer is at an otherwise ambiguous position, exactly /// between two lines, the LogicalDirection property is used to determine /// current position. So a TextPointer with backward LogicalDirection /// is considered to be at the end of line, and calling MoveToLineBoundary(0) /// would reposition it at the start of the preceding line. Making the /// same call with forward LogicalDirection would leave the TextPointer /// positioned where it started -- at the start of the following line. /// public TextPointer GetLineStartPosition(int count, out int actualCount) { this.ValidateLayout(); TextPointer position = new TextPointer(this); if (this.HasValidLayout) { actualCount = position.MoveToLineBoundary(count); } else { actualCount = 0; } position.SetLogicalDirection(LogicalDirection.Forward); position.Freeze(); return position; } ////// Returns the bounding box of the content bordering this TextPointer /// in a specified direction. /// /// /// Direction of content. /// ////// public Rect GetCharacterRect(LogicalDirection direction) { ValidationHelper.VerifyDirection(direction, "direction"); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); this.ValidateLayout(); if (!this.HasValidLayout) { return Rect.Empty; } return TextPointerBase.GetCharacterRect(this, direction); } ///TextElement edges are not considered content for the purposes of /// this method. If the TextPointer is positioned before a TextElement /// edge, the return value will be the bounding box of the next /// non-TextElement content. ///If there is no content in the specified direction, a zero-width /// Rect is returned with height matching the preceding content. ////// Inserts text at this TextPointer's position. /// /// /// Text to insert. /// ////// The LogicalDirection property specifies whether this TextPointer /// will be positioned before or after the new text. /// public void InsertTextInRun(string textData) { if (textData == null) { throw new ArgumentNullException("textData"); } _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); TextPointer insertPosition; if (TextSchema.IsInTextContent(this)) { insertPosition = this; } else { insertPosition = TextRangeEditTables.EnsureInsertionPosition(this); } _tree.BeginChange(); try { _tree.InsertTextInternal(insertPosition, textData); } finally { _tree.EndChange(); } } ////// Deletes text in Run at this TextPointer's position /// ////// /// Number of characters to delete. /// Positive count deletes text following this TextPointer in Run. /// Negative count deletes text preceding this TextPointer in Run. /// /// /// Returns the actual count of deleted chars. /// The actual count may be less than requested in cases /// when original requested count exceeds text run length in given direction. /// public int DeleteTextInRun(int count) { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); // TextSchema Validation if (!TextSchema.IsInTextContent(this)) { return 0; } // Direction to delete text in run LogicalDirection direction = count < 0 ? LogicalDirection.Backward : LogicalDirection.Forward; // Get text run length in given direction int maxDeleteCount = this.GetTextRunLength(direction); // Truncate count if it extends past the run in given direction if (count > 0 && count > maxDeleteCount) { count = maxDeleteCount; } else if (count < 0 && count < -maxDeleteCount) { count = -maxDeleteCount; } // Get a new pointer for deletion TextPointer deleteToPosition = new TextPointer(this, count); _tree.BeginChange(); try { if (count > 0) { _tree.DeleteContentInternal(this, deleteToPosition); } else if (count < 0) { _tree.DeleteContentInternal(deleteToPosition, this); } } finally { _tree.EndChange(); } return count; } ////// Inserts a TextElement at this TextPointer's position. /// /// /// ContentElement to insert. /// ////// The LogicalDirection property specifies whether this TextPointer /// will be positioned before or after the TextElement. /// ////// Throws ArgumentException is textElement is not valid /// according to flow schema. /// ////// Throws InvalidOperationException if textElement cannot be inserted /// at this position because it belongs to another tree. /// internal void InsertTextElement(TextElement textElement) { Invariant.Assert(textElement != null); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); ValidationHelper.ValidateChild(this, textElement, "textElement"); if (textElement.Parent != null) { throw new InvalidOperationException(SR.Get(SRID.TextPointer_CannotInsertTextElementBecauseItBelongsToAnotherTree)); } textElement.RepositionWithContent(this); } ////// Insert a paragraph break at this position by splitting all elements upto its paragraph ancestor. /// ////// When this position has a paragraph parent, this method returns a /// normalized position in the beginning of a second paragraph. /// /// Otherwise, if the position is not parented by a paragraph /// (for special insertion positions such as table row end, BlockUIContainer boundaries, etc), /// this method creates a paragraph by using rules of EnsureInsertionPosition() /// and returns a normalized position at the start of the paragraph created. /// ////// Throws InvalidOperationException when this position has a non-splittable ancestor such as Hyperlink, /// since we cannot successfully split upto the parent paragraph in this case. /// public TextPointer InsertParagraphBreak() { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); Type containerType = this.TextContainer.Parent.GetType(); if (!TextSchema.IsValidChildOfContainer(containerType, typeof(Paragraph))) { throw new InvalidOperationException(SR.Get(SRID.TextSchema_IllegalElement, "Paragraph", containerType)); } Inline ancestor = this.GetNonMergeableInlineAncestor(); if (ancestor != null) { // Cannot split a hyperlink element! throw new InvalidOperationException(SR.Get(SRID.TextSchema_CannotSplitElement, ancestor.GetType().Name)); } TextPointer position; _tree.BeginChange(); try { position = TextRangeEdit.InsertParagraphBreak(this, /*moveIntoSecondParagraph:*/true); } finally { _tree.EndChange(); } return position; } ////// Insert a line break at this position. /// If the position is parented by a Run, the Run element is split at this position and then a line break inserted. /// ////// TextPointer positioned immediately after the closing tag of /// a public TextPointer InsertLineBreak() { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); TextPointer position; _tree.BeginChange(); try { position = TextRangeEdit.InsertLineBreak(this); } finally { _tree.EndChange(); } return position; } ///element inserted by this method. /// /// Debug only ToString override. /// public override string ToString() { #if DEBUG return "TextPointer Id=" + _debugId + " NodeId=" + _node.DebugId + " Edge=" + this.Edge; #else return base.ToString(); #endif // DEBUG } #endregion Public Methods //------------------------------------------------------ // // Public Properties // //------------------------------------------------------ #region Public Properties ////// Returns true if layout is calculated at the current position. /// ////// Methods that depend on layout -- GetLineStartPosition, /// GetCharacterRect, and IsAtLineStartPosition -- will attempt /// to re-calculate a dirty layout when called. Recalculating /// layout can be extremely expensive, however, and this method /// lets the caller detect when layout is dirty. /// // Internal methods that depend on this property: // - MoveToNextCaretPosition // - MoveToBackspaceCaretPosition public bool HasValidLayout { get { return _tree.TextView == null ? false : _tree.TextView.IsValid && _tree.TextView.Contains(this); } } ////// Specifies whether the TextPointer is associated with preceding or /// following content. /// ////// public LogicalDirection LogicalDirection { get { return GetGravityInternal(); } } ///If new content is insert at the TextPointer's current position, it /// will move to the edge of the new content that also borders its /// original associated content. ////// Returns the logical parent scoping this TextPointer. /// public DependencyObject Parent { get { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); return GetLogicalTreeNode(); } } ////// Returns true if this TextPointer is positioned at an insertion /// position. /// ////// public bool IsAtInsertionPosition { get { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); return TextPointerBase.IsAtInsertionPosition(this); } } ///An "insertion position" is a position where where the containing document /// would normally place the caret. Examples of positions that are not /// insertion positions include spaces between Paragraphs, or between /// Unicode surrogate pairs. ////// Returns true if this TextPointer is positioned at the start of a /// line. /// ////// Throws an InvalidOperationException if this TextPointer's HasValidLayout /// property is set false. Without a calculated layout it is not possible /// to determine where the current line starts or ends. /// ////// public bool IsAtLineStartPosition { get { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); this.ValidateLayout(); if (!this.HasValidLayout) { return false; } TextSegment lineRange = _tree.TextView.GetLineRange(this); // Null lineRange if no layout is available. if (!lineRange.IsNull) { TextPointer position = new TextPointer(this); TextPointerContext backwardContext = position.GetPointerContext(LogicalDirection.Backward); // Skip past any formatting. while ((backwardContext == TextPointerContext.ElementStart || backwardContext == TextPointerContext.ElementEnd) && TextSchema.IsFormattingType(position.GetAdjacentElement(LogicalDirection.Backward).GetType())) { position.MoveToNextContextPosition(LogicalDirection.Backward); backwardContext = position.GetPointerContext(LogicalDirection.Backward); } if (position.CompareTo((TextPointer)lineRange.Start) <= 0) { return true; } } return false; } } ///If this TextPointer is at an otherwise ambiguous position, exactly /// between two lines, the LogicalDirection property is used to determine /// current position. So a TextPointer with backward LogicalDirection /// will never have a true IsAtLineStartPosition unless it is positioned at the /// head of a document. ///This property is always false when HasValidLayout is false. ////// Returns the paragraph scoping this textpointer /// ////// public Paragraph Paragraph { get { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); return this.ParentBlock as Paragraph; } } ///When TextPointer is at insertion position it usually /// have non-null paragraph. The only exception is when /// it is positioned at the end of TableRow, where /// there is no scoping paragraph. ///When TextPointer is positioned outside of a paragraph, /// the property returns null. ////// Returns the paragraph-like parent of the pointer /// ////// If we would have a common base class for Paragraph and BlockUIContainer, /// we would return it here. /// internal Block ParagraphOrBlockUIContainer { // get { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); Block parentBlock = this.ParentBlock; return (parentBlock is Paragraph) || (parentBlock is BlockUIContainer) ? parentBlock : null; } } ////// The start position of the document's content /// ////// public TextPointer DocumentStart { get { return TextContainer.Start; } } ///This property may be useful as a base for persistent /// position indexing - for calculating offsets /// to all other pointers. ///The ///property for this /// position is not a TextElement - it is a text container, /// which can be one of , , /// . /// The end position of the document's content. /// ////// public TextPointer DocumentEnd { get { return TextContainer.End; } } #endregion Public Properties //----------------------------------------------------- // // Internal Methods // //------------------------------------------------------ #region Internal Methods // Returns this TextPointer's topmost Inline ancestor, which is not a mergeable (or splittable) Inline element. (e.g. Hyperlink) internal Inline GetNonMergeableInlineAncestor() { Inline ancestor = this.Parent as Inline; while (ancestor != null && TextSchema.IsMergeableInline(ancestor.GetType())) { ancestor = ancestor.Parent as Inline; } return ancestor; } // Returns this TextPointer's closest ListItem ancestor. internal ListItem GetListAncestor() { TextElement ancestor = this.Parent as TextElement; while (ancestor != null && !(ancestor is ListItem)) { ancestor = ancestor.Parent as TextElement; } return ancestor as ListItem; } internal static int GetTextInRun(TextContainer textContainer, int symbolOffset, TextTreeTextNode textNode, int nodeOffset, LogicalDirection direction, char[] textBuffer, int startIndex, int count) { int skipCount; int finalCount; if (textBuffer == null) { throw new ArgumentNullException("textBuffer"); } if (startIndex < 0) { throw new ArgumentException(SR.Get(SRID.NegativeValue, "startIndex")); } if (startIndex > textBuffer.Length) { throw new ArgumentException(SR.Get(SRID.StartIndexExceedsBufferSize, startIndex, textBuffer.Length)); } if (count < 0) { throw new ArgumentException(SR.Get(SRID.NegativeValue, "count")); } if (count > textBuffer.Length - startIndex) { throw new ArgumentException(SR.Get(SRID.MaxLengthExceedsBufferSize, count, textBuffer.Length, startIndex)); } Invariant.Assert(textNode != null, "textNode is expected to be non-null"); textContainer.EmptyDeadPositionList(); if (nodeOffset < 0) { skipCount = 0; } else { skipCount = (direction == LogicalDirection.Backward) ? nodeOffset : textNode.SymbolCount - nodeOffset; symbolOffset += nodeOffset; } finalCount = 0; // Loop and combine adjacent text nodes into a single run. // This isn't just a perf optimization. Because text positions // split text nodes, if we just returned a single node's text // callers would see strange side effects where position.GetTextLength() != // position.GetText() if another position is moved between the calls. while (textNode != null) { // Never return more textBuffer than the text following this position in the current text node. finalCount += Math.Min(count - finalCount, textNode.SymbolCount - skipCount); skipCount = 0; if (finalCount == count) break; textNode = ((direction == LogicalDirection.Forward) ? textNode.GetNextNode() : textNode.GetPreviousNode()) as TextTreeTextNode; } // If we're reading backwards, need to fixup symbolOffset to point into the node. if (direction == LogicalDirection.Backward) { symbolOffset -= finalCount; } if (finalCount > 0) // We may not have allocated textContainer.RootTextBlock if no text was ever inserted. { TextTreeText.ReadText(textContainer.RootTextBlock, symbolOffset, finalCount, textBuffer, startIndex); } return finalCount; } internal static DependencyObject GetAdjacentElement(TextTreeNode node, ElementEdge edge, LogicalDirection direction) { TextTreeNode adjacentNode; DependencyObject element; adjacentNode = GetAdjacentNode(node, edge, direction); if (adjacentNode is TextTreeObjectNode) { element = ((TextTreeObjectNode)adjacentNode).EmbeddedElement; } else if (adjacentNode is TextTreeTextElementNode) { element = ((TextTreeTextElementNode)adjacentNode).TextElement; } else { // We're adjacent to a text node, or have no sibling in the specified direction. element = null; } return element; } ///The ///property for this /// position is not a TextElement - it is a text container, /// which can be one of , , /// . /// Moves this TextPointer to another TextPointer's position. /// /// /// Position to move to. /// ////// Throws an ArgumentException if textPosition is not /// positioned within the same document. /// ////// Throws an InvalidOperationException if this TextPointer's IsFrozen /// property is set true. Frozen TextPointers may not be repositioned. /// internal void MoveToPosition(TextPointer textPosition) { ValidationHelper.VerifyPosition(_tree, textPosition); VerifyNotFrozen(); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); textPosition.SyncToTreeGeneration(); MoveToNode(_tree, textPosition.Node, textPosition.Edge); } ////// Advances this TextPointer to a new position by a specified symbol /// count. /// /// /// Number of symbols to advance. offset may be negative, in which /// case the TextPointer is moved backwards. /// ////// Throws an InvalidOperationException if this TextPointer's IsFrozen /// property is set true. Frozen TextPointers may not be repositioned. /// ////// This method, like all other TextPointer methods, defines a symbol /// as a /// - 16 bit Unicode character. /// - TextElement start or end edge. /// - UIElement. /// - ContentElement other than TextElement. /// ////// The number of symbols actually advanced. The absolute value of the /// count returned may be less than requested if the end of document is /// encountered while advancing. /// internal int MoveByOffset(int offset) { SplayTreeNode node; ElementEdge edge; int symbolOffset; int currentOffset; VerifyNotFrozen(); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); if (offset != 0) { currentOffset = GetSymbolOffset(); symbolOffset = unchecked(currentOffset + offset); if (symbolOffset < 1) { if (offset > 0) { // Rolled past Int32.MaxValue. Go to end of doc. symbolOffset = _tree.InternalSymbolCount - 1; offset = symbolOffset - currentOffset; } else { // Underflow. Go to start of doc. offset += (1 - symbolOffset); symbolOffset = 1; } } else if (symbolOffset > _tree.InternalSymbolCount - 1) { // Overflow. Go to end of doc. // NB: there's no symmetric check here for rolling under with distance=Int32.MinValue. // Since GetSymbolOffset is always positive, we can't roll-around with a min value. offset -= (symbolOffset - (_tree.InternalSymbolCount - 1)); symbolOffset = _tree.InternalSymbolCount - 1; } _tree.GetNodeAndEdgeAtOffset(symbolOffset, out node, out edge); MoveToNode(_tree, (TextTreeNode)node, edge); } return offset; } ////// Advances this TextPointer to the next symbol in a specified /// direction, or past all following Unicode characters if the /// bordering content is Unicode text. /// /// /// Direction to move. /// ////// Throws an InvalidOperationException if this TextPointer's IsFrozen /// property is set true. Frozen TextPointers may not be repositioned. /// ////// true if the TextPointer is repositioned, false if the TextPointer /// borders the start or end of the document. /// ////// If the following symbol is of type EmbeddedElement, ElementStart, /// or ElementEnd (as returned by the GetPointerContext method), then /// the TextPointer is advanced by exactly one symbol. /// /// If the following symbol is of type Text, then the TextPointer is /// advanced until it passes all following text (ie, until it reaches /// a position with a different return value for GetPointerContext). /// The exact symbol count crossed can be calculated in advance by /// calling GetTextLength. /// /// If there is no following symbol (start or end of the document), /// then the method does nothing and returns false. /// internal bool MoveToNextContextPosition(LogicalDirection direction) { TextTreeNode node; ElementEdge edge; bool moved; ValidationHelper.VerifyDirection(direction, "direction"); VerifyNotFrozen(); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); if (direction == LogicalDirection.Forward) { moved = GetNextNodeAndEdge(out node, out edge); } else { moved = GetPreviousNodeAndEdge(out node, out edge); } if (moved) { SetNodeAndEdge(AdjustRefCounts(node, edge, _node, this.Edge), edge); DebugAssertGeneration(); } AssertState(); return moved; } ////// Moves this TextPointer to the closest insertion position in a /// specified direction. If the pointer is already at insertion point /// but there is a non-empty sequence formatting in the given direction, /// then the position moves to the other instance of this insertion /// position. /// /// /// Direction to move. /// ////// Throws an InvalidOperationException if this TextPointer's IsFrozen /// property is set true. Frozen TextPointers may not be repositioned. /// ////// An "insertion position" is a position where new content may be added /// without breaking any semantic rules of the containing document. /// /// In practice, an insertion position is anywhere the containing document /// would normally place the caret. Examples of positions that are not /// insertion positions include spaces between Paragraphs, or between /// Unicode surrogate pairs. /// ////// True if the TextPointer is repositioned, false otherwise. /// internal bool MoveToInsertionPosition(LogicalDirection direction) { ValidationHelper.VerifyDirection(direction, "direction"); VerifyNotFrozen(); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); return TextPointerBase.MoveToInsertionPosition(this, direction); } ////// Advances this TextPointer in the direction indicated to the following /// insertion position. /// /// /// Direction to move. /// ////// Throws an InvalidOperationException if this TextPointer's IsFrozen /// property is set true. Frozen TextPointers may not be repositioned. /// ////// An "insertion position" is a position where new content may be added /// without breaking any semantic rules of the containing document. /// /// In practice, an insertion position is anywhere the containing document /// would normally place the caret. Examples of positions that are not /// insertion positions include spaces between Paragraphs, or between /// Unicode surrogate pairs. /// /// If the TextPointer is not currently at an insertion position, this /// method will move the TextPointer to the next insertion position in /// the indicated direction, just like the MoveToInsertionPosition /// method. /// /// If the TextPointer is currently at an insertion position, this /// method will move the TextPointer to following insertion position, /// if the end of document is not encountered. /// ////// True if the TextPointer is repositioned, false otherwise. /// internal bool MoveToNextInsertionPosition(LogicalDirection direction) { ValidationHelper.VerifyDirection(direction, "direction"); VerifyNotFrozen(); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); return TextPointerBase.MoveToNextInsertionPosition(this, direction); } ////// Advances this TextPointer to the start of a neighboring line. /// /// /// Offset of the destination line. Negative values specify preceding /// lines, zero specifies the current line, positive values specify /// following lines. /// ////// Throws an InvalidOperationException if this TextPointer's IsFrozen /// property is set true. Frozen TextPointers may not be repositioned. /// ////// The offset of the line moved to. This value may be less than /// requested if the beginning or end of document is encountered. /// ////// If this TextPointer is at an otherwise ambiguous position, exactly /// between two lines, the LogicalDirection property is used to determine /// current position. So a TextPointer with backward LogicalDirection /// is considered to be at the end of line, and calling MoveToLineBoundary(0) /// would reposition it at the start of the preceding line. Making the /// same call with forward LogicalDirection would leave the TextPointer /// positioned where it started -- at the start of the following line. /// internal int MoveToLineBoundary(int count) { VerifyNotFrozen(); this.ValidateLayout(); if (!this.HasValidLayout) { return 0; } _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); return TextPointerBase.MoveToLineBoundary(this, _tree.TextView, count); } ////// Inserts a UIElement at this TextPointer's position. /// /// /// UIElement to insert. /// ////// The LogicalDirection property specifies whether this TextPointer /// will be positioned before or after the UIElement. /// ////// Throws ArgumentException is contentElement is not valid /// according to flow schema. /// internal void InsertUIElement(UIElement uiElement) { if (uiElement == null) { throw new ArgumentNullException("uiElement"); } _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); ValidationHelper.ValidateChild(this, uiElement, "uiElement"); if (!((TextElement)this.Parent).IsEmpty) // the parent may be InlineUIContainer or BlockUIContainer { throw new InvalidOperationException(SR.Get(SRID.TextSchema_UIElementNotAllowedInThisPosition)); } _tree.BeginChange(); try { _tree.InsertEmbeddedObjectInternal(this, uiElement); } finally { _tree.EndChange(); } } // internal TextElement GetAdjacentElementFromOuterPosition(LogicalDirection direction) { TextTreeTextElementNode elementNode; _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); elementNode = GetAdjacentTextElementNodeSibling(direction); return (elementNode == null) ? null : elementNode.TextElement; } ////// Sets the logical direction of this textpointer. /// ////// Throws an InvalidOperationException if this TextPointer's Freeze() method has been called. /// /// internal void SetLogicalDirection(LogicalDirection direction) { SplayTreeNode newNode; ElementEdge edge; ValidationHelper.VerifyDirection(direction, "direction"); VerifyNotFrozen(); _tree.EmptyDeadPositionList(); if (GetGravityInternal() != direction) { SyncToTreeGeneration(); newNode = _node; // We need to shift nodes to match the new gravity. switch (this.Edge) { case ElementEdge.BeforeStart: newNode = _node.GetPreviousNode(); if (newNode != null) { // Move to the previous sibling. edge = ElementEdge.AfterEnd; } else { // Move to parent inner edge. newNode = _node.GetContainingNode(); Invariant.Assert(newNode != null, "Bad tree state: newNode must be non-null (BeforeStart)"); edge = ElementEdge.AfterStart; } break; case ElementEdge.AfterStart: newNode = _node.GetFirstContainedNode(); if (newNode != null) { // Move to first child. edge = ElementEdge.BeforeStart; } else { // Move to opposite edge. newNode = _node; edge = ElementEdge.BeforeEnd; } break; case ElementEdge.BeforeEnd: newNode = _node.GetLastContainedNode(); if (newNode != null) { // Move to last child. edge = ElementEdge.AfterEnd; } else { // Move to opposite edge. newNode = _node; edge = ElementEdge.AfterStart; } break; case ElementEdge.AfterEnd: newNode = _node.GetNextNode(); if (newNode != null) { // Move to the next sibling. edge = ElementEdge.BeforeStart; } else { // Move to parent inner edge. newNode = _node.GetContainingNode(); Invariant.Assert(newNode != null, "Bad tree state: newNode must be non-null (AfterEnd)"); edge = ElementEdge.BeforeEnd; } break; default: Invariant.Assert(false, "Bad ElementEdge value"); edge = this.Edge; break; } SetNodeAndEdge(AdjustRefCounts((TextTreeNode)newNode, edge, _node, this.Edge), edge); Invariant.Assert(GetGravityInternal() == direction, "Inconsistent position gravity"); } } ////// True if the Freeze method has been called, in which case /// this TextPointer is immutable and may not be repositioned. /// ////// By default, TextPointers are mutable -- they may be /// repositioned with calls to methods like MoveByOffset, and /// LogicalDirection may be changed freely. After Freeze is /// called, a TextPointer is locked down -- any attempt to set /// LogicalDirection or call repositioning methods will raise an /// InvalidOperationException. /// internal bool IsFrozen { get { _tree.EmptyDeadPositionList(); return (_flags & (uint)Flags.IsFrozen) == (uint)Flags.IsFrozen; } } ////// Makes this TextPointer immutable. /// ////// By default, TextPointers are mutable -- they may be /// repositioned with calls to methods like MoveByOffset, and /// LogicalDirection may be changed freely. After this method is /// called, a TextPointer is locked down -- any attempt to set /// LogicalDirection or call repositioning methods will raise an /// InvalidOperationException. /// /// The IsFrozen property will return true after this method is called. /// /// Calling Freeze multiple times has no additional effect. /// internal void Freeze() { _tree.EmptyDeadPositionList(); SetIsFrozen(); } ////// Returns an immutable TextPointer instance positioned equally to /// this one, with a specified LogicalDirection. /// /// /// LogicalDirection of the returned TextPointer. /// ////// The TextPointer returned will always have its IsFrozen property set /// true. /// /// The return value will be a new TextPointer instance unless this /// TextPointer is already frozen with a matching LogicalDirection, in /// which case this TextPointer will be returned. /// internal TextPointer GetFrozenPointer(LogicalDirection logicalDirection) { ValidationHelper.VerifyDirection(logicalDirection, "logicalDirection"); _tree.EmptyDeadPositionList(); return (TextPointer)TextPointerBase.GetFrozenPointer(this, logicalDirection); } void ITextPointer.SetLogicalDirection(LogicalDirection direction) { SetLogicalDirection(direction); } int ITextPointer.CompareTo(ITextPointer position) { return CompareTo((TextPointer)position); } int ITextPointer.CompareTo(StaticTextPointer position) { int offsetThis; int offsetPosition; int result; offsetThis = this.Offset + 1; offsetPosition = TextContainer.GetInternalOffset(position); if (offsetThis < offsetPosition) { result = -1; } else if (offsetThis > offsetPosition) { result = +1; } else { result = 0; } return result; } int ITextPointer.GetOffsetToPosition(ITextPointer position) { return GetOffsetToPosition((TextPointer)position); } TextPointerContext ITextPointer.GetPointerContext(LogicalDirection direction) { return GetPointerContext(direction); } int ITextPointer.GetTextRunLength(LogicalDirection direction) { return GetTextRunLength(direction); } //string ITextPointer.GetTextInRun(LogicalDirection direction) { return TextPointerBase.GetTextInRun(this, direction); } int ITextPointer.GetTextInRun(LogicalDirection direction, char[] textBuffer, int startIndex, int count) { return GetTextInRun(direction, textBuffer, startIndex, count); } object ITextPointer.GetAdjacentElement(LogicalDirection direction) { return GetAdjacentElement(direction); } Type ITextPointer.GetElementType(LogicalDirection direction) { DependencyObject element; ValidationHelper.VerifyDirection(direction, "direction"); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); element = GetElement(direction); return element != null ? element.GetType() : null; } bool ITextPointer.HasEqualScope(ITextPointer position) { TextTreeNode parent1; TextTreeNode parent2; TextPointer textPointer; _tree.EmptyDeadPositionList(); ValidationHelper.VerifyPosition(_tree, position); textPointer = (TextPointer)position; SyncToTreeGeneration(); textPointer.SyncToTreeGeneration(); parent1 = GetScopingNode(); parent2 = textPointer.GetScopingNode(); return (parent1 == parent2); } // Candidate for replacing MoveToNextContextPosition for immutable TextPointer model ITextPointer ITextPointer.GetNextContextPosition(LogicalDirection direction) { ITextPointer pointer = ((ITextPointer)this).CreatePointer(); if (pointer.MoveToNextContextPosition(direction)) { pointer.Freeze(); } else { pointer = null; } return pointer; } // Candidate for replacing MoveToInsertionPosition for immutable TextPointer model ITextPointer ITextPointer.GetInsertionPosition(LogicalDirection direction) { ITextPointer pointer = ((ITextPointer)this).CreatePointer(); pointer.MoveToInsertionPosition(direction); pointer.Freeze(); return pointer; } // Returns the closest insertion position, treating all unicode code points // as valid insertion positions. A useful performance win over // GetNextInsertionPosition when only formatting scopes are important. ITextPointer ITextPointer.GetFormatNormalizedPosition(LogicalDirection direction) { ITextPointer pointer = ((ITextPointer)this).CreatePointer(); TextPointerBase.MoveToFormatNormalizedPosition(pointer, direction); pointer.Freeze(); return pointer; } // Candidate for replacing MoveToNextInsertionPosition for immutable TextPointer model ITextPointer ITextPointer.GetNextInsertionPosition(LogicalDirection direction) { ITextPointer pointer = ((ITextPointer)this).CreatePointer(); if (pointer.MoveToNextInsertionPosition(direction)) { pointer.Freeze(); } else { pointer = null; } return pointer; } object ITextPointer.GetValue(DependencyProperty formattingProperty) { DependencyObject parent; object val; if (formattingProperty == null) { throw new ArgumentNullException("formattingProperty"); } _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); parent = GetDependencyParent(); if (parent == null) { val = DependencyProperty.UnsetValue; } else { val = parent.GetValue(formattingProperty); } return val; } object ITextPointer.ReadLocalValue(DependencyProperty formattingProperty) { TextElement element; if (formattingProperty == null) { throw new ArgumentNullException("formattingProperty"); } _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); element = this.Parent as TextElement; if (element == null) { throw new InvalidOperationException(SR.Get(SRID.NoScopingElement, "This TextPointer")); } return element.ReadLocalValue(formattingProperty); } LocalValueEnumerator ITextPointer.GetLocalValueEnumerator() { DependencyObject element; _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); element = this.Parent as TextElement; if (element == null) { // return (new DependencyObject()).GetLocalValueEnumerator(); } return element.GetLocalValueEnumerator(); } ITextPointer ITextPointer.CreatePointer() { return ((ITextPointer)this).CreatePointer(0, this.LogicalDirection); } StaticTextPointer ITextPointer.CreateStaticPointer() { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); return new StaticTextPointer(_tree, _node, _node.GetOffsetFromEdge(this.Edge)); } ITextPointer ITextPointer.CreatePointer(int offset) { return ((ITextPointer)this).CreatePointer(offset, this.LogicalDirection); } ITextPointer ITextPointer.CreatePointer(LogicalDirection gravity) { return ((ITextPointer)this).CreatePointer(0, gravity); } ITextPointer ITextPointer.CreatePointer(int offset, LogicalDirection gravity) { return new TextPointer(this, offset, gravity); } // void ITextPointer.Freeze() { Freeze(); } ITextPointer ITextPointer.GetFrozenPointer(LogicalDirection logicalDirection) { return GetFrozenPointer(logicalDirection); } // Worker for Min, accepts any ITextPointer. bool ITextPointer.MoveToNextContextPosition(LogicalDirection direction) { return MoveToNextContextPosition(direction); } int ITextPointer.MoveByOffset(int offset) { return MoveByOffset(offset); } void ITextPointer.MoveToPosition(ITextPointer position) { MoveToPosition((TextPointer)position); } void ITextPointer.MoveToElementEdge(ElementEdge edge) { MoveToElementEdge(edge); } internal void MoveToElementEdge(ElementEdge edge) { TextTreeTextElementNode elementNode; ValidationHelper.VerifyElementEdge(edge, "edge"); VerifyNotFrozen(); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); elementNode = GetScopingNode() as TextTreeTextElementNode; if (elementNode == null) { throw new InvalidOperationException(SR.Get(SRID.NoScopingElement, "This TextNavigator")); } MoveToNode(_tree, elementNode, edge); } // int ITextPointer.MoveToLineBoundary(int count) { return MoveToLineBoundary(count); } // Rect ITextPointer.GetCharacterRect(LogicalDirection direction) { return GetCharacterRect(direction); } bool ITextPointer.MoveToInsertionPosition(LogicalDirection direction) { return MoveToInsertionPosition(direction); } bool ITextPointer.MoveToNextInsertionPosition(LogicalDirection direction) { return MoveToNextInsertionPosition(direction); } // The caret methods are debug only until we actually start to use them. // #if DEBUG /// /// internal bool MoveToCaretPosition(LogicalDirection contentDirection) { TextPointer position; LogicalDirection oppositeDirection; bool moved; ValidationHelper.VerifyDirection(contentDirection, "contentDirection"); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); VerifyNotFrozen(); this.ValidateLayout(); if (!this.HasValidLayout) { return false; } moved = false; if (!_tree.TextView.IsAtCaretUnitBoundary(this)) { oppositeDirection = (contentDirection == LogicalDirection.Forward) ? LogicalDirection.Backward : LogicalDirection.Forward; position = (TextPointer)_tree.TextView.GetNextCaretUnitPosition(this, oppositeDirection); MoveToPosition(position); moved = true; } return moved; } ////// internal bool MoveToNextCaretPosition(LogicalDirection direction) { TextPointer position; bool moved; ValidationHelper.VerifyDirection(direction, "direction"); _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); VerifyNotFrozen(); this.ValidateLayout(); if (!this.HasValidLayout) { return false; } position = (TextPointer)_tree.TextView.GetNextCaretUnitPosition(this, direction); moved = false; if (this.CompareTo(position) != 0) { MoveToPosition(position); moved = true; } return moved; } ////// internal bool MoveToBackspaceCaretPosition() { TextPointer position; bool moved; _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); VerifyNotFrozen(); this.ValidateLayout(); if (!this.HasValidLayout) { return false; } position = (TextPointer)_tree.TextView.GetBackspaceCaretUnitPosition(this); moved = false; if (this.CompareTo(position) != 0) { MoveToPosition(position); moved = true; } return moved; } #endif void ITextPointer.InsertTextInRun(string textData) { this.InsertTextInRun(textData); } // void ITextPointer.DeleteContentToPosition(ITextPointer limit) { _tree.BeginChange(); try { // DeleteContent is clever enough to handle the this > limit case. TextRangeEditTables.DeleteContent(this, (TextPointer)limit); } finally { _tree.EndChange(); } } ///bool ITextPointer.ValidateLayout() { return this.ValidateLayout(); } /// internal bool ValidateLayout() { return TextPointerBase.ValidateLayout(this, _tree.TextView); } // Returns the TextTreeTextNode in the direction indicated bordering // a TextPointer, or null if no such node exists. internal TextTreeTextNode GetAdjacentTextNodeSibling(LogicalDirection direction) { return GetAdjacentSiblingNode(direction) as TextTreeTextNode; } // Returns the TextTreeTextNode in the direction indicated bordering // a TextPointer, or null if no such node exists. internal static TextTreeTextNode GetAdjacentTextNodeSibling(TextTreeNode node, ElementEdge edge, LogicalDirection direction) { return GetAdjacentSiblingNode(node, edge, direction) as TextTreeTextNode; } // Returns the TextTreeTextNode in the direction indicated bordering // a TextPointer, or null if no such node exists. internal TextTreeTextElementNode GetAdjacentTextElementNodeSibling(LogicalDirection direction) { return GetAdjacentSiblingNode(direction) as TextTreeTextElementNode; } // Returns the TextTreeTextNode in the direction indicated bordering // a TextPointer, or null if no such node exists. internal TextTreeTextElementNode GetAdjacentTextElementNode(LogicalDirection direction) { return GetAdjacentNode(direction) as TextTreeTextElementNode; } // Returns the sibling node (ie, node in the same scope) in the direction indicated bordering // a TextPointer, or null if no such node exists. internal TextTreeNode GetAdjacentSiblingNode(LogicalDirection direction) { DebugAssertGeneration(); return GetAdjacentSiblingNode(_node, this.Edge, direction); } internal static TextTreeNode GetAdjacentSiblingNode(TextTreeNode node, ElementEdge edge, LogicalDirection direction) { SplayTreeNode sibling; if (direction == LogicalDirection.Forward) { switch (edge) { case ElementEdge.BeforeStart: sibling = node; break; case ElementEdge.AfterStart: sibling = node.GetFirstContainedNode(); break; case ElementEdge.BeforeEnd: default: sibling = null; break; case ElementEdge.AfterEnd: sibling = node.GetNextNode(); break; } } else // direction == LogicalDirection.Backward { switch (edge) { case ElementEdge.BeforeStart: sibling = node.GetPreviousNode(); break; case ElementEdge.AfterStart: default: sibling = null; break; case ElementEdge.BeforeEnd: sibling = node.GetLastContainedNode(); break; case ElementEdge.AfterEnd: sibling = node; break; } } return (TextTreeNode)sibling; } // Returns the symbol offset within the TextContainer of this Position. internal int GetSymbolOffset() { DebugAssertGeneration(); return GetSymbolOffset(_tree, _node, this.Edge); } // Returns the symbol offset within the TextContainer of this Position. internal static int GetSymbolOffset(TextContainer tree, TextTreeNode node, ElementEdge edge) { int offset; switch (edge) { case ElementEdge.BeforeStart: offset = node.GetSymbolOffset(tree.Generation); break; case ElementEdge.AfterStart: offset = node.GetSymbolOffset(tree.Generation) + 1; break; case ElementEdge.BeforeEnd: offset = node.GetSymbolOffset(tree.Generation) + node.SymbolCount - 1; break; case ElementEdge.AfterEnd: offset = node.GetSymbolOffset(tree.Generation) + node.SymbolCount; break; default: Invariant.Assert(false, "Unknown value for position edge"); offset = 0; break; } return offset; } // Returns the Logical Tree Node scoping this position. internal DependencyObject GetLogicalTreeNode() { DebugAssertGeneration(); return GetScopingNode().GetLogicalTreeNode(); } // Updates the position state if the node referenced by this position has // been removed from the TextContainer. This method must be called before // referencing the position's state when a public entry point is called. internal void SyncToTreeGeneration() { SplayTreeNode node; SplayTreeNode searchNode; SplayTreeNode parentNode; SplayTreeNode splayNode; ElementEdge edge; TextTreeFixupNode fixup = null; // If the tree hasn't had any deletions since the last time we // checked there's no work to do. if (_generation == _tree.PositionGeneration) return; // Invalidate the caret unit boundary cache -- the surrounding // content may have changed. this.IsCaretUnitBoundaryCacheValid = false; node = _node; edge = this.Edge; // If we can find a fixup node in the ancestor chain, this position // needs to be updated. // // It's possible to have cascading deletes -- some content was // deleted, then the nodes pointed to by a fixup node were themselves // deleted, and so forth. So we have to keep checking all the // way up to the root. while (true) { searchNode = node; splayNode = node; while (true) { parentNode = searchNode.ParentNode; if (parentNode == null) // The root node is always valid. break; fixup = parentNode as TextTreeFixupNode; if (fixup != null) break; if (searchNode.Role == SplayTreeNodeRole.LocalRoot) { splayNode.Splay(); splayNode = parentNode; } searchNode = parentNode; } if (parentNode == null) break; // Checked all the way to the root, position is valid. // If we make it here we've found a fixup node. Our gravity // tells us which direction to follow it. if (GetGravityInternal() == LogicalDirection.Forward) { if (edge == ElementEdge.BeforeStart && fixup.FirstContainedNode != null) { // We get here if and only if a single TextElementNode was removed. // Because only a single element was removed, we don't have to worry // about whether the position was originally in some contained content. // It originally pointed to the extracted node, so we can always // move to contained content. node = fixup.FirstContainedNode; Invariant.Assert(edge == ElementEdge.BeforeStart, "edge BeforeStart is expected"); } else { node = fixup.NextNode; edge = fixup.NextEdge; } } else { if (edge == ElementEdge.AfterEnd && fixup.LastContainedNode != null) { // We get here if and only if a single TextElementNode was removed. // Because only a single element was removed, we don't have to worry // about whether the position was originally in some contained content. // It originally pointed to the extracted node, so we can always // move to contained content. node = fixup.LastContainedNode; Invariant.Assert(edge == ElementEdge.AfterEnd, "edge AfterEnd is expected"); } else { node = fixup.PreviousNode; edge = fixup.PreviousEdge; } } } // Note we intentionally don't call AdjustRefCounts here. // We already incremented ref counts when the old target // node was deleted. SetNodeAndEdge((TextTreeNode)node, edge); // Update the position generation, so we don't do this work again // until the tree changes. _generation = _tree.PositionGeneration; AssertState(); } // Returns the logical parent node of a text position. internal TextTreeNode GetScopingNode() { return GetScopingNode(_node, this.Edge); } internal static TextTreeNode GetScopingNode(TextTreeNode node, ElementEdge edge) { TextTreeNode scopingNode; switch (edge) { case ElementEdge.BeforeStart: case ElementEdge.AfterEnd: scopingNode = (TextTreeNode)node.GetContainingNode(); break; case ElementEdge.AfterStart: case ElementEdge.BeforeEnd: default: scopingNode = node; break; } return scopingNode; } // Debug only -- asserts this TextPointer is synchronized to the current tree generation. internal void DebugAssertGeneration() { Invariant.Assert(_generation == _tree.PositionGeneration, "TextPointer not synchronized to tree generation!"); } internal bool GetNextNodeAndEdge(out TextTreeNode node, out ElementEdge edge) { DebugAssertGeneration(); return GetNextNodeAndEdge(_node, this.Edge, _tree.PlainTextOnly, out node, out edge); } // Finds the next run, returned as a node/edge pair. // Returns false if there is no following run, in which case node/edge will match the input position. // The returned node/edge pair respects the input position's gravity. internal static bool GetNextNodeAndEdge(TextTreeNode sourceNode, ElementEdge sourceEdge, bool plainTextOnly, out TextTreeNode node, out ElementEdge edge) { SplayTreeNode currentNode; SplayTreeNode newNode; SplayTreeNode nextNode; SplayTreeNode containingNode; bool startedAdjacentToTextNode; bool endedAdjacentToTextNode; node = sourceNode; edge = sourceEdge; newNode = node; currentNode = node; // If we started next to a TextTreeTextNode, and the next node // is also a TextTreeTextNode, then skip past the second node // as well -- multiple text nodes count as a single Move run. do { startedAdjacentToTextNode = false; endedAdjacentToTextNode = false; switch (edge) { case ElementEdge.BeforeStart: newNode = currentNode.GetFirstContainedNode(); if (newNode != null) { // Move to inner edge/first child. } else if (currentNode is TextTreeTextElementNode) { // Move to inner edge. newNode = currentNode; edge = ElementEdge.BeforeEnd; } else { // Move to next node. startedAdjacentToTextNode = currentNode is TextTreeTextNode; edge = ElementEdge.BeforeEnd; goto case ElementEdge.BeforeEnd; } break; case ElementEdge.AfterStart: newNode = currentNode.GetFirstContainedNode(); if (newNode != null) { // Move to first child/second child or first child/first child child if (newNode is TextTreeTextElementNode) { edge = ElementEdge.AfterStart; } else { startedAdjacentToTextNode = newNode is TextTreeTextNode; endedAdjacentToTextNode = newNode.GetNextNode() is TextTreeTextNode; edge = ElementEdge.AfterEnd; } } else if (currentNode is TextTreeTextElementNode) { // Move to next node. newNode = currentNode; edge = ElementEdge.AfterEnd; } else { Invariant.Assert(currentNode is TextTreeRootNode, "currentNode is expected to be TextTreeRootNode"); // This is the root node, leave newNode null. } break; case ElementEdge.BeforeEnd: newNode = currentNode.GetNextNode(); if (newNode != null) { // Move to next node; endedAdjacentToTextNode = newNode is TextTreeTextNode; edge = ElementEdge.BeforeStart; } else { // Move to inner edge of parent. newNode = currentNode.GetContainingNode(); } break; case ElementEdge.AfterEnd: nextNode = currentNode.GetNextNode(); startedAdjacentToTextNode = nextNode is TextTreeTextNode; newNode = nextNode; if (newNode != null) { // Move to next node/first child; if (newNode is TextTreeTextElementNode) { edge = ElementEdge.AfterStart; } else { // Move to next node/next next node. endedAdjacentToTextNode = newNode.GetNextNode() is TextTreeTextNode; } } else { containingNode = currentNode.GetContainingNode(); if (!(containingNode is TextTreeRootNode)) { // Move to parent. newNode = containingNode; } } break; default: Invariant.Assert(false, "Unknown ElementEdge value"); break; } currentNode = newNode; // Multiple text nodes count as a single Move run. // Instead of iterating through N text nodes, exploit // the fact (when we can) that text nodes are only ever contained in // runs with no other content. Jump straight to the end. if (startedAdjacentToTextNode && endedAdjacentToTextNode && plainTextOnly) { newNode = newNode.GetContainingNode(); Invariant.Assert(newNode is TextTreeRootNode); if (edge == ElementEdge.BeforeStart) { edge = ElementEdge.BeforeEnd; } else { newNode = newNode.GetLastContainedNode(); Invariant.Assert(newNode != null); Invariant.Assert(edge == ElementEdge.AfterEnd); } break; } } while (startedAdjacentToTextNode && endedAdjacentToTextNode); if (newNode != null) { node = (TextTreeNode)newNode; } return (newNode != null); } internal bool GetPreviousNodeAndEdge(out TextTreeNode node, out ElementEdge edge) { DebugAssertGeneration(); return GetPreviousNodeAndEdge(_node, this.Edge, _tree.PlainTextOnly, out node, out edge); } // Finds the previous run, returned as a node/edge pair. // Returns false if there is no preceding run, in which case node/edge will match the input position. // The returned node/edge pair respects the input positon's gravity. internal static bool GetPreviousNodeAndEdge(TextTreeNode sourceNode, ElementEdge sourceEdge, bool plainTextOnly, out TextTreeNode node, out ElementEdge edge) { SplayTreeNode currentNode; SplayTreeNode newNode; SplayTreeNode containingNode; bool startedAdjacentToTextNode; bool endedAdjacentToTextNode; node = sourceNode; edge = sourceEdge; newNode = node; currentNode = node; // If we started next to a TextTreeTextNode, and the next node // is also a TextTreeTextNode, then skip past the second node // as well -- multiple text nodes count as a single Move run. do { startedAdjacentToTextNode = false; endedAdjacentToTextNode = false; switch (edge) { case ElementEdge.BeforeStart: newNode = currentNode.GetPreviousNode(); if (newNode != null) { // Move to next node/last child; if (newNode is TextTreeTextElementNode) { // Move to previous node last child/previous node edge = ElementEdge.BeforeEnd; } else { // Move to previous previous node/previous node. startedAdjacentToTextNode = newNode is TextTreeTextNode; endedAdjacentToTextNode = startedAdjacentToTextNode && newNode.GetPreviousNode() is TextTreeTextNode; } } else { containingNode = currentNode.GetContainingNode(); if (!(containingNode is TextTreeRootNode)) { // Move to parent. newNode = containingNode; } } break; case ElementEdge.AfterStart: newNode = currentNode.GetPreviousNode(); if (newNode != null) { endedAdjacentToTextNode = newNode is TextTreeTextNode; // Move to previous node; edge = ElementEdge.AfterEnd; } else { // Move to inner edge of parent. newNode = currentNode.GetContainingNode(); } break; case ElementEdge.BeforeEnd: newNode = currentNode.GetLastContainedNode(); if (newNode != null) { // Move to penultimate child/last child or inner edge of last child. if (newNode is TextTreeTextElementNode) { edge = ElementEdge.BeforeEnd; } else { startedAdjacentToTextNode = newNode is TextTreeTextNode; endedAdjacentToTextNode = startedAdjacentToTextNode && newNode.GetPreviousNode() is TextTreeTextNode; edge = ElementEdge.BeforeStart; } } else if (currentNode is TextTreeTextElementNode) { // Move to next node. newNode = currentNode; edge = ElementEdge.BeforeStart; } else { Invariant.Assert(currentNode is TextTreeRootNode, "currentNode is expected to be a TextTreeRootNode"); // This is the root node, leave newNode null. } break; case ElementEdge.AfterEnd: newNode = currentNode.GetLastContainedNode(); if (newNode != null) { // Move to inner edge/last child. } else if (currentNode is TextTreeTextElementNode) { // Move to opposite edge. newNode = currentNode; edge = ElementEdge.AfterStart; } else { // Move to previous node. startedAdjacentToTextNode = currentNode is TextTreeTextNode; edge = ElementEdge.AfterStart; goto case ElementEdge.AfterStart; } break; default: Invariant.Assert(false, "Unknown ElementEdge value"); break; } currentNode = newNode; // Multiple text nodes count as a single Move run. // Instead of iterating through N text nodes, exploit // the fact (when we can) that text nodes are only ever contained in // runs with no other content. Jump straight to the start. if (startedAdjacentToTextNode && endedAdjacentToTextNode && plainTextOnly) { newNode = newNode.GetContainingNode(); Invariant.Assert(newNode is TextTreeRootNode); if (edge == ElementEdge.AfterEnd) { edge = ElementEdge.AfterStart; } else { newNode = newNode.GetFirstContainedNode(); Invariant.Assert(newNode != null); Invariant.Assert(edge == ElementEdge.BeforeStart); } break; } } while (startedAdjacentToTextNode && endedAdjacentToTextNode); if (newNode != null) { node = (TextTreeNode)newNode; } return (newNode != null); } internal static TextPointerContext GetPointerContextForward(TextTreeNode node, ElementEdge edge) { TextTreeNode nextNode; TextTreeNode firstContainedNode; TextPointerContext symbolType; switch (edge) { case ElementEdge.BeforeStart: symbolType = node.GetPointerContext(LogicalDirection.Forward); break; case ElementEdge.AfterStart: if (node.ContainedNode != null) { firstContainedNode = (TextTreeNode)node.GetFirstContainedNode(); symbolType = firstContainedNode.GetPointerContext(LogicalDirection.Forward); } else { goto case ElementEdge.BeforeEnd; } break; case ElementEdge.BeforeEnd: // The root node is special, there's no ElementStart/End, so test for null parent. Invariant.Assert(node.ParentNode != null || node is TextTreeRootNode, "Inconsistent node.ParentNode"); symbolType = (node.ParentNode != null) ? TextPointerContext.ElementEnd : TextPointerContext.None; break; case ElementEdge.AfterEnd: nextNode = (TextTreeNode)node.GetNextNode(); if (nextNode != null) { symbolType = nextNode.GetPointerContext(LogicalDirection.Forward); } else { // The root node is special, there's no ElementStart/End, so test for null parent. Invariant.Assert(node.GetContainingNode() != null, "Bad position!"); // Illegal to be at root AfterEnd. symbolType = (node.GetContainingNode() is TextTreeRootNode) ? TextPointerContext.None : TextPointerContext.ElementEnd; } break; default: Invariant.Assert(false, "Unreachable code."); symbolType = TextPointerContext.Text; break; } return symbolType; } // Returns the symbol type preceding thisPosition. internal static TextPointerContext GetPointerContextBackward(TextTreeNode node, ElementEdge edge) { TextPointerContext symbolType; TextTreeNode previousNode; TextTreeNode lastChildNode; switch (edge) { case ElementEdge.BeforeStart: previousNode = (TextTreeNode)node.GetPreviousNode(); if (previousNode != null) { symbolType = previousNode.GetPointerContext(LogicalDirection.Backward); } else { // The root node is special, there's no ElementStart/End, so test for null parent. Invariant.Assert(node.GetContainingNode() != null, "Bad position!"); // Illegal to be at root BeforeStart. symbolType = (node.GetContainingNode() is TextTreeRootNode) ? TextPointerContext.None : TextPointerContext.ElementStart; } break; case ElementEdge.AfterStart: // The root node is special, there's no ElementStart/End, so test for null parent. Invariant.Assert(node.ParentNode != null || node is TextTreeRootNode, "Inconsistent node.ParentNode"); symbolType = (node.ParentNode != null) ? TextPointerContext.ElementStart : TextPointerContext.None; break; case ElementEdge.BeforeEnd: lastChildNode = (TextTreeNode)node.GetLastContainedNode(); if (lastChildNode != null) { symbolType = lastChildNode.GetPointerContext(LogicalDirection.Backward); } else { goto case ElementEdge.AfterStart; } break; case ElementEdge.AfterEnd: symbolType = node.GetPointerContext(LogicalDirection.Backward); break; default: Invariant.Assert(false, "Unknown ElementEdge value"); symbolType = TextPointerContext.Text; break; } return symbolType; } // Inserts an Inline at the current location, adding contextual // elements as needed to enforce the schema. internal void InsertInline(Inline inline) { TextPointer position = this; // Check for hyperlink schema validity first -- we'll throw on an illegal Hyperlink descendent insert. bool isValidChild = TextSchema.ValidateChild(position, /*childType*/inline.GetType(), false /* throwIfIllegalChild */, true /* throwIfIllegalHyperlinkDescendent */); // Now, it is safe to assume that !isValidChild will be the case of incomplete content. if (!isValidChild) { // Ensure text content. position = TextRangeEditTables.EnsureInsertionPosition(this); Invariant.Assert(position.Parent is Run, "EnsureInsertionPosition() must return a position in text content"); Run run = (Run)position.Parent; if (run.IsEmpty) { // Remove the implicit (empty) Run, since we are going to insert an inline at this position. run.RepositionWithContent(null); } else { // Position is parented by Run, split formatting elements to prepare for inserting inline at this position. position = TextRangeEdit.SplitFormattingElement(position, /*keepEmptyFormatting:*/false); } Invariant.Assert(TextSchema.IsValidChild(position, /*childType*/inline.GetType())); } inline.RepositionWithContent(position); } // Helper that returns a DependencyObject which is a common ancestor of two pointers. internal static DependencyObject GetCommonAncestor(TextPointer position1, TextPointer position2) { TextElement element1 = position1.Parent as TextElement; TextElement element2 = position2.Parent as TextElement; DependencyObject commonAncestor; if (element1 == null) { commonAncestor = position1.Parent; } else if (element2 == null) { commonAncestor = position2.Parent; } else { commonAncestor = TextElement.GetCommonAncestor(element1, element2); } return commonAncestor; } #endregion Internal methods //----------------------------------------------------- // // Internal Properties // //----------------------------------------------------- #region Internal Properties // Type ITextPointer.ParentType { get { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); DependencyObject element = this.Parent; return element != null ? element.GetType() : null; } } /// /// Returns the TextContainer that this TextPointer is a part of. /// ITextContainer ITextPointer.TextContainer { get { return this.TextContainer; } } //bool ITextPointer.HasValidLayout { get { return this.HasValidLayout; } } // bool ITextPointer.IsAtCaretUnitBoundary { get { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); // NB: this call might set this.IsCaretUnitBoundaryCacheValid == false. this.ValidateLayout(); if (!this.HasValidLayout) { return false; } if (_layoutGeneration != _tree.LayoutGeneration) { this.IsCaretUnitBoundaryCacheValid = false; } if (!this.IsCaretUnitBoundaryCacheValid) { this.CaretUnitBoundaryCache = _tree.IsAtCaretUnitBoundary(this); _layoutGeneration = _tree.LayoutGeneration; this.IsCaretUnitBoundaryCacheValid = true; } return this.CaretUnitBoundaryCache; } } LogicalDirection ITextPointer.LogicalDirection { get { return this.LogicalDirection; } /* set { this.LogicalDirection = value; } */ } bool ITextPointer.IsAtInsertionPosition { get { return this.IsAtInsertionPosition; } } // bool ITextPointer.IsFrozen { get { return this.IsFrozen; } } // int ITextPointer.Offset { get { return this.Offset; } } // internal int Offset { get { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); return GetSymbolOffset() - 1; } } // Offset in unicode chars within the document. int ITextPointer.CharOffset { get { return this.CharOffset; } } // Offset in unicode chars within the document. internal int CharOffset { get { TextTreeTextElementNode elementNode; _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); int charOffset; switch (this.Edge) { case ElementEdge.BeforeStart: charOffset = _node.GetIMECharOffset(); break; case ElementEdge.AfterStart: charOffset = _node.GetIMECharOffset(); elementNode = _node as TextTreeTextElementNode; if (elementNode != null) { charOffset += elementNode.IMELeftEdgeCharCount; } break; case ElementEdge.BeforeEnd: case ElementEdge.AfterEnd: charOffset = _node.GetIMECharOffset() + _node.IMECharCount; break; default: Invariant.Assert(false, "Unknown value for position edge"); charOffset = 0; break; } return charOffset; } } /// /// Returns the TextContainer that this TextPointer is a part of. /// internal TextContainer TextContainer { get { return _tree; } } ////// A FrameworkElement owning a TextContainer to which this TextPointer belongs. /// internal FrameworkElement ContainingFrameworkElement { get { return ((FrameworkElement)_tree.Parent); } } // Position at row end (immediately before Row closing tag) is a valid stopper for a caret. // Editing operations are restricted here (e.g. typing should automatically jump // to the following character position. // This property identifies such special position. internal bool IsAtRowEnd { get { return TextPointerBase.IsAtRowEnd(this); } } #if DEBUG // Debug-only unique identifier for this instance. int DebugId { get { return _debugId; } } #endif // DEBUG // Indicates if this TextPointer has an ancestor that is not a mergeable (or splittable) Inline element. (e.g. Hyperlink) internal bool HasNonMergeableInlineAncestor { get { Inline ancestor = this.GetNonMergeableInlineAncestor(); return ancestor != null; } } // Returns true if position is at the start boundary of a non-mergeable inline ancestor (hyperlink) internal bool IsAtNonMergeableInlineStart { get { return TextPointerBase.IsAtNonMergeableInlineStart(this); } } // The node referenced by this position. internal TextTreeNode Node { get { return _node; } } // The edge referenced by this position. internal ElementEdge Edge { get { return (ElementEdge)(_flags & (uint)Flags.EdgeMask); } } // Returns the Block parenting this TextPointer, or null if none exists. internal Block ParentBlock { get { _tree.EmptyDeadPositionList(); SyncToTreeGeneration(); DependencyObject parentBlock = this.Parent; while (parentBlock is Inline && !(parentBlock is AnchoredBlock)) { parentBlock = ((Inline)parentBlock).Parent; } return parentBlock as Block; } } #endregion Internal Properties //----------------------------------------------------- // // 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); } // Called by the TextPointer ctor. Initializes this instance. private void Initialize(TextContainer tree, TextTreeNode node, ElementEdge edge, LogicalDirection gravity, uint generation, bool caretUnitBoundaryCache, bool isCaretUnitBoundaryCacheValid, uint layoutGeneration) { _tree = tree; // Fixup of the target node based on gravity. // Positions always cling to a node edge that matches their gravity, // so that insert ops never affect the position. RepositionForGravity(ref node, ref edge, gravity); SetNodeAndEdge(node.IncrementReferenceCount(edge), edge); _generation = generation; this.CaretUnitBoundaryCache = caretUnitBoundaryCache; this.IsCaretUnitBoundaryCacheValid = isCaretUnitBoundaryCacheValid; _layoutGeneration = layoutGeneration; VerifyFlags(); tree.AssertTree(); AssertState(); } // Throws an exception if this TextPointer is frozen. private void VerifyNotFrozen() { if (this.IsFrozen) { throw new InvalidOperationException(SR.Get(SRID.TextPositionIsFrozen)); } } // Inc/decs the position ref counts on TextTreeTextNodes as the navigator // is repositioned. // If the new ref is to a TextTreeTextNode, the node may be split. // Returns the actual node referenced, which will always be newNode, // unless newNode is a TextTreeTextNode that gets split. The caller // should use the returned node to position navigators. private TextTreeNode AdjustRefCounts(TextTreeNode newNode, ElementEdge newNodeEdge, TextTreeNode oldNode, ElementEdge oldNodeEdge) { TextTreeNode node; // This test should walk the tree upwards to catch all errors...probably not worth the slowdown though. Invariant.Assert(oldNode.ParentNode == null || oldNode.IsChildOfNode(oldNode.ParentNode), "Trying to add ref a dead node!"); Invariant.Assert(newNode.ParentNode == null || newNode.IsChildOfNode(newNode.ParentNode), "Trying to add ref a dead node!"); node = newNode; if (newNode != oldNode || newNodeEdge != oldNodeEdge) { node = newNode.IncrementReferenceCount(newNodeEdge); oldNode.DecrementReferenceCount(oldNodeEdge); } return node; } // For any logical position (location between two symbols) there are two // possible node/edge pairs. This method choses the pair that fits a // specified gravity, such that future inserts won't require that a text // position be moved, based on its gravity, at the node/edge pair. private static void RepositionForGravity(ref TextTreeNode node, ref ElementEdge edge, LogicalDirection gravity) { SplayTreeNode newNode; ElementEdge newEdge; newNode = node; newEdge = edge; switch (edge) { case ElementEdge.BeforeStart: if (gravity == LogicalDirection.Backward) { newNode = node.GetPreviousNode(); newEdge = ElementEdge.AfterEnd; if (newNode == null) { newNode = node.GetContainingNode(); newEdge = ElementEdge.AfterStart; } } break; case ElementEdge.AfterStart: if (gravity == LogicalDirection.Forward) { newNode = node.GetFirstContainedNode(); newEdge = ElementEdge.BeforeStart; if (newNode == null) { newNode = node; newEdge = ElementEdge.BeforeEnd; } } break; case ElementEdge.BeforeEnd: if (gravity == LogicalDirection.Backward) { newNode = node.GetLastContainedNode(); newEdge = ElementEdge.AfterEnd; if (newNode == null) { newNode = node; newEdge = ElementEdge.AfterStart; } } break; case ElementEdge.AfterEnd: if (gravity == LogicalDirection.Forward) { newNode = node.GetNextNode(); newEdge = ElementEdge.BeforeStart; if (newNode == null) { newNode = node.GetContainingNode(); newEdge = ElementEdge.BeforeEnd; } } break; } node = (TextTreeNode)newNode; edge = newEdge; } // Worker for GetGravity. No parameter validation. private LogicalDirection GetGravityInternal() { return (this.Edge == ElementEdge.BeforeStart || this.Edge == ElementEdge.BeforeEnd) ? LogicalDirection.Forward : LogicalDirection.Backward; } // Returns the DependencyObject scoping this position. private DependencyObject GetDependencyParent() { DebugAssertGeneration(); return GetScopingNode().GetDependencyParent(); } // Returns the node in the direction indicated bordering // a TextPointer, or null if no such node exists. internal TextTreeNode GetAdjacentNode(LogicalDirection direction) { return GetAdjacentNode(_node, this.Edge, direction); } internal static TextTreeNode GetAdjacentNode(TextTreeNode node, ElementEdge edge, LogicalDirection direction) { TextTreeNode adjacentNode; adjacentNode = GetAdjacentSiblingNode(node, edge, direction); if (adjacentNode == null) { // We're the first or last child, try the parent. if (edge == ElementEdge.AfterStart || edge == ElementEdge.BeforeEnd) { adjacentNode = node; } else { adjacentNode = (TextTreeNode)node.GetContainingNode(); } } return adjacentNode; } // Positions this navigator at a node/edge pair. // Node/edge are adjusted based on the current gravity. private void MoveToNode(TextContainer tree, TextTreeNode node, ElementEdge edge) { RepositionForGravity(ref node, ref edge, GetGravityInternal()); _tree = tree; SetNodeAndEdge(AdjustRefCounts(node, edge, _node, this.Edge), edge); _generation = tree.PositionGeneration; } ////// Returns the text element whose edge is in a specified direction /// from position. /// ////// If the symbol in the specified direction is /// TextPointerContext.ElementStart or TextPointerContext.ElementEnd, then this /// method will return the element whose edge preceeds this TextPointer. /// /// Otherwise, the method returns null. /// private TextElement GetElement(LogicalDirection direction) { TextTreeTextElementNode elementNode; DebugAssertGeneration(); elementNode = GetAdjacentTextElementNode(direction); return (elementNode == null) ? null : elementNode.TextElement; } // Invariant.Strict only. Asserts this position has good state. private void AssertState() { if (Invariant.Strict) { // Positions must never have a null tree pointer. Invariant.Assert(_node != null, "Null position node!"); if (GetGravityInternal() == LogicalDirection.Forward) { // Positions with forward gravity must stay at left edges, otherwise inserts could displace them. Invariant.Assert(this.Edge == ElementEdge.BeforeStart || this.Edge == ElementEdge.BeforeEnd, "Bad position edge/gravity pair! (1)"); } else { // Positions with backward gravity must stay at right edges, otherwise inserts could displace them. Invariant.Assert(this.Edge == ElementEdge.AfterStart || this.Edge == ElementEdge.AfterEnd, "Bad position edge/gravity pair! (2)"); } if (_node is TextTreeRootNode) { // Positions may never be at the outer edge of the root node, since you can't insert content there. Invariant.Assert(this.Edge != ElementEdge.BeforeStart && this.Edge != ElementEdge.AfterEnd, "Position at outer edge of root!"); } else if (_node is TextTreeTextNode || _node is TextTreeObjectNode) { // Text and object nodes have no inner edges/chilren, so you can't put a position there. Invariant.Assert(this.Edge != ElementEdge.AfterStart && this.Edge != ElementEdge.BeforeEnd, "Position at inner leaf node edge!"); } else { // Add new asserts for new node types here. Invariant.Assert(_node is TextTreeTextElementNode, "Unknown node type!"); } Invariant.Assert(_tree != null, "Position has no tree!"); #if DEBUG_SLOW // This test is so slow we can't afford to run it even with Invariant.Strict. // It grinds execution to a halt. int count; if (_tree.RootTextBlock == null) { count = 2; // Empty tree has two implicit edge symbols. } else { count = 0; for (TextTreeTextBlock textBlock = (TextTreeTextBlock)_tree.RootTextBlock.ContainedNode.GetMinSibling(); textBlock != null; textBlock = (TextTreeTextBlock)textBlock.GetNextNode()) { Invariant.Assert(textBlock.Count > 0, "Empty TextBlock!"); count += textBlock.Count; } } Invariant.Assert(_tree.InternalSymbolCount == count, "Bad root symbol count!"); Invariant.Assert((_tree.RootNode == null && count == 2) || count == GetNodeSymbolCountSlow(_tree.RootNode), "TextNode symbol count not in synch with tree!"); if (_tree.RootNode != null) { DebugWalkTree(_tree.RootNode.GetMinSibling()); } #endif // DEBUG_SLOW } } #if DEBUG_SLOW // This test is so slow we can't afford to run it even with Invariant.Strict. // It grinds execution to a halt. private static void DebugWalkTree(SplayTreeNode node) { SplayTreeNode previousNode; SplayTreeNode previousPreviousNode; previousNode = null; previousPreviousNode = null; for (; node != null; node = node.GetNextNode()) { if (node.SymbolCount == 0 && previousNode != null && previousNode.SymbolCount == 0 && previousPreviousNode != null && previousPreviousNode.SymbolCount == 0) { Invariant.Assert(false, "Found three consecuative zero length nodes!"); } previousPreviousNode = previousNode; previousNode = node; if (node.ContainedNode != null) { DebugWalkTree(node.ContainedNode.GetMinSibling()); } } } // Debug only. Walks a node and all its children to get a brute force // symbol count. private static int GetNodeSymbolCountSlow(SplayTreeNode node) { SplayTreeNode child; int count; if (node is TextTreeRootNode || node is TextTreeTextElementNode) { count = 2; for (child = node.GetFirstContainedNode(); child != null; child = child.GetNextNode()) { count += GetNodeSymbolCountSlow(child); } } else { Invariant.Assert(node.ContainedNode == null, "Expected leaf node!"); count = node.SymbolCount; } return count; } #endif // DEBUG_SLOW // Repositions the TextPointer and clears any relevant caches. private void SetNodeAndEdge(TextTreeNode node, ElementEdge edge) { Invariant.Assert(edge == ElementEdge.BeforeStart || edge == ElementEdge.AfterStart || edge == ElementEdge.BeforeEnd || edge == ElementEdge.AfterEnd); _node = node; _flags = (_flags & ~(uint)Flags.EdgeMask) | (uint)edge; VerifyFlags(); // Always clear the caret unit boundary cache when we move to a new position. this.IsCaretUnitBoundaryCacheValid = false; } // Setter for the public IsFrozen property. private void SetIsFrozen() { _flags |= (uint)Flags.IsFrozen; VerifyFlags(); } // Ensure we have a valid _flags field. // See bug 1249258. private void VerifyFlags() { ElementEdge edge = (ElementEdge)(_flags & (uint)Flags.EdgeMask); Invariant.Assert(edge == ElementEdge.BeforeStart || edge == ElementEdge.AfterStart || edge == ElementEdge.BeforeEnd || edge == ElementEdge.AfterEnd); } #endregion Private methods // True when the CaretUnitBoundaryCache is ready for use. // If false the cache is not reliable. private bool IsCaretUnitBoundaryCacheValid { get { return (_flags & (uint)Flags.IsCaretUnitBoundaryCacheValid) == (uint)Flags.IsCaretUnitBoundaryCacheValid; } set { _flags = (_flags & ~(uint)Flags.IsCaretUnitBoundaryCacheValid) | (value ? (uint)Flags.IsCaretUnitBoundaryCacheValid : 0); VerifyFlags(); } } // Cached value from this.TextContainer.TextView.IsAtCaretUnitBoundary. private bool CaretUnitBoundaryCache { get { return (_flags & (uint)Flags.CaretUnitBoundaryCache) == (uint)Flags.CaretUnitBoundaryCache; } set { _flags = (_flags & ~(uint)Flags.CaretUnitBoundaryCache) | (value ? (uint)Flags.CaretUnitBoundaryCache : 0); VerifyFlags(); } } //----------------------------------------------------- // // Private Types // //------------------------------------------------------ #region Private Types // Enum used for the _flags bitfield. [Flags] private enum Flags { EdgeMask = 15, // 4 low-order bis are an ElementEdge. IsFrozen = 16, IsCaretUnitBoundaryCacheValid = 32, CaretUnitBoundaryCache = 64, } #endregion Private Types //------------------------------------------------------ // // Private Fields // //----------------------------------------------------- #region Private Fields // The position's TextContainer. private TextContainer _tree; // The node referenced by this position. private TextTreeNode _node; // The value of TextContainer.PositionGeneration the last time this position // called SyncToTreeGeneration. private uint _generation; // The value of TextContainer.LayoutGeneration the last time // this position queried ITextView.IsAtCaretUnitBoundary. private uint _layoutGeneration; // Bitfield used by Edge, IsFrozen, IsCaretUnitBoundaryCacheValid, and // CaretUnitBoundaryCache properties. private uint _flags; #if DEBUG // Debug-only unique identifier for this instance. private readonly int _debugId = _debugIdCounter++; // Debug-only id counter. private static int _debugIdCounter; #endif // DEBUG #endregion Private Fields } } // File provided for Reference Use Only by Microsoft Corporation (c) 2007. // Copyright (c) Microsoft Corporation. All rights reserved.
Link Menu
This book is available now!
Buy at Amazon US or
Buy at Amazon UK
- IERequestCache.cs
- EditBehavior.cs
- NullToBooleanConverter.cs
- ReadOnlyDictionary.cs
- RequestSecurityTokenResponseCollection.cs
- MenuItemCollectionEditor.cs
- AutomationTextAttribute.cs
- DataMemberAttribute.cs
- ConfigurationErrorsException.cs
- RadioButtonPopupAdapter.cs
- Binding.cs
- Accessors.cs
- XamlHostingSectionGroup.cs
- TriggerActionCollection.cs
- TrustManagerPromptUI.cs
- ExceptionValidationRule.cs
- DataGridLength.cs
- Tool.cs
- AudioDeviceOut.cs
- DatePickerAutomationPeer.cs
- StatusBar.cs
- DetailsViewDeleteEventArgs.cs
- BasePropertyDescriptor.cs
- RightsManagementEncryptedStream.cs
- ShapingEngine.cs
- DataObjectAttribute.cs
- XamlParser.cs
- DiagnosticTraceSource.cs
- TrackingRecord.cs
- ClosableStream.cs
- WindowHelperService.cs
- StreamInfo.cs
- HelpEvent.cs
- DataGridColumnHeader.cs
- XamlPoint3DCollectionSerializer.cs
- ActivityExecutorDelegateInfo.cs
- Int32EqualityComparer.cs
- ResourceSet.cs
- XmlElementAttribute.cs
- ClusterRegistryConfigurationProvider.cs
- ItemChangedEventArgs.cs
- WebContentFormatHelper.cs
- IncrementalReadDecoders.cs
- ResumeStoryboard.cs
- FlowLayout.cs
- ModelPerspective.cs
- BStrWrapper.cs
- externdll.cs
- RijndaelCryptoServiceProvider.cs
- VirtualStackFrame.cs
- WindowPattern.cs
- RenderData.cs
- __Filters.cs
- CrossSiteScriptingValidation.cs
- ElapsedEventArgs.cs
- InkPresenterAutomationPeer.cs
- FormViewRow.cs
- SqlExpander.cs
- StringExpressionSet.cs
- ParameterExpression.cs
- TraceLevelHelper.cs
- CacheHelper.cs
- ReliableMessagingHelpers.cs
- ImageMap.cs
- CryptoKeySecurity.cs
- FigureParagraph.cs
- SmiTypedGetterSetter.cs
- MsmqIntegrationSecurityMode.cs
- listitem.cs
- CodeTypeReferenceCollection.cs
- EditingCommands.cs
- ControllableStoryboardAction.cs
- ValueType.cs
- Semaphore.cs
- ObjectCloneHelper.cs
- UInt64.cs
- HttpClientCertificate.cs
- TextTreeExtractElementUndoUnit.cs
- ConfigXmlDocument.cs
- ControlCachePolicy.cs
- RuntimeConfigurationRecord.cs
- SqlCacheDependencySection.cs
- CoTaskMemHandle.cs
- SamlAdvice.cs
- X509ThumbprintKeyIdentifierClause.cs
- ElementNotAvailableException.cs
- StringUtil.cs
- XsltFunctions.cs
- DataGridViewMethods.cs
- LookupBindingPropertiesAttribute.cs
- DirectionalLight.cs
- ProxyWebPartManager.cs
- GridViewUpdatedEventArgs.cs
- Keyboard.cs
- DataGridPageChangedEventArgs.cs
- SurrogateSelector.cs
- ComboBox.cs
- CustomCredentialPolicy.cs
- ClientApiGenerator.cs
- ExecutionEngineException.cs