|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
//
// Description: TextPointer object representing a location in formatted text.
//
using MS.Internal;
using System.Windows.Controls; // doc comments
namespace System.Windows.Documents
{
/// <summary>
/// Represents a location in a formatted text content.
/// </summary>
/// <remarks>
/// <para>In Avalon formatted text can be contained in elements such as
/// <see cref="TextBlock"/> or <see cref="FlowDocument"/>.
/// We will refer to these elements as to "text containers".</para>
/// <para>Using the properties and the methods of the TextPointer object, you can:</para>
/// <para>a) Find out what kind of content is in forward or in backward directions from its position;</para>
/// <para>b) Get a <see cref="TextElement"/> scoping or adjacent a position of this TextPointer;</para>
/// <para>c) Get characters preceding or following the TextPointer when it is positioned within text run - <see cref="Run"/> element;</para>
/// <para>d) Insert characters in a position where the TextPointer is located;</para>
/// <para>e) Inspect line layout structure by finding line boundary positions;</para>
/// <para>f) Perform visual hit-testing by translating back and forth positions of TextPointer objects into Point objects representing coordinates;</para>
/// <para>g) Create an instance of a <see cref="TextRange"/> object and use it for formatting, copying, pasting and other editing operations;</para>
/// <para></para>
/// <para>Positions in formatted document where TextPointer objects can be located
/// are places between characters and element tags.</para>
/// <para>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 <see cref="TextPointer.GetOffsetToPosition"/> method).</para>
/// <para>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
/// <see cref="TextPointer.LogicalDirection"/> property.</para>
/// <para>Class <see cref="TextPointerContext"/> is an enum specifying what kind of
/// content can be found in immediate vicility of a TextPointer. The kinds include
/// <c>None</c> for text container boundaries, <c>ElementStart</c> and <c>ElementEnd</c>
/// for opening and closing tags of <see cref="TextElement"/> elements, <c>EmbeddedElement</c>
/// for UIElements inserted in text as atomic objects. The kind of context can be
/// get from a TextPointer using method <see cref="TextPointer.GetPointerContext"/>.</para>
/// <para>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.</para>
/// <para>To traverse a document content you can use a bunch of <c>Get*Position</c>
/// methods - <see cref="GetNextContextPosition"/>, <see cref="GetNextInsertionPosition"/>, etc.</para>
/// <para></para>
/// <para>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:
/// <see cref="TextRange.Start"/> and <see cref="TextRange.End"/>, etc.
/// <see cref="TextElement.ElementStart"/> and <see cref="TextElement.ElementEnd"/>,
/// <see cref="TextElement.ContentStart"/> and <see cref="TextElement.ContentEnd"/>, etc.
/// TextPointer objects can be also produced from other TextPointer objects
/// using traversal methods like <see cref="TextPointer.GetNextContextPosition"/>,
/// <see cref="TextPointer.GetNextInsertionPosition"/>, <see cref="TextPointer.GetPositionAtOffset(int)"/>,
/// etc. TextPointer can be also gotten from a visual coordinate via
/// methods like <see cref="TextBlock.GetPositionFromPoint"/>.</para>
/// <para></para>
/// <para>We use a concept of "insertion positions" in association with TextPointer objects,
/// which is a key for editor behavior and for various api members.</para>
/// <para>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 <see cref="TextRange"/> and <see cref="TextSelection"/>
/// objects are always forcefully set to insertion positions, even if you pass
/// arbitrary position in TextRange constructor or <see cref="TextRange.Select"/> method.</para>
/// <para>From TextPointer located at arbitrary (possibly non-insertion) position, you
/// can get a TextPointer located at a nearest insertion position by calling
/// <see cref="GetInsertionPosition()"/> method. To get from one insertion position to another
/// you can use <see cref="GetNextInsertionPosition"/> method.</para>
/// </remarks>
/// <example>
/// <para>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.</para>
/// <code>
/// 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);
/// }
/// </code>
/// <para>Example 1. This code shows how to use TextPointer for finding a first Run element
/// from a particular position in forard direction.</para>
/// <code>
/// 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;
/// }
/// </code>
/// <para>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.</para>
/// <code>
/// 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.
/// }
/// </code>
/// <para>Example 3. This code shows how to enumerate and count all Paragraphs in a given TextRange.</para>
/// <code>
/// 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;
/// }
/// </code>
/// <para>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.</para>
/// <code>
/// 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;
/// }
/// </code>
/// </example>
//
// 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
/// <summary>
/// Creates a new instance of TextPointer object.
/// </summary>
/// <param name="textPointer">
/// TextPointer from which initial properties and location are initialized.
/// </param>
/// <remarks>
/// 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.
/// </remarks>
internal TextPointer(TextPointer textPointer)
{
ArgumentNullException.ThrowIfNull(textPointer);
InitializeOffset(textPointer, 0, textPointer.GetGravityInternal());
}
// Creates a new TextPointer instance.
internal TextPointer(TextPointer position, int offset)
{
ArgumentNullException.ThrowIfNull(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.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.
/// <summary>
/// </summary>
~TextPointer()
{
ArrayList deadPositionList;
deadPositionList = _tree.DeadPositionList;
lock (deadPositionList)
{
deadPositionList.Add(this);
}
}
#endif // REFCOUNT_DEAD_TEXTPOINTERS
#endregion Constructors
//------------------------------------------------------
//
// Public Methods
//
//------------------------------------------------------
#region Public Methods
/// <summary>
/// Returns true if this TextPointer is positioned within the same
/// text containner as another TextPointer.
/// </summary>
/// <param name="textPosition">
/// TextPointer to compare.
/// </param>
/// <remarks>
/// <para>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 <see cref="CompareTo"/>).</para>
/// <para>The purpose of this method is to test whether two TextPointer
/// objects belong to the same text container or not.</para>
/// <para>Formatted text can be contained within one these elements in Avalon:
/// <see cref="TextBlock"/> or <see cref="FlowDocument"/>.
/// We refer to them as to "text containers".</para>
/// <para>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.</para>
/// </remarks>
/// <example>
/// <para>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</para>
/// <code>
/// 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;
/// }
/// </code>
/// </example>
public bool IsInSameDocument(TextPointer textPosition)
{
ArgumentNullException.ThrowIfNull(textPosition);
_tree.EmptyDeadPositionList();
return (this.TextContainer == textPosition.TextContainer);
}
/// <summary>
/// Compares positions of this TextPointer with another TextPointer.
/// </summary>
/// <param name="position">
/// The TextPointer to compare with.
/// </param>
/// <returns>
/// Less than zero: this TextPointer preceeds position.
/// Zero: this TextPointer is at the same location as position.
/// Greater than zero: this TextPointer follows position.
/// </returns>
/// <exception cref="System.ArgumentException">
/// Throws ArgumentException if position does not belong to the same
/// text container as this TextPointer (you can use <see cref="TextPointer.IsInSameDocument"/>
/// method to detect whether comparison is possible).
/// </exception>
public int CompareTo(TextPointer position)
{
int offsetThis;
int offsetPosition;
int result;
_tree.EmptyDeadPositionList();
ValidationHelper.VerifyPosition(_tree, position);
SyncToTreeGeneration();
position.SyncToTreeGeneration();
offsetThis = GetSymbolOffset();
offsetPosition = position.GetSymbolOffset();
if (offsetThis < offsetPosition)
{
result = -1;
}
else if (offsetThis > offsetPosition)
{
result = +1;
}
else
{
result = 0;
}
return result;
}
/// <summary>
/// Returns the type of content to one side of this TextPointer.
/// </summary>
/// <param name="direction">
/// Direction to query.
/// </param>
/// <returns>
/// <para>Returns <see cref="TextPointerContext.None"/> if this TextPointer
/// is positioned at the beginning of a text container and the requested direction
/// is <see cref="System.Windows.Documents.LogicalDirection.Backward"/>, or if it is positioned
/// at the end of a text container and the requested direction is
/// <see cref="System.Windows.Documents.LogicalDirection.Forward"/>.</para>
/// <para>Returns <see cref="TextPointerContext.ElementStart"/> if the TextPointer
/// has an openenig tag of some of TextElements in the requested direction.</para>
/// <para>Returns <see cref="TextPointerContext.ElementEnd"/> if the TextPointer
/// has a closing tag of some of TextElements in the requested direction.</para>
/// <para>Returns <see cref="TextPointerContext.Text"/> if the TextPointer
/// is positioned within <see cref="Run"/> element and has some non-emty sequence of characters
/// in requested direction.</para>
/// <para>Returns <see cref="TextPointerContext.EmbeddedElement"/> is the TextPointer
/// is positioned within <see cref="InlineUIContainer"/> or <see cref="BlockUIContainer"/>
/// element and has <see cref="UIElement"/> as atomic symbol in a requested direction.</para>
/// </returns>
/// <example>
/// <para>This example shows how to use <c>GetPointerContext</c> 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).</para>
/// <code>
/// 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;
/// }
/// </code>
/// </example>
public TextPointerContext GetPointerContext(LogicalDirection direction)
{
ValidationHelper.VerifyDirection(direction, "direction");
_tree.EmptyDeadPositionList();
SyncToTreeGeneration();
return (direction == LogicalDirection.Forward) ? GetPointerContextForward(_node, this.Edge) : GetPointerContextBackward(_node, this.Edge);
}
/// <summary>
/// Returns the count of Unicode characters between this TextPointer and the
/// edge of an element in the given direction.
/// </summary>
/// <param name="direction">
/// Direction to query.
/// </param>
/// <remarks>
/// If the TetPointer is positioned not inside a <see cref="Run"/> element,
/// then the method always returns zero.
/// </remarks>
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;
}
/// <summary>
/// Returns the distance between this TextPointer and another.
/// </summary>
/// <param name="position">
/// TextPointer to compare.
/// </param>
/// <exception cref="System.ArgumentException">
/// Throws an ArgumentException if the TextPointer position is not
/// positioned within the same document as this TextPointer.
/// </exception>
/// <returns>
/// <para>The return value will be negative if the TextPointer position
/// preceeds this TextPointer, zero if the two TextPointers
/// are equally positioned, or positive if position follows this
/// TextPointer.</para>
/// </returns>
/// <remarks>
/// <para>The distance is represented as a number of "symbols"
/// between these two pointers.</para>
/// <para>Each opening and each closing tag of any TextElement
/// is considered as one symbol. So an empty TextElement contributes
/// two symbols - one for each of tags.</para>
/// <para>UIElement placed within InlineUIContainer or BlockUIContainer
/// represented as one symbol - independently of how complex
/// is its content. Even if the UIElement contains or is a
/// text container it is treated as atomic entity - single symbol.
/// This may be confusing especially if you do not pay
/// muchy attention to a difference between the <see cref="TextElement"/>
/// the <see cref="UIElement"/> class.</para>
/// <para>Each 16-bit unicode character inside a <see cref="Run"/> element
/// is considered as one symbol.</para>
/// <para>For instance, for the following xaml:
/// <Run>abc</Run><InlineUIContainer><Button>OK</Button></InlineUIContainer>
/// the offset from itw content start to content end will be 8 -
/// one for each of: (1) Run start, (2) "a", (3) "b", (4) "c", (5) Run end, (6) InlineUIContainer start,
/// (7) whole Button element, (8) InlineUIContainer end. Note that <c>Button</c>
/// element considered as one symbol even though it is represented
/// by two tags and two characters.</para>
/// </remarks>
/// <example>
/// <para>In this example we show how to use TextPointer offsets for
/// persisting positional information. Assuming that the content of
/// a RichTextBox is not changed between calls of
/// GetPersistedSelection and RestoreSelectionFromPersistedRange
/// methods, the selection will be restored to its original state.</para>
/// <code>
/// struct PersistedTextRange { int Start; int End; }
///
/// PersistedTextRange GetPersistedSelection(RichTextBox richTextBox)
/// {
/// PersistedTextRange persistedSelection;
///
/// TextPointer contentStart = richTextBox.Document.ContentStart;
/// persistedSelection.Start = contentStart.GetOffsetToPosition(richTextBox.Selection.Start);
/// persistedSelection.End = contentStart.GetOffsetToPosition(richTextBox.Selection.End);
///
/// return persistedSelection;
/// }
///
/// RestoreSelectionFromPersistedRange(RichTextBox richTextBox, PersistedTextRange persistedRange)
/// {
/// TextPointer contentStart = richTextBox.Document.ContentStart;
///
/// richTextBox.Selection.Select(
/// contentStart.GetPositionAtOffset(persistedRange.Start),
/// contentStart.GetPositionAtOffset(persistedRange.End));
/// }
///
/// </code>
/// </example>
public int GetOffsetToPosition(TextPointer position)
{
_tree.EmptyDeadPositionList();
ValidationHelper.VerifyPosition(_tree, position);
SyncToTreeGeneration();
position.SyncToTreeGeneration();
return (position.GetSymbolOffset() - GetSymbolOffset());
}
/// <summary>
/// Returns text bordering this TextPointer from one side or another.
/// </summary>
/// <param name="direction">
/// Direction to query.
/// </param>
/// <remarks>
/// See GetTextInRun(direction, textBuffer, startIndex, count) method
/// remarks for semantics of the returned text.
/// </remarks>
/// <example>
/// <para>This is an example of simplistic plain text converter.
/// This algorithm produces a string concatenating all text runs
/// between two TextPointers.</para>
/// <para>Note that this is really simplistic algorithm. You sould use
/// <see cref="TextRange.Text"/> property for more sophisticated
/// plain text conversion.</para>
/// <code>
/// 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();
/// }
/// </code>
/// </example>
public string GetTextInRun(LogicalDirection direction)
{
ValidationHelper.VerifyDirection(direction, "direction");
return TextPointerBase.GetTextInRun(this, direction);
}
/// <summary>
/// Copies characters bordering this TextPointer into a caller supplied char array.
/// </summary>
/// <param name="direction">
/// Direction to query.
/// </param>
/// <param name="textBuffer">
/// Buffer into which chars are copied.
/// </param>
/// <param name="startIndex">
/// Index within the textBuffer array at which the copy is started.
/// </param>
/// <param name="count">
/// The maximum number of characters to copy. Must be less than
/// or equal to a (<c>textBuffer.Length - startIndex</c>).
/// </param>
/// <returns>
/// The count of chars actually copied.
/// </returns>
/// <exception cref="ArgumentException">
/// Is thrown in the following cases: (a) when <c>startIndex</c> is less than zero,
/// (b) when <c>startIndex</c> is greater than <c>textBuffer.Length</c>,
/// (c) when <c>count</c> is less than zero, (d) when <c>count</c>
/// is greater than size available for copying (<c>textBuffer.Length - startIndex</c>).
/// </exception>
/// <remarks>
/// 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.
/// </remarks>
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);
}
/// <summary>
/// Returns an element represented by a symbol, if any, bordering
/// this TextPointer in the specified direction.
/// </summary>
/// <param name="direction">
/// Direction to query.
/// </param>
/// <returns>
/// The element if its opening or closing tag exists
/// in a specified direction. Otherwize returns null.
/// </returns>
/// <remarks>
/// <para>The returned element may be both a <see cref="TextElement"/>
/// and a <see cref="UIElement"/>.</para>
/// <para><see cref="TextElement"/> object will be returned when
/// this TextPointer is located before or after of either opening
/// or closing tag in appropriate direction.</para>
/// <para><see cref="UIElement"/> object can be returned only when
/// the pointer is located outside its opening or closing tag - within
/// <see cref="InlineUIContainer"/> or <see cref="BlockUIContainer"/>.</para>
/// </remarks>
public DependencyObject GetAdjacentElement(LogicalDirection direction)
{
ValidationHelper.VerifyDirection(direction, "direction");
_tree.EmptyDeadPositionList();
SyncToTreeGeneration();
return GetAdjacentElement(_node, this.Edge, direction);
}
/// <summary>
/// Returns a TextPointer at a new position by a specified symbol
/// count.
/// </summary>
/// <param name="offset">
/// Number of symbols to advance. offset may be negative, in which
/// case the TextPointer is moved backwards.
/// </param>
/// <returns>
/// 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.
/// </returns>
/// <remarks>
/// <para>This method, like all other TextPointer methods, defines a symbol
/// as one of:</para>
/// <para>- 16 bit Unicode character.</para>
/// <para>- opening or closing tag of a <see cref="TextElement"/>.</para>
/// <para>- the whole <see cref="UIElement"/> as atomic embedded object.</para>
/// </remarks>
/// <example>
/// <para>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.</para>
/// <code>
/// 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);
/// }
/// </code>
/// </example>
public TextPointer GetPositionAtOffset(int offset)
{
return GetPositionAtOffset(offset, this.LogicalDirection);
}
/// <summary>
/// Returns a TextPointer at a new position by a specified symbol
/// count.
/// </summary>
/// <param name="offset">
/// Number of symbols to advance. offset may be negative, in which
/// case the TextPointer is moved backwards.
/// </param>
/// <param name="direction">
/// LogicalDirection desired for a returned TextPointer.
/// </param>
/// <returns>
/// 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 <paramref name="direction"/>.
/// </returns>
/// <remarks>
/// <para>This method, like all other TextPointer methods, defines a symbol
/// as one of:</para>
/// <para>- 16 bit Unicode character.</para>
/// <para>- opening or closing tag of a <see cref="TextElement"/>.</para>
/// <para>- the whole <see cref="UIElement"/> as atomic embedded object.</para>
/// <para>See examples in <seealso cref="TextPointer.GetPositionAtOffset(int)"/> method with one parameter.</para>
/// </remarks>
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;
}
}
/// <summary>
/// Returns a pointer at the next symbol in a specified
/// direction, or past all following Unicode characters if the
/// bordering content is Unicode text.
/// </summary>
/// <param name="direction">
/// Direction to move.
/// </param>
/// <returns>
/// TextPointer in a requested direction, null if this TextPointer
/// borders the start or end of the document.
/// </returns>
/// <remarks>
/// <para>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.</para>
/// <para>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.</para>
/// <para>If there is no following symbol (start or end of the document),
/// then the method returns null.</para>
/// </remarks>
/// <example>
/// <para>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.</para>
/// <para>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.</para>
/// <code>
/// 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");
/// }
/// }
/// </code>
/// </example>
public TextPointer GetNextContextPosition(LogicalDirection direction)
{
return (TextPointer)((ITextPointer)this).GetNextContextPosition(direction);
}
/// <summary>
/// Returns a TextPointer at the closest insertion position in a
/// specified direction.
/// </summary>
/// <param name="direction">
/// Direction to search a closest insertion position.
/// </param>
/// <returns>
/// TextPointer positioned at inserion point. The value is never null.
/// </returns>
/// <remarks>
/// <para>The concept of insertion position is a convenience
/// for traversing text content across structural boundaries,
/// between table cells, paragraphs, list items etc.</para>
/// <para>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.</para>
/// <para>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.</para>
/// <para>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 <c>direction</c>
/// parameter will tell which of two possible positions to take:
/// in the end of the preceding or in the begining of the following paragraph.</para>
/// <para>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.</para>
/// <para>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.</para>
/// <para>Somethimes the whole document does not have even
/// one insertion position - it happens when the content
/// is structurally incomplete, say in empty <see cref="List"/>
/// or <see cref="Table"/>element. In such case the method
/// will return the original position even though it is not
/// an insertion position. The method never returns null.</para>
/// </remarks>
/// <example>
/// <para>This example shows how to use the method <c>GetInsertionPosition</c>
/// as a convenience of finding a starting "editable" position.</para>
/// <code>
/// 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;
/// }
/// </code>
/// </example>
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);
}
/// <summary>
/// Returns a TextPointer in the direction indicated to the following
/// insertion position.
/// </summary>
/// <param name="direction">
/// Direction to move.
/// </param>
/// <returns>
/// A TextPointer at an insertion position in a requested direction,
/// null if there is no more insertion positions in that direction.
/// </returns>
/// <remarks>
/// <para>The concept of insertion position is a convenience
/// for traversing text content across structural boundaries,
/// between table cells, paragraphs, list items etc.</para>
/// <para>See more detailed definition of the concept of
/// "insertion position" in the <see cref="TextPointer.GetInsertionPosition(LogicalDirection)"/>
/// method.</para>
/// <para>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.</para>
/// <para>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.</para>
/// </remarks>
/// <example>
/// <para>In this example we use the method <c>GetNextInsertionPosition</c>
/// for passing over structural boundaries in a proces of
/// enumerating all <see cref="Paragraph"/> in a range.</para>
/// <code>
/// 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;
/// }
/// </code>
/// </example>
public TextPointer GetNextInsertionPosition(LogicalDirection direction)
{
return (TextPointer)((ITextPointer)this).GetNextInsertionPosition(direction);
}
/// <summary>
/// Returns a TextPointer at the start of line after skipping
/// a given number of line starts in forward or backward direction.
/// </summary>
/// <param name="count">
/// 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.
/// </param>
/// <exception cref="System.InvalidOperationException">
/// 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.
/// </exception>
/// <returns>
/// 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.
/// </returns>
/// <remarks>
/// <para>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
/// </para>
/// <para>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.
/// </para>
/// </remarks>
public TextPointer GetLineStartPosition(int count)
{
int actualCount;
TextPointer lineStartPosition = GetLineStartPosition(count, out actualCount);
return (actualCount != count) ? null : lineStartPosition;
}
/// <summary>
/// Returns a TextPointer at the start of line after skipping
/// a given number of line starts in forward or backward direction.
/// </summary>
/// <param name="count">
/// Offset of the destination line. Negative values specify preceding
/// lines, zero specifies the current line, positive values specify
/// following lines.
/// </param>
/// <param name="actualCount">
/// The offset of the line moved to. This value may be less than
/// requested if the beginning or end of document is encountered.
/// </param>
/// <returns>
/// 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.
/// </returns>
/// <remarks>
/// 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.
/// </remarks>
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;
}
/// <summary>
/// Returns the bounding box of the content bordering this TextPointer
/// in a specified direction.
/// </summary>
/// <param name="direction">
/// Direction of content.
/// </param>
/// <remarks>
/// <para>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.</para>
/// <para>If there is no content in the specified direction, a zero-width
/// Rect is returned with height matching the preceding content.</para>
/// </remarks>
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);
}
/// <summary>
/// Inserts text at this TextPointer's position.
/// </summary>
/// <param name="textData">
/// Text to insert.
/// </param>
/// <remarks>
/// The LogicalDirection property specifies whether this TextPointer
/// will be positioned before or after the new text.
/// </remarks>
public void InsertTextInRun(string textData)
{
ArgumentNullException.ThrowIfNull(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();
}
}
/// <summary>
/// Deletes text in Run at this TextPointer's position
/// </summary>
/// <remarks></remarks>
/// <param name="count">
/// Number of characters to delete.
/// Positive count deletes text following this TextPointer in Run.
/// Negative count deletes text preceding this TextPointer in Run.
/// </param>
/// <returns>
/// 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.
/// </returns>
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;
}
/// <summary>
/// Inserts a TextElement at this TextPointer's position.
/// </summary>
/// <param name="textElement">
/// ContentElement to insert.
/// </param>
/// <remarks>
/// The LogicalDirection property specifies whether this TextPointer
/// will be positioned before or after the TextElement.
/// </remarks>
/// <exception cref="ArgumentException">
/// Throws ArgumentException is textElement is not valid
/// according to flow schema.
/// </exception>
/// <exception cref="InvalidOperationException">
/// Throws InvalidOperationException if textElement cannot be inserted
/// at this position because it belongs to another tree.
/// </exception>
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.TextPointer_CannotInsertTextElementBecauseItBelongsToAnotherTree);
}
textElement.RepositionWithContent(this);
}
/// <summary>
/// Insert a paragraph break at this position by splitting all elements upto its paragraph ancestor.
/// </summary>
/// <returns>
/// 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.
/// </returns>
/// <exception cref="InvalidOperationException">
/// 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.
/// </exception>
public TextPointer InsertParagraphBreak()
{
_tree.EmptyDeadPositionList();
SyncToTreeGeneration();
if (this.TextContainer.Parent != null)
{
Type containerType = this.TextContainer.Parent.GetType();
if (!TextSchema.IsValidChildOfContainer(containerType, typeof(Paragraph)))
{
throw new InvalidOperationException(SR.Format(SR.TextSchema_IllegalElement, "Paragraph", containerType));
}
}
Inline ancestor = this.GetNonMergeableInlineAncestor();
if (ancestor != null)
{
// Cannot split a hyperlink element!
throw new InvalidOperationException(SR.Format(SR.TextSchema_CannotSplitElement, ancestor.GetType().Name));
}
TextPointer position;
_tree.BeginChange();
try
{
position = TextRangeEdit.InsertParagraphBreak(this, /*moveIntoSecondParagraph:*/true);
}
finally
{
_tree.EndChange();
}
return position;
}
/// <summary>
/// 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.
/// </summary>
/// <returns>
/// TextPointer positioned immediately after the closing tag of
/// a <see cref="LineBreak"/> element inserted by this method.
/// </returns>
public TextPointer InsertLineBreak()
{
_tree.EmptyDeadPositionList();
SyncToTreeGeneration();
TextPointer position;
_tree.BeginChange();
try
{
position = TextRangeEdit.InsertLineBreak(this);
}
finally
{
_tree.EndChange();
}
return position;
}
/// <summary>
/// Debug only ToString override.
/// </summary>
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
/// <summary>
/// Returns true if layout is calculated at the current position.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
// 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);
}
}
/// <summary>
/// Specifies whether the TextPointer is associated with preceding or
/// following content.
/// </summary>
/// <remarks>
/// <para>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.</para>
/// </remarks>
public LogicalDirection LogicalDirection
{
get
{
return GetGravityInternal();
}
}
/// <summary>
/// Returns the logical parent scoping this TextPointer.
/// </summary>
public DependencyObject Parent
{
get
{
_tree.EmptyDeadPositionList();
SyncToTreeGeneration();
return GetLogicalTreeNode();
}
}
/// <summary>
/// Returns true if this TextPointer is positioned at an insertion
/// position.
/// </summary>
/// <remarks>
/// <para>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.</para>
/// </remarks>
public bool IsAtInsertionPosition
{
get
{
_tree.EmptyDeadPositionList();
SyncToTreeGeneration();
return TextPointerBase.IsAtInsertionPosition(this);
}
}
/// <summary>
/// Returns true if this TextPointer is positioned at the start of a
/// line.
/// </summary>
/// <exception cref="System.InvalidOperationException">
/// 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.
/// </exception>
/// <remarks>
/// <para>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.</para>
/// <para>This property is always false when HasValidLayout is false.</para>
/// </remarks>
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;
}
}
/// <summary>
/// Returns the paragraph scoping this textpointer
/// </summary>
/// <remarks>
/// <para>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.</para>
/// <para>When TextPointer is positioned outside of a paragraph,
/// the property returns null.</para>
/// </remarks>
public Paragraph Paragraph
{
get
{
_tree.EmptyDeadPositionList();
SyncToTreeGeneration();
return this.ParentBlock as Paragraph;
}
}
/// <summary>
/// Returns the paragraph-like parent of the pointer
/// </summary>
/// <remarks>
/// If we would have a common base class for Paragraph and BlockUIContainer,
/// we would return it here.
/// </remarks>
internal Block ParagraphOrBlockUIContainer
{
// Introduce a new class - common base for Paragraph and BlockUIContainer
get
{
_tree.EmptyDeadPositionList();
SyncToTreeGeneration();
Block parentBlock = this.ParentBlock;
return (parentBlock is Paragraph) || (parentBlock is BlockUIContainer) ? parentBlock : null;
}
}
/// <summary>
/// The start position of the document's content
/// </summary>
/// <remarks>
/// <para>This property may be useful as a base for persistent
/// position indexing - for calculating offsets
/// to all other pointers.</para>
/// <para>The <see cref="TextPointer.Parent"/> property for this
/// position is not a TextElement - it is a text container,
/// which can be one of <see cref="TextBlock"/> or
/// <see cref="FlowDocument"/>.</para>
/// </remarks>
public TextPointer DocumentStart
{
get
{
return TextContainer.Start;
}
}
/// <summary>
/// The end position of the document's content.
/// </summary>
/// <remarks>
/// <para>The <see cref="TextPointer.Parent"/> property for this
/// position is not a TextElement - it is a text container,
/// which can be one of <see cref="TextBlock"/> or
/// <see cref="FlowDocument"/>.</para>
/// </remarks>
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;
ArgumentNullException.ThrowIfNull(textBuffer);
if (startIndex < 0)
{
throw new ArgumentException(SR.Format(SR.NegativeValue, "startIndex"));
}
if (startIndex > textBuffer.Length)
{
throw new ArgumentException(SR.Format(SR.StartIndexExceedsBufferSize, startIndex, textBuffer.Length));
}
if (count < 0)
{
throw new ArgumentException(SR.Format(SR.NegativeValue, "count"));
}
if (count > textBuffer.Length - startIndex)
{
throw new ArgumentException(SR.Format(SR.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.Forward) ? 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;
}
/// <summary>
/// Moves this TextPointer to another TextPointer's position.
/// </summary>
/// <param name="textPosition">
/// Position to move to.
/// </param>
/// <exception cref="System.ArgumentException">
/// Throws an ArgumentException if textPosition is not
/// positioned within the same document.
/// </exception>
/// <exception cref="System.InvalidOperationException">
/// Throws an InvalidOperationException if this TextPointer's IsFrozen
/// property is set true. Frozen TextPointers may not be repositioned.
/// </exception>
internal void MoveToPosition(TextPointer textPosition)
{
ValidationHelper.VerifyPosition(_tree, textPosition);
VerifyNotFrozen();
_tree.EmptyDeadPositionList();
SyncToTreeGeneration();
textPosition.SyncToTreeGeneration();
MoveToNode(_tree, textPosition.Node, textPosition.Edge);
}
/// <summary>
/// Advances this TextPointer to a new position by a specified symbol
/// count.
/// </summary>
/// <param name="offset">
/// Number of symbols to advance. offset may be negative, in which
/// case the TextPointer is moved backwards.
/// </param>
/// <exception cref="System.InvalidOperationException">
/// Throws an InvalidOperationException if this TextPointer's IsFrozen
/// property is set true. Frozen TextPointers may not be repositioned.
/// </exception>
/// <remarks>
/// 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.
/// </remarks>
/// <returns>
/// 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.
/// </returns>
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;
}
/// <summary>
/// Advances this TextPointer to the next symbol in a specified
/// direction, or past all following Unicode characters if the
/// bordering content is Unicode text.
/// </summary>
/// <param name="direction">
/// Direction to move.
/// </param>
/// <exception cref="System.InvalidOperationException">
/// Throws an InvalidOperationException if this TextPointer's IsFrozen
/// property is set true. Frozen TextPointers may not be repositioned.
/// </exception>
/// <returns>
/// true if the TextPointer is repositioned, false if the TextPointer
/// borders the start or end of the document.
/// </returns>
/// <remarks>
/// 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.
/// </remarks>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="direction">
/// Direction to move.
/// </param>
/// <exception cref="System.InvalidOperationException">
/// Throws an InvalidOperationException if this TextPointer's IsFrozen
/// property is set true. Frozen TextPointers may not be repositioned.
/// </exception>
/// <remarks>
/// 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.
/// </remarks>
/// <returns>
/// True if the TextPointer is repositioned, false otherwise.
/// </returns>
internal bool MoveToInsertionPosition(LogicalDirection direction)
{
ValidationHelper.VerifyDirection(direction, "direction");
VerifyNotFrozen();
_tree.EmptyDeadPositionList();
SyncToTreeGeneration();
return TextPointerBase.MoveToInsertionPosition(this, direction);
}
/// <summary>
/// Advances this TextPointer in the direction indicated to the following
/// insertion position.
/// </summary>
/// <param name="direction">
/// Direction to move.
/// </param>
/// <exception cref="System.InvalidOperationException">
/// Throws an InvalidOperationException if this TextPointer's IsFrozen
/// property is set true. Frozen TextPointers may not be repositioned.
/// </exception>
/// <remarks>
/// 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.
/// </remarks>
/// <returns>
/// True if the TextPointer is repositioned, false otherwise.
/// </returns>
internal bool MoveToNextInsertionPosition(LogicalDirection direction)
{
ValidationHelper.VerifyDirection(direction, "direction");
VerifyNotFrozen();
_tree.EmptyDeadPositionList();
SyncToTreeGeneration();
return TextPointerBase.MoveToNextInsertionPosition(this, direction);
}
/// <summary>
/// Advances this TextPointer to the start of a neighboring line.
/// </summary>
/// <param name="count">
/// Offset of the destination line. Negative values specify preceding
/// lines, zero specifies the current line, positive values specify
/// following lines.
/// </param>
/// <exception cref="System.InvalidOperationException">
/// Throws an InvalidOperationException if this TextPointer's IsFrozen
/// property is set true. Frozen TextPointers may not be repositioned.
/// </exception>
/// <returns>
/// The offset of the line moved to. This value may be less than
/// requested if the beginning or end of document is encountered.
/// </returns>
/// <remarks>
/// 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.
/// </remarks>
internal int MoveToLineBoundary(int count)
{
VerifyNotFrozen();
this.ValidateLayout();
if (!this.HasValidLayout)
{
return 0;
}
_tree.EmptyDeadPositionList();
SyncToTreeGeneration();
return TextPointerBase.MoveToLineBoundary(this, _tree.TextView, count);
}
/// <summary>
/// Inserts a UIElement at this TextPointer's position.
/// </summary>
/// <param name="uiElement">
/// UIElement to insert.
/// </param>
/// <remarks>
/// The LogicalDirection property specifies whether this TextPointer
/// will be positioned before or after the UIElement.
/// </remarks>
/// <exception cref="ArgumentException">
/// Throws ArgumentException is contentElement is not valid
/// according to flow schema.
/// </exception>
internal void InsertUIElement(UIElement uiElement)
{
ArgumentNullException.ThrowIfNull(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.TextSchema_UIElementNotAllowedInThisPosition);
}
_tree.BeginChange();
try
{
_tree.InsertEmbeddedObjectInternal(this, uiElement);
}
finally
{
_tree.EndChange();
}
}
// consider adding this to public API.
internal TextElement GetAdjacentElementFromOuterPosition(LogicalDirection direction)
{
TextTreeTextElementNode elementNode;
_tree.EmptyDeadPositionList();
SyncToTreeGeneration();
elementNode = GetAdjacentTextElementNodeSibling(direction);
return (elementNode == null) ? null : elementNode.TextElement;
}
/// <summary>
/// Sets the logical direction of this textpointer.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Throws an InvalidOperationException if this TextPointer's Freeze() method has been called.
/// </exception>
/// <param name="direction"></param>
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");
}
}
/// <summary>
/// True if the Freeze method has been called, in which case
/// this TextPointer is immutable and may not be repositioned.
/// </summary>
/// <Remarks>
/// 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.
/// </Remarks>
internal bool IsFrozen
{
get
{
_tree.EmptyDeadPositionList();
return (_flags & (uint)Flags.IsFrozen) == (uint)Flags.IsFrozen;
}
}
/// <summary>
/// Makes this TextPointer immutable.
/// </summary>
/// <Remarks>
/// 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.
/// </Remarks>
internal void Freeze()
{
_tree.EmptyDeadPositionList();
SetIsFrozen();
}
/// <summary>
/// Returns an immutable TextPointer instance positioned equally to
/// this one, with a specified LogicalDirection.
/// </summary>
/// <param name="logicalDirection">
/// LogicalDirection of the returned TextPointer.
/// </param>
/// <remarks>
/// 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.
/// </remarks>
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);
}
// <see cref="System.Windows.Documents.ITextPointer.GetTextInRun"/>
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;
ArgumentNullException.ThrowIfNull(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;
ArgumentNullException.ThrowIfNull(formattingProperty);
_tree.EmptyDeadPositionList();
SyncToTreeGeneration();
element = this.Parent as TextElement;
if (element == null)
{
throw new InvalidOperationException(SR.Format(SR.NoScopingElement, "This TextPointer"));
}
return element.ReadLocalValue(formattingProperty);
}
LocalValueEnumerator ITextPointer.GetLocalValueEnumerator()
{
DependencyObject element;
_tree.EmptyDeadPositionList();
SyncToTreeGeneration();
element = this.Parent as TextElement;
if (element == null)
{
// Look into adding an empty ctor to LocalValueEnumerator.
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);
}
// <see cref="ITextPointer.Freeze"/>
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();
TextTreeNode scopingNode = GetScopingNode();
elementNode = scopingNode as TextTreeTextElementNode;
if (elementNode == null)
{
// if we're at the root of the tree, the pointer is
// already at the element edge, and nothing more need be done.
// This case can arise when a text tree contains only a
// BlockUIContainer (and no text)
if (scopingNode is TextTreeRootNode)
{
return;
}
throw new InvalidOperationException(SR.Format(SR.NoScopingElement, "This TextNavigator"));
}
MoveToNode(_tree, elementNode, edge);
}
// <see cref="TextPointer.MoveToLineBoundary"/>
int ITextPointer.MoveToLineBoundary(int count)
{
return MoveToLineBoundary(count);
}
// <see cref="TextPointer.GetCharacterRect"/>
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.
// enable this code in retail once it is used.
#if DEBUG
/// <summary>
/// </summary>
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;
}
/// <summary>
/// </summary>
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;
}
/// <summary>
/// </summary>
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);
}
// this method no longer has a matching public analogue.
// We should consider removing it, probably replacing it with
// DeleteTextInRun.
// Also need to consider whether or not it's appropriate to create a
// default change block here.
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();
}
}
/// <see cref="ITextPointer.ValidateLayout"/>
bool ITextPointer.ValidateLayout()
{
return this.ValidateLayout();
}
/// <see cref="ITextPointer.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)
{
if (position.Parent == null)
{
// We should try to fix up the schema by adding elements instead of throwing here.
throw new InvalidOperationException(SR.TextSchema_CannotInsertContentInThisPosition);
}
// 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
// <see cref="System.Windows.Documents.ITextPointer.ParentType"/>
Type ITextPointer.ParentType
{
get
{
_tree.EmptyDeadPositionList();
SyncToTreeGeneration();
DependencyObject element = this.Parent;
return element != null ? element.GetType() : null;
}
}
/// <summary>
/// Returns the TextContainer that this TextPointer is a part of.
/// </summary>
ITextContainer ITextPointer.TextContainer
{
get
{
return this.TextContainer;
}
}
// <see cref="TextPointer.HasValidLayout"/>
bool ITextPointer.HasValidLayout
{
get
{
return this.HasValidLayout;
}
}
// <see cref="ITextPointer.IsAtCaretUnitBoundary"/>
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; }
}
// <see cref="TextPointer.IsFrozen"/>
bool ITextPointer.IsFrozen
{
get
{
return this.IsFrozen;
}
}
// <see cref="ITextPointer.Offset"/>
int ITextPointer.Offset
{
get
{
return this.Offset;
}
}
// <see cref="ITextPointer.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;
}
}
/// <summary>
/// Returns the TextContainer that this TextPointer is a part of.
/// </summary>
internal TextContainer TextContainer
{
get
{
return _tree;
}
}
/// <summary>
/// A FrameworkElement owning a TextContainer to which this TextPointer belongs.
/// </summary>
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 sync 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.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.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;
}
/// <summary>
/// Returns the text element whose edge is in a specified direction
/// from position.
/// </summary>
/// <returns>
/// 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.
/// </returns>
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
}
}
|