// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using MS.Internal; using MS.Internal.Documents; using System.Collections.Specialized; using System.Collections.ObjectModel; namespace System.Windows.Documents { //===================================================================== /// <summary> /// DocumentSequenceTextContainer is a TextContainer that aggregates /// 0 or more TextContainer and expose them as single TextContainer. /// </summary> internal sealed class DocumentSequenceTextContainer : ITextContainer { //-------------------------------------------------------------------- // // Connstructors // //--------------------------------------------------------------------- #region Constructors internal DocumentSequenceTextContainer(DependencyObject parent) { Debug.Assert(parent != null); Debug.Assert(parent is FixedDocumentSequence); _parent = (FixedDocumentSequence)parent; _Initialize(); } #endregion Constructors //-------------------------------------------------------------------- // // Public Methods // //--------------------------------------------------------------------- #region Public Methods /// <summary> /// </summary> void ITextContainer.BeginChange() { _changeBlockLevel++; // We'll raise the Changing event when/if we get an actual // change added, inside AddChangeSegment. } /// <summary> /// <see cref="System.Windows.Documents.ITextContainer.BeginChangeNoUndo"/> /// </summary> void ITextContainer.BeginChangeNoUndo() { // We don't support undo, so follow the BeginChange codepath. ((ITextContainer)this).BeginChange(); } void ITextContainer.EndChange() { ((ITextContainer)this).EndChange(false /* skipEvents */); } /// <summary> /// </summary> void ITextContainer.EndChange(bool skipEvents) { TextContainerChangedEventArgs changes; Invariant.Assert(_changeBlockLevel > 0, "Unmatched EndChange call!"); _changeBlockLevel--; if (_changeBlockLevel == 0 && _changes != null) { changes = _changes; _changes = null; if (this.Changed != null && !skipEvents) { Changed(this, changes); } } } // Allocate a new ITextPointer at the specified offset. // Equivalent to this.Start.CreatePointer(offset), but does not // necessarily allocate this.Start. ITextPointer ITextContainer.CreatePointerAtOffset(int offset, LogicalDirection direction) { return ((ITextContainer)this).Start.CreatePointer(offset, direction); } // Not Implemented. ITextPointer ITextContainer.CreatePointerAtCharOffset(int charOffset, LogicalDirection direction) { throw new NotImplementedException(); } ITextPointer ITextContainer.CreateDynamicTextPointer(StaticTextPointer position, LogicalDirection direction) { return ((ITextPointer)position.Handle0).CreatePointer(direction); } StaticTextPointer ITextContainer.CreateStaticPointerAtOffset(int offset) { return new StaticTextPointer(this, ((ITextContainer)this).CreatePointerAtOffset(offset, LogicalDirection.Forward)); } TextPointerContext ITextContainer.GetPointerContext(StaticTextPointer pointer, LogicalDirection direction) { return ((ITextPointer)pointer.Handle0).GetPointerContext(direction); } int ITextContainer.GetOffsetToPosition(StaticTextPointer position1, StaticTextPointer position2) { return ((ITextPointer)position1.Handle0).GetOffsetToPosition((ITextPointer)position2.Handle0); } int ITextContainer.GetTextInRun(StaticTextPointer position, LogicalDirection direction, char[] textBuffer, int startIndex, int count) { return ((ITextPointer)position.Handle0).GetTextInRun(direction, textBuffer, startIndex, count); } object ITextContainer.GetAdjacentElement(StaticTextPointer position, LogicalDirection direction) { return ((ITextPointer)position.Handle0).GetAdjacentElement(direction); } DependencyObject ITextContainer.GetParent(StaticTextPointer position) { return null; } StaticTextPointer ITextContainer.CreatePointer(StaticTextPointer position, int offset) { return new StaticTextPointer(this, ((ITextPointer)position.Handle0).CreatePointer(offset)); } StaticTextPointer ITextContainer.GetNextContextPosition(StaticTextPointer position, LogicalDirection direction) { return new StaticTextPointer(this, ((ITextPointer)position.Handle0).GetNextContextPosition(direction)); } int ITextContainer.CompareTo(StaticTextPointer position1, StaticTextPointer position2) { return ((ITextPointer)position1.Handle0).CompareTo((ITextPointer)position2.Handle0); } int ITextContainer.CompareTo(StaticTextPointer position1, ITextPointer position2) { return ((ITextPointer)position1.Handle0).CompareTo(position2); } object ITextContainer.GetValue(StaticTextPointer position, DependencyProperty formattingProperty) { return ((ITextPointer)position.Handle0).GetValue(formattingProperty); } #if DEBUG /// <summary> /// Debug only ToString override. /// </summary> public override string ToString() { int blocks = 0; ChildDocumentBlock cdb = this._doclistHead; while (cdb != null) { blocks++; cdb = cdb.NextBlock; } return $"DSTC Id={DebugId} Blocks= {blocks}"; } #endif // DEBUG #endregion Public Methods //-------------------------------------------------------------------- // // Public Properties // //--------------------------------------------------------------------- #region Public Properties /// <summary> /// Specifies whether or not the content of this TextContainer may be /// modified. /// </summary> /// <value> /// True if content may be modified, false otherwise. /// </value> /// <remarks> /// This TextContainer implementation always returns true. /// </remarks> bool ITextContainer.IsReadOnly { get { return true; } } /// <summary> /// <see cref="TextContainer.Start"/> /// </summary> ITextPointer ITextContainer.Start { get { Debug.Assert(_start != null); return _start; } } /// <summary> /// <see cref="TextContainer.End"/> /// </summary> ITextPointer ITextContainer.End { get { Debug.Assert(_end != null); return _end; } } /// <summary> /// Autoincremented counter of content changes in this TextContainer /// </summary> uint ITextContainer.Generation { get { // For read-only content, return some constant value. return 0; } } /// <summary> /// Collection of highlights applied to TextContainer content. /// </summary> Highlights ITextContainer.Highlights { get { return this.Highlights; } } /// <summary> /// <see cref="TextContainer.Parent"/> /// </summary> DependencyObject ITextContainer.Parent { get { return _parent; } } // TextEditor owns setting and clearing this property inside its // ctor/OnDetach methods. ITextSelection ITextContainer.TextSelection { get { return this.TextSelection; } set { _textSelection = value; } } // Optional undo manager, always null for this ITextContainer. UndoManager ITextContainer.UndoManager { get { return null; } } // <see cref="System.Windows.Documents.ITextContainer/> ITextView ITextContainer.TextView { get { return _textview; } set { _textview = value; } } // Count of symbols in this tree, equivalent to this.Start.GetOffsetToPosition(this.End), // but doesn't necessarily allocate anything. int ITextContainer.SymbolCount { get { return ((ITextContainer)this).Start.GetOffsetToPosition(((ITextContainer)this).End); } } // Not implemented. int ITextContainer.IMECharCount { get { throw new NotImplementedException(); } } #endregion Public Properties //-------------------------------------------------------------------- // // Public Events // //--------------------------------------------------------------------- #region Public Events public event EventHandler Changing; public event TextContainerChangeEventHandler Change; public event TextContainerChangedEventHandler Changed; #endregion Public Events //-------------------------------------------------------------------- // // Internal Methods // //--------------------------------------------------------------------- #region Internal Methods //-------------------------------------------------------------------- // Utility Method //--------------------------------------------------------------------- // Verify parameter. Throw Exception if necessary. internal DocumentSequenceTextPointer VerifyPosition(ITextPointer position) { ArgumentNullException.ThrowIfNull(position); if (position.TextContainer != this) { throw new ArgumentException(SR.Format(SR.NotInAssociatedContainer, "position")); } DocumentSequenceTextPointer tp = position as DocumentSequenceTextPointer; if (tp == null) { throw new ArgumentException(SR.Format(SR.BadFixedTextPosition, "position")); } return tp; } // Given an ITextPointer in a child TextContainer, create a position in parent's // address space to represent it. internal DocumentSequenceTextPointer MapChildPositionToParent(ITextPointer tp) { ChildDocumentBlock cdb = this._doclistHead; while (cdb != null) { if (cdb.ChildContainer == tp.TextContainer) { return new DocumentSequenceTextPointer(cdb, tp); } cdb = cdb.NextBlock; } return null; } // Find a ChildBlock in the list that corresponds to the given DocumentReference // Return null if cannot find internal ChildDocumentBlock FindChildBlock(DocumentReference docRef) { Debug.Assert(docRef != null); ChildDocumentBlock cdb = _doclistHead.NextBlock; while (cdb != null) { if (cdb.DocRef == docRef) { return cdb; } cdb = cdb.NextBlock; } return null; } // Return distance between two child TextContainer // 0 means the same container // n means block1 is n steps before block2 in the link list // -n means block1 is n steps after block2 in the link list internal int GetChildBlockDistance(ChildDocumentBlock block1, ChildDocumentBlock block2) { // Note: we can improve perf of this function by using generation // mark + caching index, if this function turns out to be costly. // However, I would expect it to not happen very often. if ((object)block1 == (object)block2) { return 0; } // Assuming block1 is before block2 int count = 0; ChildDocumentBlock cdb = block1; while (cdb != null) { if (cdb == block2) { return count; } count++; cdb = cdb.NextBlock; } // Now block2 has to be before block1 count = 0; cdb = block1; while (cdb != null) { if (cdb == block2) { return count; } count--; cdb = cdb.PreviousBlock; } Debug.Fail("should never be here"); return 0; } #endregion Internal Methods //-------------------------------------------------------------------- // // Internal Properties // //--------------------------------------------------------------------- #region Internal Properties /// <summary> /// Collection of highlights applied to TextContainer content. /// </summary> internal Highlights Highlights { get { if (_highlights == null) { _highlights = new DocumentSequenceHighlights(this); } return _highlights; } } // TextSelection associated with this container. internal ITextSelection TextSelection { get { return _textSelection; } } #if DEBUG internal uint DebugId { get { return _debugId; } } #endif #endregion Internal Properties //-------------------------------------------------------------------- // // Private Methods // //--------------------------------------------------------------------- #region Private Methods //-------------------------------------------------------------------- // Initilization //--------------------------------------------------------------------- private void _Initialize() { Debug.Assert(_parent != null); // Create Start Block/Container/Position _doclistHead = new ChildDocumentBlock(this, new NullTextContainer()); // Create End Block/Container/Position _doclistTail = new ChildDocumentBlock(this, new NullTextContainer()); // Link Start and End container together _doclistHead.InsertNextBlock(_doclistTail); // Now initialize the child doc block list ChildDocumentBlock currentBlock = _doclistHead; foreach (DocumentReference docRef in _parent.References) { currentBlock.InsertNextBlock(new ChildDocumentBlock(this, docRef)); currentBlock = currentBlock.NextBlock; } //if we have at least one document, start and end pointers should be set to valid child blocks not to the placeholders if (_parent.References.Count != 0) { _start = new DocumentSequenceTextPointer(_doclistHead.NextBlock, _doclistHead.NextBlock.ChildContainer.Start); _end = new DocumentSequenceTextPointer(_doclistTail.PreviousBlock, _doclistTail.PreviousBlock.ChildContainer.End); } else { _start = new DocumentSequenceTextPointer(_doclistHead, _doclistHead.ChildContainer.Start); _end = new DocumentSequenceTextPointer(_doclistTail, _doclistTail.ChildContainer.End); } // Listen to collection changes _parent.References.CollectionChanged += new NotifyCollectionChangedEventHandler(_OnContentChanged); // Listen to Highlights changes so that it can notify sub-TextContainer this.Highlights.Changed += new HighlightChangedEventHandler(_OnHighlightChanged); } private void AddChange(ITextPointer startPosition, int symbolCount, PrecursorTextChangeType precursorTextChange) { Invariant.Assert(!_isReadOnly, "Illegal to modify DocumentSequenceTextContainer inside Change event scope!"); ITextContainer textContainer = (ITextContainer)this; textContainer.BeginChange(); try { // Contact any listeners. if (this.Changing != null) { Changing(this, EventArgs.Empty); } // Fire the ChangingEvent now if we haven't already. if (_changes == null) { _changes = new TextContainerChangedEventArgs(); } _changes.AddChange(precursorTextChange, DocumentSequenceTextPointer.GetOffsetToPosition(_start, startPosition), symbolCount, false /* collectTextChanges */); if (this.Change != null) { Invariant.Assert(precursorTextChange == PrecursorTextChangeType.ContentAdded || precursorTextChange == PrecursorTextChangeType.ContentRemoved); TextChangeType textChange = (precursorTextChange == PrecursorTextChangeType.ContentAdded) ? TextChangeType.ContentAdded : TextChangeType.ContentRemoved; _isReadOnly = true; try { // Pass in a -1 for charCount parameter. DocumentSequenceTextContainer // doesn't support this feature because it is only consumed by IMEs // which never run on read-only documents. Change(this, new TextContainerChangeEventArgs(startPosition, symbolCount, -1, textChange)); } finally { _isReadOnly = false; } } } finally { textContainer.EndChange(); } } //-------------------------------------------------------------------- // ContentChange //--------------------------------------------------------------------- private void _OnContentChanged(object sender, NotifyCollectionChangedEventArgs args) { #if DEBUG this._generation++; #endif if (args.Action == NotifyCollectionChangedAction.Add) { if (args.NewItems.Count != 1) { throw new NotSupportedException(SR.RangeActionsNotSupported); } else { object item = args.NewItems[0]; int startingIndex = args.NewStartingIndex; if (startingIndex != _parent.References.Count - 1) { throw new NotSupportedException(SR.Format(SR.UnexpectedCollectionChangeAction, args.Action)); } ChildDocumentBlock newBlock = new ChildDocumentBlock(this, (DocumentReference)item); ChildDocumentBlock insertAfter = _doclistTail.PreviousBlock; insertAfter.InsertNextBlock(newBlock); DocumentSequenceTextPointer changeStart = new DocumentSequenceTextPointer(insertAfter, insertAfter.End); //Update end pointer _end = new DocumentSequenceTextPointer(newBlock, newBlock.ChildContainer.End); if (newBlock.NextBlock == _doclistTail && newBlock.PreviousBlock == _doclistHead) { //Update start pointer for the first block _start = new DocumentSequenceTextPointer(newBlock, newBlock.ChildContainer.Start); } // Record Change Notifications ITextContainer container = newBlock.ChildContainer; int symbolCount = 1; // takes too long to calculate for large documents, and no one will use this info // this does not affect state, only fires event handlers AddChange(changeStart, symbolCount, PrecursorTextChangeType.ContentAdded); } } else { throw new NotSupportedException(SR.Format(SR.UnexpectedCollectionChangeAction, args.Action)); } } //-------------------------------------------------------------------- // Highlight compositing //--------------------------------------------------------------------- private void _OnHighlightChanged(object sender, HighlightChangedEventArgs args) { Debug.Assert(sender != null); Debug.Assert(args != null); Debug.Assert(args.Ranges != null); #if DEBUG { Highlights highlights = this.Highlights; StaticTextPointer highlightTransitionPosition; StaticTextPointer highlightRangeStart; object selected; DocumentsTrace.FixedDocumentSequence.Highlights.Trace("===BeginNewHighlightRange==="); highlightTransitionPosition = ((ITextContainer)this).CreateStaticPointerAtOffset(0); while (true) { // Move to the next highlight start. if (!highlights.IsContentHighlighted(highlightTransitionPosition, LogicalDirection.Forward)) { highlightTransitionPosition = highlights.GetNextHighlightChangePosition(highlightTransitionPosition, LogicalDirection.Forward); // No more highlights? if (highlightTransitionPosition.IsNull) break; } // highlightTransitionPosition is at the start of a new highlight run. selected = highlights.GetHighlightValue(highlightTransitionPosition, LogicalDirection.Forward, typeof(TextSelection)); // Save the start position and find the end. highlightRangeStart = highlightTransitionPosition; highlightTransitionPosition = highlights.GetNextHighlightChangePosition(highlightTransitionPosition, LogicalDirection.Forward); Invariant.Assert(!highlightTransitionPosition.IsNull, "Highlight start not followed by highlight end!"); // Store the highlight. if (selected != DependencyProperty.UnsetValue) { DocumentsTrace.FixedDocumentSequence.Highlights.Trace($"HightlightRange {highlightRangeStart}-{highlightTransitionPosition}"); if (highlightRangeStart.GetPointerContext(LogicalDirection.Forward) != TextPointerContext.Text) { DocumentsTrace.FixedDocumentSequence.Highlights.Trace("<HighlightNotOnText>"); } else { char[] sb = new char[256]; TextPointerBase.GetTextWithLimit(highlightRangeStart.CreateDynamicTextPointer(LogicalDirection.Forward), LogicalDirection.Forward, sb, 0, 256, highlightTransitionPosition.CreateDynamicTextPointer(LogicalDirection.Forward)); DocumentsTrace.FixedDocumentSequence.TextOM.Trace($"HightlightContent [{new String(sb)}]"); } } } DocumentsTrace.FixedDocumentSequence.TextOM.Trace("===EndNewHighlightRange==="); } #endif Debug.Assert(args.Ranges.Count > 0 && args.Ranges[0].Start.CompareTo(args.Ranges[0].End) < 0); // For each change range we received, we need to figure out // affected child TextContainer, and notify it with appropriate // ranges that are in the child's address space. // // We only fire one highlight change notification for any child // TextContainer even if there is multiple change ranges fall // into the same child TextContainer. // // We scan the ranges and the child TextContainer in the same loop, // moving forward two scanning pointers and at boundary of each // TextContainer, we fire a change notification. // int idxScan = 0; DocumentSequenceTextPointer tsScan = null; ChildDocumentBlock cdbScan = null; List<TextSegment> rangeArray = new List<TextSegment>(4); while (idxScan < args.Ranges.Count) { TextSegment ts = args.Ranges[idxScan]; DocumentSequenceTextPointer tsEnd = (DocumentSequenceTextPointer)ts.End; ITextPointer tpChildStart, tpChildEnd; ChildDocumentBlock lastBlock; // If tsScan == null, we were done with previous range, // so we are going to set tsScan to begining of this range. // Otherwise the previous range was split so we will simply // start from what was left over from previous loop. if (tsScan == null) { tsScan = (DocumentSequenceTextPointer)ts.Start; } lastBlock = cdbScan; cdbScan = tsScan.ChildBlock; if (lastBlock != null && cdbScan != lastBlock && !(lastBlock.ChildContainer is NullTextContainer) && rangeArray.Count != 0) { // This range is in a different block, so take care of old blocks first lastBlock.ChildHighlightLayer.RaiseHighlightChangedEvent(new ReadOnlyCollection<TextSegment>(rangeArray)); rangeArray.Clear(); } tpChildStart = tsScan.ChildPointer; if (tsEnd.ChildBlock != cdbScan) { // If this range crosses blocks, we are done with current block tpChildEnd = tsScan.ChildPointer.TextContainer.End; if (tpChildStart.CompareTo(tpChildEnd) != 0) { rangeArray.Add(new TextSegment(tpChildStart, tpChildEnd)); } // Notify child container if (!(cdbScan.ChildContainer is NullTextContainer) && rangeArray.Count != 0) { cdbScan.ChildHighlightLayer.RaiseHighlightChangedEvent(new ReadOnlyCollection<TextSegment>(rangeArray)); } // Move on to next block; cdbScan = cdbScan.NextBlock; tsScan = new DocumentSequenceTextPointer(cdbScan, cdbScan.ChildContainer.Start); rangeArray.Clear(); } else { // Otherwise we need to go on to see if there is more ranges // fall withing the same block. Simply add this change range tpChildEnd = tsEnd.ChildPointer; if (tpChildStart.CompareTo(tpChildEnd) != 0) { rangeArray.Add(new TextSegment(tpChildStart, tpChildEnd)); } // Move on to next range idxScan++; tsScan = null; } } // Fire change notification for the last child block. if (rangeArray.Count > 0 && (!(cdbScan == null || cdbScan.ChildContainer is NullTextContainer))) { cdbScan.ChildHighlightLayer.RaiseHighlightChangedEvent(new ReadOnlyCollection<TextSegment>(rangeArray)); } } #endregion Private Methods //-------------------------------------------------------------------- // // Private Fields // //--------------------------------------------------------------------- #region Private Fields private readonly FixedDocumentSequence _parent; // The content aggregator, supplied in ctor private DocumentSequenceTextPointer _start; // Start of the aggregated TextContainer private DocumentSequenceTextPointer _end; // End of the aggregated TextContainer private ChildDocumentBlock _doclistHead; // Head of the doubly linked list of child TextContainer entries private ChildDocumentBlock _doclistTail; // Tail of the doubly linked list of child TextContainer entries private ITextSelection _textSelection; // Collection of highlights applied to TextContainer content. private Highlights _highlights; // BeginChange ref count. When non-zero, we are inside a change block. private int _changeBlockLevel; // Array of pending changes in the current change block. // Null outside of a change block. private TextContainerChangedEventArgs _changes; // TextView associated with this TextContainer. private ITextView _textview; // Set true during Change event callback. // When true, modifying the TextContainer is disallowed. private bool _isReadOnly; #if DEBUG // The container generation, increamented whenever container content changes. private uint _generation; private uint _debugId = (DocumentSequenceTextContainer._debugIdCounter++); private static uint _debugIdCounter = 0; #endif #endregion Private Fields //-------------------------------------------------------------------- // // Private Classes // //--------------------------------------------------------------------- #region Private Classes /// <summary> /// DocumentSequence specific Highlights subclass accepts text pointers from /// either the DocSequence or any of its child FixedDocuments. This allows /// the highlights to be set on the DocSequence but the FixedDocuments continue /// to handle the rendering (FixedDocs look for their parent's Highlights if /// the parent is a DocSequence). /// </summary> private sealed class DocumentSequenceHighlights : Highlights { internal DocumentSequenceHighlights(DocumentSequenceTextContainer textContainer) : base(textContainer) { } /// <summary> /// Returns the value of a property stored on scoping highlight, if any. /// </summary> /// <param name="textPosition"> /// Position to query. /// </param> /// <param name="direction"> /// Direction of content to query. /// </param> /// <param name="highlightLayerOwnerType"> /// Type of the matching highlight layer owner. /// </param> /// <returns> /// The highlight value if set on any scoping highlight. If no property /// value is set, returns DependencyProperty.UnsetValue. /// </returns> internal override object GetHighlightValue(StaticTextPointer textPosition, LogicalDirection direction, Type highlightLayerOwnerType) { StaticTextPointer parentPosition; if (EnsureParentPosition(textPosition, direction, out parentPosition)) { return base.GetHighlightValue(parentPosition, direction, highlightLayerOwnerType); } return DependencyProperty.UnsetValue; } /// <summary> /// Returns true iff the indicated content has scoping highlights. /// </summary> /// <param name="textPosition"> /// Position to query. /// </param> /// <param name="direction"> /// Direction of content to query. /// </param> internal override bool IsContentHighlighted(StaticTextPointer textPosition, LogicalDirection direction) { StaticTextPointer parentPosition; if (EnsureParentPosition(textPosition, direction, out parentPosition)) { return base.IsContentHighlighted(parentPosition, direction); } return false; } /// <summary> /// Returns the position of the next highlight start or end in an /// indicated direction, or null if there is no such position. /// </summary> /// <param name="textPosition"> /// Position to query. /// </param> /// <param name="direction"> /// Direction of content to query. /// </param> internal override StaticTextPointer GetNextHighlightChangePosition(StaticTextPointer textPosition, LogicalDirection direction) { StaticTextPointer parentPosition; StaticTextPointer returnPointer = StaticTextPointer.Null; if (EnsureParentPosition(textPosition, direction, out parentPosition)) { returnPointer = base.GetNextHighlightChangePosition(parentPosition, direction); // If we were passed a child position, we need to convert the result back to a child position if (textPosition.TextContainer.Highlights != this) { returnPointer = GetStaticPositionInChildContainer(returnPointer, direction, textPosition); } } return returnPointer; } /// <summary> /// Returns the closest neighboring TextPointer in an indicated /// direction where a property value calculated from an embedded /// object, scoping text element, or scoping highlight could /// change. /// </summary> /// <param name="textPosition"> /// Position to query. /// </param> /// <param name="direction"> /// Direction of content to query. /// </param> /// <returns> /// If the following symbol is TextPointerContext.EmbeddedElement, /// TextPointerContext.ElementBegin, or TextPointerContext.ElementEnd, returns /// a TextPointer exactly one symbol distant. /// /// If the following symbol is TextPointerContext.Text, the distance /// of the returned TextPointer is the minimum of the value returned /// by textPosition.GetTextLength and the distance to any highlight /// start or end edge. /// /// If the following symbol is TextPointerContext.None, returns null. /// </returns> internal override StaticTextPointer GetNextPropertyChangePosition(StaticTextPointer textPosition, LogicalDirection direction) { StaticTextPointer parentPosition; StaticTextPointer returnPointer = StaticTextPointer.Null; if (EnsureParentPosition(textPosition, direction, out parentPosition)) { returnPointer = base.GetNextPropertyChangePosition(parentPosition, direction); // If we were passed a child position, we need to convert the result back to a child position if (textPosition.TextContainer.Highlights != this) { returnPointer = GetStaticPositionInChildContainer(returnPointer, direction, textPosition); } } return returnPointer; } /// <summary> /// Sets parentPosition to be a valid TextPointer in the parent document. This could either /// be the textPosition passed in (if its already on the parent document) or a conversion /// of the textPosition passed in. /// </summary> /// <returns>whether or not parentPosition is valid and should be used</returns> private bool EnsureParentPosition(StaticTextPointer textPosition, LogicalDirection direction, out StaticTextPointer parentPosition) { // Simple case - textPosition is already in the parent TextContainer parentPosition = textPosition; // If textPosition is on a child TextContainer, we convert it if (textPosition.TextContainer.Highlights != this) { // This case can't be converted so return false, out parameter should not be used if (textPosition.GetPointerContext(direction) == TextPointerContext.None) return false; // Turn the textPosition (which should be in the scope of a FixedDocument) // into a position in the scope of the DocumentSequence. ITextPointer dynamicTextPointer = textPosition.CreateDynamicTextPointer(LogicalDirection.Forward); ITextPointer parentTextPointer = ((DocumentSequenceTextContainer)this.TextContainer).MapChildPositionToParent(dynamicTextPointer); Debug.Assert(parentTextPointer != null); parentPosition = parentTextPointer.CreateStaticPointer(); } // Returning true - either we started with a parent position or we converted to one return true; } /// <summary> /// Conversion from a StaticTextPointer on a DocumentSequence into a StaticTextPointer /// on a specified FixedDocument. If the conversion results in a pointer on a different /// FixedDocument then we return one end of the FixedDocument (based on direction). /// </summary> /// <param name="textPosition">position in a DocumentSequence to convert</param> /// <param name="direction">direction of the desired conversion</param> /// <param name="originalPosition">original pointer from FixedDocument</param> private StaticTextPointer GetStaticPositionInChildContainer(StaticTextPointer textPosition, LogicalDirection direction, StaticTextPointer originalPosition) { StaticTextPointer parentTextPointer = StaticTextPointer.Null; if (!textPosition.IsNull) { DocumentSequenceTextPointer parentChangePosition = textPosition.CreateDynamicTextPointer(LogicalDirection.Forward) as DocumentSequenceTextPointer; Debug.Assert(parentChangePosition != null); // If the DocSequence position translates into a position in a different FixedDocument than // the original request, we return an end of the original FixedDocument (which end depends on direction) ITextPointer childTp = parentChangePosition.ChildPointer; if (childTp.TextContainer != originalPosition.TextContainer) { // If the position we started searching from is highlighted, cut the highlight // at the end of the text container. Otherwise return null (the highlight must // start in the next document). if (IsContentHighlighted(originalPosition, direction)) { childTp = direction == LogicalDirection.Forward ? originalPosition.TextContainer.End : originalPosition.TextContainer.Start; parentTextPointer = childTp.CreateStaticPointer(); } else { parentTextPointer = StaticTextPointer.Null; } } else { parentTextPointer = childTp.CreateStaticPointer(); } } return parentTextPointer; } } #endregion Private Classes } }