Code:
/ DotNET / DotNET / 8.0 / untmp / WIN_WINDOWS / lh_tools_devdiv_wpf / Windows / wcp / Framework / System / Windows / Documents / TextRangeBase.cs / 4 / TextRangeBase.cs
//---------------------------------------------------------------------------- // // File: TextRangeBase.cs // // Copyright (C) Microsoft Corporation. All rights reserved. // // Description: Provides an abstract level of TextRange implementation // Implemented as a static class containing a set of methods // implementing members of abstract ITextRange interface. // These members are supposed to be called from concrete // classes TextRange and TextSelection - to ensure that the // both have the same base implementation. // // TextSelection is allowed to add additional actions over // base ones. TextRange must do pure call redirections, // otherwise TextSelection inheritance from TextRange // will be broken. // // Only methods that require virtualization for TextSelection // implementation go here. All other methods of ITextRange // are implemented directly in TextRange clas in appropriate // ITextRange.Member. // //--------------------------------------------------------------------------- namespace System.Windows.Documents { using MS.Internal; using System.Collections; using System.Collections.Generic; using System.Threading; using System.Globalization; using System.Text; using System.Xml; using System.IO; using MS.Internal.Documents; using System.Windows.Controls; // TextBlock using MS.Internal.PresentationFramework; // SecurityHelper ////// A class a portion of text content. /// Can be contigous or disjoint; supports rectangular table ranges. /// Provides an API for text and table editing operations. /// internal static class TextRangeBase { //----------------------------------------------------- // // ITextRange Methods // //----------------------------------------------------- #region ITextRange Methods //...................................................... // // Selection Building // //...................................................... ////// // internal static bool Contains(ITextRange thisRange, ITextPointer textPointer) { NormalizeRange(thisRange); if (textPointer == null) { throw new ArgumentNullException("textPointer"); } if (textPointer.TextContainer != thisRange.Start.TextContainer) { throw new ArgumentException(SR.Get(SRID.NotInAssociatedTree), "textPointer"); } // Correct position normalization on range boundary so that // our test would not depend on what side of formatting tags // pointer is located. if (textPointer.CompareTo(thisRange.Start) < 0) { textPointer = textPointer.GetFormatNormalizedPosition(LogicalDirection.Forward); } else if (textPointer.CompareTo(thisRange.End) > 0) { textPointer = textPointer.GetFormatNormalizedPosition(LogicalDirection.Backward); } // Check if at least one segment contains this position. for (int i = 0; i < thisRange._TextSegments.Count; i++) { if (thisRange._TextSegments[i].Contains(textPointer)) { return true; } } return false; } ////// Base implementation of ITextRange.Select method. /// /// /// The range which is an object of this operation. /// /// /// One of two boundary positions for building selection. /// In case of table cell-crossing selection it is considered /// as an "anchor" position, so the selection will always include /// the cell at this position /// /// /// The other of two buondary positions for building selection. /// In case of table cell-crossing selection it is considered /// as a "moving" position, so the selection may not include the cell /// at this position - when the cell has bigger index than the anchor /// one and when it is positioned at the very beginning of a cell, /// (if any of these two conditions if false then the cell at this position is included). /// internal static void Select(ITextRange thisRange, ITextPointer position1, ITextPointer position2) { Select(thisRange, position1, position2, /*includeCellAtMovingPosition:*/false); } ////// Base implementation of ITextRange.Select method. /// /// /// The range which is an object of this operation. /// /// /// One of two boundary positions for building selection. /// In case of table cell-crossing selection it is considered /// as an "anchor" position, so the selection will always include /// the cell at this position /// /// /// The other of two buondary positions for building selection. /// In case of table cell-crossing selection it is considered /// as a "moving" position, so the selection may not include the cell /// at this position - when the cell has bigger index than the anchor /// one and when it is positioned at the very beginning of a cell, /// and when includeCellAtMovingPosition==false (if any of these three /// conditions if false then the cell at this position is included). /// /// /// True indicates that a cell at a movingPosition must be included /// into a selection even when it is at cell start. /// False indicates that when a movingPosition is at cell start /// and the cell has bigger index than anchor cell, then selection /// should not include it - it only indicates cell crossing. /// When we build a table range from existing range's Start/End pair /// we must use false for this parameter - because the end position /// of a table range is not included into it - by construction. /// When you use independent position - say, from hit-testing - /// then you typically use "true" for this parameter, unnless /// you intentially cross cell boundary - as for one cell celection. /// internal static void Select(ITextRange thisRange, ITextPointer position1, ITextPointer position2, bool includeCellAtMovingPosition) { if (thisRange._TextSegments == null) { // This is initializing call from TextRange constructor. // No need in change notifications, no need in position verification. TextRangeBase.SelectPrivate(thisRange, position1, position2, includeCellAtMovingPosition, /*markRangeChanged*/false); } else { ValidationHelper.VerifyPosition(thisRange.Start.TextContainer, position1, "position1"); ValidationHelper.VerifyPosition(thisRange.Start.TextContainer, position2, "position2"); TextRangeBase.BeginChange(thisRange); try { TextRangeBase.SelectPrivate(thisRange, position1, position2, includeCellAtMovingPosition, /*markRangeChanged*/true); } finally { TextRangeBase.EndChange(thisRange); } } } ////// Selects a word containing this position /// /// /// /// A TextPointer containing a word to select. /// internal static void SelectWord(ITextRange thisRange, ITextPointer position) { if (position == null) { throw new ArgumentNullException("position"); } // Move position to character boundary (also respect atomics) // ITextPointer normalizedPosition = position.CreatePointer(); normalizedPosition.MoveToInsertionPosition(LogicalDirection.Backward); TextSegment wordRange = TextPointerBase.GetWordRange(normalizedPosition); TextRangeBase.Select(thisRange, wordRange.Start, wordRange.End); } // Returns a word within which empty selection is located. // Returns TextSegment.Null if selection is not empty or // if the position is between or at word boundary. internal static TextSegment GetAutoWord(ITextRange thisRange) { TextSegment autoWordRange = TextSegment.Null; if (thisRange.IsEmpty && // !TextPointerBase.IsAtWordBoundary(thisRange.Start, LogicalDirection.Forward) && // !TextPointerBase.IsAtWordBoundary(thisRange.Start, LogicalDirection.Backward)) { // autoWordRange = TextPointerBase.GetWordRange(thisRange.Start); string autoWord = TextRangeBase.GetTextInternal(autoWordRange.Start, autoWordRange.End).TrimEnd(' '); string textFromWordStart = TextRangeBase.GetTextInternal(autoWordRange.Start, thisRange.Start); if (textFromWordStart.Length >= autoWord.Length) { // The caret is beyond the end of a word (in a whitespace area) autoWordRange = TextSegment.Null; } } return autoWordRange; } ////// Selects a paragraph around the given position. /// /// /// /// A position identifying a paragraph to select. /// internal static void SelectParagraph(ITextRange thisRange, ITextPointer position) { if (position == null) { throw new ArgumentNullException("position"); } ITextPointer start; ITextPointer end; FindParagraphOrListItemBoundaries(position, out start, out end); // Select the paragraph contents TextRangeBase.Select(thisRange, start, end); } // Apply initial typing heuristics -- adjust range for typing // when it spans one or more TableCells. // // ApplyInitialTypingHeuristics/ApplyFinalTypingHeuristics are // called together, with a an extra step in between for TextSelection // overrides of the ApplyTypingHueristic method. internal static void ApplyInitialTypingHeuristics(ITextRange thisRange) { // When table cells selected, clear the start cell and collapse selection into it if (thisRange.IsTableCellRange) { TableCell cell; if (thisRange.Start is TextPointer && (cell = TextRangeEditTables.GetTableCellFromPosition((TextPointer)thisRange.Start)) != null) { // Select the first cell content to make springload formatting happen below thisRange.Select(cell.ContentStart, cell.ContentEnd); } else { thisRange.Select(thisRange.Start, thisRange.Start); } } } // Apply typing heuristics // - extend for overtype. // - prevent paragraph merges when only the leading edge of that // last paragraph is selected. // // ApplyInitialTypingHeuristics/ApplyFinalTypingHeuristics are // called together, with a an extra step in between for TextSelection // overrides of the ApplyTypingHueristic method. internal static void ApplyFinalTypingHeuristics(ITextRange thisRange, bool overType) { // Expand empty selection forward in overtype mode if (overType && thisRange.IsEmpty && !TextPointerBase.IsNextToAnyBreak(thisRange.End, LogicalDirection.Forward)) { // ITextPointer nextPosition = thisRange.End.CreatePointer(); nextPosition.MoveToNextInsertionPosition(LogicalDirection.Forward); if (!TextRangeEditTables.IsTableStructureCrossed(thisRange.Start, nextPosition)) { TextRange range = new TextRange(thisRange.Start, nextPosition); Invariant.Assert(!range.IsTableCellRange); range.Text = String.Empty; } } // If the range is non-empty, and its end just passes a paragraph break, // pull the end back to stop a paragraph merge on the next keystroke. if (!thisRange.IsEmpty && (TextPointerBase.IsNextToAnyBreak(thisRange.End, LogicalDirection.Backward) || TextPointerBase.IsAfterLastParagraph(thisRange.End))) { ITextPointer newEnd = thisRange.End.GetNextInsertionPosition(LogicalDirection.Backward); thisRange.Select(thisRange.Start, newEnd); } } ////// internal static void ApplyTypingHeuristics(ITextRange thisRange, bool overType) { BeginChange(thisRange); try { ApplyInitialTypingHeuristics(thisRange); ApplyFinalTypingHeuristics(thisRange, overType); } finally { EndChange(thisRange); } } internal static void FindParagraphOrListItemBoundaries(ITextPointer position, out ITextPointer start, out ITextPointer end) { // Identify a maximum portion of text around navigator // which may be wrapped by Paragraph start = position.CreatePointer(); end = position.CreatePointer(); SkipParagraphContent(start, LogicalDirection.Backward); SkipParagraphContent(end, LogicalDirection.Forward); } // Moves the navigator in the given direction over all characters, // embedded objects and formatting tags. // private static void SkipParagraphContent(ITextPointer navigator, LogicalDirection direction) { TextPointerContext nextContext = navigator.GetPointerContext(direction); while (true) { if (nextContext == TextPointerContext.None // || // // Entering non-inline content (nextContext == TextPointerContext.ElementStart && direction == LogicalDirection.Forward || // nextContext == TextPointerContext.ElementEnd && direction == LogicalDirection.Backward) && // !typeof(Inline).IsAssignableFrom(navigator.GetElementType(direction)) // || // Exiting non-inline content (nextContext == TextPointerContext.ElementEnd && direction == LogicalDirection.Forward || // nextContext == TextPointerContext.ElementStart && direction == LogicalDirection.Backward) && // !typeof(Inline).IsAssignableFrom(navigator.ParentType)) { // End of paragraph content reached. Stop here. break; } //Need to bail out if MoveToNextContentPosition fails if (!navigator.MoveToNextContextPosition(direction)) { break; } nextContext = navigator.GetPointerContext(direction); } } // Calculates a value of a given property on this range internal static object GetPropertyValue(ITextRange thisRange, DependencyProperty formattingProperty) { if (TextSchema.IsCharacterProperty(formattingProperty)) { return GetCharacterPropertyValue(thisRange, formattingProperty); } else { Invariant.Assert(TextSchema.IsParagraphProperty(formattingProperty), "The property is expected to be one of either character or paragraph formatting one"); return GetParagraphPropertyValue(thisRange, formattingProperty); } } // Calculates character formatting property private static object GetCharacterPropertyValue(ITextRange thisRange, DependencyProperty formattingProperty) { // object startValue = GetCharacterValueFromPosition(thisRange.Start, formattingProperty); // Need to run over all text runs to check that the value is the same for all of them. // We'll stop on the first different value if any; and return MixedValue.Instance for (int i = 0; i < thisRange._TextSegments.Count; i++) { TextSegment textSegment = thisRange._TextSegments[i]; ITextPointer position = textSegment.Start.CreatePointer(); bool moved = true; while (moved && position.CompareTo(textSegment.End) < 0) { // Check whether the value in this text run is the same as at the beginning object value = GetCharacterValueFromPosition(position, formattingProperty); if (!TextSchema.ValuesAreEqual(value, startValue)) { return DependencyProperty.UnsetValue; // } // Skip text run if (position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text) { moved = position.MoveToNextContextPosition(LogicalDirection.Forward); } // try to skip formatting tags moved = position.MoveToInsertionPosition(LogicalDirection.Forward); if (!moved) { // Go to the next run, of there was no formatting boundary moved = position.MoveToNextInsertionPosition(LogicalDirection.Forward); } } } return startValue; } // Gets a non-inherited property from a given position private static object GetCharacterValueFromPosition(ITextPointer pointer, DependencyProperty formattingProperty) { object value = null; if (formattingProperty != Inline.TextDecorationsProperty) { value = pointer.GetValue(formattingProperty); } else { if (pointer is TextPointer) // Implement only for concrete TextCotainer returning null otherwise - for optimization { DependencyObject element = ((TextPointer)pointer).Parent as TextElement; while (value == null && (element is Inline || element is Paragraph || element is TextBlock)) { value = element.GetValue(formattingProperty); element = element is TextElement ? ((TextElement)element).Parent : null; } } } return value; } // Calculates paragraph formatting property // Returns DependencyProperty.UnsetValue if different areas of the range have different value for this property. private static object GetParagraphPropertyValue(ITextRange thisRange, DependencyProperty formattingProperty) { object startValue = null; // Need to run over all text runs to check that the value is the same for all of them. // We'll stop on the first different value if any; and return MixedValue.Instance for (int i = 0; i < thisRange._TextSegments.Count; i++) { TextSegment textSegment = thisRange._TextSegments[i]; ITextPointer position = textSegment.Start.CreatePointer(); // Find position scoped by paragraph - in backward direction - to get start value while (!typeof(Paragraph).IsAssignableFrom(position.ParentType) && position.MoveToNextContextPosition(LogicalDirection.Backward)) ; // Traverse the segment to find all other paragraph positions bool moved = true; while (moved && position.CompareTo(textSegment.End) <= 0) { if (typeof(Paragraph).IsAssignableFrom(position.ParentType)) { object value = position.GetValue(formattingProperty); if (startValue == null) { startValue = value; } if (!TextSchema.ValuesAreEqual(value, startValue)) { return DependencyProperty.UnsetValue; } position.MoveToElementEdge(ElementEdge.AfterEnd); } moved = position.MoveToNextContextPosition(LogicalDirection.Forward); } } // Most properties does not allow null as a value, // so if we still have null try to get a value from range start position. // For some properties (like TextDecorations) it still may remain null. if (startValue == null) { startValue = thisRange.Start.GetValue(formattingProperty); } return startValue; } // Returns true if this range start and end pointers cross a paragraph boundary, false otherwise. internal static bool IsParagraphBoundaryCrossed(ITextRange thisRange) { ITextPointer startNavigator = thisRange.Start.CreatePointer(); ITextPointer endNavigator = thisRange.End.CreatePointer(); if (TextPointerBase.IsAfterLastParagraph(endNavigator)) { endNavigator.MoveToInsertionPosition(LogicalDirection.Backward); } // Walk upto the closest block ancestor while (typeof(Inline).IsAssignableFrom(startNavigator.ParentType)) { startNavigator.MoveToElementEdge(ElementEdge.AfterEnd); } while (typeof(Inline).IsAssignableFrom(endNavigator.ParentType)) { endNavigator.MoveToElementEdge(ElementEdge.AfterEnd); } // start and end are within the scope of the same paragraph? return !startNavigator.HasEqualScope(endNavigator); } //......................................................... // // Change Notifications // //......................................................... ////// /// internal static void BeginChange(ITextRange thisRange) { BeginChangeWorker(thisRange, String.Empty); } ////// /// internal static void BeginChangeNoUndo(ITextRange thisRange) { BeginChangeWorker(thisRange, null); } ////// /// internal static void EndChange(ITextRange thisRange) { EndChange(thisRange, false /* disableScroll */, false /* skipEvents */ ); } ////// /// internal static void EndChange(ITextRange thisRange, bool disableScroll, bool skipEvents) { ChangeBlockUndoRecord changeBlockUndoRecord; bool isChanged; ITextContainer textContainer; Invariant.Assert(thisRange._ChangeBlockLevel > 0, "Unmatched EndChange call!"); textContainer = thisRange.Start.TextContainer; try { // // Complete the content changed block. // try { // Raise first public event -- TextContainer.EndChange. textContainer.EndChange(skipEvents); } finally { // Always drop the ChangeBlockLevel, no matter what happens. // This ensures that we won't ignore future events if the // application recovers from an exception. thisRange._ChangeBlockLevel--; // Clear out thisRange.IsChanged now so that it isn't // left dangling if TextContainer.EndChange throws // an exception. isChanged = thisRange._IsChanged; if (thisRange._ChangeBlockLevel == 0) { thisRange._IsChanged = false; } } // // Complete the range repositioned block. // if (thisRange._ChangeBlockLevel == 0 && isChanged) { // Raise the second public event -- TextRange.Changed. thisRange.NotifyChanged(disableScroll, skipEvents); } } finally { // Make sure we close the undo record no matter what happened. changeBlockUndoRecord = (ChangeBlockUndoRecord)thisRange._ChangeBlockUndoRecord; if (changeBlockUndoRecord != null && thisRange._ChangeBlockLevel == 0) { try { changeBlockUndoRecord.OnEndChange(); } finally { thisRange._ChangeBlockUndoRecord = null; } } } } internal static void NotifyChanged(ITextRange thisRange, bool disableScroll) { thisRange.FireChanged(); } #endregion ITextRange Methods // .................................................................... // // Static Helpers for dealing with content without range instantiation // // .................................................................... #region TextRange Helpers // Returns the text covered by two TextPositions as a string. // Includes rules for translating paragraph breaks and embedded objects. internal static string GetTextInternal(ITextPointer startPosition, ITextPointer endPosition) { Char[] charArray = null; // used for extracting text runs return GetTextInternal(startPosition, endPosition, ref charArray); } // Returns the text covered by two TextPositions as a string. // Includes rules for translating paragraph breaks and embedded objects. // // Use this overload when looping over large quantities of text to avoid // re-allocating a temporary buffer. internal static string GetTextInternal(ITextPointer startPosition, ITextPointer endPosition, ref Char[] charArray) { // Buffer for building a resulting plain text StringBuilder textBuffer = new StringBuilder(); // Stack of List context - needed for efficient bullet generation Stack/// listItemCounter = null; ITextPointer navigator = startPosition.CreatePointer(); Invariant.Assert(startPosition.CompareTo(endPosition) <= 0, "expecting: startPosition <= endPosition"); while (navigator.CompareTo(endPosition) < 0) { Type elementType; TextPointerContext symbolType = navigator.GetPointerContext(LogicalDirection.Forward); switch (symbolType) { case TextPointerContext.Text: PlainConvertTextRun(textBuffer, navigator, endPosition, ref charArray); break; case TextPointerContext.ElementEnd: elementType = navigator.ParentType; if (typeof(Paragraph).IsAssignableFrom(elementType) || typeof(BlockUIContainer).IsAssignableFrom(elementType)) { PlainConvertParagraphEnd(textBuffer, navigator); } else if (typeof(LineBreak).IsAssignableFrom(elementType)) { navigator.MoveToNextContextPosition(LogicalDirection.Forward); textBuffer.Append(Environment.NewLine); } else if (typeof(List).IsAssignableFrom(elementType)) { PlainConvertListEnd(navigator, ref listItemCounter); } else { // All other closing tags - just skip them navigator.MoveToNextContextPosition(LogicalDirection.Forward); } break; case TextPointerContext.EmbeddedElement : textBuffer.Append('\u0020'); // Substitute SPACE for embedded objects. navigator.MoveToNextContextPosition(LogicalDirection.Forward); break; case TextPointerContext.ElementStart : elementType = navigator.GetElementType(LogicalDirection.Forward); if (typeof(AnchoredBlock).IsAssignableFrom(elementType)) { // Floaters and figures must start from a new line textBuffer.Append(Environment.NewLine); } else if (typeof(List).IsAssignableFrom(elementType) && navigator is TextPointer) { // New list level opens PlainConvertListStart(navigator, ref listItemCounter); } else if (typeof(ListItem).IsAssignableFrom(elementType)) { // List items must be preceeded by a list marker PlainConvertListItemStart(textBuffer, navigator, ref listItemCounter); } else { PlainConvertAccessKey(textBuffer, navigator); } navigator.MoveToNextContextPosition(LogicalDirection.Forward); break; default: Invariant.Assert(false, "Unexpected vlue for TextPointerContext"); break; } } return textBuffer.ToString(); } // Part of plain text converter: called from GetTextInternal when processing Text runs private static void PlainConvertTextRun(StringBuilder textBuffer, ITextPointer navigator, ITextPointer endPosition, ref Char[] charArray) { // Copy this text run into destination int runLength = navigator.GetTextRunLength(LogicalDirection.Forward); charArray = EnsureCharArraySize(charArray, runLength); runLength = TextPointerBase.GetTextWithLimit(navigator, LogicalDirection.Forward, charArray, 0, runLength, endPosition); textBuffer.Append(charArray, 0, runLength); navigator.MoveToNextContextPosition(LogicalDirection.Forward); } // Part of plain text converter: called from GetTextInternal when processing ElementEnd for Paragraph elements. // Outputs \n - for regular paragraphs and TableRow ends or \t for TableCell ends. private static void PlainConvertParagraphEnd(StringBuilder textBuffer, ITextPointer navigator) { // Check for a special case for a single paragraph within a TableCell // which must be serialized as "\t" character. navigator.MoveToElementEdge(ElementEdge.BeforeStart); bool theParagraphIsTheFirstInCollection = navigator.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart; navigator.MoveToNextContextPosition(LogicalDirection.Forward); navigator.MoveToElementEdge(ElementEdge.AfterEnd); TextPointerContext symbolType = navigator.GetPointerContext(LogicalDirection.Forward); if (theParagraphIsTheFirstInCollection && symbolType == TextPointerContext.ElementEnd && typeof(TableCell).IsAssignableFrom(navigator.ParentType)) { // This is an end of a table cell navigator.MoveToNextContextPosition(LogicalDirection.Forward); symbolType = navigator.GetPointerContext(LogicalDirection.Forward); if (symbolType == TextPointerContext.ElementStart) { // Next table cell starts after this one. Use '\t' as a cell separator textBuffer.Append('\t'); } else { // This was the last cell in a row. Use '\r\n' as a line separator textBuffer.Append(Environment.NewLine); } } else { // Ordinary paragraph end textBuffer.Append(Environment.NewLine); } } // Part of plain text converter: called from GetTextInternal when processing ElementStart for List elements. // Initializes a stack of list item counters and pushes a new zero for the opened list level. private static void PlainConvertListStart(ITextPointer navigator, ref Stack listItemCounter) { List list = (List)navigator.GetAdjacentElement(LogicalDirection.Forward); // Initialize list context if (listItemCounter == null) { listItemCounter = new Stack (1); } listItemCounter.Push(0); } // Part of plain text converter: called from GetTextInternal when processing ElementEnd for Listelements // Pops a current value from a stack of list item indices. private static void PlainConvertListEnd(ITextPointer navigator, ref Stack listItemCounter) { // Note that we do not expect List tag balansing: // We can get more List closing tags than we had opening ones - // it happens when range starts in the middle of a list. if (listItemCounter != null && listItemCounter.Count > 0) { listItemCounter.Pop(); } navigator.MoveToNextContextPosition(LogicalDirection.Forward); } // Part of plain text converter: called from GetTextInternal when processing ElementStart for ListItem elements // Uses s stack of list items indices and updates it for following list items. private static void PlainConvertListItemStart(StringBuilder textBuffer, ITextPointer navigator, ref Stack listItemCounter) { if (navigator is TextPointer) // can do somethinng useful only in concrete TextContainer - not in an abstract one { List list = (List)((TextPointer)navigator).Parent; ListItem listItem = (ListItem)navigator.GetAdjacentElement(LogicalDirection.Forward); // Initialize list context if (listItemCounter == null) { listItemCounter = new Stack (1); } if (listItemCounter.Count == 0) { // List is taken from its middle position. Need to identify starting item number listItemCounter.Push(((IList)listItem.SiblingListItems).IndexOf(listItem)); } // Get list item number Invariant.Assert(listItemCounter.Count > 0, "expectinng listItemCounter.Count > 0"); int listItemIndex = listItemCounter.Pop(); int indexBase = list != null ? list.StartIndex : 0; TextMarkerStyle markerStyle = list != null ? list.MarkerStyle : TextMarkerStyle.Disc; WriteListMarker(textBuffer, markerStyle, listItemIndex + indexBase); // Advance listItemIndex++; listItemCounter.Push(listItemIndex); } } // Part of plain text converter: called from GetTextInternal when processing ElementStart for AccessKey elements // Uses s stack of list items indices and updates it for following list items. private static void PlainConvertAccessKey(StringBuilder textBuffer, ITextPointer navigator) { // Creating an "_" prefix for AccessKey character (represented as a Run with special serialization attribution) object element = navigator.GetAdjacentElement(LogicalDirection.Forward); if (AccessText.HasCustomSerialization(element)) { textBuffer.Append(AccessText.AccessKeyMarker); } } // Helper for GetTextInternal, manages a char buffer. // NOTE: Does not preserve the content of a buffer private static Char[] EnsureCharArraySize(Char[] charArray, int textLength) { if (charArray == null) { charArray = new char[textLength + 10]; } else if (charArray.Length < textLength) { int newLength = charArray.Length * 2; if (newLength < textLength) { newLength = textLength + 10; } charArray = new Char[newLength]; } return charArray; } // Writes a text representation of a list marker private static void WriteListMarker(StringBuilder textBuffer, TextMarkerStyle listMarkerStyle, int listItemNumber) { string markerText = null; Char[] charArray = null; switch (listMarkerStyle) { case TextMarkerStyle.None : markerText = ""; break; case TextMarkerStyle.Disc : markerText = "\x2022"; // Bullet // not a "\x9f"; break; case TextMarkerStyle.Circle : markerText = "\x25CB"; // White Circle // not a "\xa1"; break; case TextMarkerStyle.Square : markerText = "\x25A1"; // White Box // not a "\x71"; break; case TextMarkerStyle.Box : markerText = "\x25A0"; // Black Box // not a "\xa7"; break; case TextMarkerStyle.Decimal: charArray = ConvertNumberToString(listItemNumber, false, DecimalNumerics); break; case TextMarkerStyle.LowerLatin: charArray = ConvertNumberToString(listItemNumber, true, LowerLatinNumerics); break; case TextMarkerStyle.UpperLatin: charArray = ConvertNumberToString(listItemNumber, true, UpperLatinNumerics); break; case TextMarkerStyle.LowerRoman: markerText = ConvertNumberToRomanString(listItemNumber, false); break; case TextMarkerStyle.UpperRoman: markerText = ConvertNumberToRomanString(listItemNumber, true); break; } if (markerText != null) { textBuffer.Append(markerText); } else if (charArray != null) { textBuffer.Append(charArray, 0, charArray.Length); } textBuffer.Append('\t'); } private const char NumberSuffix = '.'; private const string DecimalNumerics = "0123456789"; private const string LowerLatinNumerics = "abcdefghijklmnopqrstuvwxyz"; private const string UpperLatinNumerics = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; private static string[][] RomanNumerics = new string[][] { new string[] { "m??", "cdm", "xlc", "ivx" }, new string[] { "M??", "CDM", "XLC", "IVX" } }; /// /// Convert a number to string, consisting of digits followed by the NumberSuffix character. /// /// Number to convert. /// True if there is no zero digit (e.g., alpha numbering). /// Set of digits (e.g., 0-9 or a-z). ///Returns the number string as an array of characters. private static char[] ConvertNumberToString(int number, bool oneBased, string numericSymbols) { // Whether zero-based or one-based numbering is used affects how we // count and how we determine the maximum number of values for a // given number of digits. // // The following table illustrates how counting differs. In both // cases we're using base-2 numbering (i.e., two distinct digits), // but with 1-based counting each of those two digits can be a // significant leading digit. // // 0-based 1-based // ---------------------------- // 0 0 -- // 1 1 a // 2 10 b // 3 11 aa // 4 100 ab // 5 101 ba // 6 110 bb // 7 111 aaa // 8 1000 aab // 9 1001 aba // 10 1010 abb // 11 1011 baa // 12 1100 bab // 13 1101 bba // 14 1110 bbb // 15 1111 aaaa // 16 10000 aaab // // For zero-based counting, adding a leading zero does not change // the value of a number. Thus, the set of all N-digit numbers is // a proper subset of the set of (N+1)-digit numbers. Thus the set // of values that can be represented by N *or fewer* digits is the // same as the number of combinations of exactly N digits, i.e., // // b ^ N // // where b is the base of the numbering system. // // For one-based counting, there is no zero digit. Thus, the set // of N-digit numbers and the set of (N+1)-digit numbers are // disjoint sets. Thus, while the number of combinations of // *exactly* N digits is still b ^ N, the maximum value that // can be represented by N *or fewer* digits is: // // Max(N) // where N = 1 : b // where N > 1 : (b ^ N) + Max(N - 1) // if (oneBased) { // Subtract 1 from 1-based numbers so we can use zero-based // indexing. The formula for Max(N) given above should now be // thought of as a limit rather than a maximum. --number; } Invariant.Assert(number >= 0, "expecting: number >= 0"); char[] result; int b = numericSymbols.Length; if (number < b) { // Optimize common case of single-digit numbers. result = new char[2]; // digit + suffix result[0] = numericSymbols[number]; result[1] = NumberSuffix; } else { // Disjoint is 1 if and only if the set of numbers with N // digits and the set of numbers with (N+1) digits are // disjoint (see comment above). Otherwise it is zero. int disjoint = oneBased ? 1 : 0; // Count digits. int digits = 1; for (int limit = b, pow = b; number >= limit; ++digits) { pow *= b; limit = pow + (limit * disjoint); } // Build string in reverse order starting with suffix. result = new char[digits + 1]; // digits + suffix result[digits] = NumberSuffix; for (int i = digits - 1; i >= 0; --i) { result[i] = numericSymbols[number % b]; number = (number / b) - disjoint; } } return result; } ////// Convert 1-based number to a Roman numeric string /// followed by NumberSuffix character. /// ////// Roman number is 1-based. The Roman numeric string is a series of symbols. Following /// is the list of symbols and its value. /// /// Symbol Value /// I 1 /// V 5 /// X 10 /// L 50 /// C 100 /// D 500 /// M 1000 /// /// The rule of Roman number prohibits the use of more than 3 consecutive identical symbol /// but using subtraction of symbol standing for multiples of 10, so the value 4 is written /// as IV (5-1) rather than IIII. /// /// Due to the writing rule and the fact that the symbol represents not the numeral digit /// but the value of the number. Roman number system cannot represent value larger than 3999. /// /// See, http://www.ccsn.nevada.edu/math/ancient_systems.htm /// /// However, there exists a more relaxing use of Roman numbers to represent values 4000 and /// 4999 by using 4 consecutive M. The value 4999 is than written as 'MMMMCMXCIX'. Such use /// however is not widely accepted. /// /// See, http://www.guernsey.net/~sgibbs/roman.html /// /// For values larger than 3999, an overscore is used on the symbol to indicate 1000 multiplication. /// ___ /// So, value 7000 would be written as VII. This writing rule has a fair amount of disagreement /// since it is widely understood that it is not invented by the Romans and they rarely had a /// need for large numbers during their time. Furthermore, accepting this writing rule just /// for the sake of being able to write larger number would create a new limitation of the values /// greater than 3,999,999. Unicode 4.0 does not encode these overscore symbols. /// /// See, http://www.gwydir.demon.co.uk/jo/roman/number.htm /// http://www.novaroma.org/via_romana/numbers.html /// /// Implementation-wise, IE adopts a general limitation of 3999 and simply convert the value /// into a regular numeric form. /// /// We'll follow the mainstream and adopt the 3999 limit. The fallback would also do would IE does. /// /// private static string ConvertNumberToRomanString( int number, bool uppercase ) { if (number > 3999) { // Roman numeric string not supported return number.ToString(CultureInfo.InvariantCulture); } StringBuilder builder = new StringBuilder(); AddRomanNumeric(builder, number / 1000, RomanNumerics[uppercase ? 1 : 0][0]); number %= 1000; AddRomanNumeric(builder, number / 100, RomanNumerics[uppercase ? 1 : 0][1]); number %= 100; AddRomanNumeric(builder, number / 10, RomanNumerics[uppercase ? 1 : 0][2]); number %= 10; AddRomanNumeric(builder, number, RomanNumerics[uppercase ? 1 : 0][3]); builder.Append(NumberSuffix); return builder.ToString(); } ////// Convert number 0 - 9 into Roman numeric /// /// string builder /// number to convert /// Roman numeric char for one five and ten private static void AddRomanNumeric( StringBuilder builder, int number, string oneFiveTen ) { Invariant.Assert(number >= 0 && number <= 9, "expecting: number >= 0 && number <= 9"); if (number >= 1 && number <= 9) { if (number == 4 || number == 9) builder.Append(oneFiveTen[0]); if (number == 9) { builder.Append(oneFiveTen[2]); } else { if (number >= 4) builder.Append(oneFiveTen[1]); for (int i = number % 5; i > 0 && i < 4; i--) builder.Append(oneFiveTen[0]); } } } #endregion TextRange Helpers //------------------------------------------------------ // // ITextRange Properties // //----------------------------------------------------- #region ITextRange Properties //...................................................... // // Boundary Positions // //...................................................... internal static ITextPointer GetStart(ITextRange thisRange) { NormalizeRange(thisRange); Invariant.Assert(thisRange._TextSegments != null && thisRange._TextSegments.Count > 0, "expecting nonempty _TextSegments array for Start position"); return thisRange._TextSegments[0].Start; } internal static ITextPointer GetEnd(ITextRange thisRange) { NormalizeRange(thisRange); Invariant.Assert(thisRange._TextSegments != null && thisRange._TextSegments.Count > 0, "expecting nonempty _TextSegments array for End position"); return thisRange._TextSegments[thisRange._TextSegments.Count - 1].End; } internal static bool GetIsEmpty(ITextRange thisRange) { NormalizeRange(thisRange); // We assume that if a range is empty then it uses the same instance // of TextPointer for both Start and End positions. Invariant.Assert( (thisRange._TextSegments.Count == 1 && (object)thisRange._TextSegments[0].Start == (object)thisRange._TextSegments[0].End) == (thisRange.Start.CompareTo(thisRange.End) == 0), "Range emptiness assumes using one instance of TextPointer for both start and end"); return (thisRange._TextSegments.Count == 1 && (object)thisRange._TextSegments[0].Start == (object)thisRange._TextSegments[0].End); } internal static ListGetTextSegments(ITextRange thisRange) { // NOTE: We cannot normalize thisRange because it will rebuild a collection // of textSegments and will cause range move notification, leading to stack overflow. // return thisRange._TextSegments; } //...................................................... // // Content - rich and plain // //...................................................... // Implementation of a getter for a ITextRange.Text property internal static string GetText(ITextRange thisRange) { NormalizeRange(thisRange); if (!thisRange.IsTableCellRange) { // // Extend the range from its start position to include initial list marker (if any). // We do not do this auto-extension inside GetTextInternal to avoid undesirable // "bulleting" effects on random plain text (say, in Run.TextProperty serialization). // THis is TextRange.get_Text-specific feature. ITextPointer start = thisRange.Start; while (start.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && !typeof(AnchoredBlock).IsAssignableFrom(start.ParentType)) { // Any start tag is harmless except for AnchoredBlocks that would produce extra NewLines. So don't cross them. start = start.GetNextContextPosition(LogicalDirection.Backward); } return TextRangeBase.GetTextInternal(start, thisRange.End); } else { string text; // text = String.Empty; for (int i = 0; i < thisRange._TextSegments.Count; i++) { TextSegment textSegment; textSegment = thisRange._TextSegments[i]; text += TextRangeBase.GetTextInternal(textSegment.Start, textSegment.End); // } return text; } } // Implementation of a setter fot ITextRange.Text property internal static void SetText(ITextRange thisRange, string textData) { NormalizeRange(thisRange); if (textData == null) { throw new ArgumentNullException("textData"); } ITextPointer explicitInsertPosition = null; TextRangeBase.BeginChange(thisRange); try { // Delete content covered by this range if (!thisRange.IsEmpty) { if (thisRange.Start is TextPointer && ((TextPointer)thisRange.Start).Parent == ((TextPointer)thisRange.End).Parent && ((TextPointer)thisRange.Start).Parent is Run && textData.Length > 0) { // When textrange start/end are parented by the same Run, we can optimize // and delete content without any checks. // // Note that NOT doing so has a serious side effect in this case. // Low-level code in TextRangeEdit does not preserve an empty run // with no formatting properties after deletion. // We dont want to loose the empty Run, // when we are just about to set the range text to non-empty string. // Otherwise, newly inserted text might have undesirable formatting properties // applied due to an insertion position within an adjacent Run. if (thisRange.Start.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.Text && thisRange.End.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text) { // If we're deleting with surrounding text, make sure we insert later between the surrounding text. // Because we will invalidate layout with the delete, it's possible that thisRange.Start // will normalize itself to a different character offset on the next reference. // This is because -- unfortunately -- when layout is valid we use ITextView.IsAtCaretUnitBoundary // to normalize unicode offsets, but when layout is dirty we use a different code path // that ignores the current font and simply checks Unicode values for surrogates and // combining marks. See bug 1683515 for an example. explicitInsertPosition = thisRange.Start; } TextContainer textContainer = ((TextPointer)thisRange.Start).TextContainer; textContainer.DeleteContentInternal((TextPointer)thisRange.Start, (TextPointer)thisRange.End); } else { thisRange.Start.DeleteContentToPosition(thisRange.End); } if (thisRange.Start is TextPointer) { TextRangeEdit.MergeFlowDirection((TextPointer)thisRange.Start); } thisRange.Select(thisRange.Start, thisRange.Start); } // Insert text at end position // Note that the non-emptiness check below is not an optimization: // In case of empty text the code block in it would change an empty range // orientation, which is undesirable side effect. // Also if the inserted text is empty we need to avoid ensuring insertion position, // which can create paragraphs etc. if (textData.Length > 0) { ITextPointer insertPosition = (explicitInsertPosition == null) ? thisRange.Start : explicitInsertPosition; // Ensure last paragraph existence and prepare ends for the new selection bool pastedFragmentEndsWithNewLine = textData.EndsWith("\n", StringComparison.Ordinal); // We are going to insert paragraph implicitly when the block content becomes totally empty. // Store the fact that implicit paragraph was inserted to exclude ane extra paragraph break // from the end of pasted fragment bool implicitParagraphInserted = insertPosition is TextPointer && TextSchema.IsValidChild(/*position*/insertPosition, /*childType*/typeof(Block)) && (insertPosition.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.None || insertPosition.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart) && (insertPosition.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.None || insertPosition.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd); // Make sure that the range is positioned at insertion position if (insertPosition is TextPointer && explicitInsertPosition == null) { TextPointer insertionPosition = TextRangeEditTables.EnsureInsertionPosition((TextPointer)insertPosition); thisRange.Select(insertionPosition, insertionPosition); insertPosition = thisRange.Start; } Invariant.Assert(TextSchema.IsInTextContent(insertPosition), "range.Start is expected to be in text content"); ITextPointer newStart = insertPosition.GetFrozenPointer(LogicalDirection.Backward); ITextPointer newEnd = insertPosition.CreatePointer(LogicalDirection.Forward); if ((newStart is TextPointer) && ((TextPointer)newStart).Paragraph != null) { // Rich text - '\n' must be replaced by Paragraphs TextPointer insertionPosition = (TextPointer)newStart.CreatePointer(LogicalDirection.Forward); string[] textParagraphs = textData.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None); int length = textParagraphs.Length; if (implicitParagraphInserted && pastedFragmentEndsWithNewLine) { length--; } for (int i = 0; i < length; i++) { insertionPosition.InsertTextInRun(textParagraphs[i]); if (i < length - 1) { if (insertionPosition.HasNonMergeableInlineAncestor) { // We cannot split a Hyperlink or other non-mergeable Inline element, // so insert a space character instead (similar to embedded object). // Note that this means, SetText would loose // paragraph break information in this case. insertionPosition.InsertTextInRun(" "); } else { // insertionPosition gets repositioned to just inside // the following Paragraph. insertionPosition = insertionPosition.InsertParagraphBreak(); } // Keep newEnd in sync with the paragraph break. // We can't rely on LogicalDirection alone for // anything other than simple text inserts. newEnd = insertionPosition; } } if (implicitParagraphInserted && pastedFragmentEndsWithNewLine) { // We must include ending paragraph break into a resulting range newEnd = newEnd.GetNextInsertionPosition(LogicalDirection.Forward); if (newEnd == null) { newEnd = newStart.TextContainer.End; // set end of range to IsAfterLastParagraph position } // Note: As a result of this logic with implicitParagraphInserted && pastedFragmentEndsWithNewLine // we have the following behavior: // Given that: // range = new TextRange(flowDocument.ContentStart, flowDocument.ContentEnd); // the statement: // range.Text = "foo\r\n"; // has the effect of leaving flowDocument with this content (note: just one paragraph): // foo // and range selecting the whole content: // range.Text == "foo\r\n" // // the statement: // range.Text = "foo"; // results with the same content in flowDocument (one paragraph) // but the range is not extended beyond last paragraph end: // range.Text == "foo". } } else { // Non-paragraph text - insert without '\n' conversion newStart.InsertTextInRun(textData); } // Select the range TextRangeBase.SelectPrivate(thisRange, newStart, newEnd, /*includeCellAtMovingPosition:*/false, /*markRangeChanged*/true); } } finally { TextRangeBase.EndChange(thisRange); } } internal static string GetXml(ITextRange thisRange) { NormalizeRange(thisRange); // Create XmlWriter StringWriter stringWriter = new StringWriter(CultureInfo.InvariantCulture); XmlTextWriter xmlWriter = new XmlTextWriter(stringWriter); TextRangeSerialization.WriteXaml(xmlWriter, thisRange, /*useFlowDocumentAsRoot:*/false, /*wpfPayload:*/null); return stringWriter.ToString(); } internal static bool CanSave(ITextRange thisRange, string dataFormat) { NormalizeRange(thisRange); bool canSave = ( dataFormat == DataFormats.Text || dataFormat == DataFormats.Xaml || (SecurityHelper.CheckUnmanagedCodePermission() && ( dataFormat == DataFormats.XamlPackage || dataFormat == DataFormats.Rtf))); return canSave; } internal static bool CanLoad(ITextRange thisRange, string dataFormat) { NormalizeRange(thisRange); bool canLoad = ( dataFormat == DataFormats.Text || dataFormat == DataFormats.Xaml || (SecurityHelper.CheckUnmanagedCodePermission() && ( dataFormat == DataFormats.XamlPackage || dataFormat == DataFormats.Rtf))); return canLoad; } internal static void Save(ITextRange thisRange, Stream stream, string dataFormat, bool preserveTextElements) { if (stream == null) { throw new ArgumentNullException("stream"); } if (dataFormat == null) { throw new ArgumentNullException("dataFormat"); } NormalizeRange(thisRange); if (dataFormat == DataFormats.Text) { string text = thisRange.Text; StreamWriter textStreamWriter = new StreamWriter(stream); textStreamWriter.Write(text); textStreamWriter.Flush(); } else if (dataFormat == DataFormats.Xaml) { StreamWriter xamlStreamWriter = new StreamWriter(stream); XmlTextWriter xamlXmlWriter = new XmlTextWriter(xamlStreamWriter); // Passing null as wpfPayload parameter we request to produce // xaml without images - all of them will be repllaced by whitespaces. TextRangeSerialization.WriteXaml(xamlXmlWriter, thisRange, /*useFlowDocumentAsRoot:*/false, /*wpfPayload:*/null, preserveTextElements); xamlXmlWriter.Flush(); } else if (dataFormat == DataFormats.XamlPackage && SecurityHelper.CheckUnmanagedCodePermission()) { // Non-null stream here means unconditional request to create a WPF package for the range // independently whether there are images in it or not. WpfPayload.SaveRange(thisRange, ref stream, /*useFlowDocumentAsRoot:*/false); } else if (dataFormat == DataFormats.Rtf && SecurityHelper.CheckUnmanagedCodePermission()) { Stream wpfPayloadMemory = null; // Passing null as a wpfPayloadStream we allow to not create wpf package // when it is not needed (there is no images in the range) string xamlText = WpfPayload.SaveRange(thisRange, ref wpfPayloadMemory, /*useFlowDocumentAsRoot:*/false); // Convert xaml to rtf text to set rtf data into data object. string rtfText = TextEditorCopyPaste.ConvertXamlToRtf(xamlText, wpfPayloadMemory); StreamWriter rtfStreamWriter = new StreamWriter(stream); rtfStreamWriter.Write(rtfText); rtfStreamWriter.Flush(); } else { // Unsupported format - thows exception throw new ArgumentException(SR.Get(SRID.TextRange_UnsupportedDataFormat, dataFormat), "dataFormat"); } } internal static void Load(TextRange thisRange, Stream stream, string dataFormat) { if (stream == null) { throw new ArgumentNullException("stream"); } if (dataFormat == null) { throw new ArgumentNullException("dataFormat"); } NormalizeRange(thisRange); // Reset the stream position to the beginning if (stream.CanSeek) { stream.Seek(0, SeekOrigin.Begin); } if (dataFormat == DataFormats.Text) { StreamReader textStreamReader = new StreamReader(stream); string text = textStreamReader.ReadToEnd(); thisRange.Text = text; } else if (dataFormat == DataFormats.Xaml) { StreamReader xamlStreamReader = new StreamReader(stream); string xamlText = xamlStreamReader.ReadToEnd(); thisRange.Xml = xamlText; } else if (dataFormat == DataFormats.XamlPackage && SecurityHelper.CheckUnmanagedCodePermission()) { object element = WpfPayload.LoadElement(stream); if (!(element is Section) && !(element is Span)) { throw new ArgumentException(SR.Get(SRID.TextRange_UnrecognizedStructureInDataFormat, dataFormat), "stream"); } thisRange.SetXmlVirtual((TextElement)element); } else if (dataFormat == DataFormats.Rtf && SecurityHelper.CheckUnmanagedCodePermission()) { // StreamReader rtfStreamReader = new StreamReader(stream); string rtfText = rtfStreamReader.ReadToEnd(); MemoryStream memoryStream = TextEditorCopyPaste.ConvertRtfToXaml(rtfText); if (memoryStream == null) { throw new ArgumentException(SR.Get(SRID.TextRange_UnrecognizedStructureInDataFormat, dataFormat), "stream"); } TextElement textElement = WpfPayload.LoadElement(memoryStream) as TextElement; if (!(textElement is Section) && !(textElement is Span)) { throw new ArgumentException(SR.Get(SRID.TextRange_UnrecognizedStructureInDataFormat, dataFormat), "stream"); } thisRange.SetXmlVirtual(textElement); } else { // Unsupported format - thows exception throw new ArgumentException(SR.Get(SRID.TextRange_UnsupportedDataFormat, dataFormat), "dataFormat"); } } // Ref count of open change blocks -- incremented/decremented // around BeginChange/EndChange calls. internal static int GetChangeBlockLevel(ITextRange thisRange) { return thisRange._ChangeBlockLevel; } //...................................................... // // Embedded Object Selection // //...................................................... internal static UIElement GetUIElementSelected(ITextRange range) { ITextPointer start = range.Start.CreatePointer(); TextPointerContext context = start.GetPointerContext(LogicalDirection.Forward); while (context == TextPointerContext.ElementStart || context == TextPointerContext.ElementEnd) { start.MoveToNextContextPosition(LogicalDirection.Forward); context = start.GetPointerContext(LogicalDirection.Forward); } if (context == TextPointerContext.EmbeddedElement) { ITextPointer end = range.End.CreatePointer(); context = end.GetPointerContext(LogicalDirection.Backward); while (context == TextPointerContext.ElementStart || context == TextPointerContext.ElementEnd) { end.MoveToNextContextPosition(LogicalDirection.Backward); context = end.GetPointerContext(LogicalDirection.Backward); } if (context == TextPointerContext.EmbeddedElement && start.GetOffsetToPosition(end) == 1) { return start.GetAdjacentElement(LogicalDirection.Forward) as UIElement; } } return null; } //...................................................... // // Table Selection Properties // //...................................................... internal static bool GetIsTableCellRange(ITextRange thisRange) { NormalizeRange(thisRange); return thisRange._IsTableCellRange; } #endregion ITextRange Properties //------------------------------------------------------ // // Private Methods // //------------------------------------------------------ #region Private Methods // Worker for the BeginChange/BeginChangeNoUndo variants. // If description is null, no default undo unit is opened. private static void BeginChangeWorker(ITextRange thisRange, string description) { ITextContainer textContainer = thisRange.Start.TextContainer; if (description != null && thisRange._ChangeBlockUndoRecord == null && thisRange._ChangeBlockLevel == 0) { thisRange._ChangeBlockUndoRecord = new ChangeBlockUndoRecord(textContainer, description); } Invariant.Assert(thisRange._ChangeBlockLevel > 0 || !thisRange._IsChanged, "_changed must be false on new move sequence"); thisRange._ChangeBlockLevel++; if (description != null) { textContainer.BeginChange(); } else { textContainer.BeginChangeNoUndo(); } } // Creates a one-segment collection from a pair of text positions // The segment normalization is done by the following rules: // 1. If start and end pointers have equal positions, or became equal after normalization, // then the segment uses one instance of ITextPointer for both ends, // which guarantees the segment emptiness in the subsequente editing // around it. This single pointer takes orientation from start parameter // and normalized in that direction. // 2. In case when a segment is non-empty, two positions will be created // and normalized inward - towards a segment contents; // Their gravities will be directed outward (start - Backward, end - Forward), // so that any insertion happend at segment edge position goes inside a segment // - the behavior we need to inserting stuff into TextRanges. private static void CreateNormalizedTextSegment(ITextRange thisRange, ITextPointer start, ITextPointer end) { ValidationHelper.VerifyPositionPair(start, end); // Normalize the segment if (start.CompareTo(end) == 0) { // When the range is empty we must keep it that way during normalization if (!IsAtNormalizedPosition(thisRange, start, start.LogicalDirection)) { start = GetNormalizedPosition(thisRange, start, start.LogicalDirection); end = start; } } else { start = GetNormalizedPosition(thisRange, start, LogicalDirection.Forward); if (!TextPointerBase.IsAfterLastParagraph(end)) { // NOTE: Position after the last paragraph is special. // Even though this position is not valid insertion position, // we allow ranges to reach it. This is necessary to be able // to "select all" content, and select the last paragraph. end = GetNormalizedPosition(thisRange, end, LogicalDirection.Backward); } // Collapse range in case of overlapped normalization result if (start.CompareTo(end) >= 0) { // The range is effectuvely empty, so collapse it to single pointer instance if (start.LogicalDirection == LogicalDirection.Backward) { // Choose a position normalized backward, start = end.GetFrozenPointer(LogicalDirection.Backward); // NOTE that otherwise we will use start position, // which is oriented and normalizd Forward } end = start; } else { // Handle Floater/Figure boundaries: non-empty ranges never cross them if (start is TextPointer) { TextPointer adjustedStart = (TextPointer)start; TextPointer adjustedEnd = (TextPointer)end; NormalizeAnchoredBlockBoundaries(ref adjustedStart, ref adjustedEnd); start = adjustedStart; end = adjustedEnd; } Invariant.Assert(start.CompareTo(end) <= 0, "expecting start <= end"); // Normalize the segment, start and end may have become equal now. if (start.CompareTo(end) == 0) { // When the range is empty we must keep it that way during normalization if (!IsAtNormalizedPosition(thisRange, start, start.LogicalDirection)) { start = GetNormalizedPosition(thisRange, start, start.LogicalDirection); end = start; } } // } } // Set this text segment as a selected range // thisRange._TextSegments = new List(1); thisRange._TextSegments.Add(new TextSegment(start, end)); thisRange._IsTableCellRange = false; } private static bool IsAtNormalizedPosition(ITextRange thisRange, ITextPointer position, LogicalDirection direction) { bool isAtNormalizedPosition; if (thisRange.IgnoreTextUnitBoundaries) { isAtNormalizedPosition = TextPointerBase.IsAtFormatNormalizedPosition(position, direction); } else { isAtNormalizedPosition = TextPointerBase.IsAtInsertionPosition(position, direction); } return isAtNormalizedPosition; } private static ITextPointer GetNormalizedPosition(ITextRange thisRange, ITextPointer position, LogicalDirection direction) { ITextPointer normalizedPosition; if (thisRange.IgnoreTextUnitBoundaries) { normalizedPosition = position.GetFormatNormalizedPosition(direction); } else { normalizedPosition = position.GetInsertionPosition(direction); } return normalizedPosition; } // Helper for CreateNormalizedTextSegment // Checks whether start and end cross any Floater/Figure boundaries. // If yes, normalizes the position(s) so that a non-empty range never crosses Floater/Figure boundaries. // Returns true if the range crosses AnchoredBlock boundary and was adjusted to not do so. // Returns false if it does not cross AnchoredBlock boundary and start/end stay where they were. internal static void NormalizeAnchoredBlockBoundaries(ref TextPointer start, ref TextPointer end) { // Check AnchoredBlocks ancestors at start TextElement outerAnchoredBlock = start.Parent as TextElement; while (outerAnchoredBlock != null) { // Find the next ancestor AncoredBlock while (outerAnchoredBlock != null && !typeof(AnchoredBlock).IsAssignableFrom(outerAnchoredBlock.GetType())) { outerAnchoredBlock = outerAnchoredBlock.Parent as TextElement; } if (outerAnchoredBlock != null) { // Anchored block found. Check whether the other position belongs to it. AnchoredBlock innerAnchoredBlock = null; TextElement innerElement = end.Parent as TextElement; while (innerElement != null && innerElement != outerAnchoredBlock) { if (innerElement is AnchoredBlock) { innerAnchoredBlock = (AnchoredBlock)innerElement; } innerElement = innerElement.Parent as TextElement; } if (innerElement == outerAnchoredBlock) { // Common ancestor AnchoredBlock is found. if (innerAnchoredBlock != null) { end = innerAnchoredBlock.ElementEnd; } return; } // The AnchoredElement found at start position does not include end. // Expand start to include the whole outerAnchoredBlock start = outerAnchoredBlock.ElementStart; // and go to the next possible AnchoredBlock level outerAnchoredBlock = outerAnchoredBlock.Parent as TextElement; } } // Check AnchoredBlocks ancestors at end outerAnchoredBlock = end.Parent as TextElement; while (outerAnchoredBlock != null) { // Find the next ancestor AncoredBlock while (outerAnchoredBlock != null && !typeof(AnchoredBlock).IsAssignableFrom(outerAnchoredBlock.GetType())) { outerAnchoredBlock = outerAnchoredBlock.Parent as TextElement; } if (outerAnchoredBlock != null) { // Anchored block found. Check whether the other position belongs to it. AnchoredBlock innerAnchoredBlock = null; TextElement innerElement = start.Parent as TextElement; while (innerElement != null && innerElement != outerAnchoredBlock) { if (innerElement is AnchoredBlock) { innerAnchoredBlock = (AnchoredBlock)innerElement; } innerElement = innerElement.Parent as TextElement; } if (innerElement == outerAnchoredBlock) { // Common ancestor AnchoredBlock is found. if (innerAnchoredBlock != null) { start = innerAnchoredBlock.ElementStart; } return; } // The AnchoredElement found at end position does not include start. // Expand end to include the whole outerAnchoredBlock end = outerAnchoredBlock.ElementEnd; // and go to the next possible AnchoredBlock level outerAnchoredBlock = outerAnchoredBlock.Parent as TextElement; } } } // Method used in all public entry points to // ensure that thisRange is really normalized appropriately. private static void NormalizeRange(ITextRange thisRange) { if (thisRange._ContentGeneration == thisRange._TextSegments[0].Start.TextContainer.Generation) { // There were no content changes since range has been built, // so no normalization needed. return; } ITextPointer start = thisRange._TextSegments[0].Start; ITextPointer end = thisRange._TextSegments[thisRange._TextSegments.Count - 1].End; if (thisRange._IsTableCellRange) { Invariant.Assert(thisRange._TextSegments[0].Start is TextPointer); // Table range - normalization may lead to full range rebuild TextRangeEditTables.IdentifyValidBoundaries(thisRange, out start, out end); // SelectPrivate(thisRange, start, end, /*includeCellAtMovingPosition:*/false, /*markRangeChanged*/false); } else { // Text range - normalization on both ends must be ensured bool needNormalization = false; if ((object)start == (object)end) { if (!TextPointerBase.IsAtInsertionPosition(start, start.LogicalDirection)) { // Empty range can be normalized in any direction, // so we use direction-neutral predicate needNormalization = true; } } else if (start.CompareTo(end) == 0) { // The range which initially was not empty is not collapsed, // so we need to re-create it to use one TextPointer instance // instead of two. // Note that gravity for the start pointer is Backward // in this case - so that resulting caret will be normalized/ // oriented backward. needNormalization = true; } else if ( !TextPointerBase.IsAtInsertionPosition(start, LogicalDirection.Forward) || !TextPointerBase.IsAtInsertionPosition(end, LogicalDirection.Backward)) { // If for a non-empty range, start/end are not at insertion position, // we need to normalize it. needNormalization = true; } if (needNormalization) { CreateNormalizedTextSegment(thisRange, start, end); } } // Store content generation which will be needed in range normalization for // avoiding unnecessary work. thisRange._ContentGeneration = thisRange._TextSegments[0].Start.TextContainer.Generation; } // Implementation body of range Select method // When the range is being built for the very first time OR a table range is being rebuilt during normalization, // we do not fire any range notifications. // NOTE: Because this method is called from NormalizeRange method we should totally avoid calling // any ITextRange methods involving normalization (such as Start/End) private static void SelectPrivate(ITextRange thisRange, ITextPointer position1, ITextPointer position2, bool includeCellAtMovingPosition, bool markRangeChanged) { List textSegments; bool isTableCellRange; Invariant.Assert(position1 != null, "null check: position1"); Invariant.Assert(position2 != null, "null check: position2"); if (position1 is TextPointer) { textSegments = TextRangeEditTables.BuildTableRange( /*anchorPosition:*/(TextPointer)position1, /*movingPosition:*/(TextPointer)position2, includeCellAtMovingPosition, out isTableCellRange); } else { // We have abstract TextContainer - never expect/build table range in this case Invariant.Assert(!thisRange._IsTableCellRange, "range is not expected to be in IsTableCellRange state - 1"); textSegments = null; isTableCellRange = false; } if (textSegments != null) { // Note also that table range is always in normalized condition - by construction // thisRange._TextSegments = textSegments; thisRange._IsTableCellRange = isTableCellRange; } else { // Simple textsegment case ITextPointer newStart = position1; ITextPointer newEnd = position2; // Swap pointers to order them properly. // We do not sewap them when they are equal, // so that for empty range we are predictable about // collapsed position orientation - taken from the first parameter // (position1) (as per CreateNormalizedTextSegment semantics). if (position1.CompareTo(position2) > 0) { newStart = position2; newEnd = position1; } // Create new segment. Note that we do this unconditionally, // not trying to bypass it if new positions look the same as currently set, // because we need to ensure range normalization here. TextRangeBase.CreateNormalizedTextSegment(thisRange, newStart, newEnd); Invariant.Assert(!thisRange._IsTableCellRange, "Expecting that the range is in text segment state now - must be set by CreateNOrmalizedTextSegment"); // Before setting final range state we need to check if we are still in TextSegment condition if (position1 is TextPointer) { ITextPointer finalStart = thisRange._TextSegments[0].Start; ITextPointer finalEnd = thisRange._TextSegments[thisRange._TextSegments.Count - 1].End; if (finalStart.CompareTo(newStart) != 0 || finalEnd.CompareTo(newEnd) != 0) { // This means that as a result of position normalization they have been moved // so we must check what is the range state now. // NOTE: We are in TextSegment state now, so anchor/moving ordering is not important, // so we use thisRange.Start/End (normalized) whose order may be different from // position1/position2. // Note: we use includeCellAtMovingPosition=false here because the movingPosition is taken from a constructed table range, not from input textSegments = TextRangeEditTables.BuildTableRange( /*anchorPosition:*/(TextPointer)finalStart, /*movingPosition:*/(TextPointer)finalEnd, /*includeCellAtMovingPosition:*/false, out isTableCellRange); if (textSegments != null) { thisRange._TextSegments = textSegments; thisRange._IsTableCellRange = isTableCellRange; } } } } // Store content generation which will be needed in range normalization for // avoiding unnecessary work. thisRange._ContentGeneration = thisRange._TextSegments[0].Start.TextContainer.Generation; if (markRangeChanged) { // TextRangeBase.MarkRangeChanged(thisRange); } } /// /// Raises the Changed event for this range. /// /// It must be called within a BeginChange/EndChange /// block. /// private static void MarkRangeChanged(ITextRange thisRange) { Invariant.Assert(thisRange._ChangeBlockLevel > 0, "changeBlockLevel > 0 is expected"); thisRange._IsChanged = true; } #endregion Private Methods } } // File provided for Reference Use Only by Microsoft Corporation (c) 2007. // Copyright (c) Microsoft Corporation. All rights reserved.
Link Menu
This book is available now!
Buy at Amazon US or
Buy at Amazon UK
- ElementHostAutomationPeer.cs
- MemoryMappedViewAccessor.cs
- FontNamesConverter.cs
- TrackingMemoryStream.cs
- ListControlActionList.cs
- FixedLineResult.cs
- DatePicker.cs
- WebPartMenu.cs
- SafeRightsManagementQueryHandle.cs
- MailSettingsSection.cs
- CodePrimitiveExpression.cs
- DesignerImageAdapter.cs
- HttpStaticObjectsCollectionWrapper.cs
- MasterPageCodeDomTreeGenerator.cs
- SchemaRegistration.cs
- SqlClientMetaDataCollectionNames.cs
- DesignerAttribute.cs
- HandlerFactoryWrapper.cs
- HMACSHA512.cs
- SQLBytes.cs
- FixedSOMTable.cs
- Operand.cs
- ClientBuildManager.cs
- SqlFactory.cs
- JsonReader.cs
- ColorKeyFrameCollection.cs
- WebPartCancelEventArgs.cs
- XmlSerializerVersionAttribute.cs
- PageFunction.cs
- ButtonField.cs
- TypedDatasetGenerator.cs
- SchemaDeclBase.cs
- Int32CAMarshaler.cs
- CapabilitiesPattern.cs
- CompiledScopeCriteria.cs
- NumberFormatInfo.cs
- TemplateInstanceAttribute.cs
- PnrpPermission.cs
- Byte.cs
- ListMarkerLine.cs
- HtmlInputControl.cs
- Argument.cs
- ConstructorNeedsTagAttribute.cs
- SegmentInfo.cs
- TraceLevelHelper.cs
- Pair.cs
- X509ImageLogo.cs
- DataGridAddNewRow.cs
- DataGridViewButtonCell.cs
- ReachDocumentPageSerializerAsync.cs
- WithParamAction.cs
- IisTraceWebEventProvider.cs
- AesCryptoServiceProvider.cs
- AddInBase.cs
- XmlAttributeOverrides.cs
- ClientConfigurationHost.cs
- SqlDataSourceCache.cs
- KnownTypesProvider.cs
- DependencyPropertyDescriptor.cs
- OutputCacheSection.cs
- Security.cs
- DataTableClearEvent.cs
- HttpWebRequest.cs
- AbstractSvcMapFileLoader.cs
- RoutedEventValueSerializer.cs
- EventRecord.cs
- TemplateAction.cs
- PerspectiveCamera.cs
- PlatformCulture.cs
- XsltSettings.cs
- SqlNotificationEventArgs.cs
- InternalConfigRoot.cs
- CodeTypeDeclaration.cs
- ProxyWebPartManager.cs
- Helpers.cs
- IdentitySection.cs
- TimeManager.cs
- Registry.cs
- BitmapPalettes.cs
- ObjectIDGenerator.cs
- SizeConverter.cs
- Page.cs
- PolyBezierSegment.cs
- TargetPerspective.cs
- GrammarBuilderBase.cs
- ContentControl.cs
- AutoScrollExpandMessageFilter.cs
- InputMethodStateTypeInfo.cs
- IPipelineRuntime.cs
- ReadOnlyMetadataCollection.cs
- FormView.cs
- ReachIDocumentPaginatorSerializerAsync.cs
- InputReportEventArgs.cs
- CodeIndexerExpression.cs
- AdCreatedEventArgs.cs
- Margins.cs
- SHA384.cs
- DocumentXmlWriter.cs
- SetIterators.cs
- SqlDependencyUtils.cs