/// <summary> /// </summary> void ITextSelection.SetCaretToPosition(ITextPointer caretPosition, LogicalDirection direction, bool allowStopAtLineEnd, bool allowStopNearSpace) { // We need a pointer with appropriate direction, // becasue it will be used in textRangeBase.Select method for // pointer normalization. caretPosition = caretPosition.CreatePointer(direction); // Normalize the position in its logical direction - to get to text content over there. caretPosition.MoveToInsertionPosition(direction); // We need a pointer with the reverse direction to confirm // the line wrapping position. So we can ensure Bidi caret navigation. // Bidi can have the different caret position by setting the // logical direction, so we have to only set the logical direction // as the forward for the real line wrapping position. ITextPointer reversePosition = caretPosition.CreatePointer(direction == LogicalDirection.Forward ? LogicalDirection.Backward : LogicalDirection.Forward); // Check line wrapping condition if (!allowStopAtLineEnd && ((TextPointerBase.IsAtLineWrappingPosition(caretPosition, this.TextView) && TextPointerBase.IsAtLineWrappingPosition(reversePosition, this.TextView)) || TextPointerBase.IsNextToPlainLineBreak(caretPosition, LogicalDirection.Backward) || TextSchema.IsBreak(caretPosition.GetElementType(LogicalDirection.Backward)))) { // Caret is at wrapping position, and we are not allowed to stay at end of line, // so we choose forward direction to appear in the begiinning of a second line caretPosition.SetLogicalDirection(LogicalDirection.Forward); } else { if (caretPosition.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.Text && caretPosition.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text) { // This is statistically most typical case. No "smartness" needed // to choose standard Forward orientation for the caret. // NOTE: By using caretPosition's direction we solve BiDi caret orientation case: // The orietnation reflects a direction from where caret has been moved // or orientation where mouse clicked. So we will stick with appropriate // character. // Nothing to do. The caretPosition is good to go. } else if (!allowStopNearSpace) { // There are some tags around, and we are not allowed to choose a side near to space. // So we need to perform some content analysis. char[] charBuffer = new char[1]; if (caretPosition.GetPointerContext(direction) == TextPointerContext.Text && caretPosition.GetTextInRun(direction, charBuffer, 0, 1) == 1 && Char.IsWhiteSpace(charBuffer[0])) { LogicalDirection oppositeDirection = direction == LogicalDirection.Forward ? LogicalDirection.Backward : LogicalDirection.Forward; // Check formatting switch condition at this position FlowDirection initialFlowDirection = (FlowDirection)caretPosition.GetValue(FrameworkElement.FlowDirectionProperty); bool moved = caretPosition.MoveToInsertionPosition(oppositeDirection); if (moved && initialFlowDirection == (FlowDirection)caretPosition.GetValue(FrameworkElement.FlowDirectionProperty) && (caretPosition.GetPointerContext(oppositeDirection) != TextPointerContext.Text || caretPosition.GetTextInRun(oppositeDirection, charBuffer, 0, 1) != 1 || !Char.IsWhiteSpace(charBuffer[0]))) { // In the opposite direction we have a non-space // character. So we choose that direction direction = oppositeDirection; caretPosition.SetLogicalDirection(direction); } } } } // Now that orientation of a caretPosition is identified, // build an empty selection at this position TextRangeBase.BeginChange(this); try { TextRangeBase.Select(this, caretPosition, caretPosition); // Note how Select method works for the case of empty range: // It creates a single instance TextPointer normalized and oriented // in a direction taken from caretPosition: ITextSelection thisSelection = this; Invariant.Assert(thisSelection.Start.LogicalDirection == caretPosition.LogicalDirection); // orientation must be as passed Invariant.Assert(this.IsEmpty); //Invariant.Assert((object)thisSelection.Start == (object)thisSelection.End); // it must be the same instance of TextPointer //Invariant.Assert(TextPointerBase.IsAtInsertionPosition(thisSelection.Start, caretPosition.LogicalDirection)); // normalization must be done in the same diredction as orientation // Clear active positions when selection is empty SetActivePositions(null, null); } finally { TextRangeBase.EndChange(this); } }
// <see cref="System.Windows.Documents.TextPointer.MoveToLineBoundary"/> internal static int MoveToLineBoundary(ITextPointer thisPointer, ITextView textView, int count, bool respectNonMeargeableInlineStart) { ITextPointer position; double newSuggestedX; Invariant.Assert(!thisPointer.IsFrozen, "Can't reposition a frozen pointer!"); Invariant.Assert(textView != null, "Null TextView!"); // Did you check ITextPointer.HasValidLayout? position = textView.GetPositionAtNextLine(thisPointer, Double.NaN, count, out newSuggestedX, out count); if (!position.IsAtInsertionPosition) { if (!respectNonMeargeableInlineStart || (!IsAtNonMergeableInlineStart(position) && !IsAtNonMergeableInlineEnd(position))) { position.MoveToInsertionPosition(position.LogicalDirection); } } if (IsAtRowEnd(position)) { // We will find outselves at a row end when we have incomplete // markup like // // <TableCell></TableCell> <!-- No inner Run! --> // // In that case the end-of-row is the entire line. thisPointer.MoveToPosition(position); thisPointer.SetLogicalDirection(position.LogicalDirection); } else { TextSegment lineRange = textView.GetLineRange(position); if (!lineRange.IsNull) { thisPointer.MoveToPosition(lineRange.Start); thisPointer.SetLogicalDirection(lineRange.Start.LogicalDirection); } else if (count > 0) { // It is possible to get a non-zero return value from ITextView.GetPositionAtNextLine // when moving into a BlockUIContainer. The container is the "next line" but does // not contain any lines itself -- GetLineRange will return null. thisPointer.MoveToPosition(position); thisPointer.SetLogicalDirection(position.LogicalDirection); } } return count; }
/// <summary> /// Moves the position to the closes unit boundary. /// </summary> private bool MoveToUnitBoundary(ITextPointer position, bool isStart, LogicalDirection direction, TextUnit unit) { bool moved = false; ITextView textView; switch (unit) { case TextUnit.Character: if (!TextPointerBase.IsAtInsertionPosition(position)) { if (TextPointerBase.MoveToNextInsertionPosition(position, direction)) { moved = true; } } break; case TextUnit.Word: if (!IsAtWordBoundary(position)) { if (MoveToNextWordBoundary(position, direction)) { moved = true; } } break; case TextUnit.Format: // Formatting changes can be introduced by elements. Hence it is fair to // assume that formatting boundaries are defined by non-text context. while (position.GetPointerContext(direction) == TextPointerContext.Text) { if (position.MoveToNextContextPosition(direction)) { moved = true; } } // Make sure we end with text on the right, so that later ExpandToEnclosingUnit calls // do the right thing. if (moved && direction == LogicalDirection.Forward) { while (true) { TextPointerContext context = position.GetPointerContext(LogicalDirection.Forward); if (context != TextPointerContext.ElementStart && context != TextPointerContext.ElementEnd) break; position.MoveToNextContextPosition(LogicalDirection.Forward); } } break; case TextUnit.Line: // Positions are snapped to closest line boundaries. But since line information // is based on the layout, positions are not changed, if: // a) they are not currently in the view, or // b) containing line cannot be found. textView = _textAdaptor.GetUpdatedTextView(); if (textView != null && textView.IsValid && textView.Contains(position)) { TextSegment lineRange = textView.GetLineRange(position); if (!lineRange.IsNull) { double newSuggestedX; int linesMoved = 0; if (direction == LogicalDirection.Forward) { ITextPointer nextLineStart = null; if (isStart) { nextLineStart = textView.GetPositionAtNextLine(lineRange.End, Double.NaN, 1, out newSuggestedX, out linesMoved); } if (linesMoved != 0) { lineRange = textView.GetLineRange(nextLineStart); nextLineStart = lineRange.Start; } else { nextLineStart = lineRange.End; } nextLineStart = GetInsertionPosition(nextLineStart, LogicalDirection.Forward); if (position.CompareTo(nextLineStart) != 0) { position.MoveToPosition(nextLineStart); position.SetLogicalDirection(isStart ? LogicalDirection.Forward : LogicalDirection.Backward); moved = true; } } else { ITextPointer previousLineEnd = null; if (!isStart) { previousLineEnd = textView.GetPositionAtNextLine(lineRange.Start, Double.NaN, -1, out newSuggestedX, out linesMoved); } if (linesMoved != 0) { lineRange = textView.GetLineRange(previousLineEnd); previousLineEnd = lineRange.End; } else { previousLineEnd = lineRange.Start; } previousLineEnd = GetInsertionPosition(previousLineEnd, LogicalDirection.Backward); if (position.CompareTo(previousLineEnd) != 0) { position.MoveToPosition(previousLineEnd); position.SetLogicalDirection(isStart ? LogicalDirection.Forward : LogicalDirection.Backward); moved = true; } } } } break; case TextUnit.Paragraph: // Utilize TextRange logic to determine paragraph boundaries. ITextRange textRange = new TextRange(position, position); TextRangeBase.SelectParagraph(textRange, position); if (direction == LogicalDirection.Forward) { ITextPointer nextParagraphStart = textRange.End; if (isStart) { nextParagraphStart = nextParagraphStart.CreatePointer(); if (nextParagraphStart.MoveToNextInsertionPosition(LogicalDirection.Forward)) { TextRangeBase.SelectParagraph(textRange, nextParagraphStart); nextParagraphStart = textRange.Start; } } if (position.CompareTo(nextParagraphStart) != 0) { position.MoveToPosition(nextParagraphStart); position.SetLogicalDirection(isStart ? LogicalDirection.Forward : LogicalDirection.Backward); moved = true; } } else { ITextPointer previousParagraphEnd = textRange.Start; if (!isStart) { previousParagraphEnd = previousParagraphEnd.CreatePointer(); if (previousParagraphEnd.MoveToNextInsertionPosition(LogicalDirection.Backward)) { TextRangeBase.SelectParagraph(textRange, previousParagraphEnd); previousParagraphEnd = textRange.End; } } if (position.CompareTo(previousParagraphEnd) != 0) { position.MoveToPosition(previousParagraphEnd); position.SetLogicalDirection(isStart ? LogicalDirection.Forward : LogicalDirection.Backward); moved = true; } } break; case TextUnit.Page: // Positions are snapped to nearest page boundaries. But since page information // is based on the layout, positions are not changed, if they are not currently in the view. // We need to consider 2 types of scenarios: single page and multi-page. // In case of multi-page scenario, first need to find a page associated with the position. // If page is found, move the start position to the beginning of the first range of that page // and move the end position to the end of the last range of that page. textView = _textAdaptor.GetUpdatedTextView(); if (textView != null && textView.IsValid && textView.Contains(position)) { ITextView pageTextView = textView; if (textView is MultiPageTextView) { // This is "multi page" case. Find page associated with the start position. pageTextView = ((MultiPageTextView)textView).GetPageTextViewFromPosition(position); } ReadOnlyCollection<TextSegment> textSegments = pageTextView.TextSegments; if (textSegments != null && textSegments.Count > 0) { //When comparing, we need to take into account if the pointer is not right at //the end of the page (or beginning) because of normalization if (direction == LogicalDirection.Forward) { while (position.CompareTo(textSegments[textSegments.Count - 1].End) != 0) { if (position.GetPointerContext(LogicalDirection.Forward) != TextPointerContext.ElementEnd) { position.MoveToPosition(textSegments[textSegments.Count - 1].End); moved = true; break; } Invariant.Assert(position.MoveToNextContextPosition(LogicalDirection.Forward)); } MoveToInsertionPosition(position, LogicalDirection.Forward); } else { while (position.CompareTo(textSegments[0].Start) != 0) { if (position.GetPointerContext(LogicalDirection.Backward) != TextPointerContext.ElementStart) { position.MoveToPosition(textSegments[0].Start); moved = true; break; } Invariant.Assert(position.MoveToNextContextPosition(LogicalDirection.Backward)); } MoveToInsertionPosition(position, LogicalDirection.Backward); } } } break; case TextUnit.Document: if (direction == LogicalDirection.Forward) { if (position.CompareTo(GetInsertionPosition(position.TextContainer.End, LogicalDirection.Backward)) != 0) { position.MoveToPosition(position.TextContainer.End); moved = true; } } else { if (position.CompareTo(GetInsertionPosition(position.TextContainer.Start, LogicalDirection.Forward)) != 0) { position.MoveToPosition(position.TextContainer.Start); moved = true; } } break; default: // Unknown unit break; } return moved; }
/// <summary> /// Re-positions the given position by an integral number of text units, but it does /// not guarantee that position is snapped to TextUnit boundary. /// This method assumes that input position is already snapped to appropriate TextUnit boundary. /// </summary> /// <param name="position">The position to move</param> /// <param name="unit">Text units to step by</param> /// <param name="count">Number of units to step over. Also specifies the direction of moving: /// forward if positive, backward otherwise</param> /// <returns>The actual number of units the position was moved over</returns> private int MovePositionByUnits(ITextPointer position, TextUnit unit, int count) { ITextView textView; int moved = 0; int absCount = (count == int.MinValue) ? int.MaxValue : Math.Abs(count); LogicalDirection direction = (count > 0) ? LogicalDirection.Forward : LogicalDirection.Backward; // This method assumes that position is already snapped to appropriate TextUnit. switch (unit) { case TextUnit.Character: while (moved < absCount) { if (!TextPointerBase.MoveToNextInsertionPosition(position, direction)) { break; } moved++; } break; case TextUnit.Word: while (moved < absCount) { if (!MoveToNextWordBoundary(position, direction)) { break; } moved++; } break; case TextUnit.Format: // Formatting changes can be introduced by elements. Hence it is fair to // assume that formatting boundaries are defined by non-text context. while (moved < absCount) { ITextPointer positionOrig = position.CreatePointer(); // First skip all text in given direction. while (position.GetPointerContext(direction) == TextPointerContext.Text) { if (!position.MoveToNextContextPosition(direction)) { break; } } // Move to next context if (!position.MoveToNextContextPosition(direction)) { break; } // Skip all formatting elements and position the pointer next to text. while (position.GetPointerContext(direction) != TextPointerContext.Text) { if (!position.MoveToNextContextPosition(direction)) { break; } } // If moving backwards, position the pointer at the beginning of formatting range. if (direction == LogicalDirection.Backward) { while (position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.Text) { if (!position.MoveToNextContextPosition(LogicalDirection.Backward)) { break; } } } if (position.GetPointerContext(direction) != TextPointerContext.None) { moved++; } else { position.MoveToPosition(positionOrig); break; } } // Adjust logical direction to point to the following text (forward or backward movement). // If we don't do this, we'll normalize in the wrong direction and get stuck in a loop // if caller tries to advance again. position.SetLogicalDirection(LogicalDirection.Forward); break; case TextUnit.Line: // Position is snapped to nearest line boundary. But since line information // is based on the layout, position is not changed, if: // a) it is not currently in the view, or // b) containing line cannot be found. textView = _textAdaptor.GetUpdatedTextView(); if (textView != null && textView.IsValid && textView.Contains(position)) { // ITextPointer.MoveToLineBoundary can't handle Table row end positions. // Mimic TextEditor's caret navigation code and move into the preceding // TableCell. if (TextPointerBase.IsAtRowEnd(position)) { position.MoveToNextInsertionPosition(LogicalDirection.Backward); } moved = position.MoveToLineBoundary(count); MoveToInsertionPosition(position, LogicalDirection.Forward); if (moved < 0) { moved = -moved; // Will be reversed below. } } break; case TextUnit.Paragraph: // Utilize TextRange logic to determine paragraph boundaries. ITextRange paragraphRange = new TextRange(position, position); paragraphRange.SelectParagraph(position); while (moved < absCount) { position.MoveToPosition(direction == LogicalDirection.Forward ? paragraphRange.End : paragraphRange.Start); if (!position.MoveToNextInsertionPosition(direction)) { break; } moved++; paragraphRange.SelectParagraph(position); position.MoveToPosition(paragraphRange.Start); // Position it always at the beginning of the paragraph. } break; case TextUnit.Page: // But since page information is based on the layout, position is not changed, if: // a) it is not currently in the view, or // b) containing page cannot be found. // Page movement is possible only in multi-page scenario. textView = _textAdaptor.GetUpdatedTextView(); if (textView != null && textView.IsValid && textView.Contains(position)) { if (textView is MultiPageTextView) { // Get embedded page ITextView for given position. ITextView pageTextView = ((MultiPageTextView)textView).GetPageTextViewFromPosition(position); ReadOnlyCollection<TextSegment> textSegments = pageTextView.TextSegments; while (moved < absCount) { if (textSegments == null || textSegments.Count == 0) { break; } // Move the position to appropriate edge. if (direction == LogicalDirection.Backward) { position.MoveToPosition(textSegments[0].Start); MoveToInsertionPosition(position, LogicalDirection.Backward); } else { position.MoveToPosition(textSegments[textSegments.Count - 1].End); MoveToInsertionPosition(position, LogicalDirection.Forward); } // Try to move the position to the next page. ITextPointer positionTemp = position.CreatePointer(); if (!positionTemp.MoveToNextInsertionPosition(direction)) { break; } else { // MoveToNextInsertionPosition may return 'true' and move the position // in oposite direction. if (direction == LogicalDirection.Forward) { if (positionTemp.CompareTo(position) <= 0) { break; } } else { if (positionTemp.CompareTo(position) >= 0) { break; } } } // Get embedded page ITextView for given position. if (!textView.Contains(positionTemp)) { break; } pageTextView = ((MultiPageTextView)textView).GetPageTextViewFromPosition(positionTemp); textSegments = pageTextView.TextSegments; moved++; } } } break; case TextUnit.Document: // This method assumes that position is already snapped to appropriate TextUnit. break; } return (direction == LogicalDirection.Forward) ? moved : -moved; }
/// <summary> /// Helper function to move given position to word boundary. TextPointerBase.MoveToNextWordBoundary /// cannot be used directly, because it does not modify LogicalDirection. Because of that, IsAtWordBoundary /// for just moved positions may return FALSE. /// </summary> private static bool MoveToNextWordBoundary(ITextPointer position, LogicalDirection direction) { int moveCounter = 0; ITextPointer startPosition = position.CreatePointer(); // Move the position in the given direction until word boundary is reached. while (position.MoveToNextInsertionPosition(direction)) { moveCounter++; if (IsAtWordBoundary(position)) { break; } // Need to break the loop for weird case when there is no word break in text content. // When the word looks too long, consider end of textRun as a word break. if (moveCounter > 128) // 128 was taken as a random number. Probably not big enough though... { position.MoveToPosition(startPosition); position.MoveToNextContextPosition(direction); break; } } // Note that we always use Forward direction for word orientation. if (moveCounter > 0) { position.SetLogicalDirection(LogicalDirection.Forward); } return moveCounter > 0; }